This document explains how JavaSkript automatically registers commands for scripts without needing plugin.yml entries.
JavaSkript uses dynamic command registration to automatically register commands from scripts at runtime. This means:
- No
plugin.ymlentries needed - Commands are registered when scripts load
- Commands are unregistered when scripts unload
- Tab completion works automatically
- Works on Paper and Folia 1.21.1+ (including 1.21.11)
When a script class implements CommandExecutor, JavaSkript automatically detects it:
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
public class FlyCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
// Command logic here
return true;
}
}The command name is automatically derived from the class name:
| Class Name | Command Name | How It Works |
|---|---|---|
FlyCommand |
/fly |
Removes "Command" suffix, converts to lowercase |
HealCommand |
/heal |
Removes "Command" suffix, converts to lowercase |
TeleportCmd |
/teleport |
Removes "Cmd" suffix, converts to lowercase |
Warp |
/warp |
Uses class name as-is, converts to lowercase |
Code in ScriptInstance.java:
String className = scriptClass.getSimpleName();
String commandName = className.toLowerCase()
.replace("command", "")
.replace("cmd", "");
if (commandName.isEmpty()) {
commandName = className.toLowerCase();
}JavaSkript uses the DynamicCommandRegistry to register commands:
Step-by-step process:
-
Create PluginCommand: Uses reflection to create a
PluginCommandinstanceConstructor<PluginCommand> constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); constructor.setAccessible(true); PluginCommand command = constructor.newInstance(commandName, plugin);
-
Set Executor: Assigns the script as the command executor
command.setExecutor(executor);
-
Set Tab Completer: If the script implements
TabCompleter, it's automatically setif (executor instanceof TabCompleter) { command.setTabCompleter((TabCompleter) executor); }
-
Register with CommandMap: Adds the command to Bukkit's command map
CommandMap commandMap = getCommandMap(); commandMap.register(plugin.getName().toLowerCase(), command);
-
Sync to Clients: Updates tab completion for all online players
Bukkit.getOnlinePlayers().forEach(player -> player.updateCommands());
import dev.mukulx.javaskript.script.FoliaSupport;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import net.kyori.adventure.text.Component;
import java.util.List;
@FoliaSupport
public class FlyCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Only players can use this command"));
return true;
}
boolean fly = !player.getAllowFlight();
player.setAllowFlight(fly);
player.sendMessage(Component.text("Flight: " + (fly ? "ON" : "OFF")));
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
return List.of();
}
}Result: /fly command is automatically registered!
import dev.mukulx.javaskript.script.FoliaSupport;
import org.bukkit.Bukkit;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import net.kyori.adventure.text.Component;
import java.util.List;
import java.util.stream.Collectors;
@FoliaSupport
public class HealCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
Player target;
if (args.length == 0) {
if (!(sender instanceof Player)) {
sender.sendMessage(Component.text("Usage: /heal <player>"));
return true;
}
target = (Player) sender;
} else {
target = Bukkit.getPlayer(args[0]);
if (target == null) {
sender.sendMessage(Component.text("Player not found: " + args[0]));
return true;
}
}
target.setHealth(target.getMaxHealth());
target.setFoodLevel(20);
target.sendMessage(Component.text("You have been healed!"));
if (!sender.equals(target)) {
sender.sendMessage(Component.text("Healed " + target.getName()));
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
if (args.length == 1) {
return Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(name -> name.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
}
return List.of();
}
}Result: /heal and /heal <player> with tab completion!
- Script is compiled
- Class is loaded
- Instance is created
- JavaSkript checks if it implements
CommandExecutor - If yes, command name is extracted from class name
- Command is registered via
DynamicCommandRegistry - Tab completion is synced to all players
- Script unload is triggered (via
/js unload,/js reload, or server shutdown) - Command is unregistered from CommandMap
- All aliases are removed
- Tab completion is synced to all players
- Script instance is destroyed
JavaSkript uses Paper's modern plugin system with paper-plugin.yml instead of the legacy plugin.yml.
Traditional Bukkit/Spigot plugins require commands to be declared in plugin.yml:
commands:
fly:
description: Toggle flight
usage: /flyProblems with this approach:
- Static - can't add commands at runtime
- Requires plugin reload to add new commands
- Scripts would need their own plugin.yml entries
- Not flexible for dynamic scripting
JavaSkript's solution:
- Uses Paper's modern
paper-plugin.ymlformat - Uses reflection to create
PluginCommandinstances - Registers directly with Bukkit's
CommandMap - Fully dynamic - commands appear/disappear with scripts
- No command declarations needed in YAML
JavaSkript uses reflection to access internal Bukkit APIs:
// Create PluginCommand (normally only Bukkit can do this)
Constructor<PluginCommand> constructor =
PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class);
constructor.setAccessible(true);
PluginCommand command = constructor.newInstance(commandName, plugin);
// Get CommandMap (normally private)
Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap");
commandMapField.setAccessible(true);
CommandMap commandMap = (CommandMap) commandMapField.get(Bukkit.getServer());
// Register command
commandMap.register(pluginName, command);Bukkit's CommandMap stores all server commands:
CommandMap (SimpleCommandMap)
├── knownCommands (Map<String, Command>)
│ ├── "fly" → FlyCommand
│ ├── "heal" → HealCommand
│ ├── "javaskript:fly" → FlyCommand (with plugin prefix)
│ └── ...
When a player types /fly, Bukkit:
- Looks up "fly" in
knownCommands - Finds the registered
Commandobject - Calls its
execute()method - Which calls your script's
onCommand()method
Commands don't automatically have permissions. You can check permissions in your script:
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!sender.hasPermission("myscript.fly")) {
sender.sendMessage(Component.text("No permission!"));
return true;
}
// Command logic...
return true;
}To register permissions, use the DynamicPermissionRegistry (see API.md).
Currently, JavaSkript doesn't support command aliases from class names. If you need aliases, you can:
-
Check the label in onCommand:
@Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { // label will be "fly" or whatever alias was used if (label.equalsIgnoreCase("f")) { // Handle alias } return true; }
-
Register multiple commands (create multiple script classes)
Check:
- Does your class implement
CommandExecutor? - Is the class name valid? (e.g.,
FlyCommand, notFly Command) - Check console for errors during script load
- Try
/js reloadto reload the script
If another plugin has the same command:
- JavaSkript will try to override it
- Use the plugin prefix:
/javaskript:fly - Or rename your script class
Check:
- Does your class implement
TabCompleter? - Is
onTabComplete()returning a valid list? - Try reconnecting to the server
command /fly:
permission: skript.fly
trigger:
toggle flight of player
public class FlyCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
// Full Java power!
}
}Advantages:
- Full Java language features
- Type safety
- IDE autocomplete
- Compile-time error checking
- Access to all Bukkit APIs
In addition to traditional implements CommandExecutor, JavaSkript provides an expressive, fluent Command API injected via private CommandHelper commands;.
- Subcommands: Recursive subcommands with dedicated permissions and execution guards.
- Typed Arguments: Automatic parsing and validation for
player,integer,doubleNum,bool,choice,greedyString, andcustom. - Automatic Tab-Completion: Context-aware completions without boilerplate streams.
- Guarded Callbacks:
.executesPlayer((player, ctx) -> ...)and.executesConsole(...). - Automatic Cleanup: All fluent commands unregister cleanly when the script unloads.
@FoliaSupport
public class WarpScript {
private CommandHelper commands;
private final Map<String, Location> warps = new HashMap<>();
public void onEnable() {
commands.create("warp")
.description("Warp system")
.permission("server.warp")
.aliases("warppoint")
// Subcommand: /warp set <name>
.subcommand("set", sub -> sub
.permission("server.warp.admin")
.argument(CommandArgs.string("name"))
.executesPlayer((player, ctx) -> {
String name = ctx.getString("name");
warps.put(name.toLowerCase(), player.getLocation());
ctx.replySuccess("Warp '" + name + "' set!");
})
)
// Subcommand: /warp delete <name>
.subcommand("delete", sub -> sub
.permission("server.warp.admin")
.argument(CommandArgs.choice("name", () -> warps.keySet()))
.executes((sender, ctx) -> {
String name = ctx.getString("name");
if (warps.remove(name.toLowerCase()) != null) {
ctx.replySuccess("Warp deleted.");
} else {
ctx.replyError("Warp not found.");
}
})
)
// Root command: /warp <name> -> teleport
.argument(CommandArgs.choice("name", () -> warps.keySet()))
.executesPlayer((player, ctx) -> {
String name = ctx.getString("name").toLowerCase();
Location loc = warps.get(name);
if (loc != null) {
player.teleportAsync(loc);
ctx.replySuccess("Teleported!");
} else {
ctx.replyError("Unknown warp: " + name);
}
})
.register();
}
}- Name your classes clearly:
FlyCommand, notFlyorFlyScript - Implement TabCompleter: Provides better UX
- Check permissions: Don't rely on external permission plugins
- Validate arguments: Check
args.lengthbefore accessing - Return true: Always return
truefromonCommand()to prevent usage message - Use Components: Use Adventure API for colored messages
- API.md - Full API documentation
- QUICKSTART.md - Getting started guide
- EXAMPLES.md - More command examples
- FlyCommand.java - Example script
- HealCommand.java - Example script
Last Updated: 2026-05-30