From 7ce1ba71748fb2b60bc898cc668244e4839b68c2 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 04:27:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(velocity):=20=E6=96=B0=E5=A2=9E=20/felis=20?= =?UTF-8?q?lobby=EF=BC=8C/felis=20go=20lobby=20=E4=B9=9F=E8=83=BD=E5=9B=9E?= =?UTF-8?q?=E5=A4=A7=E5=8E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/README.md | 1 + .../felis/velocity/FelisVelocityPlugin.java | 24 +++++++++++ .../lolicon/felis/velocity/WaitingRouter.java | 27 ++++++++++++ .../felis/velocity/WaitingRouterTest.java | 43 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/plugins/README.md b/plugins/README.md index 7bf55d3..42b3857 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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 ` | 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): diff --git a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/FelisVelocityPlugin.java b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/FelisVelocityPlugin.java index 76b7a85..d609e98 100644 --- a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/FelisVelocityPlugin.java +++ b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/FelisVelocityPlugin.java @@ -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 ", 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)) { diff --git a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java index ba4283c..944596b 100644 --- a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java +++ b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java @@ -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 }. 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 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 diff --git a/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java b/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java index 873b8b6..24febd0 100644 --- a/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java +++ b/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java @@ -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");