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 diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 6199ad7a9e0..a3921f8201f 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 @@ -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.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.15.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.15.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.15.0 + uses: docker/build-push-action@v6.18.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 131c0dde258..20b2200e6dd 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,13 +43,13 @@ 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 - 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 d2e9eb0d0e5..fa20d19128a 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,12 +54,12 @@ 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 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 }} @@ -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 @@ -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-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..cabda54be89 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 @@ -28,10 +28,10 @@ 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 + php-version: 8.3 + tools: php-cs-fixer:3.75 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -40,7 +40,7 @@ jobs: shellcheck: name: ShellCheck - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false diff --git a/.github/workflows/pr-remove-waiting-label.yml b/.github/workflows/pr-remove-waiting-label.yml index eb46043bdd2..b7cd85acdaf 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; + async 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"); 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 }} 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/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/changelogs/5.28.md b/changelogs/5.28.md new file mode 100644 index 00000000000..f378031f7b8 --- /dev/null +++ b/changelogs/5.28.md @@ -0,0 +1,34 @@ +# 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 +- Added support for Minecraft: Bedrock Edition 1.21.80. +- 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. + +# 5.28.1 +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/composer.json b/composer.json index 8da46090b72..d2064bcd610 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.1.0+bedrock-1.21.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", @@ -45,18 +45,22 @@ "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", "symfony/filesystem": "~6.4.0" }, "require-dev": { - "phpstan/phpstan": "2.1.8", + "phpstan/phpstan": "2.1.16", "phpstan/phpstan-phpunit": "^2.0.0", "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-mbstring": "*" + }, "autoload": { "psr-4": { "pocketmine\\": "src/" diff --git a/composer.lock b/composer.lock index 405a9241426..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": "28b4de9a23a293646dbad2707cdfd9e0", + "content-hash": "fe62caebfdb35cd8bd57c8e61879b7c0", "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.1.0+bedrock-1.21.80", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "7091dad2c12ed4a4106432df21fc698960c6be9e" + "reference": "a1fa215563517050045309bb779a67f75843b867" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/7091dad2c12ed4a4106432df21fc698960c6be9e", - "reference": "7091dad2c12ed4a4106432df21fc698960c6be9e", + "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/37.0.0+bedrock-1.21.70" + "source": "https://github.com/pmmp/BedrockProtocol/tree/38.1.0+bedrock-1.21.80" }, - "time": "2025-03-27T15:19:36+00:00" + "time": "2025-05-28T22:19:59+00:00" }, { "name": "pocketmine/binaryutils", @@ -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", @@ -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", @@ -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", @@ -973,180 +973,21 @@ } ], "time": "2024-10-25T15:07:50+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.31.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.31.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" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "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.31.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": [ { "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 +1026,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 +1034,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 +1214,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.8", + "version": "2.1.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f" + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", "shasum": "" }, "require": { @@ -1427,20 +1268,20 @@ "type": "github" } ], - "time": "2025-03-09T09:30:48+00:00" + "time": "2025-05-16T09:40:10+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 +1292,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 +1319,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", @@ -1851,16 +1694,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": { @@ -1870,7 +1713,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", @@ -1932,7 +1775,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": [ { @@ -1943,12 +1786,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", @@ -2919,7 +2770,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2950,7 +2801,7 @@ "ext-zlib": ">=1.2.11", "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.0" }, 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/src/VersionInfo.php b/src/VersionInfo.php index 0f776dc1d20..4e35b71bd99 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -30,9 +30,9 @@ use function str_repeat; final class VersionInfo{ - public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.27.1"; - public const IS_DEVELOPMENT_BUILD = true; + public const NAME = "STCraft-MP"; + public const BASE_VERSION = "5.28.1"; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** 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..7fa93b27496 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,27 +850,30 @@ 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)); @@ -874,6 +881,8 @@ public function getBreakTime(Item $item) : float{ 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("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 +1047,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 +1111,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 +1228,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 +1239,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 +1273,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 +1334,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 +1357,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 +1366,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 +1385,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 +1403,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 +1415,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 +1543,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 +1570,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 +1581,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 +1654,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 +1679,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 +1696,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 +1772,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 +1790,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 +1803,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 +1825,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/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/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/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/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/entity/Entity.php b/src/entity/Entity.php index e24c6067cf0..6681558adac 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; @@ -76,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; @@ -700,9 +702,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; 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 @@ + */ + 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..37bbe41ac4d --- /dev/null +++ b/src/form/FormAPI/Form.php @@ -0,0 +1,74 @@ + + */ + 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/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; } } } 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()); 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/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/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..927ba38fa53 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; @@ -212,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); @@ -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/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. 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); diff --git a/src/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php index 3e2b089c6c5..efb704634b3 100644 --- a/src/scheduler/TaskScheduler.php +++ b/src/scheduler/TaskScheduler.php @@ -1,189 +1,199 @@ > */ - 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); + } + + public function getTasks(): array { + return $this->tasks->toArray(); + } + + + /** + * Cancel all scheduled tasks. + */ + public function cancelAllTasks(): void { + foreach ($this->tasks as $task) { + $task->cancel(); + } + $this->tasks->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 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); 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); } } 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); + } + } } 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){ diff --git a/src/world/World.php b/src/world/World.php index 3a7d0c538b4..c1de8e8ceb9 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; @@ -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/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 @@ +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/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php index 5b19457392b..d39c17a4711 100644 --- a/src/world/format/io/data/BedrockWorldData.php +++ b/src/world/format/io/data/BedrockWorldData.php @@ -51,11 +51,11 @@ class BedrockWorldData extends BaseNbtWorldData{ public const CURRENT_STORAGE_VERSION = 10; - public const CURRENT_STORAGE_NETWORK_VERSION = 786; + public const CURRENT_STORAGE_NETWORK_VERSION = 800; public const CURRENT_CLIENT_VERSION_TARGET = [ 1, //major 21, //minor - 70, //patch + 80, //patch 3, //revision 0 //is beta ]; 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) + ); + } } 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; } } diff --git a/tools/generate-bedrock-data-from-packets.php b/tools/generate-bedrock-data-from-packets.php index 50639f51daf..b0aae57df2a 100644 --- a/tools/generate-bedrock-data-from-packets.php +++ b/tools/generate-bedrock-data-from-packets.php @@ -54,7 +54,6 @@ use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\StartGamePacket; -use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData; @@ -76,6 +75,8 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; +use pocketmine\world\biome\model\BiomeDefinitionEntryData; +use pocketmine\world\biome\model\ColorData; use pocketmine\world\format\io\GlobalBlockStateHandlers; use Ramsey\Uuid\Exception\InvalidArgumentException; use Symfony\Component\Filesystem\Path; @@ -100,6 +101,7 @@ use function ksort; use function mkdir; use function ord; +use function round; use function strlen; use const FILE_IGNORE_NEW_LINES; use const JSON_PRETTY_PRINT; @@ -572,34 +574,34 @@ public function handleAvailableActorIdentifiers(AvailableActorIdentifiersPacket public function handleBiomeDefinitionList(BiomeDefinitionListPacket $packet) : bool{ echo "storing biome definitions" . PHP_EOL; - file_put_contents($this->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; }