diff --git a/composer.json b/composer.json index 391224a94..b5ffbf9a5 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "pocketmine/callback-validator": "dev-rewrite", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.7.0", - "pocketmine/locale-data": "~2.26.0", + "pocketmine/locale-data": "~2.27.0", "pocketmine/log": "^0.4.0", "pocketmine/math": "dev-major-next as 1.0.0", "pocketmine/nbt": "~1.2.0", diff --git a/composer.lock b/composer.lock index 7cadc08ba..d28231186 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "65b59e25fafa99c7a989d28f89a91c44", + "content-hash": "c4bc1cdab4e8ce569b94bc49ce51d480", "packages": [ { "name": "adhocore/json-comment", @@ -472,16 +472,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.26.0", + "version": "2.27.0", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "f791369ae082fc5cdf0f2b0bd683e611ff7f90f6" + "reference": "83d0d7e5bc53522ca1f10a4720ca1839f5d4d9df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/f791369ae082fc5cdf0f2b0bd683e611ff7f90f6", - "reference": "f791369ae082fc5cdf0f2b0bd683e611ff7f90f6", + "url": "https://api.github.com/repos/pmmp/Language/zipball/83d0d7e5bc53522ca1f10a4720ca1839f5d4d9df", + "reference": "83d0d7e5bc53522ca1f10a4720ca1839f5d4d9df", "shasum": "" }, "type": "library", @@ -489,9 +489,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.26.0" + "source": "https://github.com/pmmp/Language/tree/2.27.0" }, - "time": "2025-10-07T17:26:32+00:00" + "time": "2025-10-10T22:27:39+00:00" }, { "name": "pocketmine/log", diff --git a/src/Server.php b/src/Server.php index 94d6e8d01..0062b025a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -680,11 +680,8 @@ class Server{ * @phpstan-return (Command&PluginOwned)|null */ public function getPluginCommand(string $name){ - if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){ - return $command; - }else{ - return null; - } + $command = $this->commandMap->getCommand($name); + return $command instanceof PluginOwned ? $command : null; } public function getNameBans() : BanList{ diff --git a/src/command/ClosureCommand.php b/src/command/ClosureCommand.php index 289c82853..4242348fd 100644 --- a/src/command/ClosureCommand.php +++ b/src/command/ClosureCommand.php @@ -38,19 +38,19 @@ final class ClosureCommand extends Command{ * @phpstan-param Execute $execute */ public function __construct( + string $namespace, 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($namespace, $name, $description, $usageMessage); $this->setPermissions($permissions); } diff --git a/src/command/Command.php b/src/command/Command.php index 127d1886f..f64bea7a1 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -33,51 +33,36 @@ 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 function strtolower; +use function trim; 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; + private readonly string $namespace; + private readonly string $name; /** @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( + string $namespace, + string $name, + private Translatable|string $description = "", + private Translatable|string|null $usageMessage = null + ){ + if($namespace === ""){ + throw new \InvalidArgumentException("Command namespace cannot be empty (set it to, for example, your plugin's name)"); + } + if($name === ""){ + throw new \InvalidArgumentException("Command name cannot be empty"); + } + $this->namespace = strtolower(trim($namespace)); + //TODO: case handling inconsistency preserved from old code + $this->name = trim($name); } /** @@ -89,10 +74,25 @@ abstract class Command{ */ abstract public function execute(CommandSender $sender, string $commandLabel, array $args); - public function getName() : string{ + final public function getNamespace() : string{ + return $this->namespace; + } + + /** + * Returns the local identifier of the command (without namespace or leading slash). + * This cannot be changed after creation. + */ + final public function getName() : string{ return $this->name; } + /** + * Returns the globally unique ID for the command. This typically looks like namespace:name + */ + final public function getId() : string{ + return "$this->namespace:$this->name"; + } + /** * @return string[] */ @@ -117,12 +117,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 +148,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 +156,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 +168,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 +189,4 @@ abstract class Command{ } } } - - public function __toString() : string{ - return $this->name; - } } diff --git a/src/command/CommandAliasMap.php b/src/command/CommandAliasMap.php new file mode 100644 index 000000000..4d42b74df --- /dev/null +++ b/src/command/CommandAliasMap.php @@ -0,0 +1,171 @@ +> + */ + private array $aliasToCommandMap = []; + + /** + * @var string[][] + * @phpstan-var array> + */ + private array $commandToAliasesMap = []; + + private function mapSingleAlias(string $commandId, string $alias) : bool{ + $existing = $this->aliasToCommandMap[$alias] ?? null; + if($existing !== null){ + if(!is_array($existing)){ + //old command can't use this alias anymore, since it's conflicted + unset($this->commandToAliasesMap[$existing][$alias]); + $existing = [$existing]; + } + $existing[] = $commandId; + $this->aliasToCommandMap[$alias] = $existing; + return false; + } + + $this->commandToAliasesMap[$commandId][$alias] = true; + $this->aliasToCommandMap[$alias] = $commandId; + return true; + } + + public function bindAlias(string $commandId, string $newAlias, bool $override) : void{ + if($override){ + //explicit alias registration overrides everything else, including conflicts + $this->unbindAlias($newAlias); + } + + $this->mapSingleAlias($commandId, $newAlias); + } + + public function unbindAlias(string $alias) : bool{ + $commandIds = $this->aliasToCommandMap[$alias] ?? null; + if($commandIds === null){ + return false; + } + unset($this->aliasToCommandMap[$alias]); + if(!is_array($commandIds)){ + //this should only be set if the alias wasn't conflicted + unset($this->commandToAliasesMap[$commandIds][$alias]); + } + + return true; + } + + public function unbindAliasesForCommand(string $commandId) : void{ + foreach($this->getAliases($commandId) as $alias){ + $aliasMap = $this->aliasToCommandMap[$alias] ?? null; + + if($aliasMap === $commandId){ + unset($this->aliasToCommandMap[$alias]); + }elseif(is_array($aliasMap)){ + //this may leave 1 command remaining - we purposely don't deconflict it here because successive command + //invocations should be predictable. Leave the conflict and let the user override it if they want. + $replacement = array_filter($aliasMap, fn(string $cid) => $cid !== $commandId); + if(count($replacement) === 0){ + unset($this->aliasToCommandMap[$alias]); + }else{ + $this->aliasToCommandMap[$alias] = array_values($replacement); + } + }else{ + throw new \LogicException("Alias map state corrupted"); + } + } + } + + /** + * Returns all (non-conflicted) aliases for the command. + * @return string[] + * @phpstan-return list + */ + public function getAliases(string $commandId) : array{ + return array_keys($this->commandToAliasesMap[$commandId] ?? []); + } + + /** + * Returns the ID of the command bound to the given alias. + * If there are conflicting commands bound, an array of all the bound command IDs will be returned. + * If the alias is not bound, null will be returned. + * + * @return string|string[]|null + * @phpstan-return string|list|null + */ + public function resolveAlias(string $alias) : string|array|null{ + return $this->aliasToCommandMap[$alias] ?? null; + } + + /** + * Returns a list of all the names a command could be invoked by. + * Aliases from this map override the fallback map. + * The command ID is also included, since that can always be used to invoke a command. + * + * @return string[] + * @phpstan-return non-empty-list + */ + public function getMergedAliases(string $commandId, CommandAliasMap $fallbackMap) : array{ + $localAliases = $this->getAliases($commandId); + + foreach($fallbackMap->getAliases($commandId) as $globalAlias){ + $userMappedCommandId = $this->resolveAlias($globalAlias); + if($userMappedCommandId === null){ + //only include if this map doesn't have this alias at all + $localAliases[] = $globalAlias; + } + } + + $localAliases[] = $commandId; + + return $localAliases; + } + + /** + * Returns a preferred alias for the given command ID. Used for displaying errors and help tips. + * For example, the user might not have /help bound, but might have a different alias for it that's more convenient + * than /pocketmine:help. + */ + public function getPreferredAlias(string $commandId, CommandAliasMap $fallbackMap) : string{ + $aliasList = $this->getMergedAliases($commandId, $fallbackMap); + return $aliasList[array_key_first($aliasList)]; + } + + /** + * @return string[]|string[][] + * @phpstan-return array> + */ + public function getAllAliases() : array{ + return $this->aliasToCommandMap; + } +} diff --git a/src/command/CommandMap.php b/src/command/CommandMap.php index a7cf64ca4..43a0fc246 100644 --- a/src/command/CommandMap.php +++ b/src/command/CommandMap.php @@ -24,18 +24,39 @@ declare(strict_types=1); namespace pocketmine\command; interface CommandMap{ - /** - * @param Command[] $commands + * Registering a command with (command(namespace="myplugin", 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 in the alias map. + * 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. + * + * @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(Command $command, array $otherAliases = []) : void; 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 Command|Command[]|null + * @phpstan-return Command|array|null + */ + public function getCommand(string $name, ?CommandAliasMap $senderAliasMap = null) : Command|array|null; + /** + * Returns the global alias map for this command map. + * Aliases in this map will be used as a fallback when user-specific aliases don't give any results. + */ + public function getAliasMap() : CommandAliasMap; } diff --git a/src/command/CommandSender.php b/src/command/CommandSender.php index 5429b6f5e..d04e56526 100644 --- a/src/command/CommandSender.php +++ b/src/command/CommandSender.php @@ -50,4 +50,10 @@ interface CommandSender extends Permissible{ * @phpstan-param positive-int|null $height */ public function setScreenLineHeight(?int $height) : void; + + /** + * Returns the user's local alias map. This may contain special user-specific aliases that differ from the global + * command map's aliases. + */ + public function getCommandAliasMap() : CommandAliasMap; } diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php index b47363397..3af7376aa 100644 --- a/src/command/FormattedCommandAlias.php +++ b/src/command/FormattedCommandAlias.php @@ -50,10 +50,11 @@ class FormattedCommandAlias extends Command{ * @param string[] $formatStrings */ public function __construct( - string $alias, + string $namespace, + string $name, private array $formatStrings ){ - parent::__construct($alias, KnownTranslationFactory::pocketmine_command_userDefined_description()); + parent::__construct($namespace, $name, KnownTranslationFactory::pocketmine_command_userDefined_description()); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -95,18 +96,22 @@ 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()); + //formatted command aliases don't use user-specific aliases since they are globally defined in pocketmine.yml + //using user-specific aliases might break the behaviour + if(($target = $commandMap->getCommand($commandLabel)) instanceof Command){ + + $timings = Timings::getCommandDispatchTimings($target->getId()); $timings->startTiming(); try{ $target->execute($sender, $commandLabel, $commandArgs); }catch(InvalidCommandSyntaxException $e){ - $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage()))); + $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage() ?? "/$commandLabel"))); }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() diff --git a/src/command/PluginCommand.php b/src/command/PluginCommand.php index 55b8df3ed..3d8762db2 100644 --- a/src/command/PluginCommand.php +++ b/src/command/PluginCommand.php @@ -24,17 +24,20 @@ 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 $namespace, 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($namespace, $name, $description, $usageMessage); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -43,13 +46,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 9f5441746..18ae20178 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -27,6 +27,7 @@ use pocketmine\command\defaults\BanCommand; use pocketmine\command\defaults\BanIpCommand; use pocketmine\command\defaults\BanListCommand; use pocketmine\command\defaults\ClearCommand; +use pocketmine\command\defaults\CommandAliasCommand; use pocketmine\command\defaults\DefaultGamemodeCommand; use pocketmine\command\defaults\DeopCommand; use pocketmine\command\defaults\DifficultyCommand; @@ -61,7 +62,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; @@ -72,10 +72,13 @@ use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; +use function array_filter; +use function array_map; use function array_shift; -use function array_values; use function count; use function implode; +use function is_array; +use function is_string; use function str_contains; use function strcasecmp; use function strtolower; @@ -87,121 +90,83 @@ class SimpleCommandMap implements CommandMap{ * @var Command[] * @phpstan-var array */ - protected array $knownCommands = []; + private array $uniqueCommands = []; + + private CommandAliasMap $aliasMap; public function __construct(private Server $server){ + $this->aliasMap = new CommandAliasMap(); $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(new BanCommand($pmPrefix, "ban")); + $this->register(new BanIpCommand($pmPrefix, "ban-ip")); + $this->register(new BanListCommand($pmPrefix, "banlist")); + $this->register(new ClearCommand($pmPrefix, "clear")); + $this->register(new CommandAliasCommand($pmPrefix, "cmdalias")); + $this->register(new DefaultGamemodeCommand($pmPrefix, "defaultgamemode")); + $this->register(new DeopCommand($pmPrefix, "deop")); + $this->register(new DifficultyCommand($pmPrefix, "difficulty")); + $this->register(new DumpMemoryCommand($pmPrefix, "dumpmemory")); + $this->register(new EffectCommand($pmPrefix, "effect")); + $this->register(new EnchantCommand($pmPrefix, "enchant")); + $this->register(new GamemodeCommand($pmPrefix, "gamemode")); + $this->register(new GarbageCollectorCommand($pmPrefix, "gc")); + $this->register(new GiveCommand($pmPrefix, "give")); + $this->register(new HelpCommand($pmPrefix, "help"), ["?"]); + $this->register(new KickCommand($pmPrefix, "kick")); + $this->register(new KillCommand($pmPrefix, "kill"), ["suicide"]); + $this->register(new ListCommand($pmPrefix, "list")); + $this->register(new MeCommand($pmPrefix, "me")); + $this->register(new OpCommand($pmPrefix, "op")); + $this->register(new PardonCommand($pmPrefix, "pardon"), ["unban"]); + $this->register(new PardonIpCommand($pmPrefix, "pardon-ip"), ["unban-ip"]); + $this->register(new ParticleCommand($pmPrefix, "particle")); + $this->register(new PluginsCommand($pmPrefix, "plugins"), ["pl"]); + $this->register(new SaveCommand($pmPrefix, "save-all")); + $this->register(new SaveOffCommand($pmPrefix, "save-off")); + $this->register(new SaveOnCommand($pmPrefix, "save-on")); + $this->register(new SayCommand($pmPrefix, "say")); + $this->register(new SeedCommand($pmPrefix, "seed")); + $this->register(new SetWorldSpawnCommand($pmPrefix, "setworldspawn")); + $this->register(new SpawnpointCommand($pmPrefix, "spawnpoint")); + $this->register(new StatusCommand($pmPrefix, "status")); + $this->register(new StopCommand($pmPrefix, "stop")); + $this->register(new TeleportCommand($pmPrefix, "tp"), ["teleport"]); + $this->register(new TellCommand($pmPrefix, "tell"), ["w", "msg"]); + $this->register(new TimeCommand($pmPrefix, "time")); + $this->register(new TimingsCommand($pmPrefix, "timings")); + $this->register(new TitleCommand($pmPrefix, "title")); + $this->register(new TransferServerCommand($pmPrefix, "transferserver")); + $this->register(new VersionCommand($pmPrefix, "version"), ["ver", "about"]); + $this->register(new WhitelistCommand($pmPrefix, "whitelist")); + $this->register(new XpCommand($pmPrefix, "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(Command $command, array $otherAliases = []) : void{ if(count($command->getPermissions()) === 0){ throw new \InvalidArgumentException("Commands must have a permission set"); } - if($label === null){ - $label = $command->getLabel(); - } - $label = trim($label); - $fallbackPrefix = strtolower(trim($fallbackPrefix)); - - $registered = $this->registerAlias($command, false, $fallbackPrefix, $label); - - $aliases = $command->getAliases(); - foreach($aliases as $index => $alias){ - if(!$this->registerAlias($command, true, $fallbackPrefix, $alias)){ - unset($aliases[$index]); - } - } - $command->setAliases(array_values($aliases)); - - if(!$registered){ - $command->setLabel($fallbackPrefix . ":" . $label); + $commandId = $command->getId(); + if(isset($this->uniqueCommands[$commandId])){ + throw new \InvalidArgumentException("A command with ID $commandId has already been registered"); } - $command->register($this); + $preferredAlias = trim($command->getName()); + $this->aliasMap->bindAlias($commandId, $preferredAlias, override: false); + foreach($otherAliases as $alias){ + $this->aliasMap->bindAlias($commandId, $alias, override: false); + } - return $registered; + $this->uniqueCommands[$commandId] = $command; } public function unregister(Command $command) : bool{ - foreach(Utils::promoteKeys($this->knownCommands) as $lbl => $cmd){ - if($cmd === $command){ - unset($this->knownCommands[$lbl]); - } - } - - $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; + unset($this->uniqueCommands[$command->getId()]); + $this->aliasMap->unbindAliasesForCommand($command->getId()); return true; } @@ -210,44 +175,98 @@ 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->getCommand($sentCommandLabel, $sender->getCommandAliasMap())) !== null){ + if(is_array($target)){ + self::handleConflicted($sender, $sentCommandLabel, $target, $this->aliasMap); + return true; + } + $timings = Timings::getCommandDispatchTimings($target->getId()); $timings->startTiming(); try{ - if($target->testPermission($sender)){ + if($target->testPermission($sentCommandLabel, $sender)){ $target->execute($sender, $sentCommandLabel, $args); } }catch(InvalidCommandSyntaxException $e){ - $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage()))); + //TODO: localised command message should use user-provided alias, it shouldn't be hard-baked into the language strings + $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage() ?? "/$sentCommandLabel"))); }finally{ $timings->stopTiming(); } return true; } - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->prefix(TextFormat::RED)); + //Don't love hardcoding the command ID here, but it seems like the only way for now + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound( + $sentCommandLabel ?? "", + "/" . $sender->getCommandAliasMap()->getPreferredAlias("pocketmine:help", $this->aliasMap) + )->prefix(TextFormat::RED)); return false; } - public function clearCommands() : void{ - foreach($this->knownCommands as $command){ - $command->unregister($this); + /** + * TODO: probably need to find a better place to put this + * @internal + * @param Command[] $conflictedEntries + * @phpstan-param array $conflictedEntries + */ + public static function handleConflicted(CommandSender $sender, string $alias, array $conflictedEntries, CommandAliasMap $fallbackAliasMap) : void{ + $candidates = []; + $userAliasMap = $sender->getCommandAliasMap(); + foreach($conflictedEntries as $c){ + if($c->testPermissionSilent($sender)){ + $candidates[] = "/" . $c->getId(); + } } - $this->knownCommands = []; + if(count($candidates) > 0){ + //there might only be 1 permissible command here, but we still don't auto-select in this case + //because it might cause surprising behaviour if the user's permissions change between command + //invocations. Better to force them to use an unambiguous alias in all cases. + $candidateNames = implode(", ", $candidates); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_error_aliasConflict("/$alias", $candidateNames)->prefix(TextFormat::RED)); + //Don't love hardcoding the command ID here, but it seems like the only way for now + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_error_aliasConflictTip( + "/" . $userAliasMap->getPreferredAlias("pocketmine:cmdalias", $fallbackAliasMap) + )->prefix(TextFormat::RED)); + }else{ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($alias)->prefix(TextFormat::RED)); + } + } + + public function clearCommands() : void{ + $this->aliasMap = new CommandAliasMap(); + $this->uniqueCommands = []; $this->setDefaultCommands(); } - public function getCommand(string $name) : ?Command{ - return $this->knownCommands[$name] ?? null; + public function getCommand(string $name, ?CommandAliasMap $senderAliasMap = null) : Command|array|null{ + if(isset($this->uniqueCommands[$name])){ //direct command ID reference + return $this->uniqueCommands[$name]; + } + $commandId = $senderAliasMap?->resolveAlias($name) ?? $this->aliasMap->resolveAlias($name); + if(is_string($commandId)){ + return $this->uniqueCommands[$commandId] ?? null; + } + if(is_array($commandId)){ + //the user's command map may refer to commands that are no longer registered, so we need to filter these + //from the result set + //we don't deconflict if there's only 1 command left because we don't want re-running a command to randomly + //have a different result if the global command map was modified - the user can explicitly rebind the + //alias in this case + return array_filter(array_map( + fn(string $c) => $this->uniqueCommands[$c] ?? null, + $commandId + ), is_object(...)); + } + return null; } /** * @return Command[] * @phpstan-return array */ - public function getCommands() : array{ - return $this->knownCommands; + public function getUniqueCommands() : array{ + return $this->uniqueCommands; } public function registerServerAliases() : void{ @@ -268,7 +287,7 @@ class SimpleCommandMap implements CommandMap{ $commandName = array_shift($args) ?? ""; $command = $this->getCommand($commandName); - if($command === null){ + if(!$command instanceof Command){ $bad[] = $commandString; }elseif(strcasecmp($commandName, $alias) === 0){ $recursive[] = $commandString; @@ -290,11 +309,15 @@ class SimpleCommandMap implements CommandMap{ //These registered commands have absolute priority $lowerAlias = strtolower($alias); if(count($targets) > 0){ - $this->knownCommands[$lowerAlias] = new FormattedCommandAlias($lowerAlias, $targets); + $aliasInstance = new FormattedCommandAlias("pocketmine-config-defined", $lowerAlias, $targets); + $this->aliasMap->bindAlias($aliasInstance->getId(), $lowerAlias, override: true); + $this->uniqueCommands[$aliasInstance->getId()] = $aliasInstance; }else{ - unset($this->knownCommands[$lowerAlias]); + //no targets blackholes the alias - this allows config to delete unwanted aliases + $this->aliasMap->unbindAlias($lowerAlias); } - } } + + public function getAliasMap() : CommandAliasMap{ return $this->aliasMap; } } diff --git a/src/command/defaults/BanCommand.php b/src/command/defaults/BanCommand.php index adbdb6d3a..a2587b092 100644 --- a/src/command/defaults/BanCommand.php +++ b/src/command/defaults/BanCommand.php @@ -35,9 +35,10 @@ use function implode; class BanCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "ban", + $namespace, + $name, 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 a23477e0d..5f44cc276 100644 --- a/src/command/defaults/BanIpCommand.php +++ b/src/command/defaults/BanIpCommand.php @@ -36,9 +36,10 @@ use function inet_pton; class BanIpCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "ban-ip", + $namespace, + $name, 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 95c214969..1ece9dced 100644 --- a/src/command/defaults/BanListCommand.php +++ b/src/command/defaults/BanListCommand.php @@ -37,9 +37,10 @@ use const SORT_STRING; class BanListCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "banlist", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_banlist_description(), KnownTranslationFactory::commands_banlist_usage() ); diff --git a/src/command/defaults/ClearCommand.php b/src/command/defaults/ClearCommand.php index f6f491fbf..5388e275c 100644 --- a/src/command/defaults/ClearCommand.php +++ b/src/command/defaults/ClearCommand.php @@ -39,9 +39,10 @@ use function min; class ClearCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "clear", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_clear_description(), KnownTranslationFactory::pocketmine_command_clear_usage() ); @@ -53,7 +54,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/CommandAliasCommand.php b/src/command/defaults/CommandAliasCommand.php new file mode 100644 index 000000000..5cc9a1d0e --- /dev/null +++ b/src/command/defaults/CommandAliasCommand.php @@ -0,0 +1,161 @@ + OR /cmdalias [global] delete " + ); + $this->setPermissions([self::GLOBAL_PERM, self::SELF_PERM, self::LIST_PERM]); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(count($args) === 0){ + throw new InvalidCommandSyntaxException(); + } + $parsedArgs = $args; + $commandMap = $sender->getServer()->getCommandMap(); + if($parsedArgs[0] === "global"){ + $editPermission = self::GLOBAL_PERM; + $permissionCtx = $commandLabel . " global"; + array_shift($parsedArgs); + $aliasMap = $commandMap->getAliasMap(); + $messageScope = fn(Translatable $t) => KnownTranslationFactory::pocketmine_command_cmdalias_template($t, KnownTranslationFactory::pocketmine_command_cmdalias_scope_global()); + $auditLog = true; + }else{ + $editPermission = self::SELF_PERM; + $permissionCtx = $commandLabel; + $aliasMap = $sender->getCommandAliasMap(); + $messageScope = fn(Translatable $t) => KnownTranslationFactory::pocketmine_command_cmdalias_template($t, KnownTranslationFactory::pocketmine_command_cmdalias_scope_userSpecific()); + $auditLog = false; + } + $operation = array_shift($parsedArgs); + + if($operation === "create"){ + if(count($parsedArgs) !== 2){ + throw new InvalidCommandSyntaxException(); + } + if(!$this->testPermission($permissionCtx, $sender, $editPermission)){ + return true; + } + + [$alias, $target] = $parsedArgs; + $command = $commandMap->getCommand($target, $sender->getCommandAliasMap()); + if($command === null){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound( + "/$target", + "/" . $sender->getCommandAliasMap()->getPreferredAlias("pocketmine:help", $sender->getServer()->getCommandMap()->getAliasMap()) + )->prefix(TextFormat::RED)); + return true; + } + if(is_array($command)){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_error_aliasConflict("/$target", implode(", ", array_map(fn(Command $c) => "/" . $c->getId(), $command)))->prefix(TextFormat::RED)); + return true; + } + $aliasMap->bindAlias($command->getId(), $alias, override: true); + $message = $messageScope(KnownTranslationFactory::pocketmine_command_cmdalias_create_success("/$alias", "/" . $command->getId())); + if($auditLog){ + Command::broadcastCommandMessage($sender, $message); + }else{ + $sender->sendMessage($message); + } + return true; + } + if($operation === "delete"){ + if(count($parsedArgs) !== 1){ + throw new InvalidCommandSyntaxException(); + } + if(!$this->testPermission($permissionCtx, $sender, $editPermission)){ + return true; + } + + $alias = $parsedArgs[0]; + + if($aliasMap->unbindAlias($alias)){ + $message = $messageScope(KnownTranslationFactory::pocketmine_command_cmdalias_delete_success("/$alias")); + if($auditLog){ + Command::broadcastCommandMessage($sender, $message); + }else{ + $sender->sendMessage($message); + } + }else{ + $sender->sendMessage($messageScope(KnownTranslationFactory::pocketmine_command_cmdalias_delete_notFound("/$alias"))->prefix(TextFormat::RED)); + } + return true; + } + if($operation === "list"){ + if(count($parsedArgs) !== 0){ + throw new InvalidCommandSyntaxException(); + } + if(!$this->testPermission($permissionCtx, $sender, self::LIST_PERM)){ + return true; + } + $allAliases = $aliasMap->getAllAliases(); + if(count($allAliases) === 0){ + $sender->sendMessage($messageScope(KnownTranslationFactory::pocketmine_command_cmdalias_list_noneSet())->prefix(TextFormat::RED)); + return true; + } + ksort($allAliases); + foreach(Utils::promoteKeys($allAliases) as $alias => $commandIds){ + if(is_array($commandIds)){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_cmdalias_list_conflicted( + TextFormat::RED . "/$alias" . TextFormat::RESET, + implode(", ", array_map(fn(string $c) => TextFormat::RED . "/$c" . TextFormat::RESET, $commandIds)) + )); + }else{ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_cmdalias_list_normal( + TextFormat::DARK_GREEN . "/$alias" . TextFormat::RESET, + TextFormat::DARK_GREEN . "/$commandIds" . TextFormat::RESET + )); + } + } + return true; + } + + throw new InvalidCommandSyntaxException(); + } +} diff --git a/src/command/defaults/DefaultGamemodeCommand.php b/src/command/defaults/DefaultGamemodeCommand.php index d3030eb27..3e3797a4a 100644 --- a/src/command/defaults/DefaultGamemodeCommand.php +++ b/src/command/defaults/DefaultGamemodeCommand.php @@ -33,9 +33,10 @@ use function count; class DefaultGamemodeCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "defaultgamemode", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_defaultgamemode_description(), KnownTranslationFactory::commands_defaultgamemode_usage() ); diff --git a/src/command/defaults/DeopCommand.php b/src/command/defaults/DeopCommand.php index 4167012f7..4ec9dd91c 100644 --- a/src/command/defaults/DeopCommand.php +++ b/src/command/defaults/DeopCommand.php @@ -35,9 +35,10 @@ use function count; class DeopCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "deop", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_deop_description(), KnownTranslationFactory::commands_deop_usage() ); diff --git a/src/command/defaults/DifficultyCommand.php b/src/command/defaults/DifficultyCommand.php index dee75e025..9beb2f4a6 100644 --- a/src/command/defaults/DifficultyCommand.php +++ b/src/command/defaults/DifficultyCommand.php @@ -34,9 +34,10 @@ use function count; class DifficultyCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "difficulty", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_difficulty_description(), KnownTranslationFactory::commands_difficulty_usage() ); diff --git a/src/command/defaults/DumpMemoryCommand.php b/src/command/defaults/DumpMemoryCommand.php index 08dc0deba..9045a17cc 100644 --- a/src/command/defaults/DumpMemoryCommand.php +++ b/src/command/defaults/DumpMemoryCommand.php @@ -31,9 +31,10 @@ use function date; class DumpMemoryCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "dumpmemory", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_dumpmemory_description(), "/dumpmemory [path]" ); diff --git a/src/command/defaults/EffectCommand.php b/src/command/defaults/EffectCommand.php index 938323222..6127b8627 100644 --- a/src/command/defaults/EffectCommand.php +++ b/src/command/defaults/EffectCommand.php @@ -36,9 +36,10 @@ use function strtolower; class EffectCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "effect", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_effect_description(), KnownTranslationFactory::commands_effect_usage() ); @@ -53,7 +54,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 9160eef14..428e78826 100644 --- a/src/command/defaults/EnchantCommand.php +++ b/src/command/defaults/EnchantCommand.php @@ -34,9 +34,10 @@ use function count; class EnchantCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "enchant", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_enchant_description(), KnownTranslationFactory::commands_enchant_usage() ); @@ -51,7 +52,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 666626a69..a7afbefb9 100644 --- a/src/command/defaults/GamemodeCommand.php +++ b/src/command/defaults/GamemodeCommand.php @@ -33,9 +33,10 @@ use function count; class GamemodeCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "gamemode", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_gamemode_description(), KnownTranslationFactory::commands_gamemode_usage() ); @@ -56,7 +57,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 7bd64666f..d29fbaa58 100644 --- a/src/command/defaults/GarbageCollectorCommand.php +++ b/src/command/defaults/GarbageCollectorCommand.php @@ -34,9 +34,10 @@ use function round; class GarbageCollectorCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "gc", + $namespace, + $name, 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 72d33688b..a711245a4 100644 --- a/src/command/defaults/GiveCommand.php +++ b/src/command/defaults/GiveCommand.php @@ -41,9 +41,10 @@ use function implode; class GiveCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "give", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_give_description(), KnownTranslationFactory::pocketmine_command_give_usage() ); @@ -58,7 +59,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 054585455..818fdb9ea 100644 --- a/src/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -25,15 +25,20 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; +use pocketmine\command\SimpleCommandMap; use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\Translatable; use pocketmine\permission\DefaultPermissionNames; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\TextFormat; +use pocketmine\utils\Utils; use function array_chunk; +use function array_key_first; use function array_pop; use function count; use function explode; use function implode; +use function is_array; use function is_numeric; use function ksort; use function min; @@ -45,12 +50,12 @@ use const SORT_NATURAL; class HelpCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "help", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_help_description(), - KnownTranslationFactory::commands_help_usage(), - ["?"] + KnownTranslationFactory::commands_help_usage() ); $this->setPermission(DefaultPermissionNames::COMMAND_HELP); } @@ -72,15 +77,23 @@ 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(); + $userAliasMap = $sender->getCommandAliasMap(); if($commandName === ""){ $commands = []; - foreach($sender->getServer()->getCommandMap()->getCommands() as $command){ + foreach($commandMap->getUniqueCommands() as $command){ if($command->testPermissionSilent($sender)){ - $commands[$command->getLabel()] = $command; + $userAliases = $userAliasMap->getMergedAliases($command->getId(), $commandMap->getAliasMap()); + $preferredAlias = $userAliases[array_key_first($userAliases)]; + if(isset($commands[$preferredAlias])){ + throw new AssumptionFailedError("Something weird happened during user/global alias resolving"); + } + $commands[$preferredAlias] = $command; } } ksort($commands, SORT_NATURAL | SORT_FLAG_CASE); - $commands = array_chunk($commands, $pageHeight); + $commands = array_chunk($commands, $pageHeight, preserve_keys: true); $pageNumber = min(count($commands), $pageNumber); if($pageNumber < 1){ $pageNumber = 1; @@ -88,31 +101,35 @@ 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){ + foreach(Utils::promoteKeys($commands[$pageNumber - 1]) as $preferredAlias => $command){ $description = $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 . "/$preferredAlias: " . TextFormat::RESET . $descriptionString); } } return true; }else{ - if(($cmd = $sender->getServer()->getCommandMap()->getCommand(strtolower($commandName))) instanceof Command){ - if($cmd->testPermissionSilent($sender)){ + if(($command = $commandMap->getCommand(strtolower($commandName), $userAliasMap)) !== null){ + if(is_array($command)){ + SimpleCommandMap::handleConflicted($sender, $commandName, $command, $commandMap->getAliasMap()); + return true; + } + if($command->testPermissionSilent($sender)){ $lang = $sender->getLanguage(); - $description = $cmd->getDescription(); + $description = $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($commandName) ->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 = $command->getUsage() ?? "/$commandName"; $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 = $userAliasMap->getMergedAliases($command->getId(), $commandMap->getAliasMap()); sort($aliases, SORT_NATURAL); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) ->prefix(TextFormat::GOLD)); @@ -120,7 +137,7 @@ class HelpCommand extends VanillaCommand{ return true; } } - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/" . $userAliasMap->getPreferredAlias("pocketmine:help", $commandMap->getAliasMap()))->prefix(TextFormat::RED)); return true; } diff --git a/src/command/defaults/KickCommand.php b/src/command/defaults/KickCommand.php index 3d63e8bb3..01a1bbbfc 100644 --- a/src/command/defaults/KickCommand.php +++ b/src/command/defaults/KickCommand.php @@ -37,9 +37,10 @@ use function trim; class KickCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "kick", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_kick_description(), KnownTranslationFactory::commands_kick_usage() ); diff --git a/src/command/defaults/KillCommand.php b/src/command/defaults/KillCommand.php index fbc743690..91d390221 100644 --- a/src/command/defaults/KillCommand.php +++ b/src/command/defaults/KillCommand.php @@ -33,12 +33,12 @@ use function count; class KillCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "kill", + $namespace, + $name, 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 +48,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 0264a2ef4..c797be7c5 100644 --- a/src/command/defaults/ListCommand.php +++ b/src/command/defaults/ListCommand.php @@ -36,9 +36,10 @@ use const SORT_STRING; class ListCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "list", + $namespace, + $name, 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 a6708840c..7d3cff521 100644 --- a/src/command/defaults/MeCommand.php +++ b/src/command/defaults/MeCommand.php @@ -34,9 +34,10 @@ use function implode; class MeCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "me", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_me_description(), KnownTranslationFactory::commands_me_usage() ); diff --git a/src/command/defaults/OpCommand.php b/src/command/defaults/OpCommand.php index bcd119351..43e38de00 100644 --- a/src/command/defaults/OpCommand.php +++ b/src/command/defaults/OpCommand.php @@ -35,9 +35,10 @@ use function count; class OpCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "op", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_op_description(), KnownTranslationFactory::commands_op_usage() ); diff --git a/src/command/defaults/PardonCommand.php b/src/command/defaults/PardonCommand.php index 425b9e69f..6f17c27d8 100644 --- a/src/command/defaults/PardonCommand.php +++ b/src/command/defaults/PardonCommand.php @@ -32,12 +32,12 @@ use function count; class PardonCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "pardon", + $namespace, + $name, 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 65bed221b..7fc850195 100644 --- a/src/command/defaults/PardonIpCommand.php +++ b/src/command/defaults/PardonIpCommand.php @@ -33,12 +33,12 @@ use function inet_pton; class PardonIpCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "pardon-ip", + $namespace, + $name, 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 4867e3eb5..e0ec57686 100644 --- a/src/command/defaults/ParticleCommand.php +++ b/src/command/defaults/ParticleCommand.php @@ -74,9 +74,10 @@ use function strtolower; class ParticleCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "particle", + $namespace, + $name, 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 d68fcbb01..678ed9518 100644 --- a/src/command/defaults/PluginsCommand.php +++ b/src/command/defaults/PluginsCommand.php @@ -36,12 +36,12 @@ use const SORT_STRING; class PluginsCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "plugins", + $namespace, + $name, 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 4e406e6a3..fcc046b16 100644 --- a/src/command/defaults/SaveCommand.php +++ b/src/command/defaults/SaveCommand.php @@ -32,9 +32,10 @@ use function round; class SaveCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "save-all", + $namespace, + $name, 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 73b8ca151..fd4871a4f 100644 --- a/src/command/defaults/SaveOffCommand.php +++ b/src/command/defaults/SaveOffCommand.php @@ -30,9 +30,10 @@ use pocketmine\permission\DefaultPermissionNames; class SaveOffCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "save-off", + $namespace, + $name, 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 f0a324aeb..17ed8f49c 100644 --- a/src/command/defaults/SaveOnCommand.php +++ b/src/command/defaults/SaveOnCommand.php @@ -30,9 +30,10 @@ use pocketmine\permission\DefaultPermissionNames; class SaveOnCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "save-on", + $namespace, + $name, 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 5c3203b5f..c421b29ca 100644 --- a/src/command/defaults/SayCommand.php +++ b/src/command/defaults/SayCommand.php @@ -35,9 +35,10 @@ use function implode; class SayCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "say", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_say_description(), KnownTranslationFactory::commands_say_usage() ); diff --git a/src/command/defaults/SeedCommand.php b/src/command/defaults/SeedCommand.php index 6b426bd45..8cf71f2fb 100644 --- a/src/command/defaults/SeedCommand.php +++ b/src/command/defaults/SeedCommand.php @@ -30,9 +30,10 @@ use pocketmine\player\Player; class SeedCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "seed", + $namespace, + $name, 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 f251ab8a4..4255ac465 100644 --- a/src/command/defaults/SetWorldSpawnCommand.php +++ b/src/command/defaults/SetWorldSpawnCommand.php @@ -36,9 +36,10 @@ use function count; class SetWorldSpawnCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "setworldspawn", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_setworldspawn_description(), KnownTranslationFactory::commands_setworldspawn_usage() ); diff --git a/src/command/defaults/SpawnpointCommand.php b/src/command/defaults/SpawnpointCommand.php index 614a749d5..c544083f4 100644 --- a/src/command/defaults/SpawnpointCommand.php +++ b/src/command/defaults/SpawnpointCommand.php @@ -36,9 +36,10 @@ use function round; class SpawnpointCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "spawnpoint", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_spawnpoint_description(), KnownTranslationFactory::commands_spawnpoint_usage() ); @@ -49,7 +50,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 033a2f260..b8b8d499e 100644 --- a/src/command/defaults/StatusCommand.php +++ b/src/command/defaults/StatusCommand.php @@ -36,9 +36,10 @@ use function round; class StatusCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "status", + $namespace, + $name, 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 bc4eef91c..bf017bf91 100644 --- a/src/command/defaults/StopCommand.php +++ b/src/command/defaults/StopCommand.php @@ -30,9 +30,10 @@ use pocketmine\permission\DefaultPermissionNames; class StopCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "stop", + $namespace, + $name, 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 b506364bf..ba9b49dca 100644 --- a/src/command/defaults/TeleportCommand.php +++ b/src/command/defaults/TeleportCommand.php @@ -39,12 +39,12 @@ use function round; class TeleportCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "tp", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_tp_description(), - KnownTranslationFactory::commands_tp_usage(), - ["teleport"] + KnownTranslationFactory::commands_tp_usage() ); $this->setPermissions([ DefaultPermissionNames::COMMAND_TELEPORT_SELF, @@ -77,7 +77,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 713023382..93b99181f 100644 --- a/src/command/defaults/TellCommand.php +++ b/src/command/defaults/TellCommand.php @@ -36,12 +36,12 @@ use function implode; class TellCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "tell", + $namespace, + $name, 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 8e937b21b..3565450a3 100644 --- a/src/command/defaults/TimeCommand.php +++ b/src/command/defaults/TimeCommand.php @@ -34,9 +34,10 @@ use function count; class TimeCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "time", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_time_description(), KnownTranslationFactory::pocketmine_command_time_usage() ); @@ -53,9 +54,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 +66,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 +75,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 +92,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 +125,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 ffbc08491..2498d6203 100644 --- a/src/command/defaults/TimingsCommand.php +++ b/src/command/defaults/TimingsCommand.php @@ -60,9 +60,10 @@ use const PHP_EOL; class TimingsCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "timings", + $namespace, + $name, 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 ecc4c569c..0297b55be 100644 --- a/src/command/defaults/TitleCommand.php +++ b/src/command/defaults/TitleCommand.php @@ -33,9 +33,10 @@ use function implode; class TitleCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "title", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_title_description(), KnownTranslationFactory::commands_title_usage() ); @@ -50,7 +51,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 d0cbd316a..7a36fd2a3 100644 --- a/src/command/defaults/TransferServerCommand.php +++ b/src/command/defaults/TransferServerCommand.php @@ -32,9 +32,10 @@ use function count; class TransferServerCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "transferserver", + $namespace, + $name, 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 9bdd63739..9efead379 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 bafb129d1..1c306f0ea 100644 --- a/src/command/defaults/VersionCommand.php +++ b/src/command/defaults/VersionCommand.php @@ -40,12 +40,12 @@ use const PHP_VERSION; class VersionCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "version", + $namespace, + $name, 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 fdf01ff56..d18a2f1a5 100644 --- a/src/command/defaults/WhitelistCommand.php +++ b/src/command/defaults/WhitelistCommand.php @@ -39,9 +39,10 @@ use const SORT_STRING; class WhitelistCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "whitelist", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_whitelist_description(), KnownTranslationFactory::commands_whitelist_usage() ); @@ -56,10 +57,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 +75,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 +84,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 +117,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 cb0352365..33ac7fab3 100644 --- a/src/command/defaults/XpCommand.php +++ b/src/command/defaults/XpCommand.php @@ -38,9 +38,10 @@ use function substr; class XpCommand extends VanillaCommand{ - public function __construct(){ + public function __construct(string $namespace, string $name){ parent::__construct( - "xp", + $namespace, + $name, KnownTranslationFactory::pocketmine_command_xp_description(), KnownTranslationFactory::pocketmine_command_xp_usage() ); @@ -55,7 +56,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/console/ConsoleCommandSender.php b/src/console/ConsoleCommandSender.php index a0c1c5200..b9f587e42 100644 --- a/src/console/ConsoleCommandSender.php +++ b/src/console/ConsoleCommandSender.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\console; +use pocketmine\command\CommandAliasMap; use pocketmine\command\CommandSender; use pocketmine\lang\Language; use pocketmine\lang\Translatable; @@ -42,11 +43,14 @@ class ConsoleCommandSender implements CommandSender{ /** @phpstan-var positive-int|null */ protected ?int $lineHeight = null; + private CommandAliasMap $commandAliasMap; + public function __construct( private Server $server, private Language $language ){ $this->perm = new PermissibleBase([DefaultPermissions::ROOT_CONSOLE => true]); + $this->commandAliasMap = new CommandAliasMap(); } public function getServer() : Server{ @@ -81,4 +85,6 @@ class ConsoleCommandSender implements CommandSender{ } $this->lineHeight = $height; } + + public function getCommandAliasMap() : CommandAliasMap{ return $this->commandAliasMap; } } diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php index cfd8d378e..fdbfc4676 100644 --- a/src/lang/KnownTranslationFactory.php +++ b/src/lang/KnownTranslationFactory.php @@ -1535,6 +1535,62 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CLEAR_USAGE, []); } + public static function pocketmine_command_cmdalias_create_success(Translatable|string $alias, Translatable|string $target) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_CREATE_SUCCESS, [ + "alias" => $alias, + "target" => $target, + ]); + } + + public static function pocketmine_command_cmdalias_delete_notFound(Translatable|string $alias) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_DELETE_NOTFOUND, [ + "alias" => $alias, + ]); + } + + public static function pocketmine_command_cmdalias_delete_success(Translatable|string $alias) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_DELETE_SUCCESS, [ + "alias" => $alias, + ]); + } + + public static function pocketmine_command_cmdalias_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_DESCRIPTION, []); + } + + public static function pocketmine_command_cmdalias_list_conflicted(Translatable|string $alias, Translatable|string $commandIds) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_LIST_CONFLICTED, [ + "alias" => $alias, + "commandIds" => $commandIds, + ]); + } + + public static function pocketmine_command_cmdalias_list_noneSet() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_LIST_NONESET, []); + } + + public static function pocketmine_command_cmdalias_list_normal(Translatable|string $alias, Translatable|string $commandId) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_LIST_NORMAL, [ + "alias" => $alias, + "commandId" => $commandId, + ]); + } + + public static function pocketmine_command_cmdalias_scope_global() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_SCOPE_GLOBAL, []); + } + + public static function pocketmine_command_cmdalias_scope_userSpecific() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_SCOPE_USERSPECIFIC, []); + } + + public static function pocketmine_command_cmdalias_template(Translatable|string $message, Translatable|string $scope) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CMDALIAS_TEMPLATE, [ + "message" => $message, + "scope" => $scope, + ]); + } + public static function pocketmine_command_defaultgamemode_description() : Translatable{ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DEFAULTGAMEMODE_DESCRIPTION, []); } @@ -1559,6 +1615,19 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ENCHANT_DESCRIPTION, []); } + public static function pocketmine_command_error_aliasConflict(Translatable|string $alias, Translatable|string $commandIdList) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ERROR_ALIASCONFLICT, [ + "alias" => $alias, + "commandIdList" => $commandIdList, + ]); + } + + public static function pocketmine_command_error_aliasConflictTip(Translatable|string $cmdAliasCommand) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ERROR_ALIASCONFLICTTIP, [ + "cmdAliasCommand" => $cmdAliasCommand, + ]); + } + public static function pocketmine_command_error_permission(Translatable|string $commandName) : Translatable{ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ERROR_PERMISSION, [ "commandName" => $commandName, @@ -2227,6 +2296,18 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CLEAR_SELF, []); } + public static function pocketmine_permission_command_cmdalias_edit_global() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CMDALIAS_EDIT_GLOBAL, []); + } + + public static function pocketmine_permission_command_cmdalias_edit_self() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CMDALIAS_EDIT_SELF, []); + } + + public static function pocketmine_permission_command_cmdalias_list() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CMDALIAS_LIST, []); + } + public static function pocketmine_permission_command_defaultgamemode() : Translatable{ return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_DEFAULTGAMEMODE, []); } diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php index d0e2c1a68..598edc501 100644 --- a/src/lang/KnownTranslationKeys.php +++ b/src/lang/KnownTranslationKeys.php @@ -347,12 +347,24 @@ final class KnownTranslationKeys{ public const POCKETMINE_COMMAND_BANLIST_DESCRIPTION = "pocketmine.command.banlist.description"; public const POCKETMINE_COMMAND_CLEAR_DESCRIPTION = "pocketmine.command.clear.description"; public const POCKETMINE_COMMAND_CLEAR_USAGE = "pocketmine.command.clear.usage"; + public const POCKETMINE_COMMAND_CMDALIAS_CREATE_SUCCESS = "pocketmine.command.cmdalias.create.success"; + public const POCKETMINE_COMMAND_CMDALIAS_DELETE_NOTFOUND = "pocketmine.command.cmdalias.delete.notFound"; + public const POCKETMINE_COMMAND_CMDALIAS_DELETE_SUCCESS = "pocketmine.command.cmdalias.delete.success"; + public const POCKETMINE_COMMAND_CMDALIAS_DESCRIPTION = "pocketmine.command.cmdalias.description"; + public const POCKETMINE_COMMAND_CMDALIAS_LIST_CONFLICTED = "pocketmine.command.cmdalias.list.conflicted"; + public const POCKETMINE_COMMAND_CMDALIAS_LIST_NONESET = "pocketmine.command.cmdalias.list.noneSet"; + public const POCKETMINE_COMMAND_CMDALIAS_LIST_NORMAL = "pocketmine.command.cmdalias.list.normal"; + public const POCKETMINE_COMMAND_CMDALIAS_SCOPE_GLOBAL = "pocketmine.command.cmdalias.scope.global"; + public const POCKETMINE_COMMAND_CMDALIAS_SCOPE_USERSPECIFIC = "pocketmine.command.cmdalias.scope.userSpecific"; + public const POCKETMINE_COMMAND_CMDALIAS_TEMPLATE = "pocketmine.command.cmdalias.template"; public const POCKETMINE_COMMAND_DEFAULTGAMEMODE_DESCRIPTION = "pocketmine.command.defaultgamemode.description"; public const POCKETMINE_COMMAND_DEOP_DESCRIPTION = "pocketmine.command.deop.description"; public const POCKETMINE_COMMAND_DIFFICULTY_DESCRIPTION = "pocketmine.command.difficulty.description"; public const POCKETMINE_COMMAND_DUMPMEMORY_DESCRIPTION = "pocketmine.command.dumpmemory.description"; public const POCKETMINE_COMMAND_EFFECT_DESCRIPTION = "pocketmine.command.effect.description"; public const POCKETMINE_COMMAND_ENCHANT_DESCRIPTION = "pocketmine.command.enchant.description"; + public const POCKETMINE_COMMAND_ERROR_ALIASCONFLICT = "pocketmine.command.error.aliasConflict"; + public const POCKETMINE_COMMAND_ERROR_ALIASCONFLICTTIP = "pocketmine.command.error.aliasConflictTip"; public const POCKETMINE_COMMAND_ERROR_PERMISSION = "pocketmine.command.error.permission"; public const POCKETMINE_COMMAND_ERROR_PLAYERNOTFOUND = "pocketmine.command.error.playerNotFound"; public const POCKETMINE_COMMAND_EXCEPTION = "pocketmine.command.exception"; @@ -489,6 +501,9 @@ final class KnownTranslationKeys{ public const POCKETMINE_PERMISSION_COMMAND_BAN_PLAYER = "pocketmine.permission.command.ban.player"; public const POCKETMINE_PERMISSION_COMMAND_CLEAR_OTHER = "pocketmine.permission.command.clear.other"; public const POCKETMINE_PERMISSION_COMMAND_CLEAR_SELF = "pocketmine.permission.command.clear.self"; + public const POCKETMINE_PERMISSION_COMMAND_CMDALIAS_EDIT_GLOBAL = "pocketmine.permission.command.cmdalias.edit.global"; + public const POCKETMINE_PERMISSION_COMMAND_CMDALIAS_EDIT_SELF = "pocketmine.permission.command.cmdalias.edit.self"; + public const POCKETMINE_PERMISSION_COMMAND_CMDALIAS_LIST = "pocketmine.permission.command.cmdalias.list"; public const POCKETMINE_PERMISSION_COMMAND_DEFAULTGAMEMODE = "pocketmine.permission.command.defaultgamemode"; public const POCKETMINE_PERMISSION_COMMAND_DIFFICULTY = "pocketmine.permission.command.difficulty"; public const POCKETMINE_PERMISSION_COMMAND_DUMPMEMORY = "pocketmine.permission.command.dumpmemory"; diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 75281e426..da54ee25d 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; @@ -1095,21 +1096,24 @@ 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)){ + $globalAliasMap = $this->server->getCommandMap()->getAliasMap(); + $userAliasMap = $this->player->getCommandAliasMap(); + foreach($this->server->getCommandMap()->getUniqueCommands() as $command){ + if(!$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); + $userAliases = $userAliasMap->getMergedAliases($command->getId(), $globalAliasMap); + //the client doesn't like it when we override /help + $aliases = array_values(array_filter($userAliases, 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(); $data = new CommandData( @@ -1124,7 +1128,7 @@ class NetworkSession{ chainedSubCommandData: [] ); - $commandData[$command->getLabel()] = $data; + $commandData[] = $data; } $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], [])); diff --git a/src/permission/DefaultPermissionNames.php b/src/permission/DefaultPermissionNames.php index a916e813c..4303e101e 100644 --- a/src/permission/DefaultPermissionNames.php +++ b/src/permission/DefaultPermissionNames.php @@ -29,6 +29,9 @@ final class DefaultPermissionNames{ public const COMMAND_BAN_IP = "pocketmine.command.ban.ip"; public const COMMAND_BAN_LIST = "pocketmine.command.ban.list"; public const COMMAND_BAN_PLAYER = "pocketmine.command.ban.player"; + public const COMMAND_CMDALIAS_LIST = "pocketmine.command.cmdalias.list"; + public const COMMAND_CMDALIAS_EDIT_SELF = "pocketmine.command.cmdalias.edit.self"; + public const COMMAND_CMDALIAS_EDIT_GLOBAL = "pocketmine.command.cmdalias.edit.global"; public const COMMAND_CLEAR_OTHER = "pocketmine.command.clear.other"; public const COMMAND_CLEAR_SELF = "pocketmine.command.clear.self"; public const COMMAND_DEFAULTGAMEMODE = "pocketmine.command.defaultgamemode"; diff --git a/src/permission/DefaultPermissions.php b/src/permission/DefaultPermissions.php index 1030e9ff8..9297fc0b7 100644 --- a/src/permission/DefaultPermissions.php +++ b/src/permission/DefaultPermissions.php @@ -59,6 +59,9 @@ abstract class DefaultPermissions{ self::registerPermission(new Permission(Names::COMMAND_BAN_PLAYER, l10n::pocketmine_permission_command_ban_player()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_CLEAR_OTHER, l10n::pocketmine_permission_command_clear_other()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_CLEAR_SELF, l10n::pocketmine_permission_command_clear_self()), [$everyoneRoot]); + self::registerPermission(new Permission(Names::COMMAND_CMDALIAS_LIST, l10n::pocketmine_permission_command_cmdalias_list()), [$everyoneRoot]); + self::registerPermission(new Permission(Names::COMMAND_CMDALIAS_EDIT_SELF, l10n::pocketmine_permission_command_cmdalias_edit_self()), [$everyoneRoot]); + self::registerPermission(new Permission(Names::COMMAND_CMDALIAS_EDIT_GLOBAL, l10n::pocketmine_permission_command_cmdalias_edit_global()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_DEFAULTGAMEMODE, l10n::pocketmine_permission_command_defaultgamemode()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_DIFFICULTY, l10n::pocketmine_permission_command_difficulty()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_DUMPMEMORY, l10n::pocketmine_permission_command_dumpmemory()), [$consoleRoot]); diff --git a/src/player/Player.php b/src/player/Player.php index 3fb192a7d..3d39bc874 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -30,6 +30,7 @@ use pocketmine\block\BlockTypeTags; use pocketmine\block\RespawnAnchor; use pocketmine\block\UnknownBlock; use pocketmine\block\VanillaBlocks; +use pocketmine\command\CommandAliasMap; use pocketmine\command\CommandSender; use pocketmine\crafting\CraftingGrid; use pocketmine\data\java\GameModeIdMap; @@ -296,6 +297,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer, Nev /** @phpstan-var positive-int|null */ protected ?int $lineHeight = null; + private CommandAliasMap $commandAliasMap; + protected string $locale = "en_US"; protected int $startAction = -1; @@ -339,6 +342,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer, Nev $rootPermissions[DefaultPermissions::ROOT_OPERATOR] = true; } $this->perm = new PermissibleBase($rootPermissions); + $this->commandAliasMap = new CommandAliasMap(); + $this->chunksPerTick = $this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_SENDING_PER_TICK, 4); $this->spawnThreshold = (int) (($this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_SENDING_SPAWN_RADIUS, 4) ** 2) * M_PI); $this->chunkSelector = new ChunkSelector(); @@ -598,6 +603,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer, Nev $this->lineHeight = $height; } + public function getCommandAliasMap() : CommandAliasMap{ return $this->commandAliasMap; } + public function canSee(Player $player) : bool{ return !isset($this->hiddenPlayers[$player->getUniqueId()->getBytes()]); } diff --git a/src/plugin/PluginBase.php b/src/plugin/PluginBase.php index 3b401d44a..d39df3895 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; @@ -144,23 +143,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, ":")){ @@ -170,7 +158,14 @@ abstract class PluginBase implements Plugin, CommandExecutor{ $aliasList[] = $alias; } - $newCmd->setAliases($aliasList); + $newCmd = new PluginCommand( + $this->description->getName(), + $key, + $this, + $this, + $data->getDescription() ?? "", + $data->getUsageMessage() + ); $newCmd->setPermission($data->getPermission()); @@ -178,11 +173,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($newCmd, $aliasList); } } diff --git a/src/utils/BroadcastLoggerForwarder.php b/src/utils/BroadcastLoggerForwarder.php index 1900421fc..aee6939a5 100644 --- a/src/utils/BroadcastLoggerForwarder.php +++ b/src/utils/BroadcastLoggerForwarder.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\utils; +use pocketmine\command\CommandAliasMap; use pocketmine\command\CommandSender; use pocketmine\lang\Language; use pocketmine\lang\Translatable; @@ -41,6 +42,8 @@ use const PHP_INT_MAX; final class BroadcastLoggerForwarder implements CommandSender{ use PermissibleDelegateTrait; + private CommandAliasMap $commandAliasMap; + public function __construct( private Server $server, //annoying useless dependency private \Logger $logger, @@ -48,6 +51,7 @@ final class BroadcastLoggerForwarder implements CommandSender{ ){ //this doesn't need any permissions $this->perm = new PermissibleBase([]); + $this->commandAliasMap = new CommandAliasMap(); } public function getLanguage() : Language{ @@ -77,4 +81,6 @@ final class BroadcastLoggerForwarder implements CommandSender{ public function setScreenLineHeight(?int $height) : void{ //NOOP } + + public function getCommandAliasMap() : CommandAliasMap{ return $this->commandAliasMap; } } diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index d9279066c..20d89ad3b 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -696,6 +696,12 @@ parameters: count: 1 path: ../../../src/network/mcpe/NetworkSession.php + - + message: '#^Cannot call method getCommandAliasMap\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject + count: 1 + path: ../../../src/network/mcpe/NetworkSession.php + - message: '#^Cannot call method getLanguage\(\) on pocketmine\\player\\Player\|null\.$#' identifier: method.nonObject