fix(velocity): 路由开启时让出 Velocity 自带的 /server,大厅的 /server 菜单才能打开
This commit is contained in:
5 files changed
+204
-5
No files matched your search
@@ -82,13 +82,14 @@ sourceSets {
|
||||
include 'best/lolicon/felis/velocity/ServerRegistryTest.java'
|
||||
include 'best/lolicon/felis/velocity/WaitingRouterTest.java'
|
||||
include 'best/lolicon/felis/velocity/ControlChannelTest.java'
|
||||
include 'best/lolicon/felis/velocity/ServerCommandTest.java'
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + configurations.compileClasspath
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
def routingMains = ['ServerRegistryTest', 'WaitingRouterTest', 'ControlChannelTest']
|
||||
def routingMains = ['ServerRegistryTest', 'WaitingRouterTest', 'ControlChannelTest', 'ServerCommandTest']
|
||||
routingMains.each { name ->
|
||||
tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
@@ -100,7 +101,7 @@ routingMains.each { name ->
|
||||
|
||||
tasks.register('routingTest') {
|
||||
group = 'verification'
|
||||
description = 'Runs the routing self-tests (ServerRegistry, WaitingRouter, ControlChannel).'
|
||||
description = 'Runs the routing self-tests (ServerRegistry, WaitingRouter, ControlChannel, ServerCommand).'
|
||||
dependsOn routingMains
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
@@ -185,11 +186,43 @@ public final class FelisVelocityPlugin {
|
||||
// refresh above reached even a fork that reads the legacy list only once.
|
||||
legacyForwarding.accepting();
|
||||
|
||||
if (yieldServerCommand(proxy.getCommandManager(), proxy.getPluginManager())) {
|
||||
logger.info("Felis: Velocity's /server removed; /server now reaches the backend "
|
||||
+ "(the lobby's menu, or a server's own command).");
|
||||
}
|
||||
|
||||
this.routingActive = true;
|
||||
logger.info("Felis routing ready: rootDomain={}, login={}, lobby={}. /link, /felis and /invite registered.",
|
||||
config.rootDomain(), config.loginServer(), config.lobbyServer());
|
||||
}
|
||||
|
||||
/**
|
||||
* yieldServerCommand removes Velocity's own /server, which answers every player
|
||||
* before any backend sees the command: it lists the login gate, the lobby and every
|
||||
* running server, and it shadows the lobby's /server, which opens the Felis menu.
|
||||
* Without it the command reaches the backend: the menu in the lobby, a user
|
||||
* server's own /server anywhere else. A /server another proxy plugin registered is
|
||||
* that plugin's choice and stays. Called only with routing active; without routing
|
||||
* the built-in is the one way to change servers.
|
||||
*/
|
||||
static boolean yieldServerCommand(CommandManager commands, PluginManager plugins) {
|
||||
CommandMeta meta = commands.getCommandMeta("server");
|
||||
if (meta == null) {
|
||||
return false;
|
||||
}
|
||||
// Velocity registers its built-ins under its virtual plugin, id "velocity";
|
||||
// older proxies registered them with no plugin at all.
|
||||
Object owner = meta.getPlugin();
|
||||
boolean builtin = owner == null || plugins.fromInstance(owner)
|
||||
.map(c -> "velocity".equals(c.getDescription().getId()))
|
||||
.orElse(false);
|
||||
if (!builtin) {
|
||||
return false;
|
||||
}
|
||||
commands.unregister("server");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDisconnect(DisconnectEvent event) {
|
||||
commandBudget.forget(event.getPlayer().getUniqueId());
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package best.lolicon.felis.velocity;
|
||||
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import static best.lolicon.felis.velocity.Fakes.UNANSWERED;
|
||||
import static best.lolicon.felis.velocity.Fakes.fake;
|
||||
|
||||
/**
|
||||
* ServerCommandTest drives FelisVelocityPlugin.yieldServerCommand against a fake
|
||||
* command and plugin manager shaped like Velocity 3.5.1's: its built-ins are owned by
|
||||
* the virtual plugin whose id is "velocity", aliases are case-insensitive, and
|
||||
* unregistering removes only the alias named. Velocity's own /server goes, and so does
|
||||
* one with no owner (how older proxies registered built-ins); a /server another plugin
|
||||
* registered, or whose owner the plugin manager cannot name, stays; with no /server at
|
||||
* all nothing is touched. Framework free: a failed assertion throws.
|
||||
*
|
||||
* <p>Run: {@code ./gradlew routingTest} in plugins/velocity.
|
||||
*/
|
||||
public final class ServerCommandTest {
|
||||
|
||||
private static int checks;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Object velocity = new Object();
|
||||
Object other = new Object();
|
||||
Plugins plugins = new Plugins();
|
||||
plugins.add(velocity, "velocity");
|
||||
plugins.add(other, "serverselector");
|
||||
|
||||
Commands builtin = new Commands();
|
||||
builtin.add("server", velocity);
|
||||
builtin.add("glist", velocity);
|
||||
builtin.add("felis", other);
|
||||
assertEq("Velocity's /server is yielded", true,
|
||||
FelisVelocityPlugin.yieldServerCommand(builtin.manager, plugins.manager));
|
||||
assertEq("... and gone", Set.of("glist", "felis"), builtin.aliases());
|
||||
assertEq("... by one unregister", 1, builtin.unregisters);
|
||||
|
||||
Commands ownerless = new Commands();
|
||||
ownerless.add("Server", null);
|
||||
assertEq("an ownerless /server is yielded", true,
|
||||
FelisVelocityPlugin.yieldServerCommand(ownerless.manager, plugins.manager));
|
||||
assertEq("... and gone", Set.of(), ownerless.aliases());
|
||||
|
||||
Commands theirs = new Commands();
|
||||
theirs.add("server", other);
|
||||
assertEq("another plugin's /server is kept", false,
|
||||
FelisVelocityPlugin.yieldServerCommand(theirs.manager, plugins.manager));
|
||||
assertEq("... registered", Set.of("server"), theirs.aliases());
|
||||
assertEq("... untouched", 0, theirs.unregisters);
|
||||
|
||||
Object lookalike = new Object();
|
||||
plugins.add(lookalike, "velocitytools");
|
||||
Commands near = new Commands();
|
||||
near.add("server", lookalike);
|
||||
assertEq("a plugin whose id only starts with velocity keeps its /server", false,
|
||||
FelisVelocityPlugin.yieldServerCommand(near.manager, plugins.manager));
|
||||
assertEq("... registered", Set.of("server"), near.aliases());
|
||||
|
||||
Commands unknown = new Commands();
|
||||
unknown.add("server", new Object());
|
||||
assertEq("a /server of an owner no plugin claims is kept", false,
|
||||
FelisVelocityPlugin.yieldServerCommand(unknown.manager, plugins.manager));
|
||||
assertEq("... registered", Set.of("server"), unknown.aliases());
|
||||
|
||||
Commands none = new Commands();
|
||||
none.add("glist", velocity);
|
||||
assertEq("no /server: nothing to yield", false,
|
||||
FelisVelocityPlugin.yieldServerCommand(none.manager, plugins.manager));
|
||||
assertEq("... nothing unregistered", 0, none.unregisters);
|
||||
assertEq("... the rest kept", Set.of("glist"), none.aliases());
|
||||
|
||||
System.out.println("ServerCommandTest OK (" + checks + " checks)");
|
||||
}
|
||||
|
||||
/** Commands holds one meta per alias, keyed the way Velocity keys them: lower case. */
|
||||
private static final class Commands {
|
||||
final Map<String, CommandMeta> byAlias = new TreeMap<>();
|
||||
int unregisters;
|
||||
final CommandManager manager = fake(CommandManager.class, (m, a) -> {
|
||||
switch (m) {
|
||||
case "getCommandMeta":
|
||||
return byAlias.get(((String) a[0]).toLowerCase(Locale.ENGLISH));
|
||||
case "hasCommand":
|
||||
return a.length == 1 && byAlias.containsKey(((String) a[0]).toLowerCase(Locale.ENGLISH));
|
||||
case "unregister":
|
||||
if (a[0] instanceof String alias) {
|
||||
unregisters++;
|
||||
byAlias.remove(alias.toLowerCase(Locale.ENGLISH));
|
||||
return null;
|
||||
}
|
||||
throw new UnsupportedOperationException("unregister(" + a[0].getClass().getSimpleName() + ")");
|
||||
default:
|
||||
return UNANSWERED;
|
||||
}
|
||||
});
|
||||
|
||||
void add(String alias, Object owner) {
|
||||
byAlias.put(alias.toLowerCase(Locale.ENGLISH), fake(CommandMeta.class, (m, a) -> {
|
||||
switch (m) {
|
||||
case "getPlugin":
|
||||
return owner;
|
||||
case "getAliases":
|
||||
return Set.of(alias.toLowerCase(Locale.ENGLISH));
|
||||
default:
|
||||
return UNANSWERED;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Set<String> aliases() {
|
||||
return Set.copyOf(byAlias.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
/** Plugins maps each plugin instance to its container, as fromInstance does. */
|
||||
private static final class Plugins {
|
||||
final Map<Object, PluginContainer> byInstance = new IdentityHashMap<>();
|
||||
final PluginManager manager = fake(PluginManager.class, (m, a) ->
|
||||
"fromInstance".equals(m) ? fromInstance(a[0]) : UNANSWERED);
|
||||
|
||||
// Velocity's own fromInstance refuses a null instance.
|
||||
private Optional<PluginContainer> fromInstance(Object instance) {
|
||||
if (instance == null) {
|
||||
throw new NullPointerException("instance");
|
||||
}
|
||||
return Optional.ofNullable(byInstance.get(instance));
|
||||
}
|
||||
|
||||
void add(Object instance, String id) {
|
||||
PluginDescription description = fake(PluginDescription.class,
|
||||
(m, a) -> "getId".equals(m) ? id : UNANSWERED);
|
||||
byInstance.put(instance, fake(PluginContainer.class, (m, a) -> {
|
||||
switch (m) {
|
||||
case "getDescription":
|
||||
return description;
|
||||
case "getInstance":
|
||||
return Optional.of(instance);
|
||||
default:
|
||||
return UNANSWERED;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertEq(String what, Object want, Object got) {
|
||||
if (want == null ? got != null : !want.equals(got)) {
|
||||
throw new AssertionError(what + ": got " + got + ", want " + want);
|
||||
}
|
||||
checks++;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user