-
-
Notifications
You must be signed in to change notification settings - Fork 57
/
CommandAPIBukkit.java
774 lines (663 loc) · 31.4 KB
/
CommandAPIBukkit.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
package dev.jorel.commandapi;
import static dev.jorel.commandapi.preprocessor.Unimplemented.REASON.REQUIRES_CRAFTBUKKIT;
import static dev.jorel.commandapi.preprocessor.Unimplemented.REASON.REQUIRES_CSS;
import static dev.jorel.commandapi.preprocessor.Unimplemented.REASON.REQUIRES_MINECRAFT_SERVER;
import static dev.jorel.commandapi.preprocessor.Unimplemented.REASON.VERSION_SPECIFIC_IMPLEMENTATION;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import dev.jorel.commandapi.commandsenders.*;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Keyed;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.help.HelpTopic;
import org.bukkit.inventory.Recipe;
import org.bukkit.permissions.Permission;
import org.bukkit.plugin.java.JavaPlugin;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import dev.jorel.commandapi.arguments.Argument;
import dev.jorel.commandapi.arguments.LiteralArgument;
import dev.jorel.commandapi.arguments.MultiLiteralArgument;
import dev.jorel.commandapi.arguments.SuggestionProviders;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.nms.NMS;
import dev.jorel.commandapi.preprocessor.RequireField;
import dev.jorel.commandapi.preprocessor.Unimplemented;
import dev.jorel.commandapi.wrappers.NativeProxyCommandSender;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.chat.BaseComponent;
// CommandAPIBukkit is an CommandAPIPlatform, but also needs all of the methods from
// NMS, so it implements NMS. Our implementation of CommandAPIBukkit is now derived
// using the version handler (and thus, deferred to our NMS-specific implementations)
@RequireField(in = CommandNode.class, name = "children", ofType = Map.class)
@RequireField(in = CommandNode.class, name = "literals", ofType = Map.class)
@RequireField(in = CommandNode.class, name = "arguments", ofType = Map.class)
public abstract class CommandAPIBukkit<Source> implements CommandAPIPlatform<Argument<?>, CommandSender, Source>, NMS<Source> {
// References to utility classes
private static CommandAPIBukkit<?> instance;
private static InternalBukkitConfig config;
private PaperImplementations paper;
// Static VarHandles
// I'd like to make the Maps here `Map<String, CommandNode<Source>>`, but these static fields cannot use the type
// parameter Source. We still need to cast to that signature for map, so Map is raw.
private static final SafeVarHandle<CommandNode<?>, Map> commandNodeChildren;
private static final SafeVarHandle<CommandNode<?>, Map> commandNodeLiterals;
private static final SafeVarHandle<CommandNode<?>, Map> commandNodeArguments;
private static final SafeVarHandle<SimpleCommandMap, Map<String, Command>> commandMapKnownCommands;
// Compute all var handles all in one go so we don't do this during main server runtime
static {
commandNodeChildren = SafeVarHandle.ofOrNull(CommandNode.class, "children", "children", Map.class);
commandNodeLiterals = SafeVarHandle.ofOrNull(CommandNode.class, "literals", "literals", Map.class);
commandNodeArguments = SafeVarHandle.ofOrNull(CommandNode.class, "arguments", "arguments", Map.class);
commandMapKnownCommands = SafeVarHandle.ofOrNull(SimpleCommandMap.class, "knownCommands", "knownCommands", Map.class);
}
protected CommandAPIBukkit() {
CommandAPIBukkit.instance = this;
}
@SuppressWarnings("unchecked")
public static <Source> CommandAPIBukkit<Source> get() {
if(CommandAPIBukkit.instance != null) {
return (CommandAPIBukkit<Source>) instance;
} else {
throw new IllegalStateException("Tried to access CommandAPIBukkit instance, but it was null! Are you using CommandAPI features before calling CommandAPI#onLoad?");
}
}
public PaperImplementations getPaper() {
return paper;
}
public static InternalBukkitConfig getConfiguration() {
if(config != null) {
return config;
} else {
throw new IllegalStateException("Tried to access InternalBukkitConfig, but it was null! Did you load the CommandAPI properly with CommandAPI#onLoad?");
}
}
@Override
public void onLoad(CommandAPIConfig<?> config) {
if(config instanceof CommandAPIBukkitConfig bukkitConfig) {
CommandAPIBukkit.setInternalConfig(new InternalBukkitConfig(bukkitConfig));
} else {
CommandAPI.logError("CommandAPIBukkit was loaded with non-Bukkit config!");
CommandAPI.logError("Attempts to access Bukkit-specific config variables will fail!");
}
checkDependencies();
}
private static void setInternalConfig(InternalBukkitConfig internalBukkitConfig) {
CommandAPIBukkit.config = internalBukkitConfig;
}
private void checkDependencies() {
// Log successful hooks
Class<?> nbtContainerClass = CommandAPI.getConfiguration().getNBTContainerClass();
if (nbtContainerClass != null && CommandAPI.getConfiguration().getNBTContainerConstructor() != null) {
CommandAPI.logNormal("Hooked into an NBT API with class " + nbtContainerClass.getName());
} else {
if (CommandAPI.getConfiguration().hasVerboseOutput()) {
CommandAPI.logWarning(
"Could not hook into the NBT API for NBT support. Download it from https://www.spigotmc.org/resources/nbt-api.7939/");
}
}
try {
Class.forName("org.spigotmc.SpigotConfig");
CommandAPI.logNormal("Hooked into Spigot successfully for Chat/ChatComponents");
} catch (ClassNotFoundException e) {
if (CommandAPI.getConfiguration().hasVerboseOutput()) {
CommandAPI.logWarning("Could not hook into Spigot for Chat/ChatComponents");
}
}
try {
Class.forName("net.kyori.adventure.text.Component");
CommandAPI.logNormal("Hooked into Adventure for AdventureChat/AdventureChatComponents");
} catch (ClassNotFoundException e) {
if (CommandAPI.getConfiguration().hasVerboseOutput()) {
CommandAPI.logWarning("Could not hook into Adventure for AdventureChat/AdventureChatComponents");
}
}
boolean isPaperPresent = false;
try {
Class.forName("io.papermc.paper.event.server.ServerResourcesReloadedEvent");
isPaperPresent = true;
CommandAPI.logNormal("Hooked into Paper for paper-specific API implementations");
} catch (ClassNotFoundException e) {
isPaperPresent = false;
if (CommandAPI.getConfiguration().hasVerboseOutput()) {
CommandAPI.logWarning("Could not hook into Paper for /minecraft:reload. Consider upgrading to Paper: https://papermc.io/");
}
}
boolean isFoliaPresent = false;
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServerInitEvent");
isFoliaPresent = true;
CommandAPI.logNormal("Hooked into Folia for folia-specific API implementations");
CommandAPI.logNormal("Folia support is still in development. Please report any issues to the CommandAPI developers!");
} catch (ClassNotFoundException e) {
isFoliaPresent = false;
}
paper = new PaperImplementations(isPaperPresent, isFoliaPresent, this);
}
@Override
public void onEnable() {
JavaPlugin plugin = config.getPlugin();
new Schedulers(paper).scheduleSyncDelayed(plugin, () -> {
// Sort out permissions after the server has finished registering them all
fixPermissions();
setupNamespaces();
if (paper.isFoliaPresent()) {
CommandAPI.logNormal("Skipping initial datapack reloading because Folia was detected");
} else {
reloadDataPacks();
}
updateHelpForCommands(CommandAPI.getRegisteredCommands());
}, 0L);
// Prevent command registration after server has loaded
Bukkit.getServer().getPluginManager().registerEvents(new Listener() {
// We want the lowest priority so that we always get to this first, in case a dependent plugin is using
// CommandAPI features in their own ServerLoadEvent listener for some reason
@EventHandler(priority = EventPriority.LOWEST)
public void onServerLoad(ServerLoadEvent event) {
CommandAPI.stopCommandRegistration();
}
}, getConfiguration().getPlugin());
paper.registerReloadHandler(plugin);
}
/*
* Makes permission checks more "Bukkit" like and less "Vanilla Minecraft" like
*/
private void fixPermissions() {
// Get the command map to find registered commands
CommandMap map = paper.getCommandMap();
final Map<String, CommandPermission> permissionsToFix = CommandAPIHandler.getInstance().registeredPermissions;
if (!permissionsToFix.isEmpty()) {
CommandAPI.logInfo("Linking permissions to commands:");
for (Map.Entry<String, CommandPermission> entry : permissionsToFix.entrySet()) {
String cmdName = entry.getKey();
CommandPermission perm = entry.getValue();
CommandAPI.logInfo(" " + perm.toString() + " -> /" + cmdName);
final String permNode = unpackInternalPermissionNodeString(perm);
/*
* Sets the permission. If you have to be OP to run this command, we set the
* permission to null. Doing so means that Bukkit's "testPermission" will always
* return true, however since the command's permission check occurs internally
* via the CommandAPI, this isn't a problem.
*
* If anyone dares tries to use testPermission() on this command, seriously,
* what are you doing and why?
*/
for(Command command : new Command[] { map.getCommand(cmdName), map.getCommand("minecraft:" + cmdName) }) {
if (command != null && isVanillaCommandWrapper(command)) {
command.setPermission(permNode);
}
}
}
}
CommandAPI.logNormal("Linked " + permissionsToFix.size() + " Bukkit permissions to commands");
}
private String unpackInternalPermissionNodeString(CommandPermission perm) {
final Optional<String> optionalPerm = perm.getPermission();
if (perm.isNegated() || perm.equals(CommandPermission.NONE) || perm.equals(CommandPermission.OP)) {
return "";
} else if (optionalPerm.isPresent()) {
return optionalPerm.get();
} else {
throw new IllegalStateException("Invalid permission detected: " + perm +
"! This should never happen - if you're seeing this message, please" +
"contact the developers of the CommandAPI, we'd love to know how you managed to get this error!");
}
}
/*
* Generate and register help topics
*/
private String generateCommandHelpPrefix(String command) {
return (Bukkit.getPluginCommand(command) == null ? "/" : "/minecraft:") + command;
}
private void generateHelpUsage(StringBuilder sb, RegisteredCommand command) {
// Generate usages
String[] usages = getUsageList(command);
if (usages.length == 0) {
// Might happen if the developer calls `.withUsage()` with no parameters
// They didn't give any usage, so we won't put any there
return;
}
sb.append(ChatColor.GOLD).append("Usage: ").append(ChatColor.WHITE);
// If 1 usage, put it on the same line, otherwise format like a list
if (usages.length == 1) {
sb.append(usages[0]);
} else {
for (String usage : usages) {
sb.append("\n- ").append(usage);
}
}
}
private String[] getUsageList(RegisteredCommand currentCommand) {
List<RegisteredCommand> commandsWithIdenticalNames = new ArrayList<>();
// Collect every command with the same name
for (RegisteredCommand registeredCommand : CommandAPIHandler.getInstance().registeredCommands) {
if (registeredCommand.commandName().equals(currentCommand.commandName())) {
commandsWithIdenticalNames.add(registeredCommand);
}
}
// Generate command usage or fill it with a user provided one
final String[] usages;
final Optional<String[]> usageDescription = currentCommand.usageDescription();
if (usageDescription.isPresent()) {
usages = usageDescription.get();
} else {
// TODO: Figure out if default usage generation should be updated
final int numCommandsWithIdenticalNames = commandsWithIdenticalNames.size();
usages = new String[numCommandsWithIdenticalNames];
for (int i = 0; i < numCommandsWithIdenticalNames; i++) {
final RegisteredCommand command = commandsWithIdenticalNames.get(i);
StringBuilder usageString = new StringBuilder();
usageString.append("/").append(command.commandName()).append(" ");
for (String arg : command.argsAsStr()) {
usageString.append("<").append(arg.split(":")[0]).append("> ");
}
usages[i] = usageString.toString().trim();
}
}
return usages;
}
void updateHelpForCommands(List<RegisteredCommand> commands) {
Map<String, HelpTopic> helpTopicsToAdd = new HashMap<>();
for (RegisteredCommand command : commands) {
// Generate short description
final String shortDescription;
final Optional<String> shortDescriptionOptional = command.shortDescription();
final Optional<String> fullDescriptionOptional = command.fullDescription();
if (shortDescriptionOptional.isPresent()) {
shortDescription = shortDescriptionOptional.get();
} else if (fullDescriptionOptional.isPresent()) {
shortDescription = fullDescriptionOptional.get();
} else {
shortDescription = "A command by the " + config.getPlugin().getName() + " plugin.";
}
// Generate full description
StringBuilder sb = new StringBuilder();
if (fullDescriptionOptional.isPresent()) {
sb.append(ChatColor.GOLD).append("Description: ").append(ChatColor.WHITE).append(fullDescriptionOptional.get()).append("\n");
}
generateHelpUsage(sb, command);
sb.append("\n");
// Generate aliases. We make a copy of the StringBuilder because we
// want to change the output when we register aliases
StringBuilder aliasSb = new StringBuilder(sb.toString());
if (command.aliases().length > 0) {
sb.append(ChatColor.GOLD).append("Aliases: ").append(ChatColor.WHITE).append(String.join(", ", command.aliases()));
}
// Must be empty string, not null as defined by OBC::CustomHelpTopic
String permission = command.permission().getPermission().orElse("");
// Don't override the plugin help topic
String commandPrefix = generateCommandHelpPrefix(command.commandName());
helpTopicsToAdd.put(commandPrefix, generateHelpTopic(commandPrefix, shortDescription, sb.toString().trim(), permission));
for (String alias : command.aliases()) {
StringBuilder currentAliasSb = new StringBuilder(aliasSb.toString());
currentAliasSb.append(ChatColor.GOLD).append("Aliases: ").append(ChatColor.WHITE);
// We want to get all aliases (including the original command name),
// except for the current alias
List<String> aliases = new ArrayList<>(Arrays.asList(command.aliases()));
aliases.add(command.commandName());
aliases.remove(alias);
currentAliasSb.append(ChatColor.WHITE).append(String.join(", ", aliases));
// Don't override the plugin help topic
commandPrefix = generateCommandHelpPrefix(alias);
helpTopicsToAdd.put(commandPrefix, generateHelpTopic(commandPrefix, shortDescription, currentAliasSb.toString().trim(), permission));
}
}
// We have to use helpTopics.put (instead of .addTopic) because we're overwriting an existing help topic, not adding a new help topic
getHelpMap().putAll(helpTopicsToAdd);
}
private void setupNamespaces() {
Map<String, Command> knownCommands = commandMapKnownCommands.get((SimpleCommandMap) paper.getCommandMap());
List<String> commandsToRemove = new ArrayList<>();
Map<String, Command> commandsToAdd = new HashMap<>();
Map<String, String> commandsWithPrefix = new HashMap<>();
for (RegisteredCommand command : CommandAPI.getRegisteredCommands()) {
for (String key : knownCommands.keySet()) {
String commandName = (key.contains(":")) ? key.split(":")[1] : key;
if (commandName.equals(command.commandName()) || Arrays.asList(command.aliases()).contains(commandName)) {
Command registeredCommand = knownCommands.get(commandName);
commandsToAdd.put(commandName, registeredCommand);
commandsWithPrefix.put(commandName, command.namespace());
commandsToRemove.add(key);
}
}
}
for (String command : commandsToRemove) {
knownCommands.remove(command);
}
for (String command : commandsToAdd.keySet()) {
Command bukkitCommand = commandsToAdd.get(command);
knownCommands.put(command, bukkitCommand);
knownCommands.put(commandsWithPrefix.get(command) + ":" + command, knownCommands.get(command));
}
syncCommands();
CommandAPIHandler.getInstance().writeDispatcherToFile();
}
@Override
public void onDisable() {
// Nothing to do
}
@Override
@Unimplemented(because = REQUIRES_CSS)
public abstract BukkitCommandSender<? extends CommandSender> getSenderForCommand(CommandContext<Source> cmdCtx, boolean forceNative);
@Override
@Unimplemented(because = REQUIRES_CSS)
public abstract BukkitCommandSender<? extends CommandSender> getCommandSenderFromCommandSource(Source cs);
@Override
@Unimplemented(because = REQUIRES_CRAFTBUKKIT)
public abstract Source getBrigadierSourceFromCommandSender(AbstractCommandSender<? extends CommandSender> sender);
public BukkitCommandSender<? extends CommandSender> wrapCommandSender(CommandSender sender) {
if (sender instanceof BlockCommandSender block) {
return new BukkitBlockCommandSender(block);
}
if (sender instanceof ConsoleCommandSender console) {
return new BukkitConsoleCommandSender(console);
}
if (sender instanceof Player player) {
return new BukkitPlayer(player);
}
if (sender instanceof org.bukkit.entity.Entity entity) {
return new BukkitEntity(entity);
}
if (sender instanceof NativeProxyCommandSender nativeProxy) {
return new BukkitNativeProxyCommandSender(nativeProxy);
}
if (sender instanceof ProxiedCommandSender proxy) {
return new BukkitProxiedCommandSender(proxy);
}
if (sender instanceof RemoteConsoleCommandSender remote) {
return new BukkitRemoteConsoleCommandSender(remote);
}
if (paper.isPaperPresent()) {
final Class<? extends CommandSender> FeedbackForwardingSender = paper.getFeedbackForwardingCommandSender();
if (FeedbackForwardingSender.isInstance(sender)) {
// We literally cannot type this at compile-time, so let's use a placeholder CommandSender instance
return new BukkitFeedbackForwardingCommandSender<CommandSender>(FeedbackForwardingSender.cast(sender));
}
}
throw new RuntimeException("Failed to wrap CommandSender " + sender + " to a CommandAPI-compatible BukkitCommandSender");
}
@Override
public void registerPermission(String string) {
try {
Bukkit.getPluginManager().addPermission(new Permission(string));
} catch (IllegalArgumentException e) {
assert true; // nop, not an error.
}
}
@Override
@Unimplemented(because = REQUIRES_MINECRAFT_SERVER)
public abstract SuggestionProvider<Source> getSuggestionProvider(SuggestionProviders suggestionProvider);
@Override
public void preCommandRegistration(String commandName) {
// Warn if the command we're registering already exists in this plugin's
// plugin.yml file
final PluginCommand pluginCommand = Bukkit.getPluginCommand(commandName);
if (pluginCommand == null) {
return;
}
String pluginName = pluginCommand.getPlugin().getName();
if (config.getPlugin().getName().equals(pluginName)) {
CommandAPI.logWarning(
"Plugin command /%s is registered by Bukkit (%s). Did you forget to remove this from your plugin.yml file?"
.formatted(commandName, pluginName));
} else {
CommandAPI.logNormal(
"Plugin command /%s is registered by Bukkit (%s). You may have to use /minecraft:%s to execute your command."
.formatted(commandName, pluginName, commandName));
}
}
@Override
public void postCommandRegistration(RegisteredCommand registeredCommand, LiteralCommandNode<Source> resultantNode, List<LiteralCommandNode<Source>> aliasNodes) {
if(!CommandAPI.canRegister()) {
// Usually, when registering commands during server startup, we can just put our commands into the
// `net.minecraft.server.MinecraftServer#vanillaCommandDispatcher` and leave it. As the server finishes setup,
// it and the CommandAPI do some extra stuff to make everything work, and we move on.
// So, if we want to register commands while the server is running, we need to do all that extra stuff, and
// that is what this code does.
// We could probably call all those methods to sync everything up, but in the spirit of avoiding side effects
// and avoiding doing things twice for existing commands, this is a distilled version of those methods.
CommandMap map = paper.getCommandMap();
String permNode = unpackInternalPermissionNodeString(registeredCommand.permission());
RootCommandNode<Source> root = getResourcesDispatcher().getRoot();
// Wrapping Brigadier nodes into VanillaCommandWrappers and putting them in the CommandMap usually happens
// in `CraftServer#setVanillaCommands`
Command command = wrapToVanillaCommandWrapper(resultantNode);
map.register("minecraft", command);
// Adding permissions to these Commands usually happens in `CommandAPIBukkit#onEnable`
command.setPermission(permNode);
// Adding commands to the other (Why bukkit/spigot?!) dispatcher usually happens in `CraftServer#syncCommands`
root.addChild(resultantNode);
root.addChild(namespaceNode(resultantNode));
// Do the same for the aliases
for(LiteralCommandNode<Source> node: aliasNodes) {
command = wrapToVanillaCommandWrapper(node);
map.register("minecraft", command);
command.setPermission(permNode);
root.addChild(node);
root.addChild(namespaceNode(node));
}
// Adding the command to the help map usually happens in `CommandAPIBukkit#onEnable`
updateHelpForCommands(List.of(registeredCommand));
// Sending command dispatcher packets usually happens when Players join the server
for(Player p: Bukkit.getOnlinePlayers()) {
p.updateCommands();
}
}
}
private LiteralCommandNode<Source> namespaceNode(LiteralCommandNode<Source> original) {
// Adapted from a section of `CraftServer#syncCommands`
LiteralCommandNode<Source> clone = new LiteralCommandNode<>(
"minecraft:" + original.getLiteral(),
original.getCommand(),
original.getRequirement(),
original.getRedirect(),
original.getRedirectModifier(),
original.isFork()
);
for (CommandNode<Source> child : original.getChildren()) {
clone.addChild(child);
}
return clone;
}
@Override
public LiteralCommandNode<Source> registerCommandNode(LiteralArgumentBuilder<Source> node) {
return getBrigadierDispatcher().register(node);
}
@Override
public void unregister(String commandName, boolean unregisterNamespaces) {
unregisterInternal(commandName, unregisterNamespaces, false);
}
/**
* Unregisters a command from the CommandGraph, so it can't be run anymore. This Bukkit-specific unregister has an
* additional parameter, {@code unregisterBukkit}, compared to {@link CommandAPI#unregister(String, boolean)}.
*
* @param commandName the name of the command to unregister
* @param unregisterNamespaces whether the unregistration system should attempt to remove versions of the
* command that start with a namespace. E.g. `minecraft:command`, `bukkit:command`,
* or `plugin:command`. If true, these namespaced versions of a command are also
* unregistered.
* @param unregisterBukkit whether the unregistration system should unregister Vanilla or Bukkit commands. If true,
* only Bukkit commands are unregistered, otherwise only Vanilla commands are unregistered.
* For the purposes of this parameter, commands registered using the CommandAPI are Vanilla
* commands, and commands registered by other plugin using Bukkit API are Bukkit commands.
*/
public static void unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit) {
CommandAPIBukkit.get().unregisterInternal(commandName, unregisterNamespaces, unregisterBukkit);
}
private void unregisterInternal(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, c -> true);
// 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
Map<String, Command> knownCommands = commandMapKnownCommands.get((SimpleCommandMap) paper.getCommandMap());
// If we are unregistering a Bukkit command, DO NOT unregister VanillaCommandWrappers
// If we are unregistering a Vanilla command, ONLY unregister VanillaCommandWrappers
boolean isMainVanilla = isVanillaCommandWrapper(knownCommands.get(commandName));
if(unregisterBukkit ^ isMainVanilla) knownCommands.remove(commandName);
if(unregisterNamespaces) {
removeCommandNamespace(knownCommands, commandName, c -> unregisterBukkit ^ isVanillaCommandWrapper(c));
}
}
if(!CommandAPI.canRegister()) {
// If the server is enabled, we have extra cleanup to do
// Remove commands from the resources dispatcher
// If we are unregistering a Bukkit command, ONLY unregister BukkitCommandWrappers
// If we are unregistering a Vanilla command, DO NOT unregister BukkitCommandWrappers
removeBrigadierCommands(getResourcesDispatcher(), commandName, unregisterNamespaces,
c -> !unregisterBukkit ^ isBukkitCommandWrapper(c));
// 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();
}
}
}
@Override
@Unimplemented(because = VERSION_SPECIFIC_IMPLEMENTATION)
public abstract void syncCommands();
private void removeBrigadierCommands(CommandDispatcher<Source> dispatcher, String commandName,
boolean unregisterNamespaces, Predicate<CommandNode<Source>> extraCheck) {
RootCommandNode<?> root = dispatcher.getRoot();
Map<String, CommandNode<Source>> children = (Map<String, CommandNode<Source>>) commandNodeChildren.get(root);
Map<String, CommandNode<Source>> literals = (Map<String, CommandNode<Source>>) commandNodeLiterals.get(root);
Map<String, CommandNode<Source>> arguments = (Map<String, CommandNode<Source>>) commandNodeArguments.get(root);
removeCommandFromMapIfCheckPasses(children, commandName, extraCheck);
removeCommandFromMapIfCheckPasses(literals, commandName, extraCheck);
// Commands should really only be represented as literals, but it is technically possible
// to put an ArgumentCommandNode in the root, so we'll check
removeCommandFromMapIfCheckPasses(arguments, commandName, extraCheck);
if (unregisterNamespaces) {
removeCommandNamespace(children, commandName, extraCheck);
removeCommandNamespace(literals, commandName, extraCheck);
removeCommandNamespace(arguments, commandName, extraCheck);
}
}
private static <T> void removeCommandNamespace(Map<String, T> map, String commandName, Predicate<T> extraCheck) {
for (String key : new HashSet<>(map.keySet())) {
if (!isThisTheCommandButNamespaced(commandName, key)) continue;
removeCommandFromMapIfCheckPasses(map, key, extraCheck);
}
}
private static <T> void removeCommandFromMapIfCheckPasses(Map<String, T> map, String key, Predicate<T> extraCheck) {
T element = map.get(key);
if (element == null) return;
if (extraCheck.test(map.get(key))) map.remove(key);
}
private static boolean isThisTheCommandButNamespaced(String commandName, String key) {
if(!key.contains(":")) return false;
String[] split = key.split(":");
if(split.length < 2) return false;
return split[1].equalsIgnoreCase(commandName);
}
@Override
@Unimplemented(because = REQUIRES_MINECRAFT_SERVER)
public abstract CommandDispatcher<Source> getBrigadierDispatcher();
@Override
@Unimplemented(because = {REQUIRES_MINECRAFT_SERVER, VERSION_SPECIFIC_IMPLEMENTATION})
public abstract void createDispatcherFile(File file, CommandDispatcher<Source> brigadierDispatcher) throws IOException;
@Unimplemented(because = REQUIRES_MINECRAFT_SERVER) // What are the odds?
public abstract <T> T getMinecraftServer();
@Override
public CommandAPILogger getLogger() {
return new DefaultLogger();
}
private static class DefaultLogger extends Logger implements CommandAPILogger {
protected DefaultLogger() {
super("CommandAPI", null);
setParent(Bukkit.getServer().getLogger());
setLevel(Level.ALL);
}
@Override
public void severe(String message, Throwable exception) {
super.log(Level.SEVERE, message, exception);
}
}
@Override
@Unimplemented(because = VERSION_SPECIFIC_IMPLEMENTATION)
public abstract void reloadDataPacks();
@Override
public void updateRequirements(AbstractPlayer<?> player) {
((Player) player.getSource()).updateCommands();
}
@Override
public Argument<String> newConcreteMultiLiteralArgument(String nodeName, String[] literals) {
return new MultiLiteralArgument(nodeName, literals);
}
@Override
public Argument<String> newConcreteLiteralArgument(String nodeName, String literal) {
return new LiteralArgument(nodeName, literal);
}
@Override
public CommandAPICommand newConcreteCommandAPICommand(CommandMetaData<CommandSender> meta) {
return new CommandAPICommand(meta);
}
/**
* Forces a command to return a success value of 0
*
* @param message Description of the error message, formatted as an array of base components
* @return a {@link WrapperCommandSyntaxException} that wraps Brigadier's
* {@link CommandSyntaxException}
*/
public static WrapperCommandSyntaxException failWithBaseComponents(BaseComponent... message) {
return CommandAPI.failWithMessage(BukkitTooltip.messageFromBaseComponents(message));
}
/**
* Forces a command to return a success value of 0
*
* @param message Description of the error message, formatted as an adventure chat component
* @return a {@link WrapperCommandSyntaxException} that wraps Brigadier's
* {@link CommandSyntaxException}
*/
public static WrapperCommandSyntaxException failWithAdventureComponent(Component message) {
return CommandAPI.failWithMessage(BukkitTooltip.messageFromAdventureComponent(message));
}
protected void registerBukkitRecipesSafely(Iterator<Recipe> recipes) {
Recipe recipe;
while (recipes.hasNext()) {
recipe = recipes.next();
try {
Bukkit.addRecipe(recipe);
if (recipe instanceof Keyed keyedRecipe) {
CommandAPI.logInfo("Re-registering recipe: " + keyedRecipe.getKey());
}
} catch (IllegalStateException e) { // From CraftingManager - "Duplicate recipe ignored with ID %id%"
assert true; // Can't re-register registered recipes. Not an error.
}
}
}
}