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

Filter by extension

Filter by extension

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

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

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

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

abstract class Command{

private string $name;

private string $nextLabel;
private string $label;

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

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

private ?CommandMap $commandMap = null;

protected Translatable|string $description = "";

protected Translatable|string $usageMessage;

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

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

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

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

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

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

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

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

return true;
}

return false;
}

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

return true;
}

return false;
}

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

return true;
}

return false;
}

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

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

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

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

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

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

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

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

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

public function __toString() : string{
return $this->name;
}
}
160 changes: 160 additions & 0 deletions src/command/CommandAliasMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?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 function array_filter;
use function array_keys;
use function array_values;
use function count;
use function is_array;

final class CommandAliasMap{

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

/**
* @var string[][]
* @phpstan-var array<string, array<string, true>>
*/
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<string>
*/
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<string>|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<string>
*/
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;
}

/**
* @return string[]|string[][]
* @phpstan-return array<string, string|list<string>>
*/
public function getAllAliases() : array{
return $this->aliasToCommandMap;
}
}
Loading