Advanced Usage - LilBroCodes/commander GitHub Wiki

Advanced Usage

This page covers advanced techniques and patterns for using Commander in more complex scenarios. Once you're comfortable with the basics, these approaches can help you build more sophisticated command systems.

Custom Command Registration

While the standard CommanderCommand.register() method works for most cases, you can extend CommanderCommand to customize the initialization process.

public class MyCustomCommand extends CommanderCommand {
    private final MyPlugin plugin;
    
    public MyCustomCommand(ExecutorNode<CommandGroupNode> root, boolean tabComplete, MyPlugin plugin) {
        super(root, tabComplete);
        this.plugin = plugin;
    }
    
    @Override
    public void initialize(ExecutorNode<CommandGroupNode> root) {
        // Add standard help command
        CommandActionNode helpNode = createHelpCommand();
        ((CommandGroupNode) root).addChild(helpNode);
        
        // Add other standard commands
        // ...
    }
    
    private CommandActionNode createHelpCommand() {
        // Custom help command implementation
        // ...
    }
}

Dynamic Command Trees

You can build command trees dynamically based on configuration, permissions, or other factors.

public CommandGroupNode buildCommandTree() {
    CommandGroupNode rootNode = new CommandGroupNode(
        "myplugin",
        "MyPlugin commands",
        "MyPlugin"
    );
    
    // Add standard commands
    rootNode.addChild(createVersionCommand());
    
    // Add commands based on configuration
    if (config.isFeatureEnabled("economy")) {
        rootNode.addChild(createEconomyCommands());
    }
    
    // Add admin commands if server has permission system
    if (Bukkit.getPluginManager().isPluginEnabled("Vault")) {
        rootNode.addChild(createAdminCommands());
    }
    
    return rootNode;
}

Command Aliases

Commander doesn't directly support command aliases within its tree structure, but you can implement them by registering multiple commands that share the same executor node.

// Create the command tree
CommandGroupNode rootNode = new CommandGroupNode(
    "myplugin",
    "MyPlugin commands",
    "MyPlugin"
);

// Add subcommands
// ...

// Register the main command
CommanderCommand mainCommand = new CommanderCommand(rootNode, true);
mainCommand.register(this, "myplugin");

// Register aliases
CommanderCommand aliasCommand1 = new CommanderCommand(rootNode, true);
aliasCommand1.register(this, "mp");

CommanderCommand aliasCommand2 = new CommanderCommand(rootNode, true);
aliasCommand2.register(this, "mplugin");

Subcommand Aliases

To create aliases for subcommands, you can add multiple command nodes that execute the same logic.

// Create the main subcommand
CommandActionNode teleportCommand = new CommandActionNode(
    "teleport",
    "Teleport to a location",
    "MyPlugin",
    parameters,
    executor
);

// Create an alias with the same executor
CommandActionNode tpCommand = new CommandActionNode(
    "tp",
    "Teleport to a location (alias)",
    "MyPlugin",
    parameters,
    executor
);

// Add both to the parent
rootNode.addChild(teleportCommand);
rootNode.addChild(tpCommand);

Shared Parameter Definitions

For commands that share similar parameters, you can define them once and reuse them.

// Define common parameters
public static final TypedParameter PLAYER_PARAM = new TypedParameter(
    "player",
    ParameterType.STRING,
    () -> Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList())
);

public static final TypedParameter AMOUNT_PARAM = new TypedParameter(
    "amount",
    ParameterType.INT,
    () -> List.of("1", "5", "10", "50", "100")
);

// Use them in multiple commands
CommandActionNode giveMoneyCommand = new CommandActionNode(
    "give",
    "Give money to a player",
    "MyPlugin",
    List.of(PLAYER_PARAM, AMOUNT_PARAM),
    (sender, args) -> {
        // Command logic
    }
);

CommandActionNode takeMoneyCommand = new CommandActionNode(
    "take",
    "Take money from a player",
    "MyPlugin",
    List.of(PLAYER_PARAM, AMOUNT_PARAM),
    (sender, args) -> {
        // Command logic
    }
);

Command Factories

For plugins with many similar commands, you can create factory methods to reduce repetition.

public CommandActionNode createToggleCommand(String name, String description, String permission, Consumer<Player> action) {
    return new CommandActionNode(
        name,
        description,
        "MyPlugin",
        List.of(),
        (sender, args) -> {
            if (!(sender instanceof Player player)) {
                sender.sendMessage("This command can only be used by players");
                return;
            }
            
            action.accept(player);
        }
    ).withPermission(permission);
}

// Usage
CommandGroupNode togglesNode = new CommandGroupNode("toggle", "Toggle features", "MyPlugin");

toggleNode.addChild(createToggleCommand(
    "fly",
    "Toggle flight mode",
    "myplugin.toggle.fly",
    player -> {
        player.setAllowFlight(!player.getAllowFlight());
        player.sendMessage("Flight mode: " + (player.getAllowFlight() ? "enabled" : "disabled"));
    }
));

toggleNode.addChild(createToggleCommand(
    "god",
    "Toggle god mode",
    "myplugin.toggle.god",
    player -> {
        boolean godMode = !player.isInvulnerable();
        player.setInvulnerable(godMode);
        player.sendMessage("God mode: " + (godMode ? "enabled" : "disabled"));
    }
));

Custom Parameter Types

While Commander doesn't directly support custom parameter types, you can implement them by using the existing types and performing additional validation in your executor.

// Define a UUID parameter
TypedParameter uuidParam = new TypedParameter(
    "uuid",
    ParameterType.STRING,
    () -> {
        // Provide some example UUIDs for tab completion
        return Bukkit.getOnlinePlayers().stream()
            .map(Player::getUniqueId)
            .map(UUID::toString)
            .collect(Collectors.toList());
    }
);

// Create a command that uses the UUID parameter
CommandActionNode lookupCommand = new CommandActionNode(
    "lookup",
    "Look up a player by UUID",
    "MyPlugin",
    List.of(uuidParam),
    (sender, args) -> {
        String uuidString = (String) args.get(0);
        
        // Validate the UUID format
        try {
            UUID uuid = UUID.fromString(uuidString);
            
            // Look up the player
            OfflinePlayer player = Bukkit.getOfflinePlayer(uuid);
            sender.sendMessage("Player: " + player.getName());
        } catch (IllegalArgumentException e) {
            sender.sendMessage("Invalid UUID format");
        }
    }
);

Integration with Other Plugins

Commander can be integrated with other plugins to extend its functionality.

Vault Integration for Permissions

public boolean hasPermission(CommandSender sender, String permission) {
    if (vaultPermissions != null) {
        if (sender instanceof Player player) {
            return vaultPermissions.has(player, permission);
        }
    }
    
    // Fall back to Bukkit permissions
    return sender.hasPermission(permission);
}

// Create a command with custom permission checking
CommandActionNode adminCommand = new CommandActionNode(
    "admin",
    "Admin commands",
    "MyPlugin",
    List.of(),
    (sender, args) -> {
        // Command logic
    }
) {
    @Override
    public boolean hasPermission(CommandSender sender) {
        return MyPlugin.getInstance().hasPermission(sender, "myplugin.admin");
    }
};

PlaceholderAPI Integration

public String parsePlaceholders(Player player, String message) {
    if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
        return PlaceholderAPI.setPlaceholders(player, message);
    }
    return message;
}

// Create a command that uses placeholders
CommandActionNode infoCommand = new CommandActionNode(
    "info",
    "Show player info",
    "MyPlugin",
    List.of(),
    (sender, args) -> {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("This command can only be used by players");
            return;
        }
        
        String message = "Your health: %player_health%, Your level: %player_level%";
        player.sendMessage(parsePlaceholders(player, message));
    }
);

Performance Considerations

Lazy Initialization

For large command trees, you can use lazy initialization to improve startup performance.

public class MyPlugin extends JavaPlugin {
    private CommandGroupNode rootNode;
    
    @Override
    public void onEnable() {
        // Initialize only the root node
        rootNode = new CommandGroupNode(
            "myplugin",
            "MyPlugin commands",
            "MyPlugin"
        );
        
        // Register the command
        CommanderCommand command = new LazyCommanderCommand(rootNode, true, this);
        command.register(this, "myplugin");
    }
    
    // Lazy initialization of command tree
    private class LazyCommanderCommand extends CommanderCommand {
        private final MyPlugin plugin;
        private boolean initialized = false;
        
        public LazyCommanderCommand(ExecutorNode<CommandGroupNode> root, boolean tabComplete, MyPlugin plugin) {
            super(root, tabComplete);
            this.plugin = plugin;
        }
        
        @Override
        public void initialize(ExecutorNode<CommandGroupNode> root) {
            if (initialized) return;
            
            // Initialize the full command tree
            CommandGroupNode groupNode = (CommandGroupNode) root;
            groupNode.addChild(createVersionCommand());
            groupNode.addChild(createAdminCommands());
            // Add more commands...
            
            initialized = true;
        }
    }
}

Caching Tab Completion Results

For expensive tab completion operations, you can cache the results.

public class TabCompletionCache {
    private final Map<String, List<String>> cache = new HashMap<>();
    private final Map<String, Long> lastUpdated = new HashMap<>();
    private final long cacheTimeMs;
    
    public TabCompletionCache(long cacheTimeMs) {
        this.cacheTimeMs = cacheTimeMs;
    }
    
    public List<String> getOrCompute(String key, Supplier<List<String>> supplier) {
        long now = System.currentTimeMillis();
        Long lastUpdate = lastUpdated.get(key);
        
        if (lastUpdate == null || now - lastUpdate > cacheTimeMs) {
            List<String> result = supplier.get();
            cache.put(key, result);
            lastUpdated.put(key, now);
            return result;
        }
        
        return cache.get(key);
    }
    
    public void invalidate(String key) {
        cache.remove(key);
        lastUpdated.remove(key);
    }
    
    public void invalidateAll() {
        cache.clear();
        lastUpdated.clear();
    }
}

// Usage
TabCompletionCache cache = new TabCompletionCache(30000); // 30 seconds

TypedParameter worldParam = new TypedParameter(
    "world",
    ParameterType.STRING,
    () -> cache.getOrCompute("worlds", () -> {
        // Expensive operation to get all world names
        return Bukkit.getWorlds().stream()
            .map(World::getName)
            .collect(Collectors.toList());
    })
);

Conclusion

Commander provides a solid foundation for building command systems in your Bukkit/Spigot/Paper plugins. By leveraging these advanced techniques, you can create more sophisticated, maintainable, and user-friendly command interfaces for your players.

Remember that good command design is about more than just the technical implementation—it's about creating an intuitive and consistent experience for your users. Take the time to plan your command structure, use clear naming conventions, and provide helpful descriptions and tab completion suggestions.

⚠️ **GitHub.com Fallback** ⚠️