From df8b09788ce4d9d741c6b86dfea5c605d726f514 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sat, 26 Sep 2026 21:28:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(velocity):=20=E7=9B=AE=E6=A0=87=E6=9C=8D?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E7=8E=A9=E5=AE=B6=E6=97=B6=E5=B8=A6=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=E6=8F=90=E7=A4=BA=E5=B9=B6=E7=9B=B4=E6=8E=A5=E6=94=BE?= =?UTF-8?q?=E8=A1=8C=E5=88=B0=E5=A4=A7=E5=8E=85=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=9C=A8=20login=20=E9=87=8C=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lolicon/felis/velocity/WaitingRouter.java | 55 +++++++++++++++++-- .../best/lolicon/felis/velocity/Fakes.java | 36 +++++++++--- .../felis/velocity/WaitingRouterTest.java | 53 +++++++++++++++++- 3 files changed, 129 insertions(+), 15 deletions(-) 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 f40707b..91486e5 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 @@ -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 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 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); } }); } diff --git a/plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java b/plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java index ee728b0..3d55052 100644 --- a/plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java +++ b/plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java @@ -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 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 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 preConnect = Function.identity(); + /** connects lists every server a connection went to (after pre-connect), by name. */ final List connects = Collections.synchronizedList(new ArrayList<>()); /** pluginMessages lists every plugin message sent down any of this player's server connections. */ final List 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 - ? ConnectionRequestBuilder.Status.SUCCESS - : ConnectionRequestBuilder.Status.SERVER_DISCONNECTED; + 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; } diff --git a/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java b/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java index 2ff99df..196f80b 100644 --- a/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java +++ b/plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java @@ -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() {