From c4fb8832fe99e042801dca60f124b7938c94036f Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 14 Jun 2025 16:45:23 +0100 Subject: [PATCH] Add support for base text formats for Translatable fixes #5342 and various other issues related to formatting codes in user input --- src/command/Command.php | 4 ++-- src/command/FormattedCommandAlias.php | 2 +- src/command/SimpleCommandMap.php | 2 +- src/command/defaults/ClearCommand.php | 6 +++--- src/command/defaults/EffectCommand.php | 2 +- src/command/defaults/GiveCommand.php | 4 ++-- src/command/defaults/HelpCommand.php | 8 ++++---- src/command/defaults/ParticleCommand.php | 2 +- src/command/defaults/SayCommand.php | 2 +- src/command/defaults/TellCommand.php | 4 ++-- src/command/defaults/VanillaCommand.php | 4 ++-- src/lang/Language.php | 24 +++++++++++++++++++----- src/lang/Translatable.php | 14 +++++++++++++- src/network/mcpe/NetworkSession.php | 10 +++++++--- src/player/Player.php | 4 ++-- src/utils/TextFormat.php | 6 ++++-- 16 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/command/Command.php b/src/command/Command.php index 54822d80e..4016b4e0a 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -123,7 +123,7 @@ abstract class Command{ } if($this->permissionMessage === null){ - $target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->prefix(TextFormat::RED)); + $target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->baseTextFormat(TextFormat::RED)); }elseif($this->permissionMessage !== ""){ $target->sendMessage(str_replace("", $permission ?? implode(";", $this->permission), $this->permissionMessage)); } @@ -237,7 +237,7 @@ abstract class Command{ public static function broadcastCommandMessage(CommandSender $source, Translatable|string $message, bool $sendToSource = true) : void{ $users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE); $result = KnownTranslationFactory::chat_type_admin($source->getName(), $message); - $colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC); + $colored = $result->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC); if($sendToSource){ $source->sendMessage($message); diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php index b47363397..980889856 100644 --- a/src/command/FormattedCommandAlias.php +++ b/src/command/FormattedCommandAlias.php @@ -107,7 +107,7 @@ class FormattedCommandAlias extends Command{ $timings->stopTiming(); } }else{ - $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->prefix(TextFormat::RED))); + $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->baseTextFormat(TextFormat::RED))); //to match the behaviour of SimpleCommandMap::dispatch() //this shouldn't normally happen, but might happen if the command was unregistered or modified after diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index 9f5441746..5bad1dc42 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -226,7 +226,7 @@ class SimpleCommandMap implements CommandMap{ return true; } - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->baseTextFormat(TextFormat::RED)); return false; } diff --git a/src/command/defaults/ClearCommand.php b/src/command/defaults/ClearCommand.php index f6f491fbf..14ec90c39 100644 --- a/src/command/defaults/ClearCommand.php +++ b/src/command/defaults/ClearCommand.php @@ -69,7 +69,7 @@ class ClearCommand extends VanillaCommand{ } }catch(LegacyStringToItemParserException $e){ //vanilla checks this at argument parsing layer, can't come up with a better alternative - $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED)); return true; } } @@ -90,7 +90,7 @@ class ClearCommand extends VanillaCommand{ if($count > 0){ $sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count)); }else{ - $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED)); } return true; @@ -132,7 +132,7 @@ class ClearCommand extends VanillaCommand{ if($clearedCount > 0){ Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount)); }else{ - $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED)); } return true; diff --git a/src/command/defaults/EffectCommand.php b/src/command/defaults/EffectCommand.php index 938323222..210fb370a 100644 --- a/src/command/defaults/EffectCommand.php +++ b/src/command/defaults/EffectCommand.php @@ -68,7 +68,7 @@ class EffectCommand extends VanillaCommand{ $effect = StringToEffectParser::getInstance()->parse($args[1]); if($effect === null){ - $sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->baseTextFormat(TextFormat::RED)); return true; } diff --git a/src/command/defaults/GiveCommand.php b/src/command/defaults/GiveCommand.php index 72d33688b..c582ab64f 100644 --- a/src/command/defaults/GiveCommand.php +++ b/src/command/defaults/GiveCommand.php @@ -66,7 +66,7 @@ class GiveCommand extends VanillaCommand{ try{ $item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]); }catch(LegacyStringToItemParserException $e){ - $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED)); return true; } @@ -101,7 +101,7 @@ class GiveCommand extends VanillaCommand{ $player->getInventory()->addItem($item); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_give_success( - $item->getName() . " (" . $args[1] . ")", + $item->getName() . TextFormat::RESET . " (" . $args[1] . ")", (string) $item->getCount(), $player->getName() )); diff --git a/src/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php index 054585455..13a96204a 100644 --- a/src/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -105,22 +105,22 @@ class HelpCommand extends VanillaCommand{ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel()) ->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------")); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString) - ->prefix(TextFormat::GOLD)); + ->baseTextFormat(TextFormat::GOLD)); $usage = $cmd->getUsage(); $usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX))) - ->prefix(TextFormat::GOLD)); + ->baseTextFormat(TextFormat::GOLD)); $aliases = $cmd->getAliases(); sort($aliases, SORT_NATURAL); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) - ->prefix(TextFormat::GOLD)); + ->baseTextFormat(TextFormat::GOLD)); return true; } } - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->baseTextFormat(TextFormat::RED)); return true; } diff --git a/src/command/defaults/ParticleCommand.php b/src/command/defaults/ParticleCommand.php index 4867e3eb5..5c58b9505 100644 --- a/src/command/defaults/ParticleCommand.php +++ b/src/command/defaults/ParticleCommand.php @@ -114,7 +114,7 @@ class ParticleCommand extends VanillaCommand{ $particle = $this->getParticle($name, $data); if($particle === null){ - $sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->baseTextFormat(TextFormat::RED)); return true; } diff --git a/src/command/defaults/SayCommand.php b/src/command/defaults/SayCommand.php index 5c3203b5f..abba8f6ae 100644 --- a/src/command/defaults/SayCommand.php +++ b/src/command/defaults/SayCommand.php @@ -52,7 +52,7 @@ class SayCommand extends VanillaCommand{ $sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_announcement( $sender instanceof Player ? $sender->getDisplayName() : ($sender instanceof ConsoleCommandSender ? "Server" : $sender->getName()), implode(" ", $args) - )->prefix(TextFormat::LIGHT_PURPLE)); + )->baseTextFormat(TextFormat::LIGHT_PURPLE)); return true; } } diff --git a/src/command/defaults/TellCommand.php b/src/command/defaults/TellCommand.php index 713023382..7a710f568 100644 --- a/src/command/defaults/TellCommand.php +++ b/src/command/defaults/TellCommand.php @@ -60,9 +60,9 @@ class TellCommand extends VanillaCommand{ if($player instanceof Player){ $message = implode(" ", $args); - $sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC)); + $sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC)); $name = $sender instanceof Player ? $sender->getDisplayName() : $sender->getName(); - $player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC)); + $player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC)); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message), false); }else{ $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()); diff --git a/src/command/defaults/VanillaCommand.php b/src/command/defaults/VanillaCommand.php index 9bdd63739..704f08d67 100644 --- a/src/command/defaults/VanillaCommand.php +++ b/src/command/defaults/VanillaCommand.php @@ -99,11 +99,11 @@ abstract class VanillaCommand extends Command{ $v = (int) $input; if($v > $max){ - $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->baseTextFormat(TextFormat::RED)); return null; } if($v < $min){ - $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->prefix(TextFormat::RED)); + $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->baseTextFormat(TextFormat::RED)); return null; } diff --git a/src/lang/Language.php b/src/lang/Language.php index 59a309524..9e241d1aa 100644 --- a/src/lang/Language.php +++ b/src/lang/Language.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\lang; +use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; use Symfony\Component\Filesystem\Path; use function array_filter; @@ -141,18 +142,26 @@ class Language{ /** * @param (float|int|string|Translatable)[] $params */ - public function translateString(string $str, array $params = [], ?string $onlyPrefix = null) : string{ - $baseText = ($onlyPrefix === null || str_starts_with($str, $onlyPrefix)) ? $this->internalGet($str) : null; - if($baseText === null){ //key not found, embedded inside format string, or doesn't match prefix + public function translateString(string $str, array $params = [], ?string $onlyPrefix = null, string $baseFormat = "") : string{ + if($onlyPrefix !== null && !str_starts_with($str, $onlyPrefix)){ + //plain key for client-side translation + //% is added here if we add base format since this will turn into an embedded key + return $baseFormat !== "" ? TextFormat::addBase($baseFormat, "%" . $str) : $str; + } + $baseText = $this->internalGet($str); + if($baseText === null){ //key not found, embedded inside format string with %, or doesn't match prefix $baseText = $this->parseTranslation($str, $onlyPrefix); } foreach(Utils::promoteKeys($params) as $i => $p){ $replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p; + if($baseFormat !== ""){ + $replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET; + } $baseText = str_replace("{%$i}", $replacement, $baseText); } - return $baseText; + return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText; } public function translate(Translatable $c) : string{ @@ -161,12 +170,17 @@ class Language{ $baseText = $this->parseTranslation($c->getText()); } + $baseFormat = $c->getBaseFormat(); + foreach(Utils::promoteKeys($c->getParameters()) as $i => $p){ $replacement = $p instanceof Translatable ? $this->translate($p) : $p; + if($baseFormat !== ""){ + $replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET; + } $baseText = str_replace("{%$i}", $replacement, $baseText); } - return $baseText; + return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText; } protected function internalGet(string $id) : ?string{ diff --git a/src/lang/Translatable.php b/src/lang/Translatable.php index 8dee8f477..095d9dceb 100644 --- a/src/lang/Translatable.php +++ b/src/lang/Translatable.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\lang; +use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; final class Translatable{ @@ -34,7 +35,8 @@ final class Translatable{ */ public function __construct( protected string $text, - array $params = [] + array $params = [], + private string $baseFormat = "" ){ foreach(Utils::promoteKeys($params) as $k => $param){ if(!($param instanceof Translatable)){ @@ -60,6 +62,8 @@ final class Translatable{ return $this->params[$i] ?? null; } + public function getBaseFormat() : string{ return $this->baseFormat; } + public function format(string $before, string $after) : self{ return new self("$before%$this->text$after", $this->params); } @@ -71,4 +75,12 @@ final class Translatable{ public function postfix(string $postfix) : self{ return new self("%$this->text" . $postfix); } + + /** + * Sets the base format to be applied to the translation result by {@link TextFormat::addBase()}. + * Any existing base format is overwritten. + */ + public function baseTextFormat(string $baseFormat) : self{ + return new self($this->text, $this->params, $baseFormat); + } } diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index bea3f8131..65c93216c 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -767,7 +767,7 @@ class NetworkSession{ $errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4)); $this->disconnect( - reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED), + reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->baseTextFormat(TextFormat::RED), disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId), ); } @@ -1130,8 +1130,12 @@ class NetworkSession{ public function prepareClientTranslatableMessage(Translatable $message) : array{ //we can't send nested translations to the client, so make sure they are always pre-translated by the server $language = $this->player->getLanguage(); - $parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters()); - return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters]; + $baseFormat = $message->getBaseFormat(); + $parameters = array_map(function(string|Translatable $p) use ($baseFormat, $language){ + $string = $p instanceof Translatable ? $language->translate($p) : $p; + return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $string) . TextFormat::RESET : $string; + }, $message->getParameters()); + return [$language->translateString($message->getText(), $parameters, "pocketmine.", $baseFormat), $parameters]; } public function onChatMessage(Translatable|string $message) : void{ diff --git a/src/player/Player.php b/src/player/Player.php index aa2d2af88..b9e08f6d8 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -411,7 +411,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ public function getLeaveMessage() : Translatable|string{ if($this->spawned){ - return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW); + return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW); } return ""; @@ -946,7 +946,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ }); $ev = new PlayerJoinEvent($this, - KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW) + KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW) ); $ev->call(); if($ev->getJoinMessage() !== ""){ diff --git a/src/utils/TextFormat.php b/src/utils/TextFormat.php index 0c948592a..1c921df0a 100644 --- a/src/utils/TextFormat.php +++ b/src/utils/TextFormat.php @@ -190,8 +190,10 @@ abstract class TextFormat{ * - Base format "§c" (red) + "Hello" (no format) = "§r§cHello" * - Base format "§c" + "Hello §rWorld" = "§r§cHello §r§cWorld" * - * Note: Adding base formatting to the output string a second time will result in a combination of formats from both - * calls. This is not by design, but simply a consequence of the way the function is implemented. + * Note: Adding base formatting to the output string a second time won't override conflicting formatting from the + * earlier call (e.g. adding base format BLUE to a string which already has YELLOW base formatting will + * still result in yellow text after any RESET code). However, complementary codes (e.g. italic, bold) will combine + * with the existing codes (e.g. adding ITALIC to a string with base format YELLOW will give yellow & italic text). */ public static function addBase(string $baseFormat, string $string) : string{ $baseFormatParts = self::tokenize($baseFormat);