diff --git a/build/generate-known-translation-apis.php b/build/generate-known-translation-apis.php index e799735c0..af617c523 100644 --- a/build/generate-known-translation-apis.php +++ b/build/generate-known-translation-apis.php @@ -25,7 +25,7 @@ namespace pocketmine\build\generate_known_translation_apis; use pocketmine\lang\Translatable; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_map; use function count; use function dirname; diff --git a/changelogs/4.11-beta.md b/changelogs/4.11-beta.md new file mode 100644 index 000000000..0d8bc257c --- /dev/null +++ b/changelogs/4.11-beta.md @@ -0,0 +1,92 @@ +**For Minecraft: Bedrock Edition 1.19.40** + +This is a minor feature release for PocketMine-MP, introducing some new features and improvements. + +### Note about API versions +Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps. +Plugin developers should **only** update their required API to this version if you need the changes in this build. + +**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do. + +# 4.11.0-BETA1 +Released 7th November 2022. + +## General +- Packet receive timings have now been split into two subcategories - Decode and Handle. +- Console command entry can now be disabled via the `console.enable-input` setting in `pocketmine.yml`. + - Best suited for headless servers (e.g. in a Docker container) where the console will never be used anyway. + - Disabling the console reader slightly reduces memory usage, because console reading currently requires an additional subprocess. +- Console command output now appears on the terminal only, and is not written to the log file. +- The output from console commands now appears with a `Command output |` prefix, instead of as a log message. +- Introduced validation for the `--data` and `--plugins` command line options. +- Encrypted resource packs are now supported, by means of adding a `.key` file alongside the pack in the `resource_packs` folder. + - e.g. `MyEncryptedPack.zip` -> `MyEncryptedPack.zip.key` + +## Gameplay +- Fixed supporting blocks of dead bush to be in line with vanilla. +- Sugarcane can now be grown using bonemeal on any part of the sugarcane. Previously, it only worked when used on the bottom block. +- Fixed modifier values for Instant Damage and Regeneration effects. + +## API +### General +- Plugins are now always disabled before their dependencies, to ensure that they are able to shutdown properly (e.g. a core plugin depending on a database plugin may want to save data to a DB during `onDisable()`). +- [`webmozart/path-util`](https://packagist.org/packages/webmozart/path-util) has been deprecated, and will be dropped in favour of [`symfony/filesystem`](https://packagist.org/packages/symfony/filesystem) in PM5. + - To prepare for this change, simply replace any usage of `Webmozart\PathUtil\Path` with `Symfony\Component\Filesystem\Path`, which is available as a dependency in this release. + +### `pocketmine` +- The following API methods are now deprecated: + - `Server->getPlayerByPrefix()` + +### `pocketmine\entity` +- `EntitySpawnEvent` and `ItemSpawnEvent` are now fired on the first tick after the entity is added to the world. Previously, these events were called directly from the entity constructor, making it impossible to get properties like velocity which are often set after the entity is created. +- The following API methods are now deprecated: + - `Living->hasLineOfSight()` + +### `pocketmine\item` +- The following new API methods have been added: + - `public Armor->clearCustomColor() : $this` + +### `pocketmine\inventory\transaction` +- Introduced a `TransactionBuilder` class. This makes it less of a hassle to build an `InventoryTransaction` server-side, since the regular `Inventory` API methods can be used, rather than having to manually create `SlotChangeAction`s. + +### `pocketmine\player` +- The following new API methods have been added: + - `public Player->sendToastNotification(string $title, string $body) : void` - makes a grey box appear at the top of the player's screen containing the specified message + +### `pocketmine\utils` +- The following new API methods have been added: + - `public static TextFormat::addBase(string $baseFormat, string $string) : string` - used for coloured log messages, changes the base formatting of a string by inserting the given formatting codes after every RESET code + +## Internals +- Improved performance of `ContainerTrait` dropping items on block destroy. (24e72ec109c1442b09558df89b6833cf2f2e0ec7) +- Avoid repeated calls to `Position->getWorld()` (use local variables). (2940547026db40ce76deb46e992870de3ead79ad) +- Revamped the way `InventoryManager` handles fake inventory slot mappings for stuff like crafting tables. (e90abecf38d9c57635fa0497514bba7e546a2469) +- Console polling is now done on the main thread (no longer a performance concern). +- Console reader subprocess should now automatically die if the server main process is killed, instead of persisting as a zombie. +- `ConsoleCommandSender` is no longer responsible for relaying broadcast messages to `MainLogger`. A new `BroadcastLoggerForwarder` has been added, which is subscribed to the appropriate server broadcast channels in order to relay messages. This ensures that chat messages and command audit messages are logged. +- `DelegateInventory` now uses `WeakReference` to track its inventory listener. This allows the delegate to be reused. + +# 4.11.0-BETA2 +Released 13th November 2022. + +## Configuration +- The `chunk-ticking.per-tick` setting is now deprecated, and will be removed in a future release. + - The functionality of this setting has been removed, since it caused more problems than it solved. + - Setting it to zero will still disable chunk ticking (for now), but this should now be done by setting `chunk-ticking.tick-radius` to `0` instead. + +## Gameplay +- Improved chunk random ticking: + - Removed the limit on chunks ticked per tick, and its associated config option is no longer respected. + - This change significantly improves crop and plant growth with large numbers of players, but may cause higher CPU usage. + - This limit was causing a linear decrease in chunk ticking speed with larger numbers of players, leading to worsened gameplay experience. + - Every chunk within the configured tick radius of a player will be ticked. Previously, chunks were randomly selected from the radius. +- Implemented Darkness effect. + +## API +### `pocketmine\world` +- The following new API methods have been added: + - `public World->getChunkTickRadius() : int` - returns the world's simulation radius + - `public World->setChunkTickRadius(int $radius) : void` - sets the world's simulation radius + +## Internals +- Non-arrow projectile damage is now unscaled. Scaling according to velocity is only applied to arrows. This currently doesn't cause any observable change in behaviour, but is required for future additions. diff --git a/changelogs/4.11.md b/changelogs/4.11.md new file mode 100644 index 000000000..04de9824d --- /dev/null +++ b/changelogs/4.11.md @@ -0,0 +1,102 @@ +**For Minecraft: Bedrock Edition 1.19.40** + +This is a minor feature release for PocketMine-MP, introducing some new features and improvements. + +### Note about API versions +Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps. +Plugin developers should **only** update their required API to this version if you need the changes in this build. + +**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do. + +# 4.11.0 +Released 25th November 2022. + +## General +- Packet receive timings have now been split into two subcategories - Decode and Handle. +- Console command entry can now be disabled via the `console.enable-input` setting in `pocketmine.yml`. + - Best suited for headless servers (e.g. in a Docker container) where the console will never be used anyway. + - Disabling the console reader slightly reduces memory usage, because console reading currently requires an additional subprocess. +- Console command output now appears on the terminal only, and is not written to the log file. +- The output from console commands now appears with a `Command output |` prefix, instead of as a log message. +- User-defined `pocketmine.yml` custom commands now use a generic description which makes clear the command is config-defined. +- Introduced validation for the `--data` and `--plugins` command line options. +- Encrypted resource packs are now supported, by means of adding a `.key` file alongside the pack in the `resource_packs` folder. + - e.g. `MyEncryptedPack.zip` -> `MyEncryptedPack.zip.key` + - The file must contain the raw key bytes, and must not end with a newline. + +## Configuration +- The `chunk-ticking.per-tick` setting is now deprecated, and will be removed in a future release. + - The functionality of this setting has been removed, since it caused more problems than it solved. + - Setting it to zero will still disable chunk ticking (for now), but this should now be done by setting `chunk-ticking.tick-radius` to `0` instead. + +## Gameplay +- Fixed supporting blocks of dead bush to be in line with vanilla. +- Sugarcane can now be grown using bonemeal on any part of the sugarcane. Previously, it only worked when used on the bottom block. +- Fixed missing sounds when adding, rotating, or removing items in item frames. +- Fixed modifier values for Instant Damage and Regeneration effects. +- Implemented Darkness effect. +- Improved chunk random ticking: + - Removed the limit on chunks ticked per tick, and its associated config option is no longer respected. + - This change significantly improves crop and plant growth with large numbers of players. + - This limit was causing a linear decrease in chunk ticking speed with larger numbers of players, leading to worsened gameplay experience. + - **Warning: This change will result in increased CPU usage if players are spread over a very large area.** + - Every chunk within the configured tick radius of a player will be ticked. Previously, chunks were randomly selected from the radius. + +## API +### General +- Plugins are now always disabled before their dependencies, to ensure that they are able to shutdown properly (e.g. a core plugin depending on a database plugin may want to save data to a DB during `onDisable()`). +- [`webmozart/path-util`](https://packagist.org/packages/webmozart/path-util) has been deprecated, and will be dropped in favour of [`symfony/filesystem`](https://packagist.org/packages/symfony/filesystem) in PM5. + - To prepare for this change, simply replace any usage of `Webmozart\PathUtil\Path` with `Symfony\Component\Filesystem\Path`, which is available as a dependency in this release. + +### `pocketmine` +- The following API methods are now deprecated: + - `Server->getPlayerByPrefix()` + +### `pocketmine\entity` +- `EntitySpawnEvent` and `ItemSpawnEvent` are now fired on the first tick after the entity is added to the world. Previously, these events were called directly from the entity constructor, making it impossible to get properties like velocity which are often set after the entity is created. +- The following API methods are now deprecated: + - `Living->hasLineOfSight()` + +### `pocketmine\event\block` +- The following new classes have been added: + - `BlockDeathEvent` - event called when coral or coral blocks die due to lack of water + +### `pocketmine\item` +- The following new API methods have been added: + - `public Armor->clearCustomColor() : $this` + +### `pocketmine\inventory\transaction` +- Introduced a `TransactionBuilder` class. This makes it less of a hassle to build an `InventoryTransaction` server-side, since the regular `Inventory` API methods can be used, rather than having to manually create `SlotChangeAction`s. + +### `pocketmine\lang` +- The following new API methods have been added: + - `public Language->getAll() : array` + +### `pocketmine\player` +- The following new API methods have been added: + - `public Player->sendToastNotification(string $title, string $body) : void` - makes a grey box appear at the top of the player's screen containing the specified message + +### `pocketmine\utils` +- The following new API methods have been added: + - `public static TextFormat::addBase(string $baseFormat, string $string) : string` - used for coloured log messages, changes the base formatting of a string by inserting the given formatting codes after every RESET code + +### `pocketmine\world` +- The following new API methods have been added: + - `public World->getChunkTickRadius() : int` - returns the world's simulation radius + - `public World->setChunkTickRadius(int $radius) : void` - sets the world's simulation radius + +### `pocketmine\world\sound` +- The following new classes have been added: + - `ItemFrameAddItemSound` + - `ItemFrameRemoveItemSound` + - `ItemFrameRotateItemSound` + +## Internals +- Improved performance of `ContainerTrait` dropping items on block destroy. (24e72ec109c1442b09558df89b6833cf2f2e0ec7) +- Avoid repeated calls to `Position->getWorld()` (use local variables). (2940547026db40ce76deb46e992870de3ead79ad) +- Revamped the way `InventoryManager` handles fake inventory slot mappings for stuff like crafting tables. (e90abecf38d9c57635fa0497514bba7e546a2469) +- Console polling is now done on the main thread (no longer a performance concern). +- Console reader subprocess should now automatically die if the server main process is killed, instead of persisting as a zombie. +- `ConsoleCommandSender` is no longer responsible for relaying broadcast messages to `MainLogger`. A new `BroadcastLoggerForwarder` has been added, which is subscribed to the appropriate server broadcast channels in order to relay messages. This ensures that chat messages and command audit messages are logged. +- `DelegateInventory` now uses `WeakReference` to track its inventory listener. This allows the delegate to be reused. +- Non-arrow projectile damage is now unscaled. Scaling according to velocity is only applied to arrows. This currently doesn't cause any observable change in behaviour, but is required for future additions. \ No newline at end of file diff --git a/composer.json b/composer.json index b30da3832..e47286f4a 100644 --- a/composer.json +++ b/composer.json @@ -35,13 +35,13 @@ "fgrosse/phpasn1": "^2.3", "netresearch/jsonmapper": "^4.0", "pocketmine/bedrock-data": "~1.12.0+bedrock-1.19.40", - "pocketmine/bedrock-protocol": "~14.0.0+bedrock-1.19.40", + "pocketmine/bedrock-protocol": "~16.0.0+bedrock-1.19.40", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/classloader": "^0.2.0", "pocketmine/color": "^0.2.0", "pocketmine/errorhandler": "^0.6.0", - "pocketmine/locale-data": "~2.9.0", + "pocketmine/locale-data": "~2.11.0", "pocketmine/log": "^0.4.0", "pocketmine/log-pthreads": "^0.4.0", "pocketmine/math": "^0.4.0", @@ -50,6 +50,7 @@ "pocketmine/raklib-ipc": "^0.1.0", "pocketmine/snooze": "^0.3.0", "ramsey/uuid": "^4.1", + "symfony/filesystem": "^5.4", "webmozart/path-util": "^2.3" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 7e98bd15b..6843484d2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9d5a8688ca7ac8143921659525641a00", + "content-hash": "ff2bc73e9b0acccb1e63ddef2412fe31", "packages": [ { "name": "adhocore/json-comment", @@ -275,16 +275,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "14.0.0+bedrock-1.19.40", + "version": "16.0.0+bedrock-1.19.40", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "b455a742779fee94d25f931cc2cbf6b2c5d61c1f" + "reference": "ce900ffa6a4cc07af92686f27d580dd2e2541382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/b455a742779fee94d25f931cc2cbf6b2c5d61c1f", - "reference": "b455a742779fee94d25f931cc2cbf6b2c5d61c1f", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/ce900ffa6a4cc07af92686f27d580dd2e2541382", + "reference": "ce900ffa6a4cc07af92686f27d580dd2e2541382", "shasum": "" }, "require": { @@ -298,7 +298,7 @@ "ramsey/uuid": "^4.1" }, "require-dev": { - "phpstan/phpstan": "1.8.8", + "phpstan/phpstan": "1.9.0", "phpstan/phpstan-phpunit": "^1.0.0", "phpstan/phpstan-strict-rules": "^1.0.0", "phpunit/phpunit": "^9.5" @@ -316,9 +316,9 @@ "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "support": { "issues": "https://github.com/pmmp/BedrockProtocol/issues", - "source": "https://github.com/pmmp/BedrockProtocol/tree/bedrock-1.19.40" + "source": "https://github.com/pmmp/BedrockProtocol/tree/16.0.0+bedrock-1.19.40" }, - "time": "2022-10-25T21:51:46+00:00" + "time": "2022-11-19T16:11:48+00:00" }, { "name": "pocketmine/binaryutils", @@ -536,16 +536,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.9.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "73db4397b4150b29819bf39cc371924cc2e3f502" + "reference": "4b33d8fa53eda53d9662a7478806ebae2e4a5c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/73db4397b4150b29819bf39cc371924cc2e3f502", - "reference": "73db4397b4150b29819bf39cc371924cc2e3f502", + "url": "https://api.github.com/repos/pmmp/Language/zipball/4b33d8fa53eda53d9662a7478806ebae2e4a5c53", + "reference": "4b33d8fa53eda53d9662a7478806ebae2e4a5c53", "shasum": "" }, "type": "library", @@ -553,9 +553,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.9.3" + "source": "https://github.com/pmmp/Language/tree/2.11.0" }, - "time": "2022-11-12T13:59:25+00:00" + "time": "2022-11-25T14:24:34+00:00" }, { "name": "pocketmine/log", @@ -1020,6 +1020,318 @@ ], "time": "2022-11-05T23:03:38+00:00" }, + { + "name": "symfony/filesystem", + "version": "v5.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "ac09569844a9109a5966b9438fc29113ce77cf51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51", + "reference": "ac09569844a9109a5966b9438fc29113ce77cf51", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-09-21T19:53:16+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, { "name": "symfony/polyfill-php81", "version": "v1.27.0", diff --git a/resources/pocketmine.yml b/resources/pocketmine.yml index f922cb225..ac60afe53 100644 --- a/resources/pocketmine.yml +++ b/resources/pocketmine.yml @@ -119,8 +119,6 @@ chunk-sending: spawn-radius: 4 chunk-ticking: - #Max amount of chunks processed each tick - per-tick: 40 #Radius of chunks around a player to tick tick-radius: 3 #Number of blocks inside ticking areas' subchunks that get ticked every tick. Higher values will accelerate events @@ -168,6 +166,9 @@ timings: host: timings.pmmp.io console: + #Whether to accept commands via the console. If disabled, anything typed on the console will be ignored. + #Useful to save resources on headless servers where the console is never used (e.g. hosted server, Docker, etc.) + enable-input: true #Choose whether to enable server stats reporting on the console title. #NOTE: The title ticker will be disabled regardless if console colours are not enabled. title-tick: true diff --git a/resources/resource_packs.yml b/resources/resource_packs.yml index 39677852d..f236117d5 100644 --- a/resources/resource_packs.yml +++ b/resources/resource_packs.yml @@ -10,3 +10,4 @@ resource_stack: # - natural.zip # - vanilla.zip #If you want to force clients to use vanilla resources, you must place a vanilla resource pack in your resources folder and add it to the stack here. + #To specify a resource encryption key, put the key in the .key file alongside the resource pack. Example: vanilla.zip.key diff --git a/src/MemoryManager.php b/src/MemoryManager.php index d2dafe706..70e5d8a77 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -30,7 +30,7 @@ use pocketmine\scheduler\GarbageCollectionTask; use pocketmine\timings\Timings; use pocketmine\utils\Process; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function arsort; use function count; use function fclose; diff --git a/src/PocketMine.php b/src/PocketMine.php index b2e9a57a7..d84cf50a2 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -34,7 +34,7 @@ namespace pocketmine { use pocketmine\utils\Timezone; use pocketmine\utils\Utils; use pocketmine\wizard\SetupWizard; - use Webmozart\PathUtil\Path; + use Symfony\Component\Filesystem\Path; use function defined; use function extension_loaded; use function function_exists; @@ -201,6 +201,22 @@ JIT_WARNING ini_set('assert.exception', '1'); } + function getopt_string(string $opt) : ?string{ + $opts = getopt("", ["$opt:"]); + if(isset($opts[$opt])){ + if(is_string($opts[$opt])){ + return $opts[$opt]; + } + if(is_array($opts[$opt])){ + critical_error("Cannot specify --$opt multiple times"); + }else{ + critical_error("Missing value for --$opt"); + } + exit(1); + } + return null; + } + /** * @return void */ @@ -252,16 +268,22 @@ JIT_WARNING ErrorToExceptionHandler::set(); - $opts = getopt("", ["data:", "plugins:", "no-wizard", "enable-ansi", "disable-ansi"]); - $cwd = Utils::assumeNotFalse(realpath(Utils::assumeNotFalse(getcwd()))); - $dataPath = isset($opts["data"]) ? $opts["data"] . DIRECTORY_SEPARATOR : $cwd . DIRECTORY_SEPARATOR; - $pluginPath = isset($opts["plugins"]) ? $opts["plugins"] . DIRECTORY_SEPARATOR : $cwd . DIRECTORY_SEPARATOR . "plugins" . DIRECTORY_SEPARATOR; + $dataPath = getopt_string("data") ?? $cwd; + $pluginPath = getopt_string("plugins") ?? $cwd . DIRECTORY_SEPARATOR . "plugins"; Filesystem::addCleanedPath($pluginPath, Filesystem::CLEAN_PATH_PLUGINS_PREFIX); - if(!file_exists($dataPath)){ - mkdir($dataPath, 0777, true); + if(!@mkdir($dataPath, 0777, true) && (!is_dir($dataPath) || !is_writable($dataPath))){ + critical_error("Unable to create/access data directory at $dataPath. Check that the target location is accessible by the current user."); + exit(1); } + //this has to be done after we're sure the data path exists + $dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR; + if(!@mkdir($pluginPath, 0777, true) && (!is_dir($pluginPath) || !is_writable($pluginPath))){ + critical_error("Unable to create plugin directory at $pluginPath. Check that the target location is accessible by the current user."); + exit(1); + } + $pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR; $lockFilePath = Path::join($dataPath, 'server.lock'); if(($pid = Filesystem::createLockFile($lockFilePath)) !== null){ @@ -273,6 +295,7 @@ JIT_WARNING //Logger has a dependency on timezone Timezone::init(); + $opts = getopt("", ["no-wizard", "enable-ansi", "disable-ansi"]); if(isset($opts["enable-ansi"])){ Terminal::init(true); }elseif(isset($opts["disable-ansi"])){ diff --git a/src/Server.php b/src/Server.php index 88888445b..147e5a00b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -31,7 +31,7 @@ use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\SimpleCommandMap; use pocketmine\console\ConsoleCommandSender; -use pocketmine\console\ConsoleReaderThread; +use pocketmine\console\ConsoleReaderChildProcessDaemon; use pocketmine\crafting\CraftingManager; use pocketmine\crafting\CraftingManagerFromDataHelper; use pocketmine\crash\CrashDump; @@ -88,12 +88,12 @@ use pocketmine\promise\PromiseResolver; use pocketmine\resourcepacks\ResourcePackManager; use pocketmine\scheduler\AsyncPool; use pocketmine\snooze\SleeperHandler; -use pocketmine\snooze\SleeperNotifier; use pocketmine\stats\SendUsageTask; use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; use pocketmine\updater\UpdateChecker; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\BroadcastLoggerForwarder; use pocketmine\utils\Config; use pocketmine\utils\Filesystem; use pocketmine\utils\Internet; @@ -115,7 +115,7 @@ use pocketmine\world\World; use pocketmine\world\WorldCreationOptions; use pocketmine\world\WorldManager; use Ramsey\Uuid\UuidInterface; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_sum; use function base64_encode; use function cli_set_process_title; @@ -166,7 +166,6 @@ use function zlib_encode; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use const PHP_INT_MAX; -use const PTHREADS_INHERIT_NONE; use const ZLIB_ENCODING_GZIP; /** @@ -226,7 +225,8 @@ class Server{ private MemoryManager $memoryManager; - private ConsoleReaderThread $console; + private ?ConsoleReaderChildProcessDaemon $console = null; + private ?ConsoleCommandSender $consoleSender = null; private SimpleCommandMap $commandMap; @@ -609,6 +609,10 @@ class Server{ } /** + * @deprecated This method's results are unpredictable. The string "Steve" will return the player named "SteveJobs", + * until another player named "SteveJ" joins the server, at which point it will return that player instead. Prefer + * filtering the results of {@link Server::getOnlinePlayers()} yourself. + * * Returns an online player whose name begins with or equals the given string (case insensitive). * The closest match will be returned, or null if there are no online matches. * @@ -1045,22 +1049,14 @@ class Server{ $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET))); $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3))))); - //TODO: move console parts to a separate component - $consoleSender = new ConsoleCommandSender($this, $this->language); - $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $consoleSender); - $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $consoleSender); + $forwarder = new BroadcastLoggerForwarder($this, $this->logger, $this->language); + $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $forwarder); + $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $forwarder); - $consoleNotifier = new SleeperNotifier(); - $commandBuffer = new \Threaded(); - $this->console = new ConsoleReaderThread($commandBuffer, $consoleNotifier); - $this->tickSleeper->addNotifier($consoleNotifier, function() use ($commandBuffer, $consoleSender) : void{ - Timings::$serverCommand->startTiming(); - while(($line = $commandBuffer->shift()) !== null){ - $this->dispatchCommand($consoleSender, (string) $line); - } - Timings::$serverCommand->stopTiming(); - }); - $this->console->start(PTHREADS_INHERIT_NONE); + //TODO: move console parts to a separate component + if($this->configGroup->getPropertyBool("console.enable-input", true)){ + $this->console = new ConsoleReaderChildProcessDaemon($this->logger); + } $this->tickProcessor(); $this->forceShutdown(); @@ -1513,7 +1509,7 @@ class Server{ $this->configGroup->save(); } - if(isset($this->console)){ + if($this->console !== null){ $this->getLogger()->debug("Closing console"); $this->console->quit(); } @@ -1720,7 +1716,7 @@ class Server{ $session = $player->getNetworkSession(); $position = $player->getPosition(); $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn( - TextFormat::AQUA . $player->getName() . TextFormat::WHITE, + TextFormat::AQUA . $player->getName() . TextFormat::RESET, $session->getIp(), (string) $session->getPort(), (string) $player->getId(), @@ -1857,6 +1853,15 @@ class Server{ $this->getMemoryManager()->check(); + if($this->console !== null){ + Timings::$serverCommand->startTiming(); + while(($line = $this->console->readLine()) !== null){ + $this->consoleSender ??= new ConsoleCommandSender($this, $this->language); + $this->dispatchCommand($this->consoleSender, $line); + } + Timings::$serverCommand->stopTiming(); + } + Timings::$serverTick->stopTiming(); $now = microtime(true); diff --git a/src/VersionInfo.php b/src/VersionInfo.php index b9c1ad6aa..1c93eb558 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,7 +31,7 @@ use function str_repeat; final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "4.10.3"; + public const BASE_VERSION = "4.11.1"; public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; diff --git a/src/block/Bamboo.php b/src/block/Bamboo.php index 427b0b8e7..e33022824 100644 --- a/src/block/Bamboo.php +++ b/src/block/Bamboo.php @@ -164,9 +164,10 @@ class Bamboo extends Transparent{ } public function onNearbyBlockChange() : void{ - $below = $this->position->getWorld()->getBlock($this->position->down()); + $world = $this->position->getWorld(); + $below = $world->getBlock($this->position->down()); if(!$this->canBeSupportedBy($below) && !$below->isSameType($this)){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); } } @@ -212,7 +213,7 @@ class Bamboo extends Transparent{ } } - $tx = new BlockTransaction($this->position->getWorld()); + $tx = new BlockTransaction($world); foreach($newBlocks as $idx => $newBlock){ $tx->addBlock($this->position->subtract(0, $idx - $growAmount, 0), $newBlock); } diff --git a/src/block/BambooSapling.php b/src/block/BambooSapling.php index a00361416..af3e65364 100644 --- a/src/block/BambooSapling.php +++ b/src/block/BambooSapling.php @@ -82,8 +82,9 @@ final class BambooSapling extends Flowable{ } public function onNearbyBlockChange() : void{ - if(!$this->canBeSupportedBy($this->position->getWorld()->getBlock($this->position->down()))){ - $this->position->getWorld()->useBreakOn($this->position); + $world = $this->position->getWorld(); + if(!$this->canBeSupportedBy($world->getBlock($this->position->down()))){ + $world->useBreakOn($this->position); } } diff --git a/src/block/BaseCoral.php b/src/block/BaseCoral.php index f3bb9255d..0912a85ba 100644 --- a/src/block/BaseCoral.php +++ b/src/block/BaseCoral.php @@ -26,6 +26,7 @@ namespace pocketmine\block; use pocketmine\block\utils\CoralType; use pocketmine\block\utils\CoralTypeTrait; use pocketmine\block\utils\SupportType; +use pocketmine\event\block\BlockDeathEvent; use pocketmine\item\Item; abstract class BaseCoral extends Transparent{ @@ -50,7 +51,11 @@ abstract class BaseCoral extends Transparent{ //TODO: check water inside the block itself (not supported on the API yet) if(!$hasWater){ - $world->setBlock($this->position, $this->setDead(true)); + $ev = new BlockDeathEvent($this, $this->setDead(true)); + $ev->call(); + if(!$ev->isCancelled()){ + $world->setBlock($this->position, $ev->getNewState()); + } } } } diff --git a/src/block/BaseRail.php b/src/block/BaseRail.php index 933763122..971beffac 100644 --- a/src/block/BaseRail.php +++ b/src/block/BaseRail.php @@ -162,6 +162,7 @@ abstract class BaseRail extends Flowable{ $thisConnections = $this->getConnectedDirections(); $changed = false; + $world = $this->position->getWorld(); do{ $possible = $this->getPossibleConnectionDirections($thisConnections); $continue = false; @@ -189,7 +190,7 @@ abstract class BaseRail extends Flowable{ if(isset($otherPossible[$otherSide])){ $otherConnections[] = $otherSide; $other->setConnections($otherConnections); - $this->position->getWorld()->setBlock($other->position, $other); + $world->setBlock($other->position, $other); $changed = true; $thisConnections[] = $thisSide; @@ -202,7 +203,7 @@ abstract class BaseRail extends Flowable{ if($changed){ $this->setConnections($thisConnections); - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } } @@ -220,12 +221,13 @@ abstract class BaseRail extends Flowable{ } public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); if(!$this->getSide(Facing::DOWN)->getSupportType(Facing::UP)->hasEdgeSupport()){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); }else{ foreach($this->getCurrentShapeConnections() as $connection){ if(($connection & RailConnectionInfo::FLAG_ASCEND) !== 0 && !$this->getSide($connection & ~RailConnectionInfo::FLAG_ASCEND)->getSupportType(Facing::UP)->hasEdgeSupport()){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); break; } } diff --git a/src/block/Bell.php b/src/block/Bell.php index c75d7d785..70c78d027 100644 --- a/src/block/Bell.php +++ b/src/block/Bell.php @@ -179,10 +179,11 @@ final class Bell extends Transparent{ } public function ring(int $faceHit) : void{ - $this->position->getWorld()->addSound($this->position, new BellRingSound()); - $tile = $this->position->getWorld()->getTile($this->position); + $world = $this->position->getWorld(); + $world->addSound($this->position, new BellRingSound()); + $tile = $world->getTile($this->position); if($tile instanceof TileBell){ - $this->position->getWorld()->broadcastPacketToViewers($this->position, $tile->createFakeUpdatePacket($faceHit)); + $world->broadcastPacketToViewers($this->position, $tile->createFakeUpdatePacket($faceHit)); } } } diff --git a/src/block/Block.php b/src/block/Block.php index 73e1b1ccb..6f2f1b27e 100644 --- a/src/block/Block.php +++ b/src/block/Block.php @@ -179,10 +179,11 @@ class Block{ * Note: Do not call this directly. Pass the block to {@link World::setBlock()} instead. */ public function writeStateToWorld() : void{ - $this->position->getWorld()->getOrLoadChunkAtPosition($this->position)->setFullBlock($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getFullId()); + $world = $this->position->getWorld(); + $world->getOrLoadChunkAtPosition($this->position)->setFullBlock($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getFullId()); $tileType = $this->idInfo->getTileClass(); - $oldTile = $this->position->getWorld()->getTile($this->position); + $oldTile = $world->getTile($this->position); if($oldTile !== null){ if($tileType === null || !($oldTile instanceof $tileType)){ $oldTile->close(); @@ -196,8 +197,8 @@ class Block{ * @var Tile $tile * @see Tile::__construct() */ - $tile = new $tileType($this->position->getWorld(), $this->position->asVector3()); - $this->position->getWorld()->addTile($tile); + $tile = new $tileType($world, $this->position->asVector3()); + $world->addTile($tile); } } @@ -278,10 +279,11 @@ class Block{ * Do the actions needed so the block is broken with the Item */ public function onBreak(Item $item, ?Player $player = null) : bool{ - if(($t = $this->position->getWorld()->getTile($this->position)) !== null){ + $world = $this->position->getWorld(); + if(($t = $world->getTile($this->position)) !== null){ $t->onBlockDestroyed(); } - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + $world->setBlock($this->position, VanillaBlocks::AIR()); return true; } diff --git a/src/block/BrewingStand.php b/src/block/BrewingStand.php index c1840502e..19d239f82 100644 --- a/src/block/BrewingStand.php +++ b/src/block/BrewingStand.php @@ -130,10 +130,11 @@ class BrewingStand extends Transparent{ } public function onScheduledUpdate() : void{ - $brewing = $this->position->getWorld()->getTile($this->position); + $world = $this->position->getWorld(); + $brewing = $world->getTile($this->position); if($brewing instanceof TileBrewingStand){ if($brewing->onUpdate()){ - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + $world->scheduleDelayedBlockUpdate($this->position, 1); } $changed = false; @@ -146,7 +147,7 @@ class BrewingStand extends Transparent{ } if($changed){ - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } } } diff --git a/src/block/Button.php b/src/block/Button.php index 967e0d4f0..c447ddb8d 100644 --- a/src/block/Button.php +++ b/src/block/Button.php @@ -73,9 +73,10 @@ abstract class Button extends Flowable{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if(!$this->pressed){ $this->pressed = true; - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, $this->getActivationTime()); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOnSound()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this); + $world->scheduleDelayedBlockUpdate($this->position, $this->getActivationTime()); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOnSound()); } return true; @@ -84,8 +85,9 @@ abstract class Button extends Flowable{ public function onScheduledUpdate() : void{ if($this->pressed){ $this->pressed = false; - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOffSound()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOffSound()); } } diff --git a/src/block/Cactus.php b/src/block/Cactus.php index 11be13086..15e5fdc99 100644 --- a/src/block/Cactus.php +++ b/src/block/Cactus.php @@ -88,13 +88,14 @@ class Cactus extends Transparent{ public function onNearbyBlockChange() : void{ $down = $this->getSide(Facing::DOWN); + $world = $this->position->getWorld(); if($down->getId() !== BlockLegacyIds::SAND && !$down->isSameType($this)){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); }else{ foreach(Facing::HORIZONTAL as $side){ $b = $this->getSide($side); if($b->isSolid()){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); break; } } @@ -107,28 +108,29 @@ class Cactus extends Transparent{ public function onRandomTick() : void{ if(!$this->getSide(Facing::DOWN)->isSameType($this)){ + $world = $this->position->getWorld(); if($this->age === self::MAX_AGE){ for($y = 1; $y < 3; ++$y){ - if(!$this->position->getWorld()->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){ + if(!$world->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){ break; } - $b = $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z); + $b = $world->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z); if($b->getId() === BlockLegacyIds::AIR){ $ev = new BlockGrowEvent($b, VanillaBlocks::CACTUS()); $ev->call(); if($ev->isCancelled()){ break; } - $this->position->getWorld()->setBlock($b->position, $ev->getNewState()); + $world->setBlock($b->position, $ev->getNewState()); }else{ break; } } $this->age = 0; - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); }else{ ++$this->age; - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } } } diff --git a/src/block/Chest.php b/src/block/Chest.php index 59c21e1b6..9e012ee39 100644 --- a/src/block/Chest.php +++ b/src/block/Chest.php @@ -51,13 +51,13 @@ class Chest extends Transparent{ } public function onPostPlace() : void{ - $tile = $this->position->getWorld()->getTile($this->position); + $world = $this->position->getWorld(); + $tile = $world->getTile($this->position); if($tile instanceof TileChest){ foreach([false, true] as $clockwise){ $side = Facing::rotateY($this->facing, $clockwise); $c = $this->getSide($side); if($c instanceof Chest && $c->isSameType($this) && $c->facing === $this->facing){ - $world = $this->position->getWorld(); $pair = $world->getTile($c->position); if($pair instanceof TileChest && !$pair->isPaired()){ [$left, $right] = $clockwise ? [$c, $this] : [$this, $c]; diff --git a/src/block/CoralBlock.php b/src/block/CoralBlock.php index e29591b3d..0718112eb 100644 --- a/src/block/CoralBlock.php +++ b/src/block/CoralBlock.php @@ -27,6 +27,7 @@ use pocketmine\block\utils\CoralType; use pocketmine\block\utils\CoralTypeTrait; use pocketmine\block\utils\InvalidBlockStateException; use pocketmine\data\bedrock\CoralTypeIdMap; +use pocketmine\event\block\BlockDeathEvent; use pocketmine\item\Item; use function mt_rand; @@ -77,7 +78,11 @@ final class CoralBlock extends Opaque{ } } if(!$hasWater){ - $world->setBlock($this->position, $this->setDead(true)); + $ev = new BlockDeathEvent($this, $this->setDead(true)); + $ev->call(); + if(!$ev->isCancelled()){ + $world->setBlock($this->position, $ev->getNewState()); + } } } } diff --git a/src/block/DaylightSensor.php b/src/block/DaylightSensor.php index c13df4b6a..b9e8a58f4 100644 --- a/src/block/DaylightSensor.php +++ b/src/block/DaylightSensor.php @@ -100,21 +100,23 @@ class DaylightSensor extends Transparent{ } public function onScheduledUpdate() : void{ + $world = $this->position->getWorld(); $signalStrength = $this->recalculateSignalStrength(); if($this->signalStrength !== $signalStrength){ $this->signalStrength = $signalStrength; - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 20); + $world->scheduleDelayedBlockUpdate($this->position, 20); } private function recalculateSignalStrength() : int{ - $lightLevel = $this->position->getWorld()->getRealBlockSkyLightAt($this->position->x, $this->position->y, $this->position->z); + $world = $this->position->getWorld(); + $lightLevel = $world->getRealBlockSkyLightAt($this->position->x, $this->position->y, $this->position->z); if($this->inverted){ return 15 - $lightLevel; } - $sunAngle = $this->position->getWorld()->getSunAnglePercentage(); + $sunAngle = $world->getSunAnglePercentage(); return max(0, (int) round($lightLevel * cos(($sunAngle + ((($sunAngle < 0.5 ? 0 : 1) - $sunAngle) / 5)) * 2 * M_PI))); } diff --git a/src/block/DeadBush.php b/src/block/DeadBush.php index 09754b07d..72cbc0de5 100644 --- a/src/block/DeadBush.php +++ b/src/block/DeadBush.php @@ -34,7 +34,7 @@ use function mt_rand; class DeadBush extends Flowable{ public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ - if(!$this->getSide(Facing::DOWN)->isTransparent()){ + if($this->canBeSupportedBy($this->getSide(Facing::DOWN))){ return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } @@ -42,7 +42,7 @@ class DeadBush extends Flowable{ } public function onNearbyBlockChange() : void{ - if($this->getSide(Facing::DOWN)->isTransparent()){ + if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){ $this->position->getWorld()->useBreakOn($this->position); } } @@ -64,4 +64,14 @@ class DeadBush extends Flowable{ public function getFlammability() : int{ return 100; } + + private function canBeSupportedBy(Block $block) : bool{ + $blockId = $block->getId(); + return $blockId === BlockLegacyIds::SAND + || $blockId === BlockLegacyIds::PODZOL + || $blockId === BlockLegacyIds::MYCELIUM + || $blockId === BlockLegacyIds::DIRT + || $blockId === BlockLegacyIds::HARDENED_CLAY + || $blockId === BlockLegacyIds::STAINED_HARDENED_CLAY; + } } diff --git a/src/block/Dirt.php b/src/block/Dirt.php index 8197f1415..cca195a64 100644 --- a/src/block/Dirt.php +++ b/src/block/Dirt.php @@ -63,8 +63,9 @@ class Dirt extends Opaque{ $item->applyDamage(1); $newBlock = $this->coarse ? VanillaBlocks::DIRT() : VanillaBlocks::FARMLAND(); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); - $this->position->getWorld()->setBlock($this->position, $newBlock); + $world = $this->position->getWorld(); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); + $world->setBlock($this->position, $newBlock); return true; } diff --git a/src/block/Door.php b/src/block/Door.php index 191a8fe0f..a0f2169b8 100644 --- a/src/block/Door.php +++ b/src/block/Door.php @@ -163,13 +163,14 @@ class Door extends Transparent{ $this->open = !$this->open; $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + $world = $this->position->getWorld(); if($other instanceof Door && $other->isSameType($this)){ $other->open = $this->open; - $this->position->getWorld()->setBlock($other->position, $other); + $world->setBlock($other->position, $other); } - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->addSound($this->position, new DoorSound()); + $world->setBlock($this->position, $this); + $world->addSound($this->position, new DoorSound()); return true; } diff --git a/src/block/DragonEgg.php b/src/block/DragonEgg.php index db6512a9c..04b30dabe 100644 --- a/src/block/DragonEgg.php +++ b/src/block/DragonEgg.php @@ -62,8 +62,9 @@ class DragonEgg extends Transparent implements Fallable{ } public function teleport() : void{ + $world = $this->position->getWorld(); for($tries = 0; $tries < 16; ++$tries){ - $block = $this->position->getWorld()->getBlockAt( + $block = $world->getBlockAt( $this->position->x + mt_rand(-16, 16), max(World::Y_MIN, min(World::Y_MAX - 1, $this->position->y + mt_rand(-8, 8))), $this->position->z + mt_rand(-16, 16) @@ -76,9 +77,9 @@ class DragonEgg extends Transparent implements Fallable{ } $blockPos = $ev->getTo(); - $this->position->getWorld()->addParticle($this->position, new DragonEggTeleportParticle($this->position->x - $blockPos->x, $this->position->y - $blockPos->y, $this->position->z - $blockPos->z)); - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); - $this->position->getWorld()->setBlock($blockPos, $this); + $world->addParticle($this->position, new DragonEggTeleportParticle($this->position->x - $blockPos->x, $this->position->y - $blockPos->y, $this->position->z - $blockPos->z)); + $world->setBlock($this->position, VanillaBlocks::AIR()); + $world->setBlock($blockPos, $this); break; } } diff --git a/src/block/Farmland.php b/src/block/Farmland.php index 6e1ad13f0..57392300f 100644 --- a/src/block/Farmland.php +++ b/src/block/Farmland.php @@ -78,16 +78,17 @@ class Farmland extends Transparent{ } public function onRandomTick() : void{ + $world = $this->position->getWorld(); if(!$this->canHydrate()){ if($this->wetness > 0){ $this->wetness--; - $this->position->getWorld()->setBlock($this->position, $this, false); + $world->setBlock($this->position, $this, false); }else{ - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::DIRT()); + $world->setBlock($this->position, VanillaBlocks::DIRT()); } }elseif($this->wetness < self::MAX_WETNESS){ $this->wetness = self::MAX_WETNESS; - $this->position->getWorld()->setBlock($this->position, $this, false); + $world->setBlock($this->position, $this, false); } } diff --git a/src/block/FenceGate.php b/src/block/FenceGate.php index fefd49b6c..7d45a917e 100644 --- a/src/block/FenceGate.php +++ b/src/block/FenceGate.php @@ -117,8 +117,9 @@ class FenceGate extends Transparent{ } } - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->addSound($this->position, new DoorSound()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this); + $world->addSound($this->position, new DoorSound()); return true; } diff --git a/src/block/Fire.php b/src/block/Fire.php index b599f65ae..648724529 100644 --- a/src/block/Fire.php +++ b/src/block/Fire.php @@ -100,10 +100,11 @@ class Fire extends Flowable{ } public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); if($this->getSide(Facing::DOWN)->isTransparent() && !$this->hasAdjacentFlammableBlocks()){ - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + $world->setBlock($this->position, VanillaBlocks::AIR()); }else{ - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); + $world->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); } } @@ -136,11 +137,12 @@ class Fire extends Flowable{ } } + $world = $this->position->getWorld(); if($result !== null){ - $this->position->getWorld()->setBlock($this->position, $result); + $world->setBlock($this->position, $result); } - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); + $world->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); if($canSpread){ $this->burnBlocksAround(); @@ -181,7 +183,8 @@ class Fire extends Flowable{ if(!$ev->isCancelled()){ $block->onIncinerate(); - if($this->position->getWorld()->getBlock($block->getPosition())->isSameState($block)){ + $world = $this->position->getWorld(); + if($world->getBlock($block->getPosition())->isSameState($block)){ $spreadedFire = false; if(mt_rand(0, $this->age + 9) < 5){ //TODO: check rain $fire = clone $this; @@ -189,7 +192,7 @@ class Fire extends Flowable{ $spreadedFire = $this->spreadBlock($block, $fire); } if(!$spreadedFire){ - $this->position->getWorld()->setBlock($block->position, VanillaBlocks::AIR()); + $world->setBlock($block->position, VanillaBlocks::AIR()); } } } diff --git a/src/block/FlowerPot.php b/src/block/FlowerPot.php index a60719675..5ebe6b8ba 100644 --- a/src/block/FlowerPot.php +++ b/src/block/FlowerPot.php @@ -122,6 +122,7 @@ class FlowerPot extends Flowable{ } public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $world = $this->position->getWorld(); $plant = $item->getBlock(); if($this->plant !== null){ if($this->isValidPlant($plant)){ @@ -137,16 +138,16 @@ class FlowerPot extends Flowable{ $removedItems = $player->getInventory()->addItem(...$removedItems); } foreach($removedItems as $drops){ - $this->position->getWorld()->dropItem($this->position->add(0.5, 0.5, 0.5), $drops); + $world->dropItem($this->position->add(0.5, 0.5, 0.5), $drops); } $this->setPlant(null); - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); return true; }elseif($this->isValidPlant($plant)){ $this->setPlant($plant); $item->pop(); - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); return true; } diff --git a/src/block/FrostedIce.php b/src/block/FrostedIce.php index 065caf589..95e15f3f7 100644 --- a/src/block/FrostedIce.php +++ b/src/block/FrostedIce.php @@ -56,16 +56,18 @@ class FrostedIce extends Ice{ } public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); if(!$this->checkAdjacentBlocks(2)){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); }else{ - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + $world->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); } } public function onRandomTick() : void{ + $world = $this->position->getWorld(); if((!$this->checkAdjacentBlocks(4) || mt_rand(0, 2) === 0) && - $this->position->getWorld()->getHighestAdjacentFullLightAt($this->position->x, $this->position->y, $this->position->z) >= 12 - $this->age){ + $world->getHighestAdjacentFullLightAt($this->position->x, $this->position->y, $this->position->z) >= 12 - $this->age){ if($this->tryMelt()){ foreach($this->getAllSides() as $block){ if($block instanceof FrostedIce){ @@ -74,7 +76,7 @@ class FrostedIce extends Ice{ } } }else{ - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + $world->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); } } @@ -106,18 +108,19 @@ class FrostedIce extends Ice{ * @return bool Whether the ice was destroyed. */ private function tryMelt() : bool{ + $world = $this->position->getWorld(); if($this->age >= self::MAX_AGE){ $ev = new BlockMeltEvent($this, VanillaBlocks::WATER()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $world->setBlock($this->position, $ev->getNewState()); } return true; } $this->age++; - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + $world->setBlock($this->position, $this); + $world->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); return false; } } diff --git a/src/block/Furnace.php b/src/block/Furnace.php index f5a81c524..d4b1ba9f1 100644 --- a/src/block/Furnace.php +++ b/src/block/Furnace.php @@ -83,12 +83,13 @@ class Furnace extends Opaque{ } public function onScheduledUpdate() : void{ - $furnace = $this->position->getWorld()->getTile($this->position); + $world = $this->position->getWorld(); + $furnace = $world->getTile($this->position); if($furnace instanceof TileFurnace && $furnace->onUpdate()){ if(mt_rand(1, 60) === 1){ //in vanilla this is between 1 and 5 seconds; try to average about 3 - $this->position->getWorld()->addSound($this->position, $furnace->getFurnaceType()->getCookSound()); + $world->addSound($this->position, $furnace->getFurnaceType()->getCookSound()); } - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); //TODO: check this + $world->scheduleDelayedBlockUpdate($this->position, 1); //TODO: check this } } } diff --git a/src/block/Grass.php b/src/block/Grass.php index d0ea0520d..8ddbb7f0c 100644 --- a/src/block/Grass.php +++ b/src/block/Grass.php @@ -53,13 +53,14 @@ class Grass extends Opaque{ } public function onRandomTick() : void{ - $lightAbove = $this->position->getWorld()->getFullLightAt($this->position->x, $this->position->y + 1, $this->position->z); - if($lightAbove < 4 && $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + 1, $this->position->z)->getLightFilter() >= 2){ + $world = $this->position->getWorld(); + $lightAbove = $world->getFullLightAt($this->position->x, $this->position->y + 1, $this->position->z); + if($lightAbove < 4 && $world->getBlockAt($this->position->x, $this->position->y + 1, $this->position->z)->getLightFilter() >= 2){ //grass dies $ev = new BlockSpreadEvent($this, $this, VanillaBlocks::DIRT()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState(), false); + $world->setBlock($this->position, $ev->getNewState(), false); } }elseif($lightAbove >= 9){ //try grass spread @@ -68,12 +69,12 @@ class Grass extends Opaque{ $y = mt_rand($this->position->y - 3, $this->position->y + 1); $z = mt_rand($this->position->z - 1, $this->position->z + 1); - $b = $this->position->getWorld()->getBlockAt($x, $y, $z); + $b = $world->getBlockAt($x, $y, $z); if( !($b instanceof Dirt) || $b->isCoarse() || - $this->position->getWorld()->getFullLightAt($x, $y + 1, $z) < 4 || - $this->position->getWorld()->getBlockAt($x, $y + 1, $z)->getLightFilter() >= 2 + $world->getFullLightAt($x, $y + 1, $z) < 4 || + $world->getBlockAt($x, $y + 1, $z)->getLightFilter() >= 2 ){ continue; } @@ -81,7 +82,7 @@ class Grass extends Opaque{ $ev = new BlockSpreadEvent($b, $this, VanillaBlocks::GRASS()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($b->position, $ev->getNewState(), false); + $world->setBlock($b->position, $ev->getNewState(), false); } } } @@ -91,23 +92,24 @@ class Grass extends Opaque{ if($face !== Facing::UP){ return false; } + $world = $this->position->getWorld(); if($item instanceof Fertilizer){ $item->pop(); - TallGrassObject::growGrass($this->position->getWorld(), $this->position, new Random(mt_rand()), 8, 2); + TallGrassObject::growGrass($world, $this->position, new Random(mt_rand()), 8, 2); return true; }elseif($item instanceof Hoe){ $item->applyDamage(1); $newBlock = VanillaBlocks::FARMLAND(); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); - $this->position->getWorld()->setBlock($this->position, $newBlock); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); + $world->setBlock($this->position, $newBlock); return true; }elseif($item instanceof Shovel && $this->getSide(Facing::UP)->getId() === BlockLegacyIds::AIR){ $item->applyDamage(1); $newBlock = VanillaBlocks::GRASS_PATH(); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); - $this->position->getWorld()->setBlock($this->position, $newBlock); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock)); + $world->setBlock($this->position, $newBlock); return true; } diff --git a/src/block/Ice.php b/src/block/Ice.php index 7edbc2659..5f62fc032 100644 --- a/src/block/Ice.php +++ b/src/block/Ice.php @@ -51,11 +51,12 @@ class Ice extends Transparent{ } public function onRandomTick() : void{ - if($this->position->getWorld()->getHighestAdjacentBlockLight($this->position->x, $this->position->y, $this->position->z) >= 12){ + $world = $this->position->getWorld(); + if($world->getHighestAdjacentBlockLight($this->position->x, $this->position->y, $this->position->z) >= 12){ $ev = new BlockMeltEvent($this, VanillaBlocks::WATER()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $world->setBlock($this->position, $ev->getNewState()); } } } diff --git a/src/block/ItemFrame.php b/src/block/ItemFrame.php index 3e7a7378a..8df62c72f 100644 --- a/src/block/ItemFrame.php +++ b/src/block/ItemFrame.php @@ -31,6 +31,9 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; +use pocketmine\world\sound\ItemFrameAddItemSound; +use pocketmine\world\sound\ItemFrameRemoveItemSound; +use pocketmine\world\sound\ItemFrameRotateItemSound; use function is_infinite; use function is_nan; use function lcg_value; @@ -136,8 +139,12 @@ class ItemFrame extends Flowable{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($this->framedItem !== null){ $this->itemRotation = ($this->itemRotation + 1) % self::ROTATIONS; + + $this->position->getWorld()->addSound($this->position, new ItemFrameRotateItemSound()); }elseif(!$item->isNull()){ $this->framedItem = $item->pop(); + + $this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound()); }else{ return true; } @@ -151,11 +158,13 @@ class ItemFrame extends Flowable{ if($this->framedItem === null){ return false; } + $world = $this->position->getWorld(); if(lcg_value() <= $this->itemDropChance){ - $this->position->getWorld()->dropItem($this->position->add(0.5, 0.5, 0.5), clone $this->framedItem); + $world->dropItem($this->position->add(0.5, 0.5, 0.5), clone $this->framedItem); + $world->addSound($this->position, new ItemFrameRemoveItemSound()); } $this->setFramedItem(null); - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); return true; } diff --git a/src/block/Leaves.php b/src/block/Leaves.php index 4c421b1de..c845176e3 100644 --- a/src/block/Leaves.php +++ b/src/block/Leaves.php @@ -124,11 +124,12 @@ class Leaves extends Transparent{ if(!$this->noDecay && $this->checkDecay){ $ev = new LeavesDecayEvent($this); $ev->call(); + $world = $this->position->getWorld(); if($ev->isCancelled() || $this->findLog($this->position)){ $this->checkDecay = false; - $this->position->getWorld()->setBlock($this->position, $this, false); + $world->setBlock($this->position, $this, false); }else{ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); } } } diff --git a/src/block/Lectern.php b/src/block/Lectern.php index e94c5c706..1f339ad51 100644 --- a/src/block/Lectern.php +++ b/src/block/Lectern.php @@ -128,8 +128,9 @@ class Lectern extends Transparent{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($this->book === null && $item instanceof WritableBookBase){ - $this->position->getWorld()->setBlock($this->position, $this->setBook($item)); - $this->position->getWorld()->addSound($this->position, new LecternPlaceBookSound()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this->setBook($item)); + $world->addSound($this->position, new LecternPlaceBookSound()); $item->pop(); } return true; @@ -137,8 +138,9 @@ class Lectern extends Transparent{ public function onAttack(Item $item, int $face, ?Player $player = null) : bool{ if($this->book !== null){ - $this->position->getWorld()->dropItem($this->position->up(), $this->book); - $this->position->getWorld()->setBlock($this->position, $this->setBook(null)); + $world = $this->position->getWorld(); + $world->dropItem($this->position->up(), $this->book); + $world->setBlock($this->position, $this->setBook(null)); } return false; } @@ -152,12 +154,13 @@ class Lectern extends Transparent{ } $this->viewedPage = $newPage; + $world = $this->position->getWorld(); if(!$this->producingSignal){ $this->producingSignal = true; - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + $world->scheduleDelayedBlockUpdate($this->position, 1); } - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); return true; } diff --git a/src/block/Lever.php b/src/block/Lever.php index e45a95345..beec393b7 100644 --- a/src/block/Lever.php +++ b/src/block/Lever.php @@ -127,8 +127,9 @@ class Lever extends Flowable{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ $this->activated = !$this->activated; - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->addSound( + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this); + $world->addSound( $this->position->add(0.5, 0.5, 0.5), $this->activated ? new RedstonePowerOnSound() : new RedstonePowerOffSound() ); diff --git a/src/block/Liquid.php b/src/block/Liquid.php index 64018b9df..10feaeecf 100644 --- a/src/block/Liquid.php +++ b/src/block/Liquid.php @@ -329,7 +329,7 @@ abstract class Liquid extends Transparent{ } if($adjacentDecay <= self::MAX_DECAY){ - $calculator = new MinimumCostFlowCalculator($this->position->getWorld(), $this->getFlowDecayPerBlock(), \Closure::fromCallable([$this, 'canFlowInto'])); + $calculator = new MinimumCostFlowCalculator($world, $this->getFlowDecayPerBlock(), \Closure::fromCallable([$this, 'canFlowInto'])); foreach($calculator->getOptimalFlowDirections($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) as $facing){ $this->flowIntoBlock($world->getBlock($this->position->getSide($facing)), $adjacentDecay, false); } @@ -348,11 +348,12 @@ abstract class Liquid extends Transparent{ $ev = new BlockSpreadEvent($block, $this, $new); $ev->call(); if(!$ev->isCancelled()){ + $world = $this->position->getWorld(); if($block->getId() !== BlockLegacyIds::AIR){ - $this->position->getWorld()->useBreakOn($block->position); + $world->useBreakOn($block->position); } - $this->position->getWorld()->setBlock($block->position, $ev->getNewState()); + $world->setBlock($block->position, $ev->getNewState()); } } } @@ -382,8 +383,9 @@ abstract class Liquid extends Transparent{ $ev = new BlockFormEvent($this, $result); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); - $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new FizzSound(2.6 + (lcg_value() - lcg_value()) * 0.8)); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $ev->getNewState()); + $world->addSound($this->position->add(0.5, 0.5, 0.5), new FizzSound(2.6 + (lcg_value() - lcg_value()) * 0.8)); } return true; } diff --git a/src/block/Mycelium.php b/src/block/Mycelium.php index 11d00c5d8..f7e989c45 100644 --- a/src/block/Mycelium.php +++ b/src/block/Mycelium.php @@ -49,13 +49,14 @@ class Mycelium extends Opaque{ $x = mt_rand($this->position->x - 1, $this->position->x + 1); $y = mt_rand($this->position->y - 2, $this->position->y + 2); $z = mt_rand($this->position->z - 1, $this->position->z + 1); - $block = $this->position->getWorld()->getBlockAt($x, $y, $z); + $world = $this->position->getWorld(); + $block = $world->getBlockAt($x, $y, $z); if($block instanceof Dirt && !$block->isCoarse()){ if($block->getSide(Facing::UP) instanceof Transparent){ $ev = new BlockSpreadEvent($block, $this, VanillaBlocks::MYCELIUM()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($block->position, $ev->getNewState()); + $world->setBlock($block->position, $ev->getNewState()); } } } diff --git a/src/block/Pumpkin.php b/src/block/Pumpkin.php index 2f374670e..29dc196b7 100644 --- a/src/block/Pumpkin.php +++ b/src/block/Pumpkin.php @@ -36,8 +36,9 @@ class Pumpkin extends Opaque{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($item instanceof Shears && in_array($face, Facing::HORIZONTAL, true)){ $item->applyDamage(1); - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::CARVED_PUMPKIN()->setFacing($face)); - $this->position->getWorld()->dropItem($this->position->add(0.5, 0.5, 0.5), VanillaItems::PUMPKIN_SEEDS()->setCount(1)); + $world = $this->position->getWorld(); + $world->setBlock($this->position, VanillaBlocks::CARVED_PUMPKIN()->setFacing($face)); + $world->dropItem($this->position->add(0.5, 0.5, 0.5), VanillaItems::PUMPKIN_SEEDS()->setCount(1)); return true; } return false; diff --git a/src/block/Sapling.php b/src/block/Sapling.php index fb67dbc46..ea6f71d54 100644 --- a/src/block/Sapling.php +++ b/src/block/Sapling.php @@ -96,12 +96,13 @@ class Sapling extends Flowable{ } public function onRandomTick() : void{ - if($this->position->getWorld()->getFullLightAt($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) >= 8 && mt_rand(1, 7) === 1){ + $world = $this->position->getWorld(); + if($world->getFullLightAt($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) >= 8 && mt_rand(1, 7) === 1){ if($this->ready){ $this->grow(null); }else{ $this->ready = true; - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } } } diff --git a/src/block/SnowLayer.php b/src/block/SnowLayer.php index 4e65b5eeb..40c4b3633 100644 --- a/src/block/SnowLayer.php +++ b/src/block/SnowLayer.php @@ -111,11 +111,12 @@ class SnowLayer extends Flowable implements Fallable{ } public function onRandomTick() : void{ - if($this->position->getWorld()->getBlockLightAt($this->position->x, $this->position->y, $this->position->z) >= 12){ + $world = $this->position->getWorld(); + if($world->getBlockLightAt($this->position->x, $this->position->y, $this->position->z) >= 12){ $ev = new BlockMeltEvent($this, VanillaBlocks::AIR()); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $world->setBlock($this->position, $ev->getNewState()); } } } diff --git a/src/block/Stem.php b/src/block/Stem.php index b17a5c646..7db17a8d4 100644 --- a/src/block/Stem.php +++ b/src/block/Stem.php @@ -35,13 +35,14 @@ abstract class Stem extends Crops{ public function onRandomTick() : void{ if(mt_rand(0, 2) === 1){ + $world = $this->position->getWorld(); if($this->age < self::MAX_AGE){ $block = clone $this; ++$block->age; $ev = new BlockGrowEvent($this, $block); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $world->setBlock($this->position, $ev->getNewState()); } }else{ $grow = $this->getPlant(); @@ -57,7 +58,7 @@ abstract class Stem extends Crops{ $ev = new BlockGrowEvent($side, $grow); $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($side->position, $ev->getNewState()); + $world->setBlock($side->position, $ev->getNewState()); } } } diff --git a/src/block/Sugarcane.php b/src/block/Sugarcane.php index a5f2782cd..80c758504 100644 --- a/src/block/Sugarcane.php +++ b/src/block/Sugarcane.php @@ -31,6 +31,7 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; +use pocketmine\world\Position; class Sugarcane extends Flowable{ public const MAX_AGE = 15; @@ -49,27 +50,37 @@ class Sugarcane extends Flowable{ return 0b1111; } - private function grow() : bool{ + private function seekToBottom() : Position{ + $world = $this->position->getWorld(); + $bottom = $this->position; + while(($next = $world->getBlock($bottom->down()))->isSameType($this)){ + $bottom = $next->position; + } + return $bottom; + } + + private function grow(Position $pos) : bool{ $grew = false; + $world = $pos->getWorld(); for($y = 1; $y < 3; ++$y){ - if(!$this->position->getWorld()->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){ + if(!$world->isInWorld($pos->x, $pos->y + $y, $pos->z)){ break; } - $b = $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z); + $b = $world->getBlockAt($pos->x, $pos->y + $y, $pos->z); if($b->getId() === BlockLegacyIds::AIR){ $ev = new BlockGrowEvent($b, VanillaBlocks::SUGARCANE()); $ev->call(); if($ev->isCancelled()){ break; } - $this->position->getWorld()->setBlock($b->position, $ev->getNewState()); + $world->setBlock($b->position, $ev->getNewState()); $grew = true; - }else{ + }elseif(!$b->isSameType($this)){ break; } } $this->age = 0; - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($pos, $this); return $grew; } @@ -86,7 +97,7 @@ class Sugarcane extends Flowable{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($item instanceof Fertilizer){ - if(!$this->getSide(Facing::DOWN)->isSameType($this) && $this->grow()){ + if($this->grow($this->seekToBottom())){ $item->pop(); } @@ -110,7 +121,7 @@ class Sugarcane extends Flowable{ public function onRandomTick() : void{ if(!$this->getSide(Facing::DOWN)->isSameType($this)){ if($this->age === self::MAX_AGE){ - $this->grow(); + $this->grow($this->position); }else{ ++$this->age; $this->position->getWorld()->setBlock($this->position, $this); diff --git a/src/block/SweetBerryBush.php b/src/block/SweetBerryBush.php index 5f1163bc8..0f18beba2 100644 --- a/src/block/SweetBerryBush.php +++ b/src/block/SweetBerryBush.php @@ -90,6 +90,7 @@ class SweetBerryBush extends Flowable{ } public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $world = $this->position->getWorld(); if($this->age < self::STAGE_MATURE && $item instanceof Fertilizer){ $block = clone $this; $block->age++; @@ -98,13 +99,13 @@ class SweetBerryBush extends Flowable{ $ev->call(); if(!$ev->isCancelled()){ - $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $world->setBlock($this->position, $ev->getNewState()); $item->pop(); } }elseif(($dropAmount = $this->getBerryDropAmount()) > 0){ - $this->position->getWorld()->setBlock($this->position, $this->setAge(self::STAGE_BUSH_NO_BERRIES)); - $this->position->getWorld()->dropItem($this->position, $this->asItem()->setCount($dropAmount)); + $world->setBlock($this->position, $this->setAge(self::STAGE_BUSH_NO_BERRIES)); + $world->dropItem($this->position, $this->asItem()->setCount($dropAmount)); } return true; diff --git a/src/block/TNT.php b/src/block/TNT.php index 561e36e37..6bd5178a3 100644 --- a/src/block/TNT.php +++ b/src/block/TNT.php @@ -110,11 +110,12 @@ class TNT extends Opaque{ } public function ignite(int $fuse = 80) : void{ - $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, VanillaBlocks::AIR()); $mot = (new Random())->nextSignedFloat() * M_PI * 2; - $tnt = new PrimedTNT(Location::fromObject($this->position->add(0.5, 0, 0.5), $this->position->getWorld())); + $tnt = new PrimedTNT(Location::fromObject($this->position->add(0.5, 0, 0.5), $world)); $tnt->setFuse($fuse); $tnt->setWorksUnderwater($this->worksUnderwater); $tnt->setMotion(new Vector3(-sin($mot) * 0.02, 0.2, -cos($mot) * 0.02)); diff --git a/src/block/Trapdoor.php b/src/block/Trapdoor.php index 054737d74..6082aee51 100644 --- a/src/block/Trapdoor.php +++ b/src/block/Trapdoor.php @@ -96,8 +96,9 @@ class Trapdoor extends Transparent{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ $this->open = !$this->open; - $this->position->getWorld()->setBlock($this->position, $this); - $this->position->getWorld()->addSound($this->position, new DoorSound()); + $world = $this->position->getWorld(); + $world->setBlock($this->position, $this); + $world->addSound($this->position, new DoorSound()); return true; } } diff --git a/src/block/Vine.php b/src/block/Vine.php index 3ff454c43..9159315da 100644 --- a/src/block/Vine.php +++ b/src/block/Vine.php @@ -141,10 +141,11 @@ class Vine extends Flowable{ } if($changed){ + $world = $this->position->getWorld(); if(count($this->faces) === 0){ - $this->position->getWorld()->useBreakOn($this->position); + $world->useBreakOn($this->position); }else{ - $this->position->getWorld()->setBlock($this->position, $this); + $world->setBlock($this->position, $this); } } } diff --git a/src/block/inventory/BarrelInventory.php b/src/block/inventory/BarrelInventory.php index 7de83bb03..0d17d2a3e 100644 --- a/src/block/inventory/BarrelInventory.php +++ b/src/block/inventory/BarrelInventory.php @@ -48,9 +48,10 @@ class BarrelInventory extends SimpleInventory implements BlockInventory{ protected function animateBlock(bool $isOpen) : void{ $holder = $this->getHolder(); - $block = $holder->getWorld()->getBlock($holder); + $world = $holder->getWorld(); + $block = $world->getBlock($holder); if($block instanceof Barrel){ - $holder->getWorld()->setBlock($holder, $block->setOpen($isOpen)); + $world->setBlock($holder, $block->setOpen($isOpen)); } } } diff --git a/src/block/tile/ContainerTrait.php b/src/block/tile/ContainerTrait.php index 43107cd6a..4c4907a9b 100644 --- a/src/block/tile/ContainerTrait.php +++ b/src/block/tile/ContainerTrait.php @@ -96,8 +96,10 @@ trait ContainerTrait{ $inv = $this->getRealInventory(); $pos = $this->getPosition(); + $world = $pos->getWorld(); + $dropPos = $pos->add(0.5, 0.5, 0.5); foreach($inv->getContents() as $k => $item){ - $pos->getWorld()->dropItem($pos->add(0.5, 0.5, 0.5), $item); + $world->dropItem($dropPos, $item); } $inv->clearAll(); } diff --git a/src/block/utils/FallableTrait.php b/src/block/utils/FallableTrait.php index 9275b9d4f..3d7e12214 100644 --- a/src/block/utils/FallableTrait.php +++ b/src/block/utils/FallableTrait.php @@ -46,14 +46,15 @@ trait FallableTrait{ public function onNearbyBlockChange() : void{ $pos = $this->getPosition(); - $down = $pos->getWorld()->getBlock($pos->getSide(Facing::DOWN)); + $world = $pos->getWorld(); + $down = $world->getBlock($pos->getSide(Facing::DOWN)); if($down->canBeReplaced()){ - $pos->getWorld()->setBlock($pos, VanillaBlocks::AIR()); + $world->setBlock($pos, VanillaBlocks::AIR()); $block = $this; if(!($block instanceof Block)) throw new AssumptionFailedError(__TRAIT__ . " should only be used by Blocks"); - $fall = new FallingBlock(Location::fromObject($pos->add(0.5, 0, 0.5), $pos->getWorld()), $block); + $fall = new FallingBlock(Location::fromObject($pos->add(0.5, 0, 0.5), $world), $block); $fall->spawnToAll(); } } diff --git a/src/command/Command.php b/src/command/Command.php index a85184777..12a1b8e38 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -27,13 +27,13 @@ declare(strict_types=1); namespace pocketmine\command; use pocketmine\command\utils\CommandException; -use pocketmine\console\ConsoleCommandSender; use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\Translatable; use pocketmine\permission\PermissionManager; use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; +use pocketmine\utils\BroadcastLoggerForwarder; use pocketmine\utils\TextFormat; use function explode; use function str_replace; @@ -227,12 +227,12 @@ abstract class Command{ $result = KnownTranslationFactory::chat_type_admin($source->getName(), $message); $colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC); - if($sendToSource && !($source instanceof ConsoleCommandSender)){ + if($sendToSource){ $source->sendMessage($message); } foreach($users as $user){ - if($user instanceof ConsoleCommandSender){ + if($user instanceof BroadcastLoggerForwarder){ $user->sendMessage($result); }elseif($user !== $source){ $user->sendMessage($colored); diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php index 6ec7b129a..df2b8f4fe 100644 --- a/src/command/FormattedCommandAlias.php +++ b/src/command/FormattedCommandAlias.php @@ -52,7 +52,7 @@ class FormattedCommandAlias extends Command{ string $alias, private array $formatStrings ){ - parent::__construct($alias); + parent::__construct($alias, KnownTranslationFactory::pocketmine_command_userDefined_description()); } public function execute(CommandSender $sender, string $commandLabel, array $args){ diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index bab101d5d..a618dd1c0 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -272,10 +272,11 @@ class SimpleCommandMap implements CommandMap{ } //These registered commands have absolute priority + $lowerAlias = strtolower($alias); if(count($targets) > 0){ - $this->knownCommands[strtolower($alias)] = new FormattedCommandAlias(strtolower($alias), $targets); + $this->knownCommands[$lowerAlias] = new FormattedCommandAlias($lowerAlias, $targets); }else{ - unset($this->knownCommands[strtolower($alias)]); + unset($this->knownCommands[$lowerAlias]); } } diff --git a/src/command/defaults/DumpMemoryCommand.php b/src/command/defaults/DumpMemoryCommand.php index 83f177e01..2e425a740 100644 --- a/src/command/defaults/DumpMemoryCommand.php +++ b/src/command/defaults/DumpMemoryCommand.php @@ -25,7 +25,7 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\permission\DefaultPermissionNames; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function date; class DumpMemoryCommand extends VanillaCommand{ diff --git a/src/command/defaults/GarbageCollectorCommand.php b/src/command/defaults/GarbageCollectorCommand.php index 69875c7d5..cdfe2b883 100644 --- a/src/command/defaults/GarbageCollectorCommand.php +++ b/src/command/defaults/GarbageCollectorCommand.php @@ -63,7 +63,7 @@ class GarbageCollectorCommand extends VanillaCommand{ $cyclesCollected = $sender->getServer()->getMemoryManager()->triggerGarbageCollector(); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_header()->format(TextFormat::GREEN . "---- " . TextFormat::WHITE, TextFormat::GREEN . " ----" . TextFormat::WHITE)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_header()->format(TextFormat::GREEN . "---- " . TextFormat::RESET, TextFormat::GREEN . " ----" . TextFormat::RESET)); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_chunks(TextFormat::RED . number_format($chunksCollected))->prefix(TextFormat::GOLD)); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_entities(TextFormat::RED . number_format($entitiesCollected))->prefix(TextFormat::GOLD)); diff --git a/src/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php index 3173a1ee2..9eec9bc68 100644 --- a/src/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -94,7 +94,7 @@ class HelpCommand extends VanillaCommand{ foreach($commands[$pageNumber - 1] as $command){ $description = $command->getDescription(); $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; - $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getLabel() . ": " . TextFormat::WHITE . $descriptionString); + $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getLabel() . ": " . TextFormat::RESET . $descriptionString); } } @@ -106,18 +106,18 @@ class HelpCommand extends VanillaCommand{ $description = $cmd->getDescription(); $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel()) - ->format(TextFormat::YELLOW . "--------- " . TextFormat::WHITE, TextFormat::YELLOW . " ---------")); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::WHITE . $descriptionString) + ->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------")); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString) ->prefix(TextFormat::GOLD)); $usage = $cmd->getUsage(); $usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::WHITE . implode("\n" . TextFormat::WHITE, explode("\n", $usageString))) + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString))) ->prefix(TextFormat::GOLD)); $aliases = $cmd->getAliases(); sort($aliases, SORT_NATURAL); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::WHITE . implode(", ", $aliases)) + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) ->prefix(TextFormat::GOLD)); return true; diff --git a/src/command/defaults/MeCommand.php b/src/command/defaults/MeCommand.php index 586d6cb39..82bec234f 100644 --- a/src/command/defaults/MeCommand.php +++ b/src/command/defaults/MeCommand.php @@ -52,7 +52,7 @@ class MeCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_emote($sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::WHITE . implode(" ", $args))); + $sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_emote($sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::RESET . implode(" ", $args))); return true; } diff --git a/src/command/defaults/PluginsCommand.php b/src/command/defaults/PluginsCommand.php index e4c83354d..420b7708c 100644 --- a/src/command/defaults/PluginsCommand.php +++ b/src/command/defaults/PluginsCommand.php @@ -56,7 +56,7 @@ class PluginsCommand extends VanillaCommand{ }, $sender->getServer()->getPluginManager()->getPlugins()); sort($list, SORT_STRING); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_plugins_success((string) count($list), implode(TextFormat::WHITE . ", ", $list))); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_plugins_success((string) count($list), implode(TextFormat::RESET . ", ", $list))); return true; } } diff --git a/src/command/defaults/StatusCommand.php b/src/command/defaults/StatusCommand.php index 0dca696d0..7c2884993 100644 --- a/src/command/defaults/StatusCommand.php +++ b/src/command/defaults/StatusCommand.php @@ -52,7 +52,7 @@ class StatusCommand extends VanillaCommand{ $mUsage = Process::getAdvancedMemoryUsage(); $server = $sender->getServer(); - $sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::WHITE . "Server status" . TextFormat::GREEN . " ----"); + $sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::RESET . "Server status" . TextFormat::GREEN . " ----"); $time = (int) (microtime(true) - $server->getStartTime()); diff --git a/src/command/defaults/TimingsCommand.php b/src/command/defaults/TimingsCommand.php index 14fae62f5..ecfa7b281 100644 --- a/src/command/defaults/TimingsCommand.php +++ b/src/command/defaults/TimingsCommand.php @@ -35,7 +35,7 @@ use pocketmine\timings\TimingsHandler; use pocketmine\utils\InternetException; use pocketmine\utils\InternetRequestResult; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function count; use function fclose; use function file_exists; diff --git a/src/command/defaults/VersionCommand.php b/src/command/defaults/VersionCommand.php index 135c8e780..487f84aff 100644 --- a/src/command/defaults/VersionCommand.php +++ b/src/command/defaults/VersionCommand.php @@ -57,17 +57,18 @@ class VersionCommand extends VanillaCommand{ if(count($args) === 0){ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareName( - VersionInfo::NAME + TextFormat::GREEN . VersionInfo::NAME . TextFormat::RESET )); + $versionColor = VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : TextFormat::GREEN; $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareVersion( - VersionInfo::VERSION()->getFullVersion(), - VersionInfo::GIT_HASH() + $versionColor . VersionInfo::VERSION()->getFullVersion() . TextFormat::RESET, + TextFormat::GREEN . VersionInfo::GIT_HASH() . TextFormat::RESET )); $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_minecraftVersion( - ProtocolInfo::MINECRAFT_VERSION_NETWORK, - (string) ProtocolInfo::CURRENT_PROTOCOL + TextFormat::GREEN . ProtocolInfo::MINECRAFT_VERSION_NETWORK . TextFormat::RESET, + TextFormat::GREEN . ProtocolInfo::CURRENT_PROTOCOL . TextFormat::RESET )); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpVersion(PHP_VERSION)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpVersion(TextFormat::GREEN . PHP_VERSION . TextFormat::RESET)); $jitMode = Utils::getOpcacheJitMode(); if($jitMode !== null){ @@ -79,8 +80,8 @@ class VersionCommand extends VanillaCommand{ }else{ $jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitNotSupported(); } - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpJitStatus($jitStatus)); - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_operatingSystem(Utils::getOS())); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpJitStatus($jitStatus->format(TextFormat::GREEN, TextFormat::RESET))); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_operatingSystem(TextFormat::GREEN . Utils::getOS() . TextFormat::RESET)); }else{ $pluginName = implode(" ", $args); $exactPlugin = $sender->getServer()->getPluginManager()->getPlugin($pluginName); @@ -110,7 +111,7 @@ class VersionCommand extends VanillaCommand{ private function describeToSender(Plugin $plugin, CommandSender $sender) : void{ $desc = $plugin->getDescription(); - $sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::WHITE . " version " . TextFormat::DARK_GREEN . $desc->getVersion()); + $sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::RESET . " version " . TextFormat::DARK_GREEN . $desc->getVersion()); if($desc->getDescription() !== ""){ $sender->sendMessage($desc->getDescription()); diff --git a/src/console/ConsoleCommandSender.php b/src/console/ConsoleCommandSender.php index 30035ac3e..ef3683b0b 100644 --- a/src/console/ConsoleCommandSender.php +++ b/src/console/ConsoleCommandSender.php @@ -30,6 +30,8 @@ use pocketmine\permission\DefaultPermissions; use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissibleDelegateTrait; use pocketmine\Server; +use pocketmine\utils\Terminal; +use pocketmine\utils\TextFormat; use function explode; use function trim; use const PHP_INT_MAX; @@ -59,13 +61,12 @@ class ConsoleCommandSender implements CommandSender{ } public function sendMessage(Translatable|string $message) : void{ - $server = $this->getServer(); if($message instanceof Translatable){ $message = $this->getLanguage()->translate($message); } foreach(explode("\n", trim($message)) as $line){ - $server->getLogger()->info($line); + Terminal::writeLine(TextFormat::GREEN . "Command output | " . TextFormat::addBase(TextFormat::WHITE, $line)); } } diff --git a/src/console/ConsoleReaderChildProcess.php b/src/console/ConsoleReaderChildProcess.php index 5bf2ff71f..2d4e3fc56 100644 --- a/src/console/ConsoleReaderChildProcess.php +++ b/src/console/ConsoleReaderChildProcess.php @@ -23,12 +23,14 @@ declare(strict_types=1); namespace pocketmine\console; +use pocketmine\utils\Process; use function cli_set_process_title; use function count; use function dirname; use function feof; use function fwrite; use function stream_socket_client; +use const PTHREADS_INHERIT_NONE; require dirname(__DIR__, 2) . '/vendor/autoload.php'; @@ -43,9 +45,40 @@ $socket = stream_socket_client($argv[1], $errCode, $errMessage, 15.0); if($socket === false){ throw new \RuntimeException("Failed to connect to server process ($errCode): $errMessage"); } -$consoleReader = new ConsoleReader(); + +$channel = new \Threaded(); +$thread = new class($channel) extends \Thread{ + public function __construct( + private \Threaded $channel, + ){} + + public function run(){ + require dirname(__DIR__, 2) . '/vendor/autoload.php'; + + $channel = $this->channel; + $reader = new ConsoleReader(); + while(true){ // @phpstan-ignore-line + $line = $reader->readLine(); + if($line !== null){ + $channel->synchronized(function() use ($channel, $line) : void{ + $channel[] = $line; + $channel->notify(); + }); + } + } + } +}; + +$thread->start(PTHREADS_INHERIT_NONE); while(!feof($socket)){ - $line = $consoleReader->readLine(); + $line = $channel->synchronized(function() use ($channel) : ?string{ + if(count($channel) === 0){ + $channel->wait(1_000_000); + } + /** @var string|null $line */ + $line = $channel->shift(); + return $line; + }); if(@fwrite($socket, ($line ?? "") . "\n") === false){ //Always send even if there's no line, to check if the parent is alive //If the parent process was terminated forcibly, it won't close the connection properly, so feof() will return @@ -53,3 +86,8 @@ while(!feof($socket)){ break; } } + +//For simplicity's sake, we don't bother with a graceful shutdown here. +//The parent process would normally forcibly terminate the child process anyway, so we only reach this point if the +//parent process was terminated forcibly and didn't clean up after itself. +Process::kill(Process::pid(), false); diff --git a/src/console/ConsoleReaderThread.php b/src/console/ConsoleReaderChildProcessDaemon.php similarity index 50% rename from src/console/ConsoleReaderThread.php rename to src/console/ConsoleReaderChildProcessDaemon.php index eac19ef84..138559f06 100644 --- a/src/console/ConsoleReaderThread.php +++ b/src/console/ConsoleReaderChildProcessDaemon.php @@ -23,11 +23,9 @@ declare(strict_types=1); namespace pocketmine\console; -use pocketmine\snooze\SleeperNotifier; -use pocketmine\thread\Thread; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function base64_encode; use function fgets; use function fopen; @@ -45,36 +43,35 @@ use function trim; use const PHP_BINARY; use const STREAM_SHUT_RDWR; -final class ConsoleReaderThread extends Thread{ +/** + * This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes + * properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets() + * waiting for a line that might never come (and Windows doesn't support character-based reading either), and + * pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless. + * + * This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill + * the entire server process to make it go away. + * + * To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can + * then brutally murder when the server shuts down, without killing the entire server process. + * Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process + * communication. + */ +final class ConsoleReaderChildProcessDaemon{ + private \PrefixedLogger $logger; + /** @var resource */ + private $subprocess; + /** @var resource */ + private $socket; + public function __construct( - private \Threaded $buffer, - private ?SleeperNotifier $notifier = null - ){} - - protected function onRun() : void{ - $buffer = $this->buffer; - $notifier = $this->notifier; - - while(!$this->isKilled){ - $this->runSubprocess($buffer, $notifier); - } + \Logger $logger + ){ + $this->logger = new \PrefixedLogger($logger, "Console Reader Daemon"); + $this->prepareSubprocess(); } - /** - * This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes - * properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets() - * waiting for a line that might never come (and Windows doesn't support character-based reading either), and - * pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless. - * - * This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill - * the entire server process to make it go away. - * - * To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can - * then brutally murder when the server shuts down, without killing the entire server process. - * Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process - * communication. - */ - private function runSubprocess(\Threaded $buffer, ?SleeperNotifier $notifier) : void{ + private function prepareSubprocess() : void{ $server = stream_socket_server("tcp://127.0.0.1:0"); if($server === false){ throw new \RuntimeException("Failed to open console reader socket server"); @@ -96,41 +93,43 @@ final class ConsoleReaderThread extends Thread{ throw new AssumptionFailedError("stream_socket_accept() returned false"); } stream_socket_shutdown($server, STREAM_SHUT_RDWR); - while(!$this->isKilled){ - $r = [$client]; - $w = null; - $e = null; - if(stream_select($r, $w, $e, 0, 200000) === 1){ - $command = fgets($client); - if($command === false){ - //subprocess died for some reason; this could be someone killed it manually from outside (e.g. - //mistyped PID) or it might be a ctrl+c signal to this process that the child is handling - //differently (different signal handlers). - //since we have no way to know the difference, we just kill the sub and start a new one. - break; - } - $command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid"); - $command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid"); - if($command === ""){ - continue; - } - $buffer[] = $command; - if($notifier !== null){ - $notifier->wakeupSleeper(); - } - } - } + $this->subprocess = $sub; + $this->socket = $client; + } + private function shutdownSubprocess() : void{ //we have no way to signal to the subprocess to shut down gracefully; besides, Windows sucks, and the subprocess //gets stuck in a blocking fgets() read because stream_select() is a hunk of junk (hence the separate process in //the first place). - proc_terminate($sub); - proc_close($sub); - stream_socket_shutdown($client, STREAM_SHUT_RDWR); + proc_terminate($this->subprocess); + proc_close($this->subprocess); + stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); } - public function getThreadName() : string{ - return "Console"; + public function readLine() : ?string{ + $r = [$this->socket]; + $w = null; + $e = null; + if(stream_select($r, $w, $e, 0, 0) === 1){ + $command = fgets($this->socket); + if($command === false){ + $this->logger->debug("Lost connection to subprocess, restarting (maybe the child process was killed from outside?)"); + $this->shutdownSubprocess(); + $this->prepareSubprocess(); + return null; + } + + $command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid"); + $command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid"); + + return $command !== "" ? $command : null; + } + + return null; + } + + public function quit() : void{ + $this->shutdownSubprocess(); } } diff --git a/src/crash/CrashDump.php b/src/crash/CrashDump.php index b715ce5d3..e6d2b52a0 100644 --- a/src/crash/CrashDump.php +++ b/src/crash/CrashDump.php @@ -33,7 +33,7 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; use pocketmine\VersionInfo; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function base64_encode; use function error_get_last; use function file; diff --git a/src/data/bedrock/EffectIdMap.php b/src/data/bedrock/EffectIdMap.php index 23985fa12..6dce86d9b 100644 --- a/src/data/bedrock/EffectIdMap.php +++ b/src/data/bedrock/EffectIdMap.php @@ -74,6 +74,7 @@ final class EffectIdMap{ //TODO: SLOW_FALLING //TODO: BAD_OMEN //TODO: VILLAGE_HERO + $this->register(EffectIds::DARKNESS, VanillaEffects::DARKNESS()); } //TODO: not a big fan of the code duplication here :( diff --git a/src/data/bedrock/EffectIds.php b/src/data/bedrock/EffectIds.php index 3acf56569..a2ada01d9 100644 --- a/src/data/bedrock/EffectIds.php +++ b/src/data/bedrock/EffectIds.php @@ -58,4 +58,5 @@ final class EffectIds{ public const SLOW_FALLING = 27; public const BAD_OMEN = 28; public const VILLAGE_HERO = 29; + public const DARKNESS = 30; } diff --git a/src/data/bedrock/LegacyBiomeIdToStringIdMap.php b/src/data/bedrock/LegacyBiomeIdToStringIdMap.php index 974792eba..eba0034e8 100644 --- a/src/data/bedrock/LegacyBiomeIdToStringIdMap.php +++ b/src/data/bedrock/LegacyBiomeIdToStringIdMap.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\data\bedrock; use pocketmine\utils\SingletonTrait; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; final class LegacyBiomeIdToStringIdMap extends LegacyToStringBidirectionalIdMap{ use SingletonTrait; diff --git a/src/data/bedrock/LegacyBlockIdToStringIdMap.php b/src/data/bedrock/LegacyBlockIdToStringIdMap.php index 616c56bcf..35c24caf9 100644 --- a/src/data/bedrock/LegacyBlockIdToStringIdMap.php +++ b/src/data/bedrock/LegacyBlockIdToStringIdMap.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\data\bedrock; use pocketmine\utils\SingletonTrait; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; final class LegacyBlockIdToStringIdMap extends LegacyToStringBidirectionalIdMap{ use SingletonTrait; diff --git a/src/data/bedrock/LegacyEntityIdToStringIdMap.php b/src/data/bedrock/LegacyEntityIdToStringIdMap.php index 2e3e4aecc..8427e4594 100644 --- a/src/data/bedrock/LegacyEntityIdToStringIdMap.php +++ b/src/data/bedrock/LegacyEntityIdToStringIdMap.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\data\bedrock; use pocketmine\utils\SingletonTrait; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; final class LegacyEntityIdToStringIdMap extends LegacyToStringBidirectionalIdMap{ use SingletonTrait; diff --git a/src/data/bedrock/LegacyItemIdToStringIdMap.php b/src/data/bedrock/LegacyItemIdToStringIdMap.php index 85b6ff1bf..254ad96bb 100644 --- a/src/data/bedrock/LegacyItemIdToStringIdMap.php +++ b/src/data/bedrock/LegacyItemIdToStringIdMap.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\data\bedrock; use pocketmine\utils\SingletonTrait; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; final class LegacyItemIdToStringIdMap extends LegacyToStringBidirectionalIdMap{ use SingletonTrait; diff --git a/src/entity/Entity.php b/src/entity/Entity.php index be4bfa4e1..eb42813ec 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -250,10 +250,8 @@ abstract class Entity{ $this->getWorld()->addEntity($this); $this->lastUpdate = $this->server->getTick(); - (new EntitySpawnEvent($this))->call(); $this->scheduleUpdate(); - } abstract protected function getInitialSizeInfo() : EntitySizeInfo; @@ -938,6 +936,14 @@ abstract class Entity{ return (new Vector2(-cos(deg2rad($this->location->yaw) - M_PI_2), -sin(deg2rad($this->location->yaw) - M_PI_2)))->normalize(); } + /** + * Called from onUpdate() on the first tick of a new entity. This is called before any movement processing or + * main ticking logic. Use this to fire any events related to spawning the entity. + */ + protected function onFirstUpdate(int $currentTick) : void{ + (new EntitySpawnEvent($this))->call(); + } + public function onUpdate(int $currentTick) : bool{ if($this->closed){ return false; @@ -954,6 +960,10 @@ abstract class Entity{ $this->lastUpdate = $currentTick; + if($this->justCreated){ + $this->onFirstUpdate($currentTick); + } + if(!$this->isAlive()){ if($this->onDeathUpdate($tickDiff)){ $this->flagForDespawn(); diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php index cd1f96a5e..fb3a1f33d 100644 --- a/src/entity/EntityFactory.php +++ b/src/entity/EntityFactory.php @@ -175,20 +175,6 @@ final class EntityFactory{ }, ['Human']); } - /** - * @phpstan-param \Closure(World, CompoundTag) : Entity $creationFunc - */ - private static function validateCreationFunc(\Closure $creationFunc) : void{ - $sig = new CallbackType( - new ReturnType(Entity::class), - new ParameterType("world", World::class), - new ParameterType("nbt", CompoundTag::class) - ); - if(!$sig->isSatisfiedBy($creationFunc)){ - throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($creationFunc) . "` must be compatible with `" . $sig . "`"); - } - } - /** * Registers an entity type into the index. * @@ -207,7 +193,11 @@ final class EntityFactory{ throw new \InvalidArgumentException("At least one save name must be provided"); } Utils::testValidInstance($className, Entity::class); - self::validateCreationFunc($creationFunc); + Utils::validateCallableSignature(new CallbackType( + new ReturnType(Entity::class), + new ParameterType("world", World::class), + new ParameterType("nbt", CompoundTag::class) + ), $creationFunc); foreach($saveNames as $name){ $this->creationFuncs[$name] = $creationFunc; diff --git a/src/entity/Living.php b/src/entity/Living.php index 43a1a392d..aa4690068 100644 --- a/src/entity/Living.php +++ b/src/entity/Living.php @@ -293,6 +293,10 @@ abstract class Living extends Entity{ return $nbt; } + /** + * @deprecated This function always returns true, no matter whether the target is in the line of sight or not. + * @see VoxelRayTrace::inDirection() for a more generalized method of ray-tracing to a target. + */ public function hasLineOfSight(Entity $entity) : bool{ //TODO: head height return true; diff --git a/src/entity/effect/InstantDamageEffect.php b/src/entity/effect/InstantDamageEffect.php index 9461fd979..28d731079 100644 --- a/src/entity/effect/InstantDamageEffect.php +++ b/src/entity/effect/InstantDamageEffect.php @@ -33,7 +33,7 @@ class InstantDamageEffect extends InstantEffect{ public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ //TODO: add particles (witch spell) - $damage = (4 << $instance->getAmplifier()) * $potency; + $damage = (6 << $instance->getAmplifier()) * $potency; if($source !== null){ $sourceOwner = $source->getOwningEntity(); if($sourceOwner !== null){ diff --git a/src/entity/effect/RegenerationEffect.php b/src/entity/effect/RegenerationEffect.php index 28ba2b18b..f06ce67db 100644 --- a/src/entity/effect/RegenerationEffect.php +++ b/src/entity/effect/RegenerationEffect.php @@ -30,7 +30,7 @@ use pocketmine\event\entity\EntityRegainHealthEvent; class RegenerationEffect extends Effect{ public function canTick(EffectInstance $instance) : bool{ - if(($interval = (40 >> $instance->getAmplifier())) > 0){ + if(($interval = (50 >> $instance->getAmplifier())) > 0){ return ($instance->getDuration() % $interval) === 0; } return true; diff --git a/src/entity/effect/StringToEffectParser.php b/src/entity/effect/StringToEffectParser.php index d336af33b..23bd29bd0 100644 --- a/src/entity/effect/StringToEffectParser.php +++ b/src/entity/effect/StringToEffectParser.php @@ -40,6 +40,7 @@ final class StringToEffectParser extends StringToTParser{ $result->register("absorption", fn() => VanillaEffects::ABSORPTION()); $result->register("blindness", fn() => VanillaEffects::BLINDNESS()); $result->register("conduit_power", fn() => VanillaEffects::CONDUIT_POWER()); + $result->register("darkness", fn() => VanillaEffects::DARKNESS()); $result->register("fatal_poison", fn() => VanillaEffects::FATAL_POISON()); $result->register("fire_resistance", fn() => VanillaEffects::FIRE_RESISTANCE()); $result->register("haste", fn() => VanillaEffects::HASTE()); diff --git a/src/entity/effect/VanillaEffects.php b/src/entity/effect/VanillaEffects.php index 04f7985da..50544054a 100644 --- a/src/entity/effect/VanillaEffects.php +++ b/src/entity/effect/VanillaEffects.php @@ -36,6 +36,7 @@ use pocketmine\utils\RegistryTrait; * @method static AbsorptionEffect ABSORPTION() * @method static Effect BLINDNESS() * @method static Effect CONDUIT_POWER() + * @method static Effect DARKNESS() * @method static PoisonEffect FATAL_POISON() * @method static Effect FIRE_RESISTANCE() * @method static Effect HASTE() @@ -68,6 +69,7 @@ final class VanillaEffects{ //TODO: bad_omen self::register("blindness", new Effect(KnownTranslationFactory::potion_blindness(), new Color(0x1f, 0x1f, 0x23), true)); self::register("conduit_power", new Effect(KnownTranslationFactory::potion_conduitPower(), new Color(0x1d, 0xc2, 0xd1))); + self::register("darkness", new Effect(KnownTranslationFactory::effect_darkness(), new Color(0x29, 0x27, 0x21), true, 600, false)); self::register("fatal_poison", new PoisonEffect(KnownTranslationFactory::potion_poison(), new Color(0x4e, 0x93, 0x31), true, 600, true, true)); self::register("fire_resistance", new Effect(KnownTranslationFactory::potion_fireResistance(), new Color(0xe4, 0x9a, 0x3a))); self::register("haste", new Effect(KnownTranslationFactory::potion_digSpeed(), new Color(0xd9, 0xc0, 0x43))); diff --git a/src/entity/object/ItemEntity.php b/src/entity/object/ItemEntity.php index 5c39f3bec..4fe844f7e 100644 --- a/src/entity/object/ItemEntity.php +++ b/src/entity/object/ItemEntity.php @@ -92,8 +92,11 @@ class ItemEntity extends Entity{ $this->pickupDelay = $nbt->getShort("PickupDelay", $this->pickupDelay); $this->owner = $nbt->getString("Owner", $this->owner); $this->thrower = $nbt->getString("Thrower", $this->thrower); + } - (new ItemSpawnEvent($this))->call(); + protected function onFirstUpdate(int $currentTick) : void{ + (new ItemSpawnEvent($this))->call(); //this must be called before EntitySpawnEvent, to maintain backwards compatibility + parent::onFirstUpdate($currentTick); } protected function entityBaseTick(int $tickDiff = 1) : bool{ diff --git a/src/event/block/BlockDeathEvent.php b/src/event/block/BlockDeathEvent.php new file mode 100644 index 000000000..c07e3ad6e --- /dev/null +++ b/src/event/block/BlockDeathEvent.php @@ -0,0 +1,32 @@ +backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener( - function(Inventory $unused, int $slot, Item $oldItem) : void{ - $this->onSlotChange($slot, $oldItem); + static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{ + if(($strongThis = $weakThis->get()) !== null){ + $strongThis->onSlotChange($slot, $oldItem); + } }, - function(Inventory $unused, array $oldContents) : void{ - $this->onContentChange($oldContents); + static function(Inventory $unused, array $oldContents) use ($weakThis) : void{ + if(($strongThis = $weakThis->get()) !== null){ + $strongThis->onContentChange($oldContents); + } } )); } + public function __destruct(){ + $this->backingInventory->getListeners()->remove($this->inventoryListener); + } + public function getSize() : int{ return $this->backingInventory->getSize(); } @@ -66,12 +73,4 @@ class DelegateInventory extends BaseInventory{ protected function internalSetContents(array $items) : void{ $this->backingInventory->setContents($items); } - - public function onClose(Player $who) : void{ - parent::onClose($who); - if(count($this->getViewers()) === 0 && count($this->getListeners()->toArray()) === 1){ - $this->backingInventory->getListeners()->remove($this->inventoryListener); - $this->inventoryListener = CallbackInventoryListener::onAnyChange(static function() : void{}); //break cyclic reference - } - } } diff --git a/src/inventory/transaction/TransactionBuilder.php b/src/inventory/transaction/TransactionBuilder.php new file mode 100644 index 000000000..f56b2aaa1 --- /dev/null +++ b/src/inventory/transaction/TransactionBuilder.php @@ -0,0 +1,61 @@ +extraActions[spl_object_id($action)] = $action; + } + + public function getInventory(Inventory $inventory) : TransactionBuilderInventory{ + $id = spl_object_id($inventory); + return $this->inventories[$id] ??= new TransactionBuilderInventory($inventory); + } + + /** + * @return InventoryAction[] + */ + public function generateActions() : array{ + $actions = $this->extraActions; + + foreach($this->inventories as $inventory){ + foreach($inventory->generateActions() as $action){ + $actions[spl_object_id($action)] = $action; + } + } + + return $actions; + } +} diff --git a/src/item/Armor.php b/src/item/Armor.php index 8d77db8dc..25374c6a0 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -86,6 +86,12 @@ class Armor extends Durable{ return $this; } + /** @return $this */ + public function clearCustomColor() : self{ + $this->customColor = null; + return $this; + } + /** * Returns the total enchantment protection factor this armour piece offers from all applicable protection * enchantments on the item. diff --git a/src/item/LegacyStringToItemParser.php b/src/item/LegacyStringToItemParser.php index e05f4d031..9ec97ede8 100644 --- a/src/item/LegacyStringToItemParser.php +++ b/src/item/LegacyStringToItemParser.php @@ -26,7 +26,7 @@ namespace pocketmine\item; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function explode; use function file_get_contents; use function is_array; @@ -108,8 +108,9 @@ final class LegacyStringToItemParser{ throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value"); } - if(isset($this->map[strtolower($b[0])])){ - $item = $this->itemFactory->get($this->map[strtolower($b[0])], $meta); + $id = strtolower($b[0]); + if(isset($this->map[$id])){ + $item = $this->itemFactory->get($this->map[$id], $meta); }else{ throw new LegacyStringToItemParserException("Unable to resolve \"" . $input . "\" to a valid item"); } diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php index 577c125fa..f1db1c6d1 100644 --- a/src/lang/KnownTranslationFactory.php +++ b/src/lang/KnownTranslationFactory.php @@ -43,6 +43,34 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ACCEPT_LICENSE, []); } + public static function action_interact_armorstand_equip() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_ARMORSTAND_EQUIP, []); + } + + public static function action_interact_armorstand_pose() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_ARMORSTAND_POSE, []); + } + + public static function action_interact_exit_boat() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_EXIT_BOAT, []); + } + + public static function action_interact_fishing() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_FISHING, []); + } + + public static function action_interact_name() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_NAME, []); + } + + public static function action_interact_ride_boat() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_RIDE_BOAT, []); + } + + public static function action_interact_ride_minecart() : Translatable{ + return new Translatable(KnownTranslationKeys::ACTION_INTERACT_RIDE_MINECART, []); + } + public static function chat_type_achievement(Translatable|string $param0, Translatable|string $param1) : Translatable{ return new Translatable(KnownTranslationKeys::CHAT_TYPE_ACHIEVEMENT, [ 0 => $param0, @@ -627,6 +655,12 @@ final class KnownTranslationFactory{ ]); } + public static function death_attack_fallingBlock(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_FALLINGBLOCK, [ + 0 => $param0, + ]); + } + public static function death_attack_generic(Translatable|string $param0) : Translatable{ return new Translatable(KnownTranslationKeys::DEATH_ATTACK_GENERIC, [ 0 => $param0, @@ -691,6 +725,13 @@ final class KnownTranslationFactory{ ]); } + public static function death_attack_trident(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_TRIDENT, [ + 0 => $param0, + 1 => $param1, + ]); + } + public static function death_attack_wither(Translatable|string $param0) : Translatable{ return new Translatable(KnownTranslationKeys::DEATH_ATTACK_WITHER, [ 0 => $param0, @@ -863,6 +904,10 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ENCHANTMENT_SOUL_SPEED, []); } + public static function enchantment_swift_sneak() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_SWIFT_SNEAK, []); + } + public static function enchantment_thorns() : Translatable{ return new Translatable(KnownTranslationKeys::ENCHANTMENT_THORNS, []); } @@ -954,6 +999,10 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ITEM_RECORD_13_DESC, []); } + public static function item_record_5_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_5_DESC, []); + } + public static function item_record_blocks_desc() : Translatable{ return new Translatable(KnownTranslationKeys::ITEM_RECORD_BLOCKS_DESC, []); } @@ -978,6 +1027,10 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ITEM_RECORD_MELLOHI_DESC, []); } + public static function item_record_otherside_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_OTHERSIDE_DESC, []); + } + public static function item_record_pigstep_desc() : Translatable{ return new Translatable(KnownTranslationKeys::ITEM_RECORD_PIGSTEP_DESC, []); } diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php index 87f88515d..bd98496cb 100644 --- a/src/lang/KnownTranslationKeys.php +++ b/src/lang/KnownTranslationKeys.php @@ -33,6 +33,13 @@ final class KnownTranslationKeys{ public const ABILITY_FLIGHT = "ability.flight"; public const ABILITY_NOCLIP = "ability.noclip"; public const ACCEPT_LICENSE = "accept_license"; + public const ACTION_INTERACT_ARMORSTAND_EQUIP = "action.interact.armorstand.equip"; + public const ACTION_INTERACT_ARMORSTAND_POSE = "action.interact.armorstand.pose"; + public const ACTION_INTERACT_EXIT_BOAT = "action.interact.exit.boat"; + public const ACTION_INTERACT_FISHING = "action.interact.fishing"; + public const ACTION_INTERACT_NAME = "action.interact.name"; + public const ACTION_INTERACT_RIDE_BOAT = "action.interact.ride.boat"; + public const ACTION_INTERACT_RIDE_MINECART = "action.interact.ride.minecart"; public const CHAT_TYPE_ACHIEVEMENT = "chat.type.achievement"; public const CHAT_TYPE_ADMIN = "chat.type.admin"; public const CHAT_TYPE_ANNOUNCEMENT = "chat.type.announcement"; @@ -138,6 +145,7 @@ final class KnownTranslationKeys{ public const DEATH_ATTACK_EXPLOSION = "death.attack.explosion"; public const DEATH_ATTACK_EXPLOSION_PLAYER = "death.attack.explosion.player"; public const DEATH_ATTACK_FALL = "death.attack.fall"; + public const DEATH_ATTACK_FALLINGBLOCK = "death.attack.fallingBlock"; public const DEATH_ATTACK_GENERIC = "death.attack.generic"; public const DEATH_ATTACK_INFIRE = "death.attack.inFire"; public const DEATH_ATTACK_INWALL = "death.attack.inWall"; @@ -148,6 +156,7 @@ final class KnownTranslationKeys{ public const DEATH_ATTACK_OUTOFWORLD = "death.attack.outOfWorld"; public const DEATH_ATTACK_PLAYER = "death.attack.player"; public const DEATH_ATTACK_PLAYER_ITEM = "death.attack.player.item"; + public const DEATH_ATTACK_TRIDENT = "death.attack.trident"; public const DEATH_ATTACK_WITHER = "death.attack.wither"; public const DEATH_FELL_ACCIDENT_GENERIC = "death.fell.accident.generic"; public const DEFAULT_GAMEMODE = "default_gamemode"; @@ -190,6 +199,7 @@ final class KnownTranslationKeys{ public const ENCHANTMENT_PROTECT_FIRE = "enchantment.protect.fire"; public const ENCHANTMENT_PROTECT_PROJECTILE = "enchantment.protect.projectile"; public const ENCHANTMENT_SOUL_SPEED = "enchantment.soul_speed"; + public const ENCHANTMENT_SWIFT_SNEAK = "enchantment.swift_sneak"; public const ENCHANTMENT_THORNS = "enchantment.thorns"; public const ENCHANTMENT_TRIDENTCHANNELING = "enchantment.tridentChanneling"; public const ENCHANTMENT_TRIDENTIMPALING = "enchantment.tridentImpaling"; @@ -211,12 +221,14 @@ final class KnownTranslationKeys{ public const IP_WARNING = "ip_warning"; public const ITEM_RECORD_11_DESC = "item.record_11.desc"; public const ITEM_RECORD_13_DESC = "item.record_13.desc"; + public const ITEM_RECORD_5_DESC = "item.record_5.desc"; public const ITEM_RECORD_BLOCKS_DESC = "item.record_blocks.desc"; public const ITEM_RECORD_CAT_DESC = "item.record_cat.desc"; public const ITEM_RECORD_CHIRP_DESC = "item.record_chirp.desc"; public const ITEM_RECORD_FAR_DESC = "item.record_far.desc"; public const ITEM_RECORD_MALL_DESC = "item.record_mall.desc"; public const ITEM_RECORD_MELLOHI_DESC = "item.record_mellohi.desc"; + public const ITEM_RECORD_OTHERSIDE_DESC = "item.record_otherside.desc"; public const ITEM_RECORD_PIGSTEP_DESC = "item.record_pigstep.desc"; public const ITEM_RECORD_STAL_DESC = "item.record_stal.desc"; public const ITEM_RECORD_STRAD_DESC = "item.record_strad.desc"; diff --git a/src/lang/Language.php b/src/lang/Language.php index 7e6f6cfcc..57c5c78e1 100644 --- a/src/lang/Language.php +++ b/src/lang/Language.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\lang; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_filter; use function array_map; use function count; @@ -173,6 +173,14 @@ class Language{ return $this->internalGet($id) ?? $id; } + /** + * @return string[] + * @phpstan-return array + */ + public function getAll() : array{ + return $this->lang; + } + protected function parseTranslation(string $text, ?string $onlyPrefix = null) : string{ $newString = ""; diff --git a/src/network/mcpe/ComplexWindowMapEntry.php b/src/network/mcpe/ComplexWindowMapEntry.php new file mode 100644 index 000000000..c2792297b --- /dev/null +++ b/src/network/mcpe/ComplexWindowMapEntry.php @@ -0,0 +1,64 @@ + + */ + private array $reverseSlotMap = []; + + /** + * @param int[] $slotMap + * @phpstan-param array $slotMap + */ + public function __construct( + private Inventory $inventory, + private array $slotMap + ){ + foreach($slotMap as $slot => $index){ + $this->reverseSlotMap[$index] = $slot; + } + } + + public function getInventory() : Inventory{ return $this->inventory; } + + /** + * @return int[] + * @phpstan-return array + */ + public function getSlotMap() : array{ return $this->slotMap; } + + public function mapNetToCore(int $slot) : ?int{ + return $this->slotMap[$slot] ?? null; + } + + public function mapCoreToNet(int $slot) : ?int{ + return $this->reverseSlotMap[$slot] ?? null; + } +} diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index a9578cf67..2aea0e0da 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -53,6 +53,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds; use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction; +use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset; use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes; use pocketmine\network\PacketHandlingException; use pocketmine\player\Player; @@ -61,6 +62,7 @@ use pocketmine\utils\ObjectSet; use function array_map; use function array_search; use function get_class; +use function is_int; use function max; use function spl_object_id; @@ -70,6 +72,17 @@ use function spl_object_id; class InventoryManager{ /** @var Inventory[] */ private array $windowMap = []; + /** + * @var ComplexWindowMapEntry[] + * @phpstan-var array + */ + private array $complexWindows = []; + /** + * @var ComplexWindowMapEntry[] + * @phpstan-var array + */ + private array $complexSlotToWindowMap = []; + private int $lastInventoryNetworkId = ContainerIds::FIRST; /** @@ -96,7 +109,8 @@ class InventoryManager{ $this->add(ContainerIds::INVENTORY, $this->player->getInventory()); $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory()); $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); - $this->add(ContainerIds::UI, $this->player->getCursorInventory()); + $this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory()); + $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid()); $this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{ $this->syncSelectedHotbarSlot(); @@ -113,8 +127,27 @@ class InventoryManager{ return $this->lastInventoryNetworkId; } + /** + * @param int[]|int $slotMap + * @phpstan-param array|int $slotMap + */ + private function addComplex(array|int $slotMap, Inventory $inventory) : void{ + $entry = new ComplexWindowMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap); + $this->complexWindows[spl_object_id($inventory)] = $entry; + foreach($entry->getSlotMap() as $netSlot => $coreSlot){ + $this->complexSlotToWindowMap[$netSlot] = $entry; + } + } + private function remove(int $id) : void{ - unset($this->windowMap[$id], $this->initiatedSlotChanges[$id]); + $inventory = $this->windowMap[$id]; + $splObjectId = spl_object_id($inventory); + unset($this->windowMap[$id], $this->initiatedSlotChanges[$id], $this->complexWindows[$splObjectId]); + foreach($this->complexSlotToWindowMap as $netSlot => $entry){ + if($entry->getInventory() === $inventory){ + unset($this->complexSlotToWindowMap[$netSlot]); + } + } } public function getWindowId(Inventory $inventory) : ?int{ @@ -125,8 +158,22 @@ class InventoryManager{ return $this->lastInventoryNetworkId; } - public function getWindow(int $windowId) : ?Inventory{ - return $this->windowMap[$windowId] ?? null; + /** + * @phpstan-return array{Inventory, int} + */ + public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{ + if($windowId === ContainerIds::UI){ + $entry = $this->complexSlotToWindowMap[$netSlotId] ?? null; + if($entry === null){ + return null; + } + $coreSlotId = $entry->mapNetToCore($netSlotId); + return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null; + } + if(isset($this->windowMap[$windowId])){ + return [$this->windowMap[$windowId], $netSlotId]; + } + return null; } public function onTransactionStart(InventoryTransaction $tx) : void{ @@ -179,11 +226,30 @@ class InventoryManager{ } } + /** + * @return int[]|null + * @phpstan-return array|null + */ + private function createComplexSlotMapping(Inventory $inventory) : ?array{ + //TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM + return match(true){ + $inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL, + $inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE, + $inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM, + $inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT], + $inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT, + default => null, + }; + } + public function onCurrentWindowChange(Inventory $inventory) : void{ $this->onCurrentWindowRemove(); $this->openWindowDeferred(function() use ($inventory) : void{ $windowId = $this->addDynamic($inventory); + if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){ + $this->addComplex($slotMap, $inventory); + } foreach($this->containerOpenCallbacks as $callback){ $pks = $callback($windowId, $inventory); @@ -282,10 +348,17 @@ class InventoryManager{ } public function syncSlot(Inventory $inventory, int $slot) : void{ - $windowId = $this->getWindowId($inventory); - if($windowId !== null){ + $slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null; + if($slotMap !== null){ + $windowId = ContainerIds::UI; + $netSlot = $slotMap->mapCoreToNet($slot) ?? null; + }else{ + $windowId = $this->getWindowId($inventory); + $netSlot = $slot; + } + if($windowId !== null && $netSlot !== null){ $currentItem = $inventory->getItem($slot); - $clientSideItem = $this->initiatedSlotChanges[$windowId][$slot] ?? null; + $clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null; if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){ $itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem)); if($windowId === ContainerIds::OFFHAND){ @@ -296,30 +369,37 @@ class InventoryManager{ //BDS (Bedrock Dedicated Server) also seems to work this way. $this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper])); }else{ - $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $slot, $itemStackWrapper)); + $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper)); } } - unset($this->initiatedSlotChanges[$windowId][$slot]); + unset($this->initiatedSlotChanges[$windowId][$netSlot]); } } public function syncContents(Inventory $inventory) : void{ - $windowId = $this->getWindowId($inventory); + $slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null; + if($slotMap !== null){ + $windowId = ContainerIds::UI; + }else{ + $windowId = $this->getWindowId($inventory); + } + $typeConverter = TypeConverter::getInstance(); if($windowId !== null){ - unset($this->initiatedSlotChanges[$windowId]); - $typeConverter = TypeConverter::getInstance(); - if($windowId === ContainerIds::UI){ - //TODO: HACK! - //Since 1.13, cursor is now part of a larger "UI inventory", and sending contents for this larger inventory does - //not work the way it's intended to. Even if it did, it would be necessary to send all 51 slots just to update - //this one, which is just not worth it. - //This workaround isn't great, but it's at least simple. - $this->session->sendDataPacket(InventorySlotPacket::create( - $windowId, - 0, - ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem(0))) - )); + if($slotMap !== null){ + foreach($inventory->getContents(true) as $slotId => $item){ + $packetSlot = $slotMap->mapCoreToNet($slotId) ?? null; + if($packetSlot === null){ + continue; + } + unset($this->initiatedSlotChanges[$windowId][$packetSlot]); + $this->session->sendDataPacket(InventorySlotPacket::create( + $windowId, + $packetSlot, + ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId))) + )); + } }else{ + unset($this->initiatedSlotChanges[$windowId]); $this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{ return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack)); }, $inventory->getContents(true)))); @@ -331,16 +411,20 @@ class InventoryManager{ foreach($this->windowMap as $inventory){ $this->syncContents($inventory); } + foreach($this->complexWindows as $entry){ + $this->syncContents($entry->getInventory()); + } } public function syncMismatchedPredictedSlotChanges() : void{ foreach($this->initiatedSlotChanges as $windowId => $slots){ - if(!isset($this->windowMap[$windowId])){ - continue; - } - $inventory = $this->windowMap[$windowId]; + foreach($slots as $netSlot => $expectedItem){ + $located = $this->locateWindowAndSlot($windowId, $netSlot); + if($located === null){ + continue; + } + [$inventory, $slot] = $located; - foreach($slots as $slot => $expectedItem){ if(!$inventory->slotExists($slot)){ continue; //TODO: size desync ??? } diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index dd8668637..8e76bced5 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -88,6 +88,7 @@ use pocketmine\network\mcpe\protocol\SetTimePacket; use pocketmine\network\mcpe\protocol\SetTitlePacket; use pocketmine\network\mcpe\protocol\TakeItemActorPacket; use pocketmine\network\mcpe\protocol\TextPacket; +use pocketmine\network\mcpe\protocol\ToastRequestPacket; use pocketmine\network\mcpe\protocol\TransferPacket; use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\network\mcpe\protocol\types\command\CommandData; @@ -390,9 +391,8 @@ class NetworkSession{ throw new PacketHandlingException("Unexpected non-serverbound packet"); } - $timings = Timings::getReceiveDataPacketTimings($packet); + $timings = Timings::getDecodeDataPacketTimings($packet); $timings->startTiming(); - try{ $stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext); try{ @@ -404,7 +404,15 @@ class NetworkSession{ $remains = substr($stream->getBuffer(), $stream->getOffset()); $this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains)); } + }finally{ + $timings->stopTiming(); + } + $timings = Timings::getHandleDataPacketTimings($packet); + $timings->startTiming(); + try{ + //TODO: I'm not sure DataPacketReceiveEvent should be included in the handler timings, but it needs to be + //included for now to ensure the receivePacket timings are counted the way they were before $ev = new DataPacketReceiveEvent($this, $packet); $ev->call(); if(!$ev->isCancelled() && ($this->handler === null || !$packet->handle($this->handler))){ @@ -1094,6 +1102,10 @@ class NetworkSession{ $this->sendDataPacket(EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER)); } + public function onToastNotification(string $title, string $body) : void{ + $this->sendDataPacket(ToastRequestPacket::create($title, $body)); + } + public function tick() : void{ if($this->info === null){ if(time() >= $this->connectTime + 10){ diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php index b4e1a7150..c3eca501d 100644 --- a/src/network/mcpe/cache/StaticPacketCache.php +++ b/src/network/mcpe/cache/StaticPacketCache.php @@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\utils\SingletonTrait; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function file_get_contents; class StaticPacketCache{ diff --git a/src/network/mcpe/convert/GlobalItemTypeDictionary.php b/src/network/mcpe/convert/GlobalItemTypeDictionary.php index 6940a91d7..f33683346 100644 --- a/src/network/mcpe/convert/GlobalItemTypeDictionary.php +++ b/src/network/mcpe/convert/GlobalItemTypeDictionary.php @@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function file_get_contents; use function is_array; use function is_bool; diff --git a/src/network/mcpe/convert/ItemTranslator.php b/src/network/mcpe/convert/ItemTranslator.php index 759eefbca..f58e0bc22 100644 --- a/src/network/mcpe/convert/ItemTranslator.php +++ b/src/network/mcpe/convert/ItemTranslator.php @@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_key_exists; use function file_get_contents; use function is_array; diff --git a/src/network/mcpe/convert/RuntimeBlockMapping.php b/src/network/mcpe/convert/RuntimeBlockMapping.php index 8a47de971..547bd0d81 100644 --- a/src/network/mcpe/convert/RuntimeBlockMapping.php +++ b/src/network/mcpe/convert/RuntimeBlockMapping.php @@ -32,7 +32,7 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext; use pocketmine\utils\SingletonTrait; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function file_get_contents; /** diff --git a/src/network/mcpe/convert/TypeConverter.php b/src/network/mcpe/convert/TypeConverter.php index 424641257..b1f5f1f38 100644 --- a/src/network/mcpe/convert/TypeConverter.php +++ b/src/network/mcpe/convert/TypeConverter.php @@ -24,11 +24,6 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; use pocketmine\block\BlockLegacyIds; -use pocketmine\block\inventory\AnvilInventory; -use pocketmine\block\inventory\CraftingTableInventory; -use pocketmine\block\inventory\EnchantInventory; -use pocketmine\block\inventory\LoomInventory; -use pocketmine\block\inventory\StonecutterInventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DropItemAction; @@ -285,38 +280,12 @@ class TypeConverter{ } switch($action->sourceType){ case NetworkInventoryAction::SOURCE_CONTAINER: - $window = null; - if($action->windowId === ContainerIds::UI && $action->inventorySlot > 0){ - if($action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){ - return null; //useless noise - } - $pSlot = $action->inventorySlot; - - $slot = UIInventorySlotOffset::CRAFTING2X2_INPUT[$pSlot] ?? null; - if($slot !== null){ - $window = $player->getCraftingGrid(); - }elseif(($current = $player->getCurrentWindow()) !== null){ - $slotMap = match(true){ - $current instanceof AnvilInventory => UIInventorySlotOffset::ANVIL, - $current instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE, - $current instanceof LoomInventory => UIInventorySlotOffset::LOOM, - $current instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT], - $current instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT, - default => null - }; - if($slotMap !== null){ - $window = $current; - $slot = $slotMap[$pSlot] ?? null; - } - } - if($slot === null){ - throw new TypeConversionException("Unmatched UI inventory slot offset $pSlot"); - } - }else{ - $window = $inventoryManager->getWindow($action->windowId); - $slot = $action->inventorySlot; + if($action->windowId === ContainerIds::UI && $action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){ + return null; //useless noise } - if($window !== null){ + $located = $inventoryManager->locateWindowAndSlot($action->windowId, $action->inventorySlot); + if($located !== null){ + [$window, $slot] = $located; return new SlotChangeAction($window, $slot, $old, $new); } @@ -338,15 +307,10 @@ class TypeConverter{ } case NetworkInventoryAction::SOURCE_TODO: - //These types need special handling. - switch($action->windowId){ - case NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT: - case NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT: - return null; - } - - //TODO: more stuff - throw new TypeConversionException("No open container with window ID $action->windowId"); + //These are used to balance a transaction that involves special actions, like crafting, enchanting, etc. + //The vanilla server just accepted these without verifying them. We don't need to care about them since + //we verify crafting by checking for imbalances anyway. + return null; default: throw new TypeConversionException("Unknown inventory source type $action->sourceType"); } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index 940926602..53bcd0345 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -340,12 +340,7 @@ class InGamePacketHandler extends PacketHandler{ $converter = TypeConverter::getInstance(); foreach($data->getActions() as $networkInventoryAction){ if( - ( - $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO && ( - $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT || - $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT - ) - ) || ( + $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO || ( $this->craftingTransaction !== null && !$networkInventoryAction->oldItem->getItemStack()->equals($networkInventoryAction->newItem->getItemStack()) && $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php index fbe549189..04c51d3fb 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -61,16 +61,17 @@ class PreSpawnPacketHandler extends PacketHandler{ public function setUp() : void{ $location = $this->player->getLocation(); + $world = $location->getWorld(); $this->session->getLogger()->debug("Preparing StartGamePacket"); $levelSettings = new LevelSettings(); $levelSettings->seed = -1; $levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly $levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode()); - $levelSettings->difficulty = $location->getWorld()->getDifficulty(); - $levelSettings->spawnPosition = BlockPosition::fromVector3($location->getWorld()->getSpawnLocation()); + $levelSettings->difficulty = $world->getDifficulty(); + $levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation()); $levelSettings->hasAchievementsDisabled = true; - $levelSettings->time = $location->getWorld()->getTime(); + $levelSettings->time = $world->getTime(); $levelSettings->eduEditionOffer = 0; $levelSettings->rainLevel = 0; //TODO: implement these properly $levelSettings->lightningLevel = 0; diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php index a7c603ceb..d1ba85724 100644 --- a/src/network/mcpe/handler/ResourcePacksPacketHandler.php +++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php @@ -65,9 +65,19 @@ class ResourcePacksPacketHandler extends PacketHandler{ ){} public function setUp() : void{ - $resourcePackEntries = array_map(static function(ResourcePack $pack) : ResourcePackInfoEntry{ + $resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{ //TODO: more stuff - return new ResourcePackInfoEntry($pack->getPackId(), $pack->getPackVersion(), $pack->getPackSize(), "", "", "", false); + $encryptionKey = $this->resourcePackManager->getPackEncryptionKey($pack->getPackId()); + + return new ResourcePackInfoEntry( + $pack->getPackId(), + $pack->getPackVersion(), + $pack->getPackSize(), + $encryptionKey ?? "", + "", + $pack->getPackId(), + false + ); }, $this->resourcePackManager->getResourceStack()); //TODO: support forcing server packs $this->session->sendDataPacket(ResourcePacksInfoPacket::create($resourcePackEntries, [], $this->resourcePackManager->resourcePacksRequired(), false, false)); diff --git a/src/player/Player.php b/src/player/Player.php index 904bf5793..0eb4fe5a5 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -85,7 +85,7 @@ use pocketmine\inventory\PlayerCursorInventory; use pocketmine\inventory\TemporaryInventory; use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\InventoryTransaction; -use pocketmine\inventory\transaction\TransactionBuilderInventory; +use pocketmine\inventory\transaction\TransactionBuilder; use pocketmine\inventory\transaction\TransactionCancelledException; use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\ConsumableItem; @@ -1343,6 +1343,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->lastUpdate = $currentTick; + if($this->justCreated){ + $this->onFirstUpdate($currentTick); + } + if(!$this->isAlive() && $this->spawned){ $this->onDeathUpdate($tickDiff); return true; @@ -2013,6 +2017,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->getNetworkSession()->onTip($message); } + /** + * Sends a toast message to the player, or queue to send it if a toast message is already shown. + */ + public function sendToastNotification(string $title, string $body) : void{ + $this->getNetworkSession()->onToastNotification($title, $body); + } + /** * Sends a Form to the player, or queue to send it if a form is already open. * @@ -2460,29 +2471,23 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $inventories[] = $this->currentWindow; } - $transaction = new InventoryTransaction($this); - $mainInventoryTransactionBuilder = new TransactionBuilderInventory($this->inventory); + $builder = new TransactionBuilder(); foreach($inventories as $inventory){ $contents = $inventory->getContents(); if(count($contents) > 0){ - $drops = $mainInventoryTransactionBuilder->addItem(...$contents); + $drops = $builder->getInventory($this->inventory)->addItem(...$contents); foreach($drops as $drop){ - $transaction->addAction(new DropItemAction($drop)); + $builder->addAction(new DropItemAction($drop)); } - $clearedInventoryTransactionBuilder = new TransactionBuilderInventory($inventory); - $clearedInventoryTransactionBuilder->clearAll(); - foreach($clearedInventoryTransactionBuilder->generateActions() as $action){ - $transaction->addAction($action); - } + $builder->getInventory($inventory)->clearAll(); } } - foreach($mainInventoryTransactionBuilder->generateActions() as $action){ - $transaction->addAction($action); - } - if(count($transaction->getActions()) !== 0){ + $actions = $builder->generateActions(); + if(count($actions) !== 0){ + $transaction = new InventoryTransaction($this, $actions); try{ $transaction->execute(); $this->logger->debug("Successfully evacuated items from temporary inventories"); diff --git a/src/plugin/PluginBase.php b/src/plugin/PluginBase.php index e9bdc2f20..9b0ac2399 100644 --- a/src/plugin/PluginBase.php +++ b/src/plugin/PluginBase.php @@ -33,7 +33,7 @@ use pocketmine\Server; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Config; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function count; use function dirname; use function fclose; diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index ea8634bf2..07a099f2f 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -40,7 +40,7 @@ use pocketmine\Server; use pocketmine\timings\TimingsHandler; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_diff_key; use function array_key_exists; use function array_keys; @@ -75,6 +75,9 @@ class PluginManager{ /** @var Plugin[] */ protected $enabledPlugins = []; + /** @var array> */ + private array $pluginDependents = []; + private bool $loadPluginsGuard = false; /** @@ -456,6 +459,15 @@ class PluginManager{ if($plugin->isEnabled()){ //the plugin may have disabled itself during onEnable() $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin; + foreach($plugin->getDescription()->getDepend() as $dependency){ + $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true; + } + foreach($plugin->getDescription()->getSoftDepend() as $dependency){ + if(isset($this->plugins[$dependency])){ + $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true; + } + } + (new PluginEnableEvent($plugin))->call(); return true; @@ -475,8 +487,19 @@ class PluginManager{ } public function disablePlugins() : void{ - foreach($this->getPlugins() as $plugin){ - $this->disablePlugin($plugin); + while(count($this->enabledPlugins) > 0){ + foreach($this->enabledPlugins as $plugin){ + if(!$plugin->isEnabled()){ + continue; //in case a plugin disabled another plugin + } + $name = $plugin->getDescription()->getName(); + if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){ + $this->server->getLogger()->debug("Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(", ", array_keys($this->pluginDependents[$name]))); + continue; + } + + $this->disablePlugin($plugin); + } } } @@ -486,6 +509,12 @@ class PluginManager{ (new PluginDisableEvent($plugin))->call(); unset($this->enabledPlugins[$plugin->getDescription()->getName()]); + foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){ + unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]); + if(count($this->pluginDependents[$dependency]) === 0){ + unset($this->pluginDependents[$dependency]); + } + } $plugin->onEnableStateChange(false); $plugin->getScheduler()->shutdown(); diff --git a/src/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php index 4aaa9afc7..d1482d8b6 100644 --- a/src/resourcepacks/ResourcePackManager.php +++ b/src/resourcepacks/ResourcePackManager.php @@ -23,12 +23,14 @@ declare(strict_types=1); namespace pocketmine\resourcepacks; +use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\utils\Config; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_keys; use function copy; use function count; use function file_exists; +use function file_get_contents; use function gettype; use function is_array; use function is_dir; @@ -49,6 +51,12 @@ class ResourcePackManager{ /** @var ResourcePack[] */ private array $uuidList = []; + /** + * @var string[] + * @phpstan-var array + */ + private array $encryptionKeys = []; + /** * @param string $path Path to resource-packs directory. */ @@ -105,7 +113,19 @@ class ResourcePackManager{ if($newPack instanceof ResourcePack){ $this->resourcePacks[] = $newPack; - $this->uuidList[strtolower($newPack->getPackId())] = $newPack; + $index = strtolower($newPack->getPackId()); + $this->uuidList[$index] = $newPack; + + $keyPath = Path::join($this->path, $pack . ".key"); + if(file_exists($keyPath)){ + try{ + $this->encryptionKeys[$index] = ErrorToExceptionHandler::trapAndRemoveFalse( + fn() => file_get_contents($keyPath) + ); + }catch(\ErrorException $e){ + throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e); + } + } }else{ throw new ResourcePackException("Format not recognized"); } @@ -153,4 +173,11 @@ class ResourcePackManager{ public function getPackIdList() : array{ return array_keys($this->uuidList); } + + /** + * Returns the key with which the pack was encrypted, or null if the pack has no key. + */ + public function getPackEncryptionKey(string $id) : ?string{ + return $this->encryptionKeys[strtolower($id)] ?? null; + } } diff --git a/src/scheduler/DumpWorkerMemoryTask.php b/src/scheduler/DumpWorkerMemoryTask.php index 98b5e8909..b1cf3840c 100644 --- a/src/scheduler/DumpWorkerMemoryTask.php +++ b/src/scheduler/DumpWorkerMemoryTask.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\scheduler; use pocketmine\MemoryManager; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; /** * Task used to dump memory from AsyncWorkers diff --git a/src/timings/Timings.php b/src/timings/Timings.php index 6f0b38cc7..5d0ece80d 100644 --- a/src/timings/Timings.php +++ b/src/timings/Timings.php @@ -119,6 +119,12 @@ abstract class Timings{ public static $tileEntityTypeTimingMap = []; /** @var TimingsHandler[] */ public static $packetReceiveTimingMap = []; + + /** @var TimingsHandler[] */ + private static array $packetDecodeTimingMap = []; + /** @var TimingsHandler[] */ + private static array $packetHandleTimingMap = []; + /** @var TimingsHandler[] */ public static $packetSendTimingMap = []; /** @var TimingsHandler[] */ @@ -229,6 +235,22 @@ abstract class Timings{ return self::$packetReceiveTimingMap[$pid]; } + public static function getDecodeDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{ + $pid = $pk->pid(); + return self::$packetDecodeTimingMap[$pid] ??= new TimingsHandler( + self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Decode - " . $pk->getName() . " [0x" . dechex($pid) . "]", + self::getReceiveDataPacketTimings($pk) + ); + } + + public static function getHandleDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{ + $pid = $pk->pid(); + return self::$packetHandleTimingMap[$pid] ??= new TimingsHandler( + self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Handler - " . $pk->getName() . " [0x" . dechex($pid) . "]", + self::getReceiveDataPacketTimings($pk) + ); + } + public static function getSendDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{ $pid = $pk->pid(); if(!isset(self::$packetSendTimingMap[$pid])){ diff --git a/src/utils/BroadcastLoggerForwarder.php b/src/utils/BroadcastLoggerForwarder.php new file mode 100644 index 000000000..a015615b5 --- /dev/null +++ b/src/utils/BroadcastLoggerForwarder.php @@ -0,0 +1,79 @@ +perm = new PermissibleBase([]); + } + + public function getLanguage() : Language{ + return $this->language; + } + + public function sendMessage(Translatable|string $message) : void{ + if($message instanceof Translatable){ + $this->logger->info($this->language->translate($message)); + }else{ + $this->logger->info($message); + } + } + + public function getServer() : Server{ + return $this->server; + } + + public function getName() : string{ + return "Broadcast Logger Forwarder"; + } + + public function getScreenLineHeight() : int{ + return PHP_INT_MAX; + } + + public function setScreenLineHeight(?int $height) : void{ + //NOOP + } +} diff --git a/src/utils/Config.php b/src/utils/Config.php index e2bc7733a..1e6c187dd 100644 --- a/src/utils/Config.php +++ b/src/utils/Config.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\utils; use pocketmine\errorhandler\ErrorToExceptionHandler; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_change_key_case; use function array_fill_keys; use function array_keys; diff --git a/src/utils/Filesystem.php b/src/utils/Filesystem.php index 20ec8b312..a4c1ff236 100644 --- a/src/utils/Filesystem.php +++ b/src/utils/Filesystem.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\utils; use pocketmine\errorhandler\ErrorToExceptionHandler; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function copy; use function dirname; use function fclose; diff --git a/src/utils/MainLogger.php b/src/utils/MainLogger.php index 6cefce218..139388af7 100644 --- a/src/utils/MainLogger.php +++ b/src/utils/MainLogger.php @@ -191,7 +191,7 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{ $threadName = (new \ReflectionClass($thread))->getShortName() . " thread"; } - $message = sprintf($this->format, $time->format("H:i:s.v"), $color, $threadName, $prefix, TextFormat::clean($message, false)); + $message = sprintf($this->format, $time->format("H:i:s.v"), $color, $threadName, $prefix, TextFormat::addBase($color, TextFormat::clean($message, false))); if(!Terminal::isInit()){ Terminal::init($this->useFormattingCodes); //lazy-init colour codes because we don't know if they've been registered on this thread diff --git a/src/utils/TextFormat.php b/src/utils/TextFormat.php index d2250057f..dfd6a359a 100644 --- a/src/utils/TextFormat.php +++ b/src/utils/TextFormat.php @@ -158,6 +158,31 @@ abstract class TextFormat{ return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-gk-or])/u', TextFormat::ESCAPE . '$1', $string); } + /** + * Adds base formatting to the string. The given format codes will be inserted directly after any RESET (§r) codes. + * + * This is useful for log messages, where a RESET code should return to the log message's original colour (e.g. + * blue for NOTICE), rather than whatever the terminal's base text colour is (usually some off-white colour). + * + * Example behaviour: + * - 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. + */ + public static function addBase(string $baseFormat, string $string) : string{ + $baseFormatParts = self::tokenize($baseFormat); + foreach($baseFormatParts as $part){ + if(!isset(self::FORMATS[$part]) && !isset(self::COLORS[$part])){ + throw new \InvalidArgumentException("Unexpected base format token \"$part\", expected only color and format tokens"); + } + } + $baseFormat = self::RESET . $baseFormat; + + return $baseFormat . str_replace(TextFormat::RESET, $baseFormat, $string); + } + /** * Returns an HTML-formatted string with colors/markup */ diff --git a/src/wizard/SetupWizard.php b/src/wizard/SetupWizard.php index 8373b0e8b..c9170bd8d 100644 --- a/src/wizard/SetupWizard.php +++ b/src/wizard/SetupWizard.php @@ -39,7 +39,7 @@ use pocketmine\utils\Internet; use pocketmine\utils\InternetException; use pocketmine\utils\Utils; use pocketmine\VersionInfo; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function fgets; use function sleep; use function strtolower; diff --git a/src/world/World.php b/src/world/World.php index f820a787f..b41ba3bb4 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -66,6 +66,7 @@ use pocketmine\network\mcpe\protocol\BlockActorDataPacket; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\network\mcpe\protocol\UpdateBlockPacket; +use pocketmine\player\ChunkSelector; use pocketmine\player\Player; use pocketmine\promise\Promise; use pocketmine\promise\PromiseResolver; @@ -317,7 +318,6 @@ class World implements ChunkManager{ private int $sleepTicks = 0; private int $chunkTickRadius; - private int $chunksPerTick; private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK; /** * @var true[] @@ -493,7 +493,11 @@ class World implements ChunkManager{ $cfg = $this->server->getConfigGroup(); $this->chunkTickRadius = min($this->server->getViewDistance(), max(1, $cfg->getPropertyInt("chunk-ticking.tick-radius", 4))); - $this->chunksPerTick = $cfg->getPropertyInt("chunk-ticking.per-tick", 40); + if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){ + //TODO: this needs l10n + $this->logger->warning("\"chunk-ticking.per-tick\" setting is deprecated, but you've used it to disable chunk ticking. Set \"chunk-ticking.tick-radius\" to 0 in \"pocketmine.yml\" instead."); + $this->chunkTickRadius = 0; + } $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt("chunk-ticking.blocks-per-subchunk-per-tick", self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK); $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt("chunk-generation.population-queue-size", 2); @@ -1100,8 +1104,23 @@ class World implements ChunkManager{ unset($this->randomTickBlocks[$block->getFullId()]); } + /** + * Returns the radius of chunks to be ticked around each ticking chunk loader (usually players). This is referred to + * as "simulation distance" in the Minecraft: Bedrock world options screen. + */ + public function getChunkTickRadius() : int{ + return $this->chunkTickRadius; + } + + /** + * Sets the radius of chunks ticked around each ticking chunk loader (usually players). + */ + public function setChunkTickRadius(int $radius) : void{ + $this->chunkTickRadius = $radius; + } + private function tickChunks() : void{ - if($this->chunksPerTick <= 0 || count($this->tickingLoaders) === 0){ + if($this->chunkTickRadius <= 0 || count($this->tickingLoaders) === 0){ return; } @@ -1110,19 +1129,26 @@ class World implements ChunkManager{ /** @var bool[] $chunkTickList chunkhash => dummy */ $chunkTickList = []; - $chunksPerLoader = min(200, max(1, (int) ((($this->chunksPerTick - count($this->tickingLoaders)) / count($this->tickingLoaders)) + 0.5))); - $randRange = 3 + $chunksPerLoader / 30; - $randRange = (int) ($randRange > $this->chunkTickRadius ? $this->chunkTickRadius : $randRange); + $centerChunks = []; + $selector = new ChunkSelector(); foreach($this->tickingLoaders as $loader){ - $chunkX = (int) floor($loader->getX()) >> Chunk::COORD_BIT_SIZE; - $chunkZ = (int) floor($loader->getZ()) >> Chunk::COORD_BIT_SIZE; + $centerChunkX = (int) floor($loader->getX()) >> Chunk::COORD_BIT_SIZE; + $centerChunkZ = (int) floor($loader->getZ()) >> Chunk::COORD_BIT_SIZE; + $centerChunkPosHash = World::chunkHash($centerChunkX, $centerChunkZ); + if(isset($centerChunks[$centerChunkPosHash])){ + //we already queued chunks in this radius because of a previous loader on the same chunk + continue; + } + $centerChunks[$centerChunkPosHash] = true; - for($chunk = 0; $chunk < $chunksPerLoader; ++$chunk){ - $dx = mt_rand(-$randRange, $randRange); - $dz = mt_rand(-$randRange, $randRange); - $hash = World::chunkHash($dx + $chunkX, $dz + $chunkZ); - if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($dx + $chunkX, $dz + $chunkZ)){ + foreach($selector->selectChunks( + $this->chunkTickRadius, + $centerChunkX, + $centerChunkZ + ) as $hash){ + World::getXZ($hash, $chunkX, $chunkZ); + if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($chunkX, $chunkZ)){ $chunkTickList[$hash] = true; } } diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php index 83f7aac98..1c26d494b 100644 --- a/src/world/WorldManager.php +++ b/src/world/WorldManager.php @@ -39,7 +39,7 @@ use pocketmine\world\format\io\WorldProviderManager; use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\generator\InvalidGeneratorOptionsException; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_keys; use function array_shift; use function assert; diff --git a/src/world/format/io/FormatConverter.php b/src/world/format/io/FormatConverter.php index 48f4d67b9..5f93f3a7b 100644 --- a/src/world/format/io/FormatConverter.php +++ b/src/world/format/io/FormatConverter.php @@ -27,7 +27,7 @@ use pocketmine\utils\Filesystem; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\generator\normal\Normal; use pocketmine\world\WorldCreationOptions; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function basename; use function crc32; use function file_exists; diff --git a/src/world/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php index b93acac68..995f64898 100644 --- a/src/world/format/io/data/BedrockWorldData.php +++ b/src/world/format/io/data/BedrockWorldData.php @@ -38,7 +38,7 @@ use pocketmine\world\generator\Flat; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\World; use pocketmine\world\WorldCreationOptions; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function file_get_contents; use function file_put_contents; use function strlen; diff --git a/src/world/format/io/data/JavaWorldData.php b/src/world/format/io/data/JavaWorldData.php index e57bbe941..e53d857ad 100644 --- a/src/world/format/io/data/JavaWorldData.php +++ b/src/world/format/io/data/JavaWorldData.php @@ -35,7 +35,7 @@ use pocketmine\world\format\io\exception\CorruptedWorldException; use pocketmine\world\generator\GeneratorManager; use pocketmine\world\World; use pocketmine\world\WorldCreationOptions; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function ceil; use function file_get_contents; use function file_put_contents; diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php index 9732a9fbb..f1f8d6daf 100644 --- a/src/world/format/io/leveldb/LevelDB.php +++ b/src/world/format/io/leveldb/LevelDB.php @@ -50,7 +50,7 @@ use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\SubChunk; use pocketmine\world\WorldCreationOptions; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function array_map; use function array_values; use function chr; diff --git a/src/world/format/io/region/RegionWorldProvider.php b/src/world/format/io/region/RegionWorldProvider.php index 3007b0629..0efd50c4c 100644 --- a/src/world/format/io/region/RegionWorldProvider.php +++ b/src/world/format/io/region/RegionWorldProvider.php @@ -32,7 +32,7 @@ use pocketmine\world\format\io\ChunkData; use pocketmine\world\format\io\data\JavaWorldData; use pocketmine\world\format\io\exception\CorruptedChunkException; use pocketmine\world\format\io\WorldData; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function assert; use function file_exists; use function is_dir; diff --git a/src/world/format/io/region/WritableRegionWorldProvider.php b/src/world/format/io/region/WritableRegionWorldProvider.php index fd4399474..56cc2ff71 100644 --- a/src/world/format/io/region/WritableRegionWorldProvider.php +++ b/src/world/format/io/region/WritableRegionWorldProvider.php @@ -27,7 +27,7 @@ use pocketmine\world\format\io\ChunkData; use pocketmine\world\format\io\data\JavaWorldData; use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\WorldCreationOptions; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function file_exists; use function mkdir; diff --git a/src/world/sound/ItemFrameAddItemSound.php b/src/world/sound/ItemFrameAddItemSound.php new file mode 100644 index 000000000..22e35aead --- /dev/null +++ b/src/world/sound/ItemFrameAddItemSound.php @@ -0,0 +1,35 @@ +\\|string\\|false and '/'\\|'\\\\\\\\' results in an error\\.$#" - count: 2 - path: ../../../src/PocketMine.php - - message: "#^Do\\-while loop condition is always false\\.$#" count: 1 path: ../../../src/PocketMine.php - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: ../../../src/Server.php - - message: "#^Parameter \\#1 \\$array of static method pocketmine\\\\plugin\\\\PluginGraylist\\:\\:fromArray\\(\\) expects array, mixed given\\.$#" count: 1 diff --git a/tests/phpunit/world/format/io/region/RegionLoaderTest.php b/tests/phpunit/world/format/io/region/RegionLoaderTest.php index abf73a0f5..e4db90072 100644 --- a/tests/phpunit/world/format/io/region/RegionLoaderTest.php +++ b/tests/phpunit/world/format/io/region/RegionLoaderTest.php @@ -25,7 +25,7 @@ namespace pocketmine\world\format\io\region; use PHPUnit\Framework\TestCase; use pocketmine\world\format\ChunkException; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function bin2hex; use function clearstatcache; use function file_exists; diff --git a/tools/generate-permission-doc.php b/tools/generate-permission-doc.php index 7d66740aa..fd04f1a29 100644 --- a/tools/generate-permission-doc.php +++ b/tools/generate-permission-doc.php @@ -27,7 +27,7 @@ use pocketmine\permission\DefaultPermissions; use pocketmine\permission\PermissionManager; use pocketmine\utils\Utils; use pocketmine\VersionInfo; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function count; use function dirname; use function fclose; diff --git a/tools/simulate-chunk-selector.php b/tools/simulate-chunk-selector.php index e360a96e9..81beb6bb3 100644 --- a/tools/simulate-chunk-selector.php +++ b/tools/simulate-chunk-selector.php @@ -28,7 +28,7 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; use pocketmine\world\format\Chunk; use pocketmine\world\World; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; use function assert; use function count; use function dirname;