diff --git a/.github/workflows/discord-release-notify.yml b/.github/workflows/discord-release-notify.yml index deb8e9ff0..bc3658fd1 100644 --- a/.github/workflows/discord-release-notify.yml +++ b/.github/workflows/discord-release-notify.yml @@ -15,7 +15,7 @@ jobs: - name: Setup PHP and tools uses: shivammathur/setup-php@2.24.0 with: - php-version: 8.0 + php-version: 8.1 - name: Restore Composer package cache uses: actions/cache@v3 diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index cce750118..604a69bba 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@2.24.0 with: - php-version: 8.0 + php-version: 8.1 - name: Restore Composer package cache uses: actions/cache@v3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e3f7124c5..da8468a81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: image: [ubuntu-20.04] - php: [8.0.28, 8.1.18, 8.2.5] + php: [8.1.19, 8.2.6] steps: - name: Build and prepare PHP cache @@ -32,7 +32,7 @@ jobs: fail-fast: false matrix: image: [ubuntu-20.04] - php: [8.0.28, 8.1.18, 8.2.5] + php: [8.1.19, 8.2.6] steps: - uses: actions/checkout@v3 @@ -71,7 +71,7 @@ jobs: fail-fast: false matrix: image: [ubuntu-20.04] - php: [8.0.28, 8.1.18, 8.2.5] + php: [8.1.19, 8.2.6] steps: - uses: actions/checkout@v3 @@ -110,7 +110,7 @@ jobs: fail-fast: false matrix: image: [ubuntu-20.04] - php: [8.0.28, 8.1.18, 8.2.5] + php: [8.1.19, 8.2.6] steps: - uses: actions/checkout@v3 @@ -151,7 +151,7 @@ jobs: fail-fast: false matrix: image: [ubuntu-20.04] - php: [8.0.28, 8.1.18, 8.2.5] + php: [8.1.19, 8.2.6] steps: - uses: actions/checkout@v3 @@ -205,7 +205,7 @@ jobs: - name: Setup PHP and tools uses: shivammathur/setup-php@2.24.0 with: - php-version: 8.0 + php-version: 8.1 tools: php-cs-fixer:3.16 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-php-versions.php b/.github/workflows/update-php-versions.php index 92e79a6de..2455ba101 100644 --- a/.github/workflows/update-php-versions.php +++ b/.github/workflows/update-php-versions.php @@ -22,7 +22,6 @@ declare(strict_types=1); const VERSIONS = [ - "8.0", "8.1", "8.2" ]; diff --git a/BUILDING.md b/BUILDING.md index d6e97e05c..95197de6b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -2,13 +2,13 @@ ## Pre-requisites - A bash shell (git bash is sufficient for Windows) - [`git`](https://git-scm.com) available in your shell -- PHP 8.0 or newer available in your shell +- PHP 8.1 or newer available in your shell - [`composer`](https://getcomposer.org) available in your shell ## Custom PHP binaries Because PocketMine-MP requires several non-standard PHP extensions and configuration, PMMP provides scripts to build custom binaries for running PocketMine-MP, as well as prebuilt binaries. -- [Prebuilt binaries](https://jenkins.pmmp.io/job/PHP-8.0-Aggregate) +- [Prebuilt binaries](https://github.com/pmmp/PHP-Binaries/releases) - [Compile scripts](https://github.com/pmmp/php-build-scripts) are provided as a submodule in the path `build/php` If you use a custom binary, you'll need to replace `composer` usages in this guide with `path/to/your/php path/to/your/composer.phar`. @@ -29,11 +29,5 @@ Run `composer make-server` using your preferred PHP binary. It'll drop a `Pocket You can also use the `--out` option to change the output filename. -There is a bug in PHP that might cause an error which looks like this: -``` -Fatal error: Uncaught BadMethodCallException: unable to create temporary file in PocketMine-MP/build/server-phar.php:119 -``` -You can work around it by setting `ulimit -n` to some bigger number, e.g. `8192`, or by updating your PHP version to at least 8.0.3. - ## Running PocketMine-MP from source code Run `src/PocketMine.php` using your preferred PHP binary. diff --git a/build/php b/build/php index 9d8807be8..f860ade30 160000 --- a/build/php +++ b/build/php @@ -1 +1 @@ -Subproject commit 9d8807be825b3fafd420534f2c29351c1bda6398 +Subproject commit f860ade30acc074a98bbf5ff286f35b5eda10c86 diff --git a/composer.json b/composer.json index 630b31c17..0c1470be3 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "homepage": "https://pmmp.io", "license": "LGPL-3.0", "require": { - "php": "^8.0", + "php": "^8.1", "php-64bit": "*", "ext-chunkutils2": "^0.3.1", "ext-crypto": "^0.3.1", @@ -77,7 +77,7 @@ }, "config": { "platform": { - "php": "8.0.0" + "php": "8.1.0" }, "sort-packages": true }, diff --git a/composer.lock b/composer.lock index a0e4aa8e4..fa28f6b62 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": "a5ffe862f4e6376eaf78593a4bb8aeb6", + "content-hash": "ecb1e46a4410fdc7efb7e3dac60b5322", "packages": [ { "name": "adhocore/json-comment", @@ -903,21 +903,20 @@ }, { "name": "ramsey/collection", - "version": "1.3.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4" + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/ad7475d1c9e70b190ecffc58f2d989416af339b4", - "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "symfony/polyfill-php81": "^1.23" + "php": "^8.1" }, "require-dev": { "captainhook/plugin-composer": "^5.3", @@ -977,7 +976,7 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.3.0" + "source": "https://github.com/ramsey/collection/tree/2.0.0" }, "funding": [ { @@ -989,7 +988,7 @@ "type": "tidelift" } ], - "time": "2022-12-27T19:12:24+00:00" + "time": "2022-12-31T21:50:55+00:00" }, { "name": "ramsey/uuid", @@ -1395,85 +1394,6 @@ ], "time": "2022-11-03T14:55:06+00:00" }, - { - "name": "symfony/polyfill-php81", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", - "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\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "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 backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/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": "webmozart/assert", "version": "1.11.0", @@ -1587,30 +1507,30 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -1637,7 +1557,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -1653,7 +1573,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "myclabs/deep-copy", @@ -3486,7 +3406,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.0", + "php": "^8.1", "php-64bit": "*", "ext-chunkutils2": "^0.3.1", "ext-crypto": "^0.3.1", @@ -3515,7 +3435,7 @@ }, "platform-dev": [], "platform-overrides": { - "php": "8.0.0" + "php": "8.1.0" }, "plugin-api-version": "2.3.0" } diff --git a/resources/pocketmine.yml b/resources/pocketmine.yml index ac60afe53..408b5b95b 100644 --- a/resources/pocketmine.yml +++ b/resources/pocketmine.yml @@ -85,8 +85,11 @@ network: batch-threshold: 256 #Compression level used when sending batched packets. Higher = more CPU, less bandwidth usage compression-level: 6 - #Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread + #Use AsyncTasks for compression during the main game session. Increases latency, but may reduce main thread load async-compression: false + #Threshold for async compression, in bytes. Only packets larger than this will be compressed asynchronously + #Due to large overhead of AsyncTask, async compression isn't worth it except for large packets + async-compression-threshold: 10000 #Experimental. Use UPnP to automatically port forward upnp-forwarding: false #Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be diff --git a/src/PocketMine.php b/src/PocketMine.php index 4b0b644ec..c653d33ea 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -50,7 +50,7 @@ namespace pocketmine { require_once __DIR__ . '/VersionInfo.php'; - const MIN_PHP_VERSION = "8.0.0"; + const MIN_PHP_VERSION = "8.1.0"; /** * @param string $message @@ -265,9 +265,6 @@ JIT_WARNING exit(1); } } - if(extension_loaded('parallel')){ - \parallel\bootstrap(\pocketmine\COMPOSER_AUTOLOADER_PATH); - } ErrorToExceptionHandler::set(); diff --git a/src/Server.php b/src/Server.php index a81b1d74b..11a2b157a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -208,6 +208,8 @@ class Server{ private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND; private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND; + private const DEFAULT_ASYNC_COMPRESSION_THRESHOLD = 10_000; + private static ?Server $instance = null; private TimeTrackingSleeperHandler $tickSleeper; @@ -266,6 +268,7 @@ class Server{ private Network $network; private bool $networkCompressionAsync = true; + private int $networkCompressionAsyncThreshold = self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD; private Language $language; private bool $forceLanguage = false; @@ -908,6 +911,10 @@ class Server{ ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE)); $this->networkCompressionAsync = $this->configGroup->getPropertyBool("network.async-compression", true); + $this->networkCompressionAsyncThreshold = max( + $this->configGroup->getPropertyInt("network.async-compression-threshold", self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD), + $netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD + ); EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool("network.enable-encryption", true); @@ -1375,7 +1382,7 @@ class Server{ } $promise = new CompressBatchPromise(); - if(!$sync){ + if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){ $task = new CompressBatchTask($buffer, $promise, $compressor); $this->asyncPool->submitTask($task); }else{ diff --git a/src/block/inventory/DoubleChestInventory.php b/src/block/inventory/DoubleChestInventory.php index 92c75ef9f..8c38e190d 100644 --- a/src/block/inventory/DoubleChestInventory.php +++ b/src/block/inventory/DoubleChestInventory.php @@ -85,6 +85,20 @@ class DoubleChestInventory extends BaseInventory implements BlockInventory, Inve $this->right->setContents($rightContents); } + protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{ + $leftSize = $this->left->getSize(); + return $slot < $leftSize ? + $this->left->getMatchingItemCount($slot, $test, $checkDamage, $checkTags) : + $this->right->getMatchingItemCount($slot - $leftSize, $test, $checkDamage, $checkTags); + } + + public function isSlotEmpty(int $index) : bool{ + $leftSize = $this->left->getSize(); + return $index < $leftSize ? + $this->left->isSlotEmpty($index) : + $this->right->isSlotEmpty($index - $leftSize); + } + protected function getOpenSound() : Sound{ return new ChestOpenSound(); } protected function getCloseSound() : Sound{ return new ChestCloseSound(); } diff --git a/src/command/defaults/StatusCommand.php b/src/command/defaults/StatusCommand.php index 7c2884993..1d7545f0b 100644 --- a/src/command/defaults/StatusCommand.php +++ b/src/command/defaults/StatusCommand.php @@ -110,7 +110,8 @@ class StatusCommand extends VanillaCommand{ $worldName = $world->getFolderName() !== $world->getDisplayName() ? " (" . $world->getDisplayName() . ")" : ""; $timeColor = $world->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW; $sender->sendMessage(TextFormat::GOLD . "World \"{$world->getFolderName()}\"$worldName: " . - TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " chunks, " . + TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " loaded chunks, " . + TextFormat::RED . number_format(count($world->getTickingChunks())) . TextFormat::GREEN . " ticking chunks, " . TextFormat::RED . number_format(count($world->getEntities())) . TextFormat::GREEN . " entities. " . "Time $timeColor" . round($world->getTickRateTime(), 2) . "ms" ); diff --git a/src/entity/Human.php b/src/entity/Human.php index cdbd70bd7..707c816f8 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -92,7 +92,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ private const TAG_XP_PROGRESS = "XpP"; //TAG_Float private const TAG_LIFETIME_XP_TOTAL = "XpTotal"; //TAG_Int private const TAG_XP_SEED = "XpSeed"; //TAG_Int - private const TAG_NAME_TAG = "NameTag"; //TAG_String private const TAG_SKIN = "Skin"; //TAG_Compound private const TAG_SKIN_NAME = "Name"; //TAG_String private const TAG_SKIN_DATA = "Data"; //TAG_ByteArray @@ -245,10 +244,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. */ protected function initHumanData(CompoundTag $nbt) : void{ - if(($nameTagTag = $nbt->getTag(self::TAG_NAME_TAG)) instanceof StringTag){ - $this->setNameTag($nameTagTag->getValue()); - } - //TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace $this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag()); } diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 9968c283e..308850ffd 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -108,13 +108,23 @@ abstract class BaseInventory implements Inventory{ $this->onContentChange($oldContents); } + /** + * Helper for utility functions which search the inventory. + * TODO: make this abstract instead of providing a slow default implementation (BC break) + */ + protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{ + $item = $this->getItem($slot); + return $item->equals($test, $checkDamage, $checkTags) ? $item->getCount() : 0; + } + public function contains(Item $item) : bool{ $count = max(1, $item->getCount()); $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasNamedTag(); - foreach($this->getContents() as $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $count -= $i->getCount(); + for($i = 0, $size = $this->getSize(); $i < $size; $i++){ + $slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags); + if($slotCount > 0){ + $count -= $slotCount; if($count <= 0){ return true; } @@ -128,9 +138,9 @@ abstract class BaseInventory implements Inventory{ $slots = []; $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasNamedTag(); - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $slots[$index] = $i; + for($i = 0, $size = $this->getSize(); $i < $size; $i++){ + if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){ + $slots[$i] = $this->getItem($i); } } @@ -142,18 +152,9 @@ abstract class BaseInventory implements Inventory{ $checkDamage = $exact || !$item->hasAnyDamageValue(); $checkTags = $exact || $item->hasNamedTag(); - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){ - return $index; - } - } - - return -1; - } - - public function firstEmpty() : int{ - foreach($this->getContents(true) as $i => $slot){ - if($slot->isNull()){ + for($i = 0, $size = $this->getSize(); $i < $size; $i++){ + $slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags); + if($slotCount > 0 && ($slotCount === $count || (!$exact && $slotCount > $count))){ return $i; } } @@ -161,6 +162,20 @@ abstract class BaseInventory implements Inventory{ return -1; } + public function firstEmpty() : int{ + for($i = 0, $size = $this->getSize(); $i < $size; $i++){ + if($this->isSlotEmpty($i)){ + return $i; + } + } + + return -1; + } + + /** + * TODO: make this abstract and force implementations to implement it properly (BC break) + * This default implementation works, but is slow. + */ public function isSlotEmpty(int $index) : bool{ return $this->getItem($index)->isNull(); } @@ -171,14 +186,16 @@ abstract class BaseInventory implements Inventory{ public function getAddableItemQuantity(Item $item) : int{ $count = $item->getCount(); + $maxStackSize = min($this->getMaxStackSize(), $item->getMaxStackSize()); + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $slot = $this->getItem($i); - if($item->canStackWith($slot)){ - if(($diff = min($slot->getMaxStackSize(), $item->getMaxStackSize()) - $slot->getCount()) > 0){ + if($this->isSlotEmpty($i)){ + $count -= $maxStackSize; + }else{ + $slotCount = $this->getMatchingItemCount($i, $item, true, true); + if($slotCount > 0 && ($diff = $maxStackSize - $slotCount) > 0){ $count -= $diff; } - }elseif($slot->isNull()){ - $count -= min($this->getMaxStackSize(), $item->getMaxStackSize()); } if($count <= 0){ @@ -212,23 +229,29 @@ abstract class BaseInventory implements Inventory{ return $returnSlots; } - private function internalAddItem(Item $slot) : Item{ + private function internalAddItem(Item $newItem) : Item{ $emptySlots = []; + $maxStackSize = min($this->getMaxStackSize(), $newItem->getMaxStackSize()); + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $item = $this->getItem($i); - if($item->isNull()){ + if($this->isSlotEmpty($i)){ $emptySlots[] = $i; continue; } + $slotCount = $this->getMatchingItemCount($i, $newItem, true, true); + if($slotCount === 0){ + continue; + } - if($slot->canStackWith($item) && $item->getCount() < $item->getMaxStackSize()){ - $amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize()); + if($slotCount < $maxStackSize){ + $amount = min($maxStackSize - $slotCount, $newItem->getCount()); if($amount > 0){ - $slot->setCount($slot->getCount() - $amount); - $item->setCount($item->getCount() + $amount); - $this->setItem($i, $item); - if($slot->getCount() <= 0){ + $newItem->setCount($newItem->getCount() - $amount); + $slotItem = $this->getItem($i); + $slotItem->setCount($slotItem->getCount() + $amount); + $this->setItem($i, $slotItem); + if($newItem->getCount() <= 0){ break; } } @@ -237,65 +260,67 @@ abstract class BaseInventory implements Inventory{ if(count($emptySlots) > 0){ foreach($emptySlots as $slotIndex){ - $amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize()); - $slot->setCount($slot->getCount() - $amount); - $item = clone $slot; - $item->setCount($amount); - $this->setItem($slotIndex, $item); - if($slot->getCount() <= 0){ + $amount = min($maxStackSize, $newItem->getCount()); + $newItem->setCount($newItem->getCount() - $amount); + $slotItem = clone $newItem; + $slotItem->setCount($amount); + $this->setItem($slotIndex, $slotItem); + if($newItem->getCount() <= 0){ break; } } } - return $slot; + return $newItem; } public function remove(Item $item) : void{ $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasNamedTag(); - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $this->clear($index); + for($i = 0, $size = $this->getSize(); $i < $size; $i++){ + if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){ + $this->clear($i); } } } public function removeItem(Item ...$slots) : array{ - /** @var Item[] $itemSlots */ + /** @var Item[] $searchItems */ /** @var Item[] $slots */ - $itemSlots = []; + $searchItems = []; foreach($slots as $slot){ if(!$slot->isNull()){ - $itemSlots[] = clone $slot; + $searchItems[] = clone $slot; } } for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $item = $this->getItem($i); - if($item->isNull()){ + if($this->isSlotEmpty($i)){ continue; } - foreach($itemSlots as $index => $slot){ - if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasNamedTag())){ - $amount = min($item->getCount(), $slot->getCount()); - $slot->setCount($slot->getCount() - $amount); - $item->setCount($item->getCount() - $amount); - $this->setItem($i, $item); - if($slot->getCount() <= 0){ - unset($itemSlots[$index]); + foreach($searchItems as $index => $search){ + $slotCount = $this->getMatchingItemCount($i, $search, !$search->hasAnyDamageValue(), $search->hasNamedTag()); + if($slotCount > 0){ + $amount = min($slotCount, $search->getCount()); + $search->setCount($search->getCount() - $amount); + + $slotItem = $this->getItem($i); + $slotItem->setCount($slotItem->getCount() - $amount); + $this->setItem($i, $slotItem); + if($search->getCount() <= 0){ + unset($searchItems[$index]); } } } - if(count($itemSlots) === 0){ + if(count($searchItems) === 0){ break; } } - return $itemSlots; + return $searchItems; } public function clear(int $index) : void{ diff --git a/src/inventory/DelegateInventory.php b/src/inventory/DelegateInventory.php index ba9e5a983..a211732cf 100644 --- a/src/inventory/DelegateInventory.php +++ b/src/inventory/DelegateInventory.php @@ -85,6 +85,10 @@ class DelegateInventory extends BaseInventory{ $this->backingInventory->setContents($items); } + public function isSlotEmpty(int $index) : bool{ + return $this->backingInventory->isSlotEmpty($index); + } + protected function onSlotChange(int $index, Item $before) : void{ if($this->backingInventoryChanging){ parent::onSlotChange($index, $before); diff --git a/src/inventory/SimpleInventory.php b/src/inventory/SimpleInventory.php index aae11c84c..c1f352b7b 100644 --- a/src/inventory/SimpleInventory.php +++ b/src/inventory/SimpleInventory.php @@ -83,4 +83,13 @@ class SimpleInventory extends BaseInventory{ } } } + + protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{ + $slotItem = $this->slots[$slot]; + return $slotItem !== null && $slotItem->equals($test, $checkDamage, $checkTags) ? $slotItem->getCount() : 0; + } + + public function isSlotEmpty(int $index) : bool{ + return $this->slots[$index] === null || $this->slots[$index]->isNull(); + } } diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 92a992ffb..d87c1b9a8 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -107,7 +107,6 @@ use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryStream; use pocketmine\utils\ObjectSet; use pocketmine\utils\TextFormat; -use pocketmine\utils\Utils; use pocketmine\world\Position; use function array_map; use function array_values; @@ -1000,8 +999,6 @@ class NetworkSession{ * @phpstan-param \Closure() : void $onCompletion */ public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{ - Utils::validateCallableSignature(function() : void{}, $onCompletion); - $world = $this->player->getLocation()->getWorld(); ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve( diff --git a/src/network/mcpe/compression/CompressBatchPromise.php b/src/network/mcpe/compression/CompressBatchPromise.php index 3b8c9680b..12ac35c60 100644 --- a/src/network/mcpe/compression/CompressBatchPromise.php +++ b/src/network/mcpe/compression/CompressBatchPromise.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\compression; -use pocketmine\utils\Utils; use function array_push; class CompressBatchPromise{ @@ -42,9 +41,6 @@ class CompressBatchPromise{ */ public function onResolve(\Closure ...$callbacks) : void{ $this->checkCancelled(); - foreach($callbacks as $callback){ - Utils::validateCallableSignature(function(CompressBatchPromise $promise) : void{}, $callback); - } if($this->result !== null){ foreach($callbacks as $callback){ $callback($this); diff --git a/src/network/mcpe/convert/RuntimeBlockMapping.php b/src/network/mcpe/convert/RuntimeBlockMapping.php index c2123364b..1ad2c5ff7 100644 --- a/src/network/mcpe/convert/RuntimeBlockMapping.php +++ b/src/network/mcpe/convert/RuntimeBlockMapping.php @@ -48,8 +48,8 @@ final class RuntimeBlockMapping{ private array $legacyToRuntimeMap = []; /** @var int[] */ private array $runtimeToLegacyMap = []; - /** @var CompoundTag[] */ - private array $bedrockKnownStates; + /** @var CompoundTag[]|null */ + private ?array $bedrockKnownStates = null; private static function make() : self{ return new self( @@ -85,25 +85,13 @@ final class RuntimeBlockMapping{ return $newTag; } - public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){ - $stream = new BinaryStream(Filesystem::fileGetContents($canonicalBlockStatesFile)); - $list = []; - $nbtReader = new NetworkNbtSerializer(); + public function __construct( + private string $canonicalBlockStatesFile, + string $r12ToCurrentBlockMapFile + ){ + //do not cache this - we only need it to set up mappings under normal circumstances + $bedrockKnownStates = $this->loadBedrockKnownStates(); - $keyIndex = []; - $valueIndex = []; - while(!$stream->feof()){ - $offset = $stream->getOffset(); - $blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag(); - $stream->setOffset($offset); - $list[] = self::deduplicateCompound($blockState, $keyIndex, $valueIndex); - } - $this->bedrockKnownStates = $list; - - $this->setupLegacyMappings($r12ToCurrentBlockMapFile); - } - - private function setupLegacyMappings(string $r12ToCurrentBlockMapFile) : void{ $legacyIdMap = LegacyBlockIdToStringIdMap::getInstance(); /** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */ $legacyStateMap = []; @@ -123,7 +111,7 @@ final class RuntimeBlockMapping{ * @var int[][] $idToStatesMap string id -> int[] list of candidate state indices */ $idToStatesMap = []; - foreach($this->bedrockKnownStates as $k => $state){ + foreach($bedrockKnownStates as $k => $state){ $idToStatesMap[$state->getString("name")][] = $k; } foreach($legacyStateMap as $pair){ @@ -142,7 +130,7 @@ final class RuntimeBlockMapping{ throw new \RuntimeException("Mapped new state does not appear in network table"); } foreach($idToStatesMap[$mappedName] as $k){ - $networkState = $this->bedrockKnownStates[$k]; + $networkState = $bedrockKnownStates[$k]; if($mappedState->equals($networkState)){ $this->registerMapping($k, $id, $data); continue 2; @@ -152,6 +140,25 @@ final class RuntimeBlockMapping{ } } + /** + * @return CompoundTag[] + */ + private function loadBedrockKnownStates() : array{ + $stream = new BinaryStream(Filesystem::fileGetContents($this->canonicalBlockStatesFile)); + $list = []; + $nbtReader = new NetworkNbtSerializer(); + + $keyIndex = []; + $valueIndex = []; + while(!$stream->feof()){ + $offset = $stream->getOffset(); + $blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag(); + $stream->setOffset($offset); + $list[] = self::deduplicateCompound($blockState, $keyIndex, $valueIndex); + } + return $list; + } + public function toRuntimeId(int $internalStateId) : int{ return $this->legacyToRuntimeMap[$internalStateId] ?? $this->legacyToRuntimeMap[BlockLegacyIds::INFO_UPDATE << Block::INTERNAL_METADATA_BITS]; } @@ -166,9 +173,14 @@ final class RuntimeBlockMapping{ } /** + * WARNING: This method may load the palette from disk, which is a slow operation. + * Afterwards, it will cache the palette in memory, which requires (in some cases) tens of MB of memory. + * Avoid using this where possible. + * + * @deprecated * @return CompoundTag[] */ public function getBedrockKnownStates() : array{ - return $this->bedrockKnownStates; + return $this->bedrockKnownStates ??= $this->loadBedrockKnownStates(); } } diff --git a/src/player/Player.php b/src/player/Player.php index 279dd1ebf..e8dae37f4 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -1191,7 +1191,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ * TODO: make this a dynamic ability instead of being hardcoded */ public function hasFiniteResources() : bool{ - return $this->gamemode->equals(GameMode::SURVIVAL()) || $this->gamemode->equals(GameMode::ADVENTURE()); + return !$this->gamemode->equals(GameMode::CREATIVE()); } public function isFireProof() : bool{ @@ -1657,7 +1657,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $ev = new PlayerBlockPickEvent($this, $block, $item); $existingSlot = $this->inventory->first($item); - if($existingSlot === -1 && ($this->hasFiniteResources() || $this->isSpectator())){ + if($existingSlot === -1 && $this->hasFiniteResources()){ $ev->cancel(); } $ev->call(); diff --git a/src/scheduler/AsyncPool.php b/src/scheduler/AsyncPool.php index 9029eb593..99175651d 100644 --- a/src/scheduler/AsyncPool.php +++ b/src/scheduler/AsyncPool.php @@ -247,7 +247,6 @@ class AsyncPool{ while(!$queue->isEmpty()){ /** @var AsyncTask $task */ $task = $queue->bottom(); - $task->checkProgressUpdates(); if($task->isFinished()){ //make sure the task actually executed before trying to collect $queue->dequeue(); @@ -268,6 +267,7 @@ class AsyncPool{ $task->onCompletion(); } }else{ + $task->checkProgressUpdates(); $more = true; break; //current task is still running, skip to next worker } diff --git a/src/timings/Timings.php b/src/timings/Timings.php index 74fa40dde..70a2a03cf 100644 --- a/src/timings/Timings.php +++ b/src/timings/Timings.php @@ -87,14 +87,6 @@ abstract class Timings{ /** @var TimingsHandler */ public static $serverCommand; /** @var TimingsHandler */ - public static $worldLoad; - /** @var TimingsHandler */ - public static $worldSave; - /** @var TimingsHandler */ - public static $population; - /** @var TimingsHandler */ - public static $generationCallback; - /** @var TimingsHandler */ public static $permissibleCalculation; /** @var TimingsHandler */ public static $permissibleCalculationDiff; @@ -111,10 +103,6 @@ abstract class Timings{ /** @var TimingsHandler */ public static $playerCheckNearEntities; - /** @var TimingsHandler */ - public static $tickEntity; - /** @var TimingsHandler */ - public static $tickTileEntity; /** @var TimingsHandler */ public static $entityBaseTick; @@ -203,10 +191,6 @@ abstract class Timings{ self::$playerChunkSend = new TimingsHandler("Player Network Send - Chunks", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN); self::$scheduler = new TimingsHandler("Scheduler"); self::$serverCommand = new TimingsHandler("Server Command"); - self::$worldLoad = new TimingsHandler("World Load"); - self::$worldSave = new TimingsHandler("World Save"); - self::$population = new TimingsHandler("World Population"); - self::$generationCallback = new TimingsHandler("World Generation Callback"); self::$permissibleCalculation = new TimingsHandler("Permissible Calculation"); self::$permissibleCalculationDiff = new TimingsHandler("Permissible Calculation - Diff", self::$permissibleCalculation, group: self::GROUP_BREAKDOWN); self::$permissibleCalculationCallback = new TimingsHandler("Permissible Calculation - Callbacks", self::$permissibleCalculation, group: self::GROUP_BREAKDOWN); @@ -221,9 +205,6 @@ abstract class Timings{ self::$projectileMoveRayTrace = new TimingsHandler("Projectile Movement - Ray Tracing", self::$projectileMove, group: self::GROUP_BREAKDOWN); self::$playerCheckNearEntities = new TimingsHandler("checkNearEntities", group: self::GROUP_BREAKDOWN); - self::$tickEntity = new TimingsHandler("Entity Tick", group: self::GROUP_BREAKDOWN); - self::$tickTileEntity = new TimingsHandler("Block Entity Tick", group: self::GROUP_BREAKDOWN); - self::$entityBaseTick = new TimingsHandler("Entity Base Tick", group: self::GROUP_BREAKDOWN); self::$livingEntityBaseTick = new TimingsHandler("Entity Base Tick - Living", group: self::GROUP_BREAKDOWN); self::$itemEntityBaseTick = new TimingsHandler("Entity Base Tick - ItemEntity", group: self::GROUP_BREAKDOWN); @@ -272,7 +253,7 @@ abstract class Timings{ }else{ $displayName = self::shortenCoreClassName($entity::class, "pocketmine\\entity\\"); } - self::$entityTypeTimingMap[$entity::class] = new TimingsHandler("Entity Tick - " . $displayName, self::$tickEntity, group: self::GROUP_BREAKDOWN); + self::$entityTypeTimingMap[$entity::class] = new TimingsHandler("Entity Tick - " . $displayName, group: self::GROUP_BREAKDOWN); } return self::$entityTypeTimingMap[$entity::class]; @@ -282,7 +263,6 @@ abstract class Timings{ if(!isset(self::$tileEntityTypeTimingMap[$tile::class])){ self::$tileEntityTypeTimingMap[$tile::class] = new TimingsHandler( "Block Entity Tick - " . self::shortenCoreClassName($tile::class, "pocketmine\\block\\tile\\"), - self::$tickTileEntity, group: self::GROUP_BREAKDOWN ); } diff --git a/src/timings/TimingsHandler.php b/src/timings/TimingsHandler.php index 45ce022c9..ba6c3cfea 100644 --- a/src/timings/TimingsHandler.php +++ b/src/timings/TimingsHandler.php @@ -23,10 +23,8 @@ declare(strict_types=1); namespace pocketmine\timings; -use pocketmine\entity\Living; use pocketmine\Server; use pocketmine\utils\Utils; -use function count; use function hrtime; use function implode; use function spl_object_id; @@ -77,19 +75,6 @@ class TimingsHandler{ $result[] = "# Version " . Server::getInstance()->getVersion(); $result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion(); - $entities = 0; - $livingEntities = 0; - foreach(Server::getInstance()->getWorldManager()->getWorlds() as $world){ - $entities += count($world->getEntities()); - foreach($world->getEntities() as $e){ - if($e instanceof Living){ - ++$livingEntities; - } - } - } - - $result[] = "# Entities " . $entities; - $result[] = "# LivingEntities " . $livingEntities; $result[] = "# FormatVersion " . self::FORMAT_VERSION; $sampleTime = hrtime(true) - self::$timingStart; diff --git a/src/world/TickingChunkEntry.php b/src/world/TickingChunkEntry.php deleted file mode 100644 index ca965463d..000000000 --- a/src/world/TickingChunkEntry.php +++ /dev/null @@ -1,37 +0,0 @@ - ChunkTicker - * @phpstan-var array - */ - public array $tickers = []; - - public bool $ready = false; -} diff --git a/src/world/World.php b/src/world/World.php index 607da30cb..ef62cb987 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -77,7 +77,6 @@ use pocketmine\promise\PromiseResolver; use pocketmine\scheduler\AsyncPool; use pocketmine\Server; use pocketmine\ServerConfigGroup; -use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Limits; use pocketmine\utils\ReversePriorityQueue; @@ -104,6 +103,7 @@ use pocketmine\world\utils\SubChunkExplorer; use function abs; use function array_filter; use function array_key_exists; +use function array_keys; use function array_map; use function array_merge; use function array_sum; @@ -223,10 +223,25 @@ class World implements ChunkManager{ private array $tickingLoaderCounter = []; /** - * @var TickingChunkEntry[] chunkHash => TickingChunkEntry - * @phpstan-var array + * @var ChunkTicker[][] chunkHash => [spl_object_id => ChunkTicker] + * @phpstan-var array> */ - private array $tickingChunks = []; + private array $registeredTickingChunks = []; + + /** + * Set of chunks which are definitely ready for ticking. + * + * @var int[] + * @phpstan-var array + */ + private array $validTickingChunks = []; + + /** + * Set of chunks which might be ready for ticking. These will be checked at the next tick. + * @var int[] + * @phpstan-var array + */ + private array $recheckTickingChunks = []; /** * @var ChunkLoader[][] chunkHash => [spl_object_id => ChunkLoader] @@ -983,7 +998,6 @@ class World implements ChunkManager{ $this->timings->entityTick->startTiming(); //Update entities that need update - Timings::$tickEntity->startTiming(); foreach($this->updateEntities as $id => $entity){ if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){ unset($this->updateEntities[$id]); @@ -992,7 +1006,6 @@ class World implements ChunkManager{ $entity->close(); } } - Timings::$tickEntity->stopTiming(); $this->timings->entityTick->stopTiming(); $this->timings->randomChunkUpdates->startTiming(); @@ -1155,17 +1168,25 @@ class World implements ChunkManager{ $this->chunkTickRadius = $radius; } + /** + * Returns a list of chunk position hashes (as returned by World::chunkHash()) which are currently valid for + * ticking. + * + * @return int[] + * @phpstan-return list + */ + public function getTickingChunks() : array{ + return array_keys($this->validTickingChunks); + } + /** * Instructs the World to tick the specified chunk, for as long as this chunk ticker (or any other chunk ticker) is * registered to it. */ public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{ $chunkPosHash = World::chunkHash($chunkX, $chunkZ); - $entry = $this->tickingChunks[$chunkPosHash] ?? null; - if($entry === null){ - $entry = $this->tickingChunks[$chunkPosHash] = new TickingChunkEntry(); - } - $entry->tickers[spl_object_id($ticker)] = $ticker; + $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker; + $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash; } /** @@ -1175,10 +1196,14 @@ class World implements ChunkManager{ public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $tickerId = spl_object_id($ticker); - if(isset($this->tickingChunks[$chunkHash]->tickers[$tickerId])){ - unset($this->tickingChunks[$chunkHash]->tickers[$tickerId]); - if(count($this->tickingChunks[$chunkHash]->tickers) === 0){ - unset($this->tickingChunks[$chunkHash]); + if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){ + unset($this->registeredTickingChunks[$chunkHash][$tickerId]); + if(count($this->registeredTickingChunks[$chunkHash]) === 0){ + unset( + $this->registeredTickingChunks[$chunkHash], + $this->recheckTickingChunks[$chunkHash], + $this->validTickingChunks[$chunkHash] + ); } } } @@ -1222,37 +1247,37 @@ class World implements ChunkManager{ } private function tickChunks() : void{ - if($this->chunkTickRadius <= 0 || (count($this->tickingChunks) === 0 && count($this->tickingLoaders) === 0)){ + if($this->chunkTickRadius <= 0 || (count($this->registeredTickingChunks) === 0 && count($this->tickingLoaders) === 0)){ return; } - $this->timings->randomChunkUpdatesChunkSelection->startTiming(); + if(count($this->recheckTickingChunks) > 0 || count($this->tickingLoaders) > 0){ + $this->timings->randomChunkUpdatesChunkSelection->startTiming(); - /** @var bool[] $chunkTickList chunkhash => dummy */ - $chunkTickList = []; + $chunkTickableCache = []; - $chunkTickableCache = []; - - foreach($this->tickingChunks as $hash => $entry){ - if(!$entry->ready){ + foreach($this->recheckTickingChunks as $hash => $_){ World::getXZ($hash, $chunkX, $chunkZ); if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){ - $entry->ready = true; - }else{ - //the chunk has been flagged as temporarily not tickable, so we don't want to tick it this time - continue; + $this->validTickingChunks[$hash] = $hash; } } - $chunkTickList[$hash] = true; - } + $this->recheckTickingChunks = []; - //TODO: REMOVE THIS - //backwards compatibility for TickingChunkLoader, although I'm not sure this is really necessary in practice - if(count($this->tickingLoaders) !== 0){ - $this->selectTickableChunksLegacy($chunkTickList, $chunkTickableCache); - } + //TODO: REMOVE THIS - we need a local var to add extra chunks to if we have legacy ticking loaders + //this is copy-on-write, so it won't have any performance impact if there are no legacy ticking loaders + $chunkTickList = $this->validTickingChunks; - $this->timings->randomChunkUpdatesChunkSelection->stopTiming(); + //TODO: REMOVE THIS + //backwards compatibility for TickingChunkLoader, although I'm not sure this is really necessary in practice + if(count($this->tickingLoaders) !== 0){ + $this->selectTickableChunksLegacy($chunkTickList, $chunkTickableCache); + } + + $this->timings->randomChunkUpdatesChunkSelection->stopTiming(); + }else{ + $chunkTickList = $this->validTickingChunks; + } foreach($chunkTickList as $index => $_){ World::getXZ($index, $chunkX, $chunkZ); @@ -1303,16 +1328,23 @@ class World implements ChunkManager{ } /** - * Marks the 3x3 chunks around the specified chunk as not ready to be ticked. This is used to prevent chunk ticking - * while a chunk is being populated, light-populated, or unloaded. - * Each chunk will be rechecked every tick until it is ready to be ticked again. + * Marks the 3x3 square of chunks centered on the specified chunk for chunk ticking eligibility recheck. + * + * This should be used whenever the chunk's eligibility to be ticked is changed. This includes: + * - Loading/unloading the chunk (the chunk may be registered for ticking before it is loaded) + * - Locking/unlocking the chunk (e.g. world population) + * - Light populated state change (i.e. scheduled for light population, or light population completed) + * - Arbitrary chunk replacement (i.e. setChunk() or similar) */ - private function markTickingChunkUnavailable(int $chunkX, int $chunkZ) : void{ + private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{ for($cx = -1; $cx <= 1; ++$cx){ for($cz = -1; $cz <= 1; ++$cz){ $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz); - if(isset($this->tickingChunks[$chunkHash])){ - $this->tickingChunks[$chunkHash]->ready = false; + unset($this->validTickingChunks[$chunkHash]); + if(isset($this->registeredTickingChunks[$chunkHash])){ + $this->recheckTickingChunks[$chunkHash] = $chunkHash; + }else{ + unset($this->recheckTickingChunks[$chunkHash]); } } } @@ -1323,7 +1355,7 @@ class World implements ChunkManager{ $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated(); if($lightPopulatedState === false){ $this->chunks[$chunkHash]->setLightPopulated(null); - $this->markTickingChunkUnavailable($chunkX, $chunkZ); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); $this->workerPool->submitTask(new LightPopulationTask( $this->chunks[$chunkHash], @@ -1347,6 +1379,7 @@ class World implements ChunkManager{ $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray); } $chunk->setLightPopulated(true); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); } )); } @@ -1403,10 +1436,15 @@ class World implements ChunkManager{ (new WorldSaveEvent($this))->call(); + $timings = $this->timings->syncDataSave; + $timings->startTiming(); + $this->provider->getWorldData()->setTime($this->time); $this->saveChunks(); $this->provider->getWorldData()->save(); + $timings->stopTiming(); + return true; } @@ -2391,7 +2429,7 @@ class World implements ChunkManager{ throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked"); } $this->chunkLock[$chunkHash] = $lockId; - $this->markTickingChunkUnavailable($chunkX, $chunkZ); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); } /** @@ -2406,6 +2444,7 @@ class World implements ChunkManager{ $chunkHash = World::chunkHash($chunkX, $chunkZ); if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){ unset($this->chunkLock[$chunkHash]); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); return true; } return false; @@ -2457,7 +2496,7 @@ class World implements ChunkManager{ unset($this->blockCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); $chunk->setTerrainDirty(); - $this->markTickingChunkUnavailable($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking + $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking if(!$this->isChunkInUse($chunkX, $chunkZ)){ $this->unloadChunkRequest($chunkX, $chunkZ); @@ -2739,6 +2778,7 @@ class World implements ChunkManager{ foreach($this->getChunkListeners($x, $z) as $listener){ $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]); } + $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded $this->timings->syncChunkLoad->stopTiming(); @@ -2900,8 +2940,8 @@ class World implements ChunkManager{ unset($this->chunks[$chunkHash]); unset($this->blockCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); - unset($this->tickingChunks[$chunkHash]); - $this->markTickingChunkUnavailable($x, $z); + unset($this->registeredTickingChunks[$chunkHash]); + $this->markTickingChunkForRecheck($x, $z); if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){ $this->logger->debug("Rejecting population promise for chunk $x $z"); @@ -3210,7 +3250,8 @@ class World implements ChunkManager{ private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{ $chunkHash = World::chunkHash($chunkX, $chunkZ); - Timings::$population->startTiming(); + $timings = $this->timings->chunkPopulationOrder; + $timings->startTiming(); try{ for($xx = -1; $xx <= 1; ++$xx){ @@ -3267,7 +3308,7 @@ class World implements ChunkManager{ return $resolver->getPromise(); }finally{ - Timings::$population->stopTiming(); + $timings->stopTiming(); } } @@ -3276,7 +3317,8 @@ class World implements ChunkManager{ * @phpstan-param array $adjacentChunks */ private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{ - Timings::$generationCallback->startTiming(); + $timings = $this->timings->chunkPopulationCompletion; + $timings->startTiming(); $dirtyChunks = 0; for($xx = -1; $xx <= 1; ++$xx){ @@ -3345,7 +3387,7 @@ class World implements ChunkManager{ $this->drainPopulationRequestQueue(); } - Timings::$generationCallback->stopTiming(); + $timings->stopTiming(); } public function doChunkGarbageCollection() : void{ diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php index 1124d513d..f056608d7 100644 --- a/src/world/WorldManager.php +++ b/src/world/WorldManager.php @@ -30,7 +30,6 @@ use pocketmine\event\world\WorldUnloadEvent; use pocketmine\lang\KnownTranslationFactory; use pocketmine\player\ChunkSelector; use pocketmine\Server; -use pocketmine\timings\Timings; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\exception\CorruptedWorldException; use pocketmine\world\format\io\exception\UnsupportedWorldFormatException; @@ -391,7 +390,6 @@ class WorldManager{ } private function doAutoSave() : void{ - Timings::$worldSave->startTiming(); foreach($this->worlds as $world){ foreach($world->getPlayers() as $player){ if($player->spawned){ @@ -400,6 +398,5 @@ class WorldManager{ } $world->save(false); } - Timings::$worldSave->stopTiming(); } } diff --git a/src/world/WorldTimings.php b/src/world/WorldTimings.php index 5a51f920b..5c1a56011 100644 --- a/src/world/WorldTimings.php +++ b/src/world/WorldTimings.php @@ -38,6 +38,7 @@ class WorldTimings{ public TimingsHandler $randomChunkUpdatesChunkSelection; public TimingsHandler $doChunkGC; public TimingsHandler $entityTick; + public TimingsHandler $tileTick; public TimingsHandler $doTick; public TimingsHandler $syncChunkSend; @@ -48,33 +49,54 @@ class WorldTimings{ public TimingsHandler $syncChunkLoadFixInvalidBlocks; public TimingsHandler $syncChunkLoadEntities; public TimingsHandler $syncChunkLoadTileEntities; + + public TimingsHandler $syncDataSave; public TimingsHandler $syncChunkSave; + public TimingsHandler $chunkPopulationOrder; + public TimingsHandler $chunkPopulationCompletion; + + /** + * @var TimingsHandler[] + * @phpstan-var array + */ + private static array $aggregators = []; + + private static function newTimer(string $worldName, string $timerName) : TimingsHandler{ + $aggregator = self::$aggregators[$timerName] ??= new TimingsHandler("Worlds - $timerName"); //displayed in Minecraft primary table + + return new TimingsHandler("$worldName - $timerName", $aggregator, Timings::GROUP_BREAKDOWN); + } + public function __construct(World $world){ - $name = $world->getFolderName() . " - "; + $name = $world->getFolderName(); - $this->setBlock = new TimingsHandler($name . "setBlock", group: Timings::GROUP_BREAKDOWN); - $this->doBlockLightUpdates = new TimingsHandler($name . "Block Light Updates", group: Timings::GROUP_BREAKDOWN); - $this->doBlockSkyLightUpdates = new TimingsHandler($name . "Sky Light Updates", group: Timings::GROUP_BREAKDOWN); + $this->setBlock = self::newTimer($name, "Set Blocks"); + $this->doBlockLightUpdates = self::newTimer($name, "Block Light Updates"); + $this->doBlockSkyLightUpdates = self::newTimer($name, "Sky Light Updates"); - $this->doChunkUnload = new TimingsHandler($name . "Unload Chunks", group: Timings::GROUP_BREAKDOWN); - $this->scheduledBlockUpdates = new TimingsHandler($name . "Scheduled Block Updates", group: Timings::GROUP_BREAKDOWN); - $this->randomChunkUpdates = new TimingsHandler($name . "Random Chunk Updates", group: Timings::GROUP_BREAKDOWN); - $this->randomChunkUpdatesChunkSelection = new TimingsHandler($name . "Random Chunk Updates - Chunk Selection", group: Timings::GROUP_BREAKDOWN); - $this->doChunkGC = new TimingsHandler($name . "Garbage Collection", group: Timings::GROUP_BREAKDOWN); - $this->entityTick = new TimingsHandler($name . "Tick Entities", group: Timings::GROUP_BREAKDOWN); + $this->doChunkUnload = self::newTimer($name, "Unload Chunks"); + $this->scheduledBlockUpdates = self::newTimer($name, "Scheduled Block Updates"); + $this->randomChunkUpdates = self::newTimer($name, "Random Chunk Updates"); + $this->randomChunkUpdatesChunkSelection = self::newTimer($name, "Random Chunk Updates - Chunk Selection"); + $this->doChunkGC = self::newTimer($name, "Garbage Collection"); + $this->entityTick = self::newTimer($name, "Entity Tick"); + $this->tileTick = self::newTimer($name, "Block Entity Tick"); + $this->doTick = self::newTimer($name, "World Tick"); - Timings::init(); //make sure the timers we want are available - $this->syncChunkSend = new TimingsHandler($name . "Player Send Chunks", Timings::$playerChunkSend, group: Timings::GROUP_BREAKDOWN); - $this->syncChunkSendPrepare = new TimingsHandler($name . "Player Send Chunk Prepare", Timings::$playerChunkSend, group: Timings::GROUP_BREAKDOWN); + $this->syncChunkSend = self::newTimer($name, "Player Send Chunks"); + $this->syncChunkSendPrepare = self::newTimer($name, "Player Send Chunk Prepare"); - $this->syncChunkLoad = new TimingsHandler($name . "Chunk Load", Timings::$worldLoad, group: Timings::GROUP_BREAKDOWN); - $this->syncChunkLoadData = new TimingsHandler($name . "Chunk Load - Data", group: Timings::GROUP_BREAKDOWN); - $this->syncChunkLoadFixInvalidBlocks = new TimingsHandler($name . "Chunk Load - Fix Invalid Blocks", group: Timings::GROUP_BREAKDOWN); - $this->syncChunkLoadEntities = new TimingsHandler($name . "Chunk Load - Entities", group: Timings::GROUP_BREAKDOWN); - $this->syncChunkLoadTileEntities = new TimingsHandler($name . "Chunk Load - TileEntities", group: Timings::GROUP_BREAKDOWN); - $this->syncChunkSave = new TimingsHandler($name . "Chunk Save", Timings::$worldSave, group: Timings::GROUP_BREAKDOWN); + $this->syncChunkLoad = self::newTimer($name, "Chunk Load"); + $this->syncChunkLoadData = self::newTimer($name, "Chunk Load - Data"); + $this->syncChunkLoadFixInvalidBlocks = self::newTimer($name, "Chunk Load - Fix Invalid Blocks"); + $this->syncChunkLoadEntities = self::newTimer($name, "Chunk Load - Entities"); + $this->syncChunkLoadTileEntities = self::newTimer($name, "Chunk Load - Block Entities"); - $this->doTick = new TimingsHandler($name . "World Tick"); + $this->syncDataSave = self::newTimer($name, "Data Save"); + $this->syncChunkSave = self::newTimer($name, "Chunk Save"); + + $this->chunkPopulationOrder = self::newTimer($name, "Chunk Population - Order"); + $this->chunkPopulationCompletion = self::newTimer($name, "Chunk Population - Completion"); } }