diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 304dddad8..b358a63d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -202,6 +202,8 @@ jobs: with: php-version: 8.0 tools: php-cs-fixer:3.11 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run PHP-CS-Fixer run: php-cs-fixer fix --dry-run --diff --ansi diff --git a/composer.lock b/composer.lock index b91c8901b..75639aa99 100644 --- a/composer.lock +++ b/composer.lock @@ -1825,16 +1825,16 @@ }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "cd9c6938f8bbfcb6da3ed5a3c7ea60873825d088" + "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/cd9c6938f8bbfcb6da3ed5a3c7ea60873825d088", - "reference": "cd9c6938f8bbfcb6da3ed5a3c7ea60873825d088", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/54a24bd23e9e80ee918cdc24f909d376c2e273f7", + "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7", "shasum": "" }, "require": { @@ -1871,9 +1871,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.2" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.3" }, - "time": "2022-12-13T15:08:22+00:00" + "time": "2022-12-21T15:25:00+00:00" }, { "name": "phpstan/phpstan-strict-rules", diff --git a/src/command/defaults/GamemodeCommand.php b/src/command/defaults/GamemodeCommand.php index 77fade0b0..70ac6d1a6 100644 --- a/src/command/defaults/GamemodeCommand.php +++ b/src/command/defaults/GamemodeCommand.php @@ -68,6 +68,11 @@ class GamemodeCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } + if($target->getGamemode()->equals($gameMode)){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_failure($target->getName())); + return true; + } + $target->setGamemode($gameMode); if(!$gameMode->equals($target->getGamemode())){ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_failure($target->getName())); diff --git a/src/event/HandlerList.php b/src/event/HandlerList.php index 57388cef5..8a3fb8d92 100644 --- a/src/event/HandlerList.php +++ b/src/event/HandlerList.php @@ -31,6 +31,10 @@ class HandlerList{ /** @var RegisteredListener[][] */ private array $handlerSlots = []; + /** + * @phpstan-template TEvent of Event + * @phpstan-param class-string $class + */ public function __construct( private string $class, private ?HandlerList $parentList diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 592fea262..dd88a0a84 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -746,9 +746,9 @@ class NetworkSession{ $this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager)); } - public function onServerDeath() : void{ + public function onServerDeath(Translatable|string $deathMessage) : void{ if($this->handler instanceof InGamePacketHandler){ //TODO: this is a bad fix for pre-spawn death, this shouldn't be reachable at all at this stage :( - $this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError())); + $this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError(), $deathMessage)); } } @@ -933,15 +933,22 @@ class NetworkSession{ $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], [])); } - public function onRawChatMessage(string $message) : void{ - $this->sendDataPacket(TextPacket::raw($message)); - } - - /** - * @param string[] $parameters - */ - public function onTranslatedChatMessage(string $key, array $parameters) : void{ - $this->sendDataPacket(TextPacket::translation($key, $parameters)); + public function onChatMessage(Translatable|string $message) : void{ + if($message instanceof Translatable){ + //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()); + if(!$this->server->isLanguageForced()){ + foreach($parameters as $i => $p){ + $parameters[$i] = $language->translateString($p, [], "pocketmine."); + } + $this->sendDataPacket(TextPacket::translation($language->translateString($message->getText(), $parameters, "pocketmine."), $parameters)); + }else{ + $this->sendDataPacket(TextPacket::raw($language->translateString($message->getText(), $parameters))); + } + }else{ + $this->sendDataPacket(TextPacket::raw($message)); + } } /** diff --git a/src/network/mcpe/handler/DeathPacketHandler.php b/src/network/mcpe/handler/DeathPacketHandler.php index 0ae5f444f..43deec463 100644 --- a/src/network/mcpe/handler/DeathPacketHandler.php +++ b/src/network/mcpe/handler/DeathPacketHandler.php @@ -23,19 +23,23 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\handler; +use pocketmine\lang\Translatable; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\ContainerClosePacket; +use pocketmine\network\mcpe\protocol\DeathInfoPacket; use pocketmine\network\mcpe\protocol\PlayerActionPacket; use pocketmine\network\mcpe\protocol\RespawnPacket; use pocketmine\network\mcpe\protocol\types\PlayerAction; use pocketmine\player\Player; +use function array_map; class DeathPacketHandler extends PacketHandler{ public function __construct( private Player $player, private NetworkSession $session, - private InventoryManager $inventoryManager + private InventoryManager $inventoryManager, + private Translatable|string $deathMessage ){} public function setUp() : void{ @@ -44,6 +48,26 @@ class DeathPacketHandler extends PacketHandler{ RespawnPacket::SEARCHING_FOR_SPAWN, $this->player->getId() )); + + /** @var string[] $parameters */ + $parameters = []; + if($this->deathMessage instanceof Translatable){ + //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, $this->deathMessage->getParameters()); + if(!$this->player->getServer()->isLanguageForced()){ + foreach($parameters as $i => $p){ + $parameters[$i] = $language->translateString($p, [], "pocketmine."); + } + $message = $language->translateString($this->deathMessage->getText(), $parameters, "pocketmine."); + }else{ + $message = $language->translateString($this->deathMessage->getText(), $parameters); + $parameters = []; + } + }else{ + $message = $this->deathMessage; + } + $this->session->sendDataPacket(DeathInfoPacket::create($message, $parameters)); } public function handlePlayerAction(PlayerActionPacket $packet) : bool{ diff --git a/src/permission/PermissionParser.php b/src/permission/PermissionParser.php index 0c08702c8..bbc68aaa5 100644 --- a/src/permission/PermissionParser.php +++ b/src/permission/PermissionParser.php @@ -53,16 +53,16 @@ class PermissionParser{ "false" => self::DEFAULT_FALSE, ]; + private const KEY_DEFAULT = "default"; + private const KEY_CHILDREN = "children"; + private const KEY_DESCRIPTION = "description"; + /** * @throws PermissionParserException */ public static function defaultFromString(bool|string $value) : string{ if(is_bool($value)){ - if($value){ - return "true"; - }else{ - return "false"; - } + return $value ? self::DEFAULT_TRUE : self::DEFAULT_FALSE; } $lower = strtolower($value); if(isset(self::DEFAULT_STRING_MAP[$lower])){ @@ -84,16 +84,16 @@ class PermissionParser{ $result = []; foreach(Utils::stringifyKeys($data) as $name => $entry){ $desc = null; - if(isset($entry["default"])){ - $default = PermissionParser::defaultFromString($entry["default"]); + if(isset($entry[self::KEY_DEFAULT])){ + $default = PermissionParser::defaultFromString($entry[self::KEY_DEFAULT]); } - if(isset($entry["children"])){ + if(isset($entry[self::KEY_CHILDREN])){ throw new PermissionParserException("Nested permission declarations are no longer supported. Declare each permission separately."); } - if(isset($entry["description"])){ - $desc = $entry["description"]; + if(isset($entry[self::KEY_DESCRIPTION])){ + $desc = $entry[self::KEY_DESCRIPTION]; } $result[$default][] = new Permission($name, $desc); diff --git a/src/player/Player.php b/src/player/Player.php index 5b352acf7..507747b0f 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -132,7 +132,6 @@ use pocketmine\world\World; use Ramsey\Uuid\UuidInterface; use function abs; use function array_filter; -use function array_map; use function array_shift; use function assert; use function count; @@ -2012,28 +2011,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ * Sends a direct chat message to a player */ public function sendMessage(Translatable|string $message) : void{ - if($message instanceof Translatable){ - $this->sendTranslation($message->getText(), $message->getParameters()); - return; - } - - $this->getNetworkSession()->onRawChatMessage($message); + $this->getNetworkSession()->onChatMessage($message); } /** + * @deprecated Use {@link Player::sendMessage()} with a Translatable instead. * @param string[]|Translatable[] $parameters */ public function sendTranslation(string $message, array $parameters = []) : void{ - //we can't send nested translations to the client, so make sure they are always pre-translated by the server - $parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $this->getLanguage()->translate($p) : $p, $parameters); - if(!$this->server->isLanguageForced()){ - foreach($parameters as $i => $p){ - $parameters[$i] = $this->getLanguage()->translateString($p, [], "pocketmine."); - } - $this->getNetworkSession()->onTranslatedChatMessage($this->getLanguage()->translateString($message, $parameters, "pocketmine."), $parameters); - }else{ - $this->sendMessage($this->getLanguage()->translateString($message, $parameters)); - } + $this->sendMessage(new Translatable($message, $parameters)); } /** @@ -2309,7 +2295,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->startDeathAnimation(); - $this->getNetworkSession()->onServerDeath(); + $this->getNetworkSession()->onServerDeath($ev->getDeathMessage()); } protected function onDeathUpdate(int $tickDiff) : bool{ diff --git a/src/plugin/PluginDescription.php b/src/plugin/PluginDescription.php index 522da2a78..9fc562af9 100644 --- a/src/plugin/PluginDescription.php +++ b/src/plugin/PluginDescription.php @@ -37,6 +37,32 @@ use function stripos; use function yaml_parse; class PluginDescription{ + private const KEY_NAME = "name"; + private const KEY_VERSION = "version"; + private const KEY_MAIN = "main"; + private const KEY_SRC_NAMESPACE_PREFIX = "src-namespace-prefix"; + private const KEY_API = "api"; + private const KEY_MCPE_PROTOCOL = "mcpe-protocol"; + private const KEY_OS = "os"; + private const KEY_DEPEND = "depend"; + private const KEY_SOFTDEPEND = "softdepend"; + private const KEY_LOADBEFORE = "loadbefore"; + private const KEY_EXTENSIONS = "extensions"; + private const KEY_WEBSITE = "website"; + private const KEY_DESCRIPTION = "description"; + private const KEY_LOGGER_PREFIX = "prefix"; + private const KEY_LOAD = "load"; + private const KEY_AUTHOR = "author"; + private const KEY_AUTHORS = "authors"; + private const KEY_PERMISSIONS = "permissions"; + + private const KEY_COMMANDS = "commands"; + private const KEY_COMMAND_PERMISSION = "permission"; + private const KEY_COMMAND_DESCRIPTION = self::KEY_DESCRIPTION; + private const KEY_COMMAND_USAGE = "usage"; + private const KEY_COMMAND_ALIASES = "aliases"; + private const KEY_COMMAND_PERMISSION_MESSAGE = "permission-message"; + /** * @var mixed[] * @phpstan-var array @@ -107,49 +133,49 @@ class PluginDescription{ private function loadMap(array $plugin) : void{ $this->map = $plugin; - $this->name = $plugin["name"]; + $this->name = $plugin[self::KEY_NAME]; if(preg_match('/^[A-Za-z0-9 _.-]+$/', $this->name) === 0){ throw new PluginDescriptionParseException("Invalid Plugin name"); } $this->name = str_replace(" ", "_", $this->name); - $this->version = (string) $plugin["version"]; - $this->main = $plugin["main"]; + $this->version = (string) $plugin[self::KEY_VERSION]; + $this->main = $plugin[self::KEY_MAIN]; if(stripos($this->main, "pocketmine\\") === 0){ throw new PluginDescriptionParseException("Invalid Plugin main, cannot start within the PocketMine namespace"); } - $this->srcNamespacePrefix = $plugin["src-namespace-prefix"] ?? ""; + $this->srcNamespacePrefix = $plugin[self::KEY_SRC_NAMESPACE_PREFIX] ?? ""; - $this->api = array_map("\strval", (array) ($plugin["api"] ?? [])); - $this->compatibleMcpeProtocols = array_map("\intval", (array) ($plugin["mcpe-protocol"] ?? [])); - $this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin["os"] ?? [])); + $this->api = array_map("\strval", (array) ($plugin[self::KEY_API] ?? [])); + $this->compatibleMcpeProtocols = array_map("\intval", (array) ($plugin[self::KEY_MCPE_PROTOCOL] ?? [])); + $this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin[self::KEY_OS] ?? [])); - if(isset($plugin["commands"]) && is_array($plugin["commands"])){ - foreach($plugin["commands"] as $commandName => $commandData){ + if(isset($plugin[self::KEY_COMMANDS]) && is_array($plugin[self::KEY_COMMANDS])){ + foreach($plugin[self::KEY_COMMANDS] as $commandName => $commandData){ if(!is_string($commandName)){ throw new PluginDescriptionParseException("Invalid Plugin commands, key must be the name of the command"); } if(!is_array($commandData)){ throw new PluginDescriptionParseException("Command $commandName has invalid properties"); } - if(!isset($commandData["permission"]) || !is_string($commandData["permission"])){ + if(!isset($commandData[self::KEY_COMMAND_PERMISSION]) || !is_string($commandData[self::KEY_COMMAND_PERMISSION])){ throw new PluginDescriptionParseException("Command $commandName does not have a valid permission set"); } $this->commands[$commandName] = new PluginDescriptionCommandEntry( - $commandData["description"] ?? null, - $commandData["usage"] ?? null, - $commandData["aliases"] ?? [], - $commandData["permission"], - $commandData["permission-message"] ?? null + $commandData[self::KEY_COMMAND_DESCRIPTION] ?? null, + $commandData[self::KEY_COMMAND_USAGE] ?? null, + $commandData[self::KEY_COMMAND_ALIASES] ?? [], + $commandData[self::KEY_COMMAND_PERMISSION], + $commandData[self::KEY_COMMAND_PERMISSION_MESSAGE] ?? null ); } } - if(isset($plugin["depend"])){ - $this->depend = (array) $plugin["depend"]; + if(isset($plugin[self::KEY_DEPEND])){ + $this->depend = (array) $plugin[self::KEY_DEPEND]; } - if(isset($plugin["extensions"])){ - $extensions = (array) $plugin["extensions"]; + if(isset($plugin[self::KEY_EXTENSIONS])){ + $extensions = (array) $plugin[self::KEY_EXTENSIONS]; $isLinear = $extensions === array_values($extensions); foreach($extensions as $k => $v){ if($isLinear){ @@ -160,20 +186,20 @@ class PluginDescription{ } } - $this->softDepend = (array) ($plugin["softdepend"] ?? $this->softDepend); + $this->softDepend = (array) ($plugin[self::KEY_SOFTDEPEND] ?? $this->softDepend); - $this->loadBefore = (array) ($plugin["loadbefore"] ?? $this->loadBefore); + $this->loadBefore = (array) ($plugin[self::KEY_LOADBEFORE] ?? $this->loadBefore); - $this->website = (string) ($plugin["website"] ?? $this->website); + $this->website = (string) ($plugin[self::KEY_WEBSITE] ?? $this->website); - $this->description = (string) ($plugin["description"] ?? $this->description); + $this->description = (string) ($plugin[self::KEY_DESCRIPTION] ?? $this->description); - $this->prefix = (string) ($plugin["prefix"] ?? $this->prefix); + $this->prefix = (string) ($plugin[self::KEY_LOGGER_PREFIX] ?? $this->prefix); - if(isset($plugin["load"])){ - $order = PluginEnableOrder::fromString($plugin["load"]); + if(isset($plugin[self::KEY_LOAD])){ + $order = PluginEnableOrder::fromString($plugin[self::KEY_LOAD]); if($order === null){ - throw new PluginDescriptionParseException("Invalid Plugin \"load\""); + throw new PluginDescriptionParseException("Invalid Plugin \"" . self::KEY_LOAD . "\""); } $this->order = $order; }else{ @@ -181,24 +207,24 @@ class PluginDescription{ } $this->authors = []; - if(isset($plugin["author"])){ - if(is_array($plugin["author"])){ - $this->authors = $plugin["author"]; + if(isset($plugin[self::KEY_AUTHOR])){ + if(is_array($plugin[self::KEY_AUTHOR])){ + $this->authors = $plugin[self::KEY_AUTHOR]; }else{ - $this->authors[] = $plugin["author"]; + $this->authors[] = $plugin[self::KEY_AUTHOR]; } } - if(isset($plugin["authors"])){ - foreach($plugin["authors"] as $author){ + if(isset($plugin[self::KEY_AUTHORS])){ + foreach($plugin[self::KEY_AUTHORS] as $author){ $this->authors[] = $author; } } - if(isset($plugin["permissions"])){ + if(isset($plugin[self::KEY_PERMISSIONS])){ try{ - $this->permissions = PermissionParser::loadPermissions($plugin["permissions"]); + $this->permissions = PermissionParser::loadPermissions($plugin[self::KEY_PERMISSIONS]); }catch(PermissionParserException $e){ - throw new PluginDescriptionParseException("Invalid Plugin \"permissions\": " . $e->getMessage(), 0, $e); + throw new PluginDescriptionParseException("Invalid Plugin \"" . self::KEY_PERMISSIONS . "\": " . $e->getMessage(), 0, $e); } } } diff --git a/src/world/World.php b/src/world/World.php index 5a19195ee..cc29ee8ab 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -1174,6 +1174,8 @@ class World implements ChunkManager{ /** @var bool[] $chunkTickList chunkhash => dummy */ $chunkTickList = []; + $chunkTickableCache = []; + $centerChunks = []; $selector = new ChunkSelector(); @@ -1193,7 +1195,7 @@ class World implements ChunkManager{ $centerChunkZ ) as $hash){ World::getXZ($hash, $chunkX, $chunkZ); - if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($chunkX, $chunkZ)){ + if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){ $chunkTickList[$hash] = true; } } @@ -1208,14 +1210,29 @@ class World implements ChunkManager{ } } - private function isChunkTickable(int $chunkX, int $chunkZ) : bool{ + /** + * @param bool[] &$cache + * + * @phpstan-param array $cache + * @phpstan-param-out array $cache + */ + private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{ for($cx = -1; $cx <= 1; ++$cx){ for($cz = -1; $cz <= 1; ++$cz){ + $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz); + if(isset($cache[$chunkHash])){ + if(!$cache[$chunkHash]){ + return false; + } + continue; + } if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){ + $cache[$chunkHash] = false; return false; } $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz); if($adjacentChunk === null || !$adjacentChunk->isPopulated()){ + $cache[$chunkHash] = false; return false; } $lightPopulatedState = $adjacentChunk->isLightPopulated(); @@ -1223,8 +1240,11 @@ class World implements ChunkManager{ if($lightPopulatedState === false){ $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz); } + $cache[$chunkHash] = false; return false; } + + $cache[$chunkHash] = true; } } @@ -1267,7 +1287,8 @@ class World implements ChunkManager{ private function tickChunk(int $chunkX, int $chunkZ) : void{ $chunk = $this->getChunk($chunkX, $chunkZ); if($chunk === null){ - throw new \InvalidArgumentException("Chunk is not loaded"); + //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent) + return; } foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){ $entity->onRandomUpdate(); diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index 3d1976d7a..38a306f8c 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -617,7 +617,7 @@ parameters: - message: "#^Cannot call method getLanguage\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" - count: 1 + count: 2 path: ../../../src/network/mcpe/NetworkSession.php -