Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
579aecf
First step in restructuring command alias handling
dktapps May 4, 2025
084774e
Don't allow registering the same command instance twice
dktapps May 4, 2025
7eff658
Add ability to register & unregister specific command aliases for exi…
dktapps May 4, 2025
91dad0a
Merge branch 'major-next' into command-alias-handling
dktapps May 4, 2025
8035d66
Merge branch 'major-next' into command-alias-handling
dktapps May 4, 2025
bc54380
CS
dktapps May 4, 2025
de15737
Merge branch 'major-next' into command-alias-handling
dktapps May 24, 2025
9a7d94c
Merge branch 'major-next' into command-alias-handling
dktapps Aug 3, 2025
743e6c8
Merge branch 'major-next' into command-alias-handling
dktapps Oct 4, 2025
9e93c40
Don't register prefixed non-primary aliases
dktapps Oct 4, 2025
02712d0
Revert changes to command name
dktapps Oct 5, 2025
b4c5576
Make alias conflicts more sane
dktapps Oct 8, 2025
7eeb436
Fix PHPStan
dktapps Oct 8, 2025
a8b6a42
Fixed PluginCommand usage messages
dktapps Oct 8, 2025
bdb962b
First look at user-local alias maps and /cmdalias
dktapps Oct 8, 2025
5510e62
Suppress error
dktapps Oct 8, 2025
5ac6f01
Merge branch 'major-next' into command-alias-handling
dktapps Oct 8, 2025
1ba86fc
Put namespace inside Command
dktapps Oct 8, 2025
846bd99
Tidy namespace & name handling & validation
dktapps Oct 8, 2025
84af5df
Merge branch 'major-next' into command-alias-handling
dktapps Oct 10, 2025
5bbd179
UX improvements & localisations
dktapps Oct 10, 2025
c41f316
stfu
dktapps Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
namespace pocketmine;

use pocketmine\command\Command;
use pocketmine\command\CommandMapEntry;
use pocketmine\command\CommandSender;
use pocketmine\command\SimpleCommandMap;
use pocketmine\console\ConsoleCommandSender;
Expand Down Expand Up @@ -680,11 +681,8 @@ public function getConfigGroup() : ServerConfigGroup{
* @phpstan-return (Command&PluginOwned)|null
*/
public function getPluginCommand(string $name){
if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){
return $command;
}else{
return null;
}
$entry = $this->commandMap->getEntry($name);
return $entry instanceof CommandMapEntry && $entry->command instanceof PluginOwned ? $entry->command : null;
}

public function getNameBans() : BanList{
Expand Down
3 changes: 1 addition & 2 deletions src/command/ClosureCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,13 @@ public function __construct(
array $permissions,
Translatable|string $description = "",
Translatable|string|null $usageMessage = null,
array $aliases = []
){
Utils::validateCallableSignature(
fn(CommandSender $sender, Command $command, string $commandLabel, array $args) : mixed => 1,
$execute,
);
$this->execute = $execute;
parent::__construct($name, $description, $usageMessage, $aliases);
parent::__construct($name, $description, $usageMessage);
$this->setPermissions($permissions);
}

Expand Down
130 changes: 18 additions & 112 deletions src/command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,52 +33,21 @@
use pocketmine\Server;
use pocketmine\utils\BroadcastLoggerForwarder;
use pocketmine\utils\TextFormat;
use function array_values;
use function explode;
use function implode;
use function str_replace;
use const PHP_INT_MAX;

abstract class Command{

private string $name;

private string $nextLabel;
private string $label;

/**
* @var string[]
* @phpstan-var list<string>
*/
private array $aliases = [];

/**
* @var string[]
* @phpstan-var list<string>
*/
private array $activeAliases = [];

private ?CommandMap $commandMap = null;

protected Translatable|string $description = "";

protected Translatable|string $usageMessage;

/** @var string[] */
private array $permission = [];
private Translatable|string|null $permissionMessage = null;

/**
* @param string[] $aliases
* @phpstan-param list<string> $aliases
*/
public function __construct(string $name, Translatable|string $description = "", Translatable|string|null $usageMessage = null, array $aliases = []){
$this->name = $name;
$this->setLabel($name);
$this->setDescription($description);
$this->usageMessage = $usageMessage ?? ("/" . $name);
$this->setAliases($aliases);
}
public function __construct(
private string $name,
private Translatable|string $description = "",
private Translatable|string|null $usageMessage = null
){}

/**
* @param string[] $args
Expand All @@ -89,6 +58,10 @@ public function __construct(string $name, Translatable|string $description = "",
*/
abstract public function execute(CommandSender $sender, string $commandLabel, array $args);

/**
* Returns the local identifier of the command (without namespace or leading slash).
* This cannot be changed after creation.
*/
public function getName() : string{
return $this->name;
}
Expand Down Expand Up @@ -117,12 +90,17 @@ public function setPermission(?string $permission) : void{
$this->setPermissions($permission === null ? [] : explode(";", $permission, limit: PHP_INT_MAX));
}

public function testPermission(CommandSender $target, ?string $permission = null) : bool{
/**
* @param string $context usually the command name, but may include extra args if useful (e.g. for subcommands)
* @param CommandSender $target the target to check the permission for
* @param string|null $permission the permission to check, if null, will check if the target has any of the command's permissions
*/
public function testPermission(string $context, CommandSender $target, ?string $permission = null) : bool{
if($this->testPermissionSilent($target, $permission)){
return true;
}

$message = $this->permissionMessage ?? KnownTranslationFactory::pocketmine_command_error_permission($this->name);
$message = $this->permissionMessage ?? KnownTranslationFactory::pocketmine_command_error_permission($context);
if($message instanceof Translatable){
$target->sendMessage($message->prefix(TextFormat::RED));
}elseif($message !== ""){
Expand All @@ -143,62 +121,6 @@ public function testPermissionSilent(CommandSender $target, ?string $permission
return false;
}

public function getLabel() : string{
return $this->label;
}

public function setLabel(string $name) : bool{
$this->nextLabel = $name;
if(!$this->isRegistered()){
$this->label = $name;

return true;
}

return false;
}

/**
* Registers the command into a Command map
*/
public function register(CommandMap $commandMap) : bool{
if($this->allowChangesFrom($commandMap)){
$this->commandMap = $commandMap;

return true;
}

return false;
}

public function unregister(CommandMap $commandMap) : bool{
if($this->allowChangesFrom($commandMap)){
$this->commandMap = null;
$this->activeAliases = $this->aliases;
$this->label = $this->nextLabel;

return true;
}

return false;
}

private function allowChangesFrom(CommandMap $commandMap) : bool{
return $this->commandMap === null || $this->commandMap === $commandMap;
}

public function isRegistered() : bool{
return $this->commandMap !== null;
}

/**
* @return string[]
* @phpstan-return list<string>
*/
public function getAliases() : array{
return $this->activeAliases;
}

public function getPermissionMessage() : Translatable|string|null{
return $this->permissionMessage;
}
Expand All @@ -207,22 +129,10 @@ public function getDescription() : Translatable|string{
return $this->description;
}

public function getUsage() : Translatable|string{
public function getUsage() : Translatable|string|null{
return $this->usageMessage;
}

/**
* @param string[] $aliases
* @phpstan-param list<string> $aliases
*/
public function setAliases(array $aliases) : void{
$aliases = array_values($aliases); //because plugins can and will pass crap
$this->aliases = $aliases;
if(!$this->isRegistered()){
$this->activeAliases = $aliases;
}
}

public function setDescription(Translatable|string $description) : void{
$this->description = $description;
}
Expand All @@ -231,7 +141,7 @@ public function setPermissionMessage(Translatable|string $permissionMessage) : v
$this->permissionMessage = $permissionMessage;
}

public function setUsage(Translatable|string $usage) : void{
public function setUsage(Translatable|string|null $usage) : void{
$this->usageMessage = $usage;
}

Expand All @@ -252,8 +162,4 @@ public static function broadcastCommandMessage(CommandSender $source, Translatab
}
}
}

public function __toString() : string{
return $this->name;
}
}
29 changes: 23 additions & 6 deletions src/command/CommandMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,35 @@
namespace pocketmine\command;

interface CommandMap{

/**
* @param Command[] $commands
* Registering a command with (namespace="myplugin", command(name="mycommand"), otherAliases=["myc"]) will bind:
* - /myplugin:mycommand (always works, error thrown if not unique)
* - /mycommand (only works if not conflicted, not required to be unique)
* - /myc (only works if not conflicted, not required to be unique)
*
* If two commands claim the same alias, it will become conflicted, and neither command will be usable with that
* alias unless the alias is explicitly rebound with registerAlias().
* The user will be shown an error when trying to use it, listing all namespaced names (not aliases) of the commands
* bound to it. The user can then use one of the namespaced names to run the command they want.
* Conflicted aliases will not be included in the returned CommandMapEntry.
*
* @param string[] $otherAliases
*
* @phpstan-param list<string> $otherAliases
*/
public function registerAll(string $fallbackPrefix, array $commands) : void;

public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool;
public function register(string $namespace, Command $command, array $otherAliases = []) : CommandMapEntry;

public function dispatch(CommandSender $sender, string $cmdLine) : bool;

public function clearCommands() : void;

public function getCommand(string $name) : ?Command;
/**
* Returns the command(s) bound to the given name or alias.
* This will return an array if the alias is conflicted (multiple commands bound to it).
*
* @return CommandMapEntry|CommandMapEntry[]|null
* @phpstan-return CommandMapEntry|array<int, CommandMapEntry>|null
*/
public function getEntry(string $name) : CommandMapEntry|array|null;

}
53 changes: 53 additions & 0 deletions src/command/CommandMapEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/

declare(strict_types=1);

namespace pocketmine\command;

use pocketmine\lang\Translatable;

final class CommandMapEntry{

/**
* @param string[] $aliases
* @phpstan-param non-empty-list<string> $aliases
*/
public function __construct(
public readonly string $namespace,
public readonly Command $command,
public readonly array $aliases
){}

public function getNamespacedName() : string{
return $this->namespace . ":" . $this->command->getName();
}

public function getPreferredAlias() : string{
return $this->aliases[0];
}

public function getUsage() : Translatable|string{
//TODO: usage messages ought to use user-specified alias, not command preferred
//command-preferred is confusing if the user used a different alias
return $this->command->getUsage() ?? "/" . $this->getPreferredAlias();
}
}
12 changes: 7 additions & 5 deletions src/command/FormattedCommandAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ class FormattedCommandAlias extends Command{
* @param string[] $formatStrings
*/
public function __construct(
string $alias,
string $name,
private array $formatStrings
){
parent::__construct($alias, KnownTranslationFactory::pocketmine_command_userDefined_description());
parent::__construct($name, KnownTranslationFactory::pocketmine_command_userDefined_description());
}

public function execute(CommandSender $sender, string $commandLabel, array $args){
Expand Down Expand Up @@ -95,18 +95,20 @@ public function execute(CommandSender $sender, string $commandLabel, array $args
throw new AssumptionFailedError("This should have been checked before construction");
}

if(($target = $commandMap->getCommand($commandLabel)) !== null){
$timings = Timings::getCommandDispatchTimings($target->getLabel());
if(($target = $commandMap->getEntry($commandLabel)) instanceof CommandMapEntry){

$timings = Timings::getCommandDispatchTimings($target->getNamespacedName());
$timings->startTiming();

try{
$target->execute($sender, $commandLabel, $commandArgs);
$target->command->execute($sender, $commandLabel, $commandArgs);
}catch(InvalidCommandSyntaxException $e){
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage())));
}finally{
$timings->stopTiming();
}
}else{
//TODO: this seems suspicious - why do we continue alias execution if one of the commands is borked?
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->prefix(TextFormat::RED)));

//to match the behaviour of SimpleCommandMap::dispatch()
Expand Down
Loading