Loading plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java +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 Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -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); } }); } Loading plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java +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 Loading @@ -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: Loading Loading @@ -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<>()); Loading Loading @@ -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; } Loading plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java +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 Loading @@ -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() { Loading @@ -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); Loading Loading @@ -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() { Loading Loading
plugins/velocity/src/main/java/best/lolicon/felis/velocity/WaitingRouter.java +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 Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -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); } }); } Loading
plugins/velocity/test/best/lolicon/felis/velocity/Fakes.java +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 Loading @@ -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: Loading Loading @@ -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<>()); Loading Loading @@ -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; } Loading
plugins/velocity/test/best/lolicon/felis/velocity/WaitingRouterTest.java +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 Loading @@ -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() { Loading @@ -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); Loading Loading @@ -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() { Loading