First step in restructuring command alias handling

This decouples Command from CommandMap, and moves the burden of tracking registered
aliases to CommandMap. This allows lots of simplification, and also solves a few
weird usage message issues.

- Active (registered) aliases are now tracked via CommandMapEntry
- Commands overriding other commands' aliases now update the original command's registered alias list properly
- Command alias lists now include prefixed aliases
- Prefixed aliases are now included in command data provided to the client
- Server-side /pocketmine:help is now visible on the client
- Permission testing can now provide context that's more relevant to the command invocation - e.g. if a user doesn't have /time set permission, it'll now show a more specific message where previously it would just show that the permission for /time was denied
- User-specified label is now used for permission messages instead of command name - this is more consistent with user expectations
- /help can now see prefixed aliases
- Removed magic alias registration behaviour for VanillaCommand that I don't think anyone expected
- Aliases are now provided to CommandMap via register() parameters, instead of being retrieved from the provided Command
- Command->get/setAliases(), get/setLabel() and getName() are removed
- Command->getName() pushed down to PluginCommand, as it's only useful for CommandExecutors as a command ID and shouldn't be used anywhere else
This commit is contained in:
Dylan K. Taylor 2025-05-04 15:23:25 +01:00
parent cb4364f8fd
commit 579aecfad7
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
51 changed files with 294 additions and 369 deletions

View File

@ -38,19 +38,17 @@ final class ClosureCommand extends Command{
* @phpstan-param Execute $execute * @phpstan-param Execute $execute
*/ */
public function __construct( public function __construct(
string $name,
\Closure $execute, \Closure $execute,
array $permissions, array $permissions,
Translatable|string $description = "", Translatable|string $description = "",
Translatable|string|null $usageMessage = null, Translatable|string|null $usageMessage = null,
array $aliases = []
){ ){
Utils::validateCallableSignature( Utils::validateCallableSignature(
fn(CommandSender $sender, Command $command, string $commandLabel, array $args) : mixed => 1, fn(CommandSender $sender, Command $command, string $commandLabel, array $args) : mixed => 1,
$execute, $execute,
); );
$this->execute = $execute; $this->execute = $execute;
parent::__construct($name, $description, $usageMessage, $aliases); parent::__construct($description, $usageMessage);
$this->setPermissions($permissions); $this->setPermissions($permissions);
} }

View File

@ -33,52 +33,20 @@ use pocketmine\permission\PermissionManager;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\utils\BroadcastLoggerForwarder; use pocketmine\utils\BroadcastLoggerForwarder;
use pocketmine\utils\TextFormat; use pocketmine\utils\TextFormat;
use function array_values;
use function explode; use function explode;
use function implode; use function implode;
use function str_replace; use function str_replace;
use const PHP_INT_MAX; use const PHP_INT_MAX;
abstract class Command{ abstract class Command{
private string $name;
private string $nextLabel;
private string $label;
/**
* @var string[]
* @phpstan-var list<string>
*/
private array $aliases = [];
/**
* @var string[]
* @phpstan-var list<string>
*/
private array $activeAliases = [];
private ?CommandMap $commandMap = null;
protected Translatable|string $description = "";
protected Translatable|string $usageMessage;
/** @var string[] */ /** @var string[] */
private array $permission = []; private array $permission = [];
private Translatable|string|null $permissionMessage = null; private Translatable|string|null $permissionMessage = null;
/** public function __construct(
* @param string[] $aliases private Translatable|string $description = "",
* @phpstan-param list<string> $aliases private Translatable|string|null $usageMessage = null
*/ ){}
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);
}
/** /**
* @param string[] $args * @param string[] $args
@ -89,10 +57,6 @@ abstract class Command{
*/ */
abstract public function execute(CommandSender $sender, string $commandLabel, array $args); abstract public function execute(CommandSender $sender, string $commandLabel, array $args);
public function getName() : string{
return $this->name;
}
/** /**
* @return string[] * @return string[]
*/ */
@ -117,12 +81,17 @@ abstract class Command{
$this->setPermissions($permission === null ? [] : explode(";", $permission, limit: PHP_INT_MAX)); $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)){ if($this->testPermissionSilent($target, $permission)){
return true; 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){ if($message instanceof Translatable){
$target->sendMessage($message->prefix(TextFormat::RED)); $target->sendMessage($message->prefix(TextFormat::RED));
}elseif($message !== ""){ }elseif($message !== ""){
@ -143,62 +112,6 @@ abstract class Command{
return false; return false;
} }
public function getLabel() : string{
return $this->label;
}
public function setLabel(string $name) : bool{
$this->nextLabel = $name;
if(!$this->isRegistered()){
$this->label = $name;
return true;
}
return false;
}
/**
* Registers the command into a Command map
*/
public function register(CommandMap $commandMap) : bool{
if($this->allowChangesFrom($commandMap)){
$this->commandMap = $commandMap;
return true;
}
return false;
}
public function unregister(CommandMap $commandMap) : bool{
if($this->allowChangesFrom($commandMap)){
$this->commandMap = null;
$this->activeAliases = $this->aliases;
$this->label = $this->nextLabel;
return true;
}
return false;
}
private function allowChangesFrom(CommandMap $commandMap) : bool{
return $this->commandMap === null || $this->commandMap === $commandMap;
}
public function isRegistered() : bool{
return $this->commandMap !== null;
}
/**
* @return string[]
* @phpstan-return list<string>
*/
public function getAliases() : array{
return $this->activeAliases;
}
public function getPermissionMessage() : Translatable|string|null{ public function getPermissionMessage() : Translatable|string|null{
return $this->permissionMessage; return $this->permissionMessage;
} }
@ -207,22 +120,10 @@ abstract class Command{
return $this->description; return $this->description;
} }
public function getUsage() : Translatable|string{ public function getUsage() : Translatable|string|null{
return $this->usageMessage; return $this->usageMessage;
} }
/**
* @param string[] $aliases
* @phpstan-param list<string> $aliases
*/
public function setAliases(array $aliases) : void{
$aliases = array_values($aliases); //because plugins can and will pass crap
$this->aliases = $aliases;
if(!$this->isRegistered()){
$this->activeAliases = $aliases;
}
}
public function setDescription(Translatable|string $description) : void{ public function setDescription(Translatable|string $description) : void{
$this->description = $description; $this->description = $description;
} }
@ -231,7 +132,7 @@ abstract class Command{
$this->permissionMessage = $permissionMessage; $this->permissionMessage = $permissionMessage;
} }
public function setUsage(Translatable|string $usage) : void{ public function setUsage(Translatable|string|null $usage) : void{
$this->usageMessage = $usage; $this->usageMessage = $usage;
} }
@ -252,8 +153,4 @@ abstract class Command{
} }
} }
} }
public function __toString() : string{
return $this->name;
}
} }

View File

@ -24,13 +24,12 @@ declare(strict_types=1);
namespace pocketmine\command; namespace pocketmine\command;
interface CommandMap{ interface CommandMap{
/** /**
* @param Command[] $commands * @param string[] $otherAliases
*
* @phpstan-param list<string> $otherAliases
*/ */
public function registerAll(string $fallbackPrefix, array $commands) : void; public function register(string $fallbackPrefix, Command $command, string $preferredAlias, array $otherAliases = []) : CommandMapEntry;
public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool;
public function dispatch(CommandSender $sender, string $cmdLine) : bool; public function dispatch(CommandSender $sender, string $cmdLine) : bool;

View File

@ -0,0 +1,48 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\command;
use pocketmine\lang\Translatable;
final class CommandMapEntry{
/**
* @param string[] $aliases
* @phpstan-param non-empty-list<string> $aliases
*/
public function __construct(
public readonly Command $command,
public readonly array $aliases
){}
public function getPreferredAlias() : string{
return $this->aliases[0];
}
public function getUsage() : Translatable|string{
//TODO: usage messages ought to use user-specified alias, not command preferred
//command-preferred is confusing if the user used a different alias
return $this->command->getUsage() ?? "/" . $this->getPreferredAlias();
}
}

View File

@ -50,10 +50,9 @@ class FormattedCommandAlias extends Command{
* @param string[] $formatStrings * @param string[] $formatStrings
*/ */
public function __construct( public function __construct(
string $alias,
private array $formatStrings private array $formatStrings
){ ){
parent::__construct($alias, KnownTranslationFactory::pocketmine_command_userDefined_description()); parent::__construct(KnownTranslationFactory::pocketmine_command_userDefined_description());
} }
public function execute(CommandSender $sender, string $commandLabel, array $args){ public function execute(CommandSender $sender, string $commandLabel, array $args){
@ -95,12 +94,14 @@ class FormattedCommandAlias extends Command{
throw new AssumptionFailedError("This should have been checked before construction"); throw new AssumptionFailedError("This should have been checked before construction");
} }
if(($target = $commandMap->getCommand($commandLabel)) !== null){ if(($target = $commandMap->getEntry($commandLabel)) !== null){
$timings = Timings::getCommandDispatchTimings($target->getLabel()); //TODO: using labels for command dispatch is problematic - what if the label changes?
//maybe this should use command class instead?
$timings = Timings::getCommandDispatchTimings($target->getPreferredAlias());
$timings->startTiming(); $timings->startTiming();
try{ try{
$target->execute($sender, $commandLabel, $commandArgs); $target->command->execute($sender, $commandLabel, $commandArgs);
}catch(InvalidCommandSyntaxException $e){ }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())));
}finally{ }finally{

View File

@ -24,17 +24,23 @@ declare(strict_types=1);
namespace pocketmine\command; namespace pocketmine\command;
use pocketmine\command\utils\InvalidCommandSyntaxException; use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\Translatable;
use pocketmine\plugin\Plugin; use pocketmine\plugin\Plugin;
use pocketmine\plugin\PluginOwned; use pocketmine\plugin\PluginOwned;
final class PluginCommand extends Command implements PluginOwned{ final class PluginCommand extends Command implements PluginOwned{
public function __construct( public function __construct(
string $name, private string $name,
private Plugin $owner, private Plugin $owner,
private CommandExecutor $executor private CommandExecutor $executor,
Translatable|string $description = "",
Translatable|string|null $usageMessage = null
){ ){
parent::__construct($name); parent::__construct($description, $usageMessage);
$this->usageMessage = ""; }
public function getName() : string{
return $this->name;
} }
public function execute(CommandSender $sender, string $commandLabel, array $args){ public function execute(CommandSender $sender, string $commandLabel, array $args){
@ -43,13 +49,11 @@ final class PluginCommand extends Command implements PluginOwned{
return false; return false;
} }
$success = $this->executor->onCommand($sender, $this, $commandLabel, $args); if(!$this->executor->onCommand($sender, $this, $commandLabel, $args)){
if(!$success && $this->usageMessage !== ""){
throw new InvalidCommandSyntaxException(); throw new InvalidCommandSyntaxException();
} }
return $success; return true;
} }
public function getOwningPlugin() : Plugin{ public function getOwningPlugin() : Plugin{

View File

@ -61,7 +61,6 @@ use pocketmine\command\defaults\TimeCommand;
use pocketmine\command\defaults\TimingsCommand; use pocketmine\command\defaults\TimingsCommand;
use pocketmine\command\defaults\TitleCommand; use pocketmine\command\defaults\TitleCommand;
use pocketmine\command\defaults\TransferServerCommand; use pocketmine\command\defaults\TransferServerCommand;
use pocketmine\command\defaults\VanillaCommand;
use pocketmine\command\defaults\VersionCommand; use pocketmine\command\defaults\VersionCommand;
use pocketmine\command\defaults\WhitelistCommand; use pocketmine\command\defaults\WhitelistCommand;
use pocketmine\command\defaults\XpCommand; use pocketmine\command\defaults\XpCommand;
@ -70,12 +69,15 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\KnownTranslationFactory;
use pocketmine\Server; use pocketmine\Server;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\TextFormat; use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function array_filter;
use function array_shift; use function array_shift;
use function array_values; use function array_values;
use function count; use function count;
use function implode; use function implode;
use function spl_object_id;
use function str_contains; use function str_contains;
use function strcasecmp; use function strcasecmp;
use function strtolower; use function strtolower;
@ -87,122 +89,124 @@ class SimpleCommandMap implements CommandMap{
* @var Command[] * @var Command[]
* @phpstan-var array<string, Command> * @phpstan-var array<string, Command>
*/ */
protected array $knownCommands = []; protected array $aliasToCommandMap = [];
/**
* @var CommandMapEntry[]
* @phpstan-var array<int, CommandMapEntry>
*/
private array $uniqueCommands = [];
public function __construct(private Server $server){ public function __construct(private Server $server){
$this->setDefaultCommands(); $this->setDefaultCommands();
} }
private function setDefaultCommands() : void{ private function setDefaultCommands() : void{
$this->registerAll("pocketmine", [ $pmPrefix = "pocketmine";
new BanCommand(), $this->register($pmPrefix, new BanCommand(), "ban");
new BanIpCommand(), $this->register($pmPrefix, new BanIpCommand(), "ban-ip");
new BanListCommand(), $this->register($pmPrefix, new BanListCommand(), "banlist");
new ClearCommand(), $this->register($pmPrefix, new ClearCommand(), "clear");
new DefaultGamemodeCommand(), $this->register($pmPrefix, new DefaultGamemodeCommand(), "defaultgamemode");
new DeopCommand(), $this->register($pmPrefix, new DeopCommand(), "deop");
new DifficultyCommand(), $this->register($pmPrefix, new DifficultyCommand(), "difficulty");
new DumpMemoryCommand(), $this->register($pmPrefix, new DumpMemoryCommand(), "dumpmemory");
new EffectCommand(), $this->register($pmPrefix, new EffectCommand(), "effect");
new EnchantCommand(), $this->register($pmPrefix, new EnchantCommand(), "enchant");
new GamemodeCommand(), $this->register($pmPrefix, new GamemodeCommand(), "gamemode");
new GarbageCollectorCommand(), $this->register($pmPrefix, new GarbageCollectorCommand(), "gc");
new GiveCommand(), $this->register($pmPrefix, new GiveCommand(), "give");
new HelpCommand(), $this->register($pmPrefix, new HelpCommand(), "help", ["?"]);
new KickCommand(), $this->register($pmPrefix, new KickCommand(), "kick");
new KillCommand(), $this->register($pmPrefix, new KillCommand(), "kill", ["suicide"]);
new ListCommand(), $this->register($pmPrefix, new ListCommand(), "list");
new MeCommand(), $this->register($pmPrefix, new MeCommand(), "me");
new OpCommand(), $this->register($pmPrefix, new OpCommand(), "op");
new PardonCommand(), $this->register($pmPrefix, new PardonCommand(), "pardon", ["unban"]);
new PardonIpCommand(), $this->register($pmPrefix, new PardonIpCommand(), "pardon-ip", ["unban-ip"]);
new ParticleCommand(), $this->register($pmPrefix, new ParticleCommand(), "particle");
new PluginsCommand(), $this->register($pmPrefix, new PluginsCommand(), "plugins", ["pl"]);
new SaveCommand(), $this->register($pmPrefix, new SaveCommand(), "save-all");
new SaveOffCommand(), $this->register($pmPrefix, new SaveOffCommand(), "save-off");
new SaveOnCommand(), $this->register($pmPrefix, new SaveOnCommand(), "save-on");
new SayCommand(), $this->register($pmPrefix, new SayCommand(), "say");
new SeedCommand(), $this->register($pmPrefix, new SeedCommand(), "seed");
new SetWorldSpawnCommand(), $this->register($pmPrefix, new SetWorldSpawnCommand(), "setworldspawn");
new SpawnpointCommand(), $this->register($pmPrefix, new SpawnpointCommand(), "spawnpoint");
new StatusCommand(), $this->register($pmPrefix, new StatusCommand(), "status");
new StopCommand(), $this->register($pmPrefix, new StopCommand(), "stop");
new TeleportCommand(), $this->register($pmPrefix, new TeleportCommand(), "tp", ["teleport"]);
new TellCommand(), $this->register($pmPrefix, new TellCommand(), "tell", ["w", "msg"]);
new TimeCommand(), $this->register($pmPrefix, new TimeCommand(), "time");
new TimingsCommand(), $this->register($pmPrefix, new TimingsCommand(), "timings");
new TitleCommand(), $this->register($pmPrefix, new TitleCommand(), "title");
new TransferServerCommand(), $this->register($pmPrefix, new TransferServerCommand(), "transferserver");
new VersionCommand(), $this->register($pmPrefix, new VersionCommand(), "version", ["ver", "about"]);
new WhitelistCommand(), $this->register($pmPrefix, new WhitelistCommand(), "whitelist");
new XpCommand(), $this->register($pmPrefix, new XpCommand(), "xp");
]);
} }
public function registerAll(string $fallbackPrefix, array $commands) : void{ public function register(string $fallbackPrefix, Command $command, string $preferredAlias, array $otherAliases = []) : CommandMapEntry{
foreach($commands as $command){
$this->register($fallbackPrefix, $command);
}
}
public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool{
if(count($command->getPermissions()) === 0){ if(count($command->getPermissions()) === 0){
throw new \InvalidArgumentException("Commands must have a permission set"); throw new \InvalidArgumentException("Commands must have a permission set");
} }
if($label === null){ $preferredAlias = trim($preferredAlias);
$label = $command->getLabel();
}
$label = trim($label);
$fallbackPrefix = strtolower(trim($fallbackPrefix)); $fallbackPrefix = strtolower(trim($fallbackPrefix));
$registered = $this->registerAlias($command, false, $fallbackPrefix, $label); $registeredAliases = [];
//primary labels take precedence over any existing registrations
$this->mapAlias($preferredAlias, $command, $registeredAliases);
$this->mapAlias($fallbackPrefix . ":" . $preferredAlias, $command, $registeredAliases);
$aliases = $command->getAliases(); foreach($otherAliases as $alias){
foreach($aliases as $index => $alias){ $this->mapAlias($fallbackPrefix . ":" . $alias, $command, $registeredAliases);
if(!$this->registerAlias($command, true, $fallbackPrefix, $alias)){ if(!isset($this->aliasToCommandMap[$alias])){
unset($aliases[$index]); $this->mapAlias($alias, $command, $registeredAliases);
} }
} }
$command->setAliases(array_values($aliases));
if(!$registered){ $entry = new CommandMapEntry($command, $registeredAliases);
$command->setLabel($fallbackPrefix . ":" . $label); $this->uniqueCommands[spl_object_id($command)] = $entry;
return $entry;
} }
$command->register($this); /**
* @param string[] &$registeredAliases
* @phpstan-param list<string> &$registeredAliases
* @phpstan-param-out non-empty-list<string> $registeredAliases
*/
private function mapAlias(string $alias, Command $command, array &$registeredAliases) : void{
$this->unmapAlias($alias);
$this->aliasToCommandMap[$alias] = $command;
$registeredAliases[] = $alias;
}
return $registered; private function unmapAlias(string $alias) : void{
$oldCommand = $this->aliasToCommandMap[$alias] ?? null;
if($oldCommand !== null){
unset($this->aliasToCommandMap[$alias]);
$oldCommandKey = spl_object_id($oldCommand);
$oldCommandEntry = $this->uniqueCommands[$oldCommandKey];
$filteredAliases = array_values(array_filter($oldCommandEntry->aliases, fn(string $oldAlias) => $oldAlias !== $alias));
if(count($filteredAliases) > 0){
$this->uniqueCommands[$oldCommandKey] = new CommandMapEntry($oldCommand, $filteredAliases);
}else{
unset($this->uniqueCommands[$oldCommandKey]);
}
}
} }
public function unregister(Command $command) : bool{ public function unregister(Command $command) : bool{
foreach(Utils::promoteKeys($this->knownCommands) as $lbl => $cmd){ $entry = $this->uniqueCommands[spl_object_id($command)] ?? null;
if($cmd === $command){ if($entry !== null){
unset($this->knownCommands[$lbl]); unset($this->uniqueCommands[spl_object_id($command)]);
foreach($entry->aliases as $alias){
unset($this->aliasToCommandMap[$alias]);
} }
} }
$command->unregister($this);
return true;
}
private function registerAlias(Command $command, bool $isAlias, string $fallbackPrefix, string $label) : bool{
$this->knownCommands[$fallbackPrefix . ":" . $label] = $command;
if(($command instanceof VanillaCommand || $isAlias) && isset($this->knownCommands[$label])){
return false;
}
if(isset($this->knownCommands[$label]) && $this->knownCommands[$label]->getLabel() === $label){
return false;
}
if(!$isAlias){
$command->setLabel($label);
}
$this->knownCommands[$label] = $command;
return true; return true;
} }
@ -210,13 +214,15 @@ class SimpleCommandMap implements CommandMap{
$args = CommandStringHelper::parseQuoteAware($commandLine); $args = CommandStringHelper::parseQuoteAware($commandLine);
$sentCommandLabel = array_shift($args); $sentCommandLabel = array_shift($args);
if($sentCommandLabel !== null && ($target = $this->getCommand($sentCommandLabel)) !== null){ if($sentCommandLabel !== null && ($target = $this->getEntry($sentCommandLabel)) !== null){
$timings = Timings::getCommandDispatchTimings($target->getLabel()); //TODO: using labels for command dispatch is problematic - what if the label changes?
//maybe this should use command class instead?
$timings = Timings::getCommandDispatchTimings($target->getPreferredAlias());
$timings->startTiming(); $timings->startTiming();
try{ try{
if($target->testPermission($sender)){ if($target->command->testPermission($sentCommandLabel, $sender)){
$target->execute($sender, $sentCommandLabel, $args); $target->command->execute($sender, $sentCommandLabel, $args);
} }
}catch(InvalidCommandSyntaxException $e){ }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())));
@ -231,23 +237,36 @@ class SimpleCommandMap implements CommandMap{
} }
public function clearCommands() : void{ public function clearCommands() : void{
foreach($this->knownCommands as $command){ $this->aliasToCommandMap = [];
$command->unregister($this); $this->uniqueCommands = [];
}
$this->knownCommands = [];
$this->setDefaultCommands(); $this->setDefaultCommands();
} }
public function getCommand(string $name) : ?Command{ public function getCommand(string $name) : ?Command{
return $this->knownCommands[$name] ?? null; return $this->aliasToCommandMap[$name] ?? null;
} }
/** /**
* @return Command[] * @return Command[]
* @phpstan-return array<string, Command> * @phpstan-return array<string, Command>
*/ */
public function getCommands() : array{ public function getAliasToCommandMap() : array{
return $this->knownCommands; return $this->aliasToCommandMap;
}
/**
* @return CommandMapEntry[]
* @phpstan-return array<int, CommandMapEntry>
*/
public function getUniqueCommands() : array{
return $this->uniqueCommands;
}
public function getEntry(string $name) : ?CommandMapEntry{
$command = $this->getCommand($name);
return $command !== null ?
$this->uniqueCommands[spl_object_id($command)] ?? throw new AssumptionFailedError("This should never be unset") :
null;
} }
public function registerServerAliases() : void{ public function registerServerAliases() : void{
@ -289,12 +308,13 @@ class SimpleCommandMap implements CommandMap{
//These registered commands have absolute priority //These registered commands have absolute priority
$lowerAlias = strtolower($alias); $lowerAlias = strtolower($alias);
$this->unmapAlias($lowerAlias);
if(count($targets) > 0){ if(count($targets) > 0){
$this->knownCommands[$lowerAlias] = new FormattedCommandAlias($lowerAlias, $targets); $aliasInstance = new FormattedCommandAlias($targets);
}else{ $registeredAliases = [];
unset($this->knownCommands[$lowerAlias]); $this->mapAlias($lowerAlias, $aliasInstance, $registeredAliases);
} $this->uniqueCommands[spl_object_id($aliasInstance)] = new CommandMapEntry($aliasInstance, $registeredAliases);
}
} }
} }
} }

View File

@ -37,7 +37,6 @@ class BanCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"ban",
KnownTranslationFactory::pocketmine_command_ban_player_description(), KnownTranslationFactory::pocketmine_command_ban_player_description(),
KnownTranslationFactory::commands_ban_usage() KnownTranslationFactory::commands_ban_usage()
); );

View File

@ -38,7 +38,6 @@ class BanIpCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"ban-ip",
KnownTranslationFactory::pocketmine_command_ban_ip_description(), KnownTranslationFactory::pocketmine_command_ban_ip_description(),
KnownTranslationFactory::commands_banip_usage() KnownTranslationFactory::commands_banip_usage()
); );

View File

@ -39,7 +39,6 @@ class BanListCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"banlist",
KnownTranslationFactory::pocketmine_command_banlist_description(), KnownTranslationFactory::pocketmine_command_banlist_description(),
KnownTranslationFactory::commands_banlist_usage() KnownTranslationFactory::commands_banlist_usage()
); );

View File

@ -41,7 +41,6 @@ class ClearCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"clear",
KnownTranslationFactory::pocketmine_command_clear_description(), KnownTranslationFactory::pocketmine_command_clear_description(),
KnownTranslationFactory::pocketmine_command_clear_usage() KnownTranslationFactory::pocketmine_command_clear_usage()
); );
@ -53,7 +52,7 @@ class ClearCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($target === null){
return true; return true;
} }

View File

@ -35,7 +35,6 @@ class DefaultGamemodeCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"defaultgamemode",
KnownTranslationFactory::pocketmine_command_defaultgamemode_description(), KnownTranslationFactory::pocketmine_command_defaultgamemode_description(),
KnownTranslationFactory::commands_defaultgamemode_usage() KnownTranslationFactory::commands_defaultgamemode_usage()
); );

View File

@ -37,7 +37,6 @@ class DeopCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"deop",
KnownTranslationFactory::pocketmine_command_deop_description(), KnownTranslationFactory::pocketmine_command_deop_description(),
KnownTranslationFactory::commands_deop_usage() KnownTranslationFactory::commands_deop_usage()
); );

View File

@ -36,7 +36,6 @@ class DifficultyCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"difficulty",
KnownTranslationFactory::pocketmine_command_difficulty_description(), KnownTranslationFactory::pocketmine_command_difficulty_description(),
KnownTranslationFactory::commands_difficulty_usage() KnownTranslationFactory::commands_difficulty_usage()
); );

View File

@ -33,7 +33,6 @@ class DumpMemoryCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"dumpmemory",
KnownTranslationFactory::pocketmine_command_dumpmemory_description(), KnownTranslationFactory::pocketmine_command_dumpmemory_description(),
"/dumpmemory [path]" "/dumpmemory [path]"
); );

View File

@ -38,7 +38,6 @@ class EffectCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"effect",
KnownTranslationFactory::pocketmine_command_effect_description(), KnownTranslationFactory::pocketmine_command_effect_description(),
KnownTranslationFactory::commands_effect_usage() KnownTranslationFactory::commands_effect_usage()
); );
@ -53,7 +52,7 @@ class EffectCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -36,7 +36,6 @@ class EnchantCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"enchant",
KnownTranslationFactory::pocketmine_command_enchant_description(), KnownTranslationFactory::pocketmine_command_enchant_description(),
KnownTranslationFactory::commands_enchant_usage() KnownTranslationFactory::commands_enchant_usage()
); );
@ -51,7 +50,7 @@ class EnchantCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -35,7 +35,6 @@ class GamemodeCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"gamemode",
KnownTranslationFactory::pocketmine_command_gamemode_description(), KnownTranslationFactory::pocketmine_command_gamemode_description(),
KnownTranslationFactory::commands_gamemode_usage() KnownTranslationFactory::commands_gamemode_usage()
); );
@ -56,7 +55,7 @@ class GamemodeCommand extends VanillaCommand{
return true; 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){ if($target === null){
return true; return true;
} }

View File

@ -36,7 +36,6 @@ class GarbageCollectorCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"gc",
KnownTranslationFactory::pocketmine_command_gc_description() KnownTranslationFactory::pocketmine_command_gc_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_GC); $this->setPermission(DefaultPermissionNames::COMMAND_GC);

View File

@ -43,7 +43,6 @@ class GiveCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"give",
KnownTranslationFactory::pocketmine_command_give_description(), KnownTranslationFactory::pocketmine_command_give_description(),
KnownTranslationFactory::pocketmine_command_give_usage() KnownTranslationFactory::pocketmine_command_give_usage()
); );
@ -58,7 +57,7 @@ class GiveCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -47,10 +47,8 @@ class HelpCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"help",
KnownTranslationFactory::pocketmine_command_help_description(), KnownTranslationFactory::pocketmine_command_help_description(),
KnownTranslationFactory::commands_help_usage(), KnownTranslationFactory::commands_help_usage()
["?"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_HELP); $this->setPermission(DefaultPermissionNames::COMMAND_HELP);
} }
@ -72,11 +70,13 @@ class HelpCommand extends VanillaCommand{
$pageHeight = $sender->getScreenLineHeight(); $pageHeight = $sender->getScreenLineHeight();
//TODO: maybe inject this in the constructor instead of assuming the server's command map?
$commandMap = $sender->getServer()->getCommandMap();
if($commandName === ""){ if($commandName === ""){
$commands = []; $commands = [];
foreach($sender->getServer()->getCommandMap()->getCommands() as $command){ foreach($commandMap->getUniqueCommands() as $commandEntry){
if($command->testPermissionSilent($sender)){ if($commandEntry->command->testPermissionSilent($sender)){
$commands[$command->getLabel()] = $command; $commands[$commandEntry->getPreferredAlias()] = $commandEntry;
} }
} }
ksort($commands, SORT_NATURAL | SORT_FLAG_CASE); ksort($commands, SORT_NATURAL | SORT_FLAG_CASE);
@ -88,31 +88,31 @@ class HelpCommand extends VanillaCommand{
$sender->sendMessage(KnownTranslationFactory::commands_help_header((string) $pageNumber, (string) count($commands))); $sender->sendMessage(KnownTranslationFactory::commands_help_header((string) $pageNumber, (string) count($commands)));
$lang = $sender->getLanguage(); $lang = $sender->getLanguage();
if(isset($commands[$pageNumber - 1])){ if(isset($commands[$pageNumber - 1])){
foreach($commands[$pageNumber - 1] as $command){ foreach($commands[$pageNumber - 1] as $commandEntry){
$description = $command->getDescription(); $description = $commandEntry->command->getDescription();
$descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description;
$sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getLabel() . ": " . TextFormat::RESET . $descriptionString); $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $commandEntry->getPreferredAlias() . ": " . TextFormat::RESET . $descriptionString);
} }
} }
return true; return true;
}else{ }else{
if(($cmd = $sender->getServer()->getCommandMap()->getCommand(strtolower($commandName))) instanceof Command){ if(($commandEntry = $commandMap->getEntry(strtolower($commandName))) !== null){
if($cmd->testPermissionSilent($sender)){ if($commandEntry->command->testPermissionSilent($sender)){
$lang = $sender->getLanguage(); $lang = $sender->getLanguage();
$description = $cmd->getDescription(); $description = $commandEntry->command->getDescription();
$descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel()) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($commandEntry->getPreferredAlias())
->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------")); ->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------"));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString)
->prefix(TextFormat::GOLD)); ->prefix(TextFormat::GOLD));
$usage = $cmd->getUsage(); $usage = $commandEntry->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; $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))) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX)))
->prefix(TextFormat::GOLD)); ->prefix(TextFormat::GOLD));
$aliases = $cmd->getAliases(); $aliases = $commandEntry->aliases;
sort($aliases, SORT_NATURAL); sort($aliases, SORT_NATURAL);
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases))
->prefix(TextFormat::GOLD)); ->prefix(TextFormat::GOLD));

View File

@ -39,7 +39,6 @@ class KickCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"kick",
KnownTranslationFactory::pocketmine_command_kick_description(), KnownTranslationFactory::pocketmine_command_kick_description(),
KnownTranslationFactory::commands_kick_usage() KnownTranslationFactory::commands_kick_usage()
); );

View File

@ -35,10 +35,8 @@ class KillCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"kill",
KnownTranslationFactory::pocketmine_command_kill_description(), KnownTranslationFactory::pocketmine_command_kill_description(),
KnownTranslationFactory::pocketmine_command_kill_usage(), KnownTranslationFactory::pocketmine_command_kill_usage()
["suicide"]
); );
$this->setPermissions([DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER]); $this->setPermissions([DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER]);
} }
@ -48,7 +46,7 @@ class KillCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -38,7 +38,6 @@ class ListCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"list",
KnownTranslationFactory::pocketmine_command_list_description() KnownTranslationFactory::pocketmine_command_list_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_LIST); $this->setPermission(DefaultPermissionNames::COMMAND_LIST);

View File

@ -36,7 +36,6 @@ class MeCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"me",
KnownTranslationFactory::pocketmine_command_me_description(), KnownTranslationFactory::pocketmine_command_me_description(),
KnownTranslationFactory::commands_me_usage() KnownTranslationFactory::commands_me_usage()
); );

View File

@ -37,7 +37,6 @@ class OpCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"op",
KnownTranslationFactory::pocketmine_command_op_description(), KnownTranslationFactory::pocketmine_command_op_description(),
KnownTranslationFactory::commands_op_usage() KnownTranslationFactory::commands_op_usage()
); );

View File

@ -34,10 +34,8 @@ class PardonCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"pardon",
KnownTranslationFactory::pocketmine_command_unban_player_description(), KnownTranslationFactory::pocketmine_command_unban_player_description(),
KnownTranslationFactory::commands_unban_usage(), KnownTranslationFactory::commands_unban_usage()
["unban"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER); $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER);
} }

View File

@ -35,10 +35,8 @@ class PardonIpCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"pardon-ip",
KnownTranslationFactory::pocketmine_command_unban_ip_description(), KnownTranslationFactory::pocketmine_command_unban_ip_description(),
KnownTranslationFactory::commands_unbanip_usage(), KnownTranslationFactory::commands_unbanip_usage()
["unban-ip"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_IP); $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_IP);
} }

View File

@ -76,7 +76,6 @@ class ParticleCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"particle",
KnownTranslationFactory::pocketmine_command_particle_description(), KnownTranslationFactory::pocketmine_command_particle_description(),
KnownTranslationFactory::pocketmine_command_particle_usage() KnownTranslationFactory::pocketmine_command_particle_usage()
); );

View File

@ -38,10 +38,8 @@ class PluginsCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"plugins",
KnownTranslationFactory::pocketmine_command_plugins_description(), KnownTranslationFactory::pocketmine_command_plugins_description(),
null, null
["pl"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_PLUGINS); $this->setPermission(DefaultPermissionNames::COMMAND_PLUGINS);
} }

View File

@ -34,7 +34,6 @@ class SaveCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"save-all",
KnownTranslationFactory::pocketmine_command_save_description() KnownTranslationFactory::pocketmine_command_save_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_SAVE_PERFORM); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_PERFORM);

View File

@ -32,7 +32,6 @@ class SaveOffCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"save-off",
KnownTranslationFactory::pocketmine_command_saveoff_description() KnownTranslationFactory::pocketmine_command_saveoff_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_SAVE_DISABLE); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_DISABLE);

View File

@ -32,7 +32,6 @@ class SaveOnCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"save-on",
KnownTranslationFactory::pocketmine_command_saveon_description() KnownTranslationFactory::pocketmine_command_saveon_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_SAVE_ENABLE); $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_ENABLE);

View File

@ -37,7 +37,6 @@ class SayCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"say",
KnownTranslationFactory::pocketmine_command_say_description(), KnownTranslationFactory::pocketmine_command_say_description(),
KnownTranslationFactory::commands_say_usage() KnownTranslationFactory::commands_say_usage()
); );

View File

@ -32,7 +32,6 @@ class SeedCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"seed",
KnownTranslationFactory::pocketmine_command_seed_description() KnownTranslationFactory::pocketmine_command_seed_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_SEED); $this->setPermission(DefaultPermissionNames::COMMAND_SEED);

View File

@ -38,7 +38,6 @@ class SetWorldSpawnCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"setworldspawn",
KnownTranslationFactory::pocketmine_command_setworldspawn_description(), KnownTranslationFactory::pocketmine_command_setworldspawn_description(),
KnownTranslationFactory::commands_setworldspawn_usage() KnownTranslationFactory::commands_setworldspawn_usage()
); );

View File

@ -38,7 +38,6 @@ class SpawnpointCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"spawnpoint",
KnownTranslationFactory::pocketmine_command_spawnpoint_description(), KnownTranslationFactory::pocketmine_command_spawnpoint_description(),
KnownTranslationFactory::commands_spawnpoint_usage() KnownTranslationFactory::commands_spawnpoint_usage()
); );
@ -49,7 +48,7 @@ class SpawnpointCommand extends VanillaCommand{
} }
public function execute(CommandSender $sender, string $commandLabel, array $args){ 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){ if($target === null){
return true; return true;
} }

View File

@ -38,7 +38,6 @@ class StatusCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"status",
KnownTranslationFactory::pocketmine_command_status_description() KnownTranslationFactory::pocketmine_command_status_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_STATUS); $this->setPermission(DefaultPermissionNames::COMMAND_STATUS);

View File

@ -32,7 +32,6 @@ class StopCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"stop",
KnownTranslationFactory::pocketmine_command_stop_description() KnownTranslationFactory::pocketmine_command_stop_description()
); );
$this->setPermission(DefaultPermissionNames::COMMAND_STOP); $this->setPermission(DefaultPermissionNames::COMMAND_STOP);

View File

@ -41,10 +41,8 @@ class TeleportCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"tp",
KnownTranslationFactory::pocketmine_command_tp_description(), KnownTranslationFactory::pocketmine_command_tp_description(),
KnownTranslationFactory::commands_tp_usage(), KnownTranslationFactory::commands_tp_usage()
["teleport"]
); );
$this->setPermissions([ $this->setPermissions([
DefaultPermissionNames::COMMAND_TELEPORT_SELF, DefaultPermissionNames::COMMAND_TELEPORT_SELF,
@ -77,7 +75,7 @@ class TeleportCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($subject === null){
return true; return true;
} }

View File

@ -38,10 +38,8 @@ class TellCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"tell",
KnownTranslationFactory::pocketmine_command_tell_description(), KnownTranslationFactory::pocketmine_command_tell_description(),
KnownTranslationFactory::commands_message_usage(), KnownTranslationFactory::commands_message_usage()
["w", "msg"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_TELL); $this->setPermission(DefaultPermissionNames::COMMAND_TELL);
} }

View File

@ -36,7 +36,6 @@ class TimeCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"time",
KnownTranslationFactory::pocketmine_command_time_description(), KnownTranslationFactory::pocketmine_command_time_description(),
KnownTranslationFactory::pocketmine_command_time_usage() KnownTranslationFactory::pocketmine_command_time_usage()
); );
@ -53,9 +52,10 @@ class TimeCommand extends VanillaCommand{
if(count($args) < 1){ if(count($args) < 1){
throw new InvalidCommandSyntaxException(); throw new InvalidCommandSyntaxException();
} }
$testPermissionCtx = $commandLabel . " " . $args[0];
if($args[0] === "start"){ if($args[0] === "start"){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_START)){ if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_START)){
return true; return true;
} }
foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){
@ -64,7 +64,7 @@ class TimeCommand extends VanillaCommand{
Command::broadcastCommandMessage($sender, "Restarted the time"); Command::broadcastCommandMessage($sender, "Restarted the time");
return true; return true;
}elseif($args[0] === "stop"){ }elseif($args[0] === "stop"){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_STOP)){ if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_STOP)){
return true; return true;
} }
foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){
@ -73,7 +73,7 @@ class TimeCommand extends VanillaCommand{
Command::broadcastCommandMessage($sender, "Stopped the time"); Command::broadcastCommandMessage($sender, "Stopped the time");
return true; return true;
}elseif($args[0] === "query"){ }elseif($args[0] === "query"){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_QUERY)){ if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_QUERY)){
return true; return true;
} }
if($sender instanceof Player){ if($sender instanceof Player){
@ -90,7 +90,7 @@ class TimeCommand extends VanillaCommand{
} }
if($args[0] === "set"){ if($args[0] === "set"){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_SET)){ if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_SET)){
return true; return true;
} }
@ -123,7 +123,7 @@ class TimeCommand extends VanillaCommand{
} }
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_time_set((string) $value)); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_time_set((string) $value));
}elseif($args[0] === "add"){ }elseif($args[0] === "add"){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_ADD)){ if(!$this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_TIME_ADD)){
return true; return true;
} }

View File

@ -62,7 +62,6 @@ class TimingsCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"timings",
KnownTranslationFactory::pocketmine_command_timings_description(), KnownTranslationFactory::pocketmine_command_timings_description(),
KnownTranslationFactory::pocketmine_command_timings_usage() KnownTranslationFactory::pocketmine_command_timings_usage()
); );

View File

@ -35,7 +35,6 @@ class TitleCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"title",
KnownTranslationFactory::pocketmine_command_title_description(), KnownTranslationFactory::pocketmine_command_title_description(),
KnownTranslationFactory::commands_title_usage() KnownTranslationFactory::commands_title_usage()
); );
@ -50,7 +49,7 @@ class TitleCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -34,7 +34,6 @@ class TransferServerCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"transferserver",
KnownTranslationFactory::pocketmine_command_transferserver_description(), KnownTranslationFactory::pocketmine_command_transferserver_description(),
KnownTranslationFactory::pocketmine_command_transferserver_usage() KnownTranslationFactory::pocketmine_command_transferserver_usage()
); );

View File

@ -36,7 +36,13 @@ abstract class VanillaCommand extends Command{
public const MAX_COORD = 30000000; public const MAX_COORD = 30000000;
public const MIN_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){ if($target !== null){
$player = $sender->getServer()->getPlayerByPrefix($target); $player = $sender->getServer()->getPlayerByPrefix($target);
}elseif($sender instanceof Player){ }elseif($sender instanceof Player){
@ -49,9 +55,12 @@ abstract class VanillaCommand extends Command{
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return null; 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( if(
($player === $sender && $this->testPermission($sender, $selfPermission)) || ($player === $sender && $this->testPermission($testPermissionContext, $sender, $selfPermission)) ||
($player !== $sender && $this->testPermission($sender, $otherPermission)) ($player !== $sender && $this->testPermission($testPermissionContext, $sender, $otherPermission))
){ ){
return $player; return $player;
} }

View File

@ -42,10 +42,8 @@ class VersionCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"version",
KnownTranslationFactory::pocketmine_command_version_description(), KnownTranslationFactory::pocketmine_command_version_description(),
KnownTranslationFactory::pocketmine_command_version_usage(), KnownTranslationFactory::pocketmine_command_version_usage()
["ver", "about"]
); );
$this->setPermission(DefaultPermissionNames::COMMAND_VERSION); $this->setPermission(DefaultPermissionNames::COMMAND_VERSION);
} }

View File

@ -41,7 +41,6 @@ class WhitelistCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"whitelist",
KnownTranslationFactory::pocketmine_command_whitelist_description(), KnownTranslationFactory::pocketmine_command_whitelist_description(),
KnownTranslationFactory::commands_whitelist_usage() KnownTranslationFactory::commands_whitelist_usage()
); );
@ -56,10 +55,14 @@ class WhitelistCommand extends VanillaCommand{
} }
public function execute(CommandSender $sender, string $commandLabel, array $args){ public function execute(CommandSender $sender, string $commandLabel, array $args){
if(count($args) === 0){
throw new InvalidCommandSyntaxException();
}
$testPermissionCtx = $commandLabel . " " . $args[0];
if(count($args) === 1){ if(count($args) === 1){
switch(strtolower($args[0])){ switch(strtolower($args[0])){
case "reload": case "reload":
if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_RELOAD)){ if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_RELOAD)){
$server = $sender->getServer(); $server = $sender->getServer();
$server->getWhitelisted()->reload(); $server->getWhitelisted()->reload();
if($server->hasWhitelist()){ if($server->hasWhitelist()){
@ -70,7 +73,7 @@ class WhitelistCommand extends VanillaCommand{
return true; return true;
case "on": case "on":
if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ENABLE)){ if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_ENABLE)){
$server = $sender->getServer(); $server = $sender->getServer();
$server->getConfigGroup()->setConfigBool(ServerProperties::WHITELIST, true); $server->getConfigGroup()->setConfigBool(ServerProperties::WHITELIST, true);
$this->kickNonWhitelistedPlayers($server); $this->kickNonWhitelistedPlayers($server);
@ -79,14 +82,14 @@ class WhitelistCommand extends VanillaCommand{
return true; return true;
case "off": 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); $sender->getServer()->getConfigGroup()->setConfigBool(ServerProperties::WHITELIST, false);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_disabled()); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_disabled());
} }
return true; return true;
case "list": 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); $entries = $sender->getServer()->getWhitelisted()->getAll(true);
sort($entries, SORT_STRING); sort($entries, SORT_STRING);
$result = implode(", ", $entries); $result = implode(", ", $entries);
@ -112,14 +115,14 @@ class WhitelistCommand extends VanillaCommand{
} }
switch(strtolower($args[0])){ switch(strtolower($args[0])){
case "add": case "add":
if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ADD)){ if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_ADD)){
$sender->getServer()->addWhitelist($args[1]); $sender->getServer()->addWhitelist($args[1]);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_add_success($args[1])); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_add_success($args[1]));
} }
return true; return true;
case "remove": case "remove":
if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_REMOVE)){ if($this->testPermission($testPermissionCtx, $sender, DefaultPermissionNames::COMMAND_WHITELIST_REMOVE)){
$server = $sender->getServer(); $server = $sender->getServer();
$server->removeWhitelist($args[1]); $server->removeWhitelist($args[1]);
if(!$server->isWhitelisted($args[1])){ if(!$server->isWhitelisted($args[1])){

View File

@ -40,7 +40,6 @@ class XpCommand extends VanillaCommand{
public function __construct(){ public function __construct(){
parent::__construct( parent::__construct(
"xp",
KnownTranslationFactory::pocketmine_command_xp_description(), KnownTranslationFactory::pocketmine_command_xp_description(),
KnownTranslationFactory::pocketmine_command_xp_usage() KnownTranslationFactory::pocketmine_command_xp_usage()
); );
@ -55,7 +54,7 @@ class XpCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException(); 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){ if($player === null){
return true; return true;
} }

View File

@ -117,13 +117,14 @@ use pocketmine\world\format\io\GlobalItemDataHandlers;
use pocketmine\world\Position; use pocketmine\world\Position;
use pocketmine\world\World; use pocketmine\world\World;
use pocketmine\YmlServerProperties; use pocketmine\YmlServerProperties;
use function array_filter;
use function array_map; use function array_map;
use function array_values;
use function base64_encode; use function base64_encode;
use function bin2hex; use function bin2hex;
use function count; use function count;
use function get_class; use function get_class;
use function implode; use function implode;
use function in_array;
use function is_string; use function is_string;
use function json_encode; use function json_encode;
use function ord; use function ord;
@ -1088,23 +1089,23 @@ class NetworkSession{
public function syncAvailableCommands() : void{ public function syncAvailableCommands() : void{
$commandData = []; $commandData = [];
foreach($this->server->getCommandMap()->getCommands() as $command){ foreach($this->server->getCommandMap()->getUniqueCommands() as $commandEntry){
if(isset($commandData[$command->getLabel()]) || $command->getLabel() === "help" || !$command->testPermissionSilent($this->player)){ if(!$commandEntry->command->testPermissionSilent($this->player)){
continue; continue;
} }
$lname = strtolower($command->getLabel()); //the client doesn't like it when we override /help
$aliases = $command->getAliases(); $aliases = array_values(array_filter($commandEntry->aliases, fn(string $alias) => $alias !== "help" && $alias !== "?"));
$aliasObj = null; if(count($aliases) === 0){
if(count($aliases) > 0){ continue;
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);
} }
$firstNetworkAlias = $aliases[0];
//use filtered aliases for command name discovery - this allows /help to still be shown as /pocketmine:help
//on the client without conflicting with the client's built-in /help command
$lname = strtolower($firstNetworkAlias);
$aliasObj = new CommandEnum(ucfirst($firstNetworkAlias) . "Aliases", $aliases);
$description = $command->getDescription(); $description = $commandEntry->command->getDescription();
$data = new CommandData( $data = new CommandData(
$lname, //TODO: commands containing uppercase letters in the name crash 1.9.0 client $lname, //TODO: commands containing uppercase letters in the name crash 1.9.0 client
$description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description, $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description,
@ -1117,7 +1118,7 @@ class NetworkSession{
chainedSubCommandData: [] chainedSubCommandData: []
); );
$commandData[$command->getLabel()] = $data; $commandData[] = $data;
} }
$this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], [])); $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));

View File

@ -34,7 +34,6 @@ use pocketmine\utils\Config;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path; use Symfony\Component\Filesystem\Path;
use function copy; use function copy;
use function count;
use function dirname; use function dirname;
use function file_exists; use function file_exists;
use function fopen; use function fopen;
@ -146,23 +145,12 @@ abstract class PluginBase implements Plugin, CommandExecutor{
* Registers commands declared in the plugin manifest * Registers commands declared in the plugin manifest
*/ */
private function registerYamlCommands() : void{ private function registerYamlCommands() : void{
$pluginCmds = [];
foreach(Utils::stringifyKeys($this->description->getCommands()) as $key => $data){ foreach(Utils::stringifyKeys($this->description->getCommands()) as $key => $data){
if(str_contains($key, ":")){ if(str_contains($key, ":")){
$this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->description->getFullName(), ":"))); $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->description->getFullName(), ":")));
continue; continue;
} }
$newCmd = new PluginCommand($key, $this, $this);
if(($description = $data->getDescription()) !== null){
$newCmd->setDescription($description);
}
if(($usageMessage = $data->getUsageMessage()) !== null){
$newCmd->setUsage($usageMessage);
}
$aliasList = []; $aliasList = [];
foreach($data->getAliases() as $alias){ foreach($data->getAliases() as $alias){
if(str_contains($alias, ":")){ if(str_contains($alias, ":")){
@ -172,7 +160,13 @@ abstract class PluginBase implements Plugin, CommandExecutor{
$aliasList[] = $alias; $aliasList[] = $alias;
} }
$newCmd->setAliases($aliasList); $newCmd = new PluginCommand(
$key,
$this,
$this,
$data->getDescription() ?? "",
$data->getUsageMessage() ?? ""
);
$newCmd->setPermission($data->getPermission()); $newCmd->setPermission($data->getPermission());
@ -180,11 +174,7 @@ abstract class PluginBase implements Plugin, CommandExecutor{
$newCmd->setPermissionMessage($permissionDeniedMessage); $newCmd->setPermissionMessage($permissionDeniedMessage);
} }
$pluginCmds[] = $newCmd; $this->server->getCommandMap()->register($this->description->getName(), $newCmd, $key, $aliasList);
}
if(count($pluginCmds) > 0){
$this->server->getCommandMap()->registerAll($this->description->getName(), $pluginCmds);
} }
} }