From 6f3851be80b148d83f696f38f0fe16088877fda6 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Sun, 6 Apr 2025 04:52:15 +0100 Subject: [PATCH 01/43] 5.27.1 (#6670) --- changelogs/5.27.md | 6 ++++++ composer.json | 2 +- composer.lock | 18 +++++++++--------- src/VersionInfo.php | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/changelogs/5.27.md b/changelogs/5.27.md index f3923d46627..07bc8c26ec0 100644 --- a/changelogs/5.27.md +++ b/changelogs/5.27.md @@ -16,3 +16,9 @@ If you're upgrading from 5.25.x directly to 5.27.0, please also read the followi ## General - Aded support for Minecraft: Bedrock Edition 1.21.70. - Removed support for earlier versions. + +# 5.27.1 +Released 6th April 2025. + +## Fixes +- Updated RakLib to get ping timestamp handling fixes. diff --git a/composer.json b/composer.json index 8da46090b72..77e32e3dac1 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "pocketmine/log": "^0.4.0", "pocketmine/math": "~1.0.0", "pocketmine/nbt": "~1.1.0", - "pocketmine/raklib": "~1.1.0", + "pocketmine/raklib": "~1.1.2", "pocketmine/raklib-ipc": "~1.0.0", "pocketmine/snooze": "^0.5.0", "ramsey/uuid": "~4.7.0", diff --git a/composer.lock b/composer.lock index 405a9241426..16dcb5e3d44 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": "28b4de9a23a293646dbad2707cdfd9e0", + "content-hash": "8d3061c5cc77e5b1dfa1fcf77f5146c6", "packages": [ { "name": "adhocore/json-comment", @@ -618,16 +618,16 @@ }, { "name": "pocketmine/raklib", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/pmmp/RakLib.git", - "reference": "be2783be516bf6e2872ff5c81fb9048596617b97" + "reference": "4145a31cd812fe8931c3c9c691fcd2ded2f47e7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/RakLib/zipball/be2783be516bf6e2872ff5c81fb9048596617b97", - "reference": "be2783be516bf6e2872ff5c81fb9048596617b97", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/4145a31cd812fe8931c3c9c691fcd2ded2f47e7f", + "reference": "4145a31cd812fe8931c3c9c691fcd2ded2f47e7f", "shasum": "" }, "require": { @@ -639,8 +639,8 @@ "pocketmine/log": "^0.3.0 || ^0.4.0" }, "require-dev": { - "phpstan/phpstan": "1.10.1", - "phpstan/phpstan-strict-rules": "^1.0" + "phpstan/phpstan": "2.1.0", + "phpstan/phpstan-strict-rules": "^2.0" }, "type": "library", "autoload": { @@ -655,9 +655,9 @@ "description": "A RakNet server implementation written in PHP", "support": { "issues": "https://github.com/pmmp/RakLib/issues", - "source": "https://github.com/pmmp/RakLib/tree/1.1.1" + "source": "https://github.com/pmmp/RakLib/tree/1.1.2" }, - "time": "2024-03-04T14:02:14+00:00" + "time": "2025-04-06T03:38:21+00:00" }, { "name": "pocketmine/raklib-ipc", diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 0f776dc1d20..249a271766e 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -32,7 +32,7 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; public const BASE_VERSION = "5.27.1"; - public const IS_DEVELOPMENT_BUILD = true; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** From d3f6c22996de69c215b87d51d21514761462e979 Mon Sep 17 00:00:00 2001 From: "pmmp-admin-bot[bot]" <188621379+pmmp-admin-bot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 03:53:08 +0000 Subject: [PATCH 02/43] 5.27.2 is next Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14288755593 --- src/VersionInfo.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 249a271766e..44238dba3a6 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.27.1"; - public const IS_DEVELOPMENT_BUILD = false; + public const BASE_VERSION = "5.27.2"; + public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; /** From 835c383d4e126df6f38000e3217ad6a325b7a1f7 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 6 Apr 2025 19:59:02 +0100 Subject: [PATCH 03/43] Update Composer dependencies --- composer.json | 2 +- composer.lock | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 77e32e3dac1..87086f456d4 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "symfony/filesystem": "~6.4.0" }, "require-dev": { - "phpstan/phpstan": "2.1.8", + "phpstan/phpstan": "2.1.11", "phpstan/phpstan-phpunit": "^2.0.0", "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" diff --git a/composer.lock b/composer.lock index 16dcb5e3d44..23f31231797 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": "8d3061c5cc77e5b1dfa1fcf77f5146c6", + "content-hash": "818c679a25da8e6b466bc454ad48dec3", "packages": [ { "name": "adhocore/json-comment", @@ -471,16 +471,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.24.1", + "version": "2.24.2", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "8f48cbe1fb5835a8bb573bed00ef04c65c26c7e5" + "reference": "2a00c44c52bce98e7a43aa31517df78cbb2ba23b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/8f48cbe1fb5835a8bb573bed00ef04c65c26c7e5", - "reference": "8f48cbe1fb5835a8bb573bed00ef04c65c26c7e5", + "url": "https://api.github.com/repos/pmmp/Language/zipball/2a00c44c52bce98e7a43aa31517df78cbb2ba23b", + "reference": "2a00c44c52bce98e7a43aa31517df78cbb2ba23b", "shasum": "" }, "type": "library", @@ -488,9 +488,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.24.1" + "source": "https://github.com/pmmp/Language/tree/2.24.2" }, - "time": "2025-03-16T19:04:15+00:00" + "time": "2025-04-03T01:23:27+00:00" }, { "name": "pocketmine/log", @@ -742,16 +742,16 @@ }, { "name": "ramsey/collection", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -812,9 +812,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-02T04:48:29+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", @@ -1373,16 +1373,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.8", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -1427,20 +1427,20 @@ "type": "github" } ], - "time": "2025-03-09T09:30:48+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.4", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "d09e152f403c843998d7a52b5d87040c937525dd" + "reference": "6b92469f8a7995e626da3aa487099617b8dfa260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d09e152f403c843998d7a52b5d87040c937525dd", - "reference": "d09e152f403c843998d7a52b5d87040c937525dd", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6b92469f8a7995e626da3aa487099617b8dfa260", + "reference": "6b92469f8a7995e626da3aa487099617b8dfa260", "shasum": "" }, "require": { @@ -1451,7 +1451,9 @@ "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6" }, @@ -1476,9 +1478,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.4" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.6" }, - "time": "2025-01-22T13:07:38+00:00" + "time": "2025-03-26T12:47:06+00:00" }, { "name": "phpstan/phpstan-strict-rules", From f661443ec79d44bbe850e18b58bc0d670dfe093a Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 15 Apr 2025 16:48:13 +0100 Subject: [PATCH 04/43] Update Ubuntu base image for GitHub Actions --- .github/workflows/build-docker-image.yml | 2 +- .github/workflows/draft-release-pr-check.yml | 4 ++-- .github/workflows/draft-release.yml | 6 +++--- .github/workflows/main-php-matrix.yml | 2 +- .github/workflows/main.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 6199ad7a9e0..83d56887838 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -8,7 +8,7 @@ on: jobs: build: name: Update Docker Hub images - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Set up Docker Buildx diff --git a/.github/workflows/draft-release-pr-check.yml b/.github/workflows/draft-release-pr-check.yml index 131c0dde258..303f61ccfb0 100644 --- a/.github/workflows/draft-release-pr-check.yml +++ b/.github/workflows/draft-release-pr-check.yml @@ -24,7 +24,7 @@ permissions: jobs: check-intent: name: Check release trigger - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: valid: ${{ steps.validate.outputs.DEV_BUILD == 'false' }} @@ -43,7 +43,7 @@ jobs: #don't do these checks if this isn't a release - we don't want to generate unnecessary failed statuses if: needs.check-intent.outputs.valid == 'true' - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index d2e9eb0d0e5..02cdeec6f47 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -23,7 +23,7 @@ env: jobs: skip: name: Check whether to ignore this tag - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: skip: ${{ steps.exists.outputs.exists == 'true' }} @@ -54,7 +54,7 @@ jobs: needs: [check] if: needs.check.outputs.valid == 'true' && github.ref_type != 'tag' #can't do post-commit for a tag - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Generate access token @@ -79,7 +79,7 @@ jobs: needs: [check] if: needs.check.outputs.valid == 'true' || github.ref_type == 'tag' #ignore validity check for tags - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/main-php-matrix.yml b/.github/workflows/main-php-matrix.yml index e26f7c3187a..015a3318878 100644 --- a/.github/workflows/main-php-matrix.yml +++ b/.github/workflows/main-php-matrix.yml @@ -15,7 +15,7 @@ on: type: number image: description: 'Runner image to use' - default: 'ubuntu-20.04' + default: 'ubuntu-22.04' type: string jobs: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 57186874771..051a3a790d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: codestyle: name: Code Style checks - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -40,7 +40,7 @@ jobs: shellcheck: name: ShellCheck - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false From 2548422973a4b6f4417f9fe9b0e02164991ad583 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 20 Apr 2025 16:44:23 +0100 Subject: [PATCH 05/43] AvailableEnchantmentRegistry: reject non-string tags fixes https://crash.pmmp.io/view/12627328 --- src/item/enchantment/AvailableEnchantmentRegistry.php | 3 +++ src/utils/Utils.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php index eed7bff5273..2d8dafa4bbe 100644 --- a/src/item/enchantment/AvailableEnchantmentRegistry.php +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -28,6 +28,7 @@ use pocketmine\item\enchantment\VanillaEnchantments as Enchantments; use pocketmine\item\Item; use pocketmine\utils\SingletonTrait; +use pocketmine\utils\Utils; use function array_filter; use function array_values; use function count; @@ -129,6 +130,7 @@ public function setPrimaryItemTags(Enchantment $enchantment, array $tags) : void if(!$this->isRegistered($enchantment)){ throw new \LogicException("Cannot set primary item tags for non-registered enchantment"); } + Utils::validateArrayValueType($tags, fn(string $v) => 1); $this->primaryItemTags[spl_object_id($enchantment)] = array_values($tags); } @@ -152,6 +154,7 @@ public function setSecondaryItemTags(Enchantment $enchantment, array $tags) : vo if(!$this->isRegistered($enchantment)){ throw new \LogicException("Cannot set secondary item tags for non-registered enchantment"); } + Utils::validateArrayValueType($tags, fn(string $v) => 1); $this->secondaryItemTags[spl_object_id($enchantment)] = array_values($tags); } diff --git a/src/utils/Utils.php b/src/utils/Utils.php index 046296cf487..800bd018312 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -584,7 +584,7 @@ public static function validateCallableSignature(callable|CallbackType $signatur /** * @phpstan-template TMemberType * @phpstan-param array $array - * @phpstan-param \Closure(TMemberType) : void $validator + * @phpstan-param \Closure(TMemberType) : mixed $validator */ public static function validateArrayValueType(array $array, \Closure $validator) : void{ foreach(Utils::promoteKeys($array) as $k => $v){ From 4a5c1e75407f9cf8b37795210fc0957f9a37059a Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 20 Apr 2025 16:57:44 +0100 Subject: [PATCH 06/43] Entity: truncate fire ticks instead of throwing exceptions as written in the comments, it's not reasonable to propagate this limitation, since it ultimately comes from a shortfall in the Mojang save format, not a limitation of PM's capability. It's also not obvious how this would be propagated to the likes of setOnFire(), as this would translate into a max time of 1638 seconds, a value no one is going to remember. There's a case to be made for truncating this on save rather than on initial set, but this is at least better than having Fire Aspect level 1000 cause crashes and whatever other gameplay logic that would have to work around this stupid limitation. --- src/entity/Entity.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/entity/Entity.php b/src/entity/Entity.php index e24c6067cf0..97cdc19debe 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -60,6 +60,7 @@ use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; +use pocketmine\utils\Limits; use pocketmine\utils\Utils; use pocketmine\VersionInfo; use pocketmine\world\format\Chunk; @@ -700,9 +701,16 @@ public function getFireTicks() : int{ * @throws \InvalidArgumentException */ public function setFireTicks(int $fireTicks) : void{ - if($fireTicks < 0 || $fireTicks > 0x7fff){ - throw new \InvalidArgumentException("Fire ticks must be in range 0 ... " . 0x7fff . ", got $fireTicks"); + if($fireTicks < 0){ + throw new \InvalidArgumentException("Fire ticks cannot be negative"); } + + //Since the max value is not externally obvious or intuitive, many plugins use this without being aware that + //reasonably large values are not accepted. We even have such usages within PM itself. It doesn't make sense + //to force all those calls to be aware of this limitation, as it's not a functional limit but a limitation of + //the Mojang save format. Truncating this to the max acceptable value is the next best thing we can do. + $fireTicks = min($fireTicks, Limits::INT16_MAX); + if(!$this->isFireProof()){ $this->fireTicks = $fireTicks; $this->networkPropertiesDirty = true; From 1ea5c060fdd23023857cdc7f874a6562d117a72d Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 20 Apr 2025 18:16:54 +0100 Subject: [PATCH 07/43] bruh --- src/entity/Entity.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entity/Entity.php b/src/entity/Entity.php index 97cdc19debe..6681558adac 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -77,6 +77,7 @@ use function floor; use function fmod; use function get_class; +use function min; use function sin; use function spl_object_id; use const M_PI_2; From 6f3506360e8777dbe7b0e0eb05f717678e755658 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 08:30:26 +0000 Subject: [PATCH 08/43] Bump the github-actions group with 3 updates (#6683) --- .github/workflows/build-docker-image.yml | 8 ++++---- .github/workflows/discord-release-notify.yml | 2 +- .github/workflows/draft-release-pr-check.yml | 2 +- .github/workflows/draft-release.yml | 4 ++-- .github/workflows/main.yml | 2 +- .github/workflows/team-pr-auto-approve.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 83d56887838..dc282ab712c 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -53,7 +53,7 @@ jobs: run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT - name: Build image for tag - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: push: true context: ./pocketmine-mp @@ -66,7 +66,7 @@ jobs: - name: Build image for major tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: push: true context: ./pocketmine-mp @@ -79,7 +79,7 @@ jobs: - name: Build image for minor tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: push: true context: ./pocketmine-mp @@ -92,7 +92,7 @@ jobs: - name: Build image for latest tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: push: true context: ./pocketmine-mp diff --git a/.github/workflows/discord-release-notify.yml b/.github/workflows/discord-release-notify.yml index fde5e3099c8..93b2978aad7 100644 --- a/.github/workflows/discord-release-notify.yml +++ b/.github/workflows/discord-release-notify.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.32.0 + uses: shivammathur/setup-php@2.33.0 with: php-version: 8.2 diff --git a/.github/workflows/draft-release-pr-check.yml b/.github/workflows/draft-release-pr-check.yml index 303f61ccfb0..20b2200e6dd 100644 --- a/.github/workflows/draft-release-pr-check.yml +++ b/.github/workflows/draft-release-pr-check.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup PHP - uses: shivammathur/setup-php@2.32.0 + uses: shivammathur/setup-php@2.33.0 with: php-version: 8.2 diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 02cdeec6f47..fa20d19128a 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -59,7 +59,7 @@ jobs: steps: - name: Generate access token id: generate-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} @@ -87,7 +87,7 @@ jobs: submodules: true - name: Setup PHP - uses: shivammathur/setup-php@2.32.0 + uses: shivammathur/setup-php@2.33.0 with: php-version: ${{ env.PHP_VERSION }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 051a3a790d8..cfe97aa7ed1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.32.0 + uses: shivammathur/setup-php@2.33.0 with: php-version: 8.2 tools: php-cs-fixer:3.49 diff --git a/.github/workflows/team-pr-auto-approve.yml b/.github/workflows/team-pr-auto-approve.yml index 0c2fdd81c0e..cc5c471395c 100644 --- a/.github/workflows/team-pr-auto-approve.yml +++ b/.github/workflows/team-pr-auto-approve.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Generate access token id: generate-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} From f2e74736296eca93f3732cc64041535e824dab00 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 4 May 2025 17:18:58 +0100 Subject: [PATCH 09/43] Update PHP-CS-Fixer --- .github/workflows/main.yml | 4 ++-- .php-cs-fixer.php | 6 ++++++ build/dump-version-info.php | 4 ++-- src/block/tile/Spawnable.php | 4 ++-- src/world/World.php | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cfe97aa7ed1..cabda54be89 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,8 +30,8 @@ jobs: - name: Setup PHP and tools uses: shivammathur/setup-php@2.33.0 with: - php-version: 8.2 - tools: php-cs-fixer:3.49 + php-version: 8.3 + tools: php-cs-fixer:3.75 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 32af1ef4815..5a14b1d3550 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -6,6 +6,12 @@ ->in(__DIR__ . '/tests') ->in(__DIR__ . '/tools') ->notPath('plugins/DevTools') + //JsonMapper will break if the FQNs in the doc comments for these are shortened :( + ->notPath('crafting/json') + ->notPath('inventory/json') + ->notPath('data/bedrock/block/upgrade/model') + ->notPath('data/bedrock/item/upgrade/model') + ->notName('PocketMine.php'); return (new PhpCsFixer\Config) diff --git a/build/dump-version-info.php b/build/dump-version-info.php index e13696f3da4..3181acba608 100644 --- a/build/dump-version-info.php +++ b/build/dump-version-info.php @@ -31,8 +31,8 @@ */ /** - * @var string[]|\Closure[] $options - * @phpstan-var array $options + * @var string[]|Closure[] $options + * @phpstan-var array $options */ $options = [ "base_version" => VersionInfo::BASE_VERSION, diff --git a/src/block/tile/Spawnable.php b/src/block/tile/Spawnable.php index 67bc72fd91f..0c41713f2aa 100644 --- a/src/block/tile/Spawnable.php +++ b/src/block/tile/Spawnable.php @@ -32,7 +32,7 @@ use function get_class; abstract class Spawnable extends Tile{ - /** @phpstan-var CacheableNbt<\pocketmine\nbt\tag\CompoundTag>|null */ + /** @phpstan-var CacheableNbt|null */ private ?CacheableNbt $spawnCompoundCache = null; /** @@ -73,7 +73,7 @@ public function getRenderUpdateBugWorkaroundStateProperties(Block $block) : arra * Returns encoded NBT (varint, little-endian) used to spawn this tile to clients. Uses cache where possible, * populates cache if it is null. * - * @phpstan-return CacheableNbt<\pocketmine\nbt\tag\CompoundTag> + * @phpstan-return CacheableNbt */ final public function getSerializedSpawnCompound() : CacheableNbt{ if($this->spawnCompoundCache === null){ diff --git a/src/world/World.php b/src/world/World.php index 3a7d0c538b4..63a6171260d 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -360,7 +360,7 @@ class World implements ChunkManager{ private bool $doingTick = false; - /** @phpstan-var class-string<\pocketmine\world\generator\Generator> */ + /** @phpstan-var class-string */ private string $generator; private bool $unloaded = false; From d789c75c0084cacac09baa33d8ec5462d1f9c089 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Thu, 8 May 2025 02:21:39 +0100 Subject: [PATCH 10/43] Improve PHPStan error reporting for unsafe foreaches these are actually two separate concerns: one for dodgy PHPStan type suppression on implicit keys, and the other for arrays being casted to strings by PHP. --- phpstan.neon.dist | 2 +- ...OfStringRule.php => UnsafeForeachRule.php} | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) rename tests/phpstan/rules/{UnsafeForeachArrayOfStringRule.php => UnsafeForeachRule.php} (69%) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 13f35c12188..12c739f2fb6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -15,7 +15,7 @@ rules: - pocketmine\phpstan\rules\DisallowEnumComparisonRule - pocketmine\phpstan\rules\DisallowForeachByReferenceRule - pocketmine\phpstan\rules\ExplodeLimitRule - - pocketmine\phpstan\rules\UnsafeForeachArrayOfStringRule + - pocketmine\phpstan\rules\UnsafeForeachRule # - pocketmine\phpstan\rules\ThreadedSupportedTypesRule parameters: diff --git a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php b/tests/phpstan/rules/UnsafeForeachRule.php similarity index 69% rename from tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php rename to tests/phpstan/rules/UnsafeForeachRule.php index 34056131b55..cb463c61df7 100644 --- a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php +++ b/tests/phpstan/rules/UnsafeForeachRule.php @@ -41,7 +41,7 @@ /** * @implements Rule */ -final class UnsafeForeachArrayOfStringRule implements Rule{ +final class UnsafeForeachRule implements Rule{ public function getNodeType() : string{ return Foreach_::class; @@ -73,7 +73,7 @@ public function processNode(Node $node, Scope $scope) : array{ $benevolentUnionDepth--; return $result; } - if($type instanceof IntegerType && $benevolentUnionDepth === 0){ + if($type instanceof IntegerType){ $expectsIntKeyTypes = true; return $type; } @@ -87,24 +87,31 @@ public function processNode(Node $node, Scope $scope) : array{ $hasCastableKeyTypes = true; return $type; }); - if($hasCastableKeyTypes && !$expectsIntKeyTypes){ - $tip = $implicitType ? - sprintf( - "Declare a key type using @phpstan-var or @phpstan-param, or use %s() to promote the key type to get proper error reporting", + $errors = []; + if($implicitType){ + $errors[] = RuleErrorBuilder::message("Possible unreported errors in foreach on array with unspecified key type.") + ->tip(sprintf( + <<getIterableKeyType()->describe(VerbosityLevel::value()) - ))->tip($tip)->identifier('pocketmine.foreach.stringKeys')->build() - ]; + ))->identifier('pocketmine.foreach.implicitKeys')->build(); + } + if($hasCastableKeyTypes && !$expectsIntKeyTypes){ + $errors[] = RuleErrorBuilder::message(sprintf( + "Unsafe foreach on array with key type %s.", + $iterableType->getIterableKeyType()->describe(VerbosityLevel::value()) + )) + ->tip(sprintf( + <<identifier('pocketmine.foreach.stringKeys')->build(); } - return []; + return $errors; } } From 5e830c732075d067887eaac9fb605d6a347e8115 Mon Sep 17 00:00:00 2001 From: Dries C Date: Fri, 9 May 2025 16:29:05 +0200 Subject: [PATCH 11/43] Protocol changes for 1.21.80 (#6687) * Bedrock 1.21.80 support * Update bedrock-data * Add required tags to models * Fixed biome data loading * Support newest world format apparently I messed up the blockstate data version last time around... it hasn't changed since 1.21.60 * always CS has to complain... * Sync with release versions * Ready 5.28.0 release * this might help... --------- Co-authored-by: Dylan T. --- changelogs/5.28.md | 21 ++++++ composer.json | 4 +- composer.lock | 30 ++++---- src/VersionInfo.php | 4 +- src/data/bedrock/BedrockDataFiles.php | 3 +- src/data/bedrock/block/BlockStateData.php | 4 +- src/data/bedrock/block/BlockStateNames.php | 1 + src/data/bedrock/block/BlockTypeNames.php | 1 + src/data/bedrock/item/ItemTypeNames.php | 17 +++++ src/network/mcpe/cache/StaticPacketCache.php | 63 ++++++++++++++++- .../mcpe/handler/InGamePacketHandler.php | 5 -- .../biome/model/BiomeDefinitionEntryData.php | 69 +++++++++++++++++++ src/world/biome/model/ColorData.php | 41 +++++++++++ src/world/format/io/data/BedrockWorldData.php | 4 +- tools/generate-bedrock-data-from-packets.php | 60 ++++++++-------- 15 files changed, 267 insertions(+), 60 deletions(-) create mode 100644 changelogs/5.28.md create mode 100644 src/world/biome/model/BiomeDefinitionEntryData.php create mode 100644 src/world/biome/model/ColorData.php diff --git a/changelogs/5.28.md b/changelogs/5.28.md new file mode 100644 index 00000000000..f368e819e3e --- /dev/null +++ b/changelogs/5.28.md @@ -0,0 +1,21 @@ +# 5.28.0 +Released 9th May 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.80. + +**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace. +Do not update plugin minimum API versions unless you need new features added in this release. + +**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.** +Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. + +## General +- Aded support for Minecraft: Bedrock Edition 1.21.70. +- Removed support for earlier versions. + +## Fixes +- `AvailableEnchantmentRegistry` now requires provided tags to always be `string`. Previously, this wasn't enforced, leading to random crashes in core code related to enchanting. +- `Entity->setFireTicks()` and `Entity->setOnFire()` now truncate the fire time to the max value instead of throwing exceptions. + +## Internals +- Improved PHPStan error reporting for unsafe foreaches. Foreach on an array with implicit keys now generates different errors than foreach on an array with string keys. diff --git a/composer.json b/composer.json index 87086f456d4..7545806b4dc 100644 --- a/composer.json +++ b/composer.json @@ -34,9 +34,9 @@ "adhocore/json-comment": "~1.2.0", "netresearch/jsonmapper": "~v5.0.0", "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", - "pocketmine/bedrock-data": "~4.1.0+bedrock-1.21.70", + "pocketmine/bedrock-data": "5.0.0+bedrock-1.21.80", "pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50", - "pocketmine/bedrock-protocol": "~37.0.0+bedrock-1.21.70", + "pocketmine/bedrock-protocol": "38.0.0+bedrock-1.21.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", diff --git a/composer.lock b/composer.lock index 23f31231797..d45311018c3 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": "818c679a25da8e6b466bc454ad48dec3", + "content-hash": "4dfc7b8c912d8d5fa194ddc0e97903fb", "packages": [ { "name": "adhocore/json-comment", @@ -204,16 +204,16 @@ }, { "name": "pocketmine/bedrock-data", - "version": "4.1.0+bedrock-1.21.70", + "version": "5.0.0+bedrock-1.21.80", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "d53fe98cb3b596ac016e275df5bd5e89b04a4817" + "reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/d53fe98cb3b596ac016e275df5bd5e89b04a4817", - "reference": "d53fe98cb3b596ac016e275df5bd5e89b04a4817", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/e38d5ea19f794ec5216e5f96742237e8c4e7f080", + "reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080", "shasum": "" }, "type": "library", @@ -224,9 +224,9 @@ "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/BedrockData/issues", - "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.70" + "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.80" }, - "time": "2025-03-25T19:43:31+00:00" + "time": "2025-05-09T14:15:18+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", @@ -256,16 +256,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "37.0.0+bedrock-1.21.70", + "version": "38.0.0+bedrock-1.21.80", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "7091dad2c12ed4a4106432df21fc698960c6be9e" + "reference": "a626561eaefeb6333c0d2726e2789ceb0aac0724" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/7091dad2c12ed4a4106432df21fc698960c6be9e", - "reference": "7091dad2c12ed4a4106432df21fc698960c6be9e", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/a626561eaefeb6333c0d2726e2789ceb0aac0724", + "reference": "a626561eaefeb6333c0d2726e2789ceb0aac0724", "shasum": "" }, "require": { @@ -296,9 +296,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/37.0.0+bedrock-1.21.70" + "source": "https://github.com/pmmp/BedrockProtocol/tree/38.0.0+bedrock-1.21.80" }, - "time": "2025-03-27T15:19:36+00:00" + "time": "2025-05-09T14:17:07+00:00" }, { "name": "pocketmine/binaryutils", @@ -2921,7 +2921,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2952,7 +2952,7 @@ "ext-zlib": ">=1.2.11", "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.0" }, diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 44238dba3a6..c5b38f07228 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.27.2"; - public const IS_DEVELOPMENT_BUILD = true; + public const BASE_VERSION = "5.28.0"; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** diff --git a/src/data/bedrock/BedrockDataFiles.php b/src/data/bedrock/BedrockDataFiles.php index 1ecb707cc5f..53bd9b11e05 100644 --- a/src/data/bedrock/BedrockDataFiles.php +++ b/src/data/bedrock/BedrockDataFiles.php @@ -31,8 +31,7 @@ private function __construct(){ } public const BANNER_PATTERNS_JSON = BEDROCK_DATA_PATH . '/banner_patterns.json'; - public const BIOME_DEFINITIONS_NBT = BEDROCK_DATA_PATH . '/biome_definitions.nbt'; - public const BIOME_DEFINITIONS_FULL_NBT = BEDROCK_DATA_PATH . '/biome_definitions_full.nbt'; + public const BIOME_DEFINITIONS_JSON = BEDROCK_DATA_PATH . '/biome_definitions.json'; public const BIOME_ID_MAP_JSON = BEDROCK_DATA_PATH . '/biome_id_map.json'; public const BLOCK_ID_TO_ITEM_ID_MAP_JSON = BEDROCK_DATA_PATH . '/block_id_to_item_id_map.json'; public const BLOCK_PROPERTIES_TABLE_JSON = BEDROCK_DATA_PATH . '/block_properties_table.json'; diff --git a/src/data/bedrock/block/BlockStateData.php b/src/data/bedrock/block/BlockStateData.php index 600eba938cd..e90410ac7f0 100644 --- a/src/data/bedrock/block/BlockStateData.php +++ b/src/data/bedrock/block/BlockStateData.php @@ -45,8 +45,8 @@ final class BlockStateData{ public const CURRENT_VERSION = (1 << 24) | //major (21 << 16) | //minor - (70 << 8) | //patch - (1); //revision + (60 << 8) | //patch + (33); //revision public const TAG_NAME = "name"; public const TAG_STATES = "states"; diff --git a/src/data/bedrock/block/BlockStateNames.php b/src/data/bedrock/block/BlockStateNames.php index 9fed77e4a30..704798d1d5b 100644 --- a/src/data/bedrock/block/BlockStateNames.php +++ b/src/data/bedrock/block/BlockStateNames.php @@ -113,6 +113,7 @@ private function __construct(){ public const RAIL_DATA_BIT = "rail_data_bit"; public const RAIL_DIRECTION = "rail_direction"; public const REDSTONE_SIGNAL = "redstone_signal"; + public const REHYDRATION_LEVEL = "rehydration_level"; public const REPEATER_DELAY = "repeater_delay"; public const RESPAWN_ANCHOR_CHARGE = "respawn_anchor_charge"; public const ROTATION = "rotation"; diff --git a/src/data/bedrock/block/BlockTypeNames.php b/src/data/bedrock/block/BlockTypeNames.php index bc30800fc83..527a01345e6 100644 --- a/src/data/bedrock/block/BlockTypeNames.php +++ b/src/data/bedrock/block/BlockTypeNames.php @@ -392,6 +392,7 @@ private function __construct(){ public const DOUBLE_CUT_COPPER_SLAB = "minecraft:double_cut_copper_slab"; public const DRAGON_EGG = "minecraft:dragon_egg"; public const DRAGON_HEAD = "minecraft:dragon_head"; + public const DRIED_GHAST = "minecraft:dried_ghast"; public const DRIED_KELP_BLOCK = "minecraft:dried_kelp_block"; public const DRIPSTONE_BLOCK = "minecraft:dripstone_block"; public const DROPPER = "minecraft:dropper"; diff --git a/src/data/bedrock/item/ItemTypeNames.php b/src/data/bedrock/item/ItemTypeNames.php index ea95d57f08e..5f86cde9666 100644 --- a/src/data/bedrock/item/ItemTypeNames.php +++ b/src/data/bedrock/item/ItemTypeNames.php @@ -68,6 +68,7 @@ final class ItemTypeNames{ public const BIRCH_SIGN = "minecraft:birch_sign"; public const BLACK_BUNDLE = "minecraft:black_bundle"; public const BLACK_DYE = "minecraft:black_dye"; + public const BLACK_HARNESS = "minecraft:black_harness"; public const BLADE_POTTERY_SHERD = "minecraft:blade_pottery_sherd"; public const BLAZE_POWDER = "minecraft:blaze_powder"; public const BLAZE_ROD = "minecraft:blaze_rod"; @@ -76,6 +77,7 @@ final class ItemTypeNames{ public const BLUE_BUNDLE = "minecraft:blue_bundle"; public const BLUE_DYE = "minecraft:blue_dye"; public const BLUE_EGG = "minecraft:blue_egg"; + public const BLUE_HARNESS = "minecraft:blue_harness"; public const BOARD = "minecraft:board"; public const BOAT = "minecraft:boat"; public const BOGGED_SPAWN_EGG = "minecraft:bogged_spawn_egg"; @@ -95,6 +97,7 @@ final class ItemTypeNames{ public const BROWN_BUNDLE = "minecraft:brown_bundle"; public const BROWN_DYE = "minecraft:brown_dye"; public const BROWN_EGG = "minecraft:brown_egg"; + public const BROWN_HARNESS = "minecraft:brown_harness"; public const BRUSH = "minecraft:brush"; public const BUCKET = "minecraft:bucket"; public const BUNDLE = "minecraft:bundle"; @@ -166,6 +169,7 @@ final class ItemTypeNames{ public const CROSSBOW = "minecraft:crossbow"; public const CYAN_BUNDLE = "minecraft:cyan_bundle"; public const CYAN_DYE = "minecraft:cyan_dye"; + public const CYAN_HARNESS = "minecraft:cyan_harness"; public const DANGER_POTTERY_SHERD = "minecraft:danger_pottery_sherd"; public const DARK_OAK_BOAT = "minecraft:dark_oak_boat"; public const DARK_OAK_CHEST_BOAT = "minecraft:dark_oak_chest_boat"; @@ -265,12 +269,15 @@ final class ItemTypeNames{ public const GOLDEN_SWORD = "minecraft:golden_sword"; public const GRAY_BUNDLE = "minecraft:gray_bundle"; public const GRAY_DYE = "minecraft:gray_dye"; + public const GRAY_HARNESS = "minecraft:gray_harness"; public const GREEN_BUNDLE = "minecraft:green_bundle"; public const GREEN_DYE = "minecraft:green_dye"; + public const GREEN_HARNESS = "minecraft:green_harness"; public const GUARDIAN_SPAWN_EGG = "minecraft:guardian_spawn_egg"; public const GUNPOWDER = "minecraft:gunpowder"; public const GUSTER_BANNER_PATTERN = "minecraft:guster_banner_pattern"; public const GUSTER_POTTERY_SHERD = "minecraft:guster_pottery_sherd"; + public const HAPPY_GHAST_SPAWN_EGG = "minecraft:happy_ghast_spawn_egg"; public const HARD_STAINED_GLASS = "minecraft:hard_stained_glass"; public const HARD_STAINED_GLASS_PANE = "minecraft:hard_stained_glass_pane"; public const HEART_OF_THE_SEA = "minecraft:heart_of_the_sea"; @@ -321,10 +328,13 @@ final class ItemTypeNames{ public const LIGHT_BLOCK = "minecraft:light_block"; public const LIGHT_BLUE_BUNDLE = "minecraft:light_blue_bundle"; public const LIGHT_BLUE_DYE = "minecraft:light_blue_dye"; + public const LIGHT_BLUE_HARNESS = "minecraft:light_blue_harness"; public const LIGHT_GRAY_BUNDLE = "minecraft:light_gray_bundle"; public const LIGHT_GRAY_DYE = "minecraft:light_gray_dye"; + public const LIGHT_GRAY_HARNESS = "minecraft:light_gray_harness"; public const LIME_BUNDLE = "minecraft:lime_bundle"; public const LIME_DYE = "minecraft:lime_dye"; + public const LIME_HARNESS = "minecraft:lime_harness"; public const LINGERING_POTION = "minecraft:lingering_potion"; public const LLAMA_SPAWN_EGG = "minecraft:llama_spawn_egg"; public const LODESTONE_COMPASS = "minecraft:lodestone_compass"; @@ -333,6 +343,7 @@ final class ItemTypeNames{ public const MACE = "minecraft:mace"; public const MAGENTA_BUNDLE = "minecraft:magenta_bundle"; public const MAGENTA_DYE = "minecraft:magenta_dye"; + public const MAGENTA_HARNESS = "minecraft:magenta_harness"; public const MAGMA_CREAM = "minecraft:magma_cream"; public const MAGMA_CUBE_SPAWN_EGG = "minecraft:magma_cube_spawn_egg"; public const MANGROVE_BOAT = "minecraft:mangrove_boat"; @@ -400,6 +411,7 @@ final class ItemTypeNames{ public const OMINOUS_TRIAL_KEY = "minecraft:ominous_trial_key"; public const ORANGE_BUNDLE = "minecraft:orange_bundle"; public const ORANGE_DYE = "minecraft:orange_dye"; + public const ORANGE_HARNESS = "minecraft:orange_harness"; public const OXIDIZED_COPPER_DOOR = "minecraft:oxidized_copper_door"; public const PAINTING = "minecraft:painting"; public const PALE_OAK_BOAT = "minecraft:pale_oak_boat"; @@ -419,6 +431,7 @@ final class ItemTypeNames{ public const PILLAGER_SPAWN_EGG = "minecraft:pillager_spawn_egg"; public const PINK_BUNDLE = "minecraft:pink_bundle"; public const PINK_DYE = "minecraft:pink_dye"; + public const PINK_HARNESS = "minecraft:pink_harness"; public const PITCHER_POD = "minecraft:pitcher_pod"; public const PLANKS = "minecraft:planks"; public const PLENTY_POTTERY_SHERD = "minecraft:plenty_pottery_sherd"; @@ -439,6 +452,7 @@ final class ItemTypeNames{ public const PUMPKIN_SEEDS = "minecraft:pumpkin_seeds"; public const PURPLE_BUNDLE = "minecraft:purple_bundle"; public const PURPLE_DYE = "minecraft:purple_dye"; + public const PURPLE_HARNESS = "minecraft:purple_harness"; public const QUARTZ = "minecraft:quartz"; public const RABBIT = "minecraft:rabbit"; public const RABBIT_FOOT = "minecraft:rabbit_foot"; @@ -455,6 +469,7 @@ final class ItemTypeNames{ public const RED_BUNDLE = "minecraft:red_bundle"; public const RED_DYE = "minecraft:red_dye"; public const RED_FLOWER = "minecraft:red_flower"; + public const RED_HARNESS = "minecraft:red_harness"; public const REDSTONE = "minecraft:redstone"; public const REPEATER = "minecraft:repeater"; public const RESIN_BRICK = "minecraft:resin_brick"; @@ -563,6 +578,7 @@ final class ItemTypeNames{ public const WHEAT_SEEDS = "minecraft:wheat_seeds"; public const WHITE_BUNDLE = "minecraft:white_bundle"; public const WHITE_DYE = "minecraft:white_dye"; + public const WHITE_HARNESS = "minecraft:white_harness"; public const WILD_ARMOR_TRIM_SMITHING_TEMPLATE = "minecraft:wild_armor_trim_smithing_template"; public const WIND_CHARGE = "minecraft:wind_charge"; public const WITCH_SPAWN_EGG = "minecraft:witch_spawn_egg"; @@ -583,6 +599,7 @@ final class ItemTypeNames{ public const WRITTEN_BOOK = "minecraft:written_book"; public const YELLOW_BUNDLE = "minecraft:yellow_bundle"; public const YELLOW_DYE = "minecraft:yellow_dye"; + public const YELLOW_HARNESS = "minecraft:yellow_harness"; public const ZOGLIN_SPAWN_EGG = "minecraft:zoglin_spawn_egg"; public const ZOMBIE_HORSE_SPAWN_EGG = "minecraft:zombie_horse_spawn_egg"; public const ZOMBIE_PIGMAN_SPAWN_EGG = "minecraft:zombie_pigman_spawn_egg"; diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php index 88a52260026..861881437b9 100644 --- a/src/network/mcpe/cache/StaticPacketCache.php +++ b/src/network/mcpe/cache/StaticPacketCache.php @@ -23,13 +23,22 @@ namespace pocketmine\network\mcpe\cache; +use pocketmine\color\Color; use pocketmine\data\bedrock\BedrockDataFiles; +use pocketmine\data\SavedDataLoadingException; use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket; use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; +use pocketmine\network\mcpe\protocol\types\biome\BiomeDefinitionEntry; use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\utils\Filesystem; use pocketmine\utils\SingletonTrait; +use pocketmine\utils\Utils; +use pocketmine\world\biome\model\BiomeDefinitionEntryData; +use function count; +use function get_debug_type; +use function is_array; +use function json_decode; class StaticPacketCache{ use SingletonTrait; @@ -41,9 +50,61 @@ private static function loadCompoundFromFile(string $filePath) : CacheableNbt{ return new CacheableNbt((new NetworkNbtSerializer())->read(Filesystem::fileGetContents($filePath))->mustGetCompoundTag()); } + /** + * @return list + */ + private static function loadBiomeDefinitionModel(string $filePath) : array{ + $biomeEntries = json_decode(Filesystem::fileGetContents($filePath), associative: true); + if(!is_array($biomeEntries)){ + throw new SavedDataLoadingException("$filePath root should be an array, got " . get_debug_type($biomeEntries)); + } + + $jsonMapper = new \JsonMapper(); + $jsonMapper->bExceptionOnMissingData = true; + $jsonMapper->bStrictObjectTypeChecking = true; + $jsonMapper->bEnforceMapType = false; + + $entries = []; + foreach(Utils::promoteKeys($biomeEntries) as $biomeName => $entry){ + if(!is_array($entry)){ + throw new SavedDataLoadingException("$filePath should be an array of objects, got " . get_debug_type($entry)); + } + + try{ + $biomeDefinition = $jsonMapper->map($entry, new BiomeDefinitionEntryData()); + + $mapWaterColour = $biomeDefinition->mapWaterColour; + $entries[] = new BiomeDefinitionEntry( + (string) $biomeName, + $biomeDefinition->id, + $biomeDefinition->temperature, + $biomeDefinition->downfall, + $biomeDefinition->redSporeDensity, + $biomeDefinition->blueSporeDensity, + $biomeDefinition->ashDensity, + $biomeDefinition->whiteAshDensity, + $biomeDefinition->depth, + $biomeDefinition->scale, + new Color( + $mapWaterColour->r, + $mapWaterColour->g, + $mapWaterColour->b, + $mapWaterColour->a + ), + $biomeDefinition->rain, + count($biomeDefinition->tags) > 0 ? $biomeDefinition->tags : null, + ); + }catch(\JsonMapper_Exception $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + } + + return $entries; + } + private static function make() : self{ return new self( - BiomeDefinitionListPacket::create(self::loadCompoundFromFile(BedrockDataFiles::BIOME_DEFINITIONS_NBT)), + BiomeDefinitionListPacket::fromDefinitions(self::loadBiomeDefinitionModel(BedrockDataFiles::BIOME_DEFINITIONS_JSON)), AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(BedrockDataFiles::ENTITY_IDENTIFIERS_NBT)) ); } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index 93a01fdcc09..eec200e4b9c 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -73,7 +73,6 @@ use pocketmine\network\mcpe\protocol\PlayerActionPacket; use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket; use pocketmine\network\mcpe\protocol\PlayerHotbarPacket; -use pocketmine\network\mcpe\protocol\PlayerInputPacket; use pocketmine\network\mcpe\protocol\PlayerSkinPacket; use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket; use pocketmine\network\mcpe\protocol\serializer\BitSet; @@ -781,10 +780,6 @@ public function handleBlockActorData(BlockActorDataPacket $packet) : bool{ return false; } - public function handlePlayerInput(PlayerInputPacket $packet) : bool{ - return false; //TODO - } - public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{ $gameMode = $this->session->getTypeConverter()->protocolGameModeToCore($packet->gamemode); if($gameMode !== $this->player->getGamemode()){ diff --git a/src/world/biome/model/BiomeDefinitionEntryData.php b/src/world/biome/model/BiomeDefinitionEntryData.php new file mode 100644 index 00000000000..8a5c3d35424 --- /dev/null +++ b/src/world/biome/model/BiomeDefinitionEntryData.php @@ -0,0 +1,69 @@ + + */ + public array $tags; +} diff --git a/src/world/biome/model/ColorData.php b/src/world/biome/model/ColorData.php new file mode 100644 index 00000000000..f70a77d1559 --- /dev/null +++ b/src/world/biome/model/ColorData.php @@ -0,0 +1,41 @@ +bedrockDataPath . '/biome_definitions_full.nbt', $packet->definitions->getEncodedNbt()); - - $nbt = $packet->definitions->getRoot(); - if(!$nbt instanceof CompoundTag){ - throw new AssumptionFailedError(); - } - $strippedNbt = clone $nbt; - foreach($strippedNbt as $compound){ - if($compound instanceof CompoundTag){ - foreach([ - "minecraft:capped_surface", - "minecraft:consolidated_features", - "minecraft:frozen_ocean_surface", - "minecraft:legacy_world_generation_rules", - "minecraft:mesa_surface", - "minecraft:mountain_parameters", - "minecraft:multinoise_generation_rules", - "minecraft:overworld_generation_rules", - "minecraft:surface_material_adjustments", - "minecraft:surface_parameters", - "minecraft:swamp_surface", - ] as $remove){ - $compound->removeTag($remove); - } - } - } - - file_put_contents($this->bedrockDataPath . '/biome_definitions.nbt', (new CacheableNbt($strippedNbt))->getEncodedNbt()); + $definitions = []; + foreach($packet->buildDefinitionsFromData() as $entry){ + $mapWaterColor = new ColorData(); + $mapWaterColor->r = $entry->getMapWaterColor()->getR(); + $mapWaterColor->g = $entry->getMapWaterColor()->getG(); + $mapWaterColor->b = $entry->getMapWaterColor()->getB(); + $mapWaterColor->a = $entry->getMapWaterColor()->getA(); + + $data = new BiomeDefinitionEntryData(); + $data->id = $entry->getId(); + $data->temperature = round($entry->getTemperature(), 3); + $data->downfall = round($entry->getDownfall(), 3); + $data->redSporeDensity = round($entry->getRedSporeDensity(), 3); + $data->blueSporeDensity = round($entry->getBlueSporeDensity(), 3); + $data->ashDensity = round($entry->getAshDensity(), 3); + $data->whiteAshDensity = round($entry->getWhiteAshDensity(), 3); + $data->depth = round($entry->getDepth(), 3); + $data->scale = round($entry->getScale(), 3); + $data->mapWaterColour = $mapWaterColor; + $data->rain = $entry->hasRain(); + $data->tags = $entry->getTags() ?? []; + + $definitions[$entry->getBiomeName()] = self::objectToOrderedArray($data); + } + + ksort($definitions, SORT_STRING); + + file_put_contents($this->bedrockDataPath . '/biome_definitions.json', json_encode($definitions, JSON_PRETTY_PRINT) . "\n"); return true; } From 134c7309c5985cd6df0a323a8465f44e9099510a Mon Sep 17 00:00:00 2001 From: "pmmp-admin-bot[bot]" <188621379+pmmp-admin-bot[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 14:30:04 +0000 Subject: [PATCH 12/43] 5.28.1 is next Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14931216524 --- src/VersionInfo.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VersionInfo.php b/src/VersionInfo.php index c5b38f07228..acc7db91c0f 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.28.0"; - public const IS_DEVELOPMENT_BUILD = false; + public const BASE_VERSION = "5.28.1"; + public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; /** From d90fc3415c611a976cdde80bbcd79574fadb38bf Mon Sep 17 00:00:00 2001 From: ItzxDwi <107537435+ItzxDwi@users.noreply.github.com> Date: Fri, 9 May 2025 23:33:55 +0800 Subject: [PATCH 13/43] fixed wrong version info (#6689) --- changelogs/5.28.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/5.28.md b/changelogs/5.28.md index f368e819e3e..8a69b97e0d0 100644 --- a/changelogs/5.28.md +++ b/changelogs/5.28.md @@ -10,7 +10,7 @@ Do not update plugin minimum API versions unless you need new features added in Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. ## General -- Aded support for Minecraft: Bedrock Edition 1.21.70. +- Aded support for Minecraft: Bedrock Edition 1.21.80. - Removed support for earlier versions. ## Fixes From 04de72e85ec8c8da36e1d527db3cbe4ee855a124 Mon Sep 17 00:00:00 2001 From: Sergi del Olmo Date: Sat, 10 May 2025 15:34:37 +0200 Subject: [PATCH 14/43] Fix changelog typo (#6690) --- changelogs/5.28.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/5.28.md b/changelogs/5.28.md index 8a69b97e0d0..a2ede942fa2 100644 --- a/changelogs/5.28.md +++ b/changelogs/5.28.md @@ -10,7 +10,7 @@ Do not update plugin minimum API versions unless you need new features added in Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. ## General -- Aded support for Minecraft: Bedrock Edition 1.21.80. +- Added support for Minecraft: Bedrock Edition 1.21.80. - Removed support for earlier versions. ## Fixes From 67f3bb9c5242dfab49ccb25d65edb4e00ac3f7e2 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 13:46:33 +0100 Subject: [PATCH 15/43] Update composer dependencies and fix an error found by new PHPStan update --- composer.json | 2 +- composer.lock | 63 +++++++++++++++++++--------------- src/thread/ThreadCrashInfo.php | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index 7545806b4dc..8900eea5133 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "symfony/filesystem": "~6.4.0" }, "require-dev": { - "phpstan/phpstan": "2.1.11", + "phpstan/phpstan": "2.1.16", "phpstan/phpstan-phpunit": "^2.0.0", "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" diff --git a/composer.lock b/composer.lock index d45311018c3..94eebda8c9f 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": "4dfc7b8c912d8d5fa194ddc0e97903fb", + "content-hash": "e3fffa76c2ce9dd0f5c2cd66a5aa097c", "packages": [ { "name": "adhocore/json-comment", @@ -976,7 +976,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -1035,7 +1035,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -1055,19 +1055,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -1115,7 +1116,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -1131,22 +1132,22 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" } ], "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -1185,7 +1186,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -1193,7 +1194,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nikic/php-parser", @@ -1373,16 +1374,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.11", + "version": "2.1.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", - "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", "shasum": "" }, "require": { @@ -1427,7 +1428,7 @@ "type": "github" } ], - "time": "2025-03-24T13:45:00+00:00" + "time": "2025-05-16T09:40:10+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -1853,16 +1854,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.45", + "version": "10.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d", + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d", "shasum": "" }, "require": { @@ -1872,7 +1873,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -1934,7 +1935,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46" }, "funding": [ { @@ -1945,12 +1946,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-02-06T16:08:12+00:00" + "time": "2025-05-02T06:46:24+00:00" }, { "name": "sebastian/cli-parser", diff --git a/src/thread/ThreadCrashInfo.php b/src/thread/ThreadCrashInfo.php index 66aae927a88..6fffdc83bb4 100644 --- a/src/thread/ThreadCrashInfo.php +++ b/src/thread/ThreadCrashInfo.php @@ -84,6 +84,6 @@ public function getTrace() : array{ public function getThreadName() : string{ return $this->threadName; } public function makePrettyMessage() : string{ - return sprintf("%s: \"%s\" in \"%s\" on line %d", $this->type ?? "Fatal error", $this->message, Filesystem::cleanPath($this->file), $this->line); + return sprintf("%s: \"%s\" in \"%s\" on line %d", $this->type, $this->message, Filesystem::cleanPath($this->file), $this->line); } } From dca37d5842a6405004aff5eeb3fca264f2b260df Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 14:11:57 +0100 Subject: [PATCH 16/43] Hack: forcibly remove symfony/polyfill-mbstring we don't need this dependency anyway because mbstring is already provided. --- composer.json | 3 ++ composer.lock | 83 +-------------------------------------------------- 2 files changed, 4 insertions(+), 82 deletions(-) diff --git a/composer.json b/composer.json index 8900eea5133..f24ddc7e54a 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,9 @@ "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" }, + "provide": { + "symfony/polyfill-mbstring": "*" + }, "autoload": { "psr-4": { "pocketmine\\": "src/" diff --git a/composer.lock b/composer.lock index 94eebda8c9f..2350246ed6a 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": "e3fffa76c2ce9dd0f5c2cd66a5aa097c", + "content-hash": "c2f2a1e28028894c1b12484f115732f0", "packages": [ { "name": "adhocore/json-comment", @@ -1052,87 +1052,6 @@ } ], "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "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.32.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": "2024-12-23T08:48:59+00:00" } ], "packages-dev": [ From e0864e7ee82d4295e2b1d80d0d057016b5e94956 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 14:54:26 +0100 Subject: [PATCH 17/43] composer: also axe unnecessary ctype polyfill --- composer.json | 1 + composer.lock | 81 +-------------------------------------------------- 2 files changed, 2 insertions(+), 80 deletions(-) diff --git a/composer.json b/composer.json index f24ddc7e54a..1482aa4cb04 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ "phpunit/phpunit": "^10.5.24" }, "provide": { + "symfony/polyfill-ctype": "*", "symfony/polyfill-mbstring": "*" }, "autoload": { diff --git a/composer.lock b/composer.lock index 2350246ed6a..1326fbc9af2 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": "c2f2a1e28028894c1b12484f115732f0", + "content-hash": "b25d87be51beaaad7285a6b2e771ab4e", "packages": [ { "name": "adhocore/json-comment", @@ -973,85 +973,6 @@ } ], "time": "2024-10-25T15:07:50+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "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.32.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": "2024-09-09T11:45:10+00:00" } ], "packages-dev": [ From abb004fbc5d03ff58a5f68f069b70d7e682fc70a Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Sat, 17 May 2025 15:00:53 +0100 Subject: [PATCH 18/43] Ready 5.28.1 (#6696) --- changelogs/5.28.md | 6 ++++++ src/VersionInfo.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelogs/5.28.md b/changelogs/5.28.md index a2ede942fa2..74906ecc7f1 100644 --- a/changelogs/5.28.md +++ b/changelogs/5.28.md @@ -19,3 +19,9 @@ Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if ## Internals - Improved PHPStan error reporting for unsafe foreaches. Foreach on an array with implicit keys now generates different errors than foreach on an array with string keys. + +# 5.28.1 +Released 17th May 2025. + +## Fixes +- Fixed errors when PlayStation players attempt to join due to null `TitleID`. diff --git a/src/VersionInfo.php b/src/VersionInfo.php index acc7db91c0f..7344085cf98 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -32,7 +32,7 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; public const BASE_VERSION = "5.28.1"; - public const IS_DEVELOPMENT_BUILD = true; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** From 280911ec59103128b60e40a94c388fbd5907ea59 Mon Sep 17 00:00:00 2001 From: "pmmp-admin-bot[bot]" <188621379+pmmp-admin-bot[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 14:01:49 +0000 Subject: [PATCH 19/43] 5.28.2 is next Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15085916916 --- src/VersionInfo.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 7344085cf98..88509970102 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.28.1"; - public const IS_DEVELOPMENT_BUILD = false; + public const BASE_VERSION = "5.28.2"; + public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; /** From a37353c0605d6b6424ffa66dd150c2b691c6a651 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 16:37:05 +0100 Subject: [PATCH 20/43] composer: fixed borked version constraints bruhhhhhhhhhhhh --- composer.json | 4 ++-- composer.lock | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 1482aa4cb04..c744c320eab 100644 --- a/composer.json +++ b/composer.json @@ -34,9 +34,9 @@ "adhocore/json-comment": "~1.2.0", "netresearch/jsonmapper": "~v5.0.0", "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", - "pocketmine/bedrock-data": "5.0.0+bedrock-1.21.80", + "pocketmine/bedrock-data": "~5.0.0+bedrock-1.21.80", "pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50", - "pocketmine/bedrock-protocol": "38.0.0+bedrock-1.21.80", + "pocketmine/bedrock-protocol": "~38.0.0+bedrock-1.21.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", diff --git a/composer.lock b/composer.lock index 1326fbc9af2..b82a014da3e 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": "b25d87be51beaaad7285a6b2e771ab4e", + "content-hash": "d8fa42f33a3bcb26014e6f862366dbd6", "packages": [ { "name": "adhocore/json-comment", @@ -256,16 +256,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "38.0.0+bedrock-1.21.80", + "version": "38.0.1+bedrock-1.21.80", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "a626561eaefeb6333c0d2726e2789ceb0aac0724" + "reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/a626561eaefeb6333c0d2726e2789ceb0aac0724", - "reference": "a626561eaefeb6333c0d2726e2789ceb0aac0724", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f", + "reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f", "shasum": "" }, "require": { @@ -296,9 +296,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/38.0.0+bedrock-1.21.80" + "source": "https://github.com/pmmp/BedrockProtocol/tree/38.0.1+bedrock-1.21.80" }, - "time": "2025-05-09T14:17:07+00:00" + "time": "2025-05-17T11:56:33+00:00" }, { "name": "pocketmine/binaryutils", From 81d3017ad5e15e8f6ca846733826b47a5e90eba2 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Sat, 17 May 2025 16:44:19 +0100 Subject: [PATCH 21/43] Murphy's Law (#6698) --- changelogs/5.28.md | 7 +++++++ src/VersionInfo.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelogs/5.28.md b/changelogs/5.28.md index 74906ecc7f1..f378031f7b8 100644 --- a/changelogs/5.28.md +++ b/changelogs/5.28.md @@ -25,3 +25,10 @@ Released 17th May 2025. ## Fixes - Fixed errors when PlayStation players attempt to join due to null `TitleID`. + +# 5.28.2 +Released 17th May 2025. + +## Fixes +- Fixed version constraints which were incorrectly updated during the 1.21.80 update. This led to an unnoticed failure to update BedrockProtocol in the previous patch release. +- Actually fixed PlayStation issues this time diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 88509970102..aa42e2e039a 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -32,7 +32,7 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; public const BASE_VERSION = "5.28.2"; - public const IS_DEVELOPMENT_BUILD = true; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** From 647c2587a8c40e641cebca331d06105e92edee8c Mon Sep 17 00:00:00 2001 From: "pmmp-admin-bot[bot]" <188621379+pmmp-admin-bot[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 15:45:22 +0000 Subject: [PATCH 22/43] 5.28.3 is next Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15086729525 --- src/VersionInfo.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VersionInfo.php b/src/VersionInfo.php index aa42e2e039a..6150246563b 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.28.2"; - public const IS_DEVELOPMENT_BUILD = false; + public const BASE_VERSION = "5.28.3"; + public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; /** From 657e6c8130154629c21e4ed9c148386ca6d3af89 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 17:24:53 +0100 Subject: [PATCH 23/43] Added trigger cron workflow for RestrictedActions branch sync we're having problems with the restricted action getting disabled due to repo inactivity, so it's best we trigger it from here, since this repo's activity is what it's interested in anyway. --- .../workflows/branch-sync-cron-trigger.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/branch-sync-cron-trigger.yml diff --git a/.github/workflows/branch-sync-cron-trigger.yml b/.github/workflows/branch-sync-cron-trigger.yml new file mode 100644 index 00000000000..145fcd222f6 --- /dev/null +++ b/.github/workflows/branch-sync-cron-trigger.yml @@ -0,0 +1,32 @@ +#Since GitHub automatically disables cron actions after 60 days of repo inactivity, we need the active repo (PM) +#to trigger the branch merge workflow explicitly. This avoids the need for TOS-violating actions which we previously +#used to keep the restricted action active, as the workflow depends on the activity of this repo anyway. + +name: Trigger branch sync + +on: + schedule: + - cron: "0 0 * * *" #once per day so we don't spam merge commits on busy days + workflow_dispatch: #for testing + +jobs: + trigger: + name: Trigger branch sync RestrictedActions workflow + runs-on: ubuntu-22.04 + + steps: + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} + private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} + owner: ${{ github.repository_owner }} + repositories: RestrictedActions + + - name: Dispatch branch sync restricted action + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.generate-token.outputs.token }} + repository: ${{ github.repository_owner }}/RestrictedActions + event-type: pocketmine_mp_branch_sync From b5f236c019f360f0ac57aa4355810354f9ae80ac Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 18:09:14 +0100 Subject: [PATCH 24/43] Apparently we're supposed to use replace for this, not provide --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c744c320eab..979973893a0 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" }, - "provide": { + "replace": { "symfony/polyfill-ctype": "*", "symfony/polyfill-mbstring": "*" }, diff --git a/composer.lock b/composer.lock index b82a014da3e..9cb0721fcc7 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": "d8fa42f33a3bcb26014e6f862366dbd6", + "content-hash": "ceb98091ac3f61f1a4b87708c48dc75a", "packages": [ { "name": "adhocore/json-comment", From 94fb5d95b92604840dabb719f04327efa559cf94 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 17 May 2025 19:09:54 +0100 Subject: [PATCH 25/43] CommonThreadPartsTrait: fixed thread crashes sometimes missing cause info closes #6669 this happens because isTerminated returns true before the thread's shutdown handler runs, so we join with the thread to make sure that shutdown handlers are done before returning. ... hopefully we don't get servers randomly deadlocking in shutdown handlers ??? --- src/thread/CommonThreadPartsTrait.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/thread/CommonThreadPartsTrait.php b/src/thread/CommonThreadPartsTrait.php index e1c9d7c6bb1..de606a7b290 100644 --- a/src/thread/CommonThreadPartsTrait.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -94,7 +94,17 @@ public function registerClassLoaders() : void{ } } - public function getCrashInfo() : ?ThreadCrashInfo{ return $this->crashInfo; } + public function getCrashInfo() : ?ThreadCrashInfo{ + //TODO: Joining a crashed worker might be a bit sus, but we need to make sure the thread's shutdown + //handler has run before we try to collect the crash info. As of 6.1.1, pmmpthread sets isTerminated=true + //*before* the shutdown handler is invoked, so we might land here before the crash info has been set. + //In the future this should probably be fixed by running the shutdown handlers before setting isTerminated, + //but this workaround should be good enough for now. + if($this->isTerminated() && !$this->isJoined()){ + $this->join(); + } + return $this->crashInfo; + } public function start(int $options = NativeThread::INHERIT_NONE) : bool{ ThreadManager::getInstance()->add($this); From 9606c0e0bbe054061714e48503d993a9aa8ca7b5 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Fri, 23 May 2025 22:16:57 +0100 Subject: [PATCH 26/43] Remove stale labels as well as Waiting on Author labels actions/stale is far too slow to do this itself since it processes lots of irrelevant crap on every run --- .github/workflows/pr-remove-waiting-label.yml | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-remove-waiting-label.yml b/.github/workflows/pr-remove-waiting-label.yml index eb46043bdd2..da14e36bab7 100644 --- a/.github/workflows/pr-remove-waiting-label.yml +++ b/.github/workflows/pr-remove-waiting-label.yml @@ -15,19 +15,23 @@ jobs: with: github-token: ${{ github.token }} script: | - const [owner, repo] = context.payload.repository.full_name.split('/'); - try { - await github.rest.issues.removeLabel({ - owner: owner, - repo: repo, - issue_number: context.payload.number, - name: "Status: Waiting on Author", - }); - } catch (error) { - if (error.status === 404) { - //probably label wasn't set on the issue - console.log('Failed to remove label (probably label isn\'t on the PR): ' + error.message); - } else { - throw error; + function removeLabel(owner, repo, issue_number, name) { + try { + await github.rest.issues.removeLabel({ + owner: owner, + repo: repo, + issue_number: issue_number, + name: name, + }); + } catch (error) { + if (error.status === 404) { + //probably label wasn't set on the issue + console.log('Failed to remove label ' + name + ' (probably label isn\'t on the PR): ' + error.message); + } else { + throw error; + } } } + const [owner, repo] = context.payload.repository.full_name.split('/'); + removeLabel(owner, repo, context.payload.number, "Status: Waiting on Author"); + removeLabel(owner, repo, context.payload.number, "Stale"); From 3636173d75d7b97414c86d7c6f32bade005185e9 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Fri, 23 May 2025 23:28:15 +0100 Subject: [PATCH 27/43] ... --- .github/workflows/pr-remove-waiting-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-remove-waiting-label.yml b/.github/workflows/pr-remove-waiting-label.yml index da14e36bab7..b7cd85acdaf 100644 --- a/.github/workflows/pr-remove-waiting-label.yml +++ b/.github/workflows/pr-remove-waiting-label.yml @@ -15,7 +15,7 @@ jobs: with: github-token: ${{ github.token }} script: | - function removeLabel(owner, repo, issue_number, name) { + async function removeLabel(owner, repo, issue_number, name) { try { await github.rest.issues.removeLabel({ owner: owner, From 5527a0c6bf4343b39cd6ed4526f75539ac6ddf19 Mon Sep 17 00:00:00 2001 From: ItzxDwi <107537435+ItzxDwi@users.noreply.github.com> Date: Sun, 25 May 2025 16:07:41 +0800 Subject: [PATCH 28/43] Entity: make stepHeight accessable (#6702) --- src/entity/Entity.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/entity/Entity.php b/src/entity/Entity.php index 6681558adac..eb7098f1e7a 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -1187,12 +1187,14 @@ protected function move(float $dx, float $dy, float $dz) : void{ $moveBB->offset(0, 0, $dz); - if($this->stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){ + $stepHeight = $this->getStepHeight(); + + if($stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){ $cx = $dx; $cy = $dy; $cz = $dz; $dx = $wantedX; - $dy = $this->stepHeight; + $dy = $stepHeight; $dz = $wantedZ; $stepBB = clone $this->boundingBox; @@ -1262,6 +1264,14 @@ protected function move(float $dx, float $dy, float $dz) : void{ Timings::$entityMove->stopTiming(); } + public function setStepHeight(float $stepHeight) : void{ + $this->stepHeight = $stepHeight; + } + + public function getStepHeight() : float{ + return $this->stepHeight; + } + protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{ $this->isCollidedVertically = $wantedY !== $dy; $this->isCollidedHorizontally = ($wantedX !== $dx || $wantedZ !== $dz); From a554d2acf5db00f577493517e36bb3ad07a6bdd0 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 25 May 2025 11:32:20 +0100 Subject: [PATCH 29/43] Revert change that can't go on stable API additions need to wait for the next minor release Revert "Entity: make stepHeight accessable (#6702)" This reverts commit 5527a0c6bf4343b39cd6ed4526f75539ac6ddf19. --- src/entity/Entity.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/entity/Entity.php b/src/entity/Entity.php index eb7098f1e7a..6681558adac 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -1187,14 +1187,12 @@ protected function move(float $dx, float $dy, float $dz) : void{ $moveBB->offset(0, 0, $dz); - $stepHeight = $this->getStepHeight(); - - if($stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){ + if($this->stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){ $cx = $dx; $cy = $dy; $cz = $dz; $dx = $wantedX; - $dy = $stepHeight; + $dy = $this->stepHeight; $dz = $wantedZ; $stepBB = clone $this->boundingBox; @@ -1264,14 +1262,6 @@ protected function move(float $dx, float $dy, float $dz) : void{ Timings::$entityMove->stopTiming(); } - public function setStepHeight(float $stepHeight) : void{ - $this->stepHeight = $stepHeight; - } - - public function getStepHeight() : float{ - return $this->stepHeight; - } - protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{ $this->isCollidedVertically = $wantedY !== $dy; $this->isCollidedHorizontally = ($wantedX !== $dx || $wantedZ !== $dz); From b40b99fe72cec89b0b785e904cbaff9664c6fb0c Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Wed, 28 May 2025 21:32:48 +0100 Subject: [PATCH 30/43] Player: fixed crash on action item return this can happen if the old item had a lower max damage than the new one, and the new one has a damage higher than the old one's max damage. it can also happen if the damage was overridden to some illegal value by a custom item as seen in https://crash.pmmp.io/view/12754811 --- src/player/Player.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/player/Player.php b/src/player/Player.php index 1c67b7182c5..e0a42ed1d95 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -1641,7 +1641,10 @@ private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, arr $newReplica = clone $oldHeldItem; $newReplica->setCount($newHeldItem->getCount()); if($newReplica instanceof Durable && $newHeldItem instanceof Durable){ - $newReplica->setDamage($newHeldItem->getDamage()); + $newDamage = $newHeldItem->getDamage(); + if($newDamage >= 0 && $newDamage <= $newReplica->getMaxDurability()){ + $newReplica->setDamage($newDamage); + } } $damagedOrDeducted = $newReplica->equalsExact($newHeldItem); From 0910a219d4b66d53f1387eea27f25efbc4e34572 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Wed, 28 May 2025 23:29:37 +0100 Subject: [PATCH 31/43] Fixed improper pre-checking of PlayerAuthInputPacket flags --- composer.json | 2 +- composer.lock | 14 +++++++------- src/network/mcpe/handler/InGamePacketHandler.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 979973893a0..d2064bcd610 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", "pocketmine/bedrock-data": "~5.0.0+bedrock-1.21.80", "pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50", - "pocketmine/bedrock-protocol": "~38.0.0+bedrock-1.21.80", + "pocketmine/bedrock-protocol": "~38.1.0+bedrock-1.21.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", diff --git a/composer.lock b/composer.lock index 9cb0721fcc7..2e2e5a6007c 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": "ceb98091ac3f61f1a4b87708c48dc75a", + "content-hash": "fe62caebfdb35cd8bd57c8e61879b7c0", "packages": [ { "name": "adhocore/json-comment", @@ -256,16 +256,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "38.0.1+bedrock-1.21.80", + "version": "38.1.0+bedrock-1.21.80", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f" + "reference": "a1fa215563517050045309bb779a67f75843b867" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f", - "reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/a1fa215563517050045309bb779a67f75843b867", + "reference": "a1fa215563517050045309bb779a67f75843b867", "shasum": "" }, "require": { @@ -296,9 +296,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/38.0.1+bedrock-1.21.80" + "source": "https://github.com/pmmp/BedrockProtocol/tree/38.1.0+bedrock-1.21.80" }, - "time": "2025-05-17T11:56:33+00:00" + "time": "2025-05-28T22:19:59+00:00" }, { "name": "pocketmine/binaryutils", diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index eec200e4b9c..927ba38fa53 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -211,7 +211,7 @@ public function handlePlayerAuthInput(PlayerAuthInputPacket $packet) : bool{ } $inputFlags = $packet->getInputFlags(); - if($inputFlags !== $this->lastPlayerAuthInputFlags){ + if($this->lastPlayerAuthInputFlags === null || !$inputFlags->equals($this->lastPlayerAuthInputFlags)){ $this->lastPlayerAuthInputFlags = $inputFlags; $sneaking = $inputFlags->get(PlayerAuthInputFlags::SNEAKING); From b4b6bbe29f21754039db11ab8ca7d0758e4b43b5 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Thu, 29 May 2025 17:18:45 +0100 Subject: [PATCH 32/43] BaseInventory: fixed internalAddItem() setting air slots to air this bug was introduced in #4237, but it was unnoticed due to having no adverse effects other than noisy debugs and network traffic. --- src/inventory/BaseInventory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 0d5d1ffe602..c4afda43a07 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -256,7 +256,7 @@ private function internalAddItem(Item $newItem) : Item{ $slotItem->setCount($slotItem->getCount() + $amount); $this->setItem($i, $slotItem); if($newItem->getCount() <= 0){ - break; + return $newItem; } } } @@ -270,7 +270,7 @@ private function internalAddItem(Item $newItem) : Item{ $slotItem->setCount($amount); $this->setItem($slotIndex, $slotItem); if($newItem->getCount() <= 0){ - break; + return $newItem; } } } From e99665fb12299519ad50f0da7f08000af9e2bc45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:08:14 +0000 Subject: [PATCH 33/43] Bump docker/build-push-action in the github-actions group (#6719) --- .github/workflows/build-docker-image.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index dc282ab712c..a3921f8201f 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -53,7 +53,7 @@ jobs: run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT - name: Build image for tag - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp @@ -66,7 +66,7 @@ jobs: - name: Build image for major tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp @@ -79,7 +79,7 @@ jobs: - name: Build image for minor tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp @@ -92,7 +92,7 @@ jobs: - name: Build image for latest tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.16.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp From 69b3980781339160fa2782668bacb0c4f1d2df43 Mon Sep 17 00:00:00 2001 From: KanadeBlue <124839201+KanadeBlue@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:12:57 +0000 Subject: [PATCH 34/43] prob everything --- src/VersionInfo.php | 2 +- src/form/FormAPI/CustomForm.php | 173 ++++++++ src/form/FormAPI/Form.php | 73 ++++ src/form/FormAPI/IForm.php | 73 ++++ src/form/FormAPI/SimpleForm.php | 102 +++++ src/form/PMForm/BaseForm.php | 73 ++++ src/form/PMForm/CustomForm.php | 139 +++++++ src/form/PMForm/CustomFormResponse.php | 77 ++++ src/form/PMForm/FormIcon.php | 62 +++ src/form/PMForm/MenuForm.php | 106 +++++ src/form/PMForm/MenuOption.php | 65 ++++ src/form/PMForm/ModalForm.php | 96 +++++ src/form/PMForm/ServerSettingsForm.php | 59 +++ src/form/PMForm/element/BaseSelector.php | 82 ++++ src/form/PMForm/element/CustomFormElement.php | 87 +++++ src/form/PMForm/element/Dropdown.php | 38 ++ src/form/PMForm/element/Input.php | 77 ++++ src/form/PMForm/element/Label.php | 44 +++ src/form/PMForm/element/Slider.php | 103 +++++ src/form/PMForm/element/StepSlider.php | 38 ++ src/form/PMForm/element/Toggle.php | 61 +++ src/scheduler/TaskScheduler.php | 368 +++++++++--------- 22 files changed, 1816 insertions(+), 182 deletions(-) create mode 100644 src/form/FormAPI/CustomForm.php create mode 100644 src/form/FormAPI/Form.php create mode 100644 src/form/FormAPI/IForm.php create mode 100644 src/form/FormAPI/SimpleForm.php create mode 100644 src/form/PMForm/BaseForm.php create mode 100644 src/form/PMForm/CustomForm.php create mode 100644 src/form/PMForm/CustomFormResponse.php create mode 100644 src/form/PMForm/FormIcon.php create mode 100644 src/form/PMForm/MenuForm.php create mode 100644 src/form/PMForm/MenuOption.php create mode 100644 src/form/PMForm/ModalForm.php create mode 100644 src/form/PMForm/ServerSettingsForm.php create mode 100644 src/form/PMForm/element/BaseSelector.php create mode 100644 src/form/PMForm/element/CustomFormElement.php create mode 100644 src/form/PMForm/element/Dropdown.php create mode 100644 src/form/PMForm/element/Input.php create mode 100644 src/form/PMForm/element/Label.php create mode 100644 src/form/PMForm/element/Slider.php create mode 100644 src/form/PMForm/element/StepSlider.php create mode 100644 src/form/PMForm/element/Toggle.php diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 6150246563b..ebc4e8366c0 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -30,7 +30,7 @@ use function str_repeat; final class VersionInfo{ - public const NAME = "PocketMine-MP"; + public const NAME = "STCraft-MP"; public const BASE_VERSION = "5.28.3"; public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; diff --git a/src/form/FormAPI/CustomForm.php b/src/form/FormAPI/CustomForm.php new file mode 100644 index 00000000000..2e18e3baaa7 --- /dev/null +++ b/src/form/FormAPI/CustomForm.php @@ -0,0 +1,173 @@ + */ + private array $labelMap = []; + + /** @var array */ + private array $validationMethods = []; + + /** + * @param callable(?array): void|null $callable + */ + public function __construct(?callable $callable) { + parent::__construct($callable); + $this->data["type"] = "custom_form"; + $this->data["title"] = ""; + $this->data["content"] = []; + } + + public function processData(&$data) : void { + if ($data !== null && !is_array($data)) { + throw new FormValidationException("Expected an array response, got " . gettype($data)); + } + if (is_array($data)) { + if (count($data) !== count($this->validationMethods)) { + throw new FormValidationException("Expected an array response with the size " . count($this->validationMethods) . ", got " . count($data)); + } + $new = []; + foreach ($data as $i => $v) { + $validationMethod = $this->validationMethods[$i] ?? null; + if ($validationMethod === null) { + throw new FormValidationException("Invalid element " . $i); + } + if (!$validationMethod($v)) { + throw new FormValidationException("Invalid type given for element " . $this->labelMap[$i]); + } + $new[$this->labelMap[$i]] = $v; + } + $data = $new; + } + } + + /** + * @param string $title + * @return $this + */ + public function setTitle(string $title) : self { + $this->data["title"] = $title; + return $this; + } + + /** + * @return string + */ + public function getTitle() : string { + return $this->data["title"]; + } + + /** + * @param string $text + * @param string|null $label + * @return $this + */ + public function addLabel(string $text, ?string $label = null) : self { + $this->addContent(["type" => "label", "text" => $text]); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => $v === null; + return $this; + } + + /** + * @param string $text + * @param bool|null $default + * @param string|null $label + * @return $this + */ + public function addToggle(string $text, ?bool $default = null, ?string $label = null) : self { + $content = ["type" => "toggle", "text" => $text]; + if ($default !== null) { + $content["default"] = $default; + } + $this->addContent($content); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => is_bool($v); + return $this; + } + + /** + * @param string $text + * @param int $min + * @param int $max + * @param int $step + * @param int $default + * @param string|null $label + * @return $this + */ + public function addSlider(string $text, int $min, int $max, int $step = -1, int $default = -1, ?string $label = null) : self { + $content = ["type" => "slider", "text" => $text, "min" => $min, "max" => $max]; + if ($step !== -1) { + $content["step"] = $step; + } + if ($default !== -1) { + $content["default"] = $default; + } + $this->addContent($content); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => (is_float($v) || is_int($v)) && $v >= $min && $v <= $max; + return $this; + } + + /** + * @param string $text + * @param array $steps + * @param int $defaultIndex + * @param string|null $label + * @return $this + */ + public function addStepSlider(string $text, array $steps, int $defaultIndex = -1, ?string $label = null) : self { + $content = ["type" => "step_slider", "text" => $text, "steps" => $steps]; + if ($defaultIndex !== -1) { + $content["default"] = $defaultIndex; + } + $this->addContent($content); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => is_int($v) && isset($steps[$v]); + return $this; + } + + /** + * @param string $text + * @param array $options + * @param int|null $default + * @param string|null $label + * @return $this + */ + public function addDropdown(string $text, array $options, ?int $default = null, ?string $label = null) : self { + $this->addContent(["type" => "dropdown", "text" => $text, "options" => $options, "default" => $default]); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => is_int($v) && isset($options[$v]); + return $this; + } + + /** + * @param string $text + * @param string $placeholder + * @param string|null $default + * @param string|null $label + * @return $this + */ + public function addInput(string $text, string $placeholder = "", ?string $default = null, ?string $label = null) : self { + $this->addContent(["type" => "input", "text" => $text, "placeholder" => $placeholder, "default" => $default]); + $this->labelMap[] = $label ?? count($this->labelMap); + $this->validationMethods[] = static fn($v) => is_string($v); + return $this; + } + + /** + * @param array $content + * @return $this + */ + private function addContent(array $content) : self { + $this->data["content"][] = $content; + return $this; + } + +} \ No newline at end of file diff --git a/src/form/FormAPI/Form.php b/src/form/FormAPI/Form.php new file mode 100644 index 00000000000..e91c07390bc --- /dev/null +++ b/src/form/FormAPI/Form.php @@ -0,0 +1,73 @@ + + */ + public function jsonSerialize() : array; +} \ No newline at end of file diff --git a/src/form/FormAPI/IForm.php b/src/form/FormAPI/IForm.php new file mode 100644 index 00000000000..6c9f1afb146 --- /dev/null +++ b/src/form/FormAPI/IForm.php @@ -0,0 +1,73 @@ + */ + protected array $data = []; + /** @var callable(Player, mixed): void|null */ + private mixed $callable; + + /** + * @param callable(Player, mixed): void|null $callable + */ + public function __construct(?callable $callable) { + $this->callable = $callable; + } + + /** + * @param Player $player + * @throws InvalidArgumentException + * @deprecated + * @see Player::sendForm() + */ + public function sendToPlayer(Player $player) : void { + $player->sendForm($this); + } + + /** + * @return callable(Player, mixed): void|null + */ + public function getCallable() : ?callable { + return $this->callable; + } + + /** + * @param callable(Player, mixed): void|null $callable + */ + public function setCallable(?callable $callable): void { + $this->callable = $callable; + } + + /** + * @param Player $player + * @param mixed $data + */ + public function handleResponse(Player $player, $data) : void { + $this->processData($data); + $callable = $this->getCallable(); + if ($callable !== null) { + $callable($player, $data); + } + } + + /** + * @param mixed $data + */ + public function processData(&$data) : void { + // Process the data as needed. + } + + /** + * @return array + */ + public function jsonSerialize() : array { + return $this->data; + } +} \ No newline at end of file diff --git a/src/form/FormAPI/SimpleForm.php b/src/form/FormAPI/SimpleForm.php new file mode 100644 index 00000000000..bc45fc66dcb --- /dev/null +++ b/src/form/FormAPI/SimpleForm.php @@ -0,0 +1,102 @@ +, label: string}> */ + private array $buttons = []; + + /** + * @param callable(Player, mixed): void|null $callable + */ + public function __construct(?callable $callable) { + parent::__construct($callable); + $this->data = [ + "type" => "form", + "title" => "", + "content" => $this->content, + "buttons" => [] + ]; + } + + public function processData(&$data) : void { + if ($data !== null) { + if (!is_int($data)) { + throw new FormValidationException("Expected an integer response, got " . gettype($data)); + } + if (!isset($this->buttons[$data])) { + throw new FormValidationException("Button $data does not exist"); + } + $data = $this->buttons[$data]['label'] ?? null; + } + } + + /** + * @param string $title + * @return $this + */ + public function setTitle(string $title) : self { + $this->data["title"] = $title; + return $this; + } + + /** + * @return string + */ + public function getTitle() : string { + return $this->data["title"]; + } + + /** + * @return string + */ + public function getContent() : string { + return $this->data["content"]; + } + + /** + * @param string $content + * @return $this + */ + public function setContent(string $content) : self { + $this->data["content"] = $content; + return $this; + } + + /** + * Add a button to the form. + * + * @param string $text + * @param int $imageType + * @param string $imagePath + * @param string|null $label + * @return $this + */ + public function addButton(string $text, int $imageType = self::IMAGE_TYPE_NONE, string $imagePath = "", ?string $label = null) : self { + $button = ["text" => $text]; + + if ($imageType !== self::IMAGE_TYPE_NONE) { + $button["image"] = [ + "type" => $imageType === self::IMAGE_TYPE_PATH ? "path" : "url", + "data" => $imagePath + ]; + } + + $this->buttons[] = ["button" => $button, "label" => $label ?? (string)count($this->buttons)]; + $this->data["buttons"] = array_column($this->buttons, 'button'); + + return $this; + } +} \ No newline at end of file diff --git a/src/form/PMForm/BaseForm.php b/src/form/PMForm/BaseForm.php new file mode 100644 index 00000000000..eb7d11ea2aa --- /dev/null +++ b/src/form/PMForm/BaseForm.php @@ -0,0 +1,73 @@ +title = $title; + } + + /** + * Returns the text shown on the form title-bar. + */ + public function getTitle() : string{ + return $this->title; + } + + /** + * Serializes the form to JSON for sending to clients. + * @return mixed[] + */ + final public function jsonSerialize() : array{ + $ret = $this->serializeFormData(); + $ret["type"] = $this->getType(); + $ret["title"] = $this->getTitle(); + + return $ret; + } + + /** + * Returns the type used to show this form to clients + */ + abstract protected function getType() : string; + + /** + * Serializes additional data needed to show this form to clients. + * @return mixed[] + */ + abstract protected function serializeFormData() : array; + +} \ No newline at end of file diff --git a/src/form/PMForm/CustomForm.php b/src/form/PMForm/CustomForm.php new file mode 100644 index 00000000000..db1834b873b --- /dev/null +++ b/src/form/PMForm/CustomForm.php @@ -0,0 +1,139 @@ +elements = array_values($elements); + foreach($this->elements as $element){ + if(isset($this->elementMap[$element->getName()])){ + throw new \InvalidArgumentException("Multiple elements cannot have the same name, found \"" . $element->getName() . "\" more than once"); + } + $this->elementMap[$element->getName()] = $element; + } + + Utils::validateCallableSignature(function(Player $player, CustomFormResponse $response) : void{}, $onSubmit); + $this->onSubmit = $onSubmit; + if($onClose !== null){ + Utils::validateCallableSignature(function(Player $player) : void{}, $onClose); + $this->onClose = $onClose; + } + } + + public function getElement(int $index) : ?CustomFormElement{ + return $this->elements[$index] ?? null; + } + + public function getElementByName(string $name) : ?CustomFormElement{ + return $this->elementMap[$name] ?? null; + } + + /** + * @return CustomFormElement[] + */ + public function getAllElements() : array{ + return $this->elements; + } + + final public function handleResponse(Player $player, $data) : void{ + if($data === null){ + if($this->onClose !== null){ + ($this->onClose)($player); + } + }elseif(is_array($data)){ + if(($actual = count($data)) !== ($expected = count($this->elements))){ + throw new FormValidationException("Expected $expected result data, got $actual"); + } + + $values = []; + + foreach($data as $index => $value){ + if(!isset($this->elements[$index])){ + throw new FormValidationException("Element at offset $index does not exist"); + } + $element = $this->elements[$index]; + try{ + $element->validateValue($value); + }catch(FormValidationException $e){ + throw new FormValidationException("Validation failed for element \"" . $element->getName() . "\": " . $e->getMessage(), 0, $e); + } + $values[$element->getName()] = $value; + } + + ($this->onSubmit)($player, new CustomFormResponse($values)); + }else{ + throw new FormValidationException("Expected array or null, got " . gettype($data)); + } + } + + protected function getType() : string{ + return "custom_form"; + } + + protected function serializeFormData() : array{ + return [ + "content" => $this->elements + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/CustomFormResponse.php b/src/form/PMForm/CustomFormResponse.php new file mode 100644 index 00000000000..50f0accc05a --- /dev/null +++ b/src/form/PMForm/CustomFormResponse.php @@ -0,0 +1,77 @@ + + */ +class CustomFormResponse{ + /** + * @var mixed[] + * @phpstan-var ResponseData + */ + private $data; + + /** + * @param mixed[] $data + * @phpstan-param ResponseData $data + */ + public function __construct(array $data){ + $this->data = $data; + } + + public function getInt(string $name) : int{ + $this->checkExists($name); + return $this->data[$name]; + } + + public function getString(string $name) : string{ + $this->checkExists($name); + return $this->data[$name]; + } + + public function getFloat(string $name) : float{ + $this->checkExists($name); + return $this->data[$name]; + } + + public function getBool(string $name) : bool{ + $this->checkExists($name); + return $this->data[$name]; + } + + /** + * @return mixed[] + * @phpstan-return ResponseData + */ + public function getAll() : array{ + return $this->data; + } + + private function checkExists(string $name) : void{ + if(!isset($this->data[$name])){ + throw new \InvalidArgumentException("Value \"$name\" not found"); + } + } +} \ No newline at end of file diff --git a/src/form/PMForm/FormIcon.php b/src/form/PMForm/FormIcon.php new file mode 100644 index 00000000000..8e413eaeab5 --- /dev/null +++ b/src/form/PMForm/FormIcon.php @@ -0,0 +1,62 @@ +type = $type; + $this->data = $data; + } + + public function getType() : string{ + return $this->type; + } + + public function getData() : string{ + return $this->data; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize(){ + return [ + "type" => $this->type, + "data" => $this->data + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/MenuForm.php b/src/form/PMForm/MenuForm.php new file mode 100644 index 00000000000..8273c11d357 --- /dev/null +++ b/src/form/PMForm/MenuForm.php @@ -0,0 +1,106 @@ +content = $text; + $this->options = array_values($options); + Utils::validateCallableSignature(function(Player $player, int $selectedOption) : void{}, $onSubmit); + $this->onSubmit = $onSubmit; + if($onClose !== null){ + Utils::validateCallableSignature(function(Player $player) : void{}, $onClose); + $this->onClose = $onClose; + } + } + + public function getOption(int $position) : ?MenuOption{ + return $this->options[$position] ?? null; + } + + final public function handleResponse(Player $player, $data) : void{ + if($data === null){ + if($this->onClose !== null){ + ($this->onClose)($player); + } + }elseif(is_int($data)){ + if(!isset($this->options[$data])){ + throw new FormValidationException("Option $data does not exist"); + } + ($this->onSubmit)($player, $data); + }else{ + throw new FormValidationException("Expected int or null, got " . gettype($data)); + } + } + + protected function getType() : string{ + return "form"; + } + + protected function serializeFormData() : array{ + return [ + "content" => $this->content, + "buttons" => $this->options //yes, this is intended (MCPE calls them buttons) + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/MenuOption.php b/src/form/PMForm/MenuOption.php new file mode 100644 index 00000000000..41b3ed04674 --- /dev/null +++ b/src/form/PMForm/MenuOption.php @@ -0,0 +1,65 @@ +text = $text; + $this->image = $image; + } + + public function getText() : string{ + return $this->text; + } + + public function hasImage() : bool{ + return $this->image !== null; + } + + public function getImage() : ?FormIcon{ + return $this->image; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize(){ + $json = [ + "text" => $this->text + ]; + + if($this->hasImage()){ + $json["image"] = $this->image; + } + + return $json; + } +} \ No newline at end of file diff --git a/src/form/PMForm/ModalForm.php b/src/form/PMForm/ModalForm.php new file mode 100644 index 00000000000..e30a709ee2c --- /dev/null +++ b/src/form/PMForm/ModalForm.php @@ -0,0 +1,96 @@ +content = $text; + Utils::validateCallableSignature(function(Player $player, bool $choice) : void{}, $onSubmit); + $this->onSubmit = $onSubmit; + $this->button1 = $yesButtonText; + $this->button2 = $noButtonText; + } + + public function getYesButtonText() : string{ + return $this->button1; + } + + public function getNoButtonText() : string{ + return $this->button2; + } + + final public function handleResponse(Player $player, $data) : void{ + if(!is_bool($data)){ + throw new FormValidationException("Expected bool, got " . gettype($data)); + } + + ($this->onSubmit)($player, $data); + } + + protected function getType() : string{ + return "modal"; + } + + protected function serializeFormData() : array{ + return [ + "content" => $this->content, + "button1" => $this->button1, + "button2" => $this->button2 + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/ServerSettingsForm.php b/src/form/PMForm/ServerSettingsForm.php new file mode 100644 index 00000000000..ce9660d88fa --- /dev/null +++ b/src/form/PMForm/ServerSettingsForm.php @@ -0,0 +1,59 @@ +icon = $icon; + } + + public function hasIcon() : bool{ + return $this->icon !== null; + } + + public function getIcon() : ?FormIcon{ + return $this->icon; + } + + protected function serializeFormData() : array{ + $data = parent::serializeFormData(); + + if($this->hasIcon()){ + $data["icon"] = $this->icon; + } + + return $data; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/BaseSelector.php b/src/form/PMForm/element/BaseSelector.php new file mode 100644 index 00000000000..81d86088539 --- /dev/null +++ b/src/form/PMForm/element/BaseSelector.php @@ -0,0 +1,82 @@ +options = array_values($options); + + if(!isset($this->options[$defaultOptionIndex])){ + throw new \InvalidArgumentException("No option at index $defaultOptionIndex, cannot set as default"); + } + $this->defaultOptionIndex = $defaultOptionIndex; + } + + public function validateValue($value) : void{ + if(!is_int($value)){ + throw new FormValidationException("Expected int, got " . gettype($value)); + } + if(!isset($this->options[$value])){ + throw new FormValidationException("Option $value does not exist"); + } + } + + /** + * Returns the text of the option at the specified index, or null if it doesn't exist. + */ + public function getOption(int $index) : ?string{ + return $this->options[$index] ?? null; + } + + public function getDefaultOptionIndex() : int{ + return $this->defaultOptionIndex; + } + + public function getDefaultOption() : string{ + return $this->options[$this->defaultOptionIndex]; + } + + /** + * @return string[] + */ + public function getOptions() : array{ + return $this->options; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/CustomFormElement.php b/src/form/PMForm/element/CustomFormElement.php new file mode 100644 index 00000000000..2b61ffbd34b --- /dev/null +++ b/src/form/PMForm/element/CustomFormElement.php @@ -0,0 +1,87 @@ +name = $name; + $this->text = $text; + } + + /** + * Returns the type of element. + */ + abstract public function getType() : string; + + /** + * Returns the element's name. This is used to identify the element in code. + */ + public function getName() : string{ + return $this->name; + } + + /** + * Returns the element's label. Usually this is used to explain to the user what a control does. + */ + public function getText() : string{ + return $this->text; + } + + /** + * Validates that the given value is of the correct type and fits the constraints for the component. This function + * should do appropriate type checking and throw whatever errors necessary if the value is not valid. + * + * @param mixed $value + * @throws FormValidationException + */ + abstract public function validateValue($value) : void; + + /** + * Returns an array of properties which can be serialized to JSON for sending. + * @return mixed[] + */ + final public function jsonSerialize() : array{ + $ret = $this->serializeElementData(); + $ret["type"] = $this->getType(); + $ret["text"] = $this->getText(); + + return $ret; + } + + /** + * Returns an array of extra data needed to serialize this element to JSON for showing to a player on a form. + * @return mixed[] + */ + abstract protected function serializeElementData() : array; +} \ No newline at end of file diff --git a/src/form/PMForm/element/Dropdown.php b/src/form/PMForm/element/Dropdown.php new file mode 100644 index 00000000000..844ec7bc3f0 --- /dev/null +++ b/src/form/PMForm/element/Dropdown.php @@ -0,0 +1,38 @@ + $this->options, + "default" => $this->defaultOptionIndex + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/Input.php b/src/form/PMForm/element/Input.php new file mode 100644 index 00000000000..2e73fd66538 --- /dev/null +++ b/src/form/PMForm/element/Input.php @@ -0,0 +1,77 @@ +hint = $hintText; + $this->default = $defaultText; + } + + public function getType() : string{ + return "input"; + } + + public function validateValue($value) : void{ + if(!is_string($value)){ + throw new FormValidationException("Expected string, got " . gettype($value)); + } + } + + /** + * Returns the text shown in the text-box when the box is not focused and there is no text in it. + */ + public function getHintText() : string{ + return $this->hint; + } + + /** + * Returns the text which will be in the text-box by default. + */ + public function getDefaultText() : string{ + return $this->default; + } + + protected function serializeElementData() : array{ + return [ + "placeholder" => $this->hint, + "default" => $this->default + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/Label.php b/src/form/PMForm/element/Label.php new file mode 100644 index 00000000000..55416c3f1a5 --- /dev/null +++ b/src/form/PMForm/element/Label.php @@ -0,0 +1,44 @@ +min > $this->max){ + throw new \InvalidArgumentException("Slider min value should be less than max value"); + } + $this->min = $min; + $this->max = $max; + + if($default !== null){ + if($default > $this->max or $default < $this->min){ + throw new \InvalidArgumentException("Default must be in range $this->min ... $this->max"); + } + $this->default = $default; + }else{ + $this->default = $this->min; + } + + if($step <= 0){ + throw new \InvalidArgumentException("Step must be greater than zero"); + } + $this->step = $step; + } + + public function getType() : string{ + return "slider"; + } + + public function validateValue($value) : void{ + if(!is_float($value) and !is_int($value)){ + throw new FormValidationException("Expected float, got " . gettype($value)); + } + if($value < $this->min or $value > $this->max){ + throw new FormValidationException("Value $value is out of bounds (min $this->min, max $this->max)"); + } + } + + public function getMin() : float{ + return $this->min; + } + + public function getMax() : float{ + return $this->max; + } + + public function getStep() : float{ + return $this->step; + } + + public function getDefault() : float{ + return $this->default; + } + + protected function serializeElementData() : array{ + return [ + "min" => $this->min, + "max" => $this->max, + "default" => $this->default, + "step" => $this->step + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/StepSlider.php b/src/form/PMForm/element/StepSlider.php new file mode 100644 index 00000000000..b6efdba8e8f --- /dev/null +++ b/src/form/PMForm/element/StepSlider.php @@ -0,0 +1,38 @@ + $this->options, + "default" => $this->defaultOptionIndex + ]; + } +} \ No newline at end of file diff --git a/src/form/PMForm/element/Toggle.php b/src/form/PMForm/element/Toggle.php new file mode 100644 index 00000000000..ee3e72594d4 --- /dev/null +++ b/src/form/PMForm/element/Toggle.php @@ -0,0 +1,61 @@ +default = $defaultValue; + } + + public function getType() : string{ + return "toggle"; + } + + public function getDefaultValue() : bool{ + return $this->default; + } + + public function validateValue($value) : void{ + if(!is_bool($value)){ + throw new FormValidationException("Expected bool, got " . gettype($value)); + } + } + + protected function serializeElementData() : array{ + return [ + "default" => $this->default + ]; + } +} \ No newline at end of file diff --git a/src/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php index 3e2b089c6c5..7cc932cd3b7 100644 --- a/src/scheduler/TaskScheduler.php +++ b/src/scheduler/TaskScheduler.php @@ -1,189 +1,195 @@ > */ - protected ReversePriorityQueue $queue; - - /** - * @var ObjectSet|TaskHandler[] - * @phpstan-var ObjectSet> - */ - protected ObjectSet $tasks; - - protected int $currentTick = 0; - - public function __construct( - private ?string $owner = null - ){ - $this->queue = new ReversePriorityQueue(); - $this->tasks = new ObjectSet(); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TTask $task - * - * @phpstan-return TaskHandler - */ - public function scheduleTask(Task $task) : TaskHandler{ - return $this->addTask($task, -1, -1); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TTask $task - * - * @phpstan-return TaskHandler - */ - public function scheduleDelayedTask(Task $task, int $delay) : TaskHandler{ - return $this->addTask($task, $delay, -1); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TTask $task - * - * @phpstan-return TaskHandler - */ - public function scheduleRepeatingTask(Task $task, int $period) : TaskHandler{ - return $this->addTask($task, -1, $period); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TTask $task - * - * @phpstan-return TaskHandler - */ - public function scheduleDelayedRepeatingTask(Task $task, int $delay, int $period) : TaskHandler{ - return $this->addTask($task, $delay, $period); - } - - public function cancelAllTasks() : void{ - foreach($this->tasks as $id => $task){ - $task->cancel(); - } - $this->tasks->clear(); - while(!$this->queue->isEmpty()){ - $this->queue->extract(); - } - } - - /** - * @phpstan-param TaskHandler $task - */ - public function isQueued(TaskHandler $task) : bool{ - return $this->tasks->contains($task); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TTask $task - * - * @phpstan-return TaskHandler - */ - private function addTask(Task $task, int $delay, int $period) : TaskHandler{ - if(!$this->enabled){ - throw new \LogicException("Tried to schedule task to disabled scheduler"); - } - - if($delay <= 0){ - $delay = -1; - } - - if($period <= -1){ - $period = -1; - }elseif($period < 1){ - $period = 1; - } - - return $this->handle(new TaskHandler($task, $delay, $period, $this->owner)); - } - - /** - * @phpstan-template TTask of Task - * @phpstan-param TaskHandler $handler - * @phpstan-return TaskHandler - */ - private function handle(TaskHandler $handler) : TaskHandler{ - if($handler->isDelayed()){ - $nextRun = $this->currentTick + $handler->getDelay(); - }else{ - $nextRun = $this->currentTick; - } - - $handler->setNextRun($nextRun); - $this->tasks->add($handler); - $this->queue->insert($handler, $nextRun); - - return $handler; - } - - public function shutdown() : void{ - $this->enabled = false; - $this->cancelAllTasks(); - } - - public function setEnabled(bool $enabled) : void{ - $this->enabled = $enabled; - } - - public function mainThreadHeartbeat(int $currentTick) : void{ - if(!$this->enabled){ - throw new \LogicException("Cannot run heartbeat on a disabled scheduler"); - } - $this->currentTick = $currentTick; - while($this->isReady($this->currentTick)){ - /** @phpstan-var TaskHandler $task */ - $task = $this->queue->extract(); - if($task->isCancelled()){ - $this->tasks->remove($task); - continue; - } - $task->run(); - if(!$task->isCancelled() && $task->isRepeating()){ - $task->setNextRun($this->currentTick + $task->getPeriod()); - $this->queue->insert($task, $this->currentTick + $task->getPeriod()); - }else{ - $task->remove(); - $this->tasks->remove($task); - } - } - } - - private function isReady(int $currentTick) : bool{ - return !$this->queue->isEmpty() && $this->queue->current()->getNextRun() <= $currentTick; - } -} +use pocketmine\player\Player; +use pocketmine\utils\TextFormat; +use pocketmine\utils\UUID; + +class TaskScheduler { + private bool $enabled = true; + + /** @phpstan-var ReversePriorityQueue */ + protected ReversePriorityQueue $queue; + + /** + * @var ObjectSet|TaskHandler[] + * @phpstan-var ObjectSet + */ + protected ObjectSet $tasks; + + protected int $currentTick = 0; + + /** @var array */ + private array $playerTasks = []; + + public function __construct(private ?string $owner = null) { + $this->queue = new ReversePriorityQueue(); + $this->tasks = new ObjectSet(); + } + + /** + * Schedule a new task. + */ + public function scheduleTask(Task $task): TaskHandler { + return $this->addTask($task, -1, -1); + } + + /** + * Schedule a task with a delay. + */ + public function scheduleDelayedTask(Task $task, int $delay): TaskHandler { + return $this->addTask($task, $delay, -1); + } + + /** + * Schedule a repeating task. + */ + public function scheduleRepeatingTask(Task $task, int $period): TaskHandler { + return $this->addTask($task, -1, $period); + } + + /** + * Schedule a task with a delay and a repeating period. + */ + public function scheduleDelayedRepeatingTask(Task $task, int $delay, int $period): TaskHandler { + return $this->addTask($task, $delay, $period); + } + + /** + * Cancel all scheduled tasks. + */ + public function cancelAllTasks(): void { + foreach ($this->tasks as $task) { + $task->cancel(); + } + $this->tasks->clear(); + $this->queue->clear(); + } + + /** + * Check if a task is in the queue. + */ + public function isQueued(TaskHandler $task): bool { + return $this->tasks->contains($task); + } + + /** + * Check if a task is scheduled for a specific player. + */ + public function isScheduledFor(Player $player): bool { + return isset($this->playerTasks[$player->getUniqueId()->toString()]); + } + + /** + * Schedule a task for a player and store the reference. + */ + public function scheduleForPlayer(Player $player, Task $task, int $ticks = 20): TaskHandler { + $uniqueId = $player->getUniqueId()->toString(); + + if ($this->isScheduledFor($player)) { + return $this->playerTasks[$uniqueId]; + } + + $taskHandler = $this->scheduleRepeatingTask($task, $ticks); + $this->playerTasks[$uniqueId] = $taskHandler; + + return $taskHandler; + } + + /** + * Cancel all tasks associated with a specific player. + */ + public function cancelForPlayer(Player $player): void { + $uniqueId = $player->getUniqueId()->toString(); + + if (isset($this->playerTasks[$uniqueId])) { + $this->playerTasks[$uniqueId]->cancel(); + unset($this->playerTasks[$uniqueId]); + } + } + + /** + * Add a task to the scheduler. + */ + private function addTask(Task $task, int $delay, int $period): TaskHandler { + if (!$this->enabled) { + throw new \LogicException("Tried to schedule task to disabled scheduler"); + } + + $delay = max($delay, -1); + $period = max($period, -1); + + return $this->handle(new TaskHandler($task, $delay, $period, $this->owner)); + } + + /** + * Handle the scheduling of a task. + */ + private function handle(TaskHandler $handler): TaskHandler { + $nextRun = $this->currentTick + max($handler->getDelay(), 0); + $handler->setNextRun($nextRun); + $this->tasks->add($handler); + $this->queue->insert($handler, $nextRun); + + return $handler; + } + + /** + * Shutdown the scheduler and cancel all tasks. + */ + public function shutdown(): void { + $this->enabled = false; + $this->cancelAllTasks(); + } + + /** + * Enable or disable the scheduler. + */ + public function setEnabled(bool $enabled): void { + $this->enabled = $enabled; + } + + /** + * Process tasks in the main thread heartbeat. + */ + public function mainThreadHeartbeat(int $currentTick): void { + if (!$this->enabled) { + throw new \LogicException("Cannot run heartbeat on a disabled scheduler"); + } + + $this->currentTick = $currentTick; + + while ($this->isReady($this->currentTick)) { + /** @var TaskHandler $task */ + $task = $this->queue->extract(); + + if ($task->isCancelled()) { + $this->tasks->remove($task); + continue; + } + + $task->run(); + + if (!$task->isCancelled() && $task->isRepeating()) { + $nextRun = $this->currentTick + $task->getPeriod(); + $task->setNextRun($nextRun); + $this->queue->insert($task, $nextRun); + } else { + $task->remove(); + $this->tasks->remove($task); + } + } + } + + /** + * Check if there are tasks ready to be executed. + */ + private function isReady(int $currentTick): bool { + return !$this->queue->isEmpty() && $this->queue->current()->getNextRun() <= $currentTick; + } +} \ No newline at end of file From 5d7e88a6ec0a6c35f0203357261064d87d59ee5d Mon Sep 17 00:00:00 2001 From: KanadeBlue Date: Fri, 6 Jun 2025 17:30:00 +0100 Subject: [PATCH 35/43] yes --- src/network/mcpe/handler/LoginPacketHandler.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/network/mcpe/handler/LoginPacketHandler.php b/src/network/mcpe/handler/LoginPacketHandler.php index c15753dad35..74e5c83fb3c 100644 --- a/src/network/mcpe/handler/LoginPacketHandler.php +++ b/src/network/mcpe/handler/LoginPacketHandler.php @@ -193,7 +193,12 @@ protected function parseClientData(string $clientDataJwt) : ClientData{ }catch(JwtException $e){ throw PacketHandlingException::wrap($e); } - + + // Ensure GraphicsMode exists + if (!isset($clientDataClaims["GraphicsMode"])) { + $clientDataClaims["GraphicsMode"] = 1; + } + $mapper = new \JsonMapper(); $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models $mapper->bExceptionOnMissingData = true; @@ -205,7 +210,7 @@ protected function parseClientData(string $clientDataJwt) : ClientData{ throw PacketHandlingException::wrap($e); } return $clientData; - } + } /** * TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication. From 67a9164aebd44ba7a4c164a953f22e80c98ff537 Mon Sep 17 00:00:00 2001 From: KanadeBlue Date: Fri, 6 Jun 2025 17:30:03 +0100 Subject: [PATCH 36/43] fff --- src/VersionInfo.php | 4 +- src/form/ChestAPI/InvMenu.php | 200 +++++++++++++++ src/form/ChestAPI/InvMenuEventHandler.php | 97 ++++++++ src/form/ChestAPI/InvMenuHandler.php | 46 ++++ .../ChestAPI/inventory/InvMenuInventory.php | 23 ++ .../inventory/SharedInvMenuSynchronizer.php | 32 +++ .../inventory/SharedInventoryNotifier.php | 31 +++ .../inventory/SharedInventorySynchronizer.php | 28 +++ src/form/ChestAPI/session/InvMenuInfo.php | 17 ++ src/form/ChestAPI/session/PlayerManager.php | 64 +++++ src/form/ChestAPI/session/PlayerSession.php | 102 ++++++++ .../network/NetworkStackLatencyEntry.php | 21 ++ .../session/network/PlayerNetwork.php | 230 ++++++++++++++++++ .../handler/ClosurePlayerNetworkHandler.php | 22 ++ .../network/handler/PlayerNetworkHandler.php | 13 + .../handler/PlayerNetworkHandlerRegistry.php | 41 ++++ .../DeterministicInvMenuTransaction.php | 60 +++++ .../transaction/InvMenuTransaction.php | 43 ++++ .../transaction/InvMenuTransactionResult.php | 48 ++++ .../transaction/SimpleInvMenuTransaction.php | 57 +++++ .../ChestAPI/type/ActorFixedInvMenuType.php | 44 ++++ .../type/BlockActorFixedInvMenuType.php | 54 ++++ .../ChestAPI/type/BlockFixedInvMenuType.php | 41 ++++ ...ublePairableBlockActorFixedInvMenuType.php | 70 ++++++ src/form/ChestAPI/type/FixedInvMenuType.php | 18 ++ src/form/ChestAPI/type/InvMenuType.php | 17 ++ src/form/ChestAPI/type/InvMenuTypeIds.php | 12 + .../ChestAPI/type/InvMenuTypeRegistry.php | 72 ++++++ .../type/graphic/ActorInvMenuGraphic.php | 71 ++++++ .../type/graphic/BlockActorInvMenuGraphic.php | 70 ++++++ .../type/graphic/BlockInvMenuGraphic.php | 51 ++++ .../ChestAPI/type/graphic/InvMenuGraphic.php | 28 +++ .../type/graphic/MultiBlockInvMenuGraphic.php | 65 +++++ .../type/graphic/PositionedInvMenuGraphic.php | 12 + .../ActorInvMenuGraphicNetworkTranslator.php | 22 ++ .../BlockInvMenuGraphicNetworkTranslator.php | 33 +++ .../InvMenuGraphicNetworkTranslator.php | 14 ++ .../MultiInvMenuGraphicNetworkTranslator.php | 25 ++ ...dowTypeInvMenuGraphicNetworkTranslator.php | 20 ++ .../type/util/InvMenuTypeBuilders.php | 29 +++ .../ChestAPI/type/util/InvMenuTypeHelper.php | 52 ++++ .../builder/ActorFixedInvMenuTypeBuilder.php | 38 +++ .../builder/ActorInvMenuTypeBuilderTrait.php | 45 ++++ ...imationDurationInvMenuTypeBuilderTrait.php | 19 ++ .../BlockActorFixedInvMenuTypeBuilder.php | 35 +++ .../builder/BlockFixedInvMenuTypeBuilder.php | 22 ++ .../builder/BlockInvMenuTypeBuilderTrait.php | 22 ++ ...rableBlockActorFixedInvMenuTypeBuilder.php | 35 +++ .../builder/FixedInvMenuTypeBuilderTrait.php | 21 ++ ...orkTranslatableInvMenuTypeBuilderTrait.php | 37 +++ .../type/util/builder/InvMenuTypeBuilder.php | 12 + 51 files changed, 2283 insertions(+), 2 deletions(-) create mode 100644 src/form/ChestAPI/InvMenu.php create mode 100644 src/form/ChestAPI/InvMenuEventHandler.php create mode 100644 src/form/ChestAPI/InvMenuHandler.php create mode 100644 src/form/ChestAPI/inventory/InvMenuInventory.php create mode 100644 src/form/ChestAPI/inventory/SharedInvMenuSynchronizer.php create mode 100644 src/form/ChestAPI/inventory/SharedInventoryNotifier.php create mode 100644 src/form/ChestAPI/inventory/SharedInventorySynchronizer.php create mode 100644 src/form/ChestAPI/session/InvMenuInfo.php create mode 100644 src/form/ChestAPI/session/PlayerManager.php create mode 100644 src/form/ChestAPI/session/PlayerSession.php create mode 100644 src/form/ChestAPI/session/network/NetworkStackLatencyEntry.php create mode 100644 src/form/ChestAPI/session/network/PlayerNetwork.php create mode 100644 src/form/ChestAPI/session/network/handler/ClosurePlayerNetworkHandler.php create mode 100644 src/form/ChestAPI/session/network/handler/PlayerNetworkHandler.php create mode 100644 src/form/ChestAPI/session/network/handler/PlayerNetworkHandlerRegistry.php create mode 100644 src/form/ChestAPI/transaction/DeterministicInvMenuTransaction.php create mode 100644 src/form/ChestAPI/transaction/InvMenuTransaction.php create mode 100644 src/form/ChestAPI/transaction/InvMenuTransactionResult.php create mode 100644 src/form/ChestAPI/transaction/SimpleInvMenuTransaction.php create mode 100644 src/form/ChestAPI/type/ActorFixedInvMenuType.php create mode 100644 src/form/ChestAPI/type/BlockActorFixedInvMenuType.php create mode 100644 src/form/ChestAPI/type/BlockFixedInvMenuType.php create mode 100644 src/form/ChestAPI/type/DoublePairableBlockActorFixedInvMenuType.php create mode 100644 src/form/ChestAPI/type/FixedInvMenuType.php create mode 100644 src/form/ChestAPI/type/InvMenuType.php create mode 100644 src/form/ChestAPI/type/InvMenuTypeIds.php create mode 100644 src/form/ChestAPI/type/InvMenuTypeRegistry.php create mode 100644 src/form/ChestAPI/type/graphic/ActorInvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/BlockActorInvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/BlockInvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/InvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/MultiBlockInvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/PositionedInvMenuGraphic.php create mode 100644 src/form/ChestAPI/type/graphic/network/ActorInvMenuGraphicNetworkTranslator.php create mode 100644 src/form/ChestAPI/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php create mode 100644 src/form/ChestAPI/type/graphic/network/InvMenuGraphicNetworkTranslator.php create mode 100644 src/form/ChestAPI/type/graphic/network/MultiInvMenuGraphicNetworkTranslator.php create mode 100644 src/form/ChestAPI/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php create mode 100644 src/form/ChestAPI/type/util/InvMenuTypeBuilders.php create mode 100644 src/form/ChestAPI/type/util/InvMenuTypeHelper.php create mode 100644 src/form/ChestAPI/type/util/builder/ActorFixedInvMenuTypeBuilder.php create mode 100644 src/form/ChestAPI/type/util/builder/ActorInvMenuTypeBuilderTrait.php create mode 100644 src/form/ChestAPI/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php create mode 100644 src/form/ChestAPI/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php create mode 100644 src/form/ChestAPI/type/util/builder/BlockFixedInvMenuTypeBuilder.php create mode 100644 src/form/ChestAPI/type/util/builder/BlockInvMenuTypeBuilderTrait.php create mode 100644 src/form/ChestAPI/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php create mode 100644 src/form/ChestAPI/type/util/builder/FixedInvMenuTypeBuilderTrait.php create mode 100644 src/form/ChestAPI/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php create mode 100644 src/form/ChestAPI/type/util/builder/InvMenuTypeBuilder.php diff --git a/src/VersionInfo.php b/src/VersionInfo.php index ebc4e8366c0..4e35b71bd99 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "STCraft-MP"; - public const BASE_VERSION = "5.28.3"; - public const IS_DEVELOPMENT_BUILD = true; + public const BASE_VERSION = "5.28.1"; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** diff --git a/src/form/ChestAPI/InvMenu.php b/src/form/ChestAPI/InvMenu.php new file mode 100644 index 00000000000..afc42402d14 --- /dev/null +++ b/src/form/ChestAPI/InvMenu.php @@ -0,0 +1,200 @@ +get($identifier), ...$args); + } + + /** + * @param (Closure(DeterministicInvMenuTransaction) : void)|null $listener + * @return Closure(InvMenuTransaction) : InvMenuTransactionResult + */ + public static function readonly(?Closure $listener = null) : Closure{ + return static function(InvMenuTransaction $transaction) use($listener) : InvMenuTransactionResult{ + $result = $transaction->discard(); + if($listener !== null){ + $listener(new DeterministicInvMenuTransaction($transaction, $result)); + } + return $result; + }; + } + + readonly public InvMenuType $type; + protected ?string $name = null; + protected ?Closure $listener = null; + protected ?Closure $inventory_close_listener = null; + protected Inventory $inventory; + protected ?SharedInvMenuSynchronizer $synchronizer = null; + + public function __construct(InvMenuType $type, ?Inventory $custom_inventory = null){ + if(!InvMenuHandler::isRegistered()){ + throw new LogicException("Tried creating menu before calling " . InvMenuHandler::class . "::register()"); + } + $this->type = $type; + $this->inventory = $this->type->createInventory(); + $this->setInventory($custom_inventory); + } + + public function __destruct(){ + $this->setInventory(null); + } + + /** + * @deprecated Access {@see InvMenu::$type} directly + * @return InvMenuType + */ + public function getType() : InvMenuType{ + return $this->type; + } + + public function getName() : ?string{ + return $this->name; + } + + public function setName(?string $name) : self{ + $this->name = $name; + return $this; + } + + /** + * @return (Closure(InvMenuTransaction) : InvMenuTransactionResult)|null + */ + public function getListener() : ?Closure{ + return $this->listener; + } + + /** + * @param (Closure(InvMenuTransaction) : InvMenuTransactionResult)|null $listener + * @return self + */ + public function setListener(?Closure $listener) : self{ + $this->listener = $listener; + return $this; + } + + /** + * @return (Closure(Player, Inventory) : void)|null + */ + public function getInventoryCloseListener() : ?Closure{ + return $this->inventory_close_listener; + } + + /** + * @param (Closure(Player, Inventory) : void)|null $listener + * @return self + */ + public function setInventoryCloseListener(?Closure $listener) : self{ + $this->inventory_close_listener = $listener; + return $this; + } + + public function getInventory() : Inventory{ + return $this->inventory; + } + + public function setInventory(?Inventory $custom_inventory) : void{ + if($this->synchronizer !== null){ + $this->synchronizer->destroy(); + $this->synchronizer = null; + } + + if($custom_inventory !== null){ + $this->synchronizer = new SharedInvMenuSynchronizer($this, $custom_inventory); + } + } + + /** + * @param Player $player + * @param string|null $name + * @param (Closure(bool) : void)|null $callback + */ + final public function send(Player $player, ?string $name = null, ?Closure $callback = null) : void{ + $player->removeCurrentWindow(); + + $session = InvMenuHandler::getPlayerManager()->get($player); + $network = $session->network; + + // Avoid players from spamming InvMenu::send() and other similar + // requests and filling up queued tasks in memory. + // It would be better if this check were implemented by plugins, + // however I suppose it is more convenient if done within InvMenu... + if($network->getPending() >= 8){ + $network->dropPending(); + }else{ + $network->dropPendingOfType(PlayerNetwork::DELAY_TYPE_OPERATION); + } + + $network->waitUntil(PlayerNetwork::DELAY_TYPE_OPERATION, 0, function(bool $success) use($player, $session, $name, $callback) : bool{ + if(!$success){ + if($callback !== null){ + $callback(false); + } + return false; + } + + $graphic = $this->type->createGraphic($this, $player); + if($graphic !== null){ + $session->setCurrentMenu(new InvMenuInfo($this, $graphic, $name), static function(bool $success) use($callback) : void{ + if($callback !== null){ + $callback($success); + } + }); + }else{ + if($callback !== null){ + $callback(false); + } + } + return false; + }); + } + + /** + * @internal use InvMenu::send() instead. + * + * @param Player $player + * @return bool + */ + public function sendInventory(Player $player) : bool{ + return $player->setCurrentWindow($this->getInventory()); + } + + public function handleInventoryTransaction(Player $player, Item $out, Item $in, SlotChangeAction $action, InventoryTransaction $transaction) : InvMenuTransactionResult{ + $inv_menu_txn = new SimpleInvMenuTransaction($player, $out, $in, $action, $transaction); + return $this->listener !== null ? ($this->listener)($inv_menu_txn) : $inv_menu_txn->continue(); + } + + public function onClose(Player $player) : void{ + if($this->inventory_close_listener !== null){ + ($this->inventory_close_listener)($player, $this->getInventory()); + } + + InvMenuHandler::getPlayerManager()->get($player)->removeCurrentMenu(); + } +} diff --git a/src/form/ChestAPI/InvMenuEventHandler.php b/src/form/ChestAPI/InvMenuEventHandler.php new file mode 100644 index 00000000000..405af788e1f --- /dev/null +++ b/src/form/ChestAPI/InvMenuEventHandler.php @@ -0,0 +1,97 @@ +getPacket(); + if($packet instanceof NetworkStackLatencyPacket){ + $player = $event->getOrigin()->getPlayer(); + if($player !== null){ + $this->player_manager->getNullable($player)?->network->notify($packet->timestamp); + } + } + } + + /** + * @param InventoryCloseEvent $event + * @priority MONITOR + */ + public function onInventoryClose(InventoryCloseEvent $event) : void{ + $player = $event->getPlayer(); + $session = $this->player_manager->getNullable($player); + if($session === null){ + return; + } + + $current = $session->getCurrent(); + if($current !== null && $event->getInventory() === $current->menu->getInventory()){ + $current->menu->onClose($player); + } + $session->network->waitUntil(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, 325, static fn(bool $success) : bool => false); + } + + /** + * @param InventoryTransactionEvent $event + * @priority NORMAL + */ + public function onInventoryTransaction(InventoryTransactionEvent $event) : void{ + $transaction = $event->getTransaction(); + $player = $transaction->getSource(); + + $player_instance = $this->player_manager->get($player); + $current = $player_instance->getCurrent(); + if($current === null){ + return; + } + + $inventory = $current->menu->getInventory(); + $network_stack_callbacks = []; + foreach($transaction->getActions() as $action){ + if(!($action instanceof SlotChangeAction) || $action->getInventory() !== $inventory){ + continue; + } + + $result = $current->menu->handleInventoryTransaction($player, $action->getSourceItem(), $action->getTargetItem(), $action, $transaction); + $network_stack_callback = $result->post_transaction_callback; + if($network_stack_callback !== null){ + $network_stack_callbacks[] = $network_stack_callback; + } + if($result->cancelled){ + $event->cancel(); + break; + } + } + + if(count($network_stack_callbacks) > 0){ + $player_instance->network->wait(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, static function(bool $success) use($player, $network_stack_callbacks) : bool{ + if($success){ + foreach($network_stack_callbacks as $callback){ + $callback($player); + } + } + return false; + }); + } + } +} diff --git a/src/form/ChestAPI/InvMenuHandler.php b/src/form/ChestAPI/InvMenuHandler.php new file mode 100644 index 00000000000..2baed4c8948 --- /dev/null +++ b/src/form/ChestAPI/InvMenuHandler.php @@ -0,0 +1,46 @@ +getName()} attempted to register " . self::class . " twice."); + } + + self::$registrant = $plugin; + self::$type_registry = new InvMenuTypeRegistry(); + self::$player_manager = new PlayerManager(self::getRegistrant()); + Server::getInstance()->getPluginManager()->registerEvents(new InvMenuEventHandler(self::getPlayerManager()), $plugin); + } + + public static function isRegistered() : bool{ + return self::$registrant instanceof Plugin; + } + + public static function getRegistrant() : Plugin{ + return self::$registrant ?? throw new LogicException("Cannot obtain registrant before registration"); + } + + public static function getTypeRegistry() : InvMenuTypeRegistry{ + return self::$type_registry; + } + + public static function getPlayerManager() : PlayerManager{ + return self::$player_manager; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/inventory/InvMenuInventory.php b/src/form/ChestAPI/inventory/InvMenuInventory.php new file mode 100644 index 00000000000..64663777db0 --- /dev/null +++ b/src/form/ChestAPI/inventory/InvMenuInventory.php @@ -0,0 +1,23 @@ +holder = new Position(0, 0, 0, null); + } + + public function getHolder() : Position{ + return $this->holder; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/inventory/SharedInvMenuSynchronizer.php b/src/form/ChestAPI/inventory/SharedInvMenuSynchronizer.php new file mode 100644 index 00000000000..86defa0062f --- /dev/null +++ b/src/form/ChestAPI/inventory/SharedInvMenuSynchronizer.php @@ -0,0 +1,32 @@ +inventory = $inventory; + + $menu_inventory = $menu->getInventory(); + $this->synchronizer = new SharedInventorySynchronizer($menu_inventory); + $inventory->getListeners()->add($this->synchronizer); + + $this->notifier = new SharedInventoryNotifier($this->inventory, $this->synchronizer); + $menu_inventory->setContents($inventory->getContents()); + $menu_inventory->getListeners()->add($this->notifier); + } + + public function destroy() : void{ + $this->synchronizer->getSynchronizingInventory()->getListeners()->remove($this->notifier); + $this->inventory->getListeners()->remove($this->synchronizer); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/inventory/SharedInventoryNotifier.php b/src/form/ChestAPI/inventory/SharedInventoryNotifier.php new file mode 100644 index 00000000000..0a54f4cde30 --- /dev/null +++ b/src/form/ChestAPI/inventory/SharedInventoryNotifier.php @@ -0,0 +1,31 @@ +inventory->getListeners()->remove($this->synchronizer); + $this->inventory->setContents($inventory->getContents()); + $this->inventory->getListeners()->add($this->synchronizer); + } + + public function onSlotChange(Inventory $inventory, int $slot, Item $old_item) : void{ + if($slot < $inventory->getSize()){ + $this->inventory->getListeners()->remove($this->synchronizer); + $this->inventory->setItem($slot, $inventory->getItem($slot)); + $this->inventory->getListeners()->add($this->synchronizer); + } + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/inventory/SharedInventorySynchronizer.php b/src/form/ChestAPI/inventory/SharedInventorySynchronizer.php new file mode 100644 index 00000000000..ccdc142f0a9 --- /dev/null +++ b/src/form/ChestAPI/inventory/SharedInventorySynchronizer.php @@ -0,0 +1,28 @@ +inventory; + } + + public function onContentChange(Inventory $inventory, array $old_contents) : void{ + $this->inventory->setContents($inventory->getContents()); + } + + public function onSlotChange(Inventory $inventory, int $slot, Item $old_item) : void{ + $this->inventory->setItem($slot, $inventory->getItem($slot)); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/session/InvMenuInfo.php b/src/form/ChestAPI/session/InvMenuInfo.php new file mode 100644 index 00000000000..dd139ee84e8 --- /dev/null +++ b/src/form/ChestAPI/session/InvMenuInfo.php @@ -0,0 +1,17 @@ +network_handler_registry = new PlayerNetworkHandlerRegistry(); + + $plugin_manager = Server::getInstance()->getPluginManager(); + $plugin_manager->registerEvent(PlayerLoginEvent::class, function(PlayerLoginEvent $event) : void{ + $this->create($event->getPlayer()); + }, EventPriority::MONITOR, $registrant); + $plugin_manager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{ + $this->destroy($event->getPlayer()); + }, EventPriority::MONITOR, $registrant); + } + + private function create(Player $player) : void{ + $this->sessions[$player->getId()] = new PlayerSession($player, new PlayerNetwork( + $player->getNetworkSession(), + $this->network_handler_registry->get($player->getPlayerInfo()->getExtraData()["DeviceOS"] ?? -1) + )); + } + + private function destroy(Player $player) : void{ + if(isset($this->sessions[$player_id = $player->getId()])){ + $this->sessions[$player_id]->finalize(); + unset($this->sessions[$player_id]); + } + } + + public function get(Player $player) : PlayerSession{ + return $this->sessions[$player->getId()]; + } + + public function getNullable(Player $player) : ?PlayerSession{ + return $this->sessions[$player->getId()] ?? null; + } + + /** + * @deprecated Access {@see PlayerManager::$network_handler_registry} directly + * @return PlayerNetworkHandlerRegistry + */ + public function getNetworkHandlerRegistry() : PlayerNetworkHandlerRegistry{ + return $this->network_handler_registry; + } +} diff --git a/src/form/ChestAPI/session/PlayerSession.php b/src/form/ChestAPI/session/PlayerSession.php new file mode 100644 index 00000000000..10b93798ac4 --- /dev/null +++ b/src/form/ChestAPI/session/PlayerSession.php @@ -0,0 +1,102 @@ +current !== null){ + $this->current->graphic->remove($this->player); + $this->player->removeCurrentWindow(); + } + $this->network->finalize(); + } + + public function getCurrent() : ?InvMenuInfo{ + return $this->current; + } + + /** + * @internal use InvMenu::send() instead. + * + * @param InvMenuInfo|null $current + * @param (Closure(bool) : void)|null $callback + */ + public function setCurrentMenu(?InvMenuInfo $current, ?Closure $callback = null) : void{ + if($this->current !== null){ + $this->current->graphic->remove($this->player); + } + + $this->current = $current; + + if($this->current !== null){ + $current_id = spl_object_id($this->current); + $this->current->graphic->send($this->player, $this->current->graphic_name); + $this->network->waitUntil(PlayerNetwork::DELAY_TYPE_OPERATION, $this->current->graphic->getAnimationDuration(), function(bool $success) use($callback, $current_id) : bool{ + $current = $this->current; + if($current !== null && spl_object_id($current) === $current_id){ + if($success){ + $this->network->onBeforeSendMenu($this, $current); + $result = $current->graphic->sendInventory($this->player, $current->menu->getInventory()); + if($result){ + if($callback !== null){ + $callback(true); + } + return false; + } + } + + $this->removeCurrentMenu(); + } + if($callback !== null){ + $callback(false); + } + return false; + }); + }else{ + $this->network->wait(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, static function(bool $success) use($callback) : bool{ + if($callback !== null){ + $callback($success); + } + return false; + }); + } + } + + /** + * @deprecated Access {@see PlayerSession::$network} directly + * @return PlayerNetwork + */ + public function getNetwork() : PlayerNetwork{ + return $this->network; + } + + /** + * @internal use Player::removeCurrentWindow() instead + * @return bool + */ + public function removeCurrentMenu() : bool{ + if($this->current !== null){ + $this->setCurrentMenu(null); + return true; + } + return false; + } +} diff --git a/src/form/ChestAPI/session/network/NetworkStackLatencyEntry.php b/src/form/ChestAPI/session/network/NetworkStackLatencyEntry.php new file mode 100644 index 00000000000..a5a5ee1dc45 --- /dev/null +++ b/src/form/ChestAPI/session/network/NetworkStackLatencyEntry.php @@ -0,0 +1,21 @@ +timestamp = $timestamp; + $this->then = $then; + $this->network_timestamp = $network_timestamp ?? $timestamp; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/session/network/PlayerNetwork.php b/src/form/ChestAPI/session/network/PlayerNetwork.php new file mode 100644 index 00000000000..9b967857946 --- /dev/null +++ b/src/form/ChestAPI/session/network/PlayerNetwork.php @@ -0,0 +1,230 @@ +|null) */ + private Closure $container_open_callback; + + private ?NetworkStackLatencyEntry $current = null; + private int $graphic_wait_duration = 200; + + /** @var SplQueue */ + private SplQueue $queue; + + /** @var array */ + private array $entry_types = []; + + public function __construct( + readonly private NetworkSession $network_session, + readonly private PlayerNetworkHandler $handler + ){ + $this->queue = new SplQueue(); + $this->nullifyContainerOpenCallback(); + } + + public function finalize() : void{ + $this->dropPending(); + $this->network_session->getInvManager()?->getContainerOpenCallbacks()->remove($this->container_open_callback); + $this->nullifyContainerOpenCallback(); + } + + public function getGraphicWaitDuration() : int{ + return $this->graphic_wait_duration; + } + + /** + * Duration (in milliseconds) to wait between sending the graphic (block) + * and sending the inventory. + * + * @param int $graphic_wait_duration + */ + public function setGraphicWaitDuration(int $graphic_wait_duration) : void{ + if($graphic_wait_duration < 0){ + throw new InvalidArgumentException("graphic_wait_duration must be >= 0, got {$graphic_wait_duration}"); + } + + $this->graphic_wait_duration = $graphic_wait_duration; + } + + public function getPending() : int{ + return $this->queue->count(); + } + + public function dropPending() : void{ + foreach($this->queue as $entry){ + ($entry->then)(false); + } + $this->queue = new SplQueue(); + $this->entry_types = []; + $this->setCurrent(null); + } + + /** + * @param self::DELAY_TYPE_* $type + */ + public function dropPendingOfType(int $type) : void{ + $previous = $this->queue; + $this->queue = new SplQueue(); + foreach($previous as $entry){ + if($this->entry_types[$id = spl_object_id($entry)] === $type){ + ($entry->then)(false); + unset($this->entry_types[$id]); + }else{ + $this->queue->enqueue($entry); + } + } + } + + /** + * @param self::DELAY_TYPE_* $type + * @param Closure(bool) : bool $then + */ + public function wait(int $type, Closure $then) : void{ + $entry = $this->handler->createNetworkStackLatencyEntry($then); + if($this->current !== null){ + $this->queue->enqueue($entry); + $this->entry_types[spl_object_id($entry)] = $type; + }else{ + $this->setCurrent($entry); + } + } + + /** + * Waits at least $wait_ms before calling $then(true). + * + * @param self::DELAY_TYPE_* $type + * @param int $wait_ms + * @param Closure(bool) : bool $then + */ + public function waitUntil(int $type, int $wait_ms, Closure $then) : void{ + if($wait_ms <= 0 && $this->queue->isEmpty()){ + $then(true); + return; + } + + $elapsed_ms = 0.0; + $this->wait($type, function(bool $success) use($wait_ms, $then, &$elapsed_ms) : bool{ + if($this->current === null){ + $then(false); + return false; + } + + $elapsed_ms += (microtime(true) * 1000) - $this->current->sent_at; + if(!$success || $elapsed_ms >= $wait_ms){ + $then($success); + return false; + } + + return true; + }); + } + + private function setCurrent(?NetworkStackLatencyEntry $entry) : void{ + if($this->current !== null){ + $this->processCurrent(false); + } + + $this->current = $entry; + if($entry !== null){ + unset($this->entry_types[spl_object_id($entry)]); + if($this->network_session->sendDataPacket(NetworkStackLatencyPacket::create($entry->network_timestamp, true))){ + $entry->sent_at = microtime(true) * 1000; + }else{ + $this->processCurrent(false); + } + } + } + + private function processCurrent(bool $success) : void{ + if($this->current !== null){ + $current = $this->current; + $repeat = ($current->then)($success); + $this->current = null; + if($repeat && $success){ + $this->setCurrent($current); + }elseif(!$this->queue->isEmpty()){ + $this->setCurrent($this->queue->dequeue()); + } + } + } + + public function notify(int $timestamp) : void{ + if($this->current !== null && $timestamp === $this->current->timestamp){ + $this->processCurrent(true); + } + } + + public function onBeforeSendMenu(PlayerSession $session, InvMenuInfo $info) : void{ + $translator = $info->graphic->getNetworkTranslator(); + if($translator === null){ + return; + } + + $callbacks = $this->network_session->getInvManager()?->getContainerOpenCallbacks(); + if($callbacks === null){ + return; + } + + $callbacks->remove($this->container_open_callback); + + // Take priority over other container open callbacks. + // PocketMine's default container open callback disallows any BlockInventory + // from having a custom callback + $previous = $callbacks->toArray(); + $callbacks->clear(); + $callbacks->add($this->container_open_callback = function(int $window_id, Inventory $inventory) use($info, $session, $translator, $previous, $callbacks) : ?array{ + $callbacks->remove($this->container_open_callback); + $this->nullifyContainerOpenCallback(); + if($inventory === $info->menu->getInventory()){ + $packets = null; + foreach($previous as $callback){ + $packets = $callback($window_id, $inventory); + if($packets !== null){ + break; + } + } + + $packets ??= [ContainerOpenPacket::blockInv( + $window_id, + WindowTypes::CONTAINER, + $inventory instanceof BlockInventory ? BlockPosition::fromVector3($inventory->getHolder()) : new BlockPosition(0, 0, 0) + )]; + + foreach($packets as $packet){ + if($packet instanceof ContainerOpenPacket){ + $translator->translate($session, $info, $packet); + } + } + return $packets; + } + return null; + }, ...$previous); + } + + private function nullifyContainerOpenCallback() : void{ + $this->container_open_callback = static fn(int $window_id, Inventory $inventory) : ?array => null; + } +} diff --git a/src/form/ChestAPI/session/network/handler/ClosurePlayerNetworkHandler.php b/src/form/ChestAPI/session/network/handler/ClosurePlayerNetworkHandler.php new file mode 100644 index 00000000000..dac02464e15 --- /dev/null +++ b/src/form/ChestAPI/session/network/handler/ClosurePlayerNetworkHandler.php @@ -0,0 +1,22 @@ +creator)($then); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/session/network/handler/PlayerNetworkHandler.php b/src/form/ChestAPI/session/network/handler/PlayerNetworkHandler.php new file mode 100644 index 00000000000..d8dac7668ef --- /dev/null +++ b/src/form/ChestAPI/session/network/handler/PlayerNetworkHandler.php @@ -0,0 +1,13 @@ +registerDefault(new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{ + $timestamp = mt_rand(); + return new NetworkStackLatencyEntry($timestamp * 1000000, $then, $timestamp); + })); + $this->register(DeviceOS::PLAYSTATION, new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{ + $timestamp = mt_rand(); + return new NetworkStackLatencyEntry($timestamp * 1000000, $then, $timestamp * 1000); + })); + } + + public function registerDefault(PlayerNetworkHandler $handler) : void{ + $this->default = $handler; + } + + public function register(int $os_id, PlayerNetworkHandler $handler) : void{ + $this->game_os_handlers[$os_id] = $handler; + } + + public function get(int $os_id) : PlayerNetworkHandler{ + return $this->game_os_handlers[$os_id] ?? $this->default; + } +} diff --git a/src/form/ChestAPI/transaction/DeterministicInvMenuTransaction.php b/src/form/ChestAPI/transaction/DeterministicInvMenuTransaction.php new file mode 100644 index 00000000000..85c063fbe52 --- /dev/null +++ b/src/form/ChestAPI/transaction/DeterministicInvMenuTransaction.php @@ -0,0 +1,60 @@ +result->then($callback); + } + + public function getPlayer() : Player{ + return $this->inner->getPlayer(); + } + + public function getOut() : Item{ + return $this->inner->getOut(); + } + + public function getIn() : Item{ + return $this->inner->getIn(); + } + + public function getItemClicked() : Item{ + return $this->inner->getItemClicked(); + } + + public function getItemClickedWith() : Item{ + return $this->inner->getItemClickedWith(); + } + + public function getAction() : SlotChangeAction{ + return $this->inner->getAction(); + } + + public function getTransaction() : InventoryTransaction{ + return $this->inner->getTransaction(); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/transaction/InvMenuTransaction.php b/src/form/ChestAPI/transaction/InvMenuTransaction.php new file mode 100644 index 00000000000..089b807f20d --- /dev/null +++ b/src/form/ChestAPI/transaction/InvMenuTransaction.php @@ -0,0 +1,43 @@ +cancelled; + } + + /** + * Notify when we have escaped from the event stack trace and the + * client's network stack trace. + * Useful for sending forms and other stuff that cant be sent right + * after closing inventory. + * + * @param (Closure(Player) : void)|null $callback + * @return self + */ + public function then(?Closure $callback) : self{ + $this->post_transaction_callback = $callback; + return $this; + } + + /** + * @deprecated Access {@see InvMenuTransactionResult::$post_transaction_callback} directly + * @return (Closure(Player) : void)|null + */ + public function getPostTransactionCallback() : ?Closure{ + return $this->post_transaction_callback; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/transaction/SimpleInvMenuTransaction.php b/src/form/ChestAPI/transaction/SimpleInvMenuTransaction.php new file mode 100644 index 00000000000..a47ac5107cf --- /dev/null +++ b/src/form/ChestAPI/transaction/SimpleInvMenuTransaction.php @@ -0,0 +1,57 @@ +player; + } + + public function getOut() : Item{ + return $this->out; + } + + public function getIn() : Item{ + return $this->in; + } + + public function getItemClicked() : Item{ + return $this->getOut(); + } + + public function getItemClickedWith() : Item{ + return $this->getIn(); + } + + public function getAction() : SlotChangeAction{ + return $this->action; + } + + public function getTransaction() : InventoryTransaction{ + return $this->transaction; + } + + public function continue() : InvMenuTransactionResult{ + return new InvMenuTransactionResult(false); + } + + public function discard() : InvMenuTransactionResult{ + return new InvMenuTransactionResult(true); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/ActorFixedInvMenuType.php b/src/form/ChestAPI/type/ActorFixedInvMenuType.php new file mode 100644 index 00000000000..96fd6851087 --- /dev/null +++ b/src/form/ChestAPI/type/ActorFixedInvMenuType.php @@ -0,0 +1,44 @@ + $actor_metadata + * @param int $size + * @param InvMenuGraphicNetworkTranslator|null $network_translator + */ + public function __construct( + readonly private string $actor_identifier, + readonly private int $actor_runtime_identifier, + readonly private array $actor_metadata, + readonly private int $size, + readonly private ?InvMenuGraphicNetworkTranslator $network_translator = null + ){} + + public function getSize() : int{ + return $this->size; + } + + public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{ + return new ActorInvMenuGraphic($this->actor_identifier, $this->actor_runtime_identifier, $this->actor_metadata, $this->network_translator); + } + + public function createInventory() : Inventory{ + return new InvMenuInventory($this->size); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/BlockActorFixedInvMenuType.php b/src/form/ChestAPI/type/BlockActorFixedInvMenuType.php new file mode 100644 index 00000000000..b4d62d9077c --- /dev/null +++ b/src/form/ChestAPI/type/BlockActorFixedInvMenuType.php @@ -0,0 +1,54 @@ +size; + } + + public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{ + $position = $player->getPosition(); + $origin = $position->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor(); + if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){ + return null; + } + + $graphics = [new BlockActorInvMenuGraphic($this->block, $origin, BlockActorInvMenuGraphic::createTile($this->tile_id, $menu->getName()), $this->network_translator, $this->animation_duration)]; + foreach(InvMenuTypeHelper::findConnectedBlocks("Chest", $position->getWorld(), $origin, Facing::HORIZONTAL) as $side){ + $graphics[] = new BlockInvMenuGraphic(VanillaBlocks::BARRIER(), $side); + } + + return count($graphics) > 1 ? new MultiBlockInvMenuGraphic($graphics) : $graphics[0]; + } + + public function createInventory() : Inventory{ + return new InvMenuInventory($this->size); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/BlockFixedInvMenuType.php b/src/form/ChestAPI/type/BlockFixedInvMenuType.php new file mode 100644 index 00000000000..988cfd6caa3 --- /dev/null +++ b/src/form/ChestAPI/type/BlockFixedInvMenuType.php @@ -0,0 +1,41 @@ +size; + } + + public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{ + $origin = $player->getPosition()->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor(); + if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){ + return null; + } + + return new BlockInvMenuGraphic($this->block, $origin, $this->network_translator); + } + + public function createInventory() : Inventory{ + return new InvMenuInventory($this->size); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/DoublePairableBlockActorFixedInvMenuType.php b/src/form/ChestAPI/type/DoublePairableBlockActorFixedInvMenuType.php new file mode 100644 index 00000000000..b79d3db9b20 --- /dev/null +++ b/src/form/ChestAPI/type/DoublePairableBlockActorFixedInvMenuType.php @@ -0,0 +1,70 @@ +size; + } + + public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{ + $position = $player->getPosition(); + $origin = $position->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor(); + if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){ + return null; + } + + $graphics = []; + $menu_name = $menu->getName(); + $world = $position->getWorld(); + foreach([ + [$origin, $origin->east(), [Facing::NORTH, Facing::SOUTH, Facing::WEST]], + [$origin->east(), $origin, [Facing::NORTH, Facing::SOUTH, Facing::EAST]] + ] as [$origin_pos, $pair_pos, $connected_sides]){ + $graphics[] = new BlockActorInvMenuGraphic( + $this->block, + $origin_pos, + BlockActorInvMenuGraphic::createTile($this->tile_id, $menu_name) + ->setInt(Chest::TAG_PAIRX, $pair_pos->x) + ->setInt(Chest::TAG_PAIRZ, $pair_pos->z), + $this->network_translator, + $this->animation_duration + ); + foreach(InvMenuTypeHelper::findConnectedBlocks("Chest", $world, $origin_pos, $connected_sides) as $side){ + $graphics[] = new BlockInvMenuGraphic(VanillaBlocks::BARRIER(), $side); + } + } + + return count($graphics) > 1 ? new MultiBlockInvMenuGraphic($graphics) : $graphics[0]; + } + + public function createInventory() : Inventory{ + return new InvMenuInventory($this->size); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/FixedInvMenuType.php b/src/form/ChestAPI/type/FixedInvMenuType.php new file mode 100644 index 00000000000..6f88f428c25 --- /dev/null +++ b/src/form/ChestAPI/type/FixedInvMenuType.php @@ -0,0 +1,18 @@ + */ + private array $types = []; + + /** @var array */ + private array $identifiers = []; + + public function __construct(){ + $this->register(InvMenuTypeIds::TYPE_CHEST, InvMenuTypeBuilders::BLOCK_ACTOR_FIXED() + ->setBlock(VanillaBlocks::CHEST()) + ->setSize(27) + ->setBlockActorId("Chest") + ->build()); + + $this->register(InvMenuTypeIds::TYPE_DOUBLE_CHEST, InvMenuTypeBuilders::DOUBLE_PAIRABLE_BLOCK_ACTOR_FIXED() + ->setBlock(VanillaBlocks::CHEST()) + ->setSize(54) + ->setBlockActorId("Chest") + ->setAnimationDuration(75) + ->build()); + + $this->register(InvMenuTypeIds::TYPE_HOPPER, InvMenuTypeBuilders::BLOCK_ACTOR_FIXED() + ->setBlock(VanillaBlocks::HOPPER()) + ->setSize(5) + ->setBlockActorId("Hopper") + ->setNetworkWindowType(WindowTypes::HOPPER) + ->build()); + } + + public function register(string $identifier, InvMenuType $type) : void{ + if(isset($this->types[$identifier])){ + unset($this->identifiers[spl_object_id($this->types[$identifier])], $this->types[$identifier]); + } + + $this->types[$identifier] = $type; + $this->identifiers[spl_object_id($type)] = $identifier; + } + + public function exists(string $identifier) : bool{ + return isset($this->types[$identifier]); + } + + public function get(string $identifier) : InvMenuType{ + return $this->types[$identifier]; + } + + public function getIdentifier(InvMenuType $type) : string{ + return $this->identifiers[spl_object_id($type)]; + } + + public function getOrNull(string $identifier) : ?InvMenuType{ + return $this->types[$identifier] ?? null; + } + + /** + * @return array + */ + public function getAll() : array{ + return $this->types; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/ActorInvMenuGraphic.php b/src/form/ChestAPI/type/graphic/ActorInvMenuGraphic.php new file mode 100644 index 00000000000..6d9590ce5a5 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/ActorInvMenuGraphic.php @@ -0,0 +1,71 @@ + $actor_metadata + * @param InvMenuGraphicNetworkTranslator|null $network_translator + * @param int $animation_duration + */ + public function __construct( + readonly private string $actor_identifier, + readonly private int $actor_runtime_identifier, + readonly private array $actor_metadata, + readonly private ?InvMenuGraphicNetworkTranslator $network_translator = null, + readonly private int $animation_duration = 0 + ){} + + public function send(Player $player, ?string $name) : void{ + $metadata = $this->actor_metadata; + if($name !== null){ + $metadata[EntityMetadataProperties::NAMETAG] = new StringMetadataProperty($name); + } + $player->getNetworkSession()->sendDataPacket(AddActorPacket::create( + $this->actor_runtime_identifier, + $this->actor_runtime_identifier, + $this->actor_identifier, + $player->getPosition()->asVector3(), + null, + 0.0, + 0.0, + 0.0, + 0.0, + [], + $metadata, + new PropertySyncData([], []), + [] + )); + } + + public function sendInventory(Player $player, Inventory $inventory) : bool{ + return $player->setCurrentWindow($inventory); + } + + public function remove(Player $player) : void{ + $player->getNetworkSession()->sendDataPacket(RemoveActorPacket::create($this->actor_runtime_identifier)); + } + + public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{ + return $this->network_translator; + } + + public function getAnimationDuration() : int{ + return $this->animation_duration; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/BlockActorInvMenuGraphic.php b/src/form/ChestAPI/type/graphic/BlockActorInvMenuGraphic.php new file mode 100644 index 00000000000..b0083da544b --- /dev/null +++ b/src/form/ChestAPI/type/graphic/BlockActorInvMenuGraphic.php @@ -0,0 +1,70 @@ +setString(Tile::TAG_ID, $tile_id); + if($name !== null){ + $tag->setString(Nameable::TAG_CUSTOM_NAME, $name); + } + return $tag; + } + + readonly private BlockInvMenuGraphic $block_graphic; + readonly private Vector3 $position; + readonly private CompoundTag $tile; + readonly private ?InvMenuGraphicNetworkTranslator $network_translator; + readonly private int $animation_duration; + + public function __construct(Block $block, Vector3 $position, CompoundTag $tile, ?InvMenuGraphicNetworkTranslator $network_translator = null, int $animation_duration = 0){ + $this->block_graphic = new BlockInvMenuGraphic($block, $position); + $this->position = $position; + $this->tile = $tile; + $this->network_translator = $network_translator; + $this->animation_duration = $animation_duration; + } + + public function getPosition() : Vector3{ + return $this->position; + } + + public function send(Player $player, ?string $name) : void{ + $this->block_graphic->send($player, $name); + if($name !== null){ + $this->tile->setString(Nameable::TAG_CUSTOM_NAME, $name); + } + $player->getNetworkSession()->sendDataPacket(BlockActorDataPacket::create(BlockPosition::fromVector3($this->position), new CacheableNbt($this->tile))); + } + + public function sendInventory(Player $player, Inventory $inventory) : bool{ + return $player->setCurrentWindow($inventory); + } + + public function remove(Player $player) : void{ + $this->block_graphic->remove($player); + } + + public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{ + return $this->network_translator; + } + + public function getAnimationDuration() : int{ + return $this->animation_duration; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/BlockInvMenuGraphic.php b/src/form/ChestAPI/type/graphic/BlockInvMenuGraphic.php new file mode 100644 index 00000000000..47bd8e3b2f4 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/BlockInvMenuGraphic.php @@ -0,0 +1,51 @@ +position; + } + + public function send(Player $player, ?string $name) : void{ + $player->getNetworkSession()->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), TypeConverter::getInstance()->getBlockTranslator()->internalIdToNetworkId($this->block->getStateId()), UpdateBlockPacket::FLAG_NETWORK, UpdateBlockPacket::DATA_LAYER_NORMAL)); + } + + public function sendInventory(Player $player, Inventory $inventory) : bool{ + return $player->setCurrentWindow($inventory); + } + + public function remove(Player $player) : void{ + $network = $player->getNetworkSession(); + foreach($player->getWorld()->createBlockUpdatePackets([$this->position]) as $packet){ + $network->sendDataPacket($packet); + } + } + + public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{ + return $this->network_translator; + } + + public function getAnimationDuration() : int{ + return $this->animation_duration; + } +} diff --git a/src/form/ChestAPI/type/graphic/InvMenuGraphic.php b/src/form/ChestAPI/type/graphic/InvMenuGraphic.php new file mode 100644 index 00000000000..6ae360d5f08 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/InvMenuGraphic.php @@ -0,0 +1,28 @@ +graphics); + if($first === false){ + throw new LogicException("Tried sending inventory from a multi graphic consisting of zero entries"); + } + + return $first; + } + + public function send(Player $player, ?string $name) : void{ + foreach($this->graphics as $graphic){ + $graphic->send($player, $name); + } + } + + public function sendInventory(Player $player, Inventory $inventory) : bool{ + return $this->first()->sendInventory($player, $inventory); + } + + public function remove(Player $player) : void{ + foreach($this->graphics as $graphic){ + $graphic->remove($player); + } + } + + public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{ + return $this->first()->getNetworkTranslator(); + } + + public function getPosition() : Vector3{ + return $this->first()->getPosition(); + } + + public function getAnimationDuration() : int{ + $max = 0; + foreach($this->graphics as $graphic){ + $duration = $graphic->getAnimationDuration(); + if($duration > $max){ + $max = $duration; + } + } + return $max; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/PositionedInvMenuGraphic.php b/src/form/ChestAPI/type/graphic/PositionedInvMenuGraphic.php new file mode 100644 index 00000000000..654d1dcafc8 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/PositionedInvMenuGraphic.php @@ -0,0 +1,12 @@ +actorUniqueId = $this->actor_runtime_id; + $packet->blockPosition = new BlockPosition(0, 0, 0); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php b/src/form/ChestAPI/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php new file mode 100644 index 00000000000..b38f6e4088b --- /dev/null +++ b/src/form/ChestAPI/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php @@ -0,0 +1,33 @@ +graphic; + if(!($graphic instanceof PositionedInvMenuGraphic)){ + throw new InvalidArgumentException("Expected " . PositionedInvMenuGraphic::class . ", got " . get_class($graphic)); + } + + $pos = $graphic->getPosition(); + $packet->blockPosition = new BlockPosition((int) $pos->x, (int) $pos->y, (int) $pos->z); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/network/InvMenuGraphicNetworkTranslator.php b/src/form/ChestAPI/type/graphic/network/InvMenuGraphicNetworkTranslator.php new file mode 100644 index 00000000000..eaa04256ce6 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/network/InvMenuGraphicNetworkTranslator.php @@ -0,0 +1,14 @@ +translators as $translator){ + $translator->translate($session, $current, $packet); + } + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php b/src/form/ChestAPI/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php new file mode 100644 index 00000000000..c37e3a12cb1 --- /dev/null +++ b/src/form/ChestAPI/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php @@ -0,0 +1,20 @@ +windowType = $this->window_type; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/InvMenuTypeBuilders.php b/src/form/ChestAPI/type/util/InvMenuTypeBuilders.php new file mode 100644 index 00000000000..e569f73756f --- /dev/null +++ b/src/form/ChestAPI/type/util/InvMenuTypeBuilders.php @@ -0,0 +1,29 @@ +getDirectionVector(); + $size = $player->size; + $offset->x *= -(1 + $size->getWidth()); + $offset->y *= -(1 + $size->getHeight()); + $offset->z *= -(1 + $size->getWidth()); + return $offset; + } + + public static function isValidYCoordinate(float $y) : bool{ + return $y >= self::NETWORK_WORLD_Y_MIN && $y <= self::NETWORK_WORLD_Y_MAX; + } + + /** + * @param string $tile_id + * @param World $world + * @param Vector3 $position + * @param list $sides + * @return Generator + */ + public static function findConnectedBlocks(string $tile_id, World $world, Vector3 $position, array $sides) : Generator{ + if($tile_id === "Chest"){ + // setting a single chest at the spot of a pairable chest sends the client a double chest + // https://github.com/Muqsit/InvMenu/issues/207 + foreach($sides as $side){ + $pos = $position->getSide($side); + $tile = $world->getTileAt($pos->x, $pos->y, $pos->z); + if($tile instanceof Chest && $tile->getPair() !== null){ + yield $pos; + } + } + } + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/ActorFixedInvMenuTypeBuilder.php b/src/form/ChestAPI/type/util/builder/ActorFixedInvMenuTypeBuilder.php new file mode 100644 index 00000000000..0a5bdc525dc --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/ActorFixedInvMenuTypeBuilder.php @@ -0,0 +1,38 @@ +getActorMetadata(); + $metadata->setFloat(EntityMetadataProperties::BOUNDING_BOX_HEIGHT, 0.01); + $metadata->setFloat(EntityMetadataProperties::BOUNDING_BOX_WIDTH, 0.01); + $metadata->setGenericFlag(EntityMetadataFlags::INVISIBLE, true); + } + + public function setNetworkWindowType(int $window_type) : self{ + $this->parentSetNetworkWindowType($window_type); + $this->getActorMetadata()->setByte(EntityMetadataProperties::CONTAINER_TYPE, $window_type); + return $this; + } + + public function setSize(int $size) : self{ + $this->parentSetSize($size); + $this->getActorMetadata()->setInt(EntityMetadataProperties::CONTAINER_BASE_SIZE, $size); + return $this; + } + + public function build() : ActorFixedInvMenuType{ + return new ActorFixedInvMenuType($this->getActorIdentifier(), $this->getActorRuntimeIdentifier(), $this->getActorMetadata()->getAll(), $this->getSize(), $this->getGraphicNetworkTranslator()); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/ActorInvMenuTypeBuilderTrait.php b/src/form/ChestAPI/type/util/builder/ActorInvMenuTypeBuilderTrait.php new file mode 100644 index 00000000000..496de428d17 --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/ActorInvMenuTypeBuilderTrait.php @@ -0,0 +1,45 @@ +actor_runtime_identifier ?? $this->setActorRuntimeIdentifier(Entity::nextRuntimeId())->getActorRuntimeIdentifier(); + } + + public function setActorRuntimeIdentifier(int $actor_runtime_identifier) : self{ + $this->actor_runtime_identifier = $actor_runtime_identifier; + $this->addGraphicNetworkTranslator(new ActorInvMenuGraphicNetworkTranslator($this->actor_runtime_identifier)); + return $this; + } + + public function getActorMetadata() : EntityMetadataCollection{ + return $this->actor_metadata ?? $this->setActorMetadata(new EntityMetadataCollection())->getActorMetadata(); + } + + public function setActorMetadata(EntityMetadataCollection $actor_metadata) : self{ + $this->actor_metadata = $actor_metadata; + return $this; + } + + public function getActorIdentifier() : string{ + return $this->actor_identifier ?? $this->setActorIdentifier(EntityIds::CHEST_MINECART)->getActorIdentifier(); + } + + public function setActorIdentifier(string $actor_identifier) : self{ + $this->actor_identifier = $actor_identifier; + return $this; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php b/src/form/ChestAPI/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php new file mode 100644 index 00000000000..6f49e6baa9d --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php @@ -0,0 +1,19 @@ +animation_duration = $animation_duration; + return $this; + } + + protected function getAnimationDuration() : int{ + return $this->animation_duration; + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php b/src/form/ChestAPI/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php new file mode 100644 index 00000000000..3105929266f --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php @@ -0,0 +1,35 @@ +addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance()); + } + + public function setBlockActorId(string $block_actor_id) : self{ + $this->block_actor_id = $block_actor_id; + return $this; + } + + private function getBlockActorId() : string{ + return $this->block_actor_id ?? throw new LogicException("No block actor ID was specified"); + } + + public function build() : BlockActorFixedInvMenuType{ + return new BlockActorFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getBlockActorId(), $this->getGraphicNetworkTranslator(), $this->getAnimationDuration()); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/BlockFixedInvMenuTypeBuilder.php b/src/form/ChestAPI/type/util/builder/BlockFixedInvMenuTypeBuilder.php new file mode 100644 index 00000000000..4968229f291 --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/BlockFixedInvMenuTypeBuilder.php @@ -0,0 +1,22 @@ +addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance()); + } + + public function build() : BlockFixedInvMenuType{ + return new BlockFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getGraphicNetworkTranslator()); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/BlockInvMenuTypeBuilderTrait.php b/src/form/ChestAPI/type/util/builder/BlockInvMenuTypeBuilderTrait.php new file mode 100644 index 00000000000..aff6b89b948 --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/BlockInvMenuTypeBuilderTrait.php @@ -0,0 +1,22 @@ +block = $block; + return $this; + } + + protected function getBlock() : Block{ + return $this->block ?? throw new LogicException("No block was provided"); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php b/src/form/ChestAPI/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php new file mode 100644 index 00000000000..d1ae522498d --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php @@ -0,0 +1,35 @@ +addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance()); + } + + public function setBlockActorId(string $block_actor_id) : self{ + $this->block_actor_id = $block_actor_id; + return $this; + } + + private function getBlockActorId() : string{ + return $this->block_actor_id ?? throw new LogicException("No block actor ID was specified"); + } + + public function build() : DoublePairableBlockActorFixedInvMenuType{ + return new DoublePairableBlockActorFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getBlockActorId(), $this->getGraphicNetworkTranslator(), $this->getAnimationDuration()); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/FixedInvMenuTypeBuilderTrait.php b/src/form/ChestAPI/type/util/builder/FixedInvMenuTypeBuilderTrait.php new file mode 100644 index 00000000000..7d7ae53ba15 --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/FixedInvMenuTypeBuilderTrait.php @@ -0,0 +1,21 @@ +size = $size; + return $this; + } + + protected function getSize() : int{ + return $this->size ?? throw new LogicException("No size was provided"); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php b/src/form/ChestAPI/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php new file mode 100644 index 00000000000..e77a6430f1c --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php @@ -0,0 +1,37 @@ +graphic_network_translators[] = $translator; + return $this; + } + + public function setNetworkWindowType(int $window_type) : self{ + $this->addGraphicNetworkTranslator(new WindowTypeInvMenuGraphicNetworkTranslator($window_type)); + return $this; + } + + protected function getGraphicNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{ + if(count($this->graphic_network_translators) === 0){ + return null; + } + + if(count($this->graphic_network_translators) === 1){ + return $this->graphic_network_translators[array_key_first($this->graphic_network_translators)]; + } + + return new MultiInvMenuGraphicNetworkTranslator($this->graphic_network_translators); + } +} \ No newline at end of file diff --git a/src/form/ChestAPI/type/util/builder/InvMenuTypeBuilder.php b/src/form/ChestAPI/type/util/builder/InvMenuTypeBuilder.php new file mode 100644 index 00000000000..1cb9d90db38 --- /dev/null +++ b/src/form/ChestAPI/type/util/builder/InvMenuTypeBuilder.php @@ -0,0 +1,12 @@ + Date: Fri, 6 Jun 2025 18:37:42 +0100 Subject: [PATCH 37/43] fixed? --- src/form/FormAPI/Form.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/form/FormAPI/Form.php b/src/form/FormAPI/Form.php index e91c07390bc..37bbe41ac4d 100644 --- a/src/form/FormAPI/Form.php +++ b/src/form/FormAPI/Form.php @@ -25,12 +25,13 @@ use pocketmine\player\Player; use InvalidArgumentException; +use pocketmine\form\Form as PMForm; /** * Form implementations must implement this interface to be able to utilize the Player form-sending mechanism. * There is no restriction on custom implementations other than that they must implement this. */ -interface Form extends \JsonSerializable +interface Form extends PMForm { /** * @param callable(Player, mixed): void|null $callable From ca3c452b4c87fdfa4a57ed102be3566c0430617f Mon Sep 17 00:00:00 2001 From: KanadeBlue <124839201+KanadeBlue@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:39:59 +0000 Subject: [PATCH 38/43] ddd --- src/scheduler/TaskScheduler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php index 7cc932cd3b7..4c64130bde8 100644 --- a/src/scheduler/TaskScheduler.php +++ b/src/scheduler/TaskScheduler.php @@ -68,7 +68,6 @@ public function cancelAllTasks(): void { $task->cancel(); } $this->tasks->clear(); - $this->queue->clear(); } /** From 1a3961839187be34936be4a86eab2d468fa92586 Mon Sep 17 00:00:00 2001 From: KanadeBlue Date: Fri, 6 Jun 2025 22:35:23 +0100 Subject: [PATCH 39/43] Stucture_Void --- src/block/BlockTypeIds.php | 4 +- src/block/VanillaBlocks.php | 194 +++++++++++------- .../convert/BlockObjectToStateSerializer.php | 1 + .../BlockStateToObjectDeserializer.php | 1 + src/item/StringToItemParser.php | 1 + 5 files changed, 125 insertions(+), 76 deletions(-) diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index c440cefdc68..e2047516a95 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -787,7 +787,9 @@ private function __construct(){ public const RESIN_CLUMP = 10757; public const CHISELED_RESIN_BRICKS = 10758; - public const FIRST_UNUSED_BLOCK_ID = 10759; + public const FIRST_UNUSED_BLOCK_ID = 10760; + + public const STRUCTURE_VOID = 10759; private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID; diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 231004dfa32..23fd1a6bd78 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -113,6 +113,7 @@ * @method static FloorBanner BANNER() * @method static Barrel BARREL() * @method static Transparent BARRIER() + * @method static Transparent STRUCTURE_VOID name() * @method static SimplePillar BASALT() * @method static Beacon BEACON() * @method static Bed BED() @@ -811,10 +812,12 @@ * @method static WitherRose WITHER_ROSE() * @method static Wool WOOL() */ -final class VanillaBlocks{ +final class VanillaBlocks +{ use CloningRegistryTrait; - private function __construct(){ + private function __construct() + { //NOOP } @@ -824,13 +827,14 @@ private function __construct(){ * @phpstan-param class-string $tileClass * @phpstan-return TBlock */ - protected static function register(string $name, \Closure $createBlock, ?string $tileClass = null) : Block{ + protected static function register(string $name, \Closure $createBlock, ?string $tileClass = null): Block + { //this sketchy hack allows us to avoid manually writing the constants inline //since type IDs are generated from this class anyway, I'm OK with this hack //nonetheless, we should try to get rid of it in a future major version (e.g by using string type IDs) $reflect = new \ReflectionClass(BlockTypeIds::class); $typeId = $reflect->getConstant(mb_strtoupper($name)); - if(!is_int($typeId)){ + if (!is_int($typeId)) { //this allows registering new stuff without adding new type ID constants //this reduces the number of mandatory steps to test new features in local development \GlobalLogger::get()->error(self::class . ": No constant type ID found for $name, generating a new one"); @@ -846,34 +850,40 @@ protected static function register(string $name, \Closure $createBlock, ?string * @return Block[] * @phpstan-return array */ - public static function getAll() : array{ + public static function getAll(): array + { //phpstan doesn't support generic traits yet :( /** @var Block[] $result */ $result = self::_registryGetAll(); return $result; } - protected static function setup() : void{ + protected static function setup(): void + { self::register("air", fn(BID $id) => new Air($id, "Air", new Info(BreakInfo::indestructible(-1.0)))); $railBreakInfo = new Info(new BreakInfo(0.7)); self::register("activator_rail", fn(BID $id) => new ActivatorRail($id, "Activator Rail", $railBreakInfo)); self::register("anvil", fn(BID $id) => new Anvil($id, "Anvil", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 6000.0)))); - self::register("bamboo", fn(BID $id) => new Bamboo($id, "Bamboo", new Info(new class(2.0 /* 1.0 in PC */, ToolType::AXE) extends BreakInfo{ - public function getBreakTime(Item $item) : float{ - if($item->getBlockToolType() === ToolType::SWORD){ - return 0.0; + self::register("bamboo", fn(BID $id) => new Bamboo($id, "Bamboo", new Info(new class (2.0 /* 1.0 in PC */ , ToolType::AXE) extends BreakInfo { + public function getBreakTime(Item $item): float + { + if ($item->getBlockToolType() === ToolType::SWORD) { + return 0.0; + } + return parent::getBreakTime($item); } - return parent::getBreakTime($item); - } - }, [Tags::POTTABLE_PLANTS]))); + }, [Tags::POTTABLE_PLANTS]))); self::register("bamboo_sapling", fn(BID $id) => new BambooSapling($id, "Bamboo Sapling", new Info(BreakInfo::instant()))); $bannerBreakInfo = new Info(BreakInfo::axe(1.0)); self::register("banner", fn(BID $id) => new FloorBanner($id, "Banner", $bannerBreakInfo), TileBanner::class); self::register("wall_banner", fn(BID $id) => new WallBanner($id, "Wall Banner", $bannerBreakInfo), TileBanner::class); self::register("barrel", fn(BID $id) => new Barrel($id, "Barrel", new Info(BreakInfo::axe(2.5))), TileBarrel::class); + self::register("barrel", fn(BID $id) => new Barrel($id, "Barrel", new Info(BreakInfo::axe(2.5))), TileBarrel::class); self::register("barrier", fn(BID $id) => new Transparent($id, "Barrier", new Info(BreakInfo::indestructible()))); + self::register("structure_void", fn(BID $id) => new Transparent($id, "Structure Void", new Info(BreakInfo::indestructible()))); + self::register("beacon", fn(BID $id) => new Beacon($id, "Beacon", new Info(new BreakInfo(3.0))), TileBeacon::class); self::register("bed", fn(BID $id) => new Bed($id, "Bed Block", new Info(new BreakInfo(0.2))), TileBed::class); self::register("bedrock", fn(BID $id) => new Bedrock($id, "Bedrock", new Info(BreakInfo::indestructible()))); @@ -1038,7 +1048,7 @@ public function getBreakTime(Item $item) : float{ self::register("nether_wart", fn(BID $id) => new NetherWartPlant($id, "Nether Wart", new Info(BreakInfo::instant()))); self::register("netherrack", fn(BID $id) => new Netherrack($id, "Netherrack", new Info(BreakInfo::pickaxe(0.4, ToolTier::WOOD)))); self::register("note_block", fn(BID $id) => new Note($id, "Note Block", new Info(BreakInfo::axe(0.8))), TileNote::class); - self::register("obsidian", fn(BID $id) => new Opaque($id, "Obsidian", new Info(BreakInfo::pickaxe(35.0 /* 50 in PC */, ToolTier::DIAMOND, 6000.0)))); + self::register("obsidian", fn(BID $id) => new Opaque($id, "Obsidian", new Info(BreakInfo::pickaxe(35.0 /* 50 in PC */ , ToolTier::DIAMOND, 6000.0)))); self::register("packed_ice", fn(BID $id) => new PackedIce($id, "Packed Ice", new Info(BreakInfo::pickaxe(0.5)))); self::register("podzol", fn(BID $id) => new Podzol($id, "Podzol", new Info(BreakInfo::shovel(0.5), [Tags::DIRT]))); self::register("potatoes", fn(BID $id) => new Potato($id, "Potato Block", new Info(BreakInfo::instant()))); @@ -1102,15 +1112,17 @@ public function getBreakTime(Item $item) : float{ $stoneBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); $stone = self::register( "stone", - fn(BID $id) => new class($id, "Stone", $stoneBreakInfo) extends Opaque{ - public function getDropsForCompatibleTool(Item $item) : array{ + fn(BID $id) => new class ($id, "Stone", $stoneBreakInfo) extends Opaque { + public function getDropsForCompatibleTool(Item $item): array + { return [VanillaBlocks::COBBLESTONE()->asItem()]; } - public function isAffectedBySilkTouch() : bool{ + public function isAffectedBySilkTouch(): bool + { return true; } - } + } ); self::register("andesite", fn(BID $id) => new Opaque($id, "Andesite", $stoneBreakInfo)); self::register("diorite", fn(BID $id) => new Opaque($id, "Diorite", $stoneBreakInfo)); @@ -1217,9 +1229,10 @@ public function isAffectedBySilkTouch() : bool{ )); self::register("wheat", fn(BID $id) => new Wheat($id, "Wheat Block", new Info(BreakInfo::instant()))); - $leavesBreakInfo = new Info(new class(0.2, ToolType::HOE) extends BreakInfo{ - public function getBreakTime(Item $item) : float{ - if($item->getBlockToolType() === ToolType::SHEARS){ + $leavesBreakInfo = new Info(new class (0.2, ToolType::HOE) extends BreakInfo { + public function getBreakTime(Item $item): float + { + if ($item->getBlockToolType() === ToolType::SHEARS) { return 0.0; } return parent::getBreakTime($item); @@ -1227,11 +1240,11 @@ public function getBreakTime(Item $item) : float{ }); $saplingTypeInfo = new Info(BreakInfo::instant(), [Tags::POTTABLE_PLANTS]); - foreach(SaplingType::cases() as $saplingType){ + foreach (SaplingType::cases() as $saplingType) { $name = $saplingType->getDisplayName(); self::register(strtolower($saplingType->name) . "_sapling", fn(BID $id) => new Sapling($id, $name . " Sapling", $saplingTypeInfo, $saplingType)); } - foreach(LeavesType::cases() as $leavesType){ + foreach (LeavesType::cases() as $leavesType) { $name = $leavesType->getDisplayName(); self::register(strtolower($leavesType->name) . "_leaves", fn(BID $id) => new Leaves($id, $name . " Leaves", $leavesBreakInfo, $leavesType)); } @@ -1261,16 +1274,17 @@ public function getBreakTime(Item $item) : float{ self::register("carpet", fn(BID $id) => new Carpet($id, "Carpet", new Info(new BreakInfo(0.1)))); self::register("concrete", fn(BID $id) => new Concrete($id, "Concrete", new Info(BreakInfo::pickaxe(1.8, ToolTier::WOOD)))); self::register("concrete_powder", fn(BID $id) => new ConcretePowder($id, "Concrete Powder", new Info(BreakInfo::shovel(0.5)))); - self::register("wool", fn(BID $id) => new Wool($id, "Wool", new Info(new class(0.8, ToolType::SHEARS) extends BreakInfo{ - public function getBreakTime(Item $item) : float{ - $time = parent::getBreakTime($item); - if($item->getBlockToolType() === ToolType::SHEARS){ - $time *= 3; //shears break compatible blocks 15x faster, but wool 5x + self::register("wool", fn(BID $id) => new Wool($id, "Wool", new Info(new class (0.8, ToolType::SHEARS) extends BreakInfo { + public function getBreakTime(Item $item): float + { + $time = parent::getBreakTime($item); + if ($item->getBlockToolType() === ToolType::SHEARS) { + $time *= 3; //shears break compatible blocks 15x faster, but wool 5x + } + + return $time; } - - return $time; - } - }))); + }))); //TODO: in the future these won't all have the same hardness; they only do now because of the old metadata crap $wallBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); @@ -1321,11 +1335,12 @@ public function getBreakTime(Item $item) : float{ self::register("muddy_mangrove_roots", fn(BID $id) => new SimplePillar($id, "Muddy Mangrove Roots", new Info(BreakInfo::shovel(0.7), [Tags::MUD]))); self::register("froglight", fn(BID $id) => new Froglight($id, "Froglight", new Info(new BreakInfo(0.3)))); self::register("sculk", fn(BID $id) => new Sculk($id, "Sculk", new Info(new BreakInfo(0.6, ToolType::HOE)))); - self::register("reinforced_deepslate", fn(BID $id) => new class($id, "Reinforced Deepslate", new Info(new BreakInfo(55.0, ToolType::NONE, 0, 3600.0))) extends Opaque{ - public function getDropsForCompatibleTool(Item $item) : array{ - return []; - } - }); + self::register("reinforced_deepslate", fn(BID $id) => new class ($id, "Reinforced Deepslate", new Info(new BreakInfo(55.0, ToolType::NONE, 0, 3600.0))) extends Opaque { + public function getDropsForCompatibleTool(Item $item): array + { + return []; + } + }); self::registerBlocksR13(); self::registerBlocksR14(); @@ -1343,7 +1358,8 @@ public function getDropsForCompatibleTool(Item $item) : array{ self::registerCauldronBlocks(); } - private static function registerWoodenBlocks() : void{ + private static function registerWoodenBlocks(): void + { $planksBreakInfo = new Info(BreakInfo::axe(2.0, null, 15.0)); $signBreakInfo = new Info(BreakInfo::axe(1.0)); $logBreakInfo = new Info(BreakInfo::axe(2.0)); @@ -1351,7 +1367,7 @@ private static function registerWoodenBlocks() : void{ $woodenButtonBreakInfo = new Info(BreakInfo::axe(0.5)); $woodenPressurePlateBreakInfo = new Info(BreakInfo::axe(0.5)); - foreach(WoodType::cases() as $woodType){ + foreach (WoodType::cases() as $woodType) { $name = $woodType->getDisplayName(); $idName = fn(string $suffix) => strtolower($woodType->name) . "_" . $suffix; @@ -1370,7 +1386,7 @@ private static function registerWoodenBlocks() : void{ self::register($idName("pressure_plate"), fn(BID $id) => new WoodenPressurePlate($id, $name . " Pressure Plate", $woodenPressurePlateBreakInfo, $woodType, 20)); self::register($idName("trapdoor"), fn(BID $id) => new WoodenTrapdoor($id, $name . " Trapdoor", $woodenDoorBreakInfo, $woodType)); - $signAsItem = match($woodType){ + $signAsItem = match ($woodType) { WoodType::OAK => VanillaItems::OAK_SIGN(...), WoodType::SPRUCE => VanillaItems::SPRUCE_SIGN(...), WoodType::BIRCH => VanillaItems::BIRCH_SIGN(...), @@ -1388,7 +1404,8 @@ private static function registerWoodenBlocks() : void{ } } - private static function registerMushroomBlocks() : void{ + private static function registerMushroomBlocks(): void + { $mushroomBlockBreakInfo = new Info(BreakInfo::axe(0.2)); self::register("brown_mushroom_block", fn(BID $id) => new BrownMushroomBlock($id, "Brown Mushroom Block", $mushroomBlockBreakInfo)); @@ -1399,7 +1416,8 @@ private static function registerMushroomBlocks() : void{ self::register("all_sided_mushroom_stem", fn(BID $id) => new MushroomStem($id, "All Sided Mushroom Stem", $mushroomBlockBreakInfo)); } - private static function registerElements() : void{ + private static function registerElements(): void + { $instaBreak = new Info(BreakInfo::instant()); self::register("element_zero", fn(BID $id) => new Opaque($id, "???", $instaBreak)); @@ -1526,7 +1544,8 @@ private static function registerElements() : void{ $register("oganesson", "Oganesson", "og", 118, 7); } - private static function registerOres() : void{ + private static function registerOres(): void + { $stoneOreBreakInfo = fn(ToolTier $toolTier) => new Info(BreakInfo::pickaxe(3.0, $toolTier)); self::register("coal_ore", fn(BID $id) => new CoalOre($id, "Coal Ore", $stoneOreBreakInfo(ToolTier::WOOD))); self::register("copper_ore", fn(BID $id) => new CopperOre($id, "Copper Ore", $stoneOreBreakInfo(ToolTier::STONE))); @@ -1552,7 +1571,8 @@ private static function registerOres() : void{ self::register("nether_gold_ore", fn(BID $id) => new NetherGoldOre($id, "Nether Gold Ore", $netherrackOreBreakInfo)); } - private static function registerCraftingTables() : void{ + private static function registerCraftingTables(): void + { //TODO: this is the same for all wooden crafting blocks $craftingBlockBreakInfo = new Info(BreakInfo::axe(2.5)); self::register("cartography_table", fn(BID $id) => new CartographyTable($id, "Cartography Table", $craftingBlockBreakInfo)); @@ -1562,32 +1582,42 @@ private static function registerCraftingTables() : void{ self::register("smithing_table", fn(BID $id) => new SmithingTable($id, "Smithing Table", $craftingBlockBreakInfo)); } - private static function registerChorusBlocks() : void{ + private static function registerChorusBlocks(): void + { $chorusBlockBreakInfo = new Info(BreakInfo::axe(0.4)); self::register("chorus_plant", fn(BID $id) => new ChorusPlant($id, "Chorus Plant", $chorusBlockBreakInfo)); self::register("chorus_flower", fn(BID $id) => new ChorusFlower($id, "Chorus Flower", $chorusBlockBreakInfo)); } - private static function registerBlocksR13() : void{ + private static function registerBlocksR13(): void + { self::register("light", fn(BID $id) => new Light($id, "Light Block", new Info(BreakInfo::indestructible()))); self::register("wither_rose", fn(BID $id) => new WitherRose($id, "Wither Rose", new Info(BreakInfo::instant(), [Tags::POTTABLE_PLANTS]))); } - private static function registerBlocksR14() : void{ + private static function registerBlocksR14(): void + { self::register("honeycomb", fn(BID $id) => new Opaque($id, "Honeycomb Block", new Info(new BreakInfo(0.6)))); } - private static function registerBlocksR16() : void{ + private static function registerBlocksR16(): void + { //for some reason, slabs have weird hardness like the legacy ones $slabBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); - self::register("ancient_debris", fn(BID $id) => new class($id, "Ancient Debris", new Info(BreakInfo::pickaxe(30, ToolTier::DIAMOND, 3600.0))) extends Opaque{ - public function isFireProofAsItem() : bool{ return true; } - }); + self::register("ancient_debris", fn(BID $id) => new class ($id, "Ancient Debris", new Info(BreakInfo::pickaxe(30, ToolTier::DIAMOND, 3600.0))) extends Opaque { + public function isFireProofAsItem(): bool + { + return true; + } + }); $netheriteBreakInfo = new Info(BreakInfo::pickaxe(50, ToolTier::DIAMOND, 3600.0)); - self::register("netherite", fn(BID $id) => new class($id, "Netherite Block", $netheriteBreakInfo) extends Opaque{ - public function isFireProofAsItem() : bool{ return true; } - }); + self::register("netherite", fn(BID $id) => new class ($id, "Netherite Block", $netheriteBreakInfo) extends Opaque { + public function isFireProofAsItem(): bool + { + return true; + } + }); $basaltBreakInfo = new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 21.0)); self::register("basalt", fn(BID $id) => new SimplePillar($id, "Basalt", $basaltBreakInfo)); @@ -1625,14 +1655,20 @@ public function isFireProofAsItem() : bool{ return true; } //TODO: soul soul ought to have 0.5 hardness (as per java) but it's 1.0 in Bedrock (probably parity bug) self::register("soul_soil", fn(BID $id) => new Opaque($id, "Soul Soil", new Info(BreakInfo::shovel(1.0)))); - self::register("shroomlight", fn(BID $id) => new class($id, "Shroomlight", new Info(new BreakInfo(1.0, ToolType::HOE))) extends Opaque{ - public function getLightLevel() : int{ return 15; } - }); + self::register("shroomlight", fn(BID $id) => new class ($id, "Shroomlight", new Info(new BreakInfo(1.0, ToolType::HOE))) extends Opaque { + public function getLightLevel(): int + { + return 15; + } + }); self::register("warped_wart_block", fn(BID $id) => new Opaque($id, "Warped Wart Block", new Info(new BreakInfo(1.0, ToolType::HOE)))); - self::register("crying_obsidian", fn(BID $id) => new class($id, "Crying Obsidian", new Info(BreakInfo::pickaxe(35.0 /* 50 in Java */, ToolTier::DIAMOND, 6000.0))) extends Opaque{ - public function getLightLevel() : int{ return 10;} - }); + self::register("crying_obsidian", fn(BID $id) => new class ($id, "Crying Obsidian", new Info(BreakInfo::pickaxe(35.0 /* 50 in Java */ , ToolTier::DIAMOND, 6000.0))) extends Opaque { + public function getLightLevel(): int + { + return 10; + } + }); self::register("twisting_vines", fn(BID $id) => new NetherVines($id, "Twisting Vines", new Info(BreakInfo::instant()), Facing::UP)); self::register("weeping_vines", fn(BID $id) => new NetherVines($id, "Weeping Vines", new Info(BreakInfo::instant()), Facing::DOWN)); @@ -1644,10 +1680,11 @@ public function getLightLevel() : int{ return 10;} self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD)))); } - private static function registerBlocksR17() : void{ + private static function registerBlocksR17(): void + { //in java this can be acquired using any tool - seems to be a parity issue in bedrock $amethystInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD)); - self::register("amethyst", fn(BID $id) => new class($id, "Amethyst", $amethystInfo) extends Opaque{ + self::register("amethyst", fn(BID $id) => new class ($id, "Amethyst", $amethystInfo) extends Opaque { use AmethystTrait; }); self::register("budding_amethyst", fn(BID $id) => new BuddingAmethyst($id, "Budding Amethyst", $amethystInfo)); @@ -1660,15 +1697,17 @@ private static function registerBlocksR17() : void{ self::register("raw_iron", fn(BID $id) => new Opaque($id, "Raw Iron Block", new Info(BreakInfo::pickaxe(5, ToolTier::STONE, 30.0)))); $deepslateBreakInfo = new Info(BreakInfo::pickaxe(3, ToolTier::WOOD, 18.0)); - self::register("deepslate", fn(BID $id) => new class($id, "Deepslate", $deepslateBreakInfo) extends SimplePillar{ - public function getDropsForCompatibleTool(Item $item) : array{ - return [VanillaBlocks::COBBLED_DEEPSLATE()->asItem()]; - } + self::register("deepslate", fn(BID $id) => new class ($id, "Deepslate", $deepslateBreakInfo) extends SimplePillar { + public function getDropsForCompatibleTool(Item $item): array + { + return [VanillaBlocks::COBBLED_DEEPSLATE()->asItem()]; + } - public function isAffectedBySilkTouch() : bool{ - return true; - } - }); + public function isAffectedBySilkTouch(): bool + { + return true; + } + }); //TODO: parity issue here - in Java this has a hardness of 3.0, but in bedrock it's 3.5 self::register("chiseled_deepslate", fn(BID $id) => new Opaque($id, "Chiseled Deepslate", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0)))); @@ -1734,11 +1773,13 @@ public function isAffectedBySilkTouch() : bool{ self::register("big_dripleaf_stem", fn(BID $id) => new BigDripleafStem($id, "Big Dripleaf Stem", new Info(BreakInfo::instant()))); } - private static function registerBlocksR18() : void{ + private static function registerBlocksR18(): void + { self::register("spore_blossom", fn(BID $id) => new SporeBlossom($id, "Spore Blossom", new Info(BreakInfo::instant()))); } - private static function registerMudBlocks() : void{ + private static function registerMudBlocks(): void + { self::register("mud", fn(BID $id) => new Opaque($id, "Mud", new Info(BreakInfo::shovel(0.5), [Tags::MUD]))); self::register("packed_mud", fn(BID $id) => new Opaque($id, "Packed Mud", new Info(BreakInfo::pickaxe(1.0, null, 15.0)))); @@ -1750,7 +1791,8 @@ private static function registerMudBlocks() : void{ self::register("mud_brick_wall", fn(BID $id) => new Wall($id, "Mud Brick Wall", $mudBricksBreakInfo)); } - private static function registerResinBlocks() : void{ + private static function registerResinBlocks(): void + { self::register("resin", fn(BID $id) => new Opaque($id, "Block of Resin", new Info(BreakInfo::instant()))); self::register("resin_clump", fn(BID $id) => new ResinClump($id, "Resin Clump", new Info(BreakInfo::instant()))); @@ -1762,7 +1804,8 @@ private static function registerResinBlocks() : void{ self::register("chiseled_resin_bricks", fn(BID $id) => new Opaque($id, "Chiseled Resin Bricks", $resinBricksInfo)); } - private static function registerTuffBlocks() : void{ + private static function registerTuffBlocks(): void + { $tuffBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); self::register("tuff", fn(BID $id) => new Opaque($id, "Tuff", $tuffBreakInfo)); @@ -1783,7 +1826,8 @@ private static function registerTuffBlocks() : void{ self::register("polished_tuff_wall", fn(BID $id) => new Wall($id, "Polished Tuff Wall", $tuffBreakInfo)); } - private static function registerCauldronBlocks() : void{ + private static function registerCauldronBlocks(): void + { $cauldronBreakInfo = new Info(BreakInfo::pickaxe(2, ToolTier::WOOD)); self::register("cauldron", fn(BID $id) => new Cauldron($id, "Cauldron", $cauldronBreakInfo), TileCauldron::class); diff --git a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php index 367d3844954..6f903caf32d 100644 --- a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php +++ b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php @@ -970,6 +970,7 @@ private function registerSimpleSerializers() : void{ $this->mapSimple(Blocks::ANCIENT_DEBRIS(), Ids::ANCIENT_DEBRIS); $this->mapSimple(Blocks::ANDESITE(), Ids::ANDESITE); $this->mapSimple(Blocks::BARRIER(), Ids::BARRIER); + $this->mapSimple(Blocks::STRUCTURE_VOID(), Ids::STRUCTURE_VOID); $this->mapSimple(Blocks::BEACON(), Ids::BEACON); $this->mapSimple(Blocks::BLACKSTONE(), Ids::BLACKSTONE); $this->mapSimple(Blocks::BLUE_ICE(), Ids::BLUE_ICE); diff --git a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php index ed45a47d354..2e7164b0666 100644 --- a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php +++ b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php @@ -887,6 +887,7 @@ private function registerSimpleDeserializers() : void{ $this->mapSimple(Ids::ANCIENT_DEBRIS, fn() => Blocks::ANCIENT_DEBRIS()); $this->mapSimple(Ids::ANDESITE, fn() => Blocks::ANDESITE()); $this->mapSimple(Ids::BARRIER, fn() => Blocks::BARRIER()); + $this->mapSimple(Ids::STRUCTURE_VOID, fn() => Blocks::STRUCTURE_VOID()); $this->mapSimple(Ids::BEACON, fn() => Blocks::BEACON()); $this->mapSimple(Ids::BLACKSTONE, fn() => Blocks::BLACKSTONE()); $this->mapSimple(Ids::BLUE_ICE, fn() => Blocks::BLUE_ICE()); diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index a3bd7b87272..20cb36ab99b 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -155,6 +155,7 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("banner", fn() => Blocks::BANNER()); $result->registerBlock("barrel", fn() => Blocks::BARREL()); $result->registerBlock("barrier", fn() => Blocks::BARRIER()); + $result->registerBlock("structure_void", fn() => Blocks::STRUCTURE_VOID()); $result->registerBlock("basalt", fn() => Blocks::BASALT()); $result->registerBlock("beacon", fn() => Blocks::BEACON()); $result->registerBlock("bed", fn() => Blocks::BED()); From 8e9f4943abbbe3b03ba8076aa3e9582c03e8ce2d Mon Sep 17 00:00:00 2001 From: KanadeBlue Date: Fri, 6 Jun 2025 22:38:27 +0100 Subject: [PATCH 40/43] oops --- src/block/VanillaBlocks.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 23fd1a6bd78..7fa93b27496 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -880,7 +880,6 @@ public function getBreakTime(Item $item): float self::register("banner", fn(BID $id) => new FloorBanner($id, "Banner", $bannerBreakInfo), TileBanner::class); self::register("wall_banner", fn(BID $id) => new WallBanner($id, "Wall Banner", $bannerBreakInfo), TileBanner::class); self::register("barrel", fn(BID $id) => new Barrel($id, "Barrel", new Info(BreakInfo::axe(2.5))), TileBarrel::class); - self::register("barrel", fn(BID $id) => new Barrel($id, "Barrel", new Info(BreakInfo::axe(2.5))), TileBarrel::class); self::register("barrier", fn(BID $id) => new Transparent($id, "Barrier", new Info(BreakInfo::indestructible()))); self::register("structure_void", fn(BID $id) => new Transparent($id, "Structure Void", new Info(BreakInfo::indestructible()))); From 8730c27cb5002337ca15dc73681ff610d2c025c3 Mon Sep 17 00:00:00 2001 From: KanadeBlue <124839201+KanadeBlue@users.noreply.github.com> Date: Sun, 15 Jun 2025 19:19:24 +0000 Subject: [PATCH 41/43] ChunkRequestTask & UpdateCheckTask used more cpu% then needed now alot more optimised. --- src/network/mcpe/ChunkRequestTask.php | 67 +++++--------- src/updater/UpdateCheckTask.php | 123 ++++++++++++-------------- 2 files changed, 78 insertions(+), 112 deletions(-) diff --git a/src/network/mcpe/ChunkRequestTask.php b/src/network/mcpe/ChunkRequestTask.php index 13b5db5b795..39c774716cb 100644 --- a/src/network/mcpe/ChunkRequestTask.php +++ b/src/network/mcpe/ChunkRequestTask.php @@ -1,24 +1,5 @@ */ protected NonThreadSafeValue $compressor; - private string $tiles; - /** - * @phpstan-param DimensionIds::* $dimensionId - */ public function __construct(int $chunkX, int $chunkZ, int $dimensionId, Chunk $chunk, CompressBatchPromise $promise, Compressor $compressor){ $this->compressor = new NonThreadSafeValue($compressor); - $this->chunk = FastChunkSerializer::serializeTerrain($chunk); - $this->chunkX = $chunkX; - $this->chunkZ = $chunkZ; - $this->dimensionId = $dimensionId; - $this->tiles = ChunkSerializer::serializeTiles($chunk); + $tiles = ChunkSerializer::serializeTiles($chunk); + $blockTranslator = TypeConverter::getInstance()->getBlockTranslator(); + $subCount = ChunkSerializer::getSubChunkCount($chunk, $dimensionId); + $payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $blockTranslator, $tiles); + $stream = new BinaryStream(); + PacketBatch::encodePackets($stream, [ + LevelChunkPacket::create( + new ChunkPosition($chunkX, $chunkZ), + $dimensionId, + $subCount, + false, + null, + $payload + ) + ]); + + $this->encodedData = $stream->getBuffer(); $this->storeLocal(self::TLS_KEY_PROMISE, $promise); } public function onRun() : void{ - $chunk = FastChunkSerializer::deserializeTerrain($this->chunk); - $dimensionId = $this->dimensionId; - - $subCount = ChunkSerializer::getSubChunkCount($chunk, $dimensionId); - $converter = TypeConverter::getInstance(); - $payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $converter->getBlockTranslator(), $this->tiles); - - $stream = new BinaryStream(); - PacketBatch::encodePackets($stream, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $dimensionId, $subCount, false, null, $payload)]); - $compressor = $this->compressor->deserialize(); - $this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getBuffer())); + $compressed = $compressor->compress($this->encodedData); + $this->setResult(chr($compressor->getNetworkId()) . $compressed); } - public function onCompletion() : void{ - /** @var CompressBatchPromise $promise */ + public function onCompletion() : void{ $promise = $this->fetchLocal(self::TLS_KEY_PROMISE); $promise->resolve($this->getResult()); } diff --git a/src/updater/UpdateCheckTask.php b/src/updater/UpdateCheckTask.php index af73f05af49..4e04d109ea2 100644 --- a/src/updater/UpdateCheckTask.php +++ b/src/updater/UpdateCheckTask.php @@ -1,24 +1,5 @@ storeLocal(self::TLS_KEY_UPDATER, $updater); + } + + public function onRun() : void { + $error = ""; + $url = sprintf("%s?channel=%s", $this->endpoint, $this->channel); + + $response = Internet::getURL($url, 4, [], $error); + $this->error = $error; + + if ($response === null) { + return; + } - private string $error = "Unknown error"; + $data = json_decode($response->getBody(), true); + if (!is_array($data)) { + $this->error = "Invalid response data"; + return; + } - public function __construct( - UpdateChecker $updater, - private string $endpoint, - private string $channel - ){ - $this->storeLocal(self::TLS_KEY_UPDATER, $updater); - } + if (isset($data["error"]) && is_string($data["error"])) { + $this->error = $data["error"]; + return; + } - public function onRun() : void{ - $error = ""; - $response = Internet::getURL($this->endpoint . "?channel=" . $this->channel, 4, [], $error); - $this->error = $error; + static $mapper = null; + if ($mapper === null) { + $mapper = new \JsonMapper(); + $mapper->bExceptionOnMissingData = false; // avoid exceptions on missing data for speed + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + } - if($response !== null){ - $response = json_decode($response->getBody(), true); - if(is_array($response)){ - if(isset($response["error"]) && is_string($response["error"])){ - $this->error = $response["error"]; - }else{ - $mapper = new \JsonMapper(); - $mapper->bExceptionOnMissingData = true; - $mapper->bStrictObjectTypeChecking = true; - $mapper->bEnforceMapType = false; - try{ - /** @var UpdateInfo $responseObj */ - $responseObj = $mapper->map($response, new UpdateInfo()); - $this->setResult($responseObj); - }catch(\JsonMapper_Exception $e){ - $this->error = "Invalid JSON response data: " . $e->getMessage(); - } - } - }else{ - $this->error = "Invalid response data"; - } - } - } + try { + /** @var UpdateInfo $responseObj */ + $responseObj = $mapper->map($data, new UpdateInfo()); + $this->setResult($responseObj); + } catch (\JsonMapper_Exception $e) { + $this->error = "Invalid JSON response data: " . $e->getMessage(); + } + } - public function onCompletion() : void{ - /** @var UpdateChecker $updater */ - $updater = $this->fetchLocal(self::TLS_KEY_UPDATER); - if($this->hasResult()){ - /** @var UpdateInfo $response */ - $response = $this->getResult(); - $updater->checkUpdateCallback($response); - }else{ - $updater->checkUpdateError($this->error); - } - } + public function onCompletion() : void { + /** @var UpdateChecker $updater */ + $updater = $this->fetchLocal(self::TLS_KEY_UPDATER); + if ($this->hasResult()) { + /** @var UpdateInfo $response */ + $response = $this->getResult(); + $updater->checkUpdateCallback($response); + } else { + $updater->checkUpdateError($this->error); + } + } } From bb444a9b2179649352bd5667d6a4c577582d7ab4 Mon Sep 17 00:00:00 2001 From: KanadeBlue <124839201+KanadeBlue@users.noreply.github.com> Date: Tue, 17 Jun 2025 03:46:51 +0000 Subject: [PATCH 42/43] Improved slightly --- src/world/World.php | 72 ++++--- src/world/format/io/FastChunkSerializer.php | 203 +++++++++-------- src/world/generator/PopulationTask.php | 227 ++++++++------------ src/world/light/LightPopulationTask.php | 139 +++++------- 4 files changed, 275 insertions(+), 366 deletions(-) diff --git a/src/world/World.php b/src/world/World.php index 63a6171260d..c1de8e8ceb9 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -1352,40 +1352,48 @@ private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{ } } - private function orderLightPopulation(int $chunkX, int $chunkZ) : void{ - $chunkHash = World::chunkHash($chunkX, $chunkZ); - $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated(); - if($lightPopulatedState === false){ - $this->chunks[$chunkHash]->setLightPopulated(null); - $this->markTickingChunkForRecheck($chunkX, $chunkZ); + private function orderLightPopulation(int $chunkX, int $chunkZ): void { + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $chunk = $this->chunks[$chunkHash] ?? null; + + if ($chunk === null || $chunk->isLightPopulated() !== false) { + return; + } + + $chunk->setLightPopulated(null); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); + + $this->workerPool->submitTask(new LightPopulationTask( + $chunk, + function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ): void { + if ( + $this->unloaded || + ($chunk = $this->getChunk($chunkX, $chunkZ)) === null + ) { + return; + } + + if ($chunk->isLightPopulated() !== null) { + return; + } + + $chunk->setHeightMapArray($heightMap); + + foreach ($blockLight as $y => $lightArray) { + $chunk->getSubChunk($y)?->setBlockLightArray($lightArray); + } + + foreach ($skyLight as $y => $lightArray) { + $chunk->getSubChunk($y)?->setBlockSkyLightArray($lightArray); + } + + $chunk->setLightPopulated(true); + $this->markTickingChunkForRecheck($chunkX, $chunkZ); + } + )); +} - $this->workerPool->submitTask(new LightPopulationTask( - $this->chunks[$chunkHash], - function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{ - /** - * TODO: phpstan can't infer these types yet :( - * @phpstan-var array $blockLight - * @phpstan-var array $skyLight - * @phpstan-var non-empty-list $heightMap - */ - if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){ - return; - } - //TODO: calculated light information might not be valid if the terrain changed during light calculation - $chunk->setHeightMapArray($heightMap); - foreach($blockLight as $y => $lightArray){ - $chunk->getSubChunk($y)->setBlockLightArray($lightArray); - } - foreach($skyLight as $y => $lightArray){ - $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray); - } - $chunk->setLightPopulated(true); - $this->markTickingChunkForRecheck($chunkX, $chunkZ); - } - )); - } - } private function tickChunk(int $chunkX, int $chunkZ) : void{ $chunk = $this->getChunk($chunkX, $chunkZ); diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php index 35a8ff42f74..af0147f6a24 100644 --- a/src/world/format/io/FastChunkSerializer.php +++ b/src/world/format/io/FastChunkSerializer.php @@ -1,24 +1,5 @@ getWordArray(); - $palette = $array->getPalette(); - - $stream->putByte($array->getBitsPerBlock()); - $stream->put($wordArray); - $serialPalette = pack("L*", ...$palette); - $stream->putInt(strlen($serialPalette)); - $stream->put($serialPalette); - } - - /** - * Fast-serializes the chunk for passing between threads - * TODO: tiles and entities - */ - public static function serializeTerrain(Chunk $chunk) : string{ - $stream = new BinaryStream(); - $stream->putByte( - ($chunk->isPopulated() ? self::FLAG_POPULATED : 0) - ); - - //subchunks - $subChunks = $chunk->getSubChunks(); - $count = count($subChunks); - $stream->putByte($count); - - foreach($subChunks as $y => $subChunk){ - $stream->putByte($y); - $stream->putInt($subChunk->getEmptyBlockId()); - $layers = $subChunk->getBlockLayers(); - $stream->putByte(count($layers)); - foreach($layers as $blocks){ - self::serializePalettedArray($stream, $blocks); - } - self::serializePalettedArray($stream, $subChunk->getBiomeArray()); - - } - - return $stream->getBuffer(); - } - - private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{ - $bitsPerBlock = $stream->getByte(); - $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); - /** @var int[] $unpackedPalette */ - $unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here - $palette = array_values($unpackedPalette); - - return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); - } - - /** - * Deserializes a fast-serialized chunk - */ - public static function deserializeTerrain(string $data) : Chunk{ - $stream = new BinaryStream($data); - - $flags = $stream->getByte(); - $terrainPopulated = (bool) ($flags & self::FLAG_POPULATED); - - $subChunks = []; - - $count = $stream->getByte(); - for($subCount = 0; $subCount < $count; ++$subCount){ - $y = Binary::signByte($stream->getByte()); - $airBlockId = $stream->getInt(); - - $layers = []; - for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ - $layers[] = self::deserializePalettedArray($stream); - } - $biomeArray = self::deserializePalettedArray($stream); - $subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray); - } - - return new Chunk($subChunks, $terrainPopulated); - } + private const FLAG_POPULATED = 1 << 1; + + private function __construct(){ + // Static utility class — do not instantiate + } + + private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array): void{ + $bitsPerBlock = $array->getBitsPerBlock(); + $wordArray = $array->getWordArray(); + $palette = $array->getPalette(); + + $stream->putByte($bitsPerBlock); + $stream->put($wordArray); + + // Efficiently pack palette as binary + $serialPalette = pack('L*', ...$palette); + $stream->putInt(strlen($serialPalette)); + $stream->put($serialPalette); + } + + /** + * Serializes chunk terrain for thread transfer. + */ + public static function serializeTerrain(Chunk $chunk): string{ + $stream = new BinaryStream(); + $flags = $chunk->isPopulated() ? self::FLAG_POPULATED : 0; + $stream->putByte($flags); + + $subChunks = $chunk->getSubChunks(); + $count = \count($subChunks); + $stream->putByte($count); + + foreach ($subChunks as $y => $subChunk) { + $stream->putByte($y); + $stream->putInt($subChunk->getEmptyBlockId()); + + $layers = $subChunk->getBlockLayers(); + $layerCount = \count($layers); + $stream->putByte($layerCount); + + foreach ($layers as $layer) { + self::serializePalettedArray($stream, $layer); + } + + self::serializePalettedArray($stream, $subChunk->getBiomeArray()); + } + + return $stream->getBuffer(); + } + + private static function deserializePalettedArray(BinaryStream $stream): PalettedBlockArray{ + $bitsPerBlock = $stream->getByte(); + $wordArraySize = PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock); + $words = $stream->get($wordArraySize); + + $paletteSize = $stream->getInt(); + $paletteData = $stream->get($paletteSize); + + /** @var int[] $palette */ + $palette = unpack('L*', $paletteData); + + return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); + } + + /** + * Deserializes terrain data into a chunk. + */ + public static function deserializeTerrain(string $data): Chunk{ + $stream = new BinaryStream($data); + + $flags = $stream->getByte(); + $terrainPopulated = ($flags & self::FLAG_POPULATED) !== 0; + + $subChunkCount = $stream->getByte(); + $subChunks = []; + + for ($i = 0; $i < $subChunkCount; ++$i) { + $y = Binary::signByte($stream->getByte()); + $airBlockId = $stream->getInt(); + + $layerCount = $stream->getByte(); + $layers = []; + for ($j = 0; $j < $layerCount; ++$j) { + $layers[] = self::deserializePalettedArray($stream); + } + + $biomeArray = self::deserializePalettedArray($stream); + + $subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray); + } + + return new Chunk($subChunks, $terrainPopulated); + } } diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php index bad13432408..538d12bb9df 100644 --- a/src/world/generator/PopulationTask.php +++ b/src/world/generator/PopulationTask.php @@ -1,24 +1,4 @@ $adjacentChunks) : void - */ -class PopulationTask extends AsyncTask{ - private const TLS_KEY_ON_COMPLETION = "onCompletion"; - - private ?string $chunk; - - private string $adjacentChunks; - - /** - * @param Chunk[]|null[] $adjacentChunks - * @phpstan-param array $adjacentChunks - * @phpstan-param OnCompletion $onCompletion - */ - public function __construct( - private int $worldId, - private int $chunkX, - private int $chunkZ, - ?Chunk $chunk, - array $adjacentChunks, - \Closure $onCompletion - ){ - $this->chunk = $chunk !== null ? FastChunkSerializer::serializeTerrain($chunk) : null; - - $this->adjacentChunks = igbinary_serialize(array_map( - fn(?Chunk $c) => $c !== null ? FastChunkSerializer::serializeTerrain($c) : null, - $adjacentChunks - )) ?? throw new AssumptionFailedError("igbinary_serialize() returned null"); - - $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); - } - - public function onRun() : void{ - $context = ThreadLocalGeneratorContext::fetch($this->worldId); - if($context === null){ - throw new AssumptionFailedError("Generator context should have been initialized before any PopulationTask execution"); - } - $generator = $context->getGenerator(); - $manager = new SimpleChunkManager($context->getWorldMinY(), $context->getWorldMaxY()); - - $chunk = $this->chunk !== null ? FastChunkSerializer::deserializeTerrain($this->chunk) : null; - - /** - * @var string[] $serialChunks - * @phpstan-var array $serialChunks - */ - $serialChunks = igbinary_unserialize($this->adjacentChunks); - $chunks = array_map( - function(?string $serialized) : ?Chunk{ - if($serialized === null){ - return null; - } - $chunk = FastChunkSerializer::deserializeTerrain($serialized); - $chunk->clearTerrainDirtyFlags(); //this allows us to avoid sending existing chunks back to the main thread if they haven't changed during generation - return $chunk; - }, - $serialChunks - ); - - self::setOrGenerateChunk($manager, $generator, $this->chunkX, $this->chunkZ, $chunk); - - $resultChunks = []; //this is just to keep phpstan's type inference happy - foreach($chunks as $relativeChunkHash => $c){ - World::getXZ($relativeChunkHash, $relativeX, $relativeZ); - $resultChunks[$relativeChunkHash] = self::setOrGenerateChunk($manager, $generator, $this->chunkX + $relativeX, $this->chunkZ + $relativeZ, $c); - } - $chunks = $resultChunks; - - $generator->populateChunk($manager, $this->chunkX, $this->chunkZ); - $chunk = $manager->getChunk($this->chunkX, $this->chunkZ); - if($chunk === null){ - throw new AssumptionFailedError("We just generated this chunk, so it must exist"); - } - $chunk->setPopulated(); - - $this->chunk = FastChunkSerializer::serializeTerrain($chunk); - - $serialChunks = []; - foreach($chunks as $relativeChunkHash => $c){ - $serialChunks[$relativeChunkHash] = $c->isTerrainDirty() ? FastChunkSerializer::serializeTerrain($c) : null; - } - $this->adjacentChunks = igbinary_serialize($serialChunks) ?? throw new AssumptionFailedError("igbinary_serialize() returned null"); - } - - private static function setOrGenerateChunk(SimpleChunkManager $manager, Generator $generator, int $chunkX, int $chunkZ, ?Chunk $chunk) : Chunk{ - $manager->setChunk($chunkX, $chunkZ, $chunk ?? new Chunk([], false)); - if($chunk === null){ - $generator->generateChunk($manager, $chunkX, $chunkZ); - $chunk = $manager->getChunk($chunkX, $chunkZ); - if($chunk === null){ - throw new AssumptionFailedError("We just set this chunk, so it must exist"); - } - } - return $chunk; - } - - public function onCompletion() : void{ - /** - * @var \Closure $onCompletion - * @phpstan-var OnCompletion $onCompletion - */ - $onCompletion = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); - - $chunk = $this->chunk !== null ? - FastChunkSerializer::deserializeTerrain($this->chunk) : - throw new AssumptionFailedError("Center chunk should never be null"); - - /** - * @var string[]|null[] $serialAdjacentChunks - * @phpstan-var array $serialAdjacentChunks - */ - $serialAdjacentChunks = igbinary_unserialize($this->adjacentChunks); - $adjacentChunks = []; - foreach($serialAdjacentChunks as $relativeChunkHash => $c){ - if($c !== null){ - $adjacentChunks[$relativeChunkHash] = FastChunkSerializer::deserializeTerrain($c); - } - } - - $onCompletion($chunk, $adjacentChunks); - } +class PopulationTask extends AsyncTask { + private const TLS_KEY_ON_COMPLETION = "onCompletion"; + + private ?string $centerChunk; + private string $adjacentChunksData; + + public function __construct( + private int $worldId, + private int $chunkX, + private int $chunkZ, + ?Chunk $chunk, + array $adjacentChunks, // array + \Closure $onCompletion // fn(Chunk, array): void + ) { + $this->centerChunk = $chunk?->serializeTerrain(); + $this->adjacentChunksData = igbinary_serialize(array_map( + fn(?Chunk $c) => $c?->serializeTerrain(), + $adjacentChunks + )); + $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + } + + public function onRun(): void { + $context = ThreadLocalGeneratorContext::fetch($this->worldId) + ?? throw new AssumptionFailedError("Generator context not initialized"); + + $gen = $context->getGenerator(); + $mgr = new SimpleChunkManager($context->getWorldMinY(), $context->getWorldMaxY()); + + $center = $this->centerChunk !== null ? FastChunkSerializer::deserializeTerrain($this->centerChunk) : null; + $adjacent = igbinary_unserialize($this->adjacentChunksData); + $adjChunks = []; + + foreach ($adjacent as $hash => $data) { + $adjChunks[$hash] = $data !== null + ? FastChunkSerializer::deserializeTerrain($data)->clearTerrainDirtyFlags() + : null; + } + + self::setOrGenerate($mgr, $gen, $this->chunkX, $this->chunkZ, $center); + + foreach ($adjChunks as $hash => $chunkObj) { + [$dx, $dz] = World::getXZ($hash); + $adjChunks[$hash] = self::setOrGenerate($mgr, $gen, $this->chunkX + $dx, $this->chunkZ + $dz, $chunkObj); + } + + $gen->populateChunk($mgr, $this->chunkX, $this->chunkZ); + + $popChunk = $mgr->getChunk($this->chunkX, $this->chunkZ) + ?? throw new AssumptionFailedError("Generated chunk missing"); + $popChunk->setPopulated(); + + $this->centerChunk = FastChunkSerializer::serializeTerrain($popChunk); + $this->adjacentChunksData = igbinary_serialize(array_map( + fn(Chunk $c) => $c->isTerrainDirty() ? FastChunkSerializer::serializeTerrain($c) : null, + $adjChunks + )); + } + + private static function setOrGenerate(SimpleChunkManager $mgr, $gen, int $x, int $z, ?Chunk $chunk): Chunk { + $mgr->setChunk($x, $z, $chunk ?? new Chunk([], false)); + if ($chunk === null) { + $gen->generateChunk($mgr, $x, $z); + return $mgr->getChunk($x, $z) + ?? throw new AssumptionFailedError("Generated chunk missing"); + } + return $chunk; + } + + public function onCompletion(): void { + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + $center = FastChunkSerializer::deserializeTerrain($this->centerChunk); + $adjData = igbinary_unserialize($this->adjacentChunksData); + $readyAdj = []; + + foreach ($adjData as $hash => $data) { + if ($data !== null) { + $readyAdj[$hash] = FastChunkSerializer::deserializeTerrain($data); + } + } + + $callback($center, $readyAdj); + } } diff --git a/src/world/light/LightPopulationTask.php b/src/world/light/LightPopulationTask.php index 29d95783115..1f2e763ae14 100644 --- a/src/world/light/LightPopulationTask.php +++ b/src/world/light/LightPopulationTask.php @@ -1,24 +1,4 @@ $blockLight, array $skyLight, non-empty-list $heightMap) : void $onCompletion - */ - public function __construct(Chunk $chunk, \Closure $onCompletion){ - $this->chunk = FastChunkSerializer::serializeTerrain($chunk); - $this->storeLocal(self::TLS_KEY_COMPLETION_CALLBACK, $onCompletion); - } - - public function onRun() : void{ - $chunk = FastChunkSerializer::deserializeTerrain($this->chunk); - - $manager = new SimpleChunkManager(World::Y_MIN, World::Y_MAX); - $manager->setChunk(0, 0, $chunk); - - $blockFactory = RuntimeBlockStateRegistry::getInstance(); - foreach([ - "Block" => new BlockLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->light), - "Sky" => new SkyLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight), - ] as $name => $update){ - $update->recalculateChunk(0, 0); - $update->execute(); - } - - $chunk->setLightPopulated(); - - $this->resultHeightMap = igbinary_serialize($chunk->getHeightMapArray()); - $skyLightArrays = []; - $blockLightArrays = []; - foreach($chunk->getSubChunks() as $y => $subChunk){ - $skyLightArrays[$y] = $subChunk->getBlockSkyLightArray(); - $blockLightArrays[$y] = $subChunk->getBlockLightArray(); - } - $this->resultSkyLightArrays = igbinary_serialize($skyLightArrays); - $this->resultBlockLightArrays = igbinary_serialize($blockLightArrays); - } - - public function onCompletion() : void{ - /** - * @var int[] $heightMapArray - * @phpstan-var non-empty-list $heightMapArray - */ - $heightMapArray = igbinary_unserialize($this->resultHeightMap); - - /** @var LightArray[] $skyLightArrays */ - $skyLightArrays = igbinary_unserialize($this->resultSkyLightArrays); - /** @var LightArray[] $blockLightArrays */ - $blockLightArrays = igbinary_unserialize($this->resultBlockLightArrays); - - /** - * @var \Closure - * @phpstan-var \Closure(array $blockLight, array $skyLight, non-empty-list $heightMap) : void - */ - $callback = $this->fetchLocal(self::TLS_KEY_COMPLETION_CALLBACK); - $callback($blockLightArrays, $skyLightArrays, $heightMapArray); - } +class LightPopulationTask extends AsyncTask { + private const TLS_KEY_ON_COMPLETION = "onCompletion"; + + private string $chunkSerialized; + private string $heightMapData; + private string $skyLightData; + private string $blockLightData; + + /** + * @param \Closure(array, array, non-empty-list) $onCompletion + */ + public function __construct(Chunk $chunk, \Closure $onCompletion) { + $this->chunkSerialized = FastChunkSerializer::serializeTerrain($chunk); + $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + } + + public function onRun(): void { + $chunk = FastChunkSerializer::deserializeTerrain($this->chunkSerialized); + $manager = new SimpleChunkManager(World::Y_MIN, World::Y_MAX); + $manager->setChunk(0, 0, $chunk); + + $registry = RuntimeBlockStateRegistry::getInstance(); + $explorer = new SubChunkExplorer($manager); + + foreach ([ + function() use ($explorer, $registry): void { + $update = new BlockLightUpdate($explorer, $registry->lightFilter, $registry->light); + $update->recalculateChunk(0, 0); + $update->execute(); + }, + function() use ($explorer, $registry): void { + $update = new SkyLightUpdate($explorer, $registry->lightFilter, $registry->blocksDirectSkyLight); + $update->recalculateChunk(0, 0); + $update->execute(); + } + ] as $perform) { + $perform(); + } + + $chunk->setLightPopulated(); + + $this->heightMapData = igbinary_serialize($chunk->getHeightMapArray()); + $this->skyLightData = igbinary_serialize(array_map(fn($s) => $s->getBlockSkyLightArray(), $chunk->getSubChunks())); + $this->blockLightData = igbinary_serialize(array_map(fn($s) => $s->getBlockLightArray(), $chunk->getSubChunks())); + } + + public function onCompletion(): void { + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + $callback( + igbinary_unserialize($this->blockLightData), + igbinary_unserialize($this->skyLightData), + igbinary_unserialize($this->heightMapData) + ); + } } From e98e2423f2e8b16f3542a9eceb8a3c16d109b7f3 Mon Sep 17 00:00:00 2001 From: KanadeBlue <124839201+KanadeBlue@users.noreply.github.com> Date: Tue, 17 Jun 2025 03:48:59 +0000 Subject: [PATCH 43/43] need for better profiling --- src/scheduler/TaskScheduler.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php index 4c64130bde8..efb704634b3 100644 --- a/src/scheduler/TaskScheduler.php +++ b/src/scheduler/TaskScheduler.php @@ -60,6 +60,11 @@ public function scheduleDelayedRepeatingTask(Task $task, int $delay, int $period return $this->addTask($task, $delay, $period); } + public function getTasks(): array { + return $this->tasks->toArray(); + } + + /** * Cancel all scheduled tasks. */