Unverified Commit 7ce1ba71 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

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

parent d2730c6e
Loading
Loading
Loading
Loading
+1 −0
Changes for plugins/README.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -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):
+24 −0
Changes for plugins/velocity/src/main/java/best/lolicon/felis/velocity/FelisVelocityPlugin.java: 24 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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)) {
+27 −0
Changes for plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java: 27 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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
+43 −0
Changes for plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java: 43 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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");