An advanced, modular command framework for PocketMine-MP plugins. LibCommand provides typed arguments, execution constraints, dynamic command building, subcommand nesting, async execution, scheduled commands, command macros, interactive wizards, and full client-side UI integration via LibPacket.
- Typed Arguments: Define arguments with automatic parsing and validation (string, int, float, boolean, enum, soft enum, player, target, item, world, position, range).
- Execution Constraints: Gate commands behind permissions, cooldowns, rate limits, game modes, worlds, or console/in-game requirements.
- Subcommand Nesting: Organize complex commands with arbitrarily nested subcommands, each with their own arguments and constraints.
- Dynamic Commands: Build commands at runtime using a fluent builder API, without creating new classes.
- Async Commands: Execute long-running operations (database queries, HTTP requests) on worker threads without blocking the main thread.
- Command Scheduling: Delay or repeat command execution using the PocketMine task scheduler.
- Command Macros: Let players create personal aliases that execute multiple commands in sequence.
- Interactive Wizards: Guide players through multi-step input flows via chat.
- Command History: Track command execution per sender for debugging and auditing.
- Help Generation: Automatically produce detailed, compact, or usage-only help text for any command.
- Permission Trees: Visualize command hierarchies and permission access for players.
- Batch Registration: Register or unregister groups of commands in a single call.
- Client UI Integration: Intercepts
AvailableCommandsPacketvia LibPacket to provide dynamic autocomplete and command suggestions in the Bedrock client.
Requirements:
- PocketMine-MP API 5.0.0+
- PHP 8.2+
- LibPacket (required dependency)
Installation:
- As a source library: place the
imperazim/commandnamespace in your project'ssrc/directory and ensure autoloading is configured. - As a PHAR plugin: download the
.pharrelease and place it inplugins/.
Registering the interceptor (required once per server, handled automatically if LibCommand is loaded as a plugin):
use imperazim\command\LibCommandHooker;
// In your plugin's onEnable():
LibCommandHooker::registerInterceptor($this);- Added
Commandabstract class -- the base class for all custom commands. Extends PocketMine'sCommandwith constraints, subcommands, typed arguments, help generation, custom messages, and automatic history recording. - Added
SubCommandabstract class -- represents a subcommand within a parent command or another subcommand. Supports nested subcommands, constraints, and argument parsing. - Added
AsyncCommandabstract class -- base class for commands that perform async operations on worker threads. - Added
LibCommandclass -- the plugin entry point that registers the packet interceptor on enable. - Added
LibCommandHookerclass -- static utility that registers theLibCommandInterceptorwith LibPacket. - Added
LibCommandInterceptorclass -- interceptsAvailableCommandsPacketto dynamically modify command UI based on player permissions and constraints. - Added
CommandArgumentsclass -- array-like container for parsed arguments, implementingArrayAccess,IteratorAggregate, andCountable. - Added
CommandGroupclass -- utility for batch-registering or unregistering groups of commands. - Added
CommandHistoryclass -- tracks the last N commands executed per sender for debugging and auditing. - Added
HelpGeneratorclass -- generates automatic help text in detailed, compact, or usage-only formats.
Defining a basic command:
use pocketmine\plugin\Plugin;
use imperazim\command\Command;
use imperazim\command\result\CommandResult;
use imperazim\command\result\CommandFailure;
class PingCommand extends Command {
public function onBuild(): array {
return [
'name' => 'ping',
'description' => 'Shows your current ping',
'aliases' => ['latency'],
'permission' => 'myplugin.ping',
];
}
public function onExecute(CommandResult $result): void {
$sender = $result->getSender();
$sender->sendMessage("Pong! Label used: " . $result->getLabel());
}
public function onFailure(CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
}
}Defining a command with subcommands:
use pocketmine\plugin\Plugin;
use imperazim\command\Command;
use imperazim\command\SubCommand;
use imperazim\command\result\CommandResult;
use imperazim\command\result\CommandFailure;
use imperazim\command\argument\PlayerArgument;
use imperazim\command\argument\StringArgument;
class KickSub extends SubCommand {
public function onBuild(): array {
return [
'name' => 'kick',
'description' => 'Kick a player',
'aliases' => ['k'],
'arguments' => [
new PlayerArgument('target'),
new StringArgument('reason', optional: true, default: 'No reason'),
],
];
}
public function onExecute(CommandResult $result): void {
$target = $result->getArgumentsList()->get('target');
$reason = $result->getArgumentsList()->get('reason') ?? 'No reason';
$result->getSender()->sendMessage("Kicked {$target->getName()}: $reason");
}
public function onFailure(CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
}
}
class AdminCommand extends Command {
public function onBuild(): array {
return [
'name' => 'admin',
'description' => 'Administrative commands',
'permission' => 'myplugin.admin',
'subcommands' => [
new KickSub($this),
],
];
}
public function onExecute(CommandResult $result): void {
$result->getSender()->sendMessage("Usage: /admin <kick>");
}
public function onFailure(CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
}
}Async command example:
use pocketmine\plugin\Plugin;
use pocketmine\command\CommandSender;
use imperazim\command\AsyncCommand;
use imperazim\command\result\CommandFailure;
use imperazim\command\argument\StringArgument;
class LookupCommand extends AsyncCommand {
public function onBuild(): array {
return [
'name' => 'lookup',
'description' => 'Look up player data asynchronously',
'arguments' => [
new StringArgument('player'),
],
];
}
protected function prepareAsyncData(array $args): mixed {
return $args; // pass serializable data to the worker
}
public function onAsyncComplete(CommandSender $sender, mixed $data, array $args, string $label): void {
$sender->sendMessage("Lookup complete for: " . ($data['player'] ?? 'unknown'));
}
public function onFailure(CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
}
}Batch registration with CommandGroup:
use imperazim\command\CommandGroup;
// Register multiple command instances
CommandGroup::register($plugin, [
new PingCommand($plugin),
new AdminCommand($plugin),
]);
// Or register from class names
CommandGroup::registerClasses($plugin, [
PingCommand::class,
AdminCommand::class,
]);
// Unregister
CommandGroup::unregister($plugin, $commands);Querying command history:
use imperazim\command\CommandHistory;
// Get last 10 commands for a player
$entries = CommandHistory::get("Steve", limit: 10);
// Get the most recent command
$last = CommandHistory::getLast("Steve");
// Returns: ['command' => '/tp', 'args' => ['Steve'], 'timestamp' => 1712700000.0]
// Clear history
CommandHistory::clear("Steve");
CommandHistory::clearAll();Generating help text:
use imperazim\command\HelpGenerator;
// Detailed help
$help = HelpGenerator::generate($command, "detailed");
// Compact help
$help = HelpGenerator::generate($command, "compact");
// Usage-only
$help = HelpGenerator::generate($command, "usage");
// Subcommand help
$help = HelpGenerator::generateSubCommand($subcommand, "admin", "detailed");- Added
Argumentabstract class -- base class for all argument types. Supports names, optional/required flags, default values, descriptions, aliases, custom validators, tab-completion suggestions, and network parameter data generation. - Added
StringArgumentclass -- accepts any string input. Consumes multiple tokens when followed by more arguments. - Added
IntegerArgumentclass -- parses integer values with optional min/max range constraints. - Added
FloatArgumentclass -- parses floating-point values with optional min/max range constraints. - Added
BooleanArgumentclass -- acceptstrue,false,yes,no,1,0and parses tobool. - Added
EnumArgumentclass -- restricts input to a fixed set of predefined string choices (hard enum). Provides client-side autocomplete. - Added
SoftEnumArgumentclass -- uses dynamically updateable enums viaCommandEnumManager. Values can change at runtime without re-registering commands. - Added
PlayerArgumentclass -- resolves online player names by prefix, returning aPlayerobject. - Added
TargetArgumentclass -- resolves entity/player targets including selector syntax (@a,@e,@p,@r,@s) with filter arguments. - Added
ItemArgumentclass -- parses item identifiers using PocketMine'sStringToItemParserand legacy parser. - Added
WorldArgumentclass -- resolves loaded world names, returning aWorldobject. - Added
PositionArgumentclass -- parses 3D coordinates (x y z) with support for relative notation (~,~5). - Added
RangeArgumentclass -- parses numeric ranges inmin..maxformat with optional absolute bounds.
Using typed arguments:
use imperazim\command\argument\StringArgument;
use imperazim\command\argument\IntegerArgument;
use imperazim\command\argument\FloatArgument;
use imperazim\command\argument\BooleanArgument;
use imperazim\command\argument\PlayerArgument;
use imperazim\command\argument\ItemArgument;
use imperazim\command\argument\WorldArgument;
use imperazim\command\argument\TargetArgument;
use imperazim\command\argument\PositionArgument;
use imperazim\command\argument\RangeArgument;
// Required string
new StringArgument('name');
// Optional integer with min/max and default
new IntegerArgument('amount', optional: true, default: 1, min: 1, max: 64);
// Float with range
new FloatArgument('speed', optional: false, min: 0.1, max: 10.0);
// Boolean with description
new BooleanArgument('confirm', optional: true, description: 'Confirm the action');
// Player (resolves to Player object)
new PlayerArgument('target');
// Item (resolves to Item object)
new ItemArgument('item');
// World (resolves to World object)
new WorldArgument('world', optional: true);
// Target with selector support (@a, @e, @p, @r, @s)
new TargetArgument('entity');
// Position with relative coordinates (100 64 200 or ~ ~1 ~)
new PositionArgument('location');
// Range (e.g., "1..10" or "5")
new RangeArgument('level', optional: false, absoluteMin: 1, absoluteMax: 100);Using enum arguments:
use imperazim\command\argument\EnumArgument;
use imperazim\command\argument\SoftEnumArgument;
use imperazim\command\enum\CommandEnumManager;
use pocketmine\network\mcpe\protocol\types\command\CommandSoftEnum;
// Hard enum (fixed choices)
new EnumArgument('color', optional: false, choices: ['red', 'blue', 'green']);
// Soft enum (dynamic, updateable at runtime)
CommandEnumManager::addEnum(new CommandSoftEnum("warps", ["spawn", "pvp", "shop"]));
new SoftEnumArgument('warp', optional: false, enumName: 'warps');
// Update values later -- all connected clients are notified
CommandEnumManager::updateEnum("warps", ["spawn", "pvp", "shop", "arena"]);
// Remove an enum
CommandEnumManager::removeEnum("warps");Custom argument with validator:
use imperazim\command\argument\StringArgument;
// Custom validator: only accept lowercase alphanumeric names
new StringArgument('username', optional: false, validator: function(mixed $value): bool {
return (bool) preg_match('/^[a-z0-9_]{3,16}$/', (string) $value);
});Creating a custom argument type:
use imperazim\command\argument\Argument;
use pocketmine\command\CommandSender;
use pocketmine\network\mcpe\protocol\AvailableCommandsPacket;
class UppercaseStringArgument extends Argument {
public function getTypeName(): string {
return 'upperstring';
}
public function getNetworkType(): int {
return AvailableCommandsPacket::ARG_TYPE_STRING;
}
public function canParse(string $testString, CommandSender $sender): bool {
return strtoupper($testString) === $testString;
}
public function parse(string $argument, CommandSender $sender): mixed {
return strtoupper($argument);
}
}- Added
Constraintabstract class -- base class for all execution constraints. Defines the contract withisSatisfiedBy(),onFailure(),onSuccess(), andgetDescription(). - Added
PermissionConstraintclass -- enforces that the sender holds a specific permission node. Supports custom failure messages. - Added
CooldownConstraintclass -- enforces a cooldown period (in seconds) between command uses per sender. Tracks usage via UUID. - Added
RateLimiterConstraintclass -- caps total command usage within a sliding time window (e.g., max 3 uses per 60 seconds). UnlikeCooldownConstraint, allows bursts. - Added
InGameConstraintclass -- requires the sender to be an in-game player (not console). - Added
RequireConsoleConstraintclass -- requires the sender to be the server console (not a player). - Added
WorldConstraintclass -- restricts command usage to one or more specific worlds. - Added
GameModeConstraintclass -- restricts command usage to a specific game mode (Survival, Creative, Adventure, or Spectator).
Applying constraints in onBuild:
use imperazim\command\Command;
use imperazim\command\result\CommandResult;
use imperazim\command\result\CommandFailure;
use imperazim\command\constraint\InGameConstraint;
use imperazim\command\constraint\CooldownConstraint;
use imperazim\command\constraint\WorldConstraint;
use imperazim\command\constraint\GameModeConstraint;
use imperazim\command\constraint\RateLimiterConstraint;
use pocketmine\player\GameMode;
class ArenaCommand extends Command {
public function onBuild(): array {
return [
'name' => 'arena',
'description' => 'Join the arena',
'permission' => 'myplugin.arena',
'cooldown' => 10.0, // shorthand: 10-second cooldown
'constraints' => [
new InGameConstraint(),
new WorldConstraint(['lobby', 'arena']),
new GameModeConstraint(GameMode::SURVIVAL),
new RateLimiterConstraint(maxUses: 5, windowSeconds: 60.0),
],
];
}
public function onExecute(CommandResult $result): void {
$result->getSender()->sendMessage("Joining the arena...");
}
public function onFailure(CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
}
}Console-only command:
use imperazim\command\constraint\RequireConsoleConstraint;
// In onBuild():
'constraints' => [
new RequireConsoleConstraint(),
],Custom constraint:
use imperazim\command\constraint\Constraint;
use pocketmine\command\CommandSender;
use pocketmine\player\Player;
class VIPConstraint extends Constraint {
public function isSatisfiedBy(CommandSender $sender): bool {
return $sender instanceof Player && $sender->hasPermission('group.vip');
}
public function onFailure(CommandSender $sender): void {
$sender->sendMessage('This command is VIP-only!');
}
public function getDescription(): string {
return 'Requires VIP rank';
}
}- Added
CommandResultclass -- represents a successful command execution. Provides access to the sender, parsedCommandArguments, and the command label used. - Added
CommandFailureclass -- represents a command execution failure with typed reason constants (INVALID_ARGUMENT,MISSING_ARGUMENT,CONSTRAINT_FAILED,EXECUTION_ERROR,COOLDOWN), contextual data, and formatted messages.
Working with CommandResult:
use imperazim\command\result\CommandResult;
public function onExecute(CommandResult $result): void {
$sender = $result->getSender();
$args = $result->getArgumentsList();
$label = $result->getLabel();
// Access arguments by name
$target = $args->get('target');
$amount = $args->get('amount', 1); // with default
// Iterate over all arguments
foreach ($args as $name => $value) {
$sender->sendMessage("$name = $value");
}
// Array-like access
$count = count($args);
$exists = isset($args['target']);
}Handling CommandFailure:
use imperazim\command\result\CommandFailure;
public function onFailure(CommandFailure $failure): void {
$sender = $failure->getSender();
$reason = $failure->getReason();
switch ($reason) {
case CommandFailure::MISSING_ARGUMENT:
$sender->sendMessage("Missing arguments: " . $failure->getMessage());
break;
case CommandFailure::INVALID_ARGUMENT:
$sender->sendMessage("Invalid input: " . $failure->getMessage());
break;
case CommandFailure::CONSTRAINT_FAILED:
// Constraints already notify the sender via onFailure()
break;
case CommandFailure::EXECUTION_ERROR:
$data = $failure->getData();
$sender->sendMessage("Error: " . ($data['message'] ?? 'Unknown'));
break;
case CommandFailure::COOLDOWN:
$sender->sendMessage("Please wait before using this again.");
break;
}
}- Added
DynamicCommandclass -- a fluent builder for creating commands at runtime without defining new classes. Supports all features ofCommandvia method chaining and closure callbacks. - Added
DynamicSubCommandclass -- a fluent builder for creating subcommands at runtime. Supports closure-based execution and failure handlers.
Building a command dynamically:
use imperazim\command\dynamic\DynamicCommand;
use imperazim\command\dynamic\DynamicSubCommand;
use imperazim\command\argument\StringArgument;
use imperazim\command\argument\IntegerArgument;
use imperazim\command\constraint\InGameConstraint;
use imperazim\command\result\CommandResult;
use imperazim\command\result\CommandFailure;
$cmd = DynamicCommand::create($plugin, 'give')
->withDescription('Give items to a player')
->withAliases(['g'])
->withPermission('myplugin.give')
->addConstraint(new InGameConstraint())
->addArgument(new StringArgument('item'))
->addArgument(new IntegerArgument('amount', optional: true, default: 1, min: 1, max: 64))
->setOnExecute(function (CommandResult $result): void {
$sender = $result->getSender();
$item = $result->getArgumentsList()->get('item');
$amount = $result->getArgumentsList()->get('amount') ?? 1;
$sender->sendMessage("Giving {$amount}x {$item}");
})
->setOnFailure(function (CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
});
$cmd->registerCommand();Building dynamic subcommands:
use imperazim\command\dynamic\DynamicCommand;
use imperazim\command\dynamic\DynamicSubCommand;
use imperazim\command\argument\StringArgument;
use imperazim\command\result\CommandResult;
use imperazim\command\result\CommandFailure;
$cmd = DynamicCommand::create($plugin, 'warp')
->withDescription('Manage warps');
$setSub = DynamicSubCommand::create($cmd, 'set')
->setDescription('Set a warp at your location')
->addArgument(new StringArgument('name'))
->setOnExecute(function (CommandResult $result): void {
$name = $result->getArgumentsList()->get('name');
$result->getSender()->sendMessage("Warp '$name' set!");
})
->setOnFailure(function (CommandFailure $failure): void {
$failure->getSender()->sendMessage($failure->getMessage());
});
$cmd->addSubCommand($setSub);
$cmd->registerCommand();- Added
CommandSchedulerclass -- schedules command execution in the future using PocketMine's task scheduler. Supports delayed one-shot execution, repeating execution with optional max runs, task cancellation by ID, and listing active tasks.
use imperazim\command\scheduler\CommandScheduler;
// Initialize once (typically in onEnable)
CommandScheduler::init($plugin);
// Run a command after 5 seconds (100 ticks)
CommandScheduler::delay($sender, "broadcast The game starts!", 100);
// Run a command every second (20 ticks), with an ID for cancellation
CommandScheduler::repeat($sender, "say tick!", 20, "heartbeat");
// Run a command 5 times, once per second
CommandScheduler::repeat($sender, "say countdown!", 20, "countdown", maxRuns: 5);
// Cancel a specific task
CommandScheduler::cancel("heartbeat");
// Check if a task is active
$active = CommandScheduler::isActive("heartbeat");
// Get all active task IDs
$tasks = CommandScheduler::getActiveTasks();
// Cancel all scheduled tasks
CommandScheduler::cancelAll();- Added
CommandMacroclass -- allows players to create personal command aliases that execute multiple commands in sequence. Supports creation with limits (max 20 macros, max 10 commands each), execution, removal, listing, existence checks, and automatic cleanup on disconnect.
use imperazim\command\macro\CommandMacro;
// Create a macro that runs three commands in sequence
CommandMacro::create($player, "attack", ["/kit pvp", "/heal", "/tp arena"]);
// Execute the macro
CommandMacro::execute($player, "attack");
// Check if a macro exists
if (CommandMacro::exists($player, "attack")) {
$commands = CommandMacro::get($player, "attack");
}
// List all macros for a player
$macros = CommandMacro::getList($player);
// Remove a macro
CommandMacro::remove($player, "attack");
// Clean up on player quit (call in PlayerQuitEvent handler)
CommandMacro::cleanup($player);- Added
CommandWizardclass -- an interactive step-by-step wizard that collects input from players via chat. Each step asks a question and processes the answer through a closure handler. Supports data storage between steps, completion and cancellation callbacks, and automatic cleanup. Only one wizard can be active per player at a time. Players typecancelto abort.
use imperazim\command\wizard\CommandWizard;
use pocketmine\player\Player;
$wizard = new CommandWizard($plugin, $player, "Create Warp");
$wizard->step("What is the warp name?", function (Player $p, string $answer, CommandWizard $w): bool {
if (strlen($answer) < 3) {
$p->sendMessage("Name must be at least 3 characters.");
return false; // repeat this step
}
$w->setData("name", $answer);
return true; // proceed to next step
});
$wizard->step("Should it be public? (yes/no)", function (Player $p, string $answer, CommandWizard $w): bool {
if (!in_array(strtolower($answer), ['yes', 'no'])) {
$p->sendMessage("Please answer yes or no.");
return false;
}
$w->setData("public", strtolower($answer) === 'yes');
return true;
});
$wizard->onComplete(function (Player $p, array $data): void {
$p->sendMessage("Warp '{$data['name']}' created! Public: " . ($data['public'] ? 'yes' : 'no'));
});
$wizard->onCancel(function (Player $p): void {
$p->sendMessage("Warp creation cancelled.");
});
$wizard->start();
// Check if player has an active wizard
$hasWizard = CommandWizard::hasActiveWizard($player);
// Clean up on quit (call in PlayerQuitEvent handler)
CommandWizard::cleanup($player);- Added
CommandPermissionTreeclass -- generates a visual, color-coded permission tree showing command hierarchies, arguments, subcommands, and constraint requirements. Colors indicate access (green) or denial (red) based on the viewer's permissions.
use imperazim\command\permission\CommandPermissionTree;
// Generate a tree for all commands (no viewer context)
$tree = CommandPermissionTree::generate($commands);
// Generate with viewer permissions (color-coded)
$tree = CommandPermissionTree::generate($commands, $player);
// Send tree directly to a player
CommandPermissionTree::sendTo($player, $commands);
// Generate tree for a single command with full details
$tree = CommandPermissionTree::forCommand($command, $player);- Added
CommandEnumManagerclass -- manages soft command enums (dynamic suggestion lists). Supports adding, updating, and removing enums at runtime with automatic broadcast to all connected players viaUpdateSoftEnumPacket.
use imperazim\command\enum\CommandEnumManager;
use pocketmine\network\mcpe\protocol\types\command\CommandSoftEnum;
// Register a new soft enum
CommandEnumManager::addEnum(new CommandSoftEnum("kits", ["warrior", "archer", "mage"]));
// Update values (broadcasts to all players)
CommandEnumManager::updateEnum("kits", ["warrior", "archer", "mage", "healer"]);
// Retrieve an enum
$enum = CommandEnumManager::getEnumByName("kits");
$values = $enum?->getValues();
// Get all registered enums
$allEnums = CommandEnumManager::getEnums();
// Remove an enum
CommandEnumManager::removeEnum("kits");- Added
ArgumentExceptionclass -- aRuntimeExceptionthrown for argument-related errors during parsing, validation, or constraint violations.
use imperazim\command\exception\ArgumentException;
// Thrown automatically by argument types on parse failure.
// Can also be thrown in custom argument implementations:
throw new ArgumentException("Value must be a positive number");- Added
SubCommandTrait-- provides subcommand registration, retrieval by name, and listing. Used internally byCommand. - Added
ArgumentableTrait-- provides argument registration, validation, raw argument parsing (with special handling forPositionArgumentandStringArgumentmulti-token consumption), and usage string generation. Used internally byCommandandSubCommand. - Added
ConstraintableTrait-- provides constraint registration, retrieval (including by type), and batch testing against aCommandSender. CallsonSuccess()on all constraints when all pass, oronFailure()on failed constraints. Used internally byCommandandSubCommand.
This project is licensed under MIT. Please see the LICENSE file for details.