fix(velocity): 目标服拒绝玩家时带原因提示并直接放行到大厅,不再在 login 里循环
This commit is contained in:
3 files changed
+129
-15
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
? 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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user