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

fix(velocity): 目标服拒绝玩家时带原因提示并直接放行到大厅,不再在 login 里循环

parent 4afabc39
Loading
Loading
Loading
Loading
+50 −5
Changes for plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java: 50 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -120,10 +121,46 @@ public final class WaitingRouter {
        player.createConnectionRequest(lobby.get()).connect().whenComplete((result, err) -> {
            if (err != null) {
                log.warn("Felis: login release for {} failed: {}", player.getUniqueId(), err.toString());
                return;
            }
            if (result != null && !result.isSuccessful()) {
                releaseRefused(player, result);
            }
        });
    }

    /**
     * releaseRefused handles a login release that did not land. When the pre-connect
     * check had redirected it to the player's requested server and that server turned
     * them away — banned, not whitelisted, full, the wrong mods — the remembered target
     * would send every retry of the gate into the same refusal, silently, until the gate
     * gave up two minutes later with "couldn't enter the lobby". Forget the target, show
     * the server's own reason, and release the player to the lobby at once.
     */
    private void releaseRefused(Player player, ConnectionRequestBuilder.Result result) {
        UUID id = player.getUniqueId();
        RegisteredServer attempted = result.getAttemptedConnection();
        String pending = pendingTargets.get(id);
        if (attempted == null || pending == null || !serverNamed(attempted, pending)
                || !pendingTargets.remove(id, pending)) {
            log.warn("Felis: login release for {} did not land: {}", id, result.getStatus());
            return; // a failed move to the lobby itself: the gate retries
        }
        boolean zh = FelisVelocityPlugin.zh(player);
        Component line = Component.text(
                zh ? "「" + pending + "」拒绝了你的连接,已把你送到大厅。"
                   : "« " + pending + " » turned you away, so you're in the lobby instead.",
                NamedTextColor.RED);
        Optional<Component> reason = result.getReasonComponent();
        if (reason.isPresent()) {
            line = line.append(Component.text(zh ? " 原因:" : " Reason: ", NamedTextColor.RED))
                    .append(reason.get());
        }
        player.sendMessage(line);
        log.info("Felis: {} refused {} on the login release ({})", pending, id, result.getStatus());
        releaseFromLogin(player);
    }

    // linked runs the link check through the gate and notes when the answer came from
    // the outage fallback rather than felis-api.
    private boolean linked(UUID id) throws LinkException {
@@ -589,11 +626,19 @@ public final class WaitingRouter {
                plugin.stats().count(ProxyStats.Event.TRANSFER_FAILED);
                log.warn("Felis: transfer of {} to {} failed: {}", player.getUniqueId(), serverName,
                        err != null ? err.toString() : result.getStatus());
                player.sendMessage(Component.text(
                        FelisVelocityPlugin.zh(player)
                                ? "无法把你连接到「" + serverName + "」。请重试。"
                                : "Couldn't connect you to « " + serverName + " ». Please try again.",
                        NamedTextColor.RED));
                boolean zh = FelisVelocityPlugin.zh(player);
                Component line = Component.text(
                        zh ? "无法把你连接到「" + serverName + "」。"
                           : "Couldn't connect you to « " + serverName + " ».",
                        NamedTextColor.RED);
                // A backend that refuses the login says why (ban, whitelist, full, mods);
                // "please try again" is wrong advice for all of those.
                Optional<Component> reason = result == null ? Optional.empty() : result.getReasonComponent();
                line = reason.isPresent()
                        ? line.append(Component.text(zh ? " 原因:" : " Reason: ", NamedTextColor.RED))
                              .append(reason.get())
                        : line.append(Component.text(zh ? "请重试。" : " Please try again.", NamedTextColor.RED));
                player.sendMessage(line);
            }
        });
    }
+27 −5
Changes for plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java: 27 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Function;

/**
 * Fakes are the test doubles the routing tests run the real proxy classes against:
@@ -236,7 +237,17 @@ final class Fakes {
        volatile boolean connectSucceeds = true;
        volatile String disconnectedWith;
        final List<String> messages = Collections.synchronizedList(new ArrayList<>());
        /** connects lists every server a connection request was sent to, by name. */
        /**
         * refusals maps a backend to the kick it answers this player's login with (a ban,
         * the whitelist, "server is full"): the connect fails with that reason.
         */
        final Map<String, String> refusals = new ConcurrentHashMap<>();
        /**
         * preConnect stands in for the proxy's ServerPreConnectEvent: it maps the server a
         * request asked for to the one it goes to, or null when a listener denied it.
         */
        volatile Function<RegisteredServer, RegisteredServer> preConnect = Function.identity();
        /** connects lists every server a connection went to (after pre-connect), by name. */
        final List<String> connects = Collections.synchronizedList(new ArrayList<>());
        /** pluginMessages lists every plugin message sent down any of this player's server connections. */
        final List<byte[]> pluginMessages = Collections.synchronizedList(new ArrayList<>());
@@ -308,18 +319,29 @@ final class Fakes {
                    case "getServer":
                        return target;
                    case "connect":
                        connects.add(target.getServerInfo().getName());
                        boolean ok = connectSucceeds;
                        ConnectionRequestBuilder.Status status = ok
                        RegisteredServer dest = preConnect.apply(target);
                        RegisteredServer attempted = dest == null ? target : dest;
                        String refusal = dest == null ? null : refusals.get(dest.getServerInfo().getName());
                        ConnectionRequestBuilder.Status status;
                        if (dest == null) {
                            status = ConnectionRequestBuilder.Status.CONNECTION_CANCELLED;
                        } else {
                            connects.add(dest.getServerInfo().getName());
                            status = connectSucceeds && refusal == null
                                    ? ConnectionRequestBuilder.Status.SUCCESS
                                    : ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
                        }
                        return CompletableFuture.completedFuture(fake(ConnectionRequestBuilder.Result.class,
                                (m2, a2) -> {
                                    switch (m2) {
                                        case "getStatus":
                                            return status;
                                        case "getAttemptedConnection":
                                            return target;
                                            return attempted;
                                        case "getReasonComponent":
                                            return refusal == null
                                                    ? Optional.empty()
                                                    : Optional.of(Component.text(refusal));
                                        default:
                                            return UNANSWERED;
                                    }
+50 −3
Changes for plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java: 50 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -419,9 +419,19 @@ public final class WaitingRouterTest {
        refused.current = lobby;
        refused.connectSucceeds = false;
        router.enqueueFromInvite(refused.player, "beta");
        Fakes.await("failed transfer counted", () -> plugin.stats().total(ProxyStats.Event.TRANSFER_FAILED) == 1);
        assertEq("failed transfer: told", true, refused.said("Couldn't connect you to « beta »"));
        // The chat line is the last thing the failure path does; the count comes first.
        Fakes.await("failed transfer: told", () -> refused.said("Couldn't connect you to « beta »"));
        assertEq("failed transfer counted", 1L, plugin.stats().total(ProxyStats.Event.TRANSFER_FAILED));
        assertEq("failed transfer: logged", 1, log.count("WARN", "transfer of " + refused.id + " to beta failed"));

        // A backend that kicks the login says why; "please try again" is wrong for a ban.
        Fakes.FakePlayer full = player(null, true);
        full.current = lobby;
        full.refusals.put("beta", "The server is full");
        router.enqueueFromInvite(full.player, "beta");
        Fakes.await("refused transfer: told the reason", () -> full.said("Couldn't connect you to « beta ». Reason: The server is full"));
        assertEq("refused transfer counted", 2L, plugin.stats().total(ProxyStats.Event.TRANSFER_FAILED));
        assertEq("refused transfer: no retry advice", false, full.said("try again"));
    }

    private static void disconnectAndRelease() {
@@ -439,6 +449,29 @@ public final class WaitingRouterTest {
        router.releaseFromLogin(released.player);
        assertEq("release connects to the lobby", List.of("lobby"), List.copyOf(released.connects));

        // A requested server that turns the player away (a ban, the whitelist, full) is
        // shown with its own reason and the player goes to the lobby at once: the
        // remembered target sent every retry of the gate into the same refusal, silently,
        // until the gate gave up two minutes later.
        Fakes.FakePlayer banned = player("beta.mc.test", true);
        choose(banned);
        gated(banned);
        banned.refusals.put("beta", "You are banned from this server");
        router.releaseFromLogin(banned.player);
        assertEq("refused target: tried, then the lobby", List.of("beta", "lobby"), List.copyOf(banned.connects));
        assertEq("refused target: told why", true, banned.said(
                "« beta » turned you away, so you're in the lobby instead. Reason: You are banned from this server"));
        assertEq("refused target: forgotten", "lobby", allowedTo(release(banned)));

        // A lobby that refuses is the gate's to retry: one attempt, no release loop.
        Fakes.FakePlayer bounced = player(null, true);
        gated(bounced);
        bounced.refusals.put("lobby", "Server is full");
        router.releaseFromLogin(bounced.player);
        assertEq("refused lobby: one attempt", List.of("lobby"), List.copyOf(bounced.connects));
        assertEq("refused lobby: nothing said", 0, bounced.messages.size());
        assertEq("refused lobby: logged", 1, log.count("WARN", "login release for " + bounced.id + " did not land"));

        net.remove("lobby");
        Fakes.FakePlayer early = player(null, true);
        router.releaseFromLogin(early.player);
@@ -479,7 +512,21 @@ public final class WaitingRouterTest {
    // router's async part run to completion the way Velocity's event manager would.
    private static ServerPreConnectEvent release(Fakes.FakePlayer p) {
        p.current = login;
        ServerPreConnectEvent e = new ServerPreConnectEvent(p.player, lobby, login);
        return preConnect(p, lobby);
    }

    // gated puts the player on the login gate and runs every connection request it
    // makes through the router's pre-connect check, as the proxy does.
    private static void gated(Fakes.FakePlayer p) {
        p.current = login;
        p.preConnect = target -> {
            ServerPreConnectEvent e = preConnect(p, target);
            return e.getResult().isAllowed() ? e.getResult().getServer().orElse(target) : null;
        };
    }

    private static ServerPreConnectEvent preConnect(Fakes.FakePlayer p, RegisteredServer target) {
        ServerPreConnectEvent e = new ServerPreConnectEvent(p.player, target, p.current);
        EventTask task = router.onServerPreConnect(e);
        if (task != null) {
            task.execute(new Continuation() {