-
-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Registering commands during server runtime (Bukkit) #417
Conversation
a8daa8b
to
f567f9c
Compare
8f6f81b
to
acebbdd
Compare
This PR now also allows unregistering commands during server runtime. Taking a closer look at the command system, I was also able to clean up the logic as well. public void unregister(String commandName, boolean force) {
CommandAPI.logInfo("Unregistering command /" + commandName);
// Remove nodes from the Brigadier dispatcher
RootCommandNode<Source> brigRoot = getBrigadierDispatcher().getRoot();
commandNodeChildren.get(brigRoot).remove(commandName);
commandNodeLiterals.get(brigRoot).remove(commandName);
// We really only expect commands to be represented as literals, but it is technically possible
// to put an ArgumentCommandNode in here, so we'll check
commandNodeArguments.get(brigRoot).remove(commandName);
if (!CommandAPI.canRegister() || force) {
// Bukkit is done with normal command stuff, so we have to modify their CommandMap ourselves
// If we're forcing, we'll also go here to make sure commands are really gone
Map<String, Command> knownCommands = getKnownCommands();
knownCommands.remove(commandName);
if (force) removeCommandNamespace(knownCommands, commandName);
// Remove commands from the resources dispatcher
RootCommandNode<Source> resourcesRoot = getResourcesDispatcher().getRoot();
Map<String, CommandNode<?>> children = commandNodeChildren.get(resourcesRoot);
Map<String, CommandNode<?>> literals = commandNodeLiterals.get(resourcesRoot);
Map<String, CommandNode<?>> arguments = commandNodeArguments.get(resourcesRoot);
children.remove(commandName);
literals.remove(commandName);
arguments.remove(commandName);
// Since the commands in here are copied from Bukkit's map, there may be namespaced versions of the command, which we should remove
if (force) {
removeCommandNamespace(children, commandName);
removeCommandNamespace(literals, commandName);
removeCommandNamespace(arguments, commandName);
}
if (!CommandAPI.canRegister()) {
// If the server actually is running (not just force unregistering), we should also update help and notify players
getHelpMap().remove("/" + commandName);
for (Player p : Bukkit.getOnlinePlayers()) {
resendPackets(p);
}
}
}
}
private void removeCommandNamespace(Map<String, ?> map, String commandName) {
for(String key : new HashSet<>(map.keySet())) {
if(key.contains(":") && key.split(":")[1].equalsIgnoreCase(commandName)) {
map.remove(key);
}
}
}
private Map<String, Command> getKnownCommands() {
return commandMapKnownCommands.get((SimpleCommandMap) paper.getCommandMap());
} Notable changes:
|
8f42594
to
56fc552
Compare
Rebased JorelAli#471 into runtime registering (JorelAli#417)
Rebased JorelAli#471 into runtime registering (JorelAli#417)
Remove messages that registering/unregistering while the server is running does not work Remove test commands Update relevant documentation
If my understanding is correct, the only thing left to do is Add tests for unregistering commands? From what I can tell, the majority of this PR looks feature complete? |
Yep. I'm almost done with that too. I just need to finish applying some changes to all of the MockNMS classes that let me test executing VanillaCommandWrappers. A final code review would probably also be good. I've also technically checked compatibility with PaperMC/Paper#8235. It seems the CommandAPI without these changes is currently incompatible. I get this exception when trying to register a command, shading version 9.0.3: Error occurred while enabling CommandAPITest v1.0.0 (Is it up to date?)
java.lang.NoSuchFieldError: vanillaCommandDispatcher
at me.willkroboth.CommandAPITest.commandapi.nms.NMS_1_20_R1.getBrigadierDispatcher(NMS_1_20_R1.java:333) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at me.willkroboth.CommandAPITest.commandapi.CommandAPIBukkit.registerCommandNode(CommandAPIBukkit.java:442) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at me.willkroboth.CommandAPITest.commandapi.CommandAPIHandler.register(CommandAPIHandler.java:640) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at me.willkroboth.CommandAPITest.commandapi.AbstractCommandAPICommand.register(AbstractCommandAPICommand.java:301) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at me.willkroboth.CommandAPITest.Main.register(Main.java:43) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at me.willkroboth.CommandAPITest.Main.onEnable(Main.java:26) ~[CommandAPITest-1.0-SNAPSHOT.jar:?]
at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:281) ~[paper-api-1.20.1-R0.1-SNAPSHOT.jar:?]
at io.papermc.paper.plugin.manager.PaperPluginInstanceManager.enablePlugin(PaperPluginInstanceManager.java:189) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.enablePlugin(PaperPluginManagerImpl.java:104) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:507) ~[paper-api-1.20.1-R0.1-SNAPSHOT.jar:?]
at org.bukkit.craftbukkit.v1_20_R1.CraftServer.enablePlugin(CraftServer.java:612) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at org.bukkit.craftbukkit.v1_20_R1.CraftServer.enablePlugins(CraftServer.java:556) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at net.minecraft.server.MinecraftServer.loadWorld0(MinecraftServer.java:636) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at net.minecraft.server.MinecraftServer.loadLevel(MinecraftServer.java:435) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at net.minecraft.server.dedicated.DedicatedServer.initServer(DedicatedServer.java:308) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1103) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:318) ~[paper-1.20.1.jar:git-Paper-"fa50e35"]
at java.lang.Thread.run(Thread.java:833) ~[?:?] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor points to address.
One-liner methods that are only ever called once by one area of code aren't necessary nor needed - just inline the function.
To ensure we don't shoot ourselves in the foot, we should probably state how unregistering a command without using the force
parameter will still cause the command to be present but unusable. In the case of "unusable", we should state what happens if they try to run the command (e.g. does it crash? does it throw an exception? does it tell the user the command doesn't exist?). In this case, we should also probably provide a recommendation (e.g. always unregister commands by force).
...ndapi-bukkit/commandapi-bukkit-core/src/main/java/dev/jorel/commandapi/CommandAPIBukkit.java
Outdated
Show resolved
Hide resolved
...test/commandapi-bukkit-test-tests/src/test/java/dev/jorel/commandapi/test/OnEnableTests.java
Show resolved
Hide resolved
Rebased JorelAli#471 into runtime registering (JorelAli#417)
Remove messages that registering/unregistering while the server is running does not work Remove test commands Update relevant documentation
While I was trying to figure out how to document the unregistering process, I noticed some limitations with the current API for Bukkit unregistering. Specifically, it was impossible to:
This happened because
You can't choose between all the possible combinations there with only one boolean. This is related to the discussion over at #210 (comment). To fix this, I added a parameter For records sake, this is what @Override
public void unregister(String commandName, boolean unregisterNamespaces) {
unregister(commandName, unregisterNamespaces, false);
}
public void unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit) {
CommandAPI.logInfo("Unregistering command /" + commandName);
if(!unregisterBukkit) {
// Remove nodes from the Vanilla dispatcher
// This dispatcher doesn't usually have namespaced version of commands (those are created when commands
// are transferred to Bukkit's CommandMap), but if they ask, we'll do it
removeBrigadierCommands(getBrigadierDispatcher(), commandName, unregisterNamespaces);
// Update the dispatcher file
CommandAPIHandler.getInstance().writeDispatcherToFile();
}
if(unregisterBukkit || !CommandAPI.canRegister()) {
// We need to remove commands from Bukkit's CommandMap if we're unregistering a Bukkit command, or
// if we're unregistering after the server is enabled, because `CraftServer#setVanillaCommands` will have
// moved the Vanilla command into the CommandMap
// If we are unregistering a Bukkit command, DO NOT unregister VanillaCommandWrappers
// If we are unregistering a Vanilla command, ONLY unregister VanillaCommandWrappers
Map<String, Command> knownCommands = commandMapKnownCommands.get((SimpleCommandMap) paper.getCommandMap());
boolean isMainVanilla = isVanillaCommandWrapper(knownCommands.get(commandName));
if(unregisterBukkit ^ isMainVanilla) knownCommands.remove(commandName);
if(unregisterNamespaces) {
for (String key : new HashSet<>(knownCommands.keySet())) {
if(!isThisTheCommandButNamespaced(commandName, key)) continue;
boolean isVanilla = isVanillaCommandWrapper(knownCommands.get(key));
if(unregisterBukkit ^ isVanilla) knownCommands.remove(key);
}
}
}
if(!CommandAPI.canRegister()) {
// If the server is enabled, we have extra cleanup to do
// Remove commands from the resources dispatcher
removeBrigadierCommands(getResourcesDispatcher(), commandName, unregisterNamespaces);
// Help topics (from Bukkit and CommandAPI) are only setup after plugins enable, so we only need to worry
// about removing them once the server is loaded.
getHelpMap().remove("/" + commandName);
// Notify players
for (Player p : Bukkit.getOnlinePlayers()) {
p.updateCommands();
}
}
}
private void removeBrigadierCommands(CommandDispatcher<Source> dispatcher, String commandName, boolean unregisterNamespaces) {
RootCommandNode<Source> root = dispatcher.getRoot();
Map<String, CommandNode<?>> children = commandNodeChildren.get(root);
Map<String, CommandNode<?>> literals = commandNodeLiterals.get(root);
Map<String, CommandNode<?>> arguments = commandNodeArguments.get(root);
children.remove(commandName);
literals.remove(commandName);
// Commands should really only be represented as literals, but it is technically possible
// to put an ArgumentCommandNode in the root, so we'll check
arguments.remove(commandName);
if (unregisterNamespaces) {
removeBrigadierCommandNamespace(children, commandName);
removeBrigadierCommandNamespace(literals, commandName);
removeBrigadierCommandNamespace(arguments, commandName);
}
}
private void removeBrigadierCommandNamespace(Map<String, CommandNode<?>> map, String commandName) {
for(String key : new HashSet<>(map.keySet())) {
if(isThisTheCommandButNamespaced(commandName, key)) {
map.remove(key);
}
}
}
private static boolean isThisTheCommandButNamespaced(String commandName, String key) {
return key.contains(":") && key.split(":")[1].equalsIgnoreCase(commandName);
} No major logic changes. I just needed to separate out the logic that was merged together under
With these changes though, the original situations I mentioned before are possible like so: // Unregister `reload` and `bukkit:reload`, but not `minecraft:reload`
CommandAPIBukkit.get().unregister("reload", true, true);
// Unregister `luckperms` but not `luckperms:luckperms`
CommandAPIBukkit.get().unregister("luckperms", false, true); TODO:
|
…kkit is enabled would not be sent to the player
…atcher Reorganize shared logic in 3 unregistering steps Add NMS#isBukkitCommandWrapper method to tell when a node in the resources dispatcher represents a Bukkit command
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly some anchor and typo stuff.
Regarding the anchors, I've written I explained the reason in the initial comment starting this review
which I am doing now.
I think I've never really explained what my goal was when refactoring the documentation examples during 9.0.0 development besides being better to find.
By "better to find" I mean something like "I look at a page's link (as this mirrors the file name), find the example I want to edit, find its place (for example I want to edit the first example on a page called x.html
) and then look for the example with the anchor [pageName]1
(so for the page x.html
the anchor name would be x1
) which is placed in an alphabetical order that follows the anchor names."
That results in an order where the examples of the advancement page are pretty much at the top while the examples for tooltips are pretty much at the bottom.
To keep consistency with basically the rest of the documentation I would suggest following the anchor name changes I am suggesting in the relevant comments.
Ah, I didn't know about that naming convention, so I came up with my own based on what the example did. I can change it to the numbering 👍 Also thanks for pointing out those typos. Not sure how spellcheck didn't catch |
…istration#` to fit with other documentation anchors. Fix unregistration example alphabetization in relation to `commandTree`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's one hell of a documentation for unregistering commands.
It's long, but the examples are really easy to digest. While normally I'd consider this to be a bit of information-overload, I think the explanation that there is a specific order to how commands are set up in the server loading sequence is informative and helpful in understanding how things work and guiding developers to know what they need to do to achieve the output they want.
The example for the specific edge cases (e.g. Bukkit help and Minecraft namespaced commands only) is helpful and useful.
I'm happy with the result. I think that's everything for this PR, feel free to merge this to dev/dev
.
Well, a changelog entry is missing but yes, this is definitely ready to be merged. |
This PR makes it possible to register commands while a Bukkit server is running.
Previously, if a developer attempted to register a command while the server was running, there would be a little warning
Command /(command name) is being registered after the server had loaded. Undefined behavior ahead!
. Unfortunately, it would pretty much seem like nothing at all happened.Usually, the CommandAPI can just put commands into the
MinecraftServer#vanillaCommandDispatcher
CommandDispatcher and be done. Behind the scenes, CraftServer would sync everything up, and the commands would work. However, that sync process usually only happens as the server is finishing up its load and enabling processes. In order to get commands registered during server runtime to work properly, those synchronization processes just need to happen again.So, with this PR,
CommandAPIBukkit#postCommandRegistration
now looks like this:If the server has finished loading (indicated by
CommandAPI.canRegister()
being false), this extra code is run to:CommandMap
CommandDispatcher
VanillaCommandWrapper
version of the commandTo help with this, two NMS methods were added:
To test this feature, these commands were added.
These test commands should be removed before merging
TODO:
Check compatibility with Brigadier Command Support PaperMC/Paper#8235?9.0.3 is not currently compatible