*/ private array $uniqueCommands = []; private CommandAliasMap $aliasMap; public function __construct(private Server $server){ $this->aliasMap = new CommandAliasMap(); $this->setDefaultCommands(); } private function setDefaultCommands() : void{ $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 register(Command $command, array $otherAliases = []) : void{ if(count($command->getPermissions()) === 0){ throw new \InvalidArgumentException("Commands must have a permission set"); } $commandId = $command->getId(); if(isset($this->uniqueCommands[$commandId])){ throw new \InvalidArgumentException("A command with ID $commandId has already been registered"); } $preferredAlias = trim($command->getName()); $this->aliasMap->bindAlias($commandId, $preferredAlias, override: false); foreach($otherAliases as $alias){ $this->aliasMap->bindAlias($commandId, $alias, override: false); } $this->uniqueCommands[$commandId] = $command; } public function unregister(Command $command) : bool{ unset($this->uniqueCommands[$command->getId()]); $this->aliasMap->unbindAliasesForCommand($command->getId()); return true; } public function dispatch(CommandSender $sender, string $commandLine) : bool{ $args = CommandStringHelper::parseQuoteAware($commandLine); $sentCommandLabel = array_shift($args); 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($sentCommandLabel, $sender)){ $target->execute($sender, $sentCommandLabel, $args); } }catch(InvalidCommandSyntaxException $e){ //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; } //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; } /** * 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(); } } 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, ?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 getUniqueCommands() : array{ return $this->uniqueCommands; } public function registerServerAliases() : void{ $values = $this->server->getCommandAliases(); foreach(Utils::stringifyKeys($values) as $alias => $commandStrings){ if(str_contains($alias, ":")){ $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_illegal($alias))); continue; } $targets = []; $bad = []; $recursive = []; foreach($commandStrings as $commandString){ $args = CommandStringHelper::parseQuoteAware($commandString); $commandName = array_shift($args) ?? ""; $command = $this->getCommand($commandName); if(!$command instanceof Command){ $bad[] = $commandString; }elseif(strcasecmp($commandName, $alias) === 0){ $recursive[] = $commandString; }else{ $targets[] = $commandString; } } if(count($recursive) > 0){ $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_recursive($alias, implode(", ", $recursive)))); continue; } if(count($bad) > 0){ $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_notFound($alias, implode(", ", $bad)))); continue; } //These registered commands have absolute priority $lowerAlias = strtolower($alias); if(count($targets) > 0){ $aliasInstance = new FormattedCommandAlias("pocketmine-config-defined", $lowerAlias, $targets); $this->aliasMap->bindAlias($aliasInstance->getId(), $lowerAlias, override: true); $this->uniqueCommands[$aliasInstance->getId()] = $aliasInstance; }else{ //no targets blackholes the alias - this allows config to delete unwanted aliases $this->aliasMap->unbindAlias($lowerAlias); } } } public function getAliasMap() : CommandAliasMap{ return $this->aliasMap; } }