fix(velocity): 新增 /felis lobby,/felis go lobby 也能回大厅

This commit is contained in:
Lemon-miaow committed 2026-09-27 04:27:39 +08:00
1 parent d2730c6e5a
commit 7ce1ba7174
4 files changed
+95

No files matched your search

+1
View File
@@ -173,6 +173,7 @@ What it does when routing is active:
| Server-list ping (`ProxyPingEvent`) | Answers from the cached lifecycle view with a phase-aware MOTD (online / starting / sleeping) — **read-only, never wakes** anything. Mirroring each backend's own MOTD by background-pinging ready servers is a later slice. |
| Join report (`ServerConnectedEvent`) | Reports real joins to a felis backend via `POST …/join-event`, so the reaper sees activity and the player is auto-added to the server allowlist. |
| `/felis`, `/felis list` | Operator status: online-mode, root-domain, lobby, and the known server set with phase/ready. |
| `/felis lobby`, `/felis go <lobby>` | Moves the player back to the lobby from any backend. Nothing is woken, and a wait already queued still moves them when its server is ready. Kept under `/felis` so a user server's own `/lobby` or `/hub` is not shadowed by the proxy. |
Velocity-only config keys (read from the same `felis-link.properties` / env as
`/link`; env wins):
@@ -439,6 +439,11 @@ public final class FelisVelocityPlugin {
sendServerList(ctx.getSource());
return Command.SINGLE_SUCCESS;
}))
.then(BrigadierCommand.literalArgumentBuilder("lobby")
.executes(ctx -> {
doLobby(ctx.getSource());
return Command.SINGLE_SUCCESS;
}))
.then(BrigadierCommand.literalArgumentBuilder("go")
.then(BrigadierCommand.requiredArgumentBuilder("server", StringArgumentType.word())
.executes(ctx -> {
@@ -569,6 +574,8 @@ public final class FelisVelocityPlugin {
zh ? "此代理已知的 felis 服务器" : "the felis servers this proxy knows");
helpLine(source, "/felis go <server>",
zh ? "启动服务器并在就绪后把你传送过去" : "start a server and move you in when it's ready");
helpLine(source, "/felis lobby",
zh ? "回到大厅" : "back to the lobby");
helpLine(source, "/felis claim",
zh ? "认领你所在的服务器" : "take ownership of the server you're on");
helpLine(source, "/felis migrate",
@@ -626,6 +633,11 @@ public final class FelisVelocityPlugin {
return false;
}
String target = serverArg.trim();
if (target.equalsIgnoreCase(config.lobbyServer())) {
// The lobby is a system server, so the loop below would call it unknown.
router.toLobby(player);
return true;
}
ServerView match = null;
for (ServerView v : registry.all()) {
if (!isSystemServer(v.name()) && v.name().equalsIgnoreCase(target)) {
@@ -659,6 +671,18 @@ public final class FelisVelocityPlugin {
return true;
}
private void doLobby(CommandSource source) {
Player player = requirePlayer(source);
if (player == null || !ensureOutOfLimbo(player)) {
return;
}
if (!routingActive) {
player.sendMessage(routingDisabled(zh(player)));
return;
}
router.toLobby(player);
}
private void doClaim(CommandSource source) {
Player player = requirePlayer(source);
if (player == null || !ensureOutOfLimbo(player)) {
@@ -225,6 +225,33 @@ public final class WaitingRouter {
authorizeAndWait(player, serverName, false);
}
/**
* toLobby moves a player who is already past the login gate back to the lobby, for
* {@code /felis lobby} and {@code /felis go <lobby>}. The lobby is always up, so there
* is nothing to wake and no queue to join; a failed connect is reported like any other
* transfer. A wait the player joined earlier stays: the lobby is where the queue parks
* players anyway, and it still moves them when their server is ready.
*/
void toLobby(Player player) {
boolean zh = FelisVelocityPlugin.zh(player);
if (player.getCurrentServer()
.map(sc -> serverNamed(sc.getServer(), lobbyServer))
.orElse(false)) {
player.sendMessage(Component.text(
zh ? "你已经在大厅了。" : "You're already in the lobby.", NamedTextColor.YELLOW));
return;
}
Optional<RegisteredServer> lobby = proxy.getServer(lobbyServer);
if (lobby.isEmpty()) {
log.warn("Felis: {} asked for the lobby but '{}' is not registered", player.getUniqueId(), lobbyServer);
player.sendMessage(Component.text(
zh ? "大厅暂时不可用,请稍后再试。" : "The lobby is unavailable right now. Try again shortly.",
NamedTextColor.RED));
return;
}
transfer(player, lobbyServer, lobby.get());
}
/**
* enqueueFromInvite is {@link #enqueueFromCommand} for an accepted invite. An invite can
* only name the server its sender is standing on, so the target is running by
@@ -75,6 +75,7 @@ public final class WaitingRouterTest {
longStart();
menuAndCommands();
joins();
backToLobby();
disconnectAndRelease();
} finally {
api.close();
@@ -605,6 +606,43 @@ public final class WaitingRouterTest {
assertEq("refused transfer: no retry advice", false, full.said("try again"));
}
// /felis lobby and /felis go lobby: the lobby is a system server, so it never
// appeared among the servers /felis go knows.
private static void backToLobby() {
Fakes.FakePlayer back = player(null, true);
back.current = beta;
router.toLobby(back.player);
assertEq("from a server: moved to the lobby", List.of("lobby"), List.copyOf(back.connects));
assertEq("from a server: nothing asked of felis-api", 0, api.count(LINK + back.id));
Fakes.FakePlayer there = player(null, true);
there.current = lobby;
router.toLobby(there.player);
assertEq("in the lobby: told", true, there.said("You're already in the lobby."));
assertEq("in the lobby: no move", 0, there.connects.size());
Fakes.FakePlayer between = player(null, true);
router.toLobby(between.player);
assertEq("on no server yet: moved to the lobby", List.of("lobby"), List.copyOf(between.connects));
// A wait survives the trip: the queue parks players in the lobby anyway.
Fakes.FakePlayer waiter = player(null, true);
waiter.current = beta;
int before = router.waitingCount();
router.enqueueFromCommand(waiter.player, "zeta");
Fakes.await("waiter queued", () -> router.waitingCount() == before + 1);
router.toLobby(waiter.player);
assertEq("waiting: moved to the lobby", List.of("lobby"), List.copyOf(waiter.connects));
assertEq("waiting: still queued", before + 1, router.waitingCount());
router.onDisconnect(new DisconnectEvent(waiter.player, DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN));
Fakes.FakePlayer full = player(null, true);
full.current = beta;
full.refusals.put("lobby", "Server is full");
router.toLobby(full.player);
Fakes.await("lobby refused: told", () -> full.said("Couldn't connect you to « lobby ». Reason: Server is full"));
}
private static void disconnectAndRelease() {
// Leaving the proxy drops the queue entry.
Fakes.FakePlayer leaver = player(null, true);
@@ -648,6 +686,11 @@ public final class WaitingRouterTest {
router.releaseFromLogin(early.player);
assertEq("no lobby yet: nothing sent", 0, early.connects.size());
assertEq("no lobby yet: logged", 1, log.count("WARN", "login release for " + early.id + " but the lobby"));
Fakes.FakePlayer asker = player(null, true);
asker.current = beta;
router.toLobby(asker.player);
assertEq("no lobby: told", true, asker.said("The lobby is unavailable right now."));
assertEq("no lobby: no move", 0, asker.connects.size());
// With no login gate registered a host-routed player is turned away.
net.remove("login");