From 579aecfad760f1344a4188398d7b0f1df8b3ddb3 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 4 May 2025 15:23:25 +0100 Subject: [PATCH] First step in restructuring command alias handling This decouples Command from CommandMap, and moves the burden of tracking registered aliases to CommandMap. This allows lots of simplification, and also solves a few weird usage message issues. - Active (registered) aliases are now tracked via CommandMapEntry - Commands overriding other commands' aliases now update the original command's registered alias list properly - Command alias lists now include prefixed aliases - Prefixed aliases are now included in command data provided to the client - Server-side /pocketmine:help is now visible on the client - Permission testing can now provide context that's more relevant to the command invocation - e.g. if a user doesn't have /time set permission, it'll now show a more specific message where previously it would just show that the permission for /time was denied - User-specified label is now used for permission messages instead of command name - this is more consistent with user expectations - /help can now see prefixed aliases - Removed magic alias registration behaviour for VanillaCommand that I don't think anyone expected - Aliases are now provided to CommandMap via register() parameters, instead of being retrieved from the provided Command - Command->get/setAliases(), get/setLabel() and getName() are removed - Command->getName() pushed down to PluginCommand, as it's only useful for CommandExecutors as a command ID and shouldn't be used anywhere else --- src/command/ClosureCommand.php | 4 +- src/command/Command.php | 129 +--------- src/command/CommandMap.php | 9 +- src/command/CommandMapEntry.php | 48 ++++ src/command/FormattedCommandAlias.php | 11 +- src/command/PluginCommand.php | 20 +- src/command/SimpleCommandMap.php | 234 ++++++++++-------- src/command/defaults/BanCommand.php | 1 - src/command/defaults/BanIpCommand.php | 1 - src/command/defaults/BanListCommand.php | 1 - src/command/defaults/ClearCommand.php | 3 +- .../defaults/DefaultGamemodeCommand.php | 1 - src/command/defaults/DeopCommand.php | 1 - src/command/defaults/DifficultyCommand.php | 1 - src/command/defaults/DumpMemoryCommand.php | 1 - src/command/defaults/EffectCommand.php | 3 +- src/command/defaults/EnchantCommand.php | 3 +- src/command/defaults/GamemodeCommand.php | 3 +- .../defaults/GarbageCollectorCommand.php | 1 - src/command/defaults/GiveCommand.php | 3 +- src/command/defaults/HelpCommand.php | 30 +-- src/command/defaults/KickCommand.php | 1 - src/command/defaults/KillCommand.php | 6 +- src/command/defaults/ListCommand.php | 1 - src/command/defaults/MeCommand.php | 1 - src/command/defaults/OpCommand.php | 1 - src/command/defaults/PardonCommand.php | 4 +- src/command/defaults/PardonIpCommand.php | 4 +- src/command/defaults/ParticleCommand.php | 1 - src/command/defaults/PluginsCommand.php | 4 +- src/command/defaults/SaveCommand.php | 1 - src/command/defaults/SaveOffCommand.php | 1 - src/command/defaults/SaveOnCommand.php | 1 - src/command/defaults/SayCommand.php | 1 - src/command/defaults/SeedCommand.php | 1 - src/command/defaults/SetWorldSpawnCommand.php | 1 - src/command/defaults/SpawnpointCommand.php | 3 +- src/command/defaults/StatusCommand.php | 1 - src/command/defaults/StopCommand.php | 1 - src/command/defaults/TeleportCommand.php | 6 +- src/command/defaults/TellCommand.php | 4 +- src/command/defaults/TimeCommand.php | 12 +- src/command/defaults/TimingsCommand.php | 1 - src/command/defaults/TitleCommand.php | 3 +- .../defaults/TransferServerCommand.php | 1 - src/command/defaults/VanillaCommand.php | 15 +- src/command/defaults/VersionCommand.php | 4 +- src/command/defaults/WhitelistCommand.php | 17 +- src/command/defaults/XpCommand.php | 3 +- src/network/mcpe/NetworkSession.php | 29 +-- src/plugin/PluginBase.php | 26 +- 51 files changed, 294 insertions(+), 369 deletions(-) create mode 100644 src/command/CommandMapEntry.php diff --git a/src/command/ClosureCommand.php b/src/command/ClosureCommand.php index 289c82853e..aa09beef68 100644 --- a/src/command/ClosureCommand.php +++ b/src/command/ClosureCommand.php @@ -38,19 +38,17 @@ final class ClosureCommand extends Command{ * @phpstan-param Execute $execute */ public function __construct( - string $name, \Closure $execute, 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($description, $usageMessage); $this->setPermissions($permissions); } diff --git a/src/command/Command.php b/src/command/Command.php index 127d1886fc..a728356231 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -33,52 +33,20 @@ use pocketmine\permission\PermissionManager; 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 - */ - private array $aliases = []; - - /** - * @var string[] - * @phpstan-var list - */ - 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 $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 Translatable|string $description = "", + private Translatable|string|null $usageMessage = null + ){} /** * @param string[] $args @@ -89,10 +57,6 @@ abstract class Command{ */ abstract public function execute(CommandSender $sender, string $commandLabel, array $args); - public function getName() : string{ - return $this->name; - } - /** * @return string[] */ @@ -117,12 +81,17 @@ abstract class Command{ $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 !== ""){ @@ -143,62 +112,6 @@ abstract class Command{ 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 - */ - public function getAliases() : array{ - return $this->activeAliases; - } - public function getPermissionMessage() : Translatable|string|null{ return $this->permissionMessage; } @@ -207,22 +120,10 @@ abstract class Command{ return $this->description; } - public function getUsage() : Translatable|string{ + public function getUsage() : Translatable|string|null{ return $this->usageMessage; } - /** - * @param string[] $aliases - * @phpstan-param list $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; } @@ -231,7 +132,7 @@ abstract class Command{ $this->permissionMessage = $permissionMessage; } - public function setUsage(Translatable|string $usage) : void{ + public function setUsage(Translatable|string|null $usage) : void{ $this->usageMessage = $usage; } @@ -252,8 +153,4 @@ abstract class Command{ } } } - - public function __toString() : string{ - return $this->name; - } } diff --git a/src/command/CommandMap.php b/src/command/CommandMap.php index a7cf64ca41..4a8eced6c5 100644 --- a/src/command/CommandMap.php +++ b/src/command/CommandMap.php @@ -24,13 +24,12 @@ declare(strict_types=1); namespace pocketmine\command; interface CommandMap{ - /** - * @param Command[] $commands + * @param string[] $otherAliases + * + * @phpstan-param list $otherAliases */ - public function registerAll(string $fallbackPrefix, array $commands) : void; - - public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool; + public function register(string $fallbackPrefix, Command $command, string $preferredAlias, array $otherAliases = []) : CommandMapEntry; public function dispatch(CommandSender $sender, string $cmdLine) : bool; diff --git a/src/command/CommandMapEntry.php b/src/command/CommandMapEntry.php new file mode 100644 index 0000000000..ac5fd50d86 --- /dev/null +++ b/src/command/CommandMapEntry.php @@ -0,0 +1,48 @@ + $aliases + */ + public function __construct( + public readonly Command $command, + public readonly array $aliases + ){} + + 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(); + } +} diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php index b473633972..e3a5a328c2 100644 --- a/src/command/FormattedCommandAlias.php +++ b/src/command/FormattedCommandAlias.php @@ -50,10 +50,9 @@ class FormattedCommandAlias extends Command{ * @param string[] $formatStrings */ public function __construct( - string $alias, private array $formatStrings ){ - parent::__construct($alias, KnownTranslationFactory::pocketmine_command_userDefined_description()); + parent::__construct(KnownTranslationFactory::pocketmine_command_userDefined_description()); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -95,12 +94,14 @@ class FormattedCommandAlias extends Command{ 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)) !== null){ + //TODO: using labels for command dispatch is problematic - what if the label changes? + //maybe this should use command class instead? + $timings = Timings::getCommandDispatchTimings($target->getPreferredAlias()); $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{ diff --git a/src/command/PluginCommand.php b/src/command/PluginCommand.php index 55b8df3ed9..914b3c94a0 100644 --- a/src/command/PluginCommand.php +++ b/src/command/PluginCommand.php @@ -24,17 +24,23 @@ declare(strict_types=1); namespace pocketmine\command; use pocketmine\command\utils\InvalidCommandSyntaxException; +use pocketmine\lang\Translatable; use pocketmine\plugin\Plugin; use pocketmine\plugin\PluginOwned; final class PluginCommand extends Command implements PluginOwned{ public function __construct( - string $name, + private string $name, private Plugin $owner, - private CommandExecutor $executor + private CommandExecutor $executor, + Translatable|string $description = "", + Translatable|string|null $usageMessage = null ){ - parent::__construct($name); - $this->usageMessage = ""; + parent::__construct($description, $usageMessage); + } + + public function getName() : string{ + return $this->name; } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -43,13 +49,11 @@ final class PluginCommand extends Command implements PluginOwned{ return false; } - $success = $this->executor->onCommand($sender, $this, $commandLabel, $args); - - if(!$success && $this->usageMessage !== ""){ + if(!$this->executor->onCommand($sender, $this, $commandLabel, $args)){ throw new InvalidCommandSyntaxException(); } - return $success; + return true; } public function getOwningPlugin() : Plugin{ diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index 9f54417469..2e4d623eff 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -61,7 +61,6 @@ use pocketmine\command\defaults\TimeCommand; use pocketmine\command\defaults\TimingsCommand; use pocketmine\command\defaults\TitleCommand; use pocketmine\command\defaults\TransferServerCommand; -use pocketmine\command\defaults\VanillaCommand; use pocketmine\command\defaults\VersionCommand; use pocketmine\command\defaults\WhitelistCommand; use pocketmine\command\defaults\XpCommand; @@ -70,12 +69,15 @@ use pocketmine\command\utils\InvalidCommandSyntaxException; use pocketmine\lang\KnownTranslationFactory; use pocketmine\Server; use pocketmine\timings\Timings; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; +use function array_filter; use function array_shift; use function array_values; use function count; use function implode; +use function spl_object_id; use function str_contains; use function strcasecmp; use function strtolower; @@ -87,122 +89,124 @@ class SimpleCommandMap implements CommandMap{ * @var Command[] * @phpstan-var array */ - protected array $knownCommands = []; + protected array $aliasToCommandMap = []; + + /** + * @var CommandMapEntry[] + * @phpstan-var array + */ + private array $uniqueCommands = []; public function __construct(private Server $server){ $this->setDefaultCommands(); } private function setDefaultCommands() : void{ - $this->registerAll("pocketmine", [ - new BanCommand(), - new BanIpCommand(), - new BanListCommand(), - new ClearCommand(), - new DefaultGamemodeCommand(), - new DeopCommand(), - new DifficultyCommand(), - new DumpMemoryCommand(), - new EffectCommand(), - new EnchantCommand(), - new GamemodeCommand(), - new GarbageCollectorCommand(), - new GiveCommand(), - new HelpCommand(), - new KickCommand(), - new KillCommand(), - new ListCommand(), - new MeCommand(), - new OpCommand(), - new PardonCommand(), - new PardonIpCommand(), - new ParticleCommand(), - new PluginsCommand(), - new SaveCommand(), - new SaveOffCommand(), - new SaveOnCommand(), - new SayCommand(), - new SeedCommand(), - new SetWorldSpawnCommand(), - new SpawnpointCommand(), - new StatusCommand(), - new StopCommand(), - new TeleportCommand(), - new TellCommand(), - new TimeCommand(), - new TimingsCommand(), - new TitleCommand(), - new TransferServerCommand(), - new VersionCommand(), - new WhitelistCommand(), - new XpCommand(), - ]); + $pmPrefix = "pocketmine"; + $this->register($pmPrefix, new BanCommand(), "ban"); + $this->register($pmPrefix, new BanIpCommand(), "ban-ip"); + $this->register($pmPrefix, new BanListCommand(), "banlist"); + $this->register($pmPrefix, new ClearCommand(), "clear"); + $this->register($pmPrefix, new DefaultGamemodeCommand(), "defaultgamemode"); + $this->register($pmPrefix, new DeopCommand(), "deop"); + $this->register($pmPrefix, new DifficultyCommand(), "difficulty"); + $this->register($pmPrefix, new DumpMemoryCommand(), "dumpmemory"); + $this->register($pmPrefix, new EffectCommand(), "effect"); + $this->register($pmPrefix, new EnchantCommand(), "enchant"); + $this->register($pmPrefix, new GamemodeCommand(), "gamemode"); + $this->register($pmPrefix, new GarbageCollectorCommand(), "gc"); + $this->register($pmPrefix, new GiveCommand(), "give"); + $this->register($pmPrefix, new HelpCommand(), "help", ["?"]); + $this->register($pmPrefix, new KickCommand(), "kick"); + $this->register($pmPrefix, new KillCommand(), "kill", ["suicide"]); + $this->register($pmPrefix, new ListCommand(), "list"); + $this->register($pmPrefix, new MeCommand(), "me"); + $this->register($pmPrefix, new OpCommand(), "op"); + $this->register($pmPrefix, new PardonCommand(), "pardon", ["unban"]); + $this->register($pmPrefix, new PardonIpCommand(), "pardon-ip", ["unban-ip"]); + $this->register($pmPrefix, new ParticleCommand(), "particle"); + $this->register($pmPrefix, new PluginsCommand(), "plugins", ["pl"]); + $this->register($pmPrefix, new SaveCommand(), "save-all"); + $this->register($pmPrefix, new SaveOffCommand(), "save-off"); + $this->register($pmPrefix, new SaveOnCommand(), "save-on"); + $this->register($pmPrefix, new SayCommand(), "say"); + $this->register($pmPrefix, new SeedCommand(), "seed"); + $this->register($pmPrefix, new SetWorldSpawnCommand(), "setworldspawn"); + $this->register($pmPrefix, new SpawnpointCommand(), "spawnpoint"); + $this->register($pmPrefix, new StatusCommand(), "status"); + $this->register($pmPrefix, new StopCommand(), "stop"); + $this->register($pmPrefix, new TeleportCommand(), "tp", ["teleport"]); + $this->register($pmPrefix, new TellCommand(), "tell", ["w", "msg"]); + $this->register($pmPrefix, new TimeCommand(), "time"); + $this->register($pmPrefix, new TimingsCommand(), "timings"); + $this->register($pmPrefix, new TitleCommand(), "title"); + $this->register($pmPrefix, new TransferServerCommand(), "transferserver"); + $this->register($pmPrefix, new VersionCommand(), "version", ["ver", "about"]); + $this->register($pmPrefix, new WhitelistCommand(), "whitelist"); + $this->register($pmPrefix, new XpCommand(), "xp"); } - public function registerAll(string $fallbackPrefix, array $commands) : void{ - foreach($commands as $command){ - $this->register($fallbackPrefix, $command); - } - } - - public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool{ + public function register(string $fallbackPrefix, Command $command, string $preferredAlias, array $otherAliases = []) : CommandMapEntry{ if(count($command->getPermissions()) === 0){ throw new \InvalidArgumentException("Commands must have a permission set"); } - if($label === null){ - $label = $command->getLabel(); - } - $label = trim($label); + $preferredAlias = trim($preferredAlias); $fallbackPrefix = strtolower(trim($fallbackPrefix)); - $registered = $this->registerAlias($command, false, $fallbackPrefix, $label); + $registeredAliases = []; + //primary labels take precedence over any existing registrations + $this->mapAlias($preferredAlias, $command, $registeredAliases); + $this->mapAlias($fallbackPrefix . ":" . $preferredAlias, $command, $registeredAliases); - $aliases = $command->getAliases(); - foreach($aliases as $index => $alias){ - if(!$this->registerAlias($command, true, $fallbackPrefix, $alias)){ - unset($aliases[$index]); + foreach($otherAliases as $alias){ + $this->mapAlias($fallbackPrefix . ":" . $alias, $command, $registeredAliases); + if(!isset($this->aliasToCommandMap[$alias])){ + $this->mapAlias($alias, $command, $registeredAliases); } } - $command->setAliases(array_values($aliases)); - if(!$registered){ - $command->setLabel($fallbackPrefix . ":" . $label); + $entry = new CommandMapEntry($command, $registeredAliases); + $this->uniqueCommands[spl_object_id($command)] = $entry; + + return $entry; + } + + /** + * @param string[] &$registeredAliases + * @phpstan-param list &$registeredAliases + * @phpstan-param-out non-empty-list $registeredAliases + */ + private function mapAlias(string $alias, Command $command, array &$registeredAliases) : void{ + $this->unmapAlias($alias); + $this->aliasToCommandMap[$alias] = $command; + $registeredAliases[] = $alias; + } + + private function unmapAlias(string $alias) : void{ + $oldCommand = $this->aliasToCommandMap[$alias] ?? null; + if($oldCommand !== null){ + unset($this->aliasToCommandMap[$alias]); + $oldCommandKey = spl_object_id($oldCommand); + $oldCommandEntry = $this->uniqueCommands[$oldCommandKey]; + $filteredAliases = array_values(array_filter($oldCommandEntry->aliases, fn(string $oldAlias) => $oldAlias !== $alias)); + if(count($filteredAliases) > 0){ + $this->uniqueCommands[$oldCommandKey] = new CommandMapEntry($oldCommand, $filteredAliases); + }else{ + unset($this->uniqueCommands[$oldCommandKey]); + } } - - $command->register($this); - - return $registered; } public function unregister(Command $command) : bool{ - foreach(Utils::promoteKeys($this->knownCommands) as $lbl => $cmd){ - if($cmd === $command){ - unset($this->knownCommands[$lbl]); + $entry = $this->uniqueCommands[spl_object_id($command)] ?? null; + if($entry !== null){ + unset($this->uniqueCommands[spl_object_id($command)]); + foreach($entry->aliases as $alias){ + unset($this->aliasToCommandMap[$alias]); } } - $command->unregister($this); - - return true; - } - - private function registerAlias(Command $command, bool $isAlias, string $fallbackPrefix, string $label) : bool{ - $this->knownCommands[$fallbackPrefix . ":" . $label] = $command; - if(($command instanceof VanillaCommand || $isAlias) && isset($this->knownCommands[$label])){ - return false; - } - - if(isset($this->knownCommands[$label]) && $this->knownCommands[$label]->getLabel() === $label){ - return false; - } - - if(!$isAlias){ - $command->setLabel($label); - } - - $this->knownCommands[$label] = $command; - return true; } @@ -210,13 +214,15 @@ class SimpleCommandMap implements CommandMap{ $args = CommandStringHelper::parseQuoteAware($commandLine); $sentCommandLabel = array_shift($args); - if($sentCommandLabel !== null && ($target = $this->getCommand($sentCommandLabel)) !== null){ - $timings = Timings::getCommandDispatchTimings($target->getLabel()); + if($sentCommandLabel !== null && ($target = $this->getEntry($sentCommandLabel)) !== null){ + //TODO: using labels for command dispatch is problematic - what if the label changes? + //maybe this should use command class instead? + $timings = Timings::getCommandDispatchTimings($target->getPreferredAlias()); $timings->startTiming(); try{ - if($target->testPermission($sender)){ - $target->execute($sender, $sentCommandLabel, $args); + if($target->command->testPermission($sentCommandLabel, $sender)){ + $target->command->execute($sender, $sentCommandLabel, $args); } }catch(InvalidCommandSyntaxException $e){ $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage()))); @@ -231,23 +237,36 @@ class SimpleCommandMap implements CommandMap{ } public function clearCommands() : void{ - foreach($this->knownCommands as $command){ - $command->unregister($this); - } - $this->knownCommands = []; + $this->aliasToCommandMap = []; + $this->uniqueCommands = []; $this->setDefaultCommands(); } public function getCommand(string $name) : ?Command{ - return $this->knownCommands[$name] ?? null; + return $this->aliasToCommandMap[$name] ?? null; } /** * @return Command[] * @phpstan-return array */ - public function getCommands() : array{ - return $this->knownCommands; + public function getAliasToCommandMap() : array{ + return $this->aliasToCommandMap; + } + + /** + * @return CommandMapEntry[] + * @phpstan-return array + */ + public function getUniqueCommands() : array{ + return $this->uniqueCommands; + } + + public function getEntry(string $name) : ?CommandMapEntry{ + $command = $this->getCommand($name); + return $command !== null ? + $this->uniqueCommands[spl_object_id($command)] ?? throw new AssumptionFailedError("This should never be unset") : + null; } public function registerServerAliases() : void{ @@ -289,12 +308,13 @@ class SimpleCommandMap implements CommandMap{ //These registered commands have absolute priority $lowerAlias = strtolower($alias); + $this->unmapAlias($lowerAlias); if(count($targets) > 0){ - $this->knownCommands[$lowerAlias] = new FormattedCommandAlias($lowerAlias, $targets); - }else{ - unset($this->knownCommands[$lowerAlias]); + $aliasInstance = new FormattedCommandAlias($targets); + $registeredAliases = []; + $this->mapAlias($lowerAlias, $aliasInstance, $registeredAliases); + $this->uniqueCommands[spl_object_id($aliasInstance)] = new CommandMapEntry($aliasInstance, $registeredAliases); } - } } } diff --git a/src/command/defaults/BanCommand.php b/src/command/defaults/BanCommand.php index adbdb6d3ad..faa234e87f 100644 --- a/src/command/defaults/BanCommand.php +++ b/src/command/defaults/BanCommand.php @@ -37,7 +37,6 @@ class BanCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "ban", KnownTranslationFactory::pocketmine_command_ban_player_description(), KnownTranslationFactory::commands_ban_usage() ); diff --git a/src/command/defaults/BanIpCommand.php b/src/command/defaults/BanIpCommand.php index a23477e0d7..af7b59d56d 100644 --- a/src/command/defaults/BanIpCommand.php +++ b/src/command/defaults/BanIpCommand.php @@ -38,7 +38,6 @@ class BanIpCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "ban-ip", KnownTranslationFactory::pocketmine_command_ban_ip_description(), KnownTranslationFactory::commands_banip_usage() ); diff --git a/src/command/defaults/BanListCommand.php b/src/command/defaults/BanListCommand.php index 95c2149696..63e411c84d 100644 --- a/src/command/defaults/BanListCommand.php +++ b/src/command/defaults/BanListCommand.php @@ -39,7 +39,6 @@ class BanListCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "banlist", KnownTranslationFactory::pocketmine_command_banlist_description(), KnownTranslationFactory::commands_banlist_usage() ); diff --git a/src/command/defaults/ClearCommand.php b/src/command/defaults/ClearCommand.php index f6f491fbfa..4421ca58b5 100644 --- a/src/command/defaults/ClearCommand.php +++ b/src/command/defaults/ClearCommand.php @@ -41,7 +41,6 @@ class ClearCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "clear", KnownTranslationFactory::pocketmine_command_clear_description(), KnownTranslationFactory::pocketmine_command_clear_usage() ); @@ -53,7 +52,7 @@ class ClearCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $target = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_CLEAR_SELF, DefaultPermissionNames::COMMAND_CLEAR_OTHER); + $target = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_CLEAR_SELF, DefaultPermissionNames::COMMAND_CLEAR_OTHER); if($target === null){ return true; } diff --git a/src/command/defaults/DefaultGamemodeCommand.php b/src/command/defaults/DefaultGamemodeCommand.php index d3030eb27b..d369244c21 100644 --- a/src/command/defaults/DefaultGamemodeCommand.php +++ b/src/command/defaults/DefaultGamemodeCommand.php @@ -35,7 +35,6 @@ class DefaultGamemodeCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "defaultgamemode", KnownTranslationFactory::pocketmine_command_defaultgamemode_description(), KnownTranslationFactory::commands_defaultgamemode_usage() ); diff --git a/src/command/defaults/DeopCommand.php b/src/command/defaults/DeopCommand.php index 4167012f76..db15b5d35d 100644 --- a/src/command/defaults/DeopCommand.php +++ b/src/command/defaults/DeopCommand.php @@ -37,7 +37,6 @@ class DeopCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "deop", KnownTranslationFactory::pocketmine_command_deop_description(), KnownTranslationFactory::commands_deop_usage() ); diff --git a/src/command/defaults/DifficultyCommand.php b/src/command/defaults/DifficultyCommand.php index dee75e025e..c66b7b0272 100644 --- a/src/command/defaults/DifficultyCommand.php +++ b/src/command/defaults/DifficultyCommand.php @@ -36,7 +36,6 @@ class DifficultyCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "difficulty", KnownTranslationFactory::pocketmine_command_difficulty_description(), KnownTranslationFactory::commands_difficulty_usage() ); diff --git a/src/command/defaults/DumpMemoryCommand.php b/src/command/defaults/DumpMemoryCommand.php index 08dc0debae..68f6d5cc4f 100644 --- a/src/command/defaults/DumpMemoryCommand.php +++ b/src/command/defaults/DumpMemoryCommand.php @@ -33,7 +33,6 @@ class DumpMemoryCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "dumpmemory", KnownTranslationFactory::pocketmine_command_dumpmemory_description(), "/dumpmemory [path]" ); diff --git a/src/command/defaults/EffectCommand.php b/src/command/defaults/EffectCommand.php index 9383232229..934ac3fd0a 100644 --- a/src/command/defaults/EffectCommand.php +++ b/src/command/defaults/EffectCommand.php @@ -38,7 +38,6 @@ class EffectCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "effect", KnownTranslationFactory::pocketmine_command_effect_description(), KnownTranslationFactory::commands_effect_usage() ); @@ -53,7 +52,7 @@ class EffectCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_EFFECT_SELF, DefaultPermissionNames::COMMAND_EFFECT_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0], DefaultPermissionNames::COMMAND_EFFECT_SELF, DefaultPermissionNames::COMMAND_EFFECT_OTHER); if($player === null){ return true; } diff --git a/src/command/defaults/EnchantCommand.php b/src/command/defaults/EnchantCommand.php index 191a146b03..70e20c8cfd 100644 --- a/src/command/defaults/EnchantCommand.php +++ b/src/command/defaults/EnchantCommand.php @@ -36,7 +36,6 @@ class EnchantCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "enchant", KnownTranslationFactory::pocketmine_command_enchant_description(), KnownTranslationFactory::commands_enchant_usage() ); @@ -51,7 +50,7 @@ class EnchantCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_ENCHANT_SELF, DefaultPermissionNames::COMMAND_ENCHANT_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0], DefaultPermissionNames::COMMAND_ENCHANT_SELF, DefaultPermissionNames::COMMAND_ENCHANT_OTHER); if($player === null){ return true; } diff --git a/src/command/defaults/GamemodeCommand.php b/src/command/defaults/GamemodeCommand.php index 666626a692..2d82b0fdbf 100644 --- a/src/command/defaults/GamemodeCommand.php +++ b/src/command/defaults/GamemodeCommand.php @@ -35,7 +35,6 @@ class GamemodeCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "gamemode", KnownTranslationFactory::pocketmine_command_gamemode_description(), KnownTranslationFactory::commands_gamemode_usage() ); @@ -56,7 +55,7 @@ class GamemodeCommand extends VanillaCommand{ return true; } - $target = $this->fetchPermittedPlayerTarget($sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_GAMEMODE_SELF, DefaultPermissionNames::COMMAND_GAMEMODE_OTHER); + $target = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_GAMEMODE_SELF, DefaultPermissionNames::COMMAND_GAMEMODE_OTHER); if($target === null){ return true; } diff --git a/src/command/defaults/GarbageCollectorCommand.php b/src/command/defaults/GarbageCollectorCommand.php index 7bd64666f1..ab8d21e853 100644 --- a/src/command/defaults/GarbageCollectorCommand.php +++ b/src/command/defaults/GarbageCollectorCommand.php @@ -36,7 +36,6 @@ class GarbageCollectorCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "gc", KnownTranslationFactory::pocketmine_command_gc_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_GC); diff --git a/src/command/defaults/GiveCommand.php b/src/command/defaults/GiveCommand.php index 72d33688b0..2916d9bc73 100644 --- a/src/command/defaults/GiveCommand.php +++ b/src/command/defaults/GiveCommand.php @@ -43,7 +43,6 @@ class GiveCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "give", KnownTranslationFactory::pocketmine_command_give_description(), KnownTranslationFactory::pocketmine_command_give_usage() ); @@ -58,7 +57,7 @@ class GiveCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_GIVE_SELF, DefaultPermissionNames::COMMAND_GIVE_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0], DefaultPermissionNames::COMMAND_GIVE_SELF, DefaultPermissionNames::COMMAND_GIVE_OTHER); if($player === null){ return true; } diff --git a/src/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php index 054585455f..060a209622 100644 --- a/src/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -47,10 +47,8 @@ class HelpCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "help", KnownTranslationFactory::pocketmine_command_help_description(), - KnownTranslationFactory::commands_help_usage(), - ["?"] + KnownTranslationFactory::commands_help_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_HELP); } @@ -72,11 +70,13 @@ class HelpCommand extends VanillaCommand{ $pageHeight = $sender->getScreenLineHeight(); + //TODO: maybe inject this in the constructor instead of assuming the server's command map? + $commandMap = $sender->getServer()->getCommandMap(); if($commandName === ""){ $commands = []; - foreach($sender->getServer()->getCommandMap()->getCommands() as $command){ - if($command->testPermissionSilent($sender)){ - $commands[$command->getLabel()] = $command; + foreach($commandMap->getUniqueCommands() as $commandEntry){ + if($commandEntry->command->testPermissionSilent($sender)){ + $commands[$commandEntry->getPreferredAlias()] = $commandEntry; } } ksort($commands, SORT_NATURAL | SORT_FLAG_CASE); @@ -88,31 +88,31 @@ class HelpCommand extends VanillaCommand{ $sender->sendMessage(KnownTranslationFactory::commands_help_header((string) $pageNumber, (string) count($commands))); $lang = $sender->getLanguage(); if(isset($commands[$pageNumber - 1])){ - foreach($commands[$pageNumber - 1] as $command){ - $description = $command->getDescription(); + foreach($commands[$pageNumber - 1] as $commandEntry){ + $description = $commandEntry->command->getDescription(); $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; - $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getLabel() . ": " . TextFormat::RESET . $descriptionString); + $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $commandEntry->getPreferredAlias() . ": " . TextFormat::RESET . $descriptionString); } } return true; }else{ - if(($cmd = $sender->getServer()->getCommandMap()->getCommand(strtolower($commandName))) instanceof Command){ - if($cmd->testPermissionSilent($sender)){ + if(($commandEntry = $commandMap->getEntry(strtolower($commandName))) !== null){ + if($commandEntry->command->testPermissionSilent($sender)){ $lang = $sender->getLanguage(); - $description = $cmd->getDescription(); + $description = $commandEntry->command->getDescription(); $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel()) + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($commandEntry->getPreferredAlias()) ->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------")); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString) ->prefix(TextFormat::GOLD)); - $usage = $cmd->getUsage(); + $usage = $commandEntry->getUsage(); $usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX))) ->prefix(TextFormat::GOLD)); - $aliases = $cmd->getAliases(); + $aliases = $commandEntry->aliases; sort($aliases, SORT_NATURAL); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) ->prefix(TextFormat::GOLD)); diff --git a/src/command/defaults/KickCommand.php b/src/command/defaults/KickCommand.php index 3d63e8bb33..69d33cbf53 100644 --- a/src/command/defaults/KickCommand.php +++ b/src/command/defaults/KickCommand.php @@ -39,7 +39,6 @@ class KickCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "kick", KnownTranslationFactory::pocketmine_command_kick_description(), KnownTranslationFactory::commands_kick_usage() ); diff --git a/src/command/defaults/KillCommand.php b/src/command/defaults/KillCommand.php index fbc7436902..0522380c60 100644 --- a/src/command/defaults/KillCommand.php +++ b/src/command/defaults/KillCommand.php @@ -35,10 +35,8 @@ class KillCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "kill", KnownTranslationFactory::pocketmine_command_kill_description(), - KnownTranslationFactory::pocketmine_command_kill_usage(), - ["suicide"] + KnownTranslationFactory::pocketmine_command_kill_usage() ); $this->setPermissions([DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER]); } @@ -48,7 +46,7 @@ class KillCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER); if($player === null){ return true; } diff --git a/src/command/defaults/ListCommand.php b/src/command/defaults/ListCommand.php index 0264a2ef4f..62546e1d79 100644 --- a/src/command/defaults/ListCommand.php +++ b/src/command/defaults/ListCommand.php @@ -38,7 +38,6 @@ class ListCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "list", KnownTranslationFactory::pocketmine_command_list_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_LIST); diff --git a/src/command/defaults/MeCommand.php b/src/command/defaults/MeCommand.php index a6708840c3..4959cf679c 100644 --- a/src/command/defaults/MeCommand.php +++ b/src/command/defaults/MeCommand.php @@ -36,7 +36,6 @@ class MeCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "me", KnownTranslationFactory::pocketmine_command_me_description(), KnownTranslationFactory::commands_me_usage() ); diff --git a/src/command/defaults/OpCommand.php b/src/command/defaults/OpCommand.php index bcd1193511..431ea7c098 100644 --- a/src/command/defaults/OpCommand.php +++ b/src/command/defaults/OpCommand.php @@ -37,7 +37,6 @@ class OpCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "op", KnownTranslationFactory::pocketmine_command_op_description(), KnownTranslationFactory::commands_op_usage() ); diff --git a/src/command/defaults/PardonCommand.php b/src/command/defaults/PardonCommand.php index 425b9e69f1..56dea2ed20 100644 --- a/src/command/defaults/PardonCommand.php +++ b/src/command/defaults/PardonCommand.php @@ -34,10 +34,8 @@ class PardonCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "pardon", KnownTranslationFactory::pocketmine_command_unban_player_description(), - KnownTranslationFactory::commands_unban_usage(), - ["unban"] + KnownTranslationFactory::commands_unban_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER); } diff --git a/src/command/defaults/PardonIpCommand.php b/src/command/defaults/PardonIpCommand.php index 65bed221b0..b1ff0fd969 100644 --- a/src/command/defaults/PardonIpCommand.php +++ b/src/command/defaults/PardonIpCommand.php @@ -35,10 +35,8 @@ class PardonIpCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "pardon-ip", KnownTranslationFactory::pocketmine_command_unban_ip_description(), - KnownTranslationFactory::commands_unbanip_usage(), - ["unban-ip"] + KnownTranslationFactory::commands_unbanip_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_IP); } diff --git a/src/command/defaults/ParticleCommand.php b/src/command/defaults/ParticleCommand.php index 4867e3eb59..10f18f2f11 100644 --- a/src/command/defaults/ParticleCommand.php +++ b/src/command/defaults/ParticleCommand.php @@ -76,7 +76,6 @@ class ParticleCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "particle", KnownTranslationFactory::pocketmine_command_particle_description(), KnownTranslationFactory::pocketmine_command_particle_usage() ); diff --git a/src/command/defaults/PluginsCommand.php b/src/command/defaults/PluginsCommand.php index d68fcbb01a..660ebf0bb9 100644 --- a/src/command/defaults/PluginsCommand.php +++ b/src/command/defaults/PluginsCommand.php @@ -38,10 +38,8 @@ class PluginsCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "plugins", KnownTranslationFactory::pocketmine_command_plugins_description(), - null, - ["pl"] + null ); $this->setPermission(DefaultPermissionNames::COMMAND_PLUGINS); } diff --git a/src/command/defaults/SaveCommand.php b/src/command/defaults/SaveCommand.php index 4e406e6a3c..6ba1bd5118 100644 --- a/src/command/defaults/SaveCommand.php +++ b/src/command/defaults/SaveCommand.php @@ -34,7 +34,6 @@ class SaveCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "save-all", KnownTranslationFactory::pocketmine_command_save_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_PERFORM); diff --git a/src/command/defaults/SaveOffCommand.php b/src/command/defaults/SaveOffCommand.php index 73b8ca1517..caae842164 100644 --- a/src/command/defaults/SaveOffCommand.php +++ b/src/command/defaults/SaveOffCommand.php @@ -32,7 +32,6 @@ class SaveOffCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "save-off", KnownTranslationFactory::pocketmine_command_saveoff_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_DISABLE); diff --git a/src/command/defaults/SaveOnCommand.php b/src/command/defaults/SaveOnCommand.php index f0a324aebe..1d0e77bbc5 100644 --- a/src/command/defaults/SaveOnCommand.php +++ b/src/command/defaults/SaveOnCommand.php @@ -32,7 +32,6 @@ class SaveOnCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "save-on", KnownTranslationFactory::pocketmine_command_saveon_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_ENABLE); diff --git a/src/command/defaults/SayCommand.php b/src/command/defaults/SayCommand.php index 5c3203b5f0..9918836738 100644 --- a/src/command/defaults/SayCommand.php +++ b/src/command/defaults/SayCommand.php @@ -37,7 +37,6 @@ class SayCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "say", KnownTranslationFactory::pocketmine_command_say_description(), KnownTranslationFactory::commands_say_usage() ); diff --git a/src/command/defaults/SeedCommand.php b/src/command/defaults/SeedCommand.php index 6b426bd45e..1dca68307d 100644 --- a/src/command/defaults/SeedCommand.php +++ b/src/command/defaults/SeedCommand.php @@ -32,7 +32,6 @@ class SeedCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "seed", KnownTranslationFactory::pocketmine_command_seed_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_SEED); diff --git a/src/command/defaults/SetWorldSpawnCommand.php b/src/command/defaults/SetWorldSpawnCommand.php index f251ab8a44..13e6759185 100644 --- a/src/command/defaults/SetWorldSpawnCommand.php +++ b/src/command/defaults/SetWorldSpawnCommand.php @@ -38,7 +38,6 @@ class SetWorldSpawnCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "setworldspawn", KnownTranslationFactory::pocketmine_command_setworldspawn_description(), KnownTranslationFactory::commands_setworldspawn_usage() ); diff --git a/src/command/defaults/SpawnpointCommand.php b/src/command/defaults/SpawnpointCommand.php index 614a749d53..a4c9160205 100644 --- a/src/command/defaults/SpawnpointCommand.php +++ b/src/command/defaults/SpawnpointCommand.php @@ -38,7 +38,6 @@ class SpawnpointCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "spawnpoint", KnownTranslationFactory::pocketmine_command_spawnpoint_description(), KnownTranslationFactory::commands_spawnpoint_usage() ); @@ -49,7 +48,7 @@ class SpawnpointCommand extends VanillaCommand{ } public function execute(CommandSender $sender, string $commandLabel, array $args){ - $target = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_SPAWNPOINT_SELF, DefaultPermissionNames::COMMAND_SPAWNPOINT_OTHER); + $target = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_SPAWNPOINT_SELF, DefaultPermissionNames::COMMAND_SPAWNPOINT_OTHER); if($target === null){ return true; } diff --git a/src/command/defaults/StatusCommand.php b/src/command/defaults/StatusCommand.php index 033a2f2607..81d90076fd 100644 --- a/src/command/defaults/StatusCommand.php +++ b/src/command/defaults/StatusCommand.php @@ -38,7 +38,6 @@ class StatusCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "status", KnownTranslationFactory::pocketmine_command_status_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_STATUS); diff --git a/src/command/defaults/StopCommand.php b/src/command/defaults/StopCommand.php index bc4eef91cb..9f4c55bbf9 100644 --- a/src/command/defaults/StopCommand.php +++ b/src/command/defaults/StopCommand.php @@ -32,7 +32,6 @@ class StopCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "stop", KnownTranslationFactory::pocketmine_command_stop_description() ); $this->setPermission(DefaultPermissionNames::COMMAND_STOP); diff --git a/src/command/defaults/TeleportCommand.php b/src/command/defaults/TeleportCommand.php index b506364bf5..edd30914cd 100644 --- a/src/command/defaults/TeleportCommand.php +++ b/src/command/defaults/TeleportCommand.php @@ -41,10 +41,8 @@ class TeleportCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "tp", KnownTranslationFactory::pocketmine_command_tp_description(), - KnownTranslationFactory::commands_tp_usage(), - ["teleport"] + KnownTranslationFactory::commands_tp_usage() ); $this->setPermissions([ DefaultPermissionNames::COMMAND_TELEPORT_SELF, @@ -77,7 +75,7 @@ class TeleportCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $subject = $this->fetchPermittedPlayerTarget($sender, $subjectName, DefaultPermissionNames::COMMAND_TELEPORT_SELF, DefaultPermissionNames::COMMAND_TELEPORT_OTHER); + $subject = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $subjectName, DefaultPermissionNames::COMMAND_TELEPORT_SELF, DefaultPermissionNames::COMMAND_TELEPORT_OTHER); if($subject === null){ return true; } diff --git a/src/command/defaults/TellCommand.php b/src/command/defaults/TellCommand.php index 713023382b..954a418532 100644 --- a/src/command/defaults/TellCommand.php +++ b/src/command/defaults/TellCommand.php @@ -38,10 +38,8 @@ class TellCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "tell", KnownTranslationFactory::pocketmine_command_tell_description(), - KnownTranslationFactory::commands_message_usage(), - ["w", "msg"] + KnownTranslationFactory::commands_message_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_TELL); } diff --git a/src/command/defaults/TimeCommand.php b/src/command/defaults/TimeCommand.php index 8e937b21be..05c1801bd2 100644 --- a/src/command/defaults/TimeCommand.php +++ b/src/command/defaults/TimeCommand.php @@ -36,7 +36,6 @@ class TimeCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "time", KnownTranslationFactory::pocketmine_command_time_description(), KnownTranslationFactory::pocketmine_command_time_usage() ); @@ -53,9 +52,10 @@ class TimeCommand extends VanillaCommand{ if(count($args) < 1){ throw new InvalidCommandSyntaxException(); } + $testPermissionCtx = $commandLabel . " " . $args[0]; if($args[0] === "start"){ - if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_START)){ + if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_START)){ return true; } foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ @@ -64,7 +64,7 @@ class TimeCommand extends VanillaCommand{ Command::broadcastCommandMessage($sender, "Restarted the time"); return true; }elseif($args[0] === "stop"){ - if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_STOP)){ + if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_STOP)){ return true; } foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ @@ -73,7 +73,7 @@ class TimeCommand extends VanillaCommand{ Command::broadcastCommandMessage($sender, "Stopped the time"); return true; }elseif($args[0] === "query"){ - if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_QUERY)){ + if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_QUERY)){ return true; } if($sender instanceof Player){ @@ -90,7 +90,7 @@ class TimeCommand extends VanillaCommand{ } if($args[0] === "set"){ - if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_SET)){ + if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_SET)){ return true; } @@ -123,7 +123,7 @@ class TimeCommand extends VanillaCommand{ } Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_time_set((string) $value)); }elseif($args[0] === "add"){ - if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_ADD)){ + if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_ADD)){ return true; } diff --git a/src/command/defaults/TimingsCommand.php b/src/command/defaults/TimingsCommand.php index 08a8b82aa0..f2540723ea 100644 --- a/src/command/defaults/TimingsCommand.php +++ b/src/command/defaults/TimingsCommand.php @@ -62,7 +62,6 @@ class TimingsCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "timings", KnownTranslationFactory::pocketmine_command_timings_description(), KnownTranslationFactory::pocketmine_command_timings_usage() ); diff --git a/src/command/defaults/TitleCommand.php b/src/command/defaults/TitleCommand.php index ecc4c569cf..836181a92c 100644 --- a/src/command/defaults/TitleCommand.php +++ b/src/command/defaults/TitleCommand.php @@ -35,7 +35,6 @@ class TitleCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "title", KnownTranslationFactory::pocketmine_command_title_description(), KnownTranslationFactory::commands_title_usage() ); @@ -50,7 +49,7 @@ class TitleCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_TITLE_SELF, DefaultPermissionNames::COMMAND_TITLE_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[0], DefaultPermissionNames::COMMAND_TITLE_SELF, DefaultPermissionNames::COMMAND_TITLE_OTHER); if($player === null){ return true; } diff --git a/src/command/defaults/TransferServerCommand.php b/src/command/defaults/TransferServerCommand.php index d0cbd316a9..168656d15f 100644 --- a/src/command/defaults/TransferServerCommand.php +++ b/src/command/defaults/TransferServerCommand.php @@ -34,7 +34,6 @@ class TransferServerCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "transferserver", KnownTranslationFactory::pocketmine_command_transferserver_description(), KnownTranslationFactory::pocketmine_command_transferserver_usage() ); diff --git a/src/command/defaults/VanillaCommand.php b/src/command/defaults/VanillaCommand.php index 9bdd63739b..9efead3791 100644 --- a/src/command/defaults/VanillaCommand.php +++ b/src/command/defaults/VanillaCommand.php @@ -36,7 +36,13 @@ abstract class VanillaCommand extends Command{ public const MAX_COORD = 30000000; public const MIN_COORD = -30000000; - protected function fetchPermittedPlayerTarget(CommandSender $sender, ?string $target, string $selfPermission, string $otherPermission) : ?Player{ + protected function fetchPermittedPlayerTarget( + string $testPermissionContext, + CommandSender $sender, + ?string $target, + string $selfPermission, + string $otherPermission + ) : ?Player{ if($target !== null){ $player = $sender->getServer()->getPlayerByPrefix($target); }elseif($sender instanceof Player){ @@ -49,9 +55,12 @@ abstract class VanillaCommand extends Command{ $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return null; } + //TODO: using loud testPermission here will generate misleading messages + //e.g. if the sender has self permission and tries to use the command on another player, it will give them a + //generic message saying that they don't have permission to use the command, which is not correct if( - ($player === $sender && $this->testPermission($sender, $selfPermission)) || - ($player !== $sender && $this->testPermission($sender, $otherPermission)) + ($player === $sender && $this->testPermission($testPermissionContext, $sender, $selfPermission)) || + ($player !== $sender && $this->testPermission($testPermissionContext, $sender, $otherPermission)) ){ return $player; } diff --git a/src/command/defaults/VersionCommand.php b/src/command/defaults/VersionCommand.php index bafb129d1a..d702da4619 100644 --- a/src/command/defaults/VersionCommand.php +++ b/src/command/defaults/VersionCommand.php @@ -42,10 +42,8 @@ class VersionCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "version", KnownTranslationFactory::pocketmine_command_version_description(), - KnownTranslationFactory::pocketmine_command_version_usage(), - ["ver", "about"] + KnownTranslationFactory::pocketmine_command_version_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_VERSION); } diff --git a/src/command/defaults/WhitelistCommand.php b/src/command/defaults/WhitelistCommand.php index fdf01ff562..b0380abefa 100644 --- a/src/command/defaults/WhitelistCommand.php +++ b/src/command/defaults/WhitelistCommand.php @@ -41,7 +41,6 @@ class WhitelistCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "whitelist", KnownTranslationFactory::pocketmine_command_whitelist_description(), KnownTranslationFactory::commands_whitelist_usage() ); @@ -56,10 +55,14 @@ class WhitelistCommand extends VanillaCommand{ } public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(count($args) === 0){ + throw new InvalidCommandSyntaxException(); + } + $testPermissionCtx = $commandLabel . " " . $args[0]; if(count($args) === 1){ switch(strtolower($args[0])){ case "reload": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_RELOAD)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_RELOAD)){ $server = $sender->getServer(); $server->getWhitelisted()->reload(); if($server->hasWhitelist()){ @@ -70,7 +73,7 @@ class WhitelistCommand extends VanillaCommand{ return true; case "on": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ENABLE)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_ENABLE)){ $server = $sender->getServer(); $server->getConfigGroup()->setConfigBool(ServerProperties::WHITELIST, true); $this->kickNonWhitelistedPlayers($server); @@ -79,14 +82,14 @@ class WhitelistCommand extends VanillaCommand{ return true; case "off": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_DISABLE)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_DISABLE)){ $sender->getServer()->getConfigGroup()->setConfigBool(ServerProperties::WHITELIST, false); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_disabled()); } return true; case "list": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_LIST)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_LIST)){ $entries = $sender->getServer()->getWhitelisted()->getAll(true); sort($entries, SORT_STRING); $result = implode(", ", $entries); @@ -112,14 +115,14 @@ class WhitelistCommand extends VanillaCommand{ } switch(strtolower($args[0])){ case "add": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ADD)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_ADD)){ $sender->getServer()->addWhitelist($args[1]); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_add_success($args[1])); } return true; case "remove": - if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_REMOVE)){ + if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_REMOVE)){ $server = $sender->getServer(); $server->removeWhitelist($args[1]); if(!$server->isWhitelisted($args[1])){ diff --git a/src/command/defaults/XpCommand.php b/src/command/defaults/XpCommand.php index cb03523657..1714e74bcb 100644 --- a/src/command/defaults/XpCommand.php +++ b/src/command/defaults/XpCommand.php @@ -40,7 +40,6 @@ class XpCommand extends VanillaCommand{ public function __construct(){ parent::__construct( - "xp", KnownTranslationFactory::pocketmine_command_xp_description(), KnownTranslationFactory::pocketmine_command_xp_usage() ); @@ -55,7 +54,7 @@ class XpCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $this->fetchPermittedPlayerTarget($sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_XP_SELF, DefaultPermissionNames::COMMAND_XP_OTHER); + $player = $this->fetchPermittedPlayerTarget($commandLabel, $sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_XP_SELF, DefaultPermissionNames::COMMAND_XP_OTHER); if($player === null){ return true; } diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index bea3f81313..5075663ade 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -117,13 +117,14 @@ use pocketmine\world\format\io\GlobalItemDataHandlers; use pocketmine\world\Position; use pocketmine\world\World; use pocketmine\YmlServerProperties; +use function array_filter; use function array_map; +use function array_values; use function base64_encode; use function bin2hex; use function count; use function get_class; use function implode; -use function in_array; use function is_string; use function json_encode; use function ord; @@ -1088,23 +1089,23 @@ class NetworkSession{ public function syncAvailableCommands() : void{ $commandData = []; - foreach($this->server->getCommandMap()->getCommands() as $command){ - if(isset($commandData[$command->getLabel()]) || $command->getLabel() === "help" || !$command->testPermissionSilent($this->player)){ + foreach($this->server->getCommandMap()->getUniqueCommands() as $commandEntry){ + if(!$commandEntry->command->testPermissionSilent($this->player)){ continue; } - $lname = strtolower($command->getLabel()); - $aliases = $command->getAliases(); - $aliasObj = null; - if(count($aliases) > 0){ - if(!in_array($lname, $aliases, true)){ - //work around a client bug which makes the original name not show when aliases are used - $aliases[] = $lname; - } - $aliasObj = new CommandEnum(ucfirst($command->getLabel()) . "Aliases", $aliases); + //the client doesn't like it when we override /help + $aliases = array_values(array_filter($commandEntry->aliases, fn(string $alias) => $alias !== "help" && $alias !== "?")); + if(count($aliases) === 0){ + continue; } + $firstNetworkAlias = $aliases[0]; + //use filtered aliases for command name discovery - this allows /help to still be shown as /pocketmine:help + //on the client without conflicting with the client's built-in /help command + $lname = strtolower($firstNetworkAlias); + $aliasObj = new CommandEnum(ucfirst($firstNetworkAlias) . "Aliases", $aliases); - $description = $command->getDescription(); + $description = $commandEntry->command->getDescription(); $data = new CommandData( $lname, //TODO: commands containing uppercase letters in the name crash 1.9.0 client $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description, @@ -1117,7 +1118,7 @@ class NetworkSession{ chainedSubCommandData: [] ); - $commandData[$command->getLabel()] = $data; + $commandData[] = $data; } $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], [])); diff --git a/src/plugin/PluginBase.php b/src/plugin/PluginBase.php index a32339e84e..89584556ab 100644 --- a/src/plugin/PluginBase.php +++ b/src/plugin/PluginBase.php @@ -34,7 +34,6 @@ use pocketmine\utils\Config; use pocketmine\utils\Utils; use Symfony\Component\Filesystem\Path; use function copy; -use function count; use function dirname; use function file_exists; use function fopen; @@ -146,23 +145,12 @@ abstract class PluginBase implements Plugin, CommandExecutor{ * Registers commands declared in the plugin manifest */ private function registerYamlCommands() : void{ - $pluginCmds = []; - foreach(Utils::stringifyKeys($this->description->getCommands()) as $key => $data){ if(str_contains($key, ":")){ $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->description->getFullName(), ":"))); continue; } - $newCmd = new PluginCommand($key, $this, $this); - if(($description = $data->getDescription()) !== null){ - $newCmd->setDescription($description); - } - - if(($usageMessage = $data->getUsageMessage()) !== null){ - $newCmd->setUsage($usageMessage); - } - $aliasList = []; foreach($data->getAliases() as $alias){ if(str_contains($alias, ":")){ @@ -172,7 +160,13 @@ abstract class PluginBase implements Plugin, CommandExecutor{ $aliasList[] = $alias; } - $newCmd->setAliases($aliasList); + $newCmd = new PluginCommand( + $key, + $this, + $this, + $data->getDescription() ?? "", + $data->getUsageMessage() ?? "" + ); $newCmd->setPermission($data->getPermission()); @@ -180,11 +174,7 @@ abstract class PluginBase implements Plugin, CommandExecutor{ $newCmd->setPermissionMessage($permissionDeniedMessage); } - $pluginCmds[] = $newCmd; - } - - if(count($pluginCmds) > 0){ - $this->server->getCommandMap()->registerAll($this->description->getName(), $pluginCmds); + $this->server->getCommandMap()->register($this->description->getName(), $newCmd, $key, $aliasList); } }