diff --git a/.github/ISSUE_TEMPLATE/api-change-request.md b/.github/ISSUE_TEMPLATE/api-change-request.md deleted file mode 100644 index e3d24ea0f..000000000 --- a/.github/ISSUE_TEMPLATE/api-change-request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: API change request -about: Suggest a change, addition or removal to the plugin API -title: '' -labels: '' -assignees: '' - ---- - - -## Problem description - - - -## Proposed solution - - - -## Alternative solutions that don't require API changes diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..209ba8880 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,87 @@ +name: Bug report +description: Report a feature of PocketMine-MP not working as expected +body: + - type: markdown + attributes: + value: | + ## Plugin information + + > [!IMPORTANT] + > It's strongly recommended to test for bugs without plugins before reporting an issue. + > This helps avoid wasting maintainers' time on bugs that are not actually caused by PocketMine-MP. + > + > If you're not sure whether a plugin might be causing your issue, please seek help on our [Discord](https://discord.gg/bmSAZBG) before writing an issue. + + - type: dropdown + attributes: + label: Plugin information + options: + - "I haven't tested without plugins" + - Bug happens without plugins + - Bug only happens with certain plugins (describe below) + validations: + required: true + + - type: markdown + attributes: + value: | + ## Bug description + + > [!TIP] + > Helpful information to include: + > - Steps to reproduce the issue + > - Error backtraces + > - Crashdumps + > - Plugin code that triggers the issue + > - List of installed plugins (use /plugins) + + > [!IMPORTANT] + > **Steps to reproduce are critical to finding the cause of the problem!** + > Without reproducing steps, the issue will probably not be solvable and may be closed. + + - type: textarea + attributes: + label: Problem description + description: Describe the problem, and how you encountered it + placeholder: e.g. Steps to reproduce the issue + validations: + required: true + - type: textarea + attributes: + label: Expected behaviour + description: What did you expect to happen? + validations: + required: true + + - type: markdown + attributes: + value: | + ## Version, OS and game info + > [!WARNING] + > "Latest" is not a valid version. + > Failure to fill these fields with valid information may result in your issue being closed. + + - type: input + attributes: + label: PocketMine-MP version + placeholder: Use the /version command in PocketMine-MP + validations: + required: true + - type: input + attributes: + label: PHP version + placeholder: Use the /version command in PocketMine-MP + validations: + required: true + - type: input + attributes: + label: Server OS + placeholder: Use the /version command in PocketMine-MP + validations: + required: true + - type: input + attributes: + label: Game version (if applicable) + placeholder: e.g. Android, iOS, Windows, Xbox, PS4, Switch + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 730d6e811..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Bug report -about: Unexpected non-crash behaviour (except missing gameplay features) -title: '' -labels: '' -assignees: '' - ---- - -### Issue description - -- Expected result: What were you expecting to happen? -- Actual result: What actually happened? - -### Steps to reproduce the issue -1. ... -2. ... - -### OS and versions - -* PocketMine-MP: -* PHP: -* Using JIT: yes/no (delete as appropriate) -* Server OS: -* Game version: Android/iOS/Win10/Xbox/PS4/Switch (delete as appropriate) - -### Plugins - - -- If you remove all plugins, does the issue still occur? -- If the issue is **not** reproducible without plugins: - - Have you asked for help on our forums before creating an issue? - - Can you provide sample, *minimal* reproducing code for the issue? If so, paste it in the bottom section - -### Crashdump, backtrace or other files - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d13fb4498..d18b277e7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,9 +3,6 @@ contact_links: - name: Help & support on Discord url: https://discord.gg/bmSAZBG about: We don't accept support requests on the issue tracker. Please try asking on Discord instead. - - name: Help & support on forums - url: https://forums.pmmp.io - about: We don't accept support requests on the issue tracker. Please try asking on forums instead. - name: Documentation url: https://pmmp.rtfd.io about: PocketMine-MP documentation diff --git a/.github/ISSUE_TEMPLATE/crash.md b/.github/ISSUE_TEMPLATE/crash.md deleted file mode 100644 index ee91d230e..000000000 --- a/.github/ISSUE_TEMPLATE/crash.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Crash -about: Report a crash in PocketMine-MP (not plugins) -title: Server crashed -labels: '' -assignees: '' - ---- - - - - -Link to crashdump: - - -### Additional comments (optional) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 000000000..735255de2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,25 @@ +name: Crash +description: Report a crash in PocketMine-MP (not plugins) +title: Server crashed +body: + - type: markdown + attributes: + value: | + > [!TIP] + > Submit crashdump `.log` files to the [Crash Archive](https://crash.pmmp.io/submit). + > If you can't submit the crashdump to the Crash Archive, paste it on a site like [GitHub Gist](https://gist.github.com) or [Pastebin](https://pastebin.com). + + > [!CAUTION] + > DON'T paste the crashdump data directly into an issue. + + - type: input + id: crashdump-url + attributes: + label: Link to crashdump + validations: + required: true + + - type: textarea + attributes: + label: Additional comments (optional) + description: Any other information that might help us solve the problem diff --git a/.github/ISSUE_TEMPLATE/feature-proposal.yml b/.github/ISSUE_TEMPLATE/feature-proposal.yml new file mode 100644 index 000000000..e0d37ef06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-proposal.yml @@ -0,0 +1,19 @@ +name: Feature addition, change, or removal +description: Propose adding new features, or changing/removing existing ones +body: + - type: textarea + attributes: + label: Problem description + description: Explain why a change is needed + validations: + required: true + - type: textarea + attributes: + label: Proposed solution + description: Describe what changes you think should be made + validations: + required: true + - type: textarea + attributes: + label: "Alternative solutions or workarounds" + description: "Describe other ways you've explored to achieve your goal" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 97607ab8f..ded96ca5d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,10 @@ updates: update-types: - "version-update:semver-major" - "version-update:semver-minor" + + #since we lock this to exact versions, it causes conflicts with minor-next & major-next in composer.lock + #better to just test updates to this locally anyway since almost every version breaks something + - dependency-name: phpstan/phpstan groups: production-patch-updates: dependency-type: production @@ -37,4 +41,7 @@ updates: - package-ecosystem: github-actions directory: "/" schedule: - interval: weekly + interval: monthly + groups: + github-actions: + patterns: ["*"] diff --git a/.github/workflows/branch-sync-cron-trigger.yml b/.github/workflows/branch-sync-cron-trigger.yml new file mode 100644 index 000000000..896141d83 --- /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 21 * * *" #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 4325c63f2..acfc3d3a7 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -4,11 +4,16 @@ on: release: types: - published + workflow_dispatch: + inputs: + release: + description: 'Tag name to build' + required: true jobs: build: name: Update Docker Hub images - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Set up Docker Buildx @@ -28,16 +33,28 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Clone pmmp/PocketMine-Docker repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pmmp/PocketMine-Docker fetch-depth: 1 - - name: Get tag names + + - name: Get tag name id: tag-name run: | - VERSION=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{') - echo TAG_NAME=$VERSION >> $GITHUB_OUTPUT + if [[ "${{ github.event_name }}" == "release" ]]; then + echo TAG_NAME="${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo TAG_NAME="${{ github.event.inputs.release }}" >> $GITHUB_OUTPUT + else + echo "Unsupported event type: ${{ github.event_name }}" + exit 1 + fi + + - name: Parse version + id: version + run: | + VERSION="${{ steps.tag-name.outputs.TAG_NAME }}" echo MAJOR=$(echo $VERSION | cut -d. -f1) >> $GITHUB_OUTPUT echo MINOR=$(echo $VERSION | cut -d. -f1-2) >> $GITHUB_OUTPUT @@ -53,7 +70,7 @@ jobs: run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT - name: Build image for tag - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp @@ -66,33 +83,33 @@ jobs: - name: Build image for major tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp tags: | - ${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MAJOR }} - ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MAJOR }} + ${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.version.outputs.MAJOR }} + ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.version.outputs.MAJOR }} build-args: | PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }} PMMP_REPO=${{ github.repository }} - name: Build image for minor tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp tags: | - ${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MINOR }} - ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MINOR }} + ${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.version.outputs.MINOR }} + ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.version.outputs.MINOR }} build-args: | PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }} PMMP_REPO=${{ github.repository }} - name: Build image for latest tag if: steps.channel.outputs.CHANNEL == 'stable' - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.18.0 with: push: true context: ./pocketmine-mp diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..c644876e0 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,47 @@ +name: "Copilot Agent environment setup" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@v5 + + - name: Setup PHP + uses: pmmp/setup-php-action@3.2.0 + with: + php-version: 8.3 + install-path: "./bin" + pm-version-major: 5 + + - name: Restore Composer package cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/composer/files + ~/.cache/composer/vcs + key: "composer-v2-cache-8.3-${{ hashFiles('./composer.lock') }}" + restore-keys: | + composer-v2-cache- + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction + + - name: Clone extension stubs + uses: actions/checkout@v5 + with: + repository: pmmp/phpstorm-stubs + path: extension-stubs diff --git a/.github/workflows/discord-release-notify.yml b/.github/workflows/discord-release-notify.yml index 8d0add224..76780d633 100644 --- a/.github/workflows/discord-release-notify.yml +++ b/.github/workflows/discord-release-notify.yml @@ -4,16 +4,21 @@ on: release: types: - published + workflow_dispatch: + inputs: + release: + description: 'Release to make notification for' + required: true jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.31.1 + uses: shivammathur/setup-php@2.35.4 with: php-version: 8.2 @@ -30,9 +35,17 @@ jobs: - name: Install Composer dependencies run: composer install --no-dev --prefer-dist --no-interaction --ignore-platform-reqs - - name: Get actual tag name + - name: Get tag name id: tag-name - run: echo TAG_NAME=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{') >> $GITHUB_OUTPUT + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo TAG_NAME="${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo TAG_NAME="${{ github.event.inputs.release }}" >> $GITHUB_OUTPUT + else + echo "Unsupported event type: ${{ github.event_name }}" + exit 1 + fi - name: Run webhook post script run: php .github/workflows/discord-release-embed.php ${{ github.repository }} ${{ steps.tag-name.outputs.TAG_NAME }} ${{ github.token }} ${{ secrets.DISCORD_RELEASE_WEBHOOK }} ${{ secrets.DISCORD_NEWS_PING_ROLE_ID }} diff --git a/.github/workflows/draft-release-from-pr.yml b/.github/workflows/draft-release-from-pr.yml deleted file mode 100644 index 8a347853b..000000000 --- a/.github/workflows/draft-release-from-pr.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Draft release from PR - -on: - #presume that pull_request_target is safe at this point, since the PR was approved and merged - #we need write access to prepare the release & create comments - pull_request_target: - types: - - closed - branches: - - stable - - minor-next - - major-next - - "legacy/*" - paths: - - "src/VersionInfo.php" - -jobs: - check: - name: Check release - uses: ./.github/workflows/draft-release-pr-check.yml - - draft: - name: Create GitHub draft release - needs: [check] - if: needs.check.outputs.valid == 'true' - - uses: ./.github/workflows/draft-release.yml - - post-draft-url-comment: - name: Post draft release URL as comment - needs: [draft] - - runs-on: ubuntu-20.04 - - steps: - - name: Post draft release URL on PR - uses: thollander/actions-comment-pull-request@v3 - with: - message: "[Draft release ${{ needs.draft.outputs.version }}](${{ needs.draft.outputs.draft-url }}) has been created for commit ${{ github.sha }}. Please review and publish it." - - trigger-post-release-workflow: - name: Trigger post-release RestrictedActions workflow - # Not sure if needs is actually needed here - needs: [check] - if: needs.check.outputs.valid == 'true' - - runs-on: ubuntu-20.04 - - steps: - - name: Generate access token - id: generate-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} - private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} - owner: ${{ github.repository_owner }} - repositories: RestrictedActions - - - name: Dispatch post-release restricted action - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ steps.generate-token.outputs.token }} - repository: ${{ github.repository_owner }}/RestrictedActions - event-type: pocketmine_mp_post_release - client-payload: '{"branch": "${{ github.ref }}"}' diff --git a/.github/workflows/draft-release-from-tag.yml b/.github/workflows/draft-release-from-tag.yml deleted file mode 100644 index f7a5df544..000000000 --- a/.github/workflows/draft-release-from-tag.yml +++ /dev/null @@ -1,13 +0,0 @@ -#Allows creating a release by pushing a tag -#This might be useful for retroactive releases -name: Draft release from git tag - -on: - push: - tags: "*" - -jobs: - draft: - name: Create GitHub draft release - if: "startsWith(github.event.head_commit.message, 'Release ')" - uses: ./.github/workflows/draft-release.yml diff --git a/.github/workflows/draft-release-pr-check.yml b/.github/workflows/draft-release-pr-check.yml index 4c8d0f685..9905d843d 100644 --- a/.github/workflows/draft-release-pr-check.yml +++ b/.github/workflows/draft-release-pr-check.yml @@ -24,13 +24,13 @@ 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' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check IS_DEVELOPMENT_BUILD flag id: validate @@ -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 + - uses: actions/checkout@v5 - name: Setup PHP - uses: shivammathur/setup-php@2.31.1 + uses: shivammathur/setup-php@2.35.4 with: php-version: 8.2 diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index cd1841e4f..6edf05dfb 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -1,38 +1,95 @@ name: Draft release on: - workflow_call: - outputs: - draft-url: - description: 'The URL of the draft release' - value: ${{ jobs.draft.outputs.draft-url }} - version: - description: 'PocketMine-MP version' - value: ${{ jobs.draft.outputs.version }} + #presume that pull_request_target is safe at this point, since the PR was approved and merged + #we need write access to prepare the release & create comments + pull_request_target: + types: + - closed + branches: + - stable + - minor-next + - major-next + - "legacy/*" + paths: + - "src/VersionInfo.php" + push: + tags: + - "*" + +env: + PHP_VERSION: "8.2" jobs: - draft: - name: Create GitHub draft release - - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - php-version: [8.2] + skip: + name: Check whether to ignore this tag + runs-on: ubuntu-22.04 outputs: - draft-url: ${{ steps.create-draft.outputs.html_url }} - version: ${{ steps.get-pm-version.outputs.PM_VERSION }} + skip: ${{ steps.exists.outputs.exists == 'true' }} steps: - - uses: actions/checkout@v4 + - name: Check if release already exists + id: exists + env: + GH_TOKEN: ${{ github.token }} + run: | + exists=false + if [[ "${{ github.ref_type }}" == "tag" ]]; then + tag="$(echo "${{ github.ref }}" | cut -d/ -f3-)" + if gh release view "$tag" --repo "${{ github.repository }}"; then + exists=true + fi + fi + echo exists=$exists >> $GITHUB_OUTPUT + + check: + needs: [skip] + if: needs.skip.outputs.skip != 'true' + name: Check release + uses: ./.github/workflows/draft-release-pr-check.yml + + trigger-post-release-workflow: + name: Trigger post-release RestrictedActions workflow + needs: [check] + if: needs.check.outputs.valid == 'true' && github.ref_type != 'tag' #can't do post-commit for a tag + + 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 post-release restricted action + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.generate-token.outputs.token }} + repository: ${{ github.repository_owner }}/RestrictedActions + event-type: pocketmine_mp_post_release + client-payload: '{"branch": "${{ github.ref }}"}' + + draft: + name: Create GitHub draft release + needs: [check] + if: needs.check.outputs.valid == 'true' || github.ref_type == 'tag' #ignore validity check for tags + + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v5 with: submodules: true - name: Setup PHP - uses: shivammathur/setup-php@2.31.1 + uses: shivammathur/setup-php@2.35.4 with: - php-version: ${{ matrix.php-version }} + php-version: ${{ env.PHP_VERSION }} - name: Restore Composer package cache uses: actions/cache@v4 @@ -50,7 +107,7 @@ jobs: - name: Calculate build number id: build-number run: | - BUILD_NUMBER=$((2000+$GITHUB_RUN_NUMBER)) #to stay above jenkins + BUILD_NUMBER=$((2300+$GITHUB_RUN_NUMBER)) #to stay above jenkins echo "Build number: $BUILD_NUMBER" echo BUILD_NUMBER=$BUILD_NUMBER >> $GITHUB_OUTPUT @@ -63,23 +120,31 @@ jobs: - name: Get PocketMine-MP release version id: get-pm-version run: | - echo PM_VERSION=$(php build/dump-version-info.php base_version) >> $GITHUB_OUTPUT + PM_VERSION=$(php build/dump-version-info.php base_version) + echo PM_VERSION=$PM_VERSION >> $GITHUB_OUTPUT echo PM_MAJOR=$(php build/dump-version-info.php major_version) >> $GITHUB_OUTPUT echo MCPE_VERSION=$(php build/dump-version-info.php mcpe_version) >> $GITHUB_OUTPUT echo CHANGELOG_FILE_NAME=$(php build/dump-version-info.php changelog_file_name) >> $GITHUB_OUTPUT echo CHANGELOG_MD_HEADER=$(php build/dump-version-info.php changelog_md_header) >> $GITHUB_OUTPUT echo PRERELEASE=$(php build/dump-version-info.php prerelease) >> $GITHUB_OUTPUT + if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then + tag="$(echo "${{ github.ref }}" | cut -d/ -f3-)" + else + tag="$PM_VERSION" + fi + echo TAG_NAME=$tag >> $GITHUB_OUTPUT + - name: Generate PHP binary download URL id: php-binary-url run: | - echo PHP_BINARY_URL="${{ github.server_url }}/${{ github.repository_owner }}/PHP-Binaries/releases/tag/pm${{ steps.get-pm-version.outputs.PM_MAJOR }}-php-${{ matrix.php-version }}-latest" >> $GITHUB_OUTPUT + echo PHP_BINARY_URL="${{ github.server_url }}/${{ github.repository_owner }}/PHP-Binaries/releases/tag/pm${{ steps.get-pm-version.outputs.PM_MAJOR }}-php-${{ env.PHP_VERSION }}-latest" >> $GITHUB_OUTPUT - name: Generate build info run: | php build/generate-build-info-json.php \ ${{ github.sha }} \ - ${{ steps.get-pm-version.outputs.PM_VERSION }} \ + ${{ steps.get-pm-version.outputs.TAG_NAME }} \ ${{ github.repository }} \ ${{ steps.build-number.outputs.BUILD_NUMBER }} \ ${{ github.run_id }} \ @@ -100,7 +165,7 @@ jobs: ${{ github.workspace }}/core-permissions.rst - name: Create draft release - uses: ncipollo/release-action@v1.14.0 + uses: ncipollo/release-action@v1.18.0 id: create-draft with: artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst @@ -108,12 +173,19 @@ jobs: draft: true prerelease: ${{ steps.get-pm-version.outputs.PRERELEASE }} name: PocketMine-MP ${{ steps.get-pm-version.outputs.PM_VERSION }} - tag: ${{ steps.get-pm-version.outputs.PM_VERSION }} + tag: ${{ steps.get-pm-version.outputs.TAG_NAME }} token: ${{ secrets.GITHUB_TOKEN }} - skipIfReleaseExists: true #for release PRs, tags will be created on release publish and trigger the tag release workflow - don't create a second draft body: | **For Minecraft: Bedrock Edition ${{ steps.get-pm-version.outputs.MCPE_VERSION }}** Please see the [changelogs](${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.get-pm-version.outputs.PM_VERSION }}/changelogs/${{ steps.get-pm-version.outputs.CHANGELOG_FILE_NAME }}#${{ steps.get-pm-version.outputs.CHANGELOG_MD_HEADER }}) for details. :information_source: Download the recommended PHP binary [here](${{ steps.php-binary-url.outputs.PHP_BINARY_URL }}). + + :warning: Found a bug? Report it on our [issue tracker](${{ github.server_url }}/${{ github.repository }}/issues). **We can't fix bugs if you don't report them.** + + - name: Post draft release URL on PR + if: github.event_name == 'pull_request_target' + uses: thollander/actions-comment-pull-request@v3 + with: + message: "${{ vars.DRAFT_RELEASE_NOTIFICATION_MENTION }} [Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it." diff --git a/.github/workflows/main-php-matrix.yml b/.github/workflows/main-php-matrix.yml index e26f7c318..7637a3956 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: @@ -27,7 +27,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP uses: pmmp/setup-php-action@3.2.0 @@ -59,7 +59,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP uses: pmmp/setup-php-action@3.2.0 @@ -91,7 +91,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true @@ -125,7 +125,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP uses: pmmp/setup-php-action@3.2.0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b5a9740b5..a7d6eb0fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: ["8.1", "8.2", "8.3"] + php: ["8.1", "8.2", "8.3", "8.4"] uses: ./.github/workflows/main-php-matrix.yml with: @@ -20,18 +20,18 @@ jobs: codestyle: name: Code Style checks - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.31.1 + uses: shivammathur/setup-php@2.35.4 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,12 +40,12 @@ jobs: shellcheck: name: ShellCheck - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run ShellCheck uses: ludeeus/action-shellcheck@2.0.0 diff --git a/.github/workflows/pr-remove-waiting-label.yml b/.github/workflows/pr-remove-waiting-label.yml index eb46043bd..b7cd85acd 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/pr-stale.yml b/.github/workflows/pr-stale.yml new file mode 100644 index 000000000..23518b2cf --- /dev/null +++ b/.github/workflows/pr-stale.yml @@ -0,0 +1,29 @@ +name: 'Clean up stale PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: -1 + days-before-issue-close: -1 + stale-pr-message: | + This PR has been marked as "Waiting on Author", but we haven't seen any activity in 7 days. + + If there is no further activity, it will be closed in 28 days. + + Note for maintainers: Adding an assignee to the PR will prevent it from being marked as stale. + + close-pr-message: | + As this PR hasn't been updated for a while, unfortunately we'll have to close it. + + days-before-pr-stale: 7 + days-before-pr-close: 28 + only-labels: "Status: Waiting on Author" + close-pr-label: "Resolution: Abandoned" + exempt-all-assignees: true + diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml index 68da365cb..3a65f78a4 100644 --- a/.github/workflows/support.yml +++ b/.github/workflows/support.yml @@ -20,10 +20,7 @@ jobs: - Check our [Documentation](https://doc.pmmp.io) to see if you can find answers there - - Ask the community on our [Discord server](https://discord.gg/bmSAZBG) or our [Forums](https://forums.pmmp.io) - - - [Docs](https://pmmp.rtfd.io) | [Discord](https://discord.gg/bmSAZBG) | [Forums](https://forums.pmmp.io) + - Ask the community on our [Discord server](https://discord.gg/bmSAZBG) close-issue: true lock-issue: false diff --git a/.github/workflows/team-pr-auto-approve.yml b/.github/workflows/team-pr-auto-approve.yml index 0c2fdd81c..cc5c47139 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/.github/workflows/update-updater-api.yml b/.github/workflows/update-updater-api.yml index 3f42062fd..841fa7d44 100644 --- a/.github/workflows/update-updater-api.yml +++ b/.github/workflows/update-updater-api.yml @@ -4,6 +4,11 @@ on: release: types: - published + workflow_dispatch: + inputs: + release: + description: 'Release to publish info for' + required: true jobs: build: @@ -14,14 +19,22 @@ jobs: - name: Install jq run: sudo apt update && sudo apt install jq -y - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: repository: ${{ github.repository_owner }}/update.pmmp.io ssh-key: ${{ secrets.UPDATE_PMMP_IO_DEPLOY_KEY }} - - name: Get actual tag name + - name: Get tag name id: tag-name - run: echo TAG_NAME=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{') >> $GITHUB_OUTPUT + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo TAG_NAME="${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo TAG_NAME="${{ github.event.inputs.release }}" >> $GITHUB_OUTPUT + else + echo "Unsupported event type: ${{ github.event_name }}" + exit 1 + fi - name: Download new release information run: curl -f -L ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.tag-name.outputs.TAG_NAME }}/build_info.json -o new_build_info.json diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 32af1ef48..5a14b1d35 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -6,6 +6,12 @@ $finder = PhpCsFixer\Finder::create() ->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/CONTRIBUTING.md b/CONTRIBUTING.md index 3490542c5..799c9d99c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,7 +123,7 @@ The following are required as a minimum for pull requests. PRs that don't meet t - Remember, PRs with small diffs are much easier to review. Small PRs are generally reviewed and merged much faster than large ones. - **Start small.** Try fixing minor bugs or doing something isolated (e.g. adding a new block or item) before attempting larger changes. - This helps you get familiar with the codebase, the contribution process, and the expectations of maintainers. - - Check out the [issues page]() for something that you could tackle without too much effort. + - Check out ["Easy task" issues](https://github.com/pmmp/PocketMine-MP/issues?q=is%3Aissue+is%3Aopen+label%3A%22Easy+task%22) on the issues page for something that you could tackle without too much effort. - **Do not copy-paste other people's code**. Many PRs involve discussion about the changes, and changes are often requested by reviewers. If you don't understand the code you're copy-pasting, your PR is likely to fail. - **Do not edit code directly on github.com.** We recommend learning how to use [`git`](https://git-scm.com). `git` allows you to "clone" a repository onto your computer, so that you can make changes using an IDE. - **Use an IDE, not a text editor.** We recommend PhpStorm or VSCode. diff --git a/README.md b/README.md index b9e2e1888..352b628b6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

- CI + CI GitHub release (latest SemVer) Discord
@@ -65,13 +65,15 @@ PocketMine-MP accepts community contributions! The following resources will be u * [Building and running PocketMine-MP from source](BUILDING.md) * [Contributing Guidelines](CONTRIBUTING.md) +New here? Check out [issues with the "Easy task" label](https://github.com/pmmp/PocketMine-MP/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Easy%20task%22) for things you could work to familiarise yourself with the codebase. + ## Donate PocketMine-MP is free, but it requires a lot of time and effort from unpaid volunteers to develop. Donations enable us to keep delivering support for new versions and adding features your players love. You can support development using the following methods: - [Patreon](https://www.patreon.com/pocketminemp) -- Bitcoin (BTC): `171u8K9e4FtU6j3e5sqNoxKUgEw9qWQdRV` +- Bitcoin (BTC): `bc1q2v5ngyf8ugyd55kqa9ep35g2rv342ueqm6ks33` - Stellar Lumens (XLM): `GAAC5WZ33HCTE3BFJFZJXONMEIBNHFLBXM2HJVAZHXXPYA3HP5XPPS7T` Thanks for your support! diff --git a/build/dump-version-info.php b/build/dump-version-info.php index 166264d98..3181acba6 100644 --- a/build/dump-version-info.php +++ b/build/dump-version-info.php @@ -31,12 +31,12 @@ require dirname(__DIR__) . '/vendor/autoload.php'; */ /** - * @var string[]|\Closure[] $options - * @phpstan-var array $options + * @var string[]|Closure[] $options + * @phpstan-var array $options */ $options = [ "base_version" => VersionInfo::BASE_VERSION, - "major_version" => fn() => explode(".", VersionInfo::BASE_VERSION)[0], + "major_version" => fn() => explode(".", VersionInfo::BASE_VERSION, limit: 2)[0], "mcpe_version" => ProtocolInfo::MINECRAFT_VERSION_NETWORK, "is_dev" => VersionInfo::IS_DEVELOPMENT_BUILD, "changelog_file_name" => function() : string{ diff --git a/build/generate-bedrockdata-path-consts.php b/build/generate-bedrockdata-path-consts.php index 6ad6d83fd..f74137dd2 100644 --- a/build/generate-bedrockdata-path-consts.php +++ b/build/generate-bedrockdata-path-consts.php @@ -28,6 +28,7 @@ use function dirname; use function fclose; use function fopen; use function fwrite; +use function is_dir; use function is_file; use function scandir; use function str_replace; @@ -59,7 +60,7 @@ foreach($files as $file){ continue; } $path = Path::join(BEDROCK_DATA_PATH, $file); - if(!is_file($path)){ + if(!is_file($path) && !is_dir($path)){ continue; } @@ -67,6 +68,7 @@ foreach($files as $file){ 'README.md', 'LICENSE', 'composer.json', + '.github' ] as $ignored){ if($file === $ignored){ continue 2; diff --git a/build/generate-block-serializer-consts.php b/build/generate-block-serializer-consts.php index 875729fcf..6dab2ceb2 100644 --- a/build/generate-block-serializer-consts.php +++ b/build/generate-block-serializer-consts.php @@ -29,6 +29,9 @@ use pocketmine\data\bedrock\block\BlockStateStringValues; use pocketmine\data\bedrock\block\BlockTypeNames; use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\nbt\NbtException; +use pocketmine\nbt\tag\ByteTag; +use pocketmine\nbt\tag\IntTag; +use pocketmine\nbt\tag\StringTag; use pocketmine\network\mcpe\convert\BlockStateDictionary; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; @@ -78,6 +81,9 @@ function generateBlockPaletteReport(array $states) : BlockPaletteReport{ $name = $stateData->getName(); $result->seenTypes[$name] = $name; foreach(Utils::stringifyKeys($stateData->getStates()) as $k => $v){ + if(!$v instanceof ByteTag && !$v instanceof IntTag && !$v instanceof StringTag){ + throw new AssumptionFailedError("Assumed all state tags should be TAG_Byte, TAG_Int or TAG_String, but found $k ($v) on block $name"); + } $result->seenStateValues[$k][$v->getValue()] = $v->getValue(); asort($result->seenStateValues[$k]); } diff --git a/build/make-release.php b/build/make-release.php deleted file mode 100644 index 741f9d787..000000000 --- a/build/make-release.php +++ /dev/null @@ -1,174 +0,0 @@ - "Version to insert and tag", - "next" => "Version to put in the file after tagging", - "channel" => "Release channel to post this build into" -]; - -function systemWrapper(string $command, string $errorMessage) : void{ - system($command, $result); - if($result !== 0){ - echo "error: $errorMessage; aborting\n"; - exit(1); - } -} - -function main() : void{ - $filteredOpts = []; - $postCommitOnly = false; - foreach(Utils::stringifyKeys(getopt("", ["current:", "next:", "channel:", "help", "post"])) as $optName => $optValue){ - if($optName === "help"){ - fwrite(STDOUT, "Options:\n"); - - $maxLength = max(array_map(fn(string $str) => strlen($str), array_keys(ACCEPTED_OPTS))); - foreach(ACCEPTED_OPTS as $acceptedName => $description){ - fwrite(STDOUT, str_pad("--$acceptedName", $maxLength + 4, " ", STR_PAD_LEFT) . ": $description\n"); - } - exit(0); - } - if($optName === "post"){ - $postCommitOnly = true; - continue; - } - if(!is_string($optValue)){ - fwrite(STDERR, "--$optName expects exactly 1 value\n"); - exit(1); - } - $filteredOpts[$optName] = $optValue; - } - - $channel = $filteredOpts["channel"] ?? null; - if(isset($filteredOpts["current"])){ - $currentVer = new VersionString($filteredOpts["current"]); - }else{ - $currentVer = new VersionString(VersionInfo::BASE_VERSION); - } - - $nextVer = isset($filteredOpts["next"]) ? new VersionString($filteredOpts["next"]) : null; - - $suffix = $currentVer->getSuffix(); - if($suffix !== ""){ - if($channel === "stable"){ - fwrite(STDERR, "error: cannot release a suffixed build into the stable channel\n"); - exit(1); - } - if(preg_match('/^([A-Za-z]+)(\d+)$/', $suffix, $matches) !== 1){ - echo "error: invalid current version suffix \"$suffix\"; aborting\n"; - exit(1); - } - $nextVer ??= new VersionString(sprintf( - "%u.%u.%u-%s%u", - $currentVer->getMajor(), - $currentVer->getMinor(), - $currentVer->getPatch(), - $matches[1], - ((int) $matches[2]) + 1 - )); - $channel ??= strtolower($matches[1]); - }else{ - $nextVer ??= new VersionString(sprintf( - "%u.%u.%u", - $currentVer->getMajor(), - $currentVer->getMinor(), - $currentVer->getPatch() + 1 - )); - $channel ??= "stable"; - } - - $versionInfoPath = dirname(__DIR__) . '/src/VersionInfo.php'; - - if($postCommitOnly){ - echo "Skipping release commit & tag. Bumping to next version $nextVer directly.\n"; - }else{ - echo "About to tag version $currentVer. Next version will be $nextVer.\n"; - echo "$currentVer will be published on release channel \"$channel\".\n"; - echo "please add appropriate notes to the changelog and press enter..."; - fgets(STDIN); - systemWrapper('git add "' . dirname(__DIR__) . '/changelogs"', "failed to stage changelog changes"); - system('git diff --cached --quiet "' . dirname(__DIR__) . '/changelogs"', $result); - if($result === 0){ - echo "error: no changelog changes detected; aborting\n"; - exit(1); - } - replaceVersion($versionInfoPath, $currentVer->getBaseVersion(), false, $channel); - systemWrapper('git commit -m "Release ' . $currentVer->getBaseVersion() . '" --include "' . $versionInfoPath . '"', "failed to create release commit"); - systemWrapper('git tag ' . $currentVer->getBaseVersion(), "failed to create release tag"); - } - - replaceVersion($versionInfoPath, $nextVer->getBaseVersion(), true, $channel); - systemWrapper('git add "' . $versionInfoPath . '"', "failed to stage changes for post-release commit"); - systemWrapper('git commit -m "' . $nextVer->getBaseVersion() . ' is next" --include "' . $versionInfoPath . '"', "failed to create post-release commit"); -} - -main(); diff --git a/build/php b/build/php index 5016e0a3d..8fe187335 160000 --- a/build/php +++ b/build/php @@ -1 +1 @@ -Subproject commit 5016e0a3d54c714c12b331ea0474a6f500ffc0a3 +Subproject commit 8fe187335f666b8fb251927940f66ef372bfd3a6 diff --git a/build/server-phar-stub.php b/build/server-phar-stub.php index b4018e3a7..c713636c4 100644 --- a/build/server-phar-stub.php +++ b/build/server-phar-stub.php @@ -25,6 +25,7 @@ namespace pocketmine\server_phar_stub; use function clearstatcache; use function copy; +use function define; use function fclose; use function fflush; use function flock; @@ -165,4 +166,5 @@ $start = hrtime(true); $cacheName = preparePharCache($tmpDir, __FILE__); echo "Cache ready at $cacheName in " . number_format((hrtime(true) - $start) / 1e9, 2) . "s\n"; +define('pocketmine\ORIGINAL_PHAR_PATH', __FILE__); require 'phar://' . str_replace(DIRECTORY_SEPARATOR, '/', $cacheName) . '/src/PocketMine.php'; diff --git a/build/server-phar.php b/build/server-phar.php index f6bb29d51..7560fa5da 100644 --- a/build/server-phar.php +++ b/build/server-phar.php @@ -129,7 +129,7 @@ function buildPhar(string $pharPath, string $basePath, array $includedPaths, arr } function main() : void{ - if(ini_get("phar.readonly") == 1){ + if(ini_get("phar.readonly") === "1"){ echo "Set phar.readonly to 0 with -dphar.readonly=0" . PHP_EOL; exit(1); } diff --git a/changelogs/5.22.md b/changelogs/5.22.md new file mode 100644 index 000000000..6c9ba6d7e --- /dev/null +++ b/changelogs/5.22.md @@ -0,0 +1,16 @@ +# 5.22.0 +Released 4th December 2024. + +**For Minecraft: Bedrock Edition 1.21.50** + +This is a support release for Minecraft: Bedrock Edition 1.21.50. + +**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.50. +- Removed support for earlier versions. diff --git a/changelogs/5.23.md b/changelogs/5.23.md new file mode 100644 index 000000000..3a287608f --- /dev/null +++ b/changelogs/5.23.md @@ -0,0 +1,139 @@ +# 5.23.0 +Released 5th December 2024. + +This is a minor feature release, including new gameplay features, internals improvements, API additions and +deprecations, and improvements to timings. + +**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 +- `/timings` now supports collecting timings from async task workers. These new timings will be shown alongside `Full Server Tick` timings, but will not be counted in total load. +- Added `/xp` command. +- `start.sh` will now emit warnings when the server process exits with an unusual exit code. This helps to detect unexpected segmentation faults and other kinds of native errors. + +## Gameplay +- Added the following new items: + - End Crystal + - Goat Horn (all variants) + - Ice Bomb (from Education Edition) + - Recovery Compass +- Added the following enchantments: + - Frost Walker +- Sugarcane now self-destructs when there is no water adjacent to the base block. +- Added basic support for middle-clicking on entities to get their spawn eggs. +- Added sounds when drinking potions. +- Eating food is now allowed in creative mode and in peaceful difficulty. + +## API +### `pocketmine\block` +- Extracted `MultiAnyFacingTrait` and `MultiAnySupportTrait` from `GlowLichen` to enable reuse in other blocks. +- The following API methods have been deprecated: + - `Campfire->getInventory()` - this was added by mistake and can't be well-supported given the way that blocks work + +### `pocketmine\command` +- The following classes have been added: + - `ClosureCommand` - allows registering a closure to execute a command + +### `pocketmine\event` +- Added APIs to `PlayerInteractEvent` to allow toggling item and block interactions. + - This allows various customisations, such as allowing interactions when sneaking, selectively disabling item or block reactions, etc. + - If both item and block interactions are disabled, the event is **not** cancelled (blocks can still be placed). + - The following API methods have been added: + - `public PlayerInteractEvent->setUseBlock(bool $useBlock) : void` + - `public PlayerInteractEvent->setUseItem(bool $useItem) : void` + - `public PlayerInteractEvent->useBlock() : bool` - returns whether the block can respond to the interaction (toggling levers, opening/closing doors, etc). + - `public PlayerInteractEvent->useItem() : bool` - returns whether the item can respond to the interaction (spawn eggs, flint & steel, etc). +- The following new classes have been added: + - `player\PlayerEntityPickEvent` - called when a player middle-clicks on an entity + +### `pocketmine\inventory\transaction` +- The following API methods have been deprecated: + - `InventoryAction->onAddToTransaction()` + +### `pocketmine\permission` +- The following API methods have been deprecated: + - `PermissionManager->getPermissionSubscriptions()` + - `PermissionManager->subscribeToPermission()` + - `PermissionManager->unsubscribeFromAllPermissions()` + - `PermissionManager->unsubscribeFromPermission()` + +### `pocketmine\plugin` +- The following classes have been deprecated: + - `DiskResourceProvider` + - `ResourceProvider` + +### `pocketmine\promise` +- `Promise::all()` now accepts zero promises. This will return an already-resolved promise with an empty array. + +### `pocketmine\scheduler` +- Added PHPStan generic types to `TaskHandler` and related APIs in `TaskScheduler` and `Task`. +- The following API methods have been deprecated + - `AsyncTask->publishProgress()` + - `AsyncTask->onProgressUpdate()` + +### `pocketmine\timings` +- Timings can now notify other code when timings are enabled/disabled, reloaded, or collected. + - The intent of this is to facilitate timings usage on other threads, and have the results collected into a single timings report. + - Timings cannot directly control timings on other threads, so these callbacks allow plugins to use custom mechanisms to toggle, reset and collect timings. + - PocketMine-MP currently uses this to collect timings from async task workers. More internal threads may be supported in the future. +- The following API methods have been added: + - `public static TimingsHandler::getCollectCallbacks() : ObjectSet<\Closure() : list>>` - callbacks for (asynchronously) collecting timings (typically from other threads). The returned promises should be resolved with the result of `TimingsHandler::printCurrentThreadRecords()`. + - `public static TimingsHandler::getReloadCallbacks() : ObjectSet<\Closure() : void>` - callbacks called when timings are reset + - `public static TimingsHandler::getToggleCallbacks() : ObjectSet<\Closure(bool $enable) : void>` - callbacks called when timings are enabled/disabled + - `public static TimingsHandler::requestPrintTimings() : Promise>` - asynchronously collects timing results from all threads and assembles them into a single report +- The following API methods have been deprecated: + - `TimingsHandler::printTimings()` - this function cannot support async timings collection. Use `TimingsHandler::requestPrintTimings()` instead. + - `Timings::getAsyncTaskErrorTimings()` - internal method that is no longer needed +- The following constants have been deprecated: + - `Timings::GROUP_BREAKDOWN` - no longer used + +### `pocketmine\utils` +- The following API methods have been added: + - `public static Utils::getRandomFloat() : float` - returns a random float between 0 and 1. Drop-in replacement for `lcg_value()` in PHP 8.4. + +## Internals +- Blocks are now always synced with the client during a right-click-block interaction. This clears mispredictions on the client in case the new `PlayerInteractEvent` flags were customized by plugins. +- `VanillaBlocks` and `VanillaItems` now use reflection to lookup TypeId constants by registration name, instead of requiring TypeIds to be manually specified. + - While this is obviously a hack, it prevents incorrect constants from being used when adding new blocks, and guarantees that the names of constants in `BlockTypeIds` and `ItemTypeIds` will match their corresponding entries in `VanillaBlocks` and `VanillaItems` respectively. + - It also significantly improves readability of `VanillaBlocks` and `VanillaItems`, as well as eliminating ugly code like `WoodLikeBlockIdHelper`. + - In PM6, the team is exploring options to redesign `VanillaBlocks` and `VanillaItems` to eliminate the need for defining separate TypeIds entirely. +- `ConsoleReader` now uses socket support in `proc_open()` to transmit IPC messages to the server process. Previously, a temporary socket server was used, which was unreliable in some conditions. +- Event handler tests have been converted to PHPUnit tests by mocking `Server` and `Plugin` instances. Previously, these required integration tests for these dependencies. +- Fixed various deprecation warnings in PHP 8.4. +- `netresearch/jsonmapper` is now used at `5.0.0`. The PMMP fork of this library has been removed, as it is no longer needed. + +# 5.23.1 +Released 5th December 2024. + +## Fixes +- Fixed signs not creating a tile when placed. + +## Internals +- Improved blockstate consistency check to detect tiles disappearing during refactors. + +# 5.23.2 +Released 9th December 2024. + +## General +- Updated translations for Russian and Korean. + +## Fixes +- Fixed server build number. +- Fixed some crashes being misreported as plugin-involved. + +## Internals +- Removed legacy `build/make-release.php` script. This script is no longer used, as all releases should now follow the PR workflow. + +# 5.23.3 +Released 22nd January 2025. + +## Fixes +- Fixed crashes with PHP internal stack frames being flagged as plugin crashes. +- Fixed note block instrument sounds in 1.21.50. + +## Internals +- Updated GitHub issue templates to use issue forms. diff --git a/changelogs/5.24.md b/changelogs/5.24.md new file mode 100644 index 000000000..a159d0e76 --- /dev/null +++ b/changelogs/5.24.md @@ -0,0 +1,108 @@ +# 5.24.0 +Released 22nd January 2025. + +This is a minor feature release, including new gameplay features, performance improvements, and minor API additions. + +**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. + +## Performance +- PHP garbage collection is now managed by the server, instead of being automatically triggered by PHP. + - The mechanism for GC triggering is designed to mimic PHP's to avoid behavioural changes. Only the place it's triggered from should be significantly different. + - This change also avoids unnecessary GCs during object-heavy operations, such as encoding `CraftingDataPacket`. As such, performance during server join should see an improvement. + - Timings is now able to directly measure the impact of GC. Previously, GC would show up as random spikes under random timers, skewing timing results. +- `ChunkCache` now uses `string` for completed caches directly instead of keeping them wrapped in `CompressBatchPromise`s. This reduces memory usage, improves performance, and reduces GC workload. + +## Configuration +- The following settings have been removed from `pocketmine.yml` and will no longer have any effect: + - `memory.garbage-collection.collect-async-worker` (now always `true`) + - `memory.garbage-collection.low-memory-trigger` (now always `true`) + - `memory.max-chunks.trigger-chunk-collect` (now always `true`) + - `memory.world-caches.disable-chunk-cache` (now always `true`) + - `memory.world-caches.low-memory-trigger` (now always `true`) + +## Gameplay +- Added the following new blocks: + - All types of pale oak wood, and leaves + - Resin + - Resin Bricks, Slabs, Stairs, and Walls + - Resin Clump + - Chiseled Resin Bricks +- Some blocks have had their tool tier requirements adjusted to match latest Bedrock updates. +- Added the following new items: + - Resin Brick + - Music Disc - Creator + - Music Disc - Creator (Music Box) + - Music Disc - Precipice + - Music Disc - Relic + +## API +### General +- Many places had their PHPDoc improved to address issues highlighted by PHPStan 2.x. This may cause new, previously unreported issues to be reported in plugins using PHPStan. + +### `pocketmine` +- The following methods have been deprecated: + - `MemoryManager->canUseChunkCache()` + - `MemoryManager::dumpMemory()` - relocated to `MemoryDump` class + +### `pocketmine\item` +- The following new enum cases have been added: + - `RecordType::DISK_CREATOR` + - `RecordType::DISK_CREATOR_MUSIC_BOX` + - `RecordType::DISK_PRECIPICE` + - `RecordType::DISK_RELIC` + +### `pocketmine\player` +- The following new methods have been added: + - `public Player->getFlightSpeedMultiplier() : float` - a base multiplier for player's flight speed + - `public Player->setFlightSpeedMultiplier(float $flightSpeedMultiplier) : void` - sets the player's flight speed multiplier +- The following new constants have been added: + - `Player::DEFAULT_FLIGHT_SPEED_MULTIPLIER` + +### `pocketmine\utils` +- The following new methods have been added: + - `public static TextFormat::javaToBedrock(string $string) : string` - removes unsupported Java Edition format codes to prevent them being incorrectly displayed on Bedrock +- The following methods have behavioural changes: + - `TextFormat::toHTML()` now converts `§m` to redstone red (instead of strikethrough), and `§n` to copper orange (instead of underline). This is because the codes previously used for `STRIKETHROUGH` and `UNDERLINE` conflict with the new material codes introduced by Minecraft Bedrock. + - `Terminal::toANSI()` now converts `§m` to redstone red (instead of strikethrough), and `§n` to copper orange (instead of underline), as above. However, underline and strikethrough can still be used on the terminal using `Terminal::$FORMAT_UNDERLINE` and `Terminal::$FORMAT_STRIKETHROUGH` respectively. +- The following new constants have been added: + - `TextFormat::MATERIAL_QUARTZ` + - `TextFormat::MATERIAL_IRON` + - `TextFormat::MATERIAL_NETHERITE` + - `TextFormat::MATERIAL_REDSTONE` + - `TextFormat::MATERIAL_COPPER` + - `TextFormat::MATERIAL_GOLD` + - `TextFormat::MATERIAL_EMERALD` + - `TextFormat::MATERIAL_DIAMOND` + - `TextFormat::MATERIAL_LAPIS` + - `TextFormat::MATERIAL_AMETHYST` +- The following constants have been deprecated: + - `TextFormat::STRIKETHROUGH` + - `TextFormat::UNDERLINE` +- The following static properties have been added: + - `Terminal::$COLOR_MATERIAL_QUARTZ` + - `Terminal::$COLOR_MATERIAL_IRON` + - `Terminal::$COLOR_MATERIAL_NETHERITE` + - `Terminal::$COLOR_MATERIAL_REDSTONE` + - `Terminal::$COLOR_MATERIAL_COPPER` + - `Terminal::$COLOR_MATERIAL_GOLD` + - `Terminal::$COLOR_MATERIAL_EMERALD` + - `Terminal::$COLOR_MATERIAL_DIAMOND` + - `Terminal::$COLOR_MATERIAL_LAPIS` + - `Terminal::$COLOR_MATERIAL_AMETHYST` + +## Tools +- Fixed some UI issues in `tools/convert-world.php` + +## Internals +- Block cache in `World` is now size-limited. This prevents memory exhaustion when plugins call `getBlock()` many thousands of times with cache misses. +- `RakLibServer` now disables PHP GC. As RakLib doesn't generate any unmanaged cycles, GC is just a waste of CPU time in this context. +- `MemoryManager` now has the responsibility for triggering cycle GC. It's checked every tick, but GC won't take place unless the GC threshold is exceeded, similar to PHP. + - This mechanism could probably do with alterations to better suit PocketMine-MP, but it was chosen to mimic PHP's own GC to minimize behavioural changes for now. +- `AsyncTask` now triggers cycle GC after `onRun()` completes. As with `MemoryManager`, this is based on a threshold designed to mimic PHP's own behaviour. +- `FormatConverter` now performs world provider GC periodically. This is not needed with current active providers, but was found to be a problem while developing custom providers. +- Various internal adjustments were made to avoid returning incorrectly-keyed arrays in the code. These changes shouldn't affect anything as the arrays should have been properly ordered anyway. +- Many places that previously used `==` and `!=` have been updated to use strict variants. This kind of change needs to be done carefully to avoid breaking `int|float` comparisons. diff --git a/changelogs/5.25.md b/changelogs/5.25.md new file mode 100644 index 000000000..39862cbb7 --- /dev/null +++ b/changelogs/5.25.md @@ -0,0 +1,52 @@ +# 5.25.0 +Released 16th February 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.60. It also includes some minor API additions supporting new features in this version. + +**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.60. +- Removed support for earlier versions. + +## Documentation +- Fixed the documentation of `Utils::getOS()`. It now refers to the `Utils::OS_*` constants instead of a list of hardcoded strings. + +## API +### `pocketmine\inventory` +This release allows plugins to decide which creative tab they want to add their new items to. +It also allows creating new collapsible groups of items, and modifying or removing existing ones. + +- The following new methods have been added: + - `public CreativeInventory->getAllEntries() : list` - returns an array of objects, each containing a creative item and information about its category and collapsible group (if applicable). + - `public CreativeInventory->getEntry(int $index) : ?CreativeInventoryEntry` - returns the creative item with the specified identifier, or `null` if not found +- The following methods have signature changes: + - `CreativeInventory->add()` now accepts two additional optional parameters: `CreativeCategory $category, ?CreativeGroup $group`. If not specified, the item will be added to the Items tab without a group. +- The following new classes have been added: + - `CreativeCategory` - enum of possible creative inventory categories (each has its own tab on the GUI) + - `CreativeGroup` - contains information about a collapsible group of creative items, including tooltip text and icon + - `CreativeInventoryEntry` - contains information about a creative inventory item, including its category and collapsible group (if applicable) + +## Internals +- `CreativeContentPacket` is no longer fully cached due to the requirement for translation context during construction. The individual items are still cached (which is the most expensive part), but the packet itself is now constructed on demand, and group entries are constructed on the fly. This may affect performance, but this has not been investigated. +- `BedrockDataFiles` now includes constants for folders at the top level of `BedrockData` as well as files. +- The structure of creative data in `BedrockData` was changed to accommodate item category and grouping information. `creativeitems.json` has been replaced by `creative/*.json`, which contain information about item grouping and also segregates item lists per category. +- New information was added to `required_item_list.json` in `BedrockData`, as the server is now required to send item component NBT data in some cases. + +# 5.25.1 +Released 26th February 2025. + +## Fixes +- Fixed confusing exception message when a block-breaking tool has an efficiency value of zero. +- Fixed incorrect facing of doors since 1.21.60 (resulted in mismatched AABBs between client & server, rendering glitches etc.) +- Resource pack UUIDs are now validated on load. Previously, invalid UUIDs would be accepted, and potentially cause a server crash on player join. + +# 5.25.2 +Released 4th March 2025. + +## Fixes +- Added limits to various `explode()` calls. diff --git a/changelogs/5.26.md b/changelogs/5.26.md new file mode 100644 index 000000000..f8adc969f --- /dev/null +++ b/changelogs/5.26.md @@ -0,0 +1,71 @@ +# 5.26.0 +Released 22nd March 2025. + +This is a minor feature release focused on performance improvements. + +**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. + +## Performance +- Significantly improved performance of entity movement. Load testing with item entities showed a 3x increase in the number of entities supported without lag. +- Significantly improved performance of on-ground checks for player movement. This still needs further work, but optimisations implemented in this version should improve performance substantially. +- Updated `pocketmine/nbt` dependency with performance improvements to `TAG_Compound` and `TAG_List` comparison. This should improve performance of inventory-related actions. +- `InventoryTransaction` now avoids useless item clones when processing transactions, which should improve performance of inventory-related actions. + +## Dependencies +- `pocketmine/bedrock-protocol` has been updated to `36.2.0`, which adds new functions to access some packet fields. +- `pocketmine/nbt` has been updated to `1.1.0`, which improves performance when comparing NBT object trees. + +## Gameplay +- Block breaking animation speed now takes into account the following: jumping, being in water, haste, mining fatigue + +## Tools +- `blockstate-upgrade-schema-utils.php` now has a new `dump-table` command, which turns a `.bin` palette table file into human-readable text for debugging. + +## API +### `pocketmine\block` +- The following methods have been added: + - `public RuntimeBlockStateRegistry->hasStateId(int $stateId) : bool` - checks whether the given state ID is registered + +### `pocketmine\crafting` +- The following methods have been deprecated: + - `CraftingManager::sort()` - this was implicitly internal anyway + +### `pocketmine\utils` +- The following constants have been added: + - `TextFormat::MATERIAL_RESIN` +- The following static properties have been added: + - `Terminal::$COLOR_MATERIAL_RESIN` + +### `pocketmine\data\bedrock\block` +- `BlockStateToObjectDeserializer` now permits overriding **deserializers** for Bedrock IDs. This may be useful to implement custom state handling, or to implement missing block variants (such as snow cauldron). + - This was originally prohibited since 5.0.0. However, there is no technical reason to disallow overriding **deserializers**. + - Overriding **serializers** is still **not permitted**. Reusing type IDs doesn't make any sense and would break internal design contracts. + - If you want to make a custom version of a vanilla block, create a custom type ID for it, exactly as you would for a regular custom block. +- The following methods have been added: + - `public BlockStateToObjectDeserializer->getDeserializerForId(string $id) : ?(\Closure(BlockStateReader) : Block)` + +### `pocketmine\data\bedrock\item` +- `ItemDeserializer` now permits overriding **deserializers** for Bedrock IDs. As above, this may be useful to implement custom data handling, or to implement missing variants of existing items. + - This was originally prohibited since 5.0.0. However, there is no technical reason to disallow overriding **deserializers**. + - Overriding **serializers** is still **not permitted**. Reusing type IDs doesn't make any sense and would break internal design contracts. + - As above, if you want to make a custom version of a vanilla item, create a custom type ID for it, exactly as you would for a regular custom item. +- The following methods have been added: + - `public ItemDeserializer->getDeserializerForId(string $id) : ?(\Closure(SavedItemData) : Item)` + +## Internals +- `new $class` is now banned on new internals code by a PHPStan rule. Closures or factory objects should be used instead for greater flexibility and better static analysis. +- `CraftingManager` now uses a more stable hash function for recipe output filtering. +- `ChunkCache` now accepts `int $dimensionId` in the constructor. This may be useful for plugins which implement the nether. +- `RuntimeBlockStateRegistry` now precomputes basic collision info about known states for fast paths. + - This permits specialization for common shapes like cubes and collisionless blocks, which allows skipping complex logic in entity movement calculation. This vastly improves performance. + - Any block whose class overrides `readStateFromWorld()` or `getModelPositionOffset()` will *not* be optimised. + - `Block->recalculateCollisionBoxes()` now has a hard requirement not to depend on anything other than available properties. It must not use `World` or its position. + - This change was problematic for `ChorusPlant`, which used nearby blocks to calculate its collision boxes. + - Blocks which need nearby blocks should override `readStateFromWorld()` and set dynamic state properties, similar to fences. + - This design flaw will be corrected with a major change to `Block` internals currently in planning for a future major version. +- `Block->getCollisionBoxes()` may not be called at all during gameplay for blocks with shapes determined to be simple, like cubes and collisionless blocks. +- `BlockStateToObjectDeserializer` now checks if the returned blockstate is registered in `RuntimeBlockStateRegistry` to promote earlier error detection (instead of crashing in random code paths). diff --git a/changelogs/5.27.md b/changelogs/5.27.md new file mode 100644 index 000000000..07bc8c26e --- /dev/null +++ b/changelogs/5.27.md @@ -0,0 +1,24 @@ +# 5.27.0 +Released 27th March 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.70. + +**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. + +## Interim releases +If you're upgrading from 5.25.x directly to 5.27.0, please also read the following changelogs, as the interim releases contain important changes: +- [5.26.0](https://github.com/pmmp/PocketMine-MP/blob/5.26.0/changelogs/5.26.md#5260) - Performance improvements and other internal improvements + +## 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 000000000..f378031f7 --- /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/changelogs/5.29.md b/changelogs/5.29.md new file mode 100644 index 000000000..cb6e50da3 --- /dev/null +++ b/changelogs/5.29.md @@ -0,0 +1,25 @@ +# 5.29.0 +Released 18th June 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.90. + +**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.90. +- Removed support for earlier versions. + +## Fixes +- Fixed thread crashes sometimes not reporting proper cause information in crashdumps. +- Fixed crash when a plugin replaced a player's held tool with a different tool with a damage exceeding the old tool's max damage during an action. +- Fixed performance issue of `PlayerAuthInputPacket` input flags handling (broken change detection). +- Fixed `BaseInventory->addItem()` triggering updates on empty slots when no items were added. +- Fixed slow check in `SubChunk` block layer garbage collection. + +## Internals +- `LoginPacketHandler->processLogin()` signature has changed. This will break any plugins overriding `LoginPacketHandler`. As noted above, this is _not_ covered by the API version guarantee. +- Automated branch sync for `minor-next` and `major-next` is now triggered by `repository_dispatch` from a cron job in this repository instead of `RestrictedActions`. The `RestrictedActions` cron job was getting automatically disabled by GitHub due to repo inactivity. diff --git a/changelogs/5.30.md b/changelogs/5.30.md new file mode 100644 index 000000000..cc2ecbc1f --- /dev/null +++ b/changelogs/5.30.md @@ -0,0 +1,73 @@ +# 5.30.0 +Released 18th June 2025. + +This is a minor feature release containing API additions, internals cleanup and user experience improvements. + +**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 +- Significantly reduced log spam when unknown blocks, tiles and entities are found in saved worlds. +- The file name structure for crashdumps has been changed to improve sorting order in file browsers. +- Buffering is now skipped on the RakLib layer. In theory this could reduce player network latency by 10 ms (YMMV). + +## Gameplay +### Blocks +- Many blocks have had their hardness and blast resistance updated to match vanilla. +- Implemented Respawn Anchor. +- Melon Stem and Pumpkin Stem drop amounts should now match vanilla (using binomial distribution). + +## API +## General +- Verification of save registration has been added for blocks, entities and tiles. This is intended to make it easier to find mistakes when registering custom things, which previously would produce obscure core crashes. + +### `pocketmine\event\block` +- The following classes have been added: + - `BlockPreExplodeEvent` - called before a block tries to explode + - `BlockExplodeEvent` - called when after a block's explosion calculation has been done, but before any changes are applied + +### `pocketmine\event\entity` +- The following classes have been added: + - `EntityExtinguishEvent` - called when a burning entity is extinguished by water or other sources + - `EntityFrostWalkerEvent` - called every tick upon which an entity wearing Frost Walker boots moves; this can be used to customise or cancel the behaviour of the Frost Walker enchantment + +### `pocketmine\event\player` +- The following classes have been added: + - `PlayerRespawnAnchorUseEvent` - called when a player interacts with a charged respawn anchor + +### `pocketmine\entity` +- The following methods have been added: + - `public Entity->getStepHeight() : float` + - `public Entity->setStepHeight(float $stepHeight) : void` + +### `pocketmine\world\generator` +- Generator execution has been decoupled from `PopulationTask` and async tasks in general. The following classes have been added: + - `executor\GeneratorExecutor` + - `executor\SyncGeneratorExecutor` - runs a generator on the main thread (used for flat world generation, which doesn't need threads) + - `executor\AsyncGeneratorExecutor` - runs a generator inside an async task, as before + - `PopulationUtils` - contains population business logic previously baked into `PopulationTask` - this permits the reuse of that logic outside async tasks +- The following methods have signature changes: + - `GeneratorManager->addGenerator()` now accepts an optional `bool $fast` parameter, defaulting to `false`; setting this to `true` will cause your generator to run on the main thread +- The following methods have been added: + - `public GeneratorManagerEntry->isFast() : bool` - returns whether this generator should run on the main thread +- `PopulationTask` has been marked as `@internal`. In the next major version, it will move to the `generator\executor` namespace; however, for now it stays put because plugins currently have no other way to regenerate chunks. + +## Internals +- World data version numbers have been consolidated in `pocketmine\data\bedrock\WorldDataVersions`. This removes the need to modify several different files to support new world versions, and reduces the chances of things getting missed. +- Block hardness and blast resistance is now unit-tested against `block_properties_table.json` in `BedrockData`. This file comes from vanilla BDS, so we can use it to verify compliance. +- Protocol-layer "server auth block breaking" has been enabled. Functionally, this is no different from the previous system, it just works differently on the network layer. +- Various internal classes in the `pocketmine\world\generator` namespace have been moved to the `generator\executor` namespace. +- Removed `World->registerGenerator()` and `World->unregisterGenerator()`. +- Removed redundant calls to `curl_close()` (obsolete since PHP 8.0). + +# 5.30.1 +Released 23rd June 2025. + +## Fixes +- Fixed accidental break of backwards compatibility in `EntityExplodeEvent` introduced in the previous release. +- Fixed placement of player holding block when exploding respawn anchor. +- Updated BedrockProtocol to fix incorrect encoding of `ServerScriptDebugDrawerPacket`. +- Disabled client-side locator bar, allowing plugins to write their own implementations. diff --git a/changelogs/5.31.md b/changelogs/5.31.md new file mode 100644 index 000000000..60e797425 --- /dev/null +++ b/changelogs/5.31.md @@ -0,0 +1,14 @@ +# 5.31.0 +Released 8th July 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.93. + +**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.93. +- Removed support for earlier versions. diff --git a/changelogs/5.32.md b/changelogs/5.32.md new file mode 100644 index 000000000..414330351 --- /dev/null +++ b/changelogs/5.32.md @@ -0,0 +1,25 @@ +# 5.32.0 +Released 6th August 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.100. + +**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.100. +- Removed support for earlier versions. + +## Fixes +- Fixed deadlock on RakLib thread crash (e.g. due to port binding failure). + +# 5.32.1 +Released 14th August 2025. + +## Fixes +- Hardened checks when processing resource pack sending during player logins. +- Fixed content log warning about crafting recipe with missing ID. +- Fixed packets in a batch still being processed after one of them caused the session to be terminated. diff --git a/changelogs/5.33.md b/changelogs/5.33.md new file mode 100644 index 000000000..65ff6620d --- /dev/null +++ b/changelogs/5.33.md @@ -0,0 +1,147 @@ +# 5.33.0 +Released 30th August 2025. + +This is a minor feature release containing internals improvements, API improvements and new gameplay features. + +**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. + +## Performance +- Worlds now remember when a chunk isn't generated. This reduces world I/O during world generation. +- `BlockObjectToStateSerializer` now creates fewer objects in certain cases. + +## Gameplay +- The following blocks have been added and/or are now properly supported: + - Hanging signs + - Illager banners + +## Tools +- `generate-bedrock-data-from-packets.php` now represents items as strings directly when only an ID is present. This significantly improves readability in `BedrockData` and reduces file sizes. + +## API +### `pocketmine\block` +- Added (and implemented) interfaces for many common block properties, to allow `instanceof` to be used: + - `Ageable`: for blocks with age, such as crops + - `AnyFacing`: for blocks which can face up, down, and horizontal directions (not the same as `HorizontalFacing`!) + - `Colored`: for blocks with 16 `DyeColor` variants + - `CoralMaterial`: for coral blocks, provides access to coral type and dead/alive + - `HorizontalFacing`: for blocks which can **only** face horizontal directions (not the same as `AnyFacing`!) + - `Lightable`: for light-source blocks which can be turned on and off, e.g. redstone lamp + - `MultiAnyFacing`: for blocks which can appear in multiple faces of the same block (including up, down, and horizontal faces), e.g. glow lichen + - `PillarRotation`: for blocks which can be oriented on an axis, e.g. logs + - `PoweredByRedstone`: for blocks which receive power from a redstone component, e.g. redstone lamp + - `SignLikeRotation`: for blocks which can be rotated 16 ways, e.g. signs, banners + - `WoodMaterial`: for blocks made from wood + - These interfaces have been implemented on many blocks. For the sake of brevity, they are not listed here, but you can expect to see them wherever the corresponding traits were used. +- The following classes have been added: + - `BaseOminousBanner` + - `CeilingCenterHangingSign` - both chains connected to the same point on the block above, can face 16 directions + - `CeilingEdgesHangingSign` - each chain connected to separate edges of the block above, can face 4 directions + - `OminousFloorBanner` - floor version of illager banner, can face 16 directions + - `OminousWallBanner` - wall version of illager banner, can face 4 directions + - `WallHangingSign` - hangs from a horizontal beam, can face 4 directions +- The following API methods have been added: + - `public ChiseledBookshelf->setSlots(list $slots) : $this` + - `public static VanillaBlocks` methods: + - `ACACIA_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `ACACIA_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `ACACIA_WALL_HANGING_SIGN() : WallHangingSign` + - `BIRCH_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `BIRCH_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `BIRCH_WALL_HANGING_SIGN() : WallHangingSign` + - `CHERRY_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `CHERRY_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `CHERRY_WALL_HANGING_SIGN() : WallHangingSign` + - `CRIMSON_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `CRIMSON_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `CRIMSON_WALL_HANGING_SIGN() : WallHangingSign` + - `DARK_OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `DARK_OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `DARK_OAK_WALL_HANGING_SIGN() : WallHangingSign` + - `JUNGLE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `JUNGLE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `JUNGLE_WALL_HANGING_SIGN() : WallHangingSign` + - `MANGROVE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `MANGROVE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `MANGROVE_WALL_HANGING_SIGN() : WallHangingSign` + - `OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `OAK_WALL_HANGING_SIGN() : WallHangingSign` + - `OMINOUS_FLOOR_BANNER() : OminousFloorBanner` + - `OMINOUS_WALL_BANNER() : OminousWallBanner` + - `PALE_OAK_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `PALE_OAK_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `PALE_OAK_WALL_HANGING_SIGN() : WallHangingSign` + - `SPRUCE_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `SPRUCE_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `SPRUCE_WALL_HANGING_SIGN() : WallHangingSign` + - `WARPED_CEILING_CENTER_HANGING_SIGN() : CeilingCenterHangingSign` + - `WARPED_CEILING_EDGES_HANGING_SIGN() : CeilingEdgesHangingSign` + - `WARPED_WALL_HANGING_SIGN() : WallHangingSign` + - `public AgeableTrait->getMaxAge() : int` (included by all growable plant-like blocks, e.g. crops) + +### `pocketmine\data\bedrock\block\convert` +- A new system for symmetric block serializers and deserializers has been introduced. + - This allows registering both a serializer and a deserializer with the same code, meaning way less code + - It also eliminates information duplication and potential inconsistencies, improving maintainability. + - A proper way to deal with flattened IDs (e.g. color blocks) has been introduced which _doesn't_ require hardcoding a giant mess of IDs + - This symmetric system covers 99% of blocks which have a 1:1 association between PM and vanilla blocks, or 1:N where IDs are flattened + - However, there are still some special cases which require registering separate serializers and deserializers (usually in cases where the PM implementation deviates from Mojang where Mojang's implementation sucks, such as hanging signs or big dripleaf). + - No backwards compatibility breaks are expected as a result of this change. However, it's recommended to migrate old code to this new system for maintainability. + - The following new classes have been added: + - `BlockSerializerDeserializerRegistrar` - handles unified registration of block serializers and deserializers, based on a provided block model + - `FlattenedIdModel` - represents a block with some properties baked into its Minecraft ID, e.g. coral or color blocks + - `Model` - represents a regular block with all properties in its `states` NBT + - `property\BoolFromStringProperty` - property mapping a bool value from a string NBT state + - `property\BoolProperty` + - `property\CommonProperties` - singleton containing commonly-used block property definitions and groups, e.g. facing, stair properties + - `property\EnumFromRawStateMap` - maps a raw NBT value to a PHP `enum` and vice versa + - `property\IntFromRawStateMap` - maps a raw NBT value to PM integer constants and vice versa + - `property\IntProperty` - an integer range property with a min, max, and optional offset + - `property\Property` - interface implemented by all property definitions accepted by a `Model` or `FlattenedIdModel` + - `property\StateMap` - interface implemented by classes accepted by mapping properties, e.g. `BoolFromStringProperty` + - `property\StringProperty` - interface implemented by properties whose raw outputs are strings - these can be used as ID components in `FlattenedIdModel` + - `property\ValueFromIntProperty` - property mapping a generic PM value from an int NBT state + - `property\ValueFromStringProperty` - same as above, but for a string NBT state + - `property\ValueSetFromIntProperty` - a property mapping an `int[]` or `enum[]` from a set of flags in NBT states + - `property\ValueMappings` - singleton containing commonly-needed `StateMap`s + - The following classes have been deprecated: + - `BlockStateDeserializerHelper` + - `BlockStateSerializerHelper` + - The following methods have been deprecated: + - All methods for decoding mapped property types in `BlockStateReader`, e.g. `readFacingDirection()` + - All methods for encoding mapped property types in `BlockStateWriter`, e.g. `writeFacingDirection()` + - All specific blocktype mapping functions in `BlockStateToObjectDeserializer`, e.g. `mapStairs()` + - All specific blocktype mapping functions in `BlockObjectToStateSerializer`, e.g. `mapStairs()` + +### `pocketmine\item` +- The following hooks have been added: + - `public Item->getPlacementTransaction(Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : ?BlockTransaction` - allows more complex logic for itemblocks to place blocks, without duplicating their placement conditions (used for hanging signs) + +### `pocketmine\world` +- `World->setChunk()` now verifies that blockstate IDs in the provided chunk are all registered in `RuntimeBlockStateRegistry`. This should provide earlier detection for custom block registration errors by plugins. + +## Internals +- `BlockStateUpgrader` is now almost entirely independent from `BlockStateData`. It's anticipated that the upgrader library will be separable from the core in the future. +- `Block->readStateFromWorld()` is now triggered on chunk load for any position containing a tile. This should allow more effective updating of blocks with properties in their tiles. + +# 5.33.1 +Released 31st August 2025. + +## Fixes +- Fixed banners placed in prior versions getting their tiles deleted (due to missing `Type` tags). + +# 5.33.2 +Released 16th September 2025. + +## General +- PHP 8.4 has now been added to the test matrix. + +## Fixes +- Fixed PHP 8.4 deprecation notice in `AsyncGeneratorExecutor`. +- Fixed inventory windows breaking if a window is sent while the player has the chat window open (e.g. quickly pressing E followed by / could trigger this). +- Fixed `BlockBreakEvent` being called twice in creative if cancelled. +- Reduced block lag when towering and other situations where the placed block might intersect with the player's AABB. Block lag may still appear on higher latency clients - this is still being worked on. diff --git a/composer.json b/composer.json index 16bed54b7..9451fd6eb 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "ext-ctype": "*", "ext-curl": "*", "ext-date": "*", + "ext-encoding": "~1.0.0", "ext-gmp": "*", "ext-hash": "*", "ext-igbinary": "^3.0.1", @@ -32,31 +33,35 @@ "ext-zlib": ">=1.2.11", "composer-runtime-api": "^2.0", "adhocore/json-comment": "~1.2.0", - "pocketmine/netresearch-jsonmapper": "~v4.4.999", - "pocketmine/bedrock-block-upgrade-schema": "~5.0.0+bedrock-1.21.40", - "pocketmine/bedrock-data": "~2.14.0+bedrock-1.21.40", - "pocketmine/bedrock-item-upgrade-schema": "~1.13.0+bedrock-1.21.40", - "pocketmine/bedrock-protocol": "~35.0.0+bedrock-1.21.40", + "netresearch/jsonmapper": "~v5.0.0", + "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", + "pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100", + "pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100", + "pocketmine/bedrock-protocol": "~50.0.0+bedrock-1.21.100", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.7.0", - "pocketmine/locale-data": "~2.22.0", + "pocketmine/locale-data": "~2.25.0", "pocketmine/log": "^0.4.0", "pocketmine/math": "~1.0.0", - "pocketmine/nbt": "~1.0.0", - "pocketmine/raklib": "~1.1.0", + "pocketmine/nbt": "~1.2.0", + "pocketmine/raklib": "~1.2.0", "pocketmine/raklib-ipc": "~1.0.0", "pocketmine/snooze": "^0.5.0", - "ramsey/uuid": "~4.7.0", + "ramsey/uuid": "~4.9.0", "symfony/filesystem": "~6.4.0" }, "require-dev": { - "phpstan/phpstan": "1.11.11", - "phpstan/phpstan-phpunit": "^1.1.0", - "phpstan/phpstan-strict-rules": "^1.2.0", + "phpstan/phpstan": "2.1.25", + "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 7eda66b35..ce923690e 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": "c57e8f52250edfd03906219fe14fc240", + "content-hash": "0d71d3fba23ba8c4734cac59b9e10129", "packages": [ { "name": "adhocore/json-comment", @@ -67,16 +67,16 @@ }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -85,7 +85,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -115,7 +115,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -123,20 +123,71 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { - "name": "pocketmine/bedrock-block-upgrade-schema", - "version": "5.0.0", + "name": "netresearch/jsonmapper", + "version": "v5.0.0", "source": { "type": "git", - "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", - "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52" + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/20dd5c11e9915bacea4fe2cf649e1d23697a6e52", - "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + }, + "time": "2024-09-08T10:20:00+00:00" + }, + { + "name": "pocketmine/bedrock-block-upgrade-schema", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", + "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", + "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", "shasum": "" }, "type": "library", @@ -147,22 +198,22 @@ "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", "support": { "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.0.0" + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.1.0" }, - "time": "2024-11-03T14:13:50+00:00" + "time": "2025-02-11T17:41:44+00:00" }, { "name": "pocketmine/bedrock-data", - "version": "2.14.1+bedrock-1.21.40", + "version": "6.0.0+bedrock-1.21.100", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "4a41864ed09613ecec6791e2ae076a8ec7089cc4" + "reference": "edc0d829175e5e1e57c87001acfd03526c63fd34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/4a41864ed09613ecec6791e2ae076a8ec7089cc4", - "reference": "4a41864ed09613ecec6791e2ae076a8ec7089cc4", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/edc0d829175e5e1e57c87001acfd03526c63fd34", + "reference": "edc0d829175e5e1e57c87001acfd03526c63fd34", "shasum": "" }, "type": "library", @@ -173,22 +224,22 @@ "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/2.14.1+bedrock-1.21.40" + "source": "https://github.com/pmmp/BedrockData/tree/6.0.0+bedrock-1.21.100" }, - "time": "2024-11-12T21:36:20+00:00" + "time": "2025-08-30T17:25:42+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", - "version": "1.13.1", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git", - "reference": "1cf81305f2ffcf7dde9577c4f16a55c765192b03" + "reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/1cf81305f2ffcf7dde9577c4f16a55c765192b03", - "reference": "1cf81305f2ffcf7dde9577c4f16a55c765192b03", + "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/09e0dbe9743f21a76b1fe04b2b4136785775f52b", + "reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b", "shasum": "" }, "type": "library", @@ -199,25 +250,26 @@ "description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves", "support": { "issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.13.1" + "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.15.0" }, - "time": "2024-11-12T21:33:17+00:00" + "time": "2025-08-06T15:08:48+00:00" }, { "name": "pocketmine/bedrock-protocol", - "version": "35.0.0+bedrock-1.21.40", + "version": "50.0.0+bedrock-1.21.100", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459" + "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459", - "reference": "6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2d7aa27a5537ae593fb1c39158648ea462fef72a", + "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a", "shasum": "" }, "require": { + "ext-encoding": "~1.0.0", "ext-json": "*", "php": "^8.1", "pocketmine/binaryutils": "^0.2.0", @@ -227,10 +279,10 @@ "ramsey/uuid": "^4.1" }, "require-dev": { - "phpstan/phpstan": "1.11.9", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpstan/phpstan-strict-rules": "^1.0.0", - "phpunit/phpunit": "^9.5 || ^10.0" + "phpstan/phpstan": "2.1.0", + "phpstan/phpstan-phpunit": "^2.0.0", + "phpstan/phpstan-strict-rules": "^2.0.0", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0" }, "type": "library", "autoload": { @@ -245,9 +297,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/35.0.0+bedrock-1.21.40" + "source": "https://github.com/pmmp/BedrockProtocol/tree/50.0.0+bedrock-1.21.100" }, - "time": "2024-10-24T15:45:43+00:00" + "time": "2025-09-20T23:09:19+00:00" }, { "name": "pocketmine/binaryutils", @@ -420,16 +472,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.22.0", + "version": "2.25.1", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d" + "reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d", - "reference": "aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d", + "url": "https://api.github.com/repos/pmmp/Language/zipball/8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd", + "reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd", "shasum": "" }, "type": "library", @@ -437,9 +489,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.22.0" + "source": "https://github.com/pmmp/Language/tree/2.25.1" }, - "time": "2024-11-16T13:28:01+00:00" + "time": "2025-04-16T11:15:32+00:00" }, { "name": "pocketmine/log", @@ -525,16 +577,16 @@ }, { "name": "pocketmine/nbt", - "version": "1.0.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/pmmp/NBT.git", - "reference": "20540271cb59e04672cb163dca73366f207974f1" + "reference": "51b8d6a97065fb93e0b4f660b65164b6e1ed2fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/NBT/zipball/20540271cb59e04672cb163dca73366f207974f1", - "reference": "20540271cb59e04672cb163dca73366f207974f1", + "url": "https://api.github.com/repos/pmmp/NBT/zipball/51b8d6a97065fb93e0b4f660b65164b6e1ed2fff", + "reference": "51b8d6a97065fb93e0b4f660b65164b6e1ed2fff", "shasum": "" }, "require": { @@ -544,8 +596,9 @@ }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "1.10.25", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan": "2.1.27", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.5" }, "type": "library", @@ -561,76 +614,22 @@ "description": "PHP library for working with Named Binary Tags", "support": { "issues": "https://github.com/pmmp/NBT/issues", - "source": "https://github.com/pmmp/NBT/tree/1.0.0" + "source": "https://github.com/pmmp/NBT/tree/1.2.0" }, - "time": "2023-07-14T13:01:49+00:00" - }, - { - "name": "pocketmine/netresearch-jsonmapper", - "version": "v4.4.999", - "source": { - "type": "git", - "url": "https://github.com/pmmp/netresearch-jsonmapper.git", - "reference": "9a6610033d56e358e86a3e4fd5f87063c7318833" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pmmp/netresearch-jsonmapper/zipball/9a6610033d56e358e86a3e4fd5f87063c7318833", - "reference": "9a6610033d56e358e86a3e4fd5f87063c7318833", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=7.1" - }, - "replace": { - "netresearch/jsonmapper": "~4.2.0" - }, - "require-dev": { - "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", - "squizlabs/php_codesniffer": "~3.5" - }, - "type": "library", - "autoload": { - "psr-0": { - "JsonMapper": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "OSL-3.0" - ], - "authors": [ - { - "name": "Christian Weiske", - "email": "cweiske@cweiske.de", - "homepage": "http://github.com/cweiske/jsonmapper/", - "role": "Developer" - } - ], - "description": "Fork of netresearch/jsonmapper with security fixes needed by pocketmine/pocketmine-mp", - "support": { - "email": "cweiske@cweiske.de", - "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/pmmp/netresearch-jsonmapper/tree/v4.4.999" - }, - "time": "2024-02-23T13:17:01+00:00" + "time": "2025-09-19T18:09:30+00:00" }, { "name": "pocketmine/raklib", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/pmmp/RakLib.git", - "reference": "be2783be516bf6e2872ff5c81fb9048596617b97" + "reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/RakLib/zipball/be2783be516bf6e2872ff5c81fb9048596617b97", - "reference": "be2783be516bf6e2872ff5c81fb9048596617b97", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/a28d05216d34dbd00e8aed827a58df6b4c11510b", + "reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b", "shasum": "" }, "require": { @@ -642,8 +641,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": { @@ -658,9 +657,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.2.0" }, - "time": "2024-03-04T14:02:14+00:00" + "time": "2025-06-08T17:36:06+00:00" }, { "name": "pocketmine/raklib-ipc", @@ -745,16 +744,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -762,25 +761,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -818,37 +814,26 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.1", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", - "ext-json": "*", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -856,26 +841,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -910,32 +892,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.1" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-09-04T20:59:21+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", "shasum": "" }, "require": { @@ -972,7 +944,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" }, "funding": [ { @@ -984,82 +956,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "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": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -1067,102 +964,22 @@ "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": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.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" + "time": "2025-07-10T08:14:14+00:00" } ], "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1201,7 +1018,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -1209,20 +1026,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -1241,7 +1058,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1265,9 +1082,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", @@ -1389,20 +1206,20 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.11", + "version": "2.1.25", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3" + "reference": "4087d28bd252895874e174d65e26b2c202ed893a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3", - "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4087d28bd252895874e174d65e26b2c202ed893a", + "reference": "4087d28bd252895874e174d65e26b2c202ed893a", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -1443,34 +1260,35 @@ "type": "github" } ], - "time": "2024-08-19T14:37:29+00:00" + "time": "2025-09-12T14:26:42+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.0", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f3ea021866f4263f07ca3636bf22c64be9610c11", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc", + "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.18" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -1493,34 +1311,33 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7" }, - "time": "2024-04-20T06:39:00+00:00" + "time": "2025-07-13T11:31:46+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.6.0", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "363f921dd8441777d4fc137deb99beb486c77df1" + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1", - "reference": "363f921dd8441777d4fc137deb99beb486c77df1", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", + "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -1542,9 +1359,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.0" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" }, - "time": "2024-04-20T06:37:51+00:00" + "time": "2025-07-21T12:19:29+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1869,16 +1686,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.38", + "version": "10.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132" + "reference": "4b2d546b336876bd9562f24641b08a25335b06b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b2d546b336876bd9562f24641b08a25335b06b6", + "reference": "4b2d546b336876bd9562f24641b08a25335b06b6", "shasum": "" }, "require": { @@ -1888,7 +1705,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -1899,13 +1716,13 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", + "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.2", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", - "sebastian/recursion-context": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", "sebastian/type": "^4.0.0", "sebastian/version": "^4.0.1" }, @@ -1950,7 +1767,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.38" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.55" }, "funding": [ { @@ -1961,12 +1778,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": "2024-10-28T13:06:21+00:00" + "time": "2025-09-14T06:19:20+00:00" }, { "name": "sebastian/cli-parser", @@ -2138,16 +1963,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { @@ -2203,15 +2028,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "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/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", @@ -2714,23 +2551,23 @@ }, { "name": "sebastian/recursion-context", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -2765,15 +2602,28 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { "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/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", @@ -2937,7 +2787,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2948,6 +2798,7 @@ "ext-ctype": "*", "ext-curl": "*", "ext-date": "*", + "ext-encoding": "~1.0.0", "ext-gmp": "*", "ext-hash": "*", "ext-igbinary": "^3.0.1", @@ -2968,7 +2819,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 6e8578652..07ca545c4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,6 +4,8 @@ includes: - tests/phpstan/configs/impossible-generics.neon - tests/phpstan/configs/php-bugs.neon - tests/phpstan/configs/phpstan-bugs.neon + - tests/phpstan/configs/property-hook-sadness.neon + - tests/phpstan/configs/reflection-class-sadness.neon - tests/phpstan/configs/spl-fixed-array-sucks.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon @@ -11,14 +13,17 @@ includes: rules: - pocketmine\phpstan\rules\DeprecatedLegacyEnumAccessRule + - pocketmine\phpstan\rules\DisallowDynamicNewRule - pocketmine\phpstan\rules\DisallowEnumComparisonRule - pocketmine\phpstan\rules\DisallowForeachByReferenceRule - - pocketmine\phpstan\rules\UnsafeForeachArrayOfStringRule + - pocketmine\phpstan\rules\ExplodeLimitRule + - pocketmine\phpstan\rules\UnsafeForeachRule # - pocketmine\phpstan\rules\ThreadedSupportedTypesRule parameters: level: 9 checkMissingCallableSignature: true + rememberPossiblyImpureFunctionValues: false #risky to remember these, better for performance to avoid repeated calls anyway treatPhpDocTypesAsCertain: false bootstrapFiles: - tests/phpstan/bootstrap.php @@ -31,6 +36,7 @@ parameters: paths: - build - src + - tests/phpstan/DummyPluginOwned.php - tests/phpstan/rules - tests/phpunit - tests/plugins/TesterPlugin @@ -44,6 +50,7 @@ parameters: - pocketmine\DEBUG - pocketmine\IS_DEVELOPMENT_BUILD stubFiles: + - tests/phpstan/stubs/chunkutils2.stub - tests/phpstan/stubs/JsonMapper.stub - tests/phpstan/stubs/leveldb.stub - tests/phpstan/stubs/pmmpthread.stub diff --git a/resources/pocketmine.yml b/resources/pocketmine.yml index 408b5b95b..531924973 100644 --- a/resources/pocketmine.yml +++ b/resources/pocketmine.yml @@ -54,12 +54,6 @@ memory: #This only affects the main thread. Other threads should fire their own collections period: 36000 - #Fire asynchronous tasks to collect garbage from workers - collect-async-worker: true - - #Trigger on low memory - low-memory-trigger: true - #Settings controlling memory dump handling. memory-dump: #Dump memory from async workers as well as the main thread. If you have issues with segfaults when dumping memory, disable this setting. @@ -69,16 +63,6 @@ memory: #Cap maximum render distance per player when low memory is triggered. Set to 0 to disable cap. chunk-radius: 4 - #Do chunk garbage collection on trigger - trigger-chunk-collect: true - - world-caches: - #Disallow adding to world chunk-packet caches when memory is low - disable-chunk-cache: true - #Clear world caches when memory is low - low-memory-trigger: true - - network: #Threshold for batching packets, in bytes. Only these packets will be compressed #Set to 0 to compress everything, -1 to disable. diff --git a/src/GarbageCollectorManager.php b/src/GarbageCollectorManager.php new file mode 100644 index 000000000..294403a05 --- /dev/null +++ b/src/GarbageCollectorManager.php @@ -0,0 +1,114 @@ +logger = new \PrefixedLogger($logger, "Cyclic Garbage Collector"); + $this->timings = new TimingsHandler("Cyclic Garbage Collector", $parentTimings); + } + + private function adjustGcThreshold(int $cyclesCollected, int $rootsAfterGC) : void{ + //TODO Very simple heuristic for dynamic GC buffer resizing: + //If there are "too few" collections, increase the collection threshold + //by a fixed step + //Adapted from zend_gc.c/gc_adjust_threshold() as of PHP 8.3.14 + if($cyclesCollected < self::GC_THRESHOLD_TRIGGER || $rootsAfterGC >= $this->threshold){ + $this->threshold = min(self::GC_THRESHOLD_MAX, $this->threshold + self::GC_THRESHOLD_STEP); + }elseif($this->threshold > self::GC_THRESHOLD_DEFAULT){ + $this->threshold = max(self::GC_THRESHOLD_DEFAULT, $this->threshold - self::GC_THRESHOLD_STEP); + } + } + + public function getThreshold() : int{ return $this->threshold; } + + public function getCollectionTimeTotalNs() : int{ return $this->collectionTimeTotalNs; } + + public function maybeCollectCycles() : int{ + $rootsBefore = gc_status()["roots"]; + if($rootsBefore < $this->threshold){ + return 0; + } + + $this->timings->startTiming(); + + $start = hrtime(true); + $cycles = gc_collect_cycles(); + $end = hrtime(true); + + $rootsAfter = gc_status()["roots"]; + $this->adjustGcThreshold($cycles, $rootsAfter); + + $this->timings->stopTiming(); + + $time = $end - $start; + $this->collectionTimeTotalNs += $time; + $this->runs++; + $this->logger->info(sprintf( + "Run #%d took %s ms (%s -> %s roots, %s cycles collected) - cumulative GC time: %s ms", + $this->runs, + number_format($time / 1_000_000, 2), + $rootsBefore, + $rootsAfter, + $cycles, + number_format($this->collectionTimeTotalNs / 1_000_000, 2) + )); + + return $cycles; + } +} diff --git a/src/MemoryDump.php b/src/MemoryDump.php new file mode 100644 index 000000000..bd1e0fc9a --- /dev/null +++ b/src/MemoryDump.php @@ -0,0 +1,305 @@ +getProperties() as $property){ + if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){ + continue; + } + + if(!$property->isInitialized()){ + continue; + } + + $staticCount++; + $staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + + if(count($staticProperties[$className]) === 0){ + unset($staticProperties[$className]); + } + + foreach($reflection->getMethods() as $method){ + if($method->getDeclaringClass()->getName() !== $reflection->getName()){ + continue; + } + $methodStatics = []; + foreach(Utils::promoteKeys($method->getStaticVariables()) as $name => $variable){ + $methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + if(count($methodStatics) > 0){ + $functionStaticVars[$className . "::" . $method->getName()] = $methodStatics; + $functionStaticVarsCount += count($functionStaticVars); + } + } + } + + file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + $logger->info("Wrote $staticCount static properties"); + + $globalVariables = []; + $globalCount = 0; + + $ignoredGlobals = [ + 'GLOBALS' => true, + '_SERVER' => true, + '_REQUEST' => true, + '_POST' => true, + '_GET' => true, + '_FILES' => true, + '_ENV' => true, + '_COOKIE' => true, + '_SESSION' => true + ]; + + foreach(Utils::promoteKeys($GLOBALS) as $varName => $value){ + if(isset($ignoredGlobals[$varName])){ + continue; + } + + $globalCount++; + $globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + + file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + $logger->info("Wrote $globalCount global variables"); + + foreach(get_defined_functions()["user"] as $function){ + $reflect = new \ReflectionFunction($function); + + $vars = []; + foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $varName => $variable){ + $vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + if(count($vars) > 0){ + $functionStaticVars[$function] = $vars; + $functionStaticVarsCount += count($vars); + } + } + file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + $logger->info("Wrote $functionStaticVarsCount function static variables"); + + $data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + + do{ + $continue = false; + foreach(Utils::stringifyKeys($objects) as $hash => $object){ + if(!is_object($object)){ + continue; + } + $continue = true; + + $className = get_class($object); + if(!isset($instanceCounts[$className])){ + $instanceCounts[$className] = 1; + }else{ + $instanceCounts[$className]++; + } + + $objects[$hash] = true; + $info = [ + "information" => "$hash@$className", + ]; + if($object instanceof \Closure){ + $info["definition"] = Utils::getNiceClosureName($object); + $info["referencedVars"] = []; + $reflect = new \ReflectionFunction($object); + if(($closureThis = $reflect->getClosureThis()) !== null){ + $info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + + foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $name => $variable){ + $info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + }else{ + $reflection = new \ReflectionObject($object); + + $info["properties"] = []; + + for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){ + foreach($reflection->getProperties() as $property){ + if($property->isStatic()){ + continue; + } + + $name = $property->getName(); + if($reflection !== $original){ + if($property->isPrivate()){ + $name = $reflection->getName() . ":" . $name; + }else{ + continue; + } + } + if(!$property->isInitialized($object)){ + continue; + } + + $info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + } + } + + fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); + } + + }while($continue); + + $logger->info("Wrote " . count($objects) . " objects"); + + fclose($obData); + + file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + + arsort($instanceCounts, SORT_NUMERIC); + file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + + $logger->info("Finished!"); + + ini_set('memory_limit', $hardLimit); + if($gcEnabled){ + gc_enable(); + } + } + + /** + * @param object[]|true[] $objects reference parameter + * @param int[] $refCounts reference parameter + * + * @phpstan-param array $objects + * @phpstan-param array $refCounts + * @phpstan-param-out array $objects + * @phpstan-param-out array $refCounts + */ + private static function continueDump(mixed $from, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize) : mixed{ + if($maxNesting <= 0){ + return "(error) NESTING LIMIT REACHED"; + } + + --$maxNesting; + + if(is_object($from)){ + if(!isset($objects[$hash = spl_object_hash($from)])){ + $objects[$hash] = $from; + $refCounts[$hash] = 0; + } + + ++$refCounts[$hash]; + + $data = "(object) $hash"; + }elseif(is_array($from)){ + if($recursion >= 5){ + return "(error) ARRAY RECURSION LIMIT REACHED"; + } + $data = []; + $numeric = 0; + foreach(Utils::promoteKeys($from) as $key => $value){ + $data[$numeric] = [ + "k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), + "v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), + ]; + $numeric++; + } + }elseif(is_string($from)){ + $data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize); + }elseif(is_resource($from)){ + $data = "(resource) " . print_r($from, true); + }elseif(is_float($from)){ + $data = "(float) $from"; + }else{ + $data = $from; + } + + return $data; + } +} diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 4308167d3..2b7f5f1d3 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -29,52 +29,24 @@ use pocketmine\scheduler\DumpWorkerMemoryTask; use pocketmine\scheduler\GarbageCollectionTask; use pocketmine\timings\Timings; use pocketmine\utils\Process; -use pocketmine\utils\Utils; use pocketmine\YmlServerProperties as Yml; -use Symfony\Component\Filesystem\Path; -use function arsort; -use function count; -use function fclose; -use function file_exists; -use function file_put_contents; -use function fopen; -use function fwrite; use function gc_collect_cycles; -use function gc_disable; -use function gc_enable; use function gc_mem_caches; -use function get_class; -use function get_declared_classes; -use function get_defined_functions; -use function ini_get; use function ini_set; use function intdiv; -use function is_array; -use function is_float; -use function is_object; -use function is_resource; -use function is_string; -use function json_encode; use function mb_strtoupper; use function min; -use function mkdir; use function preg_match; -use function print_r; use function round; -use function spl_object_hash; use function sprintf; -use function strlen; -use function substr; -use const JSON_PRETTY_PRINT; -use const JSON_THROW_ON_ERROR; -use const JSON_UNESCAPED_SLASHES; -use const SORT_NUMERIC; class MemoryManager{ private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND; private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2; private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND; + private GarbageCollectorManager $cycleGcManager; + private int $memoryLimit; private int $globalMemoryLimit; private int $checkRate; @@ -88,14 +60,8 @@ class MemoryManager{ private int $garbageCollectionPeriod; private int $garbageCollectionTicker = 0; - private bool $garbageCollectionTrigger; - private bool $garbageCollectionAsync; private int $lowMemChunkRadiusOverride; - private bool $lowMemChunkGC; - - private bool $lowMemDisableChunkCache; - private bool $lowMemClearWorldCache; private bool $dumpWorkers = true; @@ -105,6 +71,7 @@ class MemoryManager{ private Server $server ){ $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); + $this->cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$memoryManager); $this->init($server->getConfigGroup()); } @@ -142,17 +109,10 @@ class MemoryManager{ $this->continuousTriggerRate = $config->getPropertyInt(Yml::MEMORY_CONTINUOUS_TRIGGER_RATE, self::DEFAULT_CONTINUOUS_TRIGGER_RATE); $this->garbageCollectionPeriod = $config->getPropertyInt(Yml::MEMORY_GARBAGE_COLLECTION_PERIOD, self::DEFAULT_TICKS_PER_GC); - $this->garbageCollectionTrigger = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_LOW_MEMORY_TRIGGER, true); - $this->garbageCollectionAsync = $config->getPropertyBool(Yml::MEMORY_GARBAGE_COLLECTION_COLLECT_ASYNC_WORKER, true); $this->lowMemChunkRadiusOverride = $config->getPropertyInt(Yml::MEMORY_MAX_CHUNKS_CHUNK_RADIUS, 4); - $this->lowMemChunkGC = $config->getPropertyBool(Yml::MEMORY_MAX_CHUNKS_TRIGGER_CHUNK_COLLECT, true); - - $this->lowMemDisableChunkCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_DISABLE_CHUNK_CACHE, true); - $this->lowMemClearWorldCache = $config->getPropertyBool(Yml::MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER, true); $this->dumpWorkers = $config->getPropertyBool(Yml::MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER, true); - gc_enable(); } public function isLowMemory() : bool{ @@ -163,8 +123,11 @@ class MemoryManager{ return $this->globalMemoryLimit; } + /** + * @deprecated + */ public function canUseChunkCache() : bool{ - return !$this->lowMemory || !$this->lowMemDisableChunkCache; + return !$this->lowMemory; } /** @@ -180,26 +143,19 @@ class MemoryManager{ public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{ $this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB", $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2))); - if($this->lowMemClearWorldCache){ - foreach($this->server->getWorldManager()->getWorlds() as $world){ - $world->clearCache(true); - } - ChunkCache::pruneCaches(); + foreach($this->server->getWorldManager()->getWorlds() as $world){ + $world->clearCache(true); } + ChunkCache::pruneCaches(); - if($this->lowMemChunkGC){ - foreach($this->server->getWorldManager()->getWorlds() as $world){ - $world->doChunkGarbageCollection(); - } + foreach($this->server->getWorldManager()->getWorlds() as $world){ + $world->doChunkGarbageCollection(); } $ev = new LowMemoryEvent($memory, $limit, $global, $triggerCount); $ev->call(); - $cycles = 0; - if($this->garbageCollectionTrigger){ - $cycles = $this->triggerGarbageCollector(); - } + $cycles = $this->triggerGarbageCollector(); $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } @@ -239,6 +195,8 @@ class MemoryManager{ if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){ $this->garbageCollectionTicker = 0; $this->triggerGarbageCollector(); + }else{ + $this->cycleGcManager->maybeCollectCycles(); } Timings::$memoryManager->stopTiming(); @@ -247,14 +205,12 @@ class MemoryManager{ public function triggerGarbageCollector() : int{ Timings::$garbageCollector->startTiming(); - if($this->garbageCollectionAsync){ - $pool = $this->server->getAsyncPool(); - if(($w = $pool->shutdownUnusedWorkers()) > 0){ - $this->logger->debug("Shut down $w idle async pool workers"); - } - foreach($pool->getRunningWorkers() as $i){ - $pool->submitTaskToWorker(new GarbageCollectionTask(), $i); - } + $pool = $this->server->getAsyncPool(); + if(($w = $pool->shutdownUnusedWorkers()) > 0){ + $this->logger->debug("Shut down $w idle async pool workers"); + } + foreach($pool->getRunningWorkers() as $i){ + $pool->submitTaskToWorker(new GarbageCollectionTask(), $i); } $cycles = gc_collect_cycles(); @@ -271,7 +227,7 @@ class MemoryManager{ public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{ $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump"); $logger->notice("After the memory dump is done, the server might crash"); - self::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger); + MemoryDump::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger); if($this->dumpWorkers){ $pool = $this->server->getAsyncPool(); @@ -283,239 +239,10 @@ class MemoryManager{ /** * Static memory dumper accessible from any thread. + * @deprecated + * @see MemoryDump */ public static function dumpMemory(mixed $startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{ - $hardLimit = Utils::assumeNotFalse(ini_get('memory_limit'), "memory_limit INI directive should always exist"); - ini_set('memory_limit', '-1'); - gc_disable(); - - if(!file_exists($outputFolder)){ - mkdir($outputFolder, 0777, true); - } - - $obData = Utils::assumeNotFalse(fopen(Path::join($outputFolder, "objects.js"), "wb+")); - - $objects = []; - - $refCounts = []; - - $instanceCounts = []; - - $staticProperties = []; - $staticCount = 0; - - $functionStaticVars = []; - $functionStaticVarsCount = 0; - - foreach(get_declared_classes() as $className){ - $reflection = new \ReflectionClass($className); - $staticProperties[$className] = []; - foreach($reflection->getProperties() as $property){ - if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){ - continue; - } - - if(!$property->isInitialized()){ - continue; - } - - $staticCount++; - $staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - - if(count($staticProperties[$className]) === 0){ - unset($staticProperties[$className]); - } - - foreach($reflection->getMethods() as $method){ - if($method->getDeclaringClass()->getName() !== $reflection->getName()){ - continue; - } - $methodStatics = []; - foreach(Utils::promoteKeys($method->getStaticVariables()) as $name => $variable){ - $methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - if(count($methodStatics) > 0){ - $functionStaticVars[$className . "::" . $method->getName()] = $methodStatics; - $functionStaticVarsCount += count($functionStaticVars); - } - } - } - - file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - $logger->info("Wrote $staticCount static properties"); - - $globalVariables = []; - $globalCount = 0; - - $ignoredGlobals = [ - 'GLOBALS' => true, - '_SERVER' => true, - '_REQUEST' => true, - '_POST' => true, - '_GET' => true, - '_FILES' => true, - '_ENV' => true, - '_COOKIE' => true, - '_SESSION' => true - ]; - - foreach(Utils::promoteKeys($GLOBALS) as $varName => $value){ - if(isset($ignoredGlobals[$varName])){ - continue; - } - - $globalCount++; - $globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - - file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - $logger->info("Wrote $globalCount global variables"); - - foreach(get_defined_functions()["user"] as $function){ - $reflect = new \ReflectionFunction($function); - - $vars = []; - foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $varName => $variable){ - $vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - if(count($vars) > 0){ - $functionStaticVars[$function] = $vars; - $functionStaticVarsCount += count($vars); - } - } - file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - $logger->info("Wrote $functionStaticVarsCount function static variables"); - - $data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - - do{ - $continue = false; - foreach(Utils::stringifyKeys($objects) as $hash => $object){ - if(!is_object($object)){ - continue; - } - $continue = true; - - $className = get_class($object); - if(!isset($instanceCounts[$className])){ - $instanceCounts[$className] = 1; - }else{ - $instanceCounts[$className]++; - } - - $objects[$hash] = true; - $info = [ - "information" => "$hash@$className", - ]; - if($object instanceof \Closure){ - $info["definition"] = Utils::getNiceClosureName($object); - $info["referencedVars"] = []; - $reflect = new \ReflectionFunction($object); - if(($closureThis = $reflect->getClosureThis()) !== null){ - $info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - - foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $name => $variable){ - $info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - }else{ - $reflection = new \ReflectionObject($object); - - $info["properties"] = []; - - for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){ - foreach($reflection->getProperties() as $property){ - if($property->isStatic()){ - continue; - } - - $name = $property->getName(); - if($reflection !== $original){ - if($property->isPrivate()){ - $name = $reflection->getName() . ":" . $name; - }else{ - continue; - } - } - if(!$property->isInitialized($object)){ - continue; - } - - $info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize); - } - } - } - - fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); - } - - }while($continue); - - $logger->info("Wrote " . count($objects) . " objects"); - - fclose($obData); - - file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - - arsort($instanceCounts, SORT_NUMERIC); - file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - - $logger->info("Finished!"); - - ini_set('memory_limit', $hardLimit); - gc_enable(); - } - - /** - * @param object[] $objects reference parameter - * @param int[] $refCounts reference parameter - * - * @phpstan-param array $objects - * @phpstan-param array $refCounts - * @phpstan-param-out array $objects - * @phpstan-param-out array $refCounts - */ - private static function continueDump(mixed $from, array &$objects, array &$refCounts, int $recursion, int $maxNesting, int $maxStringSize) : mixed{ - if($maxNesting <= 0){ - return "(error) NESTING LIMIT REACHED"; - } - - --$maxNesting; - - if(is_object($from)){ - if(!isset($objects[$hash = spl_object_hash($from)])){ - $objects[$hash] = $from; - $refCounts[$hash] = 0; - } - - ++$refCounts[$hash]; - - $data = "(object) $hash"; - }elseif(is_array($from)){ - if($recursion >= 5){ - return "(error) ARRAY RECURSION LIMIT REACHED"; - } - $data = []; - $numeric = 0; - foreach(Utils::promoteKeys($from) as $key => $value){ - $data[$numeric] = [ - "k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), - "v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), - ]; - $numeric++; - } - }elseif(is_string($from)){ - $data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize); - }elseif(is_resource($from)){ - $data = "(resource) " . print_r($from, true); - }elseif(is_float($from)){ - $data = "(float) $from"; - }else{ - $data = $from; - } - - return $data; + MemoryDump::dumpMemory($startingObject, $outputFolder, $maxNesting, $maxStringSize, $logger); } } diff --git a/src/PocketMine.php b/src/PocketMine.php index b2e1cd046..c34b8e221 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -98,6 +98,7 @@ namespace pocketmine { "crypto" => "php-crypto", "ctype" => "ctype", "date" => "Date", + "encoding" => "pmmp/ext-encoding", "gmp" => "GMP", "hash" => "Hash", "igbinary" => "igbinary", @@ -155,6 +156,12 @@ namespace pocketmine { } } + if(($encoding_version = phpversion("encoding")) !== false){ + if(version_compare($encoding_version, "1.0.0") < 0 || version_compare($encoding_version, "2.0.0") >= 0){ + $messages[] = "pmmp/ext-encoding ^1.0.0 is required, while you have $encoding_version."; + } + } + if(extension_loaded("pocketmine")){ $messages[] = "The native PocketMine extension is no longer supported."; } @@ -264,7 +271,7 @@ JIT_WARNING $composerGitHash = InstalledVersions::getReference('pocketmine/pocketmine-mp'); if($composerGitHash !== null){ //we can't verify dependency versions if we were installed without using git - $currentGitHash = explode("-", VersionInfo::GIT_HASH())[0]; + $currentGitHash = explode("-", VersionInfo::GIT_HASH(), 2)[0]; if($currentGitHash !== $composerGitHash){ critical_error("Composer dependencies and/or autoloader are out of sync."); critical_error("- Current revision is $currentGitHash"); @@ -282,6 +289,11 @@ JIT_WARNING exit(0); } + if(defined('pocketmine\ORIGINAL_PHAR_PATH')){ + //if we're inside a phar cache, \pocketmine\PATH will not include the original phar + Filesystem::addCleanedPath(ORIGINAL_PHAR_PATH, Filesystem::CLEAN_PATH_SRC_PREFIX); + } + $cwd = Utils::assumeNotFalse(realpath(Utils::assumeNotFalse(getcwd()))); $dataPath = getopt_string(BootstrapOptions::DATA) ?? $cwd; $pluginPath = getopt_string(BootstrapOptions::PLUGINS) ?? $cwd . DIRECTORY_SEPARATOR . "plugins"; diff --git a/src/Server.php b/src/Server.php index 9003a7f9c..af9cbeda7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -36,6 +36,7 @@ use pocketmine\crafting\CraftingManager; use pocketmine\crafting\CraftingManagerFromDataHelper; use pocketmine\crash\CrashDump; use pocketmine\crash\CrashDumpRenderer; +use pocketmine\data\bedrock\BedrockDataFiles; use pocketmine\entity\EntityDataHelper; use pocketmine\entity\Location; use pocketmine\event\HandlerListManager; @@ -49,6 +50,7 @@ use pocketmine\lang\Language; use pocketmine\lang\LanguageNotFoundException; use pocketmine\lang\Translatable; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\auth\AuthKeyProvider; use pocketmine\network\mcpe\compression\CompressBatchPromise; use pocketmine\network\mcpe\compression\CompressBatchTask; use pocketmine\network\mcpe\compression\Compressor; @@ -138,6 +140,7 @@ use function file_put_contents; use function filemtime; use function fopen; use function get_class; +use function gettype; use function ini_set; use function is_array; use function is_dir; @@ -268,6 +271,7 @@ class Server{ private int $maxPlayers; private bool $onlineMode = true; + private AuthKeyProvider $authKeyProvider; private Network $network; private bool $networkCompressionAsync = true; @@ -697,7 +701,7 @@ class Server{ public function removeOp(string $name) : void{ $lowercaseName = strtolower($name); - foreach($this->operators->getAll() as $operatorName => $_){ + foreach(Utils::promoteKeys($this->operators->getAll()) as $operatorName => $_){ $operatorName = (string) $operatorName; if($lowercaseName === strtolower($operatorName)){ $this->operators->remove($operatorName); @@ -918,6 +922,7 @@ class Server{ TimingsHandler::getCollectCallbacks()->add(function() : array{ $promises = []; foreach($this->asyncPool->getRunningWorkers() as $workerId){ + /** @phpstan-var PromiseResolver> $resolver */ $resolver = new PromiseResolver(); $this->asyncPool->submitTaskToWorker(new TimingsCollectionTask($resolver), $workerId); @@ -979,6 +984,8 @@ class Server{ $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled())); } + $this->authKeyProvider = new AuthKeyProvider(new \PrefixedLogger($this->logger, "Minecraft Auth Key Provider"), $this->asyncPool); + if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){ $this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD); } @@ -1003,7 +1010,7 @@ class Server{ $this->commandMap = new SimpleCommandMap($this); - $this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes")); + $this->craftingManager = CraftingManagerFromDataHelper::make(BedrockDataFiles::RECIPES); $this->resourceManager = new ResourcePackManager(Path::join($this->dataPath, "resource_packs"), $this->logger); @@ -1013,7 +1020,11 @@ class Server{ copy(Path::join(\pocketmine\RESOURCE_PATH, 'plugin_list.yml'), $graylistFile); } try{ - $pluginGraylist = PluginGraylist::fromArray(yaml_parse(Filesystem::fileGetContents($graylistFile))); + $array = yaml_parse(Filesystem::fileGetContents($graylistFile)); + if(!is_array($array)){ + throw new \InvalidArgumentException("Expected array for root, but have " . gettype($array)); + } + $pluginGraylist = PluginGraylist::fromArray($array); }catch(\InvalidArgumentException $e){ $this->logger->emergency("Failed to load $graylistFile: " . $e->getMessage()); $this->forceShutdownExit(); @@ -1174,7 +1185,7 @@ class Server{ if($this->worldManager->getDefaultWorld() === null){ $default = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world"); - if(trim($default) == ""){ + if(trim($default) === ""){ $this->logger->warning("level-name cannot be null, using default"); $default = "world"; $this->configGroup->setConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world"); @@ -1611,7 +1622,7 @@ class Server{ if(!is_dir($crashFolder)){ mkdir($crashFolder); } - $crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log"); + $crashDumpPath = Path::join($crashFolder, date("Y-m-d_H.i.s_T", (int) $dump->getData()->time) . ".log"); $fp = @fopen($crashDumpPath, "wb"); if(!is_resource($fp)){ @@ -1793,6 +1804,13 @@ class Server{ return $this->forceLanguage; } + /** + * @internal + */ + public function getAuthKeyProvider() : AuthKeyProvider{ + return $this->authKeyProvider; + } + public function getNetwork() : Network{ return $this->network; } diff --git a/src/VersionInfo.php b/src/VersionInfo.php index efb38d71b..944d6d84f 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,7 +31,7 @@ use function str_repeat; final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.21.3"; + public const BASE_VERSION = "5.33.3"; public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; diff --git a/src/YmlServerProperties.php b/src/YmlServerProperties.php index 9bd203eef..282b0b3cd 100644 --- a/src/YmlServerProperties.php +++ b/src/YmlServerProperties.php @@ -75,20 +75,14 @@ final class YmlServerProperties{ public const MEMORY_CONTINUOUS_TRIGGER = 'memory.continuous-trigger'; public const MEMORY_CONTINUOUS_TRIGGER_RATE = 'memory.continuous-trigger-rate'; public const MEMORY_GARBAGE_COLLECTION = 'memory.garbage-collection'; - public const MEMORY_GARBAGE_COLLECTION_COLLECT_ASYNC_WORKER = 'memory.garbage-collection.collect-async-worker'; - public const MEMORY_GARBAGE_COLLECTION_LOW_MEMORY_TRIGGER = 'memory.garbage-collection.low-memory-trigger'; public const MEMORY_GARBAGE_COLLECTION_PERIOD = 'memory.garbage-collection.period'; public const MEMORY_GLOBAL_LIMIT = 'memory.global-limit'; public const MEMORY_MAIN_HARD_LIMIT = 'memory.main-hard-limit'; public const MEMORY_MAIN_LIMIT = 'memory.main-limit'; public const MEMORY_MAX_CHUNKS = 'memory.max-chunks'; public const MEMORY_MAX_CHUNKS_CHUNK_RADIUS = 'memory.max-chunks.chunk-radius'; - public const MEMORY_MAX_CHUNKS_TRIGGER_CHUNK_COLLECT = 'memory.max-chunks.trigger-chunk-collect'; public const MEMORY_MEMORY_DUMP = 'memory.memory-dump'; public const MEMORY_MEMORY_DUMP_DUMP_ASYNC_WORKER = 'memory.memory-dump.dump-async-worker'; - public const MEMORY_WORLD_CACHES = 'memory.world-caches'; - public const MEMORY_WORLD_CACHES_DISABLE_CHUNK_CACHE = 'memory.world-caches.disable-chunk-cache'; - public const MEMORY_WORLD_CACHES_LOW_MEMORY_TRIGGER = 'memory.world-caches.low-memory-trigger'; public const NETWORK = 'network'; public const NETWORK_ASYNC_COMPRESSION = 'network.async-compression'; public const NETWORK_ASYNC_COMPRESSION_THRESHOLD = 'network.async-compression-threshold'; diff --git a/src/block/ActivatorRail.php b/src/block/ActivatorRail.php index dcd0ef93b..da15eb1e8 100644 --- a/src/block/ActivatorRail.php +++ b/src/block/ActivatorRail.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\RailPoweredByRedstoneTrait; -class ActivatorRail extends StraightOnlyRail{ +class ActivatorRail extends StraightOnlyRail implements PoweredByRedstone{ use RailPoweredByRedstoneTrait; //TODO diff --git a/src/block/AmethystCluster.php b/src/block/AmethystCluster.php index 639490456..8a750e974 100644 --- a/src/block/AmethystCluster.php +++ b/src/block/AmethystCluster.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\AmethystTrait; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\block\utils\FortuneDropHelper; use pocketmine\block\utils\SupportType; @@ -38,7 +39,7 @@ use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; use pocketmine\world\BlockTransaction; -final class AmethystCluster extends Transparent{ +final class AmethystCluster extends Transparent implements AnyFacing{ use AmethystTrait; use AnyFacingTrait; diff --git a/src/block/Anvil.php b/src/block/Anvil.php index 0a1a47070..fcb4d045c 100644 --- a/src/block/Anvil.php +++ b/src/block/Anvil.php @@ -26,6 +26,7 @@ namespace pocketmine\block; use pocketmine\block\inventory\AnvilInventory; use pocketmine\block\utils\Fallable; use pocketmine\block\utils\FallableTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -41,7 +42,7 @@ use pocketmine\world\sound\AnvilFallSound; use pocketmine\world\sound\Sound; use function round; -class Anvil extends Transparent implements Fallable{ +class Anvil extends Transparent implements Fallable, HorizontalFacing{ use FallableTrait; use HorizontalFacingTrait; @@ -70,9 +71,6 @@ class Anvil extends Transparent implements Fallable{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->squash(Facing::axis(Facing::rotateY($this->facing, false)), 1 / 8)]; } diff --git a/src/block/Bamboo.php b/src/block/Bamboo.php index 9f605bca6..fd64e10ef 100644 --- a/src/block/Bamboo.php +++ b/src/block/Bamboo.php @@ -87,9 +87,6 @@ class Bamboo extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ //this places the BB at the northwest corner, not the center $inset = 1 - (($this->thick ? 3 : 2) / 16); diff --git a/src/block/Barrel.php b/src/block/Barrel.php index 0f0499ab9..7b2ea356e 100644 --- a/src/block/Barrel.php +++ b/src/block/Barrel.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\Barrel as TileBarrel; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -33,7 +34,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use function abs; -class Barrel extends Opaque{ +class Barrel extends Opaque implements AnyFacing{ use AnyFacingTrait; protected bool $open = false; diff --git a/src/block/BaseBanner.php b/src/block/BaseBanner.php index 6b9e493d1..e8bf187ee 100644 --- a/src/block/BaseBanner.php +++ b/src/block/BaseBanner.php @@ -25,19 +25,19 @@ namespace pocketmine\block; use pocketmine\block\tile\Banner as TileBanner; use pocketmine\block\utils\BannerPatternLayer; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\SupportType; use pocketmine\item\Banner as ItemBanner; use pocketmine\item\Item; use pocketmine\item\VanillaItems; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use function assert; use function count; -abstract class BaseBanner extends Transparent{ +abstract class BaseBanner extends Transparent implements Colored{ use ColoredTrait; /** @@ -50,6 +50,10 @@ abstract class BaseBanner extends Transparent{ parent::readStateFromWorld(); $tile = $this->position->getWorld()->getTile($this->position); if($tile instanceof TileBanner){ + if($tile->getType() === TileBanner::TYPE_OMINOUS){ + //illager banner is implemented as a separate block, as it doesn't support base color or custom patterns + return $this->getOminousVersion(); + } $this->color = $tile->getBaseColor(); $this->setPatterns($tile->getPatterns()); } @@ -57,6 +61,13 @@ abstract class BaseBanner extends Transparent{ return $this; } + /** + * TODO: make this abstract in PM6 (BC break) + */ + protected function getOminousVersion() : Block{ + return VanillaBlocks::AIR(); + } + public function writeStateToWorld() : void{ parent::writeStateToWorld(); $tile = $this->position->getWorld()->getTile($this->position); @@ -97,9 +108,6 @@ abstract class BaseBanner extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/BaseBigDripleaf.php b/src/block/BaseBigDripleaf.php index f0ff59cf0..94e2c12a2 100644 --- a/src/block/BaseBigDripleaf.php +++ b/src/block/BaseBigDripleaf.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\event\block\StructureGrowEvent; @@ -33,7 +34,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -abstract class BaseBigDripleaf extends Transparent{ +abstract class BaseBigDripleaf extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; abstract protected function isHead() : bool; diff --git a/src/block/BaseCoral.php b/src/block/BaseCoral.php index b9c595a97..c1cc9bb25 100644 --- a/src/block/BaseCoral.php +++ b/src/block/BaseCoral.php @@ -24,12 +24,13 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\CoralMaterial; use pocketmine\block\utils\CoralTypeTrait; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; use function mt_rand; -abstract class BaseCoral extends Transparent{ +abstract class BaseCoral extends Transparent implements CoralMaterial{ use CoralTypeTrait; public function onNearbyBlockChange() : void{ diff --git a/src/block/BaseOminousBanner.php b/src/block/BaseOminousBanner.php new file mode 100644 index 000000000..192e6fac2 --- /dev/null +++ b/src/block/BaseOminousBanner.php @@ -0,0 +1,90 @@ +position->getWorld()->getTile($this->position); + assert($tile instanceof TileBanner); + $tile->setBaseColor(DyeColor::WHITE); + $tile->setPatterns([]); + $tile->setType(TileBanner::TYPE_OMINOUS); + } + + public function isSolid() : bool{ + return false; + } + + public function getMaxStackSize() : int{ + return 16; + } + + public function getFuelTime() : int{ + return 300; + } + + protected function recalculateCollisionBoxes() : array{ + return []; + } + + public function getSupportType(int $facing) : SupportType{ + return SupportType::NONE; + } + + private function canBeSupportedBy(Block $block) : bool{ + return $block->isSolid(); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->canBeSupportedBy($blockReplace->getSide($this->getSupportingFace()))){ + return false; + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + abstract protected function getSupportingFace() : int; + + public function onNearbyBlockChange() : void{ + if(!$this->canBeSupportedBy($this->getSide($this->getSupportingFace()))){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function asItem() : Item{ + return VanillaItems::OMINOUS_BANNER(); + } +} diff --git a/src/block/BaseSign.php b/src/block/BaseSign.php index 5a905f8b8..8c324918b 100644 --- a/src/block/BaseSign.php +++ b/src/block/BaseSign.php @@ -27,6 +27,7 @@ use pocketmine\block\tile\Sign as TileSign; use pocketmine\block\utils\DyeColor; use pocketmine\block\utils\SignText; use pocketmine\block\utils\SupportType; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodType; use pocketmine\block\utils\WoodTypeTrait; use pocketmine\color\Color; @@ -34,21 +35,25 @@ use pocketmine\event\block\SignChangeEvent; use pocketmine\item\Dye; use pocketmine\item\Item; use pocketmine\item\ItemTypeIds; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\utils\TextFormat; use pocketmine\world\BlockTransaction; use pocketmine\world\sound\DyeUseSound; use pocketmine\world\sound\InkSacUseSound; +use function abs; use function array_map; use function assert; +use function atan2; +use function fmod; +use function rad2deg; use function strlen; -abstract class BaseSign extends Transparent{ +abstract class BaseSign extends Transparent implements WoodMaterial{ use WoodTypeTrait; - protected SignText $text; + protected SignText $text; //TODO: rename this (BC break) + protected SignText $backText; private bool $waxed = false; protected ?int $editorEntityRuntimeId = null; @@ -63,6 +68,7 @@ abstract class BaseSign extends Transparent{ $this->woodType = $woodType; parent::__construct($idInfo, $name, $typeInfo); $this->text = new SignText(); + $this->backText = new SignText(); $this->asItemCallback = $asItemCallback; } @@ -71,6 +77,7 @@ abstract class BaseSign extends Transparent{ $tile = $this->position->getWorld()->getTile($this->position); if($tile instanceof TileSign){ $this->text = $tile->getText(); + $this->backText = $tile->getBackText(); $this->waxed = $tile->isWaxed(); $this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId(); } @@ -83,6 +90,7 @@ abstract class BaseSign extends Transparent{ $tile = $this->position->getWorld()->getTile($this->position); assert($tile instanceof TileSign); $tile->setText($this->text); + $tile->setBackText($this->backText); $tile->setWaxed($this->waxed); $tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId); } @@ -95,9 +103,6 @@ abstract class BaseSign extends Transparent{ return 16; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } @@ -130,11 +135,11 @@ abstract class BaseSign extends Transparent{ } } - private function doSignChange(SignText $newText, Player $player, Item $item) : bool{ - $ev = new SignChangeEvent($this, $player, $newText); + private function doSignChange(SignText $newText, Player $player, Item $item, bool $frontFace) : bool{ + $ev = new SignChangeEvent($this, $player, $newText, $frontFace); $ev->call(); if(!$ev->isCancelled()){ - $this->text = $ev->getNewText(); + $this->setFaceText($frontFace, $ev->getNewText()); $this->position->getWorld()->setBlock($this->position, $this); $item->pop(); return true; @@ -143,8 +148,9 @@ abstract class BaseSign extends Transparent{ return false; } - private function changeSignGlowingState(bool $glowing, Player $player, Item $item) : bool{ - if($this->text->isGlowing() !== $glowing && $this->doSignChange(new SignText($this->text->getLines(), $this->text->getBaseColor(), $glowing), $player, $item)){ + private function changeSignGlowingState(bool $glowing, Player $player, Item $item, bool $frontFace) : bool{ + $text = $this->getFaceText($frontFace); + if($text->isGlowing() !== $glowing && $this->doSignChange(new SignText($text->getLines(), $text->getBaseColor(), $glowing), $player, $item, $frontFace)){ $this->position->getWorld()->addSound($this->position, new InkSacUseSound()); return true; } @@ -171,6 +177,8 @@ abstract class BaseSign extends Transparent{ return true; } + $frontFace = $this->interactsFront($this->getHitboxCenter(), $player->getPosition(), $this->getFacingDegrees()); + $dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){ ItemTypeIds::BONE_MEAL => DyeColor::WHITE, ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE, @@ -179,40 +187,82 @@ abstract class BaseSign extends Transparent{ }; if($dyeColor !== null){ $color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue(); + $text = $this->getFaceText($frontFace); if( - $color->toARGB() !== $this->text->getBaseColor()->toARGB() && - $this->doSignChange(new SignText($this->text->getLines(), $color, $this->text->isGlowing()), $player, $item) + $color->toARGB() !== $text->getBaseColor()->toARGB() && + $this->doSignChange(new SignText($text->getLines(), $color, $text->isGlowing()), $player, $item, $frontFace) ){ $this->position->getWorld()->addSound($this->position, new DyeUseSound()); return true; } }elseif(match($item->getTypeId()){ - ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item), - ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item), + ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item, $frontFace), + ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item, $frontFace), ItemTypeIds::HONEYCOMB => $this->wax($player, $item), default => false }){ return true; } - $player->openSignEditor($this->position); + $player->openSignEditor($this->position, $frontFace); return true; } + private function interactsFront(Vector3 $hitboxCenter, Vector3 $playerPosition, float $signFacingDegrees) : bool{ + $playerCenterDiffX = $playerPosition->x - $hitboxCenter->x; + $playerCenterDiffZ = $playerPosition->z - $hitboxCenter->z; + + $f1 = rad2deg(atan2($playerCenterDiffZ, $playerCenterDiffX)) - 90.0; + + $rotationDiff = $signFacingDegrees - $f1; + $rotation = fmod($rotationDiff + 180.0, 360.0) - 180.0; // Normalize to [-180, 180] + return abs($rotation) <= 90.0; + } + + /** + * Returns the center of the sign's hitbox. Used to decide which face of the sign to open when a player interacts. + */ + protected function getHitboxCenter() : Vector3{ + return $this->position->add(0.5, 0.5, 0.5); + } + + /** + * TODO: make this abstract (BC break) + */ + protected function getFacingDegrees() : float{ + return 0; + } + /** * Returns an object containing information about the sign text. + * @deprecated + * @see self::getFaceText() */ public function getText() : SignText{ return $this->text; } - /** @return $this */ + /** + * @deprecated + * @see self::setFaceText() + * @return $this + */ public function setText(SignText $text) : self{ $this->text = $text; return $this; } + public function getFaceText(bool $frontFace) : SignText{ + return $frontFace ? $this->text : $this->backText; + } + + /** @return $this */ + public function setFaceText(bool $frontFace, SignText $text) : self{ + $frontFace ? $this->text = $text : $this->backText = $text; + return $this; + } + /** * Returns whether the sign has been waxed using a honeycomb. If true, the sign cannot be edited by a player. */ @@ -237,13 +287,21 @@ abstract class BaseSign extends Transparent{ return $this; } + /** + * @deprecated + * @see self::updateFaceText() + */ + public function updateText(Player $author, SignText $text) : bool{ + return $this->updateFaceText($author, true, $text); + } + /** * Called by the player controller (network session) to update the sign text, firing events as appropriate. * * @return bool if the sign update was successful. * @throws \UnexpectedValueException if the text payload is too large */ - public function updateText(Player $author, SignText $text) : bool{ + public function updateFaceText(Player $author, bool $frontFace, SignText $text) : bool{ $size = 0; foreach($text->getLines() as $line){ $size += strlen($line); @@ -251,15 +309,16 @@ abstract class BaseSign extends Transparent{ if($size > 1000){ throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)"); } + $oldText = $this->getFaceText($frontFace); $ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{ return TextFormat::clean($line, false); - }, $text->getLines()), $this->text->getBaseColor(), $this->text->isGlowing())); + }, $text->getLines()), $oldText->getBaseColor(), $oldText->isGlowing()), $frontFace); if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){ $ev->cancel(); } $ev->call(); if(!$ev->isCancelled()){ - $this->setText($ev->getNewText()); + $this->setFaceText($frontFace, $ev->getNewText()); $this->setEditorEntityRuntimeId(null); $this->position->getWorld()->setBlock($this->position, $this); return true; diff --git a/src/block/Bed.php b/src/block/Bed.php index 8efbdfe01..21bd3a172 100644 --- a/src/block/Bed.php +++ b/src/block/Bed.php @@ -24,8 +24,10 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\Bed as TileBed; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\DyeColor; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -41,7 +43,7 @@ use pocketmine\utils\TextFormat; use pocketmine\world\BlockTransaction; use pocketmine\world\World; -class Bed extends Transparent{ +class Bed extends Transparent implements Colored, HorizontalFacing{ use ColoredTrait; use HorizontalFacingTrait; @@ -76,9 +78,6 @@ class Bed extends Transparent{ } } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 16)]; } diff --git a/src/block/Bell.php b/src/block/Bell.php index 53a6fc7fb..258abc628 100644 --- a/src/block/Bell.php +++ b/src/block/Bell.php @@ -25,6 +25,7 @@ namespace pocketmine\block; use pocketmine\block\tile\Bell as TileBell; use pocketmine\block\utils\BellAttachmentType; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -38,7 +39,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use pocketmine\world\sound\BellRingSound; -final class Bell extends Transparent{ +final class Bell extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; private BellAttachmentType $attachmentType = BellAttachmentType::FLOOR; diff --git a/src/block/Block.php b/src/block/Block.php index 89fe39265..fd644eae4 100644 --- a/src/block/Block.php +++ b/src/block/Block.php @@ -26,6 +26,8 @@ declare(strict_types=1); */ namespace pocketmine\block; +use pmmp\encoding\BE; +use pmmp\encoding\LE; use pocketmine\block\tile\Spawnable; use pocketmine\block\tile\Tile; use pocketmine\block\utils\SupportType; @@ -49,7 +51,6 @@ use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; -use pocketmine\utils\Binary; use pocketmine\world\BlockTransaction; use pocketmine\world\format\Chunk; use pocketmine\world\Position; @@ -75,7 +76,10 @@ class Block{ protected BlockTypeInfo $typeInfo; protected Position $position; - /** @var AxisAlignedBB[]|null */ + /** + * @var AxisAlignedBB[]|null + * @phpstan-var list|null + */ protected ?array $collisionBoxes = null; private int $requiredBlockItemStateDataBits; @@ -95,9 +99,10 @@ class Block{ * of operations required to compute the state ID (micro optimization). */ private static function computeStateIdXorMask(int $typeId) : int{ + //TODO: the mixed byte order here is probably a mistake, but it doesn't break anything for now return $typeId << self::INTERNAL_STATE_DATA_BITS | - (Binary::readLong(hash('xxh3', Binary::writeLLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK); + (BE::unpackSignedLong(hash('xxh3', LE::packSignedLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK); } /** @@ -907,6 +912,7 @@ class Block{ * - anti-cheat checks in plugins * * @return AxisAlignedBB[] + * @phpstan-return list */ final public function getCollisionBoxes() : array{ if($this->collisionBoxes === null){ @@ -931,6 +937,7 @@ class Block{ /** * @return AxisAlignedBB[] + * @phpstan-return list */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()]; diff --git a/src/block/BlockBreakInfo.php b/src/block/BlockBreakInfo.php index b49cb6f13..3d45caf3c 100644 --- a/src/block/BlockBreakInfo.php +++ b/src/block/BlockBreakInfo.php @@ -73,7 +73,7 @@ class BlockBreakInfo{ return new self(0.0, $toolType, $toolHarvestLevel, 0.0); } - public static function indestructible(float $blastResistance = 18000000.0) : self{ + public static function indestructible(float $blastResistance = 18000003.75) : self{ return new self(-1.0, BlockToolType::NONE, 0, $blastResistance); } @@ -95,7 +95,7 @@ class BlockBreakInfo{ * Returns whether this block can be instantly broken. */ public function breaksInstantly() : bool{ - return $this->hardness == 0.0; + return $this->hardness === 0.0; } /** @@ -154,7 +154,7 @@ class BlockBreakInfo{ $efficiency = $item->getMiningEfficiency(($this->toolType & $item->getBlockToolType()) !== 0); if($efficiency <= 0){ - throw new \InvalidArgumentException(get_class($item) . " has invalid mining efficiency: expected >= 0, got $efficiency"); + throw new \InvalidArgumentException(get_class($item) . " must have a positive mining efficiency, but got $efficiency"); } $base /= $efficiency; diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index 3914a4b74..e4d49746f 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -765,8 +765,65 @@ final class BlockTypeIds{ public const COPPER_TRAPDOOR = 10735; public const CHISELED_COPPER = 10736; public const COPPER_GRATE = 10737; + public const PALE_OAK_BUTTON = 10738; + public const PALE_OAK_DOOR = 10739; + public const PALE_OAK_FENCE = 10740; + public const PALE_OAK_FENCE_GATE = 10741; + public const PALE_OAK_LEAVES = 10742; + public const PALE_OAK_LOG = 10743; + public const PALE_OAK_PLANKS = 10744; + public const PALE_OAK_PRESSURE_PLATE = 10745; + public const PALE_OAK_SIGN = 10746; + public const PALE_OAK_SLAB = 10747; + public const PALE_OAK_STAIRS = 10748; + public const PALE_OAK_TRAPDOOR = 10749; + public const PALE_OAK_WALL_SIGN = 10750; + public const PALE_OAK_WOOD = 10751; + public const RESIN = 10752; + public const RESIN_BRICK_SLAB = 10753; + public const RESIN_BRICK_STAIRS = 10754; + public const RESIN_BRICK_WALL = 10755; + public const RESIN_BRICKS = 10756; + public const RESIN_CLUMP = 10757; + public const CHISELED_RESIN_BRICKS = 10758; + public const RESPAWN_ANCHOR = 10759; + public const OMINOUS_BANNER = 10760; + public const OMINOUS_WALL_BANNER = 10761; + public const ACACIA_CEILING_CENTER_HANGING_SIGN = 10762; + public const ACACIA_CEILING_EDGES_HANGING_SIGN = 10763; + public const ACACIA_WALL_HANGING_SIGN = 10764; + public const BIRCH_CEILING_CENTER_HANGING_SIGN = 10765; + public const BIRCH_CEILING_EDGES_HANGING_SIGN = 10766; + public const BIRCH_WALL_HANGING_SIGN = 10767; + public const CHERRY_CEILING_CENTER_HANGING_SIGN = 10768; + public const CHERRY_CEILING_EDGES_HANGING_SIGN = 10769; + public const CHERRY_WALL_HANGING_SIGN = 10770; + public const CRIMSON_CEILING_CENTER_HANGING_SIGN = 10771; + public const CRIMSON_CEILING_EDGES_HANGING_SIGN = 10772; + public const CRIMSON_WALL_HANGING_SIGN = 10773; + public const DARK_OAK_CEILING_CENTER_HANGING_SIGN = 10774; + public const DARK_OAK_CEILING_EDGES_HANGING_SIGN = 10775; + public const DARK_OAK_WALL_HANGING_SIGN = 10776; + public const JUNGLE_CEILING_CENTER_HANGING_SIGN = 10777; + public const JUNGLE_CEILING_EDGES_HANGING_SIGN = 10778; + public const JUNGLE_WALL_HANGING_SIGN = 10779; + public const MANGROVE_CEILING_CENTER_HANGING_SIGN = 10780; + public const MANGROVE_CEILING_EDGES_HANGING_SIGN = 10781; + public const MANGROVE_WALL_HANGING_SIGN = 10782; + public const OAK_CEILING_CENTER_HANGING_SIGN = 10783; + public const OAK_CEILING_EDGES_HANGING_SIGN = 10784; + public const OAK_WALL_HANGING_SIGN = 10785; + public const PALE_OAK_CEILING_CENTER_HANGING_SIGN = 10786; + public const PALE_OAK_CEILING_EDGES_HANGING_SIGN = 10787; + public const PALE_OAK_WALL_HANGING_SIGN = 10788; + public const SPRUCE_CEILING_CENTER_HANGING_SIGN = 10789; + public const SPRUCE_CEILING_EDGES_HANGING_SIGN = 10790; + public const SPRUCE_WALL_HANGING_SIGN = 10791; + public const WARPED_CEILING_CENTER_HANGING_SIGN = 10792; + public const WARPED_CEILING_EDGES_HANGING_SIGN = 10793; + public const WARPED_WALL_HANGING_SIGN = 10794; - public const FIRST_UNUSED_BLOCK_ID = 10738; + public const FIRST_UNUSED_BLOCK_ID = 10795; private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID; diff --git a/src/block/BlockTypeTags.php b/src/block/BlockTypeTags.php index 19a4825d9..531f3bcb3 100644 --- a/src/block/BlockTypeTags.php +++ b/src/block/BlockTypeTags.php @@ -31,4 +31,5 @@ final class BlockTypeTags{ public const SAND = self::PREFIX . "sand"; public const POTTABLE_PLANTS = self::PREFIX . "pottable"; public const FIRE = self::PREFIX . "fire"; + public const HANGING_SIGN = self::PREFIX . "hanging_sign"; } diff --git a/src/block/BoneBlock.php b/src/block/BoneBlock.php index 247bdb748..465d96033 100644 --- a/src/block/BoneBlock.php +++ b/src/block/BoneBlock.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PillarRotation; use pocketmine\block\utils\PillarRotationTrait; -class BoneBlock extends Opaque{ +class BoneBlock extends Opaque implements PillarRotation{ use PillarRotationTrait; } diff --git a/src/block/Button.php b/src/block/Button.php index 73bd1d556..4d1f1bbc5 100644 --- a/src/block/Button.php +++ b/src/block/Button.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -33,7 +34,7 @@ use pocketmine\world\BlockTransaction; use pocketmine\world\sound\RedstonePowerOffSound; use pocketmine\world\sound\RedstonePowerOnSound; -abstract class Button extends Flowable{ +abstract class Button extends Flowable implements AnyFacing{ use AnyFacingTrait; protected bool $pressed = false; diff --git a/src/block/Cactus.php b/src/block/Cactus.php index 6f2b04c8a..51c98786b 100644 --- a/src/block/Cactus.php +++ b/src/block/Cactus.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\StaticSupportTrait; @@ -33,7 +34,7 @@ use pocketmine\event\entity\EntityDamageEvent; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; -class Cactus extends Transparent{ +class Cactus extends Transparent implements Ageable{ use AgeableTrait; use StaticSupportTrait; @@ -43,9 +44,6 @@ class Cactus extends Transparent{ return true; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ $shrinkSize = 1 / 16; return [AxisAlignedBB::one()->contract($shrinkSize, 0, $shrinkSize)->trim(Facing::UP, $shrinkSize)]; diff --git a/src/block/Cake.php b/src/block/Cake.php index 073fc62ac..e8c6dc93e 100644 --- a/src/block/Cake.php +++ b/src/block/Cake.php @@ -40,9 +40,6 @@ class Cake extends BaseCake{ $w->boundedIntAuto(0, self::MAX_BITES, $this->bites); } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [ AxisAlignedBB::one() diff --git a/src/block/CakeWithCandle.php b/src/block/CakeWithCandle.php index 380d080c5..1462776c2 100644 --- a/src/block/CakeWithCandle.php +++ b/src/block/CakeWithCandle.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\CandleTrait; +use pocketmine\block\utils\Lightable; use pocketmine\entity\Living; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; @@ -31,14 +32,11 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; -class CakeWithCandle extends BaseCake{ +class CakeWithCandle extends BaseCake implements Lightable{ use CandleTrait { onInteract as onInteractCandle; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [ AxisAlignedBB::one() diff --git a/src/block/CakeWithDyedCandle.php b/src/block/CakeWithDyedCandle.php index 0dff358e1..04ab0c6eb 100644 --- a/src/block/CakeWithDyedCandle.php +++ b/src/block/CakeWithDyedCandle.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\DyeColor; -class CakeWithDyedCandle extends CakeWithCandle{ +class CakeWithDyedCandle extends CakeWithCandle implements Colored{ use ColoredTrait; public function __construct(BlockIdentifier $idInfo, string $name, BlockTypeInfo $typeInfo){ diff --git a/src/block/Campfire.php b/src/block/Campfire.php index 9f4c42a9c..edfc87ecb 100644 --- a/src/block/Campfire.php +++ b/src/block/Campfire.php @@ -25,7 +25,9 @@ namespace pocketmine\block; use pocketmine\block\inventory\CampfireInventory; use pocketmine\block\tile\Campfire as TileCampfire; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\LightableTrait; use pocketmine\block\utils\SupportType; use pocketmine\crafting\FurnaceRecipe; @@ -59,7 +61,7 @@ use function count; use function min; use function mt_rand; -class Campfire extends Transparent{ +class Campfire extends Transparent implements Lightable, HorizontalFacing{ use HorizontalFacingTrait{ HorizontalFacingTrait::describeBlockOnlyState as encodeFacingState; } diff --git a/src/block/Candle.php b/src/block/Candle.php index 7f22641e1..977f13a09 100644 --- a/src/block/Candle.php +++ b/src/block/Candle.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\CandleTrait; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -35,7 +36,7 @@ use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; use pocketmine\world\BlockTransaction; -class Candle extends Transparent{ +class Candle extends Transparent implements Lightable{ use CandleTrait { describeBlockOnlyState as encodeLitState; getLightLevel as getBaseLightLevel; diff --git a/src/block/Carpet.php b/src/block/Carpet.php index 1ee7240c5..fd8b42a09 100644 --- a/src/block/Carpet.php +++ b/src/block/Carpet.php @@ -23,12 +23,13 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; -class Carpet extends Flowable{ +class Carpet extends Flowable implements Colored{ use ColoredTrait; use StaticSupportTrait; @@ -36,9 +37,6 @@ class Carpet extends Flowable{ return true; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 15 / 16)]; } diff --git a/src/block/CarvedPumpkin.php b/src/block/CarvedPumpkin.php index 98f3c2307..cfdb8d49d 100644 --- a/src/block/CarvedPumpkin.php +++ b/src/block/CarvedPumpkin.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; -class CarvedPumpkin extends Opaque{ +class CarvedPumpkin extends Opaque implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; } diff --git a/src/block/CaveVines.php b/src/block/CaveVines.php index daa973507..84d7d3169 100644 --- a/src/block/CaveVines.php +++ b/src/block/CaveVines.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\StaticSupportTrait; @@ -39,7 +40,7 @@ use pocketmine\world\BlockTransaction; use pocketmine\world\sound\GlowBerriesPickSound; use function mt_rand; -class CaveVines extends Flowable{ +class CaveVines extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/CeilingCenterHangingSign.php b/src/block/CeilingCenterHangingSign.php new file mode 100644 index 000000000..df6b4e229 --- /dev/null +++ b/src/block/CeilingCenterHangingSign.php @@ -0,0 +1,65 @@ +rotation = self::getRotationFromYaw($player->getLocation()->getYaw()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + private function canBeSupportedAt(Block $block) : bool{ + $supportBlock = $block->getSide(Facing::UP); + return + $supportBlock->getSupportType(Facing::DOWN)->hasCenterSupport() || + $supportBlock->hasTypeTag(BlockTypeTags::HANGING_SIGN); + } + + protected function getFacingDegrees() : float{ + return $this->rotation * 22.5; + } +} diff --git a/src/block/CeilingEdgesHangingSign.php b/src/block/CeilingEdgesHangingSign.php new file mode 100644 index 000000000..503915fa0 --- /dev/null +++ b/src/block/CeilingEdgesHangingSign.php @@ -0,0 +1,79 @@ +facing = Facing::opposite($player->getHorizontalFacing()); + } + if(!$this->canBeSupportedAt($blockReplace)){ + return false; + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + if(!$this->canBeSupportedAt($this)){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + private function canBeSupportedAt(Block $block) : bool{ + $supportBlock = $block->getSide(Facing::UP); + return + $supportBlock->getSupportType(Facing::DOWN) === SupportType::FULL || + (($supportBlock instanceof WallHangingSign || $supportBlock instanceof CeilingEdgesHangingSign) && Facing::axis($supportBlock->getFacing()) === Facing::axis($this->facing)); + } + + protected function getFacingDegrees() : float{ + return match($this->facing){ + Facing::SOUTH => 0, + Facing::WEST => 90, + Facing::NORTH => 180, + Facing::EAST => 270, + default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing), + }; + } +} diff --git a/src/block/Chain.php b/src/block/Chain.php index e9cc2c9be..5ec3b1e2a 100644 --- a/src/block/Chain.php +++ b/src/block/Chain.php @@ -23,13 +23,14 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PillarRotation; use pocketmine\block\utils\PillarRotationTrait; use pocketmine\block\utils\SupportType; use pocketmine\math\Axis; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; -final class Chain extends Transparent{ +final class Chain extends Transparent implements PillarRotation{ use PillarRotationTrait; public function getSupportType(int $facing) : SupportType{ diff --git a/src/block/ChemistryTable.php b/src/block/ChemistryTable.php index 058e40288..9b5e78f92 100644 --- a/src/block/ChemistryTable.php +++ b/src/block/ChemistryTable.php @@ -24,11 +24,12 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\player\Player; -final class ChemistryTable extends Opaque{ +final class ChemistryTable extends Opaque implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ diff --git a/src/block/Chest.php b/src/block/Chest.php index dca21576a..c0dc09d9e 100644 --- a/src/block/Chest.php +++ b/src/block/Chest.php @@ -25,6 +25,7 @@ namespace pocketmine\block; use pocketmine\block\tile\Chest as TileChest; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\SupportType; use pocketmine\event\block\ChestPairEvent; use pocketmine\item\Item; @@ -33,12 +34,9 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; -class Chest extends Transparent{ +class Chest extends Transparent implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ //these are slightly bigger than in PC return [AxisAlignedBB::one()->contract(0.025, 0, 0.025)->trim(Facing::UP, 0.05)]; diff --git a/src/block/ChiseledBookshelf.php b/src/block/ChiseledBookshelf.php index 73c4861bf..cbe22a94b 100644 --- a/src/block/ChiseledBookshelf.php +++ b/src/block/ChiseledBookshelf.php @@ -26,6 +26,7 @@ namespace pocketmine\block; use pocketmine\block\tile\ChiseledBookshelf as TileChiseledBookshelf; use pocketmine\block\utils\ChiseledBookshelfSlot; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Book; @@ -38,7 +39,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use function spl_object_id; -class ChiseledBookshelf extends Opaque{ +class ChiseledBookshelf extends Opaque implements HorizontalFacing{ use HorizontalFacingTrait; use FacesOppositePlacingPlayerTrait; @@ -113,6 +114,18 @@ class ChiseledBookshelf extends Opaque{ return $this->slots; } + /** + * @param ChiseledBookshelfSlot[] $slots + * @return $this + */ + public function setSlots(array $slots) : self{ + $this->slots = []; + foreach($slots as $slot){ + $this->setSlot($slot, true); + } + return $this; + } + /** * Returns the last slot interacted by a player or null if no slot has been interacted with yet. */ diff --git a/src/block/ChorusFlower.php b/src/block/ChorusFlower.php index cc3c606d9..ef48d5a89 100644 --- a/src/block/ChorusFlower.php +++ b/src/block/ChorusFlower.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\entity\projectile\Projectile; @@ -40,7 +41,7 @@ use function array_rand; use function min; use function mt_rand; -final class ChorusFlower extends Flowable{ +final class ChorusFlower extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/ChorusPlant.php b/src/block/ChorusPlant.php index 9013f6825..88cf1787c 100644 --- a/src/block/ChorusPlant.php +++ b/src/block/ChorusPlant.php @@ -34,11 +34,16 @@ use function mt_rand; final class ChorusPlant extends Flowable{ use StaticSupportTrait; + /** + * @var true[] + * @phpstan-var array + */ + protected array $connections = []; + protected function recalculateCollisionBoxes() : array{ $bb = AxisAlignedBB::one(); - foreach($this->getAllSides() as $facing => $block){ - $id = $block->getTypeId(); - if($id !== BlockTypeIds::END_STONE && $id !== BlockTypeIds::CHORUS_FLOWER && !$block->hasSameTypeId($this)){ + foreach(Facing::ALL as $facing){ + if(!isset($this->connections[$facing])){ $bb->trim($facing, 2 / 16); } } @@ -46,6 +51,26 @@ final class ChorusPlant extends Flowable{ return [$bb]; } + public function readStateFromWorld() : Block{ + parent::readStateFromWorld(); + + $this->collisionBoxes = null; + + foreach(Facing::ALL as $facing){ + $block = $this->getSide($facing); + if(match($block->getTypeId()){ + BlockTypeIds::END_STONE, BlockTypeIds::CHORUS_FLOWER, $this->getTypeId() => true, + default => false + }){ + $this->connections[$facing] = true; + }else{ + unset($this->connections[$facing]); + } + } + + return $this; + } + private function canBeSupportedBy(Block $block) : bool{ return $block->hasSameTypeId($this) || $block->getTypeId() === BlockTypeIds::END_STONE; } diff --git a/src/block/CocoaBlock.php b/src/block/CocoaBlock.php index 6d8ce1adc..ae09ccb0a 100644 --- a/src/block/CocoaBlock.php +++ b/src/block/CocoaBlock.php @@ -23,8 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\WoodType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -39,7 +41,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use function mt_rand; -class CocoaBlock extends Flowable{ +class CocoaBlock extends Flowable implements Ageable, HorizontalFacing{ use HorizontalFacingTrait; use AgeableTrait; @@ -50,9 +52,6 @@ class CocoaBlock extends Flowable{ $w->boundedIntAuto(0, self::MAX_AGE, $this->age); } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [ AxisAlignedBB::one() diff --git a/src/block/Concrete.php b/src/block/Concrete.php index fae6f8e2f..6cad11193 100644 --- a/src/block/Concrete.php +++ b/src/block/Concrete.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -class Concrete extends Opaque{ +class Concrete extends Opaque implements Colored{ use ColoredTrait; } diff --git a/src/block/ConcretePowder.php b/src/block/ConcretePowder.php index 59f14bc72..89b53053b 100644 --- a/src/block/ConcretePowder.php +++ b/src/block/ConcretePowder.php @@ -24,12 +24,13 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\Fallable; use pocketmine\block\utils\FallableTrait; use pocketmine\math\Facing; -class ConcretePowder extends Opaque implements Fallable{ +class ConcretePowder extends Opaque implements Fallable, Colored{ use ColoredTrait; use FallableTrait { onNearbyBlockChange as protected startFalling; diff --git a/src/block/CopperBulb.php b/src/block/CopperBulb.php index 97fc209fe..d0bdedf3c 100644 --- a/src/block/CopperBulb.php +++ b/src/block/CopperBulb.php @@ -26,11 +26,13 @@ namespace pocketmine\block; use pocketmine\block\utils\CopperMaterial; use pocketmine\block\utils\CopperOxidation; use pocketmine\block\utils\CopperTrait; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\LightableTrait; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\data\runtime\RuntimeDataDescriber; -class CopperBulb extends Opaque implements CopperMaterial{ +class CopperBulb extends Opaque implements CopperMaterial, Lightable, PoweredByRedstone{ use CopperTrait; use PoweredByRedstoneTrait; use LightableTrait{ diff --git a/src/block/CoralBlock.php b/src/block/CoralBlock.php index 3e7ca8224..c21209998 100644 --- a/src/block/CoralBlock.php +++ b/src/block/CoralBlock.php @@ -24,11 +24,12 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\CoralMaterial; use pocketmine\block\utils\CoralTypeTrait; use pocketmine\item\Item; use function mt_rand; -final class CoralBlock extends Opaque{ +final class CoralBlock extends Opaque implements CoralMaterial{ use CoralTypeTrait; public function onNearbyBlockChange() : void{ diff --git a/src/block/Crops.php b/src/block/Crops.php index e90ac6236..b0488d150 100644 --- a/src/block/Crops.php +++ b/src/block/Crops.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\CropGrowthHelper; @@ -34,7 +35,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use function mt_rand; -abstract class Crops extends Flowable{ +abstract class Crops extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/DaylightSensor.php b/src/block/DaylightSensor.php index 4141a2b7e..ed56d2f44 100644 --- a/src/block/DaylightSensor.php +++ b/src/block/DaylightSensor.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnalogRedstoneSignalEmitter; use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -36,7 +37,7 @@ use function max; use function round; use const M_PI; -class DaylightSensor extends Transparent{ +class DaylightSensor extends Transparent implements AnalogRedstoneSignalEmitter{ use AnalogRedstoneSignalEmitterTrait; protected bool $inverted = false; @@ -62,9 +63,6 @@ class DaylightSensor extends Transparent{ return 300; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 10 / 16)]; } diff --git a/src/block/Door.php b/src/block/Door.php index 82ddaab51..2ff53645c 100644 --- a/src/block/Door.php +++ b/src/block/Door.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -34,7 +35,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use pocketmine\world\sound\DoorSound; -class Door extends Transparent{ +class Door extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; protected bool $top = false; @@ -95,9 +96,6 @@ class Door extends Transparent{ return false; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ //TODO: doors are 0.1825 blocks thick, instead of 0.1875 like JE (https://bugs.mojang.com/browse/MCPE-19214) return [AxisAlignedBB::one()->trim($this->open ? Facing::rotateY($this->facing, !$this->hingeRight) : $this->facing, 327 / 400)]; diff --git a/src/block/DoublePitcherCrop.php b/src/block/DoublePitcherCrop.php index 1233ed05d..89d9e98b5 100644 --- a/src/block/DoublePitcherCrop.php +++ b/src/block/DoublePitcherCrop.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\CropGrowthHelper; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -37,7 +38,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class DoublePitcherCrop extends DoublePlant{ +final class DoublePitcherCrop extends DoublePlant implements Ageable{ use AgeableTrait { describeBlockOnlyState as describeAge; } diff --git a/src/block/DyedCandle.php b/src/block/DyedCandle.php index a495e8d00..57a8b5923 100644 --- a/src/block/DyedCandle.php +++ b/src/block/DyedCandle.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -class DyedCandle extends Candle{ +class DyedCandle extends Candle implements Colored{ use ColoredTrait; protected function getCandleIfCompatibleType(Block $block) : ?Candle{ diff --git a/src/block/DyedShulkerBox.php b/src/block/DyedShulkerBox.php index 5eae9237e..8be2718a2 100644 --- a/src/block/DyedShulkerBox.php +++ b/src/block/DyedShulkerBox.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class DyedShulkerBox extends ShulkerBox{ +final class DyedShulkerBox extends ShulkerBox implements Colored{ use ColoredTrait; } diff --git a/src/block/EnchantingTable.php b/src/block/EnchantingTable.php index 6a6c936b2..53573d064 100644 --- a/src/block/EnchantingTable.php +++ b/src/block/EnchantingTable.php @@ -33,9 +33,6 @@ use pocketmine\player\Player; class EnchantingTable extends Transparent{ - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 0.25)]; } diff --git a/src/block/EndPortalFrame.php b/src/block/EndPortalFrame.php index 612cf3723..6c4865fdd 100644 --- a/src/block/EndPortalFrame.php +++ b/src/block/EndPortalFrame.php @@ -24,11 +24,12 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; -class EndPortalFrame extends Opaque{ +class EndPortalFrame extends Opaque implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; protected bool $eye = false; @@ -50,9 +51,6 @@ class EndPortalFrame extends Opaque{ return 1; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 3 / 16)]; } diff --git a/src/block/EndRod.php b/src/block/EndRod.php index f0b28c26d..e3d439f6a 100644 --- a/src/block/EndRod.php +++ b/src/block/EndRod.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\item\Item; use pocketmine\math\Axis; @@ -32,7 +33,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class EndRod extends Flowable{ +class EndRod extends Flowable implements AnyFacing{ use AnyFacingTrait; public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ @@ -52,9 +53,6 @@ class EndRod extends Flowable{ return 14; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ $myAxis = Facing::axis($this->facing); diff --git a/src/block/EnderChest.php b/src/block/EnderChest.php index 9004f7c79..5dec4f3af 100644 --- a/src/block/EnderChest.php +++ b/src/block/EnderChest.php @@ -26,6 +26,7 @@ namespace pocketmine\block; use pocketmine\block\inventory\EnderChestInventory; use pocketmine\block\tile\EnderChest as TileEnderChest; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; @@ -33,16 +34,13 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; -class EnderChest extends Transparent{ +class EnderChest extends Transparent implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; public function getLightLevel() : int{ return 7; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ //these are slightly bigger than in PC return [AxisAlignedBB::one()->contract(0.025, 0, 0.025)->trim(Facing::UP, 0.05)]; diff --git a/src/block/Farmland.php b/src/block/Farmland.php index b7a2500a8..83bc34561 100644 --- a/src/block/Farmland.php +++ b/src/block/Farmland.php @@ -94,9 +94,6 @@ class Farmland extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)]; } diff --git a/src/block/Fence.php b/src/block/Fence.php index 30caaa4cf..52256d9f0 100644 --- a/src/block/Fence.php +++ b/src/block/Fence.php @@ -54,13 +54,9 @@ class Fence extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ $inset = 0.5 - $this->getThickness() / 2; - /** @var AxisAlignedBB[] $bbs */ $bbs = []; $connectWest = isset($this->connections[Facing::WEST]); diff --git a/src/block/FenceGate.php b/src/block/FenceGate.php index 735456449..d8cdfe4e2 100644 --- a/src/block/FenceGate.php +++ b/src/block/FenceGate.php @@ -23,8 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -35,7 +37,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use pocketmine\world\sound\DoorSound; -class FenceGate extends Transparent{ +class FenceGate extends Transparent implements HorizontalFacing, WoodMaterial{ use WoodTypeTrait; use HorizontalFacingTrait; @@ -64,9 +66,6 @@ class FenceGate extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return $this->open ? [] : [AxisAlignedBB::one()->extend(Facing::UP, 0.5)->squash(Facing::axis($this->facing), 6 / 16)]; } diff --git a/src/block/Fire.php b/src/block/Fire.php index 35a7a696c..62809fb9d 100644 --- a/src/block/Fire.php +++ b/src/block/Fire.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\SupportType; @@ -35,7 +36,7 @@ use function max; use function min; use function mt_rand; -class Fire extends BaseFire{ +class Fire extends BaseFire implements Ageable{ use AgeableTrait; public const MAX_AGE = 15; diff --git a/src/block/FloorBanner.php b/src/block/FloorBanner.php index 73bc45787..ff57b5973 100644 --- a/src/block/FloorBanner.php +++ b/src/block/FloorBanner.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\SignLikeRotation; use pocketmine\block\utils\SignLikeRotationTrait; use pocketmine\item\Item; use pocketmine\math\Facing; @@ -30,9 +31,13 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class FloorBanner extends BaseBanner{ +final class FloorBanner extends BaseBanner implements SignLikeRotation{ use SignLikeRotationTrait; + protected function getOminousVersion() : Block{ + return VanillaBlocks::OMINOUS_BANNER()->setRotation($this->rotation); + } + protected function getSupportingFace() : int{ return Facing::DOWN; } diff --git a/src/block/FloorSign.php b/src/block/FloorSign.php index 5615d15d8..08bc480d8 100644 --- a/src/block/FloorSign.php +++ b/src/block/FloorSign.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\SignLikeRotation; use pocketmine\block\utils\SignLikeRotationTrait; use pocketmine\item\Item; use pocketmine\math\Facing; @@ -30,7 +31,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class FloorSign extends BaseSign{ +final class FloorSign extends BaseSign implements SignLikeRotation{ use SignLikeRotationTrait; protected function getSupportingFace() : int{ @@ -47,4 +48,8 @@ final class FloorSign extends BaseSign{ } return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } + + protected function getFacingDegrees() : float{ + return $this->rotation * 22.5; + } } diff --git a/src/block/Flowable.php b/src/block/Flowable.php index 0328bcd74..355c9caea 100644 --- a/src/block/Flowable.php +++ b/src/block/Flowable.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\SupportType; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Vector3; /** @@ -46,9 +45,6 @@ abstract class Flowable extends Transparent{ parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock); } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/FlowerPot.php b/src/block/FlowerPot.php index fb3e78d82..79fb73b12 100644 --- a/src/block/FlowerPot.php +++ b/src/block/FlowerPot.php @@ -83,9 +83,6 @@ class FlowerPot extends Flowable{ return $block->hasTypeTag(BlockTypeTags::POTTABLE_PLANTS); } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->contract(3 / 16, 0, 3 / 16)->trim(Facing::UP, 5 / 8)]; } diff --git a/src/block/FrostedIce.php b/src/block/FrostedIce.php index 3e8592306..046d75811 100644 --- a/src/block/FrostedIce.php +++ b/src/block/FrostedIce.php @@ -23,11 +23,12 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use function mt_rand; -class FrostedIce extends Ice{ +class FrostedIce extends Ice implements Ageable{ use AgeableTrait; public const MAX_AGE = 3; diff --git a/src/block/Furnace.php b/src/block/Furnace.php index 7a64e3cd3..43ab463a6 100644 --- a/src/block/Furnace.php +++ b/src/block/Furnace.php @@ -25,6 +25,8 @@ namespace pocketmine\block; use pocketmine\block\tile\Furnace as TileFurnace; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\LightableTrait; use pocketmine\crafting\FurnaceType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -33,7 +35,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use function mt_rand; -class Furnace extends Opaque{ +class Furnace extends Opaque implements Lightable, HorizontalFacing{ use FacesOppositePlacingPlayerTrait; use LightableTrait; diff --git a/src/block/GlazedTerracotta.php b/src/block/GlazedTerracotta.php index 15b3254e5..ccb928875 100644 --- a/src/block/GlazedTerracotta.php +++ b/src/block/GlazedTerracotta.php @@ -23,10 +23,12 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; -class GlazedTerracotta extends Opaque{ +class GlazedTerracotta extends Opaque implements Colored, HorizontalFacing{ use ColoredTrait; use FacesOppositePlacingPlayerTrait; } diff --git a/src/block/GlowLichen.php b/src/block/GlowLichen.php index d30e25395..4588d647d 100644 --- a/src/block/GlowLichen.php +++ b/src/block/GlowLichen.php @@ -24,11 +24,11 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\MultiAnyFacing; use pocketmine\block\utils\MultiAnySupportTrait; use pocketmine\block\utils\SupportType; use pocketmine\item\Fertilizer; use pocketmine\item\Item; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; @@ -36,7 +36,7 @@ use pocketmine\world\World; use function count; use function shuffle; -class GlowLichen extends Transparent{ +class GlowLichen extends Transparent implements MultiAnyFacing{ use MultiAnySupportTrait; public function getLightLevel() : int{ @@ -47,9 +47,6 @@ class GlowLichen extends Transparent{ return false; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/GrassPath.php b/src/block/GrassPath.php index 5b11bd374..ea56e4b95 100644 --- a/src/block/GrassPath.php +++ b/src/block/GrassPath.php @@ -29,9 +29,6 @@ use pocketmine\math\Facing; class GrassPath extends Transparent{ - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)]; } diff --git a/src/block/HayBale.php b/src/block/HayBale.php index 6fdd2cb63..dacfe92fa 100644 --- a/src/block/HayBale.php +++ b/src/block/HayBale.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PillarRotation; use pocketmine\block\utils\PillarRotationTrait; use pocketmine\entity\Entity; -class HayBale extends Opaque{ +class HayBale extends Opaque implements PillarRotation{ use PillarRotationTrait; public function getFlameEncouragement() : int{ diff --git a/src/block/Hopper.php b/src/block/Hopper.php index 0d823674b..4956b668f 100644 --- a/src/block/Hopper.php +++ b/src/block/Hopper.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\Hopper as TileHopper; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -34,7 +35,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class Hopper extends Transparent{ +class Hopper extends Transparent implements PoweredByRedstone{ use PoweredByRedstoneTrait; private int $facing = Facing::DOWN; diff --git a/src/block/ItemFrame.php b/src/block/ItemFrame.php index c03806a3b..0fda77758 100644 --- a/src/block/ItemFrame.php +++ b/src/block/ItemFrame.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\ItemFrame as TileItemFrame; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -39,7 +40,7 @@ use pocketmine\world\sound\ItemFrameRotateItemSound; use function is_infinite; use function is_nan; -class ItemFrame extends Flowable{ +class ItemFrame extends Flowable implements AnyFacing{ use AnyFacingTrait; public const ROTATIONS = 8; diff --git a/src/block/Ladder.php b/src/block/Ladder.php index 58f133f6e..6edaebbf2 100644 --- a/src/block/Ladder.php +++ b/src/block/Ladder.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\entity\Entity; @@ -35,7 +36,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class Ladder extends Transparent{ +class Ladder extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; public function hasEntityCollision() : bool{ @@ -58,9 +59,6 @@ class Ladder extends Transparent{ return true; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim($this->facing, 13 / 16)]; } diff --git a/src/block/Lantern.php b/src/block/Lantern.php index e9cbcc3fe..302e69fd7 100644 --- a/src/block/Lantern.php +++ b/src/block/Lantern.php @@ -59,9 +59,6 @@ class Lantern extends Transparent{ return $this->lightLevel; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [ AxisAlignedBB::one() diff --git a/src/block/Leaves.php b/src/block/Leaves.php index 7fe9eae74..847536557 100644 --- a/src/block/Leaves.php +++ b/src/block/Leaves.php @@ -157,6 +157,7 @@ class Leaves extends Transparent{ LeavesType::MANGROVE, //TODO: mangrove propagule LeavesType::AZALEA, LeavesType::FLOWERING_AZALEA => null, //TODO: azalea LeavesType::CHERRY => null, //TODO: cherry + LeavesType::PALE_OAK => null, //TODO: pale oak })?->asItem(); if($sapling !== null){ $drops[] = $sapling; diff --git a/src/block/Lectern.php b/src/block/Lectern.php index 03880b3c5..9ba01c1c5 100644 --- a/src/block/Lectern.php +++ b/src/block/Lectern.php @@ -25,6 +25,7 @@ namespace pocketmine\block; use pocketmine\block\tile\Lectern as TileLectern; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -36,7 +37,7 @@ use pocketmine\player\Player; use pocketmine\world\sound\LecternPlaceBookSound; use function count; -class Lectern extends Transparent{ +class Lectern extends Transparent implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; protected int $viewedPage = 0; diff --git a/src/block/LightningRod.php b/src/block/LightningRod.php index a0dd50542..9c1971229 100644 --- a/src/block/LightningRod.php +++ b/src/block/LightningRod.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\item\Item; use pocketmine\math\Axis; @@ -32,7 +33,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class LightningRod extends Transparent{ +final class LightningRod extends Transparent implements AnyFacing{ use AnyFacingTrait; protected function recalculateCollisionBoxes() : array{ diff --git a/src/block/Liquid.php b/src/block/Liquid.php index a37019d65..813d76904 100644 --- a/src/block/Liquid.php +++ b/src/block/Liquid.php @@ -30,7 +30,6 @@ use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\entity\Entity; use pocketmine\event\block\BlockSpreadEvent; use pocketmine\item\Item; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\utils\Utils; @@ -89,9 +88,6 @@ abstract class Liquid extends Transparent{ return false; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/Loom.php b/src/block/Loom.php index d3dd4f3c7..a2b9fc235 100644 --- a/src/block/Loom.php +++ b/src/block/Loom.php @@ -25,11 +25,12 @@ namespace pocketmine\block; use pocketmine\block\inventory\LoomInventory; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\player\Player; -final class Loom extends Opaque{ +final class Loom extends Opaque implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ diff --git a/src/block/MobHead.php b/src/block/MobHead.php index f4e945841..41e816c55 100644 --- a/src/block/MobHead.php +++ b/src/block/MobHead.php @@ -104,9 +104,6 @@ class MobHead extends Flowable{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ $collisionBox = AxisAlignedBB::one() ->contract(0.25, 0, 0.25) diff --git a/src/block/NetherPortal.php b/src/block/NetherPortal.php index 6a45fb7a0..1b199c603 100644 --- a/src/block/NetherPortal.php +++ b/src/block/NetherPortal.php @@ -28,7 +28,6 @@ use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\entity\Entity; use pocketmine\item\Item; use pocketmine\math\Axis; -use pocketmine\math\AxisAlignedBB; class NetherPortal extends Transparent{ @@ -62,9 +61,6 @@ class NetherPortal extends Transparent{ return false; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/NetherVines.php b/src/block/NetherVines.php index e8729c00f..67a0b6f94 100644 --- a/src/block/NetherVines.php +++ b/src/block/NetherVines.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\FortuneDropHelper; use pocketmine\block\utils\StaticSupportTrait; @@ -41,7 +42,7 @@ use function mt_rand; /** * This class is used for Weeping & Twisting vines, because they have same behaviour */ -class NetherVines extends Flowable{ +class NetherVines extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/NetherWartPlant.php b/src/block/NetherWartPlant.php index 34e6fd57e..df0609754 100644 --- a/src/block/NetherWartPlant.php +++ b/src/block/NetherWartPlant.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\FortuneDropHelper; @@ -31,7 +32,7 @@ use pocketmine\item\Item; use pocketmine\math\Facing; use function mt_rand; -class NetherWartPlant extends Flowable{ +class NetherWartPlant extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/OminousFloorBanner.php b/src/block/OminousFloorBanner.php new file mode 100644 index 000000000..240aeccfc --- /dev/null +++ b/src/block/OminousFloorBanner.php @@ -0,0 +1,53 @@ +rotation = self::getRotationFromYaw($player->getLocation()->getYaw()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/block/OminousWallBanner.php b/src/block/OminousWallBanner.php new file mode 100644 index 000000000..1eb5ba753 --- /dev/null +++ b/src/block/OminousWallBanner.php @@ -0,0 +1,49 @@ +facing); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(Facing::axis($face) === Axis::Y){ + return false; + } + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/block/PinkPetals.php b/src/block/PinkPetals.php index 17bc4c50a..b8b6e248c 100644 --- a/src/block/PinkPetals.php +++ b/src/block/PinkPetals.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -34,7 +35,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class PinkPetals extends Flowable{ +class PinkPetals extends Flowable implements HorizontalFacing{ use HorizontalFacingTrait; use StaticSupportTrait { canBePlacedAt as supportedWhenPlacedAt; diff --git a/src/block/PitcherCrop.php b/src/block/PitcherCrop.php index d41aed284..1c771bb8a 100644 --- a/src/block/PitcherCrop.php +++ b/src/block/PitcherCrop.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\CropGrowthHelper; @@ -38,7 +39,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class PitcherCrop extends Flowable{ +final class PitcherCrop extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/Planks.php b/src/block/Planks.php index 1074f8adf..ad7956361 100644 --- a/src/block/Planks.php +++ b/src/block/Planks.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class Planks extends Opaque{ +class Planks extends Opaque implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/PoweredRail.php b/src/block/PoweredRail.php index 25d6dc9e3..321b19a46 100644 --- a/src/block/PoweredRail.php +++ b/src/block/PoweredRail.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\RailPoweredByRedstoneTrait; -class PoweredRail extends StraightOnlyRail{ +class PoweredRail extends StraightOnlyRail implements PoweredByRedstone{ use RailPoweredByRedstoneTrait; } diff --git a/src/block/RedstoneComparator.php b/src/block/RedstoneComparator.php index ee63a77a9..88050b85a 100644 --- a/src/block/RedstoneComparator.php +++ b/src/block/RedstoneComparator.php @@ -24,8 +24,11 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\Comparator; +use pocketmine\block\utils\AnalogRedstoneSignalEmitter; use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\block\utils\SupportType; @@ -38,7 +41,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use function assert; -class RedstoneComparator extends Flowable{ +class RedstoneComparator extends Flowable implements AnalogRedstoneSignalEmitter, PoweredByRedstone, HorizontalFacing{ use HorizontalFacingTrait; use AnalogRedstoneSignalEmitterTrait; use PoweredByRedstoneTrait; @@ -79,9 +82,6 @@ class RedstoneComparator extends Flowable{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 8)]; } diff --git a/src/block/RedstoneLamp.php b/src/block/RedstoneLamp.php index 58098c395..530fa2410 100644 --- a/src/block/RedstoneLamp.php +++ b/src/block/RedstoneLamp.php @@ -23,10 +23,12 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Lightable; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\data\runtime\RuntimeDataDescriber; -class RedstoneLamp extends Opaque{ +class RedstoneLamp extends Opaque implements PoweredByRedstone, Lightable{ use PoweredByRedstoneTrait; protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ @@ -36,4 +38,14 @@ class RedstoneLamp extends Opaque{ public function getLightLevel() : int{ return $this->powered ? 15 : 0; } + + public function isLit() : bool{ + return $this->powered; + } + + /** @return $this */ + public function setLit(bool $lit = true) : self{ + $this->powered = $lit; + return $this; + } } diff --git a/src/block/RedstoneOre.php b/src/block/RedstoneOre.php index 10e701a6f..3477a3519 100644 --- a/src/block/RedstoneOre.php +++ b/src/block/RedstoneOre.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\FortuneDropHelper; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\LightableTrait; use pocketmine\item\Item; use pocketmine\item\VanillaItems; @@ -31,7 +32,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use function mt_rand; -class RedstoneOre extends Opaque{ +class RedstoneOre extends Opaque implements Lightable{ use LightableTrait; public function getLightLevel() : int{ diff --git a/src/block/RedstoneRepeater.php b/src/block/RedstoneRepeater.php index 7e6e73da8..4059ff1cc 100644 --- a/src/block/RedstoneRepeater.php +++ b/src/block/RedstoneRepeater.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; +use pocketmine\block\utils\PoweredByRedstone; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\block\utils\SupportType; @@ -35,7 +37,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class RedstoneRepeater extends Flowable{ +class RedstoneRepeater extends Flowable implements PoweredByRedstone, HorizontalFacing{ use HorizontalFacingTrait; use PoweredByRedstoneTrait; use StaticSupportTrait; @@ -62,9 +64,6 @@ class RedstoneRepeater extends Flowable{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 8)]; } diff --git a/src/block/RedstoneTorch.php b/src/block/RedstoneTorch.php index 26c86038b..f73076ccc 100644 --- a/src/block/RedstoneTorch.php +++ b/src/block/RedstoneTorch.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Lightable; use pocketmine\block\utils\LightableTrait; use pocketmine\data\runtime\RuntimeDataDescriber; -class RedstoneTorch extends Torch{ +class RedstoneTorch extends Torch implements Lightable{ use LightableTrait; public function __construct(BlockIdentifier $idInfo, string $name, BlockTypeInfo $typeInfo){ diff --git a/src/block/RedstoneWire.php b/src/block/RedstoneWire.php index a2d293fca..6be27bcb4 100644 --- a/src/block/RedstoneWire.php +++ b/src/block/RedstoneWire.php @@ -23,13 +23,14 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnalogRedstoneSignalEmitter; use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; use pocketmine\block\utils\StaticSupportTrait; use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\math\Facing; -class RedstoneWire extends Flowable{ +class RedstoneWire extends Flowable implements AnalogRedstoneSignalEmitter{ use AnalogRedstoneSignalEmitterTrait; use StaticSupportTrait; diff --git a/src/block/ResinClump.php b/src/block/ResinClump.php new file mode 100644 index 000000000..a56a386d4 --- /dev/null +++ b/src/block/ResinClump.php @@ -0,0 +1,55 @@ +faces : []; + } + + protected function recalculateCollisionBoxes() : array{ + return []; + } +} diff --git a/src/block/RespawnAnchor.php b/src/block/RespawnAnchor.php new file mode 100644 index 000000000..e19ea8f6d --- /dev/null +++ b/src/block/RespawnAnchor.php @@ -0,0 +1,123 @@ +boundedIntAuto(self::MIN_CHARGES, self::MAX_CHARGES, $this->charges); + } + + public function getCharges() : int{ + return $this->charges; + } + + /** @return $this */ + public function setCharges(int $charges) : self{ + if($charges < self::MIN_CHARGES || $charges > self::MAX_CHARGES){ + throw new \InvalidArgumentException("Charges must be between " . self::MIN_CHARGES . " and " . self::MAX_CHARGES . ", given: $charges"); + } + $this->charges = $charges; + return $this; + } + + public function getLightLevel() : int{ + return $this->charges > 0 ? ($this->charges * 4) - 1 : 0; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if($item->getTypeId() === ItemTypeIds::fromBlockTypeId(BlockTypeIds::GLOWSTONE) && $this->charges < self::MAX_CHARGES){ + $this->position->getWorld()->setBlock($this->position, $this->setCharges($this->charges + 1)); + $this->position->getWorld()->addSound($this->position, new RespawnAnchorChargeSound()); + return true; + } + + if($this->charges > self::MIN_CHARGES){ + if($player === null){ + return false; + } + + $ev = new PlayerRespawnAnchorUseEvent($player, $this, PlayerRespawnAnchorUseEvent::ACTION_EXPLODE); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + switch($ev->getAction()){ + case PlayerRespawnAnchorUseEvent::ACTION_EXPLODE: + $this->explode($player); + return true; + + case PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN: + if($player->getSpawn() !== null && $player->getSpawn()->equals($this->position)){ + return true; + } + + $player->setSpawn($this->position); + $this->position->getWorld()->addSound($this->position, new RespawnAnchorSetSpawnSound()); + $player->sendMessage(KnownTranslationFactory::tile_respawn_anchor_respawnSet()->prefix(TextFormat::GRAY)); + return true; + } + } + return false; + } + + private function explode(?Player $player) : void{ + $ev = new BlockPreExplodeEvent($this, 5, $player); + $ev->setIncendiary(true); + + $ev->call(); + if($ev->isCancelled()){ + return; + } + + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + + $explosion = new Explosion(Position::fromObject($this->position->add(0.5, 0.5, 0.5), $this->position->getWorld()), $ev->getRadius(), $this); + $explosion->setFireChance($ev->getFireChance()); + + if($ev->isBlockBreaking()){ + $explosion->explodeA(); + } + $explosion->explodeB(); + } +} diff --git a/src/block/RuntimeBlockStateRegistry.php b/src/block/RuntimeBlockStateRegistry.php index a8ece7722..a458b3368 100644 --- a/src/block/RuntimeBlockStateRegistry.php +++ b/src/block/RuntimeBlockStateRegistry.php @@ -28,6 +28,7 @@ use pocketmine\block\BlockIdentifier as BID; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\world\light\LightUpdate; +use function count; use function min; /** @@ -40,6 +41,11 @@ use function min; class RuntimeBlockStateRegistry{ use SingletonTrait; + public const COLLISION_CUSTOM = 0; + public const COLLISION_CUBE = 1; + public const COLLISION_NONE = 2; + public const COLLISION_MAY_OVERFLOW = 3; + /** * @var Block[] * @phpstan-var array @@ -74,6 +80,13 @@ class RuntimeBlockStateRegistry{ */ public array $blastResistance = []; + /** + * Map of state ID -> useful AABB info to avoid unnecessary block allocations + * @var int[] + * @phpstan-var array + */ + public array $collisionInfo = []; + public function __construct(){ foreach(VanillaBlocks::getAll() as $block){ $this->register($block); @@ -100,6 +113,70 @@ class RuntimeBlockStateRegistry{ } } + /** + * Checks if the given class method overrides a method in Block. + * Used to determine if a block might need to disable fast path optimizations. + * + * @phpstan-param anyClosure $closure + */ + private static function overridesBlockMethod(\Closure $closure) : bool{ + $declarer = (new \ReflectionFunction($closure))->getClosureScopeClass(); + return $declarer !== null && $declarer->getName() !== Block::class; + } + + /** + * A big ugly hack to set up fast paths for handling collisions on blocks with common shapes. + * The information returned here is stored in RuntimeBlockStateRegistry->collisionInfo, and is used during entity + * collision box calculations to avoid complex logic and unnecessary block object allocations. + * This hack allows significant performance improvements. + * + * TODO: We'll want to redesign block collision box handling and block shapes in the future, but that's a job for a + * major version. For now, this hack nets major performance wins. + */ + private static function calculateCollisionInfo(Block $block) : int{ + if( + self::overridesBlockMethod($block->getModelPositionOffset(...)) || + self::overridesBlockMethod($block->readStateFromWorld(...)) + ){ + //getModelPositionOffset() might cause AABBs to shift outside the cell + //readStateFromWorld() might cause overflow in ways we can't predict just by looking at known states + //TODO: excluding overriders of readStateFromWorld() also excludes blocks with tiles that don't do anything + //weird with their AABBs, but for now this is the best we can do. + return self::COLLISION_MAY_OVERFLOW; + } + + //TODO: this could blow up if any recalculateCollisionBoxes() uses the world + //it shouldn't, but that doesn't mean that custom blocks won't... + $boxes = $block->getCollisionBoxes(); + if(count($boxes) === 0){ + return self::COLLISION_NONE; + } + + if( + count($boxes) === 1 && + $boxes[0]->minX === 0.0 && + $boxes[0]->minY === 0.0 && + $boxes[0]->minZ === 0.0 && + $boxes[0]->maxX === 1.0 && + $boxes[0]->maxY === 1.0 && + $boxes[0]->maxZ === 1.0 + ){ + return self::COLLISION_CUBE; + } + + foreach($boxes as $box){ + if( + $box->minX < 0 || $box->maxX > 1 || + $box->minY < 0 || $box->maxY > 1 || + $box->minZ < 0 || $box->maxZ > 1 + ){ + return self::COLLISION_MAY_OVERFLOW; + } + } + + return self::COLLISION_CUSTOM; + } + private function fillStaticArrays(int $index, Block $block) : void{ $fullId = $block->getStateId(); if($index !== $fullId){ @@ -112,6 +189,8 @@ class RuntimeBlockStateRegistry{ if($block->blocksDirectSkyLight()){ $this->blocksDirectSkyLight[$index] = true; } + + $this->collisionInfo[$index] = self::calculateCollisionInfo($block); } } @@ -130,6 +209,10 @@ class RuntimeBlockStateRegistry{ return $block; } + public function hasStateId(int $stateId) : bool{ + return isset($this->fullList[$stateId]); + } + /** * @return Block[] * @phpstan-return array diff --git a/src/block/SeaPickle.php b/src/block/SeaPickle.php index 627af9bac..34f5c3e9e 100644 --- a/src/block/SeaPickle.php +++ b/src/block/SeaPickle.php @@ -26,7 +26,6 @@ namespace pocketmine\block; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; -use pocketmine\math\AxisAlignedBB; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; @@ -70,9 +69,6 @@ class SeaPickle extends Transparent{ return $this->underwater ? ($this->count + 1) * 3 : 0; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return []; } diff --git a/src/block/ShulkerBox.php b/src/block/ShulkerBox.php index d557401ee..52a3b83cd 100644 --- a/src/block/ShulkerBox.php +++ b/src/block/ShulkerBox.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\tile\ShulkerBox as TileShulkerBox; +use pocketmine\block\utils\AnyFacing; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -32,7 +33,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class ShulkerBox extends Opaque{ +class ShulkerBox extends Opaque implements AnyFacing{ use AnyFacingTrait; protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ diff --git a/src/block/SimplePillar.php b/src/block/SimplePillar.php index 98c89f89c..6a7af96ed 100644 --- a/src/block/SimplePillar.php +++ b/src/block/SimplePillar.php @@ -23,12 +23,13 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PillarRotation; use pocketmine\block\utils\PillarRotationTrait; /** * @internal This class provides a general base for pillar-like blocks. It **should not** be used for contract binding * in APIs, because not all pillar-like blocks extend this class. */ -class SimplePillar extends Opaque{ +class SimplePillar extends Opaque implements PillarRotation{ use PillarRotationTrait; } diff --git a/src/block/Slab.php b/src/block/Slab.php index 6000bec39..2bbb7528c 100644 --- a/src/block/Slab.php +++ b/src/block/Slab.php @@ -93,9 +93,6 @@ class Slab extends Transparent{ return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ if($this->slabType === SlabType::DOUBLE){ return [AxisAlignedBB::one()]; diff --git a/src/block/SmallDripleaf.php b/src/block/SmallDripleaf.php index d192e43db..846be5c93 100644 --- a/src/block/SmallDripleaf.php +++ b/src/block/SmallDripleaf.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -36,7 +37,7 @@ use pocketmine\world\BlockTransaction; use pocketmine\world\Position; use function mt_rand; -class SmallDripleaf extends Transparent{ +class SmallDripleaf extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; protected bool $top = false; diff --git a/src/block/SnowLayer.php b/src/block/SnowLayer.php index cca8424a9..8549f0b31 100644 --- a/src/block/SnowLayer.php +++ b/src/block/SnowLayer.php @@ -65,9 +65,6 @@ class SnowLayer extends Flowable implements Fallable{ return $this->layers < self::MAX_LAYERS; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ //TODO: this zero-height BB is intended to stay in lockstep with a MCPE bug return [AxisAlignedBB::one()->trim(Facing::UP, $this->layers >= 4 ? 0.5 : 1)]; diff --git a/src/block/SoulSand.php b/src/block/SoulSand.php index 2c6453b6c..e1285d095 100644 --- a/src/block/SoulSand.php +++ b/src/block/SoulSand.php @@ -28,9 +28,6 @@ use pocketmine\math\Facing; class SoulSand extends Opaque{ - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 8)]; } diff --git a/src/block/StainedGlass.php b/src/block/StainedGlass.php index bc0d84877..baf4755bb 100644 --- a/src/block/StainedGlass.php +++ b/src/block/StainedGlass.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class StainedGlass extends Glass{ +final class StainedGlass extends Glass implements Colored{ use ColoredTrait; } diff --git a/src/block/StainedGlassPane.php b/src/block/StainedGlassPane.php index 18ecfdee0..897c291c7 100644 --- a/src/block/StainedGlassPane.php +++ b/src/block/StainedGlassPane.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class StainedGlassPane extends GlassPane{ +final class StainedGlassPane extends GlassPane implements Colored{ use ColoredTrait; } diff --git a/src/block/StainedHardenedClay.php b/src/block/StainedHardenedClay.php index 2c2c01ba3..765e97e4f 100644 --- a/src/block/StainedHardenedClay.php +++ b/src/block/StainedHardenedClay.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class StainedHardenedClay extends HardenedClay{ +final class StainedHardenedClay extends HardenedClay implements Colored{ use ColoredTrait; } diff --git a/src/block/StainedHardenedGlass.php b/src/block/StainedHardenedGlass.php index cc609a49a..c3a794e4d 100644 --- a/src/block/StainedHardenedGlass.php +++ b/src/block/StainedHardenedGlass.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class StainedHardenedGlass extends HardenedGlass{ +final class StainedHardenedGlass extends HardenedGlass implements Colored{ use ColoredTrait; } diff --git a/src/block/StainedHardenedGlassPane.php b/src/block/StainedHardenedGlassPane.php index 63dbe1f77..9631ff5a9 100644 --- a/src/block/StainedHardenedGlassPane.php +++ b/src/block/StainedHardenedGlassPane.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -final class StainedHardenedGlassPane extends HardenedGlassPane{ +final class StainedHardenedGlassPane extends HardenedGlassPane implements Colored{ use ColoredTrait; } diff --git a/src/block/Stair.php b/src/block/Stair.php index d66a9ce5c..56101de13 100644 --- a/src/block/Stair.php +++ b/src/block/Stair.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\StairShape; use pocketmine\block\utils\SupportType; @@ -35,7 +36,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class Stair extends Transparent{ +class Stair extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; protected bool $upsideDown = false; diff --git a/src/block/Stem.php b/src/block/Stem.php index 2ac95aa3f..2b6f2150c 100644 --- a/src/block/Stem.php +++ b/src/block/Stem.php @@ -25,11 +25,12 @@ namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\CropGrowthHelper; +use pocketmine\block\utils\FortuneDropHelper; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; +use pocketmine\item\VanillaItems; use pocketmine\math\Facing; use function array_rand; -use function mt_rand; abstract class Stem extends Crops{ protected int $facing = Facing::UP; @@ -90,8 +91,10 @@ abstract class Stem extends Crops{ } public function getDropsForCompatibleTool(Item $item) : array{ + //TODO: bit annoying we have to pass an Item instance here + //this should not be affected by Fortune, but still follows a binomial distribution return [ - $this->asItem()->setCount(mt_rand(0, 2)) + $this->asItem()->setCount(FortuneDropHelper::binomial(VanillaItems::AIR(), 0, chance: ($this->age + 1) / 15)) ]; } } diff --git a/src/block/Stonecutter.php b/src/block/Stonecutter.php index 30c19d25d..0fd259326 100644 --- a/src/block/Stonecutter.php +++ b/src/block/Stonecutter.php @@ -25,6 +25,7 @@ namespace pocketmine\block; use pocketmine\block\inventory\StonecutterInventory; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; @@ -32,7 +33,7 @@ use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; -class Stonecutter extends Transparent{ +class Stonecutter extends Transparent implements HorizontalFacing{ use FacesOppositePlacingPlayerTrait; public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ diff --git a/src/block/Sugarcane.php b/src/block/Sugarcane.php index 2da2dc9b9..0874413c5 100644 --- a/src/block/Sugarcane.php +++ b/src/block/Sugarcane.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\StaticSupportTrait; @@ -34,7 +35,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use pocketmine\world\Position; -class Sugarcane extends Flowable{ +class Sugarcane extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait { onNearbyBlockChange as onSupportBlockChange; diff --git a/src/block/SweetBerryBush.php b/src/block/SweetBerryBush.php index eabdde118..881b7359f 100644 --- a/src/block/SweetBerryBush.php +++ b/src/block/SweetBerryBush.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Ageable; use pocketmine\block\utils\AgeableTrait; use pocketmine\block\utils\BlockEventHelper; use pocketmine\block\utils\FortuneDropHelper; @@ -39,7 +40,7 @@ use pocketmine\player\Player; use pocketmine\world\sound\SweetBerriesPickSound; use function mt_rand; -class SweetBerryBush extends Flowable{ +class SweetBerryBush extends Flowable implements Ageable{ use AgeableTrait; use StaticSupportTrait; diff --git a/src/block/Thin.php b/src/block/Thin.php index dde2d7d84..82010697a 100644 --- a/src/block/Thin.php +++ b/src/block/Thin.php @@ -56,7 +56,6 @@ class Thin extends Transparent{ protected function recalculateCollisionBoxes() : array{ $inset = 7 / 16; - /** @var AxisAlignedBB[] $bbs */ $bbs = []; if(isset($this->connections[Facing::WEST]) || isset($this->connections[Facing::EAST])){ diff --git a/src/block/Trapdoor.php b/src/block/Trapdoor.php index 20b6af2ab..5e8a7dc3f 100644 --- a/src/block/Trapdoor.php +++ b/src/block/Trapdoor.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; @@ -34,7 +35,7 @@ use pocketmine\player\Player; use pocketmine\world\BlockTransaction; use pocketmine\world\sound\DoorSound; -class Trapdoor extends Transparent{ +class Trapdoor extends Transparent implements HorizontalFacing{ use HorizontalFacingTrait; protected bool $open = false; @@ -62,9 +63,6 @@ class Trapdoor extends Transparent{ return $this; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->trim($this->open ? $this->facing : ($this->top ? Facing::DOWN : Facing::UP), 13 / 16)]; } diff --git a/src/block/TripwireHook.php b/src/block/TripwireHook.php index 325819825..4e40cf62e 100644 --- a/src/block/TripwireHook.php +++ b/src/block/TripwireHook.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -32,7 +33,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -class TripwireHook extends Flowable{ +class TripwireHook extends Flowable implements HorizontalFacing{ use HorizontalFacingTrait; protected bool $connected = false; diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 54cf90a0c..36074c606 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -45,6 +45,7 @@ use pocketmine\block\tile\EnchantTable as TileEnchantingTable; use pocketmine\block\tile\EnderChest as TileEnderChest; use pocketmine\block\tile\FlowerPot as TileFlowerPot; use pocketmine\block\tile\GlowingItemFrame as TileGlowingItemFrame; +use pocketmine\block\tile\HangingSign as TileHangingSign; use pocketmine\block\tile\Hopper as TileHopper; use pocketmine\block\tile\ItemFrame as TileItemFrame; use pocketmine\block\tile\Jukebox as TileJukebox; @@ -54,6 +55,7 @@ use pocketmine\block\tile\MonsterSpawner as TileMonsterSpawner; use pocketmine\block\tile\NormalFurnace as TileNormalFurnace; use pocketmine\block\tile\Note as TileNote; use pocketmine\block\tile\ShulkerBox as TileShulkerBox; +use pocketmine\block\tile\Sign as TileSign; use pocketmine\block\tile\Smoker as TileSmoker; use pocketmine\block\tile\Tile; use pocketmine\block\utils\AmethystTrait; @@ -79,6 +81,8 @@ use function strtolower; * @generate-registry-docblock * * @method static WoodenButton ACACIA_BUTTON() + * @method static CeilingCenterHangingSign ACACIA_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign ACACIA_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor ACACIA_DOOR() * @method static WoodenFence ACACIA_FENCE() * @method static FenceGate ACACIA_FENCE_GATE() @@ -91,6 +95,7 @@ use function strtolower; * @method static WoodenSlab ACACIA_SLAB() * @method static WoodenStairs ACACIA_STAIRS() * @method static WoodenTrapdoor ACACIA_TRAPDOOR() + * @method static WallHangingSign ACACIA_WALL_HANGING_SIGN() * @method static WallSign ACACIA_WALL_SIGN() * @method static Wood ACACIA_WOOD() * @method static ActivatorRail ACTIVATOR_RAIL() @@ -121,6 +126,8 @@ use function strtolower; * @method static BigDripleafHead BIG_DRIPLEAF_HEAD() * @method static BigDripleafStem BIG_DRIPLEAF_STEM() * @method static WoodenButton BIRCH_BUTTON() + * @method static CeilingCenterHangingSign BIRCH_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign BIRCH_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor BIRCH_DOOR() * @method static WoodenFence BIRCH_FENCE() * @method static FenceGate BIRCH_FENCE_GATE() @@ -133,6 +140,7 @@ use function strtolower; * @method static WoodenSlab BIRCH_SLAB() * @method static WoodenStairs BIRCH_STAIRS() * @method static WoodenTrapdoor BIRCH_TRAPDOOR() + * @method static WallHangingSign BIRCH_WALL_HANGING_SIGN() * @method static WallSign BIRCH_WALL_SIGN() * @method static Wood BIRCH_WOOD() * @method static Opaque BLACKSTONE() @@ -169,6 +177,8 @@ use function strtolower; * @method static Chain CHAIN() * @method static ChemicalHeat CHEMICAL_HEAT() * @method static WoodenButton CHERRY_BUTTON() + * @method static CeilingCenterHangingSign CHERRY_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign CHERRY_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor CHERRY_DOOR() * @method static WoodenFence CHERRY_FENCE() * @method static FenceGate CHERRY_FENCE_GATE() @@ -180,6 +190,7 @@ use function strtolower; * @method static WoodenSlab CHERRY_SLAB() * @method static WoodenStairs CHERRY_STAIRS() * @method static WoodenTrapdoor CHERRY_TRAPDOOR() + * @method static WallHangingSign CHERRY_WALL_HANGING_SIGN() * @method static WallSign CHERRY_WALL_SIGN() * @method static Wood CHERRY_WOOD() * @method static Chest CHEST() @@ -190,6 +201,7 @@ use function strtolower; * @method static Opaque CHISELED_POLISHED_BLACKSTONE() * @method static SimplePillar CHISELED_QUARTZ() * @method static Opaque CHISELED_RED_SANDSTONE() + * @method static Opaque CHISELED_RESIN_BRICKS() * @method static Opaque CHISELED_SANDSTONE() * @method static Opaque CHISELED_STONE_BRICKS() * @method static Opaque CHISELED_TUFF() @@ -229,6 +241,8 @@ use function strtolower; * @method static Opaque CRACKED_STONE_BRICKS() * @method static CraftingTable CRAFTING_TABLE() * @method static WoodenButton CRIMSON_BUTTON() + * @method static CeilingCenterHangingSign CRIMSON_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign CRIMSON_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor CRIMSON_DOOR() * @method static WoodenFence CRIMSON_FENCE() * @method static FenceGate CRIMSON_FENCE_GATE() @@ -241,6 +255,7 @@ use function strtolower; * @method static WoodenStairs CRIMSON_STAIRS() * @method static Wood CRIMSON_STEM() * @method static WoodenTrapdoor CRIMSON_TRAPDOOR() + * @method static WallHangingSign CRIMSON_WALL_HANGING_SIGN() * @method static WallSign CRIMSON_WALL_SIGN() * @method static Opaque CRYING_OBSIDIAN() * @method static Copper CUT_COPPER() @@ -252,6 +267,8 @@ use function strtolower; * @method static Slab CUT_SANDSTONE_SLAB() * @method static Flower DANDELION() * @method static WoodenButton DARK_OAK_BUTTON() + * @method static CeilingCenterHangingSign DARK_OAK_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign DARK_OAK_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor DARK_OAK_DOOR() * @method static WoodenFence DARK_OAK_FENCE() * @method static FenceGate DARK_OAK_FENCE_GATE() @@ -264,6 +281,7 @@ use function strtolower; * @method static WoodenSlab DARK_OAK_SLAB() * @method static WoodenStairs DARK_OAK_STAIRS() * @method static WoodenTrapdoor DARK_OAK_TRAPDOOR() + * @method static WallHangingSign DARK_OAK_WALL_HANGING_SIGN() * @method static WallSign DARK_OAK_WALL_SIGN() * @method static Wood DARK_OAK_WOOD() * @method static Opaque DARK_PRISMARINE() @@ -486,6 +504,8 @@ use function strtolower; * @method static ItemFrame ITEM_FRAME() * @method static Jukebox JUKEBOX() * @method static WoodenButton JUNGLE_BUTTON() + * @method static CeilingCenterHangingSign JUNGLE_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign JUNGLE_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor JUNGLE_DOOR() * @method static WoodenFence JUNGLE_FENCE() * @method static FenceGate JUNGLE_FENCE_GATE() @@ -498,6 +518,7 @@ use function strtolower; * @method static WoodenSlab JUNGLE_SLAB() * @method static WoodenStairs JUNGLE_STAIRS() * @method static WoodenTrapdoor JUNGLE_TRAPDOOR() + * @method static WallHangingSign JUNGLE_WALL_HANGING_SIGN() * @method static WallSign JUNGLE_WALL_SIGN() * @method static Wood JUNGLE_WOOD() * @method static ChemistryTable LAB_TABLE() @@ -520,6 +541,8 @@ use function strtolower; * @method static Loom LOOM() * @method static Magma MAGMA() * @method static WoodenButton MANGROVE_BUTTON() + * @method static CeilingCenterHangingSign MANGROVE_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign MANGROVE_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor MANGROVE_DOOR() * @method static WoodenFence MANGROVE_FENCE() * @method static FenceGate MANGROVE_FENCE_GATE() @@ -532,6 +555,7 @@ use function strtolower; * @method static WoodenSlab MANGROVE_SLAB() * @method static WoodenStairs MANGROVE_STAIRS() * @method static WoodenTrapdoor MANGROVE_TRAPDOOR() + * @method static WallHangingSign MANGROVE_WALL_HANGING_SIGN() * @method static WallSign MANGROVE_WALL_SIGN() * @method static Wood MANGROVE_WOOD() * @method static ChemistryTable MATERIAL_REDUCER() @@ -570,6 +594,8 @@ use function strtolower; * @method static Opaque NETHER_WART_BLOCK() * @method static Note NOTE_BLOCK() * @method static WoodenButton OAK_BUTTON() + * @method static CeilingCenterHangingSign OAK_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign OAK_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor OAK_DOOR() * @method static WoodenFence OAK_FENCE() * @method static FenceGate OAK_FENCE_GATE() @@ -582,13 +608,33 @@ use function strtolower; * @method static WoodenSlab OAK_SLAB() * @method static WoodenStairs OAK_STAIRS() * @method static WoodenTrapdoor OAK_TRAPDOOR() + * @method static WallHangingSign OAK_WALL_HANGING_SIGN() * @method static WallSign OAK_WALL_SIGN() * @method static Wood OAK_WOOD() * @method static Opaque OBSIDIAN() + * @method static OminousFloorBanner OMINOUS_BANNER() + * @method static OminousWallBanner OMINOUS_WALL_BANNER() * @method static Flower ORANGE_TULIP() * @method static Flower OXEYE_DAISY() * @method static PackedIce PACKED_ICE() * @method static Opaque PACKED_MUD() + * @method static WoodenButton PALE_OAK_BUTTON() + * @method static CeilingCenterHangingSign PALE_OAK_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign PALE_OAK_CEILING_EDGES_HANGING_SIGN() + * @method static WoodenDoor PALE_OAK_DOOR() + * @method static WoodenFence PALE_OAK_FENCE() + * @method static FenceGate PALE_OAK_FENCE_GATE() + * @method static Leaves PALE_OAK_LEAVES() + * @method static Wood PALE_OAK_LOG() + * @method static Planks PALE_OAK_PLANKS() + * @method static WoodenPressurePlate PALE_OAK_PRESSURE_PLATE() + * @method static FloorSign PALE_OAK_SIGN() + * @method static WoodenSlab PALE_OAK_SLAB() + * @method static WoodenStairs PALE_OAK_STAIRS() + * @method static WoodenTrapdoor PALE_OAK_TRAPDOOR() + * @method static WallHangingSign PALE_OAK_WALL_HANGING_SIGN() + * @method static WallSign PALE_OAK_WALL_SIGN() + * @method static Wood PALE_OAK_WOOD() * @method static DoublePlant PEONY() * @method static PinkPetals PINK_PETALS() * @method static Flower PINK_TULIP() @@ -672,6 +718,13 @@ use function strtolower; * @method static Flower RED_TULIP() * @method static Opaque REINFORCED_DEEPSLATE() * @method static Reserved6 RESERVED6() + * @method static Opaque RESIN() + * @method static Opaque RESIN_BRICKS() + * @method static Slab RESIN_BRICK_SLAB() + * @method static Stair RESIN_BRICK_STAIRS() + * @method static Wall RESIN_BRICK_WALL() + * @method static ResinClump RESIN_CLUMP() + * @method static RespawnAnchor RESPAWN_ANCHOR() * @method static DoublePlant ROSE_BUSH() * @method static Sand SAND() * @method static Opaque SANDSTONE() @@ -710,6 +763,8 @@ use function strtolower; * @method static Sponge SPONGE() * @method static SporeBlossom SPORE_BLOSSOM() * @method static WoodenButton SPRUCE_BUTTON() + * @method static CeilingCenterHangingSign SPRUCE_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign SPRUCE_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor SPRUCE_DOOR() * @method static WoodenFence SPRUCE_FENCE() * @method static FenceGate SPRUCE_FENCE_GATE() @@ -722,6 +777,7 @@ use function strtolower; * @method static WoodenSlab SPRUCE_SLAB() * @method static WoodenStairs SPRUCE_STAIRS() * @method static WoodenTrapdoor SPRUCE_TRAPDOOR() + * @method static WallHangingSign SPRUCE_WALL_HANGING_SIGN() * @method static WallSign SPRUCE_WALL_SIGN() * @method static Wood SPRUCE_WOOD() * @method static StainedHardenedClay STAINED_CLAY() @@ -765,6 +821,8 @@ use function strtolower; * @method static WallBanner WALL_BANNER() * @method static WallCoralFan WALL_CORAL_FAN() * @method static WoodenButton WARPED_BUTTON() + * @method static CeilingCenterHangingSign WARPED_CEILING_CENTER_HANGING_SIGN() + * @method static CeilingEdgesHangingSign WARPED_CEILING_EDGES_HANGING_SIGN() * @method static WoodenDoor WARPED_DOOR() * @method static WoodenFence WARPED_FENCE() * @method static FenceGate WARPED_FENCE_GATE() @@ -777,6 +835,7 @@ use function strtolower; * @method static WoodenStairs WARPED_STAIRS() * @method static Wood WARPED_STEM() * @method static WoodenTrapdoor WARPED_TRAPDOOR() + * @method static WallHangingSign WARPED_WALL_HANGING_SIGN() * @method static WallSign WARPED_WALL_SIGN() * @method static Opaque WARPED_WART_BLOCK() * @method static Water WATER() @@ -837,7 +896,7 @@ final class VanillaBlocks{ $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{ + self::register("bamboo", fn(BID $id) => new Bamboo($id, "Bamboo", new Info(new class(1.0, ToolType::AXE) extends BreakInfo{ public function getBreakTime(Item $item) : float{ if($item->getBlockToolType() === ToolType::SWORD){ return 0.0; @@ -845,24 +904,26 @@ final class VanillaBlocks{ return parent::getBreakTime($item); } }, [Tags::POTTABLE_PLANTS]))); - self::register("bamboo_sapling", fn(BID $id) => new BambooSapling($id, "Bamboo Sapling", new Info(BreakInfo::instant()))); + self::register("bamboo_sapling", fn(BID $id) => new BambooSapling($id, "Bamboo Sapling", new Info(new BreakInfo(1.0)))); $bannerBreakInfo = new Info(BreakInfo::axe(1.0)); self::register("banner", fn(BID $id) => new FloorBanner($id, "Banner", $bannerBreakInfo), TileBanner::class); self::register("wall_banner", fn(BID $id) => new WallBanner($id, "Wall Banner", $bannerBreakInfo), TileBanner::class); + self::register("ominous_banner", fn(BID $id) => new OminousFloorBanner($id, "Ominous Banner", $bannerBreakInfo), TileBanner::class); + self::register("ominous_wall_banner", fn(BID $id) => new OminousWallBanner($id, "Ominous 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("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()))); + self::register("bedrock", fn(BID $id) => new Bedrock($id, "Bedrock", new Info(BreakInfo::indestructible(18000000.0)))); self::register("beetroots", fn(BID $id) => new Beetroot($id, "Beetroot Block", new Info(BreakInfo::instant()))); - self::register("bell", fn(BID $id) => new Bell($id, "Bell", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD))), TileBell::class); + self::register("bell", fn(BID $id) => new Bell($id, "Bell", new Info(BreakInfo::pickaxe(5.0))), TileBell::class); self::register("blue_ice", fn(BID $id) => new BlueIce($id, "Blue Ice", new Info(BreakInfo::pickaxe(2.8)))); self::register("bone_block", fn(BID $id) => new BoneBlock($id, "Bone Block", new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD)))); self::register("bookshelf", fn(BID $id) => new Bookshelf($id, "Bookshelf", new Info(BreakInfo::axe(1.5)))); self::register("chiseled_bookshelf", fn(BID $id) => new ChiseledBookshelf($id, "Chiseled Bookshelf", new Info(BreakInfo::axe(1.5))), TileChiseledBookshelf::class); - self::register("brewing_stand", fn(BID $id) => new BrewingStand($id, "Brewing Stand", new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD))), TileBrewingStand::class); + self::register("brewing_stand", fn(BID $id) => new BrewingStand($id, "Brewing Stand", new Info(BreakInfo::pickaxe(0.5))), TileBrewingStand::class); $bricksBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); self::register("brick_stairs", fn(BID $id) => new Stair($id, "Brick Stairs", $bricksBreakInfo)); @@ -891,7 +952,7 @@ final class VanillaBlocks{ self::register("cobweb", fn(BID $id) => new Cobweb($id, "Cobweb", new Info(new BreakInfo(4.0, ToolType::SWORD | ToolType::SHEARS, 1)))); self::register("cocoa_pod", fn(BID $id) => new CocoaBlock($id, "Cocoa Block", new Info(BreakInfo::axe(0.2, null, 15.0)))); - self::register("coral_block", fn(BID $id) => new CoralBlock($id, "Coral Block", new Info(BreakInfo::pickaxe(7.0, ToolTier::WOOD)))); + self::register("coral_block", fn(BID $id) => new CoralBlock($id, "Coral Block", new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)))); self::register("daylight_sensor", fn(BID $id) => new DaylightSensor($id, "Daylight Sensor", new Info(BreakInfo::axe(0.2))), TileDaylightSensor::class); self::register("dead_bush", fn(BID $id) => new DeadBush($id, "Dead Bush", new Info(BreakInfo::instant(ToolType::SHEARS, 1), [Tags::POTTABLE_PLANTS]))); self::register("detector_rail", fn(BID $id) => new DetectorRail($id, "Detector Rail", $railBreakInfo)); @@ -908,19 +969,19 @@ final class VanillaBlocks{ self::register("pitcher_plant", fn(BID $id) => new DoublePlant($id, "Pitcher Plant", new Info(BreakInfo::instant()))); self::register("pitcher_crop", fn(BID $id) => new PitcherCrop($id, "Pitcher Crop", new Info(BreakInfo::instant()))); self::register("double_pitcher_crop", fn(BID $id) => new DoublePitcherCrop($id, "Double Pitcher Crop", new Info(BreakInfo::instant()))); - self::register("dragon_egg", fn(BID $id) => new DragonEgg($id, "Dragon Egg", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD)))); + self::register("dragon_egg", fn(BID $id) => new DragonEgg($id, "Dragon Egg", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, blastResistance: 45.0)))); self::register("dried_kelp", fn(BID $id) => new DriedKelp($id, "Dried Kelp Block", new Info(new BreakInfo(0.5, ToolType::NONE, 0, 12.5)))); self::register("emerald", fn(BID $id) => new Opaque($id, "Emerald Block", new Info(BreakInfo::pickaxe(5.0, ToolTier::IRON, 30.0)))); self::register("enchanting_table", fn(BID $id) => new EnchantingTable($id, "Enchanting Table", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 6000.0))), TileEnchantingTable::class); - self::register("end_portal_frame", fn(BID $id) => new EndPortalFrame($id, "End Portal Frame", new Info(BreakInfo::indestructible()))); + self::register("end_portal_frame", fn(BID $id) => new EndPortalFrame($id, "End Portal Frame", new Info(BreakInfo::indestructible(18000000.0)))); self::register("end_rod", fn(BID $id) => new EndRod($id, "End Rod", new Info(BreakInfo::instant()))); self::register("end_stone", fn(BID $id) => new Opaque($id, "End Stone", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0)))); - $endBrickBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD, 4.0)); + $endBrickBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0)); self::register("end_stone_bricks", fn(BID $id) => new Opaque($id, "End Stone Bricks", $endBrickBreakInfo)); self::register("end_stone_brick_stairs", fn(BID $id) => new Stair($id, "End Stone Brick Stairs", $endBrickBreakInfo)); - self::register("ender_chest", fn(BID $id) => new EnderChest($id, "Ender Chest", new Info(BreakInfo::pickaxe(22.5, ToolTier::WOOD, 3000.0))), TileEnderChest::class); + self::register("ender_chest", fn(BID $id) => new EnderChest($id, "Ender Chest", new Info(BreakInfo::pickaxe(22.5, blastResistance: 3000.0))), TileEnderChest::class); self::register("farmland", fn(BID $id) => new Farmland($id, "Farmland", new Info(BreakInfo::shovel(0.6), [Tags::DIRT]))); self::register("fire", fn(BID $id) => new Fire($id, "Fire Block", new Info(BreakInfo::instant(), [Tags::FIRE]))); @@ -940,7 +1001,7 @@ final class VanillaBlocks{ self::register("torchflower", fn(BID $id) => new Flower($id, "Torchflower", $flowerTypeInfo)); self::register("torchflower_crop", fn(BID $id) => new TorchflowerCrop($id, "Torchflower Crop", new Info(BreakInfo::instant()))); self::register("flower_pot", fn(BID $id) => new FlowerPot($id, "Flower Pot", new Info(BreakInfo::instant())), TileFlowerPot::class); - self::register("frosted_ice", fn(BID $id) => new FrostedIce($id, "Frosted Ice", new Info(BreakInfo::pickaxe(2.5)))); + self::register("frosted_ice", fn(BID $id) => new FrostedIce($id, "Frosted Ice", new Info(BreakInfo::pickaxe(0.5)))); self::register("furnace", fn(BID $id) => new Furnace($id, "Furnace", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::FURNACE), TileNormalFurnace::class); self::register("blast_furnace", fn(BID $id) => new Furnace($id, "Blast Furnace", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::BLAST_FURNACE), TileBlastFurnace::class); self::register("smoker", fn(BID $id) => new Furnace($id, "Smoker", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::SMOKER), TileSmoker::class); @@ -948,52 +1009,50 @@ final class VanillaBlocks{ $glassBreakInfo = new Info(new BreakInfo(0.3)); self::register("glass", fn(BID $id) => new Glass($id, "Glass", $glassBreakInfo)); self::register("glass_pane", fn(BID $id) => new GlassPane($id, "Glass Pane", $glassBreakInfo)); - self::register("glowing_obsidian", fn(BID $id) => new GlowingObsidian($id, "Glowing Obsidian", new Info(BreakInfo::pickaxe(10.0, ToolTier::DIAMOND, 50.0)))); + self::register("glowing_obsidian", fn(BID $id) => new GlowingObsidian($id, "Glowing Obsidian", new Info(BreakInfo::pickaxe(35.0, ToolTier::DIAMOND, 6000.0)))); self::register("glowstone", fn(BID $id) => new Glowstone($id, "Glowstone", new Info(BreakInfo::pickaxe(0.3)))); - self::register("glow_lichen", fn(BID $id) => new GlowLichen($id, "Glow Lichen", new Info(BreakInfo::axe(0.2, null, 0.2)))); + self::register("glow_lichen", fn(BID $id) => new GlowLichen($id, "Glow Lichen", new Info(BreakInfo::axe(0.2)))); self::register("gold", fn(BID $id) => new Opaque($id, "Gold Block", new Info(BreakInfo::pickaxe(3.0, ToolTier::IRON, 30.0)))); - $grassBreakInfo = BreakInfo::shovel(0.6); - self::register("grass", fn(BID $id) => new Grass($id, "Grass", new Info($grassBreakInfo, [Tags::DIRT]))); - self::register("grass_path", fn(BID $id) => new GrassPath($id, "Grass Path", new Info($grassBreakInfo))); + self::register("grass", fn(BID $id) => new Grass($id, "Grass", new Info(BreakInfo::shovel(0.6), [Tags::DIRT]))); + self::register("grass_path", fn(BID $id) => new GrassPath($id, "Grass Path", new Info(BreakInfo::shovel(0.65)))); self::register("gravel", fn(BID $id) => new Gravel($id, "Gravel", new Info(BreakInfo::shovel(0.6)))); - $hardenedClayBreakInfo = new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 21.0)); - self::register("hardened_clay", fn(BID $id) => new HardenedClay($id, "Hardened Clay", $hardenedClayBreakInfo)); + self::register("hardened_clay", fn(BID $id) => new HardenedClay($id, "Hardened Clay", new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 21.0)))); $hardenedGlassBreakInfo = new Info(new BreakInfo(10.0)); self::register("hardened_glass", fn(BID $id) => new HardenedGlass($id, "Hardened Glass", $hardenedGlassBreakInfo)); self::register("hardened_glass_pane", fn(BID $id) => new HardenedGlassPane($id, "Hardened Glass Pane", $hardenedGlassBreakInfo)); self::register("hay_bale", fn(BID $id) => new HayBale($id, "Hay Bale", new Info(new BreakInfo(0.5)))); - self::register("hopper", fn(BID $id) => new Hopper($id, "Hopper", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 15.0))), TileHopper::class); + self::register("hopper", fn(BID $id) => new Hopper($id, "Hopper", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 24.0))), TileHopper::class); self::register("ice", fn(BID $id) => new Ice($id, "Ice", new Info(BreakInfo::pickaxe(0.5)))); $updateBlockBreakInfo = new Info(new BreakInfo(1.0)); self::register("info_update", fn(BID $id) => new Opaque($id, "update!", $updateBlockBreakInfo)); self::register("info_update2", fn(BID $id) => new Opaque($id, "ate!upd", $updateBlockBreakInfo)); - self::register("invisible_bedrock", fn(BID $id) => new Transparent($id, "Invisible Bedrock", new Info(BreakInfo::indestructible()))); + self::register("invisible_bedrock", fn(BID $id) => new Transparent($id, "Invisible Bedrock", new Info(BreakInfo::indestructible(18000000.0)))); $ironBreakInfo = new Info(BreakInfo::pickaxe(5.0, ToolTier::STONE, 30.0)); self::register("iron", fn(BID $id) => new Opaque($id, "Iron Block", $ironBreakInfo)); self::register("iron_bars", fn(BID $id) => new Thin($id, "Iron Bars", $ironBreakInfo)); - $ironDoorBreakInfo = new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 25.0)); - self::register("iron_door", fn(BID $id) => new Door($id, "Iron Door", $ironDoorBreakInfo)); - self::register("iron_trapdoor", fn(BID $id) => new Trapdoor($id, "Iron Trapdoor", $ironDoorBreakInfo)); + + self::register("iron_door", fn(BID $id) => new Door($id, "Iron Door", new Info(BreakInfo::pickaxe(5.0)))); + self::register("iron_trapdoor", fn(BID $id) => new Trapdoor($id, "Iron Trapdoor", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD)))); $itemFrameInfo = new Info(new BreakInfo(0.25)); self::register("item_frame", fn(BID $id) => new ItemFrame($id, "Item Frame", $itemFrameInfo), TileItemFrame::class); self::register("glowing_item_frame", fn(BID $id) => new ItemFrame($id, "Glow Item Frame", $itemFrameInfo), TileGlowingItemFrame::class); - self::register("jukebox", fn(BID $id) => new Jukebox($id, "Jukebox", new Info(BreakInfo::axe(0.8))), TileJukebox::class); //TODO: in PC the hardness is 2.0, not 0.8, unsure if this is a MCPE bug or not + self::register("jukebox", fn(BID $id) => new Jukebox($id, "Jukebox", new Info(BreakInfo::axe(2.0, blastResistance: 30.0))), TileJukebox::class); self::register("ladder", fn(BID $id) => new Ladder($id, "Ladder", new Info(BreakInfo::axe(0.4)))); - $lanternBreakInfo = new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD)); + $lanternBreakInfo = new Info(BreakInfo::pickaxe(3.5)); self::register("lantern", fn(BID $id) => new Lantern($id, "Lantern", $lanternBreakInfo, 15)); self::register("soul_lantern", fn(BID $id) => new Lantern($id, "Soul Lantern", $lanternBreakInfo, 10)); self::register("lapis_lazuli", fn(BID $id) => new Opaque($id, "Lapis Lazuli Block", new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE)))); self::register("lava", fn(BID $id) => new Lava($id, "Lava", new Info(BreakInfo::indestructible(500.0)))); - self::register("lectern", fn(BID $id) => new Lectern($id, "Lectern", new Info(BreakInfo::axe(2.0))), TileLectern::class); + self::register("lectern", fn(BID $id) => new Lectern($id, "Lectern", new Info(BreakInfo::axe(2.5))), TileLectern::class); self::register("lever", fn(BID $id) => new Lever($id, "Lever", new Info(new BreakInfo(0.5)))); self::register("magma", fn(BID $id) => new Magma($id, "Magma Block", new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD)))); self::register("melon", fn(BID $id) => new Melon($id, "Melon Block", new Info(BreakInfo::axe(1.0)))); @@ -1043,14 +1102,15 @@ final class VanillaBlocks{ self::register("purpur_stairs", fn(BID $id) => new Stair($id, "Purpur Stairs", $purpurBreakInfo)); $quartzBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD)); + $smoothQuartzBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); self::register("quartz", fn(BID $id) => new Opaque($id, "Quartz Block", $quartzBreakInfo)); self::register("chiseled_quartz", fn(BID $id) => new SimplePillar($id, "Chiseled Quartz Block", $quartzBreakInfo)); self::register("quartz_pillar", fn(BID $id) => new SimplePillar($id, "Quartz Pillar", $quartzBreakInfo)); - self::register("smooth_quartz", fn(BID $id) => new Opaque($id, "Smooth Quartz Block", $quartzBreakInfo)); + self::register("smooth_quartz", fn(BID $id) => new Opaque($id, "Smooth Quartz Block", $smoothQuartzBreakInfo)); self::register("quartz_bricks", fn(BID $id) => new Opaque($id, "Quartz Bricks", $quartzBreakInfo)); self::register("quartz_stairs", fn(BID $id) => new Stair($id, "Quartz Stairs", $quartzBreakInfo)); - self::register("smooth_quartz_stairs", fn(BID $id) => new Stair($id, "Smooth Quartz Stairs", $quartzBreakInfo)); + self::register("smooth_quartz_stairs", fn(BID $id) => new Stair($id, "Smooth Quartz Stairs", $smoothQuartzBreakInfo)); self::register("rail", fn(BID $id) => new Rail($id, "Rail", $railBreakInfo)); self::register("red_mushroom", fn(BID $id) => new RedMushroom($id, "Red Mushroom", new Info(BreakInfo::instant(), [Tags::POTTABLE_PLANTS]))); @@ -1105,13 +1165,13 @@ final class VanillaBlocks{ $infestedStoneBreakInfo = new Info(BreakInfo::pickaxe(0.75)); self::register("infested_stone", fn(BID $id) => new InfestedStone($id, "Infested Stone", $infestedStoneBreakInfo, $stone)); self::register("infested_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Stone Brick", $infestedStoneBreakInfo, $stoneBrick)); - self::register("infested_cobblestone", fn(BID $id) => new InfestedStone($id, "Infested Cobblestone", $infestedStoneBreakInfo, $cobblestone)); + self::register("infested_cobblestone", fn(BID $id) => new InfestedStone($id, "Infested Cobblestone", new Info(BreakInfo::pickaxe(1.0, blastResistance: 3.75)), $cobblestone)); self::register("infested_mossy_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Mossy Stone Brick", $infestedStoneBreakInfo, $mossyStoneBrick)); self::register("infested_cracked_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Cracked Stone Brick", $infestedStoneBreakInfo, $crackedStoneBrick)); self::register("infested_chiseled_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Chiseled Stone Brick", $infestedStoneBreakInfo, $chiseledStoneBrick)); self::register("stone_stairs", fn(BID $id) => new Stair($id, "Stone Stairs", $stoneBreakInfo)); - self::register("smooth_stone", fn(BID $id) => new Opaque($id, "Smooth Stone", $stoneBreakInfo)); + self::register("smooth_stone", fn(BID $id) => new Opaque($id, "Smooth Stone", new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)))); self::register("andesite_stairs", fn(BID $id) => new Stair($id, "Andesite Stairs", $stoneBreakInfo)); self::register("diorite_stairs", fn(BID $id) => new Stair($id, "Diorite Stairs", $stoneBreakInfo)); self::register("granite_stairs", fn(BID $id) => new Stair($id, "Granite Stairs", $stoneBreakInfo)); @@ -1122,9 +1182,8 @@ final class VanillaBlocks{ self::register("mossy_stone_brick_stairs", fn(BID $id) => new Stair($id, "Mossy Stone Brick Stairs", $stoneBreakInfo)); self::register("stone_button", fn(BID $id) => new StoneButton($id, "Stone Button", new Info(BreakInfo::pickaxe(0.5)))); self::register("stonecutter", fn(BID $id) => new Stonecutter($id, "Stonecutter", new Info(BreakInfo::pickaxe(3.5)))); - self::register("stone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, "Stone Pressure Plate", new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD)))); + self::register("stone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, "Stone Pressure Plate", new Info(BreakInfo::pickaxe(0.5)))); - //TODO: in the future this won't be the same for all the types $stoneSlabBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); self::register("brick_slab", fn(BID $id) => new Slab($id, "Brick", $stoneSlabBreakInfo)); @@ -1135,28 +1194,31 @@ final class VanillaBlocks{ self::register("sandstone_slab", fn(BID $id) => new Slab($id, "Sandstone", $stoneSlabBreakInfo)); self::register("smooth_stone_slab", fn(BID $id) => new Slab($id, "Smooth Stone", $stoneSlabBreakInfo)); self::register("stone_brick_slab", fn(BID $id) => new Slab($id, "Stone Brick", $stoneSlabBreakInfo)); - self::register("dark_prismarine_slab", fn(BID $id) => new Slab($id, "Dark Prismarine", $stoneSlabBreakInfo)); - self::register("mossy_cobblestone_slab", fn(BID $id) => new Slab($id, "Mossy Cobblestone", $stoneSlabBreakInfo)); - self::register("prismarine_slab", fn(BID $id) => new Slab($id, "Prismarine", $stoneSlabBreakInfo)); - self::register("prismarine_bricks_slab", fn(BID $id) => new Slab($id, "Prismarine Bricks", $stoneSlabBreakInfo)); - self::register("purpur_slab", fn(BID $id) => new Slab($id, "Purpur", $stoneSlabBreakInfo)); self::register("red_nether_brick_slab", fn(BID $id) => new Slab($id, "Red Nether Brick", $stoneSlabBreakInfo)); self::register("red_sandstone_slab", fn(BID $id) => new Slab($id, "Red Sandstone", $stoneSlabBreakInfo)); self::register("smooth_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Sandstone", $stoneSlabBreakInfo)); - self::register("andesite_slab", fn(BID $id) => new Slab($id, "Andesite", $stoneSlabBreakInfo)); - self::register("diorite_slab", fn(BID $id) => new Slab($id, "Diorite", $stoneSlabBreakInfo)); - self::register("end_stone_brick_slab", fn(BID $id) => new Slab($id, "End Stone Brick", $stoneSlabBreakInfo)); - self::register("granite_slab", fn(BID $id) => new Slab($id, "Granite", $stoneSlabBreakInfo)); - self::register("polished_andesite_slab", fn(BID $id) => new Slab($id, "Polished Andesite", $stoneSlabBreakInfo)); - self::register("polished_diorite_slab", fn(BID $id) => new Slab($id, "Polished Diorite", $stoneSlabBreakInfo)); - self::register("polished_granite_slab", fn(BID $id) => new Slab($id, "Polished Granite", $stoneSlabBreakInfo)); - self::register("smooth_red_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Red Sandstone", $stoneSlabBreakInfo)); self::register("cut_red_sandstone_slab", fn(BID $id) => new Slab($id, "Cut Red Sandstone", $stoneSlabBreakInfo)); self::register("cut_sandstone_slab", fn(BID $id) => new Slab($id, "Cut Sandstone", $stoneSlabBreakInfo)); - self::register("mossy_stone_brick_slab", fn(BID $id) => new Slab($id, "Mossy Stone Brick", $stoneSlabBreakInfo)); + self::register("mossy_cobblestone_slab", fn(BID $id) => new Slab($id, "Mossy Cobblestone", $stoneSlabBreakInfo)); + self::register("purpur_slab", fn(BID $id) => new Slab($id, "Purpur", $stoneSlabBreakInfo)); + self::register("smooth_red_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Red Sandstone", $stoneSlabBreakInfo)); self::register("smooth_quartz_slab", fn(BID $id) => new Slab($id, "Smooth Quartz", $stoneSlabBreakInfo)); self::register("stone_slab", fn(BID $id) => new Slab($id, "Stone", $stoneSlabBreakInfo)); + self::register("end_stone_brick_slab", fn(BID $id) => new Slab($id, "End Stone Brick", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 30.0)))); + + $lightStoneSlabBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); + self::register("dark_prismarine_slab", fn(BID $id) => new Slab($id, "Dark Prismarine", $lightStoneSlabBreakInfo)); + self::register("prismarine_slab", fn(BID $id) => new Slab($id, "Prismarine", $lightStoneSlabBreakInfo)); + self::register("prismarine_bricks_slab", fn(BID $id) => new Slab($id, "Prismarine Bricks", $lightStoneSlabBreakInfo)); + self::register("andesite_slab", fn(BID $id) => new Slab($id, "Andesite", $lightStoneSlabBreakInfo)); + self::register("diorite_slab", fn(BID $id) => new Slab($id, "Diorite", $lightStoneSlabBreakInfo)); + self::register("granite_slab", fn(BID $id) => new Slab($id, "Granite", $lightStoneSlabBreakInfo)); + self::register("polished_andesite_slab", fn(BID $id) => new Slab($id, "Polished Andesite", $lightStoneSlabBreakInfo)); + self::register("polished_diorite_slab", fn(BID $id) => new Slab($id, "Polished Diorite", $lightStoneSlabBreakInfo)); + self::register("polished_granite_slab", fn(BID $id) => new Slab($id, "Polished Granite", $lightStoneSlabBreakInfo)); + self::register("mossy_stone_brick_slab", fn(BID $id) => new Slab($id, "Mossy Stone Brick", $lightStoneSlabBreakInfo)); + self::register("legacy_stonecutter", fn(BID $id) => new Opaque($id, "Legacy Stonecutter", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)))); self::register("sugarcane", fn(BID $id) => new Sugarcane($id, "Sugarcane", new Info(BreakInfo::instant()))); self::register("sweet_berry_bush", fn(BID $id) => new SweetBerryBush($id, "Sweet Berry Bush", new Info(BreakInfo::instant()))); @@ -1178,7 +1240,7 @@ final class VanillaBlocks{ self::register("water", fn(BID $id) => new Water($id, "Water", new Info(BreakInfo::indestructible(500.0)))); self::register("lily_pad", fn(BID $id) => new WaterLily($id, "Lily Pad", new Info(BreakInfo::instant()))); - $weightedPressurePlateBreakInfo = new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD)); + $weightedPressurePlateBreakInfo = new Info(BreakInfo::pickaxe(0.5)); self::register("weighted_pressure_plate_heavy", fn(BID $id) => new WeightedPressurePlateHeavy( $id, "Weighted Pressure Plate Heavy", @@ -1215,25 +1277,26 @@ final class VanillaBlocks{ } $sandstoneBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD)); + $smoothSandstoneBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); self::register("red_sandstone_stairs", fn(BID $id) => new Stair($id, "Red Sandstone Stairs", $sandstoneBreakInfo)); - self::register("smooth_red_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Red Sandstone Stairs", $sandstoneBreakInfo)); + self::register("smooth_red_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Red Sandstone Stairs", $smoothSandstoneBreakInfo)); self::register("red_sandstone", fn(BID $id) => new Opaque($id, "Red Sandstone", $sandstoneBreakInfo)); self::register("chiseled_red_sandstone", fn(BID $id) => new Opaque($id, "Chiseled Red Sandstone", $sandstoneBreakInfo)); self::register("cut_red_sandstone", fn(BID $id) => new Opaque($id, "Cut Red Sandstone", $sandstoneBreakInfo)); - self::register("smooth_red_sandstone", fn(BID $id) => new Opaque($id, "Smooth Red Sandstone", $sandstoneBreakInfo)); + self::register("smooth_red_sandstone", fn(BID $id) => new Opaque($id, "Smooth Red Sandstone", $smoothSandstoneBreakInfo)); self::register("sandstone_stairs", fn(BID $id) => new Stair($id, "Sandstone Stairs", $sandstoneBreakInfo)); - self::register("smooth_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Sandstone Stairs", $sandstoneBreakInfo)); + self::register("smooth_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Sandstone Stairs", $smoothSandstoneBreakInfo)); self::register("sandstone", fn(BID $id) => new Opaque($id, "Sandstone", $sandstoneBreakInfo)); self::register("chiseled_sandstone", fn(BID $id) => new Opaque($id, "Chiseled Sandstone", $sandstoneBreakInfo)); self::register("cut_sandstone", fn(BID $id) => new Opaque($id, "Cut Sandstone", $sandstoneBreakInfo)); - self::register("smooth_sandstone", fn(BID $id) => new Opaque($id, "Smooth Sandstone", $sandstoneBreakInfo)); + self::register("smooth_sandstone", fn(BID $id) => new Opaque($id, "Smooth Sandstone", $smoothSandstoneBreakInfo)); self::register("glazed_terracotta", fn(BID $id) => new GlazedTerracotta($id, "Glazed Terracotta", new Info(BreakInfo::pickaxe(1.4, ToolTier::WOOD)))); self::register("dyed_shulker_box", fn(BID $id) => new DyedShulkerBox($id, "Dyed Shulker Box", $shulkerBoxBreakInfo), TileShulkerBox::class); self::register("stained_glass", fn(BID $id) => new StainedGlass($id, "Stained Glass", $glassBreakInfo)); self::register("stained_glass_pane", fn(BID $id) => new StainedGlassPane($id, "Stained Glass Pane", $glassBreakInfo)); - self::register("stained_clay", fn(BID $id) => new StainedHardenedClay($id, "Stained Clay", $hardenedClayBreakInfo)); + self::register("stained_clay", fn(BID $id) => new StainedHardenedClay($id, "Stained Clay", new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 6.25)))); self::register("stained_hardened_glass", fn(BID $id) => new StainedHardenedGlass($id, "Stained Hardened Glass", $hardenedGlassBreakInfo)); self::register("stained_hardened_glass_pane", fn(BID $id) => new StainedHardenedGlassPane($id, "Stained Hardened Glass Pane", $hardenedGlassBreakInfo)); self::register("carpet", fn(BID $id) => new Carpet($id, "Carpet", new Info(new BreakInfo(0.1)))); @@ -1250,22 +1313,26 @@ final class VanillaBlocks{ } }))); - //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)); - self::register("cobblestone_wall", fn(BID $id) => new Wall($id, "Cobblestone Wall", $wallBreakInfo)); - self::register("andesite_wall", fn(BID $id) => new Wall($id, "Andesite Wall", $wallBreakInfo)); - self::register("brick_wall", fn(BID $id) => new Wall($id, "Brick Wall", $wallBreakInfo)); - self::register("diorite_wall", fn(BID $id) => new Wall($id, "Diorite Wall", $wallBreakInfo)); - self::register("end_stone_brick_wall", fn(BID $id) => new Wall($id, "End Stone Brick Wall", $wallBreakInfo)); - self::register("granite_wall", fn(BID $id) => new Wall($id, "Granite Wall", $wallBreakInfo)); - self::register("mossy_stone_brick_wall", fn(BID $id) => new Wall($id, "Mossy Stone Brick Wall", $wallBreakInfo)); - self::register("mossy_cobblestone_wall", fn(BID $id) => new Wall($id, "Mossy Cobblestone Wall", $wallBreakInfo)); - self::register("nether_brick_wall", fn(BID $id) => new Wall($id, "Nether Brick Wall", $wallBreakInfo)); - self::register("prismarine_wall", fn(BID $id) => new Wall($id, "Prismarine Wall", $wallBreakInfo)); - self::register("red_nether_brick_wall", fn(BID $id) => new Wall($id, "Red Nether Brick Wall", $wallBreakInfo)); - self::register("red_sandstone_wall", fn(BID $id) => new Wall($id, "Red Sandstone Wall", $wallBreakInfo)); - self::register("sandstone_wall", fn(BID $id) => new Wall($id, "Sandstone Wall", $wallBreakInfo)); - self::register("stone_brick_wall", fn(BID $id) => new Wall($id, "Stone Brick Wall", $wallBreakInfo)); + self::register("end_stone_brick_wall", fn(BID $id) => new Wall($id, "End Stone Brick Wall", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0)))); + + $brickWallBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); + self::register("cobblestone_wall", fn(BID $id) => new Wall($id, "Cobblestone Wall", $brickWallBreakInfo)); + self::register("brick_wall", fn(BID $id) => new Wall($id, "Brick Wall", $brickWallBreakInfo)); + self::register("mossy_cobblestone_wall", fn(BID $id) => new Wall($id, "Mossy Cobblestone Wall", $brickWallBreakInfo)); + self::register("nether_brick_wall", fn(BID $id) => new Wall($id, "Nether Brick Wall", $brickWallBreakInfo)); + self::register("red_nether_brick_wall", fn(BID $id) => new Wall($id, "Red Nether Brick Wall", $brickWallBreakInfo)); + + $stoneWallBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); + self::register("stone_brick_wall", fn(BID $id) => new Wall($id, "Stone Brick Wall", $stoneWallBreakInfo)); + self::register("mossy_stone_brick_wall", fn(BID $id) => new Wall($id, "Mossy Stone Brick Wall", $stoneWallBreakInfo)); + self::register("granite_wall", fn(BID $id) => new Wall($id, "Granite Wall", $stoneWallBreakInfo)); + self::register("diorite_wall", fn(BID $id) => new Wall($id, "Diorite Wall", $stoneWallBreakInfo)); + self::register("andesite_wall", fn(BID $id) => new Wall($id, "Andesite Wall", $stoneWallBreakInfo)); + self::register("prismarine_wall", fn(BID $id) => new Wall($id, "Prismarine Wall", $stoneWallBreakInfo)); + + $sandstoneWallBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD, 4.0)); + self::register("red_sandstone_wall", fn(BID $id) => new Wall($id, "Red Sandstone Wall", $sandstoneWallBreakInfo)); + self::register("sandstone_wall", fn(BID $id) => new Wall($id, "Sandstone Wall", $sandstoneWallBreakInfo)); self::registerElements(); @@ -1298,8 +1365,8 @@ final class VanillaBlocks{ self::register("mangrove_roots", fn(BID $id) => new MangroveRoots($id, "Mangrove Roots", new Info(BreakInfo::axe(0.7)))); 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{ + self::register("sculk", fn(BID $id) => new Sculk($id, "Sculk", new Info(new BreakInfo(0.2, ToolType::HOE)))); + self::register("reinforced_deepslate", fn(BID $id) => new class($id, "Reinforced Deepslate", new Info(new BreakInfo(55.0, ToolType::NONE, 0, 6000.0))) extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return []; } @@ -1311,6 +1378,7 @@ final class VanillaBlocks{ self::registerBlocksR17(); self::registerBlocksR18(); self::registerMudBlocks(); + self::registerResinBlocks(); self::registerTuffBlocks(); self::registerCraftingTables(); @@ -1323,6 +1391,7 @@ final class VanillaBlocks{ private static function registerWoodenBlocks() : void{ $planksBreakInfo = new Info(BreakInfo::axe(2.0, null, 15.0)); $signBreakInfo = new Info(BreakInfo::axe(1.0)); + $hangingSignBreakInfo = new Info(BreakInfo::axe(1.0), [Tags::HANGING_SIGN]); $logBreakInfo = new Info(BreakInfo::axe(2.0)); $woodenDoorBreakInfo = new Info(BreakInfo::axe(3.0, null, 15.0)); $woodenButtonBreakInfo = new Info(BreakInfo::axe(0.5)); @@ -1358,9 +1427,27 @@ final class VanillaBlocks{ WoodType::CRIMSON => VanillaItems::CRIMSON_SIGN(...), WoodType::WARPED => VanillaItems::WARPED_SIGN(...), WoodType::CHERRY => VanillaItems::CHERRY_SIGN(...), + WoodType::PALE_OAK => VanillaItems::PALE_OAK_SIGN(...), }; - self::register($idName("sign"), fn(BID $id) => new FloorSign($id, $name . " Sign", $signBreakInfo, $woodType, $signAsItem)); - self::register($idName("wall_sign"), fn(BID $id) => new WallSign($id, $name . " Wall Sign", $signBreakInfo, $woodType, $signAsItem)); + self::register($idName("sign"), fn(BID $id) => new FloorSign($id, $name . " Sign", $signBreakInfo, $woodType, $signAsItem), TileSign::class); + self::register($idName("wall_sign"), fn(BID $id) => new WallSign($id, $name . " Wall Sign", $signBreakInfo, $woodType, $signAsItem), TileSign::class); + + $hangingSignAsItem = match($woodType){ + WoodType::OAK => VanillaItems::OAK_HANGING_SIGN(...), + WoodType::SPRUCE => VanillaItems::SPRUCE_HANGING_SIGN(...), + WoodType::BIRCH => VanillaItems::BIRCH_HANGING_SIGN(...), + WoodType::JUNGLE => VanillaItems::JUNGLE_HANGING_SIGN(...), + WoodType::ACACIA => VanillaItems::ACACIA_HANGING_SIGN(...), + WoodType::DARK_OAK => VanillaItems::DARK_OAK_HANGING_SIGN(...), + WoodType::MANGROVE => VanillaItems::MANGROVE_HANGING_SIGN(...), + WoodType::CRIMSON => VanillaItems::CRIMSON_HANGING_SIGN(...), + WoodType::WARPED => VanillaItems::WARPED_HANGING_SIGN(...), + WoodType::CHERRY => VanillaItems::CHERRY_HANGING_SIGN(...), + WoodType::PALE_OAK => VanillaItems::PALE_OAK_HANGING_SIGN(...), + }; + self::register($idName("ceiling_center_hanging_sign"), fn(BID $id) => new CeilingCenterHangingSign($id, $name . " Center Hanging Sign", $hangingSignBreakInfo, $woodType, $hangingSignAsItem), TileHangingSign::class); + self::register($idName("ceiling_edges_hanging_sign"), fn(BID $id) => new CeilingEdgesHangingSign($id, $name . " Edges Hanging Sign", $hangingSignBreakInfo, $woodType, $hangingSignAsItem), TileHangingSign::class); + self::register($idName("wall_hanging_sign"), fn(BID $id) => new WallHangingSign($id, $name . " Wall Hanging Sign", $hangingSignBreakInfo, $woodType, $hangingSignAsItem), TileHangingSign::class); } } @@ -1513,7 +1600,7 @@ final class VanillaBlocks{ self::register("lapis_lazuli_ore", fn(BID $id) => new LapisOre($id, "Lapis Lazuli Ore", $stoneOreBreakInfo(ToolTier::STONE))); self::register("redstone_ore", fn(BID $id) => new RedstoneOre($id, "Redstone Ore", $stoneOreBreakInfo(ToolTier::IRON))); - $deepslateOreBreakInfo = fn(ToolTier $toolTier) => new Info(BreakInfo::pickaxe(4.5, $toolTier)); + $deepslateOreBreakInfo = fn(ToolTier $toolTier) => new Info(BreakInfo::pickaxe(4.5, $toolTier, 15.0)); self::register("deepslate_coal_ore", fn(BID $id) => new CoalOre($id, "Deepslate Coal Ore", $deepslateOreBreakInfo(ToolTier::WOOD))); self::register("deepslate_copper_ore", fn(BID $id) => new CopperOre($id, "Deepslate Copper Ore", $deepslateOreBreakInfo(ToolTier::STONE))); self::register("deepslate_diamond_ore", fn(BID $id) => new DiamondOre($id, "Deepslate Diamond Ore", $deepslateOreBreakInfo(ToolTier::IRON))); @@ -1557,10 +1644,10 @@ final class VanillaBlocks{ //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{ + self::register("ancient_debris", fn(BID $id) => new class($id, "Ancient Debris", new Info(BreakInfo::pickaxe(30, ToolTier::DIAMOND, 6000.0))) extends Opaque{ public function isFireProofAsItem() : bool{ return true; } }); - $netheriteBreakInfo = new Info(BreakInfo::pickaxe(50, ToolTier::DIAMOND, 3600.0)); + $netheriteBreakInfo = new Info(BreakInfo::pickaxe(50, ToolTier::DIAMOND, 6000.0)); self::register("netherite", fn(BID $id) => new class($id, "Netherite Block", $netheriteBreakInfo) extends Opaque{ public function isFireProofAsItem() : bool{ return true; } }); @@ -1578,14 +1665,14 @@ final class VanillaBlocks{ self::register("gilded_blackstone", fn(BID $id) => new GildedBlackstone($id, "Gilded Blackstone", $blackstoneBreakInfo)); - //TODO: polished blackstone ought to have 2.0 hardness (as per java) but it's 1.5 in Bedrock (probably parity bug) + $polishedBlackstoneBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); $prefix = fn(string $thing) => "Polished Blackstone" . ($thing !== "" ? " $thing" : ""); - self::register("polished_blackstone", fn(BID $id) => new Opaque($id, $prefix(""), $blackstoneBreakInfo)); + self::register("polished_blackstone", fn(BID $id) => new Opaque($id, $prefix(""), $polishedBlackstoneBreakInfo)); self::register("polished_blackstone_button", fn(BID $id) => new StoneButton($id, $prefix("Button"), new Info(BreakInfo::pickaxe(0.5)))); - self::register("polished_blackstone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, $prefix("Pressure Plate"), new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD)), 20)); + self::register("polished_blackstone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, $prefix("Pressure Plate"), new Info(BreakInfo::pickaxe(0.5)), 20)); self::register("polished_blackstone_slab", fn(BID $id) => new Slab($id, $prefix(""), $slabBreakInfo)); - self::register("polished_blackstone_stairs", fn(BID $id) => new Stair($id, $prefix("Stairs"), $blackstoneBreakInfo)); - self::register("polished_blackstone_wall", fn(BID $id) => new Wall($id, $prefix("Wall"), $blackstoneBreakInfo)); + self::register("polished_blackstone_stairs", fn(BID $id) => new Stair($id, $prefix("Stairs"), $polishedBlackstoneBreakInfo)); + self::register("polished_blackstone_wall", fn(BID $id) => new Wall($id, $prefix("Wall"), $polishedBlackstoneBreakInfo)); self::register("chiseled_polished_blackstone", fn(BID $id) => new Opaque($id, "Chiseled Polished Blackstone", $blackstoneBreakInfo)); $prefix = fn(string $thing) => "Polished Blackstone Brick" . ($thing !== "" ? " $thing" : ""); @@ -1598,8 +1685,7 @@ final class VanillaBlocks{ self::register("soul_torch", fn(BID $id) => new Torch($id, "Soul Torch", new Info(BreakInfo::instant()))); self::register("soul_fire", fn(BID $id) => new SoulFire($id, "Soul Fire", new Info(BreakInfo::instant(), [Tags::FIRE]))); - //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("soul_soil", fn(BID $id) => new Opaque($id, "Soul Soil", new Info(BreakInfo::shovel(0.5)))); 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; } @@ -1617,7 +1703,9 @@ final class VanillaBlocks{ self::register("crimson_roots", fn(BID $id) => new NetherRoots($id, "Crimson Roots", $netherRootsInfo)); self::register("warped_roots", fn(BID $id) => new NetherRoots($id, "Warped Roots", $netherRootsInfo)); - self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD)))); + self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 30.0)))); + + self::register("respawn_anchor", fn(BID $id) => new RespawnAnchor($id, "Respawn Anchor", new Info(BreakInfo::pickaxe(50.0, ToolTier::DIAMOND, 6000.0)))); } private static function registerBlocksR17() : void{ @@ -1635,7 +1723,7 @@ final class VanillaBlocks{ self::register("raw_gold", fn(BID $id) => new Opaque($id, "Raw Gold Block", new Info(BreakInfo::pickaxe(5, ToolTier::IRON, 30.0)))); 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)); + $deepslateBreakInfo = new Info(BreakInfo::pickaxe(3, ToolTier::WOOD, 30.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()]; @@ -1647,29 +1735,29 @@ final class VanillaBlocks{ }); //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)))); + self::register("chiseled_deepslate", fn(BID $id) => new Opaque($id, "Chiseled Deepslate", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0)))); - $deepslateBrickBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0)); + $deepslateBrickBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0)); self::register("deepslate_bricks", fn(BID $id) => new Opaque($id, "Deepslate Bricks", $deepslateBrickBreakInfo)); self::register("deepslate_brick_slab", fn(BID $id) => new Slab($id, "Deepslate Brick", $deepslateBrickBreakInfo)); self::register("deepslate_brick_stairs", fn(BID $id) => new Stair($id, "Deepslate Brick Stairs", $deepslateBrickBreakInfo)); self::register("deepslate_brick_wall", fn(BID $id) => new Wall($id, "Deepslate Brick Wall", $deepslateBrickBreakInfo)); self::register("cracked_deepslate_bricks", fn(BID $id) => new Opaque($id, "Cracked Deepslate Bricks", $deepslateBrickBreakInfo)); - $deepslateTilesBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0)); + $deepslateTilesBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0)); self::register("deepslate_tiles", fn(BID $id) => new Opaque($id, "Deepslate Tiles", $deepslateTilesBreakInfo)); self::register("deepslate_tile_slab", fn(BID $id) => new Slab($id, "Deepslate Tile", $deepslateTilesBreakInfo)); self::register("deepslate_tile_stairs", fn(BID $id) => new Stair($id, "Deepslate Tile Stairs", $deepslateTilesBreakInfo)); self::register("deepslate_tile_wall", fn(BID $id) => new Wall($id, "Deepslate Tile Wall", $deepslateTilesBreakInfo)); self::register("cracked_deepslate_tiles", fn(BID $id) => new Opaque($id, "Cracked Deepslate Tiles", $deepslateTilesBreakInfo)); - $cobbledDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0)); + $cobbledDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0)); self::register("cobbled_deepslate", fn(BID $id) => new Opaque($id, "Cobbled Deepslate", $cobbledDeepslateBreakInfo)); self::register("cobbled_deepslate_slab", fn(BID $id) => new Slab($id, "Cobbled Deepslate", $cobbledDeepslateBreakInfo)); self::register("cobbled_deepslate_stairs", fn(BID $id) => new Stair($id, "Cobbled Deepslate Stairs", $cobbledDeepslateBreakInfo)); self::register("cobbled_deepslate_wall", fn(BID $id) => new Wall($id, "Cobbled Deepslate Wall", $cobbledDeepslateBreakInfo)); - $polishedDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0)); + $polishedDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0)); self::register("polished_deepslate", fn(BID $id) => new Opaque($id, "Polished Deepslate", $polishedDeepslateBreakInfo)); self::register("polished_deepslate_slab", fn(BID $id) => new Slab($id, "Polished Deepslate", $polishedDeepslateBreakInfo)); self::register("polished_deepslate_stairs", fn(BID $id) => new Stair($id, "Polished Deepslate Stairs", $polishedDeepslateBreakInfo)); @@ -1678,7 +1766,7 @@ final class VanillaBlocks{ self::register("tinted_glass", fn(BID $id) => new TintedGlass($id, "Tinted Glass", new Info(new BreakInfo(0.3)))); //blast resistance should be 30 if we were matched with java :( - $copperBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 18.0)); + $copperBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 30.0)); self::register("lightning_rod", fn(BID $id) => new LightningRod($id, "Lightning Rod", $copperBreakInfo)); self::register("copper", fn(BID $id) => new Copper($id, "Copper Block", $copperBreakInfo)); @@ -1689,9 +1777,8 @@ final class VanillaBlocks{ self::register("cut_copper_stairs", fn(BID $id) => new CopperStairs($id, "Cut Copper Stairs", $copperBreakInfo)); self::register("copper_bulb", fn(BID $id) => new CopperBulb($id, "Copper Bulb", $copperBreakInfo)); - $copperDoorBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 30.0)); - self::register("copper_door", fn(BID $id) => new CopperDoor($id, "Copper Door", $copperDoorBreakInfo)); - self::register("copper_trapdoor", fn(BID $id) => new CopperTrapdoor($id, "Copper Trapdoor", $copperDoorBreakInfo)); + self::register("copper_door", fn(BID $id) => new CopperDoor($id, "Copper Door", new Info(BreakInfo::pickaxe(3.0, blastResistance: 30.0)))); + self::register("copper_trapdoor", fn(BID $id) => new CopperTrapdoor($id, "Copper Trapdoor", new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 30.0)))); $candleBreakInfo = new Info(new BreakInfo(0.1)); self::register("candle", fn(BID $id) => new Candle($id, "Candle", $candleBreakInfo)); @@ -1707,8 +1794,8 @@ final class VanillaBlocks{ self::register("cave_vines", fn(BID $id) => new CaveVines($id, "Cave Vines", new Info(BreakInfo::instant()))); self::register("small_dripleaf", fn(BID $id) => new SmallDripleaf($id, "Small Dripleaf", new Info(BreakInfo::instant(ToolType::SHEARS, toolHarvestLevel: 1)))); - self::register("big_dripleaf_head", fn(BID $id) => new BigDripleafHead($id, "Big Dripleaf", new Info(BreakInfo::instant()))); - self::register("big_dripleaf_stem", fn(BID $id) => new BigDripleafStem($id, "Big Dripleaf Stem", new Info(BreakInfo::instant()))); + self::register("big_dripleaf_head", fn(BID $id) => new BigDripleafHead($id, "Big Dripleaf", new Info(new BreakInfo(0.1)))); + self::register("big_dripleaf_stem", fn(BID $id) => new BigDripleafStem($id, "Big Dripleaf Stem", new Info(new BreakInfo(0.1)))); } private static function registerBlocksR18() : void{ @@ -1719,7 +1806,7 @@ final class VanillaBlocks{ 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)))); - $mudBricksBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0)); + $mudBricksBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 15.0)); self::register("mud_bricks", fn(BID $id) => new Opaque($id, "Mud Bricks", $mudBricksBreakInfo)); self::register("mud_brick_slab", fn(BID $id) => new Slab($id, "Mud Brick", $mudBricksBreakInfo)); @@ -1727,6 +1814,18 @@ final class VanillaBlocks{ self::register("mud_brick_wall", fn(BID $id) => new Wall($id, "Mud Brick Wall", $mudBricksBreakInfo)); } + 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()))); + + $resinBricksInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); + self::register("resin_brick_slab", fn(BID $id) => new Slab($id, "Resin Brick", $resinBricksInfo)); + self::register("resin_brick_stairs", fn(BID $id) => new Stair($id, "Resin Brick Stairs", $resinBricksInfo)); + self::register("resin_brick_wall", fn(BID $id) => new Wall($id, "Resin Brick Wall", $resinBricksInfo)); + self::register("resin_bricks", fn(BID $id) => new Opaque($id, "Resin Bricks", $resinBricksInfo)); + self::register("chiseled_resin_bricks", fn(BID $id) => new Opaque($id, "Chiseled Resin Bricks", $resinBricksInfo)); + } + private static function registerTuffBlocks() : void{ $tuffBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); diff --git a/src/block/WallBanner.php b/src/block/WallBanner.php index 5182fe9f8..b631e0c81 100644 --- a/src/block/WallBanner.php +++ b/src/block/WallBanner.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\item\Item; use pocketmine\math\Axis; @@ -31,9 +32,13 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class WallBanner extends BaseBanner{ +final class WallBanner extends BaseBanner implements HorizontalFacing{ use HorizontalFacingTrait; + protected function getOminousVersion() : Block{ + return VanillaBlocks::OMINOUS_WALL_BANNER()->setFacing($this->facing); + } + protected function getSupportingFace() : int{ return Facing::opposite($this->facing); } diff --git a/src/block/WallCoralFan.php b/src/block/WallCoralFan.php index f9dece1cd..67745a537 100644 --- a/src/block/WallCoralFan.php +++ b/src/block/WallCoralFan.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -33,7 +34,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; -final class WallCoralFan extends BaseCoral{ +final class WallCoralFan extends BaseCoral implements HorizontalFacing{ use HorizontalFacingTrait; protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ diff --git a/src/block/WallHangingSign.php b/src/block/WallHangingSign.php new file mode 100644 index 000000000..6d4cfb95e --- /dev/null +++ b/src/block/WallHangingSign.php @@ -0,0 +1,92 @@ +facing, clockwise: true); + } + + public function onNearbyBlockChange() : void{ + //NOOP - disable default self-destruct behaviour + } + + protected function recalculateCollisionBoxes() : array{ + //only the cross bar is collidable + return [AxisAlignedBB::one()->trim(Facing::DOWN, 7 / 8)->squash(Facing::axis($this->facing), 3 / 4)]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player === null){ + return false; + } + $attachFace = Facing::axis($face) === Axis::Y ? Facing::rotateY($player->getHorizontalFacing(), clockwise: true) : $face; + + if($this->canBeSupportedAt($blockReplace->getSide($attachFace), $attachFace)){ + $direction = $attachFace; + }elseif($this->canBeSupportedAt($blockReplace->getSide($opposite = Facing::opposite($attachFace)), $opposite)){ + $direction = $opposite; + }else{ + return false; + } + + $this->facing = Facing::rotateY(Facing::opposite($direction), clockwise: true); + //the front should always face the player if possible + if($this->facing === $player->getHorizontalFacing()){ + $this->facing = Facing::opposite($this->facing); + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + private function canBeSupportedAt(Block $block, int $face) : bool{ + return + ($block instanceof WallHangingSign && Facing::axis(Facing::rotateY($block->getFacing(), clockwise: true)) === Facing::axis($face)) || + $block->getSupportType(Facing::opposite($face)) === SupportType::FULL; + } + + protected function getFacingDegrees() : float{ + return match($this->facing){ + Facing::SOUTH => 0, + Facing::WEST => 90, + Facing::NORTH => 180, + Facing::EAST => 270, + default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing), + }; + } +} diff --git a/src/block/WallSign.php b/src/block/WallSign.php index 07826eae7..40e1ba458 100644 --- a/src/block/WallSign.php +++ b/src/block/WallSign.php @@ -23,15 +23,17 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\HorizontalFacing; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\item\Item; use pocketmine\math\Axis; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; +use pocketmine\utils\AssumptionFailedError; use pocketmine\world\BlockTransaction; -final class WallSign extends BaseSign{ +final class WallSign extends BaseSign implements HorizontalFacing{ use HorizontalFacingTrait; protected function getSupportingFace() : int{ @@ -45,4 +47,25 @@ final class WallSign extends BaseSign{ $this->facing = $face; return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } + + protected function getHitboxCenter() : Vector3{ + [$xOffset, $zOffset] = match($this->facing){ + Facing::NORTH => [0, 15 / 16], + Facing::SOUTH => [0, 1 / 16], + Facing::WEST => [15 / 16, 0], + Facing::EAST => [1 / 16, 0], + default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing), + }; + return $this->position->add($xOffset, 0.5, $zOffset); + } + + protected function getFacingDegrees() : float{ + return match($this->facing){ + Facing::SOUTH => 0, + Facing::WEST => 90, + Facing::NORTH => 180, + Facing::EAST => 270, + default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing), + }; + } } diff --git a/src/block/Water.php b/src/block/Water.php index b711ab5a1..44759783a 100644 --- a/src/block/Water.php +++ b/src/block/Water.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\entity\Entity; +use pocketmine\event\entity\EntityExtinguishEvent; use pocketmine\world\sound\BucketEmptyWaterSound; use pocketmine\world\sound\BucketFillWaterSound; use pocketmine\world\sound\Sound; @@ -53,7 +54,7 @@ class Water extends Liquid{ public function onEntityInside(Entity $entity) : bool{ $entity->resetFallDistance(); if($entity->isOnFire()){ - $entity->extinguish(); + $entity->extinguish(EntityExtinguishEvent::CAUSE_WATER); } return true; } diff --git a/src/block/WaterCauldron.php b/src/block/WaterCauldron.php index e470aa6cb..8129f2960 100644 --- a/src/block/WaterCauldron.php +++ b/src/block/WaterCauldron.php @@ -27,6 +27,7 @@ use pocketmine\block\tile\Cauldron as TileCauldron; use pocketmine\block\utils\DyeColor; use pocketmine\color\Color; use pocketmine\entity\Entity; +use pocketmine\event\entity\EntityExtinguishEvent; use pocketmine\item\Armor; use pocketmine\item\Banner; use pocketmine\item\Dye; @@ -183,7 +184,7 @@ final class WaterCauldron extends FillableCauldron{ public function onEntityInside(Entity $entity) : bool{ if($entity->isOnFire()){ - $entity->extinguish(); + $entity->extinguish(EntityExtinguishEvent::CAUSE_WATER_CAULDRON); //TODO: particles $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::ENTITY_EXTINGUISH_USE_AMOUNT)); diff --git a/src/block/WaterLily.php b/src/block/WaterLily.php index 5dfb0d74a..b04b1baed 100644 --- a/src/block/WaterLily.php +++ b/src/block/WaterLily.php @@ -33,9 +33,6 @@ class WaterLily extends Flowable{ canBePlacedAt as supportedWhenPlacedAt; } - /** - * @return AxisAlignedBB[] - */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()->contract(1 / 16, 0, 1 / 16)->trim(Facing::UP, 63 / 64)]; } diff --git a/src/block/WeightedPressurePlate.php b/src/block/WeightedPressurePlate.php index 726b31f6b..065f8b2c8 100644 --- a/src/block/WeightedPressurePlate.php +++ b/src/block/WeightedPressurePlate.php @@ -23,13 +23,14 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\AnalogRedstoneSignalEmitter; use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; use function ceil; use function count; use function max; use function min; -class WeightedPressurePlate extends PressurePlate{ +class WeightedPressurePlate extends PressurePlate implements AnalogRedstoneSignalEmitter{ use AnalogRedstoneSignalEmitterTrait; private readonly float $signalStrengthFactor; diff --git a/src/block/Wood.php b/src/block/Wood.php index 127533b98..7aa667bc8 100644 --- a/src/block/Wood.php +++ b/src/block/Wood.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\PillarRotation; use pocketmine\block\utils\PillarRotationTrait; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Axe; @@ -32,7 +34,7 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\sound\ItemUseOnBlockSound; -class Wood extends Opaque{ +class Wood extends Opaque implements PillarRotation, WoodMaterial{ use PillarRotationTrait; use WoodTypeTrait; diff --git a/src/block/WoodenButton.php b/src/block/WoodenButton.php index 7ba8a7af0..4eed3a50e 100644 --- a/src/block/WoodenButton.php +++ b/src/block/WoodenButton.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenButton extends Button{ +class WoodenButton extends Button implements WoodMaterial{ use WoodTypeTrait; protected function getActivationTime() : int{ diff --git a/src/block/WoodenDoor.php b/src/block/WoodenDoor.php index 96f349e49..b7918a820 100644 --- a/src/block/WoodenDoor.php +++ b/src/block/WoodenDoor.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenDoor extends Door{ +class WoodenDoor extends Door implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/WoodenFence.php b/src/block/WoodenFence.php index c12043a86..73c8da44d 100644 --- a/src/block/WoodenFence.php +++ b/src/block/WoodenFence.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenFence extends Fence{ +class WoodenFence extends Fence implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/WoodenPressurePlate.php b/src/block/WoodenPressurePlate.php index a629c2f1c..6d638788b 100644 --- a/src/block/WoodenPressurePlate.php +++ b/src/block/WoodenPressurePlate.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodType; use pocketmine\block\utils\WoodTypeTrait; -class WoodenPressurePlate extends SimplePressurePlate{ +class WoodenPressurePlate extends SimplePressurePlate implements WoodMaterial{ use WoodTypeTrait; public function __construct( diff --git a/src/block/WoodenSlab.php b/src/block/WoodenSlab.php index b388a36ea..3ed606c64 100644 --- a/src/block/WoodenSlab.php +++ b/src/block/WoodenSlab.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenSlab extends Slab{ +class WoodenSlab extends Slab implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/WoodenStairs.php b/src/block/WoodenStairs.php index 0d9ba62ce..8da3fceb6 100644 --- a/src/block/WoodenStairs.php +++ b/src/block/WoodenStairs.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenStairs extends Stair{ +class WoodenStairs extends Stair implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/WoodenTrapdoor.php b/src/block/WoodenTrapdoor.php index c0a6623a1..2b796df52 100644 --- a/src/block/WoodenTrapdoor.php +++ b/src/block/WoodenTrapdoor.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodMaterial; use pocketmine\block\utils\WoodTypeTrait; -class WoodenTrapdoor extends Trapdoor{ +class WoodenTrapdoor extends Trapdoor implements WoodMaterial{ use WoodTypeTrait; public function getFuelTime() : int{ diff --git a/src/block/Wool.php b/src/block/Wool.php index 0b008ac04..d47c27d27 100644 --- a/src/block/Wool.php +++ b/src/block/Wool.php @@ -23,9 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Colored; use pocketmine\block\utils\ColoredTrait; -class Wool extends Opaque{ +class Wool extends Opaque implements Colored{ use ColoredTrait; public function getFlameEncouragement() : int{ diff --git a/src/block/tile/Banner.php b/src/block/tile/Banner.php index 08a560707..ac19d9835 100644 --- a/src/block/tile/Banner.php +++ b/src/block/tile/Banner.php @@ -41,6 +41,10 @@ class Banner extends Spawnable{ public const TAG_PATTERNS = "Patterns"; public const TAG_PATTERN_COLOR = "Color"; public const TAG_PATTERN_NAME = "Pattern"; + public const TAG_TYPE = "Type"; + + public const TYPE_NORMAL = 0; + public const TYPE_OMINOUS = 1; private DyeColor $baseColor = DyeColor::BLACK; @@ -50,6 +54,8 @@ class Banner extends Spawnable{ */ private array $patterns = []; + private int $type = self::TYPE_NORMAL; + public function readSaveData(CompoundTag $nbt) : void{ $colorIdMap = DyeColorIdMap::getInstance(); if( @@ -63,9 +69,8 @@ class Banner extends Spawnable{ $patternTypeIdMap = BannerPatternTypeIdMap::getInstance(); - $patterns = $nbt->getListTag(self::TAG_PATTERNS); + $patterns = $nbt->getListTag(self::TAG_PATTERNS, CompoundTag::class); if($patterns !== null){ - /** @var CompoundTag $pattern */ foreach($patterns as $pattern){ $patternColor = $colorIdMap->fromInvertedId($pattern->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK; //TODO: missing pattern colour should be an error $patternType = $patternTypeIdMap->fromId($pattern->getString(self::TAG_PATTERN_NAME)); @@ -75,6 +80,8 @@ class Banner extends Spawnable{ $this->patterns[] = new BannerPatternLayer($patternType, $patternColor); } } + + $this->type = $nbt->getInt(self::TAG_TYPE, self::TYPE_NORMAL); } protected function writeSaveData(CompoundTag $nbt) : void{ @@ -89,6 +96,7 @@ class Banner extends Spawnable{ ); } $nbt->setTag(self::TAG_PATTERNS, $patterns); + $nbt->setInt(self::TAG_TYPE, $this->type); } protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ @@ -103,6 +111,7 @@ class Banner extends Spawnable{ ); } $nbt->setTag(self::TAG_PATTERNS, $patterns); + $nbt->setInt(self::TAG_TYPE, $this->type); } /** @@ -136,6 +145,10 @@ class Banner extends Spawnable{ $this->patterns = $patterns; } + public function getType() : int{ return $this->type; } + + public function setType(int $type) : void{ $this->type = $type; } + public function getDefaultName() : string{ return "Banner"; } diff --git a/src/block/tile/ChiseledBookshelf.php b/src/block/tile/ChiseledBookshelf.php index 06175e27f..90bf8f29b 100644 --- a/src/block/tile/ChiseledBookshelf.php +++ b/src/block/tile/ChiseledBookshelf.php @@ -34,6 +34,7 @@ use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; +use pocketmine\nbt\UnexpectedTagTypeException; use pocketmine\world\World; use function count; @@ -86,13 +87,18 @@ class ChiseledBookshelf extends Tile implements Container{ } protected function loadItems(CompoundTag $tag) : void{ - if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ + try{ + $inventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class); + }catch(UnexpectedTagTypeException){ + //preserve the old behaviour of not throwing on wrong types + $inventoryTag = null; + } + if($inventoryTag !== null){ $inventory = $this->getRealInventory(); $listeners = $inventory->getListeners()->toArray(); $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $newContents = []; - /** @var CompoundTag $itemNBT */ foreach($inventoryTag as $slot => $itemNBT){ try{ $count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT); diff --git a/src/block/tile/ContainerTrait.php b/src/block/tile/ContainerTrait.php index fdd050a41..6b9158d7a 100644 --- a/src/block/tile/ContainerTrait.php +++ b/src/block/tile/ContainerTrait.php @@ -31,6 +31,7 @@ use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; +use pocketmine\nbt\UnexpectedTagTypeException; use pocketmine\world\Position; /** @@ -43,13 +44,18 @@ trait ContainerTrait{ abstract public function getRealInventory() : Inventory; protected function loadItems(CompoundTag $tag) : void{ - if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ + try{ + $inventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class); + }catch(UnexpectedTagTypeException){ + //preserve the old behaviour of not throwing on wrong types + $inventoryTag = null; + } + if($inventoryTag !== null){ $inventory = $this->getRealInventory(); $listeners = $inventory->getListeners()->toArray(); $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $newContents = []; - /** @var CompoundTag $itemNBT */ foreach($inventoryTag as $itemNBT){ try{ $newContents[$itemNBT->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($itemNBT); diff --git a/src/block/tile/HangingSign.php b/src/block/tile/HangingSign.php new file mode 100644 index 000000000..a5be9ba5c --- /dev/null +++ b/src/block/tile/HangingSign.php @@ -0,0 +1,31 @@ +text = new SignText(); + $this->backText = new SignText(); parent::__construct($world, $pos); } - private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : void{ + private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : SignText{ $baseColor = new Color(0, 0, 0); $glowingText = false; if(($baseColorTag = $nbt->getTag(self::TAG_TEXT_COLOR)) instanceof IntTag){ @@ -88,19 +92,27 @@ class Sign extends Spawnable{ //see https://bugs.mojang.com/browse/MCPE-117835 $glowingText = $glowingTextTag->getValue() !== 0; } - $this->text = SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText); + return SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText); + } + + private function writeTextTag(SignText $text) : CompoundTag{ + return CompoundTag::create() + ->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $text->getLines()), "\n")) + ->setInt(self::TAG_TEXT_COLOR, Binary::signInt($text->getBaseColor()->toARGB())) + ->setByte(self::TAG_GLOWING_TEXT, $text->isGlowing() ? 1 : 0) + ->setByte(self::TAG_PERSIST_FORMATTING, 1); } public function readSaveData(CompoundTag $nbt) : void{ $frontTextTag = $nbt->getTag(self::TAG_FRONT_TEXT); if($frontTextTag instanceof CompoundTag){ - $this->readTextTag($frontTextTag, true); + $this->text = $this->readTextTag($frontTextTag, true); }elseif($nbt->getTag(self::TAG_TEXT_BLOB) instanceof StringTag){ //MCPE 1.2 save format $lightingBugResolved = false; if(($lightingBugResolvedTag = $nbt->getTag(self::TAG_LEGACY_BUG_RESOLVE)) instanceof ByteTag){ $lightingBugResolved = $lightingBugResolvedTag->getValue() !== 0; } - $this->readTextTag($nbt, $lightingBugResolved); + $this->text = $this->readTextTag($nbt, $lightingBugResolved); }else{ $text = []; for($i = 0; $i < SignText::LINE_COUNT; ++$i){ @@ -111,22 +123,14 @@ class Sign extends Spawnable{ } $this->text = new SignText($text); } + $backTextTag = $nbt->getTag(self::TAG_BACK_TEXT); + $this->backText = $backTextTag instanceof CompoundTag ? $this->readTextTag($backTextTag, true) : new SignText(); $this->waxed = $nbt->getByte(self::TAG_WAXED, 0) !== 0; } protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create() - ->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines())) - ->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB())) - ->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0) - ->setByte(self::TAG_PERSIST_FORMATTING, 1) - ); - $nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create() - ->setString(self::TAG_TEXT_BLOB, "") - ->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00)) - ->setByte(self::TAG_GLOWING_TEXT, 0) - ->setByte(self::TAG_PERSIST_FORMATTING, 1) - ); + $nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text)); + $nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText)); $nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0); } @@ -139,6 +143,10 @@ class Sign extends Spawnable{ $this->text = $text; } + public function getBackText() : SignText{ return $this->backText; } + + public function setBackText(SignText $backText) : void{ $this->backText = $backText; } + public function isWaxed() : bool{ return $this->waxed; } public function setWaxed(bool $waxed) : void{ $this->waxed = $waxed; } @@ -160,19 +168,8 @@ class Sign extends Spawnable{ } protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create() - ->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines())) - ->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB())) - ->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0) - ->setByte(self::TAG_PERSIST_FORMATTING, 1) //TODO: not sure what this is used for - ); - //TODO: this is not yet used by the server, but needed to rollback any client-side changes to the back text - $nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create() - ->setString(self::TAG_TEXT_BLOB, "") - ->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00)) - ->setByte(self::TAG_GLOWING_TEXT, 0) - ->setByte(self::TAG_PERSIST_FORMATTING, 1) - ); + $nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text)); + $nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText)); $nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0); $nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1); } diff --git a/src/block/tile/Spawnable.php b/src/block/tile/Spawnable.php index 67bc72fd9..0c41713f2 100644 --- a/src/block/tile/Spawnable.php +++ b/src/block/tile/Spawnable.php @@ -32,7 +32,7 @@ use pocketmine\network\mcpe\protocol\types\CacheableNbt; 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 @@ abstract class Spawnable extends Tile{ * 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/block/tile/TileFactory.php b/src/block/tile/TileFactory.php index 515dd8c63..108483894 100644 --- a/src/block/tile/TileFactory.php +++ b/src/block/tile/TileFactory.php @@ -79,6 +79,7 @@ final class TileFactory{ $this->register(SporeBlossom::class, ["SporeBlossom", "minecraft:spore_blossom"]); $this->register(MobHead::class, ["Skull", "minecraft:skull"]); $this->register(GlowingItemFrame::class, ["GlowItemFrame"]); + $this->register(HangingSign::class, ["HangingSign", "minecraft:hanging_sign"]); //TODO: ChalkboardBlock //TODO: ChemistryTable @@ -114,6 +115,13 @@ final class TileFactory{ $this->saveNames[$className] = reset($saveNames); } + /** + * @phpstan-param class-string $class + */ + public function isRegistered(string $class) : bool{ + return isset($this->saveNames[$class]); + } + /** * @internal * @throws SavedDataLoadingException diff --git a/src/block/utils/Ageable.php b/src/block/utils/Ageable.php new file mode 100644 index 000000000..180392ef3 --- /dev/null +++ b/src/block/utils/Ageable.php @@ -0,0 +1,37 @@ +age; } + public function getMaxAge() : int{ return self::MAX_AGE; } + /** * @return $this */ diff --git a/src/block/utils/AnalogRedstoneSignalEmitter.php b/src/block/utils/AnalogRedstoneSignalEmitter.php new file mode 100644 index 000000000..fa2cc4b11 --- /dev/null +++ b/src/block/utils/AnalogRedstoneSignalEmitter.php @@ -0,0 +1,34 @@ +lit ? 3 : 0; } - /** @see Block::onInteract() */ + /** + * @param Item[] &$returnedItems + * @see Block::onInteract() + */ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($item->getTypeId() === ItemTypeIds::FIRE_CHARGE || $item->getTypeId() === ItemTypeIds::FLINT_AND_STEEL || $item->hasEnchantment(VanillaEnchantments::FIRE_ASPECT())){ if($this->lit){ diff --git a/src/block/utils/Colored.php b/src/block/utils/Colored.php new file mode 100644 index 000000000..af26d0b5d --- /dev/null +++ b/src/block/utils/Colored.php @@ -0,0 +1,34 @@ +waxed && $item->getTypeId() === ItemTypeIds::HONEYCOMB){ $this->waxed = true; diff --git a/src/block/utils/CoralMaterial.php b/src/block/utils/CoralMaterial.php new file mode 100644 index 000000000..0284322ac --- /dev/null +++ b/src/block/utils/CoralMaterial.php @@ -0,0 +1,41 @@ + "Mangrove", self::AZALEA => "Azalea", self::FLOWERING_AZALEA => "Flowering Azalea", - self::CHERRY => "Cherry" + self::CHERRY => "Cherry", + self::PALE_OAK => "Pale Oak", }; } } diff --git a/src/block/utils/Lightable.php b/src/block/utils/Lightable.php new file mode 100644 index 000000000..36ac479f3 --- /dev/null +++ b/src/block/utils/Lightable.php @@ -0,0 +1,34 @@ + ["C418 - cat", LevelSoundEvent::RECORD_CAT, KnownTranslationFactory::item_record_cat_desc()], self::DISK_BLOCKS => ["C418 - blocks", LevelSoundEvent::RECORD_BLOCKS, KnownTranslationFactory::item_record_blocks_desc()], self::DISK_CHIRP => ["C418 - chirp", LevelSoundEvent::RECORD_CHIRP, KnownTranslationFactory::item_record_chirp_desc()], + self::DISK_CREATOR => ["Lena Raine - Creator", LevelSoundEvent::RECORD_CREATOR, KnownTranslationFactory::item_record_creator_desc()], + self::DISK_CREATOR_MUSIC_BOX => ["Lena Raine - Creator (Music Box)", LevelSoundEvent::RECORD_CREATOR_MUSIC_BOX, KnownTranslationFactory::item_record_creator_music_box_desc()], self::DISK_FAR => ["C418 - far", LevelSoundEvent::RECORD_FAR, KnownTranslationFactory::item_record_far_desc()], self::DISK_MALL => ["C418 - mall", LevelSoundEvent::RECORD_MALL, KnownTranslationFactory::item_record_mall_desc()], self::DISK_MELLOHI => ["C418 - mellohi", LevelSoundEvent::RECORD_MELLOHI, KnownTranslationFactory::item_record_mellohi_desc()], self::DISK_OTHERSIDE => ["Lena Raine - otherside", LevelSoundEvent::RECORD_OTHERSIDE, KnownTranslationFactory::item_record_otherside_desc()], self::DISK_PIGSTEP => ["Lena Raine - Pigstep", LevelSoundEvent::RECORD_PIGSTEP, KnownTranslationFactory::item_record_pigstep_desc()], + self::DISK_PRECIPICE => ["Aaron Cherof - Precipice", LevelSoundEvent::RECORD_PRECIPICE, KnownTranslationFactory::item_record_precipice_desc()], + self::DISK_RELIC => ["Aaron Cherof - Relic", LevelSoundEvent::RECORD_RELIC, KnownTranslationFactory::item_record_relic_desc()], self::DISK_STAL => ["C418 - stal", LevelSoundEvent::RECORD_STAL, KnownTranslationFactory::item_record_stal_desc()], self::DISK_STRAD => ["C418 - strad", LevelSoundEvent::RECORD_STRAD, KnownTranslationFactory::item_record_strad_desc()], self::DISK_WARD => ["C418 - ward", LevelSoundEvent::RECORD_WARD, KnownTranslationFactory::item_record_ward_desc()], diff --git a/src/block/utils/SignLikeRotation.php b/src/block/utils/SignLikeRotation.php new file mode 100644 index 000000000..031c4abaf --- /dev/null +++ b/src/block/utils/SignLikeRotation.php @@ -0,0 +1,39 @@ + "Crimson", self::WARPED => "Warped", self::CHERRY => "Cherry", + self::PALE_OAK => "Pale Oak", }; } diff --git a/src/command/Command.php b/src/command/Command.php index 4c2c6815b..54822d80e 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -33,9 +33,11 @@ use pocketmine\permission\PermissionManager; use pocketmine\Server; use pocketmine\utils\BroadcastLoggerForwarder; use pocketmine\utils\TextFormat; +use function array_values; use function explode; use function implode; use function str_replace; +use const PHP_INT_MAX; abstract class Command{ @@ -80,6 +82,7 @@ abstract class Command{ /** * @param string[] $args + * @phpstan-param list $args * * @return mixed * @throws CommandException @@ -111,7 +114,7 @@ abstract class Command{ } public function setPermission(?string $permission) : void{ - $this->setPermissions($permission === null ? [] : explode(";", $permission)); + $this->setPermissions($permission === null ? [] : explode(";", $permission, limit: PHP_INT_MAX)); } public function testPermission(CommandSender $target, ?string $permission = null) : bool{ @@ -212,6 +215,7 @@ abstract class Command{ * @phpstan-param list $aliases */ public function setAliases(array $aliases) : void{ + $aliases = array_values($aliases); //because plugins can and will pass crap $this->aliases = $aliases; if(!$this->isRegistered()){ $this->activeAliases = $aliases; diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php index 5086672f6..b47363397 100644 --- a/src/command/FormattedCommandAlias.php +++ b/src/command/FormattedCommandAlias.php @@ -121,6 +121,7 @@ class FormattedCommandAlias extends Command{ /** * @param string[] $args + * @phpstan-param list $args */ private function buildCommand(string $formatString, array $args) : ?string{ $index = 0; diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index e0d8e6565..9f5441746 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -73,6 +73,7 @@ use pocketmine\timings\Timings; use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; use function array_shift; +use function array_values; use function count; use function implode; use function str_contains; @@ -163,7 +164,7 @@ class SimpleCommandMap implements CommandMap{ unset($aliases[$index]); } } - $command->setAliases($aliases); + $command->setAliases(array_values($aliases)); if(!$registered){ $command->setLabel($fallbackPrefix . ":" . $label); diff --git a/src/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php index 487c915f2..054585455 100644 --- a/src/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -39,6 +39,7 @@ use function ksort; use function min; use function sort; use function strtolower; +use const PHP_INT_MAX; use const SORT_FLAG_CASE; use const SORT_NATURAL; @@ -108,7 +109,7 @@ class HelpCommand extends VanillaCommand{ $usage = $cmd->getUsage(); $usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString))) + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX))) ->prefix(TextFormat::GOLD)); $aliases = $cmd->getAliases(); diff --git a/src/command/defaults/ParticleCommand.php b/src/command/defaults/ParticleCommand.php index f20d47ccc..4867e3eb5 100644 --- a/src/command/defaults/ParticleCommand.php +++ b/src/command/defaults/ParticleCommand.php @@ -219,7 +219,11 @@ class ParticleCommand extends VanillaCommand{ break; case "blockdust": if($data !== null){ - $d = explode("_", $data); + //to preserve the old unlimited explode behaviour, allow this to split into at most 5 parts + //this allows the 4th argument to be processed normally if given without forcing it to also consume + //any unexpected parts + //we probably ought to error in this case, but this will do for now + $d = explode("_", $data, limit: 5); if(count($d) >= 3){ return new DustParticle(new Color( ((int) $d[0]) & 0xff, diff --git a/src/command/defaults/TimingsCommand.php b/src/command/defaults/TimingsCommand.php index ec26f3efe..ffbc08491 100644 --- a/src/command/defaults/TimingsCommand.php +++ b/src/command/defaults/TimingsCommand.php @@ -46,6 +46,8 @@ use function fwrite; use function http_build_query; use function implode; use function is_array; +use function is_int; +use function is_string; use function json_decode; use function mkdir; use function strtolower; @@ -146,7 +148,8 @@ class TimingsCommand extends VanillaCommand{ private function uploadReport(array $lines, CommandSender $sender) : void{ $data = [ "browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(), - "data" => implode("\n", $lines) + "data" => implode("\n", $lines), + "private" => "true" ]; $host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io"); @@ -178,9 +181,14 @@ class TimingsCommand extends VanillaCommand{ return; } $response = json_decode($result->getBody(), true); - if(is_array($response) && isset($response["id"])){ - Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead( - "https://" . $host . "/?id=" . $response["id"])); + if(is_array($response) && isset($response["id"]) && (is_int($response["id"]) || is_string($response["id"]))){ + $url = "https://" . $host . "/?id=" . $response["id"]; + if(isset($response["access_token"]) && is_string($response["access_token"])){ + $url .= "&access_token=" . $response["access_token"]; + }else{ + $sender->getServer()->getLogger()->warning("Your chosen timings host does not support private reports. Anyone will be able to see your report if they guess the ID."); + } + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead($url)); }else{ $sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody()); Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError()); diff --git a/src/command/utils/CommandStringHelper.php b/src/command/utils/CommandStringHelper.php index eacc5d3d8..76d70a9bb 100644 --- a/src/command/utils/CommandStringHelper.php +++ b/src/command/utils/CommandStringHelper.php @@ -51,9 +51,8 @@ final class CommandStringHelper{ foreach($matches[0] as $k => $_){ for($i = 1; $i <= 2; ++$i){ if($matches[$i][$k] !== ""){ - /** @var string $match */ //phpstan can't understand preg_match and friends by itself :( $match = $matches[$i][$k]; - $args[(int) $k] = preg_replace('/\\\\([\\\\"])/u', '$1', $match) ?? throw new AssumptionFailedError(preg_last_error_msg()); + $args[] = preg_replace('/\\\\([\\\\"])/u', '$1', $match) ?? throw new AssumptionFailedError(preg_last_error_msg()); break; } } diff --git a/src/console/ConsoleCommandSender.php b/src/console/ConsoleCommandSender.php index aa7ea6e69..a0c1c5200 100644 --- a/src/console/ConsoleCommandSender.php +++ b/src/console/ConsoleCommandSender.php @@ -62,7 +62,7 @@ class ConsoleCommandSender implements CommandSender{ $message = $this->getLanguage()->translate($message); } - foreach(explode("\n", trim($message)) as $line){ + foreach(explode("\n", trim($message), limit: PHP_INT_MAX) as $line){ Terminal::writeLine(TextFormat::GREEN . "Command output | " . TextFormat::addBase(TextFormat::WHITE, $line)); } } diff --git a/src/crafting/CraftingManager.php b/src/crafting/CraftingManager.php index ff2be6926..673095c6e 100644 --- a/src/crafting/CraftingManager.php +++ b/src/crafting/CraftingManager.php @@ -23,14 +23,19 @@ declare(strict_types=1); namespace pocketmine\crafting; +use pmmp\encoding\ByteBufferWriter; +use pmmp\encoding\VarInt; use pocketmine\item\Item; use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\TreeRoot; -use pocketmine\utils\BinaryStream; use pocketmine\utils\DestructorCallbackTrait; use pocketmine\utils\ObjectSet; +use function array_shift; +use function count; +use function implode; +use function ksort; use function spl_object_id; -use function usort; +use const SORT_STRING; class CraftingManager{ use DestructorCallbackTrait; @@ -100,6 +105,7 @@ class CraftingManager{ /** * Function used to arrange Shapeless Recipe ingredient lists into a consistent order. + * @deprecated */ public static function sort(Item $i1, Item $i2) : int{ //Use spaceship operator to compare each property, then try the next one if they are equivalent. @@ -108,47 +114,32 @@ class CraftingManager{ return $retval; } - /** - * @param Item[] $items - * @phpstan-param list $items - * - * @return Item[] - * @phpstan-return list - */ - private static function pack(array $items) : array{ - $result = []; + private static function hashOutput(Item $output) : string{ + $write = new ByteBufferWriter(); + VarInt::writeSignedInt($write, $output->getStateId()); + //TODO: the NBT serializer allocates its own ByteBufferWriter, we should change the API in the future to + //allow passing our own to avoid this extra allocation + $write->writeByteArray((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag()))); - foreach($items as $item){ - foreach($result as $otherItem){ - if($item->canStackWith($otherItem)){ - $otherItem->setCount($otherItem->getCount() + $item->getCount()); - continue 2; - } - } - - //No matching item found - $result[] = clone $item; - } - - return $result; + return $write->getData(); } /** * @param Item[] $outputs - * @phpstan-param list $outputs */ private static function hashOutputs(array $outputs) : string{ - $outputs = self::pack($outputs); - usort($outputs, [self::class, "sort"]); - $result = new BinaryStream(); + if(count($outputs) === 1){ + return self::hashOutput(array_shift($outputs)); + } + $unique = []; foreach($outputs as $o){ //count is not written because the outputs might be from multiple repetitions of a single recipe //this reduces the accuracy of the hash, but it won't matter in most cases. - $result->putVarInt($o->getStateId()); - $result->put((new LittleEndianNbtSerializer())->write(new TreeRoot($o->getNamedTag()))); + $hash = self::hashOutput($o); + $unique[$hash] = $hash; } - - return $result->getBuffer(); + ksort($unique, SORT_STRING); + return implode("", $unique); } /** diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php index 3df6bfb62..7c9cdd58b 100644 --- a/src/crafting/CraftingManagerFromDataHelper.php +++ b/src/crafting/CraftingManagerFromDataHelper.php @@ -162,7 +162,7 @@ final class CraftingManagerFromDataHelper{ } $mapper = new \JsonMapper(); - $mapper->bStrictObjectTypeChecking = true; + $mapper->bStrictObjectTypeChecking = false; //to allow hydrating ItemStackData - since this is only used for offline data it's safe $mapper->bExceptionOnUndefinedProperty = true; $mapper->bExceptionOnMissingData = true; diff --git a/src/crafting/ShapedRecipe.php b/src/crafting/ShapedRecipe.php index 4c40eb0f5..2af3f5134 100644 --- a/src/crafting/ShapedRecipe.php +++ b/src/crafting/ShapedRecipe.php @@ -97,6 +97,7 @@ class ShapedRecipe implements CraftingRecipe{ $this->shape = $shape; + Utils::validateArrayValueType($ingredients, function(RecipeIngredient $_) : void{}); foreach(Utils::stringifyKeys($ingredients) as $char => $i){ if(!str_contains(implode($this->shape), $char)){ throw new \InvalidArgumentException("Symbol '$char' does not appear in the recipe shape"); @@ -105,6 +106,7 @@ class ShapedRecipe implements CraftingRecipe{ $this->ingredientList[$char] = clone $i; } + Utils::validateArrayValueType($results, function(Item $_) : void{}); $this->results = Utils::cloneObjectArray($results); } diff --git a/src/crafting/ShapelessRecipe.php b/src/crafting/ShapelessRecipe.php index 7a4a22fda..b139439ef 100644 --- a/src/crafting/ShapelessRecipe.php +++ b/src/crafting/ShapelessRecipe.php @@ -53,7 +53,9 @@ class ShapelessRecipe implements CraftingRecipe{ if(count($ingredients) > 9){ throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients"); } + Utils::validateArrayValueType($ingredients, function(RecipeIngredient $_) : void{}); $this->ingredients = $ingredients; + Utils::validateArrayValueType($results, function(Item $_) : void{}); $this->results = Utils::cloneObjectArray($results); } diff --git a/src/crafting/json/ItemStackData.php b/src/crafting/json/ItemStackData.php index 032c7da7d..bf079e920 100644 --- a/src/crafting/json/ItemStackData.php +++ b/src/crafting/json/ItemStackData.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\crafting\json; -final class ItemStackData{ +use function count; + +final class ItemStackData implements \JsonSerializable{ /** @required */ public string $name; @@ -40,4 +42,15 @@ final class ItemStackData{ public function __construct(string $name){ $this->name = $name; } + + /** + * @return mixed[]|string + */ + public function jsonSerialize() : array|string{ + $result = (array) $this; + if(count($result) === 1 && isset($result["name"])){ + return $this->name; + } + return $result; + } } diff --git a/src/data/bedrock/BedrockDataFiles.php b/src/data/bedrock/BedrockDataFiles.php index 5c476ca1c..53bd9b11e 100644 --- a/src/data/bedrock/BedrockDataFiles.php +++ b/src/data/bedrock/BedrockDataFiles.php @@ -31,21 +31,23 @@ final class BedrockDataFiles{ } 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'; public const BLOCK_STATE_META_MAP_JSON = BEDROCK_DATA_PATH . '/block_state_meta_map.json'; public const CANONICAL_BLOCK_STATES_NBT = BEDROCK_DATA_PATH . '/canonical_block_states.nbt'; public const COMMAND_ARG_TYPES_JSON = BEDROCK_DATA_PATH . '/command_arg_types.json'; - public const CREATIVEITEMS_JSON = BEDROCK_DATA_PATH . '/creativeitems.json'; + public const CREATIVE = BEDROCK_DATA_PATH . '/creative'; public const ENTITY_ID_MAP_JSON = BEDROCK_DATA_PATH . '/entity_id_map.json'; public const ENTITY_IDENTIFIERS_NBT = BEDROCK_DATA_PATH . '/entity_identifiers.nbt'; + public const ENUMS = BEDROCK_DATA_PATH . '/enums'; + public const ENUMS_PY = BEDROCK_DATA_PATH . '/enums.py'; public const ITEM_TAGS_JSON = BEDROCK_DATA_PATH . '/item_tags.json'; public const LEVEL_SOUND_ID_MAP_JSON = BEDROCK_DATA_PATH . '/level_sound_id_map.json'; - public const PARTICLE_ID_MAP_JSON = BEDROCK_DATA_PATH . '/particle_id_map.json'; + public const PROTOCOL_INFO_JSON = BEDROCK_DATA_PATH . '/protocol_info.json'; public const R12_TO_CURRENT_BLOCK_MAP_BIN = BEDROCK_DATA_PATH . '/r12_to_current_block_map.bin'; public const R16_TO_CURRENT_ITEM_MAP_JSON = BEDROCK_DATA_PATH . '/r16_to_current_item_map.json'; + public const RECIPES = BEDROCK_DATA_PATH . '/recipes'; public const REQUIRED_ITEM_LIST_JSON = BEDROCK_DATA_PATH . '/required_item_list.json'; } diff --git a/src/data/bedrock/BiomeIds.php b/src/data/bedrock/BiomeIds.php index 1169a51ea..f3c38d3ed 100644 --- a/src/data/bedrock/BiomeIds.php +++ b/src/data/bedrock/BiomeIds.php @@ -122,4 +122,5 @@ final class BiomeIds{ public const DEEP_DARK = 190; public const MANGROVE_SWAMP = 191; public const CHERRY_GROVE = 192; + public const PALE_GARDEN = 193; } diff --git a/src/data/bedrock/FireworkRocketTypeIdMap.php b/src/data/bedrock/FireworkRocketTypeIdMap.php new file mode 100644 index 000000000..4358c2073 --- /dev/null +++ b/src/data/bedrock/FireworkRocketTypeIdMap.php @@ -0,0 +1,45 @@ + */ + use IntSaveIdMapTrait; + + private function __construct(){ + foreach(FireworkRocketType::cases() as $case){ + $this->register(match($case){ + FireworkRocketType::SMALL_BALL => FireworkRocketTypeIds::SMALL_BALL, + FireworkRocketType::LARGE_BALL => FireworkRocketTypeIds::LARGE_BALL, + FireworkRocketType::STAR => FireworkRocketTypeIds::STAR, + FireworkRocketType::CREEPER => FireworkRocketTypeIds::CREEPER, + FireworkRocketType::BURST => FireworkRocketTypeIds::BURST, + }, $case); + } + } +} diff --git a/src/data/bedrock/FireworkRocketTypeIds.php b/src/data/bedrock/FireworkRocketTypeIds.php new file mode 100644 index 000000000..7ddf0e6b9 --- /dev/null +++ b/src/data/bedrock/FireworkRocketTypeIds.php @@ -0,0 +1,32 @@ + */ use IntSaveIdMapTrait; public function __construct(){ + $newMapping = ValueMappings::getInstance()->mushroomBlockType; foreach(MushroomBlockType::cases() as $case){ - $this->register(match($case){ - MushroomBlockType::PORES => LegacyMeta::MUSHROOM_BLOCK_ALL_PORES, - MushroomBlockType::CAP_NORTHWEST => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHWEST_CORNER, - MushroomBlockType::CAP_NORTH => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTH_SIDE, - MushroomBlockType::CAP_NORTHEAST => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHEAST_CORNER, - MushroomBlockType::CAP_WEST => LegacyMeta::MUSHROOM_BLOCK_CAP_WEST_SIDE, - MushroomBlockType::CAP_MIDDLE => LegacyMeta::MUSHROOM_BLOCK_CAP_TOP_ONLY, - MushroomBlockType::CAP_EAST => LegacyMeta::MUSHROOM_BLOCK_CAP_EAST_SIDE, - MushroomBlockType::CAP_SOUTHWEST => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHWEST_CORNER, - MushroomBlockType::CAP_SOUTH => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTH_SIDE, - MushroomBlockType::CAP_SOUTHEAST => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHEAST_CORNER, - MushroomBlockType::ALL_CAP => LegacyMeta::MUSHROOM_BLOCK_ALL_CAP, - }, $case); + $this->register($newMapping->valueToRaw($case), $case); } } } diff --git a/src/data/bedrock/NoteInstrumentIdMap.php b/src/data/bedrock/NoteInstrumentIdMap.php index c847ecd98..7f0c0f310 100644 --- a/src/data/bedrock/NoteInstrumentIdMap.php +++ b/src/data/bedrock/NoteInstrumentIdMap.php @@ -39,10 +39,10 @@ final class NoteInstrumentIdMap{ NoteInstrument::SNARE => 2, NoteInstrument::CLICKS_AND_STICKS => 3, NoteInstrument::DOUBLE_BASS => 4, - NoteInstrument::BELL => 5, - NoteInstrument::FLUTE => 6, - NoteInstrument::CHIME => 7, - NoteInstrument::GUITAR => 8, + NoteInstrument::FLUTE => 5, + NoteInstrument::BELL => 6, + NoteInstrument::GUITAR => 7, + NoteInstrument::CHIME => 8, NoteInstrument::XYLOPHONE => 9, NoteInstrument::IRON_XYLOPHONE => 10, NoteInstrument::COW_BELL => 11, diff --git a/src/data/bedrock/WorldDataVersions.php b/src/data/bedrock/WorldDataVersions.php new file mode 100644 index 000000000..e682eb43a --- /dev/null +++ b/src/data/bedrock/WorldDataVersions.php @@ -0,0 +1,66 @@ + + * @var (\Closure|BlockStateData)[] + * @phpstan-var array */ private array $serializers = []; @@ -204,49 +52,53 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{ */ private array $cache = []; - public function __construct(){ - $this->registerCandleSerializers(); - $this->registerFlatColorBlockSerializers(); - $this->registerFlatCoralSerializers(); - $this->registerCauldronSerializers(); - $this->registerFlatWoodBlockSerializers(); - $this->registerLeavesSerializers(); - $this->registerSaplingSerializers(); - $this->registerMobHeadSerializers(); - $this->registerSimpleSerializers(); - $this->registerSerializers(); - } - public function serialize(int $stateId) : BlockStateData{ //TODO: singleton usage not ideal //TODO: we may want to deduplicate cache entries to avoid wasting memory return $this->cache[$stateId] ??= $this->serializeBlock(RuntimeBlockStateRegistry::getInstance()->fromStateId($stateId)); } + public function isRegistered(Block $block) : bool{ + return isset($this->serializers[$block->getTypeId()]); + } + /** * @phpstan-template TBlockType of Block * @phpstan-param TBlockType $block - * @phpstan-param \Closure(TBlockType) : Writer $serializer + * @phpstan-param \Closure(TBlockType) : (Writer|BlockStateData)|Writer|BlockStateData $serializer */ - public function map(Block $block, \Closure $serializer) : void{ + public function map(Block $block, \Closure|Writer|BlockStateData $serializer) : void{ if(isset($this->serializers[$block->getTypeId()])){ - throw new \InvalidArgumentException("Block type ID " . $block->getTypeId() . " already has a serializer registered"); + throw new \InvalidArgumentException("Block type ID " . $block->getTypeId() . " (" . $block->getName() . ") already has a serializer registered"); } - $this->serializers[$block->getTypeId()] = $serializer; + //writer accepted for convenience only + $this->serializers[$block->getTypeId()] = $serializer instanceof Writer ? $serializer->getBlockStateData() : $serializer; } + /** + * @deprecated + */ public function mapSimple(Block $block, string $id) : void{ - $this->map($block, fn() => Writer::create($id)); + $this->map($block, BlockStateData::current($id, [])); } + /** + * @deprecated + */ public function mapSlab(Slab $block, string $singleId, string $doubleId) : void{ $this->map($block, fn(Slab $block) => Helper::encodeSlab($block, $singleId, $doubleId)); } + /** + * @deprecated + */ public function mapStairs(Stair $block, string $id) : void{ $this->map($block, fn(Stair $block) => Helper::encodeStairs($block, Writer::create($id))); } + /** + * @deprecated + */ public function mapLog(Wood $block, string $unstrippedId, string $strippedId) : void{ $this->map($block, fn(Wood $block) => Helper::encodeLog($block, $unstrippedId, $strippedId)); } @@ -265,1588 +117,20 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{ throw new BlockStateSerializeException("No serializer registered for " . get_class($blockState) . " with type ID $typeId"); } + if($locatedSerializer instanceof BlockStateData){ //static data, not dependent on state + return $locatedSerializer; + } + /** * TODO: there is no guarantee that this type actually matches that of $blockState - a plugin may have stolen * the type ID of the block (which never makes sense, even in a world where overriding block types is a thing). * In the future we'll need some way to guarantee that type IDs are never reused (perhaps spl_object_id()?) * - * @var \Closure $serializer - * @phpstan-var \Closure(TBlockType) : Writer $serializer + * @var \Closure $locatedSerializer + * @phpstan-var \Closure(TBlockType) : (Writer|BlockStateData) $locatedSerializer */ - $serializer = $locatedSerializer; + $result = $locatedSerializer($blockState); - /** @var Writer $writer */ - $writer = $serializer($blockState); - return $writer->getBlockStateData(); - } - - private function registerCandleSerializers() : void{ - $this->map(Blocks::CANDLE(), fn(Candle $block) => Helper::encodeCandle($block, new Writer(Ids::CANDLE))); - $this->map(Blocks::DYED_CANDLE(), fn(DyedCandle $block) => Helper::encodeCandle($block, new Writer(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_CANDLE, - DyeColor::BLUE => Ids::BLUE_CANDLE, - DyeColor::BROWN => Ids::BROWN_CANDLE, - DyeColor::CYAN => Ids::CYAN_CANDLE, - DyeColor::GRAY => Ids::GRAY_CANDLE, - DyeColor::GREEN => Ids::GREEN_CANDLE, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_CANDLE, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_CANDLE, - DyeColor::LIME => Ids::LIME_CANDLE, - DyeColor::MAGENTA => Ids::MAGENTA_CANDLE, - DyeColor::ORANGE => Ids::ORANGE_CANDLE, - DyeColor::PINK => Ids::PINK_CANDLE, - DyeColor::PURPLE => Ids::PURPLE_CANDLE, - DyeColor::RED => Ids::RED_CANDLE, - DyeColor::WHITE => Ids::WHITE_CANDLE, - DyeColor::YELLOW => Ids::YELLOW_CANDLE, - }))); - $this->map(Blocks::CAKE_WITH_CANDLE(), fn(CakeWithCandle $block) => Writer::create(Ids::CANDLE_CAKE) - ->writeBool(StateNames::LIT, $block->isLit())); - $this->map(Blocks::CAKE_WITH_DYED_CANDLE(), fn(CakeWithDyedCandle $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_CANDLE_CAKE, - DyeColor::BLUE => Ids::BLUE_CANDLE_CAKE, - DyeColor::BROWN => Ids::BROWN_CANDLE_CAKE, - DyeColor::CYAN => Ids::CYAN_CANDLE_CAKE, - DyeColor::GRAY => Ids::GRAY_CANDLE_CAKE, - DyeColor::GREEN => Ids::GREEN_CANDLE_CAKE, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_CANDLE_CAKE, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_CANDLE_CAKE, - DyeColor::LIME => Ids::LIME_CANDLE_CAKE, - DyeColor::MAGENTA => Ids::MAGENTA_CANDLE_CAKE, - DyeColor::ORANGE => Ids::ORANGE_CANDLE_CAKE, - DyeColor::PINK => Ids::PINK_CANDLE_CAKE, - DyeColor::PURPLE => Ids::PURPLE_CANDLE_CAKE, - DyeColor::RED => Ids::RED_CANDLE_CAKE, - DyeColor::WHITE => Ids::WHITE_CANDLE_CAKE, - DyeColor::YELLOW => Ids::YELLOW_CANDLE_CAKE, - })->writeBool(StateNames::LIT, $block->isLit())); - } - - public function registerFlatColorBlockSerializers() : void{ - $this->map(Blocks::STAINED_HARDENED_GLASS(), fn(StainedHardenedGlass $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::HARD_BLACK_STAINED_GLASS, - DyeColor::BLUE => Ids::HARD_BLUE_STAINED_GLASS, - DyeColor::BROWN => Ids::HARD_BROWN_STAINED_GLASS, - DyeColor::CYAN => Ids::HARD_CYAN_STAINED_GLASS, - DyeColor::GRAY => Ids::HARD_GRAY_STAINED_GLASS, - DyeColor::GREEN => Ids::HARD_GREEN_STAINED_GLASS, - DyeColor::LIGHT_BLUE => Ids::HARD_LIGHT_BLUE_STAINED_GLASS, - DyeColor::LIGHT_GRAY => Ids::HARD_LIGHT_GRAY_STAINED_GLASS, - DyeColor::LIME => Ids::HARD_LIME_STAINED_GLASS, - DyeColor::MAGENTA => Ids::HARD_MAGENTA_STAINED_GLASS, - DyeColor::ORANGE => Ids::HARD_ORANGE_STAINED_GLASS, - DyeColor::PINK => Ids::HARD_PINK_STAINED_GLASS, - DyeColor::PURPLE => Ids::HARD_PURPLE_STAINED_GLASS, - DyeColor::RED => Ids::HARD_RED_STAINED_GLASS, - DyeColor::WHITE => Ids::HARD_WHITE_STAINED_GLASS, - DyeColor::YELLOW => Ids::HARD_YELLOW_STAINED_GLASS, - })); - - $this->map(Blocks::STAINED_HARDENED_GLASS_PANE(), fn(StainedHardenedGlassPane $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::HARD_BLACK_STAINED_GLASS_PANE, - DyeColor::BLUE => Ids::HARD_BLUE_STAINED_GLASS_PANE, - DyeColor::BROWN => Ids::HARD_BROWN_STAINED_GLASS_PANE, - DyeColor::CYAN => Ids::HARD_CYAN_STAINED_GLASS_PANE, - DyeColor::GRAY => Ids::HARD_GRAY_STAINED_GLASS_PANE, - DyeColor::GREEN => Ids::HARD_GREEN_STAINED_GLASS_PANE, - DyeColor::LIGHT_BLUE => Ids::HARD_LIGHT_BLUE_STAINED_GLASS_PANE, - DyeColor::LIGHT_GRAY => Ids::HARD_LIGHT_GRAY_STAINED_GLASS_PANE, - DyeColor::LIME => Ids::HARD_LIME_STAINED_GLASS_PANE, - DyeColor::MAGENTA => Ids::HARD_MAGENTA_STAINED_GLASS_PANE, - DyeColor::ORANGE => Ids::HARD_ORANGE_STAINED_GLASS_PANE, - DyeColor::PINK => Ids::HARD_PINK_STAINED_GLASS_PANE, - DyeColor::PURPLE => Ids::HARD_PURPLE_STAINED_GLASS_PANE, - DyeColor::RED => Ids::HARD_RED_STAINED_GLASS_PANE, - DyeColor::WHITE => Ids::HARD_WHITE_STAINED_GLASS_PANE, - DyeColor::YELLOW => Ids::HARD_YELLOW_STAINED_GLASS_PANE, - })); - - $this->map(Blocks::GLAZED_TERRACOTTA(), function(GlazedTerracotta $block) : Writer{ - return Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_GLAZED_TERRACOTTA, - DyeColor::BLUE => Ids::BLUE_GLAZED_TERRACOTTA, - DyeColor::BROWN => Ids::BROWN_GLAZED_TERRACOTTA, - DyeColor::CYAN => Ids::CYAN_GLAZED_TERRACOTTA, - DyeColor::GRAY => Ids::GRAY_GLAZED_TERRACOTTA, - DyeColor::GREEN => Ids::GREEN_GLAZED_TERRACOTTA, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_GLAZED_TERRACOTTA, - DyeColor::LIGHT_GRAY => Ids::SILVER_GLAZED_TERRACOTTA, - DyeColor::LIME => Ids::LIME_GLAZED_TERRACOTTA, - DyeColor::MAGENTA => Ids::MAGENTA_GLAZED_TERRACOTTA, - DyeColor::ORANGE => Ids::ORANGE_GLAZED_TERRACOTTA, - DyeColor::PINK => Ids::PINK_GLAZED_TERRACOTTA, - DyeColor::PURPLE => Ids::PURPLE_GLAZED_TERRACOTTA, - DyeColor::RED => Ids::RED_GLAZED_TERRACOTTA, - DyeColor::WHITE => Ids::WHITE_GLAZED_TERRACOTTA, - DyeColor::YELLOW => Ids::YELLOW_GLAZED_TERRACOTTA, - }) - ->writeHorizontalFacing($block->getFacing()); - }); - - $this->map(Blocks::WOOL(), fn(Wool $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_WOOL, - DyeColor::BLUE => Ids::BLUE_WOOL, - DyeColor::BROWN => Ids::BROWN_WOOL, - DyeColor::CYAN => Ids::CYAN_WOOL, - DyeColor::GRAY => Ids::GRAY_WOOL, - DyeColor::GREEN => Ids::GREEN_WOOL, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_WOOL, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_WOOL, - DyeColor::LIME => Ids::LIME_WOOL, - DyeColor::MAGENTA => Ids::MAGENTA_WOOL, - DyeColor::ORANGE => Ids::ORANGE_WOOL, - DyeColor::PINK => Ids::PINK_WOOL, - DyeColor::PURPLE => Ids::PURPLE_WOOL, - DyeColor::RED => Ids::RED_WOOL, - DyeColor::WHITE => Ids::WHITE_WOOL, - DyeColor::YELLOW => Ids::YELLOW_WOOL, - })); - - $this->map(Blocks::CARPET(), fn(Carpet $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_CARPET, - DyeColor::BLUE => Ids::BLUE_CARPET, - DyeColor::BROWN => Ids::BROWN_CARPET, - DyeColor::CYAN => Ids::CYAN_CARPET, - DyeColor::GRAY => Ids::GRAY_CARPET, - DyeColor::GREEN => Ids::GREEN_CARPET, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_CARPET, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_CARPET, - DyeColor::LIME => Ids::LIME_CARPET, - DyeColor::MAGENTA => Ids::MAGENTA_CARPET, - DyeColor::ORANGE => Ids::ORANGE_CARPET, - DyeColor::PINK => Ids::PINK_CARPET, - DyeColor::PURPLE => Ids::PURPLE_CARPET, - DyeColor::RED => Ids::RED_CARPET, - DyeColor::WHITE => Ids::WHITE_CARPET, - DyeColor::YELLOW => Ids::YELLOW_CARPET, - })); - - $this->map(Blocks::DYED_SHULKER_BOX(), fn(DyedShulkerBox $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_SHULKER_BOX, - DyeColor::BLUE => Ids::BLUE_SHULKER_BOX, - DyeColor::BROWN => Ids::BROWN_SHULKER_BOX, - DyeColor::CYAN => Ids::CYAN_SHULKER_BOX, - DyeColor::GRAY => Ids::GRAY_SHULKER_BOX, - DyeColor::GREEN => Ids::GREEN_SHULKER_BOX, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_SHULKER_BOX, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_SHULKER_BOX, - DyeColor::LIME => Ids::LIME_SHULKER_BOX, - DyeColor::MAGENTA => Ids::MAGENTA_SHULKER_BOX, - DyeColor::ORANGE => Ids::ORANGE_SHULKER_BOX, - DyeColor::PINK => Ids::PINK_SHULKER_BOX, - DyeColor::PURPLE => Ids::PURPLE_SHULKER_BOX, - DyeColor::RED => Ids::RED_SHULKER_BOX, - DyeColor::WHITE => Ids::WHITE_SHULKER_BOX, - DyeColor::YELLOW => Ids::YELLOW_SHULKER_BOX, - })); - - $this->map(Blocks::CONCRETE(), fn(Concrete $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_CONCRETE, - DyeColor::BLUE => Ids::BLUE_CONCRETE, - DyeColor::BROWN => Ids::BROWN_CONCRETE, - DyeColor::CYAN => Ids::CYAN_CONCRETE, - DyeColor::GRAY => Ids::GRAY_CONCRETE, - DyeColor::GREEN => Ids::GREEN_CONCRETE, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_CONCRETE, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_CONCRETE, - DyeColor::LIME => Ids::LIME_CONCRETE, - DyeColor::MAGENTA => Ids::MAGENTA_CONCRETE, - DyeColor::ORANGE => Ids::ORANGE_CONCRETE, - DyeColor::PINK => Ids::PINK_CONCRETE, - DyeColor::PURPLE => Ids::PURPLE_CONCRETE, - DyeColor::RED => Ids::RED_CONCRETE, - DyeColor::WHITE => Ids::WHITE_CONCRETE, - DyeColor::YELLOW => Ids::YELLOW_CONCRETE, - })); - - $this->map(Blocks::CONCRETE_POWDER(), fn(ConcretePowder $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_CONCRETE_POWDER, - DyeColor::BLUE => Ids::BLUE_CONCRETE_POWDER, - DyeColor::BROWN => Ids::BROWN_CONCRETE_POWDER, - DyeColor::CYAN => Ids::CYAN_CONCRETE_POWDER, - DyeColor::GRAY => Ids::GRAY_CONCRETE_POWDER, - DyeColor::GREEN => Ids::GREEN_CONCRETE_POWDER, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_CONCRETE_POWDER, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_CONCRETE_POWDER, - DyeColor::LIME => Ids::LIME_CONCRETE_POWDER, - DyeColor::MAGENTA => Ids::MAGENTA_CONCRETE_POWDER, - DyeColor::ORANGE => Ids::ORANGE_CONCRETE_POWDER, - DyeColor::PINK => Ids::PINK_CONCRETE_POWDER, - DyeColor::PURPLE => Ids::PURPLE_CONCRETE_POWDER, - DyeColor::RED => Ids::RED_CONCRETE_POWDER, - DyeColor::WHITE => Ids::WHITE_CONCRETE_POWDER, - DyeColor::YELLOW => Ids::YELLOW_CONCRETE_POWDER, - })); - - $this->map(Blocks::STAINED_CLAY(), fn(StainedHardenedClay $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_TERRACOTTA, - DyeColor::BLUE => Ids::BLUE_TERRACOTTA, - DyeColor::BROWN => Ids::BROWN_TERRACOTTA, - DyeColor::CYAN => Ids::CYAN_TERRACOTTA, - DyeColor::GRAY => Ids::GRAY_TERRACOTTA, - DyeColor::GREEN => Ids::GREEN_TERRACOTTA, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_TERRACOTTA, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_TERRACOTTA, - DyeColor::LIME => Ids::LIME_TERRACOTTA, - DyeColor::MAGENTA => Ids::MAGENTA_TERRACOTTA, - DyeColor::ORANGE => Ids::ORANGE_TERRACOTTA, - DyeColor::PINK => Ids::PINK_TERRACOTTA, - DyeColor::PURPLE => Ids::PURPLE_TERRACOTTA, - DyeColor::RED => Ids::RED_TERRACOTTA, - DyeColor::WHITE => Ids::WHITE_TERRACOTTA, - DyeColor::YELLOW => Ids::YELLOW_TERRACOTTA, - })); - - $this->map(Blocks::STAINED_GLASS(), fn(StainedGlass $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_STAINED_GLASS, - DyeColor::BLUE => Ids::BLUE_STAINED_GLASS, - DyeColor::BROWN => Ids::BROWN_STAINED_GLASS, - DyeColor::CYAN => Ids::CYAN_STAINED_GLASS, - DyeColor::GRAY => Ids::GRAY_STAINED_GLASS, - DyeColor::GREEN => Ids::GREEN_STAINED_GLASS, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_STAINED_GLASS, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_STAINED_GLASS, - DyeColor::LIME => Ids::LIME_STAINED_GLASS, - DyeColor::MAGENTA => Ids::MAGENTA_STAINED_GLASS, - DyeColor::ORANGE => Ids::ORANGE_STAINED_GLASS, - DyeColor::PINK => Ids::PINK_STAINED_GLASS, - DyeColor::PURPLE => Ids::PURPLE_STAINED_GLASS, - DyeColor::RED => Ids::RED_STAINED_GLASS, - DyeColor::WHITE => Ids::WHITE_STAINED_GLASS, - DyeColor::YELLOW => Ids::YELLOW_STAINED_GLASS, - })); - - $this->map(Blocks::STAINED_GLASS_PANE(), fn(StainedGlassPane $block) => Writer::create(match($block->getColor()){ - DyeColor::BLACK => Ids::BLACK_STAINED_GLASS_PANE, - DyeColor::BLUE => Ids::BLUE_STAINED_GLASS_PANE, - DyeColor::BROWN => Ids::BROWN_STAINED_GLASS_PANE, - DyeColor::CYAN => Ids::CYAN_STAINED_GLASS_PANE, - DyeColor::GRAY => Ids::GRAY_STAINED_GLASS_PANE, - DyeColor::GREEN => Ids::GREEN_STAINED_GLASS_PANE, - DyeColor::LIGHT_BLUE => Ids::LIGHT_BLUE_STAINED_GLASS_PANE, - DyeColor::LIGHT_GRAY => Ids::LIGHT_GRAY_STAINED_GLASS_PANE, - DyeColor::LIME => Ids::LIME_STAINED_GLASS_PANE, - DyeColor::MAGENTA => Ids::MAGENTA_STAINED_GLASS_PANE, - DyeColor::ORANGE => Ids::ORANGE_STAINED_GLASS_PANE, - DyeColor::PINK => Ids::PINK_STAINED_GLASS_PANE, - DyeColor::PURPLE => Ids::PURPLE_STAINED_GLASS_PANE, - DyeColor::RED => Ids::RED_STAINED_GLASS_PANE, - DyeColor::WHITE => Ids::WHITE_STAINED_GLASS_PANE, - DyeColor::YELLOW => Ids::YELLOW_STAINED_GLASS_PANE, - })); - } - - private function registerFlatCoralSerializers() : void{ - $this->map(Blocks::CORAL(), fn(Coral $block) => Writer::create( - match($block->getCoralType()){ - CoralType::BRAIN => $block->isDead() ? Ids::DEAD_BRAIN_CORAL : Ids::BRAIN_CORAL, - CoralType::BUBBLE => $block->isDead() ? Ids::DEAD_BUBBLE_CORAL : Ids::BUBBLE_CORAL, - CoralType::FIRE => $block->isDead() ? Ids::DEAD_FIRE_CORAL : Ids::FIRE_CORAL, - CoralType::HORN => $block->isDead() ? Ids::DEAD_HORN_CORAL : Ids::HORN_CORAL, - CoralType::TUBE => $block->isDead() ? Ids::DEAD_TUBE_CORAL : Ids::TUBE_CORAL, - } - )); - - $this->map(Blocks::CORAL_FAN(), fn(FloorCoralFan $block) => Writer::create( - match($block->getCoralType()){ - CoralType::BRAIN => $block->isDead() ? Ids::DEAD_BRAIN_CORAL_FAN : Ids::BRAIN_CORAL_FAN, - CoralType::BUBBLE => $block->isDead() ? Ids::DEAD_BUBBLE_CORAL_FAN : Ids::BUBBLE_CORAL_FAN, - CoralType::FIRE => $block->isDead() ? Ids::DEAD_FIRE_CORAL_FAN : Ids::FIRE_CORAL_FAN, - CoralType::HORN => $block->isDead() ? Ids::DEAD_HORN_CORAL_FAN : Ids::HORN_CORAL_FAN, - CoralType::TUBE => $block->isDead() ? Ids::DEAD_TUBE_CORAL_FAN : Ids::TUBE_CORAL_FAN, - }) - ->writeInt(StateNames::CORAL_FAN_DIRECTION, match($axis = $block->getAxis()){ - Axis::X => 0, - Axis::Z => 1, - default => throw new BlockStateSerializeException("Invalid axis {$axis}"), - })); - - $this->map(Blocks::CORAL_BLOCK(), fn(CoralBlock $block) => Writer::create( - match($block->getCoralType()){ - CoralType::BRAIN => $block->isDead() ? Ids::DEAD_BRAIN_CORAL_BLOCK : Ids::BRAIN_CORAL_BLOCK, - CoralType::BUBBLE => $block->isDead() ? Ids::DEAD_BUBBLE_CORAL_BLOCK : Ids::BUBBLE_CORAL_BLOCK, - CoralType::FIRE => $block->isDead() ? Ids::DEAD_FIRE_CORAL_BLOCK : Ids::FIRE_CORAL_BLOCK, - CoralType::HORN => $block->isDead() ? Ids::DEAD_HORN_CORAL_BLOCK : Ids::HORN_CORAL_BLOCK, - CoralType::TUBE => $block->isDead() ? Ids::DEAD_TUBE_CORAL_BLOCK : Ids::TUBE_CORAL_BLOCK, - } - )); - - $this->map(Blocks::WALL_CORAL_FAN(), fn(WallCoralFan $block) => Writer::create( - match($block->getCoralType()){ - CoralType::TUBE => $block->isDead() ? Ids::DEAD_TUBE_CORAL_WALL_FAN : Ids::TUBE_CORAL_WALL_FAN, - CoralType::BRAIN => $block->isDead() ? Ids::DEAD_BRAIN_CORAL_WALL_FAN : Ids::BRAIN_CORAL_WALL_FAN, - CoralType::BUBBLE => $block->isDead() ? Ids::DEAD_BUBBLE_CORAL_WALL_FAN : Ids::BUBBLE_CORAL_WALL_FAN, - CoralType::FIRE => $block->isDead() ? Ids::DEAD_FIRE_CORAL_WALL_FAN : Ids::FIRE_CORAL_WALL_FAN, - CoralType::HORN => $block->isDead() ? Ids::DEAD_HORN_CORAL_WALL_FAN : Ids::HORN_CORAL_WALL_FAN, - }) - ->writeCoralFacing($block->getFacing()) - ); - } - - private function registerCauldronSerializers() : void{ - $this->map(Blocks::CAULDRON(), fn() => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, 0)); - $this->map(Blocks::LAVA_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_LAVA, $b->getFillLevel())); - //potion cauldrons store their real information in the block actor data - $this->map(Blocks::POTION_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, $b->getFillLevel())); - $this->map(Blocks::WATER_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, $b->getFillLevel())); - } - - private function registerFlatWoodBlockSerializers() : void{ - $this->map(Blocks::ACACIA_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::ACACIA_BUTTON))); - $this->map(Blocks::ACACIA_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::ACACIA_DOOR))); - $this->map(Blocks::ACACIA_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::ACACIA_FENCE_GATE))); - $this->map(Blocks::ACACIA_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::ACACIA_PRESSURE_PLATE))); - $this->map(Blocks::ACACIA_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::ACACIA_STANDING_SIGN))); - $this->map(Blocks::ACACIA_STAIRS(), fn(WoodenStairs $block) => Helper::encodeStairs($block, new Writer(Ids::ACACIA_STAIRS))); - $this->map(Blocks::ACACIA_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::ACACIA_TRAPDOOR))); - $this->map(Blocks::ACACIA_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::ACACIA_WALL_SIGN))); - $this->mapLog(Blocks::ACACIA_LOG(), Ids::ACACIA_LOG, Ids::STRIPPED_ACACIA_LOG); - $this->mapLog(Blocks::ACACIA_WOOD(), Ids::ACACIA_WOOD, Ids::STRIPPED_ACACIA_WOOD); - $this->mapSimple(Blocks::ACACIA_FENCE(), Ids::ACACIA_FENCE); - $this->mapSimple(Blocks::ACACIA_PLANKS(), Ids::ACACIA_PLANKS); - $this->mapSlab(Blocks::ACACIA_SLAB(), Ids::ACACIA_SLAB, Ids::ACACIA_DOUBLE_SLAB); - - $this->map(Blocks::BIRCH_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::BIRCH_BUTTON))); - $this->map(Blocks::BIRCH_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::BIRCH_DOOR))); - $this->map(Blocks::BIRCH_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::BIRCH_FENCE_GATE))); - $this->map(Blocks::BIRCH_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::BIRCH_PRESSURE_PLATE))); - $this->map(Blocks::BIRCH_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::BIRCH_STANDING_SIGN))); - $this->map(Blocks::BIRCH_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::BIRCH_TRAPDOOR))); - $this->map(Blocks::BIRCH_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::BIRCH_WALL_SIGN))); - $this->mapLog(Blocks::BIRCH_LOG(), Ids::BIRCH_LOG, Ids::STRIPPED_BIRCH_LOG); - $this->mapLog(Blocks::BIRCH_WOOD(), Ids::BIRCH_WOOD, Ids::STRIPPED_BIRCH_WOOD); - $this->mapSimple(Blocks::BIRCH_FENCE(), Ids::BIRCH_FENCE); - $this->mapSimple(Blocks::BIRCH_PLANKS(), Ids::BIRCH_PLANKS); - $this->mapSlab(Blocks::BIRCH_SLAB(), Ids::BIRCH_SLAB, Ids::BIRCH_DOUBLE_SLAB); - $this->mapStairs(Blocks::BIRCH_STAIRS(), Ids::BIRCH_STAIRS); - - $this->map(Blocks::CHERRY_BUTTON(), fn(Button $block) => Helper::encodeButton($block, new Writer(Ids::CHERRY_BUTTON))); - $this->map(Blocks::CHERRY_DOOR(), fn(Door $block) => Helper::encodeDoor($block, new Writer(Ids::CHERRY_DOOR))); - $this->map(Blocks::CHERRY_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::CHERRY_FENCE_GATE))); - $this->map(Blocks::CHERRY_LOG(), fn(Wood $block) => Helper::encodeLog($block, Ids::CHERRY_LOG, Ids::STRIPPED_CHERRY_LOG)); - $this->map(Blocks::CHERRY_PRESSURE_PLATE(), fn(SimplePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::CHERRY_PRESSURE_PLATE))); - $this->map(Blocks::CHERRY_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::CHERRY_STANDING_SIGN))); - $this->map(Blocks::CHERRY_TRAPDOOR(), fn(Trapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::CHERRY_TRAPDOOR))); - $this->map(Blocks::CHERRY_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::CHERRY_WALL_SIGN))); - $this->mapSimple(Blocks::CHERRY_FENCE(), Ids::CHERRY_FENCE); - $this->mapSimple(Blocks::CHERRY_PLANKS(), Ids::CHERRY_PLANKS); - $this->mapSlab(Blocks::CHERRY_SLAB(), Ids::CHERRY_SLAB, Ids::CHERRY_DOUBLE_SLAB); - $this->mapStairs(Blocks::CHERRY_STAIRS(), Ids::CHERRY_STAIRS); - $this->mapLog(Blocks::CHERRY_WOOD(), Ids::CHERRY_WOOD, Ids::STRIPPED_CHERRY_WOOD); - - $this->map(Blocks::CRIMSON_BUTTON(), fn(Button $block) => Helper::encodeButton($block, new Writer(Ids::CRIMSON_BUTTON))); - $this->map(Blocks::CRIMSON_DOOR(), fn(Door $block) => Helper::encodeDoor($block, new Writer(Ids::CRIMSON_DOOR))); - $this->map(Blocks::CRIMSON_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::CRIMSON_FENCE_GATE))); - $this->map(Blocks::CRIMSON_HYPHAE(), fn(Wood $block) => Helper::encodeLog($block, Ids::CRIMSON_HYPHAE, Ids::STRIPPED_CRIMSON_HYPHAE)); - $this->map(Blocks::CRIMSON_PRESSURE_PLATE(), fn(SimplePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::CRIMSON_PRESSURE_PLATE))); - $this->map(Blocks::CRIMSON_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::CRIMSON_STANDING_SIGN))); - $this->map(Blocks::CRIMSON_STEM(), fn(Wood $block) => Helper::encodeLog($block, Ids::CRIMSON_STEM, Ids::STRIPPED_CRIMSON_STEM)); - $this->map(Blocks::CRIMSON_TRAPDOOR(), fn(Trapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::CRIMSON_TRAPDOOR))); - $this->map(Blocks::CRIMSON_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::CRIMSON_WALL_SIGN))); - $this->mapSimple(Blocks::CRIMSON_FENCE(), Ids::CRIMSON_FENCE); - $this->mapSimple(Blocks::CRIMSON_PLANKS(), Ids::CRIMSON_PLANKS); - $this->mapSlab(Blocks::CRIMSON_SLAB(), Ids::CRIMSON_SLAB, Ids::CRIMSON_DOUBLE_SLAB); - $this->mapStairs(Blocks::CRIMSON_STAIRS(), Ids::CRIMSON_STAIRS); - - $this->map(Blocks::DARK_OAK_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::DARK_OAK_BUTTON))); - $this->map(Blocks::DARK_OAK_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::DARK_OAK_DOOR))); - $this->map(Blocks::DARK_OAK_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::DARK_OAK_FENCE_GATE))); - $this->map(Blocks::DARK_OAK_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::DARK_OAK_PRESSURE_PLATE))); - $this->map(Blocks::DARK_OAK_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::DARKOAK_STANDING_SIGN))); - $this->map(Blocks::DARK_OAK_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::DARK_OAK_TRAPDOOR))); - $this->map(Blocks::DARK_OAK_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::DARKOAK_WALL_SIGN))); - $this->mapLog(Blocks::DARK_OAK_LOG(), Ids::DARK_OAK_LOG, Ids::STRIPPED_DARK_OAK_LOG); - $this->mapLog(Blocks::DARK_OAK_WOOD(), Ids::DARK_OAK_WOOD, Ids::STRIPPED_DARK_OAK_WOOD); - $this->mapSimple(Blocks::DARK_OAK_FENCE(), Ids::DARK_OAK_FENCE); - $this->mapSimple(Blocks::DARK_OAK_PLANKS(), Ids::DARK_OAK_PLANKS); - $this->mapSlab(Blocks::DARK_OAK_SLAB(), Ids::DARK_OAK_SLAB, Ids::DARK_OAK_DOUBLE_SLAB); - $this->mapStairs(Blocks::DARK_OAK_STAIRS(), Ids::DARK_OAK_STAIRS); - - $this->map(Blocks::JUNGLE_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::JUNGLE_BUTTON))); - $this->map(Blocks::JUNGLE_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::JUNGLE_DOOR))); - $this->map(Blocks::JUNGLE_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::JUNGLE_FENCE_GATE))); - $this->map(Blocks::JUNGLE_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::JUNGLE_PRESSURE_PLATE))); - $this->map(Blocks::JUNGLE_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::JUNGLE_STANDING_SIGN))); - $this->map(Blocks::JUNGLE_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::JUNGLE_TRAPDOOR))); - $this->map(Blocks::JUNGLE_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::JUNGLE_WALL_SIGN))); - $this->mapLog(Blocks::JUNGLE_LOG(), Ids::JUNGLE_LOG, Ids::STRIPPED_JUNGLE_LOG); - $this->mapLog(Blocks::JUNGLE_WOOD(), Ids::JUNGLE_WOOD, Ids::STRIPPED_JUNGLE_WOOD); - $this->mapSimple(Blocks::JUNGLE_FENCE(), Ids::JUNGLE_FENCE); - $this->mapSimple(Blocks::JUNGLE_PLANKS(), Ids::JUNGLE_PLANKS); - $this->mapSlab(Blocks::JUNGLE_SLAB(), Ids::JUNGLE_SLAB, Ids::JUNGLE_DOUBLE_SLAB); - $this->mapStairs(Blocks::JUNGLE_STAIRS(), Ids::JUNGLE_STAIRS); - - $this->map(Blocks::MANGROVE_BUTTON(), fn(Button $block) => Helper::encodeButton($block, new Writer(Ids::MANGROVE_BUTTON))); - $this->map(Blocks::MANGROVE_DOOR(), fn(Door $block) => Helper::encodeDoor($block, new Writer(Ids::MANGROVE_DOOR))); - $this->map(Blocks::MANGROVE_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::MANGROVE_FENCE_GATE))); - $this->map(Blocks::MANGROVE_LOG(), fn(Wood $block) => Helper::encodeLog($block, Ids::MANGROVE_LOG, Ids::STRIPPED_MANGROVE_LOG)); - $this->map(Blocks::MANGROVE_PRESSURE_PLATE(), fn(SimplePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::MANGROVE_PRESSURE_PLATE))); - $this->map(Blocks::MANGROVE_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::MANGROVE_STANDING_SIGN))); - $this->map(Blocks::MANGROVE_TRAPDOOR(), fn(Trapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::MANGROVE_TRAPDOOR))); - $this->map(Blocks::MANGROVE_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::MANGROVE_WALL_SIGN))); - $this->mapSimple(Blocks::MANGROVE_FENCE(), Ids::MANGROVE_FENCE); - $this->mapSimple(Blocks::MANGROVE_PLANKS(), Ids::MANGROVE_PLANKS); - $this->mapSlab(Blocks::MANGROVE_SLAB(), Ids::MANGROVE_SLAB, Ids::MANGROVE_DOUBLE_SLAB); - $this->mapStairs(Blocks::MANGROVE_STAIRS(), Ids::MANGROVE_STAIRS); - $this->mapLog(Blocks::MANGROVE_WOOD(), Ids::MANGROVE_WOOD, Ids::STRIPPED_MANGROVE_WOOD); - - $this->map(Blocks::OAK_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::WOODEN_BUTTON))); - $this->map(Blocks::OAK_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::WOODEN_DOOR))); - $this->map(Blocks::OAK_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::FENCE_GATE))); - $this->map(Blocks::OAK_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::WOODEN_PRESSURE_PLATE))); - $this->map(Blocks::OAK_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::STANDING_SIGN))); - $this->map(Blocks::OAK_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::TRAPDOOR))); - $this->map(Blocks::OAK_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::WALL_SIGN))); - $this->mapLog(Blocks::OAK_LOG(), Ids::OAK_LOG, Ids::STRIPPED_OAK_LOG); - $this->mapLog(Blocks::OAK_WOOD(), Ids::OAK_WOOD, Ids::STRIPPED_OAK_WOOD); - $this->mapSimple(Blocks::OAK_FENCE(), Ids::OAK_FENCE); - $this->mapSimple(Blocks::OAK_PLANKS(), Ids::OAK_PLANKS); - $this->mapSlab(Blocks::OAK_SLAB(), Ids::OAK_SLAB, Ids::OAK_DOUBLE_SLAB); - $this->mapStairs(Blocks::OAK_STAIRS(), Ids::OAK_STAIRS); - - $this->map(Blocks::SPRUCE_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::SPRUCE_BUTTON))); - $this->map(Blocks::SPRUCE_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::SPRUCE_DOOR))); - $this->map(Blocks::SPRUCE_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::SPRUCE_FENCE_GATE))); - $this->map(Blocks::SPRUCE_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::SPRUCE_PRESSURE_PLATE))); - $this->map(Blocks::SPRUCE_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::SPRUCE_STANDING_SIGN))); - $this->map(Blocks::SPRUCE_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::SPRUCE_TRAPDOOR))); - $this->map(Blocks::SPRUCE_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::SPRUCE_WALL_SIGN))); - $this->mapLog(Blocks::SPRUCE_LOG(), Ids::SPRUCE_LOG, Ids::STRIPPED_SPRUCE_LOG); - $this->mapLog(Blocks::SPRUCE_WOOD(), Ids::SPRUCE_WOOD, Ids::STRIPPED_SPRUCE_WOOD); - $this->mapSimple(Blocks::SPRUCE_FENCE(), Ids::SPRUCE_FENCE); - $this->mapSimple(Blocks::SPRUCE_PLANKS(), Ids::SPRUCE_PLANKS); - $this->mapSlab(Blocks::SPRUCE_SLAB(), Ids::SPRUCE_SLAB, Ids::SPRUCE_DOUBLE_SLAB); - $this->mapStairs(Blocks::SPRUCE_STAIRS(), Ids::SPRUCE_STAIRS); - //wood and slabs still use the old way of storing wood type - - $this->map(Blocks::WARPED_BUTTON(), fn(Button $block) => Helper::encodeButton($block, new Writer(Ids::WARPED_BUTTON))); - $this->map(Blocks::WARPED_DOOR(), fn(Door $block) => Helper::encodeDoor($block, new Writer(Ids::WARPED_DOOR))); - $this->map(Blocks::WARPED_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::WARPED_FENCE_GATE))); - $this->map(Blocks::WARPED_HYPHAE(), fn(Wood $block) => Helper::encodeLog($block, Ids::WARPED_HYPHAE, Ids::STRIPPED_WARPED_HYPHAE)); - $this->map(Blocks::WARPED_PRESSURE_PLATE(), fn(SimplePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::WARPED_PRESSURE_PLATE))); - $this->map(Blocks::WARPED_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::WARPED_STANDING_SIGN))); - $this->map(Blocks::WARPED_STEM(), fn(Wood $block) => Helper::encodeLog($block, Ids::WARPED_STEM, Ids::STRIPPED_WARPED_STEM)); - $this->map(Blocks::WARPED_TRAPDOOR(), fn(Trapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::WARPED_TRAPDOOR))); - $this->map(Blocks::WARPED_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::WARPED_WALL_SIGN))); - $this->mapSimple(Blocks::WARPED_FENCE(), Ids::WARPED_FENCE); - $this->mapSimple(Blocks::WARPED_PLANKS(), Ids::WARPED_PLANKS); - $this->mapSlab(Blocks::WARPED_SLAB(), Ids::WARPED_SLAB, Ids::WARPED_DOUBLE_SLAB); - $this->mapStairs(Blocks::WARPED_STAIRS(), Ids::WARPED_STAIRS); - } - - private function registerLeavesSerializers() : void{ - //flattened IDs - $this->map(Blocks::AZALEA_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::AZALEA_LEAVES))); - $this->map(Blocks::CHERRY_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::CHERRY_LEAVES))); - $this->map(Blocks::FLOWERING_AZALEA_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::AZALEA_LEAVES_FLOWERED))); - $this->map(Blocks::MANGROVE_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::MANGROVE_LEAVES))); - - //legacy mess - $this->map(Blocks::ACACIA_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::ACACIA_LEAVES))); - $this->map(Blocks::BIRCH_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::BIRCH_LEAVES))); - $this->map(Blocks::DARK_OAK_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::DARK_OAK_LEAVES))); - $this->map(Blocks::JUNGLE_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::JUNGLE_LEAVES))); - $this->map(Blocks::OAK_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::OAK_LEAVES))); - $this->map(Blocks::SPRUCE_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::SPRUCE_LEAVES))); - } - - private function registerSaplingSerializers() : void{ - foreach([ - Ids::ACACIA_SAPLING => Blocks::ACACIA_SAPLING(), - Ids::BIRCH_SAPLING => Blocks::BIRCH_SAPLING(), - Ids::DARK_OAK_SAPLING => Blocks::DARK_OAK_SAPLING(), - Ids::JUNGLE_SAPLING => Blocks::JUNGLE_SAPLING(), - Ids::OAK_SAPLING => Blocks::OAK_SAPLING(), - Ids::SPRUCE_SAPLING => Blocks::SPRUCE_SAPLING(), - ] as $id => $block){ - $this->map($block, fn(Sapling $block) => Helper::encodeSapling($block, new Writer($id))); - } - } - - private function registerMobHeadSerializers() : void{ - $this->map(Blocks::MOB_HEAD(), fn(MobHead $block) => Writer::create(match ($block->getMobHeadType()){ - MobHeadType::CREEPER => Ids::CREEPER_HEAD, - MobHeadType::DRAGON => Ids::DRAGON_HEAD, - MobHeadType::PIGLIN => Ids::PIGLIN_HEAD, - MobHeadType::PLAYER => Ids::PLAYER_HEAD, - MobHeadType::SKELETON => Ids::SKELETON_SKULL, - MobHeadType::WITHER_SKELETON => Ids::WITHER_SKELETON_SKULL, - MobHeadType::ZOMBIE => Ids::ZOMBIE_HEAD, - })->writeFacingWithoutDown($block->getFacing())); - } - - private function registerSimpleSerializers() : void{ - $this->mapSimple(Blocks::AIR(), Ids::AIR); - $this->mapSimple(Blocks::AMETHYST(), Ids::AMETHYST_BLOCK); - $this->mapSimple(Blocks::ANCIENT_DEBRIS(), Ids::ANCIENT_DEBRIS); - $this->mapSimple(Blocks::ANDESITE(), Ids::ANDESITE); - $this->mapSimple(Blocks::BARRIER(), Ids::BARRIER); - $this->mapSimple(Blocks::BEACON(), Ids::BEACON); - $this->mapSimple(Blocks::BLACKSTONE(), Ids::BLACKSTONE); - $this->mapSimple(Blocks::BLUE_ICE(), Ids::BLUE_ICE); - $this->mapSimple(Blocks::BOOKSHELF(), Ids::BOOKSHELF); - $this->mapSimple(Blocks::BRICKS(), Ids::BRICK_BLOCK); - $this->mapSimple(Blocks::BROWN_MUSHROOM(), Ids::BROWN_MUSHROOM); - $this->mapSimple(Blocks::BUDDING_AMETHYST(), Ids::BUDDING_AMETHYST); - $this->mapSimple(Blocks::CALCITE(), Ids::CALCITE); - $this->mapSimple(Blocks::CARTOGRAPHY_TABLE(), Ids::CARTOGRAPHY_TABLE); - $this->mapSimple(Blocks::CHEMICAL_HEAT(), Ids::CHEMICAL_HEAT); - $this->mapSimple(Blocks::CHISELED_DEEPSLATE(), Ids::CHISELED_DEEPSLATE); - $this->mapSimple(Blocks::CHISELED_NETHER_BRICKS(), Ids::CHISELED_NETHER_BRICKS); - $this->mapSimple(Blocks::CHISELED_POLISHED_BLACKSTONE(), Ids::CHISELED_POLISHED_BLACKSTONE); - $this->mapSimple(Blocks::CHISELED_RED_SANDSTONE(), Ids::CHISELED_RED_SANDSTONE); - $this->mapSimple(Blocks::CHISELED_SANDSTONE(), Ids::CHISELED_SANDSTONE); - $this->mapSimple(Blocks::CHISELED_STONE_BRICKS(), Ids::CHISELED_STONE_BRICKS); - $this->mapSimple(Blocks::CHISELED_TUFF(), Ids::CHISELED_TUFF); - $this->mapSimple(Blocks::CHISELED_TUFF_BRICKS(), Ids::CHISELED_TUFF_BRICKS); - $this->mapSimple(Blocks::CHORUS_PLANT(), Ids::CHORUS_PLANT); - $this->mapSimple(Blocks::CLAY(), Ids::CLAY); - $this->mapSimple(Blocks::COAL(), Ids::COAL_BLOCK); - $this->mapSimple(Blocks::COAL_ORE(), Ids::COAL_ORE); - $this->mapSimple(Blocks::COBBLED_DEEPSLATE(), Ids::COBBLED_DEEPSLATE); - $this->mapSimple(Blocks::COBBLESTONE(), Ids::COBBLESTONE); - $this->mapSimple(Blocks::COBWEB(), Ids::WEB); - $this->mapSimple(Blocks::COPPER_ORE(), Ids::COPPER_ORE); - $this->mapSimple(Blocks::CRACKED_DEEPSLATE_BRICKS(), Ids::CRACKED_DEEPSLATE_BRICKS); - $this->mapSimple(Blocks::CRACKED_DEEPSLATE_TILES(), Ids::CRACKED_DEEPSLATE_TILES); - $this->mapSimple(Blocks::CRACKED_NETHER_BRICKS(), Ids::CRACKED_NETHER_BRICKS); - $this->mapSimple(Blocks::CRACKED_POLISHED_BLACKSTONE_BRICKS(), Ids::CRACKED_POLISHED_BLACKSTONE_BRICKS); - $this->mapSimple(Blocks::CRACKED_STONE_BRICKS(), Ids::CRACKED_STONE_BRICKS); - $this->mapSimple(Blocks::CRAFTING_TABLE(), Ids::CRAFTING_TABLE); - $this->mapSimple(Blocks::CRIMSON_ROOTS(), Ids::CRIMSON_ROOTS); - $this->mapSimple(Blocks::CRYING_OBSIDIAN(), Ids::CRYING_OBSIDIAN); - $this->mapSimple(Blocks::DANDELION(), Ids::DANDELION); - $this->mapSimple(Blocks::CUT_RED_SANDSTONE(), Ids::CUT_RED_SANDSTONE); - $this->mapSimple(Blocks::CUT_SANDSTONE(), Ids::CUT_SANDSTONE); - $this->mapSimple(Blocks::DARK_PRISMARINE(), Ids::DARK_PRISMARINE); - $this->mapSimple(Blocks::DEAD_BUSH(), Ids::DEADBUSH); - $this->mapSimple(Blocks::DEEPSLATE_BRICKS(), Ids::DEEPSLATE_BRICKS); - $this->mapSimple(Blocks::DEEPSLATE_COAL_ORE(), Ids::DEEPSLATE_COAL_ORE); - $this->mapSimple(Blocks::DEEPSLATE_COPPER_ORE(), Ids::DEEPSLATE_COPPER_ORE); - $this->mapSimple(Blocks::DEEPSLATE_DIAMOND_ORE(), Ids::DEEPSLATE_DIAMOND_ORE); - $this->mapSimple(Blocks::DEEPSLATE_EMERALD_ORE(), Ids::DEEPSLATE_EMERALD_ORE); - $this->mapSimple(Blocks::DEEPSLATE_GOLD_ORE(), Ids::DEEPSLATE_GOLD_ORE); - $this->mapSimple(Blocks::DEEPSLATE_IRON_ORE(), Ids::DEEPSLATE_IRON_ORE); - $this->mapSimple(Blocks::DEEPSLATE_LAPIS_LAZULI_ORE(), Ids::DEEPSLATE_LAPIS_ORE); - $this->mapSimple(Blocks::DEEPSLATE_TILES(), Ids::DEEPSLATE_TILES); - $this->mapSimple(Blocks::DIAMOND(), Ids::DIAMOND_BLOCK); - $this->mapSimple(Blocks::DIAMOND_ORE(), Ids::DIAMOND_ORE); - $this->mapSimple(Blocks::DIORITE(), Ids::DIORITE); - $this->mapSimple(Blocks::DRAGON_EGG(), Ids::DRAGON_EGG); - $this->mapSimple(Blocks::DRIED_KELP(), Ids::DRIED_KELP_BLOCK); - $this->mapSimple(Blocks::ELEMENT_ACTINIUM(), Ids::ELEMENT_89); - $this->mapSimple(Blocks::ELEMENT_ALUMINUM(), Ids::ELEMENT_13); - $this->mapSimple(Blocks::ELEMENT_AMERICIUM(), Ids::ELEMENT_95); - $this->mapSimple(Blocks::ELEMENT_ANTIMONY(), Ids::ELEMENT_51); - $this->mapSimple(Blocks::ELEMENT_ARGON(), Ids::ELEMENT_18); - $this->mapSimple(Blocks::ELEMENT_ARSENIC(), Ids::ELEMENT_33); - $this->mapSimple(Blocks::ELEMENT_ASTATINE(), Ids::ELEMENT_85); - $this->mapSimple(Blocks::ELEMENT_BARIUM(), Ids::ELEMENT_56); - $this->mapSimple(Blocks::ELEMENT_BERKELIUM(), Ids::ELEMENT_97); - $this->mapSimple(Blocks::ELEMENT_BERYLLIUM(), Ids::ELEMENT_4); - $this->mapSimple(Blocks::ELEMENT_BISMUTH(), Ids::ELEMENT_83); - $this->mapSimple(Blocks::ELEMENT_BOHRIUM(), Ids::ELEMENT_107); - $this->mapSimple(Blocks::ELEMENT_BORON(), Ids::ELEMENT_5); - $this->mapSimple(Blocks::ELEMENT_BROMINE(), Ids::ELEMENT_35); - $this->mapSimple(Blocks::ELEMENT_CADMIUM(), Ids::ELEMENT_48); - $this->mapSimple(Blocks::ELEMENT_CALCIUM(), Ids::ELEMENT_20); - $this->mapSimple(Blocks::ELEMENT_CALIFORNIUM(), Ids::ELEMENT_98); - $this->mapSimple(Blocks::ELEMENT_CARBON(), Ids::ELEMENT_6); - $this->mapSimple(Blocks::ELEMENT_CERIUM(), Ids::ELEMENT_58); - $this->mapSimple(Blocks::ELEMENT_CESIUM(), Ids::ELEMENT_55); - $this->mapSimple(Blocks::ELEMENT_CHLORINE(), Ids::ELEMENT_17); - $this->mapSimple(Blocks::ELEMENT_CHROMIUM(), Ids::ELEMENT_24); - $this->mapSimple(Blocks::ELEMENT_COBALT(), Ids::ELEMENT_27); - $this->mapSimple(Blocks::ELEMENT_COPERNICIUM(), Ids::ELEMENT_112); - $this->mapSimple(Blocks::ELEMENT_COPPER(), Ids::ELEMENT_29); - $this->mapSimple(Blocks::ELEMENT_CURIUM(), Ids::ELEMENT_96); - $this->mapSimple(Blocks::ELEMENT_DARMSTADTIUM(), Ids::ELEMENT_110); - $this->mapSimple(Blocks::ELEMENT_DUBNIUM(), Ids::ELEMENT_105); - $this->mapSimple(Blocks::ELEMENT_DYSPROSIUM(), Ids::ELEMENT_66); - $this->mapSimple(Blocks::ELEMENT_EINSTEINIUM(), Ids::ELEMENT_99); - $this->mapSimple(Blocks::ELEMENT_ERBIUM(), Ids::ELEMENT_68); - $this->mapSimple(Blocks::ELEMENT_EUROPIUM(), Ids::ELEMENT_63); - $this->mapSimple(Blocks::ELEMENT_FERMIUM(), Ids::ELEMENT_100); - $this->mapSimple(Blocks::ELEMENT_FLEROVIUM(), Ids::ELEMENT_114); - $this->mapSimple(Blocks::ELEMENT_FLUORINE(), Ids::ELEMENT_9); - $this->mapSimple(Blocks::ELEMENT_FRANCIUM(), Ids::ELEMENT_87); - $this->mapSimple(Blocks::ELEMENT_GADOLINIUM(), Ids::ELEMENT_64); - $this->mapSimple(Blocks::ELEMENT_GALLIUM(), Ids::ELEMENT_31); - $this->mapSimple(Blocks::ELEMENT_GERMANIUM(), Ids::ELEMENT_32); - $this->mapSimple(Blocks::ELEMENT_GOLD(), Ids::ELEMENT_79); - $this->mapSimple(Blocks::ELEMENT_HAFNIUM(), Ids::ELEMENT_72); - $this->mapSimple(Blocks::ELEMENT_HASSIUM(), Ids::ELEMENT_108); - $this->mapSimple(Blocks::ELEMENT_HELIUM(), Ids::ELEMENT_2); - $this->mapSimple(Blocks::ELEMENT_HOLMIUM(), Ids::ELEMENT_67); - $this->mapSimple(Blocks::ELEMENT_HYDROGEN(), Ids::ELEMENT_1); - $this->mapSimple(Blocks::ELEMENT_INDIUM(), Ids::ELEMENT_49); - $this->mapSimple(Blocks::ELEMENT_IODINE(), Ids::ELEMENT_53); - $this->mapSimple(Blocks::ELEMENT_IRIDIUM(), Ids::ELEMENT_77); - $this->mapSimple(Blocks::ELEMENT_IRON(), Ids::ELEMENT_26); - $this->mapSimple(Blocks::ELEMENT_KRYPTON(), Ids::ELEMENT_36); - $this->mapSimple(Blocks::ELEMENT_LANTHANUM(), Ids::ELEMENT_57); - $this->mapSimple(Blocks::ELEMENT_LAWRENCIUM(), Ids::ELEMENT_103); - $this->mapSimple(Blocks::ELEMENT_LEAD(), Ids::ELEMENT_82); - $this->mapSimple(Blocks::ELEMENT_LITHIUM(), Ids::ELEMENT_3); - $this->mapSimple(Blocks::ELEMENT_LIVERMORIUM(), Ids::ELEMENT_116); - $this->mapSimple(Blocks::ELEMENT_LUTETIUM(), Ids::ELEMENT_71); - $this->mapSimple(Blocks::ELEMENT_MAGNESIUM(), Ids::ELEMENT_12); - $this->mapSimple(Blocks::ELEMENT_MANGANESE(), Ids::ELEMENT_25); - $this->mapSimple(Blocks::ELEMENT_MEITNERIUM(), Ids::ELEMENT_109); - $this->mapSimple(Blocks::ELEMENT_MENDELEVIUM(), Ids::ELEMENT_101); - $this->mapSimple(Blocks::ELEMENT_MERCURY(), Ids::ELEMENT_80); - $this->mapSimple(Blocks::ELEMENT_MOLYBDENUM(), Ids::ELEMENT_42); - $this->mapSimple(Blocks::ELEMENT_MOSCOVIUM(), Ids::ELEMENT_115); - $this->mapSimple(Blocks::ELEMENT_NEODYMIUM(), Ids::ELEMENT_60); - $this->mapSimple(Blocks::ELEMENT_NEON(), Ids::ELEMENT_10); - $this->mapSimple(Blocks::ELEMENT_NEPTUNIUM(), Ids::ELEMENT_93); - $this->mapSimple(Blocks::ELEMENT_NICKEL(), Ids::ELEMENT_28); - $this->mapSimple(Blocks::ELEMENT_NIHONIUM(), Ids::ELEMENT_113); - $this->mapSimple(Blocks::ELEMENT_NIOBIUM(), Ids::ELEMENT_41); - $this->mapSimple(Blocks::ELEMENT_NITROGEN(), Ids::ELEMENT_7); - $this->mapSimple(Blocks::ELEMENT_NOBELIUM(), Ids::ELEMENT_102); - $this->mapSimple(Blocks::ELEMENT_OGANESSON(), Ids::ELEMENT_118); - $this->mapSimple(Blocks::ELEMENT_OSMIUM(), Ids::ELEMENT_76); - $this->mapSimple(Blocks::ELEMENT_OXYGEN(), Ids::ELEMENT_8); - $this->mapSimple(Blocks::ELEMENT_PALLADIUM(), Ids::ELEMENT_46); - $this->mapSimple(Blocks::ELEMENT_PHOSPHORUS(), Ids::ELEMENT_15); - $this->mapSimple(Blocks::ELEMENT_PLATINUM(), Ids::ELEMENT_78); - $this->mapSimple(Blocks::ELEMENT_PLUTONIUM(), Ids::ELEMENT_94); - $this->mapSimple(Blocks::ELEMENT_POLONIUM(), Ids::ELEMENT_84); - $this->mapSimple(Blocks::ELEMENT_POTASSIUM(), Ids::ELEMENT_19); - $this->mapSimple(Blocks::ELEMENT_PRASEODYMIUM(), Ids::ELEMENT_59); - $this->mapSimple(Blocks::ELEMENT_PROMETHIUM(), Ids::ELEMENT_61); - $this->mapSimple(Blocks::ELEMENT_PROTACTINIUM(), Ids::ELEMENT_91); - $this->mapSimple(Blocks::ELEMENT_RADIUM(), Ids::ELEMENT_88); - $this->mapSimple(Blocks::ELEMENT_RADON(), Ids::ELEMENT_86); - $this->mapSimple(Blocks::ELEMENT_RHENIUM(), Ids::ELEMENT_75); - $this->mapSimple(Blocks::ELEMENT_RHODIUM(), Ids::ELEMENT_45); - $this->mapSimple(Blocks::ELEMENT_ROENTGENIUM(), Ids::ELEMENT_111); - $this->mapSimple(Blocks::ELEMENT_RUBIDIUM(), Ids::ELEMENT_37); - $this->mapSimple(Blocks::ELEMENT_RUTHENIUM(), Ids::ELEMENT_44); - $this->mapSimple(Blocks::ELEMENT_RUTHERFORDIUM(), Ids::ELEMENT_104); - $this->mapSimple(Blocks::ELEMENT_SAMARIUM(), Ids::ELEMENT_62); - $this->mapSimple(Blocks::ELEMENT_SCANDIUM(), Ids::ELEMENT_21); - $this->mapSimple(Blocks::ELEMENT_SEABORGIUM(), Ids::ELEMENT_106); - $this->mapSimple(Blocks::ELEMENT_SELENIUM(), Ids::ELEMENT_34); - $this->mapSimple(Blocks::ELEMENT_SILICON(), Ids::ELEMENT_14); - $this->mapSimple(Blocks::ELEMENT_SILVER(), Ids::ELEMENT_47); - $this->mapSimple(Blocks::ELEMENT_SODIUM(), Ids::ELEMENT_11); - $this->mapSimple(Blocks::ELEMENT_STRONTIUM(), Ids::ELEMENT_38); - $this->mapSimple(Blocks::ELEMENT_SULFUR(), Ids::ELEMENT_16); - $this->mapSimple(Blocks::ELEMENT_TANTALUM(), Ids::ELEMENT_73); - $this->mapSimple(Blocks::ELEMENT_TECHNETIUM(), Ids::ELEMENT_43); - $this->mapSimple(Blocks::ELEMENT_TELLURIUM(), Ids::ELEMENT_52); - $this->mapSimple(Blocks::ELEMENT_TENNESSINE(), Ids::ELEMENT_117); - $this->mapSimple(Blocks::ELEMENT_TERBIUM(), Ids::ELEMENT_65); - $this->mapSimple(Blocks::ELEMENT_THALLIUM(), Ids::ELEMENT_81); - $this->mapSimple(Blocks::ELEMENT_THORIUM(), Ids::ELEMENT_90); - $this->mapSimple(Blocks::ELEMENT_THULIUM(), Ids::ELEMENT_69); - $this->mapSimple(Blocks::ELEMENT_TIN(), Ids::ELEMENT_50); - $this->mapSimple(Blocks::ELEMENT_TITANIUM(), Ids::ELEMENT_22); - $this->mapSimple(Blocks::ELEMENT_TUNGSTEN(), Ids::ELEMENT_74); - $this->mapSimple(Blocks::ELEMENT_URANIUM(), Ids::ELEMENT_92); - $this->mapSimple(Blocks::ELEMENT_VANADIUM(), Ids::ELEMENT_23); - $this->mapSimple(Blocks::ELEMENT_XENON(), Ids::ELEMENT_54); - $this->mapSimple(Blocks::ELEMENT_YTTERBIUM(), Ids::ELEMENT_70); - $this->mapSimple(Blocks::ELEMENT_YTTRIUM(), Ids::ELEMENT_39); - $this->mapSimple(Blocks::ELEMENT_ZERO(), Ids::ELEMENT_0); - $this->mapSimple(Blocks::ELEMENT_ZINC(), Ids::ELEMENT_30); - $this->mapSimple(Blocks::ELEMENT_ZIRCONIUM(), Ids::ELEMENT_40); - $this->mapSimple(Blocks::EMERALD(), Ids::EMERALD_BLOCK); - $this->mapSimple(Blocks::EMERALD_ORE(), Ids::EMERALD_ORE); - $this->mapSimple(Blocks::ENCHANTING_TABLE(), Ids::ENCHANTING_TABLE); - $this->mapSimple(Blocks::END_STONE(), Ids::END_STONE); - $this->mapSimple(Blocks::END_STONE_BRICKS(), Ids::END_BRICKS); - $this->mapSimple(Blocks::FERN(), Ids::FERN); - $this->mapSimple(Blocks::FLETCHING_TABLE(), Ids::FLETCHING_TABLE); - $this->mapSimple(Blocks::GILDED_BLACKSTONE(), Ids::GILDED_BLACKSTONE); - $this->mapSimple(Blocks::GLASS(), Ids::GLASS); - $this->mapSimple(Blocks::GLASS_PANE(), Ids::GLASS_PANE); - $this->mapSimple(Blocks::GLOWING_OBSIDIAN(), Ids::GLOWINGOBSIDIAN); - $this->mapSimple(Blocks::GLOWSTONE(), Ids::GLOWSTONE); - $this->mapSimple(Blocks::GOLD(), Ids::GOLD_BLOCK); - $this->mapSimple(Blocks::GOLD_ORE(), Ids::GOLD_ORE); - $this->mapSimple(Blocks::GRANITE(), Ids::GRANITE); - $this->mapSimple(Blocks::GRASS(), Ids::GRASS_BLOCK); - $this->mapSimple(Blocks::GRASS_PATH(), Ids::GRASS_PATH); - $this->mapSimple(Blocks::GRAVEL(), Ids::GRAVEL); - $this->mapSimple(Blocks::HANGING_ROOTS(), Ids::HANGING_ROOTS); - $this->mapSimple(Blocks::HARDENED_CLAY(), Ids::HARDENED_CLAY); - $this->mapSimple(Blocks::HARDENED_GLASS(), Ids::HARD_GLASS); - $this->mapSimple(Blocks::HARDENED_GLASS_PANE(), Ids::HARD_GLASS_PANE); - $this->mapSimple(Blocks::HONEYCOMB(), Ids::HONEYCOMB_BLOCK); - $this->mapSimple(Blocks::ICE(), Ids::ICE); - $this->mapSimple(Blocks::INFESTED_CHISELED_STONE_BRICK(), Ids::INFESTED_CHISELED_STONE_BRICKS); - $this->mapSimple(Blocks::INFESTED_COBBLESTONE(), Ids::INFESTED_COBBLESTONE); - $this->mapSimple(Blocks::INFESTED_CRACKED_STONE_BRICK(), Ids::INFESTED_CRACKED_STONE_BRICKS); - $this->mapSimple(Blocks::INFESTED_MOSSY_STONE_BRICK(), Ids::INFESTED_MOSSY_STONE_BRICKS); - $this->mapSimple(Blocks::INFESTED_STONE(), Ids::INFESTED_STONE); - $this->mapSimple(Blocks::INFESTED_STONE_BRICK(), Ids::INFESTED_STONE_BRICKS); - $this->mapSimple(Blocks::INFO_UPDATE(), Ids::INFO_UPDATE); - $this->mapSimple(Blocks::INFO_UPDATE2(), Ids::INFO_UPDATE2); - $this->mapSimple(Blocks::INVISIBLE_BEDROCK(), Ids::INVISIBLE_BEDROCK); - $this->mapSimple(Blocks::IRON(), Ids::IRON_BLOCK); - $this->mapSimple(Blocks::IRON_BARS(), Ids::IRON_BARS); - $this->mapSimple(Blocks::IRON_ORE(), Ids::IRON_ORE); - $this->mapSimple(Blocks::JUKEBOX(), Ids::JUKEBOX); - $this->mapSimple(Blocks::LAPIS_LAZULI(), Ids::LAPIS_BLOCK); - $this->mapSimple(Blocks::LAPIS_LAZULI_ORE(), Ids::LAPIS_ORE); - $this->mapSimple(Blocks::LEGACY_STONECUTTER(), Ids::STONECUTTER); - $this->mapSimple(Blocks::LILY_PAD(), Ids::WATERLILY); - $this->mapSimple(Blocks::MAGMA(), Ids::MAGMA); - $this->mapSimple(Blocks::MANGROVE_ROOTS(), Ids::MANGROVE_ROOTS); - $this->mapSimple(Blocks::MELON(), Ids::MELON_BLOCK); - $this->mapSimple(Blocks::MONSTER_SPAWNER(), Ids::MOB_SPAWNER); - $this->mapSimple(Blocks::MOSSY_COBBLESTONE(), Ids::MOSSY_COBBLESTONE); - $this->mapSimple(Blocks::MOSSY_STONE_BRICKS(), Ids::MOSSY_STONE_BRICKS); - $this->mapSimple(Blocks::MUD(), Ids::MUD); - $this->mapSimple(Blocks::MUD_BRICKS(), Ids::MUD_BRICKS); - $this->mapSimple(Blocks::MYCELIUM(), Ids::MYCELIUM); - $this->mapSimple(Blocks::NETHERITE(), Ids::NETHERITE_BLOCK); - $this->mapSimple(Blocks::NETHERRACK(), Ids::NETHERRACK); - $this->mapSimple(Blocks::NETHER_BRICKS(), Ids::NETHER_BRICK); - $this->mapSimple(Blocks::NETHER_BRICK_FENCE(), Ids::NETHER_BRICK_FENCE); - $this->mapSimple(Blocks::NETHER_GOLD_ORE(), Ids::NETHER_GOLD_ORE); - $this->mapSimple(Blocks::NETHER_QUARTZ_ORE(), Ids::QUARTZ_ORE); - $this->mapSimple(Blocks::NETHER_REACTOR_CORE(), Ids::NETHERREACTOR); - $this->mapSimple(Blocks::NETHER_WART_BLOCK(), Ids::NETHER_WART_BLOCK); - $this->mapSimple(Blocks::NOTE_BLOCK(), Ids::NOTEBLOCK); - $this->mapSimple(Blocks::OBSIDIAN(), Ids::OBSIDIAN); - $this->mapSimple(Blocks::PACKED_ICE(), Ids::PACKED_ICE); - $this->mapSimple(Blocks::PACKED_MUD(), Ids::PACKED_MUD); - $this->mapSimple(Blocks::PODZOL(), Ids::PODZOL); - $this->mapSimple(Blocks::POLISHED_ANDESITE(), Ids::POLISHED_ANDESITE); - $this->mapSimple(Blocks::POLISHED_BLACKSTONE(), Ids::POLISHED_BLACKSTONE); - $this->mapSimple(Blocks::POLISHED_BLACKSTONE_BRICKS(), Ids::POLISHED_BLACKSTONE_BRICKS); - $this->mapSimple(Blocks::POLISHED_DEEPSLATE(), Ids::POLISHED_DEEPSLATE); - $this->mapSimple(Blocks::POLISHED_DIORITE(), Ids::POLISHED_DIORITE); - $this->mapSimple(Blocks::POLISHED_GRANITE(), Ids::POLISHED_GRANITE); - $this->mapSimple(Blocks::POLISHED_TUFF(), Ids::POLISHED_TUFF); - $this->mapSimple(Blocks::PRISMARINE(), Ids::PRISMARINE); - $this->mapSimple(Blocks::PRISMARINE_BRICKS(), Ids::PRISMARINE_BRICKS); - $this->mapSimple(Blocks::QUARTZ_BRICKS(), Ids::QUARTZ_BRICKS); - $this->mapSimple(Blocks::RAW_COPPER(), Ids::RAW_COPPER_BLOCK); - $this->mapSimple(Blocks::RAW_GOLD(), Ids::RAW_GOLD_BLOCK); - $this->mapSimple(Blocks::RAW_IRON(), Ids::RAW_IRON_BLOCK); - $this->mapSimple(Blocks::REDSTONE(), Ids::REDSTONE_BLOCK); - $this->mapSimple(Blocks::RED_MUSHROOM(), Ids::RED_MUSHROOM); - $this->mapSimple(Blocks::RED_NETHER_BRICKS(), Ids::RED_NETHER_BRICK); - $this->mapSimple(Blocks::RED_SAND(), Ids::RED_SAND); - $this->mapSimple(Blocks::RED_SANDSTONE(), Ids::RED_SANDSTONE); - $this->mapSimple(Blocks::REINFORCED_DEEPSLATE(), Ids::REINFORCED_DEEPSLATE); - $this->mapSimple(Blocks::RESERVED6(), Ids::RESERVED6); - $this->mapSimple(Blocks::SAND(), Ids::SAND); - $this->mapSimple(Blocks::SANDSTONE(), Ids::SANDSTONE); - $this->mapSimple(Blocks::SCULK(), Ids::SCULK); - $this->mapSimple(Blocks::SEA_LANTERN(), Ids::SEA_LANTERN); - $this->mapSimple(Blocks::SHROOMLIGHT(), Ids::SHROOMLIGHT); - $this->mapSimple(Blocks::SHULKER_BOX(), Ids::UNDYED_SHULKER_BOX); - $this->mapSimple(Blocks::SLIME(), Ids::SLIME); - $this->mapSimple(Blocks::SMITHING_TABLE(), Ids::SMITHING_TABLE); - $this->mapSimple(Blocks::SMOOTH_BASALT(), Ids::SMOOTH_BASALT); - $this->mapSimple(Blocks::SMOOTH_RED_SANDSTONE(), Ids::SMOOTH_RED_SANDSTONE); - $this->mapSimple(Blocks::SMOOTH_SANDSTONE(), Ids::SMOOTH_SANDSTONE); - $this->mapSimple(Blocks::SMOOTH_STONE(), Ids::SMOOTH_STONE); - $this->mapSimple(Blocks::SNOW(), Ids::SNOW); - $this->mapSimple(Blocks::SOUL_SAND(), Ids::SOUL_SAND); - $this->mapSimple(Blocks::SOUL_SOIL(), Ids::SOUL_SOIL); - $this->mapSimple(Blocks::SPORE_BLOSSOM(), Ids::SPORE_BLOSSOM); - $this->mapSimple(Blocks::STONE(), Ids::STONE); - $this->mapSimple(Blocks::STONE_BRICKS(), Ids::STONE_BRICKS); - $this->mapSimple(Blocks::TALL_GRASS(), Ids::SHORT_GRASS); //no, this is not a typo - tall_grass is now the double block, just to be confusing :( - $this->mapSimple(Blocks::TINTED_GLASS(), Ids::TINTED_GLASS); - $this->mapSimple(Blocks::TORCHFLOWER(), Ids::TORCHFLOWER); - $this->mapSimple(Blocks::TUFF(), Ids::TUFF); - $this->mapSimple(Blocks::TUFF_BRICKS(), Ids::TUFF_BRICKS); - $this->mapSimple(Blocks::WARPED_WART_BLOCK(), Ids::WARPED_WART_BLOCK); - $this->mapSimple(Blocks::WARPED_ROOTS(), Ids::WARPED_ROOTS); - $this->mapSimple(Blocks::WITHER_ROSE(), Ids::WITHER_ROSE); - - $this->mapSimple(Blocks::ALLIUM(), Ids::ALLIUM); - $this->mapSimple(Blocks::CORNFLOWER(), Ids::CORNFLOWER); - $this->mapSimple(Blocks::AZURE_BLUET(), Ids::AZURE_BLUET); - $this->mapSimple(Blocks::LILY_OF_THE_VALLEY(), Ids::LILY_OF_THE_VALLEY); - $this->mapSimple(Blocks::BLUE_ORCHID(), Ids::BLUE_ORCHID); - $this->mapSimple(Blocks::OXEYE_DAISY(), Ids::OXEYE_DAISY); - $this->mapSimple(Blocks::POPPY(), Ids::POPPY); - $this->mapSimple(Blocks::ORANGE_TULIP(), Ids::ORANGE_TULIP); - $this->mapSimple(Blocks::PINK_TULIP(), Ids::PINK_TULIP); - $this->mapSimple(Blocks::RED_TULIP(), Ids::RED_TULIP); - $this->mapSimple(Blocks::WHITE_TULIP(), Ids::WHITE_TULIP); - } - - private function registerSerializers() : void{ - $this->map(Blocks::ACTIVATOR_RAIL(), function(ActivatorRail $block) : Writer{ - return Writer::create(Ids::ACTIVATOR_RAIL) - ->writeBool(StateNames::RAIL_DATA_BIT, $block->isPowered()) - ->writeInt(StateNames::RAIL_DIRECTION, $block->getShape()); - }); - $this->map(Blocks::ALL_SIDED_MUSHROOM_STEM(), fn() => Writer::create(Ids::MUSHROOM_STEM) - ->writeInt(StateNames::HUGE_MUSHROOM_BITS, BlockLegacyMetadata::MUSHROOM_BLOCK_ALL_STEM)); - $this->map(Blocks::AMETHYST_CLUSTER(), fn(AmethystCluster $block) => Writer::create( - match($stage = $block->getStage()){ - AmethystCluster::STAGE_SMALL_BUD => Ids::SMALL_AMETHYST_BUD, - AmethystCluster::STAGE_MEDIUM_BUD => Ids::MEDIUM_AMETHYST_BUD, - AmethystCluster::STAGE_LARGE_BUD => Ids::LARGE_AMETHYST_BUD, - AmethystCluster::STAGE_CLUSTER => Ids::AMETHYST_CLUSTER, - default => throw new BlockStateSerializeException("Invalid Amethyst Cluster stage $stage"), - }) - ->writeBlockFace($block->getFacing()) - ); - $this->mapSlab(Blocks::ANDESITE_SLAB(), Ids::ANDESITE_SLAB, Ids::ANDESITE_DOUBLE_SLAB); - $this->map(Blocks::ANDESITE_STAIRS(), fn(Stair $block) => Helper::encodeStairs($block, new Writer(Ids::ANDESITE_STAIRS))); - $this->map(Blocks::ANDESITE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::ANDESITE_WALL))); - $this->map(Blocks::ANVIL(), fn(Anvil $block) : Writer => Writer::create( - match($damage = $block->getDamage()){ - 0 => Ids::ANVIL, - 1 => Ids::CHIPPED_ANVIL, - 2 => Ids::DAMAGED_ANVIL, - default => throw new BlockStateSerializeException("Invalid Anvil damage {$damage}"), - }) - ->writeCardinalHorizontalFacing($block->getFacing()) - ); - $this->map(Blocks::BAMBOO(), function(Bamboo $block) : Writer{ - return Writer::create(Ids::BAMBOO) - ->writeBool(StateNames::AGE_BIT, $block->isReady()) - ->writeString(StateNames::BAMBOO_LEAF_SIZE, match($block->getLeafSize()){ - Bamboo::NO_LEAVES => StringValues::BAMBOO_LEAF_SIZE_NO_LEAVES, - Bamboo::SMALL_LEAVES => StringValues::BAMBOO_LEAF_SIZE_SMALL_LEAVES, - Bamboo::LARGE_LEAVES => StringValues::BAMBOO_LEAF_SIZE_LARGE_LEAVES, - default => throw new BlockStateSerializeException("Invalid Bamboo leaf thickness " . $block->getLeafSize()), - }) - ->writeString(StateNames::BAMBOO_STALK_THICKNESS, $block->isThick() ? StringValues::BAMBOO_STALK_THICKNESS_THICK : StringValues::BAMBOO_STALK_THICKNESS_THIN); - }); - $this->map(Blocks::BAMBOO_SAPLING(), function(BambooSapling $block) : Writer{ - return Writer::create(Ids::BAMBOO_SAPLING) - ->writeBool(StateNames::AGE_BIT, $block->isReady()); - }); - $this->map(Blocks::BANNER(), function(FloorBanner $block) : Writer{ - return Writer::create(Ids::STANDING_BANNER) - ->writeInt(StateNames::GROUND_SIGN_DIRECTION, $block->getRotation()); - }); - $this->map(Blocks::BARREL(), function(Barrel $block) : Writer{ - return Writer::create(Ids::BARREL) - ->writeBool(StateNames::OPEN_BIT, $block->isOpen()) - ->writeFacingDirection($block->getFacing()); - }); - $this->map(Blocks::BASALT(), function(SimplePillar $block) : Writer{ - return Writer::create(Ids::BASALT) - ->writePillarAxis($block->getAxis()); - }); - $this->map(Blocks::BED(), function(Bed $block) : Writer{ - return Writer::create(Ids::BED) - ->writeBool(StateNames::HEAD_PIECE_BIT, $block->isHeadPart()) - ->writeBool(StateNames::OCCUPIED_BIT, $block->isOccupied()) - ->writeLegacyHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::BEDROCK(), function(Block $block) : Writer{ - return Writer::create(Ids::BEDROCK) - ->writeBool(StateNames::INFINIBURN_BIT, $block->burnsForever()); - }); - $this->map(Blocks::BEETROOTS(), fn(Beetroot $block) => Helper::encodeCrops($block, new Writer(Ids::BEETROOT))); - $this->map(Blocks::BELL(), function(Bell $block) : Writer{ - return Writer::create(Ids::BELL) - ->writeBellAttachmentType($block->getAttachmentType()) - ->writeBool(StateNames::TOGGLE_BIT, false) //we don't care about this; it's just to keep MCPE happy - ->writeLegacyHorizontalFacing($block->getFacing()); - - }); - $this->map(Blocks::BIG_DRIPLEAF_HEAD(), function(BigDripleafHead $block) : Writer{ - return Writer::create(Ids::BIG_DRIPLEAF) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeString(StateNames::BIG_DRIPLEAF_TILT, match($block->getLeafState()){ - DripleafState::STABLE => StringValues::BIG_DRIPLEAF_TILT_NONE, - DripleafState::UNSTABLE => StringValues::BIG_DRIPLEAF_TILT_UNSTABLE, - DripleafState::PARTIAL_TILT => StringValues::BIG_DRIPLEAF_TILT_PARTIAL_TILT, - DripleafState::FULL_TILT => StringValues::BIG_DRIPLEAF_TILT_FULL_TILT, - }) - ->writeBool(StateNames::BIG_DRIPLEAF_HEAD, true); - }); - $this->map(Blocks::BIG_DRIPLEAF_STEM(), function(BigDripleafStem $block) : Writer{ - return Writer::create(Ids::BIG_DRIPLEAF) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeString(StateNames::BIG_DRIPLEAF_TILT, StringValues::BIG_DRIPLEAF_TILT_NONE) - ->writeBool(StateNames::BIG_DRIPLEAF_HEAD, false); - }); - $this->mapSlab(Blocks::BLACKSTONE_SLAB(), Ids::BLACKSTONE_SLAB, Ids::BLACKSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::BLACKSTONE_STAIRS(), Ids::BLACKSTONE_STAIRS); - $this->map(Blocks::BLACKSTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::BLACKSTONE_WALL))); - $this->map(Blocks::BLAST_FURNACE(), fn(Furnace $block) => Helper::encodeFurnace($block, Ids::BLAST_FURNACE, Ids::LIT_BLAST_FURNACE)); - $this->map(Blocks::BLUE_TORCH(), fn(Torch $block) => Helper::encodeTorch($block, Writer::create(Ids::COLORED_TORCH_BLUE))); - $this->map(Blocks::BONE_BLOCK(), function(BoneBlock $block) : Writer{ - return Writer::create(Ids::BONE_BLOCK) - ->writeInt(StateNames::DEPRECATED, 0) - ->writePillarAxis($block->getAxis()); - }); - $this->map(Blocks::BREWING_STAND(), function(BrewingStand $block) : Writer{ - return Writer::create(Ids::BREWING_STAND) - ->writeBool(StateNames::BREWING_STAND_SLOT_A_BIT, $block->hasSlot(BrewingStandSlot::EAST)) - ->writeBool(StateNames::BREWING_STAND_SLOT_B_BIT, $block->hasSlot(BrewingStandSlot::SOUTHWEST)) - ->writeBool(StateNames::BREWING_STAND_SLOT_C_BIT, $block->hasSlot(BrewingStandSlot::NORTHWEST)); - }); - $this->mapSlab(Blocks::BRICK_SLAB(), Ids::BRICK_SLAB, Ids::BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::BRICK_STAIRS(), Ids::BRICK_STAIRS); - $this->map(Blocks::BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::BRICK_WALL))); - $this->map(Blocks::BROWN_MUSHROOM_BLOCK(), fn(BrownMushroomBlock $block) => Helper::encodeMushroomBlock($block, new Writer(Ids::BROWN_MUSHROOM_BLOCK))); - $this->map(Blocks::CACTUS(), function(Cactus $block) : Writer{ - return Writer::create(Ids::CACTUS) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->map(Blocks::CAKE(), function(Cake $block) : Writer{ - return Writer::create(Ids::CAKE) - ->writeInt(StateNames::BITE_COUNTER, $block->getBites()); - }); - $this->map(Blocks::CAMPFIRE(), function(Campfire $block) : Writer{ - return Writer::create(Ids::CAMPFIRE) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeBool(StateNames::EXTINGUISHED, !$block->isLit()); - }); - $this->map(Blocks::CARROTS(), fn(Carrot $block) => Helper::encodeCrops($block, new Writer(Ids::CARROTS))); - $this->map(Blocks::CARVED_PUMPKIN(), function(CarvedPumpkin $block) : Writer{ - return Writer::create(Ids::CARVED_PUMPKIN) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::CAVE_VINES(), function(CaveVines $block) : Writer{ - //I have no idea why this only has 3 IDs - there are 4 in Java and 4 visually distinct states in Bedrock - return Writer::create($block->hasBerries() ? - ($block->isHead() ? - Ids::CAVE_VINES_HEAD_WITH_BERRIES : - Ids::CAVE_VINES_BODY_WITH_BERRIES - ) : - Ids::CAVE_VINES - ) - ->writeInt(StateNames::GROWING_PLANT_AGE, $block->getAge()); - }); - $this->map(Blocks::CHAIN(), function(Chain $block) : Writer{ - return Writer::create(Ids::CHAIN) - ->writePillarAxis($block->getAxis()); - }); - $this->map(Blocks::CHEST(), function(Chest $block) : Writer{ - return Writer::create(Ids::CHEST) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::CHISELED_BOOKSHELF(), function(ChiseledBookshelf $block) : Writer{ - $flags = 0; - foreach($block->getSlots() as $slot){ - $flags |= 1 << $slot->value; - } - return Writer::create(Ids::CHISELED_BOOKSHELF) - ->writeLegacyHorizontalFacing($block->getFacing()) - ->writeInt(StateNames::BOOKS_STORED, $flags); - }); - $this->map(Blocks::CHISELED_QUARTZ(), fn(SimplePillar $block) => Helper::encodeQuartz($block->getAxis(), Writer::create(Ids::CHISELED_QUARTZ_BLOCK))); - $this->map(Blocks::CHORUS_FLOWER(), function(ChorusFlower $block) : Writer{ - return Writer::create(Ids::CHORUS_FLOWER) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->mapSlab(Blocks::COBBLED_DEEPSLATE_SLAB(), Ids::COBBLED_DEEPSLATE_SLAB, Ids::COBBLED_DEEPSLATE_DOUBLE_SLAB); - $this->mapStairs(Blocks::COBBLED_DEEPSLATE_STAIRS(), Ids::COBBLED_DEEPSLATE_STAIRS); - $this->map(Blocks::COBBLED_DEEPSLATE_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::COBBLED_DEEPSLATE_WALL))); - $this->mapSlab(Blocks::COBBLESTONE_SLAB(), Ids::COBBLESTONE_SLAB, Ids::COBBLESTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::COBBLESTONE_STAIRS(), Ids::STONE_STAIRS); - $this->map(Blocks::COBBLESTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::COBBLESTONE_WALL))); - $this->map(Blocks::COPPER(), function(Copper $block) : Writer{ - $oxidation = $block->getOxidation(); - return new Writer($block->isWaxed() ? - Helper::selectCopperId($oxidation, Ids::WAXED_COPPER, Ids::WAXED_EXPOSED_COPPER, Ids::WAXED_WEATHERED_COPPER, Ids::WAXED_OXIDIZED_COPPER) : - Helper::selectCopperId($oxidation, Ids::COPPER_BLOCK, Ids::EXPOSED_COPPER, Ids::WEATHERED_COPPER, Ids::OXIDIZED_COPPER) - ); - }); - $this->map(Blocks::CHISELED_COPPER(), function(Copper $block) : Writer{ - $oxidation = $block->getOxidation(); - return new Writer($block->isWaxed() ? - Helper::selectCopperId($oxidation, - Ids::WAXED_CHISELED_COPPER, - Ids::WAXED_EXPOSED_CHISELED_COPPER, - Ids::WAXED_WEATHERED_CHISELED_COPPER, - Ids::WAXED_OXIDIZED_CHISELED_COPPER - ) : - Helper::selectCopperId($oxidation, - Ids::CHISELED_COPPER, - Ids::EXPOSED_CHISELED_COPPER, - Ids::WEATHERED_CHISELED_COPPER, - Ids::OXIDIZED_CHISELED_COPPER - ) - ); - }); - $this->map(Blocks::COPPER_GRATE(), function(CopperGrate $block) : Writer{ - $oxidation = $block->getOxidation(); - return new Writer($block->isWaxed() ? - Helper::selectCopperId($oxidation, - Ids::WAXED_COPPER_GRATE, - Ids::WAXED_EXPOSED_COPPER_GRATE, - Ids::WAXED_WEATHERED_COPPER_GRATE, - Ids::WAXED_OXIDIZED_COPPER_GRATE - ) : - Helper::selectCopperId($oxidation, - Ids::COPPER_GRATE, - Ids::EXPOSED_COPPER_GRATE, - Ids::WEATHERED_COPPER_GRATE, - Ids::OXIDIZED_COPPER_GRATE - ) - ); - }); - $this->map(Blocks::CUT_COPPER(), function(Copper $block) : Writer{ - $oxidation = $block->getOxidation(); - return new Writer($block->isWaxed() ? - Helper::selectCopperId($oxidation, Ids::WAXED_CUT_COPPER, Ids::WAXED_EXPOSED_CUT_COPPER, Ids::WAXED_WEATHERED_CUT_COPPER, Ids::WAXED_OXIDIZED_CUT_COPPER) : - Helper::selectCopperId($oxidation, Ids::CUT_COPPER, Ids::EXPOSED_CUT_COPPER, Ids::WEATHERED_CUT_COPPER, Ids::OXIDIZED_CUT_COPPER) - ); - }); - $this->map(Blocks::CUT_COPPER_SLAB(), function(CopperSlab $block) : Writer{ - $oxidation = $block->getOxidation(); - return Helper::encodeSlab( - $block, - ($block->isWaxed() ? - Helper::selectCopperId( - $oxidation, - Ids::WAXED_CUT_COPPER_SLAB, - Ids::WAXED_EXPOSED_CUT_COPPER_SLAB, - Ids::WAXED_WEATHERED_CUT_COPPER_SLAB, - Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB - ) : - Helper::selectCopperId( - $oxidation, - Ids::CUT_COPPER_SLAB, - Ids::EXPOSED_CUT_COPPER_SLAB, - Ids::WEATHERED_CUT_COPPER_SLAB, - Ids::OXIDIZED_CUT_COPPER_SLAB - ) - ), - ($block->isWaxed() ? - Helper::selectCopperId( - $oxidation, - Ids::WAXED_DOUBLE_CUT_COPPER_SLAB, - Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB, - Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB, - Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB - ) : - Helper::selectCopperId( - $oxidation, - Ids::DOUBLE_CUT_COPPER_SLAB, - Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB, - Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB, - Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB - ) - ) - ); - }); - $this->map(Blocks::CUT_COPPER_STAIRS(), function(CopperStairs $block) : Writer{ - $oxidation = $block->getOxidation(); - return Helper::encodeStairs( - $block, - new Writer($block->isWaxed() ? - Helper::selectCopperId( - $oxidation, - Ids::WAXED_CUT_COPPER_STAIRS, - Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS, - Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS, - Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS - ) : - Helper::selectCopperId( - $oxidation, - Ids::CUT_COPPER_STAIRS, - Ids::EXPOSED_CUT_COPPER_STAIRS, - Ids::WEATHERED_CUT_COPPER_STAIRS, - Ids::OXIDIZED_CUT_COPPER_STAIRS - ) - ) - ); - }); - $this->map(Blocks::COPPER_BULB(), function(CopperBulb $block) : Writer{ - $oxidation = $block->getOxidation(); - return Writer::create($block->isWaxed() ? - Helper::selectCopperId($oxidation, - Ids::WAXED_COPPER_BULB, - Ids::WAXED_EXPOSED_COPPER_BULB, - Ids::WAXED_WEATHERED_COPPER_BULB, - Ids::WAXED_OXIDIZED_COPPER_BULB) : - Helper::selectCopperId($oxidation, - Ids::COPPER_BULB, - Ids::EXPOSED_COPPER_BULB, - Ids::WEATHERED_COPPER_BULB, - Ids::OXIDIZED_COPPER_BULB - )) - ->writeBool(StateNames::LIT, $block->isLit()) - ->writeBool(StateNames::POWERED_BIT, $block->isPowered()); - }); - $this->map(Blocks::COPPER_DOOR(), function(CopperDoor $block) : Writer{ - $oxidation = $block->getOxidation(); - return Helper::encodeDoor( - $block, - new Writer($block->isWaxed() ? - Helper::selectCopperId( - $oxidation, - Ids::WAXED_COPPER_DOOR, - Ids::WAXED_EXPOSED_COPPER_DOOR, - Ids::WAXED_WEATHERED_COPPER_DOOR, - Ids::WAXED_OXIDIZED_COPPER_DOOR - ) : - Helper::selectCopperId( - $oxidation, - Ids::COPPER_DOOR, - Ids::EXPOSED_COPPER_DOOR, - Ids::WEATHERED_COPPER_DOOR, - Ids::OXIDIZED_COPPER_DOOR - ) - ) - ); - }); - $this->map(Blocks::COPPER_TRAPDOOR(), function(CopperTrapdoor $block) : Writer{ - $oxidation = $block->getOxidation(); - return Helper::encodeTrapdoor( - $block, - new Writer($block->isWaxed() ? - Helper::selectCopperId( - $oxidation, - Ids::WAXED_COPPER_TRAPDOOR, - Ids::WAXED_EXPOSED_COPPER_TRAPDOOR, - Ids::WAXED_WEATHERED_COPPER_TRAPDOOR, - Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR - ) : - Helper::selectCopperId( - $oxidation, - Ids::COPPER_TRAPDOOR, - Ids::EXPOSED_COPPER_TRAPDOOR, - Ids::WEATHERED_COPPER_TRAPDOOR, - Ids::OXIDIZED_COPPER_TRAPDOOR - ) - ) - ); - }); - $this->map(Blocks::COCOA_POD(), function(CocoaBlock $block) : Writer{ - return Writer::create(Ids::COCOA) - ->writeInt(StateNames::AGE, $block->getAge()) - ->writeLegacyHorizontalFacing(Facing::opposite($block->getFacing())); - }); - $this->map(Blocks::COMPOUND_CREATOR(), fn(ChemistryTable $block) => Helper::encodeChemistryTable($block, Writer::create(Ids::COMPOUND_CREATOR))); - $this->mapSlab(Blocks::CUT_RED_SANDSTONE_SLAB(), Ids::CUT_RED_SANDSTONE_SLAB, Ids::CUT_RED_SANDSTONE_DOUBLE_SLAB); - $this->mapSlab(Blocks::CUT_SANDSTONE_SLAB(), Ids::CUT_SANDSTONE_SLAB, Ids::CUT_SANDSTONE_DOUBLE_SLAB); - $this->mapSlab(Blocks::DARK_PRISMARINE_SLAB(), Ids::DARK_PRISMARINE_SLAB, Ids::DARK_PRISMARINE_DOUBLE_SLAB); - $this->mapStairs(Blocks::DARK_PRISMARINE_STAIRS(), Ids::DARK_PRISMARINE_STAIRS); - $this->map(Blocks::DAYLIGHT_SENSOR(), function(DaylightSensor $block) : Writer{ - return Writer::create($block->isInverted() ? Ids::DAYLIGHT_DETECTOR_INVERTED : Ids::DAYLIGHT_DETECTOR) - ->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()); - }); - $this->map(Blocks::DEEPSLATE(), function(SimplePillar $block) : Writer{ - return Writer::create(Ids::DEEPSLATE) - ->writePillarAxis($block->getAxis()); - }); - $this->mapSlab(Blocks::DEEPSLATE_BRICK_SLAB(), Ids::DEEPSLATE_BRICK_SLAB, Ids::DEEPSLATE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::DEEPSLATE_BRICK_STAIRS(), Ids::DEEPSLATE_BRICK_STAIRS); - $this->map(Blocks::DEEPSLATE_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::DEEPSLATE_BRICK_WALL))); - $this->map(Blocks::DEEPSLATE_REDSTONE_ORE(), fn(RedstoneOre $block) => new Writer($block->isLit() ? Ids::LIT_DEEPSLATE_REDSTONE_ORE : Ids::DEEPSLATE_REDSTONE_ORE)); - $this->mapSlab(Blocks::DEEPSLATE_TILE_SLAB(), Ids::DEEPSLATE_TILE_SLAB, Ids::DEEPSLATE_TILE_DOUBLE_SLAB); - $this->mapStairs(Blocks::DEEPSLATE_TILE_STAIRS(), Ids::DEEPSLATE_TILE_STAIRS); - $this->map(Blocks::DEEPSLATE_TILE_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::DEEPSLATE_TILE_WALL))); - $this->map(Blocks::DETECTOR_RAIL(), function(DetectorRail $block) : Writer{ - return Writer::create(Ids::DETECTOR_RAIL) - ->writeBool(StateNames::RAIL_DATA_BIT, $block->isActivated()) - ->writeInt(StateNames::RAIL_DIRECTION, $block->getShape()); - }); - $this->mapSlab(Blocks::DIORITE_SLAB(), Ids::DIORITE_SLAB, Ids::DIORITE_DOUBLE_SLAB); - $this->mapStairs(Blocks::DIORITE_STAIRS(), Ids::DIORITE_STAIRS); - $this->map(Blocks::DIORITE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::DIORITE_WALL))); - $this->map(Blocks::DIRT(), function(Dirt $block) : Writer{ - return Writer::create(match($block->getDirtType()){ - DirtType::NORMAL => Ids::DIRT, - DirtType::COARSE => Ids::COARSE_DIRT, - DirtType::ROOTED => Ids::DIRT_WITH_ROOTS, - }); - }); - $this->map(Blocks::DOUBLE_TALLGRASS(), fn(DoubleTallGrass $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::TALL_GRASS))); - $this->map(Blocks::ELEMENT_CONSTRUCTOR(), fn(ChemistryTable $block) => Helper::encodeChemistryTable($block, Writer::create(Ids::ELEMENT_CONSTRUCTOR))); - $this->map(Blocks::ENDER_CHEST(), function(EnderChest $block) : Writer{ - return Writer::create(Ids::ENDER_CHEST) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::END_PORTAL_FRAME(), function(EndPortalFrame $block) : Writer{ - return Writer::create(Ids::END_PORTAL_FRAME) - ->writeBool(StateNames::END_PORTAL_EYE_BIT, $block->hasEye()) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::END_ROD(), function(EndRod $block) : Writer{ - return Writer::create(Ids::END_ROD) - ->writeEndRodFacingDirection($block->getFacing()); - }); - $this->mapSlab(Blocks::END_STONE_BRICK_SLAB(), Ids::END_STONE_BRICK_SLAB, Ids::END_STONE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::END_STONE_BRICK_STAIRS(), Ids::END_BRICK_STAIRS); - $this->map(Blocks::END_STONE_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::END_STONE_BRICK_WALL))); - $this->mapSlab(Blocks::FAKE_WOODEN_SLAB(), Ids::PETRIFIED_OAK_SLAB, Ids::PETRIFIED_OAK_DOUBLE_SLAB); - $this->map(Blocks::FARMLAND(), function(Farmland $block) : Writer{ - return Writer::create(Ids::FARMLAND) - ->writeInt(StateNames::MOISTURIZED_AMOUNT, $block->getWetness()); - }); - $this->map(Blocks::FIRE(), function(Fire $block) : Writer{ - return Writer::create(Ids::FIRE) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->map(Blocks::FLOWER_POT(), function() : Writer{ - return Writer::create(Ids::FLOWER_POT) - ->writeBool(StateNames::UPDATE_BIT, false); //to keep MCPE happy - }); - $this->map(Blocks::FROGLIGHT(), function(Froglight $block){ - return Writer::create(match($block->getFroglightType()){ - FroglightType::OCHRE => Ids::OCHRE_FROGLIGHT, - FroglightType::PEARLESCENT => Ids::PEARLESCENT_FROGLIGHT, - FroglightType::VERDANT => Ids::VERDANT_FROGLIGHT, - }) - ->writePillarAxis($block->getAxis()); - }); - $this->map(Blocks::FROSTED_ICE(), function(FrostedIce $block) : Writer{ - return Writer::create(Ids::FROSTED_ICE) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->map(Blocks::FURNACE(), fn(Furnace $block) => Helper::encodeFurnace($block, Ids::FURNACE, Ids::LIT_FURNACE)); - $this->map(Blocks::GLOW_LICHEN(), function(GlowLichen $block) : Writer{ - return Writer::create(Ids::GLOW_LICHEN) - ->writeFacingFlags($block->getFaces()); - }); - $this->map(Blocks::GLOWING_ITEM_FRAME(), fn(ItemFrame $block) => Helper::encodeItemFrame($block, Ids::GLOW_FRAME)); - $this->mapSlab(Blocks::GRANITE_SLAB(), Ids::GRANITE_SLAB, Ids::GRANITE_DOUBLE_SLAB); - $this->mapStairs(Blocks::GRANITE_STAIRS(), Ids::GRANITE_STAIRS); - $this->map(Blocks::GRANITE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::GRANITE_WALL))); - $this->map(Blocks::GREEN_TORCH(), fn(Torch $block) => Helper::encodeTorch($block, Writer::create(Ids::COLORED_TORCH_GREEN))); - $this->map(Blocks::HAY_BALE(), function(HayBale $block) : Writer{ - return Writer::create(Ids::HAY_BLOCK) - ->writeInt(StateNames::DEPRECATED, 0) - ->writePillarAxis($block->getAxis()); - }); - $this->map(Blocks::HOPPER(), function(Hopper $block) : Writer{ - return Writer::create(Ids::HOPPER) - ->writeBool(StateNames::TOGGLE_BIT, $block->isPowered()) - ->writeFacingWithoutUp($block->getFacing()); - }); - $this->map(Blocks::IRON_DOOR(), fn(Door $block) => Helper::encodeDoor($block, new Writer(Ids::IRON_DOOR))); - $this->map(Blocks::IRON_TRAPDOOR(), fn(Trapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::IRON_TRAPDOOR))); - $this->map(Blocks::ITEM_FRAME(), fn(ItemFrame $block) => Helper::encodeItemFrame($block, Ids::FRAME)); - $this->map(Blocks::LAB_TABLE(), fn(ChemistryTable $block) => Helper::encodeChemistryTable($block, Writer::create(Ids::LAB_TABLE))); - $this->map(Blocks::LADDER(), function(Ladder $block) : Writer{ - return Writer::create(Ids::LADDER) - ->writeHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::LANTERN(), function(Lantern $block) : Writer{ - return Writer::create(Ids::LANTERN) - ->writeBool(StateNames::HANGING, $block->isHanging()); - }); - $this->map(Blocks::LARGE_FERN(), fn(DoubleTallGrass $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::LARGE_FERN))); - $this->map(Blocks::LAVA(), fn(Lava $block) => Helper::encodeLiquid($block, Ids::LAVA, Ids::FLOWING_LAVA)); - $this->map(Blocks::LECTERN(), function(Lectern $block) : Writer{ - return Writer::create(Ids::LECTERN) - ->writeBool(StateNames::POWERED_BIT, $block->isProducingSignal()) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::LEVER(), function(Lever $block) : Writer{ - return Writer::create(Ids::LEVER) - ->writeBool(StateNames::OPEN_BIT, $block->isActivated()) - ->writeString(StateNames::LEVER_DIRECTION, match($block->getFacing()){ - LeverFacing::DOWN_AXIS_Z => StringValues::LEVER_DIRECTION_DOWN_NORTH_SOUTH, - LeverFacing::DOWN_AXIS_X => StringValues::LEVER_DIRECTION_DOWN_EAST_WEST, - LeverFacing::UP_AXIS_Z => StringValues::LEVER_DIRECTION_UP_NORTH_SOUTH, - LeverFacing::UP_AXIS_X => StringValues::LEVER_DIRECTION_UP_EAST_WEST, - LeverFacing::NORTH => StringValues::LEVER_DIRECTION_NORTH, - LeverFacing::SOUTH => StringValues::LEVER_DIRECTION_SOUTH, - LeverFacing::WEST => StringValues::LEVER_DIRECTION_WEST, - LeverFacing::EAST => StringValues::LEVER_DIRECTION_EAST, - }); - }); - $this->map(Blocks::LIGHT(), function(Light $block) : Writer{ - return Writer::create(match($block->getLightLevel()){ - 0 => Ids::LIGHT_BLOCK_0, - 1 => Ids::LIGHT_BLOCK_1, - 2 => Ids::LIGHT_BLOCK_2, - 3 => Ids::LIGHT_BLOCK_3, - 4 => Ids::LIGHT_BLOCK_4, - 5 => Ids::LIGHT_BLOCK_5, - 6 => Ids::LIGHT_BLOCK_6, - 7 => Ids::LIGHT_BLOCK_7, - 8 => Ids::LIGHT_BLOCK_8, - 9 => Ids::LIGHT_BLOCK_9, - 10 => Ids::LIGHT_BLOCK_10, - 11 => Ids::LIGHT_BLOCK_11, - 12 => Ids::LIGHT_BLOCK_12, - 13 => Ids::LIGHT_BLOCK_13, - 14 => Ids::LIGHT_BLOCK_14, - 15 => Ids::LIGHT_BLOCK_15, - default => throw new BlockStateSerializeException("Invalid light level " . $block->getLightLevel()), - }); - }); - $this->map(Blocks::LIGHTNING_ROD(), function(LightningRod $block) : Writer{ - return Writer::create(Ids::LIGHTNING_ROD) - ->writeFacingDirection($block->getFacing()); - }); - $this->map(Blocks::LILAC(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::LILAC))); - $this->map(Blocks::LIT_PUMPKIN(), function(LitPumpkin $block) : Writer{ - return Writer::create(Ids::LIT_PUMPKIN) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::LOOM(), function(Loom $block) : Writer{ - return Writer::create(Ids::LOOM) - ->writeLegacyHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::MATERIAL_REDUCER(), fn(ChemistryTable $block) => Helper::encodeChemistryTable($block, Writer::create(Ids::MATERIAL_REDUCER))); - $this->map(Blocks::MELON_STEM(), fn(MelonStem $block) => Helper::encodeStem($block, new Writer(Ids::MELON_STEM))); - $this->mapSlab(Blocks::MOSSY_COBBLESTONE_SLAB(), Ids::MOSSY_COBBLESTONE_SLAB, Ids::MOSSY_COBBLESTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::MOSSY_COBBLESTONE_STAIRS(), Ids::MOSSY_COBBLESTONE_STAIRS); - $this->map(Blocks::MOSSY_COBBLESTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::MOSSY_COBBLESTONE_WALL))); - $this->mapSlab(Blocks::MOSSY_STONE_BRICK_SLAB(), Ids::MOSSY_STONE_BRICK_SLAB, Ids::MOSSY_STONE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::MOSSY_STONE_BRICK_STAIRS(), Ids::MOSSY_STONE_BRICK_STAIRS); - $this->map(Blocks::MOSSY_STONE_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::MOSSY_STONE_BRICK_WALL))); - $this->mapSlab(Blocks::MUD_BRICK_SLAB(), Ids::MUD_BRICK_SLAB, Ids::MUD_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::MUD_BRICK_STAIRS(), Ids::MUD_BRICK_STAIRS); - $this->map(Blocks::MUD_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::MUD_BRICK_WALL))); - $this->map(Blocks::MUDDY_MANGROVE_ROOTS(), fn(SimplePillar $block) => Writer::create(Ids::MUDDY_MANGROVE_ROOTS) - ->writePillarAxis($block->getAxis())); - $this->map(Blocks::MUSHROOM_STEM(), fn() => Writer::create(Ids::MUSHROOM_STEM) - ->writeInt(StateNames::HUGE_MUSHROOM_BITS, BlockLegacyMetadata::MUSHROOM_BLOCK_STEM)); - $this->mapSlab(Blocks::NETHER_BRICK_SLAB(), Ids::NETHER_BRICK_SLAB, Ids::NETHER_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::NETHER_BRICK_STAIRS(), Ids::NETHER_BRICK_STAIRS); - $this->map(Blocks::NETHER_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::NETHER_BRICK_WALL))); - $this->map(Blocks::NETHER_PORTAL(), function(NetherPortal $block) : Writer{ - return Writer::create(Ids::PORTAL) - ->writeString(StateNames::PORTAL_AXIS, match($block->getAxis()){ - Axis::X => StringValues::PORTAL_AXIS_X, - Axis::Z => StringValues::PORTAL_AXIS_Z, - default => throw new BlockStateSerializeException("Invalid Nether Portal axis " . $block->getAxis()), - }); - }); - $this->map(Blocks::NETHER_WART(), function(NetherWartPlant $block) : Writer{ - return Writer::create(Ids::NETHER_WART) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->map(Blocks::PEONY(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::PEONY))); - $this->map(Blocks::PINK_PETALS(), function(PinkPetals $block) : Writer{ - return Writer::create(Ids::PINK_PETALS) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeInt(StateNames::GROWTH, $block->getCount() - 1); - }); - $this->map(Blocks::PITCHER_PLANT(), function(DoublePlant $block) : Writer{ - return Writer::create(Ids::PITCHER_PLANT) - ->writeBool(StateNames::UPPER_BLOCK_BIT, $block->isTop()); - }); - $this->map(Blocks::PITCHER_CROP(), function(PitcherCrop $block) : Writer{ - return Writer::create(Ids::PITCHER_CROP) - ->writeInt(StateNames::GROWTH, $block->getAge()) - ->writeBool(StateNames::UPPER_BLOCK_BIT, false); - }); - $this->map(Blocks::DOUBLE_PITCHER_CROP(), function(DoublePitcherCrop $block) : Writer{ - return Writer::create(Ids::PITCHER_CROP) - ->writeInt(StateNames::GROWTH, $block->getAge() + 1 + PitcherCrop::MAX_AGE) - ->writeBool(StateNames::UPPER_BLOCK_BIT, $block->isTop()); - }); - $this->mapSlab(Blocks::POLISHED_ANDESITE_SLAB(), Ids::POLISHED_ANDESITE_SLAB, Ids::POLISHED_ANDESITE_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_ANDESITE_STAIRS(), Ids::POLISHED_ANDESITE_STAIRS); - $this->map(Blocks::POLISHED_BASALT(), function(SimplePillar $block) : Writer{ - return Writer::create(Ids::POLISHED_BASALT) - ->writePillarAxis($block->getAxis()); - }); - $this->mapSlab(Blocks::POLISHED_BLACKSTONE_BRICK_SLAB(), Ids::POLISHED_BLACKSTONE_BRICK_SLAB, Ids::POLISHED_BLACKSTONE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_BLACKSTONE_BRICK_STAIRS(), Ids::POLISHED_BLACKSTONE_BRICK_STAIRS); - $this->map(Blocks::POLISHED_BLACKSTONE_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::POLISHED_BLACKSTONE_BRICK_WALL))); - $this->map(Blocks::POLISHED_BLACKSTONE_BUTTON(), fn(Button $block) => Helper::encodeButton($block, new Writer(Ids::POLISHED_BLACKSTONE_BUTTON))); - $this->map(Blocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), fn(SimplePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE))); - $this->mapSlab(Blocks::POLISHED_BLACKSTONE_SLAB(), Ids::POLISHED_BLACKSTONE_SLAB, Ids::POLISHED_BLACKSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_BLACKSTONE_STAIRS(), Ids::POLISHED_BLACKSTONE_STAIRS); - $this->map(Blocks::POLISHED_BLACKSTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::POLISHED_BLACKSTONE_WALL))); - $this->mapSlab(Blocks::POLISHED_DEEPSLATE_SLAB(), Ids::POLISHED_DEEPSLATE_SLAB, Ids::POLISHED_DEEPSLATE_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_DEEPSLATE_STAIRS(), Ids::POLISHED_DEEPSLATE_STAIRS); - $this->map(Blocks::POLISHED_DEEPSLATE_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::POLISHED_DEEPSLATE_WALL))); - $this->mapSlab(Blocks::POLISHED_DIORITE_SLAB(), Ids::POLISHED_DIORITE_SLAB, Ids::POLISHED_DIORITE_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_DIORITE_STAIRS(), Ids::POLISHED_DIORITE_STAIRS); - $this->mapSlab(Blocks::POLISHED_GRANITE_SLAB(), Ids::POLISHED_GRANITE_SLAB, Ids::POLISHED_GRANITE_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_GRANITE_STAIRS(), Ids::POLISHED_GRANITE_STAIRS); - $this->mapSlab(Blocks::POLISHED_TUFF_SLAB(), Ids::POLISHED_TUFF_SLAB, Ids::POLISHED_TUFF_DOUBLE_SLAB); - $this->mapStairs(Blocks::POLISHED_TUFF_STAIRS(), Ids::POLISHED_TUFF_STAIRS); - $this->map(Blocks::POLISHED_TUFF_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::POLISHED_TUFF_WALL))); - $this->map(Blocks::POTATOES(), fn(Potato $block) => Helper::encodeCrops($block, new Writer(Ids::POTATOES))); - $this->map(Blocks::POWERED_RAIL(), function(PoweredRail $block) : Writer{ - return Writer::create(Ids::GOLDEN_RAIL) - ->writeBool(StateNames::RAIL_DATA_BIT, $block->isPowered()) - ->writeInt(StateNames::RAIL_DIRECTION, $block->getShape()); - }); - $this->mapSlab(Blocks::PRISMARINE_BRICKS_SLAB(), Ids::PRISMARINE_BRICK_SLAB, Ids::PRISMARINE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::PRISMARINE_BRICKS_STAIRS(), Ids::PRISMARINE_BRICKS_STAIRS); - $this->mapSlab(Blocks::PRISMARINE_SLAB(), Ids::PRISMARINE_SLAB, Ids::PRISMARINE_DOUBLE_SLAB); - $this->mapStairs(Blocks::PRISMARINE_STAIRS(), Ids::PRISMARINE_STAIRS); - $this->map(Blocks::PRISMARINE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::PRISMARINE_WALL))); - $this->map(Blocks::PUMPKIN(), function() : Writer{ - return Writer::create(Ids::PUMPKIN) - ->writeCardinalHorizontalFacing(Facing::SOUTH); //no longer used - }); - $this->map(Blocks::PUMPKIN_STEM(), fn(PumpkinStem $block) => Helper::encodeStem($block, new Writer(Ids::PUMPKIN_STEM))); - $this->map(Blocks::PURPUR(), fn() => Writer::create(Ids::PURPUR_BLOCK)->writePillarAxis(Axis::Y)); - $this->map(Blocks::PURPLE_TORCH(), fn(Torch $block) => Helper::encodeTorch($block, Writer::create(Ids::COLORED_TORCH_PURPLE))); - $this->map(Blocks::PURPUR_PILLAR(), function(SimplePillar $block) : Writer{ - return Writer::create(Ids::PURPUR_PILLAR) - ->writePillarAxis($block->getAxis()); - }); - $this->mapSlab(Blocks::PURPUR_SLAB(), Ids::PURPUR_SLAB, Ids::PURPUR_DOUBLE_SLAB); - $this->mapStairs(Blocks::PURPUR_STAIRS(), Ids::PURPUR_STAIRS); - $this->map(Blocks::QUARTZ(), fn() => Helper::encodeQuartz(Axis::Y, Writer::create(Ids::QUARTZ_BLOCK))); - $this->map(Blocks::QUARTZ_PILLAR(), fn(SimplePillar $block) => Helper::encodeQuartz($block->getAxis(), Writer::create(Ids::QUARTZ_PILLAR))); - $this->mapSlab(Blocks::QUARTZ_SLAB(), Ids::QUARTZ_SLAB, Ids::QUARTZ_DOUBLE_SLAB); - $this->mapStairs(Blocks::QUARTZ_STAIRS(), Ids::QUARTZ_STAIRS); - $this->map(Blocks::RAIL(), function(Rail $block) : Writer{ - return Writer::create(Ids::RAIL) - ->writeInt(StateNames::RAIL_DIRECTION, $block->getShape()); - }); - $this->map(Blocks::REDSTONE_COMPARATOR(), function(RedstoneComparator $block) : Writer{ - return Writer::create($block->isPowered() ? Ids::POWERED_COMPARATOR : Ids::UNPOWERED_COMPARATOR) - ->writeBool(StateNames::OUTPUT_LIT_BIT, $block->isPowered()) - ->writeBool(StateNames::OUTPUT_SUBTRACT_BIT, $block->isSubtractMode()) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::REDSTONE_LAMP(), fn(RedstoneLamp $block) => new Writer($block->isPowered() ? Ids::LIT_REDSTONE_LAMP : Ids::REDSTONE_LAMP)); - $this->map(Blocks::REDSTONE_ORE(), fn(RedstoneOre $block) => new Writer($block->isLit() ? Ids::LIT_REDSTONE_ORE : Ids::REDSTONE_ORE)); - $this->map(Blocks::REDSTONE_REPEATER(), function(RedstoneRepeater $block) : Writer{ - return Writer::create($block->isPowered() ? Ids::POWERED_REPEATER : Ids::UNPOWERED_REPEATER) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeInt(StateNames::REPEATER_DELAY, $block->getDelay() - 1); - }); - $this->map(Blocks::REDSTONE_TORCH(), function(RedstoneTorch $block) : Writer{ - return Writer::create($block->isLit() ? Ids::REDSTONE_TORCH : Ids::UNLIT_REDSTONE_TORCH) - ->writeTorchFacing($block->getFacing()); - }); - $this->map(Blocks::REDSTONE_WIRE(), function(RedstoneWire $block) : Writer{ - return Writer::create(Ids::REDSTONE_WIRE) - ->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()); - }); - $this->map(Blocks::RED_MUSHROOM_BLOCK(), fn(RedMushroomBlock $block) => Helper::encodeMushroomBlock($block, new Writer(Ids::RED_MUSHROOM_BLOCK))); - $this->mapSlab(Blocks::RED_NETHER_BRICK_SLAB(), Ids::RED_NETHER_BRICK_SLAB, Ids::RED_NETHER_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::RED_NETHER_BRICK_STAIRS(), Ids::RED_NETHER_BRICK_STAIRS); - $this->map(Blocks::RED_NETHER_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::RED_NETHER_BRICK_WALL))); - $this->mapSlab(Blocks::RED_SANDSTONE_SLAB(), Ids::RED_SANDSTONE_SLAB, Ids::RED_SANDSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::RED_SANDSTONE_STAIRS(), Ids::RED_SANDSTONE_STAIRS); - $this->map(Blocks::RED_SANDSTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::RED_SANDSTONE_WALL))); - $this->map(Blocks::RED_TORCH(), fn(Torch $block) => Helper::encodeTorch($block, Writer::create(Ids::COLORED_TORCH_RED))); - $this->map(Blocks::ROSE_BUSH(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::ROSE_BUSH))); - $this->mapSlab(Blocks::SANDSTONE_SLAB(), Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::SANDSTONE_STAIRS(), Ids::SANDSTONE_STAIRS); - $this->map(Blocks::SANDSTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::SANDSTONE_WALL))); - $this->map(Blocks::SEA_PICKLE(), function(SeaPickle $block) : Writer{ - return Writer::create(Ids::SEA_PICKLE) - ->writeBool(StateNames::DEAD_BIT, !$block->isUnderwater()) - ->writeInt(StateNames::CLUSTER_COUNT, $block->getCount() - 1); - }); - $this->map(Blocks::SMALL_DRIPLEAF(), function(SmallDripleaf $block) : Writer{ - return Writer::create(Ids::SMALL_DRIPLEAF_BLOCK) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeBool(StateNames::UPPER_BLOCK_BIT, $block->isTop()); - }); - $this->map(Blocks::SMOKER(), fn(Furnace $block) => Helper::encodeFurnace($block, Ids::SMOKER, Ids::LIT_SMOKER)); - $this->map(Blocks::SMOOTH_QUARTZ(), fn() => Helper::encodeQuartz(Axis::Y, Writer::create(Ids::SMOOTH_QUARTZ))); - $this->mapSlab(Blocks::SMOOTH_QUARTZ_SLAB(), Ids::SMOOTH_QUARTZ_SLAB, Ids::SMOOTH_QUARTZ_DOUBLE_SLAB); - $this->mapStairs(Blocks::SMOOTH_QUARTZ_STAIRS(), Ids::SMOOTH_QUARTZ_STAIRS); - $this->mapSlab(Blocks::SMOOTH_RED_SANDSTONE_SLAB(), Ids::SMOOTH_RED_SANDSTONE_SLAB, Ids::SMOOTH_RED_SANDSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::SMOOTH_RED_SANDSTONE_STAIRS(), Ids::SMOOTH_RED_SANDSTONE_STAIRS); - $this->mapSlab(Blocks::SMOOTH_SANDSTONE_SLAB(), Ids::SMOOTH_SANDSTONE_SLAB, Ids::SMOOTH_SANDSTONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::SMOOTH_SANDSTONE_STAIRS(), Ids::SMOOTH_SANDSTONE_STAIRS); - $this->mapSlab(Blocks::SMOOTH_STONE_SLAB(), Ids::SMOOTH_STONE_SLAB, Ids::SMOOTH_STONE_DOUBLE_SLAB); - $this->map(Blocks::SNOW_LAYER(), function(SnowLayer $block) : Writer{ - return Writer::create(Ids::SNOW_LAYER) - ->writeBool(StateNames::COVERED_BIT, false) - ->writeInt(StateNames::HEIGHT, $block->getLayers() - 1); - }); - $this->map(Blocks::SOUL_CAMPFIRE(), function(SoulCampfire $block) : Writer{ - return Writer::create(Ids::SOUL_CAMPFIRE) - ->writeCardinalHorizontalFacing($block->getFacing()) - ->writeBool(StateNames::EXTINGUISHED, !$block->isLit()); - }); - $this->map(Blocks::SOUL_FIRE(), function() : Writer{ - return Writer::create(Ids::SOUL_FIRE) - ->writeInt(StateNames::AGE, 0); //useless for soul fire, we don't track it - }); - $this->map(Blocks::SOUL_LANTERN(), function(Lantern $block) : Writer{ - return Writer::create(Ids::SOUL_LANTERN) - ->writeBool(StateNames::HANGING, $block->isHanging()); - }); - $this->map(Blocks::SOUL_TORCH(), function(Torch $block) : Writer{ - return Writer::create(Ids::SOUL_TORCH) - ->writeTorchFacing($block->getFacing()); - }); - $this->map(Blocks::SPONGE(), fn(Sponge $block) => Writer::create($block->isWet() ? Ids::WET_SPONGE : Ids::SPONGE)); - $this->map(Blocks::STONECUTTER(), fn(Stonecutter $block) => Writer::create(Ids::STONECUTTER_BLOCK) - ->writeCardinalHorizontalFacing($block->getFacing())); - $this->mapSlab(Blocks::STONE_BRICK_SLAB(), Ids::STONE_BRICK_SLAB, Ids::STONE_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::STONE_BRICK_STAIRS(), Ids::STONE_BRICK_STAIRS); - $this->map(Blocks::STONE_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::STONE_BRICK_WALL))); - $this->map(Blocks::STONE_BUTTON(), fn(StoneButton $block) => Helper::encodeButton($block, new Writer(Ids::STONE_BUTTON))); - $this->map(Blocks::STONE_PRESSURE_PLATE(), fn(StonePressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::STONE_PRESSURE_PLATE))); - $this->mapSlab(Blocks::STONE_SLAB(), Ids::NORMAL_STONE_SLAB, Ids::NORMAL_STONE_DOUBLE_SLAB); - $this->mapStairs(Blocks::STONE_STAIRS(), Ids::NORMAL_STONE_STAIRS); - $this->map(Blocks::SUGARCANE(), function(Sugarcane $block) : Writer{ - return Writer::create(Ids::REEDS) - ->writeInt(StateNames::AGE, $block->getAge()); - }); - $this->map(Blocks::SUNFLOWER(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::SUNFLOWER))); - $this->map(Blocks::SWEET_BERRY_BUSH(), function(SweetBerryBush $block) : Writer{ - return Writer::create(Ids::SWEET_BERRY_BUSH) - ->writeInt(StateNames::GROWTH, $block->getAge()); - }); - $this->map(Blocks::TNT(), fn(TNT $block) => Writer::create($block->worksUnderwater() ? Ids::UNDERWATER_TNT : Ids::TNT) - ->writeBool(StateNames::EXPLODE_BIT, $block->isUnstable()) - ); - $this->map(Blocks::TORCH(), function(Torch $block) : Writer{ - return Writer::create(Ids::TORCH) - ->writeTorchFacing($block->getFacing()); - }); - $this->map(Blocks::TORCHFLOWER_CROP(), function(TorchflowerCrop $block){ - return Writer::create(Ids::TORCHFLOWER_CROP) - ->writeInt(StateNames::GROWTH, $block->isReady() ? 1 : 0); - }); - $this->map(Blocks::TRAPPED_CHEST(), function(TrappedChest $block) : Writer{ - return Writer::create(Ids::TRAPPED_CHEST) - ->writeCardinalHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::TRIPWIRE(), function(Tripwire $block) : Writer{ - return Writer::create(Ids::TRIP_WIRE) - ->writeBool(StateNames::ATTACHED_BIT, $block->isConnected()) - ->writeBool(StateNames::DISARMED_BIT, $block->isDisarmed()) - ->writeBool(StateNames::POWERED_BIT, $block->isTriggered()) - ->writeBool(StateNames::SUSPENDED_BIT, $block->isSuspended()); - }); - $this->map(Blocks::TRIPWIRE_HOOK(), function(TripwireHook $block) : Writer{ - return Writer::create(Ids::TRIPWIRE_HOOK) - ->writeBool(StateNames::ATTACHED_BIT, $block->isConnected()) - ->writeBool(StateNames::POWERED_BIT, $block->isPowered()) - ->writeLegacyHorizontalFacing($block->getFacing()); - }); - $this->mapSlab(Blocks::TUFF_BRICK_SLAB(), Ids::TUFF_BRICK_SLAB, Ids::TUFF_BRICK_DOUBLE_SLAB); - $this->mapStairs(Blocks::TUFF_BRICK_STAIRS(), Ids::TUFF_BRICK_STAIRS); - $this->map(Blocks::TUFF_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::TUFF_BRICK_WALL))); - $this->mapSlab(Blocks::TUFF_SLAB(), Ids::TUFF_SLAB, Ids::TUFF_DOUBLE_SLAB); - $this->mapStairs(Blocks::TUFF_STAIRS(), Ids::TUFF_STAIRS); - $this->map(Blocks::TUFF_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::TUFF_WALL))); - $this->map(Blocks::TWISTING_VINES(), function(NetherVines $block) : Writer{ - return Writer::create(Ids::TWISTING_VINES) - ->writeInt(StateNames::TWISTING_VINES_AGE, $block->getAge()); - }); - $this->map(Blocks::UNDERWATER_TORCH(), function(UnderwaterTorch $block) : Writer{ - return Writer::create(Ids::UNDERWATER_TORCH) - ->writeTorchFacing($block->getFacing()); - }); - $this->map(Blocks::VINES(), function(Vine $block) : Writer{ - return Writer::create(Ids::VINE) - ->writeInt(StateNames::VINE_DIRECTION_BITS, ($block->hasFace(Facing::NORTH) ? BlockLegacyMetadata::VINE_FLAG_NORTH : 0) | ($block->hasFace(Facing::SOUTH) ? BlockLegacyMetadata::VINE_FLAG_SOUTH : 0) | ($block->hasFace(Facing::WEST) ? BlockLegacyMetadata::VINE_FLAG_WEST : 0) | ($block->hasFace(Facing::EAST) ? BlockLegacyMetadata::VINE_FLAG_EAST : 0)); - }); - $this->map(Blocks::WALL_BANNER(), function(WallBanner $block) : Writer{ - return Writer::create(Ids::WALL_BANNER) - ->writeHorizontalFacing($block->getFacing()); - }); - $this->map(Blocks::WATER(), fn(Water $block) => Helper::encodeLiquid($block, Ids::WATER, Ids::FLOWING_WATER)); - $this->map(Blocks::WEEPING_VINES(), function(NetherVines $block) : Writer{ - return Writer::create(Ids::WEEPING_VINES) - ->writeInt(StateNames::WEEPING_VINES_AGE, $block->getAge()); - }); - $this->map(Blocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), function(WeightedPressurePlateHeavy $block) : Writer{ - return Writer::create(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE) - ->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()); - }); - $this->map(Blocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), function(WeightedPressurePlateLight $block) : Writer{ - return Writer::create(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE) - ->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()); - }); - $this->map(Blocks::WHEAT(), fn(Wheat $block) => Helper::encodeCrops($block, new Writer(Ids::WHEAT))); + return $result instanceof Writer ? $result->getBlockStateData() : $result; } } diff --git a/src/data/bedrock/block/convert/BlockSerializerDeserializerRegistrar.php b/src/data/bedrock/block/convert/BlockSerializerDeserializerRegistrar.php new file mode 100644 index 000000000..02491bae6 --- /dev/null +++ b/src/data/bedrock/block/convert/BlockSerializerDeserializerRegistrar.php @@ -0,0 +1,237 @@ +> $components + * + * @return string[][] + * @phpstan-return list> + */ + private static function compileFlattenedIdPartMatrix(array $components) : array{ + $result = []; + foreach($components as $component){ + $column = is_string($component) ? [$component] : $component->getPossibleValues(); + + if(count($result) === 0){ + $result = array_map(fn($value) => [$value], $column); + }else{ + $stepResult = []; + foreach($result as $parts){ + foreach($column as $value){ + $stepPart = $parts; + $stepPart[] = $value; + $stepResult[] = $stepPart; + } + } + + $result = $stepResult; + } + } + + return $result; + } + + /** + * @param string[]|StringProperty[] $idComponents + * + * @phpstan-template TBlock of Block + * + * @phpstan-param TBlock $block + * @phpstan-param list> $idComponents + */ + private static function serializeFlattenedId(Block $block, array $idComponents) : string{ + $id = ""; + foreach($idComponents as $infix){ + $id .= is_string($infix) ? $infix : $infix->serializePlain($block); + } + return $id; + } + + /** + * @param string[]|StringProperty[] $idComponents + * @param string[] $idPropertyValues + * + * @phpstan-template TBlock of Block + * + * @phpstan-param TBlock $baseBlock + * @phpstan-param list> $idComponents + * @phpstan-param list $idPropertyValues + * + * @phpstan-return TBlock + */ + private static function deserializeFlattenedId(Block $baseBlock, array $idComponents, array $idPropertyValues) : Block{ + $preparedBlock = clone $baseBlock; + foreach($idComponents as $k => $component){ + if($component instanceof StringProperty){ + $fakeValue = $idPropertyValues[$k]; + $component->deserializePlain($preparedBlock, $fakeValue); + } + } + + return $preparedBlock; + } + + public function mapSimple(Block $block, string $id) : void{ + $this->deserializer->mapSimple($id, fn() => clone $block); + $this->serializer->mapSimple($block, $id); + } + + /** + * @phpstan-template TBlock of Block + * @phpstan-param FlattenedIdModel $model + */ + public function mapFlattenedId(FlattenedIdModel $model) : void{ + $block = $model->getBlock(); + + $idComponents = $model->getIdComponents(); + if(count($idComponents) === 0){ + throw new \InvalidArgumentException("No ID components provided"); + } + $properties = $model->getProperties(); + + //This is a really cursed hack that lets us essentially write flattened properties as blockstate properties, and + //then pull them out to compile an ID :D + //This works surprisingly well and is much more elegant than I would've expected + + if(count($properties) > 0){ + $this->serializer->map($block, function(Block $block) use ($idComponents, $properties) : Writer{ + $id = self::serializeFlattenedId($block, $idComponents); + + $writer = new Writer($id); + foreach($properties as $property){ + $property->serialize($block, $writer); + } + + return $writer; + }); + }else{ + $this->serializer->map($block, function(Block $block) use ($idComponents) : BlockStateData{ + //fast path for blocks with no state properties + $id = self::serializeFlattenedId($block, $idComponents); + return BlockStateData::current($id, []); + }); + } + + $idPermutations = self::compileFlattenedIdPartMatrix($idComponents); + foreach($idPermutations as $idParts){ + //deconstruct the ID into a partial state + //we can do this at registration time since there will be multiple deserializers + $preparedBlock = self::deserializeFlattenedId($block, $idComponents, $idParts); + $id = implode("", $idParts); + + if(count($properties) > 0){ + $this->deserializer->map($id, function(Reader $reader) use ($preparedBlock, $properties) : Block{ + $block = clone $preparedBlock; + + foreach($properties as $property){ + $property->deserialize($block, $reader); + } + return $block; + }); + }else{ + //fast path for blocks with no state properties + $this->deserializer->map($id, fn() => clone $preparedBlock); + } + } + } + + /** + * @phpstan-template TBlock of Block&Colored + * @phpstan-param TBlock $block + */ + public function mapColored(Block $block, string $idPrefix, string $idSuffix) : void{ + $this->mapFlattenedId(FlattenedIdModel::create($block) + ->idComponents([ + $idPrefix, + CommonProperties::getInstance()->dyeColorIdInfix, + $idSuffix + ]) + ); + } + + public function mapSlab(Slab $block, string $type) : void{ + $commonProperties = CommonProperties::getInstance(); + $this->mapFlattenedId(FlattenedIdModel::create($block) + ->idComponents(["minecraft:", $type, "_", $commonProperties->slabIdInfix, "slab"]) + ->properties([$commonProperties->slabPositionProperty]) + ); + } + + public function mapStairs(Stair $block, string $id) : void{ + $this->mapModel(Model::create($block, $id)->properties(CommonProperties::getInstance()->stairProperties)); + } + + /** + * @phpstan-template TBlock of Block + * @phpstan-param Model $model + */ + public function mapModel(Model $model) : void{ + $id = $model->getId(); + $block = $model->getBlock(); + $propertyDescriptors = $model->getProperties(); + + $this->deserializer->map($id, static function(Reader $in) use ($block, $propertyDescriptors) : Block{ + $newBlock = clone $block; + foreach($propertyDescriptors as $descriptor){ + $descriptor->deserialize($newBlock, $in); + } + return $newBlock; + }); + $this->serializer->map($block, static function(Block $block) use ($id, $propertyDescriptors) : Writer{ + $writer = new Writer($id); + foreach($propertyDescriptors as $descriptor){ + $descriptor->serialize($block, $writer); + } + return $writer; + }); + } +} diff --git a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php index fda0455aa..1d48ec76f 100644 --- a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php @@ -56,11 +56,13 @@ use pocketmine\data\bedrock\block\BlockLegacyMetadata; use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\data\bedrock\block\BlockStateNames; use pocketmine\data\bedrock\block\BlockStateNames as StateNames; +use pocketmine\data\bedrock\block\convert\property\ValueMappings; use pocketmine\data\bedrock\MushroomBlockTypeIdMap; -use pocketmine\math\Axis; use pocketmine\math\Facing; -use pocketmine\utils\AssumptionFailedError; +/** + * @deprecated + */ final class BlockStateDeserializerHelper{ /** @throws BlockStateDeserializeException */ @@ -70,7 +72,14 @@ final class BlockStateDeserializerHelper{ ->setPressed($in->readBool(BlockStateNames::BUTTON_PRESSED_BIT)); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @phpstan-template TCandle of Candle + * @phpstan-param TCandle $block + * @phpstan-return TCandle + * + * @throws BlockStateDeserializeException + */ public static function decodeCandle(Candle $block, BlockStateReader $in) : Candle{ return $block ->setCount($in->readBoundedInt(StateNames::CANDLES, 0, 3) + 1) @@ -97,6 +106,7 @@ final class BlockStateDeserializerHelper{ } /** + * @deprecated * @phpstan-template TBlock of CopperMaterial * * @phpstan-param TBlock $block @@ -109,6 +119,7 @@ final class BlockStateDeserializerHelper{ } /** + * @deprecated * @phpstan-template TBlock of CopperMaterial * * @phpstan-param TBlock $block @@ -126,12 +137,20 @@ final class BlockStateDeserializerHelper{ ->setOutputSignalStrength($in->readBoundedInt(BlockStateNames::REDSTONE_SIGNAL, 0, 15)); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @phpstan-template TDoor of Door + * @phpstan-param TDoor $block + * @phpstan-return TDoor + * + * @throws BlockStateDeserializeException + */ public static function decodeDoor(Door $block, BlockStateReader $in) : Door{ //TODO: check if these need any special treatment to get the appropriate data to both halves of the door return $block ->setTop($in->readBool(BlockStateNames::UPPER_BLOCK_BIT)) - ->setFacing(Facing::rotateY($in->readLegacyHorizontalFacing(), false)) + //a door facing "east" is actually facing north - thanks mojang + ->setFacing(Facing::rotateY($in->readCardinalHorizontalFacing(), clockwise: false)) ->setHingeRight($in->readBool(BlockStateNames::DOOR_HINGE_BIT)) ->setOpen($in->readBool(BlockStateNames::OPEN_BIT)); } @@ -142,25 +161,30 @@ final class BlockStateDeserializerHelper{ ->setTop($in->readBool(BlockStateNames::UPPER_BLOCK_BIT)); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public static function decodeFenceGate(FenceGate $block, BlockStateReader $in) : FenceGate{ return $block - ->setFacing($in->readLegacyHorizontalFacing()) + ->setFacing($in->readCardinalHorizontalFacing()) ->setInWall($in->readBool(BlockStateNames::IN_WALL_BIT)) ->setOpen($in->readBool(BlockStateNames::OPEN_BIT)); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public static function decodeFloorCoralFan(FloorCoralFan $block, BlockStateReader $in) : FloorCoralFan{ return $block - ->setAxis(match($in->readBoundedInt(BlockStateNames::CORAL_FAN_DIRECTION, 0, 1)){ - 0 => Axis::X, - 1 => Axis::Z, - default => throw new AssumptionFailedError("readBoundedInt() should have prevented this"), - }); + ->setAxis($in->mapIntFromInt(BlockStateNames::CORAL_FAN_DIRECTION, ValueMappings::getInstance()->coralAxis)); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public static function decodeFloorSign(FloorSign $block, BlockStateReader $in) : FloorSign{ return $block ->setRotation($in->readBoundedInt(BlockStateNames::GROUND_SIGN_DIRECTION, 0, 15)); @@ -173,7 +197,10 @@ final class BlockStateDeserializerHelper{ ->setHasMap($in->readBool(StateNames::ITEM_FRAME_MAP_BIT)); } - /** @throws BlockStateDeserializeException */ + /** + * @throws BlockStateDeserializeException + * @deprecated + */ public static function decodeLeaves(Leaves $block, BlockStateReader $in) : Leaves{ return $block ->setNoDecay($in->readBool(StateNames::PERSISTENT_BIT)) @@ -223,7 +250,10 @@ final class BlockStateDeserializerHelper{ ->setDelay($in->readBoundedInt(BlockStateNames::REPEATER_DELAY, 0, 3) + 1); } - /** @throws BlockStateDeserializeException */ + /** + * @throws BlockStateDeserializeException + * @deprecated + */ public static function decodeSapling(Sapling $block, BlockStateReader $in) : Sapling{ return $block ->setReady($in->readBool(BlockStateNames::AGE_BIT)); @@ -236,18 +266,37 @@ final class BlockStateDeserializerHelper{ return $block->setPressed($in->readBoundedInt(BlockStateNames::REDSTONE_SIGNAL, 0, 15) !== 0); } - /** @throws BlockStateDeserializeException */ + /** + * @phpstan-template TSlab of Slab + * @phpstan-param TSlab $block + * @phpstan-return TSlab + * + * @throws BlockStateDeserializeException + */ public static function decodeSingleSlab(Slab $block, BlockStateReader $in) : Slab{ return $block->setSlabType($in->readSlabPosition()); } - /** @throws BlockStateDeserializeException */ + /** + * @phpstan-template TSlab of Slab + * @phpstan-param TSlab $block + * @phpstan-return TSlab + * + * @throws BlockStateDeserializeException + */ public static function decodeDoubleSlab(Slab $block, BlockStateReader $in) : Slab{ $in->ignored(StateNames::MC_VERTICAL_HALF); return $block->setSlabType(SlabType::DOUBLE); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @phpstan-template TStair of Stair + * @phpstan-param TStair $block + * @phpstan-return TStair + * + * @throws BlockStateDeserializeException + */ public static function decodeStairs(Stair $block, BlockStateReader $in) : Stair{ return $block ->setUpsideDown($in->readBool(BlockStateNames::UPSIDE_DOWN_BIT)) @@ -264,7 +313,14 @@ final class BlockStateDeserializerHelper{ ->setFacing($facing === Facing::DOWN ? Facing::UP : $facing); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @phpstan-template TTrapdoor of Trapdoor + * @phpstan-param TTrapdoor $block + * @phpstan-return TTrapdoor + * + * @throws BlockStateDeserializeException + */ public static function decodeTrapdoor(Trapdoor $block, BlockStateReader $in) : Trapdoor{ return $block ->setFacing($in->read5MinusHorizontalFacing()) @@ -283,12 +339,19 @@ final class BlockStateDeserializerHelper{ return $block; } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public static function decodeWallSign(WallSign $block, BlockStateReader $in) : WallSign{ return $block ->setFacing($in->readHorizontalFacing()); } + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public static function decodeWeightedPressurePlate(WeightedPressurePlate $block, BlockStateReader $in) : WeightedPressurePlate{ return $block ->setOutputSignalStrength($in->readBoundedInt(BlockStateNames::REDSTONE_SIGNAL, 0, 15)); diff --git a/src/data/bedrock/block/convert/BlockStateReader.php b/src/data/bedrock/block/convert/BlockStateReader.php index e3a02885f..4d09d2f85 100644 --- a/src/data/bedrock/block/convert/BlockStateReader.php +++ b/src/data/bedrock/block/convert/BlockStateReader.php @@ -31,6 +31,9 @@ use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\data\bedrock\block\BlockStateNames; use pocketmine\data\bedrock\block\BlockStateStringValues as StringValues; +use pocketmine\data\bedrock\block\convert\property\EnumFromRawStateMap; +use pocketmine\data\bedrock\block\convert\property\IntFromRawStateMap; +use pocketmine\data\bedrock\block\convert\property\ValueMappings; use pocketmine\math\Axis; use pocketmine\math\Facing; use pocketmine\nbt\tag\ByteTag; @@ -112,45 +115,45 @@ final class BlockStateReader{ } /** - * @param int[] $mapping - * @phpstan-param array $mapping - * @phpstan-return int + * @deprecated + * @phpstan-param IntFromRawStateMap $map * @throws BlockStateDeserializeException */ - private function parseFacingValue(int $value, array $mapping) : int{ - $result = $mapping[$value] ?? null; - if($result === null){ - throw new BlockStateDeserializeException("Unmapped facing value " . $value); - } - return $result; - } + public function mapIntFromString(string $name, IntFromRawStateMap $map) : int{ + $raw = $this->readString($name); - /** @throws BlockStateDeserializeException */ - public function readFacingDirection() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::FACING_DIRECTION), [ - 0 => Facing::DOWN, - 1 => Facing::UP, - 2 => Facing::NORTH, - 3 => Facing::SOUTH, - 4 => Facing::WEST, - 5 => Facing::EAST - ]); - } - - /** @throws BlockStateDeserializeException */ - public function readBlockFace() : int{ - return match($raw = $this->readString(BlockStateNames::MC_BLOCK_FACE)){ - StringValues::MC_BLOCK_FACE_DOWN => Facing::DOWN, - StringValues::MC_BLOCK_FACE_UP => Facing::UP, - StringValues::MC_BLOCK_FACE_NORTH => Facing::NORTH, - StringValues::MC_BLOCK_FACE_SOUTH => Facing::SOUTH, - StringValues::MC_BLOCK_FACE_WEST => Facing::WEST, - StringValues::MC_BLOCK_FACE_EAST => Facing::EAST, - default => throw $this->badValueException(BlockStateNames::MC_BLOCK_FACE, $raw) - }; + return $map->rawToValue($raw) ?? throw $this->badValueException($name, $raw); } /** + * @deprecated + * @phpstan-param IntFromRawStateMap $map + * @throws BlockStateDeserializeException + */ + public function mapIntFromInt(string $name, IntFromRawStateMap $map) : int{ + $raw = $this->readInt($name); + + return $map->rawToValue($raw) ?? throw $this->badValueException($name, (string) $raw); + } + + /** + * @deprecated + * @throws BlockStateDeserializeException + */ + public function readFacingDirection() : int{ + return $this->mapIntFromInt(BlockStateNames::FACING_DIRECTION, ValueMappings::getInstance()->facing); + } + + /** + * @deprecated + * @throws BlockStateDeserializeException + */ + public function readBlockFace() : int{ + return $this->mapIntFromString(BlockStateNames::MC_BLOCK_FACE, ValueMappings::getInstance()->blockFace); + } + + /** + * @deprecated * @return int[] * @phpstan-return array */ @@ -173,82 +176,69 @@ final class BlockStateReader{ return $result; } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readEndRodFacingDirection() : int{ $result = $this->readFacingDirection(); return Facing::axis($result) !== Axis::Y ? Facing::opposite($result) : $result; } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readHorizontalFacing() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::FACING_DIRECTION), [ - 0 => Facing::NORTH, //should be illegal, but 1.13 allows it - 1 => Facing::NORTH, //also should be illegal - 2 => Facing::NORTH, - 3 => Facing::SOUTH, - 4 => Facing::WEST, - 5 => Facing::EAST - ]); - } - - /** @throws BlockStateDeserializeException */ - public function readWeirdoHorizontalFacing() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::WEIRDO_DIRECTION), [ - 0 => Facing::EAST, - 1 => Facing::WEST, - 2 => Facing::SOUTH, - 3 => Facing::NORTH - ]); - } - - /** @throws BlockStateDeserializeException */ - public function readLegacyHorizontalFacing() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::DIRECTION), [ - 0 => Facing::SOUTH, - 1 => Facing::WEST, - 2 => Facing::NORTH, - 3 => Facing::EAST - ]); + return $this->mapIntFromInt(BlockStateNames::FACING_DIRECTION, ValueMappings::getInstance()->horizontalFacingClassic); } /** + * @deprecated + * @throws BlockStateDeserializeException + */ + public function readWeirdoHorizontalFacing() : int{ + return $this->mapIntFromInt(BlockStateNames::WEIRDO_DIRECTION, ValueMappings::getInstance()->horizontalFacing5Minus); + } + + /** + * @deprecated + * @throws BlockStateDeserializeException + */ + public function readLegacyHorizontalFacing() : int{ + return $this->mapIntFromInt(BlockStateNames::DIRECTION, ValueMappings::getInstance()->horizontalFacingSWNE); + } + + /** + * @deprecated * This is for trapdoors, because Mojang botched the conversion in 1.13 * @throws BlockStateDeserializeException */ public function read5MinusHorizontalFacing() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::DIRECTION), [ - 0 => Facing::EAST, - 1 => Facing::WEST, - 2 => Facing::SOUTH, - 3 => Facing::NORTH - ]); + return $this->mapIntFromInt(BlockStateNames::DIRECTION, ValueMappings::getInstance()->horizontalFacing5Minus); } /** + * @deprecated * Used by pumpkins as of 1.20.0.23 beta * @throws BlockStateDeserializeException */ public function readCardinalHorizontalFacing() : int{ - return match($raw = $this->readString(BlockStateNames::MC_CARDINAL_DIRECTION)){ - StringValues::MC_CARDINAL_DIRECTION_NORTH => Facing::NORTH, - StringValues::MC_CARDINAL_DIRECTION_SOUTH => Facing::SOUTH, - StringValues::MC_CARDINAL_DIRECTION_WEST => Facing::WEST, - StringValues::MC_CARDINAL_DIRECTION_EAST => Facing::EAST, - default => throw $this->badValueException(BlockStateNames::MC_CARDINAL_DIRECTION, $raw) - }; + return $this->mapIntFromString(BlockStateNames::MC_CARDINAL_DIRECTION, ValueMappings::getInstance()->cardinalDirection); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readCoralFacing() : int{ - return $this->parseFacingValue($this->readInt(BlockStateNames::CORAL_DIRECTION), [ - 0 => Facing::WEST, - 1 => Facing::EAST, - 2 => Facing::NORTH, - 3 => Facing::SOUTH - ]); + return $this->mapIntFromInt(BlockStateNames::CORAL_DIRECTION, ValueMappings::getInstance()->horizontalFacingCoral); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readFacingWithoutDown() : int{ $result = $this->readFacingDirection(); if($result === Facing::DOWN){ //shouldn't be legal, but 1.13 allows it @@ -257,6 +247,10 @@ final class BlockStateReader{ return $result; } + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readFacingWithoutUp() : int{ $result = $this->readFacingDirection(); if($result === Facing::UP){ @@ -266,23 +260,17 @@ final class BlockStateReader{ } /** - * @phpstan-return Axis::* + * @deprecated * @throws BlockStateDeserializeException */ public function readPillarAxis() : int{ - $rawValue = $this->readString(BlockStateNames::PILLAR_AXIS); - $value = [ - StringValues::PILLAR_AXIS_X => Axis::X, - StringValues::PILLAR_AXIS_Y => Axis::Y, - StringValues::PILLAR_AXIS_Z => Axis::Z - ][$rawValue] ?? null; - if($value === null){ - throw $this->badValueException(BlockStateNames::PILLAR_AXIS, $rawValue, "Invalid axis value"); - } - return $value; + return $this->mapIntFromString(BlockStateNames::PILLAR_AXIS, ValueMappings::getInstance()->pillarAxis); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readSlabPosition() : SlabType{ return match($rawValue = $this->readString(BlockStateNames::MC_VERTICAL_HALF)){ StringValues::MC_VERTICAL_HALF_BOTTOM => SlabType::BOTTOM, @@ -292,34 +280,25 @@ final class BlockStateReader{ } /** - * @phpstan-return Facing::UP|Facing::NORTH|Facing::SOUTH|Facing::WEST|Facing::EAST + * @deprecated * @throws BlockStateDeserializeException */ public function readTorchFacing() : int{ - //TODO: horizontal directions are flipped (MCPE bug: https://bugs.mojang.com/browse/MCPE-152036) - return match($rawValue = $this->readString(BlockStateNames::TORCH_FACING_DIRECTION)){ - StringValues::TORCH_FACING_DIRECTION_EAST => Facing::WEST, - StringValues::TORCH_FACING_DIRECTION_NORTH => Facing::SOUTH, - StringValues::TORCH_FACING_DIRECTION_SOUTH => Facing::NORTH, - StringValues::TORCH_FACING_DIRECTION_TOP => Facing::UP, - StringValues::TORCH_FACING_DIRECTION_UNKNOWN => Facing::UP, //should be illegal, but 1.13 allows it - StringValues::TORCH_FACING_DIRECTION_WEST => Facing::EAST, - default => throw $this->badValueException(BlockStateNames::TORCH_FACING_DIRECTION, $rawValue, "Invalid torch facing"), - }; + return $this->mapIntFromString(BlockStateNames::TORCH_FACING_DIRECTION, ValueMappings::getInstance()->torchFacing); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readBellAttachmentType() : BellAttachmentType{ - return match($type = $this->readString(BlockStateNames::ATTACHMENT)){ - StringValues::ATTACHMENT_HANGING => BellAttachmentType::CEILING, - StringValues::ATTACHMENT_STANDING => BellAttachmentType::FLOOR, - StringValues::ATTACHMENT_SIDE => BellAttachmentType::ONE_WALL, - StringValues::ATTACHMENT_MULTIPLE => BellAttachmentType::TWO_WALLS, - default => throw $this->badValueException(BlockStateNames::ATTACHMENT, $type), - }; + return $this->readUnitEnum(BlockStateNames::ATTACHMENT, ValueMappings::getInstance()->bellAttachmentType); } - /** @throws BlockStateDeserializeException */ + /** + * @deprecated + * @throws BlockStateDeserializeException + */ public function readWallConnectionType(string $name) : ?WallConnectionType{ return match($type = $this->readString($name)){ //TODO: this looks a bit confusing due to use of EAST, but the values are the same for all connections @@ -332,6 +311,23 @@ final class BlockStateReader{ }; } + /** + * @deprecated + * @phpstan-template TEnum of \UnitEnum + * @phpstan-param EnumFromRawStateMap $map + * @phpstan-return TEnum + * @throws BlockStateDeserializeException + */ + public function readUnitEnum(string $name, EnumFromRawStateMap $map) : \UnitEnum{ + $value = $this->readString($name); + + $mapped = $map->rawToValue($value); + if($mapped === null){ + throw $this->badValueException($name, $value); + } + return $mapped; + } + /** * Explicitly mark a property as unused, so it doesn't get flagged as an error when debug mode is enabled */ diff --git a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php index 3e2215746..da3dbb387 100644 --- a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php @@ -55,6 +55,9 @@ use pocketmine\data\bedrock\block\convert\BlockStateWriter as Writer; use pocketmine\data\bedrock\MushroomBlockTypeIdMap; use pocketmine\math\Facing; +/** + * @deprecated + */ final class BlockStateSerializerHelper{ public static function encodeButton(Button $block, Writer $out) : Writer{ return $out @@ -77,6 +80,9 @@ final class BlockStateSerializerHelper{ return $out->writeInt(BlockStateNames::GROWTH, $block->getAge()); } + /** + * @deprecated + */ public static function encodeTorch(Torch $block, Writer $out) : Writer{ return $out ->writeTorchFacing($block->getFacing()); @@ -97,10 +103,14 @@ final class BlockStateSerializerHelper{ }; } + /** + * @deprecated + */ public static function encodeDoor(Door $block, Writer $out) : Writer{ return $out ->writeBool(BlockStateNames::UPPER_BLOCK_BIT, $block->isTop()) - ->writeLegacyHorizontalFacing(Facing::rotateY($block->getFacing(), true)) + //a door facing north is encoded as "east" + ->writeCardinalHorizontalFacing(Facing::rotateY($block->getFacing(), clockwise: true)) ->writeBool(BlockStateNames::DOOR_HINGE_BIT, $block->isHingeRight()) ->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen()); } @@ -110,13 +120,19 @@ final class BlockStateSerializerHelper{ ->writeBool(BlockStateNames::UPPER_BLOCK_BIT, $block->isTop()); } + /** + * @deprecated + */ public static function encodeFenceGate(FenceGate $block, Writer $out) : Writer{ return $out - ->writeLegacyHorizontalFacing($block->getFacing()) + ->writeCardinalHorizontalFacing($block->getFacing()) ->writeBool(BlockStateNames::IN_WALL_BIT, $block->isInWall()) ->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen()); } + /** + * @deprecated + */ public static function encodeFloorSign(FloorSign $block, Writer $out) : Writer{ return $out ->writeInt(BlockStateNames::GROUND_SIGN_DIRECTION, $block->getRotation()); @@ -134,6 +150,9 @@ final class BlockStateSerializerHelper{ ->writeFacingDirection($block->getFacing()); } + /** + * @deprecated + */ public static function encodeLeaves(Leaves $block, Writer $out) : Writer{ return $out ->writeBool(BlockStateNames::PERSISTENT_BIT, $block->isNoDecay()) @@ -158,11 +177,17 @@ final class BlockStateSerializerHelper{ ->writeInt(BlockStateNames::HUGE_MUSHROOM_BITS, MushroomBlockTypeIdMap::getInstance()->toId($block->getMushroomBlockType())); } + /** + * @deprecated + */ public static function encodeQuartz(int $axis, Writer $out) : Writer{ return $out ->writePillarAxis($axis); //this isn't needed for all types, but we have to write it anyway } + /** + * @deprecated + */ public static function encodeSapling(Sapling $block, Writer $out) : Writer{ return $out ->writeBool(BlockStateNames::AGE_BIT, $block->isReady()); @@ -192,6 +217,9 @@ final class BlockStateSerializerHelper{ self::encodeSingleSlab($block, $singleId); } + /** + * @deprecated + */ public static function encodeStairs(Stair $block, Writer $out) : Writer{ return $out ->writeBool(BlockStateNames::UPSIDE_DOWN_BIT, $block->isUpsideDown()) @@ -207,6 +235,9 @@ final class BlockStateSerializerHelper{ ->writeFacingWithoutUp($facing === Facing::UP ? Facing::DOWN : $facing); } + /** + * @deprecated + */ public static function encodeTrapdoor(Trapdoor $block, Writer $out) : Writer{ return $out ->write5MinusHorizontalFacing($block->getFacing()) @@ -223,6 +254,9 @@ final class BlockStateSerializerHelper{ ->writeWallConnectionType(BlockStateNames::WALL_CONNECTION_TYPE_WEST, $block->getConnection(Facing::WEST)); } + /** + * @deprecated + */ public static function encodeWallSign(WallSign $block, Writer $out) : Writer{ return $out ->writeHorizontalFacing($block->getFacing()); diff --git a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php index 5c0a427cc..ca5c12412 100644 --- a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php +++ b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php @@ -23,45 +23,18 @@ declare(strict_types=1); namespace pocketmine\data\bedrock\block\convert; -use pocketmine\block\AmethystCluster; -use pocketmine\block\Anvil; -use pocketmine\block\Bamboo; use pocketmine\block\Block; -use pocketmine\block\CaveVines; -use pocketmine\block\ChorusFlower; -use pocketmine\block\DoublePitcherCrop; -use pocketmine\block\Opaque; -use pocketmine\block\PinkPetals; -use pocketmine\block\PitcherCrop; +use pocketmine\block\RuntimeBlockStateRegistry; use pocketmine\block\Slab; use pocketmine\block\Stair; -use pocketmine\block\SweetBerryBush; -use pocketmine\block\utils\BrewingStandSlot; -use pocketmine\block\utils\ChiseledBookshelfSlot; -use pocketmine\block\utils\CopperOxidation; -use pocketmine\block\utils\CoralType; -use pocketmine\block\utils\DirtType; -use pocketmine\block\utils\DripleafState; -use pocketmine\block\utils\DyeColor; -use pocketmine\block\utils\FroglightType; -use pocketmine\block\utils\LeverFacing; -use pocketmine\block\utils\MobHeadType; -use pocketmine\block\VanillaBlocks as Blocks; use pocketmine\block\Wood; -use pocketmine\data\bedrock\block\BlockLegacyMetadata; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\data\bedrock\block\BlockStateDeserializer; -use pocketmine\data\bedrock\block\BlockStateNames as StateNames; -use pocketmine\data\bedrock\block\BlockStateStringValues as StringValues; -use pocketmine\data\bedrock\block\BlockTypeNames as Ids; use pocketmine\data\bedrock\block\convert\BlockStateDeserializerHelper as Helper; use pocketmine\data\bedrock\block\convert\BlockStateReader as Reader; -use pocketmine\math\Axis; -use pocketmine\math\Facing; use function array_key_exists; use function count; -use function min; final class BlockStateToObjectDeserializer implements BlockStateDeserializer{ @@ -77,44 +50,52 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{ */ private array $simpleCache = []; - public function __construct(){ - $this->registerCandleDeserializers(); - $this->registerFlatColorBlockDeserializers(); - $this->registerFlatCoralDeserializers(); - $this->registerCauldronDeserializers(); - $this->registerFlatWoodBlockDeserializers(); - $this->registerLeavesDeserializers(); - $this->registerSaplingDeserializers(); - $this->registerLightDeserializers(); - $this->registerMobHeadDeserializers(); - $this->registerSimpleDeserializers(); - $this->registerDeserializers(); - } - public function deserialize(BlockStateData $stateData) : int{ if(count($stateData->getStates()) === 0){ //if a block has zero properties, we can keep a map of string ID -> internal blockstate ID - return $this->simpleCache[$stateData->getName()] ??= $this->deserializeBlock($stateData)->getStateId(); + return $this->simpleCache[$stateData->getName()] ??= $this->deserializeToStateId($stateData); } //we can't cache blocks that have properties - go ahead and deserialize the slow way - return $this->deserializeBlock($stateData)->getStateId(); + return $this->deserializeToStateId($stateData); + } + + private function deserializeToStateId(BlockStateData $stateData) : int{ + $stateId = $this->deserializeBlock($stateData)->getStateId(); + //plugin devs seem to keep missing this and causing core crashes, so we need to verify this at the earliest + //available opportunity + if(!RuntimeBlockStateRegistry::getInstance()->hasStateId($stateId)){ + throw new \LogicException("State ID $stateId returned by deserializer for " . $stateData->getName() . " is not registered in RuntimeBlockStateRegistry"); + } + return $stateId; } /** @phpstan-param \Closure(Reader) : Block $c */ public function map(string $id, \Closure $c) : void{ - if(array_key_exists($id, $this->deserializeFuncs)){ - throw new \InvalidArgumentException("Deserializer is already assigned for \"$id\""); - } $this->deserializeFuncs[$id] = $c; + $this->simpleCache = []; } - /** @phpstan-param \Closure() : Block $getBlock */ + /** + * Returns the existing data deserializer for the given ID, or null if none exists. + * This may be useful if you need to override a deserializer, but still want to be able to fall back to the original. + * + * @phpstan-return ?\Closure(Reader) : Block + */ + public function getDeserializerForId(string $id) : ?\Closure{ + return $this->deserializeFuncs[$id] ?? null; + } + + /** + * @deprecated + * @phpstan-param \Closure() : Block $getBlock + */ public function mapSimple(string $id, \Closure $getBlock) : void{ $this->map($id, $getBlock); } /** + * @deprecated * @phpstan-param \Closure(Reader) : Slab $getBlock */ public function mapSlab(string $singleId, string $doubleId, \Closure $getBlock) : void{ @@ -123,1669 +104,22 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{ } /** + * @deprecated * @phpstan-param \Closure() : Stair $getBlock */ public function mapStairs(string $id, \Closure $getBlock) : void{ $this->map($id, fn(Reader $in) : Stair => Helper::decodeStairs($getBlock(), $in)); } - /** @phpstan-param \Closure() : Wood $getBlock */ + /** + * @deprecated + * @phpstan-param \Closure() : Wood $getBlock + */ public function mapLog(string $unstrippedId, string $strippedId, \Closure $getBlock) : void{ $this->map($unstrippedId, fn(Reader $in) => Helper::decodeLog($getBlock(), false, $in)); $this->map($strippedId, fn(Reader $in) => Helper::decodeLog($getBlock(), true, $in)); } - private function registerCandleDeserializers() : void{ - $this->map(Ids::CANDLE, fn(Reader $in) => Helper::decodeCandle(Blocks::CANDLE(), $in)); - foreach([ - Ids::BLACK_CANDLE => DyeColor::BLACK, - Ids::BLUE_CANDLE => DyeColor::BLUE, - Ids::BROWN_CANDLE => DyeColor::BROWN, - Ids::CYAN_CANDLE => DyeColor::CYAN, - Ids::GRAY_CANDLE => DyeColor::GRAY, - Ids::GREEN_CANDLE => DyeColor::GREEN, - Ids::LIGHT_BLUE_CANDLE => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_CANDLE => DyeColor::LIGHT_GRAY, - Ids::LIME_CANDLE => DyeColor::LIME, - Ids::MAGENTA_CANDLE => DyeColor::MAGENTA, - Ids::ORANGE_CANDLE => DyeColor::ORANGE, - Ids::PINK_CANDLE => DyeColor::PINK, - Ids::PURPLE_CANDLE => DyeColor::PURPLE, - Ids::RED_CANDLE => DyeColor::RED, - Ids::WHITE_CANDLE => DyeColor::WHITE, - Ids::YELLOW_CANDLE => DyeColor::YELLOW, - ] as $id => $color){ - $this->map($id, fn(Reader $in) => Helper::decodeCandle(Blocks::DYED_CANDLE()->setColor($color), $in)); - } - - $this->map(Ids::CANDLE_CAKE, fn(Reader $in) => Blocks::CAKE_WITH_CANDLE()->setLit($in->readBool(StateNames::LIT))); - foreach([ - Ids::BLACK_CANDLE_CAKE => DyeColor::BLACK, - Ids::BLUE_CANDLE_CAKE => DyeColor::BLUE, - Ids::BROWN_CANDLE_CAKE => DyeColor::BROWN, - Ids::CYAN_CANDLE_CAKE => DyeColor::CYAN, - Ids::GRAY_CANDLE_CAKE => DyeColor::GRAY, - Ids::GREEN_CANDLE_CAKE => DyeColor::GREEN, - Ids::LIGHT_BLUE_CANDLE_CAKE => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_CANDLE_CAKE => DyeColor::LIGHT_GRAY, - Ids::LIME_CANDLE_CAKE => DyeColor::LIME, - Ids::MAGENTA_CANDLE_CAKE => DyeColor::MAGENTA, - Ids::ORANGE_CANDLE_CAKE => DyeColor::ORANGE, - Ids::PINK_CANDLE_CAKE => DyeColor::PINK, - Ids::PURPLE_CANDLE_CAKE => DyeColor::PURPLE, - Ids::RED_CANDLE_CAKE => DyeColor::RED, - Ids::WHITE_CANDLE_CAKE => DyeColor::WHITE, - Ids::YELLOW_CANDLE_CAKE => DyeColor::YELLOW, - ] as $id => $color){ - $this->map($id, fn(Reader $in) => Blocks::CAKE_WITH_DYED_CANDLE() - ->setColor($color) - ->setLit($in->readBool(StateNames::LIT)) - ); - } - } - - private function registerFlatColorBlockDeserializers() : void{ - foreach([ - Ids::HARD_BLACK_STAINED_GLASS => DyeColor::BLACK, - Ids::HARD_BLUE_STAINED_GLASS => DyeColor::BLUE, - Ids::HARD_BROWN_STAINED_GLASS => DyeColor::BROWN, - Ids::HARD_CYAN_STAINED_GLASS => DyeColor::CYAN, - Ids::HARD_GRAY_STAINED_GLASS => DyeColor::GRAY, - Ids::HARD_GREEN_STAINED_GLASS => DyeColor::GREEN, - Ids::HARD_LIGHT_BLUE_STAINED_GLASS => DyeColor::LIGHT_BLUE, - Ids::HARD_LIGHT_GRAY_STAINED_GLASS => DyeColor::LIGHT_GRAY, - Ids::HARD_LIME_STAINED_GLASS => DyeColor::LIME, - Ids::HARD_MAGENTA_STAINED_GLASS => DyeColor::MAGENTA, - Ids::HARD_ORANGE_STAINED_GLASS => DyeColor::ORANGE, - Ids::HARD_PINK_STAINED_GLASS => DyeColor::PINK, - Ids::HARD_PURPLE_STAINED_GLASS => DyeColor::PURPLE, - Ids::HARD_RED_STAINED_GLASS => DyeColor::RED, - Ids::HARD_WHITE_STAINED_GLASS => DyeColor::WHITE, - Ids::HARD_YELLOW_STAINED_GLASS => DyeColor::YELLOW, - ] as $id => $color){ - $this->map($id, fn(Reader $in) => Blocks::STAINED_HARDENED_GLASS()->setColor($color)); - } - - foreach([ - Ids::HARD_BLACK_STAINED_GLASS_PANE => DyeColor::BLACK, - Ids::HARD_BLUE_STAINED_GLASS_PANE => DyeColor::BLUE, - Ids::HARD_BROWN_STAINED_GLASS_PANE => DyeColor::BROWN, - Ids::HARD_CYAN_STAINED_GLASS_PANE => DyeColor::CYAN, - Ids::HARD_GRAY_STAINED_GLASS_PANE => DyeColor::GRAY, - Ids::HARD_GREEN_STAINED_GLASS_PANE => DyeColor::GREEN, - Ids::HARD_LIGHT_BLUE_STAINED_GLASS_PANE => DyeColor::LIGHT_BLUE, - Ids::HARD_LIGHT_GRAY_STAINED_GLASS_PANE => DyeColor::LIGHT_GRAY, - Ids::HARD_LIME_STAINED_GLASS_PANE => DyeColor::LIME, - Ids::HARD_MAGENTA_STAINED_GLASS_PANE => DyeColor::MAGENTA, - Ids::HARD_ORANGE_STAINED_GLASS_PANE => DyeColor::ORANGE, - Ids::HARD_PINK_STAINED_GLASS_PANE => DyeColor::PINK, - Ids::HARD_PURPLE_STAINED_GLASS_PANE => DyeColor::PURPLE, - Ids::HARD_RED_STAINED_GLASS_PANE => DyeColor::RED, - Ids::HARD_WHITE_STAINED_GLASS_PANE => DyeColor::WHITE, - Ids::HARD_YELLOW_STAINED_GLASS_PANE => DyeColor::YELLOW, - ] as $id => $color){ - $this->map($id, fn(Reader $in) => Blocks::STAINED_HARDENED_GLASS_PANE()->setColor($color)); - } - - foreach([ - Ids::BLACK_GLAZED_TERRACOTTA => DyeColor::BLACK, - Ids::BLUE_GLAZED_TERRACOTTA => DyeColor::BLUE, - Ids::BROWN_GLAZED_TERRACOTTA => DyeColor::BROWN, - Ids::CYAN_GLAZED_TERRACOTTA => DyeColor::CYAN, - Ids::GRAY_GLAZED_TERRACOTTA => DyeColor::GRAY, - Ids::GREEN_GLAZED_TERRACOTTA => DyeColor::GREEN, - Ids::LIGHT_BLUE_GLAZED_TERRACOTTA => DyeColor::LIGHT_BLUE, - Ids::SILVER_GLAZED_TERRACOTTA => DyeColor::LIGHT_GRAY, - Ids::LIME_GLAZED_TERRACOTTA => DyeColor::LIME, - Ids::MAGENTA_GLAZED_TERRACOTTA => DyeColor::MAGENTA, - Ids::ORANGE_GLAZED_TERRACOTTA => DyeColor::ORANGE, - Ids::PINK_GLAZED_TERRACOTTA => DyeColor::PINK, - Ids::PURPLE_GLAZED_TERRACOTTA => DyeColor::PURPLE, - Ids::RED_GLAZED_TERRACOTTA => DyeColor::RED, - Ids::WHITE_GLAZED_TERRACOTTA => DyeColor::WHITE, - Ids::YELLOW_GLAZED_TERRACOTTA => DyeColor::YELLOW, - ] as $id => $color){ - $this->map($id, fn(Reader $in) => Blocks::GLAZED_TERRACOTTA() - ->setColor($color) - ->setFacing($in->readHorizontalFacing()) - ); - } - - foreach([ - Ids::BLACK_WOOL => DyeColor::BLACK, - Ids::BLUE_WOOL => DyeColor::BLUE, - Ids::BROWN_WOOL => DyeColor::BROWN, - Ids::CYAN_WOOL => DyeColor::CYAN, - Ids::GRAY_WOOL => DyeColor::GRAY, - Ids::GREEN_WOOL => DyeColor::GREEN, - Ids::LIGHT_BLUE_WOOL => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_WOOL => DyeColor::LIGHT_GRAY, - Ids::LIME_WOOL => DyeColor::LIME, - Ids::MAGENTA_WOOL => DyeColor::MAGENTA, - Ids::ORANGE_WOOL => DyeColor::ORANGE, - Ids::PINK_WOOL => DyeColor::PINK, - Ids::PURPLE_WOOL => DyeColor::PURPLE, - Ids::RED_WOOL => DyeColor::RED, - Ids::WHITE_WOOL => DyeColor::WHITE, - Ids::YELLOW_WOOL => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::WOOL()->setColor($color)); - } - - foreach([ - Ids::BLACK_CARPET => DyeColor::BLACK, - Ids::BLUE_CARPET => DyeColor::BLUE, - Ids::BROWN_CARPET => DyeColor::BROWN, - Ids::CYAN_CARPET => DyeColor::CYAN, - Ids::GRAY_CARPET => DyeColor::GRAY, - Ids::GREEN_CARPET => DyeColor::GREEN, - Ids::LIGHT_BLUE_CARPET => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_CARPET => DyeColor::LIGHT_GRAY, - Ids::LIME_CARPET => DyeColor::LIME, - Ids::MAGENTA_CARPET => DyeColor::MAGENTA, - Ids::ORANGE_CARPET => DyeColor::ORANGE, - Ids::PINK_CARPET => DyeColor::PINK, - Ids::PURPLE_CARPET => DyeColor::PURPLE, - Ids::RED_CARPET => DyeColor::RED, - Ids::WHITE_CARPET => DyeColor::WHITE, - Ids::YELLOW_CARPET => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::CARPET()->setColor($color)); - } - - foreach([ - Ids::BLACK_SHULKER_BOX => DyeColor::BLACK, - Ids::BLUE_SHULKER_BOX => DyeColor::BLUE, - Ids::BROWN_SHULKER_BOX => DyeColor::BROWN, - Ids::CYAN_SHULKER_BOX => DyeColor::CYAN, - Ids::GRAY_SHULKER_BOX => DyeColor::GRAY, - Ids::GREEN_SHULKER_BOX => DyeColor::GREEN, - Ids::LIGHT_BLUE_SHULKER_BOX => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_SHULKER_BOX => DyeColor::LIGHT_GRAY, - Ids::LIME_SHULKER_BOX => DyeColor::LIME, - Ids::MAGENTA_SHULKER_BOX => DyeColor::MAGENTA, - Ids::ORANGE_SHULKER_BOX => DyeColor::ORANGE, - Ids::PINK_SHULKER_BOX => DyeColor::PINK, - Ids::PURPLE_SHULKER_BOX => DyeColor::PURPLE, - Ids::RED_SHULKER_BOX => DyeColor::RED, - Ids::WHITE_SHULKER_BOX => DyeColor::WHITE, - Ids::YELLOW_SHULKER_BOX => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::DYED_SHULKER_BOX()->setColor($color)); - } - - foreach([ - Ids::BLACK_CONCRETE => DyeColor::BLACK, - Ids::BLUE_CONCRETE => DyeColor::BLUE, - Ids::BROWN_CONCRETE => DyeColor::BROWN, - Ids::CYAN_CONCRETE => DyeColor::CYAN, - Ids::GRAY_CONCRETE => DyeColor::GRAY, - Ids::GREEN_CONCRETE => DyeColor::GREEN, - Ids::LIGHT_BLUE_CONCRETE => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_CONCRETE => DyeColor::LIGHT_GRAY, - Ids::LIME_CONCRETE => DyeColor::LIME, - Ids::MAGENTA_CONCRETE => DyeColor::MAGENTA, - Ids::ORANGE_CONCRETE => DyeColor::ORANGE, - Ids::PINK_CONCRETE => DyeColor::PINK, - Ids::PURPLE_CONCRETE => DyeColor::PURPLE, - Ids::RED_CONCRETE => DyeColor::RED, - Ids::WHITE_CONCRETE => DyeColor::WHITE, - Ids::YELLOW_CONCRETE => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::CONCRETE()->setColor($color)); - } - - foreach([ - Ids::BLACK_CONCRETE_POWDER => DyeColor::BLACK, - Ids::BLUE_CONCRETE_POWDER => DyeColor::BLUE, - Ids::BROWN_CONCRETE_POWDER => DyeColor::BROWN, - Ids::CYAN_CONCRETE_POWDER => DyeColor::CYAN, - Ids::GRAY_CONCRETE_POWDER => DyeColor::GRAY, - Ids::GREEN_CONCRETE_POWDER => DyeColor::GREEN, - Ids::LIGHT_BLUE_CONCRETE_POWDER => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_CONCRETE_POWDER => DyeColor::LIGHT_GRAY, - Ids::LIME_CONCRETE_POWDER => DyeColor::LIME, - Ids::MAGENTA_CONCRETE_POWDER => DyeColor::MAGENTA, - Ids::ORANGE_CONCRETE_POWDER => DyeColor::ORANGE, - Ids::PINK_CONCRETE_POWDER => DyeColor::PINK, - Ids::PURPLE_CONCRETE_POWDER => DyeColor::PURPLE, - Ids::RED_CONCRETE_POWDER => DyeColor::RED, - Ids::WHITE_CONCRETE_POWDER => DyeColor::WHITE, - Ids::YELLOW_CONCRETE_POWDER => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::CONCRETE_POWDER()->setColor($color)); - } - - foreach([ - Ids::BLACK_TERRACOTTA => DyeColor::BLACK, - Ids::BLUE_TERRACOTTA => DyeColor::BLUE, - Ids::BROWN_TERRACOTTA => DyeColor::BROWN, - Ids::CYAN_TERRACOTTA => DyeColor::CYAN, - Ids::GRAY_TERRACOTTA => DyeColor::GRAY, - Ids::GREEN_TERRACOTTA => DyeColor::GREEN, - Ids::LIGHT_BLUE_TERRACOTTA => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_TERRACOTTA => DyeColor::LIGHT_GRAY, - Ids::LIME_TERRACOTTA => DyeColor::LIME, - Ids::MAGENTA_TERRACOTTA => DyeColor::MAGENTA, - Ids::ORANGE_TERRACOTTA => DyeColor::ORANGE, - Ids::PINK_TERRACOTTA => DyeColor::PINK, - Ids::PURPLE_TERRACOTTA => DyeColor::PURPLE, - Ids::RED_TERRACOTTA => DyeColor::RED, - Ids::WHITE_TERRACOTTA => DyeColor::WHITE, - Ids::YELLOW_TERRACOTTA => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::STAINED_CLAY()->setColor($color)); - } - - foreach([ - Ids::BLACK_STAINED_GLASS => DyeColor::BLACK, - Ids::BLUE_STAINED_GLASS => DyeColor::BLUE, - Ids::BROWN_STAINED_GLASS => DyeColor::BROWN, - Ids::CYAN_STAINED_GLASS => DyeColor::CYAN, - Ids::GRAY_STAINED_GLASS => DyeColor::GRAY, - Ids::GREEN_STAINED_GLASS => DyeColor::GREEN, - Ids::LIGHT_BLUE_STAINED_GLASS => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_STAINED_GLASS => DyeColor::LIGHT_GRAY, - Ids::LIME_STAINED_GLASS => DyeColor::LIME, - Ids::MAGENTA_STAINED_GLASS => DyeColor::MAGENTA, - Ids::ORANGE_STAINED_GLASS => DyeColor::ORANGE, - Ids::PINK_STAINED_GLASS => DyeColor::PINK, - Ids::PURPLE_STAINED_GLASS => DyeColor::PURPLE, - Ids::RED_STAINED_GLASS => DyeColor::RED, - Ids::WHITE_STAINED_GLASS => DyeColor::WHITE, - Ids::YELLOW_STAINED_GLASS => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::STAINED_GLASS()->setColor($color)); - } - - foreach([ - Ids::BLACK_STAINED_GLASS_PANE => DyeColor::BLACK, - Ids::BLUE_STAINED_GLASS_PANE => DyeColor::BLUE, - Ids::BROWN_STAINED_GLASS_PANE => DyeColor::BROWN, - Ids::CYAN_STAINED_GLASS_PANE => DyeColor::CYAN, - Ids::GRAY_STAINED_GLASS_PANE => DyeColor::GRAY, - Ids::GREEN_STAINED_GLASS_PANE => DyeColor::GREEN, - Ids::LIGHT_BLUE_STAINED_GLASS_PANE => DyeColor::LIGHT_BLUE, - Ids::LIGHT_GRAY_STAINED_GLASS_PANE => DyeColor::LIGHT_GRAY, - Ids::LIME_STAINED_GLASS_PANE => DyeColor::LIME, - Ids::MAGENTA_STAINED_GLASS_PANE => DyeColor::MAGENTA, - Ids::ORANGE_STAINED_GLASS_PANE => DyeColor::ORANGE, - Ids::PINK_STAINED_GLASS_PANE => DyeColor::PINK, - Ids::PURPLE_STAINED_GLASS_PANE => DyeColor::PURPLE, - Ids::RED_STAINED_GLASS_PANE => DyeColor::RED, - Ids::WHITE_STAINED_GLASS_PANE => DyeColor::WHITE, - Ids::YELLOW_STAINED_GLASS_PANE => DyeColor::YELLOW, - ] as $id => $color){ - $this->mapSimple($id, fn() => Blocks::STAINED_GLASS_PANE()->setColor($color)); - } - } - - private function registerFlatCoralDeserializers() : void{ - foreach([ - Ids::BRAIN_CORAL => CoralType::BRAIN, - Ids::BUBBLE_CORAL => CoralType::BUBBLE, - Ids::FIRE_CORAL => CoralType::FIRE, - Ids::HORN_CORAL => CoralType::HORN, - Ids::TUBE_CORAL => CoralType::TUBE, - ] as $id => $coralType){ - $this->mapSimple($id, fn() => Blocks::CORAL()->setCoralType($coralType)->setDead(false)); - } - foreach([ - Ids::DEAD_BRAIN_CORAL => CoralType::BRAIN, - Ids::DEAD_BUBBLE_CORAL => CoralType::BUBBLE, - Ids::DEAD_FIRE_CORAL => CoralType::FIRE, - Ids::DEAD_HORN_CORAL => CoralType::HORN, - Ids::DEAD_TUBE_CORAL => CoralType::TUBE, - ] as $id => $coralType){ - $this->mapSimple($id, fn() => Blocks::CORAL()->setCoralType($coralType)->setDead(true)); - } - - foreach([ - [CoralType::BRAIN, Ids::BRAIN_CORAL_FAN, Ids::DEAD_BRAIN_CORAL_FAN], - [CoralType::BUBBLE, Ids::BUBBLE_CORAL_FAN, Ids::DEAD_BUBBLE_CORAL_FAN], - [CoralType::FIRE, Ids::FIRE_CORAL_FAN, Ids::DEAD_FIRE_CORAL_FAN], - [CoralType::HORN, Ids::HORN_CORAL_FAN, Ids::DEAD_HORN_CORAL_FAN], - [CoralType::TUBE, Ids::TUBE_CORAL_FAN, Ids::DEAD_TUBE_CORAL_FAN], - ] as [$coralType, $aliveId, $deadId]){ - $this->map($aliveId, fn(Reader $in) => Helper::decodeFloorCoralFan(Blocks::CORAL_FAN()->setCoralType($coralType)->setDead(false), $in)); - $this->map($deadId, fn(Reader $in) => Helper::decodeFloorCoralFan(Blocks::CORAL_FAN()->setCoralType($coralType)->setDead(true), $in)); - } - - foreach([ - [CoralType::BRAIN, Ids::BRAIN_CORAL_BLOCK, Ids::DEAD_BRAIN_CORAL_BLOCK], - [CoralType::BUBBLE, Ids::BUBBLE_CORAL_BLOCK, Ids::DEAD_BUBBLE_CORAL_BLOCK], - [CoralType::FIRE, Ids::FIRE_CORAL_BLOCK, Ids::DEAD_FIRE_CORAL_BLOCK], - [CoralType::HORN, Ids::HORN_CORAL_BLOCK, Ids::DEAD_HORN_CORAL_BLOCK], - [CoralType::TUBE, Ids::TUBE_CORAL_BLOCK, Ids::DEAD_TUBE_CORAL_BLOCK], - ] as [$coralType, $aliveId, $deadId]){ - $this->map($aliveId, fn(Reader $in) => Blocks::CORAL_BLOCK()->setCoralType($coralType)->setDead(false)); - $this->map($deadId, fn(Reader $in) => Blocks::CORAL_BLOCK()->setCoralType($coralType)->setDead(true)); - } - - foreach([ - [CoralType::BRAIN, Ids::BRAIN_CORAL_WALL_FAN, Ids::DEAD_BRAIN_CORAL_WALL_FAN], - [CoralType::BUBBLE, Ids::BUBBLE_CORAL_WALL_FAN, Ids::DEAD_BUBBLE_CORAL_WALL_FAN], - [CoralType::FIRE, Ids::FIRE_CORAL_WALL_FAN, Ids::DEAD_FIRE_CORAL_WALL_FAN], - [CoralType::HORN, Ids::HORN_CORAL_WALL_FAN, Ids::DEAD_HORN_CORAL_WALL_FAN], - [CoralType::TUBE, Ids::TUBE_CORAL_WALL_FAN, Ids::DEAD_TUBE_CORAL_WALL_FAN], - ] as [$coralType, $aliveId, $deadId]){ - $this->map($aliveId, fn(Reader $in) => Blocks::WALL_CORAL_FAN()->setFacing($in->readCoralFacing())->setCoralType($coralType)->setDead(false)); - $this->map($deadId, fn(Reader $in) => Blocks::WALL_CORAL_FAN()->setFacing($in->readCoralFacing())->setCoralType($coralType)->setDead(true)); - } - } - - private function registerCauldronDeserializers() : void{ - $deserializer = function(Reader $in) : Block{ - $level = $in->readBoundedInt(StateNames::FILL_LEVEL, 0, 6); - if($level === 0){ - $in->ignored(StateNames::CAULDRON_LIQUID); - return Blocks::CAULDRON(); - } - - return (match($liquid = $in->readString(StateNames::CAULDRON_LIQUID)){ - StringValues::CAULDRON_LIQUID_WATER => Blocks::WATER_CAULDRON(), - StringValues::CAULDRON_LIQUID_LAVA => Blocks::LAVA_CAULDRON(), - StringValues::CAULDRON_LIQUID_POWDER_SNOW => throw new UnsupportedBlockStateException("Powder snow is not supported yet"), - default => throw $in->badValueException(StateNames::CAULDRON_LIQUID, $liquid) - })->setFillLevel($level); - }; - $this->map(Ids::CAULDRON, $deserializer); - } - - private function registerFlatWoodBlockDeserializers() : void{ - $this->map(Ids::ACACIA_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::ACACIA_BUTTON(), $in)); - $this->map(Ids::ACACIA_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::ACACIA_DOOR(), $in)); - $this->map(Ids::ACACIA_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::ACACIA_FENCE_GATE(), $in)); - $this->map(Ids::ACACIA_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::ACACIA_PRESSURE_PLATE(), $in)); - $this->map(Ids::ACACIA_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::ACACIA_SIGN(), $in)); - $this->map(Ids::ACACIA_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::ACACIA_TRAPDOOR(), $in)); - $this->map(Ids::ACACIA_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::ACACIA_WALL_SIGN(), $in)); - $this->mapLog(Ids::ACACIA_LOG, Ids::STRIPPED_ACACIA_LOG, fn() => Blocks::ACACIA_LOG()); - $this->mapLog(Ids::ACACIA_WOOD, Ids::STRIPPED_ACACIA_WOOD, fn() => Blocks::ACACIA_WOOD()); - $this->mapSimple(Ids::ACACIA_FENCE, fn() => Blocks::ACACIA_FENCE()); - $this->mapSimple(Ids::ACACIA_PLANKS, fn() => Blocks::ACACIA_PLANKS()); - $this->mapSlab(Ids::ACACIA_SLAB, Ids::ACACIA_DOUBLE_SLAB, fn() => Blocks::ACACIA_SLAB()); - $this->mapStairs(Ids::ACACIA_STAIRS, fn() => Blocks::ACACIA_STAIRS()); - - $this->map(Ids::BIRCH_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::BIRCH_BUTTON(), $in)); - $this->map(Ids::BIRCH_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::BIRCH_DOOR(), $in)); - $this->map(Ids::BIRCH_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::BIRCH_FENCE_GATE(), $in)); - $this->map(Ids::BIRCH_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::BIRCH_PRESSURE_PLATE(), $in)); - $this->map(Ids::BIRCH_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::BIRCH_SIGN(), $in)); - $this->map(Ids::BIRCH_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::BIRCH_TRAPDOOR(), $in)); - $this->map(Ids::BIRCH_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::BIRCH_WALL_SIGN(), $in)); - $this->mapLog(Ids::BIRCH_LOG, Ids::STRIPPED_BIRCH_LOG, fn() => Blocks::BIRCH_LOG()); - $this->mapLog(Ids::BIRCH_WOOD, Ids::STRIPPED_BIRCH_WOOD, fn() => Blocks::BIRCH_WOOD()); - $this->mapSimple(Ids::BIRCH_FENCE, fn() => Blocks::BIRCH_FENCE()); - $this->mapSimple(Ids::BIRCH_PLANKS, fn() => Blocks::BIRCH_PLANKS()); - $this->mapSlab(Ids::BIRCH_SLAB, Ids::BIRCH_DOUBLE_SLAB, fn() => Blocks::BIRCH_SLAB()); - $this->mapStairs(Ids::BIRCH_STAIRS, fn() => Blocks::BIRCH_STAIRS()); - - $this->map(Ids::CHERRY_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::CHERRY_BUTTON(), $in)); - $this->map(Ids::CHERRY_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::CHERRY_DOOR(), $in)); - $this->map(Ids::CHERRY_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::CHERRY_FENCE_GATE(), $in)); - $this->map(Ids::CHERRY_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::CHERRY_PRESSURE_PLATE(), $in)); - $this->map(Ids::CHERRY_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::CHERRY_SIGN(), $in)); - $this->map(Ids::CHERRY_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::CHERRY_TRAPDOOR(), $in)); - $this->map(Ids::CHERRY_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::CHERRY_WALL_SIGN(), $in)); - $this->mapLog(Ids::CHERRY_LOG, Ids::STRIPPED_CHERRY_LOG, fn() => Blocks::CHERRY_LOG()); - $this->mapSimple(Ids::CHERRY_FENCE, fn() => Blocks::CHERRY_FENCE()); - $this->mapSimple(Ids::CHERRY_PLANKS, fn() => Blocks::CHERRY_PLANKS()); - $this->mapSlab(Ids::CHERRY_SLAB, Ids::CHERRY_DOUBLE_SLAB, fn() => Blocks::CHERRY_SLAB()); - $this->mapStairs(Ids::CHERRY_STAIRS, fn() => Blocks::CHERRY_STAIRS()); - $this->map(Ids::CHERRY_WOOD, fn(Reader $in) => Helper::decodeLog(Blocks::CHERRY_WOOD(), false, $in)); - $this->map(Ids::STRIPPED_CHERRY_WOOD, fn(Reader $in) => Helper::decodeLog(Blocks::CHERRY_WOOD(), true, $in)); - - $this->map(Ids::CRIMSON_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::CRIMSON_BUTTON(), $in)); - $this->map(Ids::CRIMSON_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::CRIMSON_DOOR(), $in)); - $this->map(Ids::CRIMSON_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::CRIMSON_FENCE_GATE(), $in)); - $this->map(Ids::CRIMSON_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::CRIMSON_PRESSURE_PLATE(), $in)); - $this->map(Ids::CRIMSON_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::CRIMSON_SIGN(), $in)); - $this->map(Ids::CRIMSON_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::CRIMSON_TRAPDOOR(), $in)); - $this->map(Ids::CRIMSON_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::CRIMSON_WALL_SIGN(), $in)); - $this->mapLog(Ids::CRIMSON_HYPHAE, Ids::STRIPPED_CRIMSON_HYPHAE, fn() => Blocks::CRIMSON_HYPHAE()); - $this->mapLog(Ids::CRIMSON_STEM, Ids::STRIPPED_CRIMSON_STEM, fn() => Blocks::CRIMSON_STEM()); - $this->mapSimple(Ids::CRIMSON_FENCE, fn() => Blocks::CRIMSON_FENCE()); - $this->mapSimple(Ids::CRIMSON_PLANKS, fn() => Blocks::CRIMSON_PLANKS()); - $this->mapSlab(Ids::CRIMSON_SLAB, Ids::CRIMSON_DOUBLE_SLAB, fn() => Blocks::CRIMSON_SLAB()); - $this->mapStairs(Ids::CRIMSON_STAIRS, fn() => Blocks::CRIMSON_STAIRS()); - - $this->map(Ids::DARKOAK_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::DARK_OAK_SIGN(), $in)); - $this->map(Ids::DARKOAK_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::DARK_OAK_WALL_SIGN(), $in)); - $this->map(Ids::DARK_OAK_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::DARK_OAK_BUTTON(), $in)); - $this->map(Ids::DARK_OAK_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::DARK_OAK_DOOR(), $in)); - $this->map(Ids::DARK_OAK_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::DARK_OAK_FENCE_GATE(), $in)); - $this->map(Ids::DARK_OAK_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::DARK_OAK_PRESSURE_PLATE(), $in)); - $this->map(Ids::DARK_OAK_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::DARK_OAK_TRAPDOOR(), $in)); - $this->mapLog(Ids::DARK_OAK_LOG, Ids::STRIPPED_DARK_OAK_LOG, fn() => Blocks::DARK_OAK_LOG()); - $this->mapLog(Ids::DARK_OAK_WOOD, Ids::STRIPPED_DARK_OAK_WOOD, fn() => Blocks::DARK_OAK_WOOD()); - $this->mapSimple(Ids::DARK_OAK_FENCE, fn() => Blocks::DARK_OAK_FENCE()); - $this->mapSimple(Ids::DARK_OAK_PLANKS, fn() => Blocks::DARK_OAK_PLANKS()); - $this->mapSlab(Ids::DARK_OAK_SLAB, Ids::DARK_OAK_DOUBLE_SLAB, fn() => Blocks::DARK_OAK_SLAB()); - $this->mapStairs(Ids::DARK_OAK_STAIRS, fn() => Blocks::DARK_OAK_STAIRS()); - - $this->map(Ids::JUNGLE_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::JUNGLE_BUTTON(), $in)); - $this->map(Ids::JUNGLE_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::JUNGLE_DOOR(), $in)); - $this->map(Ids::JUNGLE_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::JUNGLE_FENCE_GATE(), $in)); - $this->map(Ids::JUNGLE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::JUNGLE_PRESSURE_PLATE(), $in)); - $this->map(Ids::JUNGLE_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::JUNGLE_SIGN(), $in)); - $this->map(Ids::JUNGLE_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::JUNGLE_TRAPDOOR(), $in)); - $this->map(Ids::JUNGLE_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::JUNGLE_WALL_SIGN(), $in)); - $this->mapLog(Ids::JUNGLE_LOG, Ids::STRIPPED_JUNGLE_LOG, fn() => Blocks::JUNGLE_LOG()); - $this->mapLog(Ids::JUNGLE_WOOD, Ids::STRIPPED_JUNGLE_WOOD, fn() => Blocks::JUNGLE_WOOD()); - $this->mapSimple(Ids::JUNGLE_FENCE, fn() => Blocks::JUNGLE_FENCE()); - $this->mapSimple(Ids::JUNGLE_PLANKS, fn() => Blocks::JUNGLE_PLANKS()); - $this->mapSlab(Ids::JUNGLE_SLAB, Ids::JUNGLE_DOUBLE_SLAB, fn() => Blocks::JUNGLE_SLAB()); - $this->mapStairs(Ids::JUNGLE_STAIRS, fn() => Blocks::JUNGLE_STAIRS()); - - $this->map(Ids::MANGROVE_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::MANGROVE_BUTTON(), $in)); - $this->map(Ids::MANGROVE_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::MANGROVE_DOOR(), $in)); - $this->map(Ids::MANGROVE_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::MANGROVE_FENCE_GATE(), $in)); - $this->map(Ids::MANGROVE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::MANGROVE_PRESSURE_PLATE(), $in)); - $this->map(Ids::MANGROVE_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::MANGROVE_SIGN(), $in)); - $this->map(Ids::MANGROVE_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::MANGROVE_TRAPDOOR(), $in)); - $this->map(Ids::MANGROVE_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::MANGROVE_WALL_SIGN(), $in)); - $this->mapLog(Ids::MANGROVE_LOG, Ids::STRIPPED_MANGROVE_LOG, fn() => Blocks::MANGROVE_LOG()); - $this->mapSimple(Ids::MANGROVE_FENCE, fn() => Blocks::MANGROVE_FENCE()); - $this->mapSimple(Ids::MANGROVE_PLANKS, fn() => Blocks::MANGROVE_PLANKS()); - $this->mapSlab(Ids::MANGROVE_SLAB, Ids::MANGROVE_DOUBLE_SLAB, fn() => Blocks::MANGROVE_SLAB()); - $this->mapStairs(Ids::MANGROVE_STAIRS, fn() => Blocks::MANGROVE_STAIRS()); - $this->map(Ids::MANGROVE_WOOD, fn(Reader $in) => Helper::decodeLog(Blocks::MANGROVE_WOOD(), false, $in)); - $this->map(Ids::STRIPPED_MANGROVE_WOOD, fn(Reader $in) => Helper::decodeLog(Blocks::MANGROVE_WOOD(), true, $in)); - - //oak - due to age, many of these don't specify "oak", making for confusing reading - $this->map(Ids::WOODEN_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::OAK_BUTTON(), $in)); - $this->map(Ids::WOODEN_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::OAK_DOOR(), $in)); - $this->map(Ids::FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::OAK_FENCE_GATE(), $in)); - $this->map(Ids::WOODEN_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::OAK_PRESSURE_PLATE(), $in)); - $this->map(Ids::STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::OAK_SIGN(), $in)); - $this->map(Ids::TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::OAK_TRAPDOOR(), $in)); - $this->map(Ids::WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::OAK_WALL_SIGN(), $in)); - $this->mapLog(Ids::OAK_LOG, Ids::STRIPPED_OAK_LOG, fn() => Blocks::OAK_LOG()); - $this->mapLog(Ids::OAK_WOOD, Ids::STRIPPED_OAK_WOOD, fn() => Blocks::OAK_WOOD()); - $this->mapSimple(Ids::OAK_FENCE, fn() => Blocks::OAK_FENCE()); - $this->mapSimple(Ids::OAK_PLANKS, fn() => Blocks::OAK_PLANKS()); - $this->mapSlab(Ids::OAK_SLAB, Ids::OAK_DOUBLE_SLAB, fn() => Blocks::OAK_SLAB()); - $this->mapStairs(Ids::OAK_STAIRS, fn() => Blocks::OAK_STAIRS()); - - $this->map(Ids::SPRUCE_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::SPRUCE_BUTTON(), $in)); - $this->map(Ids::SPRUCE_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::SPRUCE_DOOR(), $in)); - $this->map(Ids::SPRUCE_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::SPRUCE_FENCE_GATE(), $in)); - $this->map(Ids::SPRUCE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::SPRUCE_PRESSURE_PLATE(), $in)); - $this->map(Ids::SPRUCE_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::SPRUCE_SIGN(), $in)); - $this->map(Ids::SPRUCE_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::SPRUCE_TRAPDOOR(), $in)); - $this->map(Ids::SPRUCE_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::SPRUCE_WALL_SIGN(), $in)); - $this->mapLog(Ids::SPRUCE_LOG, Ids::STRIPPED_SPRUCE_LOG, fn() => Blocks::SPRUCE_LOG()); - $this->mapLog(Ids::SPRUCE_WOOD, Ids::STRIPPED_SPRUCE_WOOD, fn() => Blocks::SPRUCE_WOOD()); - $this->mapSimple(Ids::SPRUCE_FENCE, fn() => Blocks::SPRUCE_FENCE()); - $this->mapSimple(Ids::SPRUCE_PLANKS, fn() => Blocks::SPRUCE_PLANKS()); - $this->mapSlab(Ids::SPRUCE_SLAB, Ids::SPRUCE_DOUBLE_SLAB, fn() => Blocks::SPRUCE_SLAB()); - $this->mapStairs(Ids::SPRUCE_STAIRS, fn() => Blocks::SPRUCE_STAIRS()); - - $this->map(Ids::WARPED_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::WARPED_BUTTON(), $in)); - $this->map(Ids::WARPED_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::WARPED_DOOR(), $in)); - $this->map(Ids::WARPED_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::WARPED_FENCE_GATE(), $in)); - $this->map(Ids::WARPED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::WARPED_PRESSURE_PLATE(), $in)); - $this->map(Ids::WARPED_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::WARPED_SIGN(), $in)); - $this->map(Ids::WARPED_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::WARPED_TRAPDOOR(), $in)); - $this->map(Ids::WARPED_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::WARPED_WALL_SIGN(), $in)); - $this->mapLog(Ids::WARPED_HYPHAE, Ids::STRIPPED_WARPED_HYPHAE, fn() => Blocks::WARPED_HYPHAE()); - $this->mapLog(Ids::WARPED_STEM, Ids::STRIPPED_WARPED_STEM, fn() => Blocks::WARPED_STEM()); - $this->mapSimple(Ids::WARPED_FENCE, fn() => Blocks::WARPED_FENCE()); - $this->mapSimple(Ids::WARPED_PLANKS, fn() => Blocks::WARPED_PLANKS()); - $this->mapSlab(Ids::WARPED_SLAB, Ids::WARPED_DOUBLE_SLAB, fn() => Blocks::WARPED_SLAB()); - $this->mapStairs(Ids::WARPED_STAIRS, fn() => Blocks::WARPED_STAIRS()); - } - - private function registerLeavesDeserializers() : void{ - $this->map(Ids::ACACIA_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::ACACIA_LEAVES(), $in)); - $this->map(Ids::AZALEA_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::AZALEA_LEAVES(), $in)); - $this->map(Ids::AZALEA_LEAVES_FLOWERED, fn(Reader $in) => Helper::decodeLeaves(Blocks::FLOWERING_AZALEA_LEAVES(), $in)); - $this->map(Ids::BIRCH_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::BIRCH_LEAVES(), $in)); - $this->map(Ids::CHERRY_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::CHERRY_LEAVES(), $in)); - $this->map(Ids::DARK_OAK_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::DARK_OAK_LEAVES(), $in)); - $this->map(Ids::JUNGLE_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::JUNGLE_LEAVES(), $in)); - $this->map(Ids::MANGROVE_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::MANGROVE_LEAVES(), $in)); - $this->map(Ids::OAK_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::OAK_LEAVES(), $in)); - $this->map(Ids::SPRUCE_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::SPRUCE_LEAVES(), $in)); - } - - private function registerSaplingDeserializers() : void{ - foreach([ - Ids::ACACIA_SAPLING => fn() => Blocks::ACACIA_SAPLING(), - Ids::BIRCH_SAPLING => fn() => Blocks::BIRCH_SAPLING(), - Ids::DARK_OAK_SAPLING => fn() => Blocks::DARK_OAK_SAPLING(), - Ids::JUNGLE_SAPLING => fn() => Blocks::JUNGLE_SAPLING(), - Ids::OAK_SAPLING => fn() => Blocks::OAK_SAPLING(), - Ids::SPRUCE_SAPLING => fn() => Blocks::SPRUCE_SAPLING(), - ] as $id => $getBlock){ - $this->map($id, fn(Reader $in) => Helper::decodeSapling($getBlock(), $in)); - } - } - - private function registerLightDeserializers() : void{ - foreach([ - Ids::LIGHT_BLOCK_0 => 0, - Ids::LIGHT_BLOCK_1 => 1, - Ids::LIGHT_BLOCK_2 => 2, - Ids::LIGHT_BLOCK_3 => 3, - Ids::LIGHT_BLOCK_4 => 4, - Ids::LIGHT_BLOCK_5 => 5, - Ids::LIGHT_BLOCK_6 => 6, - Ids::LIGHT_BLOCK_7 => 7, - Ids::LIGHT_BLOCK_8 => 8, - Ids::LIGHT_BLOCK_9 => 9, - Ids::LIGHT_BLOCK_10 => 10, - Ids::LIGHT_BLOCK_11 => 11, - Ids::LIGHT_BLOCK_12 => 12, - Ids::LIGHT_BLOCK_13 => 13, - Ids::LIGHT_BLOCK_14 => 14, - Ids::LIGHT_BLOCK_15 => 15, - ] as $id => $level){ - $this->mapSimple($id, fn() => Blocks::LIGHT()->setLightLevel($level)); - } - } - - private function registerMobHeadDeserializers() : void{ - foreach([ - Ids::CREEPER_HEAD => MobHeadType::CREEPER, - Ids::DRAGON_HEAD => MobHeadType::DRAGON, - Ids::PIGLIN_HEAD => MobHeadType::PIGLIN, - Ids::PLAYER_HEAD => MobHeadType::PLAYER, - Ids::SKELETON_SKULL => MobHeadType::SKELETON, - Ids::WITHER_SKELETON_SKULL => MobHeadType::WITHER_SKELETON, - Ids::ZOMBIE_HEAD => MobHeadType::ZOMBIE - ] as $id => $mobHeadType){ - $this->map($id, fn(Reader $in) => Blocks::MOB_HEAD()->setMobHeadType($mobHeadType)->setFacing($in->readFacingWithoutDown())); - } - } - - private function registerSimpleDeserializers() : void{ - $this->mapSimple(Ids::AIR, fn() => Blocks::AIR()); - $this->mapSimple(Ids::AMETHYST_BLOCK, fn() => Blocks::AMETHYST()); - $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::BEACON, fn() => Blocks::BEACON()); - $this->mapSimple(Ids::BLACKSTONE, fn() => Blocks::BLACKSTONE()); - $this->mapSimple(Ids::BLUE_ICE, fn() => Blocks::BLUE_ICE()); - $this->mapSimple(Ids::BOOKSHELF, fn() => Blocks::BOOKSHELF()); - $this->mapSimple(Ids::BRICK_BLOCK, fn() => Blocks::BRICKS()); - $this->mapSimple(Ids::BROWN_MUSHROOM, fn() => Blocks::BROWN_MUSHROOM()); - $this->mapSimple(Ids::BUDDING_AMETHYST, fn() => Blocks::BUDDING_AMETHYST()); - $this->mapSimple(Ids::CALCITE, fn() => Blocks::CALCITE()); - $this->mapSimple(Ids::CARTOGRAPHY_TABLE, fn() => Blocks::CARTOGRAPHY_TABLE()); - $this->mapSimple(Ids::CHEMICAL_HEAT, fn() => Blocks::CHEMICAL_HEAT()); - $this->mapSimple(Ids::CHISELED_DEEPSLATE, fn() => Blocks::CHISELED_DEEPSLATE()); - $this->mapSimple(Ids::CHISELED_NETHER_BRICKS, fn() => Blocks::CHISELED_NETHER_BRICKS()); - $this->mapSimple(Ids::CHISELED_POLISHED_BLACKSTONE, fn() => Blocks::CHISELED_POLISHED_BLACKSTONE()); - $this->mapSimple(Ids::CHISELED_RED_SANDSTONE, fn() => Blocks::CHISELED_RED_SANDSTONE()); - $this->mapSimple(Ids::CHISELED_SANDSTONE, fn() => Blocks::CHISELED_SANDSTONE()); - $this->mapSimple(Ids::CHISELED_STONE_BRICKS, fn() => Blocks::CHISELED_STONE_BRICKS()); - $this->mapSimple(Ids::CHISELED_TUFF, fn() => Blocks::CHISELED_TUFF()); - $this->mapSimple(Ids::CHISELED_TUFF_BRICKS, fn() => Blocks::CHISELED_TUFF_BRICKS()); - $this->mapSimple(Ids::CHORUS_PLANT, fn() => Blocks::CHORUS_PLANT()); - $this->mapSimple(Ids::CLAY, fn() => Blocks::CLAY()); - $this->mapSimple(Ids::COAL_BLOCK, fn() => Blocks::COAL()); - $this->mapSimple(Ids::COAL_ORE, fn() => Blocks::COAL_ORE()); - $this->mapSimple(Ids::COBBLED_DEEPSLATE, fn() => Blocks::COBBLED_DEEPSLATE()); - $this->mapSimple(Ids::COBBLESTONE, fn() => Blocks::COBBLESTONE()); - $this->mapSimple(Ids::COPPER_ORE, fn() => Blocks::COPPER_ORE()); - $this->mapSimple(Ids::CRACKED_DEEPSLATE_BRICKS, fn() => Blocks::CRACKED_DEEPSLATE_BRICKS()); - $this->mapSimple(Ids::CRACKED_DEEPSLATE_TILES, fn() => Blocks::CRACKED_DEEPSLATE_TILES()); - $this->mapSimple(Ids::CRACKED_NETHER_BRICKS, fn() => Blocks::CRACKED_NETHER_BRICKS()); - $this->mapSimple(Ids::CRACKED_POLISHED_BLACKSTONE_BRICKS, fn() => Blocks::CRACKED_POLISHED_BLACKSTONE_BRICKS()); - $this->mapSimple(Ids::CRACKED_STONE_BRICKS, fn() => Blocks::CRACKED_STONE_BRICKS()); - $this->mapSimple(Ids::CRAFTING_TABLE, fn() => Blocks::CRAFTING_TABLE()); - $this->mapSimple(Ids::CRIMSON_ROOTS, fn() => Blocks::CRIMSON_ROOTS()); - $this->mapSimple(Ids::CRYING_OBSIDIAN, fn() => Blocks::CRYING_OBSIDIAN()); - $this->mapSimple(Ids::CUT_RED_SANDSTONE, fn() => Blocks::CUT_RED_SANDSTONE()); - $this->mapSimple(Ids::CUT_SANDSTONE, fn() => Blocks::CUT_SANDSTONE()); - $this->mapSimple(Ids::DARK_PRISMARINE, fn() => Blocks::DARK_PRISMARINE()); - $this->mapSimple(Ids::DEADBUSH, fn() => Blocks::DEAD_BUSH()); - $this->mapSimple(Ids::DEEPSLATE_BRICKS, fn() => Blocks::DEEPSLATE_BRICKS()); - $this->mapSimple(Ids::DEEPSLATE_COAL_ORE, fn() => Blocks::DEEPSLATE_COAL_ORE()); - $this->mapSimple(Ids::DEEPSLATE_COPPER_ORE, fn() => Blocks::DEEPSLATE_COPPER_ORE()); - $this->mapSimple(Ids::DEEPSLATE_DIAMOND_ORE, fn() => Blocks::DEEPSLATE_DIAMOND_ORE()); - $this->mapSimple(Ids::DEEPSLATE_EMERALD_ORE, fn() => Blocks::DEEPSLATE_EMERALD_ORE()); - $this->mapSimple(Ids::DEEPSLATE_GOLD_ORE, fn() => Blocks::DEEPSLATE_GOLD_ORE()); - $this->mapSimple(Ids::DEEPSLATE_IRON_ORE, fn() => Blocks::DEEPSLATE_IRON_ORE()); - $this->mapSimple(Ids::DEEPSLATE_LAPIS_ORE, fn() => Blocks::DEEPSLATE_LAPIS_LAZULI_ORE()); - $this->mapSimple(Ids::DEEPSLATE_TILES, fn() => Blocks::DEEPSLATE_TILES()); - $this->mapSimple(Ids::DIAMOND_BLOCK, fn() => Blocks::DIAMOND()); - $this->mapSimple(Ids::DIAMOND_ORE, fn() => Blocks::DIAMOND_ORE()); - $this->mapSimple(Ids::DIORITE, fn() => Blocks::DIORITE()); - $this->mapSimple(Ids::DRAGON_EGG, fn() => Blocks::DRAGON_EGG()); - $this->mapSimple(Ids::DRIED_KELP_BLOCK, fn() => Blocks::DRIED_KELP()); - $this->mapSimple(Ids::ELEMENT_0, fn() => Blocks::ELEMENT_ZERO()); - $this->mapSimple(Ids::ELEMENT_1, fn() => Blocks::ELEMENT_HYDROGEN()); - $this->mapSimple(Ids::ELEMENT_10, fn() => Blocks::ELEMENT_NEON()); - $this->mapSimple(Ids::ELEMENT_100, fn() => Blocks::ELEMENT_FERMIUM()); - $this->mapSimple(Ids::ELEMENT_101, fn() => Blocks::ELEMENT_MENDELEVIUM()); - $this->mapSimple(Ids::ELEMENT_102, fn() => Blocks::ELEMENT_NOBELIUM()); - $this->mapSimple(Ids::ELEMENT_103, fn() => Blocks::ELEMENT_LAWRENCIUM()); - $this->mapSimple(Ids::ELEMENT_104, fn() => Blocks::ELEMENT_RUTHERFORDIUM()); - $this->mapSimple(Ids::ELEMENT_105, fn() => Blocks::ELEMENT_DUBNIUM()); - $this->mapSimple(Ids::ELEMENT_106, fn() => Blocks::ELEMENT_SEABORGIUM()); - $this->mapSimple(Ids::ELEMENT_107, fn() => Blocks::ELEMENT_BOHRIUM()); - $this->mapSimple(Ids::ELEMENT_108, fn() => Blocks::ELEMENT_HASSIUM()); - $this->mapSimple(Ids::ELEMENT_109, fn() => Blocks::ELEMENT_MEITNERIUM()); - $this->mapSimple(Ids::ELEMENT_11, fn() => Blocks::ELEMENT_SODIUM()); - $this->mapSimple(Ids::ELEMENT_110, fn() => Blocks::ELEMENT_DARMSTADTIUM()); - $this->mapSimple(Ids::ELEMENT_111, fn() => Blocks::ELEMENT_ROENTGENIUM()); - $this->mapSimple(Ids::ELEMENT_112, fn() => Blocks::ELEMENT_COPERNICIUM()); - $this->mapSimple(Ids::ELEMENT_113, fn() => Blocks::ELEMENT_NIHONIUM()); - $this->mapSimple(Ids::ELEMENT_114, fn() => Blocks::ELEMENT_FLEROVIUM()); - $this->mapSimple(Ids::ELEMENT_115, fn() => Blocks::ELEMENT_MOSCOVIUM()); - $this->mapSimple(Ids::ELEMENT_116, fn() => Blocks::ELEMENT_LIVERMORIUM()); - $this->mapSimple(Ids::ELEMENT_117, fn() => Blocks::ELEMENT_TENNESSINE()); - $this->mapSimple(Ids::ELEMENT_118, fn() => Blocks::ELEMENT_OGANESSON()); - $this->mapSimple(Ids::ELEMENT_12, fn() => Blocks::ELEMENT_MAGNESIUM()); - $this->mapSimple(Ids::ELEMENT_13, fn() => Blocks::ELEMENT_ALUMINUM()); - $this->mapSimple(Ids::ELEMENT_14, fn() => Blocks::ELEMENT_SILICON()); - $this->mapSimple(Ids::ELEMENT_15, fn() => Blocks::ELEMENT_PHOSPHORUS()); - $this->mapSimple(Ids::ELEMENT_16, fn() => Blocks::ELEMENT_SULFUR()); - $this->mapSimple(Ids::ELEMENT_17, fn() => Blocks::ELEMENT_CHLORINE()); - $this->mapSimple(Ids::ELEMENT_18, fn() => Blocks::ELEMENT_ARGON()); - $this->mapSimple(Ids::ELEMENT_19, fn() => Blocks::ELEMENT_POTASSIUM()); - $this->mapSimple(Ids::ELEMENT_2, fn() => Blocks::ELEMENT_HELIUM()); - $this->mapSimple(Ids::ELEMENT_20, fn() => Blocks::ELEMENT_CALCIUM()); - $this->mapSimple(Ids::ELEMENT_21, fn() => Blocks::ELEMENT_SCANDIUM()); - $this->mapSimple(Ids::ELEMENT_22, fn() => Blocks::ELEMENT_TITANIUM()); - $this->mapSimple(Ids::ELEMENT_23, fn() => Blocks::ELEMENT_VANADIUM()); - $this->mapSimple(Ids::ELEMENT_24, fn() => Blocks::ELEMENT_CHROMIUM()); - $this->mapSimple(Ids::ELEMENT_25, fn() => Blocks::ELEMENT_MANGANESE()); - $this->mapSimple(Ids::ELEMENT_26, fn() => Blocks::ELEMENT_IRON()); - $this->mapSimple(Ids::ELEMENT_27, fn() => Blocks::ELEMENT_COBALT()); - $this->mapSimple(Ids::ELEMENT_28, fn() => Blocks::ELEMENT_NICKEL()); - $this->mapSimple(Ids::ELEMENT_29, fn() => Blocks::ELEMENT_COPPER()); - $this->mapSimple(Ids::ELEMENT_3, fn() => Blocks::ELEMENT_LITHIUM()); - $this->mapSimple(Ids::ELEMENT_30, fn() => Blocks::ELEMENT_ZINC()); - $this->mapSimple(Ids::ELEMENT_31, fn() => Blocks::ELEMENT_GALLIUM()); - $this->mapSimple(Ids::ELEMENT_32, fn() => Blocks::ELEMENT_GERMANIUM()); - $this->mapSimple(Ids::ELEMENT_33, fn() => Blocks::ELEMENT_ARSENIC()); - $this->mapSimple(Ids::ELEMENT_34, fn() => Blocks::ELEMENT_SELENIUM()); - $this->mapSimple(Ids::ELEMENT_35, fn() => Blocks::ELEMENT_BROMINE()); - $this->mapSimple(Ids::ELEMENT_36, fn() => Blocks::ELEMENT_KRYPTON()); - $this->mapSimple(Ids::ELEMENT_37, fn() => Blocks::ELEMENT_RUBIDIUM()); - $this->mapSimple(Ids::ELEMENT_38, fn() => Blocks::ELEMENT_STRONTIUM()); - $this->mapSimple(Ids::ELEMENT_39, fn() => Blocks::ELEMENT_YTTRIUM()); - $this->mapSimple(Ids::ELEMENT_4, fn() => Blocks::ELEMENT_BERYLLIUM()); - $this->mapSimple(Ids::ELEMENT_40, fn() => Blocks::ELEMENT_ZIRCONIUM()); - $this->mapSimple(Ids::ELEMENT_41, fn() => Blocks::ELEMENT_NIOBIUM()); - $this->mapSimple(Ids::ELEMENT_42, fn() => Blocks::ELEMENT_MOLYBDENUM()); - $this->mapSimple(Ids::ELEMENT_43, fn() => Blocks::ELEMENT_TECHNETIUM()); - $this->mapSimple(Ids::ELEMENT_44, fn() => Blocks::ELEMENT_RUTHENIUM()); - $this->mapSimple(Ids::ELEMENT_45, fn() => Blocks::ELEMENT_RHODIUM()); - $this->mapSimple(Ids::ELEMENT_46, fn() => Blocks::ELEMENT_PALLADIUM()); - $this->mapSimple(Ids::ELEMENT_47, fn() => Blocks::ELEMENT_SILVER()); - $this->mapSimple(Ids::ELEMENT_48, fn() => Blocks::ELEMENT_CADMIUM()); - $this->mapSimple(Ids::ELEMENT_49, fn() => Blocks::ELEMENT_INDIUM()); - $this->mapSimple(Ids::ELEMENT_5, fn() => Blocks::ELEMENT_BORON()); - $this->mapSimple(Ids::ELEMENT_50, fn() => Blocks::ELEMENT_TIN()); - $this->mapSimple(Ids::ELEMENT_51, fn() => Blocks::ELEMENT_ANTIMONY()); - $this->mapSimple(Ids::ELEMENT_52, fn() => Blocks::ELEMENT_TELLURIUM()); - $this->mapSimple(Ids::ELEMENT_53, fn() => Blocks::ELEMENT_IODINE()); - $this->mapSimple(Ids::ELEMENT_54, fn() => Blocks::ELEMENT_XENON()); - $this->mapSimple(Ids::ELEMENT_55, fn() => Blocks::ELEMENT_CESIUM()); - $this->mapSimple(Ids::ELEMENT_56, fn() => Blocks::ELEMENT_BARIUM()); - $this->mapSimple(Ids::ELEMENT_57, fn() => Blocks::ELEMENT_LANTHANUM()); - $this->mapSimple(Ids::ELEMENT_58, fn() => Blocks::ELEMENT_CERIUM()); - $this->mapSimple(Ids::ELEMENT_59, fn() => Blocks::ELEMENT_PRASEODYMIUM()); - $this->mapSimple(Ids::ELEMENT_6, fn() => Blocks::ELEMENT_CARBON()); - $this->mapSimple(Ids::ELEMENT_60, fn() => Blocks::ELEMENT_NEODYMIUM()); - $this->mapSimple(Ids::ELEMENT_61, fn() => Blocks::ELEMENT_PROMETHIUM()); - $this->mapSimple(Ids::ELEMENT_62, fn() => Blocks::ELEMENT_SAMARIUM()); - $this->mapSimple(Ids::ELEMENT_63, fn() => Blocks::ELEMENT_EUROPIUM()); - $this->mapSimple(Ids::ELEMENT_64, fn() => Blocks::ELEMENT_GADOLINIUM()); - $this->mapSimple(Ids::ELEMENT_65, fn() => Blocks::ELEMENT_TERBIUM()); - $this->mapSimple(Ids::ELEMENT_66, fn() => Blocks::ELEMENT_DYSPROSIUM()); - $this->mapSimple(Ids::ELEMENT_67, fn() => Blocks::ELEMENT_HOLMIUM()); - $this->mapSimple(Ids::ELEMENT_68, fn() => Blocks::ELEMENT_ERBIUM()); - $this->mapSimple(Ids::ELEMENT_69, fn() => Blocks::ELEMENT_THULIUM()); - $this->mapSimple(Ids::ELEMENT_7, fn() => Blocks::ELEMENT_NITROGEN()); - $this->mapSimple(Ids::ELEMENT_70, fn() => Blocks::ELEMENT_YTTERBIUM()); - $this->mapSimple(Ids::ELEMENT_71, fn() => Blocks::ELEMENT_LUTETIUM()); - $this->mapSimple(Ids::ELEMENT_72, fn() => Blocks::ELEMENT_HAFNIUM()); - $this->mapSimple(Ids::ELEMENT_73, fn() => Blocks::ELEMENT_TANTALUM()); - $this->mapSimple(Ids::ELEMENT_74, fn() => Blocks::ELEMENT_TUNGSTEN()); - $this->mapSimple(Ids::ELEMENT_75, fn() => Blocks::ELEMENT_RHENIUM()); - $this->mapSimple(Ids::ELEMENT_76, fn() => Blocks::ELEMENT_OSMIUM()); - $this->mapSimple(Ids::ELEMENT_77, fn() => Blocks::ELEMENT_IRIDIUM()); - $this->mapSimple(Ids::ELEMENT_78, fn() => Blocks::ELEMENT_PLATINUM()); - $this->mapSimple(Ids::ELEMENT_79, fn() => Blocks::ELEMENT_GOLD()); - $this->mapSimple(Ids::ELEMENT_8, fn() => Blocks::ELEMENT_OXYGEN()); - $this->mapSimple(Ids::ELEMENT_80, fn() => Blocks::ELEMENT_MERCURY()); - $this->mapSimple(Ids::ELEMENT_81, fn() => Blocks::ELEMENT_THALLIUM()); - $this->mapSimple(Ids::ELEMENT_82, fn() => Blocks::ELEMENT_LEAD()); - $this->mapSimple(Ids::ELEMENT_83, fn() => Blocks::ELEMENT_BISMUTH()); - $this->mapSimple(Ids::ELEMENT_84, fn() => Blocks::ELEMENT_POLONIUM()); - $this->mapSimple(Ids::ELEMENT_85, fn() => Blocks::ELEMENT_ASTATINE()); - $this->mapSimple(Ids::ELEMENT_86, fn() => Blocks::ELEMENT_RADON()); - $this->mapSimple(Ids::ELEMENT_87, fn() => Blocks::ELEMENT_FRANCIUM()); - $this->mapSimple(Ids::ELEMENT_88, fn() => Blocks::ELEMENT_RADIUM()); - $this->mapSimple(Ids::ELEMENT_89, fn() => Blocks::ELEMENT_ACTINIUM()); - $this->mapSimple(Ids::ELEMENT_9, fn() => Blocks::ELEMENT_FLUORINE()); - $this->mapSimple(Ids::ELEMENT_90, fn() => Blocks::ELEMENT_THORIUM()); - $this->mapSimple(Ids::ELEMENT_91, fn() => Blocks::ELEMENT_PROTACTINIUM()); - $this->mapSimple(Ids::ELEMENT_92, fn() => Blocks::ELEMENT_URANIUM()); - $this->mapSimple(Ids::ELEMENT_93, fn() => Blocks::ELEMENT_NEPTUNIUM()); - $this->mapSimple(Ids::ELEMENT_94, fn() => Blocks::ELEMENT_PLUTONIUM()); - $this->mapSimple(Ids::ELEMENT_95, fn() => Blocks::ELEMENT_AMERICIUM()); - $this->mapSimple(Ids::ELEMENT_96, fn() => Blocks::ELEMENT_CURIUM()); - $this->mapSimple(Ids::ELEMENT_97, fn() => Blocks::ELEMENT_BERKELIUM()); - $this->mapSimple(Ids::ELEMENT_98, fn() => Blocks::ELEMENT_CALIFORNIUM()); - $this->mapSimple(Ids::ELEMENT_99, fn() => Blocks::ELEMENT_EINSTEINIUM()); - $this->mapSimple(Ids::EMERALD_BLOCK, fn() => Blocks::EMERALD()); - $this->mapSimple(Ids::EMERALD_ORE, fn() => Blocks::EMERALD_ORE()); - $this->mapSimple(Ids::ENCHANTING_TABLE, fn() => Blocks::ENCHANTING_TABLE()); - $this->mapSimple(Ids::END_BRICKS, fn() => Blocks::END_STONE_BRICKS()); - $this->mapSimple(Ids::END_STONE, fn() => Blocks::END_STONE()); - $this->mapSimple(Ids::FERN, fn() => Blocks::FERN()); - $this->mapSimple(Ids::FLETCHING_TABLE, fn() => Blocks::FLETCHING_TABLE()); - $this->mapSimple(Ids::GILDED_BLACKSTONE, fn() => Blocks::GILDED_BLACKSTONE()); - $this->mapSimple(Ids::GLASS, fn() => Blocks::GLASS()); - $this->mapSimple(Ids::GLASS_PANE, fn() => Blocks::GLASS_PANE()); - $this->mapSimple(Ids::GLOWINGOBSIDIAN, fn() => Blocks::GLOWING_OBSIDIAN()); - $this->mapSimple(Ids::GLOWSTONE, fn() => Blocks::GLOWSTONE()); - $this->mapSimple(Ids::GOLD_BLOCK, fn() => Blocks::GOLD()); - $this->mapSimple(Ids::GOLD_ORE, fn() => Blocks::GOLD_ORE()); - $this->mapSimple(Ids::GRANITE, fn() => Blocks::GRANITE()); - $this->mapSimple(Ids::GRASS_BLOCK, fn() => Blocks::GRASS()); - $this->mapSimple(Ids::GRASS_PATH, fn() => Blocks::GRASS_PATH()); - $this->mapSimple(Ids::GRAVEL, fn() => Blocks::GRAVEL()); - $this->mapSimple(Ids::HANGING_ROOTS, fn() => Blocks::HANGING_ROOTS()); - $this->mapSimple(Ids::HARD_GLASS, fn() => Blocks::HARDENED_GLASS()); - $this->mapSimple(Ids::HARD_GLASS_PANE, fn() => Blocks::HARDENED_GLASS_PANE()); - $this->mapSimple(Ids::HARDENED_CLAY, fn() => Blocks::HARDENED_CLAY()); - $this->mapSimple(Ids::HONEYCOMB_BLOCK, fn() => Blocks::HONEYCOMB()); - $this->mapSimple(Ids::ICE, fn() => Blocks::ICE()); - $this->mapSimple(Ids::INFESTED_CHISELED_STONE_BRICKS, fn() => Blocks::INFESTED_CHISELED_STONE_BRICK()); - $this->mapSimple(Ids::INFESTED_COBBLESTONE, fn() => Blocks::INFESTED_COBBLESTONE()); - $this->mapSimple(Ids::INFESTED_CRACKED_STONE_BRICKS, fn() => Blocks::INFESTED_CRACKED_STONE_BRICK()); - $this->mapSimple(Ids::INFESTED_MOSSY_STONE_BRICKS, fn() => Blocks::INFESTED_MOSSY_STONE_BRICK()); - $this->mapSimple(Ids::INFESTED_STONE, fn() => Blocks::INFESTED_STONE()); - $this->mapSimple(Ids::INFESTED_STONE_BRICKS, fn() => Blocks::INFESTED_STONE_BRICK()); - $this->mapSimple(Ids::INFO_UPDATE, fn() => Blocks::INFO_UPDATE()); - $this->mapSimple(Ids::INFO_UPDATE2, fn() => Blocks::INFO_UPDATE2()); - $this->mapSimple(Ids::INVISIBLE_BEDROCK, fn() => Blocks::INVISIBLE_BEDROCK()); - $this->mapSimple(Ids::IRON_BARS, fn() => Blocks::IRON_BARS()); - $this->mapSimple(Ids::IRON_BLOCK, fn() => Blocks::IRON()); - $this->mapSimple(Ids::IRON_ORE, fn() => Blocks::IRON_ORE()); - $this->mapSimple(Ids::JUKEBOX, fn() => Blocks::JUKEBOX()); - $this->mapSimple(Ids::LAPIS_BLOCK, fn() => Blocks::LAPIS_LAZULI()); - $this->mapSimple(Ids::LAPIS_ORE, fn() => Blocks::LAPIS_LAZULI_ORE()); - $this->mapSimple(Ids::MAGMA, fn() => Blocks::MAGMA()); - $this->mapSimple(Ids::MANGROVE_ROOTS, fn() => Blocks::MANGROVE_ROOTS()); - $this->mapSimple(Ids::MELON_BLOCK, fn() => Blocks::MELON()); - $this->mapSimple(Ids::MOB_SPAWNER, fn() => Blocks::MONSTER_SPAWNER()); - $this->mapSimple(Ids::MOSSY_COBBLESTONE, fn() => Blocks::MOSSY_COBBLESTONE()); - $this->mapSimple(Ids::MOSSY_STONE_BRICKS, fn() => Blocks::MOSSY_STONE_BRICKS()); - $this->mapSimple(Ids::MUD, fn() => Blocks::MUD()); - $this->mapSimple(Ids::MUD_BRICKS, fn() => Blocks::MUD_BRICKS()); - $this->mapSimple(Ids::MYCELIUM, fn() => Blocks::MYCELIUM()); - $this->mapSimple(Ids::NETHER_BRICK, fn() => Blocks::NETHER_BRICKS()); - $this->mapSimple(Ids::NETHER_BRICK_FENCE, fn() => Blocks::NETHER_BRICK_FENCE()); - $this->mapSimple(Ids::NETHER_GOLD_ORE, fn() => Blocks::NETHER_GOLD_ORE()); - $this->mapSimple(Ids::NETHER_WART_BLOCK, fn() => Blocks::NETHER_WART_BLOCK()); - $this->mapSimple(Ids::NETHERITE_BLOCK, fn() => Blocks::NETHERITE()); - $this->mapSimple(Ids::NETHERRACK, fn() => Blocks::NETHERRACK()); - $this->mapSimple(Ids::NETHERREACTOR, fn() => Blocks::NETHER_REACTOR_CORE()); - $this->mapSimple(Ids::NOTEBLOCK, fn() => Blocks::NOTE_BLOCK()); - $this->mapSimple(Ids::OBSIDIAN, fn() => Blocks::OBSIDIAN()); - $this->mapSimple(Ids::PACKED_ICE, fn() => Blocks::PACKED_ICE()); - $this->mapSimple(Ids::PACKED_MUD, fn() => Blocks::PACKED_MUD()); - $this->mapSimple(Ids::PODZOL, fn() => Blocks::PODZOL()); - $this->mapSimple(Ids::POLISHED_ANDESITE, fn() => Blocks::POLISHED_ANDESITE()); - $this->mapSimple(Ids::POLISHED_BLACKSTONE, fn() => Blocks::POLISHED_BLACKSTONE()); - $this->mapSimple(Ids::POLISHED_BLACKSTONE_BRICKS, fn() => Blocks::POLISHED_BLACKSTONE_BRICKS()); - $this->mapSimple(Ids::POLISHED_DEEPSLATE, fn() => Blocks::POLISHED_DEEPSLATE()); - $this->mapSimple(Ids::POLISHED_DIORITE, fn() => Blocks::POLISHED_DIORITE()); - $this->mapSimple(Ids::POLISHED_GRANITE, fn() => Blocks::POLISHED_GRANITE()); - $this->mapSimple(Ids::POLISHED_TUFF, fn() => Blocks::POLISHED_TUFF()); - $this->mapSimple(Ids::PRISMARINE, fn() => Blocks::PRISMARINE()); - $this->mapSimple(Ids::PRISMARINE_BRICKS, fn() => Blocks::PRISMARINE_BRICKS()); - $this->mapSimple(Ids::QUARTZ_BRICKS, fn() => Blocks::QUARTZ_BRICKS()); - $this->mapSimple(Ids::QUARTZ_ORE, fn() => Blocks::NETHER_QUARTZ_ORE()); - $this->mapSimple(Ids::RAW_COPPER_BLOCK, fn() => Blocks::RAW_COPPER()); - $this->mapSimple(Ids::RAW_GOLD_BLOCK, fn() => Blocks::RAW_GOLD()); - $this->mapSimple(Ids::RAW_IRON_BLOCK, fn() => Blocks::RAW_IRON()); - $this->mapSimple(Ids::RED_MUSHROOM, fn() => Blocks::RED_MUSHROOM()); - $this->mapSimple(Ids::RED_NETHER_BRICK, fn() => Blocks::RED_NETHER_BRICKS()); - $this->mapSimple(Ids::RED_SAND, fn() => Blocks::RED_SAND()); - $this->mapSimple(Ids::RED_SANDSTONE, fn() => Blocks::RED_SANDSTONE()); - $this->mapSimple(Ids::REDSTONE_BLOCK, fn() => Blocks::REDSTONE()); - $this->mapSimple(Ids::REINFORCED_DEEPSLATE, fn() => Blocks::REINFORCED_DEEPSLATE()); - $this->mapSimple(Ids::RESERVED6, fn() => Blocks::RESERVED6()); - $this->mapSimple(Ids::SAND, fn() => Blocks::SAND()); - $this->mapSimple(Ids::SANDSTONE, fn() => Blocks::SANDSTONE()); - $this->mapSimple(Ids::SCULK, fn() => Blocks::SCULK()); - $this->mapSimple(Ids::SEA_LANTERN, fn() => Blocks::SEA_LANTERN()); - $this->mapSimple(Ids::SHORT_GRASS, fn() => Blocks::TALL_GRASS()); //no, this is not a typo - tall_grass is now the double block, just to be confusing :( - $this->mapSimple(Ids::SHROOMLIGHT, fn() => Blocks::SHROOMLIGHT()); - $this->mapSimple(Ids::SLIME, fn() => Blocks::SLIME()); - $this->mapSimple(Ids::SMITHING_TABLE, fn() => Blocks::SMITHING_TABLE()); - $this->mapSimple(Ids::SMOOTH_BASALT, fn() => Blocks::SMOOTH_BASALT()); - $this->mapSimple(Ids::SMOOTH_RED_SANDSTONE, fn() => Blocks::SMOOTH_RED_SANDSTONE()); - $this->mapSimple(Ids::SMOOTH_SANDSTONE, fn() => Blocks::SMOOTH_SANDSTONE()); - $this->mapSimple(Ids::SMOOTH_STONE, fn() => Blocks::SMOOTH_STONE()); - $this->mapSimple(Ids::SNOW, fn() => Blocks::SNOW()); - $this->mapSimple(Ids::SOUL_SAND, fn() => Blocks::SOUL_SAND()); - $this->mapSimple(Ids::SOUL_SOIL, fn() => Blocks::SOUL_SOIL()); - $this->mapSimple(Ids::SPORE_BLOSSOM, fn() => Blocks::SPORE_BLOSSOM()); - $this->mapSimple(Ids::SPONGE, fn() => Blocks::SPONGE()); - $this->mapSimple(Ids::STONE, fn() => Blocks::STONE()); - $this->mapSimple(Ids::STONECUTTER, fn() => Blocks::LEGACY_STONECUTTER()); - $this->mapSimple(Ids::STONE_BRICKS, fn() => Blocks::STONE_BRICKS()); - $this->mapSimple(Ids::TINTED_GLASS, fn() => Blocks::TINTED_GLASS()); - $this->mapSimple(Ids::TORCHFLOWER, fn() => Blocks::TORCHFLOWER()); - $this->mapSimple(Ids::TUFF, fn() => Blocks::TUFF()); - $this->mapSimple(Ids::TUFF_BRICKS, fn() => Blocks::TUFF_BRICKS()); - $this->mapSimple(Ids::UNDYED_SHULKER_BOX, fn() => Blocks::SHULKER_BOX()); - $this->mapSimple(Ids::WARPED_WART_BLOCK, fn() => Blocks::WARPED_WART_BLOCK()); - $this->mapSimple(Ids::WARPED_ROOTS, fn() => Blocks::WARPED_ROOTS()); - $this->mapSimple(Ids::WATERLILY, fn() => Blocks::LILY_PAD()); - $this->mapSimple(Ids::WEB, fn() => Blocks::COBWEB()); - $this->mapSimple(Ids::WET_SPONGE, fn() => Blocks::SPONGE()->setWet(true)); - $this->mapSimple(Ids::WITHER_ROSE, fn() => Blocks::WITHER_ROSE()); - $this->mapSimple(Ids::DANDELION, fn() => Blocks::DANDELION()); - - $this->mapSimple(Ids::ALLIUM, fn() => Blocks::ALLIUM()); - $this->mapSimple(Ids::CORNFLOWER, fn() => Blocks::CORNFLOWER()); - $this->mapSimple(Ids::AZURE_BLUET, fn() => Blocks::AZURE_BLUET()); - $this->mapSimple(Ids::LILY_OF_THE_VALLEY, fn() => Blocks::LILY_OF_THE_VALLEY()); - $this->mapSimple(Ids::BLUE_ORCHID, fn() => Blocks::BLUE_ORCHID()); - $this->mapSimple(Ids::OXEYE_DAISY, fn() => Blocks::OXEYE_DAISY()); - $this->mapSimple(Ids::POPPY, fn() => Blocks::POPPY()); - $this->mapSimple(Ids::ORANGE_TULIP, fn() => Blocks::ORANGE_TULIP()); - $this->mapSimple(Ids::PINK_TULIP, fn() => Blocks::PINK_TULIP()); - $this->mapSimple(Ids::RED_TULIP, fn() => Blocks::RED_TULIP()); - $this->mapSimple(Ids::WHITE_TULIP, fn() => Blocks::WHITE_TULIP()); - } - - private function registerDeserializers() : void{ - $this->map(Ids::ACTIVATOR_RAIL, function(Reader $in) : Block{ - return Blocks::ACTIVATOR_RAIL() - ->setPowered($in->readBool(StateNames::RAIL_DATA_BIT)) - ->setShape($in->readBoundedInt(StateNames::RAIL_DIRECTION, 0, 5)); - }); - $this->map(Ids::AMETHYST_CLUSTER, function(Reader $in) : Block{ - return Blocks::AMETHYST_CLUSTER() - ->setStage(AmethystCluster::STAGE_CLUSTER) - ->setFacing($in->readBlockFace()); - }); - $this->mapSlab(Ids::ANDESITE_SLAB, Ids::ANDESITE_DOUBLE_SLAB, fn() => Blocks::ANDESITE_SLAB()); - $this->mapStairs(Ids::ANDESITE_STAIRS, fn() => Blocks::ANDESITE_STAIRS()); - $this->map(Ids::ANDESITE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::ANDESITE_WALL(), $in)); - $this->map(Ids::ANVIL, function(Reader $in) : Block{ - return Blocks::ANVIL() - ->setDamage(Anvil::UNDAMAGED) - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::CHIPPED_ANVIL, function(Reader $in) : Block{ - return Blocks::ANVIL() - ->setDamage(Anvil::SLIGHTLY_DAMAGED) - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::DAMAGED_ANVIL, function(Reader $in) : Block{ - return Blocks::ANVIL() - ->setDamage(Anvil::VERY_DAMAGED) - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::BAMBOO, function(Reader $in) : Block{ - return Blocks::BAMBOO() - ->setLeafSize(match($value = $in->readString(StateNames::BAMBOO_LEAF_SIZE)){ - StringValues::BAMBOO_LEAF_SIZE_NO_LEAVES => Bamboo::NO_LEAVES, - StringValues::BAMBOO_LEAF_SIZE_SMALL_LEAVES => Bamboo::SMALL_LEAVES, - StringValues::BAMBOO_LEAF_SIZE_LARGE_LEAVES => Bamboo::LARGE_LEAVES, - default => throw $in->badValueException(StateNames::BAMBOO_LEAF_SIZE, $value), - }) - ->setReady($in->readBool(StateNames::AGE_BIT)) - ->setThick(match($value = $in->readString(StateNames::BAMBOO_STALK_THICKNESS)){ - StringValues::BAMBOO_STALK_THICKNESS_THIN => false, - StringValues::BAMBOO_STALK_THICKNESS_THICK => true, - default => throw $in->badValueException(StateNames::BAMBOO_STALK_THICKNESS, $value), - }); - }); - $this->map(Ids::BAMBOO_SAPLING, function(Reader $in) : Block{ - return Blocks::BAMBOO_SAPLING()->setReady($in->readBool(StateNames::AGE_BIT)); - }); - $this->map(Ids::BARREL, function(Reader $in) : Block{ - return Blocks::BARREL() - ->setFacing($in->readFacingDirection()) - ->setOpen($in->readBool(StateNames::OPEN_BIT)); - }); - $this->map(Ids::BASALT, function(Reader $in){ - return Blocks::BASALT() - ->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::BED, function(Reader $in) : Block{ - return Blocks::BED() - ->setFacing($in->readLegacyHorizontalFacing()) - ->setHead($in->readBool(StateNames::HEAD_PIECE_BIT)) - ->setOccupied($in->readBool(StateNames::OCCUPIED_BIT)); - }); - $this->map(Ids::BEDROCK, function(Reader $in) : Block{ - return Blocks::BEDROCK() - ->setBurnsForever($in->readBool(StateNames::INFINIBURN_BIT)); - }); - $this->map(Ids::BEETROOT, fn(Reader $in) => Helper::decodeCrops(Blocks::BEETROOTS(), $in)); - $this->map(Ids::BELL, function(Reader $in) : Block{ - $in->ignored(StateNames::TOGGLE_BIT); //only useful at runtime - return Blocks::BELL() - ->setFacing($in->readLegacyHorizontalFacing()) - ->setAttachmentType($in->readBellAttachmentType()); - }); - $this->map(Ids::BIG_DRIPLEAF, function(Reader $in) : Block{ - if($in->readBool(StateNames::BIG_DRIPLEAF_HEAD)){ - return Blocks::BIG_DRIPLEAF_HEAD() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLeafState(match($type = $in->readString(StateNames::BIG_DRIPLEAF_TILT)){ - StringValues::BIG_DRIPLEAF_TILT_NONE => DripleafState::STABLE, - StringValues::BIG_DRIPLEAF_TILT_UNSTABLE => DripleafState::UNSTABLE, - StringValues::BIG_DRIPLEAF_TILT_PARTIAL_TILT => DripleafState::PARTIAL_TILT, - StringValues::BIG_DRIPLEAF_TILT_FULL_TILT => DripleafState::FULL_TILT, - default => throw $in->badValueException(StateNames::BIG_DRIPLEAF_TILT, $type), - }); - }else{ - $in->ignored(StateNames::BIG_DRIPLEAF_TILT); - return Blocks::BIG_DRIPLEAF_STEM()->setFacing($in->readCardinalHorizontalFacing()); - } - }); - $this->mapSlab(Ids::BLACKSTONE_SLAB, Ids::BLACKSTONE_DOUBLE_SLAB, fn() => Blocks::BLACKSTONE_SLAB()); - $this->mapStairs(Ids::BLACKSTONE_STAIRS, fn() => Blocks::BLACKSTONE_STAIRS()); - $this->map(Ids::BLACKSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::BLACKSTONE_WALL(), $in)); - $this->map(Ids::BLAST_FURNACE, function(Reader $in) : Block{ - return Blocks::BLAST_FURNACE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(false); - }); - $this->map(Ids::BONE_BLOCK, function(Reader $in) : Block{ - $in->ignored(StateNames::DEPRECATED); - return Blocks::BONE_BLOCK()->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::BREWING_STAND, function(Reader $in) : Block{ - return Blocks::BREWING_STAND() - ->setSlot(BrewingStandSlot::EAST, $in->readBool(StateNames::BREWING_STAND_SLOT_A_BIT)) - ->setSlot(BrewingStandSlot::SOUTHWEST, $in->readBool(StateNames::BREWING_STAND_SLOT_B_BIT)) - ->setSlot(BrewingStandSlot::NORTHWEST, $in->readBool(StateNames::BREWING_STAND_SLOT_C_BIT)); - }); - $this->mapSlab(Ids::BRICK_SLAB, Ids::BRICK_DOUBLE_SLAB, fn() => Blocks::BRICK_SLAB()); - $this->mapStairs(Ids::BRICK_STAIRS, fn() => Blocks::BRICK_STAIRS()); - $this->map(Ids::BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::BRICK_WALL(), $in)); - $this->map(Ids::MUSHROOM_STEM, fn(Reader $in) => match($in->readBoundedInt(StateNames::HUGE_MUSHROOM_BITS, 0, 15)){ - BlockLegacyMetadata::MUSHROOM_BLOCK_ALL_STEM => Blocks::ALL_SIDED_MUSHROOM_STEM(), - BlockLegacyMetadata::MUSHROOM_BLOCK_STEM => Blocks::MUSHROOM_STEM(), - default => throw new BlockStateDeserializeException("This state does not exist"), - }); - $this->map(Ids::BROWN_MUSHROOM_BLOCK, fn(Reader $in) => Helper::decodeMushroomBlock(Blocks::BROWN_MUSHROOM_BLOCK(), $in)); - $this->map(Ids::CACTUS, function(Reader $in) : Block{ - return Blocks::CACTUS() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 15)); - }); - $this->map(Ids::CAKE, function(Reader $in) : Block{ - return Blocks::CAKE() - ->setBites($in->readBoundedInt(StateNames::BITE_COUNTER, 0, 6)); - }); - $this->map(Ids::CAMPFIRE, function(Reader $in) : Block{ - return Blocks::CAMPFIRE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(!$in->readBool(StateNames::EXTINGUISHED)); - }); - $this->map(Ids::CARROTS, fn(Reader $in) => Helper::decodeCrops(Blocks::CARROTS(), $in)); - $this->map(Ids::CARVED_PUMPKIN, function(Reader $in) : Block{ - return Blocks::CARVED_PUMPKIN() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::CAVE_VINES, function(Reader $in) : CaveVines{ - return Blocks::CAVE_VINES() - ->setBerries(false) - ->setHead(false) - ->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25)); - }); - $this->map(Ids::CAVE_VINES_BODY_WITH_BERRIES, function(Reader $in) : CaveVines{ - return Blocks::CAVE_VINES() - ->setBerries(true) - ->setHead(false) - ->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25)); - }); - $this->map(Ids::CAVE_VINES_HEAD_WITH_BERRIES, function(Reader $in) : CaveVines{ - return Blocks::CAVE_VINES() - ->setBerries(true) - ->setHead(true) - ->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25)); - }); - $this->map(Ids::CHAIN, function(Reader $in) : Block{ - return Blocks::CHAIN() - ->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::CHISELED_BOOKSHELF, function(Reader $in) : Block{ - $block = Blocks::CHISELED_BOOKSHELF() - ->setFacing($in->readLegacyHorizontalFacing()); - - //we don't use API constant for bounds here as the data bounds might be different to what we support internally - $flags = $in->readBoundedInt(StateNames::BOOKS_STORED, 0, (1 << 6) - 1); - foreach(ChiseledBookshelfSlot::cases() as $slot){ - $block->setSlot($slot, ($flags & (1 << $slot->value)) !== 0); - } - - return $block; - }); - $this->map(Ids::CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::NONE)); - $this->map(Ids::CHISELED_QUARTZ_BLOCK, function(Reader $in) : Block{ - return Blocks::CHISELED_QUARTZ() - ->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::CHEST, function(Reader $in) : Block{ - return Blocks::CHEST() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::CHORUS_FLOWER, function(Reader $in) : Block{ - return Blocks::CHORUS_FLOWER() - ->setAge($in->readBoundedInt(StateNames::AGE, ChorusFlower::MIN_AGE, ChorusFlower::MAX_AGE)); - }); - $this->map(Ids::COARSE_DIRT, fn() => Blocks::DIRT()->setDirtType(DirtType::COARSE)); - $this->mapSlab(Ids::COBBLED_DEEPSLATE_SLAB, Ids::COBBLED_DEEPSLATE_DOUBLE_SLAB, fn() => Blocks::COBBLED_DEEPSLATE_SLAB()); - $this->mapStairs(Ids::COBBLED_DEEPSLATE_STAIRS, fn() => Blocks::COBBLED_DEEPSLATE_STAIRS()); - $this->map(Ids::COBBLED_DEEPSLATE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::COBBLED_DEEPSLATE_WALL(), $in)); - $this->mapSlab(Ids::COBBLESTONE_SLAB, Ids::COBBLESTONE_DOUBLE_SLAB, fn() => Blocks::COBBLESTONE_SLAB()); - $this->map(Ids::COBBLESTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::COBBLESTONE_WALL(), $in)); - $this->map(Ids::COCOA, function(Reader $in) : Block{ - return Blocks::COCOA_POD() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 2)) - ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())); - }); - $this->map(Ids::COLORED_TORCH_BLUE, fn(Reader $in) => Blocks::BLUE_TORCH()->setFacing($in->readTorchFacing())); - $this->map(Ids::COLORED_TORCH_GREEN, fn(Reader $in) => Blocks::GREEN_TORCH()->setFacing($in->readTorchFacing())); - $this->map(Ids::COLORED_TORCH_PURPLE, fn(Reader $in) => Blocks::PURPLE_TORCH()->setFacing($in->readTorchFacing())); - $this->map(Ids::COLORED_TORCH_RED, fn(Reader $in) => Blocks::RED_TORCH()->setFacing($in->readTorchFacing())); - $this->map(Ids::COMPOUND_CREATOR, fn(Reader $in) => Blocks::COMPOUND_CREATOR() - ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())) - ); - $this->map(Ids::COPPER_BLOCK, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::NONE)); - $this->map(Ids::COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in)); - $this->map(Ids::COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE)); - $this->map(Ids::COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in)); - $this->map(Ids::CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE)); - $this->mapSlab(Ids::CUT_COPPER_SLAB, Ids::DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE)); - $this->mapStairs(Ids::CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE)); - $this->mapSlab(Ids::CUT_RED_SANDSTONE_SLAB, Ids::CUT_RED_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::CUT_RED_SANDSTONE_SLAB()); - $this->mapSlab(Ids::CUT_SANDSTONE_SLAB, Ids::CUT_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::CUT_SANDSTONE_SLAB()); - $this->mapSlab(Ids::DARK_PRISMARINE_SLAB, Ids::DARK_PRISMARINE_DOUBLE_SLAB, fn() => Blocks::DARK_PRISMARINE_SLAB()); - $this->mapStairs(Ids::DARK_PRISMARINE_STAIRS, fn() => Blocks::DARK_PRISMARINE_STAIRS()); - $this->map(Ids::DAYLIGHT_DETECTOR, fn(Reader $in) => Helper::decodeDaylightSensor(Blocks::DAYLIGHT_SENSOR(), $in) - ->setInverted(false)); - $this->map(Ids::DAYLIGHT_DETECTOR_INVERTED, fn(Reader $in) => Helper::decodeDaylightSensor(Blocks::DAYLIGHT_SENSOR(), $in) - ->setInverted(true)); - $this->map(Ids::DEEPSLATE, function(Reader $in) : Block{ - return Blocks::DEEPSLATE() - ->setAxis($in->readPillarAxis()); - }); - $this->mapSlab(Ids::DEEPSLATE_BRICK_SLAB, Ids::DEEPSLATE_BRICK_DOUBLE_SLAB, fn() => Blocks::DEEPSLATE_BRICK_SLAB()); - $this->mapStairs(Ids::DEEPSLATE_BRICK_STAIRS, fn() => Blocks::DEEPSLATE_BRICK_STAIRS()); - $this->map(Ids::DEEPSLATE_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::DEEPSLATE_BRICK_WALL(), $in)); - $this->map(Ids::DEEPSLATE_REDSTONE_ORE, fn() => Blocks::DEEPSLATE_REDSTONE_ORE()->setLit(false)); - $this->mapSlab(Ids::DEEPSLATE_TILE_SLAB, Ids::DEEPSLATE_TILE_DOUBLE_SLAB, fn() => Blocks::DEEPSLATE_TILE_SLAB()); - $this->mapStairs(Ids::DEEPSLATE_TILE_STAIRS, fn() => Blocks::DEEPSLATE_TILE_STAIRS()); - $this->map(Ids::DEEPSLATE_TILE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::DEEPSLATE_TILE_WALL(), $in)); - $this->map(Ids::DETECTOR_RAIL, function(Reader $in) : Block{ - return Blocks::DETECTOR_RAIL() - ->setActivated($in->readBool(StateNames::RAIL_DATA_BIT)) - ->setShape($in->readBoundedInt(StateNames::RAIL_DIRECTION, 0, 5)); - }); - $this->mapSlab(Ids::DIORITE_SLAB, Ids::DIORITE_DOUBLE_SLAB, fn() => Blocks::DIORITE_SLAB()); - $this->mapStairs(Ids::DIORITE_STAIRS, fn() => Blocks::DIORITE_STAIRS()); - $this->map(Ids::DIORITE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::DIORITE_WALL(), $in)); - $this->map(Ids::DIRT, fn() => Blocks::DIRT()->setDirtType(DirtType::NORMAL)); - $this->map(Ids::DIRT_WITH_ROOTS, fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED)); - $this->map(Ids::LARGE_FERN, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::LARGE_FERN(), $in)); - $this->map(Ids::TALL_GRASS, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::DOUBLE_TALLGRASS(), $in)); - $this->map(Ids::PEONY, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::PEONY(), $in)); - $this->map(Ids::ROSE_BUSH, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::ROSE_BUSH(), $in)); - $this->map(Ids::SUNFLOWER, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::SUNFLOWER(), $in)); - $this->map(Ids::LILAC, fn(Reader $in) => Helper::decodeDoublePlant(Blocks::LILAC(), $in)); - $this->map(Ids::ELEMENT_CONSTRUCTOR, fn(Reader $in) => Blocks::ELEMENT_CONSTRUCTOR() - ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())) - ); - $this->mapStairs(Ids::END_BRICK_STAIRS, fn() => Blocks::END_STONE_BRICK_STAIRS()); - $this->map(Ids::END_STONE_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::END_STONE_BRICK_WALL(), $in)); - $this->map(Ids::END_PORTAL_FRAME, function(Reader $in) : Block{ - return Blocks::END_PORTAL_FRAME() - ->setEye($in->readBool(StateNames::END_PORTAL_EYE_BIT)) - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::END_ROD, function(Reader $in) : Block{ - return Blocks::END_ROD() - ->setFacing($in->readEndRodFacingDirection()); - }); - $this->mapSlab(Ids::END_STONE_BRICK_SLAB, Ids::END_STONE_BRICK_DOUBLE_SLAB, fn() => Blocks::END_STONE_BRICK_SLAB()); - $this->map(Ids::ENDER_CHEST, function(Reader $in) : Block{ - return Blocks::ENDER_CHEST() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::EXPOSED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::EXPOSED)); - $this->map(Ids::EXPOSED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED)); - $this->map(Ids::EXPOSED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED)); - $this->map(Ids::EXPOSED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED)); - $this->mapSlab(Ids::EXPOSED_CUT_COPPER_SLAB, Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED)); - $this->mapStairs(Ids::EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED)); - $this->map(Ids::EXPOSED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in)); - $this->map(Ids::EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in)); - $this->map(Ids::FARMLAND, function(Reader $in) : Block{ - return Blocks::FARMLAND() - ->setWetness($in->readBoundedInt(StateNames::MOISTURIZED_AMOUNT, 0, 7)); - }); - $this->map(Ids::FIRE, function(Reader $in) : Block{ - return Blocks::FIRE() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 15)); - }); - $this->map(Ids::FLOWER_POT, function(Reader $in) : Block{ - $in->ignored(StateNames::UPDATE_BIT); - return Blocks::FLOWER_POT(); - }); - $this->map(Ids::FLOWING_LAVA, fn(Reader $in) => Helper::decodeFlowingLiquid(Blocks::LAVA(), $in)); - $this->map(Ids::FLOWING_WATER, fn(Reader $in) => Helper::decodeFlowingLiquid(Blocks::WATER(), $in)); - $this->map(Ids::FRAME, fn(Reader $in) => Helper::decodeItemFrame(Blocks::ITEM_FRAME(), $in)); - $this->map(Ids::FROSTED_ICE, function(Reader $in) : Block{ - return Blocks::FROSTED_ICE() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 3)); - }); - $this->map(Ids::FURNACE, function(Reader $in) : Block{ - return Blocks::FURNACE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(false); - }); - $this->map(Ids::GLOW_LICHEN, fn(Reader $in) => Blocks::GLOW_LICHEN()->setFaces($in->readFacingFlags())); - $this->map(Ids::GLOW_FRAME, fn(Reader $in) => Helper::decodeItemFrame(Blocks::GLOWING_ITEM_FRAME(), $in)); - $this->map(Ids::GOLDEN_RAIL, function(Reader $in) : Block{ - return Blocks::POWERED_RAIL() - ->setPowered($in->readBool(StateNames::RAIL_DATA_BIT)) - ->setShape($in->readBoundedInt(StateNames::RAIL_DIRECTION, 0, 5)); - }); - $this->mapSlab(Ids::GRANITE_SLAB, Ids::GRANITE_DOUBLE_SLAB, fn() => Blocks::GRANITE_SLAB()); - $this->mapStairs(Ids::GRANITE_STAIRS, fn() => Blocks::GRANITE_STAIRS()); - $this->map(Ids::GRANITE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::GRANITE_WALL(), $in)); - $this->map(Ids::HAY_BLOCK, function(Reader $in) : Block{ - $in->ignored(StateNames::DEPRECATED); - return Blocks::HAY_BALE()->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeWeightedPressurePlate(Blocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), $in)); - $this->map(Ids::HOPPER, function(Reader $in) : Block{ - return Blocks::HOPPER() - ->setFacing($in->readFacingWithoutUp()) - ->setPowered($in->readBool(StateNames::TOGGLE_BIT)); - }); - $this->map(Ids::IRON_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::IRON_DOOR(), $in)); - $this->map(Ids::IRON_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::IRON_TRAPDOOR(), $in)); - $this->map(Ids::LAB_TABLE, fn(Reader $in) => Blocks::LAB_TABLE() - ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())) - ); - $this->map(Ids::LADDER, function(Reader $in) : Block{ - return Blocks::LADDER() - ->setFacing($in->readHorizontalFacing()); - }); - $this->map(Ids::LANTERN, function(Reader $in) : Block{ - return Blocks::LANTERN() - ->setHanging($in->readBool(StateNames::HANGING)); - }); - $this->map(Ids::LARGE_AMETHYST_BUD, function(Reader $in) : Block{ - return Blocks::AMETHYST_CLUSTER() - ->setStage(AmethystCluster::STAGE_LARGE_BUD) - ->setFacing($in->readBlockFace()); - }); - $this->map(Ids::LAVA, fn(Reader $in) => Helper::decodeStillLiquid(Blocks::LAVA(), $in)); - $this->map(Ids::LECTERN, function(Reader $in) : Block{ - return Blocks::LECTERN() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setProducingSignal($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::LEVER, function(Reader $in) : Block{ - return Blocks::LEVER() - ->setActivated($in->readBool(StateNames::OPEN_BIT)) - ->setFacing(match($value = $in->readString(StateNames::LEVER_DIRECTION)){ - StringValues::LEVER_DIRECTION_DOWN_NORTH_SOUTH => LeverFacing::DOWN_AXIS_Z, - StringValues::LEVER_DIRECTION_DOWN_EAST_WEST => LeverFacing::DOWN_AXIS_X, - StringValues::LEVER_DIRECTION_UP_NORTH_SOUTH => LeverFacing::UP_AXIS_Z, - StringValues::LEVER_DIRECTION_UP_EAST_WEST => LeverFacing::UP_AXIS_X, - StringValues::LEVER_DIRECTION_NORTH => LeverFacing::NORTH, - StringValues::LEVER_DIRECTION_SOUTH => LeverFacing::SOUTH, - StringValues::LEVER_DIRECTION_WEST => LeverFacing::WEST, - StringValues::LEVER_DIRECTION_EAST => LeverFacing::EAST, - default => throw $in->badValueException(StateNames::LEVER_DIRECTION, $value), - }); - }); - $this->map(Ids::LIGHTNING_ROD, function(Reader $in) : Block{ - return Blocks::LIGHTNING_ROD() - ->setFacing($in->readFacingDirection()); - }); - $this->map(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeWeightedPressurePlate(Blocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), $in)); - $this->map(Ids::LIT_BLAST_FURNACE, function(Reader $in) : Block{ - return Blocks::BLAST_FURNACE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(true); - }); - $this->map(Ids::LIT_DEEPSLATE_REDSTONE_ORE, fn() => Blocks::DEEPSLATE_REDSTONE_ORE()->setLit(true)); - $this->map(Ids::LIT_FURNACE, function(Reader $in) : Block{ - return Blocks::FURNACE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(true); - }); - $this->map(Ids::LIT_PUMPKIN, function(Reader $in) : Block{ - return Blocks::LIT_PUMPKIN() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::LIT_REDSTONE_LAMP, function() : Block{ - return Blocks::REDSTONE_LAMP() - ->setPowered(true); - }); - $this->map(Ids::LIT_REDSTONE_ORE, function() : Block{ - return Blocks::REDSTONE_ORE() - ->setLit(true); - }); - $this->map(Ids::LIT_SMOKER, function(Reader $in) : Block{ - return Blocks::SMOKER() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(true); - }); - $this->map(Ids::LOOM, function(Reader $in) : Block{ - return Blocks::LOOM() - ->setFacing($in->readLegacyHorizontalFacing()); - }); - $this->map(Ids::MATERIAL_REDUCER, fn(Reader $in) => Blocks::MATERIAL_REDUCER() - ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())) - ); - $this->map(Ids::MEDIUM_AMETHYST_BUD, function(Reader $in) : Block{ - return Blocks::AMETHYST_CLUSTER() - ->setStage(AmethystCluster::STAGE_MEDIUM_BUD) - ->setFacing($in->readBlockFace()); - }); - $this->map(Ids::MELON_STEM, fn(Reader $in) => Helper::decodeStem(Blocks::MELON_STEM(), $in)); - $this->mapSlab(Ids::MOSSY_COBBLESTONE_SLAB, Ids::MOSSY_COBBLESTONE_DOUBLE_SLAB, fn() => Blocks::MOSSY_COBBLESTONE_SLAB()); - $this->mapStairs(Ids::MOSSY_COBBLESTONE_STAIRS, fn() => Blocks::MOSSY_COBBLESTONE_STAIRS()); - $this->map(Ids::MOSSY_COBBLESTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::MOSSY_COBBLESTONE_WALL(), $in)); - $this->mapSlab(Ids::MOSSY_STONE_BRICK_SLAB, Ids::MOSSY_STONE_BRICK_DOUBLE_SLAB, fn() => Blocks::MOSSY_STONE_BRICK_SLAB()); - $this->mapStairs(Ids::MOSSY_STONE_BRICK_STAIRS, fn() => Blocks::MOSSY_STONE_BRICK_STAIRS()); - $this->map(Ids::MOSSY_STONE_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::MOSSY_STONE_BRICK_WALL(), $in)); - $this->mapSlab(Ids::MUD_BRICK_SLAB, Ids::MUD_BRICK_DOUBLE_SLAB, fn() => Blocks::MUD_BRICK_SLAB()); - $this->mapStairs(Ids::MUD_BRICK_STAIRS, fn() => Blocks::MUD_BRICK_STAIRS()); - $this->map(Ids::MUD_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::MUD_BRICK_WALL(), $in)); - $this->map(Ids::MUDDY_MANGROVE_ROOTS, function(Reader $in) : Block{ - return Blocks::MUDDY_MANGROVE_ROOTS() - ->setAxis($in->readPillarAxis()); - }); - $this->mapSlab(Ids::NETHER_BRICK_SLAB, Ids::NETHER_BRICK_DOUBLE_SLAB, fn() => Blocks::NETHER_BRICK_SLAB()); - $this->mapStairs(Ids::NETHER_BRICK_STAIRS, fn() => Blocks::NETHER_BRICK_STAIRS()); - $this->map(Ids::NETHER_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::NETHER_BRICK_WALL(), $in)); - $this->map(Ids::NETHER_WART, function(Reader $in) : Block{ - return Blocks::NETHER_WART() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 3)); - }); - $this->mapSlab(Ids::NORMAL_STONE_SLAB, Ids::NORMAL_STONE_DOUBLE_SLAB, fn() => Blocks::STONE_SLAB()); - $this->mapStairs(Ids::NORMAL_STONE_STAIRS, fn() => Blocks::STONE_STAIRS()); - $this->map(Ids::OCHRE_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::OCHRE)->setAxis($in->readPillarAxis())); - $this->map(Ids::OXIDIZED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED)); - $this->map(Ids::OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED)); - $this->map(Ids::OXIDIZED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED)); - $this->map(Ids::OXIDIZED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED)); - $this->mapSlab(Ids::OXIDIZED_CUT_COPPER_SLAB, Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED)); - $this->mapStairs(Ids::OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED)); - $this->map(Ids::OXIDIZED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in)); - $this->map(Ids::OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in)); - $this->map(Ids::PEARLESCENT_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::PEARLESCENT)->setAxis($in->readPillarAxis())); - $this->mapSlab(Ids::PETRIFIED_OAK_SLAB, Ids::PETRIFIED_OAK_DOUBLE_SLAB, fn() => Blocks::FAKE_WOODEN_SLAB()); - $this->map(Ids::PINK_PETALS, function(Reader $in) : Block{ - //Pink petals only uses 0-3, but GROWTH state can go up to 7 - $growth = $in->readBoundedInt(StateNames::GROWTH, 0, 7); - return Blocks::PINK_PETALS() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setCount(min($growth + 1, PinkPetals::MAX_COUNT)); - }); - $this->map(Ids::PITCHER_CROP, function(Reader $in) : Block{ - $growth = $in->readBoundedInt(StateNames::GROWTH, 0, 7); - $top = $in->readBool(StateNames::UPPER_BLOCK_BIT); - if($growth <= PitcherCrop::MAX_AGE){ - //top pitcher crop with age 0-2 is an invalid state - //only the bottom half should exist in this case - return $top ? Blocks::AIR() : Blocks::PITCHER_CROP()->setAge($growth); - } - return Blocks::DOUBLE_PITCHER_CROP() - ->setAge(min($growth - PitcherCrop::MAX_AGE - 1, DoublePitcherCrop::MAX_AGE)) - ->setTop($top); - }); - $this->map(Ids::PITCHER_PLANT, function(Reader $in) : Block{ - return Blocks::PITCHER_PLANT() - ->setTop($in->readBool(StateNames::UPPER_BLOCK_BIT)); - }); - $this->mapSlab(Ids::POLISHED_ANDESITE_SLAB, Ids::POLISHED_ANDESITE_DOUBLE_SLAB, fn() => Blocks::POLISHED_ANDESITE_SLAB()); - $this->mapStairs(Ids::POLISHED_ANDESITE_STAIRS, fn() => Blocks::POLISHED_ANDESITE_STAIRS()); - $this->map(Ids::POLISHED_BASALT, function(Reader $in) : Block{ - return Blocks::POLISHED_BASALT() - ->setAxis($in->readPillarAxis()); - }); - $this->map(Ids::POLISHED_BLACKSTONE_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::POLISHED_BLACKSTONE_BUTTON(), $in)); - $this->mapSlab(Ids::POLISHED_BLACKSTONE_SLAB, Ids::POLISHED_BLACKSTONE_DOUBLE_SLAB, fn() => Blocks::POLISHED_BLACKSTONE_SLAB()); - $this->map(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), $in)); - $this->mapStairs(Ids::POLISHED_BLACKSTONE_STAIRS, fn() => Blocks::POLISHED_BLACKSTONE_STAIRS()); - $this->map(Ids::POLISHED_BLACKSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::POLISHED_BLACKSTONE_WALL(), $in)); - $this->mapSlab(Ids::POLISHED_BLACKSTONE_BRICK_SLAB, Ids::POLISHED_BLACKSTONE_BRICK_DOUBLE_SLAB, fn() => Blocks::POLISHED_BLACKSTONE_BRICK_SLAB()); - $this->mapStairs(Ids::POLISHED_BLACKSTONE_BRICK_STAIRS, fn() => Blocks::POLISHED_BLACKSTONE_BRICK_STAIRS()); - $this->map(Ids::POLISHED_BLACKSTONE_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::POLISHED_BLACKSTONE_BRICK_WALL(), $in)); - $this->mapSlab(Ids::POLISHED_DEEPSLATE_SLAB, Ids::POLISHED_DEEPSLATE_DOUBLE_SLAB, fn() => Blocks::POLISHED_DEEPSLATE_SLAB()); - $this->mapStairs(Ids::POLISHED_DEEPSLATE_STAIRS, fn() => Blocks::POLISHED_DEEPSLATE_STAIRS()); - $this->map(Ids::POLISHED_DEEPSLATE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::POLISHED_DEEPSLATE_WALL(), $in)); - $this->mapSlab(Ids::POLISHED_DIORITE_SLAB, Ids::POLISHED_DIORITE_DOUBLE_SLAB, fn() => Blocks::POLISHED_DIORITE_SLAB()); - $this->mapStairs(Ids::POLISHED_DIORITE_STAIRS, fn() => Blocks::POLISHED_DIORITE_STAIRS()); - $this->mapSlab(Ids::POLISHED_GRANITE_SLAB, Ids::POLISHED_GRANITE_DOUBLE_SLAB, fn() => Blocks::POLISHED_GRANITE_SLAB()); - $this->mapStairs(Ids::POLISHED_GRANITE_STAIRS, fn() => Blocks::POLISHED_GRANITE_STAIRS()); - $this->mapSlab(Ids::POLISHED_TUFF_SLAB, Ids::POLISHED_TUFF_DOUBLE_SLAB, fn() => Blocks::POLISHED_TUFF_SLAB()); - $this->mapStairs(Ids::POLISHED_TUFF_STAIRS, fn() => Blocks::POLISHED_TUFF_STAIRS()); - $this->map(Ids::POLISHED_TUFF_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::POLISHED_TUFF_WALL(), $in)); - $this->map(Ids::PORTAL, function(Reader $in) : Block{ - return Blocks::NETHER_PORTAL() - ->setAxis(match($value = $in->readString(StateNames::PORTAL_AXIS)){ - StringValues::PORTAL_AXIS_UNKNOWN => Axis::X, - StringValues::PORTAL_AXIS_X => Axis::X, - StringValues::PORTAL_AXIS_Z => Axis::Z, - default => throw $in->badValueException(StateNames::PORTAL_AXIS, $value), - }); - }); - $this->map(Ids::POTATOES, fn(Reader $in) => Helper::decodeCrops(Blocks::POTATOES(), $in)); - $this->map(Ids::POWERED_COMPARATOR, fn(Reader $in) => Helper::decodeComparator(Blocks::REDSTONE_COMPARATOR(), $in)); - $this->map(Ids::POWERED_REPEATER, fn(Reader $in) => Helper::decodeRepeater(Blocks::REDSTONE_REPEATER(), $in) - ->setPowered(true)); - $this->mapSlab(Ids::PRISMARINE_BRICK_SLAB, Ids::PRISMARINE_BRICK_DOUBLE_SLAB, fn() => Blocks::PRISMARINE_BRICKS_SLAB()); - $this->mapStairs(Ids::PRISMARINE_BRICKS_STAIRS, fn() => Blocks::PRISMARINE_BRICKS_STAIRS()); - $this->map(Ids::PRISMARINE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::PRISMARINE_WALL(), $in)); - $this->mapSlab(Ids::PRISMARINE_SLAB, Ids::PRISMARINE_DOUBLE_SLAB, fn() => Blocks::PRISMARINE_SLAB()); - $this->mapStairs(Ids::PRISMARINE_STAIRS, fn() => Blocks::PRISMARINE_STAIRS()); - $this->map(Ids::PUMPKIN, function(Reader $in) : Block{ - $in->ignored(StateNames::MC_CARDINAL_DIRECTION); //obsolete - return Blocks::PUMPKIN(); - }); - $this->map(Ids::PUMPKIN_STEM, fn(Reader $in) => Helper::decodeStem(Blocks::PUMPKIN_STEM(), $in)); - $this->map(Ids::PURPUR_BLOCK, function(Reader $in) : Block{ - $in->ignored(StateNames::PILLAR_AXIS); //??? - return Blocks::PURPUR(); - }); - $this->map(Ids::PURPUR_PILLAR, fn(Reader $in) => Blocks::PURPUR_PILLAR()->setAxis($in->readPillarAxis())); - $this->mapSlab(Ids::PURPUR_SLAB, Ids::PURPUR_DOUBLE_SLAB, fn() => Blocks::PURPUR_SLAB()); - $this->mapStairs(Ids::PURPUR_STAIRS, fn() => Blocks::PURPUR_STAIRS()); - $this->map(Ids::QUARTZ_BLOCK, function(Reader $in) : Opaque{ - $in->ignored(StateNames::PILLAR_AXIS); - return Blocks::QUARTZ(); - }); - $this->map(Ids::QUARTZ_PILLAR, function(Reader $in) : Block{ - return Blocks::QUARTZ_PILLAR() - ->setAxis($in->readPillarAxis()); - }); - $this->mapSlab(Ids::QUARTZ_SLAB, Ids::QUARTZ_DOUBLE_SLAB, fn() => Blocks::QUARTZ_SLAB()); - $this->mapStairs(Ids::QUARTZ_STAIRS, fn() => Blocks::QUARTZ_STAIRS()); - $this->map(Ids::RAIL, function(Reader $in) : Block{ - return Blocks::RAIL() - ->setShape($in->readBoundedInt(StateNames::RAIL_DIRECTION, 0, 9)); - }); - $this->map(Ids::RED_MUSHROOM_BLOCK, fn(Reader $in) => Helper::decodeMushroomBlock(Blocks::RED_MUSHROOM_BLOCK(), $in)); - $this->mapSlab(Ids::RED_NETHER_BRICK_SLAB, Ids::RED_NETHER_BRICK_DOUBLE_SLAB, fn() => Blocks::RED_NETHER_BRICK_SLAB()); - $this->mapStairs(Ids::RED_NETHER_BRICK_STAIRS, fn() => Blocks::RED_NETHER_BRICK_STAIRS()); - $this->map(Ids::RED_NETHER_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RED_NETHER_BRICK_WALL(), $in)); - $this->mapSlab(Ids::RED_SANDSTONE_SLAB, Ids::RED_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::RED_SANDSTONE_SLAB()); - $this->mapStairs(Ids::RED_SANDSTONE_STAIRS, fn() => Blocks::RED_SANDSTONE_STAIRS()); - $this->map(Ids::RED_SANDSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RED_SANDSTONE_WALL(), $in)); - $this->map(Ids::REDSTONE_LAMP, function() : Block{ - return Blocks::REDSTONE_LAMP() - ->setPowered(false); - }); - $this->map(Ids::REDSTONE_ORE, function() : Block{ - return Blocks::REDSTONE_ORE() - ->setLit(false); - }); - $this->map(Ids::REDSTONE_TORCH, function(Reader $in) : Block{ - return Blocks::REDSTONE_TORCH() - ->setFacing($in->readTorchFacing()) - ->setLit(true); - }); - $this->map(Ids::REDSTONE_WIRE, function(Reader $in) : Block{ - return Blocks::REDSTONE_WIRE() - ->setOutputSignalStrength($in->readBoundedInt(StateNames::REDSTONE_SIGNAL, 0, 15)); - }); - $this->map(Ids::REEDS, function(Reader $in) : Block{ - return Blocks::SUGARCANE() - ->setAge($in->readBoundedInt(StateNames::AGE, 0, 15)); - }); - $this->mapSlab(Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SANDSTONE_SLAB()); - $this->mapStairs(Ids::SANDSTONE_STAIRS, fn() => Blocks::SANDSTONE_STAIRS()); - $this->map(Ids::SANDSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::SANDSTONE_WALL(), $in)); - $this->map(Ids::SEA_PICKLE, function(Reader $in) : Block{ - return Blocks::SEA_PICKLE() - ->setCount($in->readBoundedInt(StateNames::CLUSTER_COUNT, 0, 3) + 1) - ->setUnderwater(!$in->readBool(StateNames::DEAD_BIT)); - }); - $this->map(Ids::SMOKER, function(Reader $in) : Block{ - return Blocks::SMOKER() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(false); - }); - $this->map(Ids::SMALL_AMETHYST_BUD, function(Reader $in) : Block{ - return Blocks::AMETHYST_CLUSTER() - ->setStage(AmethystCluster::STAGE_SMALL_BUD) - ->setFacing($in->readBlockFace()); - }); - $this->map(Ids::SMALL_DRIPLEAF_BLOCK, function(Reader $in) : Block{ - return Blocks::SMALL_DRIPLEAF() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setTop($in->readBool(StateNames::UPPER_BLOCK_BIT)); - }); - $this->map(Ids::SMOOTH_QUARTZ, function(Reader $in) : Block{ - $in->ignored(StateNames::PILLAR_AXIS); - return Blocks::SMOOTH_QUARTZ(); - }); - $this->mapSlab(Ids::SMOOTH_QUARTZ_SLAB, Ids::SMOOTH_QUARTZ_DOUBLE_SLAB, fn() => Blocks::SMOOTH_QUARTZ_SLAB()); - $this->mapStairs(Ids::SMOOTH_QUARTZ_STAIRS, fn() => Blocks::SMOOTH_QUARTZ_STAIRS()); - $this->mapSlab(Ids::SMOOTH_RED_SANDSTONE_SLAB, Ids::SMOOTH_RED_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SMOOTH_RED_SANDSTONE_SLAB()); - $this->mapStairs(Ids::SMOOTH_RED_SANDSTONE_STAIRS, fn() => Blocks::SMOOTH_RED_SANDSTONE_STAIRS()); - $this->mapSlab(Ids::SMOOTH_SANDSTONE_SLAB, Ids::SMOOTH_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SMOOTH_SANDSTONE_SLAB()); - $this->mapStairs(Ids::SMOOTH_SANDSTONE_STAIRS, fn() => Blocks::SMOOTH_SANDSTONE_STAIRS()); - $this->mapSlab(Ids::SMOOTH_STONE_SLAB, Ids::SMOOTH_STONE_DOUBLE_SLAB, fn() => Blocks::SMOOTH_STONE_SLAB()); - $this->map(Ids::SNOW_LAYER, function(Reader $in) : Block{ - $in->ignored(StateNames::COVERED_BIT); //seems to be useless - return Blocks::SNOW_LAYER()->setLayers($in->readBoundedInt(StateNames::HEIGHT, 0, 7) + 1); - }); - $this->map(Ids::SOUL_CAMPFIRE, function(Reader $in) : Block{ - return Blocks::SOUL_CAMPFIRE() - ->setFacing($in->readCardinalHorizontalFacing()) - ->setLit(!$in->readBool(StateNames::EXTINGUISHED)); - }); - $this->map(Ids::SOUL_FIRE, function(Reader $in) : Block{ - $in->ignored(StateNames::AGE); //this is useless for soul fire, since it doesn't have the logic associated - return Blocks::SOUL_FIRE(); - }); - $this->map(Ids::SOUL_LANTERN, function(Reader $in) : Block{ - return Blocks::SOUL_LANTERN() - ->setHanging($in->readBool(StateNames::HANGING)); - }); - $this->map(Ids::SOUL_TORCH, function(Reader $in) : Block{ - return Blocks::SOUL_TORCH() - ->setFacing($in->readTorchFacing()); - }); - $this->map(Ids::STANDING_BANNER, function(Reader $in) : Block{ - return Blocks::BANNER() - ->setRotation($in->readBoundedInt(StateNames::GROUND_SIGN_DIRECTION, 0, 15)); - }); - $this->mapSlab(Ids::STONE_BRICK_SLAB, Ids::STONE_BRICK_DOUBLE_SLAB, fn() => Blocks::STONE_BRICK_SLAB()); - $this->mapStairs(Ids::STONE_BRICK_STAIRS, fn() => Blocks::STONE_BRICK_STAIRS()); - $this->map(Ids::STONE_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::STONE_BRICK_WALL(), $in)); - $this->map(Ids::STONE_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::STONE_BUTTON(), $in)); - $this->map(Ids::STONE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::STONE_PRESSURE_PLATE(), $in)); - $this->mapStairs(Ids::STONE_STAIRS, fn() => Blocks::COBBLESTONE_STAIRS()); - $this->map(Ids::STONECUTTER_BLOCK, function(Reader $in) : Block{ - return Blocks::STONECUTTER() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::SWEET_BERRY_BUSH, function(Reader $in) : Block{ - //berry bush only wants 0-3, but it can be bigger in MCPE due to misuse of GROWTH state which goes up to 7 - $growth = $in->readBoundedInt(StateNames::GROWTH, 0, 7); - return Blocks::SWEET_BERRY_BUSH() - ->setAge(min($growth, SweetBerryBush::STAGE_MATURE)); - }); - $this->map(Ids::TNT, function(Reader $in) : Block{ - return Blocks::TNT() - ->setUnstable($in->readBool(StateNames::EXPLODE_BIT)) - ->setWorksUnderwater(false); - }); - $this->map(Ids::TORCH, function(Reader $in) : Block{ - return Blocks::TORCH() - ->setFacing($in->readTorchFacing()); - }); - $this->map(Ids::TORCHFLOWER_CROP, function(Reader $in) : Block{ - return Blocks::TORCHFLOWER_CROP() - //this property can have values 0-7, but only 0-1 are valid - ->setReady($in->readBoundedInt(StateNames::GROWTH, 0, 7) !== 0); - }); - $this->map(Ids::TRAPPED_CHEST, function(Reader $in) : Block{ - return Blocks::TRAPPED_CHEST() - ->setFacing($in->readCardinalHorizontalFacing()); - }); - $this->map(Ids::TRIP_WIRE, function(Reader $in) : Block{ - return Blocks::TRIPWIRE() - ->setConnected($in->readBool(StateNames::ATTACHED_BIT)) - ->setDisarmed($in->readBool(StateNames::DISARMED_BIT)) - ->setSuspended($in->readBool(StateNames::SUSPENDED_BIT)) - ->setTriggered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::TRIPWIRE_HOOK, function(Reader $in) : Block{ - return Blocks::TRIPWIRE_HOOK() - ->setConnected($in->readBool(StateNames::ATTACHED_BIT)) - ->setFacing($in->readLegacyHorizontalFacing()) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->mapSlab(Ids::TUFF_BRICK_SLAB, Ids::TUFF_BRICK_DOUBLE_SLAB, fn() => Blocks::TUFF_BRICK_SLAB()); - $this->mapStairs(Ids::TUFF_BRICK_STAIRS, fn() => Blocks::TUFF_BRICK_STAIRS()); - $this->map(Ids::TUFF_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::TUFF_BRICK_WALL(), $in)); - $this->mapSlab(Ids::TUFF_SLAB, Ids::TUFF_DOUBLE_SLAB, fn() => Blocks::TUFF_SLAB()); - $this->mapStairs(Ids::TUFF_STAIRS, fn() => Blocks::TUFF_STAIRS()); - $this->map(Ids::TUFF_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::TUFF_WALL(), $in)); - $this->map(Ids::TWISTING_VINES, function(Reader $in) : Block{ - return Blocks::TWISTING_VINES() - ->setAge($in->readBoundedInt(StateNames::TWISTING_VINES_AGE, 0, 25)); - }); - $this->map(Ids::UNDERWATER_TNT, function(Reader $in) : Block{ - return Blocks::TNT() - ->setUnstable($in->readBool(StateNames::EXPLODE_BIT)) - ->setWorksUnderwater(true); - }); - $this->map(Ids::UNDERWATER_TORCH, function(Reader $in) : Block{ - return Blocks::UNDERWATER_TORCH() - ->setFacing($in->readTorchFacing()); - }); - $this->map(Ids::UNLIT_REDSTONE_TORCH, function(Reader $in) : Block{ - return Blocks::REDSTONE_TORCH() - ->setFacing($in->readTorchFacing()) - ->setLit(false); - }); - $this->map(Ids::UNPOWERED_COMPARATOR, fn(Reader $in) => Helper::decodeComparator(Blocks::REDSTONE_COMPARATOR(), $in)); - $this->map(Ids::UNPOWERED_REPEATER, fn(Reader $in) => Helper::decodeRepeater(Blocks::REDSTONE_REPEATER(), $in) - ->setPowered(false)); - $this->map(Ids::VERDANT_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::VERDANT)->setAxis($in->readPillarAxis())); - $this->map(Ids::VINE, function(Reader $in) : Block{ - $vineDirectionFlags = $in->readBoundedInt(StateNames::VINE_DIRECTION_BITS, 0, 15); - return Blocks::VINES() - ->setFace(Facing::NORTH, ($vineDirectionFlags & BlockLegacyMetadata::VINE_FLAG_NORTH) !== 0) - ->setFace(Facing::SOUTH, ($vineDirectionFlags & BlockLegacyMetadata::VINE_FLAG_SOUTH) !== 0) - ->setFace(Facing::WEST, ($vineDirectionFlags & BlockLegacyMetadata::VINE_FLAG_WEST) !== 0) - ->setFace(Facing::EAST, ($vineDirectionFlags & BlockLegacyMetadata::VINE_FLAG_EAST) !== 0); - }); - $this->map(Ids::WALL_BANNER, function(Reader $in) : Block{ - return Blocks::WALL_BANNER() - ->setFacing($in->readHorizontalFacing()); - }); - $this->map(Ids::WATER, fn(Reader $in) => Helper::decodeStillLiquid(Blocks::WATER(), $in)); - $this->map(Ids::WAXED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::NONE)); - $this->map(Ids::WAXED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::NONE)); - $this->map(Ids::WAXED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE)); - $this->map(Ids::WAXED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE)); - $this->mapSlab(Ids::WAXED_CUT_COPPER_SLAB, Ids::WAXED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE)); - $this->mapStairs(Ids::WAXED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE)); - $this->map(Ids::WAXED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::WAXED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in)); - $this->map(Ids::WAXED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in)); - $this->map(Ids::WAXED_EXPOSED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::EXPOSED)); - $this->map(Ids::WAXED_EXPOSED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED)); - $this->map(Ids::WAXED_EXPOSED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED)); - $this->map(Ids::WAXED_EXPOSED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED)); - $this->mapSlab(Ids::WAXED_EXPOSED_CUT_COPPER_SLAB, Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED)); - $this->mapStairs(Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED)); - $this->map(Ids::WAXED_EXPOSED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::WAXED_EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in)); - $this->map(Ids::WAXED_EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in)); - $this->map(Ids::WAXED_OXIDIZED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED)); - $this->map(Ids::WAXED_OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED)); - $this->map(Ids::WAXED_OXIDIZED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED)); - $this->map(Ids::WAXED_OXIDIZED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED)); - $this->mapSlab(Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB, Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED)); - $this->mapStairs(Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED)); - $this->map(Ids::WAXED_OXIDIZED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::WAXED_OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in)); - $this->map(Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in)); - $this->map(Ids::WAXED_WEATHERED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::WEATHERED)); - $this->map(Ids::WAXED_WEATHERED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED)); - $this->map(Ids::WAXED_WEATHERED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED)); - $this->map(Ids::WAXED_WEATHERED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED)); - $this->mapSlab(Ids::WAXED_WEATHERED_CUT_COPPER_SLAB, Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED)); - $this->mapStairs(Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED)); - $this->map(Ids::WAXED_WEATHERED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::WAXED_WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in)); - $this->map(Ids::WAXED_WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in)); - $this->map(Ids::WEATHERED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::WEATHERED)); - $this->map(Ids::WEATHERED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED)); - $this->map(Ids::WEATHERED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED)); - $this->map(Ids::WEATHERED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED)); - $this->mapSlab(Ids::WEATHERED_CUT_COPPER_SLAB, Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED)); - $this->mapStairs(Ids::WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED)); - $this->map(Ids::WEATHERED_COPPER_BULB, function(Reader $in) : Block{ - return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED) - ->setLit($in->readBool(StateNames::LIT)) - ->setPowered($in->readBool(StateNames::POWERED_BIT)); - }); - $this->map(Ids::WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in)); - $this->map(Ids::WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in)); - $this->map(Ids::WEEPING_VINES, function(Reader $in) : Block{ - return Blocks::WEEPING_VINES() - ->setAge($in->readBoundedInt(StateNames::WEEPING_VINES_AGE, 0, 25)); - }); - $this->map(Ids::WHEAT, fn(Reader $in) => Helper::decodeCrops(Blocks::WHEAT(), $in)); - } - /** @throws BlockStateDeserializeException */ public function deserializeBlock(BlockStateData $blockStateData) : Block{ $id = $blockStateData->getName(); diff --git a/src/data/bedrock/block/convert/BlockStateWriter.php b/src/data/bedrock/block/convert/BlockStateWriter.php index 63af92d10..0119bd02e 100644 --- a/src/data/bedrock/block/convert/BlockStateWriter.php +++ b/src/data/bedrock/block/convert/BlockStateWriter.php @@ -31,6 +31,9 @@ use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockStateNames; use pocketmine\data\bedrock\block\BlockStateSerializeException; use pocketmine\data\bedrock\block\BlockStateStringValues as StringValues; +use pocketmine\data\bedrock\block\convert\property\EnumFromRawStateMap; +use pocketmine\data\bedrock\block\convert\property\IntFromRawStateMap; +use pocketmine\data\bedrock\block\convert\property\ValueMappings; use pocketmine\math\Axis; use pocketmine\math\Facing; use pocketmine\nbt\tag\ByteTag; @@ -73,35 +76,47 @@ final class BlockStateWriter{ return $this; } - /** @return $this */ - public function writeFacingDirection(int $value) : self{ - $this->writeInt(BlockStateNames::FACING_DIRECTION, match($value){ - Facing::DOWN => 0, - Facing::UP => 1, - Facing::NORTH => 2, - Facing::SOUTH => 3, - Facing::WEST => 4, - Facing::EAST => 5, - default => throw new BlockStateSerializeException("Invalid Facing $value") - }); - return $this; - } - - /** @return $this */ - public function writeBlockFace(int $value) : self{ - $this->writeString(BlockStateNames::MC_BLOCK_FACE, match($value){ - Facing::DOWN => StringValues::MC_BLOCK_FACE_DOWN, - Facing::UP => StringValues::MC_BLOCK_FACE_UP, - Facing::NORTH => StringValues::MC_BLOCK_FACE_NORTH, - Facing::SOUTH => StringValues::MC_BLOCK_FACE_SOUTH, - Facing::WEST => StringValues::MC_BLOCK_FACE_WEST, - Facing::EAST => StringValues::MC_BLOCK_FACE_EAST, - default => throw new BlockStateSerializeException("Invalid Facing $value") - }); + /** + * @deprecated + * @phpstan-param IntFromRawStateMap $map + * @return $this + */ + public function mapIntToString(string $name, IntFromRawStateMap $map, int $value) : self{ + $raw = $map->valueToRaw($value); + $this->writeString($name, $raw); return $this; } /** + * @deprecated + * @phpstan-param IntFromRawStateMap $map + * @return $this + */ + public function mapIntToInt(string $name, IntFromRawStateMap $map, int $value) : self{ + $raw = $map->valueToRaw($value); + $this->writeInt($name, $raw); + return $this; + } + + /** + * @deprecated + * @return $this + */ + public function writeFacingDirection(int $value) : self{ + return $this->mapIntToInt(BlockStateNames::FACING_DIRECTION, ValueMappings::getInstance()->facing, $value); + } + + /** + * @deprecated + * @return $this + */ + public function writeBlockFace(int $value) : self{ + $this->mapIntToString(BlockStateNames::MC_BLOCK_FACE, ValueMappings::getInstance()->blockFace, $value); + return $this; + } + + /** + * @deprecated * @param int[] $faces * @phpstan-param array $faces * @return $this @@ -123,86 +138,69 @@ final class BlockStateWriter{ return $this->writeInt(BlockStateNames::MULTI_FACE_DIRECTION_BITS, $result); } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeEndRodFacingDirection(int $value) : self{ //end rods are stupid in bedrock and have everything except up/down the wrong way round return $this->writeFacingDirection(Facing::axis($value) !== Axis::Y ? Facing::opposite($value) : $value); } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeHorizontalFacing(int $value) : self{ - if($value === Facing::UP || $value === Facing::DOWN){ - throw new BlockStateSerializeException("Y-axis facing is not allowed"); - } - - return $this->writeFacingDirection($value); - } - - /** @return $this */ - public function writeWeirdoHorizontalFacing(int $value) : self{ - $this->writeInt(BlockStateNames::WEIRDO_DIRECTION, match($value){ - Facing::EAST => 0, - Facing::WEST => 1, - Facing::SOUTH => 2, - Facing::NORTH => 3, - default => throw new BlockStateSerializeException("Invalid horizontal facing $value") - }); - return $this; - } - - /** @return $this */ - public function writeLegacyHorizontalFacing(int $value) : self{ - $this->writeInt(BlockStateNames::DIRECTION, match($value){ - Facing::SOUTH => 0, - Facing::WEST => 1, - Facing::NORTH => 2, - Facing::EAST => 3, - default => throw new BlockStateSerializeException("Invalid horizontal facing $value") - }); - return $this; + return $this->mapIntToInt(BlockStateNames::FACING_DIRECTION, ValueMappings::getInstance()->horizontalFacingClassic, $value); } /** + * @deprecated + * @return $this + */ + public function writeWeirdoHorizontalFacing(int $value) : self{ + return $this->mapIntToInt(BlockStateNames::WEIRDO_DIRECTION, ValueMappings::getInstance()->horizontalFacing5Minus, $value); + } + + /** + * @deprecated + * @return $this + */ + public function writeLegacyHorizontalFacing(int $value) : self{ + return $this->mapIntToInt(BlockStateNames::DIRECTION, ValueMappings::getInstance()->horizontalFacingSWNE, $value); + } + + /** + * @deprecated * This is for trapdoors, because Mojang botched the conversion in 1.13 * @return $this */ public function write5MinusHorizontalFacing(int $value) : self{ - return $this->writeInt(BlockStateNames::DIRECTION, match($value){ - Facing::EAST => 0, - Facing::WEST => 1, - Facing::SOUTH => 2, - Facing::NORTH => 3, - default => throw new BlockStateSerializeException("Invalid horizontal facing $value") - }); + return $this->mapIntToInt(BlockStateNames::DIRECTION, ValueMappings::getInstance()->horizontalFacing5Minus, $value); } /** + * @deprecated * Used by pumpkins as of 1.20.0.23 beta * @return $this */ public function writeCardinalHorizontalFacing(int $value) : self{ - return $this->writeString(BlockStateNames::MC_CARDINAL_DIRECTION, match($value){ - Facing::SOUTH => StringValues::MC_CARDINAL_DIRECTION_SOUTH, - Facing::WEST => StringValues::MC_CARDINAL_DIRECTION_WEST, - Facing::NORTH => StringValues::MC_CARDINAL_DIRECTION_NORTH, - Facing::EAST => StringValues::MC_CARDINAL_DIRECTION_EAST, - default => throw new BlockStateSerializeException("Invalid horizontal facing $value") - }); + return $this->mapIntToString(BlockStateNames::MC_CARDINAL_DIRECTION, ValueMappings::getInstance()->cardinalDirection, $value); } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeCoralFacing(int $value) : self{ - $this->writeInt(BlockStateNames::CORAL_DIRECTION, match($value){ - Facing::WEST => 0, - Facing::EAST => 1, - Facing::NORTH => 2, - Facing::SOUTH => 3, - default => throw new BlockStateSerializeException("Invalid horizontal facing $value") - }); - return $this; + return $this->mapIntToInt(BlockStateNames::CORAL_DIRECTION, ValueMappings::getInstance()->horizontalFacingCoral, $value); } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeFacingWithoutDown(int $value) : self{ if($value === Facing::DOWN){ throw new BlockStateSerializeException("Invalid facing DOWN"); @@ -211,7 +209,10 @@ final class BlockStateWriter{ return $this; } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeFacingWithoutUp(int $value) : self{ if($value === Facing::UP){ throw new BlockStateSerializeException("Invalid facing UP"); @@ -220,18 +221,19 @@ final class BlockStateWriter{ return $this; } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writePillarAxis(int $axis) : self{ - $this->writeString(BlockStateNames::PILLAR_AXIS, match($axis){ - Axis::X => StringValues::PILLAR_AXIS_X, - Axis::Y => StringValues::PILLAR_AXIS_Y, - Axis::Z => StringValues::PILLAR_AXIS_Z, - default => throw new BlockStateSerializeException("Invalid axis $axis") - }); + $this->mapIntToString(BlockStateNames::PILLAR_AXIS, ValueMappings::getInstance()->pillarAxis, $axis); return $this; } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeSlabPosition(SlabType $slabType) : self{ $this->writeString(BlockStateNames::MC_VERTICAL_HALF, match($slabType){ SlabType::TOP => StringValues::MC_VERTICAL_HALF_TOP, @@ -241,32 +243,27 @@ final class BlockStateWriter{ return $this; } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeTorchFacing(int $facing) : self{ - //TODO: horizontal directions are flipped (MCPE bug: https://bugs.mojang.com/browse/MCPE-152036) - $this->writeString(BlockStateNames::TORCH_FACING_DIRECTION, match($facing){ - Facing::UP => StringValues::TORCH_FACING_DIRECTION_TOP, - Facing::SOUTH => StringValues::TORCH_FACING_DIRECTION_NORTH, - Facing::NORTH => StringValues::TORCH_FACING_DIRECTION_SOUTH, - Facing::EAST => StringValues::TORCH_FACING_DIRECTION_WEST, - Facing::WEST => StringValues::TORCH_FACING_DIRECTION_EAST, - default => throw new BlockStateSerializeException("Invalid Torch facing $facing") - }); + $this->mapIntToString(BlockStateNames::TORCH_FACING_DIRECTION, ValueMappings::getInstance()->torchFacing, $facing); return $this; } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeBellAttachmentType(BellAttachmentType $attachmentType) : self{ - $this->writeString(BlockStateNames::ATTACHMENT, match($attachmentType){ - BellAttachmentType::FLOOR => StringValues::ATTACHMENT_STANDING, - BellAttachmentType::CEILING => StringValues::ATTACHMENT_HANGING, - BellAttachmentType::ONE_WALL => StringValues::ATTACHMENT_SIDE, - BellAttachmentType::TWO_WALLS => StringValues::ATTACHMENT_MULTIPLE, - }); - return $this; + return $this->writeUnitEnum(BlockStateNames::ATTACHMENT, ValueMappings::getInstance()->bellAttachmentType, $attachmentType); } - /** @return $this */ + /** + * @deprecated + * @return $this + */ public function writeWallConnectionType(string $name, ?WallConnectionType $wallConnectionType) : self{ $this->writeString($name, match($wallConnectionType){ null => StringValues::WALL_CONNECTION_TYPE_EAST_NONE, @@ -276,6 +273,21 @@ final class BlockStateWriter{ return $this; } + /** + * @deprecated + * @phpstan-template TEnum of \UnitEnum + * @phpstan-param EnumFromRawStateMap $map + * @phpstan-param TEnum $case + * + * @return $this + */ + public function writeUnitEnum(string $name, EnumFromRawStateMap $map, \UnitEnum $case) : self{ + $value = $map->valueToRaw($case); + $this->writeString($name, $value); + + return $this; + } + public function getBlockStateData() : BlockStateData{ return BlockStateData::current($this->id, $this->states); } diff --git a/src/data/bedrock/block/convert/FlattenedIdModel.php b/src/data/bedrock/block/convert/FlattenedIdModel.php new file mode 100644 index 000000000..50bf5cdd9 --- /dev/null +++ b/src/data/bedrock/block/convert/FlattenedIdModel.php @@ -0,0 +1,107 @@ +> + */ + private array $idComponents = []; + + /** + * @var Property[] + * @phpstan-var list> + */ + private array $properties = []; + + /** + * @phpstan-param TBlock $block + */ + private function __construct( + private Block $block + ){} + + /** + * @phpstan-template TBlock_ of Block + * @phpstan-param TBlock_ $block + * @return self + */ + public static function create(Block $block) : self{ + /** @phpstan-var self $result */ + $result = new self($block); + return $result; + } + + /** @phpstan-return TBlock */ + public function getBlock() : Block{ return $this->block; } + + /** + * @return string[]|StringProperty[] + * @phpstan-return list> + */ + public function getIdComponents() : array{ return $this->idComponents; } + + /** + * @return Property[] + * @phpstan-return list> + */ + public function getProperties() : array{ return $this->properties; } + + /** + * @param string[]|StringProperty[] $components + * @phpstan-param non-empty-list> $components + * @return $this + * @phpstan-this-out self + */ + public function idComponents(array $components) : self{ + $this->idComponents = $components; + return $this; + } + + /** + * @param Property[] $properties + * @phpstan-param non-empty-list> $properties + * @return $this + * @phpstan-this-out self + */ + public function properties(array $properties) : self{ + $this->properties = $properties; + return $this; + } +} diff --git a/src/data/bedrock/block/convert/Model.php b/src/data/bedrock/block/convert/Model.php new file mode 100644 index 000000000..3474a8932 --- /dev/null +++ b/src/data/bedrock/block/convert/Model.php @@ -0,0 +1,82 @@ +> + */ + private array $properties = []; + + /** + * @phpstan-param TBlock $block + */ + private function __construct( + private Block $block, + private string $id + ){} + + /** @phpstan-return TBlock */ + public function getBlock() : Block{ return $this->block; } + + public function getId() : string{ return $this->id; } + + /** + * @return Property[] + * @phpstan-return list> + */ + public function getProperties() : array{ return $this->properties; } + + /** + * @phpstan-template TBlock_ of Block + * @phpstan-param TBlock_ $block + * @phpstan-return self + */ + public static function create(Block $block, string $id) : self{ + return new self($block, $id); + } + + /** + * @param Property[] $properties + * @phpstan-param list> $properties + * @phpstan-return $this + */ + public function properties(array $properties) : self{ + $this->properties = $properties; + return $this; + } +} diff --git a/src/data/bedrock/block/convert/VanillaBlockMappings.php b/src/data/bedrock/block/convert/VanillaBlockMappings.php new file mode 100644 index 000000000..f339ce3ef --- /dev/null +++ b/src/data/bedrock/block/convert/VanillaBlockMappings.php @@ -0,0 +1,1646 @@ +mapSimple(Blocks::AIR(), Ids::AIR); + $reg->mapSimple(Blocks::AMETHYST(), Ids::AMETHYST_BLOCK); + $reg->mapSimple(Blocks::ANCIENT_DEBRIS(), Ids::ANCIENT_DEBRIS); + $reg->mapSimple(Blocks::ANDESITE(), Ids::ANDESITE); + $reg->mapSimple(Blocks::BARRIER(), Ids::BARRIER); + $reg->mapSimple(Blocks::BEACON(), Ids::BEACON); + $reg->mapSimple(Blocks::BLACKSTONE(), Ids::BLACKSTONE); + $reg->mapSimple(Blocks::BLUE_ICE(), Ids::BLUE_ICE); + $reg->mapSimple(Blocks::BOOKSHELF(), Ids::BOOKSHELF); + $reg->mapSimple(Blocks::BRICKS(), Ids::BRICK_BLOCK); + $reg->mapSimple(Blocks::BROWN_MUSHROOM(), Ids::BROWN_MUSHROOM); + $reg->mapSimple(Blocks::BUDDING_AMETHYST(), Ids::BUDDING_AMETHYST); + $reg->mapSimple(Blocks::CALCITE(), Ids::CALCITE); + $reg->mapSimple(Blocks::CARTOGRAPHY_TABLE(), Ids::CARTOGRAPHY_TABLE); + $reg->mapSimple(Blocks::CHEMICAL_HEAT(), Ids::CHEMICAL_HEAT); + $reg->mapSimple(Blocks::CHISELED_DEEPSLATE(), Ids::CHISELED_DEEPSLATE); + $reg->mapSimple(Blocks::CHISELED_NETHER_BRICKS(), Ids::CHISELED_NETHER_BRICKS); + $reg->mapSimple(Blocks::CHISELED_POLISHED_BLACKSTONE(), Ids::CHISELED_POLISHED_BLACKSTONE); + $reg->mapSimple(Blocks::CHISELED_RED_SANDSTONE(), Ids::CHISELED_RED_SANDSTONE); + $reg->mapSimple(Blocks::CHISELED_RESIN_BRICKS(), Ids::CHISELED_RESIN_BRICKS); + $reg->mapSimple(Blocks::CHISELED_SANDSTONE(), Ids::CHISELED_SANDSTONE); + $reg->mapSimple(Blocks::CHISELED_STONE_BRICKS(), Ids::CHISELED_STONE_BRICKS); + $reg->mapSimple(Blocks::CHISELED_TUFF(), Ids::CHISELED_TUFF); + $reg->mapSimple(Blocks::CHISELED_TUFF_BRICKS(), Ids::CHISELED_TUFF_BRICKS); + $reg->mapSimple(Blocks::CHORUS_PLANT(), Ids::CHORUS_PLANT); + $reg->mapSimple(Blocks::CLAY(), Ids::CLAY); + $reg->mapSimple(Blocks::COAL(), Ids::COAL_BLOCK); + $reg->mapSimple(Blocks::COAL_ORE(), Ids::COAL_ORE); + $reg->mapSimple(Blocks::COBBLED_DEEPSLATE(), Ids::COBBLED_DEEPSLATE); + $reg->mapSimple(Blocks::COBBLESTONE(), Ids::COBBLESTONE); + $reg->mapSimple(Blocks::COBWEB(), Ids::WEB); + $reg->mapSimple(Blocks::COPPER_ORE(), Ids::COPPER_ORE); + $reg->mapSimple(Blocks::CRACKED_DEEPSLATE_BRICKS(), Ids::CRACKED_DEEPSLATE_BRICKS); + $reg->mapSimple(Blocks::CRACKED_DEEPSLATE_TILES(), Ids::CRACKED_DEEPSLATE_TILES); + $reg->mapSimple(Blocks::CRACKED_NETHER_BRICKS(), Ids::CRACKED_NETHER_BRICKS); + $reg->mapSimple(Blocks::CRACKED_POLISHED_BLACKSTONE_BRICKS(), Ids::CRACKED_POLISHED_BLACKSTONE_BRICKS); + $reg->mapSimple(Blocks::CRACKED_STONE_BRICKS(), Ids::CRACKED_STONE_BRICKS); + $reg->mapSimple(Blocks::CRAFTING_TABLE(), Ids::CRAFTING_TABLE); + $reg->mapSimple(Blocks::CRIMSON_ROOTS(), Ids::CRIMSON_ROOTS); + $reg->mapSimple(Blocks::CRYING_OBSIDIAN(), Ids::CRYING_OBSIDIAN); + $reg->mapSimple(Blocks::DANDELION(), Ids::DANDELION); + $reg->mapSimple(Blocks::CUT_RED_SANDSTONE(), Ids::CUT_RED_SANDSTONE); + $reg->mapSimple(Blocks::CUT_SANDSTONE(), Ids::CUT_SANDSTONE); + $reg->mapSimple(Blocks::DARK_PRISMARINE(), Ids::DARK_PRISMARINE); + $reg->mapSimple(Blocks::DEAD_BUSH(), Ids::DEADBUSH); + $reg->mapSimple(Blocks::DEEPSLATE_BRICKS(), Ids::DEEPSLATE_BRICKS); + $reg->mapSimple(Blocks::DEEPSLATE_COAL_ORE(), Ids::DEEPSLATE_COAL_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_COPPER_ORE(), Ids::DEEPSLATE_COPPER_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_DIAMOND_ORE(), Ids::DEEPSLATE_DIAMOND_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_EMERALD_ORE(), Ids::DEEPSLATE_EMERALD_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_GOLD_ORE(), Ids::DEEPSLATE_GOLD_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_IRON_ORE(), Ids::DEEPSLATE_IRON_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_LAPIS_LAZULI_ORE(), Ids::DEEPSLATE_LAPIS_ORE); + $reg->mapSimple(Blocks::DEEPSLATE_TILES(), Ids::DEEPSLATE_TILES); + $reg->mapSimple(Blocks::DIAMOND(), Ids::DIAMOND_BLOCK); + $reg->mapSimple(Blocks::DIAMOND_ORE(), Ids::DIAMOND_ORE); + $reg->mapSimple(Blocks::DIORITE(), Ids::DIORITE); + $reg->mapSimple(Blocks::DRAGON_EGG(), Ids::DRAGON_EGG); + $reg->mapSimple(Blocks::DRIED_KELP(), Ids::DRIED_KELP_BLOCK); + $reg->mapSimple(Blocks::ELEMENT_ACTINIUM(), Ids::ELEMENT_89); + $reg->mapSimple(Blocks::ELEMENT_ALUMINUM(), Ids::ELEMENT_13); + $reg->mapSimple(Blocks::ELEMENT_AMERICIUM(), Ids::ELEMENT_95); + $reg->mapSimple(Blocks::ELEMENT_ANTIMONY(), Ids::ELEMENT_51); + $reg->mapSimple(Blocks::ELEMENT_ARGON(), Ids::ELEMENT_18); + $reg->mapSimple(Blocks::ELEMENT_ARSENIC(), Ids::ELEMENT_33); + $reg->mapSimple(Blocks::ELEMENT_ASTATINE(), Ids::ELEMENT_85); + $reg->mapSimple(Blocks::ELEMENT_BARIUM(), Ids::ELEMENT_56); + $reg->mapSimple(Blocks::ELEMENT_BERKELIUM(), Ids::ELEMENT_97); + $reg->mapSimple(Blocks::ELEMENT_BERYLLIUM(), Ids::ELEMENT_4); + $reg->mapSimple(Blocks::ELEMENT_BISMUTH(), Ids::ELEMENT_83); + $reg->mapSimple(Blocks::ELEMENT_BOHRIUM(), Ids::ELEMENT_107); + $reg->mapSimple(Blocks::ELEMENT_BORON(), Ids::ELEMENT_5); + $reg->mapSimple(Blocks::ELEMENT_BROMINE(), Ids::ELEMENT_35); + $reg->mapSimple(Blocks::ELEMENT_CADMIUM(), Ids::ELEMENT_48); + $reg->mapSimple(Blocks::ELEMENT_CALCIUM(), Ids::ELEMENT_20); + $reg->mapSimple(Blocks::ELEMENT_CALIFORNIUM(), Ids::ELEMENT_98); + $reg->mapSimple(Blocks::ELEMENT_CARBON(), Ids::ELEMENT_6); + $reg->mapSimple(Blocks::ELEMENT_CERIUM(), Ids::ELEMENT_58); + $reg->mapSimple(Blocks::ELEMENT_CESIUM(), Ids::ELEMENT_55); + $reg->mapSimple(Blocks::ELEMENT_CHLORINE(), Ids::ELEMENT_17); + $reg->mapSimple(Blocks::ELEMENT_CHROMIUM(), Ids::ELEMENT_24); + $reg->mapSimple(Blocks::ELEMENT_COBALT(), Ids::ELEMENT_27); + $reg->mapSimple(Blocks::ELEMENT_COPERNICIUM(), Ids::ELEMENT_112); + $reg->mapSimple(Blocks::ELEMENT_COPPER(), Ids::ELEMENT_29); + $reg->mapSimple(Blocks::ELEMENT_CURIUM(), Ids::ELEMENT_96); + $reg->mapSimple(Blocks::ELEMENT_DARMSTADTIUM(), Ids::ELEMENT_110); + $reg->mapSimple(Blocks::ELEMENT_DUBNIUM(), Ids::ELEMENT_105); + $reg->mapSimple(Blocks::ELEMENT_DYSPROSIUM(), Ids::ELEMENT_66); + $reg->mapSimple(Blocks::ELEMENT_EINSTEINIUM(), Ids::ELEMENT_99); + $reg->mapSimple(Blocks::ELEMENT_ERBIUM(), Ids::ELEMENT_68); + $reg->mapSimple(Blocks::ELEMENT_EUROPIUM(), Ids::ELEMENT_63); + $reg->mapSimple(Blocks::ELEMENT_FERMIUM(), Ids::ELEMENT_100); + $reg->mapSimple(Blocks::ELEMENT_FLEROVIUM(), Ids::ELEMENT_114); + $reg->mapSimple(Blocks::ELEMENT_FLUORINE(), Ids::ELEMENT_9); + $reg->mapSimple(Blocks::ELEMENT_FRANCIUM(), Ids::ELEMENT_87); + $reg->mapSimple(Blocks::ELEMENT_GADOLINIUM(), Ids::ELEMENT_64); + $reg->mapSimple(Blocks::ELEMENT_GALLIUM(), Ids::ELEMENT_31); + $reg->mapSimple(Blocks::ELEMENT_GERMANIUM(), Ids::ELEMENT_32); + $reg->mapSimple(Blocks::ELEMENT_GOLD(), Ids::ELEMENT_79); + $reg->mapSimple(Blocks::ELEMENT_HAFNIUM(), Ids::ELEMENT_72); + $reg->mapSimple(Blocks::ELEMENT_HASSIUM(), Ids::ELEMENT_108); + $reg->mapSimple(Blocks::ELEMENT_HELIUM(), Ids::ELEMENT_2); + $reg->mapSimple(Blocks::ELEMENT_HOLMIUM(), Ids::ELEMENT_67); + $reg->mapSimple(Blocks::ELEMENT_HYDROGEN(), Ids::ELEMENT_1); + $reg->mapSimple(Blocks::ELEMENT_INDIUM(), Ids::ELEMENT_49); + $reg->mapSimple(Blocks::ELEMENT_IODINE(), Ids::ELEMENT_53); + $reg->mapSimple(Blocks::ELEMENT_IRIDIUM(), Ids::ELEMENT_77); + $reg->mapSimple(Blocks::ELEMENT_IRON(), Ids::ELEMENT_26); + $reg->mapSimple(Blocks::ELEMENT_KRYPTON(), Ids::ELEMENT_36); + $reg->mapSimple(Blocks::ELEMENT_LANTHANUM(), Ids::ELEMENT_57); + $reg->mapSimple(Blocks::ELEMENT_LAWRENCIUM(), Ids::ELEMENT_103); + $reg->mapSimple(Blocks::ELEMENT_LEAD(), Ids::ELEMENT_82); + $reg->mapSimple(Blocks::ELEMENT_LITHIUM(), Ids::ELEMENT_3); + $reg->mapSimple(Blocks::ELEMENT_LIVERMORIUM(), Ids::ELEMENT_116); + $reg->mapSimple(Blocks::ELEMENT_LUTETIUM(), Ids::ELEMENT_71); + $reg->mapSimple(Blocks::ELEMENT_MAGNESIUM(), Ids::ELEMENT_12); + $reg->mapSimple(Blocks::ELEMENT_MANGANESE(), Ids::ELEMENT_25); + $reg->mapSimple(Blocks::ELEMENT_MEITNERIUM(), Ids::ELEMENT_109); + $reg->mapSimple(Blocks::ELEMENT_MENDELEVIUM(), Ids::ELEMENT_101); + $reg->mapSimple(Blocks::ELEMENT_MERCURY(), Ids::ELEMENT_80); + $reg->mapSimple(Blocks::ELEMENT_MOLYBDENUM(), Ids::ELEMENT_42); + $reg->mapSimple(Blocks::ELEMENT_MOSCOVIUM(), Ids::ELEMENT_115); + $reg->mapSimple(Blocks::ELEMENT_NEODYMIUM(), Ids::ELEMENT_60); + $reg->mapSimple(Blocks::ELEMENT_NEON(), Ids::ELEMENT_10); + $reg->mapSimple(Blocks::ELEMENT_NEPTUNIUM(), Ids::ELEMENT_93); + $reg->mapSimple(Blocks::ELEMENT_NICKEL(), Ids::ELEMENT_28); + $reg->mapSimple(Blocks::ELEMENT_NIHONIUM(), Ids::ELEMENT_113); + $reg->mapSimple(Blocks::ELEMENT_NIOBIUM(), Ids::ELEMENT_41); + $reg->mapSimple(Blocks::ELEMENT_NITROGEN(), Ids::ELEMENT_7); + $reg->mapSimple(Blocks::ELEMENT_NOBELIUM(), Ids::ELEMENT_102); + $reg->mapSimple(Blocks::ELEMENT_OGANESSON(), Ids::ELEMENT_118); + $reg->mapSimple(Blocks::ELEMENT_OSMIUM(), Ids::ELEMENT_76); + $reg->mapSimple(Blocks::ELEMENT_OXYGEN(), Ids::ELEMENT_8); + $reg->mapSimple(Blocks::ELEMENT_PALLADIUM(), Ids::ELEMENT_46); + $reg->mapSimple(Blocks::ELEMENT_PHOSPHORUS(), Ids::ELEMENT_15); + $reg->mapSimple(Blocks::ELEMENT_PLATINUM(), Ids::ELEMENT_78); + $reg->mapSimple(Blocks::ELEMENT_PLUTONIUM(), Ids::ELEMENT_94); + $reg->mapSimple(Blocks::ELEMENT_POLONIUM(), Ids::ELEMENT_84); + $reg->mapSimple(Blocks::ELEMENT_POTASSIUM(), Ids::ELEMENT_19); + $reg->mapSimple(Blocks::ELEMENT_PRASEODYMIUM(), Ids::ELEMENT_59); + $reg->mapSimple(Blocks::ELEMENT_PROMETHIUM(), Ids::ELEMENT_61); + $reg->mapSimple(Blocks::ELEMENT_PROTACTINIUM(), Ids::ELEMENT_91); + $reg->mapSimple(Blocks::ELEMENT_RADIUM(), Ids::ELEMENT_88); + $reg->mapSimple(Blocks::ELEMENT_RADON(), Ids::ELEMENT_86); + $reg->mapSimple(Blocks::ELEMENT_RHENIUM(), Ids::ELEMENT_75); + $reg->mapSimple(Blocks::ELEMENT_RHODIUM(), Ids::ELEMENT_45); + $reg->mapSimple(Blocks::ELEMENT_ROENTGENIUM(), Ids::ELEMENT_111); + $reg->mapSimple(Blocks::ELEMENT_RUBIDIUM(), Ids::ELEMENT_37); + $reg->mapSimple(Blocks::ELEMENT_RUTHENIUM(), Ids::ELEMENT_44); + $reg->mapSimple(Blocks::ELEMENT_RUTHERFORDIUM(), Ids::ELEMENT_104); + $reg->mapSimple(Blocks::ELEMENT_SAMARIUM(), Ids::ELEMENT_62); + $reg->mapSimple(Blocks::ELEMENT_SCANDIUM(), Ids::ELEMENT_21); + $reg->mapSimple(Blocks::ELEMENT_SEABORGIUM(), Ids::ELEMENT_106); + $reg->mapSimple(Blocks::ELEMENT_SELENIUM(), Ids::ELEMENT_34); + $reg->mapSimple(Blocks::ELEMENT_SILICON(), Ids::ELEMENT_14); + $reg->mapSimple(Blocks::ELEMENT_SILVER(), Ids::ELEMENT_47); + $reg->mapSimple(Blocks::ELEMENT_SODIUM(), Ids::ELEMENT_11); + $reg->mapSimple(Blocks::ELEMENT_STRONTIUM(), Ids::ELEMENT_38); + $reg->mapSimple(Blocks::ELEMENT_SULFUR(), Ids::ELEMENT_16); + $reg->mapSimple(Blocks::ELEMENT_TANTALUM(), Ids::ELEMENT_73); + $reg->mapSimple(Blocks::ELEMENT_TECHNETIUM(), Ids::ELEMENT_43); + $reg->mapSimple(Blocks::ELEMENT_TELLURIUM(), Ids::ELEMENT_52); + $reg->mapSimple(Blocks::ELEMENT_TENNESSINE(), Ids::ELEMENT_117); + $reg->mapSimple(Blocks::ELEMENT_TERBIUM(), Ids::ELEMENT_65); + $reg->mapSimple(Blocks::ELEMENT_THALLIUM(), Ids::ELEMENT_81); + $reg->mapSimple(Blocks::ELEMENT_THORIUM(), Ids::ELEMENT_90); + $reg->mapSimple(Blocks::ELEMENT_THULIUM(), Ids::ELEMENT_69); + $reg->mapSimple(Blocks::ELEMENT_TIN(), Ids::ELEMENT_50); + $reg->mapSimple(Blocks::ELEMENT_TITANIUM(), Ids::ELEMENT_22); + $reg->mapSimple(Blocks::ELEMENT_TUNGSTEN(), Ids::ELEMENT_74); + $reg->mapSimple(Blocks::ELEMENT_URANIUM(), Ids::ELEMENT_92); + $reg->mapSimple(Blocks::ELEMENT_VANADIUM(), Ids::ELEMENT_23); + $reg->mapSimple(Blocks::ELEMENT_XENON(), Ids::ELEMENT_54); + $reg->mapSimple(Blocks::ELEMENT_YTTERBIUM(), Ids::ELEMENT_70); + $reg->mapSimple(Blocks::ELEMENT_YTTRIUM(), Ids::ELEMENT_39); + $reg->mapSimple(Blocks::ELEMENT_ZERO(), Ids::ELEMENT_0); + $reg->mapSimple(Blocks::ELEMENT_ZINC(), Ids::ELEMENT_30); + $reg->mapSimple(Blocks::ELEMENT_ZIRCONIUM(), Ids::ELEMENT_40); + $reg->mapSimple(Blocks::EMERALD(), Ids::EMERALD_BLOCK); + $reg->mapSimple(Blocks::EMERALD_ORE(), Ids::EMERALD_ORE); + $reg->mapSimple(Blocks::ENCHANTING_TABLE(), Ids::ENCHANTING_TABLE); + $reg->mapSimple(Blocks::END_STONE(), Ids::END_STONE); + $reg->mapSimple(Blocks::END_STONE_BRICKS(), Ids::END_BRICKS); + $reg->mapSimple(Blocks::FERN(), Ids::FERN); + $reg->mapSimple(Blocks::FLETCHING_TABLE(), Ids::FLETCHING_TABLE); + $reg->mapSimple(Blocks::GILDED_BLACKSTONE(), Ids::GILDED_BLACKSTONE); + $reg->mapSimple(Blocks::GLASS(), Ids::GLASS); + $reg->mapSimple(Blocks::GLASS_PANE(), Ids::GLASS_PANE); + $reg->mapSimple(Blocks::GLOWING_OBSIDIAN(), Ids::GLOWINGOBSIDIAN); + $reg->mapSimple(Blocks::GLOWSTONE(), Ids::GLOWSTONE); + $reg->mapSimple(Blocks::GOLD(), Ids::GOLD_BLOCK); + $reg->mapSimple(Blocks::GOLD_ORE(), Ids::GOLD_ORE); + $reg->mapSimple(Blocks::GRANITE(), Ids::GRANITE); + $reg->mapSimple(Blocks::GRASS(), Ids::GRASS_BLOCK); + $reg->mapSimple(Blocks::GRASS_PATH(), Ids::GRASS_PATH); + $reg->mapSimple(Blocks::GRAVEL(), Ids::GRAVEL); + $reg->mapSimple(Blocks::HANGING_ROOTS(), Ids::HANGING_ROOTS); + $reg->mapSimple(Blocks::HARDENED_CLAY(), Ids::HARDENED_CLAY); + $reg->mapSimple(Blocks::HARDENED_GLASS(), Ids::HARD_GLASS); + $reg->mapSimple(Blocks::HARDENED_GLASS_PANE(), Ids::HARD_GLASS_PANE); + $reg->mapSimple(Blocks::HONEYCOMB(), Ids::HONEYCOMB_BLOCK); + $reg->mapSimple(Blocks::ICE(), Ids::ICE); + $reg->mapSimple(Blocks::INFESTED_CHISELED_STONE_BRICK(), Ids::INFESTED_CHISELED_STONE_BRICKS); + $reg->mapSimple(Blocks::INFESTED_COBBLESTONE(), Ids::INFESTED_COBBLESTONE); + $reg->mapSimple(Blocks::INFESTED_CRACKED_STONE_BRICK(), Ids::INFESTED_CRACKED_STONE_BRICKS); + $reg->mapSimple(Blocks::INFESTED_MOSSY_STONE_BRICK(), Ids::INFESTED_MOSSY_STONE_BRICKS); + $reg->mapSimple(Blocks::INFESTED_STONE(), Ids::INFESTED_STONE); + $reg->mapSimple(Blocks::INFESTED_STONE_BRICK(), Ids::INFESTED_STONE_BRICKS); + $reg->mapSimple(Blocks::INFO_UPDATE(), Ids::INFO_UPDATE); + $reg->mapSimple(Blocks::INFO_UPDATE2(), Ids::INFO_UPDATE2); + $reg->mapSimple(Blocks::INVISIBLE_BEDROCK(), Ids::INVISIBLE_BEDROCK); + $reg->mapSimple(Blocks::IRON(), Ids::IRON_BLOCK); + $reg->mapSimple(Blocks::IRON_BARS(), Ids::IRON_BARS); + $reg->mapSimple(Blocks::IRON_ORE(), Ids::IRON_ORE); + $reg->mapSimple(Blocks::JUKEBOX(), Ids::JUKEBOX); + $reg->mapSimple(Blocks::LAPIS_LAZULI(), Ids::LAPIS_BLOCK); + $reg->mapSimple(Blocks::LAPIS_LAZULI_ORE(), Ids::LAPIS_ORE); + $reg->mapSimple(Blocks::LEGACY_STONECUTTER(), Ids::STONECUTTER); + $reg->mapSimple(Blocks::LILY_PAD(), Ids::WATERLILY); + $reg->mapSimple(Blocks::MAGMA(), Ids::MAGMA); + $reg->mapSimple(Blocks::MANGROVE_ROOTS(), Ids::MANGROVE_ROOTS); + $reg->mapSimple(Blocks::MELON(), Ids::MELON_BLOCK); + $reg->mapSimple(Blocks::MONSTER_SPAWNER(), Ids::MOB_SPAWNER); + $reg->mapSimple(Blocks::MOSSY_COBBLESTONE(), Ids::MOSSY_COBBLESTONE); + $reg->mapSimple(Blocks::MOSSY_STONE_BRICKS(), Ids::MOSSY_STONE_BRICKS); + $reg->mapSimple(Blocks::MUD(), Ids::MUD); + $reg->mapSimple(Blocks::MUD_BRICKS(), Ids::MUD_BRICKS); + $reg->mapSimple(Blocks::MYCELIUM(), Ids::MYCELIUM); + $reg->mapSimple(Blocks::NETHERITE(), Ids::NETHERITE_BLOCK); + $reg->mapSimple(Blocks::NETHERRACK(), Ids::NETHERRACK); + $reg->mapSimple(Blocks::NETHER_BRICKS(), Ids::NETHER_BRICK); + $reg->mapSimple(Blocks::NETHER_BRICK_FENCE(), Ids::NETHER_BRICK_FENCE); + $reg->mapSimple(Blocks::NETHER_GOLD_ORE(), Ids::NETHER_GOLD_ORE); + $reg->mapSimple(Blocks::NETHER_QUARTZ_ORE(), Ids::QUARTZ_ORE); + $reg->mapSimple(Blocks::NETHER_REACTOR_CORE(), Ids::NETHERREACTOR); + $reg->mapSimple(Blocks::NETHER_WART_BLOCK(), Ids::NETHER_WART_BLOCK); + $reg->mapSimple(Blocks::NOTE_BLOCK(), Ids::NOTEBLOCK); + $reg->mapSimple(Blocks::OBSIDIAN(), Ids::OBSIDIAN); + $reg->mapSimple(Blocks::PACKED_ICE(), Ids::PACKED_ICE); + $reg->mapSimple(Blocks::PACKED_MUD(), Ids::PACKED_MUD); + $reg->mapSimple(Blocks::PODZOL(), Ids::PODZOL); + $reg->mapSimple(Blocks::POLISHED_ANDESITE(), Ids::POLISHED_ANDESITE); + $reg->mapSimple(Blocks::POLISHED_BLACKSTONE(), Ids::POLISHED_BLACKSTONE); + $reg->mapSimple(Blocks::POLISHED_BLACKSTONE_BRICKS(), Ids::POLISHED_BLACKSTONE_BRICKS); + $reg->mapSimple(Blocks::POLISHED_DEEPSLATE(), Ids::POLISHED_DEEPSLATE); + $reg->mapSimple(Blocks::POLISHED_DIORITE(), Ids::POLISHED_DIORITE); + $reg->mapSimple(Blocks::POLISHED_GRANITE(), Ids::POLISHED_GRANITE); + $reg->mapSimple(Blocks::POLISHED_TUFF(), Ids::POLISHED_TUFF); + $reg->mapSimple(Blocks::PRISMARINE(), Ids::PRISMARINE); + $reg->mapSimple(Blocks::PRISMARINE_BRICKS(), Ids::PRISMARINE_BRICKS); + $reg->mapSimple(Blocks::QUARTZ_BRICKS(), Ids::QUARTZ_BRICKS); + $reg->mapSimple(Blocks::RAW_COPPER(), Ids::RAW_COPPER_BLOCK); + $reg->mapSimple(Blocks::RAW_GOLD(), Ids::RAW_GOLD_BLOCK); + $reg->mapSimple(Blocks::RAW_IRON(), Ids::RAW_IRON_BLOCK); + $reg->mapSimple(Blocks::REDSTONE(), Ids::REDSTONE_BLOCK); + $reg->mapSimple(Blocks::RED_MUSHROOM(), Ids::RED_MUSHROOM); + $reg->mapSimple(Blocks::RED_NETHER_BRICKS(), Ids::RED_NETHER_BRICK); + $reg->mapSimple(Blocks::RED_SAND(), Ids::RED_SAND); + $reg->mapSimple(Blocks::RED_SANDSTONE(), Ids::RED_SANDSTONE); + $reg->mapSimple(Blocks::REINFORCED_DEEPSLATE(), Ids::REINFORCED_DEEPSLATE); + $reg->mapSimple(Blocks::RESERVED6(), Ids::RESERVED6); + $reg->mapSimple(Blocks::RESIN(), Ids::RESIN_BLOCK); + $reg->mapSimple(Blocks::RESIN_BRICKS(), Ids::RESIN_BRICKS); + $reg->mapSimple(Blocks::SAND(), Ids::SAND); + $reg->mapSimple(Blocks::SANDSTONE(), Ids::SANDSTONE); + $reg->mapSimple(Blocks::SCULK(), Ids::SCULK); + $reg->mapSimple(Blocks::SEA_LANTERN(), Ids::SEA_LANTERN); + $reg->mapSimple(Blocks::SHROOMLIGHT(), Ids::SHROOMLIGHT); + $reg->mapSimple(Blocks::SHULKER_BOX(), Ids::UNDYED_SHULKER_BOX); + $reg->mapSimple(Blocks::SLIME(), Ids::SLIME); + $reg->mapSimple(Blocks::SMITHING_TABLE(), Ids::SMITHING_TABLE); + $reg->mapSimple(Blocks::SMOOTH_BASALT(), Ids::SMOOTH_BASALT); + $reg->mapSimple(Blocks::SMOOTH_RED_SANDSTONE(), Ids::SMOOTH_RED_SANDSTONE); + $reg->mapSimple(Blocks::SMOOTH_SANDSTONE(), Ids::SMOOTH_SANDSTONE); + $reg->mapSimple(Blocks::SMOOTH_STONE(), Ids::SMOOTH_STONE); + $reg->mapSimple(Blocks::SNOW(), Ids::SNOW); + $reg->mapSimple(Blocks::SOUL_SAND(), Ids::SOUL_SAND); + $reg->mapSimple(Blocks::SOUL_SOIL(), Ids::SOUL_SOIL); + $reg->mapSimple(Blocks::SPORE_BLOSSOM(), Ids::SPORE_BLOSSOM); + $reg->mapSimple(Blocks::STONE(), Ids::STONE); + $reg->mapSimple(Blocks::STONE_BRICKS(), Ids::STONE_BRICKS); + $reg->mapSimple(Blocks::TALL_GRASS(), Ids::SHORT_GRASS); //no, this is not a typo - tall_grass is now the double block, just to be confusing :( + $reg->mapSimple(Blocks::TINTED_GLASS(), Ids::TINTED_GLASS); + $reg->mapSimple(Blocks::TORCHFLOWER(), Ids::TORCHFLOWER); + $reg->mapSimple(Blocks::TUFF(), Ids::TUFF); + $reg->mapSimple(Blocks::TUFF_BRICKS(), Ids::TUFF_BRICKS); + $reg->mapSimple(Blocks::WARPED_WART_BLOCK(), Ids::WARPED_WART_BLOCK); + $reg->mapSimple(Blocks::WARPED_ROOTS(), Ids::WARPED_ROOTS); + $reg->mapSimple(Blocks::WITHER_ROSE(), Ids::WITHER_ROSE); + + $reg->mapSimple(Blocks::ALLIUM(), Ids::ALLIUM); + $reg->mapSimple(Blocks::CORNFLOWER(), Ids::CORNFLOWER); + $reg->mapSimple(Blocks::AZURE_BLUET(), Ids::AZURE_BLUET); + $reg->mapSimple(Blocks::LILY_OF_THE_VALLEY(), Ids::LILY_OF_THE_VALLEY); + $reg->mapSimple(Blocks::BLUE_ORCHID(), Ids::BLUE_ORCHID); + $reg->mapSimple(Blocks::OXEYE_DAISY(), Ids::OXEYE_DAISY); + $reg->mapSimple(Blocks::POPPY(), Ids::POPPY); + $reg->mapSimple(Blocks::ORANGE_TULIP(), Ids::ORANGE_TULIP); + $reg->mapSimple(Blocks::PINK_TULIP(), Ids::PINK_TULIP); + $reg->mapSimple(Blocks::RED_TULIP(), Ids::RED_TULIP); + $reg->mapSimple(Blocks::WHITE_TULIP(), Ids::WHITE_TULIP); + } + + private static function registerColoredMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + $reg->mapColored(Blocks::STAINED_HARDENED_GLASS(), "minecraft:hard_", "_stained_glass"); + $reg->mapColored(Blocks::STAINED_HARDENED_GLASS_PANE(), "minecraft:hard_", "_stained_glass_pane"); + + $reg->mapColored(Blocks::CARPET(), "minecraft:", "_carpet"); + $reg->mapColored(Blocks::CONCRETE(), "minecraft:", "_concrete"); + $reg->mapColored(Blocks::CONCRETE_POWDER(), "minecraft:", "_concrete_powder"); + $reg->mapColored(Blocks::DYED_SHULKER_BOX(), "minecraft:", "_shulker_box"); + $reg->mapColored(Blocks::STAINED_CLAY(), "minecraft:", "_terracotta"); + $reg->mapColored(Blocks::STAINED_GLASS(), "minecraft:", "_stained_glass"); + $reg->mapColored(Blocks::STAINED_GLASS_PANE(), "minecraft:", "_stained_glass_pane"); + $reg->mapColored(Blocks::WOOL(), "minecraft:", "_wool"); + + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::GLAZED_TERRACOTTA()) + ->idComponents([ + "minecraft:", + new ValueFromStringProperty("color", ValueMappings::getInstance()->dyeColorWithSilver, fn(GlazedTerracotta $b) => $b->getColor(), fn(GlazedTerracotta $b, DyeColor $v) => $b->setColor($v)), + "_glazed_terracotta" + ]) + ->properties([$commonProperties->horizontalFacingClassic]) + ); + } + + private static function registerCandleMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + $candleProperties = [ + $commonProperties->lit, + new IntProperty(StateNames::CANDLES, 0, 3, fn(Candle $b) => $b->getCount(), fn(Candle $b, int $v) => $b->setCount($v), offset: 1), + ]; + $cakeWithCandleProperties = [$commonProperties->lit]; + $reg->mapModel(Model::create(Blocks::CANDLE(), Ids::CANDLE)->properties($candleProperties)); + $reg->mapModel(Model::create(Blocks::CAKE_WITH_CANDLE(), Ids::CANDLE_CAKE)->properties($cakeWithCandleProperties)); + + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::DYED_CANDLE()) + ->idComponents([ + "minecraft:", + $commonProperties->dyeColorIdInfix, + "_candle" + ]) + ->properties($candleProperties) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CAKE_WITH_DYED_CANDLE()) + ->idComponents([ + "minecraft:", + $commonProperties->dyeColorIdInfix, + "_candle_cake" + ]) + ->properties($cakeWithCandleProperties) + ); + } + + private static function registerLeavesMappings(BlockSerializerDeserializerRegistrar $reg) : void{ + $properties = [ + new BoolProperty(StateNames::PERSISTENT_BIT, fn(Leaves $b) => $b->isNoDecay(), fn(Leaves $b, bool $v) => $b->setNoDecay($v)), + new BoolProperty(StateNames::UPDATE_BIT, fn(Leaves $b) => $b->isCheckDecay(), fn(Leaves $b, bool $v) => $b->setCheckDecay($v)), + ]; + foreach([ + Ids::ACACIA_LEAVES => Blocks::ACACIA_LEAVES(), + Ids::AZALEA_LEAVES => Blocks::AZALEA_LEAVES(), + Ids::AZALEA_LEAVES_FLOWERED => Blocks::FLOWERING_AZALEA_LEAVES(), + Ids::BIRCH_LEAVES => Blocks::BIRCH_LEAVES(), + Ids::CHERRY_LEAVES => Blocks::CHERRY_LEAVES(), + Ids::DARK_OAK_LEAVES => Blocks::DARK_OAK_LEAVES(), + Ids::JUNGLE_LEAVES => Blocks::JUNGLE_LEAVES(), + Ids::MANGROVE_LEAVES => Blocks::MANGROVE_LEAVES(), + Ids::OAK_LEAVES => Blocks::OAK_LEAVES(), + Ids::PALE_OAK_LEAVES => Blocks::PALE_OAK_LEAVES(), + Ids::SPRUCE_LEAVES => Blocks::SPRUCE_LEAVES() + ] as $id => $block){ + $reg->mapModel(Model::create($block, $id)->properties($properties)); + } + } + + private static function registerSaplingMappings(BlockSerializerDeserializerRegistrar $reg) : void{ + $properties = [ + new BoolProperty(StateNames::AGE_BIT, fn(Sapling $b) => $b->isReady(), fn(Sapling $b, bool $v) => $b->setReady($v)), + ]; + foreach([ + Ids::ACACIA_SAPLING => Blocks::ACACIA_SAPLING(), + Ids::BIRCH_SAPLING => Blocks::BIRCH_SAPLING(), + Ids::DARK_OAK_SAPLING => Blocks::DARK_OAK_SAPLING(), + Ids::JUNGLE_SAPLING => Blocks::JUNGLE_SAPLING(), + Ids::OAK_SAPLING => Blocks::OAK_SAPLING(), + Ids::SPRUCE_SAPLING => Blocks::SPRUCE_SAPLING(), + ] as $id => $block){ + $reg->mapModel(Model::create($block, $id)->properties($properties)); + } + } + + private static function registerPlantMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + $reg->mapModel(Model::create(Blocks::BEETROOTS(), Ids::BEETROOT)->properties([$commonProperties->cropAgeMax7])); + $reg->mapModel(Model::create(Blocks::CARROTS(), Ids::CARROTS)->properties([$commonProperties->cropAgeMax7])); + $reg->mapModel(Model::create(Blocks::POTATOES(), Ids::POTATOES)->properties([$commonProperties->cropAgeMax7])); + $reg->mapModel(Model::create(Blocks::WHEAT(), Ids::WHEAT)->properties([$commonProperties->cropAgeMax7])); + + $reg->mapModel(Model::create(Blocks::MELON_STEM(), Ids::MELON_STEM)->properties($commonProperties->stemProperties)); + $reg->mapModel(Model::create(Blocks::PUMPKIN_STEM(), Ids::PUMPKIN_STEM)->properties($commonProperties->stemProperties)); + + foreach([ + [Blocks::DOUBLE_TALLGRASS(), Ids::TALL_GRASS], + [Blocks::LARGE_FERN(), Ids::LARGE_FERN], + [Blocks::LILAC(), Ids::LILAC], + [Blocks::PEONY(), Ids::PEONY], + [Blocks::ROSE_BUSH(), Ids::ROSE_BUSH], + [Blocks::SUNFLOWER(), Ids::SUNFLOWER], + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([$commonProperties->doublePlantHalf])); + } + + foreach([ + [Blocks::BROWN_MUSHROOM_BLOCK(), Ids::BROWN_MUSHROOM_BLOCK], + [Blocks::RED_MUSHROOM_BLOCK(), Ids::RED_MUSHROOM_BLOCK] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([ + new ValueFromIntProperty(StateNames::HUGE_MUSHROOM_BITS, ValueMappings::getInstance()->mushroomBlockType, fn(RedMushroomBlock $b) => $b->getMushroomBlockType(), fn(RedMushroomBlock $b, MushroomBlockType $v) => $b->setMushroomBlockType($v)), + ])); + } + + $reg->mapModel(Model::create(Blocks::GLOW_LICHEN(), Ids::GLOW_LICHEN)->properties([$commonProperties->multiFacingFlags])); + $reg->mapModel(Model::create(Blocks::RESIN_CLUMP(), Ids::RESIN_CLUMP)->properties([$commonProperties->multiFacingFlags])); + + $reg->mapModel(Model::create(Blocks::VINES(), Ids::VINE)->properties([ + new ValueSetFromIntProperty( + StateNames::VINE_DIRECTION_BITS, + IntFromRawStateMap::int([ + Facing::NORTH => BlockLegacyMetadata::VINE_FLAG_NORTH, + Facing::SOUTH => BlockLegacyMetadata::VINE_FLAG_SOUTH, + Facing::WEST => BlockLegacyMetadata::VINE_FLAG_WEST, + Facing::EAST => BlockLegacyMetadata::VINE_FLAG_EAST, + ]), + fn(Vine $b) => $b->getFaces(), + fn(Vine $b, array $v) => $b->setFaces($v) + ) + ])); + + $reg->mapModel(Model::create(Blocks::SWEET_BERRY_BUSH(), Ids::SWEET_BERRY_BUSH)->properties([ + //TODO: berry bush only wants 0-3, but it can be bigger in MCPE due to misuse of GROWTH state which goes up to 7 + new IntProperty(StateNames::GROWTH, 0, 7, fn(SweetBerryBush $b) => $b->getAge(), fn(SweetBerryBush $b, int $v) => $b->setAge(min($v, SweetBerryBush::STAGE_MATURE))) + ])); + $reg->mapModel(Model::create(Blocks::TORCHFLOWER_CROP(), Ids::TORCHFLOWER_CROP)->properties([ + //TODO: this property can have values 0-7, but only 0-1 are valid + new IntProperty(StateNames::GROWTH, 0, 7, fn(TorchflowerCrop $b) => $b->isReady() ? 1 : 0, fn(TorchflowerCrop $b, int $v) => $b->setReady($v !== 0)) + ])); + } + + private static function registerCoralMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CORAL())->idComponents([...$commonProperties->coralIdPrefixes, "_coral"])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CORAL_BLOCK())->idComponents([...$commonProperties->coralIdPrefixes, "_coral_block"])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CORAL_FAN()) + ->idComponents([...$commonProperties->coralIdPrefixes, "_coral_fan"]) + ->properties([ + new ValueFromIntProperty(StateNames::CORAL_FAN_DIRECTION, ValueMappings::getInstance()->coralAxis, fn(FloorCoralFan $b) => $b->getAxis(), fn(FloorCoralFan $b, int $v) => $b->setAxis($v)) + ]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::WALL_CORAL_FAN()) + ->idComponents([...$commonProperties->coralIdPrefixes, "_coral_wall_fan"]) + ->properties([ + new ValueFromIntProperty(StateNames::CORAL_DIRECTION, ValueMappings::getInstance()->horizontalFacingCoral, fn(HorizontalFacing $b) => $b->getFacing(), fn(HorizontalFacing $b, int $v) => $b->setFacing($v)), + ]) + ); + } + + private static function registerCopperMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::COPPER_BULB()) + ->idComponents([...$commonProperties->copperIdPrefixes, "copper_bulb"]) + ->properties([ + $commonProperties->lit, + new BoolProperty(StateNames::POWERED_BIT, fn(PoweredByRedstone $b) => $b->isPowered(), fn(PoweredByRedstone $b, bool $v) => $b->setPowered($v)), + ]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::COPPER()) + ->idComponents([ + ...$commonProperties->copperIdPrefixes, + "copper", + //HACK: the non-waxed, non-oxidised variant has a _block suffix, but none of the others do + new BoolFromStringProperty("bruhhhh", "", "_block", fn(Copper $b) => !$b->isWaxed() && $b->getOxidation() === CopperOxidation::NONE, fn() => null) + ]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CHISELED_COPPER())->idComponents([...$commonProperties->copperIdPrefixes, "chiseled_copper"])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::COPPER_GRATE())->idComponents([...$commonProperties->copperIdPrefixes, "copper_grate"])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CUT_COPPER())->idComponents([...$commonProperties->copperIdPrefixes, "cut_copper"])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CUT_COPPER_STAIRS()) + ->idComponents([...$commonProperties->copperIdPrefixes, "cut_copper_stairs"]) + ->properties($commonProperties->stairProperties) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::COPPER_TRAPDOOR()) + ->idComponents([...$commonProperties->copperIdPrefixes, "copper_trapdoor"]) + ->properties($commonProperties->trapdoorProperties) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::COPPER_DOOR()) + ->idComponents([...$commonProperties->copperIdPrefixes, "copper_door"]) + ->properties($commonProperties->doorProperties) + ); + + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CUT_COPPER_SLAB()) + ->idComponents([ + ...$commonProperties->copperIdPrefixes, + $commonProperties->slabIdInfix, + "cut_copper_slab" + ]) + ->properties([$commonProperties->slabPositionProperty]) + ); + } + + private static function registerFlattenedEnumMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + //A + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::ANVIL()) + ->idComponents([ + new ValueFromStringProperty("id", IntFromRawStateMap::string([ + 0 => Ids::ANVIL, + 1 => Ids::CHIPPED_ANVIL, + 2 => Ids::DAMAGED_ANVIL, + ]), fn(Anvil $b) => $b->getDamage(), fn(Anvil $b, int $v) => $b->setDamage($v)) + ]) + ->properties([$commonProperties->horizontalFacingCardinal]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::AMETHYST_CLUSTER()) + ->idComponents([ + new ValueFromStringProperty("id", IntFromRawStateMap::string([ + AmethystCluster::STAGE_SMALL_BUD => Ids::SMALL_AMETHYST_BUD, + AmethystCluster::STAGE_MEDIUM_BUD => Ids::MEDIUM_AMETHYST_BUD, + AmethystCluster::STAGE_LARGE_BUD => Ids::LARGE_AMETHYST_BUD, + AmethystCluster::STAGE_CLUSTER => Ids::AMETHYST_CLUSTER + ]), fn(AmethystCluster $b) => $b->getStage(), fn(AmethystCluster $b, int $v) => $b->setStage($v)) + ]) + ->properties([$commonProperties->blockFace]) + ); + + //C + //This one is a special offender :< + //I have no idea why this only has 3 IDs - there are 4 in Java and 4 visually distinct states in Bedrock + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::CAVE_VINES()) + ->idComponents([ + "minecraft:cave_vines", + new ValueFromStringProperty( + "variant", + EnumFromRawStateMap::string(FlattenedCaveVinesVariant::class, fn(FlattenedCaveVinesVariant $case) => $case->value), + fn(CaveVines $b) => $b->hasBerries() ? + ($b->isHead() ? + FlattenedCaveVinesVariant::HEAD_WITH_BERRIES : + FlattenedCaveVinesVariant::BODY_WITH_BERRIES) : + FlattenedCaveVinesVariant::NO_BERRIES, + fn(CaveVines $b, FlattenedCaveVinesVariant $v) => match($v){ + FlattenedCaveVinesVariant::HEAD_WITH_BERRIES => $b->setBerries(true)->setHead(true), + FlattenedCaveVinesVariant::BODY_WITH_BERRIES => $b->setBerries(true)->setHead(false), + FlattenedCaveVinesVariant::NO_BERRIES => $b->setBerries(false)->setHead(false), //assume this isn't a head, since we don't have enough information + } + ) + ]) + ->properties([ + new IntProperty(StateNames::GROWING_PLANT_AGE, 0, 25, fn(CaveVines $b) => $b->getAge(), fn(CaveVines $b, int $v) => $b->setAge($v)), + ]) + ); + + //D + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::DIRT()) + ->idComponents([ + new ValueFromStringProperty("id", EnumFromRawStateMap::string(DirtType::class, fn(DirtType $case) => match ($case) { + DirtType::NORMAL => Ids::DIRT, + DirtType::COARSE => Ids::COARSE_DIRT, + DirtType::ROOTED => Ids::DIRT_WITH_ROOTS, + }), fn(Dirt $b) => $b->getDirtType(), fn(Dirt $b, DirtType $v) => $b->setDirtType($v)) + ]) + ); + + //F + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::FROGLIGHT()) + ->idComponents([ + new ValueFromStringProperty("id", ValueMappings::getInstance()->froglightType, fn(Froglight $b) => $b->getFroglightType(), fn(Froglight $b, FroglightType $v) => $b->setFroglightType($v)), + ]) + ->properties([$commonProperties->pillarAxis]) + ); + + //L + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::LIGHT()) + ->idComponents([ + "minecraft:light_block_", + //this is a bit shit but it's easier than adapting IntProperty to support flattening :D + new ValueFromStringProperty( + "light_level", + IntFromRawStateMap::string(array_map(strval(...), range(0, 15))), + fn(Light $b) => $b->getLightLevel(), + fn(Light $b, int $v) => $b->setLightLevel($v) + ) + ]) + ); + + //M + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::MOB_HEAD()) + ->idComponents([ + new ValueFromStringProperty("id", ValueMappings::getInstance()->mobHeadType, fn(MobHead $b) => $b->getMobHeadType(), fn(MobHead $b, MobHeadType $v) => $b->setMobHeadType($v)), + ]) + ->properties([ + new ValueFromIntProperty(StateNames::FACING_DIRECTION, ValueMappings::getInstance()->facingExceptDown, fn(MobHead $b) => $b->getFacing(), fn(MobHead $b, int $v) => $b->setFacing($v)) + ]) + ); + + foreach([ + [Blocks::LAVA(), "lava"], + [Blocks::WATER(), "water"] + ] as [$block, $idSuffix]){ + $reg->mapFlattenedId(FlattenedIdModel::create($block) + ->idComponents([...$commonProperties->liquidIdPrefixes, $idSuffix]) + ->properties([$commonProperties->liquidData]) + ); + } + } + + private static function registerFlattenedBoolMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + foreach([ + [Blocks::BLAST_FURNACE(), "blast_furnace"], + [Blocks::FURNACE(), "furnace"], + [Blocks::SMOKER(), "smoker"] + ] as [$block, $idSuffix]){ + $reg->mapFlattenedId(FlattenedIdModel::create($block) + ->idComponents([...$commonProperties->furnaceIdPrefixes, $idSuffix]) + ->properties([$commonProperties->horizontalFacingCardinal]) + ); + } + + foreach([ + [Blocks::REDSTONE_LAMP(), "redstone_lamp"], + [Blocks::REDSTONE_ORE(), "redstone_ore"], + [Blocks::DEEPSLATE_REDSTONE_ORE(), "deepslate_redstone_ore"] + ] as [$block, $idSuffix]){ + $reg->mapFlattenedId(FlattenedIdModel::create($block)->idComponents(["minecraft:", $commonProperties->litIdInfix, $idSuffix])); + } + + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::DAYLIGHT_SENSOR()) + ->idComponents([ + "minecraft:daylight_detector", + new BoolFromStringProperty("inverted", "", "_inverted", fn(DaylightSensor $b) => $b->isInverted(), fn(DaylightSensor $b, bool $v) => $b->setInverted($v)) + ]) + ->properties([$commonProperties->analogRedstoneSignal]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::REDSTONE_REPEATER()) + ->idComponents([ + "minecraft:", + new BoolFromStringProperty("powered", "un", "", fn(RedstoneRepeater $b) => $b->isPowered(), fn(RedstoneRepeater $b, bool $v) => $b->setPowered($v)), + "powered_repeater" + ]) + ->properties([ + $commonProperties->horizontalFacingCardinal, + new IntProperty(StateNames::REPEATER_DELAY, 0, 3, fn(RedstoneRepeater $b) => $b->getDelay(), fn(RedstoneRepeater $b, int $v) => $b->setDelay($v), offset: 1), + ]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::REDSTONE_COMPARATOR()) + ->idComponents([ + "minecraft:", + //this property also appears in the state, so we ignore it in the ID + //this is baked here purely to keep minecraft happy + new BoolFromStringProperty("dummy_powered", "un", "", fn(RedstoneComparator $b) => $b->isPowered(), fn() => null), + "powered_comparator" + ]) + ->properties([ + $commonProperties->horizontalFacingCardinal, + new BoolProperty(StateNames::OUTPUT_LIT_BIT, fn(RedstoneComparator $b) => $b->isPowered(), fn(RedstoneComparator $b, bool $v) => $b->setPowered($v)), + new BoolProperty(StateNames::OUTPUT_SUBTRACT_BIT, fn(RedstoneComparator $b) => $b->isSubtractMode(), fn(RedstoneComparator $b, bool $v) => $b->setSubtractMode($v)), + ]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::REDSTONE_TORCH()) + ->idComponents([ + "minecraft:", + new BoolFromStringProperty("lit", "unlit_", "", fn(RedstoneTorch $b) => $b->isLit(), fn(RedstoneTorch $b, bool $v) => $b->setLit($v)), + "redstone_torch" + ]) + ->properties([$commonProperties->torchFacing]) + ); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::SPONGE())->idComponents([ + "minecraft:", + new BoolFromStringProperty("wet", "", "wet_", fn(Sponge $b) => $b->isWet(), fn(Sponge $b, bool $v) => $b->setWet($v)), + "sponge" + ])); + $reg->mapFlattenedId(FlattenedIdModel::create(Blocks::TNT()) + ->idComponents([ + "minecraft:", + new BoolFromStringProperty("underwater", "", "underwater_", fn(TNT $b) => $b->worksUnderwater(), fn(TNT $b, bool $v) => $b->setWorksUnderwater($v)), + "tnt" + ]) + ->properties([ + new BoolProperty(StateNames::EXPLODE_BIT, fn(TNT $b) => $b->isUnstable(), fn(TNT $b, bool $v) => $b->setUnstable($v)), + ]) + ); + } + + private static function registerStoneLikeSlabMappings(BlockSerializerDeserializerRegistrar $reg) : void{ + $reg->mapSlab(Blocks::ANDESITE_SLAB(), "andesite"); + $reg->mapSlab(Blocks::BLACKSTONE_SLAB(), "blackstone"); + $reg->mapSlab(Blocks::BRICK_SLAB(), "brick"); + $reg->mapSlab(Blocks::COBBLED_DEEPSLATE_SLAB(), "cobbled_deepslate"); + $reg->mapSlab(Blocks::COBBLESTONE_SLAB(), "cobblestone"); + $reg->mapSlab(Blocks::CUT_RED_SANDSTONE_SLAB(), "cut_red_sandstone"); + $reg->mapSlab(Blocks::CUT_SANDSTONE_SLAB(), "cut_sandstone"); + $reg->mapSlab(Blocks::DARK_PRISMARINE_SLAB(), "dark_prismarine"); + $reg->mapSlab(Blocks::DEEPSLATE_BRICK_SLAB(), "deepslate_brick"); + $reg->mapSlab(Blocks::DEEPSLATE_TILE_SLAB(), "deepslate_tile"); + $reg->mapSlab(Blocks::DIORITE_SLAB(), "diorite"); + $reg->mapSlab(Blocks::END_STONE_BRICK_SLAB(), "end_stone_brick"); + $reg->mapSlab(Blocks::FAKE_WOODEN_SLAB(), "petrified_oak"); + $reg->mapSlab(Blocks::GRANITE_SLAB(), "granite"); + $reg->mapSlab(Blocks::MOSSY_COBBLESTONE_SLAB(), "mossy_cobblestone"); + $reg->mapSlab(Blocks::MOSSY_STONE_BRICK_SLAB(), "mossy_stone_brick"); + $reg->mapSlab(Blocks::MUD_BRICK_SLAB(), "mud_brick"); + $reg->mapSlab(Blocks::NETHER_BRICK_SLAB(), "nether_brick"); + $reg->mapSlab(Blocks::POLISHED_ANDESITE_SLAB(), "polished_andesite"); + $reg->mapSlab(Blocks::POLISHED_BLACKSTONE_BRICK_SLAB(), "polished_blackstone_brick"); + $reg->mapSlab(Blocks::POLISHED_BLACKSTONE_SLAB(), "polished_blackstone"); + $reg->mapSlab(Blocks::POLISHED_DEEPSLATE_SLAB(), "polished_deepslate"); + $reg->mapSlab(Blocks::POLISHED_DIORITE_SLAB(), "polished_diorite"); + $reg->mapSlab(Blocks::POLISHED_GRANITE_SLAB(), "polished_granite"); + $reg->mapSlab(Blocks::POLISHED_TUFF_SLAB(), "polished_tuff"); + $reg->mapSlab(Blocks::PRISMARINE_BRICKS_SLAB(), "prismarine_brick"); + $reg->mapSlab(Blocks::PRISMARINE_SLAB(), "prismarine"); + $reg->mapSlab(Blocks::PURPUR_SLAB(), "purpur"); + $reg->mapSlab(Blocks::QUARTZ_SLAB(), "quartz"); + $reg->mapSlab(Blocks::RED_NETHER_BRICK_SLAB(), "red_nether_brick"); + $reg->mapSlab(Blocks::RED_SANDSTONE_SLAB(), "red_sandstone"); + $reg->mapSlab(Blocks::RESIN_BRICK_SLAB(), "resin_brick"); + $reg->mapSlab(Blocks::SANDSTONE_SLAB(), "sandstone"); + $reg->mapSlab(Blocks::SMOOTH_QUARTZ_SLAB(), "smooth_quartz"); + $reg->mapSlab(Blocks::SMOOTH_RED_SANDSTONE_SLAB(), "smooth_red_sandstone"); + $reg->mapSlab(Blocks::SMOOTH_SANDSTONE_SLAB(), "smooth_sandstone"); + $reg->mapSlab(Blocks::SMOOTH_STONE_SLAB(), "smooth_stone"); + $reg->mapSlab(Blocks::STONE_BRICK_SLAB(), "stone_brick"); + $reg->mapSlab(Blocks::STONE_SLAB(), "normal_stone"); + $reg->mapSlab(Blocks::TUFF_BRICK_SLAB(), "tuff_brick"); + $reg->mapSlab(Blocks::TUFF_SLAB(), "tuff"); + } + + private static function registerStoneLikeStairMappings(BlockSerializerDeserializerRegistrar $reg) : void{ + $reg->mapStairs(Blocks::ANDESITE_STAIRS(), Ids::ANDESITE_STAIRS); + $reg->mapStairs(Blocks::BLACKSTONE_STAIRS(), Ids::BLACKSTONE_STAIRS); + $reg->mapStairs(Blocks::BRICK_STAIRS(), Ids::BRICK_STAIRS); + $reg->mapStairs(Blocks::COBBLED_DEEPSLATE_STAIRS(), Ids::COBBLED_DEEPSLATE_STAIRS); + $reg->mapStairs(Blocks::COBBLESTONE_STAIRS(), Ids::STONE_STAIRS); + $reg->mapStairs(Blocks::DARK_PRISMARINE_STAIRS(), Ids::DARK_PRISMARINE_STAIRS); + $reg->mapStairs(Blocks::DEEPSLATE_BRICK_STAIRS(), Ids::DEEPSLATE_BRICK_STAIRS); + $reg->mapStairs(Blocks::DEEPSLATE_TILE_STAIRS(), Ids::DEEPSLATE_TILE_STAIRS); + $reg->mapStairs(Blocks::DIORITE_STAIRS(), Ids::DIORITE_STAIRS); + $reg->mapStairs(Blocks::END_STONE_BRICK_STAIRS(), Ids::END_BRICK_STAIRS); + $reg->mapStairs(Blocks::GRANITE_STAIRS(), Ids::GRANITE_STAIRS); + $reg->mapStairs(Blocks::MOSSY_COBBLESTONE_STAIRS(), Ids::MOSSY_COBBLESTONE_STAIRS); + $reg->mapStairs(Blocks::MOSSY_STONE_BRICK_STAIRS(), Ids::MOSSY_STONE_BRICK_STAIRS); + $reg->mapStairs(Blocks::MUD_BRICK_STAIRS(), Ids::MUD_BRICK_STAIRS); + $reg->mapStairs(Blocks::NETHER_BRICK_STAIRS(), Ids::NETHER_BRICK_STAIRS); + $reg->mapStairs(Blocks::POLISHED_ANDESITE_STAIRS(), Ids::POLISHED_ANDESITE_STAIRS); + $reg->mapStairs(Blocks::POLISHED_BLACKSTONE_BRICK_STAIRS(), Ids::POLISHED_BLACKSTONE_BRICK_STAIRS); + $reg->mapStairs(Blocks::POLISHED_BLACKSTONE_STAIRS(), Ids::POLISHED_BLACKSTONE_STAIRS); + $reg->mapStairs(Blocks::POLISHED_DEEPSLATE_STAIRS(), Ids::POLISHED_DEEPSLATE_STAIRS); + $reg->mapStairs(Blocks::POLISHED_DIORITE_STAIRS(), Ids::POLISHED_DIORITE_STAIRS); + $reg->mapStairs(Blocks::POLISHED_GRANITE_STAIRS(), Ids::POLISHED_GRANITE_STAIRS); + $reg->mapStairs(Blocks::POLISHED_TUFF_STAIRS(), Ids::POLISHED_TUFF_STAIRS); + $reg->mapStairs(Blocks::PRISMARINE_BRICKS_STAIRS(), Ids::PRISMARINE_BRICKS_STAIRS); + $reg->mapStairs(Blocks::PRISMARINE_STAIRS(), Ids::PRISMARINE_STAIRS); + $reg->mapStairs(Blocks::PURPUR_STAIRS(), Ids::PURPUR_STAIRS); + $reg->mapStairs(Blocks::QUARTZ_STAIRS(), Ids::QUARTZ_STAIRS); + $reg->mapStairs(Blocks::RED_NETHER_BRICK_STAIRS(), Ids::RED_NETHER_BRICK_STAIRS); + $reg->mapStairs(Blocks::RED_SANDSTONE_STAIRS(), Ids::RED_SANDSTONE_STAIRS); + $reg->mapStairs(Blocks::RESIN_BRICK_STAIRS(), Ids::RESIN_BRICK_STAIRS); + $reg->mapStairs(Blocks::SANDSTONE_STAIRS(), Ids::SANDSTONE_STAIRS); + $reg->mapStairs(Blocks::SMOOTH_QUARTZ_STAIRS(), Ids::SMOOTH_QUARTZ_STAIRS); + $reg->mapStairs(Blocks::SMOOTH_RED_SANDSTONE_STAIRS(), Ids::SMOOTH_RED_SANDSTONE_STAIRS); + $reg->mapStairs(Blocks::SMOOTH_SANDSTONE_STAIRS(), Ids::SMOOTH_SANDSTONE_STAIRS); + $reg->mapStairs(Blocks::STONE_BRICK_STAIRS(), Ids::STONE_BRICK_STAIRS); + $reg->mapStairs(Blocks::STONE_STAIRS(), Ids::NORMAL_STONE_STAIRS); + $reg->mapStairs(Blocks::TUFF_BRICK_STAIRS(), Ids::TUFF_BRICK_STAIRS); + $reg->mapStairs(Blocks::TUFF_STAIRS(), Ids::TUFF_STAIRS); + } + + private static function registerStoneLikeWallMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + foreach([ + Ids::ANDESITE_WALL => Blocks::ANDESITE_WALL(), + Ids::BLACKSTONE_WALL => Blocks::BLACKSTONE_WALL(), + Ids::BRICK_WALL => Blocks::BRICK_WALL(), + Ids::COBBLED_DEEPSLATE_WALL => Blocks::COBBLED_DEEPSLATE_WALL(), + Ids::COBBLESTONE_WALL => Blocks::COBBLESTONE_WALL(), + Ids::DEEPSLATE_BRICK_WALL => Blocks::DEEPSLATE_BRICK_WALL(), + Ids::DEEPSLATE_TILE_WALL => Blocks::DEEPSLATE_TILE_WALL(), + Ids::DIORITE_WALL => Blocks::DIORITE_WALL(), + Ids::END_STONE_BRICK_WALL => Blocks::END_STONE_BRICK_WALL(), + Ids::GRANITE_WALL => Blocks::GRANITE_WALL(), + Ids::MOSSY_COBBLESTONE_WALL => Blocks::MOSSY_COBBLESTONE_WALL(), + Ids::MOSSY_STONE_BRICK_WALL => Blocks::MOSSY_STONE_BRICK_WALL(), + Ids::MUD_BRICK_WALL => Blocks::MUD_BRICK_WALL(), + Ids::NETHER_BRICK_WALL => Blocks::NETHER_BRICK_WALL(), + Ids::POLISHED_BLACKSTONE_BRICK_WALL => Blocks::POLISHED_BLACKSTONE_BRICK_WALL(), + Ids::POLISHED_BLACKSTONE_WALL => Blocks::POLISHED_BLACKSTONE_WALL(), + Ids::POLISHED_DEEPSLATE_WALL => Blocks::POLISHED_DEEPSLATE_WALL(), + Ids::POLISHED_TUFF_WALL => Blocks::POLISHED_TUFF_WALL(), + Ids::PRISMARINE_WALL => Blocks::PRISMARINE_WALL(), + Ids::RED_NETHER_BRICK_WALL => Blocks::RED_NETHER_BRICK_WALL(), + Ids::RED_SANDSTONE_WALL => Blocks::RED_SANDSTONE_WALL(), + Ids::RESIN_BRICK_WALL => Blocks::RESIN_BRICK_WALL(), + Ids::SANDSTONE_WALL => Blocks::SANDSTONE_WALL(), + Ids::STONE_BRICK_WALL => Blocks::STONE_BRICK_WALL(), + Ids::TUFF_BRICK_WALL => Blocks::TUFF_BRICK_WALL(), + Ids::TUFF_WALL => Blocks::TUFF_WALL() + ] as $id => $block){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->wallProperties)); + } + } + + private static function registerWoodMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + //buttons + foreach([ + [Blocks::ACACIA_BUTTON(), Ids::ACACIA_BUTTON], + [Blocks::BIRCH_BUTTON(), Ids::BIRCH_BUTTON], + [Blocks::CHERRY_BUTTON(), Ids::CHERRY_BUTTON], + [Blocks::CRIMSON_BUTTON(), Ids::CRIMSON_BUTTON], + [Blocks::DARK_OAK_BUTTON(), Ids::DARK_OAK_BUTTON], + [Blocks::JUNGLE_BUTTON(), Ids::JUNGLE_BUTTON], + [Blocks::MANGROVE_BUTTON(), Ids::MANGROVE_BUTTON], + [Blocks::OAK_BUTTON(), Ids::WOODEN_BUTTON], + [Blocks::PALE_OAK_BUTTON(), Ids::PALE_OAK_BUTTON], + [Blocks::SPRUCE_BUTTON(), Ids::SPRUCE_BUTTON], + [Blocks::WARPED_BUTTON(), Ids::WARPED_BUTTON] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->buttonProperties)); + } + + //doors + foreach([ + [Blocks::ACACIA_DOOR(), Ids::ACACIA_DOOR], + [Blocks::BIRCH_DOOR(), Ids::BIRCH_DOOR], + [Blocks::CHERRY_DOOR(), Ids::CHERRY_DOOR], + [Blocks::CRIMSON_DOOR(), Ids::CRIMSON_DOOR], + [Blocks::DARK_OAK_DOOR(), Ids::DARK_OAK_DOOR], + [Blocks::JUNGLE_DOOR(), Ids::JUNGLE_DOOR], + [Blocks::MANGROVE_DOOR(), Ids::MANGROVE_DOOR], + [Blocks::OAK_DOOR(), Ids::WOODEN_DOOR], + [Blocks::PALE_OAK_DOOR(), Ids::PALE_OAK_DOOR], + [Blocks::SPRUCE_DOOR(), Ids::SPRUCE_DOOR], + [Blocks::WARPED_DOOR(), Ids::WARPED_DOOR] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->doorProperties)); + } + + //fences + foreach([ + [Blocks::ACACIA_FENCE(), Ids::ACACIA_FENCE], + [Blocks::BIRCH_FENCE(), Ids::BIRCH_FENCE], + [Blocks::CHERRY_FENCE(), Ids::CHERRY_FENCE], + [Blocks::DARK_OAK_FENCE(), Ids::DARK_OAK_FENCE], + [Blocks::JUNGLE_FENCE(), Ids::JUNGLE_FENCE], + [Blocks::MANGROVE_FENCE(), Ids::MANGROVE_FENCE], + [Blocks::OAK_FENCE(), Ids::OAK_FENCE], + [Blocks::PALE_OAK_FENCE(), Ids::PALE_OAK_FENCE], + [Blocks::SPRUCE_FENCE(), Ids::SPRUCE_FENCE], + [Blocks::CRIMSON_FENCE(), Ids::CRIMSON_FENCE], + [Blocks::WARPED_FENCE(), Ids::WARPED_FENCE] + ] as [$block, $id]){ + $reg->mapSimple($block, $id); + } + + foreach([ + [Blocks::ACACIA_FENCE_GATE(), Ids::ACACIA_FENCE_GATE], + [Blocks::BIRCH_FENCE_GATE(), Ids::BIRCH_FENCE_GATE], + [Blocks::CHERRY_FENCE_GATE(), Ids::CHERRY_FENCE_GATE], + [Blocks::DARK_OAK_FENCE_GATE(), Ids::DARK_OAK_FENCE_GATE], + [Blocks::JUNGLE_FENCE_GATE(), Ids::JUNGLE_FENCE_GATE], + [Blocks::MANGROVE_FENCE_GATE(), Ids::MANGROVE_FENCE_GATE], + [Blocks::OAK_FENCE_GATE(), Ids::FENCE_GATE], + [Blocks::PALE_OAK_FENCE_GATE(), Ids::PALE_OAK_FENCE_GATE], + [Blocks::SPRUCE_FENCE_GATE(), Ids::SPRUCE_FENCE_GATE], + [Blocks::CRIMSON_FENCE_GATE(), Ids::CRIMSON_FENCE_GATE], + [Blocks::WARPED_FENCE_GATE(), Ids::WARPED_FENCE_GATE] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->fenceGateProperties)); + } + + foreach([ + [Blocks::ACACIA_SIGN(), Ids::ACACIA_STANDING_SIGN], + [Blocks::BIRCH_SIGN(), Ids::BIRCH_STANDING_SIGN], + [Blocks::CHERRY_SIGN(), Ids::CHERRY_STANDING_SIGN], + [Blocks::DARK_OAK_SIGN(), Ids::DARKOAK_STANDING_SIGN], + [Blocks::JUNGLE_SIGN(), Ids::JUNGLE_STANDING_SIGN], + [Blocks::MANGROVE_SIGN(), Ids::MANGROVE_STANDING_SIGN], + [Blocks::OAK_SIGN(), Ids::STANDING_SIGN], + [Blocks::PALE_OAK_SIGN(), Ids::PALE_OAK_STANDING_SIGN], + [Blocks::SPRUCE_SIGN(), Ids::SPRUCE_STANDING_SIGN], + [Blocks::CRIMSON_SIGN(), Ids::CRIMSON_STANDING_SIGN], + [Blocks::WARPED_SIGN(), Ids::WARPED_STANDING_SIGN] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([$commonProperties->floorSignLikeRotation])); + } + + //logs + foreach([ + [Blocks::ACACIA_LOG(), "acacia_log"], + [Blocks::BIRCH_LOG(), "birch_log"], + [Blocks::CHERRY_LOG(), "cherry_log"], + [Blocks::DARK_OAK_LOG(), "dark_oak_log"], + [Blocks::JUNGLE_LOG(), "jungle_log"], + [Blocks::MANGROVE_LOG(), "mangrove_log"], + [Blocks::OAK_LOG(), "oak_log"], + [Blocks::PALE_OAK_LOG(), "pale_oak_log"], + [Blocks::SPRUCE_LOG(), "spruce_log"], + [Blocks::CRIMSON_STEM(), "crimson_stem"], + [Blocks::WARPED_STEM(), "warped_stem"], + + //all-sided logs + [Blocks::ACACIA_WOOD(), "acacia_wood"], + [Blocks::BIRCH_WOOD(), "birch_wood"], + [Blocks::CHERRY_WOOD(), "cherry_wood"], + [Blocks::DARK_OAK_WOOD(), "dark_oak_wood"], + [Blocks::JUNGLE_WOOD(), "jungle_wood"], + [Blocks::MANGROVE_WOOD(), "mangrove_wood"], + [Blocks::OAK_WOOD(), "oak_wood"], + [Blocks::PALE_OAK_WOOD(), "pale_oak_wood"], + [Blocks::SPRUCE_WOOD(), "spruce_wood"], + [Blocks::CRIMSON_HYPHAE(), "crimson_hyphae"], + [Blocks::WARPED_HYPHAE(), "warped_hyphae"] + ] as [$block, $idSuffix]){ + $reg->mapFlattenedId(FlattenedIdModel::create($block) + ->idComponents([...$commonProperties->woodIdPrefixes, $idSuffix]) + ->properties([$commonProperties->pillarAxis]) + ); + } + + //planks + foreach([ + [Blocks::ACACIA_PLANKS(), Ids::ACACIA_PLANKS], + [Blocks::BIRCH_PLANKS(), Ids::BIRCH_PLANKS], + [Blocks::CHERRY_PLANKS(), Ids::CHERRY_PLANKS], + [Blocks::DARK_OAK_PLANKS(), Ids::DARK_OAK_PLANKS], + [Blocks::JUNGLE_PLANKS(), Ids::JUNGLE_PLANKS], + [Blocks::MANGROVE_PLANKS(), Ids::MANGROVE_PLANKS], + [Blocks::OAK_PLANKS(), Ids::OAK_PLANKS], + [Blocks::PALE_OAK_PLANKS(), Ids::PALE_OAK_PLANKS], + [Blocks::SPRUCE_PLANKS(), Ids::SPRUCE_PLANKS], + [Blocks::CRIMSON_PLANKS(), Ids::CRIMSON_PLANKS], + [Blocks::WARPED_PLANKS(), Ids::WARPED_PLANKS] + ] as [$block, $id]){ + $reg->mapSimple($block, $id); + } + + //pressure plates + foreach([ + [Blocks::ACACIA_PRESSURE_PLATE(), Ids::ACACIA_PRESSURE_PLATE], + [Blocks::BIRCH_PRESSURE_PLATE(), Ids::BIRCH_PRESSURE_PLATE], + [Blocks::CHERRY_PRESSURE_PLATE(), Ids::CHERRY_PRESSURE_PLATE], + [Blocks::DARK_OAK_PRESSURE_PLATE(), Ids::DARK_OAK_PRESSURE_PLATE], + [Blocks::JUNGLE_PRESSURE_PLATE(), Ids::JUNGLE_PRESSURE_PLATE], + [Blocks::MANGROVE_PRESSURE_PLATE(), Ids::MANGROVE_PRESSURE_PLATE], + [Blocks::OAK_PRESSURE_PLATE(), Ids::WOODEN_PRESSURE_PLATE], + [Blocks::PALE_OAK_PRESSURE_PLATE(), Ids::PALE_OAK_PRESSURE_PLATE], + [Blocks::SPRUCE_PRESSURE_PLATE(), Ids::SPRUCE_PRESSURE_PLATE], + [Blocks::CRIMSON_PRESSURE_PLATE(), Ids::CRIMSON_PRESSURE_PLATE], + [Blocks::WARPED_PRESSURE_PLATE(), Ids::WARPED_PRESSURE_PLATE] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->simplePressurePlateProperties)); + } + + //slabs + foreach([ + [Blocks::ACACIA_SLAB(), "acacia"], + [Blocks::BIRCH_SLAB(), "birch"], + [Blocks::CHERRY_SLAB(), "cherry"], + [Blocks::DARK_OAK_SLAB(), "dark_oak"], + [Blocks::JUNGLE_SLAB(), "jungle"], + [Blocks::MANGROVE_SLAB(), "mangrove"], + [Blocks::OAK_SLAB(), "oak"], + [Blocks::PALE_OAK_SLAB(), "pale_oak"], + [Blocks::SPRUCE_SLAB(), "spruce"], + [Blocks::CRIMSON_SLAB(), "crimson"], + [Blocks::WARPED_SLAB(), "warped"] + ] as [$block, $type]){ + $reg->mapSlab($block, $type); + } + + //stairs + foreach([ + [Blocks::ACACIA_STAIRS(), Ids::ACACIA_STAIRS], + [Blocks::BIRCH_STAIRS(), Ids::BIRCH_STAIRS], + [Blocks::CHERRY_STAIRS(), Ids::CHERRY_STAIRS], + [Blocks::DARK_OAK_STAIRS(), Ids::DARK_OAK_STAIRS], + [Blocks::JUNGLE_STAIRS(), Ids::JUNGLE_STAIRS], + [Blocks::MANGROVE_STAIRS(), Ids::MANGROVE_STAIRS], + [Blocks::OAK_STAIRS(), Ids::OAK_STAIRS], + [Blocks::PALE_OAK_STAIRS(), Ids::PALE_OAK_STAIRS], + [Blocks::SPRUCE_STAIRS(), Ids::SPRUCE_STAIRS], + [Blocks::CRIMSON_STAIRS(), Ids::CRIMSON_STAIRS], + [Blocks::WARPED_STAIRS(), Ids::WARPED_STAIRS] + ] as [$block, $id]){ + $reg->mapStairs($block, $id); + } + + //trapdoors + foreach([ + [Blocks::ACACIA_TRAPDOOR(), Ids::ACACIA_TRAPDOOR], + [Blocks::BIRCH_TRAPDOOR(), Ids::BIRCH_TRAPDOOR], + [Blocks::CHERRY_TRAPDOOR(), Ids::CHERRY_TRAPDOOR], + [Blocks::DARK_OAK_TRAPDOOR(), Ids::DARK_OAK_TRAPDOOR], + [Blocks::JUNGLE_TRAPDOOR(), Ids::JUNGLE_TRAPDOOR], + [Blocks::MANGROVE_TRAPDOOR(), Ids::MANGROVE_TRAPDOOR], + [Blocks::OAK_TRAPDOOR(), Ids::TRAPDOOR], + [Blocks::PALE_OAK_TRAPDOOR(), Ids::PALE_OAK_TRAPDOOR], + [Blocks::SPRUCE_TRAPDOOR(), Ids::SPRUCE_TRAPDOOR], + [Blocks::CRIMSON_TRAPDOOR(), Ids::CRIMSON_TRAPDOOR], + [Blocks::WARPED_TRAPDOOR(), Ids::WARPED_TRAPDOOR] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties($commonProperties->trapdoorProperties)); + } + + //wall signs + foreach([ + [Blocks::ACACIA_WALL_SIGN(), Ids::ACACIA_WALL_SIGN], + [Blocks::BIRCH_WALL_SIGN(), Ids::BIRCH_WALL_SIGN], + [Blocks::CHERRY_WALL_SIGN(), Ids::CHERRY_WALL_SIGN], + [Blocks::DARK_OAK_WALL_SIGN(), Ids::DARKOAK_WALL_SIGN], + [Blocks::JUNGLE_WALL_SIGN(), Ids::JUNGLE_WALL_SIGN], + [Blocks::MANGROVE_WALL_SIGN(), Ids::MANGROVE_WALL_SIGN], + [Blocks::OAK_WALL_SIGN(), Ids::WALL_SIGN], + [Blocks::PALE_OAK_WALL_SIGN(), Ids::PALE_OAK_WALL_SIGN], + [Blocks::SPRUCE_WALL_SIGN(), Ids::SPRUCE_WALL_SIGN], + [Blocks::CRIMSON_WALL_SIGN(), Ids::CRIMSON_WALL_SIGN], + [Blocks::WARPED_WALL_SIGN(), Ids::WARPED_WALL_SIGN] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([$commonProperties->horizontalFacingClassic])); + } + } + + private static function registerTorchMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + foreach([ + [Blocks::BLUE_TORCH(), Ids::COLORED_TORCH_BLUE], + [Blocks::GREEN_TORCH(), Ids::COLORED_TORCH_GREEN], + [Blocks::PURPLE_TORCH(), Ids::COLORED_TORCH_PURPLE], + [Blocks::RED_TORCH(), Ids::COLORED_TORCH_RED], + [Blocks::SOUL_TORCH(), Ids::SOUL_TORCH], + [Blocks::TORCH(), Ids::TORCH], + [Blocks::UNDERWATER_TORCH(), Ids::UNDERWATER_TORCH] + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([$commonProperties->torchFacing])); + } + } + + private static function registerChemistryMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + foreach([ + [Blocks::COMPOUND_CREATOR(), Ids::COMPOUND_CREATOR], + [Blocks::ELEMENT_CONSTRUCTOR(), Ids::ELEMENT_CONSTRUCTOR], + [Blocks::LAB_TABLE(), Ids::LAB_TABLE], + [Blocks::MATERIAL_REDUCER(), Ids::MATERIAL_REDUCER], + ] as [$block, $id]){ + $reg->mapModel(Model::create($block, $id)->properties([$commonProperties->horizontalFacingSWNEInverted])); + } + } + + private static function register1to1CustomMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + //TODO: some of these have repeated accessor refs, we might be able to deduplicate them + //A + $reg->mapModel(Model::create(Blocks::ACTIVATOR_RAIL(), Ids::ACTIVATOR_RAIL)->properties([ + new BoolProperty(StateNames::RAIL_DATA_BIT, fn(ActivatorRail $b) => $b->isPowered(), fn(ActivatorRail $b, bool $v) => $b->setPowered($v)), + new IntProperty(StateNames::RAIL_DIRECTION, 0, 5, fn(ActivatorRail $b) => $b->getShape(), fn(ActivatorRail $b, int $v) => $b->setShape($v)) + ])); + + //B + $reg->mapModel(Model::create(Blocks::BAMBOO(), Ids::BAMBOO)->properties([ + new ValueFromStringProperty(StateNames::BAMBOO_LEAF_SIZE, ValueMappings::getInstance()->bambooLeafSize, fn(Bamboo $b) => $b->getLeafSize(), fn(Bamboo $b, int $v) => $b->setLeafSize($v)), + new BoolProperty(StateNames::AGE_BIT, fn(Bamboo $b) => $b->isReady(), fn(Bamboo $b, bool $v) => $b->setReady($v)), + new BoolFromStringProperty(StateNames::BAMBOO_STALK_THICKNESS, StringValues::BAMBOO_STALK_THICKNESS_THIN, StringValues::BAMBOO_STALK_THICKNESS_THICK, fn(Bamboo $b) => $b->isThick(), fn(Bamboo $b, bool $v) => $b->setThick($v)) + ])); + $reg->mapModel(Model::create(Blocks::BAMBOO_SAPLING(), Ids::BAMBOO_SAPLING)->properties([ + new BoolProperty(StateNames::AGE_BIT, fn(BambooSapling $b) => $b->isReady(), fn(BambooSapling $b, bool $v) => $b->setReady($v)) + ])); + $reg->mapModel(Model::create(Blocks::BANNER(), Ids::STANDING_BANNER)->properties([$commonProperties->floorSignLikeRotation])); + $reg->mapModel(Model::create(Blocks::BARREL(), Ids::BARREL)->properties([ + $commonProperties->anyFacingClassic, + new BoolProperty(StateNames::OPEN_BIT, fn(Barrel $b) => $b->isOpen(), fn(Barrel $b, bool $v) => $b->setOpen($v)) + ])); + $reg->mapModel(Model::create(Blocks::BASALT(), Ids::BASALT)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::BED(), Ids::BED)->properties([ + new BoolProperty(StateNames::HEAD_PIECE_BIT, fn(Bed $b) => $b->isHeadPart(), fn(Bed $b, bool $v) => $b->setHead($v)), + new BoolProperty(StateNames::OCCUPIED_BIT, fn(Bed $b) => $b->isOccupied(), fn(Bed $b, bool $v) => $b->setOccupied($v)), + $commonProperties->horizontalFacingSWNE + ])); + $reg->mapModel(Model::create(Blocks::BEDROCK(), Ids::BEDROCK)->properties([ + new BoolProperty(StateNames::INFINIBURN_BIT, fn(Bedrock $b) => $b->burnsForever(), fn(Bedrock $b, bool $v) => $b->setBurnsForever($v)) + ])); + $reg->mapModel(Model::create(Blocks::BELL(), Ids::BELL)->properties([ + BoolProperty::unused(StateNames::TOGGLE_BIT, false), + new ValueFromStringProperty(StateNames::ATTACHMENT, ValueMappings::getInstance()->bellAttachmentType, fn(Bell $b) => $b->getAttachmentType(), fn(Bell $b, BellAttachmentType $v) => $b->setAttachmentType($v)), + $commonProperties->horizontalFacingSWNE + ])); + $reg->mapModel(Model::create(Blocks::BONE_BLOCK(), Ids::BONE_BLOCK)->properties([ + IntProperty::unused(StateNames::DEPRECATED, 0), + $commonProperties->pillarAxis + ])); + + $reg->mapModel(Model::create(Blocks::BREWING_STAND(), Ids::BREWING_STAND)->properties(array_map(fn(BrewingStandSlot $slot) => new BoolProperty(match ($slot) { + BrewingStandSlot::EAST => StateNames::BREWING_STAND_SLOT_A_BIT, + BrewingStandSlot::SOUTHWEST => StateNames::BREWING_STAND_SLOT_B_BIT, + BrewingStandSlot::NORTHWEST => StateNames::BREWING_STAND_SLOT_C_BIT + }, fn(BrewingStand $b) => $b->hasSlot($slot), fn(BrewingStand $b, bool $v) => $b->setSlot($slot, $v)), BrewingStandSlot::cases()))); + + //C + $reg->mapModel(Model::create(Blocks::CACTUS(), Ids::CACTUS)->properties([ + new IntProperty(StateNames::AGE, 0, 15, fn(Cactus $b) => $b->getAge(), fn(Cactus $b, int $v) => $b->setAge($v)) + ])); + $reg->mapModel(Model::create(Blocks::CAKE(), Ids::CAKE)->properties([ + new IntProperty(StateNames::BITE_COUNTER, 0, 6, fn(Cake $b) => $b->getBites(), fn(Cake $b, int $v) => $b->setBites($v)) + ])); + $reg->mapModel(Model::create(Blocks::CAMPFIRE(), Ids::CAMPFIRE)->properties($commonProperties->campfireProperties)); + $reg->mapModel(Model::create(Blocks::CARVED_PUMPKIN(), Ids::CARVED_PUMPKIN)->properties([ + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::CHAIN(), Ids::CHAIN)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::CHISELED_BOOKSHELF(), Ids::CHISELED_BOOKSHELF)->properties([ + $commonProperties->horizontalFacingSWNE, + new ValueSetFromIntProperty( + StateNames::BOOKS_STORED, + EnumFromRawStateMap::int(ChiseledBookshelfSlot::class, fn(ChiseledBookshelfSlot $case) => match($case){ + //these are (currently) the same as the internal values, but it's best not to rely on those in case Mojang mess with the flags + ChiseledBookshelfSlot::TOP_LEFT => 1 << 0, + ChiseledBookshelfSlot::TOP_MIDDLE => 1 << 1, + ChiseledBookshelfSlot::TOP_RIGHT => 1 << 2, + ChiseledBookshelfSlot::BOTTOM_LEFT => 1 << 3, + ChiseledBookshelfSlot::BOTTOM_MIDDLE => 1 << 4, + ChiseledBookshelfSlot::BOTTOM_RIGHT => 1 << 5 + }), + fn(ChiseledBookshelf $b) => $b->getSlots(), + fn(ChiseledBookshelf $b, array $v) => $b->setSlots($v) + ) + ])); + $reg->mapModel(Model::create(Blocks::CHISELED_QUARTZ(), Ids::CHISELED_QUARTZ_BLOCK)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::CHEST(), Ids::CHEST)->properties([$commonProperties->horizontalFacingCardinal])); + $reg->mapModel(Model::create(Blocks::CHORUS_FLOWER(), Ids::CHORUS_FLOWER)->properties([ + new IntProperty(StateNames::AGE, ChorusFlower::MIN_AGE, ChorusFlower::MAX_AGE, fn(ChorusFlower $b) => $b->getAge(), fn(ChorusFlower $b, int $v) => $b->setAge($v)) + ])); + $reg->mapModel(Model::create(Blocks::COCOA_POD(), Ids::COCOA)->properties([ + new IntProperty(StateNames::AGE, 0, 2, fn(CocoaBlock $b) => $b->getAge(), fn(CocoaBlock $b, int $v) => $b->setAge($v)), + $commonProperties->horizontalFacingSWNEInverted + ])); + + //D + $reg->mapModel(Model::create(Blocks::DEEPSLATE(), Ids::DEEPSLATE)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::DETECTOR_RAIL(), Ids::DETECTOR_RAIL)->properties([ + new BoolProperty(StateNames::RAIL_DATA_BIT, fn(DetectorRail $b) => $b->isActivated(), fn(DetectorRail $b, bool $v) => $b->setActivated($v)), + new IntProperty(StateNames::RAIL_DIRECTION, 0, 5, fn(StraightOnlyRail $b) => $b->getShape(), fn(StraightOnlyRail $b, int $v) => $b->setShape($v)) //TODO: shared with ActivatorRail + ])); + + //E + $reg->mapModel(Model::create(Blocks::ENDER_CHEST(), Ids::ENDER_CHEST)->properties([$commonProperties->horizontalFacingCardinal])); + $reg->mapModel(Model::create(Blocks::END_PORTAL_FRAME(), Ids::END_PORTAL_FRAME)->properties([ + new BoolProperty(StateNames::END_PORTAL_EYE_BIT, fn(EndPortalFrame $b) => $b->hasEye(), fn(EndPortalFrame $b, bool $v) => $b->setEye($v)), + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::END_ROD(), Ids::END_ROD)->properties([ + new ValueFromIntProperty(StateNames::FACING_DIRECTION, ValueMappings::getInstance()->facingEndRod, fn(EndRod $b) => $b->getFacing(), fn(EndRod $b, int $v) => $b->setFacing($v)), + ])); + + //F + $reg->mapModel(Model::create(Blocks::FARMLAND(), Ids::FARMLAND)->properties([ + new IntProperty(StateNames::MOISTURIZED_AMOUNT, 0, 7, fn(Farmland $b) => $b->getWetness(), fn(Farmland $b, int $v) => $b->setWetness($v)) + ])); + $reg->mapModel(Model::create(Blocks::FIRE(), Ids::FIRE)->properties([ + new IntProperty(StateNames::AGE, 0, 15, fn(Fire $b) => $b->getAge(), fn(Fire $b, int $v) => $b->setAge($v)) + ])); + $reg->mapModel(Model::create(Blocks::FLOWER_POT(), Ids::FLOWER_POT)->properties([ + BoolProperty::unused(StateNames::UPDATE_BIT, false) + ])); + $reg->mapModel(Model::create(Blocks::FROSTED_ICE(), Ids::FROSTED_ICE)->properties([ + new IntProperty(StateNames::AGE, 0, 3, fn(FrostedIce $b) => $b->getAge(), fn(FrostedIce $b, int $v) => $b->setAge($v)) + ])); + + //G + $reg->mapModel(Model::create(Blocks::GLOWING_ITEM_FRAME(), Ids::GLOW_FRAME)->properties($commonProperties->itemFrameProperties)); + + //H + $reg->mapModel(Model::create(Blocks::HAY_BALE(), Ids::HAY_BLOCK)->properties([ + IntProperty::unused(StateNames::DEPRECATED, 0), + $commonProperties->pillarAxis + ])); + $reg->mapModel(Model::create(Blocks::HOPPER(), Ids::HOPPER)->properties([ + //kinda weird this doesn't use powered_bit? + new BoolProperty(StateNames::TOGGLE_BIT, fn(PoweredByRedstone $b) => $b->isPowered(), fn(PoweredByRedstone $b, bool $v) => $b->setPowered($v)), + new ValueFromIntProperty(StateNames::FACING_DIRECTION, ValueMappings::getInstance()->facingExceptUp, fn(Hopper $b) => $b->getFacing(), fn(Hopper $b, int $v) => $b->setFacing($v)), + ])); + + //I + $reg->mapModel(Model::create(Blocks::IRON_DOOR(), Ids::IRON_DOOR)->properties($commonProperties->doorProperties)); + $reg->mapModel(Model::create(Blocks::IRON_TRAPDOOR(), Ids::IRON_TRAPDOOR)->properties($commonProperties->trapdoorProperties)); + $reg->mapModel(Model::create(Blocks::ITEM_FRAME(), Ids::FRAME)->properties($commonProperties->itemFrameProperties)); + + //L + $reg->mapModel(Model::create(Blocks::LADDER(), Ids::LADDER)->properties([$commonProperties->horizontalFacingClassic])); + $reg->mapModel(Model::create(Blocks::LANTERN(), Ids::LANTERN)->properties([ + new BoolProperty(StateNames::HANGING, fn(Lantern $b) => $b->isHanging(), fn(Lantern $b, bool $v) => $b->setHanging($v)) + ])); + $reg->mapModel(Model::create(Blocks::LECTERN(), Ids::LECTERN)->properties([ + new BoolProperty(StateNames::POWERED_BIT, fn(Lectern $b) => $b->isProducingSignal(), fn(Lectern $b, bool $v) => $b->setProducingSignal($v)), + $commonProperties->horizontalFacingCardinal, + ])); + $reg->mapModel(Model::create(Blocks::LEVER(), Ids::LEVER)->properties([ + new ValueFromStringProperty(StateNames::LEVER_DIRECTION, ValueMappings::getInstance()->leverFacing, fn(Lever $b) => $b->getFacing(), fn(Lever $b, LeverFacing $v) => $b->setFacing($v)), + new BoolProperty(StateNames::OPEN_BIT, fn(Lever $b) => $b->isActivated(), fn(Lever $b, bool $v) => $b->setActivated($v)), + ])); + $reg->mapModel(Model::create(Blocks::LIGHTNING_ROD(), Ids::LIGHTNING_ROD)->properties([$commonProperties->anyFacingClassic])); + $reg->mapModel(Model::create(Blocks::LIT_PUMPKIN(), Ids::LIT_PUMPKIN)->properties([$commonProperties->horizontalFacingCardinal])); + $reg->mapModel(Model::create(Blocks::LOOM(), Ids::LOOM)->properties([$commonProperties->horizontalFacingSWNE])); + + //M + $reg->mapModel(Model::create(Blocks::MUDDY_MANGROVE_ROOTS(), Ids::MUDDY_MANGROVE_ROOTS)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::NETHER_WART(), Ids::NETHER_WART)->properties([ + new IntProperty(StateNames::AGE, 0, 3, fn(NetherWartPlant $b) => $b->getAge(), fn(NetherWartPlant $b, int $v) => $b->setAge($v)) + ])); + $reg->mapModel(Model::create(Blocks::NETHER_PORTAL(), Ids::PORTAL)->properties([ + new ValueFromStringProperty(StateNames::PORTAL_AXIS, ValueMappings::getInstance()->portalAxis, fn(NetherPortal $b) => $b->getAxis(), fn(NetherPortal $b, int $v) => $b->setAxis($v)) + ])); + + //P + $reg->mapModel(Model::create(Blocks::PINK_PETALS(), Ids::PINK_PETALS)->properties([ + //Pink petals only uses 0-3, but GROWTH state can go up to 7 + new IntProperty(StateNames::GROWTH, 0, 7, fn(PinkPetals $b) => $b->getCount(), fn(PinkPetals $b, int $v) => $b->setCount(min($v, PinkPetals::MAX_COUNT)), offset: 1), + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::POWERED_RAIL(), Ids::GOLDEN_RAIL)->properties([ + new BoolProperty(StateNames::RAIL_DATA_BIT, fn(PoweredRail $b) => $b->isPowered(), fn(PoweredRail $b, bool $v) => $b->setPowered($v)), //TODO: shared with ActivatorRail + new IntProperty(StateNames::RAIL_DIRECTION, 0, 5, fn(StraightOnlyRail $b) => $b->getShape(), fn(StraightOnlyRail $b, int $v) => $b->setShape($v)) //TODO: shared with ActivatorRail + ])); + $reg->mapModel(Model::create(Blocks::PITCHER_PLANT(), Ids::PITCHER_PLANT)->properties([ + new BoolProperty(StateNames::UPPER_BLOCK_BIT, fn(DoublePlant $b) => $b->isTop(), fn(DoublePlant $b, bool $v) => $b->setTop($v)), //TODO: don't we have helpers for this? + ])); + $reg->mapModel(Model::create(Blocks::POLISHED_BASALT(), Ids::POLISHED_BASALT)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::POLISHED_BLACKSTONE_BUTTON(), Ids::POLISHED_BLACKSTONE_BUTTON)->properties($commonProperties->buttonProperties)); + $reg->mapModel(Model::create(Blocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE)->properties($commonProperties->simplePressurePlateProperties)); + $reg->mapModel(Model::create(Blocks::PUMPKIN(), Ids::PUMPKIN)->properties([ + //not used, has no visible effect + $commonProperties->dummyCardinalDirection + ])); + $reg->mapModel(Model::create(Blocks::PURPUR(), Ids::PURPUR_BLOCK)->properties([ + $commonProperties->dummyPillarAxis + ])); + $reg->mapModel(Model::create(Blocks::PURPUR_PILLAR(), Ids::PURPUR_PILLAR)->properties([$commonProperties->pillarAxis])); + + //Q + $reg->mapModel(Model::create(Blocks::QUARTZ(), Ids::QUARTZ_BLOCK)->properties([ + $commonProperties->dummyPillarAxis + ])); + $reg->mapModel(Model::create(Blocks::QUARTZ_PILLAR(), Ids::QUARTZ_PILLAR)->properties([$commonProperties->pillarAxis])); + + //R + $reg->mapModel(Model::create(Blocks::RAIL(), Ids::RAIL)->properties([ + new IntProperty(StateNames::RAIL_DIRECTION, 0, 9, fn(Rail $b) => $b->getShape(), fn(Rail $b, int $v) => $b->setShape($v)) + ])); + $reg->mapModel(Model::create(Blocks::REDSTONE_WIRE(), Ids::REDSTONE_WIRE)->properties([$commonProperties->analogRedstoneSignal])); + $reg->mapModel(Model::create(Blocks::RESPAWN_ANCHOR(), Ids::RESPAWN_ANCHOR)->properties([ + new IntProperty(StateNames::RESPAWN_ANCHOR_CHARGE, 0, 4, fn(RespawnAnchor $b) => $b->getCharges(), fn(RespawnAnchor $b, int $v) => $b->setCharges($v)) + ])); + + //S + $reg->mapModel(Model::create(Blocks::SEA_PICKLE(), Ids::SEA_PICKLE)->properties([ + new IntProperty(StateNames::CLUSTER_COUNT, 0, 3, fn(SeaPickle $b) => $b->getCount(), fn(SeaPickle $b, int $v) => $b->setCount($v), offset: 1), + new BoolProperty(StateNames::DEAD_BIT, fn(SeaPickle $b) => $b->isUnderwater(), fn(SeaPickle $b, bool $v) => $b->setUnderwater($v), inverted: true) + ])); + $reg->mapModel(Model::create(Blocks::SMALL_DRIPLEAF(), Ids::SMALL_DRIPLEAF_BLOCK)->properties([ + new BoolProperty(StateNames::UPPER_BLOCK_BIT, fn(SmallDripleaf $b) => $b->isTop(), fn(SmallDripleaf $b, bool $v) => $b->setTop($v)), + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::SMOOTH_QUARTZ(), Ids::SMOOTH_QUARTZ)->properties([ + $commonProperties->dummyPillarAxis + ])); + $reg->mapModel(Model::create(Blocks::SNOW_LAYER(), Ids::SNOW_LAYER)->properties([ + new DummyProperty(StateNames::COVERED_BIT, false), + new IntProperty(StateNames::HEIGHT, 0, 7, fn(SnowLayer $b) => $b->getLayers(), fn(SnowLayer $b, int $v) => $b->setLayers($v), offset: 1) + ])); + $reg->mapModel(Model::create(Blocks::SOUL_CAMPFIRE(), Ids::SOUL_CAMPFIRE)->properties($commonProperties->campfireProperties)); + $reg->mapModel(Model::create(Blocks::SOUL_FIRE(), Ids::SOUL_FIRE)->properties([ + new DummyProperty(StateNames::AGE, 0) //this is useless for soul fire, since it doesn't have the logic associated + ])); + $reg->mapModel(Model::create(Blocks::SOUL_LANTERN(), Ids::SOUL_LANTERN)->properties([ + new BoolProperty(StateNames::HANGING, fn(Lantern $b) => $b->isHanging(), fn(Lantern $b, bool $v) => $b->setHanging($v)) //TODO: repeated + ])); + $reg->mapModel(Model::create(Blocks::STONE_BUTTON(), Ids::STONE_BUTTON)->properties($commonProperties->buttonProperties)); + $reg->mapModel(Model::create(Blocks::STONE_PRESSURE_PLATE(), Ids::STONE_PRESSURE_PLATE)->properties($commonProperties->simplePressurePlateProperties)); + $reg->mapModel(Model::create(Blocks::STONECUTTER(), Ids::STONECUTTER_BLOCK)->properties([ + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::SUGARCANE(), Ids::REEDS)->properties([ + new IntProperty(StateNames::AGE, 0, 15, fn(Sugarcane $b) => $b->getAge(), fn(Sugarcane $b, int $v) => $b->setAge($v)) + ])); + + //T + $reg->mapModel(Model::create(Blocks::TRAPPED_CHEST(), Ids::TRAPPED_CHEST)->properties([ + $commonProperties->horizontalFacingCardinal + ])); + $reg->mapModel(Model::create(Blocks::TRIPWIRE(), Ids::TRIP_WIRE)->properties([ + new BoolProperty(StateNames::ATTACHED_BIT, fn(Tripwire $b) => $b->isConnected(), fn(Tripwire $b, bool $v) => $b->setConnected($v)), + new BoolProperty(StateNames::DISARMED_BIT, fn(Tripwire $b) => $b->isDisarmed(), fn(Tripwire $b, bool $v) => $b->setDisarmed($v)), + new BoolProperty(StateNames::SUSPENDED_BIT, fn(Tripwire $b) => $b->isSuspended(), fn(Tripwire $b, bool $v) => $b->setSuspended($v)), + new BoolProperty(StateNames::POWERED_BIT, fn(Tripwire $b) => $b->isTriggered(), fn(Tripwire $b, bool $v) => $b->setTriggered($v)), + ])); + $reg->mapModel(Model::create(Blocks::TRIPWIRE_HOOK(), Ids::TRIPWIRE_HOOK)->properties([ + new BoolProperty(StateNames::ATTACHED_BIT, fn(TripwireHook $b) => $b->isConnected(), fn(TripwireHook $b, bool $v) => $b->setConnected($v)), + new BoolProperty(StateNames::POWERED_BIT, fn(TripwireHook $b) => $b->isPowered(), fn(TripwireHook $b, bool $v) => $b->setPowered($v)), + $commonProperties->horizontalFacingSWNE + ])); + + $reg->mapModel(Model::create(Blocks::TWISTING_VINES(), Ids::TWISTING_VINES)->properties([ + new IntProperty(StateNames::TWISTING_VINES_AGE, 0, 25, fn(NetherVines $b) => $b->getAge(), fn(NetherVines $b, int $v) => $b->setAge($v)) + ])); + + //W + $reg->mapModel(Model::create(Blocks::WALL_BANNER(), Ids::WALL_BANNER)->properties([$commonProperties->horizontalFacingClassic])); + $reg->mapModel(Model::create(Blocks::WEEPING_VINES(), Ids::WEEPING_VINES)->properties([ + new IntProperty(StateNames::WEEPING_VINES_AGE, 0, 25, fn(NetherVines $b) => $b->getAge(), fn(NetherVines $b, int $v) => $b->setAge($v)) + ])); + $reg->mapModel(Model::create(Blocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), Ids::HEAVY_WEIGHTED_PRESSURE_PLATE)->properties([$commonProperties->analogRedstoneSignal])); + $reg->mapModel(Model::create(Blocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), Ids::LIGHT_WEIGHTED_PRESSURE_PLATE)->properties([$commonProperties->analogRedstoneSignal])); + } + + /** + * @phpstan-template TBlock of Block + * @phpstan-param Model $model + */ + private static function mapAsymmetricSerializer(BlockSerializerDeserializerRegistrar $reg, Model $model) : void{ + $id = $model->getId(); + $properties = $model->getProperties(); + $reg->serializer->map($model->getBlock(), function(Block $block) use ($id, $properties) : Writer{ + $writer = new Writer($id); + foreach($properties as $property){ + $property->serialize($block, $writer); + } + return $writer; + }); + } + + /** + * @phpstan-template TBlock of Block + * @phpstan-param Model $model + * @phpstan-return TBlock + */ + private static function deserializeAsymmetric(Model $model, Reader $in) : Block{ + $block = clone $model->getBlock(); + foreach($model->getProperties() as $property){ + $property->deserialize($block, $in); + } + return $block; + } + + /** + * All mappings that still use the split form of serializer/deserializer registration + * This is typically only used by blocks with one ID but multiple PM types (split by property) + * These currently can't be registered in a unified way, and due to their small number it may not be worth the + * effort to implement a unified way to deal with them + */ + private static function registerSplitMappings(BlockSerializerDeserializerRegistrar $reg, CommonProperties $commonProperties) : void{ + //big dripleaf - split into head / stem variants, as stems don't have tilt or leaf state + $bigDripleafHeadModel = Model::create(Blocks::BIG_DRIPLEAF_HEAD(), Ids::BIG_DRIPLEAF)->properties([ + $commonProperties->horizontalFacingCardinal, + new ValueFromStringProperty(StateNames::BIG_DRIPLEAF_TILT, ValueMappings::getInstance()->dripleafState, fn(BigDripleafHead $b) => $b->getLeafState(), fn(BigDripleafHead $b, DripleafState $v) => $b->setLeafState($v)), + new DummyProperty(StateNames::BIG_DRIPLEAF_HEAD, true) + ]); + $bigDripleafStemModel = Model::create(Blocks::BIG_DRIPLEAF_STEM(), Ids::BIG_DRIPLEAF)->properties([ + $commonProperties->horizontalFacingCardinal, + new DummyProperty(StateNames::BIG_DRIPLEAF_TILT, StringValues::BIG_DRIPLEAF_TILT_NONE), + new DummyProperty(StateNames::BIG_DRIPLEAF_HEAD, false) + ]); + self::mapAsymmetricSerializer($reg, $bigDripleafHeadModel); + self::mapAsymmetricSerializer($reg, $bigDripleafStemModel); + $reg->deserializer->map(Ids::BIG_DRIPLEAF, fn(Reader $in) => $in->readBool(StateNames::BIG_DRIPLEAF_HEAD) ? + self::deserializeAsymmetric($bigDripleafHeadModel, $in) : + self::deserializeAsymmetric($bigDripleafStemModel, $in) + ); + + $fillLevelProperty = new IntProperty(StateNames::FILL_LEVEL, 1, 6, fn(FillableCauldron $b) => $b->getFillLevel(), fn(FillableCauldron $b, int $v) => $b->setFillLevel($v)); + + //this pretends to be a water cauldron on disk and stores its real information in the block actor data, therefore only a serializer is needed + self::mapAsymmetricSerializer($reg, Model::create(Blocks::POTION_CAULDRON(), Ids::CAULDRON)->properties([$fillLevelProperty, new DummyProperty(StateNames::CAULDRON_LIQUID, StringValues::CAULDRON_LIQUID_WATER)])); + + $lavaCauldronModel = Model::create(Blocks::LAVA_CAULDRON(), Ids::CAULDRON)->properties([ + $fillLevelProperty, + new DummyProperty(StateNames::CAULDRON_LIQUID, StringValues::CAULDRON_LIQUID_LAVA) + ]); + $waterCauldronModel = Model::create(Blocks::WATER_CAULDRON(), Ids::CAULDRON)->properties([ + $fillLevelProperty, + new DummyProperty(StateNames::CAULDRON_LIQUID, StringValues::CAULDRON_LIQUID_WATER) + ]); + $emptyCauldronModel = Model::create(Blocks::CAULDRON(), Ids::CAULDRON)->properties([ + new DummyProperty(StateNames::FILL_LEVEL, 0), + new DummyProperty(StateNames::CAULDRON_LIQUID, StringValues::CAULDRON_LIQUID_WATER) + ]); + self::mapAsymmetricSerializer($reg, $lavaCauldronModel); + self::mapAsymmetricSerializer($reg, $waterCauldronModel); + self::mapAsymmetricSerializer($reg, $emptyCauldronModel); + $reg->deserializer->map(Ids::CAULDRON, fn(Reader $in) => $in->readInt(StateNames::FILL_LEVEL) === 0 ? + self::deserializeAsymmetric($emptyCauldronModel, $in) : + match ($liquid = $in->readString(StateNames::CAULDRON_LIQUID)) { + StringValues::CAULDRON_LIQUID_WATER => self::deserializeAsymmetric($waterCauldronModel, $in), + StringValues::CAULDRON_LIQUID_LAVA => self::deserializeAsymmetric($lavaCauldronModel, $in), + StringValues::CAULDRON_LIQUID_POWDER_SNOW => throw new UnsupportedBlockStateException("Powder snow is not supported yet"), + default => throw $in->badValueException(StateNames::CAULDRON_LIQUID, $liquid) + } + ); + + //mushroom stems, split for consistency with all-sided logs vs normal logs + $allSidedMushroomStemModel = Model::create(Blocks::ALL_SIDED_MUSHROOM_STEM(), Ids::MUSHROOM_STEM)->properties([new DummyProperty(StateNames::HUGE_MUSHROOM_BITS, BlockLegacyMetadata::MUSHROOM_BLOCK_ALL_STEM)]); + $mushroomStemModel = Model::create(Blocks::MUSHROOM_STEM(), Ids::MUSHROOM_STEM)->properties([new DummyProperty(StateNames::HUGE_MUSHROOM_BITS, BlockLegacyMetadata::MUSHROOM_BLOCK_STEM)]); + self::mapAsymmetricSerializer($reg, $allSidedMushroomStemModel); + self::mapAsymmetricSerializer($reg, $mushroomStemModel); + $reg->deserializer->map(Ids::MUSHROOM_STEM, fn(Reader $in) : Block => match ($in->readInt(StateNames::HUGE_MUSHROOM_BITS)) { + BlockLegacyMetadata::MUSHROOM_BLOCK_ALL_STEM => self::deserializeAsymmetric($allSidedMushroomStemModel, $in), + BlockLegacyMetadata::MUSHROOM_BLOCK_STEM => self::deserializeAsymmetric($mushroomStemModel, $in), + default => throw new BlockStateDeserializeException("This state does not exist"), + }); + + //pitcher crop, split into single and double variants as double has different properties and behaviour + //this will probably be the most annoying to unify + $pitcherCropModel = Model::create(Blocks::PITCHER_CROP(), Ids::PITCHER_CROP)->properties([ + new IntProperty(StateNames::GROWTH, 0, PitcherCrop::MAX_AGE, fn(PitcherCrop $b) => $b->getAge(), fn(PitcherCrop $b, int $v) => $b->setAge($v)), + new DummyProperty(StateNames::UPPER_BLOCK_BIT, false) + ]); + $doublePitcherCropAgeOffset = PitcherCrop::MAX_AGE + 1; + $doublePitcherCropModel = Model::create(Blocks::DOUBLE_PITCHER_CROP(), Ids::PITCHER_CROP)->properties([ + new IntProperty( + StateNames::GROWTH, + $doublePitcherCropAgeOffset, //TODO: it would be a bit less awkward if the bounds applied _after_ applying the offset, instead of before + 7, + fn(DoublePitcherCrop $b) => $b->getAge(), + fn(DoublePitcherCrop $b, int $v) => $b->setAge(min($v, DoublePitcherCrop::MAX_AGE)), //state may give up to 7, but only up to 4 is valid + offset: -$doublePitcherCropAgeOffset + ), + new BoolProperty(StateNames::UPPER_BLOCK_BIT, fn(DoublePitcherCrop $b) => $b->isTop(), fn(DoublePitcherCrop $b, bool $v) => $b->setTop($v)) + ]); + self::mapAsymmetricSerializer($reg, $pitcherCropModel); + self::mapAsymmetricSerializer($reg, $doublePitcherCropModel); + $reg->deserializer->map(Ids::PITCHER_CROP, fn(Reader $in) => $in->readInt(StateNames::GROWTH) <= PitcherCrop::MAX_AGE ? + ($in->readBool(StateNames::UPPER_BLOCK_BIT) ? + //top pitcher crop with age 0-2 is an invalid state, only the bottom half should exist in this case + Blocks::AIR() : + self::deserializeAsymmetric($pitcherCropModel, $in) + ) : self::deserializeAsymmetric($doublePitcherCropModel, $in) + ); + + //these only exist within PM (mapped from tile properties) as they don't support the same properties as a + //normal banner, therefore no deserializer is needed + self::mapAsymmetricSerializer($reg, Model::create(Blocks::OMINOUS_BANNER(), Ids::STANDING_BANNER)->properties([$commonProperties->floorSignLikeRotation])); + self::mapAsymmetricSerializer($reg, Model::create(Blocks::OMINOUS_WALL_BANNER(), Ids::WALL_BANNER)->properties([$commonProperties->horizontalFacingClassic])); + + foreach([ + Ids::ACACIA_HANGING_SIGN => [Blocks::ACACIA_CEILING_CENTER_HANGING_SIGN(), Blocks::ACACIA_CEILING_EDGES_HANGING_SIGN(), Blocks::ACACIA_WALL_HANGING_SIGN()], + Ids::BIRCH_HANGING_SIGN => [Blocks::BIRCH_CEILING_CENTER_HANGING_SIGN(), Blocks::BIRCH_CEILING_EDGES_HANGING_SIGN(), Blocks::BIRCH_WALL_HANGING_SIGN()], + Ids::CHERRY_HANGING_SIGN => [Blocks::CHERRY_CEILING_CENTER_HANGING_SIGN(), Blocks::CHERRY_CEILING_EDGES_HANGING_SIGN(), Blocks::CHERRY_WALL_HANGING_SIGN()], + Ids::CRIMSON_HANGING_SIGN => [Blocks::CRIMSON_CEILING_CENTER_HANGING_SIGN(), Blocks::CRIMSON_CEILING_EDGES_HANGING_SIGN(), Blocks::CRIMSON_WALL_HANGING_SIGN()], + Ids::DARK_OAK_HANGING_SIGN => [Blocks::DARK_OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::DARK_OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::DARK_OAK_WALL_HANGING_SIGN()], + Ids::JUNGLE_HANGING_SIGN => [Blocks::JUNGLE_CEILING_CENTER_HANGING_SIGN(), Blocks::JUNGLE_CEILING_EDGES_HANGING_SIGN(), Blocks::JUNGLE_WALL_HANGING_SIGN()], + Ids::MANGROVE_HANGING_SIGN => [Blocks::MANGROVE_CEILING_CENTER_HANGING_SIGN(), Blocks::MANGROVE_CEILING_EDGES_HANGING_SIGN(), Blocks::MANGROVE_WALL_HANGING_SIGN()], + Ids::OAK_HANGING_SIGN => [Blocks::OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::OAK_WALL_HANGING_SIGN()], + Ids::PALE_OAK_HANGING_SIGN => [Blocks::PALE_OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::PALE_OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::PALE_OAK_WALL_HANGING_SIGN()], + Ids::SPRUCE_HANGING_SIGN => [Blocks::SPRUCE_CEILING_CENTER_HANGING_SIGN(), Blocks::SPRUCE_CEILING_EDGES_HANGING_SIGN(), Blocks::SPRUCE_WALL_HANGING_SIGN()], + Ids::WARPED_HANGING_SIGN => [Blocks::WARPED_CEILING_CENTER_HANGING_SIGN(), Blocks::WARPED_CEILING_EDGES_HANGING_SIGN(), Blocks::WARPED_WALL_HANGING_SIGN()], + ] as $id => [$center, $edges, $wall]){ + //attached_bit - true for ceiling center signs, false for ceiling edges signs and wall signs + //hanging - true for all ceiling signs, false for wall signs + //facing_direction - used for ceiling edges signs and wall signs + //ground_sign_direction - used by ceiling center signs only + $centerModel = Model::create($center, $id)->properties([ + $commonProperties->floorSignLikeRotation, + new DummyProperty(StateNames::ATTACHED_BIT, true), + new DummyProperty(StateNames::HANGING, true), + new DummyProperty(StateNames::FACING_DIRECTION, 2) + ]); + $edgesModel = Model::create($edges, $id)->properties([ + new DummyProperty(StateNames::GROUND_SIGN_DIRECTION, 0), + new DummyProperty(StateNames::ATTACHED_BIT, false), + new DummyProperty(StateNames::HANGING, true), + $commonProperties->horizontalFacingClassic, + ]); + $wallModel = Model::create($wall, $id)->properties([ + new DummyProperty(StateNames::GROUND_SIGN_DIRECTION, 0), + new DummyProperty(StateNames::ATTACHED_BIT, false), + new DummyProperty(StateNames::HANGING, false), + $commonProperties->horizontalFacingClassic + ]); + self::mapAsymmetricSerializer($reg, $centerModel); + self::mapAsymmetricSerializer($reg, $edgesModel); + self::mapAsymmetricSerializer($reg, $wallModel); + $reg->deserializer->map($id, fn(Reader $in) => $in->readBool(StateNames::HANGING) ? + ($in->readBool(StateNames::ATTACHED_BIT) ? + self::deserializeAsymmetric($centerModel, $in) : + self::deserializeAsymmetric($edgesModel, $in) + ) : + self::deserializeAsymmetric($wallModel, $in)); + } + } +} diff --git a/src/data/bedrock/block/convert/property/BoolFromStringProperty.php b/src/data/bedrock/block/convert/property/BoolFromStringProperty.php new file mode 100644 index 000000000..89c64188d --- /dev/null +++ b/src/data/bedrock/block/convert/property/BoolFromStringProperty.php @@ -0,0 +1,78 @@ + + */ +final class BoolFromStringProperty implements StringProperty{ + + /** + * @param \Closure(TBlock) : bool $getter + * @param \Closure(TBlock, bool) : mixed $setter + */ + public function __construct( + private string $name, + private string $falseValue, + private string $trueValue, + private \Closure $getter, + private \Closure $setter + ){} + + public function getName() : string{ + return $this->name; + } + + public function getPossibleValues() : array{ + return [$this->falseValue, $this->trueValue]; + } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $this->deserializePlain($block, $in->readString($this->name)); + } + + public function deserializePlain(object $block, string $raw) : void{ + $value = match($raw){ + $this->falseValue => false, + $this->trueValue => true, + default => throw new BlockStateSerializeException("Invalid value for {$this->name}: $raw"), + }; + + ($this->setter)($block, $value); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + $out->writeString($this->name, $this->serializePlain($block)); + } + + public function serializePlain(object $block) : string{ + $value = ($this->getter)($block); + return $value ? $this->trueValue : $this->falseValue; + } +} diff --git a/src/data/bedrock/block/convert/property/BoolProperty.php b/src/data/bedrock/block/convert/property/BoolProperty.php new file mode 100644 index 000000000..299ec4076 --- /dev/null +++ b/src/data/bedrock/block/convert/property/BoolProperty.php @@ -0,0 +1,71 @@ + + */ +final class BoolProperty implements Property{ + /** + * @phpstan-param \Closure(TBlock) : bool $getter + * @phpstan-param \Closure(TBlock, bool) : mixed $setter + */ + public function __construct( + private string $name, + private \Closure $getter, + private \Closure $setter, + private bool $inverted = false //we don't *need* this, but it avoids accidentally forgetting a ! in the getter/setter closures (and makes it analysable) + ){} + + /** + * @phpstan-return self + */ + public static function unused(string $name, bool $serializedValue) : self{ + return new self($name, fn() => $serializedValue, fn() => null); + } + + public function getName() : string{ return $this->name; } + + /** + * @phpstan-param TBlock $block + */ + public function deserialize(object $block, BlockStateReader $in) : void{ + $raw = $in->readBool($this->name); + $value = $raw !== $this->inverted; + ($this->setter)($block, $value); + } + + /** + * @phpstan-param TBlock $block + */ + public function serialize(object $block, BlockStateWriter $out) : void{ + $value = ($this->getter)($block); + $raw = $value !== $this->inverted; + $out->writeBool($this->name, $raw); + } +} diff --git a/src/data/bedrock/block/convert/property/CommonProperties.php b/src/data/bedrock/block/convert/property/CommonProperties.php new file mode 100644 index 000000000..666637027 --- /dev/null +++ b/src/data/bedrock/block/convert/property/CommonProperties.php @@ -0,0 +1,428 @@ + */ + public readonly ValueFromStringProperty $blockFace; + /** @phpstan-var ValueFromStringProperty */ + public readonly ValueFromStringProperty $pillarAxis; + /** @phpstan-var ValueFromStringProperty */ + public readonly ValueFromStringProperty $torchFacing; + + /** @phpstan-var ValueFromStringProperty */ + public readonly ValueFromStringProperty $horizontalFacingCardinal; + /** @phpstan-var ValueFromIntProperty */ + public readonly ValueFromIntProperty $horizontalFacingSWNE; + /** @phpstan-var ValueFromIntProperty */ + public readonly ValueFromIntProperty $horizontalFacingSWNEInverted; + /** @phpstan-var ValueFromIntProperty */ + public readonly ValueFromIntProperty $horizontalFacingClassic; + + /** @phpstan-var ValueFromIntProperty */ + public readonly ValueFromIntProperty $anyFacingClassic; + + /** @phpstan-var ValueSetFromIntProperty */ + public readonly ValueSetFromIntProperty $multiFacingFlags; + + /** @phpstan-var IntProperty */ + public readonly IntProperty $floorSignLikeRotation; + + /** @phpstan-var IntProperty */ + public readonly IntProperty $analogRedstoneSignal; + + /** @phpstan-var IntProperty */ + public readonly IntProperty $cropAgeMax7; + /** @phpstan-var BoolProperty */ + public readonly BoolProperty $doublePlantHalf; + + /** @phpstan-var IntProperty */ + public readonly IntProperty $liquidData; + + /** @phpstan-var BoolProperty */ + public readonly BoolProperty $lit; + + public readonly DummyProperty $dummyCardinalDirection; + public readonly DummyProperty $dummyPillarAxis; + + /** @phpstan-var ValueFromStringProperty */ + public readonly ValueFromStringProperty $dyeColorIdInfix; + + /** @phpstan-var BoolFromStringProperty */ + public readonly BoolFromStringProperty $litIdInfix; + + /** @phpstan-var BoolFromStringProperty */ + public readonly BoolFromStringProperty $slabIdInfix; + /** @phpstan-var BoolFromStringProperty */ + public readonly BoolFromStringProperty $slabPositionProperty; + + /** + * @var StringProperty[] + * @phpstan-var non-empty-list> + */ + public readonly array $coralIdPrefixes; + /** + * @var StringProperty[] + * @phpstan-var non-empty-list> + */ + public readonly array $copperIdPrefixes; + + /** + * @var StringProperty[] + * @phpstan-var non-empty-list> + */ + public readonly array $furnaceIdPrefixes; + + /** + * @var StringProperty[]|string[] + * @phpstan-var non-empty-list> + */ + public readonly array $liquidIdPrefixes; + + /** + * @var StringProperty[] + * @phpstan-var non-empty-list> + */ + public readonly array $woodIdPrefixes; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $buttonProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $campfireProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $doorProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $fenceGateProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $itemFrameProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $simplePressurePlateProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $stairProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $stemProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $trapdoorProperties; + + /** + * @var Property[] + * @phpstan-var non-empty-list> + */ + public readonly array $wallProperties; + + private function __construct(){ + $vm = ValueMappings::getInstance(); + + $hfGet = fn(HorizontalFacing $v) => $v->getFacing(); + $hfSet = fn(HorizontalFacing $v, int $facing) => $v->setFacing($facing); + $this->horizontalFacingCardinal = new ValueFromStringProperty(StateNames::MC_CARDINAL_DIRECTION, $vm->cardinalDirection, $hfGet, $hfSet); + + $this->blockFace = new ValueFromStringProperty( + StateNames::MC_BLOCK_FACE, + $vm->blockFace, + fn(AnyFacing $b) => $b->getFacing(), + fn(AnyFacing $b, int $v) => $b->setFacing($v) + ); + + $this->pillarAxis = new ValueFromStringProperty( + StateNames::PILLAR_AXIS, + $vm->pillarAxis, + fn(PillarRotation $b) => $b->getAxis(), + fn(PillarRotation $b, int $v) => $b->setAxis($v) + ); + + $this->torchFacing = new ValueFromStringProperty( + StateNames::TORCH_FACING_DIRECTION, + $vm->torchFacing, + fn(Torch $b) => $b->getFacing(), + fn(Torch $b, int $v) => $b->setFacing($v) + ); + + $this->horizontalFacingSWNE = new ValueFromIntProperty(StateNames::DIRECTION, $vm->horizontalFacingSWNE, $hfGet, $hfSet); + $this->horizontalFacingSWNEInverted = new ValueFromIntProperty(StateNames::DIRECTION, $vm->horizontalFacingSWNEInverted, $hfGet, $hfSet); + $this->horizontalFacingClassic = new ValueFromIntProperty(StateNames::FACING_DIRECTION, $vm->horizontalFacingClassic, $hfGet, $hfSet); + + $this->anyFacingClassic = new ValueFromIntProperty( + StateNames::FACING_DIRECTION, + $vm->facing, + fn(AnyFacing $b) => $b->getFacing(), + fn(AnyFacing $b, int $v) => $b->setFacing($v) + ); + + $this->multiFacingFlags = new ValueSetFromIntProperty( + StateNames::MULTI_FACE_DIRECTION_BITS, + IntFromRawStateMap::int([ + Facing::DOWN => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_DOWN, + Facing::UP => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_UP, + Facing::NORTH => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_NORTH, + Facing::SOUTH => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_SOUTH, + Facing::WEST => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_WEST, + Facing::EAST => BlockLegacyMetadata::MULTI_FACE_DIRECTION_FLAG_EAST + ]), + fn(MultiAnyFacing $b) => $b->getFaces(), + fn(MultiAnyFacing $b, array $v) => $b->setFaces($v) + ); + + $this->floorSignLikeRotation = new IntProperty(StateNames::GROUND_SIGN_DIRECTION, 0, 15, fn(SignLikeRotation $b) => $b->getRotation(), fn(SignLikeRotation $b, int $v) => $b->setRotation($v)); + + $this->analogRedstoneSignal = new IntProperty(StateNames::REDSTONE_SIGNAL, 0, 15, fn(AnalogRedstoneSignalEmitter $b) => $b->getOutputSignalStrength(), fn(AnalogRedstoneSignalEmitter $b, int $v) => $b->setOutputSignalStrength($v)); + + $this->cropAgeMax7 = new IntProperty(StateNames::GROWTH, 0, 7, fn(Ageable $b) => $b->getAge(), fn(Ageable $b, int $v) => $b->setAge($v)); + $this->doublePlantHalf = new BoolProperty(StateNames::UPPER_BLOCK_BIT, fn(DoublePlant $b) => $b->isTop(), fn(DoublePlant $b, bool $v) => $b->setTop($v)); + + $fallingFlag = BlockLegacyMetadata::LIQUID_FALLING_FLAG; + $this->liquidData = new IntProperty( + StateNames::LIQUID_DEPTH, + 0, + 15, + fn(Liquid $b) => $b->getDecay() | ($b->isFalling() ? $fallingFlag : 0), + fn(Liquid $b, int $v) => $b->setDecay($v & ~$fallingFlag)->setFalling(($v & $fallingFlag) !== 0) + ); + + $this->lit = new BoolProperty(StateNames::LIT, fn(Lightable $b) => $b->isLit(), fn(Lightable $b, bool $v) => $b->setLit($v)); + + $this->dummyCardinalDirection = new DummyProperty(StateNames::MC_CARDINAL_DIRECTION, BlockStateStringValues::MC_CARDINAL_DIRECTION_SOUTH); + $this->dummyPillarAxis = new DummyProperty(StateNames::PILLAR_AXIS, BlockStateStringValues::PILLAR_AXIS_Y); + + $this->dyeColorIdInfix = new ValueFromStringProperty("color", $vm->dyeColor, fn(Colored $b) => $b->getColor(), fn(Colored $b, DyeColor $v) => $b->setColor($v)); + $this->litIdInfix = new BoolFromStringProperty("lit", "", "lit_", fn(Lightable $b) => $b->isLit(), fn(Lightable $b, bool $v) => $b->setLit($v)); + + $this->slabIdInfix = new BoolFromStringProperty( + "double", + "", + "double_", + fn(Slab $b) => $b->getSlabType() === SlabType::DOUBLE, + + //we don't know this is actually a bottom slab yet but we don't have enough information to set the + //correct type in this handler + //BOTTOM serves as a signal value for the state deserializer to decide whether to ignore the + //upper_block_bit property + fn(Slab $b, bool $v) => $b->setSlabType($v ? SlabType::DOUBLE : SlabType::BOTTOM) + ); + $this->slabPositionProperty = new BoolFromStringProperty( + StateNames::MC_VERTICAL_HALF, + BlockStateStringValues::MC_VERTICAL_HALF_BOTTOM, + BlockStateStringValues::MC_VERTICAL_HALF_TOP, + fn(Slab $b) => $b->getSlabType() === SlabType::TOP, + + //Ignore the value for double slabs (should be set by ID component before this is reached) + fn(Slab $b, bool $v) => $b->getSlabType() !== SlabType::DOUBLE ? $b->setSlabType($v ? SlabType::TOP : SlabType::BOTTOM) : null + ); + + $this->coralIdPrefixes = [ + "minecraft:", + new BoolFromStringProperty("dead", "", "dead_", fn(CoralMaterial $b) => $b->isDead(), fn(CoralMaterial $b, bool $v) => $b->setDead($v)), + new ValueFromStringProperty("type", EnumFromRawStateMap::string(CoralType::class, fn(CoralType $case) => match ($case) { + CoralType::BRAIN => "brain", + CoralType::BUBBLE => "bubble", + CoralType::FIRE => "fire", + CoralType::HORN => "horn", + CoralType::TUBE => "tube" + }), fn(CoralMaterial $b) => $b->getCoralType(), fn(CoralMaterial $b, CoralType $v) => $b->setCoralType($v)), + ]; + $this->copperIdPrefixes = [ + "minecraft:", + new BoolFromStringProperty("waxed", "", "waxed_", fn(CopperMaterial $b) => $b->isWaxed(), fn(CopperMaterial $b, bool $v) => $b->setWaxed($v)), + new ValueFromStringProperty("oxidation", EnumFromRawStateMap::string(CopperOxidation::class, fn(CopperOxidation $case) => match ($case) { + CopperOxidation::NONE => "", + CopperOxidation::EXPOSED => "exposed_", + CopperOxidation::WEATHERED => "weathered_", + CopperOxidation::OXIDIZED => "oxidized_", + }), fn(CopperMaterial $b) => $b->getOxidation(), fn(CopperMaterial $b, CopperOxidation $v) => $b->setOxidation($v)) + ]; + + $this->furnaceIdPrefixes = ["minecraft:", $this->litIdInfix]; + + $this->liquidIdPrefixes = [ + "minecraft:", + new BoolFromStringProperty("still", "flowing_", "", fn(Liquid $b) => $b->isStill(), fn(Liquid $b, bool $v) => $b->setStill($v)) + ]; + + $this->woodIdPrefixes = [ + "minecraft:", + new BoolFromStringProperty("stripped", "", "stripped_", fn(Wood $b) => $b->isStripped(), fn(Wood $b, bool $v) => $b->setStripped($v)), + ]; + + $this->buttonProperties = [ + $this->anyFacingClassic, + new BoolProperty(StateNames::BUTTON_PRESSED_BIT, fn(Button $b) => $b->isPressed(), fn(Button $b, bool $v) => $b->setPressed($v)), + ]; + + $this->campfireProperties = [ + $this->horizontalFacingCardinal, + new BoolProperty(StateNames::EXTINGUISHED, fn(Lightable $b) => $b->isLit(), fn(Lightable $b, bool $v) => $b->setLit($v), inverted: true), + ]; + + //TODO: check if these need any special treatment to get the appropriate data to both halves of the door + $this->doorProperties = [ + new BoolProperty(StateNames::UPPER_BLOCK_BIT, fn(Door $b) => $b->isTop(), fn(Door $b, bool $v) => $b->setTop($v)), + new BoolProperty(StateNames::DOOR_HINGE_BIT, fn(Door $b) => $b->isHingeRight(), fn(Door $b, bool $v) => $b->setHingeRight($v)), + new BoolProperty(StateNames::OPEN_BIT, fn(Door $b) => $b->isOpen(), fn(Door $b, bool $v) => $b->setOpen($v)), + new ValueFromStringProperty( + StateNames::MC_CARDINAL_DIRECTION, + IntFromRawStateMap::string([ + //a door facing "east" is actually facing north - thanks mojang + Facing::NORTH => BlockStateStringValues::MC_CARDINAL_DIRECTION_EAST, + Facing::EAST => BlockStateStringValues::MC_CARDINAL_DIRECTION_SOUTH, + Facing::SOUTH => BlockStateStringValues::MC_CARDINAL_DIRECTION_WEST, + Facing::WEST => BlockStateStringValues::MC_CARDINAL_DIRECTION_NORTH + ]), + fn(HorizontalFacing $b) => $b->getFacing(), + fn(HorizontalFacing $b, int $v) => $b->setFacing($v) + ) + ]; + + $this->fenceGateProperties = [ + new BoolProperty(StateNames::IN_WALL_BIT, fn(FenceGate $b) => $b->isInWall(), fn(FenceGate $b, bool $v) => $b->setInWall($v)), + new BoolProperty(StateNames::OPEN_BIT, fn(FenceGate $b) => $b->isOpen(), fn(FenceGate $b, bool $v) => $b->setOpen($v)), + $this->horizontalFacingCardinal, + ]; + + $this->itemFrameProperties = [ + new DummyProperty(StateNames::ITEM_FRAME_PHOTO_BIT, false), //TODO: not sure what the point of this is + new BoolProperty(StateNames::ITEM_FRAME_MAP_BIT, fn(ItemFrame $b) => $b->hasMap(), fn(ItemFrame $b, bool $v) => $b->setHasMap($v)), + $this->anyFacingClassic + ]; + + $this->simplePressurePlateProperties = [ + //TODO: not sure what the deal is here ... seems like a mojang bug / artifact of bad implementation? + //best to keep this separate from weighted plates anyway... + new IntProperty( + StateNames::REDSTONE_SIGNAL, + 0, + 15, + fn(SimplePressurePlate $b) => $b->isPressed() ? 15 : 0, + fn(SimplePressurePlate $b, int $v) => $b->setPressed($v !== 0) + ) + ]; + + $this->stairProperties = [ + new BoolProperty(StateNames::UPSIDE_DOWN_BIT, fn(Stair $b) => $b->isUpsideDown(), fn(Stair $b, bool $v) => $b->setUpsideDown($v)), + new ValueFromIntProperty(StateNames::WEIRDO_DIRECTION, $vm->horizontalFacing5Minus, $hfGet, $hfSet), + ]; + + $this->stemProperties = [ + new ValueFromIntProperty(StateNames::FACING_DIRECTION, $vm->facingStem, fn(Stem $b) => $b->getFacing(), fn(Stem $b, int $v) => $b->setFacing($v)), + $this->cropAgeMax7 + ]; + + $this->trapdoorProperties = [ + //this uses the same values as stairs, but the state is named differently + new ValueFromIntProperty(StateNames::DIRECTION, $vm->horizontalFacing5Minus, $hfGet, $hfSet), + + new BoolProperty(StateNames::UPSIDE_DOWN_BIT, fn(Trapdoor $b) => $b->isTop(), fn(Trapdoor $b, bool $v) => $b->setTop($v)), + new BoolProperty(StateNames::OPEN_BIT, fn(Trapdoor $b) => $b->isOpen(), fn(Trapdoor $b, bool $v) => $b->setOpen($v)), + ]; + + $wallProperties = [ + new BoolProperty(StateNames::WALL_POST_BIT, fn(Wall $b) => $b->isPost(), fn(Wall $b, bool $v) => $b->setPost($v)), + ]; + foreach([ + Facing::NORTH => StateNames::WALL_CONNECTION_TYPE_NORTH, + Facing::SOUTH => StateNames::WALL_CONNECTION_TYPE_SOUTH, + Facing::WEST => StateNames::WALL_CONNECTION_TYPE_WEST, + Facing::EAST => StateNames::WALL_CONNECTION_TYPE_EAST + ] as $facing => $stateName){ + $wallProperties[] = new ValueFromStringProperty( + $stateName, + EnumFromRawStateMap::string(WallConnectionTypeShim::class, fn(WallConnectionTypeShim $case) => $case->getValue()), + fn(Wall $b) => WallConnectionTypeShim::serialize($b->getConnection($facing)), + fn(Wall $b, WallConnectionTypeShim $v) => $b->setConnection($facing, $v->deserialize()) + ); + } + $this->wallProperties = $wallProperties; + } +} diff --git a/src/data/bedrock/block/convert/property/DummyProperty.php b/src/data/bedrock/block/convert/property/DummyProperty.php new file mode 100644 index 000000000..a9d32b417 --- /dev/null +++ b/src/data/bedrock/block/convert/property/DummyProperty.php @@ -0,0 +1,61 @@ + + */ +final class DummyProperty implements Property{ + public function __construct( + private string $name, + private bool|int|string $value + ){} + + public function getName() : string{ + return $this->name; + } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $in->ignored($this->name); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + if(is_bool($this->value)){ + $out->writeBool($this->name, $this->value); + }elseif(is_int($this->value)){ + $out->writeInt($this->name, $this->value); + }elseif(is_string($this->value)){ + $out->writeString($this->name, $this->value); + }else{ + throw new AssumptionFailedError(); + } + } +} diff --git a/src/data/bedrock/block/convert/property/EnumFromRawStateMap.php b/src/data/bedrock/block/convert/property/EnumFromRawStateMap.php new file mode 100644 index 000000000..c6f4d0516 --- /dev/null +++ b/src/data/bedrock/block/convert/property/EnumFromRawStateMap.php @@ -0,0 +1,109 @@ + + */ +class EnumFromRawStateMap implements StateMap{ + /** + * @var int[] + * @phpstan-var array + */ + private array $enumToValue = []; + + /** + * @var \UnitEnum[] + * @phpstan-var array + */ + private array $valueToEnum = []; + + /** + * @phpstan-param class-string $class + * @phpstan-param \Closure(TEnum) : TRaw $mapper + * @phpstan-param ?\Closure(TEnum) : list $aliasMapper + */ + public function __construct( + string $class, + \Closure $mapper, + ?\Closure $aliasMapper = null + ){ + foreach($class::cases() as $case){ + $int = $mapper($case); + $this->valueToEnum[$int] = $case; + $this->enumToValue[spl_object_id($case)] = $int; + + if($aliasMapper !== null){ + $aliases = $aliasMapper($case); + foreach($aliases as $alias){ + $this->valueToEnum[$alias] = $case; + } + } + } + } + + /** + * Workaround PHPStan too-specific literal type inference - if it ever gets fixed we can get rid of these functions + * + * @phpstan-template TEnum_ of \UnitEnum + * @phpstan-param class-string $class + * @param \Closure(TEnum_) : string $mapper + * @param ?\Closure(TEnum_) : list $aliasMapper + * + * @phpstan-return EnumFromRawStateMap + */ + public static function string(string $class, \Closure $mapper, ?\Closure $aliasMapper = null) : self{ return new self($class, $mapper, $aliasMapper); } + + /** + * Workaround PHPStan too-specific literal type inference - if it ever gets fixed we can get rid of these functions + * + * @phpstan-template TEnum_ of \UnitEnum + * @phpstan-param class-string $class + * @param \Closure(TEnum_) : int $mapper + * @param ?\Closure(TEnum_) : list $aliasMapper + * + * @phpstan-return EnumFromRawStateMap + */ + public static function int(string $class, \Closure $mapper, ?\Closure $aliasMapper = null) : self{ return new self($class, $mapper, $aliasMapper); } + + public function getRawToValueMap() : array{ + return $this->valueToEnum; + } + + public function valueToRaw(mixed $value) : int|string{ + return $this->enumToValue[spl_object_id($value)]; + } + + public function rawToValue(int|string $raw) : ?\UnitEnum{ + return $this->valueToEnum[$raw] ?? null; + } + + public function printableValue(mixed $value) : string{ + return $value::class . "::" . $value->name; + } +} diff --git a/src/data/bedrock/block/convert/property/FlattenedCaveVinesVariant.php b/src/data/bedrock/block/convert/property/FlattenedCaveVinesVariant.php new file mode 100644 index 000000000..979fc4751 --- /dev/null +++ b/src/data/bedrock/block/convert/property/FlattenedCaveVinesVariant.php @@ -0,0 +1,35 @@ + + */ +class IntFromRawStateMap implements StateMap{ + + /** + * @var int[] + * @phpstan-var array + */ + private array $deserializeMap; + + /** + * Constructs a bidirectional mapping, given a mapping of internal values -> serialized values, and an optional set + * of aliases per internal value (used for deserializing invalid serialized values). + * + * @param (int|string)[] $serializeMap + * @param (int|int[])|(string|string[]) $deserializeAliases + * + * @phpstan-param array $serializeMap + * @phpstan-param array> $deserializeAliases + */ + public function __construct( + private array $serializeMap, + array $deserializeAliases = [] + ){ + $this->deserializeMap = array_flip($this->serializeMap); + foreach($deserializeAliases as $pmValue => $mcValues){ + if(!is_array($mcValues)){ + $this->deserializeMap[$mcValues] = $pmValue; + }else{ + foreach($mcValues as $mcValue){ + $this->deserializeMap[$mcValue] = $pmValue; + } + } + } + } + + /** + * @param int[] $serializeMap + * @param (int|int[]) $deserializeAliases + * + * @phpstan-param array $serializeMap + * @phpstan-param array> $deserializeAliases + * + * @phpstan-return self + */ + public static function int(array $serializeMap, array $deserializeAliases = []) : self{ return new self($serializeMap, $deserializeAliases); } + + /** + * @param string[] $serializeMap + * @param (string|string[]) $deserializeAliases + * + * @phpstan-param array $serializeMap + * @phpstan-param array> $deserializeAliases + * + * @phpstan-return self + */ + public static function string(array $serializeMap, array $deserializeAliases = []) : self{ return new self($serializeMap, $deserializeAliases); } + + public function getRawToValueMap() : array{ + return $this->deserializeMap; + } + + public function valueToRaw(mixed $value) : int|string{ + return $this->serializeMap[$value]; + } + + public function rawToValue(int|string $raw) : mixed{ + return $this->deserializeMap[$raw] ?? null; + } + + public function printableValue(mixed $value) : string{ + return "$value"; + } +} diff --git a/src/data/bedrock/block/convert/property/IntProperty.php b/src/data/bedrock/block/convert/property/IntProperty.php new file mode 100644 index 000000000..bab865522 --- /dev/null +++ b/src/data/bedrock/block/convert/property/IntProperty.php @@ -0,0 +1,70 @@ + + */ +final class IntProperty implements Property{ + /** + * @phpstan-param \Closure(TBlock) : int $getter + * @phpstan-param \Closure(TBlock, int) : mixed $setter + */ + public function __construct( + private string $name, + private int $min, + private int $max, + private \Closure $getter, + private \Closure $setter, + private int $offset = 0 + ){ + if($min > $max){ + throw new \InvalidArgumentException("Min value cannot be greater than max value"); + } + } + + public function getName() : string{ return $this->name; } + + /** + * @phpstan-return self + */ + public static function unused(string $name, int $serializedValue) : self{ + return new self($name, Limits::INT32_MIN, Limits::INT32_MAX, fn() => $serializedValue, fn() => null); + } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $value = $in->readBoundedInt($this->name, $this->min, $this->max); + ($this->setter)($block, $value + $this->offset); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + $value = ($this->getter)($block); + $out->writeInt($this->name, $value - $this->offset); + } +} diff --git a/src/data/bedrock/block/convert/property/Property.php b/src/data/bedrock/block/convert/property/Property.php new file mode 100644 index 000000000..30868dcd1 --- /dev/null +++ b/src/data/bedrock/block/convert/property/Property.php @@ -0,0 +1,44 @@ + + */ + public function getRawToValueMap() : array; + + /** + * @phpstan-param TValue $value + * @phpstan-return TRaw + */ + public function valueToRaw(mixed $value) : int|string; + + /** + * @phpstan-param TRaw $raw + * @phpstan-return TValue|null + */ + public function rawToValue(int|string $raw) : mixed; + + /** + * @phpstan-param TValue $value + */ + public function printableValue(mixed $value) : string; +} diff --git a/src/data/bedrock/block/convert/property/StringProperty.php b/src/data/bedrock/block/convert/property/StringProperty.php new file mode 100644 index 000000000..14ef1beff --- /dev/null +++ b/src/data/bedrock/block/convert/property/StringProperty.php @@ -0,0 +1,50 @@ + + */ +interface StringProperty extends Property{ + /** + * @return string[] + * @phpstan-return list + */ + public function getPossibleValues() : array; + + /** + * TODO: These are only used for flattened IDs for now, we should expand their use to all properties + * in the future and remove the dependencies on BlockStateReader and BlockStateWriter + * @phpstan-param TBlock $block + */ + public function deserializePlain(object $block, string $raw) : void; + + /** + * TODO: These are only used for flattened IDs for now, we should expand their use to all properties + * in the future and remove the dependencies on BlockStateReader and BlockStateWriter + * @phpstan-param TBlock $block + */ + public function serializePlain(object $block) : string; +} diff --git a/src/data/bedrock/block/convert/property/ValueFromIntProperty.php b/src/data/bedrock/block/convert/property/ValueFromIntProperty.php new file mode 100644 index 000000000..46b721255 --- /dev/null +++ b/src/data/bedrock/block/convert/property/ValueFromIntProperty.php @@ -0,0 +1,75 @@ + + */ +final class ValueFromIntProperty implements Property{ + + /** + * @phpstan-param StateMap $map + * @phpstan-param \Closure(TBlock) : TValue $getter + * @phpstan-param \Closure(TBlock, TValue) : mixed $setter + */ + public function __construct( + private string $name, + private StateMap $map, + private \Closure $getter, + private \Closure $setter + ){} + + public function getName() : string{ return $this->name; } + + /** + * @return int[] + * @phpstan-return list + */ + public function getPossibleValues() : array{ + return array_keys($this->map->getRawToValueMap()); + } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $raw = $in->readInt($this->name); + $value = $this->map->rawToValue($raw); + + if($value === null){ + throw $in->badValueException($this->name, (string) $raw); + } + ($this->setter)($block, $value); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + $value = ($this->getter)($block); + $raw = $this->map->valueToRaw($value); + + $out->writeInt($this->name, $raw); + } +} diff --git a/src/data/bedrock/block/convert/property/ValueFromStringProperty.php b/src/data/bedrock/block/convert/property/ValueFromStringProperty.php new file mode 100644 index 000000000..b33634ae9 --- /dev/null +++ b/src/data/bedrock/block/convert/property/ValueFromStringProperty.php @@ -0,0 +1,81 @@ + + */ +final class ValueFromStringProperty implements StringProperty{ + + /** + * @phpstan-param StateMap $map + * @phpstan-param \Closure(TBlock) : TValue $getter + * @phpstan-param \Closure(TBlock, TValue) : mixed $setter + */ + public function __construct( + private string $name, + private StateMap $map, + private \Closure $getter, + private \Closure $setter + ){} + + public function getName() : string{ return $this->name; } + + /** + * @return string[] + * @phpstan-return list + */ + public function getPossibleValues() : array{ + //PHP sucks + return array_map(strval(...), array_keys($this->map->getRawToValueMap())); + } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $this->deserializePlain($block, $in->readString($this->name)); + } + + public function deserializePlain(object $block, string $raw) : void{ + //TODO: duplicated code from BlockStateReader :( + $value = $this->map->rawToValue($raw) ?? throw new BlockStateDeserializeException("Property \"$this->name\" has invalid value \"$raw\""); + ($this->setter)($block, $value); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + $out->writeString($this->name, $this->serializePlain($block)); + } + + public function serializePlain(object $block) : string{ + $value = ($this->getter)($block); + return $this->map->valueToRaw($value); + } +} diff --git a/src/data/bedrock/block/convert/property/ValueMappings.php b/src/data/bedrock/block/convert/property/ValueMappings.php new file mode 100644 index 000000000..22e9803a5 --- /dev/null +++ b/src/data/bedrock/block/convert/property/ValueMappings.php @@ -0,0 +1,305 @@ + */ + public readonly EnumFromRawStateMap $dyeColor; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $dyeColorWithSilver; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $mobHeadType; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $froglightType; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $dirtType; + + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $dripleafState; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $bellAttachmentType; + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $leverFacing; + + /** @phpstan-var EnumFromRawStateMap */ + public readonly EnumFromRawStateMap $mushroomBlockType; + + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $cardinalDirection; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $blockFace; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $pillarAxis; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $torchFacing; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $portalAxis; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $bambooLeafSize; + + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $horizontalFacing5Minus; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $horizontalFacingSWNE; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $horizontalFacingSWNEInverted; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $horizontalFacingCoral; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $horizontalFacingClassic; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $facing; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $facingEndRod; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $coralAxis; + + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $facingExceptDown; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $facingExceptUp; + /** @phpstan-var IntFromRawStateMap */ + public readonly IntFromRawStateMap $facingStem; + + public function __construct(){ + //flattened ID components - we can't generate constants for these + $this->dyeColor = EnumFromRawStateMap::string(DyeColor::class, fn(DyeColor $case) => match ($case) { + DyeColor::BLACK => "black", + DyeColor::BLUE => "blue", + DyeColor::BROWN => "brown", + DyeColor::CYAN => "cyan", + DyeColor::GRAY => "gray", + DyeColor::GREEN => "green", + DyeColor::LIGHT_BLUE => "light_blue", + DyeColor::LIGHT_GRAY => "light_gray", + DyeColor::LIME => "lime", + DyeColor::MAGENTA => "magenta", + DyeColor::ORANGE => "orange", + DyeColor::PINK => "pink", + DyeColor::PURPLE => "purple", + DyeColor::RED => "red", + DyeColor::WHITE => "white", + DyeColor::YELLOW => "yellow" + }); + $this->dyeColorWithSilver = EnumFromRawStateMap::string(DyeColor::class, fn(DyeColor $case) => match ($case) { + DyeColor::LIGHT_GRAY => "silver", + default => $this->dyeColor->valueToRaw($case) + }); + + $this->mobHeadType = EnumFromRawStateMap::string(MobHeadType::class, fn(MobHeadType $case) => match ($case) { + MobHeadType::CREEPER => Ids::CREEPER_HEAD, + MobHeadType::DRAGON => Ids::DRAGON_HEAD, + MobHeadType::PIGLIN => Ids::PIGLIN_HEAD, + MobHeadType::PLAYER => Ids::PLAYER_HEAD, + MobHeadType::SKELETON => Ids::SKELETON_SKULL, + MobHeadType::WITHER_SKELETON => Ids::WITHER_SKELETON_SKULL, + MobHeadType::ZOMBIE => Ids::ZOMBIE_HEAD + }); + $this->froglightType = EnumFromRawStateMap::string(FroglightType::class, fn(FroglightType $case) => match ($case) { + FroglightType::OCHRE => Ids::OCHRE_FROGLIGHT, + FroglightType::PEARLESCENT => Ids::PEARLESCENT_FROGLIGHT, + FroglightType::VERDANT => Ids::VERDANT_FROGLIGHT, + }); + $this->dirtType = EnumFromRawStateMap::string(DirtType::class, fn(DirtType $case) => match ($case) { + DirtType::NORMAL => Ids::DIRT, + DirtType::COARSE => Ids::COARSE_DIRT, + DirtType::ROOTED => Ids::DIRT_WITH_ROOTS, + }); + + //state value mappings + $this->dripleafState = EnumFromRawStateMap::string(DripleafState::class, fn(DripleafState $case) => match ($case) { + DripleafState::STABLE => StringValues::BIG_DRIPLEAF_TILT_NONE, + DripleafState::UNSTABLE => StringValues::BIG_DRIPLEAF_TILT_UNSTABLE, + DripleafState::PARTIAL_TILT => StringValues::BIG_DRIPLEAF_TILT_PARTIAL_TILT, + DripleafState::FULL_TILT => StringValues::BIG_DRIPLEAF_TILT_FULL_TILT + }); + $this->bellAttachmentType = EnumFromRawStateMap::string(BellAttachmentType::class, fn(BellAttachmentType $case) => match ($case) { + BellAttachmentType::FLOOR => StringValues::ATTACHMENT_STANDING, + BellAttachmentType::CEILING => StringValues::ATTACHMENT_HANGING, + BellAttachmentType::ONE_WALL => StringValues::ATTACHMENT_SIDE, + BellAttachmentType::TWO_WALLS => StringValues::ATTACHMENT_MULTIPLE, + }); + $this->leverFacing = EnumFromRawStateMap::string(LeverFacing::class, fn(LeverFacing $case) => match ($case) { + LeverFacing::DOWN_AXIS_Z => StringValues::LEVER_DIRECTION_DOWN_NORTH_SOUTH, + LeverFacing::DOWN_AXIS_X => StringValues::LEVER_DIRECTION_DOWN_EAST_WEST, + LeverFacing::UP_AXIS_Z => StringValues::LEVER_DIRECTION_UP_NORTH_SOUTH, + LeverFacing::UP_AXIS_X => StringValues::LEVER_DIRECTION_UP_EAST_WEST, + LeverFacing::NORTH => StringValues::LEVER_DIRECTION_NORTH, + LeverFacing::SOUTH => StringValues::LEVER_DIRECTION_SOUTH, + LeverFacing::WEST => StringValues::LEVER_DIRECTION_WEST, + LeverFacing::EAST => StringValues::LEVER_DIRECTION_EAST + }); + + $this->mushroomBlockType = EnumFromRawStateMap::int( + MushroomBlockType::class, + fn(MushroomBlockType $case) => match ($case) { + MushroomBlockType::PORES => LegacyMeta::MUSHROOM_BLOCK_ALL_PORES, + MushroomBlockType::CAP_NORTHWEST => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHWEST_CORNER, + MushroomBlockType::CAP_NORTH => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTH_SIDE, + MushroomBlockType::CAP_NORTHEAST => LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHEAST_CORNER, + MushroomBlockType::CAP_WEST => LegacyMeta::MUSHROOM_BLOCK_CAP_WEST_SIDE, + MushroomBlockType::CAP_MIDDLE => LegacyMeta::MUSHROOM_BLOCK_CAP_TOP_ONLY, + MushroomBlockType::CAP_EAST => LegacyMeta::MUSHROOM_BLOCK_CAP_EAST_SIDE, + MushroomBlockType::CAP_SOUTHWEST => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHWEST_CORNER, + MushroomBlockType::CAP_SOUTH => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTH_SIDE, + MushroomBlockType::CAP_SOUTHEAST => LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHEAST_CORNER, + MushroomBlockType::ALL_CAP => LegacyMeta::MUSHROOM_BLOCK_ALL_CAP, + }, + fn(MushroomBlockType $case) => match ($case) { + MushroomBlockType::ALL_CAP => [11, 12, 13], + default => [] + } + ); + + $this->cardinalDirection = IntFromRawStateMap::string([ + Facing::NORTH => StringValues::MC_CARDINAL_DIRECTION_NORTH, + Facing::SOUTH => StringValues::MC_CARDINAL_DIRECTION_SOUTH, + Facing::WEST => StringValues::MC_CARDINAL_DIRECTION_WEST, + Facing::EAST => StringValues::MC_CARDINAL_DIRECTION_EAST, + ]); + $this->blockFace = IntFromRawStateMap::string([ + Facing::DOWN => StringValues::MC_BLOCK_FACE_DOWN, + Facing::UP => StringValues::MC_BLOCK_FACE_UP, + Facing::NORTH => StringValues::MC_BLOCK_FACE_NORTH, + Facing::SOUTH => StringValues::MC_BLOCK_FACE_SOUTH, + Facing::WEST => StringValues::MC_BLOCK_FACE_WEST, + Facing::EAST => StringValues::MC_BLOCK_FACE_EAST, + ]); + $this->pillarAxis = IntFromRawStateMap::string([ + Axis::X => StringValues::PILLAR_AXIS_X, + Axis::Y => StringValues::PILLAR_AXIS_Y, + Axis::Z => StringValues::PILLAR_AXIS_Z + ]); + $this->torchFacing = IntFromRawStateMap::string([ + //TODO: horizontal directions are flipped (MCPE bug: https://bugs.mojang.com/browse/MCPE-152036) + Facing::WEST => StringValues::TORCH_FACING_DIRECTION_EAST, + Facing::SOUTH => StringValues::TORCH_FACING_DIRECTION_NORTH, + Facing::NORTH => StringValues::TORCH_FACING_DIRECTION_SOUTH, + Facing::UP => StringValues::TORCH_FACING_DIRECTION_TOP, + Facing::EAST => StringValues::TORCH_FACING_DIRECTION_WEST, + ], deserializeAliases: [ + Facing::UP => StringValues::TORCH_FACING_DIRECTION_UNKNOWN //should be illegal, but still supported + ]); + $this->portalAxis = IntFromRawStateMap::string([ + Axis::X => StringValues::PORTAL_AXIS_X, + Axis::Z => StringValues::PORTAL_AXIS_Z, + ], deserializeAliases: [ + Axis::X => StringValues::PORTAL_AXIS_UNKNOWN, + ]); + $this->bambooLeafSize = IntFromRawStateMap::string([ + Bamboo::NO_LEAVES => StringValues::BAMBOO_LEAF_SIZE_NO_LEAVES, + Bamboo::SMALL_LEAVES => StringValues::BAMBOO_LEAF_SIZE_SMALL_LEAVES, + Bamboo::LARGE_LEAVES => StringValues::BAMBOO_LEAF_SIZE_LARGE_LEAVES, + ]); + + $this->horizontalFacing5Minus = IntFromRawStateMap::int([ + Facing::EAST => 0, + Facing::WEST => 1, + Facing::SOUTH => 2, + Facing::NORTH => 3 + ]); + $this->horizontalFacingSWNE = IntFromRawStateMap::int([ + Facing::SOUTH => 0, + Facing::WEST => 1, + Facing::NORTH => 2, + Facing::EAST => 3 + ]); + $this->horizontalFacingSWNEInverted = IntFromRawStateMap::int([ + Facing::NORTH => 0, + Facing::EAST => 1, + Facing::SOUTH => 2, + Facing::WEST => 3, + ]); + $this->horizontalFacingCoral = IntFromRawStateMap::int([ + Facing::WEST => 0, + Facing::EAST => 1, + Facing::NORTH => 2, + Facing::SOUTH => 3 + ]); + $horizontalFacingClassicTable = [ + Facing::NORTH => 2, + Facing::SOUTH => 3, + Facing::WEST => 4, + Facing::EAST => 5 + ]; + $this->horizontalFacingClassic = IntFromRawStateMap::int($horizontalFacingClassicTable, deserializeAliases: [ + Facing::NORTH => [0, 1] //should be illegal but still technically possible + ]); + + $this->facing = IntFromRawStateMap::int([ + Facing::DOWN => 0, + Facing::UP => 1 + ] + $horizontalFacingClassicTable); + + //end rods have all the horizontal facing values opposite to classic facing + $this->facingEndRod = IntFromRawStateMap::int([ + Facing::DOWN => 0, + Facing::UP => 1, + Facing::SOUTH => 2, + Facing::NORTH => 3, + Facing::EAST => 4, + Facing::WEST => 5, + ]); + + $this->coralAxis = IntFromRawStateMap::int([ + Axis::X => 0, + Axis::Z => 1, + ]); + + //TODO: shitty copy pasta job, we can do this better but this is good enough for now + $this->facingExceptDown = IntFromRawStateMap::int( + [Facing::UP => 1] + $horizontalFacingClassicTable, + deserializeAliases: [Facing::UP => 0]); + $this->facingExceptUp = IntFromRawStateMap::int( + [Facing::DOWN => 0] + $horizontalFacingClassicTable, + deserializeAliases: [Facing::DOWN => 1] + ); + + //In PM, we use Facing::UP to indicate that the stem is not attached to a pumpkin/melon, since this makes the + //most intuitive sense (the stem is pointing at the sky). However, Bedrock uses the DOWN state for this, which + //is absurd, and I refuse to make our API similarly absurd. + $this->facingStem = IntFromRawStateMap::int( + [Facing::UP => 0] + $horizontalFacingClassicTable, + deserializeAliases: [Facing::UP => 1] + ); + } +} diff --git a/src/data/bedrock/block/convert/property/ValueSetFromIntProperty.php b/src/data/bedrock/block/convert/property/ValueSetFromIntProperty.php new file mode 100644 index 000000000..89913d78b --- /dev/null +++ b/src/data/bedrock/block/convert/property/ValueSetFromIntProperty.php @@ -0,0 +1,93 @@ + + */ +class ValueSetFromIntProperty implements Property{ + + private int $maxValue = 0; + + /** + * @phpstan-param StateMap $map + * @phpstan-param \Closure(TBlock) : array $getter + * @phpstan-param \Closure(TBlock, array) : mixed $setter + */ + public function __construct( + private string $name, + private StateMap $map, + private \Closure $getter, + private \Closure $setter + ){ + $flagsToCases = $this->map->getRawToValueMap(); + foreach($flagsToCases as $possibleFlag => $option){ + if(($this->maxValue & $possibleFlag) !== 0){ + foreach($flagsToCases as $otherFlag => $otherOption){ + if(($possibleFlag & $otherFlag) === $otherFlag && $otherOption !== $option){ + $printableOption = $this->map->printableValue($option); + $printableOtherOption = $this->map->printableValue($otherOption); + throw new \InvalidArgumentException("Flag for option $printableOption overlaps with flag for option $printableOtherOption in property $this->name"); + } + } + + throw new AssumptionFailedError("Unreachable"); + } + + $this->maxValue |= $possibleFlag; + } + } + + public function getName() : string{ return $this->name; } + + public function deserialize(object $block, BlockStateReader $in) : void{ + $flags = $in->readBoundedInt($this->name, 0, $this->maxValue); + + $value = []; + foreach($this->map->getRawToValueMap() as $possibleFlag => $option){ + if(($flags & $possibleFlag) === $possibleFlag){ + $value[] = $option; + } + } + + ($this->setter)($block, $value); + } + + public function serialize(object $block, BlockStateWriter $out) : void{ + $flags = 0; + + $value = ($this->getter)($block); + foreach($value as $option){ + $flags |= $this->map->valueToRaw($option); + } + + $out->writeInt($this->name, $flags); + } +} diff --git a/src/data/bedrock/block/convert/property/WallConnectionTypeShim.php b/src/data/bedrock/block/convert/property/WallConnectionTypeShim.php new file mode 100644 index 000000000..c7d4913cf --- /dev/null +++ b/src/data/bedrock/block/convert/property/WallConnectionTypeShim.php @@ -0,0 +1,67 @@ + BlockStateStringValues::WALL_CONNECTION_TYPE_EAST_NONE, + self::SHORT => BlockStateStringValues::WALL_CONNECTION_TYPE_EAST_SHORT, + self::TALL => BlockStateStringValues::WALL_CONNECTION_TYPE_EAST_TALL, + }; + } + + public function deserialize() : ?WallConnectionType{ + return match($this){ + self::NONE => null, + self::SHORT => WallConnectionType::SHORT, + self::TALL => WallConnectionType::TALL, + }; + } + + public static function serialize(?WallConnectionType $value) : self{ + return match($value){ + null => self::NONE, + WallConnectionType::SHORT => self::SHORT, + WallConnectionType::TALL => self::TALL, + }; + } +} diff --git a/src/data/bedrock/block/upgrade/BlockIdMetaUpgrader.php b/src/data/bedrock/block/upgrade/BlockIdMetaUpgrader.php index 1c339bd46..379784afb 100644 --- a/src/data/bedrock/block/upgrade/BlockIdMetaUpgrader.php +++ b/src/data/bedrock/block/upgrade/BlockIdMetaUpgrader.php @@ -23,11 +23,12 @@ declare(strict_types=1); namespace pocketmine\data\bedrock\block\upgrade; +use pmmp\encoding\ByteBufferReader; +use pmmp\encoding\DataDecodeException; +use pmmp\encoding\VarInt; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\nbt\LittleEndianNbtSerializer; -use pocketmine\utils\BinaryDataException; -use pocketmine\utils\BinaryStream; /** * Handles translating legacy 1.12 block ID/meta into modern blockstates. @@ -84,25 +85,25 @@ final class BlockIdMetaUpgrader{ public static function loadFromString(string $data, LegacyBlockIdToStringIdMap $idMap, BlockStateUpgrader $blockStateUpgrader) : self{ $mappingTable = []; - $legacyStateMapReader = new BinaryStream($data); + $legacyStateMapReader = new ByteBufferReader($data); $nbtReader = new LittleEndianNbtSerializer(); - $idCount = $legacyStateMapReader->getUnsignedVarInt(); + $idCount = VarInt::readUnsignedInt($legacyStateMapReader); for($idIndex = 0; $idIndex < $idCount; $idIndex++){ - $id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt()); + $id = $legacyStateMapReader->readByteArray(VarInt::readUnsignedInt($legacyStateMapReader)); - $metaCount = $legacyStateMapReader->getUnsignedVarInt(); + $metaCount = VarInt::readUnsignedInt($legacyStateMapReader); for($metaIndex = 0; $metaIndex < $metaCount; $metaIndex++){ - $meta = $legacyStateMapReader->getUnsignedVarInt(); + $meta = VarInt::readUnsignedInt($legacyStateMapReader); $offset = $legacyStateMapReader->getOffset(); - $state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag(); + $state = $nbtReader->read($legacyStateMapReader->getData(), $offset)->mustGetCompoundTag(); $legacyStateMapReader->setOffset($offset); $mappingTable[$id][$meta] = $blockStateUpgrader->upgrade(BlockStateData::fromNbt($state)); } } - if(!$legacyStateMapReader->feof()){ - throw new BinaryDataException("Unexpected trailing data in legacy state map data"); + if($legacyStateMapReader->getUnreadLength() > 0){ + throw new DataDecodeException("Unexpected trailing data in legacy state map data"); } return new self($mappingTable, $idMap); diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php index 08eba8978..b4ed0dd26 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php @@ -37,7 +37,6 @@ use pocketmine\utils\Utils; use Symfony\Component\Filesystem\Path; use function array_key_last; use function array_map; -use function array_values; use function assert; use function count; use function get_debug_type; @@ -138,8 +137,8 @@ final class BlockStateUpgradeSchemaUtils{ $convertedRemappedValuesIndex = []; foreach(Utils::stringifyKeys($model->remappedPropertyValuesIndex ?? []) as $mappingKey => $mappingValues){ - foreach($mappingValues as $k => $oldNew){ - $convertedRemappedValuesIndex[$mappingKey][$k] = new BlockStateUpgradeSchemaValueRemap( + foreach($mappingValues as $oldNew){ + $convertedRemappedValuesIndex[$mappingKey][] = new BlockStateUpgradeSchemaValueRemap( self::jsonModelToTag($oldNew->old), self::jsonModelToTag($oldNew->new) ); @@ -361,7 +360,7 @@ final class BlockStateUpgradeSchemaUtils{ //remaps with the same number of criteria should be sorted alphabetically, but this is not strictly necessary return json_encode($a->oldState ?? []) <=> json_encode($b->oldState ?? []); }); - $result->remappedStates[$oldBlockName] = array_values($keyedRemaps); + $result->remappedStates[$oldBlockName] = $keyedRemaps; //usort strips keys, so this is already a list } if(isset($result->remappedStates)){ ksort($result->remappedStates); diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php index 2dce762b8..acfd0c503 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php @@ -75,6 +75,8 @@ final class BlockStateUpgrader{ public function upgrade(BlockStateData $blockStateData) : BlockStateData{ $version = $blockStateData->getVersion(); + $name = $blockStateData->getName(); + $states = $blockStateData->getStates(); foreach($this->upgradeSchemas as $resultVersion => $schemaList){ /* * Sometimes Mojang made changes without bumping the version ID. @@ -91,57 +93,54 @@ final class BlockStateUpgrader{ } foreach($schemaList as $schema){ - $blockStateData = $this->applySchema($schema, $blockStateData); + [$name, $states] = $this->applySchema($schema, $name, $states); } } - if($this->outputVersion > $version){ - //always update the version number of the blockstate, even if it didn't change - this is needed for - //external tools - $blockStateData = new BlockStateData($blockStateData->getName(), $blockStateData->getStates(), $this->outputVersion); - } - return $blockStateData; + return new BlockStateData($name, $states, $this->outputVersion); } - private function applySchema(BlockStateUpgradeSchema $schema, BlockStateData $blockStateData) : BlockStateData{ - $newStateData = $this->applyStateRemapped($schema, $blockStateData); - if($newStateData !== null){ - return $newStateData; + /** + * @param Tag[] $states + * @phpstan-param array $states + * + * @return (string|Tag[])[] + * @phpstan-return array{0: string, 1: array} + */ + private function applySchema(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{ + $remapped = $this->applyStateRemapped($schema, $oldName, $states); + if($remapped !== null){ + return $remapped; } - $oldName = $blockStateData->getName(); - $states = $blockStateData->getStates(); - if(isset($schema->renamedIds[$oldName]) && isset($schema->flattenedProperties[$oldName])){ //TODO: this probably ought to be validated when the schema is constructed throw new AssumptionFailedError("Both renamedIds and flattenedProperties are set for the same block ID \"$oldName\" - don't know what to do"); } if(isset($schema->renamedIds[$oldName])){ - $newName = $schema->renamedIds[$oldName] ?? null; + $newName = $schema->renamedIds[$oldName]; }elseif(isset($schema->flattenedProperties[$oldName])){ [$newName, $states] = $this->applyPropertyFlattened($schema->flattenedProperties[$oldName], $oldName, $states); }else{ - $newName = null; + $newName = $oldName; } - $stateChanges = 0; + $states = $this->applyPropertyAdded($schema, $oldName, $states); + $states = $this->applyPropertyRemoved($schema, $oldName, $states); + $states = $this->applyPropertyRenamedOrValueChanged($schema, $oldName, $states); + $states = $this->applyPropertyValueChanged($schema, $oldName, $states); - $states = $this->applyPropertyAdded($schema, $oldName, $states, $stateChanges); - $states = $this->applyPropertyRemoved($schema, $oldName, $states, $stateChanges); - $states = $this->applyPropertyRenamedOrValueChanged($schema, $oldName, $states, $stateChanges); - $states = $this->applyPropertyValueChanged($schema, $oldName, $states, $stateChanges); - - if($newName !== null || $stateChanges > 0){ - return new BlockStateData($newName ?? $oldName, $states, $schema->getVersionId()); - } - - return $blockStateData; + return [$newName, $states]; } - private function applyStateRemapped(BlockStateUpgradeSchema $schema, BlockStateData $blockStateData) : ?BlockStateData{ - $oldName = $blockStateData->getName(); - $oldState = $blockStateData->getStates(); - + /** + * @param Tag[] $oldState + * @phpstan-param array $oldState + * + * @return (string|Tag[])[]|null + * @phpstan-return array{0: string, 1: array}|null + */ + private function applyStateRemapped(BlockStateUpgradeSchema $schema, string $oldName, array $oldState) : ?array{ if(isset($schema->remappedStates[$oldName])){ foreach($schema->remappedStates[$oldName] as $remap){ if(count($remap->oldState) > count($oldState)){ @@ -168,7 +167,7 @@ final class BlockStateUpgrader{ } } - return new BlockStateData($newName, $newState, $schema->getVersionId()); + return [$newName, $newState]; } } @@ -182,11 +181,10 @@ final class BlockStateUpgrader{ * @return Tag[] * @phpstan-return array */ - private function applyPropertyAdded(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{ + private function applyPropertyAdded(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{ if(isset($schema->addedProperties[$oldName])){ foreach(Utils::stringifyKeys($schema->addedProperties[$oldName]) as $propertyName => $value){ if(!isset($states[$propertyName])){ - $stateChanges++; $states[$propertyName] = $value; } } @@ -202,13 +200,10 @@ final class BlockStateUpgrader{ * @return Tag[] * @phpstan-return array */ - private function applyPropertyRemoved(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{ + private function applyPropertyRemoved(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{ if(isset($schema->removedProperties[$oldName])){ foreach($schema->removedProperties[$oldName] as $propertyName){ - if(isset($states[$propertyName])){ - $stateChanges++; - unset($states[$propertyName]); - } + unset($states[$propertyName]); } } @@ -234,12 +229,11 @@ final class BlockStateUpgrader{ * @return Tag[] * @phpstan-return array */ - private function applyPropertyRenamedOrValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{ + private function applyPropertyRenamedOrValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{ if(isset($schema->renamedProperties[$oldName])){ foreach(Utils::stringifyKeys($schema->renamedProperties[$oldName]) as $oldPropertyName => $newPropertyName){ $oldValue = $states[$oldPropertyName] ?? null; if($oldValue !== null){ - $stateChanges++; unset($states[$oldPropertyName]); //If a value remap is needed, we need to do it here, since we won't be able to locate the property @@ -260,16 +254,13 @@ final class BlockStateUpgrader{ * @return Tag[] * @phpstan-return array */ - private function applyPropertyValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states, int &$stateChanges) : array{ + private function applyPropertyValueChanged(BlockStateUpgradeSchema $schema, string $oldName, array $states) : array{ if(isset($schema->remappedPropertyValues[$oldName])){ foreach(Utils::stringifyKeys($schema->remappedPropertyValues[$oldName]) as $oldPropertyName => $remappedValues){ $oldValue = $states[$oldPropertyName] ?? null; if($oldValue !== null){ $newValue = $this->locateNewPropertyValue($schema, $oldName, $oldPropertyName, $oldValue); - if($newValue !== $oldValue){ - $stateChanges++; - $states[$oldPropertyName] = $newValue; - } + $states[$oldPropertyName] = $newValue; } } } @@ -287,6 +278,10 @@ final class BlockStateUpgrader{ private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{ $flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null; $expectedType = $flattenInfo->flattenedPropertyType; + if($expectedType === null){ + //TODO: we can't make this non-nullable in a patch release + throw new AssumptionFailedError("We never give this null"); + } if(!$flattenedValue instanceof $expectedType){ //flattened property is not of the expected type, so this transformation is not applicable return [$oldName, $states]; diff --git a/src/data/bedrock/item/ItemDeserializer.php b/src/data/bedrock/item/ItemDeserializer.php index f7854313f..ef3bee2a0 100644 --- a/src/data/bedrock/item/ItemDeserializer.php +++ b/src/data/bedrock/item/ItemDeserializer.php @@ -51,12 +51,19 @@ final class ItemDeserializer{ * @phpstan-param \Closure(Data) : Item $deserializer */ public function map(string $id, \Closure $deserializer) : void{ - if(isset($this->deserializers[$id])){ - throw new \InvalidArgumentException("Deserializer is already assigned for \"$id\""); - } $this->deserializers[$id] = $deserializer; } + /** + * Returns the existing data deserializer for the given ID, or null if none exists. + * This may be useful if you need to override a deserializer, but still want to be able to fall back to the original. + * + * @phpstan-return ?\Closure(Data) : Item + */ + public function getDeserializerForId(string $id) : ?\Closure{ + return $this->deserializers[$id] ?? null; + } + /** * @phpstan-param \Closure(Data) : Block $deserializer */ diff --git a/src/data/bedrock/item/ItemSerializer.php b/src/data/bedrock/item/ItemSerializer.php index 845d5bc5f..c6edcfb8f 100644 --- a/src/data/bedrock/item/ItemSerializer.php +++ b/src/data/bedrock/item/ItemSerializer.php @@ -117,8 +117,8 @@ final class ItemSerializer{ $data = $serializer($item); } - if($item->hasNamedTag()){ - $resultTag = $item->getNamedTag(); + $resultTag = $item->getNamedTag(); + if($resultTag->count() > 0){ $extraTag = $data->getTag(); if($extraTag !== null){ $resultTag = $resultTag->merge($extraTag); diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index df1db4211..bb4c61509 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -26,6 +26,7 @@ namespace pocketmine\data\bedrock\item; use pocketmine\block\Bed; use pocketmine\block\Block; use pocketmine\block\CopperDoor; +use pocketmine\block\tile\Banner as TileBanner; use pocketmine\block\utils\CopperOxidation; use pocketmine\block\utils\DyeColor; use pocketmine\block\VanillaBlocks as Blocks; @@ -39,6 +40,7 @@ use pocketmine\data\bedrock\PotionTypeIdMap; use pocketmine\data\bedrock\SuspiciousStewTypeIdMap; use pocketmine\item\Banner; use pocketmine\item\Dye; +use pocketmine\item\FireworkStar; use pocketmine\item\GoatHorn; use pocketmine\item\Item; use pocketmine\item\Medicine; @@ -46,6 +48,7 @@ use pocketmine\item\Potion; use pocketmine\item\SplashPotion; use pocketmine\item\SuspiciousStew; use pocketmine\item\VanillaItems as Items; +use pocketmine\nbt\tag\CompoundTag; final class ItemSerializerDeserializerRegistrar{ @@ -151,6 +154,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Block(Ids::JUNGLE_DOOR, Blocks::JUNGLE_DOOR()); $this->map1to1Block(Ids::MANGROVE_DOOR, Blocks::MANGROVE_DOOR()); $this->map1to1Block(Ids::NETHER_WART, Blocks::NETHER_WART()); + $this->map1to1Block(Ids::PALE_OAK_DOOR, Blocks::PALE_OAK_DOOR()); $this->map1to1Block(Ids::REPEATER, Blocks::REDSTONE_REPEATER()); $this->map1to1Block(Ids::SOUL_CAMPFIRE, Blocks::SOUL_CAMPFIRE()); $this->map1to1Block(Ids::SPRUCE_DOOR, Blocks::SPRUCE_DOOR()); @@ -164,6 +168,7 @@ final class ItemSerializerDeserializerRegistrar{ */ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::ACACIA_BOAT, Items::ACACIA_BOAT()); + $this->map1to1Item(Ids::ACACIA_HANGING_SIGN, Items::ACACIA_HANGING_SIGN()); $this->map1to1Item(Ids::ACACIA_SIGN, Items::ACACIA_SIGN()); $this->map1to1Item(Ids::AMETHYST_SHARD, Items::AMETHYST_SHARD()); $this->map1to1Item(Ids::APPLE, Items::APPLE()); @@ -173,6 +178,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::BEETROOT_SEEDS, Items::BEETROOT_SEEDS()); $this->map1to1Item(Ids::BEETROOT_SOUP, Items::BEETROOT_SOUP()); $this->map1to1Item(Ids::BIRCH_BOAT, Items::BIRCH_BOAT()); + $this->map1to1Item(Ids::BIRCH_HANGING_SIGN, Items::BIRCH_HANGING_SIGN()); $this->map1to1Item(Ids::BIRCH_SIGN, Items::BIRCH_SIGN()); $this->map1to1Item(Ids::BLAZE_POWDER, Items::BLAZE_POWDER()); $this->map1to1Item(Ids::BLAZE_ROD, Items::BLAZE_ROD()); @@ -191,6 +197,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::CHAINMAIL_HELMET, Items::CHAINMAIL_HELMET()); $this->map1to1Item(Ids::CHAINMAIL_LEGGINGS, Items::CHAINMAIL_LEGGINGS()); $this->map1to1Item(Ids::CHARCOAL, Items::CHARCOAL()); + $this->map1to1Item(Ids::CHERRY_HANGING_SIGN, Items::CHERRY_HANGING_SIGN()); $this->map1to1Item(Ids::CHERRY_SIGN, Items::CHERRY_SIGN()); $this->map1to1Item(Ids::CHICKEN, Items::RAW_CHICKEN()); $this->map1to1Item(Ids::CHORUS_FRUIT, Items::CHORUS_FRUIT()); @@ -210,8 +217,10 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::COOKED_SALMON, Items::COOKED_SALMON()); $this->map1to1Item(Ids::COOKIE, Items::COOKIE()); $this->map1to1Item(Ids::COPPER_INGOT, Items::COPPER_INGOT()); + $this->map1to1Item(Ids::CRIMSON_HANGING_SIGN, Items::CRIMSON_HANGING_SIGN()); $this->map1to1Item(Ids::CRIMSON_SIGN, Items::CRIMSON_SIGN()); $this->map1to1Item(Ids::DARK_OAK_BOAT, Items::DARK_OAK_BOAT()); + $this->map1to1Item(Ids::DARK_OAK_HANGING_SIGN, Items::DARK_OAK_HANGING_SIGN()); $this->map1to1Item(Ids::DARK_OAK_SIGN, Items::DARK_OAK_SIGN()); $this->map1to1Item(Ids::DIAMOND, Items::DIAMOND()); $this->map1to1Item(Ids::DIAMOND_AXE, Items::DIAMOND_AXE()); @@ -238,6 +247,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::EYE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::EYE_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::FEATHER, Items::FEATHER()); $this->map1to1Item(Ids::FERMENTED_SPIDER_EYE, Items::FERMENTED_SPIDER_EYE()); + $this->map1to1Item(Ids::FIREWORK_ROCKET, Items::FIREWORK_ROCKET()); $this->map1to1Item(Ids::FIRE_CHARGE, Items::FIRE_CHARGE()); $this->map1to1Item(Ids::FISHING_ROD, Items::FISHING_ROD()); $this->map1to1Item(Ids::FLINT, Items::FLINT()); @@ -280,6 +290,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::IRON_SHOVEL, Items::IRON_SHOVEL()); $this->map1to1Item(Ids::IRON_SWORD, Items::IRON_SWORD()); $this->map1to1Item(Ids::JUNGLE_BOAT, Items::JUNGLE_BOAT()); + $this->map1to1Item(Ids::JUNGLE_HANGING_SIGN, Items::JUNGLE_HANGING_SIGN()); $this->map1to1Item(Ids::JUNGLE_SIGN, Items::JUNGLE_SIGN()); $this->map1to1Item(Ids::LAPIS_LAZULI, Items::LAPIS_LAZULI()); $this->map1to1Item(Ids::LAVA_BUCKET, Items::LAVA_BUCKET()); @@ -290,6 +301,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::LEATHER_LEGGINGS, Items::LEATHER_PANTS()); $this->map1to1Item(Ids::MAGMA_CREAM, Items::MAGMA_CREAM()); $this->map1to1Item(Ids::MANGROVE_BOAT, Items::MANGROVE_BOAT()); + $this->map1to1Item(Ids::MANGROVE_HANGING_SIGN, Items::MANGROVE_HANGING_SIGN()); $this->map1to1Item(Ids::MANGROVE_SIGN, Items::MANGROVE_SIGN()); $this->map1to1Item(Ids::MELON_SEEDS, Items::MELON_SEEDS()); $this->map1to1Item(Ids::MELON_SLICE, Items::MELON()); @@ -302,11 +314,15 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::MUSIC_DISC_BLOCKS, Items::RECORD_BLOCKS()); $this->map1to1Item(Ids::MUSIC_DISC_CAT, Items::RECORD_CAT()); $this->map1to1Item(Ids::MUSIC_DISC_CHIRP, Items::RECORD_CHIRP()); + $this->map1to1Item(Ids::MUSIC_DISC_CREATOR, Items::RECORD_CREATOR()); + $this->map1to1Item(Ids::MUSIC_DISC_CREATOR_MUSIC_BOX, Items::RECORD_CREATOR_MUSIC_BOX()); $this->map1to1Item(Ids::MUSIC_DISC_FAR, Items::RECORD_FAR()); $this->map1to1Item(Ids::MUSIC_DISC_MALL, Items::RECORD_MALL()); $this->map1to1Item(Ids::MUSIC_DISC_MELLOHI, Items::RECORD_MELLOHI()); $this->map1to1Item(Ids::MUSIC_DISC_OTHERSIDE, Items::RECORD_OTHERSIDE()); $this->map1to1Item(Ids::MUSIC_DISC_PIGSTEP, Items::RECORD_PIGSTEP()); + $this->map1to1Item(Ids::MUSIC_DISC_PRECIPICE, Items::RECORD_PRECIPICE()); + $this->map1to1Item(Ids::MUSIC_DISC_RELIC, Items::RECORD_RELIC()); $this->map1to1Item(Ids::MUSIC_DISC_STAL, Items::RECORD_STAL()); $this->map1to1Item(Ids::MUSIC_DISC_STRAD, Items::RECORD_STRAD()); $this->map1to1Item(Ids::MUSIC_DISC_WAIT, Items::RECORD_WAIT()); @@ -329,8 +345,11 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::NETHERITE_SWORD, Items::NETHERITE_SWORD()); $this->map1to1Item(Ids::NETHERITE_UPGRADE_SMITHING_TEMPLATE, Items::NETHERITE_UPGRADE_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::OAK_BOAT, Items::OAK_BOAT()); + $this->map1to1Item(Ids::OAK_HANGING_SIGN, Items::OAK_HANGING_SIGN()); $this->map1to1Item(Ids::OAK_SIGN, Items::OAK_SIGN()); $this->map1to1Item(Ids::PAINTING, Items::PAINTING()); + $this->map1to1Item(Ids::PALE_OAK_HANGING_SIGN, Items::PALE_OAK_HANGING_SIGN()); + $this->map1to1Item(Ids::PALE_OAK_SIGN, Items::PALE_OAK_SIGN()); $this->map1to1Item(Ids::PAPER, Items::PAPER()); $this->map1to1Item(Ids::PHANTOM_MEMBRANE, Items::PHANTOM_MEMBRANE()); $this->map1to1Item(Ids::PITCHER_POD, Items::PITCHER_POD()); @@ -354,6 +373,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::RAW_IRON, Items::RAW_IRON()); $this->map1to1Item(Ids::RECOVERY_COMPASS, Items::RECOVERY_COMPASS()); $this->map1to1Item(Ids::REDSTONE, Items::REDSTONE_DUST()); + $this->map1to1Item(Ids::RESIN_BRICK, Items::RESIN_BRICK()); $this->map1to1Item(Ids::RIB_ARMOR_TRIM_SMITHING_TEMPLATE, Items::RIB_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::ROTTEN_FLESH, Items::ROTTEN_FLESH()); $this->map1to1Item(Ids::SALMON, Items::RAW_SALMON()); @@ -369,6 +389,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::SPIDER_EYE, Items::SPIDER_EYE()); $this->map1to1Item(Ids::SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::SPRUCE_BOAT, Items::SPRUCE_BOAT()); + $this->map1to1Item(Ids::SPRUCE_HANGING_SIGN, Items::SPRUCE_HANGING_SIGN()); $this->map1to1Item(Ids::SPRUCE_SIGN, Items::SPRUCE_SIGN()); $this->map1to1Item(Ids::SPYGLASS, Items::SPYGLASS()); $this->map1to1Item(Ids::SQUID_SPAWN_EGG, Items::SQUID_SPAWN_EGG()); @@ -384,11 +405,13 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::TORCHFLOWER_SEEDS, Items::TORCHFLOWER_SEEDS()); $this->map1to1Item(Ids::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::TOTEM_OF_UNDYING, Items::TOTEM()); + $this->map1to1Item(Ids::TRIDENT, Items::TRIDENT()); $this->map1to1Item(Ids::TROPICAL_FISH, Items::CLOWNFISH()); $this->map1to1Item(Ids::TURTLE_HELMET, Items::TURTLE_HELMET()); $this->map1to1Item(Ids::VEX_ARMOR_TRIM_SMITHING_TEMPLATE, Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::VILLAGER_SPAWN_EGG, Items::VILLAGER_SPAWN_EGG()); $this->map1to1Item(Ids::WARD_ARMOR_TRIM_SMITHING_TEMPLATE, Items::WARD_ARMOR_TRIM_SMITHING_TEMPLATE()); + $this->map1to1Item(Ids::WARPED_HANGING_SIGN, Items::WARPED_HANGING_SIGN()); $this->map1to1Item(Ids::WARPED_SIGN, Items::WARPED_SIGN()); $this->map1to1Item(Ids::WATER_BUCKET, Items::WATER_BUCKET()); $this->map1to1Item(Ids::WAYFINDER_ARMOR_TRIM_SMITHING_TEMPLATE, Items::WAYFINDER_ARMOR_TRIM_SMITHING_TEMPLATE()); @@ -481,12 +504,12 @@ final class ItemSerializerDeserializerRegistrar{ */ private function register1to1ItemWithMetaMappings() : void{ $this->map1to1ItemWithMeta( - Ids::BANNER, - Items::BANNER(), - function(Banner $item, int $meta) : void{ - $item->setColor(DyeColorIdMap::getInstance()->fromInvertedId($meta) ?? throw new ItemTypeDeserializeException("Unknown banner meta $meta")); + Ids::FIREWORK_STAR, + Items::FIREWORK_STAR(), + function(FireworkStar $item, int $meta) : void{ + // Colors will be defined by CompoundTag deserialization. }, - fn(Banner $item) => DyeColorIdMap::getInstance()->toInvertedId($item->getColor()) + fn(FireworkStar $item) => DyeColorIdMap::getInstance()->toInvertedId($item->getExplosion()->getFlashColor()) ); $this->map1to1ItemWithMeta( Ids::GOAT_HORN, @@ -543,6 +566,21 @@ final class ItemSerializerDeserializerRegistrar{ $this->deserializer?->map($id, fn() => Items::DYE()->setColor($color)); } $this->serializer?->map(Items::DYE(), fn(Dye $item) => new Data(DyeColorIdMap::getInstance()->toItemId($item->getColor()))); + + $this->deserializer?->map(Ids::BANNER, function(Data $data) : Item{ + $type = $data->getTag()?->getInt(TileBanner::TAG_TYPE, TileBanner::TYPE_NORMAL) ?? TileBanner::TYPE_NORMAL; + if($type === TileBanner::TYPE_OMINOUS){ + return Items::OMINOUS_BANNER(); + } + $color = DyeColorIdMap::getInstance()->fromInvertedId($data->getMeta()) ?? throw new ItemTypeDeserializeException("Unknown banner meta " . $data->getMeta()); + return Items::BANNER()->setColor($color); + }); + $this->serializer?->map(Items::OMINOUS_BANNER(), fn() => new Data(Ids::BANNER, tag: CompoundTag::create() + ->setInt(TileBanner::TAG_TYPE, TileBanner::TYPE_OMINOUS)) + ); + $this->serializer?->map(Items::BANNER(), function(Banner $item) : Data{ + return new Data(Ids::BANNER, DyeColorIdMap::getInstance()->toInvertedId($item->getColor())); + }); } /** diff --git a/src/data/bedrock/item/ItemTypeNames.php b/src/data/bedrock/item/ItemTypeNames.php index 03b32d482..5c648ff38 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"; @@ -75,6 +76,9 @@ final class ItemTypeNames{ public const BLEACH = "minecraft:bleach"; 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"; public const BOLT_ARMOR_TRIM_SMITHING_TEMPLATE = "minecraft:bolt_armor_trim_smithing_template"; @@ -92,6 +96,8 @@ final class ItemTypeNames{ public const BRICK = "minecraft:brick"; 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"; @@ -147,13 +153,25 @@ final class ItemTypeNames{ public const COOKED_RABBIT = "minecraft:cooked_rabbit"; public const COOKED_SALMON = "minecraft:cooked_salmon"; public const COOKIE = "minecraft:cookie"; + public const COPPER_AXE = "minecraft:copper_axe"; + public const COPPER_BOOTS = "minecraft:copper_boots"; + public const COPPER_CHESTPLATE = "minecraft:copper_chestplate"; public const COPPER_DOOR = "minecraft:copper_door"; + public const COPPER_GOLEM_SPAWN_EGG = "minecraft:copper_golem_spawn_egg"; + public const COPPER_HELMET = "minecraft:copper_helmet"; + public const COPPER_HOE = "minecraft:copper_hoe"; public const COPPER_INGOT = "minecraft:copper_ingot"; + public const COPPER_LEGGINGS = "minecraft:copper_leggings"; + public const COPPER_NUGGET = "minecraft:copper_nugget"; + public const COPPER_PICKAXE = "minecraft:copper_pickaxe"; + public const COPPER_SHOVEL = "minecraft:copper_shovel"; + public const COPPER_SWORD = "minecraft:copper_sword"; public const CORAL = "minecraft:coral"; public const CORAL_BLOCK = "minecraft:coral_block"; public const CORAL_FAN = "minecraft:coral_fan"; public const CORAL_FAN_DEAD = "minecraft:coral_fan_dead"; public const COW_SPAWN_EGG = "minecraft:cow_spawn_egg"; + public const CREAKING_SPAWN_EGG = "minecraft:creaking_spawn_egg"; public const CREEPER_BANNER_PATTERN = "minecraft:creeper_banner_pattern"; public const CREEPER_SPAWN_EGG = "minecraft:creeper_spawn_egg"; public const CRIMSON_DOOR = "minecraft:crimson_door"; @@ -162,6 +180,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"; @@ -261,12 +280,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"; @@ -317,10 +339,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"; @@ -329,6 +354,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"; @@ -357,6 +383,7 @@ final class ItemTypeNames{ public const MUSIC_DISC_CREATOR = "minecraft:music_disc_creator"; public const MUSIC_DISC_CREATOR_MUSIC_BOX = "minecraft:music_disc_creator_music_box"; public const MUSIC_DISC_FAR = "minecraft:music_disc_far"; + public const MUSIC_DISC_LAVA_CHICKEN = "minecraft:music_disc_lava_chicken"; public const MUSIC_DISC_MALL = "minecraft:music_disc_mall"; public const MUSIC_DISC_MELLOHI = "minecraft:music_disc_mellohi"; public const MUSIC_DISC_OTHERSIDE = "minecraft:music_disc_otherside"; @@ -365,6 +392,7 @@ final class ItemTypeNames{ public const MUSIC_DISC_RELIC = "minecraft:music_disc_relic"; public const MUSIC_DISC_STAL = "minecraft:music_disc_stal"; public const MUSIC_DISC_STRAD = "minecraft:music_disc_strad"; + public const MUSIC_DISC_TEARS = "minecraft:music_disc_tears"; public const MUSIC_DISC_WAIT = "minecraft:music_disc_wait"; public const MUSIC_DISC_WARD = "minecraft:music_disc_ward"; public const MUTTON = "minecraft:mutton"; @@ -396,8 +424,14 @@ 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"; + public const PALE_OAK_CHEST_BOAT = "minecraft:pale_oak_chest_boat"; + public const PALE_OAK_DOOR = "minecraft:pale_oak_door"; + public const PALE_OAK_HANGING_SIGN = "minecraft:pale_oak_hanging_sign"; + public const PALE_OAK_SIGN = "minecraft:pale_oak_sign"; public const PANDA_SPAWN_EGG = "minecraft:panda_spawn_egg"; public const PAPER = "minecraft:paper"; public const PARROT_SPAWN_EGG = "minecraft:parrot_spawn_egg"; @@ -410,6 +444,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"; @@ -430,6 +465,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"; @@ -446,8 +482,10 @@ 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"; public const RIB_ARMOR_TRIM_SMITHING_TEMPLATE = "minecraft:rib_armor_trim_smithing_template"; public const ROTTEN_FLESH = "minecraft:rotten_flesh"; public const SADDLE = "minecraft:saddle"; @@ -553,6 +591,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"; @@ -573,6 +612,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/data/bedrock/item/upgrade/ItemDataUpgrader.php b/src/data/bedrock/item/upgrade/ItemDataUpgrader.php index 7b34ffcb6..98af7ffb0 100644 --- a/src/data/bedrock/item/upgrade/ItemDataUpgrader.php +++ b/src/data/bedrock/item/upgrade/ItemDataUpgrader.php @@ -29,16 +29,14 @@ use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\SavedItemData; use pocketmine\data\bedrock\item\SavedItemStackData; use pocketmine\data\SavedDataLoadingException; -use pocketmine\nbt\NBT; use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\ShortTag; use pocketmine\nbt\tag\StringTag; use pocketmine\network\mcpe\convert\BlockStateDictionary; use pocketmine\utils\Binary; -use function assert; +use function array_map; final class ItemDataUpgrader{ private const TAG_LEGACY_ID = "id"; //TAG_Short (or TAG_String for Java itemstacks) @@ -169,26 +167,6 @@ final class ItemDataUpgrader{ return new SavedItemData($newNameId, $newMeta, $blockStateData, $tag->getCompoundTag(SavedItemData::TAG_TAG)); } - /** - * @return string[] - * @throws SavedDataLoadingException - */ - private static function deserializeListOfStrings(?ListTag $list, string $tagName) : array{ - if($list === null){ - return []; - } - if($list->getTagType() !== NBT::TAG_String){ - throw new SavedDataLoadingException("Unexpected type of list for tag '$tagName', expected TAG_String"); - } - $result = []; - foreach($list as $item){ - assert($item instanceof StringTag); - $result[] = $item->getValue(); - } - - return $result; - } - /** * @throws SavedDataLoadingException */ @@ -205,8 +183,8 @@ final class ItemDataUpgrader{ //optional $slot = ($slotTag = $tag->getTag(SavedItemStackData::TAG_SLOT)) instanceof ByteTag ? Binary::unsignByte($slotTag->getValue()) : null; $wasPickedUp = ($wasPickedUpTag = $tag->getTag(SavedItemStackData::TAG_WAS_PICKED_UP)) instanceof ByteTag ? $wasPickedUpTag->getValue() : null; - $canPlaceOnList = $tag->getListTag(SavedItemStackData::TAG_CAN_PLACE_ON); - $canDestroyList = $tag->getListTag(SavedItemStackData::TAG_CAN_DESTROY); + $canPlaceOnList = $tag->getListTag(SavedItemStackData::TAG_CAN_PLACE_ON, StringTag::class); + $canDestroyList = $tag->getListTag(SavedItemStackData::TAG_CAN_DESTROY, StringTag::class); }catch(NbtException $e){ throw new SavedDataLoadingException($e->getMessage(), 0, $e); } @@ -216,8 +194,8 @@ final class ItemDataUpgrader{ $count, $slot, $wasPickedUp !== 0, - self::deserializeListOfStrings($canPlaceOnList, SavedItemStackData::TAG_CAN_PLACE_ON), - self::deserializeListOfStrings($canDestroyList, SavedItemStackData::TAG_CAN_DESTROY) + $canPlaceOnList === null ? [] : array_map(fn(StringTag $t) => $t->getValue(), $canPlaceOnList->getValue()), + $canDestroyList === null ? [] : array_map(fn(StringTag $t) => $t->getValue(), $canDestroyList->getValue()) ); } diff --git a/src/data/runtime/RuntimeEnumMetadata.php b/src/data/runtime/RuntimeEnumMetadata.php index 261b7a1bc..45f831b19 100644 --- a/src/data/runtime/RuntimeEnumMetadata.php +++ b/src/data/runtime/RuntimeEnumMetadata.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace pocketmine\data\runtime; -use function array_values; use function ceil; use function count; use function log; @@ -60,7 +59,7 @@ final class RuntimeEnumMetadata{ usort($members, fn(\UnitEnum $a, \UnitEnum $b) => $a->name <=> $b->name); //sort by name to ensure consistent ordering (and thus consistent bit assignments) $this->bits = (int) ceil(log(count($members), 2)); - $this->intToEnum = array_values($members); + $this->intToEnum = $members; //usort strips keys so this is already a list $reversed = []; foreach($this->intToEnum as $int => $enum){ diff --git a/src/entity/Attribute.php b/src/entity/Attribute.php index 829e3d26c..3e9b7d7c2 100644 --- a/src/entity/Attribute.php +++ b/src/entity/Attribute.php @@ -76,7 +76,7 @@ class Attribute{ throw new \InvalidArgumentException("Minimum $minValue is greater than the maximum $max"); } - if($this->minValue != $minValue){ + if($this->minValue !== $minValue){ $this->desynchronized = true; $this->minValue = $minValue; } @@ -95,7 +95,7 @@ class Attribute{ throw new \InvalidArgumentException("Maximum $maxValue is less than the minimum $min"); } - if($this->maxValue != $maxValue){ + if($this->maxValue !== $maxValue){ $this->desynchronized = true; $this->maxValue = $maxValue; } @@ -140,7 +140,7 @@ class Attribute{ $value = min(max($value, $this->getMinValue()), $this->getMaxValue()); } - if($this->currentValue != $value){ + if($this->currentValue !== $value){ $this->desynchronized = true; $this->currentValue = $value; }elseif($forceSend){ diff --git a/src/entity/Entity.php b/src/entity/Entity.php index 9bd0de9ea..89f66d6b1 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -31,6 +31,7 @@ use pocketmine\block\Water; use pocketmine\entity\animation\Animation; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDespawnEvent; +use pocketmine\event\entity\EntityExtinguishEvent; use pocketmine\event\entity\EntityMotionEvent; use pocketmine\event\entity\EntityRegainHealthEvent; use pocketmine\event\entity\EntitySpawnEvent; @@ -60,6 +61,7 @@ use pocketmine\player\Player; 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; @@ -72,9 +74,11 @@ use function assert; use function cos; use function count; use function deg2rad; +use function floatval; 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; @@ -488,7 +492,7 @@ abstract class Entity{ new FloatTag($this->location->pitch) ])); - if(!($this instanceof Player)){ + if(!($this instanceof NeverSavedWithChunkEntity)){ EntityFactory::getInstance()->injectSaveId(get_class($this), $nbt); if($this->getNameTag() !== ""){ @@ -591,7 +595,7 @@ abstract class Entity{ * Sets the health of the Entity. This won't send any update to the players */ public function setHealth(float $amount) : void{ - if($amount == $this->health){ + if($amount === $this->health){ return; } @@ -699,16 +703,26 @@ abstract class Entity{ * @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; } } - public function extinguish() : void{ + public function extinguish(int $cause = EntityExtinguishEvent::CAUSE_CUSTOM) : void{ + $ev = new EntityExtinguishEvent($this, $cause); + $ev->call(); + $this->fireTicks = 0; $this->networkPropertiesDirty = true; } @@ -719,7 +733,7 @@ abstract class Entity{ protected function doOnFireTick(int $tickDiff = 1) : bool{ if($this->isFireProof() && $this->isOnFire()){ - $this->extinguish(); + $this->extinguish(EntityExtinguishEvent::CAUSE_FIRE_PROOF); return false; } @@ -730,7 +744,7 @@ abstract class Entity{ } if(!$this->isOnFire()){ - $this->extinguish(); + $this->extinguish(EntityExtinguishEvent::CAUSE_TICKING); }else{ return true; } @@ -760,8 +774,8 @@ abstract class Entity{ $diffMotion = $this->motion->subtractVector($this->lastMotion)->lengthSquared(); - $still = $this->motion->lengthSquared() == 0.0; - $wasStill = $this->lastMotion->lengthSquared() == 0.0; + $still = $this->motion->lengthSquared() === 0.0; + $wasStill = $this->lastMotion->lengthSquared() === 0.0; if($wasStill !== $still){ //TODO: hack for client-side AI interference: prevent client sided movement when motion is 0 $this->setNoClientPredictions($still); @@ -1004,7 +1018,7 @@ abstract class Entity{ abs($this->motion->z) <= self::MOTION_THRESHOLD ? 0 : null ); - if($this->motion->x != 0 || $this->motion->y != 0 || $this->motion->z != 0){ + if(floatval($this->motion->x) !== 0.0 || floatval($this->motion->y) !== 0.0 || floatval($this->motion->z) !== 0.0){ $this->move($this->motion->x, $this->motion->y, $this->motion->z); } @@ -1058,9 +1072,9 @@ abstract class Entity{ public function hasMovementUpdate() : bool{ return ( $this->forceMovementUpdate || - $this->motion->x != 0 || - $this->motion->y != 0 || - $this->motion->z != 0 || + floatval($this->motion->x) !== 0.0 || + floatval($this->motion->y) !== 0.0 || + floatval($this->motion->z) !== 0.0 || !$this->onGround ); } @@ -1163,7 +1177,7 @@ abstract class Entity{ $moveBB->offset(0, $dy, 0); - $fallingFlag = ($this->onGround || ($dy != $wantedY && $wantedY < 0)); + $fallingFlag = ($this->onGround || ($dy !== $wantedY && $wantedY < 0)); foreach($list as $bb){ $dx = $bb->calculateXOffset($moveBB, $dx); @@ -1177,12 +1191,14 @@ abstract class Entity{ $moveBB->offset(0, 0, $dz); - if($this->stepHeight > 0 && $fallingFlag && ($wantedX != $dx || $wantedZ != $dz)){ + $stepHeight = $this->getStepHeight(); + + if($stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){ $cx = $dx; $cy = $dy; $cz = $dz; $dx = $wantedX; - $dy = $this->stepHeight; + $dy = $stepHeight; $dz = $wantedZ; $stepBB = clone $this->boundingBox; @@ -1242,9 +1258,9 @@ abstract class Entity{ $postFallVerticalVelocity = $this->updateFallState($dy, $this->onGround); $this->motion = $this->motion->withComponents( - $wantedX != $dx ? 0 : null, - $postFallVerticalVelocity ?? ($wantedY != $dy ? 0 : null), - $wantedZ != $dz ? 0 : null + $wantedX !== $dx ? 0 : null, + $postFallVerticalVelocity ?? ($wantedY !== $dy ? 0 : null), + $wantedZ !== $dz ? 0 : null ); //TODO: vehicle collision events (first we need to spawn them!) @@ -1252,11 +1268,19 @@ abstract class Entity{ Timings::$entityMove->stopTiming(); } + public function setStepHeight(float $stepHeight) : void{ + $this->stepHeight = $stepHeight; + } + + public function getStepHeight() : float{ + return $this->stepHeight; + } + protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{ - $this->isCollidedVertically = $wantedY != $dy; - $this->isCollidedHorizontally = ($wantedX != $dx || $wantedZ != $dz); + $this->isCollidedVertically = $wantedY !== $dy; + $this->isCollidedHorizontally = ($wantedX !== $dx || $wantedZ !== $dz); $this->isCollided = ($this->isCollidedHorizontally || $this->isCollidedVertically); - $this->onGround = ($wantedY != $dy && $wantedY < 0); + $this->onGround = ($wantedY !== $dy && $wantedY < 0); } /** @@ -1494,7 +1518,7 @@ abstract class Entity{ $this->getId(), //TODO: actor unique ID $this->getId(), static::getNetworkTypeId(), - $this->location->asVector3(), + $this->getOffsetPosition($this->location->asVector3()), $this->getMotion(), $this->location->pitch, $this->location->yaw, diff --git a/src/entity/EntityDataHelper.php b/src/entity/EntityDataHelper.php index 60e45e535..4cce47127 100644 --- a/src/entity/EntityDataHelper.php +++ b/src/entity/EntityDataHelper.php @@ -25,7 +25,6 @@ namespace pocketmine\entity; use pocketmine\data\SavedDataLoadingException; use pocketmine\math\Vector3; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\DoubleTag; use pocketmine\nbt\tag\FloatTag; @@ -59,11 +58,10 @@ final class EntityDataHelper{ public static function parseLocation(CompoundTag $nbt, World $world) : Location{ $pos = self::parseVec3($nbt, Entity::TAG_POS, false); - $yawPitch = $nbt->getTag(Entity::TAG_ROTATION); - if(!($yawPitch instanceof ListTag) || $yawPitch->getTagType() !== NBT::TAG_Float){ + $generic = $nbt->getTag(Entity::TAG_ROTATION); + if(!($generic instanceof ListTag) || ($yawPitch = $generic->cast(FloatTag::class)) === null){ throw new SavedDataLoadingException("'" . Entity::TAG_ROTATION . "' should be a List"); } - /** @var FloatTag[] $values */ $values = $yawPitch->getValue(); if(count($values) !== 2){ throw new SavedDataLoadingException("Expected exactly 2 entries for 'Rotation'"); @@ -78,14 +76,13 @@ final class EntityDataHelper{ * @throws SavedDataLoadingException */ public static function parseVec3(CompoundTag $nbt, string $tagName, bool $optional) : Vector3{ - $pos = $nbt->getTag($tagName); - if($pos === null && $optional){ + $generic = $nbt->getTag($tagName); + if($generic === null && $optional){ return Vector3::zero(); } - if(!($pos instanceof ListTag) || ($pos->getTagType() !== NBT::TAG_Double && $pos->getTagType() !== NBT::TAG_Float)){ + if(!($generic instanceof ListTag) || ($pos = $generic->cast(DoubleTag::class) ?? $generic->cast(FloatTag::class)) === null){ throw new SavedDataLoadingException("'$tagName' should be a List or List"); } - /** @var DoubleTag[]|FloatTag[] $values */ $values = $pos->getValue(); if(count($values) !== 3){ throw new SavedDataLoadingException("Expected exactly 3 entries in '$tagName' tag"); diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php index 03d9c03e6..94639cd3f 100644 --- a/src/entity/EntityFactory.php +++ b/src/entity/EntityFactory.php @@ -46,6 +46,7 @@ use pocketmine\entity\projectile\ExperienceBottle; use pocketmine\entity\projectile\IceBomb; use pocketmine\entity\projectile\Snowball; use pocketmine\entity\projectile\SplashPotion; +use pocketmine\entity\projectile\Trident; use pocketmine\item\Item; use pocketmine\math\Facing; use pocketmine\math\Vector3; @@ -171,6 +172,24 @@ final class EntityFactory{ return new SplashPotion(Helper::parseLocation($nbt, $world), null, $potionType, $nbt); }, ['ThrownPotion', 'minecraft:potion', 'thrownpotion']); + $this->register(Trident::class, function(World $world, CompoundTag $nbt) : Trident{ + $itemTag = $nbt->getCompoundTag(Trident::TAG_ITEM); + if($itemTag === null){ + throw new SavedDataLoadingException("Expected \"" . Trident::TAG_ITEM . "\" NBT tag not found"); + } + + $item = Item::nbtDeserialize($itemTag); + if($item->isNull()){ + throw new SavedDataLoadingException("Trident item is invalid"); + } + return new Trident(Helper::parseLocation($nbt, $world), $item, null, $nbt); + }, [ + 'minecraft:trident', //java + 'minecraft:thrown_trident', //bedrock + 'Trident', //backwards compat for people who used #4547 before it was merged, since it was sitting around for 4 years... + 'ThrownTrident' //as above + ]); + $this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{ return new Squid(Helper::parseLocation($nbt, $world), $nbt); }, ['Squid', 'minecraft:squid']); @@ -219,6 +238,13 @@ final class EntityFactory{ $this->saveNames[$className] = reset($saveNames); } + /** + * @phpstan-param class-string $class + */ + public function isRegistered(string $class) : bool{ + return isset($this->saveNames[$class]); + } + /** * Creates an entity from data stored on a chunk. * diff --git a/src/entity/Human.php b/src/entity/Human.php index 833196651..97ebdefca 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -247,6 +247,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ return $this->enderInventory; } + public function getSneakOffset() : float{ + return 0.31; + } + /** * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. */ @@ -295,12 +299,11 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $this->enderInventory = new PlayerEnderInventory($this); $this->initHumanData($nbt); - $inventoryTag = $nbt->getListTag(self::TAG_INVENTORY); + $inventoryTag = $nbt->getListTag(self::TAG_INVENTORY, CompoundTag::class); if($inventoryTag !== null){ $inventoryItems = []; $armorInventoryItems = []; - /** @var CompoundTag $item */ foreach($inventoryTag as $i => $item){ $slot = $item->getByte(SavedItemStackData::TAG_SLOT); if($slot >= 0 && $slot < 9){ //Hotbar @@ -324,11 +327,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this) ))); - $enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY); + $enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY, CompoundTag::class); if($enderChestInventoryTag !== null){ $enderChestInventoryItems = []; - /** @var CompoundTag $item */ foreach($enderChestInventoryTag as $i => $item){ $enderChestInventoryItems[$item->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($item); } @@ -513,6 +515,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ AbilitiesLayer::LAYER_BASE, array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false), 0.0, + 0.0, 0.0 ) ])), diff --git a/src/entity/Living.php b/src/entity/Living.php index 852344784..e24e07b00 100644 --- a/src/entity/Living.php +++ b/src/entity/Living.php @@ -38,6 +38,7 @@ use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDeathEvent; +use pocketmine\event\entity\EntityFrostWalkerEvent; use pocketmine\inventory\ArmorInventory; use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\Inventory; @@ -180,8 +181,7 @@ abstract class Living extends Entity{ $this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS)); - /** @var CompoundTag[]|ListTag|null $activeEffectsTag */ - $activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS); + $activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS, CompoundTag::class); if($activeEffectsTag !== null){ foreach($activeEffectsTag as $e){ $effect = EffectIdMap::getInstance()->fromId($e->getByte(self::TAG_EFFECT_ID)); @@ -241,6 +241,10 @@ abstract class Living extends Entity{ $this->absorptionAttr->setValue($absorption); } + public function getSneakOffset() : float{ + return 0.0; + } + public function isSneaking() : bool{ return $this->sneaking; } @@ -291,7 +295,7 @@ abstract class Living extends Entity{ $width = $size->getWidth(); $this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale())); }elseif($this->isSneaking()){ - $this->setSize((new EntitySizeInfo(3 / 4 * $size->getHeight(), $size->getWidth(), 3 / 4 * $size->getEyeHeight()))->scale($this->getScale())); + $this->setSize((new EntitySizeInfo($size->getHeight() - $this->getSneakOffset(), $size->getWidth(), $size->getEyeHeight() - $this->getSneakOffset()))->scale($this->getScale())); }else{ $this->setSize($size->scale($this->getScale())); } @@ -721,19 +725,30 @@ abstract class Living extends Entity{ $y = $this->location->getFloorY() - 1; $baseZ = $this->location->getFloorZ(); - $frostedIce = VanillaBlocks::FROSTED_ICE(); + $liquid = VanillaBlocks::WATER(); + $targetBlock = VanillaBlocks::FROSTED_ICE(); + if(EntityFrostWalkerEvent::hasHandlers()){ + $ev = new EntityFrostWalkerEvent($this, $radius, $liquid, $targetBlock); + $ev->call(); + if($ev->isCancelled()){ + return; + } + $radius = $ev->getRadius(); + $liquid = $ev->getLiquid(); + $targetBlock = $ev->getTargetBlock(); + } + for($x = $baseX - $radius; $x <= $baseX + $radius; $x++){ for($z = $baseZ - $radius; $z <= $baseZ + $radius; $z++){ $block = $world->getBlockAt($x, $y, $z); if( - !$block instanceof Water || - !$block->isSource() || + !$block->isSameState($liquid) || $world->getBlockAt($x, $y + 1, $z)->getTypeId() !== BlockTypeIds::AIR || count($world->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z))) !== 0 ){ continue; } - $world->setBlockAt($x, $y, $z, $frostedIce); + $world->setBlockAt($x, $y, $z, $targetBlock); } } } diff --git a/src/entity/Location.php b/src/entity/Location.php index 990108ac4..d9c101882 100644 --- a/src/entity/Location.php +++ b/src/entity/Location.php @@ -66,7 +66,7 @@ class Location extends Position{ public function equals(Vector3 $v) : bool{ if($v instanceof Location){ - return parent::equals($v) && $v->yaw == $this->yaw && $v->pitch == $this->pitch; + return parent::equals($v) && $v->yaw === $this->yaw && $v->pitch === $this->pitch; } return parent::equals($v); } diff --git a/src/entity/NeverSavedWithChunkEntity.php b/src/entity/NeverSavedWithChunkEntity.php new file mode 100644 index 000000000..2245abdd9 --- /dev/null +++ b/src/entity/NeverSavedWithChunkEntity.php @@ -0,0 +1,36 @@ +entity->getId(), ActorEvent::FIREWORK_PARTICLES, 0) + ]; + } +} diff --git a/src/entity/object/EndCrystal.php b/src/entity/object/EndCrystal.php index afaeb6769..74c7664bf 100644 --- a/src/entity/object/EndCrystal.php +++ b/src/entity/object/EndCrystal.php @@ -129,7 +129,7 @@ class EndCrystal extends Entity implements Explosive{ $ev = new EntityPreExplodeEvent($this, 6); $ev->call(); if(!$ev->isCancelled()){ - $explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this); + $explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this, $ev->getFireChance()); if($ev->isBlockBreaking()){ $explosion->explodeA(); } diff --git a/src/entity/object/FireworkRocket.php b/src/entity/object/FireworkRocket.php new file mode 100644 index 000000000..1077dbc39 --- /dev/null +++ b/src/entity/object/FireworkRocket.php @@ -0,0 +1,204 @@ +maxFlightTimeTicks = $maxFlightTimeTicks; + $this->setExplosions($explosions); + + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } + + protected function getInitialDragMultiplier() : float{ return 0.0; } + + protected function getInitialGravity() : float{ return 0.0; } + + /** + * Returns the total number of ticks the firework will fly for before it explodes. + */ + public function getMaxFlightTimeTicks() : int{ + return $this->maxFlightTimeTicks; + } + + /** + * Sets the total number of ticks the firework will fly for before it explodes. + * + * @return $this + */ + public function setMaxFlightTimeTicks(int $maxFlightTimeTicks) : self{ + if($maxFlightTimeTicks < 0){ + throw new \InvalidArgumentException("Max flight time ticks cannot be negative"); + } + $this->maxFlightTimeTicks = $maxFlightTimeTicks; + return $this; + } + + /** + * @return FireworkRocketExplosion[] + */ + public function getExplosions() : array{ + return $this->explosions; + } + + /** + * @param FireworkRocketExplosion[] $explosions + * + * @return $this + */ + public function setExplosions(array $explosions) : self{ + Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{}); + $this->explosions = $explosions; + return $this; + } + + protected function onFirstUpdate(int $currentTick) : void{ + parent::onFirstUpdate($currentTick); + + $this->broadcastSound(new FireworkLaunchSound()); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn()){ + //Don't keep accelerating long-lived fireworks - this gets very rapidly out of control and makes the server + //die. Vanilla fireworks will only live for about 52 ticks maximum anyway, so this only makes sure plugin + //created fireworks don't murder the server + if($this->ticksLived < 60){ + $this->addMotion($this->motion->x * 0.15, 0.04, $this->motion->z * 0.15); + } + + if($this->ticksLived >= $this->maxFlightTimeTicks){ + $this->flagForDespawn(); + $this->explode(); + } + } + + return $hasUpdate; + } + + public function explode() : void{ + if(($explosionCount = count($this->explosions)) !== 0){ + $this->broadcastAnimation(new FireworkParticlesAnimation($this)); + foreach($this->explosions as $explosion){ + $this->broadcastSound($explosion->getType()->getExplosionSound()); + if($explosion->willTwinkle()){ + $this->broadcastSound(new FireworkCrackleSound()); + } + } + + $force = ($explosionCount * 2) + 5; + $world = $this->getWorld(); + foreach($world->getCollidingEntities($this->getBoundingBox()->expandedCopy(5, 5, 5), $this) as $entity){ + if(!$entity instanceof Living){ + continue; + } + + $position = $entity->getPosition(); + $distance = $position->distanceSquared($this->location); + if($distance > 25){ + continue; + } + + //cast two rays - one to the entity's feet and another to halfway up its body (according to Java, anyway) + //this seems like it'd miss some cases but who am I to argue with vanilla logic :> + $height = $entity->getBoundingBox()->getYLength(); + for($i = 0; $i < 2; $i++){ + $target = $position->add(0, 0.5 * $i * $height, 0); + foreach(VoxelRayTrace::betweenPoints($this->location, $target) as $blockPos){ + if($world->getBlock($blockPos)->calculateIntercept($this->location, $target) !== null){ + continue 2; //obstruction, try another path + } + } + + //no obstruction + $damage = $force * sqrt((5 - $position->distance($this->location)) / 5); + $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_EXPLOSION, $damage); + $entity->attack($ev); + break; + } + } + } + } + + public function canBeCollidedWith() : bool{ + return false; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $explosions = new ListTag(); + foreach($this->explosions as $explosion){ + $explosions->push($explosion->toCompoundTag()); + } + $fireworksData = CompoundTag::create() + ->setTag(FireworkItem::TAG_FIREWORK_DATA, CompoundTag::create() + ->setTag(FireworkItem::TAG_EXPLOSIONS, $explosions) + ); + + $properties->setCompoundTag(EntityMetadataProperties::FIREWORK_ITEM, new CacheableNbt($fireworksData)); + } +} diff --git a/src/entity/object/PrimedTNT.php b/src/entity/object/PrimedTNT.php index af3c97922..f3f299d70 100644 --- a/src/entity/object/PrimedTNT.php +++ b/src/entity/object/PrimedTNT.php @@ -121,7 +121,7 @@ class PrimedTNT extends Entity implements Explosive{ $ev->call(); if(!$ev->isCancelled()){ //TODO: deal with underwater TNT (underwater TNT treats water as if it has a blast resistance of 0) - $explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this); + $explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this, $ev->getFireChance()); if($ev->isBlockBreaking()){ $explosion->explodeA(); } diff --git a/src/entity/projectile/Projectile.php b/src/entity/projectile/Projectile.php index 0abc274b5..09529b6b3 100644 --- a/src/entity/projectile/Projectile.php +++ b/src/entity/projectile/Projectile.php @@ -39,12 +39,10 @@ use pocketmine\event\entity\ProjectileHitEvent; use pocketmine\math\RayTraceResult; use pocketmine\math\Vector3; use pocketmine\math\VoxelRayTrace; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\ListTag; use pocketmine\timings\Timings; -use function assert; use function atan2; use function ceil; use function count; @@ -82,12 +80,11 @@ abstract class Projectile extends Entity{ $this->setHealth(1); $this->damage = $nbt->getDouble(self::TAG_DAMAGE, $this->damage); - if(($stuckOnBlockPosTag = $nbt->getListTag(self::TAG_STUCK_ON_BLOCK_POS)) !== null){ - if($stuckOnBlockPosTag->getTagType() !== NBT::TAG_Int || count($stuckOnBlockPosTag) !== 3){ + if(($stuckOnBlockPosTag = $nbt->getListTag(self::TAG_STUCK_ON_BLOCK_POS, IntTag::class)) !== null){ + if(count($stuckOnBlockPosTag) !== 3){ throw new SavedDataLoadingException(self::TAG_STUCK_ON_BLOCK_POS . " tag should be a list of 3 TAG_Int"); } - /** @var IntTag[] $values */ $values = $stuckOnBlockPosTag->getValue(); $this->blockHit = new Vector3($values[0]->getValue(), $values[1]->getValue(), $values[2]->getValue()); @@ -170,8 +167,6 @@ abstract class Projectile extends Entity{ $start = $this->location->asVector3(); $end = $start->add($dx, $dy, $dz); - $blockHit = null; - $entityHit = null; $hitResult = null; $world = $this->getWorld(); @@ -181,8 +176,7 @@ abstract class Projectile extends Entity{ $blockHitResult = $this->calculateInterceptWithBlock($block, $start, $end); if($blockHitResult !== null){ $end = $blockHitResult->hitVector; - $blockHit = $block; - $hitResult = $blockHitResult; + $hitResult = [$block, $blockHitResult]; break; } } @@ -206,8 +200,7 @@ abstract class Projectile extends Entity{ if($distance < $entityDistance){ $entityDistance = $distance; - $entityHit = $entity; - $hitResult = $entityHitResult; + $hitResult = [$entity, $entityHitResult]; $end = $entityHitResult->hitVector; } } @@ -223,29 +216,24 @@ abstract class Projectile extends Entity{ $this->recalculateBoundingBox(); if($hitResult !== null){ - /** @var ProjectileHitEvent|null $ev */ - $ev = null; - if($entityHit !== null){ - $ev = new ProjectileHitEntityEvent($this, $hitResult, $entityHit); - }elseif($blockHit !== null){ - $ev = new ProjectileHitBlockEvent($this, $hitResult, $blockHit); + [$objectHit, $rayTraceResult] = $hitResult; + if($objectHit instanceof Entity){ + $ev = new ProjectileHitEntityEvent($this, $rayTraceResult, $objectHit); + $specificHitFunc = fn() => $this->onHitEntity($objectHit, $rayTraceResult); }else{ - assert(false, "unknown hit type"); + $ev = new ProjectileHitBlockEvent($this, $rayTraceResult, $objectHit); + $specificHitFunc = fn() => $this->onHitBlock($objectHit, $rayTraceResult); } - if($ev !== null){ - $ev->call(); - $this->onHit($ev); - - if($ev instanceof ProjectileHitEntityEvent){ - $this->onHitEntity($ev->getEntityHit(), $ev->getRayTraceResult()); - }elseif($ev instanceof ProjectileHitBlockEvent){ - $this->onHitBlock($ev->getBlockHit(), $ev->getRayTraceResult()); - } - } + $motionBeforeOnHit = clone $this->motion; + $ev->call(); + $this->onHit($ev); + $specificHitFunc(); $this->isCollided = $this->onGround = true; - $this->motion = Vector3::zero(); + if($motionBeforeOnHit->equals($this->motion)){ + $this->motion = Vector3::zero(); + } }else{ $this->isCollided = $this->onGround = false; $this->blockHit = null; @@ -290,10 +278,11 @@ abstract class Projectile extends Entity{ $damage = $this->getResultDamage(); if($damage >= 0){ - if($this->getOwningEntity() === null){ + $owner = $this->getOwningEntity(); + if($owner === null){ $ev = new EntityDamageByEntityEvent($this, $entityHit, EntityDamageEvent::CAUSE_PROJECTILE, $damage); }else{ - $ev = new EntityDamageByChildEntityEvent($this->getOwningEntity(), $this, $entityHit, EntityDamageEvent::CAUSE_PROJECTILE, $damage); + $ev = new EntityDamageByChildEntityEvent($owner, $this, $entityHit, EntityDamageEvent::CAUSE_PROJECTILE, $damage); } $entityHit->attack($ev); @@ -307,7 +296,9 @@ abstract class Projectile extends Entity{ } } - $this->flagForDespawn(); + if($this->despawnsOnEntityHit()){ + $this->flagForDespawn(); + } } /** @@ -317,4 +308,11 @@ abstract class Projectile extends Entity{ $this->blockHit = $blockHit->getPosition()->asVector3(); $blockHit->onProjectileHit($this, $hitResult); } + + /** + * @deprecated This will be dropped in favor of deciding whether to despawn within `onHitEntity()` method. + */ + protected function despawnsOnEntityHit() : bool{ + return true; + } } diff --git a/src/entity/projectile/Trident.php b/src/entity/projectile/Trident.php new file mode 100644 index 000000000..20a494e8d --- /dev/null +++ b/src/entity/projectile/Trident.php @@ -0,0 +1,183 @@ +isNull()){ + throw new \InvalidArgumentException("Trident must have a count of at least 1"); + } + $this->item = clone $item; + parent::__construct($location, $shootingEntity, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.35, 0.25); } + + protected function getInitialDragMultiplier() : float{ return 0.01; } + + protected function getInitialGravity() : float{ return 0.1; } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->spawnedInCreative = $nbt->getByte(self::TAG_SPAWNED_IN_CREATIVE, 0) === 1; + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize()); + $nbt->setByte(self::TAG_SPAWNED_IN_CREATIVE, $this->spawnedInCreative ? 1 : 0); + return $nbt; + } + + protected function onFirstUpdate(int $currentTick) : void{ + $owner = $this->getOwningEntity(); + $this->spawnedInCreative = $owner instanceof Player && $owner->isCreative(); + + parent::onFirstUpdate($currentTick); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + if($this->closed){ + return false; + } + //TODO: Loyalty enchantment. + + return parent::entityBaseTick($tickDiff); + } + + protected function despawnsOnEntityHit() : bool{ + return false; + } + + protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{ + parent::onHitEntity($entityHit, $hitResult); + + $this->canCollide = false; + $this->broadcastSound(new TridentHitEntitySound()); + $this->setMotion(new Vector3($this->motion->x * -0.01, $this->motion->y * -0.1, $this->motion->z * -0.01)); + } + + protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ + parent::onHitBlock($blockHit, $hitResult); + $this->canCollide = true; + $this->broadcastSound(new TridentHitBlockSound()); + } + + public function getItem() : Item{ + return clone $this->item; + } + + public function setItem(Item $item) : void{ + if($item->isNull()){ + throw new \InvalidArgumentException("Trident must have a count of at least 1"); + } + if($this->item->hasEnchantments() !== $item->hasEnchantments()){ + $this->networkPropertiesDirty = true; + } + $this->item = clone $item; + } + + public function canCollideWith(Entity $entity) : bool{ + return $this->canCollide && $entity->getId() !== $this->ownerId && parent::canCollideWith($entity); + } + + public function onCollideWithPlayer(Player $player) : void{ + if($this->blockHit !== null){ + $this->pickup($player); + } + } + + private function pickup(Player $player) : void{ + $shouldDespawn = false; + + $playerInventory = $player->getInventory(); + $ev = new EntityItemPickupEvent($player, $this, $this->getItem(), $playerInventory); + if($player->hasFiniteResources() && !$playerInventory->canAddItem($ev->getItem())){ + $ev->cancel(); + } + if($this->spawnedInCreative){ + $ev->cancel(); + $shouldDespawn = true; + } + + $ev->call(); + if(!$ev->isCancelled()){ + $ev->getInventory()?->addItem($ev->getItem()); + $shouldDespawn = true; + } + + if($shouldDespawn){ + //even if the item was not actually picked up, the animation must be displayed. + NetworkBroadcastUtils::broadcastEntityEvent( + $this->getViewers(), + fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this) + ); + $this->flagForDespawn(); + } + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setGenericFlag(EntityMetadataFlags::ENCHANTED, $this->item->hasEnchantments()); + } +} diff --git a/src/event/HandlerListManager.php b/src/event/HandlerListManager.php index 605a38747..9437df37f 100644 --- a/src/event/HandlerListManager.php +++ b/src/event/HandlerListManager.php @@ -119,7 +119,7 @@ class HandlerListManager{ public function getHandlersFor(string $event) : array{ $cache = $this->handlerCaches[$event] ?? null; //getListFor() will populate the cache for the next call - return $cache?->list ?? $this->getListFor($event)->getListenerList(); + return $cache->list ?? $this->getListFor($event)->getListenerList(); } /** diff --git a/src/event/block/BlockExplodeEvent.php b/src/event/block/BlockExplodeEvent.php new file mode 100644 index 000000000..a8501d475 --- /dev/null +++ b/src/event/block/BlockExplodeEvent.php @@ -0,0 +1,122 @@ + 100.0){ + throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0"); + } + } + + public function getPosition() : Position{ + return $this->position; + } + + /** + * Returns the percentage chance of drops from each block destroyed by the explosion. + * + * @return float 0-100 + */ + public function getYield() : float{ + return $this->yield; + } + + /** + * Sets the percentage chance of drops from each block destroyed by the explosion. + * + * @param float $yield 0-100 + */ + public function setYield(float $yield) : void{ + Utils::checkFloatNotInfOrNaN("yield", $yield); + if($yield < 0.0 || $yield > 100.0){ + throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0"); + } + $this->yield = $yield; + } + + /** + * Returns a list of blocks destroyed by the explosion. + * + * @return Block[] + */ + public function getAffectedBlocks() : array{ + return $this->blocks; + } + + /** + * Sets the blocks destroyed by the explosion. + * + * @param Block[] $blocks + */ + public function setAffectedBlocks(array $blocks) : void{ + Utils::validateArrayValueType($blocks, fn(Block $block) => null); + $this->blocks = $blocks; + } + + /** + * Returns a list of affected blocks that will be replaced by fire. + * + * @return Block[] + */ + public function getIgnitions() : array{ + return $this->ignitions; + } + + /** + * Set the list of blocks that will be replaced by fire. + * + * @param Block[] $ignitions + */ + public function setIgnitions(array $ignitions) : void{ + Utils::validateArrayValueType($ignitions, fn(Block $block) => null); + $this->ignitions = $ignitions; + } +} diff --git a/src/event/block/BlockPreExplodeEvent.php b/src/event/block/BlockPreExplodeEvent.php new file mode 100644 index 000000000..f41cb8a63 --- /dev/null +++ b/src/event/block/BlockPreExplodeEvent.php @@ -0,0 +1,129 @@ + 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + parent::__construct($block); + } + + public function getRadius() : float{ + return $this->radius; + } + + public function setRadius(float $radius) : void{ + Utils::checkFloatNotInfOrNaN("radius", $radius); + if($radius <= 0){ + throw new \InvalidArgumentException("Explosion radius must be positive"); + } + $this->radius = $radius; + } + + public function isBlockBreaking() : bool{ + return $this->blockBreaking; + } + + public function setBlockBreaking(bool $affectsBlocks) : void{ + $this->blockBreaking = $affectsBlocks; + } + + /** + * Returns whether the explosion will create a fire. + */ + public function isIncendiary() : bool{ + return $this->fireChance > 0; + } + + /** + * Sets whether the explosion will create a fire by filling fireChance with default values. + * + * If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0). + */ + public function setIncendiary(bool $incendiary) : void{ + if(!$incendiary){ + $this->fireChance = 0; + }elseif($this->fireChance <= 0){ + $this->fireChance = Explosion::DEFAULT_FIRE_CHANCE; + } + } + + /** + * Returns a chance between 0 and 1 of creating a fire. + */ + public function getFireChance() : float{ + return $this->fireChance; + } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + $this->fireChance = $fireChance; + } + + /** + * Returns the player who triggered the block explosion. + * Returns null if the block was exploded by other means. + */ + public function getPlayer() : ?Player{ + return $this->player; + } +} diff --git a/src/event/block/SignChangeEvent.php b/src/event/block/SignChangeEvent.php index aed59a462..e337ebc36 100644 --- a/src/event/block/SignChangeEvent.php +++ b/src/event/block/SignChangeEvent.php @@ -35,11 +35,15 @@ use pocketmine\player\Player; class SignChangeEvent extends BlockEvent implements Cancellable{ use CancellableTrait; + private SignText $oldText; + public function __construct( private BaseSign $sign, private Player $player, - private SignText $text + private SignText $text, + private bool $frontFace = true ){ + $this->oldText = $this->sign->getFaceText($this->frontFace); parent::__construct($sign); } @@ -55,7 +59,7 @@ class SignChangeEvent extends BlockEvent implements Cancellable{ * Returns the text currently on the sign. */ public function getOldText() : SignText{ - return $this->sign->getText(); + return $this->oldText; } /** @@ -71,4 +75,6 @@ class SignChangeEvent extends BlockEvent implements Cancellable{ public function setNewText(SignText $text) : void{ $this->text = $text; } + + public function isFrontFace() : bool{ return $this->frontFace; } } diff --git a/src/event/entity/EntityExplodeEvent.php b/src/event/entity/EntityExplodeEvent.php index 0a0e4f696..779ab879a 100644 --- a/src/event/entity/EntityExplodeEvent.php +++ b/src/event/entity/EntityExplodeEvent.php @@ -43,13 +43,15 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{ /** * @param Block[] $blocks - * @param float $yield 0-100 + * @param float $yield 0-100 + * @param Block[] $ignitions */ public function __construct( Entity $entity, protected Position $position, protected array $blocks, - protected float $yield + protected float $yield, + private array $ignitions = [] ){ $this->entity = $entity; if($yield < 0.0 || $yield > 100.0){ @@ -98,4 +100,23 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{ } $this->yield = $yield; } + + /** + * Set the list of blocks that will be replaced by fire. + * + * @param Block[] $ignitions + */ + public function setIgnitions(array $ignitions) : void{ + Utils::validateArrayValueType($ignitions, fn(Block $block) => null); + $this->ignitions = $ignitions; + } + + /** + * Returns a list of affected blocks that will be replaced by fire. + * + * @return Block[] + */ + public function getIgnitions() : array{ + return $this->ignitions; + } } diff --git a/src/event/entity/EntityExtinguishEvent.php b/src/event/entity/EntityExtinguishEvent.php new file mode 100644 index 000000000..b39d12231 --- /dev/null +++ b/src/event/entity/EntityExtinguishEvent.php @@ -0,0 +1,53 @@ + + */ +class EntityExtinguishEvent extends EntityEvent{ + public const CAUSE_CUSTOM = 0; + public const CAUSE_WATER = 1; + public const CAUSE_WATER_CAULDRON = 2; + public const CAUSE_RESPAWN = 3; + public const CAUSE_FIRE_PROOF = 4; + public const CAUSE_TICKING = 5; + public const CAUSE_RAIN = 6; + public const CAUSE_POWDER_SNOW = 7; + + public function __construct( + Entity $entity, + private int $cause + ){ + $this->entity = $entity; + } + + public function getCause() : int{ + return $this->cause; + } +} diff --git a/src/event/entity/EntityFrostWalkerEvent.php b/src/event/entity/EntityFrostWalkerEvent.php new file mode 100644 index 000000000..15ba28268 --- /dev/null +++ b/src/event/entity/EntityFrostWalkerEvent.php @@ -0,0 +1,84 @@ + + */ +class EntityFrostWalkerEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + + public function __construct( + Living $entity, + private int $radius, + private Liquid $liquid, + private Block $targetBlock + ){ + $this->entity = $entity; + } + + public function getRadius() : int{ + return $this->radius; + } + + public function setRadius(int $radius) : void{ + $this->radius = $radius; + } + + /** + * Returns the liquid that gets frozen + */ + public function getLiquid() : Liquid{ + return $this->liquid; + } + + /** + * Sets the liquid that gets frozen + */ + public function setLiquid(Liquid $liquid) : void{ + $this->liquid = $liquid; + } + + /** + * Returns the block that replaces the liquid + */ + public function getTargetBlock() : Block{ + return $this->targetBlock; + } + + /** + * Sets the block that replaces the liquid + */ + public function setTargetBlock(Block $targetBlock) : void{ + $this->targetBlock = $targetBlock; + } +} diff --git a/src/event/entity/EntityPreExplodeEvent.php b/src/event/entity/EntityPreExplodeEvent.php index f02a85ecd..c88e83304 100644 --- a/src/event/entity/EntityPreExplodeEvent.php +++ b/src/event/entity/EntityPreExplodeEvent.php @@ -26,6 +26,8 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; use pocketmine\event\CancellableTrait; +use pocketmine\utils\Utils; +use pocketmine\world\Explosion; /** * Called when an entity decides to explode, before the explosion's impact is calculated. @@ -42,11 +44,16 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{ public function __construct( Entity $entity, - protected float $radius + protected float $radius, + private float $fireChance = 0.0, ){ if($radius <= 0){ throw new \InvalidArgumentException("Explosion radius must be positive"); } + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be between 0 and 1."); + } $this->entity = $entity; } @@ -61,6 +68,47 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{ $this->radius = $radius; } + /** + * Returns whether the explosion will create a fire. + */ + public function isIncendiary() : bool{ + return $this->fireChance > 0; + } + + /** + * Sets whether the explosion will create a fire by filling fireChance with default values. + * + * If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0). + */ + public function setIncendiary(bool $incendiary) : void{ + if(!$incendiary){ + $this->fireChance = 0; + }elseif($this->fireChance <= 0){ + $this->fireChance = Explosion::DEFAULT_FIRE_CHANCE; + } + } + + /** + * Returns a chance between 0 and 1 of creating a fire. + */ + public function getFireChance() : float{ + return $this->fireChance; + } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be between 0 and 1."); + } + $this->fireChance = $fireChance; + } + public function isBlockBreaking() : bool{ return $this->blockBreaking; } diff --git a/src/event/player/PlayerDeathEvent.php b/src/event/player/PlayerDeathEvent.php index aacff3438..4b02b4811 100644 --- a/src/event/player/PlayerDeathEvent.php +++ b/src/event/player/PlayerDeathEvent.php @@ -26,7 +26,10 @@ namespace pocketmine\event\player; use pocketmine\block\BlockTypeIds; use pocketmine\entity\Living; use pocketmine\entity\object\FallingBlock; +use pocketmine\entity\object\FireworkRocket; +use pocketmine\entity\projectile\Trident; use pocketmine\event\entity\EntityDamageByBlockEvent; +use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDeathEvent; @@ -113,10 +116,15 @@ class PlayerDeathEvent extends EntityDeathEvent{ } break; case EntityDamageEvent::CAUSE_PROJECTILE: - if($deathCause instanceof EntityDamageByEntityEvent){ + if($deathCause instanceof EntityDamageByChildEntityEvent){ $e = $deathCause->getDamager(); if($e instanceof Living){ - return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName()); + $child = $deathCause->getChild(); + if($child instanceof Trident){ + return KnownTranslationFactory::death_attack_trident($name, $e->getDisplayName()); + }else{ + return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName()); + } } } break; @@ -157,7 +165,9 @@ class PlayerDeathEvent extends EntityDeathEvent{ case EntityDamageEvent::CAUSE_ENTITY_EXPLOSION: if($deathCause instanceof EntityDamageByEntityEvent){ $e = $deathCause->getDamager(); - if($e instanceof Living){ + if($e instanceof FireworkRocket){ + return KnownTranslationFactory::death_attack_fireworks($name); + }elseif($e instanceof Living){ return KnownTranslationFactory::death_attack_explosion_player($name, $e->getDisplayName()); } } diff --git a/src/event/player/PlayerPreLoginEvent.php b/src/event/player/PlayerPreLoginEvent.php index 5a69c0e17..4af0cdd89 100644 --- a/src/event/player/PlayerPreLoginEvent.php +++ b/src/event/player/PlayerPreLoginEvent.php @@ -52,9 +52,15 @@ class PlayerPreLoginEvent extends Event{ self::KICK_FLAG_BANNED ]; - /** @var Translatable[]|string[] reason const => associated message */ + /** + * @var Translatable[]|string[] reason const => associated message + * @phpstan-var array + */ protected array $disconnectReasons = []; - /** @var Translatable[]|string[] */ + /** + * @var Translatable[]|string[] + * @phpstan-var array + */ protected array $disconnectScreenMessages = []; public function __construct( @@ -93,6 +99,7 @@ class PlayerPreLoginEvent extends Event{ * Returns an array of kick flags currently assigned. * * @return int[] + * @phpstan-return list */ public function getKickFlags() : array{ return array_keys($this->disconnectReasons); diff --git a/src/event/player/PlayerRespawnAnchorUseEvent.php b/src/event/player/PlayerRespawnAnchorUseEvent.php new file mode 100644 index 000000000..be7697f11 --- /dev/null +++ b/src/event/player/PlayerRespawnAnchorUseEvent.php @@ -0,0 +1,56 @@ +player = $player; + } + + public function getBlock() : Block{ + return $this->block; + } + + public function getAction() : int{ + return $this->action; + } + + public function setAction(int $action) : void{ + $this->action = $action; + } +} diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 0d5d1ffe6..c4afda43a 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -256,7 +256,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{ $slotItem->setCount($slotItem->getCount() + $amount); $this->setItem($i, $slotItem); if($newItem->getCount() <= 0){ - break; + return $newItem; } } } @@ -270,7 +270,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{ $slotItem->setCount($amount); $this->setItem($slotIndex, $slotItem); if($newItem->getCount() <= 0){ - break; + return $newItem; } } } diff --git a/src/inventory/CreativeCategory.php b/src/inventory/CreativeCategory.php new file mode 100644 index 000000000..bede48b28 --- /dev/null +++ b/src/inventory/CreativeCategory.php @@ -0,0 +1,34 @@ +getText()) : strlen($name); + if($nameLength === 0){ + throw new \InvalidArgumentException("Creative group name cannot be empty"); + } + } + + public function getName() : Translatable|string{ return $this->name; } + + public function getIcon() : Item{ return clone $this->icon; } +} diff --git a/src/inventory/CreativeInventory.php b/src/inventory/CreativeInventory.php index 57e5cbb4e..ee27068c7 100644 --- a/src/inventory/CreativeInventory.php +++ b/src/inventory/CreativeInventory.php @@ -24,21 +24,24 @@ declare(strict_types=1); namespace pocketmine\inventory; use pocketmine\crafting\CraftingManagerFromDataHelper; -use pocketmine\crafting\json\ItemStackData; use pocketmine\data\bedrock\BedrockDataFiles; +use pocketmine\inventory\json\CreativeGroupData; use pocketmine\item\Item; +use pocketmine\lang\Translatable; use pocketmine\utils\DestructorCallbackTrait; use pocketmine\utils\ObjectSet; use pocketmine\utils\SingletonTrait; -use pocketmine\utils\Utils; +use Symfony\Component\Filesystem\Path; +use function array_filter; +use function array_map; final class CreativeInventory{ use SingletonTrait; use DestructorCallbackTrait; /** - * @var Item[] - * @phpstan-var array + * @var CreativeInventoryEntry[] + * @phpstan-var array */ private array $creative = []; @@ -47,17 +50,32 @@ final class CreativeInventory{ private function __construct(){ $this->contentChangedCallbacks = new ObjectSet(); - $creativeItems = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile( - BedrockDataFiles::CREATIVEITEMS_JSON, - ItemStackData::class - ); - foreach($creativeItems as $data){ - $item = CraftingManagerFromDataHelper::deserializeItemStack($data); - if($item === null){ - //unknown item - continue; + + foreach([ + "construction" => CreativeCategory::CONSTRUCTION, + "nature" => CreativeCategory::NATURE, + "equipment" => CreativeCategory::EQUIPMENT, + "items" => CreativeCategory::ITEMS, + ] as $categoryId => $categoryEnum){ + $groups = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile( + Path::join(BedrockDataFiles::CREATIVE, $categoryId . ".json"), + CreativeGroupData::class + ); + + foreach($groups as $groupData){ + $icon = $groupData->group_icon === null ? null : CraftingManagerFromDataHelper::deserializeItemStack($groupData->group_icon); + + $group = $icon === null ? null : new CreativeGroup( + new Translatable($groupData->group_name), + $icon + ); + + $items = array_filter(array_map(static fn($itemStack) => CraftingManagerFromDataHelper::deserializeItemStack($itemStack), $groupData->items)); + + foreach($items as $item){ + $this->add($item, $categoryEnum, $group); + } } - $this->add($item); } } @@ -75,16 +93,28 @@ final class CreativeInventory{ * @phpstan-return array */ public function getAll() : array{ - return Utils::cloneObjectArray($this->creative); + return array_map(fn(CreativeInventoryEntry $entry) => $entry->getItem(), $this->creative); + } + + /** + * @return CreativeInventoryEntry[] + * @phpstan-return array + */ + public function getAllEntries() : array{ + return $this->creative; } public function getItem(int $index) : ?Item{ - return isset($this->creative[$index]) ? clone $this->creative[$index] : null; + return $this->getEntry($index)?->getItem(); + } + + public function getEntry(int $index) : ?CreativeInventoryEntry{ + return $this->creative[$index] ?? null; } public function getItemIndex(Item $item) : int{ foreach($this->creative as $i => $d){ - if($item->equals($d, true, false)){ + if($d->matchesItem($item)){ return $i; } } @@ -96,8 +126,8 @@ final class CreativeInventory{ * Adds an item to the creative menu. * Note: Players who are already online when this is called will not see this change. */ - public function add(Item $item) : void{ - $this->creative[] = clone $item; + public function add(Item $item, CreativeCategory $category = CreativeCategory::ITEMS, ?CreativeGroup $group = null) : void{ + $this->creative[] = new CreativeInventoryEntry($item, $category, $group); $this->onContentChange(); } diff --git a/src/inventory/CreativeInventoryEntry.php b/src/inventory/CreativeInventoryEntry.php new file mode 100644 index 000000000..a5d568ee8 --- /dev/null +++ b/src/inventory/CreativeInventoryEntry.php @@ -0,0 +1,48 @@ +item = clone $item; + } + + public function getItem() : Item{ return clone $this->item; } + + public function getCategory() : CreativeCategory{ return $this->category; } + + public function getGroup() : ?CreativeGroup{ return $this->group; } + + public function matchesItem(Item $item) : bool{ + return $item->equals($this->item, checkDamage: true, checkCompound: false); + } +} diff --git a/src/inventory/json/CreativeGroupData.php b/src/inventory/json/CreativeGroupData.php new file mode 100644 index 000000000..70b73d3de --- /dev/null +++ b/src/inventory/json/CreativeGroupData.php @@ -0,0 +1,38 @@ + @@ -119,19 +120,6 @@ class InventoryTransaction{ } } - /** - * Shuffles actions in the transaction to prevent external things relying on any implicit ordering. - */ - private function shuffleActions() : void{ - $keys = array_keys($this->actions); - shuffle($keys); - $actions = []; - foreach($keys as $key){ - $actions[$key] = $this->actions[$key]; - } - $this->actions = $actions; - } - /** * @param Item[] $needItems * @param Item[] $haveItems @@ -144,8 +132,9 @@ class InventoryTransaction{ $needItems = []; $haveItems = []; foreach($this->actions as $key => $action){ - if(!$action->getTargetItem()->isNull()){ - $needItems[] = $action->getTargetItem(); + $targetItem = $action->getTargetItem(); + if(!$targetItem->isNull()){ + $needItems[] = $targetItem; } try{ @@ -154,8 +143,9 @@ class InventoryTransaction{ throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e); } - if(!$action->getSourceItem()->isNull()){ - $haveItems[] = $action->getSourceItem(); + $sourceItem = $action->getSourceItem(); + if(!$sourceItem->isNull()){ + $haveItems[] = $sourceItem; } } @@ -232,7 +222,7 @@ class InventoryTransaction{ /** * @param SlotChangeAction[] $possibleActions - * @phpstan-param list $possibleActions + * @phpstan-param array $possibleActions */ protected function findResultItem(Item $needOrigin, array $possibleActions) : ?Item{ assert(count($possibleActions) > 0); @@ -306,8 +296,6 @@ class InventoryTransaction{ throw new TransactionValidationException("Transaction has already been executed"); } - $this->shuffleActions(); - $this->validate(); if(!$this->callExecuteEvent()){ diff --git a/src/item/Banner.php b/src/item/Banner.php index 2fc53f5ae..f955a8a12 100644 --- a/src/item/Banner.php +++ b/src/item/Banner.php @@ -29,7 +29,6 @@ use pocketmine\block\utils\DyeColor; use pocketmine\data\bedrock\BannerPatternTypeIdMap; use pocketmine\data\bedrock\DyeColorIdMap; use pocketmine\data\runtime\RuntimeDataDescriber; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use function count; @@ -92,9 +91,8 @@ class Banner extends ItemBlockWallOrFloor{ $colorIdMap = DyeColorIdMap::getInstance(); $patternIdMap = BannerPatternTypeIdMap::getInstance(); - $patterns = $tag->getListTag(self::TAG_PATTERNS); - if($patterns !== null && $patterns->getTagType() === NBT::TAG_Compound){ - /** @var CompoundTag $t */ + $patterns = $tag->getListTag(self::TAG_PATTERNS, CompoundTag::class); + if($patterns !== null){ foreach($patterns as $t){ $patternColor = $colorIdMap->fromInvertedId($t->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK; //TODO: missing pattern colour should be an error $patternType = $patternIdMap->fromId($t->getString(self::TAG_PATTERN_NAME)); diff --git a/src/item/FireworkRocket.php b/src/item/FireworkRocket.php new file mode 100644 index 000000000..39fe6dae2 --- /dev/null +++ b/src/item/FireworkRocket.php @@ -0,0 +1,141 @@ +flightTimeMultiplier; + } + + /** + * Sets the value that will be used to calculate a randomized flight duration + * for the firework. + * + * The higher this value, the longer the flight duration. + * + * @return $this + */ + public function setFlightTimeMultiplier(int $multiplier) : self{ + if($multiplier < 1 || $multiplier > 127){ + throw new \InvalidArgumentException("Flight time multiplier must be in range 1-127"); + } + $this->flightTimeMultiplier = $multiplier; + + return $this; + } + + /** + * @return FireworkRocketExplosion[] + */ + public function getExplosions() : array{ + return $this->explosions; + } + + /** + * @param FireworkRocketExplosion[] $explosions + * + * @return $this + */ + public function setExplosions(array $explosions) : self{ + Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{}); + $this->explosions = $explosions; + + return $this; + } + + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, array &$returnedItems) : ItemUseResult{ + //TODO: this would be nicer if Vector3::getSide() accepted floats for distance + $position = $blockClicked->getPosition()->addVector($clickVector)->addVector(Vector3::zero()->getSide($face)->multiply(0.15)); + + $randomDuration = (($this->flightTimeMultiplier + 1) * 10) + mt_rand(0, 12); + + $entity = new FireworkEntity(Location::fromObject($position, $player->getWorld(), Utils::getRandomFloat() * 360, 90), $randomDuration, $this->explosions); + $entity->setOwningEntity($player); + $entity->setMotion(new Vector3( + (Utils::getRandomFloat() - Utils::getRandomFloat()) * 0.0023, + 0.05, + (Utils::getRandomFloat() - Utils::getRandomFloat()) * 0.0023 + )); + $entity->spawnToAll(); + + $this->pop(); + + return ItemUseResult::SUCCESS; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $fireworkData = $tag->getCompoundTag(self::TAG_FIREWORK_DATA); + if($fireworkData === null){ + throw new SavedDataLoadingException("Missing firework data"); + } + + $this->setFlightTimeMultiplier($fireworkData->getByte(self::TAG_FLIGHT_TIME_MULTIPLIER, 1)); + + if(($explosions = $fireworkData->getListTag(self::TAG_EXPLOSIONS, CompoundTag::class)) !== null){ + foreach($explosions as $explosion){ + $this->explosions[] = FireworkRocketExplosion::fromCompoundTag($explosion); + } + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + $fireworkData = CompoundTag::create(); + $fireworkData->setByte(self::TAG_FLIGHT_TIME_MULTIPLIER, $this->flightTimeMultiplier); + $fireworkData->setTag(self::TAG_EXPLOSIONS, new ListTag(array_map(fn(FireworkRocketExplosion $e) => $e->toCompoundTag(), $this->explosions))); + + $tag->setTag(self::TAG_FIREWORK_DATA, $fireworkData); + } +} diff --git a/src/item/FireworkRocketExplosion.php b/src/item/FireworkRocketExplosion.php new file mode 100644 index 000000000..e113a6ded --- /dev/null +++ b/src/item/FireworkRocketExplosion.php @@ -0,0 +1,190 @@ +getByteArray(self::TAG_COLORS)); + if(count($colors) === 0){ + throw new SavedDataLoadingException("Colors list cannot be empty"); + } + + return new self( + FireworkRocketTypeIdMap::getInstance()->fromId($tag->getByte(self::TAG_TYPE)) ?? throw new SavedDataLoadingException("Invalid firework type"), + $colors, + self::decodeColors($tag->getByteArray(self::TAG_FADE_COLORS)), + $tag->getByte(self::TAG_TWINKLE, 0) !== 0, + $tag->getByte(self::TAG_TRAIL, 0) !== 0 + ); + } + + /** + * @return DyeColor[] + * @phpstan-return list + * @throws SavedDataLoadingException + */ + protected static function decodeColors(string $colorsBytes) : array{ + $colors = []; + + $dyeColorIdMap = DyeColorIdMap::getInstance(); + for($i = 0, $len = strlen($colorsBytes); $i < $len; $i++){ + $colorByte = ord($colorsBytes[$i]); + $color = $dyeColorIdMap->fromInvertedId($colorByte); + if($color !== null){ + $colors[] = $color; + }else{ + throw new SavedDataLoadingException("Unknown color $colorByte"); + } + } + + return $colors; + } + + /** + * @param DyeColor[] $colors + */ + protected static function encodeColors(array $colors) : string{ + $colorsBytes = ""; + + $dyeColorIdMap = DyeColorIdMap::getInstance(); + foreach($colors as $color){ + $colorsBytes .= chr($dyeColorIdMap->toInvertedId($color)); + } + + return $colorsBytes; + } + + /** + * @param DyeColor[] $colors + * @param DyeColor[] $fadeColors + * @phpstan-param non-empty-list $colors + * @phpstan-param list $fadeColors + */ + public function __construct( + protected FireworkRocketType $type, + protected array $colors, + protected array $fadeColors = [], + protected bool $twinkle = false, + protected bool $trail = false + ){ + if(count($colors) === 0){ + throw new \InvalidArgumentException("Colors list cannot be empty"); + } + + $colorsValidator = function(DyeColor $_) : void{}; + + Utils::validateArrayValueType($colors, $colorsValidator); + Utils::validateArrayValueType($fadeColors, $colorsValidator); + } + + public function getType() : FireworkRocketType{ + return $this->type; + } + + /** + * Returns the colors of the particles. + * + * @return DyeColor[] + * @phpstan-return non-empty-list + */ + public function getColors() : array{ + return $this->colors; + } + + /** + * Returns the flash color of the explosion. + */ + public function getFlashColor() : DyeColor{ + return $this->colors[array_key_first($this->colors)]; + } + + /** + * Returns the mixure of colors from {@link FireworkRocketExplosion::getColors()}) + */ + public function getColorMix() : Color{ + /** @var Color[] $colors */ + $colors = []; + foreach($this->colors as $dyeColor){ + $colors[] = $dyeColor->getRgbValue(); + } + return Color::mix(...$colors); + } + + /** + * Returns the colors to which the particles will change their color after a few seconds. + * If it is empty, there will be no color change in the particles. + * + * @return DyeColor[] + * @phpstan-return list + */ + public function getFadeColors() : array{ + return $this->fadeColors; + } + + /** + * Returns whether the explosion has a flickering effect. + */ + public function willTwinkle() : bool{ + return $this->twinkle; + } + + /** + * Returns whether the particles have a trail effect. + */ + public function getTrail() : bool{ + return $this->trail; + } + + public function toCompoundTag() : CompoundTag{ + return CompoundTag::create() + ->setByte(self::TAG_TYPE, FireworkRocketTypeIdMap::getInstance()->toId($this->type)) + ->setByteArray(self::TAG_COLORS, self::encodeColors($this->colors)) + ->setByteArray(self::TAG_FADE_COLORS, self::encodeColors($this->fadeColors)) + ->setByte(self::TAG_TWINKLE, $this->twinkle ? 1 : 0) + ->setByte(self::TAG_TRAIL, $this->trail ? 1 : 0); + } +} diff --git a/src/item/FireworkRocketType.php b/src/item/FireworkRocketType.php new file mode 100644 index 000000000..6aa20e6d9 --- /dev/null +++ b/src/item/FireworkRocketType.php @@ -0,0 +1,46 @@ + new FireworkExplosionSound(), + self::LARGE_BALL => new FireworkLargeExplosionSound(), + }; + } +} diff --git a/src/item/FireworkStar.php b/src/item/FireworkStar.php new file mode 100644 index 000000000..ed2c3e525 --- /dev/null +++ b/src/item/FireworkStar.php @@ -0,0 +1,112 @@ +explosion = new FireworkRocketExplosion( + FireworkRocketType::SMALL_BALL, + colors: [DyeColor::BLACK], + fadeColors: [], + twinkle: false, + trail: false + ); + } + + public function getExplosion() : FireworkRocketExplosion{ + return $this->explosion; + } + + /** @return $this */ + public function setExplosion(FireworkRocketExplosion $explosion) : self{ + $this->explosion = $explosion; + return $this; + } + + /** + * Returns the displayed color of the item. + * The mixture of explosion colors, or the custom color if it is set. + */ + public function getColor() : Color{ + return $this->customColor ?? $this->explosion->getColorMix(); + } + + /** + * Returns the displayed custom color of the item that overrides + * the mixture of explosion colors, or null is it is not set. + */ + public function getCustomColor() : ?Color{ + return $this->customColor; + } + + /** + * Sets the displayed custom color of the item that overrides + * the mixture of explosion colors, or removes if $color is null. + * + * @return $this + */ + public function setCustomColor(?Color $color) : self{ + $this->customColor = $color; + return $this; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $explosionTag = $tag->getTag(self::TAG_EXPLOSION); + if(!$explosionTag instanceof CompoundTag){ + throw new SavedDataLoadingException("Missing explosion data"); + } + $this->explosion = FireworkRocketExplosion::fromCompoundTag($explosionTag); + + $customColor = Color::fromARGB(Binary::unsignInt($tag->getInt(self::TAG_CUSTOM_COLOR))); + $color = $this->explosion->getColorMix(); + if(!$customColor->equals($color)){ //check that $customColor is actually custom. + $this->customColor = $customColor; + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + $tag->setTag(self::TAG_EXPLOSION, $this->explosion->toCompoundTag()); + $tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->getColor()->toARGB())); + } +} diff --git a/src/item/HangingSign.php b/src/item/HangingSign.php new file mode 100644 index 000000000..a6752087a --- /dev/null +++ b/src/item/HangingSign.php @@ -0,0 +1,67 @@ +tryPlacementTransaction(clone $this->wallVariant, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + //ceiling edges sign has stricter placement conditions than ceiling center sign, so try that first + $ceilingEdgeTx = $player === null || !$player->isSneaking() ? + $this->tryPlacementTransaction(clone $this->edgePointCeilingVariant, $blockReplace, $blockClicked, $face, $clickVector, $player) : + null; + return $ceilingEdgeTx ?? $this->tryPlacementTransaction(clone $this->centerPointCeilingVariant, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function getBlock(?int $clickedFace = null) : Block{ + //we don't have enough information here to decide which ceiling type to use + return $clickedFace === Facing::DOWN ? clone $this->centerPointCeilingVariant : clone $this->wallVariant; + } + + public function getMaxStackSize() : int{ + return 16; + } + + public function getFuelTime() : int{ + return 200; + } +} diff --git a/src/item/Item.php b/src/item/Item.php index 205f15e13..e7c86e167 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -48,6 +48,7 @@ use pocketmine\nbt\tag\StringTag; use pocketmine\nbt\TreeRoot; use pocketmine\player\Player; use pocketmine\utils\Utils; +use pocketmine\world\BlockTransaction; use pocketmine\world\format\io\GlobalItemDataHandlers; use function base64_decode; use function base64_encode; @@ -292,9 +293,8 @@ class Item implements \JsonSerializable{ $display = $tag->getCompoundTag(self::TAG_DISPLAY); if($display !== null){ $this->customName = $display->getString(self::TAG_DISPLAY_NAME, $this->customName); - $lore = $display->getListTag(self::TAG_DISPLAY_LORE); - if($lore !== null && $lore->getTagType() === NBT::TAG_String){ - /** @var StringTag $t */ + $lore = $display->getListTag(self::TAG_DISPLAY_LORE, StringTag::class); + if($lore !== null){ foreach($lore as $t){ $this->lore[] = $t->getValue(); } @@ -302,9 +302,8 @@ class Item implements \JsonSerializable{ } $this->removeEnchantments(); - $enchantments = $tag->getListTag(self::TAG_ENCH); - if($enchantments !== null && $enchantments->getTagType() === NBT::TAG_Compound){ - /** @var CompoundTag $enchantment */ + $enchantments = $tag->getListTag(self::TAG_ENCH, CompoundTag::class); + if($enchantments !== null){ foreach($enchantments as $enchantment){ $magicNumber = $enchantment->getShort(self::TAG_ENCH_ID, -1); $level = $enchantment->getShort(self::TAG_ENCH_LVL, 0); @@ -321,17 +320,15 @@ class Item implements \JsonSerializable{ $this->blockEntityTag = $tag->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG); $this->canPlaceOn = []; - $canPlaceOn = $tag->getListTag(self::TAG_CAN_PLACE_ON); - if($canPlaceOn !== null && $canPlaceOn->getTagType() === NBT::TAG_String){ - /** @var StringTag $entry */ + $canPlaceOn = $tag->getListTag(self::TAG_CAN_PLACE_ON, StringTag::class); + if($canPlaceOn !== null){ foreach($canPlaceOn as $entry){ $this->canPlaceOn[$entry->getValue()] = $entry->getValue(); } } $this->canDestroy = []; - $canDestroy = $tag->getListTag(self::TAG_CAN_DESTROY); - if($canDestroy !== null && $canDestroy->getTagType() === NBT::TAG_String){ - /** @var StringTag $entry */ + $canDestroy = $tag->getListTag(self::TAG_CAN_DESTROY, StringTag::class); + if($canDestroy !== null){ foreach($canDestroy as $entry){ $this->canDestroy[$entry->getValue()] = $entry->getValue(); } @@ -485,6 +482,20 @@ class Item implements \JsonSerializable{ return $this->getBlock()->canBePlaced(); } + protected final function tryPlacementTransaction(Block $blockPlace, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player) : ?BlockTransaction{ + $position = $blockReplace->getPosition(); + $blockPlace->position($position->getWorld(), $position->getFloorX(), $position->getFloorY(), $position->getFloorZ()); + if(!$blockPlace->canBePlacedAt($blockReplace, $clickVector, $face, $blockReplace->getPosition()->equals($blockClicked->getPosition()))){ + return null; + } + $transaction = new BlockTransaction($position->getWorld()); + return $blockPlace->place($transaction, $this, $blockReplace, $blockClicked, $face, $clickVector, $player) ? $transaction : null; + } + + public function getPlacementTransaction(Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : ?BlockTransaction{ + return $this->tryPlacementTransaction($this->getBlock($face), $blockReplace, $blockClicked, $face, $clickVector, $player); + } + /** * Returns the block corresponding to this Item. */ diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index c93c23e81..3595d3afc 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -328,8 +328,29 @@ final class ItemTypeIds{ public const END_CRYSTAL = 20289; public const ICE_BOMB = 20290; public const RECOVERY_COMPASS = 20291; + public const PALE_OAK_SIGN = 20292; + public const RESIN_BRICK = 20293; + public const RECORD_RELIC = 20294; + public const RECORD_CREATOR = 20295; + public const RECORD_CREATOR_MUSIC_BOX = 20296; + public const RECORD_PRECIPICE = 20297; + public const OMINOUS_BANNER = 20298; + public const ACACIA_HANGING_SIGN = 20299; + public const BIRCH_HANGING_SIGN = 20300; + public const CHERRY_HANGING_SIGN = 20301; + public const CRIMSON_HANGING_SIGN = 20302; + public const DARK_OAK_HANGING_SIGN = 20303; + public const JUNGLE_HANGING_SIGN = 20304; + public const MANGROVE_HANGING_SIGN = 20305; + public const OAK_HANGING_SIGN = 20306; + public const PALE_OAK_HANGING_SIGN = 20307; + public const SPRUCE_HANGING_SIGN = 20308; + public const WARPED_HANGING_SIGN = 20309; + public const TRIDENT = 20310; + public const FIREWORK_ROCKET = 20311; + public const FIREWORK_STAR = 20312; - public const FIRST_UNUSED_ITEM_ID = 20292; + public const FIRST_UNUSED_ITEM_ID = 20313; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/LegacyStringToItemParser.php b/src/item/LegacyStringToItemParser.php index 6969190d5..19a6d1f6c 100644 --- a/src/item/LegacyStringToItemParser.php +++ b/src/item/LegacyStringToItemParser.php @@ -111,7 +111,8 @@ final class LegacyStringToItemParser{ */ public function parse(string $input) : Item{ $key = $this->reprocess($input); - $b = explode(":", $key); + //TODO: this should be limited to 2 parts, but 3 preserves old behaviour when given a string like 351:4:1 + $b = explode(":", $key, limit: 3); if(!isset($b[1])){ $meta = 0; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 09c93d5d9..63fa88538 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -243,6 +243,7 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("chiseled_polished_blackstone", fn() => Blocks::CHISELED_POLISHED_BLACKSTONE()); $result->registerBlock("chiseled_quartz", fn() => Blocks::CHISELED_QUARTZ()); $result->registerBlock("chiseled_red_sandstone", fn() => Blocks::CHISELED_RED_SANDSTONE()); + $result->registerBlock("chiseled_resin_bricks", fn() => Blocks::CHISELED_RESIN_BRICKS()); $result->registerBlock("chiseled_sandstone", fn() => Blocks::CHISELED_SANDSTONE()); $result->registerBlock("chiseled_stone_bricks", fn() => Blocks::CHISELED_STONE_BRICKS()); $result->registerBlock("chiseled_tuff", fn() => Blocks::CHISELED_TUFF()); @@ -872,6 +873,19 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("oxeye_daisy", fn() => Blocks::OXEYE_DAISY()); $result->registerBlock("packed_ice", fn() => Blocks::PACKED_ICE()); $result->registerBlock("packed_mud", fn() => Blocks::PACKED_MUD()); + $result->registerBlock("pale_oak_button", fn() => Blocks::PALE_OAK_BUTTON()); + $result->registerBlock("pale_oak_door", fn() => Blocks::PALE_OAK_DOOR()); + $result->registerBlock("pale_oak_fence", fn() => Blocks::PALE_OAK_FENCE()); + $result->registerBlock("pale_oak_fence_gate", fn() => Blocks::PALE_OAK_FENCE_GATE()); + $result->registerBlock("pale_oak_leaves", fn() => Blocks::PALE_OAK_LEAVES()); + $result->registerBlock("pale_oak_log", fn() => Blocks::PALE_OAK_LOG()->setStripped(false)); + $result->registerBlock("pale_oak_planks", fn() => Blocks::PALE_OAK_PLANKS()); + $result->registerBlock("pale_oak_pressure_plate", fn() => Blocks::PALE_OAK_PRESSURE_PLATE()); + $result->registerBlock("pale_oak_sign", fn() => Blocks::PALE_OAK_SIGN()); + $result->registerBlock("pale_oak_slab", fn() => Blocks::PALE_OAK_SLAB()); + $result->registerBlock("pale_oak_stairs", fn() => Blocks::PALE_OAK_STAIRS()); + $result->registerBlock("pale_oak_trapdoor", fn() => Blocks::PALE_OAK_TRAPDOOR()); + $result->registerBlock("pale_oak_wood", fn() => Blocks::PALE_OAK_WOOD()->setStripped(false)); $result->registerBlock("peony", fn() => Blocks::PEONY()); $result->registerBlock("pink_petals", fn() => Blocks::PINK_PETALS()); $result->registerBlock("pink_tulip", fn() => Blocks::PINK_TULIP()); @@ -972,6 +986,14 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("repeater", fn() => Blocks::REDSTONE_REPEATER()); $result->registerBlock("repeater_block", fn() => Blocks::REDSTONE_REPEATER()); $result->registerBlock("reserved6", fn() => Blocks::RESERVED6()); + $result->registerBlock("resin", fn() => Blocks::RESIN()); + $result->registerBlock("resin_block", fn() => Blocks::RESIN()); + $result->registerBlock("resin_brick_slab", fn() => Blocks::RESIN_BRICK_SLAB()); + $result->registerBlock("resin_brick_stairs", fn() => Blocks::RESIN_BRICK_STAIRS()); + $result->registerBlock("resin_brick_wall", fn() => Blocks::RESIN_BRICK_WALL()); + $result->registerBlock("resin_bricks", fn() => Blocks::RESIN_BRICKS()); + $result->registerBlock("resin_clump", fn() => Blocks::RESIN_CLUMP()); + $result->registerBlock("respawn_anchor", fn() => Blocks::RESPAWN_ANCHOR()); $result->registerBlock("rooted_dirt", fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED)); $result->registerBlock("rose", fn() => Blocks::POPPY()); $result->registerBlock("rose_bush", fn() => Blocks::ROSE_BUSH()); @@ -1084,6 +1106,8 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("stripped_mangrove_wood", fn() => Blocks::MANGROVE_WOOD()->setStripped(true)); $result->registerBlock("stripped_oak_log", fn() => Blocks::OAK_LOG()->setStripped(true)); $result->registerBlock("stripped_oak_wood", fn() => Blocks::OAK_WOOD()->setStripped(true)); + $result->registerBlock("stripped_pale_oak_log", fn() => Blocks::PALE_OAK_LOG()->setStripped(true)); + $result->registerBlock("stripped_pale_oak_wood", fn() => Blocks::PALE_OAK_WOOD()->setStripped(true)); $result->registerBlock("stripped_spruce_log", fn() => Blocks::SPRUCE_LOG()->setStripped(true)); $result->registerBlock("stripped_spruce_wood", fn() => Blocks::SPRUCE_WOOD()->setStripped(true)); $result->registerBlock("stripped_warped_hyphae", fn() => Blocks::WARPED_HYPHAE()->setStripped(true)); @@ -1208,6 +1232,7 @@ final class StringToItemParser extends StringToTParser{ private static function registerItems(self $result) : void{ $result->register("acacia_boat", fn() => Items::ACACIA_BOAT()); + $result->register("acacia_hanging_sign", fn() => Items::ACACIA_HANGING_SIGN()); $result->register("amethyst_shard", fn() => Items::AMETHYST_SHARD()); $result->register("antidote", fn() => Items::MEDICINE()->setType(MedicineType::ANTIDOTE)); $result->register("apple", fn() => Items::APPLE()); @@ -1222,6 +1247,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("beetroot_seeds", fn() => Items::BEETROOT_SEEDS()); $result->register("beetroot_soup", fn() => Items::BEETROOT_SOUP()); $result->register("birch_boat", fn() => Items::BIRCH_BOAT()); + $result->register("birch_hanging_sign", fn() => Items::BIRCH_HANGING_SIGN()); $result->register("blaze_powder", fn() => Items::BLAZE_POWDER()); $result->register("blaze_rod", fn() => Items::BLAZE_ROD()); $result->register("bleach", fn() => Items::BLEACH()); @@ -1283,6 +1309,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("chemical_sulphate", fn() => Items::CHEMICAL_SULPHATE()); $result->register("chemical_tungsten_chloride", fn() => Items::CHEMICAL_TUNGSTEN_CHLORIDE()); $result->register("chemical_water", fn() => Items::CHEMICAL_WATER()); + $result->register("cherry_hanging_sign", fn() => Items::CHERRY_HANGING_SIGN()); $result->register("chicken", fn() => Items::RAW_CHICKEN()); $result->register("chorus_fruit", fn() => Items::CHORUS_FRUIT()); $result->register("chorus_fruit_popped", fn() => Items::POPPED_CHORUS_FRUIT()); @@ -1307,7 +1334,9 @@ final class StringToItemParser extends StringToTParser{ $result->register("cooked_salmon", fn() => Items::COOKED_SALMON()); $result->register("cookie", fn() => Items::COOKIE()); $result->register("copper_ingot", fn() => Items::COPPER_INGOT()); + $result->register("crimson_hanging_sign", fn() => Items::CRIMSON_HANGING_SIGN()); $result->register("dark_oak_boat", fn() => Items::DARK_OAK_BOAT()); + $result->register("dark_oak_hanging_sign", fn() => Items::DARK_OAK_HANGING_SIGN()); $result->register("diamond", fn() => Items::DIAMOND()); $result->register("diamond_axe", fn() => Items::DIAMOND_AXE()); $result->register("diamond_boots", fn() => Items::DIAMOND_BOOTS()); @@ -1337,6 +1366,9 @@ final class StringToItemParser extends StringToTParser{ $result->register("eye_drops", fn() => Items::MEDICINE()->setType(MedicineType::EYE_DROPS)); $result->register("feather", fn() => Items::FEATHER()); $result->register("fermented_spider_eye", fn() => Items::FERMENTED_SPIDER_EYE()); + $result->register("firework_rocket", fn() => Items::FIREWORK_ROCKET()); + $result->register("firework_star", fn() => Items::FIREWORK_STAR()); + $result->register("fireworks", fn() => Items::FIREWORK_ROCKET()); $result->register("fire_charge", fn() => Items::FIRE_CHARGE()); $result->register("fish", fn() => Items::RAW_FISH()); $result->register("fishing_rod", fn() => Items::FISHING_ROD()); @@ -1392,6 +1424,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("iron_shovel", fn() => Items::IRON_SHOVEL()); $result->register("iron_sword", fn() => Items::IRON_SWORD()); $result->register("jungle_boat", fn() => Items::JUNGLE_BOAT()); + $result->register("jungle_hanging_sign", fn() => Items::JUNGLE_HANGING_SIGN()); $result->register("lapis_lazuli", fn() => Items::LAPIS_LAZULI()); $result->register("lava_bucket", fn() => Items::LAVA_BUCKET()); $result->register("leather", fn() => Items::LEATHER()); @@ -1403,6 +1436,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("leather_pants", fn() => Items::LEATHER_PANTS()); $result->register("leather_tunic", fn() => Items::LEATHER_TUNIC()); $result->register("magma_cream", fn() => Items::MAGMA_CREAM()); + $result->register("mangrove_hanging_sign", fn() => Items::MANGROVE_HANGING_SIGN()); $result->register("melon", fn() => Items::MELON()); $result->register("melon_seeds", fn() => Items::MELON_SEEDS()); $result->register("melon_slice", fn() => Items::MELON()); @@ -1434,7 +1468,9 @@ final class StringToItemParser extends StringToTParser{ $result->register("netherstar", fn() => Items::NETHER_STAR()); $result->register("netherite_upgrade_smithing_template", fn() => Items::NETHERITE_UPGRADE_SMITHING_TEMPLATE()); $result->register("oak_boat", fn() => Items::OAK_BOAT()); + $result->register("oak_hanging_sign", fn() => Items::OAK_HANGING_SIGN()); $result->register("painting", fn() => Items::PAINTING()); + $result->register("pale_oak_hanging_sign", fn() => Items::PALE_OAK_HANGING_SIGN()); $result->register("paper", fn() => Items::PAPER()); $result->register("phantom_membrane", fn() => Items::PHANTOM_MEMBRANE()); $result->register("pitcher_pod", fn() => Items::PITCHER_POD()); @@ -1472,11 +1508,15 @@ final class StringToItemParser extends StringToTParser{ $result->register("record_blocks", fn() => Items::RECORD_BLOCKS()); $result->register("record_cat", fn() => Items::RECORD_CAT()); $result->register("record_chirp", fn() => Items::RECORD_CHIRP()); + $result->register("record_creator", fn() => Items::RECORD_CREATOR()); + $result->register("record_creator_music_box", fn() => Items::RECORD_CREATOR_MUSIC_BOX()); $result->register("record_far", fn() => Items::RECORD_FAR()); $result->register("record_mall", fn() => Items::RECORD_MALL()); $result->register("record_mellohi", fn() => Items::RECORD_MELLOHI()); $result->register("record_otherside", fn() => Items::RECORD_OTHERSIDE()); $result->register("record_pigstep", fn() => Items::RECORD_PIGSTEP()); + $result->register("record_precipice", fn() => Items::RECORD_PRECIPICE()); + $result->register("record_relic", fn() => Items::RECORD_RELIC()); $result->register("record_stal", fn() => Items::RECORD_STAL()); $result->register("record_strad", fn() => Items::RECORD_STRAD()); $result->register("record_wait", fn() => Items::RECORD_WAIT()); @@ -1484,6 +1524,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("recovery_compass", fn() => Items::RECOVERY_COMPASS()); $result->register("redstone", fn() => Items::REDSTONE_DUST()); $result->register("redstone_dust", fn() => Items::REDSTONE_DUST()); + $result->register("resin_brick", fn() => Items::RESIN_BRICK()); $result->register("rib_armor_trim_smithing_template", fn() => Items::RIB_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("rotten_flesh", fn() => Items::ROTTEN_FLESH()); $result->register("salmon", fn() => Items::RAW_SALMON()); @@ -1503,6 +1544,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("spire_armor_trim_smithing_template", fn() => Items::SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("splash_potion", fn() => Items::SPLASH_POTION()); $result->register("spruce_boat", fn() => Items::SPRUCE_BOAT()); + $result->register("spruce_hanging_sign", fn() => Items::SPRUCE_HANGING_SIGN()); $result->register("spyglass", fn() => Items::SPYGLASS()); $result->register("squid_spawn_egg", fn() => Items::SQUID_SPAWN_EGG()); $result->register("steak", fn() => Items::STEAK()); @@ -1521,11 +1563,13 @@ final class StringToItemParser extends StringToTParser{ $result->register("torchflower_seeds", fn() => Items::TORCHFLOWER_SEEDS()); $result->register("tide_armor_trim_smithing_template", fn() => Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("totem", fn() => Items::TOTEM()); + $result->register("trident", fn() => Items::TRIDENT()); $result->register("turtle_helmet", fn() => Items::TURTLE_HELMET()); $result->register("vex_armor_trim_smithing_template", fn() => Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("turtle_shell_piece", fn() => Items::SCUTE()); $result->register("villager_spawn_egg", fn() => Items::VILLAGER_SPAWN_EGG()); $result->register("ward_armor_trim_smithing_template", fn() => Items::WARD_ARMOR_TRIM_SMITHING_TEMPLATE()); + $result->register("warped_hanging_sign", fn() => Items::WARPED_HANGING_SIGN()); $result->register("water_bucket", fn() => Items::WATER_BUCKET()); $result->register("wayfinder_armor_trim_smithing_template", fn() => Items::WAYFINDER_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("wheat", fn() => Items::WHEAT()); diff --git a/src/item/Trident.php b/src/item/Trident.php new file mode 100644 index 000000000..991f45b1d --- /dev/null +++ b/src/item/Trident.php @@ -0,0 +1,93 @@ +getLocation(); + + $diff = $player->getItemUseDuration(); + if($diff < 14){ + return ItemUseResult::FAIL; + } + + $item = $this->pop(); + if($player->hasFiniteResources()){ + $item->applyDamage(1); + } + $entity = new TridentEntity(Location::fromObject( + $player->getEyePos(), + $player->getWorld(), + ($location->yaw > 180 ? 360 : 0) - $location->yaw, + -$location->pitch + ), $item, $player); + $p = $diff / 20; + $baseForce = min((($p ** 2) + $p * 2) / 3, 1) * 2.4; + $entity->setMotion($player->getDirectionVector()->multiply($baseForce)); + + $ev = new ProjectileLaunchEvent($entity); + $ev->call(); + if($ev->isCancelled()){ + $ev->getEntity()->flagForDespawn(); + return ItemUseResult::FAIL; + } + $ev->getEntity()->spawnToAll(); + $location->getWorld()->addSound($location, new TridentThrowSound()); + + return ItemUseResult::SUCCESS; + } + + public function getAttackPoints() : int{ + return 9; + } + + public function canStartUsingItem(Player $player) : bool{ + return $this->damage < $this->getMaxDurability(); + } + + public function onAttackEntity(Entity $victim, array &$returnedItems) : bool{ + return $this->applyDamage(1); + } + + public function onDestroyBlock(Block $block, array &$returnedItems) : bool{ + if(!$block->getBreakInfo()->breaksInstantly()){ + return $this->applyDamage(2); + } + return false; + } +} diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index 6768ed8f0..31a62d1aa 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -48,6 +48,7 @@ use function strtolower; * @generate-registry-docblock * * @method static Boat ACACIA_BOAT() + * @method static HangingSign ACACIA_HANGING_SIGN() * @method static ItemBlockWallOrFloor ACACIA_SIGN() * @method static ItemBlock AIR() * @method static Item AMETHYST_SHARD() @@ -60,6 +61,7 @@ use function strtolower; * @method static BeetrootSeeds BEETROOT_SEEDS() * @method static BeetrootSoup BEETROOT_SOUP() * @method static Boat BIRCH_BOAT() + * @method static HangingSign BIRCH_HANGING_SIGN() * @method static ItemBlockWallOrFloor BIRCH_SIGN() * @method static Item BLAZE_POWDER() * @method static BlazeRod BLAZE_ROD() @@ -116,6 +118,7 @@ use function strtolower; * @method static Item CHEMICAL_SULPHATE() * @method static Item CHEMICAL_TUNGSTEN_CHLORIDE() * @method static Item CHEMICAL_WATER() + * @method static HangingSign CHERRY_HANGING_SIGN() * @method static ItemBlockWallOrFloor CHERRY_SIGN() * @method static ChorusFruit CHORUS_FRUIT() * @method static Item CLAY() @@ -134,8 +137,10 @@ use function strtolower; * @method static Cookie COOKIE() * @method static Item COPPER_INGOT() * @method static CoralFan CORAL_FAN() + * @method static HangingSign CRIMSON_HANGING_SIGN() * @method static ItemBlockWallOrFloor CRIMSON_SIGN() * @method static Boat DARK_OAK_BOAT() + * @method static HangingSign DARK_OAK_HANGING_SIGN() * @method static ItemBlockWallOrFloor DARK_OAK_SIGN() * @method static Item DIAMOND() * @method static Axe DIAMOND_AXE() @@ -163,6 +168,8 @@ use function strtolower; * @method static Item EYE_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static Item FEATHER() * @method static Item FERMENTED_SPIDER_EYE() + * @method static FireworkRocket FIREWORK_ROCKET() + * @method static FireworkStar FIREWORK_STAR() * @method static FireCharge FIRE_CHARGE() * @method static FishingRod FISHING_ROD() * @method static Item FLINT() @@ -206,6 +213,7 @@ use function strtolower; * @method static Shovel IRON_SHOVEL() * @method static Sword IRON_SWORD() * @method static Boat JUNGLE_BOAT() + * @method static HangingSign JUNGLE_HANGING_SIGN() * @method static ItemBlockWallOrFloor JUNGLE_SIGN() * @method static Item LAPIS_LAZULI() * @method static LiquidBucket LAVA_BUCKET() @@ -216,6 +224,7 @@ use function strtolower; * @method static Armor LEATHER_TUNIC() * @method static Item MAGMA_CREAM() * @method static Boat MANGROVE_BOAT() + * @method static HangingSign MANGROVE_HANGING_SIGN() * @method static ItemBlockWallOrFloor MANGROVE_SIGN() * @method static Medicine MEDICINE() * @method static Melon MELON() @@ -241,8 +250,12 @@ use function strtolower; * @method static Item NETHER_QUARTZ() * @method static Item NETHER_STAR() * @method static Boat OAK_BOAT() + * @method static HangingSign OAK_HANGING_SIGN() * @method static ItemBlockWallOrFloor OAK_SIGN() + * @method static ItemBlockWallOrFloor OMINOUS_BANNER() * @method static PaintingItem PAINTING() + * @method static HangingSign PALE_OAK_HANGING_SIGN() + * @method static ItemBlockWallOrFloor PALE_OAK_SIGN() * @method static Item PAPER() * @method static Item PHANTOM_MEMBRANE() * @method static PitcherPod PITCHER_POD() @@ -275,17 +288,22 @@ use function strtolower; * @method static Record RECORD_BLOCKS() * @method static Record RECORD_CAT() * @method static Record RECORD_CHIRP() + * @method static Record RECORD_CREATOR() + * @method static Record RECORD_CREATOR_MUSIC_BOX() * @method static Record RECORD_FAR() * @method static Record RECORD_MALL() * @method static Record RECORD_MELLOHI() * @method static Record RECORD_OTHERSIDE() * @method static Record RECORD_PIGSTEP() + * @method static Record RECORD_PRECIPICE() + * @method static Record RECORD_RELIC() * @method static Record RECORD_STAL() * @method static Record RECORD_STRAD() * @method static Record RECORD_WAIT() * @method static Record RECORD_WARD() * @method static Item RECOVERY_COMPASS() * @method static Redstone REDSTONE_DUST() + * @method static Item RESIN_BRICK() * @method static Item RIB_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static RottenFlesh ROTTEN_FLESH() * @method static Item SCUTE() @@ -301,6 +319,7 @@ use function strtolower; * @method static Item SPIRE_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static SplashPotion SPLASH_POTION() * @method static Boat SPRUCE_BOAT() + * @method static HangingSign SPRUCE_HANGING_SIGN() * @method static ItemBlockWallOrFloor SPRUCE_SIGN() * @method static Spyglass SPYGLASS() * @method static SpawnEgg SQUID_SPAWN_EGG() @@ -318,10 +337,12 @@ use function strtolower; * @method static Item TIDE_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static TorchflowerSeeds TORCHFLOWER_SEEDS() * @method static Totem TOTEM() + * @method static Trident TRIDENT() * @method static TurtleHelmet TURTLE_HELMET() * @method static Item VEX_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static SpawnEgg VILLAGER_SPAWN_EGG() * @method static Item WARD_ARMOR_TRIM_SMITHING_TEMPLATE() + * @method static HangingSign WARPED_HANGING_SIGN() * @method static ItemBlockWallOrFloor WARPED_SIGN() * @method static LiquidBucket WATER_BUCKET() * @method static Item WAYFINDER_ARMOR_TRIM_SMITHING_TEMPLATE() @@ -391,6 +412,7 @@ final class VanillaItems{ self::_registryRegister("air", Blocks::AIR()->asItem()->setCount(0)); self::register("acacia_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::ACACIA_SIGN(), Blocks::ACACIA_WALL_SIGN())); + self::register("acacia_hanging_sign", fn(IID $id) => new HangingSign($id, "Acacia Hanging Sign", Blocks::ACACIA_CEILING_CENTER_HANGING_SIGN(), Blocks::ACACIA_CEILING_EDGES_HANGING_SIGN(), Blocks::ACACIA_WALL_HANGING_SIGN())); self::register("amethyst_shard", fn(IID $id) => new Item($id, "Amethyst Shard")); self::register("apple", fn(IID $id) => new Apple($id, "Apple")); self::register("arrow", fn(IID $id) => new Arrow($id, "Arrow")); @@ -401,6 +423,7 @@ final class VanillaItems{ self::register("beetroot_seeds", fn(IID $id) => new BeetrootSeeds($id, "Beetroot Seeds")); self::register("beetroot_soup", fn(IID $id) => new BeetrootSoup($id, "Beetroot Soup")); self::register("birch_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::BIRCH_SIGN(), Blocks::BIRCH_WALL_SIGN())); + self::register("birch_hanging_sign", fn(IID $id) => new HangingSign($id, "Birch Hanging Sign", Blocks::BIRCH_CEILING_CENTER_HANGING_SIGN(), Blocks::BIRCH_CEILING_EDGES_HANGING_SIGN(), Blocks::BIRCH_WALL_HANGING_SIGN())); self::register("blaze_powder", fn(IID $id) => new Item($id, "Blaze Powder")); self::register("blaze_rod", fn(IID $id) => new BlazeRod($id, "Blaze Rod")); self::register("bleach", fn(IID $id) => new Item($id, "Bleach")); @@ -415,6 +438,7 @@ final class VanillaItems{ self::register("carrot", fn(IID $id) => new Carrot($id, "Carrot")); self::register("charcoal", fn(IID $id) => new Coal($id, "Charcoal")); self::register("cherry_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::CHERRY_SIGN(), Blocks::CHERRY_WALL_SIGN())); + self::register("cherry_hanging_sign", fn(IID $id) => new HangingSign($id, "Cherry Hanging Sign", Blocks::CHERRY_CEILING_CENTER_HANGING_SIGN(), Blocks::CHERRY_CEILING_EDGES_HANGING_SIGN(), Blocks::CHERRY_WALL_HANGING_SIGN())); self::register("chemical_aluminium_oxide", fn(IID $id) => new Item($id, "Aluminium Oxide")); self::register("chemical_ammonia", fn(IID $id) => new Item($id, "Ammonia")); self::register("chemical_barium_sulphate", fn(IID $id) => new Item($id, "Barium Sulphate")); @@ -470,7 +494,9 @@ final class VanillaItems{ self::register("copper_ingot", fn(IID $id) => new Item($id, "Copper Ingot")); self::register("coral_fan", fn(IID $id) => new CoralFan($id)); self::register("crimson_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::CRIMSON_SIGN(), Blocks::CRIMSON_WALL_SIGN())); + self::register("crimson_hanging_sign", fn(IID $id) => new HangingSign($id, "Crimson Hanging Sign", Blocks::CRIMSON_CEILING_CENTER_HANGING_SIGN(), Blocks::CRIMSON_CEILING_EDGES_HANGING_SIGN(), Blocks::CRIMSON_WALL_HANGING_SIGN())); self::register("dark_oak_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::DARK_OAK_SIGN(), Blocks::DARK_OAK_WALL_SIGN())); + self::register("dark_oak_hanging_sign", fn(IID $id) => new HangingSign($id, "Dark Oak Hanging Sign", Blocks::DARK_OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::DARK_OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::DARK_OAK_WALL_HANGING_SIGN())); self::register("diamond", fn(IID $id) => new Item($id, "Diamond")); self::register("disc_fragment_5", fn(IID $id) => new Item($id, "Disc Fragment (5)")); self::register("dragon_breath", fn(IID $id) => new Item($id, "Dragon's Breath")); @@ -487,6 +513,8 @@ final class VanillaItems{ self::register("experience_bottle", fn(IID $id) => new ExperienceBottle($id, "Bottle o' Enchanting")); self::register("feather", fn(IID $id) => new Item($id, "Feather")); self::register("fermented_spider_eye", fn(IID $id) => new Item($id, "Fermented Spider Eye")); + self::register("firework_rocket", fn(IID $id) => new FireworkRocket($id, "Firework Rocket")); + self::register("firework_star", fn(IID $id) => new FireworkStar($id, "Firework Star")); self::register("fire_charge", fn(IID $id) => new FireCharge($id, "Fire Charge")); self::register("fishing_rod", fn(IID $id) => new FishingRod($id, "Fishing Rod", [EnchantmentTags::FISHING_ROD])); self::register("flint", fn(IID $id) => new Item($id, "Flint")); @@ -511,11 +539,13 @@ final class VanillaItems{ self::register("iron_ingot", fn(IID $id) => new Item($id, "Iron Ingot")); self::register("iron_nugget", fn(IID $id) => new Item($id, "Iron Nugget")); self::register("jungle_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::JUNGLE_SIGN(), Blocks::JUNGLE_WALL_SIGN())); + self::register("jungle_hanging_sign", fn(IID $id) => new HangingSign($id, "Jungle Hanging Sign", Blocks::JUNGLE_CEILING_CENTER_HANGING_SIGN(), Blocks::JUNGLE_CEILING_EDGES_HANGING_SIGN(), Blocks::JUNGLE_WALL_HANGING_SIGN())); self::register("lapis_lazuli", fn(IID $id) => new Item($id, "Lapis Lazuli")); self::register("lava_bucket", fn(IID $id) => new LiquidBucket($id, "Lava Bucket", Blocks::LAVA())); self::register("leather", fn(IID $id) => new Item($id, "Leather")); self::register("magma_cream", fn(IID $id) => new Item($id, "Magma Cream")); self::register("mangrove_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::MANGROVE_SIGN(), Blocks::MANGROVE_WALL_SIGN())); + self::register("mangrove_hanging_sign", fn(IID $id) => new HangingSign($id, "Mangrove Hanging Sign", Blocks::MANGROVE_CEILING_CENTER_HANGING_SIGN(), Blocks::MANGROVE_CEILING_EDGES_HANGING_SIGN(), Blocks::MANGROVE_WALL_HANGING_SIGN())); self::register("medicine", fn(IID $id) => new Medicine($id, "Medicine")); self::register("melon", fn(IID $id) => new Melon($id, "Melon")); self::register("melon_seeds", fn(IID $id) => new MelonSeeds($id, "Melon Seeds")); @@ -534,7 +564,11 @@ final class VanillaItems{ public function isFireProof() : bool{ return true; } }); self::register("oak_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::OAK_SIGN(), Blocks::OAK_WALL_SIGN())); + self::register("oak_hanging_sign", fn(IID $id) => new HangingSign($id, "Oak Hanging Sign", Blocks::OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::OAK_WALL_HANGING_SIGN())); + self::register("ominous_banner", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::OMINOUS_BANNER(), Blocks::OMINOUS_WALL_BANNER())); self::register("painting", fn(IID $id) => new PaintingItem($id, "Painting")); + self::register("pale_oak_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::PALE_OAK_SIGN(), Blocks::PALE_OAK_WALL_SIGN())); + self::register("pale_oak_hanging_sign", fn(IID $id) => new HangingSign($id, "Pale Oak Hanging Sign", Blocks::PALE_OAK_CEILING_CENTER_HANGING_SIGN(), Blocks::PALE_OAK_CEILING_EDGES_HANGING_SIGN(), Blocks::PALE_OAK_WALL_HANGING_SIGN())); self::register("paper", fn(IID $id) => new Item($id, "Paper")); self::register("phantom_membrane", fn(IID $id) => new Item($id, "Phantom Membrane")); self::register("pitcher_pod", fn(IID $id) => new PitcherPod($id, "Pitcher Pod")); @@ -566,17 +600,22 @@ final class VanillaItems{ self::register("record_blocks", fn(IID $id) => new Record($id, RecordType::DISK_BLOCKS, "Record Blocks")); self::register("record_cat", fn(IID $id) => new Record($id, RecordType::DISK_CAT, "Record Cat")); self::register("record_chirp", fn(IID $id) => new Record($id, RecordType::DISK_CHIRP, "Record Chirp")); + self::register("record_creator", fn(IID $id) => new Record($id, RecordType::DISK_CREATOR, "Record Creator")); + self::register("record_creator_music_box", fn(IID $id) => new Record($id, RecordType::DISK_CREATOR_MUSIC_BOX, "Record Creator (Music Box)")); self::register("record_far", fn(IID $id) => new Record($id, RecordType::DISK_FAR, "Record Far")); self::register("record_mall", fn(IID $id) => new Record($id, RecordType::DISK_MALL, "Record Mall")); self::register("record_mellohi", fn(IID $id) => new Record($id, RecordType::DISK_MELLOHI, "Record Mellohi")); self::register("record_otherside", fn(IID $id) => new Record($id, RecordType::DISK_OTHERSIDE, "Record Otherside")); self::register("record_pigstep", fn(IID $id) => new Record($id, RecordType::DISK_PIGSTEP, "Record Pigstep")); + self::register("record_precipice", fn(IID $id) => new Record($id, RecordType::DISK_PRECIPICE, "Record Precipice")); + self::register("record_relic", fn(IID $id) => new Record($id, RecordType::DISK_RELIC, "Record Relic")); self::register("record_stal", fn(IID $id) => new Record($id, RecordType::DISK_STAL, "Record Stal")); self::register("record_strad", fn(IID $id) => new Record($id, RecordType::DISK_STRAD, "Record Strad")); self::register("record_wait", fn(IID $id) => new Record($id, RecordType::DISK_WAIT, "Record Wait")); self::register("record_ward", fn(IID $id) => new Record($id, RecordType::DISK_WARD, "Record Ward")); self::register("recovery_compass", fn(IID $id) => new Item($id, "Recovery Compass")); self::register("redstone_dust", fn(IID $id) => new Redstone($id, "Redstone")); + self::register("resin_brick", fn(IID $id) => new Item($id, "Resin Brick")); self::register("rotten_flesh", fn(IID $id) => new RottenFlesh($id, "Rotten Flesh")); self::register("scute", fn(IID $id) => new Item($id, "Scute")); self::register("shears", fn(IID $id) => new Shears($id, "Shears", [EnchantmentTags::SHEARS])); @@ -586,6 +625,7 @@ final class VanillaItems{ self::register("spider_eye", fn(IID $id) => new SpiderEye($id, "Spider Eye")); self::register("splash_potion", fn(IID $id) => new SplashPotion($id, "Splash Potion")); self::register("spruce_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::SPRUCE_SIGN(), Blocks::SPRUCE_WALL_SIGN())); + self::register("spruce_hanging_sign", fn(IID $id) => new HangingSign($id, "Spruce Hanging Sign", Blocks::SPRUCE_CEILING_CENTER_HANGING_SIGN(), Blocks::SPRUCE_CEILING_EDGES_HANGING_SIGN(), Blocks::SPRUCE_WALL_HANGING_SIGN())); self::register("spyglass", fn(IID $id) => new Spyglass($id, "Spyglass")); self::register("steak", fn(IID $id) => new Steak($id, "Steak")); self::register("stick", fn(IID $id) => new Stick($id, "Stick")); @@ -595,7 +635,9 @@ final class VanillaItems{ self::register("sweet_berries", fn(IID $id) => new SweetBerries($id, "Sweet Berries")); self::register("torchflower_seeds", fn(IID $id) => new TorchflowerSeeds($id, "Torchflower Seeds")); self::register("totem", fn(IID $id) => new Totem($id, "Totem of Undying")); + self::register("trident", fn(IID $id) => new Trident($id, "Trident")); self::register("warped_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::WARPED_SIGN(), Blocks::WARPED_WALL_SIGN())); + self::register("warped_hanging_sign", fn(IID $id) => new HangingSign($id, "Warped Hanging Sign", Blocks::WARPED_CEILING_CENTER_HANGING_SIGN(), Blocks::WARPED_CEILING_EDGES_HANGING_SIGN(), Blocks::WARPED_WALL_HANGING_SIGN())); self::register("water_bucket", fn(IID $id) => new LiquidBucket($id, "Water Bucket", Blocks::WATER())); self::register("wheat", fn(IID $id) => new Item($id, "Wheat")); self::register("wheat_seeds", fn(IID $id) => new WheatSeeds($id, "Wheat Seeds")); diff --git a/src/item/WritableBookBase.php b/src/item/WritableBookBase.php index 6b7e55468..de6d5414c 100644 --- a/src/item/WritableBookBase.php +++ b/src/item/WritableBookBase.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; @@ -101,8 +100,9 @@ abstract class WritableBookBase extends Item{ * @return $this */ public function deletePage(int $pageId) : self{ - unset($this->pages[$pageId]); - $this->pages = array_values($this->pages); + $newPages = $this->pages; + unset($newPages[$pageId]); + $this->pages = array_values($newPages); return $this; } @@ -166,14 +166,12 @@ abstract class WritableBookBase extends Item{ $pages = $tag->getListTag(self::TAG_PAGES); if($pages !== null){ - if($pages->getTagType() === NBT::TAG_Compound){ //PE format - /** @var CompoundTag $page */ - foreach($pages as $page){ + if(($compoundPages = $pages->cast(CompoundTag::class)) !== null){ //PE format + foreach($compoundPages as $page){ $this->pages[] = new WritableBookPage(mb_scrub($page->getString(self::TAG_PAGE_TEXT), 'UTF-8'), $page->getString(self::TAG_PAGE_PHOTONAME, "")); } - }elseif($pages->getTagType() === NBT::TAG_String){ //PC format - /** @var StringTag $page */ - foreach($pages as $page){ + }elseif(($stringPages = $pages->cast(StringTag::class)) !== null){ //PC format + foreach($stringPages as $page){ $this->pages[] = new WritableBookPage(mb_scrub($page->getValue(), 'UTF-8')); } } diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php index eed7bff52..2d8dafa4b 100644 --- a/src/item/enchantment/AvailableEnchantmentRegistry.php +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -28,6 +28,7 @@ use pocketmine\item\enchantment\ItemEnchantmentTags as Tags; 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 @@ final class AvailableEnchantmentRegistry{ 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 @@ final class AvailableEnchantmentRegistry{ 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/item/enchantment/ItemEnchantmentTagRegistry.php b/src/item/enchantment/ItemEnchantmentTagRegistry.php index 210cd8e86..b239f18a2 100644 --- a/src/item/enchantment/ItemEnchantmentTagRegistry.php +++ b/src/item/enchantment/ItemEnchantmentTagRegistry.php @@ -32,6 +32,7 @@ use function array_merge; use function array_search; use function array_shift; use function array_unique; +use function array_values; use function count; /** @@ -103,7 +104,8 @@ final class ItemEnchantmentTagRegistry{ foreach(Utils::stringifyKeys($this->tagMap) as $key => $nestedTags){ if(($nestedKey = array_search($tag, $nestedTags, true)) !== false){ - unset($this->tagMap[$key][$nestedKey]); + unset($nestedTags[$nestedKey]); + $this->tagMap[$key] = array_values($nestedTags); } } } @@ -115,7 +117,7 @@ final class ItemEnchantmentTagRegistry{ */ public function removeNested(string $tag, array $nestedTags) : void{ $this->assertNotInternalTag($tag); - $this->tagMap[$tag] = array_diff($this->tagMap[$tag], $nestedTags); + $this->tagMap[$tag] = array_values(array_diff($this->tagMap[$tag], $nestedTags)); } /** diff --git a/src/item/enchantment/ProtectionEnchantment.php b/src/item/enchantment/ProtectionEnchantment.php index be78a2306..817466875 100644 --- a/src/item/enchantment/ProtectionEnchantment.php +++ b/src/item/enchantment/ProtectionEnchantment.php @@ -25,18 +25,22 @@ namespace pocketmine\item\enchantment; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\lang\Translatable; -use function array_flip; +use function array_fill_keys; use function floor; class ProtectionEnchantment extends Enchantment{ protected float $typeModifier; - /** @var int[]|null */ + /** + * @var true[]|null + * @phpstan-var array + */ protected ?array $applicableDamageTypes = null; /** * ProtectionEnchantment constructor. * * @phpstan-param null|(\Closure(int $level) : int) $minEnchantingPower + * @phpstan-param list|null $applicableDamageTypes * * @param int $primaryItemFlags @deprecated * @param int $secondaryItemFlags @deprecated @@ -48,7 +52,7 @@ class ProtectionEnchantment extends Enchantment{ $this->typeModifier = $typeModifier; if($applicableDamageTypes !== null){ - $this->applicableDamageTypes = array_flip($applicableDamageTypes); + $this->applicableDamageTypes = array_fill_keys($applicableDamageTypes, true); } } diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php index f0b70ffd1..eadd74f32 100644 --- a/src/lang/KnownTranslationFactory.php +++ b/src/lang/KnownTranslationFactory.php @@ -897,6 +897,18 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ENCHANTMENT_FROSTWALKER, []); } + public static function enchantment_heavy_weapon_breach() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_BREACH, []); + } + + public static function enchantment_heavy_weapon_density() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_DENSITY, []); + } + + public static function enchantment_heavy_weapon_windburst() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_WINDBURST, []); + } + public static function enchantment_knockback() : Translatable{ return new Translatable(KnownTranslationKeys::ENCHANTMENT_KNOCKBACK, []); } @@ -1108,6 +1120,318 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::ITEM_RECORD_WARD_DESC, []); } + public static function itemGroup_name_anvil() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ANVIL, []); + } + + public static function itemGroup_name_arrow() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ARROW, []); + } + + public static function itemGroup_name_axe() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_AXE, []); + } + + public static function itemGroup_name_banner() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BANNER, []); + } + + public static function itemGroup_name_banner_pattern() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BANNER_PATTERN, []); + } + + public static function itemGroup_name_bed() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BED, []); + } + + public static function itemGroup_name_boat() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BOAT, []); + } + + public static function itemGroup_name_boots() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BOOTS, []); + } + + public static function itemGroup_name_bundles() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BUNDLES, []); + } + + public static function itemGroup_name_buttons() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BUTTONS, []); + } + + public static function itemGroup_name_candles() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CANDLES, []); + } + + public static function itemGroup_name_chalkboard() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHALKBOARD, []); + } + + public static function itemGroup_name_chest() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHEST, []); + } + + public static function itemGroup_name_chestboat() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHESTBOAT, []); + } + + public static function itemGroup_name_chestplate() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHESTPLATE, []); + } + + public static function itemGroup_name_compounds() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_COMPOUNDS, []); + } + + public static function itemGroup_name_concrete() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CONCRETE, []); + } + + public static function itemGroup_name_concretePowder() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CONCRETEPOWDER, []); + } + + public static function itemGroup_name_cookedFood() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_COOKEDFOOD, []); + } + + public static function itemGroup_name_coral() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CORAL, []); + } + + public static function itemGroup_name_coral_decorations() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CORAL_DECORATIONS, []); + } + + public static function itemGroup_name_crop() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CROP, []); + } + + public static function itemGroup_name_door() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_DOOR, []); + } + + public static function itemGroup_name_dye() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_DYE, []); + } + + public static function itemGroup_name_enchantedBook() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ENCHANTEDBOOK, []); + } + + public static function itemGroup_name_fence() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FENCE, []); + } + + public static function itemGroup_name_fenceGate() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FENCEGATE, []); + } + + public static function itemGroup_name_firework() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FIREWORK, []); + } + + public static function itemGroup_name_fireworkStars() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FIREWORKSTARS, []); + } + + public static function itemGroup_name_flower() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FLOWER, []); + } + + public static function itemGroup_name_glass() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLASS, []); + } + + public static function itemGroup_name_glassPane() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLASSPANE, []); + } + + public static function itemGroup_name_glazedTerracotta() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLAZEDTERRACOTTA, []); + } + + public static function itemGroup_name_goatHorn() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GOATHORN, []); + } + + public static function itemGroup_name_grass() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GRASS, []); + } + + public static function itemGroup_name_helmet() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HELMET, []); + } + + public static function itemGroup_name_hoe() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HOE, []); + } + + public static function itemGroup_name_horseArmor() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HORSEARMOR, []); + } + + public static function itemGroup_name_leaves() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LEAVES, []); + } + + public static function itemGroup_name_leggings() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LEGGINGS, []); + } + + public static function itemGroup_name_lingeringPotion() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LINGERINGPOTION, []); + } + + public static function itemGroup_name_log() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LOG, []); + } + + public static function itemGroup_name_minecart() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MINECART, []); + } + + public static function itemGroup_name_miscFood() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MISCFOOD, []); + } + + public static function itemGroup_name_mobEgg() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MOBEGG, []); + } + + public static function itemGroup_name_monsterStoneEgg() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MONSTERSTONEEGG, []); + } + + public static function itemGroup_name_mushroom() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MUSHROOM, []); + } + + public static function itemGroup_name_netherWartBlock() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_NETHERWARTBLOCK, []); + } + + public static function itemGroup_name_ominousBottle() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_OMINOUSBOTTLE, []); + } + + public static function itemGroup_name_ore() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ORE, []); + } + + public static function itemGroup_name_permission() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PERMISSION, []); + } + + public static function itemGroup_name_pickaxe() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PICKAXE, []); + } + + public static function itemGroup_name_planks() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PLANKS, []); + } + + public static function itemGroup_name_potion() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_POTION, []); + } + + public static function itemGroup_name_pressurePlate() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PRESSUREPLATE, []); + } + + public static function itemGroup_name_products() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PRODUCTS, []); + } + + public static function itemGroup_name_rail() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RAIL, []); + } + + public static function itemGroup_name_rawFood() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RAWFOOD, []); + } + + public static function itemGroup_name_record() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RECORD, []); + } + + public static function itemGroup_name_sandstone() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SANDSTONE, []); + } + + public static function itemGroup_name_sapling() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SAPLING, []); + } + + public static function itemGroup_name_seed() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SEED, []); + } + + public static function itemGroup_name_shovel() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SHOVEL, []); + } + + public static function itemGroup_name_shulkerBox() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SHULKERBOX, []); + } + + public static function itemGroup_name_sign() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SIGN, []); + } + + public static function itemGroup_name_skull() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SKULL, []); + } + + public static function itemGroup_name_slab() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SLAB, []); + } + + public static function itemGroup_name_splashPotion() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SPLASHPOTION, []); + } + + public static function itemGroup_name_stainedClay() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STAINEDCLAY, []); + } + + public static function itemGroup_name_stairs() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STAIRS, []); + } + + public static function itemGroup_name_stone() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STONE, []); + } + + public static function itemGroup_name_stoneBrick() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STONEBRICK, []); + } + + public static function itemGroup_name_sword() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SWORD, []); + } + + public static function itemGroup_name_trapdoor() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_TRAPDOOR, []); + } + + public static function itemGroup_name_walls() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WALLS, []); + } + + public static function itemGroup_name_wood() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOD, []); + } + + public static function itemGroup_name_wool() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOL, []); + } + + public static function itemGroup_name_woolCarpet() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOLCARPET, []); + } + public static function kick_admin() : Translatable{ return new Translatable(KnownTranslationKeys::KICK_ADMIN, []); } @@ -2711,6 +3035,14 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::TILE_BED_TOOFAR, []); } + public static function tile_respawn_anchor_notValid() : Translatable{ + return new Translatable(KnownTranslationKeys::TILE_RESPAWN_ANCHOR_NOTVALID, []); + } + + public static function tile_respawn_anchor_respawnSet() : Translatable{ + return new Translatable(KnownTranslationKeys::TILE_RESPAWN_ANCHOR_RESPAWNSET, []); + } + public static function view_distance() : Translatable{ return new Translatable(KnownTranslationKeys::VIEW_DISTANCE, []); } diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php index 1c1fef20f..44a64c489 100644 --- a/src/lang/KnownTranslationKeys.php +++ b/src/lang/KnownTranslationKeys.php @@ -194,6 +194,9 @@ final class KnownTranslationKeys{ public const ENCHANTMENT_FIRE = "enchantment.fire"; public const ENCHANTMENT_FISHINGSPEED = "enchantment.fishingSpeed"; public const ENCHANTMENT_FROSTWALKER = "enchantment.frostwalker"; + public const ENCHANTMENT_HEAVY_WEAPON_BREACH = "enchantment.heavy_weapon.breach"; + public const ENCHANTMENT_HEAVY_WEAPON_DENSITY = "enchantment.heavy_weapon.density"; + public const ENCHANTMENT_HEAVY_WEAPON_WINDBURST = "enchantment.heavy_weapon.windburst"; public const ENCHANTMENT_KNOCKBACK = "enchantment.knockback"; public const ENCHANTMENT_LOOTBONUS = "enchantment.lootBonus"; public const ENCHANTMENT_LOOTBONUSDIGGER = "enchantment.lootBonusDigger"; @@ -245,6 +248,84 @@ final class KnownTranslationKeys{ public const ITEM_RECORD_STRAD_DESC = "item.record_strad.desc"; public const ITEM_RECORD_WAIT_DESC = "item.record_wait.desc"; public const ITEM_RECORD_WARD_DESC = "item.record_ward.desc"; + public const ITEMGROUP_NAME_ANVIL = "itemGroup.name.anvil"; + public const ITEMGROUP_NAME_ARROW = "itemGroup.name.arrow"; + public const ITEMGROUP_NAME_AXE = "itemGroup.name.axe"; + public const ITEMGROUP_NAME_BANNER = "itemGroup.name.banner"; + public const ITEMGROUP_NAME_BANNER_PATTERN = "itemGroup.name.banner_pattern"; + public const ITEMGROUP_NAME_BED = "itemGroup.name.bed"; + public const ITEMGROUP_NAME_BOAT = "itemGroup.name.boat"; + public const ITEMGROUP_NAME_BOOTS = "itemGroup.name.boots"; + public const ITEMGROUP_NAME_BUNDLES = "itemGroup.name.bundles"; + public const ITEMGROUP_NAME_BUTTONS = "itemGroup.name.buttons"; + public const ITEMGROUP_NAME_CANDLES = "itemGroup.name.candles"; + public const ITEMGROUP_NAME_CHALKBOARD = "itemGroup.name.chalkboard"; + public const ITEMGROUP_NAME_CHEST = "itemGroup.name.chest"; + public const ITEMGROUP_NAME_CHESTBOAT = "itemGroup.name.chestboat"; + public const ITEMGROUP_NAME_CHESTPLATE = "itemGroup.name.chestplate"; + public const ITEMGROUP_NAME_COMPOUNDS = "itemGroup.name.compounds"; + public const ITEMGROUP_NAME_CONCRETE = "itemGroup.name.concrete"; + public const ITEMGROUP_NAME_CONCRETEPOWDER = "itemGroup.name.concretePowder"; + public const ITEMGROUP_NAME_COOKEDFOOD = "itemGroup.name.cookedFood"; + public const ITEMGROUP_NAME_CORAL = "itemGroup.name.coral"; + public const ITEMGROUP_NAME_CORAL_DECORATIONS = "itemGroup.name.coral_decorations"; + public const ITEMGROUP_NAME_CROP = "itemGroup.name.crop"; + public const ITEMGROUP_NAME_DOOR = "itemGroup.name.door"; + public const ITEMGROUP_NAME_DYE = "itemGroup.name.dye"; + public const ITEMGROUP_NAME_ENCHANTEDBOOK = "itemGroup.name.enchantedBook"; + public const ITEMGROUP_NAME_FENCE = "itemGroup.name.fence"; + public const ITEMGROUP_NAME_FENCEGATE = "itemGroup.name.fenceGate"; + public const ITEMGROUP_NAME_FIREWORK = "itemGroup.name.firework"; + public const ITEMGROUP_NAME_FIREWORKSTARS = "itemGroup.name.fireworkStars"; + public const ITEMGROUP_NAME_FLOWER = "itemGroup.name.flower"; + public const ITEMGROUP_NAME_GLASS = "itemGroup.name.glass"; + public const ITEMGROUP_NAME_GLASSPANE = "itemGroup.name.glassPane"; + public const ITEMGROUP_NAME_GLAZEDTERRACOTTA = "itemGroup.name.glazedTerracotta"; + public const ITEMGROUP_NAME_GOATHORN = "itemGroup.name.goatHorn"; + public const ITEMGROUP_NAME_GRASS = "itemGroup.name.grass"; + public const ITEMGROUP_NAME_HELMET = "itemGroup.name.helmet"; + public const ITEMGROUP_NAME_HOE = "itemGroup.name.hoe"; + public const ITEMGROUP_NAME_HORSEARMOR = "itemGroup.name.horseArmor"; + public const ITEMGROUP_NAME_LEAVES = "itemGroup.name.leaves"; + public const ITEMGROUP_NAME_LEGGINGS = "itemGroup.name.leggings"; + public const ITEMGROUP_NAME_LINGERINGPOTION = "itemGroup.name.lingeringPotion"; + public const ITEMGROUP_NAME_LOG = "itemGroup.name.log"; + public const ITEMGROUP_NAME_MINECART = "itemGroup.name.minecart"; + public const ITEMGROUP_NAME_MISCFOOD = "itemGroup.name.miscFood"; + public const ITEMGROUP_NAME_MOBEGG = "itemGroup.name.mobEgg"; + public const ITEMGROUP_NAME_MONSTERSTONEEGG = "itemGroup.name.monsterStoneEgg"; + public const ITEMGROUP_NAME_MUSHROOM = "itemGroup.name.mushroom"; + public const ITEMGROUP_NAME_NETHERWARTBLOCK = "itemGroup.name.netherWartBlock"; + public const ITEMGROUP_NAME_OMINOUSBOTTLE = "itemGroup.name.ominousBottle"; + public const ITEMGROUP_NAME_ORE = "itemGroup.name.ore"; + public const ITEMGROUP_NAME_PERMISSION = "itemGroup.name.permission"; + public const ITEMGROUP_NAME_PICKAXE = "itemGroup.name.pickaxe"; + public const ITEMGROUP_NAME_PLANKS = "itemGroup.name.planks"; + public const ITEMGROUP_NAME_POTION = "itemGroup.name.potion"; + public const ITEMGROUP_NAME_PRESSUREPLATE = "itemGroup.name.pressurePlate"; + public const ITEMGROUP_NAME_PRODUCTS = "itemGroup.name.products"; + public const ITEMGROUP_NAME_RAIL = "itemGroup.name.rail"; + public const ITEMGROUP_NAME_RAWFOOD = "itemGroup.name.rawFood"; + public const ITEMGROUP_NAME_RECORD = "itemGroup.name.record"; + public const ITEMGROUP_NAME_SANDSTONE = "itemGroup.name.sandstone"; + public const ITEMGROUP_NAME_SAPLING = "itemGroup.name.sapling"; + public const ITEMGROUP_NAME_SEED = "itemGroup.name.seed"; + public const ITEMGROUP_NAME_SHOVEL = "itemGroup.name.shovel"; + public const ITEMGROUP_NAME_SHULKERBOX = "itemGroup.name.shulkerBox"; + public const ITEMGROUP_NAME_SIGN = "itemGroup.name.sign"; + public const ITEMGROUP_NAME_SKULL = "itemGroup.name.skull"; + public const ITEMGROUP_NAME_SLAB = "itemGroup.name.slab"; + public const ITEMGROUP_NAME_SPLASHPOTION = "itemGroup.name.splashPotion"; + public const ITEMGROUP_NAME_STAINEDCLAY = "itemGroup.name.stainedClay"; + public const ITEMGROUP_NAME_STAIRS = "itemGroup.name.stairs"; + public const ITEMGROUP_NAME_STONE = "itemGroup.name.stone"; + public const ITEMGROUP_NAME_STONEBRICK = "itemGroup.name.stoneBrick"; + public const ITEMGROUP_NAME_SWORD = "itemGroup.name.sword"; + public const ITEMGROUP_NAME_TRAPDOOR = "itemGroup.name.trapdoor"; + public const ITEMGROUP_NAME_WALLS = "itemGroup.name.walls"; + public const ITEMGROUP_NAME_WOOD = "itemGroup.name.wood"; + public const ITEMGROUP_NAME_WOOL = "itemGroup.name.wool"; + public const ITEMGROUP_NAME_WOOLCARPET = "itemGroup.name.woolCarpet"; public const KICK_ADMIN = "kick.admin"; public const KICK_ADMIN_REASON = "kick.admin.reason"; public const KICK_REASON_CHEAT = "kick.reason.cheat"; @@ -577,6 +658,8 @@ final class KnownTranslationKeys{ public const TILE_BED_NOSLEEP = "tile.bed.noSleep"; public const TILE_BED_OCCUPIED = "tile.bed.occupied"; public const TILE_BED_TOOFAR = "tile.bed.tooFar"; + public const TILE_RESPAWN_ANCHOR_NOTVALID = "tile.respawn_anchor.notValid"; + public const TILE_RESPAWN_ANCHOR_RESPAWNSET = "tile.respawn_anchor.respawnSet"; public const VIEW_DISTANCE = "view_distance"; public const WELCOME_TO_POCKETMINE = "welcome_to_pocketmine"; public const WHITELIST_ENABLE = "whitelist_enable"; diff --git a/src/lang/Language.php b/src/lang/Language.php index 29f28917d..59a309524 100644 --- a/src/lang/Language.php +++ b/src/lang/Language.php @@ -71,7 +71,7 @@ class Language{ foreach($files as $file){ try{ - $code = explode(".", $file)[0]; + $code = explode(".", $file, limit: 2)[0]; $strings = self::loadLang($path, $code); if(isset($strings[KnownTranslationKeys::LANGUAGE_NAME])){ $result[$code] = $strings[KnownTranslationKeys::LANGUAGE_NAME]; diff --git a/src/network/mcpe/ChunkRequestTask.php b/src/network/mcpe/ChunkRequestTask.php index 13b5db5b7..357904dcb 100644 --- a/src/network/mcpe/ChunkRequestTask.php +++ b/src/network/mcpe/ChunkRequestTask.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use pmmp\encoding\ByteBufferWriter; use pocketmine\network\mcpe\compression\CompressBatchPromise; use pocketmine\network\mcpe\compression\Compressor; use pocketmine\network\mcpe\convert\TypeConverter; @@ -33,7 +34,6 @@ use pocketmine\network\mcpe\protocol\types\DimensionIds; use pocketmine\network\mcpe\serializer\ChunkSerializer; use pocketmine\scheduler\AsyncTask; use pocketmine\thread\NonThreadSafeValue; -use pocketmine\utils\BinaryStream; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\FastChunkSerializer; use function chr; @@ -73,11 +73,11 @@ class ChunkRequestTask extends AsyncTask{ $converter = TypeConverter::getInstance(); $payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $converter->getBlockTranslator(), $this->tiles); - $stream = new BinaryStream(); + $stream = new ByteBufferWriter(); 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())); + $this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getData())); } public function onCompletion() : void{ diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index 7df8c734b..01368ec0a 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -41,6 +41,7 @@ use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\item\enchantment\EnchantingOption; use pocketmine\item\enchantment\EnchantmentInstance; +use pocketmine\item\Item; use pocketmine\network\mcpe\cache\CreativeInventoryCache; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; @@ -63,6 +64,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes; use pocketmine\network\PacketHandlingException; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\Binary; use pocketmine\utils\ObjectSet; use function array_fill_keys; use function array_keys; @@ -228,17 +230,25 @@ class InventoryManager{ return null; } - private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{ + private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{ $this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item; } - public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{ + public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{ $typeConverter = $this->session->getTypeConverter(); + $itemStack = $typeConverter->coreItemStackToNet($item); + $this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack); + } + + public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{ foreach($tx->getActions() as $action){ if($action instanceof SlotChangeAction){ //TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead - $itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem()); - $this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack); + $this->addPredictedSlotChange( + $action->getInventory(), + $action->getSlot(), + $action->getTargetItem() + ); } } } @@ -267,7 +277,7 @@ class InventoryManager{ } [$inventory, $slot] = $info; - $this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack()); + $this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack()); } } @@ -410,6 +420,15 @@ class InventoryManager{ } public function onClientRemoveWindow(int $id) : void{ + if(Binary::signByte($id) === ContainerIds::NONE){ //TODO: REMOVE signByte() once BedrockProtocol + ext-encoding are implemented + //TODO: HACK! Since 1.21.100 (and probably earlier), the client will send -1 to close windows that it can't + //view for some reason, e.g. if the chat window was already open. This is pretty awkward, since it means + //that we can only assume it refers to the most recently sent window, and if we don't handle it, + //InventoryManager will never get the green light to send subsequent windows, which breaks inventory UIs. + //Fortunately, we already wait for close acks anyway, so the window ID is technically useless...? + $this->session->getLogger()->debug("Client rejected opening of a window, assuming it was $this->lastInventoryNetworkId"); + $id = $this->lastInventoryNetworkId; + } if($id === $this->lastInventoryNetworkId){ if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){ $this->remove($id); @@ -690,7 +709,7 @@ class InventoryManager{ } public function syncCreative() : void{ - $this->session->sendDataPacket(CreativeInventoryCache::getInstance()->getCache($this->player->getCreativeInventory())); + $this->session->sendDataPacket(CreativeInventoryCache::getInstance()->buildPacket($this->player->getCreativeInventory(), $this->session)); } /** diff --git a/src/network/mcpe/JwtUtils.php b/src/network/mcpe/JwtUtils.php index 259a602d4..8c25ee042 100644 --- a/src/network/mcpe/JwtUtils.php +++ b/src/network/mcpe/JwtUtils.php @@ -23,8 +23,10 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use pmmp\encoding\BE; +use pmmp\encoding\Byte; +use pmmp\encoding\ByteBufferReader; use pocketmine\utils\AssumptionFailedError; -use pocketmine\utils\BinaryStream; use pocketmine\utils\Utils; use function base64_decode; use function base64_encode; @@ -32,6 +34,7 @@ use function bin2hex; use function chr; use function count; use function explode; +use function hex2bin; use function is_array; use function json_decode; use function json_encode; @@ -54,6 +57,7 @@ use function strlen; use function strtr; use function substr; use const JSON_THROW_ON_ERROR; +use const OPENSSL_ALGO_SHA256; use const OPENSSL_ALGO_SHA384; use const STR_PAD_LEFT; @@ -72,9 +76,11 @@ final class JwtUtils{ * @throws JwtException */ public static function split(string $jwt) : array{ - $v = explode(".", $jwt); + //limit of 4 allows us to detect too many parts without having to split the string up into a potentially large + //number of parts + $v = explode(".", $jwt, limit: 4); if(count($v) !== 3){ - throw new JwtException("Expected exactly 3 JWT parts, got " . count($v)); + throw new JwtException("Expected exactly 3 JWT parts delimited by a period"); } return [$v[0], $v[1], $v[2]]; //workaround phpstan bug } @@ -128,17 +134,17 @@ final class JwtUtils{ return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence; } - private static function signaturePartFromAsn1(BinaryStream $stream) : string{ - $prefix = $stream->get(1); + private static function signaturePartFromAsn1(ByteBufferReader $stream) : string{ + $prefix = $stream->readByteArray(1); if($prefix !== self::ASN1_INTEGER_TAG){ throw new \InvalidArgumentException("Expected an ASN.1 INTEGER tag, got " . bin2hex($prefix)); } //we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed - $length = $stream->getByte(); + $length = Byte::readUnsigned($stream); if($length > self::SIGNATURE_PART_LENGTH + 1){ //each part may have an extra leading 0 byte to prevent it being interpreted as a negative number throw new \InvalidArgumentException("Expected at most 49 bytes for signature R or S, got $length"); } - $part = $stream->get($length); + $part = $stream->readByteArray($length); return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT); } @@ -154,11 +160,11 @@ final class JwtUtils{ throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts)); } - $stream = new BinaryStream($parts); + $stream = new ByteBufferReader($parts); $rRaw = self::signaturePartFromAsn1($stream); $sRaw = self::signaturePartFromAsn1($stream); - if(!$stream->feof()){ + if($stream->getUnreadLength() > 0){ throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data"); } @@ -168,17 +174,17 @@ final class JwtUtils{ /** * @throws JwtException */ - public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{ + public static function verify(string $jwt, string $signingKeyDer, bool $ec) : bool{ [$header, $body, $signature] = self::split($jwt); $rawSignature = self::b64UrlDecode($signature); - $derSignature = self::rawSignatureToDer($rawSignature); + $derSignature = $ec ? self::rawSignatureToDer($rawSignature) : $rawSignature; $v = openssl_verify( $header . '.' . $body, $derSignature, - $signingKey, - self::SIGNATURE_ALGORITHM + self::derPublicKeyToPem($signingKeyDer), + $ec ? self::SIGNATURE_ALGORITHM : OPENSSL_ALGO_SHA256 ); switch($v){ case 0: return false; @@ -236,22 +242,56 @@ final class JwtUtils{ throw new AssumptionFailedError("OpenSSL resource contains invalid public key"); } + /** + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + */ + private static function encodeDerLength(int $length) : string{ + if ($length <= 0x7F) { + return chr($length); + } + + $lengthBytes = ltrim(BE::packUnsignedInt($length), "\x00"); + + return chr(0x80 | strlen($lengthBytes)) . $lengthBytes; + } + + private static function encodeDerBytes(int $tag, string $data) : string{ + return chr($tag) . self::encodeDerLength(strlen($data)) . $data; + } + public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{ - $signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey))); + $signingKeyOpenSSL = openssl_pkey_get_public(self::derPublicKeyToPem($derKey)); if($signingKeyOpenSSL === false){ throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string()); } - $details = openssl_pkey_get_details($signingKeyOpenSSL); - if($details === false){ - throw new JwtException("OpenSSL failed to get details from key: " . openssl_error_string()); - } - if(!isset($details['ec']['curve_name'])){ - throw new JwtException("Expected an EC key"); - } - $curve = $details['ec']['curve_name']; - if($curve !== self::BEDROCK_SIGNING_KEY_CURVE_NAME){ - throw new JwtException("Key must belong to curve " . self::BEDROCK_SIGNING_KEY_CURVE_NAME . ", got $curve"); - } return $signingKeyOpenSSL; } + + public static function derPublicKeyToPem(string $derKey) : string{ + return sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey)); + } + + /** + * Create a public key represented in DER format from RSA modulus and exponent information + * + * @param string $nBase64 The RSA modulus encoded in Base64 + * @param string $eBase64 The RSA exponent encoded in Base64 + */ + public static function rsaPublicKeyModExpToDer(string $nBase64, string $eBase64) : string{ + $mod = self::b64UrlDecode($nBase64); + $exp = self::b64UrlDecode($eBase64); + + $modulus = self::encodeDerBytes(2, $mod); + $publicExponent = self::encodeDerBytes(2, $exp); + + $rsaPublicKey = self::encodeDerBytes(48, $modulus . $publicExponent); + + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = hex2bin('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = chr(0) . $rsaPublicKey; + $rsaPublicKey = self::encodeDerBytes(3, $rsaPublicKey); + + return self::encodeDerBytes(48, $rsaOID . $rsaPublicKey); + } } diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 25035c4c7..8841b1eec 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use pmmp\encoding\ByteBufferReader; +use pmmp\encoding\ByteBufferWriter; +use pmmp\encoding\DataDecodeException; use pocketmine\entity\effect\EffectInstance; use pocketmine\event\player\PlayerDuplicateLoginEvent; use pocketmine\event\player\PlayerResourcePackOfferEvent; @@ -70,7 +73,6 @@ use pocketmine\network\mcpe\protocol\PlayerStartItemCooldownPacket; use pocketmine\network\mcpe\protocol\PlayStatusPacket; use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\network\mcpe\protocol\serializer\PacketBatch; -use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\ServerboundPacket; use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket; use pocketmine\network\mcpe\protocol\SetDifficultyPacket; @@ -109,15 +111,13 @@ use pocketmine\promise\PromiseResolver; use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; -use pocketmine\utils\BinaryDataException; -use pocketmine\utils\BinaryStream; use pocketmine\utils\ObjectSet; use pocketmine\utils\TextFormat; use pocketmine\world\format\io\GlobalItemDataHandlers; use pocketmine\world\Position; +use pocketmine\world\World; use pocketmine\YmlServerProperties; use function array_map; -use function array_values; use function base64_encode; use function bin2hex; use function count; @@ -163,7 +163,10 @@ class NetworkSession{ private ?EncryptionContext $cipher = null; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $sendBuffer = []; /** * @var PromiseResolver[] @@ -171,7 +174,7 @@ class NetworkSession{ */ private array $sendBufferAckPromises = []; - /** @phpstan-var \SplQueue>}> */ + /** @phpstan-var \SplQueue>, bool}> */ private \SplQueue $compressedQueue; private bool $forceAsyncCompression = true; private bool $enableCompression = false; //disabled until handshake completed @@ -232,7 +235,7 @@ class NetworkSession{ private function onSessionStartSuccess() : void{ $this->logger->debug("Session start handshake completed, awaiting login packet"); - $this->flushSendBuffer(true); + $this->flushGamePacketQueue(); $this->enableCompression = true; $this->setHandler(new LoginPacketHandler( $this->server, @@ -398,7 +401,7 @@ class NetworkSession{ } try{ - $stream = new BinaryStream($decompressed); + $stream = new ByteBufferReader($decompressed); foreach(PacketBatch::decodeRaw($stream) as $buffer){ $this->gamePacketLimiter->decrement(); $packet = $this->packetPool->getPacket($buffer); @@ -412,8 +415,13 @@ class NetworkSession{ $this->logger->debug($packet->getName() . ": " . base64_encode($buffer)); throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName()); } + if(!$this->isConnected()){ + //handling this packet may have caused a disconnection + $this->logger->debug("Aborting batch processing due to server-side disconnection"); + break; + } } - }catch(PacketDecodeException|BinaryDataException $e){ + }catch(PacketDecodeException|DataDecodeException $e){ $this->logger->logException($e); throw PacketHandlingException::wrap($e, "Packet batch decode error"); } @@ -445,14 +453,14 @@ class NetworkSession{ $decodeTimings = Timings::getDecodeDataPacketTimings($packet); $decodeTimings->startTiming(); try{ - $stream = PacketSerializer::decoder($buffer, 0); + $stream = new ByteBufferReader($buffer); try{ $packet->decode($stream); }catch(PacketDecodeException $e){ throw PacketHandlingException::wrap($e); } - if(!$stream->feof()){ - $remains = substr($stream->getBuffer(), $stream->getOffset()); + if($stream->getUnreadLength() > 0){ + $remains = substr($stream->getData(), $stream->getOffset()); $this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains)); } }finally{ @@ -470,7 +478,7 @@ class NetworkSession{ $handlerTimings->startTiming(); try{ if($this->handler === null || !$packet->handle($this->handler)){ - $this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer())); + $this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getData())); } }finally{ $handlerTimings->stopTiming(); @@ -522,11 +530,13 @@ class NetworkSession{ if($ackReceiptResolver !== null){ $this->sendBufferAckPromises[] = $ackReceiptResolver; } + $writer = new ByteBufferWriter(); foreach($packets as $evPacket){ - $this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket)); + $writer->clear(); //memory reuse let's gooooo + $this->addToSendBuffer(self::encodePacketTimed($writer, $evPacket)); } if($immediate){ - $this->flushSendBuffer(true); + $this->flushGamePacketQueue(); } return true; @@ -543,6 +553,7 @@ class NetworkSession{ * @phpstan-return Promise */ public function sendDataPacketWithReceipt(ClientboundPacket $packet, bool $immediate = false) : Promise{ + /** @phpstan-var PromiseResolver $resolver */ $resolver = new PromiseResolver(); if(!$this->sendDataPacketInternal($packet, $immediate, $resolver)){ @@ -555,12 +566,12 @@ class NetworkSession{ /** * @internal */ - public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{ + public static function encodePacketTimed(ByteBufferWriter $serializer, ClientboundPacket $packet) : string{ $timings = Timings::getEncodeDataPacketTimings($packet); $timings->startTiming(); try{ $packet->encode($serializer); - return $serializer->getBuffer(); + return $serializer->getData(); }finally{ $timings->stopTiming(); } @@ -573,29 +584,29 @@ class NetworkSession{ $this->sendBuffer[] = $buffer; } - private function flushSendBuffer(bool $immediate = false) : void{ + private function flushGamePacketQueue() : void{ if(count($this->sendBuffer) > 0){ Timings::$playerNetworkSend->startTiming(); try{ $syncMode = null; //automatic - if($immediate){ - $syncMode = true; - }elseif($this->forceAsyncCompression){ + if($this->forceAsyncCompression){ $syncMode = false; } - $stream = new BinaryStream(); + $stream = new ByteBufferWriter(); PacketBatch::encodeRaw($stream, $this->sendBuffer); if($this->enableCompression){ - $batch = $this->server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer); + $batch = $this->server->prepareBatch($stream->getData(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer); }else{ - $batch = $stream->getBuffer(); + $batch = $stream->getData(); } $this->sendBuffer = []; $ackPromises = $this->sendBufferAckPromises; $this->sendBufferAckPromises = []; - $this->queueCompressedNoBufferFlush($batch, $immediate, $ackPromises); + //these packets were already potentially buffered for up to 50ms - make sure the transport layer doesn't + //delay them any longer + $this->queueCompressedNoGamePacketFlush($batch, networkFlush: true, ackPromises: $ackPromises); }finally{ Timings::$playerNetworkSend->stopTiming(); } @@ -615,8 +626,10 @@ class NetworkSession{ public function queueCompressed(CompressBatchPromise|string $payload, bool $immediate = false) : void{ Timings::$playerNetworkSend->startTiming(); try{ - $this->flushSendBuffer($immediate); //Maintain ordering if possible - $this->queueCompressedNoBufferFlush($payload, $immediate); + //if the next packet causes a flush, avoid unnecessarily flushing twice + //however, if the next packet does *not* cause a flush, game packets should be flushed to avoid delays + $this->flushGamePacketQueue(); + $this->queueCompressedNoGamePacketFlush($payload, $immediate); }finally{ Timings::$playerNetworkSend->stopTiming(); } @@ -627,22 +640,13 @@ class NetworkSession{ * * @phpstan-param list> $ackPromises */ - private function queueCompressedNoBufferFlush(CompressBatchPromise|string $batch, bool $immediate = false, array $ackPromises = []) : void{ + private function queueCompressedNoGamePacketFlush(CompressBatchPromise|string $batch, bool $networkFlush = false, array $ackPromises = []) : void{ Timings::$playerNetworkSend->startTiming(); try{ + $this->compressedQueue->enqueue([$batch, $ackPromises, $networkFlush]); if(is_string($batch)){ - if($immediate){ - //Skips all queues - $this->sendEncoded($batch, true, $ackPromises); - }else{ - $this->compressedQueue->enqueue([$batch, $ackPromises]); - $this->flushCompressedQueue(); - } - }elseif($immediate){ - //Skips all queues - $this->sendEncoded($batch->getResult(), true, $ackPromises); + $this->flushCompressedQueue(); }else{ - $this->compressedQueue->enqueue([$batch, $ackPromises]); $batch->onResolve(function() : void{ if($this->connected){ $this->flushCompressedQueue(); @@ -659,14 +663,14 @@ class NetworkSession{ try{ while(!$this->compressedQueue->isEmpty()){ /** @var CompressBatchPromise|string $current */ - [$current, $ackPromises] = $this->compressedQueue->bottom(); + [$current, $ackPromises, $networkFlush] = $this->compressedQueue->bottom(); if(is_string($current)){ $this->compressedQueue->dequeue(); - $this->sendEncoded($current, false, $ackPromises); + $this->sendEncoded($current, $networkFlush, $ackPromises); }elseif($current->hasResult()){ $this->compressedQueue->dequeue(); - $this->sendEncoded($current->getResult(), false, $ackPromises); + $this->sendEncoded($current->getResult(), $networkFlush, $ackPromises); }else{ //can't send any more queued until this one is ready @@ -706,7 +710,7 @@ class NetworkSession{ $this->disconnectGuard = true; $func(); $this->disconnectGuard = false; - $this->flushSendBuffer(true); + $this->flushGamePacketQueue(); $this->sender->close(""); foreach($this->disposeHooks as $callback){ $callback(); @@ -1054,8 +1058,7 @@ class NetworkSession{ ]; $layers = [ - //TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!! - new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1), + new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 1, 0.1), ]; if(!$for->hasBlockCollision()){ //TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a @@ -1065,7 +1068,7 @@ class NetworkSession{ $layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [ AbilitiesLayer::ABILITY_FLYING => true, - ], null, null); + ], null, null, null); } $this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData( @@ -1105,7 +1108,7 @@ class NetworkSession{ //work around a client bug which makes the original name not show when aliases are used $aliases[] = $lname; } - $aliasObj = new CommandEnum(ucfirst($command->getLabel()) . "Aliases", array_values($aliases)); + $aliasObj = new CommandEnum(ucfirst($command->getLabel()) . "Aliases", $aliases); } $description = $command->getDescription(); @@ -1178,6 +1181,19 @@ class NetworkSession{ $this->sendDataPacket(ClientboundCloseFormPacket::create()); } + /** + * @phpstan-param \Closure() : void $onCompletion + */ + private function sendChunkPacket(string $chunkPacket, \Closure $onCompletion, World $world) : void{ + $world->timings->syncChunkSend->startTiming(); + try{ + $this->queueCompressed($chunkPacket); + $onCompletion(); + }finally{ + $world->timings->syncChunkSend->stopTiming(); + } + } + /** * Instructs the networksession to start using the chunk at the given coordinates. This may occur asynchronously. * @param \Closure $onCompletion To be called when chunk sending has completed. @@ -1185,8 +1201,12 @@ class NetworkSession{ */ public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{ $world = $this->player->getLocation()->getWorld(); - ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve( - + $promiseOrPacket = ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ); + if(is_string($promiseOrPacket)){ + $this->sendChunkPacket($promiseOrPacket, $onCompletion, $world); + return; + } + $promiseOrPacket->onResolve( //this callback may be called synchronously or asynchronously, depending on whether the promise is resolved yet function(CompressBatchPromise $promise) use ($world, $onCompletion, $chunkX, $chunkZ) : void{ if(!$this->isConnected()){ @@ -1204,13 +1224,7 @@ class NetworkSession{ //to NEEDED if they want to be resent. return; } - $world->timings->syncChunkSend->startTiming(); - try{ - $this->queueCompressed($promise); - $onCompletion(); - }finally{ - $world->timings->syncChunkSend->stopTiming(); - } + $this->sendChunkPacket($promise->getResult(), $onCompletion, $world); } ); } @@ -1331,6 +1345,6 @@ class NetworkSession{ Timings::$playerNetworkSendInventorySync->stopTiming(); } - $this->flushSendBuffer(); + $this->flushGamePacketQueue(); } } diff --git a/src/network/mcpe/StandardPacketBroadcaster.php b/src/network/mcpe/StandardPacketBroadcaster.php index 7a91b397b..5d9bc533e 100644 --- a/src/network/mcpe/StandardPacketBroadcaster.php +++ b/src/network/mcpe/StandardPacketBroadcaster.php @@ -23,12 +23,11 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use pmmp\encoding\ByteBufferWriter; use pocketmine\event\server\DataPacketSendEvent; use pocketmine\network\mcpe\protocol\serializer\PacketBatch; -use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\Server; use pocketmine\timings\Timings; -use pocketmine\utils\BinaryStream; use function count; use function log; use function spl_object_id; @@ -64,8 +63,10 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{ $totalLength = 0; $packetBuffers = []; + $writer = new ByteBufferWriter(); foreach($packets as $packet){ - $buffer = NetworkSession::encodePacketTimed(PacketSerializer::encoder(), $packet); + $writer->clear(); //memory reuse let's gooooo + $buffer = NetworkSession::encodePacketTimed($writer, $packet); //varint length prefix + packet buffer $totalLength += (((int) log(strlen($buffer), 128)) + 1) + strlen($buffer); $packetBuffers[] = $buffer; @@ -77,9 +78,9 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{ $threshold = $compressor->getCompressionThreshold(); if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){ //do not prepare shared batch unless we're sure it will be compressed - $stream = new BinaryStream(); + $stream = new ByteBufferWriter(); PacketBatch::encodeRaw($stream, $packetBuffers); - $batchBuffer = $stream->getBuffer(); + $batchBuffer = $stream->getData(); $batch = $this->server->prepareBatch($batchBuffer, $compressor, timings: Timings::$playerNetworkSendCompressBroadcast); foreach($compressorTargets as $target){ diff --git a/src/network/mcpe/auth/AuthJwtHelper.php b/src/network/mcpe/auth/AuthJwtHelper.php new file mode 100644 index 000000000..5050c396f --- /dev/null +++ b/src/network/mcpe/auth/AuthJwtHelper.php @@ -0,0 +1,165 @@ +nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){ + throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly()); + } + + if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){ + throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate()); + } + } + + /** + * @throws VerifyLoginException if errors are encountered + */ + public static function validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience) : XboxAuthJwtBody{ + try{ + if(!JwtUtils::verify($jwt, $signingKeyDer, ec: false)){ + throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature()); + } + }catch(JwtException $e){ + throw new VerifyLoginException($e->getMessage(), null, 0, $e); + } + + try{ + [, $claimsArray, ] = JwtUtils::parse($jwt); + }catch(JwtException $e){ + throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case + $mapper->bExceptionOnMissingData = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + + try{ + //nasty dynamic new for JsonMapper + $claims = $mapper->map($claimsArray, new XboxAuthJwtBody()); + }catch(\JsonMapper_Exception $e){ + throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e); + } + + if(!isset($claims->iss) || $claims->iss !== $issuer){ + throw new VerifyLoginException("Invalid JWT issuer"); + } + + if(!isset($claims->aud) || $claims->aud !== $audience){ + throw new VerifyLoginException("Invalid JWT audience"); + } + + self::checkExpiry($claims); + + return $claims; + } + + /** + * @throws VerifyLoginException if errors are encountered + */ + public static function validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer) : LegacyAuthJwtBody{ + self::validateSelfSignedToken($jwt, $expectedKeyDer); + + //TODO: this parses the JWT twice and throws away a bunch of parts, optimize this + [, $claimsArray, ] = JwtUtils::parse($jwt); + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case + $mapper->bExceptionOnMissingData = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + try{ + /** @var LegacyAuthJwtBody $claims */ + $claims = $mapper->map($claimsArray, new LegacyAuthJwtBody()); + }catch(\JsonMapper_Exception $e){ + throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e); + } + + self::checkExpiry($claims); + + return $claims; + } + + public static function validateSelfSignedToken(string $jwt, ?string $expectedKeyDer) : void{ + try{ + [$headersArray, ] = JwtUtils::parse($jwt); + }catch(JwtException $e){ + throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnMissingData = true; + $mapper->bExceptionOnUndefinedProperty = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + + try{ + /** @var SelfSignedJwtHeader $headers */ + $headers = $mapper->map($headersArray, new SelfSignedJwtHeader()); + }catch(\JsonMapper_Exception $e){ + throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e); + } + + $headerDerKey = base64_decode($headers->x5u, true); + if($headerDerKey === false){ + throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u"); + } + if($expectedKeyDer !== null && $headerDerKey !== $expectedKeyDer){ + //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway + throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature()); + } + + try{ + if(!JwtUtils::verify($jwt, $headerDerKey, ec: true)){ + throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature()); + } + }catch(JwtException $e){ + throw new VerifyLoginException($e->getMessage(), null, 0, $e); + } + } +} diff --git a/src/network/mcpe/auth/AuthKeyProvider.php b/src/network/mcpe/auth/AuthKeyProvider.php new file mode 100644 index 000000000..67ffe4908 --- /dev/null +++ b/src/network/mcpe/auth/AuthKeyProvider.php @@ -0,0 +1,164 @@ + */ + private ?PromiseResolver $resolver = null; + + private int $lastFetch = 0; + + public function __construct( + private readonly \Logger $logger, + private readonly AsyncPool $asyncPool, + private readonly int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL + ){} + + /** + * Fetches the key for the given key ID. + * The promise will be resolved with an array of [issuer, pemPublicKey]. + * + * @phpstan-return Promise + */ + public function getKey(string $keyId) : Promise{ + /** @phpstan-var PromiseResolver $resolver */ + $resolver = new PromiseResolver(); + + if( + $this->keyring === null || //we haven't fetched keys yet + ($this->keyring->getKey($keyId) === null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds) //we don't recognise this one & keys might be outdated + ){ + //only refresh keys when we see one we don't recognise + $this->fetchKeys()->onCompletion( + onSuccess: fn(AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId), + onFailure: $resolver->reject(...) + ); + }else{ + $this->resolveKey($resolver, $this->keyring, $keyId); + } + + return $resolver->getPromise(); + } + + /** + * @phpstan-param PromiseResolver $resolver + */ + private function resolveKey(PromiseResolver $resolver, AuthKeyring $keyring, string $keyId) : void{ + $key = $keyring->getKey($keyId); + if($key === null){ + $this->logger->debug("Key $keyId not recognised!"); + $resolver->reject(); + return; + } + + $this->logger->debug("Key $keyId found in keychain"); + $resolver->resolve([$keyring->getIssuer(), $key]); + } + + /** + * @phpstan-param array|null $keys + * @phpstan-param string[]|null $errors + */ + private function onKeysFetched(?array $keys, string $issuer, ?array $errors) : void{ + $resolver = $this->resolver; + if($resolver === null){ + throw new AssumptionFailedError("Not expecting this to be called without a resolver present"); + } + if($errors !== null){ + $this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors)); + //we might've still succeeded in fetching keys even if there were errors, so don't return + } + + if($keys === null){ + $this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!"); + $resolver->reject(); + }else{ + $pemKeys = []; + foreach($keys as $keyModel){ + if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){ + $this->logger->error("Key ID $keyModel->kid doesn't have the expected properties: expected use=sig, kty=RSA, got use=$keyModel->use, kty=$keyModel->kty"); + continue; + } + $derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e); + + //make sure the key is valid + try{ + JwtUtils::parseDerPublicKey($derKey); + }catch(JwtException $e){ + $this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage()); + $this->logger->logException($e); + continue; + } + + //retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads + $pemKeys[$keyModel->kid] = $derKey; + } + + if(count($keys) === 0){ + $this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!"); + $resolver->reject(); + }else{ + $this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys))); + $this->keyring = new AuthKeyring($issuer, $pemKeys); + $this->lastFetch = time(); + $resolver->resolve($this->keyring); + } + } + } + + /** + * @phpstan-return Promise + */ + private function fetchKeys() : Promise{ + if($this->resolver !== null){ + $this->logger->debug("Key refresh was requested, but it's already in progress"); + return $this->resolver->getPromise(); + } + + $this->logger->notice("Fetching new authentication keys"); + + /** @phpstan-var PromiseResolver $resolver */ + $resolver = new PromiseResolver(); + $this->resolver = $resolver; + //TODO: extract this so it can be polyfilled for unit testing + $this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...))); + return $this->resolver->getPromise(); + } +} diff --git a/src/network/mcpe/auth/AuthKeyring.php b/src/network/mcpe/auth/AuthKeyring.php new file mode 100644 index 000000000..cd5d29f6e --- /dev/null +++ b/src/network/mcpe/auth/AuthKeyring.php @@ -0,0 +1,45 @@ + $keys + */ + public function __construct( + private string $issuer, + private array $keys + ){} + + public function getIssuer() : string{ return $this->issuer; } + + /** + * Returns a (raw) DER public key associated with the given key ID + */ + public function getKey(string $keyId) : ?string{ + return $this->keys[$keyId] ?? null; + } +} diff --git a/src/network/mcpe/auth/FetchAuthKeysTask.php b/src/network/mcpe/auth/FetchAuthKeysTask.php new file mode 100644 index 000000000..b159d42af --- /dev/null +++ b/src/network/mcpe/auth/FetchAuthKeysTask.php @@ -0,0 +1,209 @@ +> */ + private ?NonThreadSafeValue $keys = null; + private string $issuer; + + /** @phpstan-var ?NonThreadSafeValue> */ + private ?NonThreadSafeValue $errors = null; + + /** + * @phpstan-param \Closure(?array $keys, string $issuer, ?string[] $errors) : void $onCompletion + */ + public function __construct( + \Closure $onCompletion + ){ + $this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion); + } + + public function onRun() : void{ + /** @var string[] $errors */ + $errors = []; + + try{ + $authServiceUri = $this->getAuthServiceURI(); + }catch(\RuntimeException $e){ + $errors[] = $e->getMessage(); + $authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK; + } + + try { + $openIdConfig = $this->getOpenIdConfiguration($authServiceUri); + $jwksUri = $openIdConfig->jwks_uri; + + $this->issuer = $openIdConfig->issuer; + } catch (\RuntimeException $e) { + $errors[] = $e->getMessage(); + $jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH; + + $this->issuer = $authServiceUri; + } + + try{ + $this->keys = new NonThreadSafeValue($this->getKeys($jwksUri)); + }catch(\RuntimeException $e){ + $errors[] = $e->getMessage(); + } + + $this->errors = $errors === [] ? null : new NonThreadSafeValue($errors); + } + + private function getAuthServiceURI() : string{ + $result = Internet::getURL(self::MINECRAFT_SERVICES_DISCOVERY_URL); + if($result === null || $result->getCode() !== 200){ + throw new \RuntimeException("Failed to fetch Minecraft services discovery document"); + } + + try{ + $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + if(!is_object($json)){ + throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object"); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case + $mapper->bExceptionOnMissingData = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + try{ + /** @var MinecraftServicesDiscovery $discovery */ + $discovery = $mapper->map($json, new MinecraftServicesDiscovery()); + }catch(\JsonMapper_Exception $e){ + throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e); + } + + return $discovery->result->serviceEnvironments->auth->prod->serviceUri; + } + + private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{ + $result = Internet::getURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH); + if($result === null || $result->getCode() !== 200){ + throw new \RuntimeException("Failed to fetch OpenID configuration from authorization service"); + } + + try{ + $json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + if(!is_object($json)){ + throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object"); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case + $mapper->bExceptionOnMissingData = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + try{ + /** @var AuthServiceOpenIdConfiguration $configuration */ + $configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration()); + }catch(\JsonMapper_Exception $e){ + throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e); + } + + return $configuration; + } + + /** + * @return array keys indexed by key ID + */ + private function getKeys(string $jwksUri) : array{ + $result = Internet::getURL($jwksUri); + if($result === null || $result->getCode() !== 200){ + return throw new \RuntimeException("Failed to fetch keys from authorization service"); + } + + try{ + $json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + + if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){ + throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object"); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = true; + $mapper->bExceptionOnMissingData = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + + $keys = []; + foreach($keysArray as $keyJson){ + if(!is_array($keyJson)){ + throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object"); + } + + try{ + /** @var AuthServiceKey $key */ + $key = $mapper->map($keyJson, new AuthServiceKey()); + $keys[$key->kid] = $key; + }catch(\JsonMapper_Exception $e){ + throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e); + } + } + + return $keys; + } + + public function onCompletion() : void{ + /** + * @var \Closure $callback + * @phpstan-var \Closure(?AuthServiceKey[] $keys, string $issuer, ?string[] $errors) : void $callback + */ + $callback = $this->fetchLocal(self::KEYS_ON_COMPLETION); + $callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize()); + } +} diff --git a/src/network/mcpe/auth/ProcessLegacyLoginTask.php b/src/network/mcpe/auth/ProcessLegacyLoginTask.php new file mode 100644 index 000000000..2be55dab6 --- /dev/null +++ b/src/network/mcpe/auth/ProcessLegacyLoginTask.php @@ -0,0 +1,121 @@ +|string|null + */ + private NonThreadSafeValue|string|null $error = "Unknown"; + /** Whether the player has a certificate chain link signed by the given root public key. */ + private bool $authenticated = false; + private ?string $clientPublicKeyDer = null; + + /** + * @param string[] $chainJwts + * @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion + */ + public function __construct( + array $chainJwts, + private string $clientDataJwt, + private ?string $rootAuthKeyDer, + private bool $authRequired, + \Closure $onCompletion + ){ + $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + $this->chain = igbinary_serialize($chainJwts) ?? throw new AssumptionFailedError("This should never fail"); + } + + public function onRun() : void{ + try{ + $this->clientPublicKeyDer = $this->validateChain(); + AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $this->clientPublicKeyDer); + $this->error = null; + }catch(VerifyLoginException $e){ + $disconnectMessage = $e->getDisconnectMessage(); + $this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage; + } + } + + private function validateChain() : string{ + /** @var string[] $chain */ + $chain = igbinary_unserialize($this->chain); + + $identityPublicKeyDer = null; + + foreach($chain as $jwt){ + $claims = AuthJwtHelper::validateLegacyAuthToken($jwt, $identityPublicKeyDer); + if($this->rootAuthKeyDer !== null && $identityPublicKeyDer === $this->rootAuthKeyDer){ + $this->authenticated = true; //we're signed into xbox live, according to this root key + } + if(!isset($claims->identityPublicKey)){ + throw new VerifyLoginException("Missing identityPublicKey in chain link", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey()); + } + $identityPublicKey = base64_decode($claims->identityPublicKey, true); + if($identityPublicKey === false){ + throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding"); + } + $identityPublicKeyDer = $identityPublicKey; + } + + if($identityPublicKeyDer === null){ + throw new VerifyLoginException("No authentication chain links provided"); + } + + return $identityPublicKeyDer; + } + + public function onCompletion() : void{ + /** + * @var \Closure $callback + * @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback + */ + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + $callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer); + } +} diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php deleted file mode 100644 index b4c9e6d9c..000000000 --- a/src/network/mcpe/auth/ProcessLoginTask.php +++ /dev/null @@ -1,213 +0,0 @@ -|string|null - */ - private NonThreadSafeValue|string|null $error = "Unknown"; - /** - * Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang - * root public key. - */ - private bool $authenticated = false; - private ?string $clientPublicKey = null; - - /** - * @param string[] $chainJwts - * @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion - */ - public function __construct( - array $chainJwts, - private string $clientDataJwt, - private bool $authRequired, - \Closure $onCompletion - ){ - $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); - $this->chain = igbinary_serialize($chainJwts); - } - - public function onRun() : void{ - try{ - $this->clientPublicKey = $this->validateChain(); - $this->error = null; - }catch(VerifyLoginException $e){ - $disconnectMessage = $e->getDisconnectMessage(); - $this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage; - } - } - - private function validateChain() : string{ - /** @var string[] $chain */ - $chain = igbinary_unserialize($this->chain); - - $currentKey = null; - $first = true; - - foreach($chain as $jwt){ - $this->validateToken($jwt, $currentKey, $first); - if($first){ - $first = false; - } - } - - /** @var string $clientKey */ - $clientKey = $currentKey; - - $this->validateToken($this->clientDataJwt, $currentKey); - - return $clientKey; - } - - /** - * @throws VerifyLoginException if errors are encountered - */ - private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{ - try{ - [$headersArray, $claimsArray, ] = JwtUtils::parse($jwt); - }catch(JwtException $e){ - throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e); - } - - $mapper = new \JsonMapper(); - $mapper->bExceptionOnMissingData = true; - $mapper->bExceptionOnUndefinedProperty = true; - $mapper->bStrictObjectTypeChecking = true; - $mapper->bEnforceMapType = false; - - try{ - /** @var JwtHeader $headers */ - $headers = $mapper->map($headersArray, new JwtHeader()); - }catch(\JsonMapper_Exception $e){ - throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e); - } - - $headerDerKey = base64_decode($headers->x5u, true); - if($headerDerKey === false){ - throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u"); - } - - if($currentPublicKey === null){ - if(!$first){ - throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey()); - } - }elseif($headerDerKey !== $currentPublicKey){ - //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway - throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature()); - } - - try{ - $signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey); - }catch(JwtException $e){ - throw new VerifyLoginException("Invalid JWT public key: " . $e->getMessage(), null, 0, $e); - } - try{ - if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){ - throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature()); - } - }catch(JwtException $e){ - throw new VerifyLoginException($e->getMessage(), null, 0, $e); - } - - if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){ - $this->authenticated = true; //we're signed into xbox live - } - - $mapper = new \JsonMapper(); - $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case - $mapper->bExceptionOnMissingData = true; - $mapper->bStrictObjectTypeChecking = true; - $mapper->bEnforceMapType = false; - $mapper->bRemoveUndefinedAttributes = true; - try{ - /** @var JwtChainLinkBody $claims */ - $claims = $mapper->map($claimsArray, new JwtChainLinkBody()); - }catch(\JsonMapper_Exception $e){ - throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e); - } - - $time = time(); - if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){ - throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly()); - } - - if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){ - throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate()); - } - - if(isset($claims->identityPublicKey)){ - $identityPublicKey = base64_decode($claims->identityPublicKey, true); - if($identityPublicKey === false){ - throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding"); - } - try{ - //verify key format and parameters - JwtUtils::parseDerPublicKey($identityPublicKey); - }catch(JwtException $e){ - throw new VerifyLoginException("Invalid identityPublicKey: " . $e->getMessage(), null, 0, $e); - } - $currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this - } - } - - public function onCompletion() : void{ - /** - * @var \Closure $callback - * @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback - */ - $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); - $callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey); - } -} diff --git a/src/network/mcpe/auth/ProcessOpenIdLoginTask.php b/src/network/mcpe/auth/ProcessOpenIdLoginTask.php new file mode 100644 index 000000000..091317a65 --- /dev/null +++ b/src/network/mcpe/auth/ProcessOpenIdLoginTask.php @@ -0,0 +1,98 @@ +|string|null + */ + private NonThreadSafeValue|string|null $error = "Unknown"; + /** + * Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang + * root public key. + */ + private bool $authenticated = false; + private ?string $clientPublicKeyDer = null; + + /** + * @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion + */ + public function __construct( + private string $jwt, + private string $issuer, + private string $mojangPublicKeyDer, + private string $clientDataJwt, + private bool $authRequired, + \Closure $onCompletion + ){ + $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + } + + public function onRun() : void{ + try{ + $this->clientPublicKeyDer = $this->validateChain(); + $this->error = null; + }catch(VerifyLoginException $e){ + $disconnectMessage = $e->getDisconnectMessage(); + $this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage; + } + } + + private function validateChain() : string{ + $claims = AuthJwtHelper::validateOpenIdAuthToken($this->jwt, $this->mojangPublicKeyDer, issuer: $this->issuer, audience: self::MOJANG_AUDIENCE); + //validateToken will throw if the JWT is not valid + $this->authenticated = true; + + $clientDerKey = base64_decode($claims->cpk, strict: true); + if($clientDerKey === false){ + throw new VerifyLoginException("Invalid client public key: base64 error decoding"); + } + //no further validation needed - OpenSSL will bail if the key is invalid + AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $clientDerKey); + + return $clientDerKey; + } + + public function onCompletion() : void{ + /** + * @var \Closure $callback + * @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback + */ + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + $callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer); + } +} diff --git a/src/network/mcpe/cache/ChunkCache.php b/src/network/mcpe/cache/ChunkCache.php index 12e769776..2b4265fcf 100644 --- a/src/network/mcpe/cache/ChunkCache.php +++ b/src/network/mcpe/cache/ChunkCache.php @@ -32,6 +32,7 @@ use pocketmine\world\ChunkListener; use pocketmine\world\ChunkListenerNoOpTrait; use pocketmine\world\format\Chunk; use pocketmine\world\World; +use function is_string; use function spl_object_id; use function strlen; @@ -69,7 +70,7 @@ class ChunkCache implements ChunkListener{ foreach(self::$instances as $compressorMap){ foreach($compressorMap as $chunkCache){ foreach($chunkCache->caches as $chunkHash => $promise){ - if($promise->hasResult()){ + if(is_string($promise)){ //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them unset($chunkCache->caches[$chunkHash]); } @@ -79,60 +80,74 @@ class ChunkCache implements ChunkListener{ } /** - * @var CompressBatchPromise[] - * @phpstan-var array + * @var CompressBatchPromise[]|string[] + * @phpstan-var array */ private array $caches = []; private int $hits = 0; private int $misses = 0; + /** + * @phpstan-param DimensionIds::* $dimensionId + */ private function __construct( private World $world, - private Compressor $compressor + private Compressor $compressor, + private int $dimensionId = DimensionIds::OVERWORLD ){} - /** - * Requests asynchronous preparation of the chunk at the given coordinates. - * - * @return CompressBatchPromise a promise of resolution which will contain a compressed chunk packet. - */ - public function request(int $chunkX, int $chunkZ) : CompressBatchPromise{ + private function prepareChunkAsync(int $chunkX, int $chunkZ, int $chunkHash) : CompressBatchPromise{ $this->world->registerChunkListener($this, $chunkX, $chunkZ); $chunk = $this->world->getChunk($chunkX, $chunkZ); if($chunk === null){ throw new \InvalidArgumentException("Cannot request an unloaded chunk"); } - $chunkHash = World::chunkHash($chunkX, $chunkZ); - - if(isset($this->caches[$chunkHash])){ - ++$this->hits; - return $this->caches[$chunkHash]; - } - ++$this->misses; $this->world->timings->syncChunkSendPrepare->startTiming(); try{ - $this->caches[$chunkHash] = new CompressBatchPromise(); + $promise = new CompressBatchPromise(); $this->world->getServer()->getAsyncPool()->submitTask( new ChunkRequestTask( $chunkX, $chunkZ, - DimensionIds::OVERWORLD, //TODO: not hardcode this + $this->dimensionId, $chunk, - $this->caches[$chunkHash], + $promise, $this->compressor ) ); + $this->caches[$chunkHash] = $promise; + $promise->onResolve(function(CompressBatchPromise $promise) use ($chunkHash) : void{ + //the promise may have been discarded or replaced if the chunk was unloaded or modified in the meantime + if(($this->caches[$chunkHash] ?? null) === $promise){ + $this->caches[$chunkHash] = $promise->getResult(); + } + }); - return $this->caches[$chunkHash]; + return $promise; }finally{ $this->world->timings->syncChunkSendPrepare->stopTiming(); } } + /** + * Requests asynchronous preparation of the chunk at the given coordinates. + * + * @return CompressBatchPromise|string Compressed chunk packet, or a promise for one to be resolved asynchronously. + */ + public function request(int $chunkX, int $chunkZ) : CompressBatchPromise|string{ + $chunkHash = World::chunkHash($chunkX, $chunkZ); + if(isset($this->caches[$chunkHash])){ + ++$this->hits; + return $this->caches[$chunkHash]; + } + + return $this->prepareChunkAsync($chunkX, $chunkZ, $chunkHash); + } + private function destroy(int $chunkX, int $chunkZ) : bool{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $existing = $this->caches[$chunkHash] ?? null; @@ -148,12 +163,12 @@ class ChunkCache implements ChunkListener{ $chunkPosHash = World::chunkHash($chunkX, $chunkZ); $cache = $this->caches[$chunkPosHash] ?? null; if($cache !== null){ - if(!$cache->hasResult()){ + if(!is_string($cache)){ //some requesters are waiting for this chunk, so their request needs to be fulfilled $cache->cancel(); unset($this->caches[$chunkPosHash]); - $this->request($chunkX, $chunkZ)->onResolve(...$cache->getResolveCallbacks()); + $this->prepareChunkAsync($chunkX, $chunkZ, $chunkPosHash)->onResolve(...$cache->getResolveCallbacks()); }else{ //dump the cache, it'll be regenerated the next time it's requested $this->destroy($chunkX, $chunkZ); @@ -199,8 +214,8 @@ class ChunkCache implements ChunkListener{ public function calculateCacheSize() : int{ $result = 0; foreach($this->caches as $cache){ - if($cache->hasResult()){ - $result += strlen($cache->getResult()); + if(is_string($cache)){ + $result += strlen($cache); } } return $result; diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php index 14523f74c..d873a53f0 100644 --- a/src/network/mcpe/cache/CraftingDataCache.php +++ b/src/network/mcpe/cache/CraftingDataCache.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\cache; +use pmmp\encoding\BE; use pocketmine\crafting\CraftingManager; use pocketmine\crafting\FurnaceType; use pocketmine\crafting\ShapedRecipe; @@ -41,7 +42,6 @@ use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShaped use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe; use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; -use pocketmine\utils\Binary; use pocketmine\utils\SingletonTrait; use Ramsey\Uuid\Uuid; use function array_map; @@ -56,6 +56,12 @@ final class CraftingDataCache{ */ private array $caches = []; + /** + * The client doesn't like recipes with ID 0 (as of 1.21.100) and complains about them in the content log + * This doesn't actually affect the function of the recipe, but it is annoying, so this offset fixes it + */ + public const RECIPE_ID_OFFSET = 1; + public function getCache(CraftingManager $manager) : CraftingDataPacket{ $id = spl_object_id($manager); if(!isset($this->caches[$id])){ @@ -82,6 +88,8 @@ final class CraftingDataCache{ $noUnlockingRequirement = new RecipeUnlockingRequirement(null); foreach($manager->getCraftingRecipeIndex() as $index => $recipe){ + //the client doesn't like recipes with an ID of 0, so we need to offset them + $recipeNetId = $index + self::RECIPE_ID_OFFSET; if($recipe instanceof ShapelessRecipe){ $typeTag = match($recipe->getType()){ ShapelessRecipeType::CRAFTING => CraftingRecipeBlockName::CRAFTING_TABLE, @@ -91,14 +99,14 @@ final class CraftingDataCache{ }; $recipesWithTypeIds[] = new ProtocolShapelessRecipe( CraftingDataPacket::ENTRY_SHAPELESS, - Binary::writeInt($index), + BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable array_map($converter->coreRecipeIngredientToNet(...), $recipe->getIngredientList()), array_map($converter->coreItemStackToNet(...), $recipe->getResults()), $nullUUID, $typeTag, 50, $noUnlockingRequirement, - $index + $recipeNetId ); }elseif($recipe instanceof ShapedRecipe){ $inputs = []; @@ -110,7 +118,7 @@ final class CraftingDataCache{ } $recipesWithTypeIds[] = $r = new ProtocolShapedRecipe( CraftingDataPacket::ENTRY_SHAPED, - Binary::writeInt($index), + BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable $inputs, array_map($converter->coreItemStackToNet(...), $recipe->getResults()), $nullUUID, @@ -118,7 +126,7 @@ final class CraftingDataCache{ 50, true, $noUnlockingRequirement, - $index, + $recipeNetId, ); }else{ //TODO: probably special recipe types diff --git a/src/network/mcpe/cache/CreativeInventoryCache.php b/src/network/mcpe/cache/CreativeInventoryCache.php index 04fc52604..e543f343e 100644 --- a/src/network/mcpe/cache/CreativeInventoryCache.php +++ b/src/network/mcpe/cache/CreativeInventoryCache.php @@ -23,23 +23,30 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\cache; +use pocketmine\inventory\CreativeCategory; use pocketmine\inventory\CreativeInventory; +use pocketmine\lang\Translatable; use pocketmine\network\mcpe\convert\TypeConverter; +use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\CreativeContentPacket; -use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeItemEntry; +use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\utils\SingletonTrait; +use function is_string; use function spl_object_id; +use const PHP_INT_MIN; final class CreativeInventoryCache{ use SingletonTrait; /** - * @var CreativeContentPacket[] - * @phpstan-var array + * @var CreativeInventoryCacheEntry[] + * @phpstan-var array */ private array $caches = []; - public function getCache(CreativeInventory $inventory) : CreativeContentPacket{ + private function getCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{ $id = spl_object_id($inventory); if(!isset($this->caches[$id])){ $inventory->getDestructorCallbacks()->add(function() use ($id) : void{ @@ -48,7 +55,7 @@ final class CreativeInventoryCache{ $inventory->getContentChangedCallbacks()->add(function() use ($id) : void{ unset($this->caches[$id]); }); - $this->caches[$id] = $this->buildCreativeInventoryCache($inventory); + $this->caches[$id] = $this->buildCacheEntry($inventory); } return $this->caches[$id]; } @@ -56,14 +63,91 @@ final class CreativeInventoryCache{ /** * Rebuild the cache for the given inventory. */ - private function buildCreativeInventoryCache(CreativeInventory $inventory) : CreativeContentPacket{ - $entries = []; + private function buildCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{ + $categories = []; + $groups = []; + $typeConverter = TypeConverter::getInstance(); - //creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent - foreach($inventory->getAll() as $k => $item){ - $entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item)); + + $nextIndex = 0; + $groupIndexes = []; + $itemGroupIndexes = []; + + foreach($inventory->getAllEntries() as $k => $entry){ + $group = $entry->getGroup(); + $category = $entry->getCategory(); + if($group === null){ + $groupId = PHP_INT_MIN; + }else{ + $groupId = spl_object_id($group); + unset($groupIndexes[$category->name][PHP_INT_MIN]); //start a new anonymous group for this category + } + + //group object may be reused by multiple categories + if(!isset($groupIndexes[$category->name][$groupId])){ + $groupIndexes[$category->name][$groupId] = $nextIndex++; + $categories[] = $category; + $groups[] = $group; + } + $itemGroupIndexes[$k] = $groupIndexes[$category->name][$groupId]; } - return CreativeContentPacket::create($entries); + //creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent + $items = []; + foreach($inventory->getAllEntries() as $k => $entry){ + $items[] = new CreativeItemEntry( + $k, + $typeConverter->coreItemStackToNet($entry->getItem()), + $itemGroupIndexes[$k] + ); + } + + return new CreativeInventoryCacheEntry($categories, $groups, $items); + } + + public function buildPacket(CreativeInventory $inventory, NetworkSession $session) : CreativeContentPacket{ + $player = $session->getPlayer() ?? throw new \LogicException("Cannot prepare creative data for a session without a player"); + $language = $player->getLanguage(); + $forceLanguage = $player->getServer()->isLanguageForced(); + $typeConverter = $session->getTypeConverter(); + $cachedEntry = $this->getCacheEntry($inventory); + $translate = function(Translatable|string $translatable) use ($session, $language, $forceLanguage) : string{ + if(is_string($translatable)){ + $message = $translatable; + }elseif(!$forceLanguage){ + [$message,] = $session->prepareClientTranslatableMessage($translatable); + }else{ + $message = $language->translate($translatable); + } + return $message; + }; + + $groupEntries = []; + foreach($cachedEntry->categories as $index => $category){ + $group = $cachedEntry->groups[$index]; + $categoryId = match ($category) { + CreativeCategory::CONSTRUCTION => CreativeContentPacket::CATEGORY_CONSTRUCTION, + CreativeCategory::NATURE => CreativeContentPacket::CATEGORY_NATURE, + CreativeCategory::EQUIPMENT => CreativeContentPacket::CATEGORY_EQUIPMENT, + CreativeCategory::ITEMS => CreativeContentPacket::CATEGORY_ITEMS + }; + if($group === null){ + $groupEntries[] = new CreativeGroupEntry($categoryId, "", ItemStack::null()); + }else{ + $groupIcon = $group->getIcon(); + //TODO: HACK! In 1.21.60, Workaround glitchy behaviour when an item is used as an icon for a group it + //doesn't belong to. Without this hack, both instances of the item will show a +, but neither of them + //will actually expand the group work correctly. + $groupIcon->getNamedTag()->setInt("___GroupBugWorkaround___", $index); + $groupName = $group->getName(); + $groupEntries[] = new CreativeGroupEntry( + $categoryId, + $translate($groupName), + $typeConverter->coreItemStackToNet($groupIcon) + ); + } + } + + return CreativeContentPacket::create($groupEntries, $cachedEntry->items); } } diff --git a/src/network/mcpe/cache/CreativeInventoryCacheEntry.php b/src/network/mcpe/cache/CreativeInventoryCacheEntry.php new file mode 100644 index 000000000..1fc0767df --- /dev/null +++ b/src/network/mcpe/cache/CreativeInventoryCacheEntry.php @@ -0,0 +1,48 @@ + $categories + * @phpstan-param list $groups + * @phpstan-param list $items + */ + public function __construct( + public readonly array $categories, + public readonly array $groups, + public readonly array $items, + ){ + //NOOP + } +} diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php index 88a522600..861881437 100644 --- a/src/network/mcpe/cache/StaticPacketCache.php +++ b/src/network/mcpe/cache/StaticPacketCache.php @@ -23,13 +23,22 @@ declare(strict_types=1); 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 @@ class StaticPacketCache{ 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/convert/ItemTypeDictionaryFromDataHelper.php b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php index d962063d3..ed8ae2cc7 100644 --- a/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php +++ b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php @@ -23,10 +23,15 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; +use pocketmine\errorhandler\ErrorToExceptionHandler; +use pocketmine\nbt\LittleEndianNbtSerializer; +use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; +use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; +use function base64_decode; use function is_array; use function is_bool; use function is_int; @@ -41,12 +46,15 @@ final class ItemTypeDictionaryFromDataHelper{ throw new AssumptionFailedError("Invalid item list format"); } + $emptyNBT = new CacheableNbt(new CompoundTag()); + $nbtSerializer = new LittleEndianNbtSerializer(); + $params = []; foreach(Utils::promoteKeys($table) as $name => $entry){ - if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"])){ + if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"], $entry["version"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"]) || !is_int($entry["version"]) || !(is_string($componentNbt = $entry["component_nbt"] ?? null) || $componentNbt === null)){ throw new AssumptionFailedError("Invalid item list format"); } - $params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"]); + $params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"], $entry["version"], $componentNbt === null ? $emptyNBT : new CacheableNbt($nbtSerializer->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($componentNbt, true)))->mustGetCompoundTag())); } return new ItemTypeDictionary($params); } diff --git a/src/network/mcpe/convert/TypeConverter.php b/src/network/mcpe/convert/TypeConverter.php index e886b2b8b..9f946ac33 100644 --- a/src/network/mcpe/convert/TypeConverter.php +++ b/src/network/mcpe/convert/TypeConverter.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; +use pmmp\encoding\ByteBufferReader; +use pmmp\encoding\ByteBufferWriter; +use pocketmine\block\tile\Container; use pocketmine\block\VanillaBlocks; use pocketmine\crafting\ExactRecipeIngredient; use pocketmine\crafting\MetaWildcardRecipeIngredient; @@ -31,12 +34,18 @@ use pocketmine\crafting\TagWildcardRecipeIngredient; use pocketmine\data\bedrock\BedrockDataFiles; use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\ItemTypeNames; +use pocketmine\data\SavedDataLoadingException; use pocketmine\item\Item; use pocketmine\item\VanillaItems; +use pocketmine\nbt\LittleEndianNbtSerializer; +use pocketmine\nbt\NBT; use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\tag\ListTag; +use pocketmine\nbt\tag\Tag; +use pocketmine\nbt\TreeRoot; +use pocketmine\nbt\UnexpectedTagTypeException; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; -use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData; @@ -52,11 +61,13 @@ use pocketmine\utils\SingletonTrait; use pocketmine\world\format\io\GlobalBlockStateHandlers; use pocketmine\world\format\io\GlobalItemDataHandlers; use function get_class; +use function hash; class TypeConverter{ use SingletonTrait; private const PM_ID_TAG = "___Id___"; + private const PM_FULL_NBT_HASH_TAG = "___FullNbtHash___"; private const RECIPE_INPUT_WILDCARD_META = 0x7fff; @@ -197,6 +208,84 @@ class TypeConverter{ return new ExactRecipeIngredient($result); } + /** + * Strips unnecessary block actor NBT from items that have it. + * This tag can potentially be extremely large, and is not read by the client anyway. + */ + protected function stripBlockEntityNBT(CompoundTag $tag) : bool{ + if(($tag->getTag(Item::TAG_BLOCK_ENTITY_TAG)) !== null){ + //client doesn't use this tag, so it's fine to delete completely + $tag->removeTag(Item::TAG_BLOCK_ENTITY_TAG); + return true; + } + return false; + } + + /** + * Strips non-viewable data from shulker boxes and similar blocks + * The lore for shulker boxes only requires knowing the type & count of items and possibly custom name + * We don't need to, and should not allow, sending nested inventories across the network. + */ + protected function stripContainedItemNonVisualNBT(CompoundTag $tag) : bool{ + try{ + $blockEntityInventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class); + }catch(UnexpectedTagTypeException){ + return false; + } + if($blockEntityInventoryTag !== null && $blockEntityInventoryTag->count() > 0){ + $stripped = new ListTag(); + + foreach($blockEntityInventoryTag as $itemTag){ + try{ + $containedItem = Item::nbtDeserialize($itemTag); + $customName = $containedItem->getCustomName(); + $containedItem->clearNamedTag(); + $containedItem->setCustomName($customName); + $stripped->push($containedItem->nbtSerialize()); + }catch(SavedDataLoadingException){ + continue; + } + } + $tag->setTag(Container::TAG_ITEMS, $stripped); + return true; + } + return false; + } + + /** + * Computes a hash of an item's server-side NBT. + * This is baked into an item's network NBT to make sure the client doesn't try to stack items with the same network + * NBT but different server-side NBT. + */ + protected function hashNBT(Tag $tag) : string{ + $encoded = (new LittleEndianNbtSerializer())->write(new TreeRoot($tag)); + return hash('sha256', $encoded, binary: true); + } + + /** + * TODO: HACK! + * Creates a copy of an item's NBT with non-viewable data stripped. + * This is a pretty yucky hack that's mainly needed because of inventories inside blockitems containing blockentity + * data. There isn't really a good way to deal with this due to the way tiles currently require a position, + * otherwise we could just keep a copy of the tile context and ask it for persistent vs network NBT as needed. + * Unfortunately, making this nice will require significant BC breaks, so this will have to do for now. + */ + protected function cleanupUnnecessaryItemNBT(CompoundTag $original) : CompoundTag{ + $tag = clone $original; + $anythingStripped = false; + foreach([ + $this->stripContainedItemNonVisualNBT($tag), + $this->stripBlockEntityNBT($tag) + ] as $stripped){ + $anythingStripped = $anythingStripped || $stripped; + } + + if($anythingStripped){ + $tag->setByteArray(self::PM_FULL_NBT_HASH_TAG, $this->hashNBT($original)); + } + return $tag; + } + public function coreItemStackToNet(Item $itemStack) : ItemStack{ if($itemStack->isNull()){ return ItemStack::null(); @@ -205,7 +294,7 @@ class TypeConverter{ if($nbt->count() === 0){ $nbt = null; }else{ - $nbt = clone $nbt; + $nbt = $this->cleanupUnnecessaryItemNBT($nbt); } $idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack); @@ -224,7 +313,7 @@ class TypeConverter{ $extraData = $id === $this->shieldRuntimeId ? new ItemStackExtraDataShield($nbt, canPlaceOn: [], canDestroy: [], blockingTick: 0) : new ItemStackExtraData($nbt, canPlaceOn: [], canDestroy: []); - $extraDataSerializer = PacketSerializer::encoder(); + $extraDataSerializer = new ByteBufferWriter(); $extraData->write($extraDataSerializer); return new ItemStack( @@ -232,7 +321,7 @@ class TypeConverter{ $meta, $itemStack->getCount(), $blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID, - $extraDataSerializer->getBuffer(), + $extraDataSerializer->getData(), ); } @@ -271,7 +360,7 @@ class TypeConverter{ } public function deserializeItemStackExtraData(string $extraData, int $id) : ItemStackExtraData{ - $extraDataDeserializer = PacketSerializer::decoder($extraData, 0); + $extraDataDeserializer = new ByteBufferReader($extraData); return $id === $this->shieldRuntimeId ? ItemStackExtraDataShield::read($extraDataDeserializer) : ItemStackExtraData::read($extraDataDeserializer); diff --git a/src/network/mcpe/encryption/EncryptionContext.php b/src/network/mcpe/encryption/EncryptionContext.php index 8c0b9bf74..6d76a84ad 100644 --- a/src/network/mcpe/encryption/EncryptionContext.php +++ b/src/network/mcpe/encryption/EncryptionContext.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\encryption; use Crypto\Cipher; -use pocketmine\utils\Binary; +use pmmp\encoding\LE; use function bin2hex; use function openssl_digest; use function openssl_error_string; @@ -104,7 +104,7 @@ class EncryptionContext{ } private function calculateChecksum(int $counter, string $payload) : string{ - $hash = openssl_digest(Binary::writeLLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true); + $hash = openssl_digest(LE::packUnsignedLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true); if($hash === false){ throw new \RuntimeException("openssl_digest() error: " . openssl_error_string()); } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index 6c7402b52..0a296173a 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -64,7 +64,6 @@ use pocketmine\network\mcpe\protocol\ItemStackResponsePacket; use pocketmine\network\mcpe\protocol\LabTablePacket; use pocketmine\network\mcpe\protocol\LecternUpdatePacket; use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacketV1; use pocketmine\network\mcpe\protocol\MapInfoRequestPacket; use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket; use pocketmine\network\mcpe\protocol\MobEquipmentPacket; @@ -74,9 +73,9 @@ use pocketmine\network\mcpe\protocol\NetworkStackLatencyPacket; 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; use pocketmine\network\mcpe\protocol\ServerSettingsRequestPacket; use pocketmine\network\mcpe\protocol\SetActorMotionPacket; use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket; @@ -90,6 +89,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds; use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData; use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction; use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData; +use pocketmine\network\mcpe\protocol\types\inventory\PredictedResult; use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest; use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse; @@ -135,7 +135,9 @@ class InGamePacketHandler extends PacketHandler{ protected ?Vector3 $lastPlayerAuthInputPosition = null; protected ?float $lastPlayerAuthInputYaw = null; protected ?float $lastPlayerAuthInputPitch = null; - protected ?int $lastPlayerAuthInputFlags = null; + protected ?BitSet $lastPlayerAuthInputFlags = null; + + protected ?BlockPosition $lastBlockAttacked = null; public bool $forceMoveSync = false; @@ -161,9 +163,9 @@ class InGamePacketHandler extends PacketHandler{ return true; } - private function resolveOnOffInputFlags(int $inputFlags, int $startFlag, int $stopFlag) : ?bool{ - $enabled = ($inputFlags & (1 << $startFlag)) !== 0; - $disabled = ($inputFlags & (1 << $stopFlag)) !== 0; + private function resolveOnOffInputFlags(BitSet $inputFlags, int $startFlag, int $stopFlag) : ?bool{ + $enabled = $inputFlags->get($startFlag); + $disabled = $inputFlags->get($stopFlag); if($enabled !== $disabled){ return $enabled; } @@ -212,10 +214,13 @@ class InGamePacketHandler extends PacketHandler{ } $inputFlags = $packet->getInputFlags(); - if($inputFlags !== $this->lastPlayerAuthInputFlags){ + if($this->lastPlayerAuthInputFlags === null || !$inputFlags->equals($this->lastPlayerAuthInputFlags)){ $this->lastPlayerAuthInputFlags = $inputFlags; - $sneaking = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_SNEAKING, PlayerAuthInputFlags::STOP_SNEAKING); + $sneaking = $inputFlags->get(PlayerAuthInputFlags::SNEAKING); + if($this->player->isSneaking() === $sneaking){ + $sneaking = null; + } $sprinting = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_SPRINTING, PlayerAuthInputFlags::STOP_SPRINTING); $swimming = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_SWIMMING, PlayerAuthInputFlags::STOP_SWIMMING); $gliding = $this->resolveOnOffInputFlags($inputFlags, PlayerAuthInputFlags::START_GLIDING, PlayerAuthInputFlags::STOP_GLIDING); @@ -230,10 +235,10 @@ class InGamePacketHandler extends PacketHandler{ $this->player->sendData([$this->player]); } - if($packet->hasFlag(PlayerAuthInputFlags::START_JUMPING)){ + if($inputFlags->get(PlayerAuthInputFlags::START_JUMPING)){ $this->player->jump(); } - if($packet->hasFlag(PlayerAuthInputFlags::MISSED_SWING)){ + if($inputFlags->get(PlayerAuthInputFlags::MISSED_SWING)){ $this->player->missSwing(); } } @@ -246,6 +251,28 @@ class InGamePacketHandler extends PacketHandler{ $packetHandled = true; + $useItemTransaction = $packet->getItemInteractionData(); + if($useItemTransaction !== null){ + if(count($useItemTransaction->getTransactionData()->getActions()) > 100){ + throw new PacketHandlingException("Too many actions in item use transaction"); + } + + $this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId()); + $this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions()); + if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){ + $packetHandled = false; + $this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")"); + }else{ + $this->inventoryManager->syncMismatchedPredictedSlotChanges(); + } + $this->inventoryManager->setCurrentItemStackRequestId(null); + } + + $itemStackRequest = $packet->getItemStackRequest(); + $itemStackResponseBuilder = $itemStackRequest !== null ? $this->handleSingleItemStackRequest($itemStackRequest) : null; + + //itemstack request or transaction may set predictions for the outcome of these actions, so these need to be + //processed last $blockActions = $packet->getBlockActions(); if($blockActions !== null){ if(count($blockActions) > 100){ @@ -266,36 +293,14 @@ class InGamePacketHandler extends PacketHandler{ } } - $useItemTransaction = $packet->getItemInteractionData(); - if($useItemTransaction !== null){ - if(count($useItemTransaction->getTransactionData()->getActions()) > 100){ - throw new PacketHandlingException("Too many actions in item use transaction"); - } - - $this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId()); - $this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions()); - if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){ - $packetHandled = false; - $this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")"); - }else{ - $this->inventoryManager->syncMismatchedPredictedSlotChanges(); - } - $this->inventoryManager->setCurrentItemStackRequestId(null); - } - - $itemStackRequest = $packet->getItemStackRequest(); if($itemStackRequest !== null){ - $result = $this->handleSingleItemStackRequest($itemStackRequest); - $this->session->sendDataPacket(ItemStackResponsePacket::create([$result])); + $itemStackResponse = $itemStackResponseBuilder?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $itemStackRequest->getRequestId()); + $this->session->sendDataPacket(ItemStackResponsePacket::create([$itemStackResponse])); } return $packetHandled; } - public function handleLevelSoundEventPacketV1(LevelSoundEventPacketV1 $packet) : bool{ - return true; //useless leftover from 1.8 - } - public function handleActorEvent(ActorEventPacket $packet) : bool{ if($packet->actorRuntimeId !== $this->player->getId()){ //TODO HACK: EATING_ITEM is sent back to the server when the server sends it for other players (1.14 bug, maybe earlier) @@ -412,7 +417,7 @@ class InGamePacketHandler extends PacketHandler{ $droppedCount = null; foreach($data->getActions() as $networkInventoryAction){ - if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD && $networkInventoryAction->inventorySlot == NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){ + if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD && $networkInventoryAction->inventorySlot === NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){ $droppedCount = $networkInventoryAction->newItem->getItemStack()->getCount(); if($droppedCount <= 0){ throw new PacketHandlingException("Expected positive count for dropped item"); @@ -494,17 +499,12 @@ class InGamePacketHandler extends PacketHandler{ $blockPos = $data->getBlockPosition(); $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); $this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos); - //always sync this in case plugins caused a different result than the client expected - //we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating - //more information up the stack. For now I think this is good enough. - //if only the client would tell us what blocks it thinks changed... - $this->syncBlocksNearby($vBlockPos, $data->getFace()); - return true; - case UseItemTransactionData::ACTION_BREAK_BLOCK: - $blockPos = $data->getBlockPosition(); - $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); - if(!$this->player->breakBlock($vBlockPos)){ - $this->syncBlocksNearby($vBlockPos, null); + if($data->getClientInteractPrediction() === PredictedResult::SUCCESS){ + //always sync this in case plugins caused a different result than the client expected + //we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating + //more information up the stack. For now I think this is good enough. + //if only the client would tell us what blocks it thinks changed... + $this->syncBlocksNearby($vBlockPos, $data->getFace()); } return true; case UseItemTransactionData::ACTION_CLICK_AIR: @@ -574,7 +574,7 @@ class InGamePacketHandler extends PacketHandler{ private function handleReleaseItemTransaction(ReleaseItemTransactionData $data) : bool{ $this->player->selectHotbarSlot($data->getHotbarSlot()); - if($data->getActionType() == ReleaseItemTransactionData::ACTION_RELEASE){ + if($data->getActionType() === ReleaseItemTransactionData::ACTION_RELEASE){ $this->player->releaseHeldItem(); return true; } @@ -582,7 +582,7 @@ class InGamePacketHandler extends PacketHandler{ return false; } - private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{ + private function handleSingleItemStackRequest(ItemStackRequest $request) : ?ItemStackResponseBuilder{ if(count($request->getActions()) > 60){ //recipe book auto crafting can affect all slots of the inventory when consuming inputs or producing outputs //this means there could be as many as 50 CraftingConsumeInput actions or Place (taking the result) actions @@ -599,7 +599,11 @@ class InGamePacketHandler extends PacketHandler{ $executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request); try{ $transaction = $executor->generateInventoryTransaction(); - $result = $this->executeInventoryTransaction($transaction, $request->getRequestId()); + if($transaction !== null){ + $result = $this->executeInventoryTransaction($transaction, $request->getRequestId()); + }else{ + $result = true; //predictions only, just send responses + } }catch(ItemStackRequestProcessException $e){ $result = false; $this->session->getLogger()->debug("ItemStackRequest #" . $request->getRequestId() . " failed: " . $e->getMessage()); @@ -607,10 +611,7 @@ class InGamePacketHandler extends PacketHandler{ $this->inventoryManager->requestSyncAll(); } - if(!$result){ - return new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId()); - } - return $executor->buildItemStackResponse(); + return $result ? $executor->getItemStackResponseBuilder() : null; } public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{ @@ -620,7 +621,7 @@ class InGamePacketHandler extends PacketHandler{ throw new PacketHandlingException("Too many requests in ItemStackRequestPacket"); } foreach($packet->getRequests() as $request){ - $responses[] = $this->handleSingleItemStackRequest($request); + $responses[] = $this->handleSingleItemStackRequest($request)?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId()); } $this->session->sendDataPacket(ItemStackResponsePacket::create($responses)); @@ -683,16 +684,27 @@ class InGamePacketHandler extends PacketHandler{ switch($action){ case PlayerAction::START_BREAK: + case PlayerAction::CONTINUE_DESTROY_BLOCK: //destroy the next block while holding down left click self::validateFacing($face); + if($this->lastBlockAttacked !== null && $blockPosition->equals($this->lastBlockAttacked)){ + //the client will send CONTINUE_DESTROY_BLOCK for the currently targeted block directly before it + //sends PREDICT_DESTROY_BLOCK, but also when it starts to break the block + //this seems like a bug in the client and would cause spurious left-click events if we allowed it to + //be delivered to the player + $this->session->getLogger()->debug("Ignoring PlayerAction $action on $pos because we were already destroying this block"); + break; + } if(!$this->player->attackBlock($pos, $face)){ $this->syncBlocksNearby($pos, $face); } + $this->lastBlockAttacked = $blockPosition; break; case PlayerAction::ABORT_BREAK: case PlayerAction::STOP_BREAK: $this->player->stopBreakBlock($pos); + $this->lastBlockAttacked = null; break; case PlayerAction::START_SLEEPING: //unused @@ -703,11 +715,19 @@ class InGamePacketHandler extends PacketHandler{ case PlayerAction::CRACK_BREAK: self::validateFacing($face); $this->player->continueBreakBlock($pos, $face); + $this->lastBlockAttacked = $blockPosition; break; case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now) break; case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK: - //TODO: do we need to handle this? + //in server auth block breaking, we get PREDICT_DESTROY_BLOCK anyway, so this action is redundant + break; + case PlayerAction::PREDICT_DESTROY_BLOCK: + self::validateFacing($face); + if(!$this->player->breakBlock($pos)){ + $this->syncBlocksNearby($pos, $face); + } + $this->lastBlockAttacked = null; break; case PlayerAction::START_ITEM_USE_ON: case PlayerAction::STOP_ITEM_USE_ON: @@ -740,6 +760,43 @@ class InGamePacketHandler extends PacketHandler{ return true; //this packet is useless } + /** + * @throws PacketHandlingException + */ + private function updateSignText(CompoundTag $nbt, string $tagName, bool $frontFace, BaseSign $block, Vector3 $pos) : bool{ + $textTag = $nbt->getTag($tagName); + if(!$textTag instanceof CompoundTag){ + throw new PacketHandlingException("Invalid tag type " . get_debug_type($textTag) . " for tag \"$tagName\" in sign update data"); + } + $textBlobTag = $textTag->getTag(Sign::TAG_TEXT_BLOB); + if(!$textBlobTag instanceof StringTag){ + throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data"); + } + + try{ + $text = SignText::fromBlob($textBlobTag->getValue()); + }catch(\InvalidArgumentException $e){ + throw PacketHandlingException::wrap($e, "Invalid sign text update"); + } + + $oldText = $block->getFaceText($frontFace); + if($text->getLines() === $oldText->getLines()){ + return false; + } + + try{ + if(!$block->updateFaceText($this->player, $frontFace, $text)){ + foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){ + $this->session->sendDataPacket($updatePacket); + } + return false; + } + return true; + }catch(\UnexpectedValueException $e){ + throw PacketHandlingException::wrap($e); + } + } + public function handleBlockActorData(BlockActorDataPacket $packet) : bool{ $pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()); if($pos->distanceSquared($this->player->getLocation()) > 10000){ @@ -751,29 +808,9 @@ class InGamePacketHandler extends PacketHandler{ if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit if($block instanceof BaseSign){ - $frontTextTag = $nbt->getTag(Sign::TAG_FRONT_TEXT); - if(!$frontTextTag instanceof CompoundTag){ - throw new PacketHandlingException("Invalid tag type " . get_debug_type($frontTextTag) . " for tag \"" . Sign::TAG_FRONT_TEXT . "\" in sign update data"); - } - $textBlobTag = $frontTextTag->getTag(Sign::TAG_TEXT_BLOB); - if(!$textBlobTag instanceof StringTag){ - throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data"); - } - - try{ - $text = SignText::fromBlob($textBlobTag->getValue()); - }catch(\InvalidArgumentException $e){ - throw PacketHandlingException::wrap($e, "Invalid sign text update"); - } - - try{ - if(!$block->updateText($this->player, $text)){ - foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){ - $this->session->sendDataPacket($updatePacket); - } - } - }catch(\UnexpectedValueException $e){ - throw PacketHandlingException::wrap($e); + if(!$this->updateSignText($nbt, Sign::TAG_FRONT_TEXT, true, $block, $pos)){ + //only one side can be updated at a time + $this->updateSignText($nbt, Sign::TAG_BACK_TEXT, false, $block, $pos); } return true; @@ -782,10 +819,6 @@ class InGamePacketHandler extends PacketHandler{ 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/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 6db8f1e12..4eddf3100 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -33,9 +33,12 @@ use pocketmine\inventory\transaction\EnchantingTransaction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\inventory\transaction\TransactionBuilder; use pocketmine\inventory\transaction\TransactionBuilderInventory; +use pocketmine\item\Durable; use pocketmine\item\Item; +use pocketmine\network\mcpe\cache\CraftingDataCache; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds; +use pocketmine\network\mcpe\protocol\types\inventory\FullContainerName; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingCreateSpecificResultStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction; @@ -47,6 +50,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DropStackReque use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestSlotInfo; +use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\MineBlockStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\PlaceStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction; @@ -235,9 +239,10 @@ class ItemStackRequestExecutor{ throw new ItemStackRequestProcessException("Cannot craft a recipe more than 256 times"); } $craftingManager = $this->player->getServer()->getCraftingManager(); - $recipe = $craftingManager->getCraftingRecipeFromIndex($recipeId); + $recipeIndex = $recipeId - CraftingDataCache::RECIPE_ID_OFFSET; + $recipe = $craftingManager->getCraftingRecipeFromIndex($recipeIndex); if($recipe === null){ - throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeId"); + throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeIndex"); } $this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions); @@ -362,6 +367,16 @@ class ItemStackRequestExecutor{ $this->setNextCreatedItem($nextResultItem); }elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){ //no obvious use + }elseif($action instanceof MineBlockStackRequestAction){ + $slot = $action->getHotbarSlot(); + $this->requestSlotInfos[] = new ItemStackRequestSlotInfo(new FullContainerName(ContainerUIIds::HOTBAR), $slot, $action->getStackId()); + $inventory = $this->player->getInventory(); + $usedItem = $inventory->slotExists($slot) ? $inventory->getItem($slot) : null; + $predictedDamage = $action->getPredictedDurability(); + if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){ + $usedItem->setDamage($predictedDamage); + $this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem); + } }else{ throw new ItemStackRequestProcessException("Unhandled item stack request action"); } @@ -370,7 +385,7 @@ class ItemStackRequestExecutor{ /** * @throws ItemStackRequestProcessException */ - public function generateInventoryTransaction() : InventoryTransaction{ + public function generateInventoryTransaction() : ?InventoryTransaction{ foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){ try{ $this->processItemStackRequestAction($action); @@ -380,6 +395,9 @@ class ItemStackRequestExecutor{ } $this->setNextCreatedItem(null); $inventoryActions = $this->builder->generateActions(); + if(count($inventoryActions) === 0){ + return null; + } $transaction = $this->specialTransaction ?? new InventoryTransaction($this->player); foreach($inventoryActions as $action){ @@ -389,12 +407,16 @@ class ItemStackRequestExecutor{ return $transaction; } - public function buildItemStackResponse() : ItemStackResponse{ + public function getItemStackResponseBuilder() : ItemStackResponseBuilder{ $builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager); foreach($this->requestSlotInfos as $requestInfo){ $builder->addSlot($requestInfo->getContainerName()->getContainerId(), $requestInfo->getSlotId()); } - return $builder->build(); + return $builder; + } + + public function buildItemStackResponse() : ItemStackResponse{ + return $this->getItemStackResponseBuilder()->build(); } } diff --git a/src/network/mcpe/handler/ItemStackResponseBuilder.php b/src/network/mcpe/handler/ItemStackResponseBuilder.php index 1369e3ba7..faf479ee2 100644 --- a/src/network/mcpe/handler/ItemStackResponseBuilder.php +++ b/src/network/mcpe/handler/ItemStackResponseBuilder.php @@ -93,6 +93,7 @@ final class ItemStackResponseBuilder{ $item->getCount(), $itemStackInfo->getStackId(), $item->getCustomName(), + $item->getCustomName(), $item instanceof Durable ? $item->getDamage() : 0, ); } diff --git a/src/network/mcpe/handler/LoginPacketHandler.php b/src/network/mcpe/handler/LoginPacketHandler.php index c15753dad..aa7c1da7a 100644 --- a/src/network/mcpe/handler/LoginPacketHandler.php +++ b/src/network/mcpe/handler/LoginPacketHandler.php @@ -27,22 +27,36 @@ use pocketmine\entity\InvalidSkinException; use pocketmine\event\player\PlayerPreLoginEvent; use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\Translatable; -use pocketmine\network\mcpe\auth\ProcessLoginTask; +use pocketmine\network\mcpe\auth\ProcessLegacyLoginTask; +use pocketmine\network\mcpe\auth\ProcessOpenIdLoginTask; use pocketmine\network\mcpe\JwtException; use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\LoginPacket; -use pocketmine\network\mcpe\protocol\types\login\AuthenticationData; -use pocketmine\network\mcpe\protocol\types\login\ClientData; -use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper; -use pocketmine\network\mcpe\protocol\types\login\JwtChain; +use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo; +use pocketmine\network\mcpe\protocol\types\login\AuthenticationType; +use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientData; +use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper; +use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain; +use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData; +use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody; +use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtHeader; use pocketmine\network\PacketHandlingException; use pocketmine\player\Player; use pocketmine\player\PlayerInfo; use pocketmine\player\XboxLivePlayerInfo; use pocketmine\Server; use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; +use function chr; +use function count; +use function gettype; use function is_array; +use function is_object; +use function json_decode; +use function md5; +use function ord; +use const JSON_THROW_ON_ERROR; /** * Handles the initial login phase of the session. This handler is used as the initial state. @@ -59,13 +73,95 @@ class LoginPacketHandler extends PacketHandler{ private \Closure $authCallback ){} - public function handleLogin(LoginPacket $packet) : bool{ - $extraData = $this->fetchAuthData($packet->chainDataJwt); + private static function calculateUuidFromXuid(string $xuid) : UuidInterface{ + $hash = md5("pocket-auth-1-xuid:" . $xuid, binary: true); + $hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); // set version to 3 + $hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); // set variant to RFC 4122 - if(!Player::isValidUserName($extraData->displayName)){ + return Uuid::fromBytes($hash); + } + + public function handleLogin(LoginPacket $packet) : bool{ + $authInfo = $this->parseAuthInfo($packet->authInfoJson); + + if($authInfo->AuthenticationType === AuthenticationType::FULL->value){ + try{ + [$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token); + }catch(JwtException $e){ + throw PacketHandlingException::wrap($e, "Error parsing authentication token"); + } + $header = $this->mapXboxTokenHeader($headerArray); + $claims = $this->mapXboxTokenBody($claimsArray); + + $legacyUuid = self::calculateUuidFromXuid($claims->xid); + $username = $claims->xname; + $xuid = $claims->xid; + + $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid); + if($authRequired === null){ + //plugin cancelled + return true; + } + $this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired); + + }elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){ + try{ + $chainData = json_decode($authInfo->Certificate, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate chain"); + } + if(!is_object($chainData)){ + throw new PacketHandlingException("Unexpected type for self-signed certificate chain: " . gettype($chainData) . ", expected object"); + } + try{ + $chain = $this->defaultJsonMapper()->map($chainData, new LegacyAuthChain()); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate chain"); + } + if(count($chain->chain) > 1 || !isset($chain->chain[0])){ + throw new PacketHandlingException("Expected exactly one certificate in self-signed certificate chain, got " . count($chain->chain)); + } + + try{ + [, $claimsArray, ] = JwtUtils::parse($chain->chain[0]); + }catch(JwtException $e){ + throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate"); + } + if(!isset($claimsArray["extraData"]) || !is_array($claimsArray["extraData"])){ + throw new PacketHandlingException("Expected \"extraData\" to be present in self-signed certificate"); + } + + try{ + $claims = $this->defaultJsonMapper()->map($claimsArray["extraData"], new LegacyAuthIdentityData()); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate extraData"); + } + + if(!Uuid::isValid($claims->identity)){ + throw new PacketHandlingException("Invalid UUID string in self-signed certificate: " . $claims->identity); + } + $legacyUuid = Uuid::fromString($claims->identity); + $username = $claims->displayName; + $xuid = ""; + + $authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid); + if($authRequired === null){ + //plugin cancelled + return true; + } + $this->processSelfSignedLogin($chain->chain, $packet->clientDataJwt, $authRequired); + }else{ + throw new PacketHandlingException("Unsupported authentication type: $authInfo->AuthenticationType"); + } + + return true; + } + + private function processLoginCommon(LoginPacket $packet, string $username, UuidInterface $legacyUuid, string $xuid) : ?bool{ + if(!Player::isValidUserName($username)){ $this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName()); - return true; + return null; } $clientData = $this->parseClientData($packet->clientDataJwt); @@ -78,32 +174,25 @@ class LoginPacketHandler extends PacketHandler{ disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin() ); - return true; + return null; } - if(!Uuid::isValid($extraData->identity)){ - throw new PacketHandlingException("Invalid login UUID"); - } - $uuid = Uuid::fromString($extraData->identity); - $arrClientData = (array) $clientData; - $arrClientData["TitleID"] = $extraData->titleId; - - if($extraData->XUID !== ""){ + if($xuid !== ""){ $playerInfo = new XboxLivePlayerInfo( - $extraData->XUID, - $extraData->displayName, - $uuid, + $xuid, + $username, + $legacyUuid, $skin, $clientData->LanguageCode, - $arrClientData + (array) $clientData ); }else{ $playerInfo = new PlayerInfo( - $extraData->displayName, - $uuid, + $username, + $legacyUuid, $skin, $clientData->LanguageCode, - $arrClientData + (array) $clientData ); } ($this->playerInfoConsumer)($playerInfo); @@ -136,52 +225,60 @@ class LoginPacketHandler extends PacketHandler{ $ev->call(); if(!$ev->isAllowed()){ $this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage()); - return true; + return null; } - $this->processLogin($packet, $ev->isAuthRequired()); - - return true; + return $ev->isAuthRequired(); } /** * @throws PacketHandlingException */ - protected function fetchAuthData(JwtChain $chain) : AuthenticationData{ - /** @var AuthenticationData|null $extraData */ - $extraData = null; - foreach($chain->chain as $jwt){ - //validate every chain element - try{ - [, $claims, ] = JwtUtils::parse($jwt); - }catch(JwtException $e){ - throw PacketHandlingException::wrap($e); - } - if(isset($claims["extraData"])){ - if($extraData !== null){ - throw new PacketHandlingException("Found 'extraData' more than once in chainData"); - } + protected function parseAuthInfo(string $authInfo) : AuthenticationInfo{ + try{ + $authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw PacketHandlingException::wrap($e); + } + if(!is_object($authInfoJson)){ + throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object"); + } - if(!is_array($claims["extraData"])){ - throw new PacketHandlingException("'extraData' key should be an array"); - } - $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; - $mapper->bExceptionOnUndefinedProperty = true; - $mapper->bStrictObjectTypeChecking = true; - try{ - /** @var AuthenticationData $extraData */ - $extraData = $mapper->map($claims["extraData"], new AuthenticationData()); - }catch(\JsonMapper_Exception $e){ - throw PacketHandlingException::wrap($e); - } - } + $mapper = $this->defaultJsonMapper(); + try{ + $clientData = $mapper->map($authInfoJson, new AuthenticationInfo()); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e); } - if($extraData === null){ - throw new PacketHandlingException("'extraData' not found in chain data"); + return $clientData; + } + + /** + * @param array $headerArray + * @throws PacketHandlingException + */ + protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{ + $mapper = $this->defaultJsonMapper(); + try{ + $header = $mapper->map($headerArray, new XboxAuthJwtHeader()); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e); } - return $extraData; + return $header; + } + + /** + * @param array $bodyArray + * @throws PacketHandlingException + */ + protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{ + $mapper = $this->defaultJsonMapper(); + try{ + $header = $mapper->map($bodyArray, new XboxAuthJwtBody()); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e); + } + return $header; } /** @@ -194,11 +291,7 @@ class LoginPacketHandler extends PacketHandler{ throw PacketHandlingException::wrap($e); } - $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; - $mapper->bExceptionOnUndefinedProperty = true; - $mapper->bStrictObjectTypeChecking = true; + $mapper = $this->defaultJsonMapper(); try{ $clientData = $mapper->map($clientDataClaims, new ClientData()); }catch(\JsonMapper_Exception $e){ @@ -213,8 +306,35 @@ class LoginPacketHandler extends PacketHandler{ * * @throws \InvalidArgumentException */ - protected function processLogin(LoginPacket $packet, bool $authRequired) : void{ - $this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback)); + protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{ $this->session->setHandler(null); //drop packets received during login verification + + $authKeyProvider = $this->server->getAuthKeyProvider(); + + $authKeyProvider->getKey($keyId)->onCompletion( + function(array $issuerAndKey) use ($token, $clientData, $authRequired) : void{ + [$issuer, $mojangPublicKeyPem] = $issuerAndKey; + $this->server->getAsyncPool()->submitTask(new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback)); + }, + fn() => ($this->authCallback)(false, $authRequired, "Unrecognized authentication key ID: $keyId", null) + ); + } + + /** + * @param string[] $legacyCertificate + */ + protected function processSelfSignedLogin(array $legacyCertificate, string $clientDataJwt, bool $authRequired) : void{ + $this->session->setHandler(null); //drop packets received during login verification + + $this->server->getAsyncPool()->submitTask(new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: null, authRequired: $authRequired, onCompletion: $this->authCallback)); + } + + private function defaultJsonMapper() : \JsonMapper{ + $mapper = new \JsonMapper(); + $mapper->bExceptionOnMissingData = true; + $mapper->bExceptionOnUndefinedProperty = true; + $mapper->bStrictObjectTypeChecking = true; + $mapper->bEnforceMapType = false; + return $mapper; } } diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php index b80874938..4aa8be227 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -28,6 +28,7 @@ use pocketmine\network\mcpe\cache\CraftingDataCache; use pocketmine\network\mcpe\cache\StaticPacketCache; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\ItemRegistryPacket; use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket; use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket; use pocketmine\network\mcpe\protocol\StartGamePacket; @@ -39,7 +40,6 @@ use pocketmine\network\mcpe\protocol\types\Experiments; use pocketmine\network\mcpe\protocol\types\LevelSettings; use pocketmine\network\mcpe\protocol\types\NetworkPermissions; use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings; -use pocketmine\network\mcpe\protocol\types\ServerAuthMovementMode; use pocketmine\network\mcpe\protocol\types\SpawnSettings; use pocketmine\player\Player; use pocketmine\Server; @@ -81,7 +81,8 @@ class PreSpawnPacketHandler extends PacketHandler{ $levelSettings->lightningLevel = 0; $levelSettings->commandsEnabled = true; $levelSettings->gameRules = [ - "naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration + "naturalregeneration" => new BoolGameRule(false, false), //Hack for client side regeneration + "locatorbar" => new BoolGameRule(false, false) //Disable client-side tracking of nearby players ]; $levelSettings->experiments = new Experiments([], false); @@ -98,7 +99,7 @@ class PreSpawnPacketHandler extends PacketHandler{ $this->server->getMotd(), "", false, - new PlayerMovementSettings(ServerAuthMovementMode::SERVER_AUTHORITATIVE_V2, 0, false), + new PlayerMovementSettings(0, true), 0, 0, "", @@ -107,12 +108,15 @@ class PreSpawnPacketHandler extends PacketHandler{ Uuid::fromString(Uuid::NIL), false, false, + false, new NetworkPermissions(disableClientSounds: true), [], 0, - $typeConverter->getItemTypeDictionary()->getEntries(), )); + $this->session->getLogger()->debug("Sending items"); + $this->session->sendDataPacket(ItemRegistryPacket::create($typeConverter->getItemTypeDictionary()->getEntries())); + $this->session->getLogger()->debug("Sending actor identifiers"); $this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers()); diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php index 5e2671394..d98d8e9ad 100644 --- a/src/network/mcpe/handler/ResourcePacksPacketHandler.php +++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php @@ -36,12 +36,15 @@ use pocketmine\network\mcpe\protocol\types\Experiments; use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackInfoEntry; use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackStackEntry; use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackType; +use pocketmine\network\PacketHandlingException; use pocketmine\resourcepacks\ResourcePack; +use Ramsey\Uuid\Uuid; use function array_keys; use function array_map; use function ceil; use function count; use function implode; +use function sprintf; use function strpos; use function strtolower; use function substr; @@ -65,6 +68,9 @@ class ResourcePacksPacketHandler extends PacketHandler{ */ private array $resourcePacksById = []; + private bool $requestedMetadata = false; + private bool $requestedStack = false; + /** @var bool[][] uuid => [chunk index => hasSent] */ private array $downloadedChunks = []; @@ -103,7 +109,7 @@ class ResourcePacksPacketHandler extends PacketHandler{ //TODO: more stuff return new ResourcePackInfoEntry( - $pack->getPackId(), + Uuid::fromString($pack->getPackId()), $pack->getPackVersion(), $pack->getPackSize(), $this->encryptionKeys[$pack->getPackId()] ?? "", @@ -117,7 +123,10 @@ class ResourcePacksPacketHandler extends PacketHandler{ resourcePackEntries: $resourcePackEntries, mustAccept: $this->mustAccept, hasAddons: false, - hasScripts: false + hasScripts: false, + worldTemplateId: Uuid::fromString(Uuid::NIL), + worldTemplateVersion: "", + forceDisableVibrantVisuals: true, )); $this->session->getLogger()->debug("Waiting for client to accept resource packs"); } @@ -136,6 +145,21 @@ class ResourcePacksPacketHandler extends PacketHandler{ $this->session->disconnect("Refused resource packs", "You must accept resource packs to join this server.", true); break; case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: + if($this->requestedMetadata){ + throw new PacketHandlingException("Cannot request resource pack metadata multiple times"); + } + $this->requestedMetadata = true; + + if($this->requestedStack){ + //client already told us that they have all the packs, they shouldn't be asking for more + throw new PacketHandlingException("Cannot request resource pack metadata after resource pack stack"); + } + + if(count($packet->packIds) > count($this->resourcePacksById)){ + throw new PacketHandlingException(sprintf("Requested metadata for more resource packs (%d) than available on the server (%d)", count($packet->packIds), count($this->resourcePacksById))); + } + + $seen = []; foreach($packet->packIds as $uuid){ //dirty hack for mojang's dirty hack for versions $splitPos = strpos($uuid, "_"); @@ -149,6 +173,9 @@ class ResourcePacksPacketHandler extends PacketHandler{ $this->disconnectWithError("Unknown pack $uuid requested, available packs: " . implode(", ", array_keys($this->resourcePacksById))); return false; } + if(isset($seen[$pack->getPackId()])){ + throw new PacketHandlingException("Repeated metadata request for pack $uuid"); + } $this->session->sendDataPacket(ResourcePackDataInfoPacket::create( $pack->getPackId(), @@ -159,11 +186,16 @@ class ResourcePacksPacketHandler extends PacketHandler{ false, ResourcePackType::RESOURCES //TODO: this might be an addon (not behaviour pack), needed to properly support client-side custom items )); + $seen[$pack->getPackId()] = true; } $this->session->getLogger()->debug("Player requested download of " . count($packet->packIds) . " resource packs"); - break; case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: + if($this->requestedStack){ + throw new PacketHandlingException("Cannot request resource pack stack multiple times"); + } + $this->requestedStack = true; + $stack = array_map(static function(ResourcePack $pack) : ResourcePackStackEntry{ return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), ""); //TODO: subpacks }, $this->resourcePackStack); diff --git a/src/network/mcpe/raklib/RakLibServer.php b/src/network/mcpe/raklib/RakLibServer.php index 5137b94ba..f66734ee5 100644 --- a/src/network/mcpe/raklib/RakLibServer.php +++ b/src/network/mcpe/raklib/RakLibServer.php @@ -38,7 +38,7 @@ use raklib\server\ServerSocket; use raklib\server\SimpleProtocolAcceptor; use raklib\utils\ExceptionTraceCleaner; use raklib\utils\InternetAddress; -use function gc_enable; +use function gc_disable; use function ini_set; class RakLibServer extends Thread{ @@ -68,21 +68,24 @@ class RakLibServer extends Thread{ public function startAndWait(int $options = NativeThread::INHERIT_NONE) : void{ $this->start($options); $this->synchronized(function() : void{ - while(!$this->ready && $this->getCrashInfo() === null){ + while(!$this->ready && !$this->isTerminated()){ $this->wait(); } - $crashInfo = $this->getCrashInfo(); - if($crashInfo !== null){ - if($crashInfo->getType() === SocketException::class){ - throw new SocketException($crashInfo->getMessage()); - } - throw new ThreadCrashException("RakLib failed to start", $crashInfo); - } }); + $crashInfo = $this->getCrashInfo(); + if($crashInfo !== null){ + if($crashInfo->getType() === SocketException::class){ + throw new SocketException($crashInfo->getMessage()); + } + throw new ThreadCrashException("RakLib failed to start", $crashInfo); + } } protected function onRun() : void{ - gc_enable(); + //RakLib has cycles (e.g. ServerSession <-> Server) but these cycles are explicitly cleaned up anyway, and are + //very few, so it's pointless to waste CPU time on GC + gc_disable(); + ini_set("display_errors", '1'); ini_set("display_startup_errors", '1'); \GlobalLogger::set($this->logger); diff --git a/src/network/mcpe/serializer/ChunkSerializer.php b/src/network/mcpe/serializer/ChunkSerializer.php index 9120f34a7..230ff4a94 100644 --- a/src/network/mcpe/serializer/ChunkSerializer.php +++ b/src/network/mcpe/serializer/ChunkSerializer.php @@ -23,16 +23,16 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\serializer; +use pmmp\encoding\Byte; +use pmmp\encoding\ByteBufferWriter; +use pmmp\encoding\VarInt; use pocketmine\block\tile\Spawnable; use pocketmine\data\bedrock\BiomeIds; use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap; use pocketmine\nbt\TreeRoot; use pocketmine\network\mcpe\convert\BlockTranslator; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; -use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\types\DimensionIds; -use pocketmine\utils\Binary; -use pocketmine\utils\BinaryStream; use pocketmine\world\format\Chunk; use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\SubChunk; @@ -84,7 +84,7 @@ final class ChunkSerializer{ * @phpstan-param DimensionIds::* $dimensionId */ public static function serializeFullChunk(Chunk $chunk, int $dimensionId, BlockTranslator $blockTranslator, ?string $tiles = null) : string{ - $stream = PacketSerializer::encoder(); + $stream = new ByteBufferWriter(); $subChunkCount = self::getSubChunkCount($chunk, $dimensionId); $writtenCount = 0; @@ -100,37 +100,34 @@ final class ChunkSerializer{ self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream); } - $stream->putByte(0); //border block array count + Byte::writeUnsigned($stream, 0); //border block array count //Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client. if($tiles !== null){ - $stream->put($tiles); + $stream->writeByteArray($tiles); }else{ - $stream->put(self::serializeTiles($chunk)); + $stream->writeByteArray(self::serializeTiles($chunk)); } - return $stream->getBuffer(); + return $stream->getData(); } - public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{ + public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, ByteBufferWriter $stream, bool $persistentBlockStates) : void{ $layers = $subChunk->getBlockLayers(); - $stream->putByte(8); //version + Byte::writeUnsigned($stream, 8); //version - $stream->putByte(count($layers)); + Byte::writeUnsigned($stream, count($layers)); $blockStateDictionary = $blockTranslator->getBlockStateDictionary(); foreach($layers as $blocks){ $bitsPerBlock = $blocks->getBitsPerBlock(); $words = $blocks->getWordArray(); - $stream->putByte(($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1)); - $stream->put($words); + Byte::writeUnsigned($stream, ($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1)); + $stream->writeByteArray($words); $palette = $blocks->getPalette(); if($bitsPerBlock !== 0){ - //these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here - //but since we know they are always unsigned, we can avoid the extra fcall overhead of - //zigzag and just shift directly. - $stream->putUnsignedVarInt(count($palette) << 1); //yes, this is intentionally zigzag + VarInt::writeSignedInt($stream, count($palette)); //yes, this is intentionally zigzag } if($persistentBlockStates){ $nbtSerializer = new NetworkNbtSerializer(); @@ -141,46 +138,43 @@ final class ChunkSerializer{ $state = $blockTranslator->getFallbackStateData(); } - $stream->put($nbtSerializer->write(new TreeRoot($state->toNbt()))); + $stream->writeByteArray($nbtSerializer->write(new TreeRoot($state->toNbt()))); } }else{ + //we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of + //allocating a temporary array for the mapped palette IDs, especially for small palettes foreach($palette as $p){ - $stream->put(Binary::writeUnsignedVarInt($blockTranslator->internalIdToNetworkId($p) << 1)); + VarInt::writeSignedInt($stream, $blockTranslator->internalIdToNetworkId($p)); } } } } - private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $stream) : void{ + private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, ByteBufferWriter $stream) : void{ $biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock(); - $stream->putByte(($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs - $stream->put($biomePalette->getWordArray()); + Byte::writeUnsigned($stream, ($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs + $stream->writeByteArray($biomePalette->getWordArray()); - //these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here - //but since we know they are always unsigned, we can avoid the extra fcall overhead of - //zigzag and just shift directly. $biomePaletteArray = $biomePalette->getPalette(); if($biomePaletteBitsPerBlock !== 0){ - $stream->putUnsignedVarInt(count($biomePaletteArray) << 1); + VarInt::writeSignedInt($stream, count($biomePaletteArray)); } foreach($biomePaletteArray as $p){ - if($biomeIdMap->legacyToString($p) === null){ - //make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this - $p = BiomeIds::OCEAN; - } - $stream->put(Binary::writeUnsignedVarInt($p << 1)); + //we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of + //allocating a temporary array for the mapped palette IDs, especially for small palettes + VarInt::writeSignedInt($stream, $biomeIdMap->legacyToString($p) !== null ? $p : BiomeIds::OCEAN); } } public static function serializeTiles(Chunk $chunk) : string{ - $stream = new BinaryStream(); + $stream = new ByteBufferWriter(); foreach($chunk->getTiles() as $tile){ if($tile instanceof Spawnable){ - $stream->put($tile->getSerializedSpawnCompound()->getEncodedNbt()); + $stream->writeByteArray($tile->getSerializedSpawnCompound()->getEncodedNbt()); } } - return $stream->getBuffer(); + return $stream->getData(); } } diff --git a/src/network/query/QueryHandler.php b/src/network/query/QueryHandler.php index 940f64f18..41b633421 100644 --- a/src/network/query/QueryHandler.php +++ b/src/network/query/QueryHandler.php @@ -27,16 +27,16 @@ declare(strict_types=1); */ namespace pocketmine\network\query; +use pmmp\encoding\BE; +use pmmp\encoding\Byte; +use pmmp\encoding\ByteBufferReader; +use pmmp\encoding\ByteBufferWriter; +use pmmp\encoding\DataDecodeException; use pocketmine\network\AdvancedNetworkInterface; use pocketmine\network\RawPacketHandler; use pocketmine\Server; -use pocketmine\utils\Binary; -use pocketmine\utils\BinaryDataException; -use pocketmine\utils\BinaryStream; -use function chr; use function hash; use function random_bytes; -use function strlen; use function substr; class QueryHandler implements RawPacketHandler{ @@ -80,51 +80,53 @@ class QueryHandler implements RawPacketHandler{ } public static function getTokenString(string $token, string $salt) : int{ - return Binary::readInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4)); + return BE::unpackSignedInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4)); } public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{ try{ - $stream = new BinaryStream($packet); - $header = $stream->get(2); + $stream = new ByteBufferReader($packet); + $header = $stream->readByteArray(2); if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above return false; } - $packetType = $stream->getByte(); - $sessionID = $stream->getInt(); + $packetType = Byte::readUnsigned($stream); + $sessionID = BE::readUnsignedInt($stream); switch($packetType){ case self::HANDSHAKE: //Handshake - $reply = chr(self::HANDSHAKE); - $reply .= Binary::writeInt($sessionID); - $reply .= self::getTokenString($this->token, $address) . "\x00"; + $writer = new ByteBufferWriter(); + Byte::writeUnsigned($writer, self::HANDSHAKE); + BE::writeUnsignedInt($writer, $sessionID); + $writer->writeByteArray(self::getTokenString($this->token, $address) . "\x00"); - $interface->sendRawPacket($address, $port, $reply); + $interface->sendRawPacket($address, $port, $writer->getData()); return true; case self::STATISTICS: //Stat - $token = $stream->getInt(); + $token = BE::readUnsignedInt($stream); if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){ $this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2"); return true; } - $reply = chr(self::STATISTICS); - $reply .= Binary::writeInt($sessionID); + $writer = new ByteBufferWriter(); + Byte::writeUnsigned($writer, self::STATISTICS); + BE::writeUnsignedInt($writer, $sessionID); - $remaining = $stream->getRemaining(); - if(strlen($remaining) === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01 - $reply .= $this->server->getQueryInformation()->getLongQuery(); + $remaining = $stream->getUnreadLength(); + if($remaining === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01 + $writer->writeByteArray($this->server->getQueryInformation()->getLongQuery()); }else{ - $reply .= $this->server->getQueryInformation()->getShortQuery(); + $writer->writeByteArray($this->server->getQueryInformation()->getShortQuery()); } - $interface->sendRawPacket($address, $port, $reply); + $interface->sendRawPacket($address, $port, $writer->getData()); return true; default: return false; } - }catch(BinaryDataException $e){ + }catch(DataDecodeException $e){ $this->logger->debug("Bad packet from $address $port: " . $e->getMessage()); return false; } diff --git a/src/network/query/QueryInfo.php b/src/network/query/QueryInfo.php index 0bf5b4f65..30aba5347 100644 --- a/src/network/query/QueryInfo.php +++ b/src/network/query/QueryInfo.php @@ -23,11 +23,11 @@ declare(strict_types=1); namespace pocketmine\network\query; +use pmmp\encoding\LE; use pocketmine\player\GameMode; use pocketmine\player\Player; use pocketmine\plugin\Plugin; use pocketmine\Server; -use pocketmine\utils\Binary; use pocketmine\utils\Utils; use pocketmine\YmlServerProperties; use function array_map; @@ -236,6 +236,6 @@ final class QueryInfo{ } public function getShortQuery() : string{ - return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . Binary::writeLShort($this->port) . $this->ip . "\x00"); + return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . LE::packUnsignedShort($this->port) . $this->ip . "\x00"); } } diff --git a/src/network/upnp/UPnP.php b/src/network/upnp/UPnP.php index 2d48a2db8..bd8e8376f 100644 --- a/src/network/upnp/UPnP.php +++ b/src/network/upnp/UPnP.php @@ -215,6 +215,7 @@ class UPnP{ 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"' ]; + $err = ""; if(Internet::postURL($serviceURL, $contents, 3, $headers, $err) === null){ throw new UPnPException("Failed to portforward using UPnP: " . $err); } diff --git a/src/permission/BanEntry.php b/src/permission/BanEntry.php index 0d5ed0c76..a766a6fcc 100644 --- a/src/permission/BanEntry.php +++ b/src/permission/BanEntry.php @@ -101,11 +101,12 @@ class BanEntry{ } public function getString() : string{ + $expires = $this->getExpires(); return implode("|", [ $this->getName(), $this->getCreated()->format(self::$format), $this->getSource(), - $this->getExpires() === null ? "Forever" : $this->getExpires()->format(self::$format), + $expires === null ? "Forever" : $expires->format(self::$format), $this->getReason() ]); } @@ -147,7 +148,9 @@ class BanEntry{ return null; } - $parts = explode("|", trim($str)); + //we expect at most 5 parts, but accept 6 in case of an extra unexpected delimiter + //we don't want to include unexpected data into the ban reason + $parts = explode("|", trim($str), limit: 6); $entry = new BanEntry(trim(array_shift($parts))); if(count($parts) > 0){ $entry->setCreated(self::parseDate(array_shift($parts))); diff --git a/src/player/Player.php b/src/player/Player.php index 45d6b2e17..7f9c48377 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -26,6 +26,7 @@ namespace pocketmine\player; use pocketmine\block\BaseSign; use pocketmine\block\Bed; use pocketmine\block\BlockTypeTags; +use pocketmine\block\RespawnAnchor; use pocketmine\block\UnknownBlock; use pocketmine\block\VanillaBlocks; use pocketmine\command\CommandSender; @@ -40,11 +41,13 @@ use pocketmine\entity\Entity; use pocketmine\entity\Human; use pocketmine\entity\Living; use pocketmine\entity\Location; +use pocketmine\entity\NeverSavedWithChunkEntity; use pocketmine\entity\object\ItemEntity; use pocketmine\entity\projectile\Arrow; use pocketmine\entity\Skin; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\event\entity\EntityExtinguishEvent; use pocketmine\event\inventory\InventoryCloseEvent; use pocketmine\event\inventory\InventoryOpenEvent; use pocketmine\event\player\PlayerBedEnterEvent; @@ -135,6 +138,7 @@ use pocketmine\world\sound\EntityAttackNoDamageSound; use pocketmine\world\sound\EntityAttackSound; use pocketmine\world\sound\FireExtinguishSound; use pocketmine\world\sound\ItemBreakSound; +use pocketmine\world\sound\RespawnAnchorDepleteSound; use pocketmine\world\sound\Sound; use pocketmine\world\World; use pocketmine\YmlServerProperties; @@ -147,7 +151,6 @@ use function count; use function explode; use function floor; use function get_class; -use function is_int; use function max; use function mb_strlen; use function microtime; @@ -167,7 +170,7 @@ use const PHP_INT_MAX; /** * Main class that handles networking, recovery, and packet sending to the server part */ -class Player extends Human implements CommandSender, ChunkListener, IPlayer{ +class Player extends Human implements CommandSender, ChunkListener, IPlayer, NeverSavedWithChunkEntity{ use PermissibleDelegateTrait; private const MOVES_PER_TICK = 2; @@ -185,6 +188,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ private const MAX_REACH_DISTANCE_SURVIVAL = 7; private const MAX_REACH_DISTANCE_ENTITY_INTERACTION = 8; + public const DEFAULT_FLIGHT_SPEED_MULTIPLIER = 0.05; + public const TAG_FIRST_PLAYED = "firstPlayed"; //TAG_Long public const TAG_LAST_PLAYED = "lastPlayed"; //TAG_Long private const TAG_GAME_MODE = "playerGameType"; //TAG_Int @@ -284,6 +289,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ protected bool $autoJump = true; protected bool $flying = false; + protected float $flightSpeedMultiplier = self::DEFAULT_FLIGHT_SPEED_MULTIPLIER; + /** @phpstan-var positive-int|null */ protected ?int $lineHeight = null; protected string $locale = "en_US"; @@ -519,6 +526,41 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ return $this->flying; } + /** + * Sets the player's flight speed multiplier. + * + * Normal flying speed in blocks-per-tick is (multiplier * 10) blocks per tick. + * When sprint-flying, this is doubled to 20. + * + * If set to zero, the player will not be able to move in the xz plane when flying. + * Negative values will invert the controls. + * + * Note: Movement speed attribute does not influence flight speed. + * + * @see Player::DEFAULT_FLIGHT_SPEED_MULTIPLIER + */ + public function setFlightSpeedMultiplier(float $flightSpeedMultiplier) : void{ + if($this->flightSpeedMultiplier !== $flightSpeedMultiplier){ + $this->flightSpeedMultiplier = $flightSpeedMultiplier; + $this->getNetworkSession()->syncAbilities($this); + } + } + + /** + * Returns the player's flight speed multiplier. + * + * Normal flying speed in blocks-per-tick is (multiplier * 10) blocks per tick. + * When sprint-flying, this is doubled to 20. + * + * If set to zero, the player will not be able to move in the xz plane when flying. + * Negative values will invert the controls. + * + * @see Player::DEFAULT_FLIGHT_SPEED_MULTIPLIER + */ + public function getFlightSpeedMultiplier() : float{ + return $this->flightSpeedMultiplier; + } + public function setAutoJump(bool $value) : void{ if($this->autoJump !== $value){ $this->autoJump = $value; @@ -826,7 +868,6 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $X = null; $Z = null; World::getXZ($index, $X, $Z); - assert(is_int($X) && is_int($Z)); ++$count; @@ -1350,7 +1391,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->nextChunkOrderRun = 0; } - if(!$revert && $distanceSquared != 0){ + if(!$revert && $distanceSquared !== 0.0){ $dx = $newPos->x - $oldPos->x; $dy = $newPos->y - $oldPos->y; $dz = $newPos->z - $oldPos->z; @@ -1611,7 +1652,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $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); @@ -2337,7 +2381,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $ev = new PlayerQuitEvent($this, $quitMessage ?? $this->getLeaveMessage(), $reason); $ev->call(); - if(($quitMessage = $ev->getQuitMessage()) != ""){ + if(($quitMessage = $ev->getQuitMessage()) !== ""){ $this->server->broadcastMessage($quitMessage); } $this->save(); @@ -2478,7 +2522,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->xpManager->setXpAndProgress(0, 0.0); } - if($ev->getDeathMessage() != ""){ + if($ev->getDeathMessage() !== ""){ $this->server->broadcastMessage($ev->getDeathMessage()); } @@ -2518,6 +2562,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ } $this->logger->debug("Respawn position located, completing respawn"); $ev = new PlayerRespawnEvent($this, $safeSpawn); + $spawnPosition = $ev->getRespawnPosition(); + $spawnBlock = $spawnPosition->getWorld()->getBlock($spawnPosition); + if($spawnBlock instanceof RespawnAnchor){ + if($spawnBlock->getCharges() > 0){ + $spawnPosition->getWorld()->setBlock($spawnPosition, $spawnBlock->setCharges($spawnBlock->getCharges() - 1)); + $spawnPosition->getWorld()->addSound($spawnPosition, new RespawnAnchorDepleteSound()); + }else{ + $defaultSpawn = $this->server->getWorldManager()->getDefaultWorld()?->getSpawnLocation(); + if($defaultSpawn !== null){ + $this->setSpawn($defaultSpawn); + $ev->setRespawnPosition($defaultSpawn); + $this->sendMessage(KnownTranslationFactory::tile_respawn_anchor_notValid()->prefix(TextFormat::GRAY)); + } + } + } $ev->call(); $realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld()); @@ -2527,7 +2586,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->setSneaking(false); $this->setFlying(false); - $this->extinguish(); + $this->extinguish(EntityExtinguishEvent::CAUSE_RESPAWN); $this->setAirSupplyTicks($this->getMaxAirSupplyTicks()); $this->deadTicks = 0; $this->noDamageTicks = 60; @@ -2798,13 +2857,12 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ /** * Opens the player's sign editor GUI for the sign at the given position. - * TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations) */ - public function openSignEditor(Vector3 $position) : void{ + public function openSignEditor(Vector3 $position, bool $frontFace = true) : void{ $block = $this->getWorld()->getBlock($position); if($block instanceof BaseSign){ $this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId())); - $this->getNetworkSession()->onOpenSignEditor($position, true); + $this->getNetworkSession()->onOpenSignEditor($position, $frontFace); }else{ throw new \InvalidArgumentException("Block at this position is not a sign"); } diff --git a/src/player/SurvivalBlockBreakHandler.php b/src/player/SurvivalBlockBreakHandler.php index e31e77ef7..57c2842ce 100644 --- a/src/player/SurvivalBlockBreakHandler.php +++ b/src/player/SurvivalBlockBreakHandler.php @@ -25,6 +25,8 @@ namespace pocketmine\player; use pocketmine\block\Block; use pocketmine\entity\animation\ArmSwingAnimation; +use pocketmine\entity\effect\VanillaEffects; +use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\network\mcpe\protocol\LevelEventPacket; @@ -65,11 +67,29 @@ final class SurvivalBlockBreakHandler{ if(!$this->block->getBreakInfo()->isBreakable()){ return 0.0; } - //TODO: improve this to take stuff like swimming, ladders, enchanted tools into account, fix wrong tool break time calculations for bad tools (pmmp/PocketMine-MP#211) $breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20; - + if(!$this->player->isOnGround() && !$this->player->isFlying()){ + $breakTimePerTick *= 5; + } + if($this->player->isUnderwater() && !$this->player->getArmorInventory()->getHelmet()->hasEnchantment(VanillaEnchantments::AQUA_AFFINITY())){ + $breakTimePerTick *= 5; + } if($breakTimePerTick > 0){ - return 1 / $breakTimePerTick; + $progressPerTick = 1 / $breakTimePerTick; + + $haste = $this->player->getEffects()->get(VanillaEffects::HASTE()); + if($haste !== null){ + $hasteLevel = $haste->getEffectLevel(); + $progressPerTick *= (1 + 0.2 * $hasteLevel) * (1.2 ** $hasteLevel); + } + + $miningFatigue = $this->player->getEffects()->get(VanillaEffects::MINING_FATIGUE()); + if($miningFatigue !== null){ + $miningFatigueLevel = $miningFatigue->getEffectLevel(); + $progressPerTick *= 0.21 ** $miningFatigueLevel; + } + + return $progressPerTick; } return 1; } @@ -82,7 +102,10 @@ final class SurvivalBlockBreakHandler{ $newBreakSpeed = $this->calculateBreakProgressPerTick(); if(abs($newBreakSpeed - $this->breakSpeed) > 0.0001){ $this->breakSpeed = $newBreakSpeed; - //TODO: sync with client + $this->player->getWorld()->broadcastPacketToViewers( + $this->blockPos, + LevelEventPacket::create(LevelEvent::BLOCK_BREAK_SPEED, (int) (65535 * $this->breakSpeed), $this->blockPos) + ); } $this->breakProgress += $this->breakSpeed; diff --git a/src/plugin/DiskResourceProvider.php b/src/plugin/DiskResourceProvider.php index efdc9cd17..9649f565f 100644 --- a/src/plugin/DiskResourceProvider.php +++ b/src/plugin/DiskResourceProvider.php @@ -36,6 +36,8 @@ use const DIRECTORY_SEPARATOR; /** * Provides resources from the given plugin directory on disk. The path may be prefixed with a specific access protocol * to enable special types of access. + * + * @deprecated */ class DiskResourceProvider implements ResourceProvider{ private string $file; diff --git a/src/plugin/PluginDescription.php b/src/plugin/PluginDescription.php index 35ae2ba32..89ac19e05 100644 --- a/src/plugin/PluginDescription.php +++ b/src/plugin/PluginDescription.php @@ -84,11 +84,20 @@ class PluginDescription{ * @phpstan-var array> */ private array $extensions = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $depend = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $softDepend = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $loadBefore = []; private string $version; /** @@ -173,7 +182,7 @@ class PluginDescription{ } if(isset($plugin[self::KEY_DEPEND])){ - $this->depend = (array) $plugin[self::KEY_DEPEND]; + $this->depend = array_values((array) $plugin[self::KEY_DEPEND]); } if(isset($plugin[self::KEY_EXTENSIONS])){ $extensions = (array) $plugin[self::KEY_EXTENSIONS]; @@ -183,13 +192,13 @@ class PluginDescription{ $k = $v; $v = "*"; } - $this->extensions[(string) $k] = array_map('strval', is_array($v) ? $v : [$v]); + $this->extensions[(string) $k] = array_values(array_map('strval', is_array($v) ? $v : [$v])); } } - $this->softDepend = (array) ($plugin[self::KEY_SOFTDEPEND] ?? $this->softDepend); + $this->softDepend = array_values((array) ($plugin[self::KEY_SOFTDEPEND] ?? $this->softDepend)); - $this->loadBefore = (array) ($plugin[self::KEY_LOADBEFORE] ?? $this->loadBefore); + $this->loadBefore = array_values((array) ($plugin[self::KEY_LOADBEFORE] ?? $this->loadBefore)); $this->website = (string) ($plugin[self::KEY_WEBSITE] ?? $this->website); @@ -210,7 +219,7 @@ class PluginDescription{ $this->authors = []; if(isset($plugin[self::KEY_AUTHOR])){ if(is_array($plugin[self::KEY_AUTHOR])){ - $this->authors = $plugin[self::KEY_AUTHOR]; + $this->authors = array_values($plugin[self::KEY_AUTHOR]); }else{ $this->authors[] = $plugin[self::KEY_AUTHOR]; } @@ -284,6 +293,7 @@ class PluginDescription{ /** * @return string[] + * @phpstan-return list */ public function getDepend() : array{ return $this->depend; @@ -295,6 +305,7 @@ class PluginDescription{ /** * @return string[] + * @phpstan-return list */ public function getLoadBefore() : array{ return $this->loadBefore; @@ -324,6 +335,7 @@ class PluginDescription{ /** * @return string[] + * @phpstan-return list */ public function getSoftDepend() : array{ return $this->softDepend; diff --git a/src/plugin/PluginGraylist.php b/src/plugin/PluginGraylist.php index ff9d71832..f3c9cf2a3 100644 --- a/src/plugin/PluginGraylist.php +++ b/src/plugin/PluginGraylist.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace pocketmine\plugin; use pocketmine\utils\Utils; -use function array_flip; +use function array_fill_keys; +use function array_keys; use function is_array; use function is_float; use function is_int; @@ -32,23 +33,28 @@ use function is_string; class PluginGraylist{ - /** @var string[] */ + /** + * @var true[] + * @phpstan-var array + */ private array $plugins; private bool $isWhitelist = false; /** * @param string[] $plugins + * @phpstan-param list $plugins */ public function __construct(array $plugins = [], bool $whitelist = false){ - $this->plugins = array_flip($plugins); + $this->plugins = array_fill_keys($plugins, true); $this->isWhitelist = $whitelist; } /** * @return string[] + * @phpstan-return list */ public function getPlugins() : array{ - return array_flip($this->plugins); + return array_keys($this->plugins); } public function isWhitelist() : bool{ diff --git a/src/plugin/PluginLoadTriage.php b/src/plugin/PluginLoadTriage.php index 77d102668..fcf32751e 100644 --- a/src/plugin/PluginLoadTriage.php +++ b/src/plugin/PluginLoadTriage.php @@ -31,12 +31,12 @@ final class PluginLoadTriage{ public array $plugins = []; /** * @var string[][] - * @phpstan-var array> + * @phpstan-var array> */ public array $dependencies = []; /** * @var string[][] - * @phpstan-var array> + * @phpstan-var array> */ public array $softDependencies = []; } diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index f84698aa0..a8440f04f 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -327,12 +327,12 @@ class PluginManager{ * @param string[][] $dependencyLists * @param Plugin[] $loadedPlugins * - * @phpstan-param array> $dependencyLists - * @phpstan-param-out array> $dependencyLists + * @phpstan-param array> $dependencyLists + * @phpstan-param-out array> $dependencyLists */ private function checkDepsForTriage(string $pluginName, string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{ if(isset($dependencyLists[$pluginName])){ - foreach($dependencyLists[$pluginName] as $key => $dependency){ + foreach(Utils::promoteKeys($dependencyLists[$pluginName]) as $key => $dependency){ if(isset($loadedPlugins[$dependency]) || $this->getPlugin($dependency) instanceof Plugin){ $this->server->getLogger()->debug("Successfully resolved $dependencyType dependency \"$dependency\" for plugin \"$pluginName\""); unset($dependencyLists[$pluginName][$key]); @@ -399,7 +399,7 @@ class PluginManager{ //check for skippable soft dependencies first, in case the dependents could resolve hard dependencies foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){ if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){ - foreach($triage->softDependencies[$name] as $k => $dependency){ + foreach(Utils::promoteKeys($triage->softDependencies[$name]) as $k => $dependency){ if($this->getPlugin($dependency) === null && !array_key_exists($dependency, $triage->plugins)){ $this->server->getLogger()->debug("Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\""); unset($triage->softDependencies[$name][$k]); @@ -416,7 +416,7 @@ class PluginManager{ if(isset($triage->dependencies[$name])){ $unknownDependencies = []; - foreach($triage->dependencies[$name] as $k => $dependency){ + foreach($triage->dependencies[$name] as $dependency){ if($this->getPlugin($dependency) === null && !array_key_exists($dependency, $triage->plugins)){ //assume that the plugin is never going to be loaded //by this point all soft dependencies have been ignored if they were able to be, so diff --git a/src/plugin/ResourceProvider.php b/src/plugin/ResourceProvider.php index 3594d7eee..5ff8db882 100644 --- a/src/plugin/ResourceProvider.php +++ b/src/plugin/ResourceProvider.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace pocketmine\plugin; +/** + * @deprecated + */ interface ResourceProvider{ /** * Gets an embedded resource on the plugin file. diff --git a/src/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php index baf16ca20..ad4417769 100644 --- a/src/resourcepacks/ResourcePackManager.php +++ b/src/resourcepacks/ResourcePackManager.php @@ -26,6 +26,7 @@ namespace pocketmine\resourcepacks; use pocketmine\utils\Config; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; +use Ramsey\Uuid\Uuid; use Symfony\Component\Filesystem\Path; use function array_keys; use function copy; @@ -47,10 +48,16 @@ class ResourcePackManager{ private string $path; private bool $serverForceResources = false; - /** @var ResourcePack[] */ + /** + * @var ResourcePack[] + * @phpstan-var list + */ private array $resourcePacks = []; - /** @var ResourcePack[] */ + /** + * @var ResourcePack[] + * @phpstan-var array + */ private array $uuidList = []; /** @@ -97,9 +104,14 @@ class ResourcePackManager{ try{ $newPack = $this->loadPackFromPath(Path::join($this->path, $pack)); - $this->resourcePacks[] = $newPack; $index = strtolower($newPack->getPackId()); + if(!Uuid::isValid($index)){ + //TODO: we should use Uuid in ResourcePack interface directly but that would break BC + //for now we need to validate this here to make sure it doesn't cause crashes later on + throw new ResourcePackException("Invalid UUID ($index)"); + } $this->uuidList[$index] = $newPack; + $this->resourcePacks[] = $newPack; $keyPath = Path::join($this->path, $pack . ".key"); if(file_exists($keyPath)){ @@ -165,6 +177,7 @@ class ResourcePackManager{ /** * Returns an array of resource packs in use, sorted in order of priority. * @return ResourcePack[] + * @phpstan-return list */ public function getResourceStack() : array{ return $this->resourcePacks; @@ -183,6 +196,11 @@ class ResourcePackManager{ $resourcePacks = []; foreach($resourceStack as $pack){ $uuid = strtolower($pack->getPackId()); + if(!Uuid::isValid($uuid)){ + //TODO: we should use Uuid in ResourcePack interface directly but that would break BC + //for now we need to validate this here to make sure it doesn't cause crashes later on + throw new \InvalidArgumentException("Invalid resource pack UUID ($uuid)"); + } if(isset($uuidList[$uuid])){ throw new \InvalidArgumentException("Cannot load two resource pack with the same UUID ($uuid)"); } diff --git a/src/resourcepacks/ZippedResourcePack.php b/src/resourcepacks/ZippedResourcePack.php index c4daeedf7..4fcf204d9 100644 --- a/src/resourcepacks/ZippedResourcePack.php +++ b/src/resourcepacks/ZippedResourcePack.php @@ -100,7 +100,7 @@ class ZippedResourcePack implements ResourcePack{ try{ $manifest = (new CommentedJsonDecoder())->decode($manifestData); }catch(\RuntimeException $e){ - throw new ResourcePackException("Failed to parse manifest.json: " . $e->getMessage(), $e->getCode(), $e); + throw new ResourcePackException("Failed to parse manifest.json: " . $e->getMessage(), 0, $e); } if(!($manifest instanceof \stdClass)){ throw new ResourcePackException("manifest.json should contain a JSON object, not " . gettype($manifest)); diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php index 33a0949d4..a9b466f40 100644 --- a/src/scheduler/AsyncTask.php +++ b/src/scheduler/AsyncTask.php @@ -68,7 +68,10 @@ abstract class AsyncTask extends Runnable{ */ private static array $threadLocalStorage = []; - /** @phpstan-var ThreadSafeArray|null */ + /** + * @phpstan-var ThreadSafeArray|null + * @deprecated + */ private ?ThreadSafeArray $progressUpdates = null; private ThreadSafe|string|int|bool|null|float $result = null; @@ -90,6 +93,7 @@ abstract class AsyncTask extends Runnable{ $this->finished = true; AsyncWorker::getNotifier()->wakeupSleeper(); + AsyncWorker::maybeCollectCycles(); } /** @@ -161,6 +165,8 @@ abstract class AsyncTask extends Runnable{ } /** + * @deprecated + * * Call this method from {@link AsyncTask::onRun} (AsyncTask execution thread) to schedule a call to * {@link AsyncTask::onProgressUpdate} from the main thread with the given progress parameter. * @@ -175,6 +181,7 @@ abstract class AsyncTask extends Runnable{ } /** + * @deprecated * @internal Only call from AsyncPool.php on the main thread */ public function checkProgressUpdates() : void{ @@ -187,6 +194,8 @@ abstract class AsyncTask extends Runnable{ } /** + * @deprecated + * * Called from the main thread after {@link AsyncTask::publishProgress} is called. * All {@link AsyncTask::publishProgress} calls should result in {@link AsyncTask::onProgressUpdate} calls before * {@link AsyncTask::onCompletion} is called. diff --git a/src/scheduler/AsyncWorker.php b/src/scheduler/AsyncWorker.php index 5fdfb1ebb..528d632d1 100644 --- a/src/scheduler/AsyncWorker.php +++ b/src/scheduler/AsyncWorker.php @@ -24,12 +24,13 @@ declare(strict_types=1); namespace pocketmine\scheduler; use pmmp\thread\Thread as NativeThread; +use pocketmine\GarbageCollectorManager; use pocketmine\snooze\SleeperHandlerEntry; use pocketmine\snooze\SleeperNotifier; use pocketmine\thread\log\ThreadSafeLogger; use pocketmine\thread\Worker; +use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; -use function gc_enable; use function ini_set; class AsyncWorker extends Worker{ @@ -37,6 +38,7 @@ class AsyncWorker extends Worker{ private static array $store = []; private static ?SleeperNotifier $notifier = null; + private static ?GarbageCollectorManager $cycleGcManager = null; public function __construct( private ThreadSafeLogger $logger, @@ -52,11 +54,16 @@ class AsyncWorker extends Worker{ throw new AssumptionFailedError("SleeperNotifier not found in thread-local storage"); } + public static function maybeCollectCycles() : void{ + if(self::$cycleGcManager === null){ + throw new AssumptionFailedError("GarbageCollectorManager not found in thread-local storage"); + } + self::$cycleGcManager->maybeCollectCycles(); + } + protected function onRun() : void{ \GlobalLogger::set($this->logger); - gc_enable(); - if($this->memoryLimit > 0){ ini_set('memory_limit', $this->memoryLimit . 'M'); $this->logger->debug("Set memory limit to " . $this->memoryLimit . " MB"); @@ -66,6 +73,8 @@ class AsyncWorker extends Worker{ } self::$notifier = $this->sleeperEntry->createNotifier(); + Timings::init(); + self::$cycleGcManager = new GarbageCollectorManager($this->logger, Timings::$asyncTaskWorkers); } public function getLogger() : ThreadSafeLogger{ diff --git a/src/scheduler/BulkCurlTask.php b/src/scheduler/BulkCurlTask.php index ccc1b2466..21f144702 100644 --- a/src/scheduler/BulkCurlTask.php +++ b/src/scheduler/BulkCurlTask.php @@ -77,7 +77,10 @@ class BulkCurlTask extends AsyncTask{ * @phpstan-var \Closure(list) : void */ $callback = $this->fetchLocal(self::TLS_KEY_COMPLETION_CALLBACK); - /** @var InternetRequestResult[]|InternetException[] $results */ + /** + * @var InternetRequestResult[]|InternetException[] $results + * @phpstan-var list $results + */ $results = $this->getResult(); $callback($results); } diff --git a/src/scheduler/DumpWorkerMemoryTask.php b/src/scheduler/DumpWorkerMemoryTask.php index 5ef787b5b..fac8d6368 100644 --- a/src/scheduler/DumpWorkerMemoryTask.php +++ b/src/scheduler/DumpWorkerMemoryTask.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\scheduler; use pmmp\thread\Thread as NativeThread; -use pocketmine\MemoryManager; +use pocketmine\MemoryDump; use Symfony\Component\Filesystem\Path; use function assert; @@ -41,7 +41,7 @@ class DumpWorkerMemoryTask extends AsyncTask{ public function onRun() : void{ $worker = NativeThread::getCurrentThread(); assert($worker instanceof AsyncWorker); - MemoryManager::dumpMemory( + MemoryDump::dumpMemory( $worker, Path::join($this->outputFolder, "AsyncWorker#" . $worker->getAsyncWorkerId()), $this->maxNesting, diff --git a/src/stats/SendUsageTask.php b/src/stats/SendUsageTask.php index d218774b5..c49315a72 100644 --- a/src/stats/SendUsageTask.php +++ b/src/stats/SendUsageTask.php @@ -88,7 +88,7 @@ class SendUsageTask extends AsyncTask{ "phpVersion" => PHP_VERSION, "machine" => php_uname("a"), "release" => php_uname("r"), - "platform" => php_uname("i") + "platform" => php_uname("m") ]; $data["players"] = [ diff --git a/src/thread/CommonThreadPartsTrait.php b/src/thread/CommonThreadPartsTrait.php index e1c9d7c6b..d7d51d40f 100644 --- a/src/thread/CommonThreadPartsTrait.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -94,7 +94,19 @@ trait CommonThreadPartsTrait{ } } - 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. + //WARNING: Do not call this inside a synchronized block on this thread's context. Because the shutdown handler + //runs in a synchronized block, this will result in a deadlock. + 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 66aae927a..6fffdc83b 100644 --- a/src/thread/ThreadCrashInfo.php +++ b/src/thread/ThreadCrashInfo.php @@ -84,6 +84,6 @@ final class ThreadCrashInfo extends ThreadSafe{ 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/timings/Timings.php b/src/timings/Timings.php index e71700023..ee737f537 100644 --- a/src/timings/Timings.php +++ b/src/timings/Timings.php @@ -131,7 +131,7 @@ abstract class Timings{ /** @var TimingsHandler[] */ private static array $asyncTaskError = []; - private static TimingsHandler $asyncTaskWorkers; + public static TimingsHandler $asyncTaskWorkers; /** @var TimingsHandler[] */ private static array $asyncTaskRun = []; diff --git a/src/timings/TimingsHandler.php b/src/timings/TimingsHandler.php index 95f7dbacc..25f139d91 100644 --- a/src/timings/TimingsHandler.php +++ b/src/timings/TimingsHandler.php @@ -123,6 +123,7 @@ class TimingsHandler{ /** * @return string[] + * @phpstan-return list */ private static function printFooter() : array{ $result = []; @@ -173,6 +174,7 @@ class TimingsHandler{ } } + /** @phpstan-var PromiseResolver> $resolver */ $resolver = new PromiseResolver(); Promise::all($otherThreadRecordPromises)->onCompletion( function(array $promisedRecords) use ($resolver, $thisThreadRecords) : void{ diff --git a/src/timings/TimingsRecord.php b/src/timings/TimingsRecord.php index 2e4928d8a..390ab74e5 100644 --- a/src/timings/TimingsRecord.php +++ b/src/timings/TimingsRecord.php @@ -131,7 +131,7 @@ final class TimingsRecord{ } public function stopTiming(int $now) : void{ - if($this->start == 0){ + if($this->start === 0){ return; } if(self::$currentRecord !== $this){ diff --git a/src/utils/Config.php b/src/utils/Config.php index cabc8fe16..7d0501935 100644 --- a/src/utils/Config.php +++ b/src/utils/Config.php @@ -54,6 +54,7 @@ use const CASE_LOWER; use const JSON_BIGINT_AS_STRING; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; +use const PHP_INT_MAX; use const YAML_UTF8_ENCODING; /** @@ -72,7 +73,7 @@ class Config{ /** * @var mixed[] - * @phpstan-var array + * @phpstan-var array */ private array $config = []; @@ -339,7 +340,7 @@ class Config{ } public function setNested(string $key, mixed $value) : void{ - $vars = explode(".", $key); + $vars = explode(".", $key, limit: PHP_INT_MAX); $base = array_shift($vars); if(!isset($this->config[$base])){ @@ -366,7 +367,7 @@ class Config{ return $this->nestedCache[$key]; } - $vars = explode(".", $key); + $vars = explode(".", $key, limit: PHP_INT_MAX); $base = array_shift($vars); if(isset($this->config[$base])){ $base = $this->config[$base]; @@ -390,7 +391,7 @@ class Config{ $this->nestedCache = []; $this->changed = true; - $vars = explode(".", $key); + $vars = explode(".", $key, limit: PHP_INT_MAX); $currentNode = &$this->config; while(count($vars) > 0){ @@ -450,7 +451,7 @@ class Config{ /** * @return mixed[] - * @phpstan-return list|array + * @phpstan-return list|array */ public function getAll(bool $keys = false) : array{ return ($keys ? array_keys($this->config) : $this->config); @@ -458,7 +459,6 @@ class Config{ /** * @param mixed[] $defaults - * @phpstan-param array $defaults */ public function setDefaults(array $defaults) : void{ $this->fillDefaults($defaults, $this->config); @@ -467,13 +467,11 @@ class Config{ /** * @param mixed[] $default * @param mixed[] $data reference parameter - * @phpstan-param array $default - * @phpstan-param array $data - * @phpstan-param-out array $data + * @phpstan-param-out array $data */ private function fillDefaults(array $default, array &$data) : int{ $changed = 0; - foreach(Utils::stringifyKeys($default) as $k => $v){ + foreach(Utils::promoteKeys($default) as $k => $v){ if(is_array($v)){ if(!isset($data[$k]) || !is_array($data[$k])){ $data[$k] = []; @@ -498,7 +496,7 @@ class Config{ */ public static function parseList(string $content) : array{ $result = []; - foreach(explode("\n", trim(str_replace("\r\n", "\n", $content))) as $v){ + foreach(explode("\n", trim(str_replace("\r\n", "\n", $content)), limit: PHP_INT_MAX) as $v){ $v = trim($v); if($v === ""){ continue; @@ -509,8 +507,8 @@ class Config{ } /** - * @param string[] $entries - * @phpstan-param list $entries + * @param string[]|int[] $entries + * @phpstan-param list $entries */ public static function writeList(array $entries) : string{ return implode("\n", $entries); @@ -518,11 +516,11 @@ class Config{ /** * @param string[]|int[]|float[]|bool[] $config - * @phpstan-param array $config + * @phpstan-param array $config */ public static function writeProperties(array $config) : string{ $content = "#Properties Config file\r\n#" . date("D M j H:i:s T Y") . "\r\n"; - foreach(Utils::stringifyKeys($config) as $k => $v){ + foreach(Utils::promoteKeys($config) as $k => $v){ if(is_bool($v)){ $v = $v ? "on" : "off"; } @@ -534,7 +532,7 @@ class Config{ /** * @return string[]|int[]|float[]|bool[] - * @phpstan-return array + * @phpstan-return array */ public static function parseProperties(string $content) : array{ $result = []; @@ -560,7 +558,7 @@ class Config{ }; break; } - $result[(string) $k] = $v; + $result[$k] = $v; } } diff --git a/src/utils/Git.php b/src/utils/Git.php index 041d795a1..2b9e242bc 100644 --- a/src/utils/Git.php +++ b/src/utils/Git.php @@ -39,7 +39,7 @@ final class Git{ * @param bool $dirty reference parameter, set to whether the repo has local changes */ public static function getRepositoryState(string $dir, bool &$dirty) : ?string{ - if(Process::execute("git -C \"$dir\" rev-parse HEAD", $out) === 0 && $out !== false && strlen($out = trim($out)) === 40){ + if(Process::execute("git -C \"$dir\" rev-parse HEAD", $out) === 0 && strlen($out = trim($out)) === 40){ if(Process::execute("git -C \"$dir\" diff --quiet") === 1 || Process::execute("git -C \"$dir\" diff --cached --quiet") === 1){ //Locally-modified $dirty = true; } diff --git a/src/utils/Internet.php b/src/utils/Internet.php index 07b3c7a33..febc30715 100644 --- a/src/utils/Internet.php +++ b/src/utils/Internet.php @@ -25,7 +25,6 @@ namespace pocketmine\utils; use pocketmine\VersionInfo; use function array_merge; -use function curl_close; use function curl_error; use function curl_exec; use function curl_getinfo; @@ -60,6 +59,7 @@ use const CURLOPT_RETURNTRANSFER; use const CURLOPT_SSL_VERIFYHOST; use const CURLOPT_SSL_VERIFYPEER; use const CURLOPT_TIMEOUT_MS; +use const PHP_INT_MAX; use const SOCK_DGRAM; use const SOL_UDP; @@ -100,7 +100,7 @@ class Internet{ } $ip = self::getURL("http://ifconfig.me/ip"); - if($ip !== null && ($addr = trim($ip->getBody())) != ""){ + if($ip !== null && ($addr = trim($ip->getBody())) !== ""){ return self::$ip = $addr; } @@ -216,33 +216,30 @@ class Internet{ CURLOPT_HTTPHEADER => array_merge(["User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0 " . VersionInfo::NAME . "/" . VersionInfo::VERSION()->getFullVersion(true)], $extraHeaders), CURLOPT_HEADER => true ]); - try{ - $raw = curl_exec($ch); - if($raw === false){ - throw new InternetException(curl_error($ch)); - } - if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set"); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - $rawHeaders = substr($raw, 0, $headerSize); - $body = substr($raw, $headerSize); - $headers = []; - foreach(explode("\r\n\r\n", $rawHeaders) as $rawHeaderGroup){ - $headerGroup = []; - foreach(explode("\r\n", $rawHeaderGroup) as $line){ - $nameValue = explode(":", $line, 2); - if(isset($nameValue[1])){ - $headerGroup[trim(strtolower($nameValue[0]))] = trim($nameValue[1]); - } - } - $headers[] = $headerGroup; - } - if($onSuccess !== null){ - $onSuccess($ch); - } - return new InternetRequestResult($headers, $body, $httpCode); - }finally{ - curl_close($ch); + $raw = curl_exec($ch); + if($raw === false){ + throw new InternetException(curl_error($ch)); } + if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set"); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $rawHeaders = substr($raw, 0, $headerSize); + $body = substr($raw, $headerSize); + $headers = []; + //TODO: explore if we can set these limits lower + foreach(explode("\r\n\r\n", $rawHeaders, limit: PHP_INT_MAX) as $rawHeaderGroup){ + $headerGroup = []; + foreach(explode("\r\n", $rawHeaderGroup, limit: PHP_INT_MAX) as $line){ + $nameValue = explode(":", $line, 2); + if(isset($nameValue[1])){ + $headerGroup[trim(strtolower($nameValue[0]))] = trim($nameValue[1]); + } + } + $headers[] = $headerGroup; + } + if($onSuccess !== null){ + $onSuccess($ch); + } + return new InternetRequestResult($headers, $body, $httpCode); } } diff --git a/src/utils/Process.php b/src/utils/Process.php index 1370ab27c..90149870a 100644 --- a/src/utils/Process.php +++ b/src/utils/Process.php @@ -174,8 +174,17 @@ final class Process{ return -1; } - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); + $out = stream_get_contents($pipes[1]); + if($out === false){ + throw new AssumptionFailedError("Presume this can't happen for proc_open ... ???"); + } + $stdout = $out; + + $err = stream_get_contents($pipes[2]); + if($err === false){ + throw new AssumptionFailedError("Presume this can't happen for proc_open ... ???"); + } + $stderr = $err; foreach($pipes as $p){ fclose($p); diff --git a/src/utils/ReversePriorityQueue.php b/src/utils/ReversePriorityQueue.php index 53fd0f08a..03f1aea8d 100644 --- a/src/utils/ReversePriorityQueue.php +++ b/src/utils/ReversePriorityQueue.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\utils; /** - * @phpstan-template TPriority + * @phpstan-template TPriority of numeric * @phpstan-template TValue * @phpstan-extends \SplPriorityQueue */ diff --git a/src/utils/SignalHandler.php b/src/utils/SignalHandler.php index 82ae02da6..75d38ff97 100644 --- a/src/utils/SignalHandler.php +++ b/src/utils/SignalHandler.php @@ -36,14 +36,12 @@ use const SIGTERM; final class SignalHandler{ /** @phpstan-var (\Closure(int) : void)|null */ - private ?\Closure $interruptCallback; + private ?\Closure $interruptCallback = null; /** * @phpstan-param \Closure() : void $interruptCallback */ public function __construct(\Closure $interruptCallback){ - $this->interruptCallback = $interruptCallback; - if(function_exists('sapi_windows_set_ctrl_handler')){ sapi_windows_set_ctrl_handler($this->interruptCallback = function(int $signo) use ($interruptCallback) : void{ if($signo === PHP_WINDOWS_EVENT_CTRL_C || $signo === PHP_WINDOWS_EVENT_CTRL_BREAK){ diff --git a/src/utils/Terminal.php b/src/utils/Terminal.php index 49b4224ec..bbd232969 100644 --- a/src/utils/Terminal.php +++ b/src/utils/Terminal.php @@ -59,6 +59,17 @@ abstract class Terminal{ public static string $COLOR_YELLOW = ""; public static string $COLOR_WHITE = ""; public static string $COLOR_MINECOIN_GOLD = ""; + public static string $COLOR_MATERIAL_QUARTZ = ""; + public static string $COLOR_MATERIAL_IRON = ""; + public static string $COLOR_MATERIAL_NETHERITE = ""; + public static string $COLOR_MATERIAL_REDSTONE = ""; + public static string $COLOR_MATERIAL_COPPER = ""; + public static string $COLOR_MATERIAL_GOLD = ""; + public static string $COLOR_MATERIAL_EMERALD = ""; + public static string $COLOR_MATERIAL_DIAMOND = ""; + public static string $COLOR_MATERIAL_LAPIS = ""; + public static string $COLOR_MATERIAL_AMETHYST = ""; + public static string $COLOR_MATERIAL_RESIN = ""; private static ?bool $formattingCodes = null; @@ -111,6 +122,17 @@ abstract class Terminal{ self::$COLOR_YELLOW = $color(227); self::$COLOR_WHITE = $color(231); self::$COLOR_MINECOIN_GOLD = $color(184); + self::$COLOR_MATERIAL_QUARTZ = $color(188); + self::$COLOR_MATERIAL_IRON = $color(251); + self::$COLOR_MATERIAL_NETHERITE = $color(237); + self::$COLOR_MATERIAL_REDSTONE = $color(88); + self::$COLOR_MATERIAL_COPPER = $color(131); + self::$COLOR_MATERIAL_GOLD = $color(178); + self::$COLOR_MATERIAL_EMERALD = $color(35); + self::$COLOR_MATERIAL_DIAMOND = $color(37); + self::$COLOR_MATERIAL_LAPIS = $color(24); + self::$COLOR_MATERIAL_AMETHYST = $color(98); + self::$COLOR_MATERIAL_RESIN = $color(208); } protected static function getEscapeCodes() : void{ @@ -144,15 +166,26 @@ abstract class Terminal{ self::$COLOR_YELLOW = $colors >= 256 ? $setaf(227) : $setaf(11); self::$COLOR_WHITE = $colors >= 256 ? $setaf(231) : $setaf(15); self::$COLOR_MINECOIN_GOLD = $colors >= 256 ? $setaf(184) : $setaf(11); + self::$COLOR_MATERIAL_QUARTZ = $colors >= 256 ? $setaf(188) : $setaf(7); + self::$COLOR_MATERIAL_IRON = $colors >= 256 ? $setaf(251) : $setaf(7); + self::$COLOR_MATERIAL_NETHERITE = $colors >= 256 ? $setaf(237) : $setaf(1); + self::$COLOR_MATERIAL_REDSTONE = $colors >= 256 ? $setaf(88) : $setaf(9); + self::$COLOR_MATERIAL_COPPER = $colors >= 256 ? $setaf(131) : $setaf(3); + self::$COLOR_MATERIAL_GOLD = $colors >= 256 ? $setaf(178) : $setaf(11); + self::$COLOR_MATERIAL_EMERALD = $colors >= 256 ? $setaf(35) : $setaf(2); + self::$COLOR_MATERIAL_DIAMOND = $colors >= 256 ? $setaf(37) : $setaf(14); + self::$COLOR_MATERIAL_LAPIS = $colors >= 256 ? $setaf(24) : $setaf(12); + self::$COLOR_MATERIAL_AMETHYST = $colors >= 256 ? $setaf(98) : $setaf(13); + self::$COLOR_MATERIAL_RESIN = $colors >= 256 ? $setaf(208) : $setaf(11); }else{ - self::$COLOR_BLACK = self::$COLOR_DARK_GRAY = $setaf(0); - self::$COLOR_RED = self::$COLOR_DARK_RED = $setaf(1); - self::$COLOR_GREEN = self::$COLOR_DARK_GREEN = $setaf(2); - self::$COLOR_YELLOW = self::$COLOR_GOLD = self::$COLOR_MINECOIN_GOLD = $setaf(3); - self::$COLOR_BLUE = self::$COLOR_DARK_BLUE = $setaf(4); - self::$COLOR_LIGHT_PURPLE = self::$COLOR_PURPLE = $setaf(5); - self::$COLOR_AQUA = self::$COLOR_DARK_AQUA = $setaf(6); - self::$COLOR_GRAY = self::$COLOR_WHITE = $setaf(7); + self::$COLOR_BLACK = self::$COLOR_DARK_GRAY = self::$COLOR_MATERIAL_NETHERITE = $setaf(0); + self::$COLOR_RED = self::$COLOR_DARK_RED = self::$COLOR_MATERIAL_REDSTONE = self::$COLOR_MATERIAL_COPPER = $setaf(1); + self::$COLOR_GREEN = self::$COLOR_DARK_GREEN = self::$COLOR_MATERIAL_EMERALD = $setaf(2); + self::$COLOR_YELLOW = self::$COLOR_GOLD = self::$COLOR_MINECOIN_GOLD = self::$COLOR_MATERIAL_GOLD = self::$COLOR_MATERIAL_RESIN = $setaf(3); + self::$COLOR_BLUE = self::$COLOR_DARK_BLUE = self::$COLOR_MATERIAL_LAPIS = $setaf(4); + self::$COLOR_LIGHT_PURPLE = self::$COLOR_PURPLE = self::$COLOR_MATERIAL_AMETHYST = $setaf(5); + self::$COLOR_AQUA = self::$COLOR_DARK_AQUA = self::$COLOR_MATERIAL_DIAMOND = $setaf(6); + self::$COLOR_GRAY = self::$COLOR_WHITE = self::$COLOR_MATERIAL_QUARTZ = self::$COLOR_MATERIAL_IRON = $setaf(7); } } @@ -191,12 +224,10 @@ abstract class Terminal{ public static function toANSI(string $string) : string{ $newString = ""; foreach(TextFormat::tokenize($string) as $token){ - $newString .= match($token){ + $newString .= match ($token) { TextFormat::BOLD => Terminal::$FORMAT_BOLD, TextFormat::OBFUSCATED => Terminal::$FORMAT_OBFUSCATED, TextFormat::ITALIC => Terminal::$FORMAT_ITALIC, - TextFormat::UNDERLINE => Terminal::$FORMAT_UNDERLINE, - TextFormat::STRIKETHROUGH => Terminal::$FORMAT_STRIKETHROUGH, TextFormat::RESET => Terminal::$FORMAT_RESET, TextFormat::BLACK => Terminal::$COLOR_BLACK, TextFormat::DARK_BLUE => Terminal::$COLOR_DARK_BLUE, @@ -215,6 +246,17 @@ abstract class Terminal{ TextFormat::YELLOW => Terminal::$COLOR_YELLOW, TextFormat::WHITE => Terminal::$COLOR_WHITE, TextFormat::MINECOIN_GOLD => Terminal::$COLOR_MINECOIN_GOLD, + TextFormat::MATERIAL_QUARTZ => Terminal::$COLOR_MATERIAL_QUARTZ, + TextFormat::MATERIAL_IRON => Terminal::$COLOR_MATERIAL_IRON, + TextFormat::MATERIAL_NETHERITE => Terminal::$COLOR_MATERIAL_NETHERITE, + TextFormat::MATERIAL_REDSTONE => Terminal::$COLOR_MATERIAL_REDSTONE, + TextFormat::MATERIAL_COPPER => Terminal::$COLOR_MATERIAL_COPPER, + TextFormat::MATERIAL_GOLD => Terminal::$COLOR_MATERIAL_GOLD, + TextFormat::MATERIAL_EMERALD => Terminal::$COLOR_MATERIAL_EMERALD, + TextFormat::MATERIAL_DIAMOND => Terminal::$COLOR_MATERIAL_DIAMOND, + TextFormat::MATERIAL_LAPIS => Terminal::$COLOR_MATERIAL_LAPIS, + TextFormat::MATERIAL_AMETHYST => Terminal::$COLOR_MATERIAL_AMETHYST, + TextFormat::MATERIAL_RESIN => Terminal::$COLOR_MATERIAL_RESIN, default => $token, }; } diff --git a/src/utils/TextFormat.php b/src/utils/TextFormat.php index dfd6a359a..0c948592a 100644 --- a/src/utils/TextFormat.php +++ b/src/utils/TextFormat.php @@ -63,6 +63,17 @@ abstract class TextFormat{ public const YELLOW = TextFormat::ESCAPE . "e"; public const WHITE = TextFormat::ESCAPE . "f"; public const MINECOIN_GOLD = TextFormat::ESCAPE . "g"; + public const MATERIAL_QUARTZ = TextFormat::ESCAPE . "h"; + public const MATERIAL_IRON = TextFormat::ESCAPE . "i"; + public const MATERIAL_NETHERITE = TextFormat::ESCAPE . "j"; + public const MATERIAL_REDSTONE = TextFormat::ESCAPE . "m"; + public const MATERIAL_COPPER = TextFormat::ESCAPE . "n"; + public const MATERIAL_GOLD = TextFormat::ESCAPE . "p"; + public const MATERIAL_EMERALD = TextFormat::ESCAPE . "q"; + public const MATERIAL_DIAMOND = TextFormat::ESCAPE . "s"; + public const MATERIAL_LAPIS = TextFormat::ESCAPE . "t"; + public const MATERIAL_AMETHYST = TextFormat::ESCAPE . "u"; + public const MATERIAL_RESIN = TextFormat::ESCAPE . "v"; public const COLORS = [ self::BLACK => self::BLACK, @@ -82,19 +93,30 @@ abstract class TextFormat{ self::YELLOW => self::YELLOW, self::WHITE => self::WHITE, self::MINECOIN_GOLD => self::MINECOIN_GOLD, + self::MATERIAL_QUARTZ => self::MATERIAL_QUARTZ, + self::MATERIAL_IRON => self::MATERIAL_IRON, + self::MATERIAL_NETHERITE => self::MATERIAL_NETHERITE, + self::MATERIAL_REDSTONE => self::MATERIAL_REDSTONE, + self::MATERIAL_COPPER => self::MATERIAL_COPPER, + self::MATERIAL_GOLD => self::MATERIAL_GOLD, + self::MATERIAL_EMERALD => self::MATERIAL_EMERALD, + self::MATERIAL_DIAMOND => self::MATERIAL_DIAMOND, + self::MATERIAL_LAPIS => self::MATERIAL_LAPIS, + self::MATERIAL_AMETHYST => self::MATERIAL_AMETHYST, + self::MATERIAL_RESIN => self::MATERIAL_RESIN, ]; public const OBFUSCATED = TextFormat::ESCAPE . "k"; public const BOLD = TextFormat::ESCAPE . "l"; - public const STRIKETHROUGH = TextFormat::ESCAPE . "m"; - public const UNDERLINE = TextFormat::ESCAPE . "n"; + /** @deprecated */ + public const STRIKETHROUGH = ""; + /** @deprecated */ + public const UNDERLINE = ""; public const ITALIC = TextFormat::ESCAPE . "o"; public const FORMATS = [ self::OBFUSCATED => self::OBFUSCATED, self::BOLD => self::BOLD, - self::STRIKETHROUGH => self::STRIKETHROUGH, - self::UNDERLINE => self::UNDERLINE, self::ITALIC => self::ITALIC, ]; @@ -130,7 +152,7 @@ abstract class TextFormat{ * @return string[] */ public static function tokenize(string $string) : array{ - $result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-gk-or])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-v])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); if($result === false) throw self::makePcreError(); return $result; } @@ -144,7 +166,7 @@ abstract class TextFormat{ $string = mb_scrub($string, 'UTF-8'); $string = self::preg_replace("/[\x{E000}-\x{F8FF}]/u", "", $string); //remove unicode private-use-area characters (they might break the console) if($removeFormat){ - $string = str_replace(TextFormat::ESCAPE, "", self::preg_replace("/" . TextFormat::ESCAPE . "[0-9a-gk-or]/u", "", $string)); + $string = str_replace(TextFormat::ESCAPE, "", self::preg_replace("/" . TextFormat::ESCAPE . "[0-9a-v]/u", "", $string)); } return str_replace("\x1b", "", self::preg_replace("/\x1b[\\(\\][[0-9;\\[\\(]+[Bm]/u", "", $string)); } @@ -155,7 +177,7 @@ abstract class TextFormat{ * @param string $placeholder default "&" */ public static function colorize(string $string, string $placeholder = "&") : string{ - return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-gk-or])/u', TextFormat::ESCAPE . '$1', $string); + return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-v])/u', TextFormat::ESCAPE . '$1', $string); } /** @@ -183,6 +205,20 @@ abstract class TextFormat{ return $baseFormat . str_replace(TextFormat::RESET, $baseFormat, $string); } + /** + * Converts any Java formatting codes in the given string to Bedrock. + * + * As of 1.21.50, strikethrough (§m) and underline (§n) are not supported by Bedrock, and these symbols are instead + * used to represent additional colours in Bedrock. To avoid unintended formatting, this function currently strips + * those formatting codes to prevent unintended colour display in formatted text. + * + * If Bedrock starts to support these formats in the future, this function will be updated to translate them rather + * than removing them. + */ + public static function javaToBedrock(string $string) : string{ + return str_replace([TextFormat::ESCAPE . "m", TextFormat::ESCAPE . "n"], "", $string); + } + /** * Returns an HTML-formatted string with colors/markup */ @@ -190,104 +226,47 @@ abstract class TextFormat{ $newString = ""; $tokens = 0; foreach(self::tokenize($string) as $token){ - switch($token){ - case TextFormat::BOLD: - $newString .= ""; - ++$tokens; - break; - case TextFormat::OBFUSCATED: - //$newString .= ""; - //++$tokens; - break; - case TextFormat::ITALIC: - $newString .= ""; - ++$tokens; - break; - case TextFormat::UNDERLINE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::STRIKETHROUGH: - $newString .= ""; - ++$tokens; - break; - case TextFormat::RESET: - $newString .= str_repeat("", $tokens); - $tokens = 0; - break; - - //Colors - case TextFormat::BLACK: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_BLUE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_GREEN: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_AQUA: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_RED: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_PURPLE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::GOLD: - $newString .= ""; - ++$tokens; - break; - case TextFormat::GRAY: - $newString .= ""; - ++$tokens; - break; - case TextFormat::DARK_GRAY: - $newString .= ""; - ++$tokens; - break; - case TextFormat::BLUE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::GREEN: - $newString .= ""; - ++$tokens; - break; - case TextFormat::AQUA: - $newString .= ""; - ++$tokens; - break; - case TextFormat::RED: - $newString .= ""; - ++$tokens; - break; - case TextFormat::LIGHT_PURPLE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::YELLOW: - $newString .= ""; - ++$tokens; - break; - case TextFormat::WHITE: - $newString .= ""; - ++$tokens; - break; - case TextFormat::MINECOIN_GOLD: - $newString .= ""; - ++$tokens; - break; - default: - $newString .= $token; - break; + $formatString = match($token){ + TextFormat::BLACK => "color:#000", + TextFormat::DARK_BLUE => "color:#00A", + TextFormat::DARK_GREEN => "color:#0A0", + TextFormat::DARK_AQUA => "color:#0AA", + TextFormat::DARK_RED => "color:#A00", + TextFormat::DARK_PURPLE => "color:#A0A", + TextFormat::GOLD => "color:#FA0", + TextFormat::GRAY => "color:#AAA", + TextFormat::DARK_GRAY => "color:#555", + TextFormat::BLUE => "color:#55F", + TextFormat::GREEN => "color:#5F5", + TextFormat::AQUA => "color:#5FF", + TextFormat::RED => "color:#F55", + TextFormat::LIGHT_PURPLE => "color:#F5F", + TextFormat::YELLOW => "color:#FF5", + TextFormat::WHITE => "color:#FFF", + TextFormat::MINECOIN_GOLD => "color:#dd0", + TextFormat::MATERIAL_QUARTZ => "color:#e2d3d1", + TextFormat::MATERIAL_IRON => "color:#cec9c9", + TextFormat::MATERIAL_NETHERITE => "color:#44393a", + TextFormat::MATERIAL_REDSTONE => "color:#961506", + TextFormat::MATERIAL_COPPER => "color:#b4684d", + TextFormat::MATERIAL_GOLD => "color:#deb02c", + TextFormat::MATERIAL_EMERALD => "color:#119f36", + TextFormat::MATERIAL_DIAMOND => "color:#2cb9a8", + TextFormat::MATERIAL_LAPIS => "color:#20487a", + TextFormat::MATERIAL_AMETHYST => "color:#9a5cc5", + TextFormat::MATERIAL_RESIN => "color:#fc7812", + TextFormat::BOLD => "font-weight:bold", + TextFormat::ITALIC => "font-style:italic", + default => null + }; + if($formatString !== null){ + $newString .= ""; + ++$tokens; + }elseif($token === TextFormat::RESET){ + $newString .= str_repeat("", $tokens); + $tokens = 0; + }else{ + $newString .= $token; } } diff --git a/src/utils/Timezone.php b/src/utils/Timezone.php index 7f1dcf0e4..6723b12eb 100644 --- a/src/utils/Timezone.php +++ b/src/utils/Timezone.php @@ -134,7 +134,7 @@ abstract class Timezone{ $offset = $matches[2]; - if($offset == ""){ + if($offset === ""){ return "UTC"; } @@ -156,7 +156,7 @@ abstract class Timezone{ $offset = trim(exec('date +%:z')); - if($offset == "+00:00"){ + if($offset === "+00:00"){ return "UTC"; } @@ -195,7 +195,7 @@ abstract class Timezone{ $offset = $parsed['hour'] * 3600 + $parsed['minute'] * 60 + $parsed['second']; //After date_parse is done, put the sign back - if($negative_offset == true){ + if($negative_offset){ $offset = -abs($offset); } @@ -204,7 +204,7 @@ abstract class Timezone{ //That's been a bug in PHP since 2008! foreach(timezone_abbreviations_list() as $zones){ foreach($zones as $timezone){ - if($timezone['timezone_id'] !== null && $timezone['offset'] == $offset){ + if($timezone['timezone_id'] !== null && $timezone['offset'] === $offset){ return $timezone['timezone_id']; } } diff --git a/src/utils/Utils.php b/src/utils/Utils.php index c8be174d6..800bd0183 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -167,6 +167,7 @@ final class Utils{ /** * @phpstan-return \Closure(object) : object + * @deprecated */ public static function cloneCallback() : \Closure{ return static function(object $o){ @@ -179,15 +180,13 @@ final class Utils{ * @phpstan-template TValue of object * * @param object[] $array - * @phpstan-param array $array + * @phpstan-param array|list $array * * @return object[] - * @phpstan-return array + * @phpstan-return ($array is list ? list : array) */ public static function cloneObjectArray(array $array) : array{ - /** @phpstan-var \Closure(TValue) : TValue $callback */ - $callback = self::cloneCallback(); - return array_map($callback, $array); + return array_map(fn(object $o) => clone $o, $array); } /** @@ -220,7 +219,7 @@ final class Utils{ $mac = implode("\n", $mac); if(preg_match_all("#Physical Address[. ]{1,}: ([0-9A-F\\-]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ - if($v == "00-00-00-00-00-00"){ + if($v === "00-00-00-00-00-00"){ unset($matches[1][$i]); } } @@ -234,7 +233,7 @@ final class Utils{ $mac = implode("\n", $mac); if(preg_match_all("#HWaddr[ \t]{1,}([0-9a-f:]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ - if($v == "00:00:00:00:00:00"){ + if($v === "00:00:00:00:00:00"){ unset($matches[1][$i]); } } @@ -265,14 +264,7 @@ final class Utils{ } /** - * Returns the current Operating System - * Windows => win - * MacOS => mac - * iOS => ios - * Android => android - * Linux => Linux - * BSD => bsd - * Other => other + * @return string one of the Utils::OS_* constants */ public static function getOS(bool $recalculate = false) : string{ if(self::$os === null || $recalculate){ @@ -377,7 +369,7 @@ final class Utils{ debug_zval_dump($value); $contents = ob_get_contents(); if($contents === false) throw new AssumptionFailedError("ob_get_contents() should never return false here"); - $ret = explode("\n", $contents); + $ret = explode("\n", $contents, limit: 2); ob_end_clean(); if(preg_match('/^.* refcount\\(([0-9]+)\\)\\{$/', trim($ret[0]), $m) > 0){ @@ -406,6 +398,7 @@ final class Utils{ /** * @param mixed[][] $trace + * @phpstan-param list>|null $trace * @return string[] */ public static function printableExceptionInfo(\Throwable $e, $trace = null) : array{ @@ -469,7 +462,15 @@ final class Utils{ } $params = implode(", ", $paramsList); } - $messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")"; + $messages[] = "#$i " . + (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . + "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . + (isset($trace[$i]["class"]) ? + $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : + "" + ) . + $trace[$i]["function"] . + "(" . Utils::printable($params) . ")"; } return $messages; } @@ -490,7 +491,7 @@ final class Utils{ $rawFrame = $rawTrace[$frameId]; $safeTrace[$frameId] = new ThreadCrashInfoFrame( $printableFrame, - $rawFrame["file"] ?? "unknown", + $rawFrame["file"] ?? null, $rawFrame["line"] ?? 0 ); } @@ -583,10 +584,10 @@ final class Utils{ /** * @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($array as $k => $v){ + foreach(Utils::promoteKeys($array) as $k => $v){ try{ $validator($v); }catch(\TypeError $e){ diff --git a/src/world/Explosion.php b/src/world/Explosion.php index 601f9109e..9e83d06be 100644 --- a/src/world/Explosion.php +++ b/src/world/Explosion.php @@ -26,16 +26,20 @@ namespace pocketmine\world; use pocketmine\block\Block; use pocketmine\block\RuntimeBlockStateRegistry; use pocketmine\block\TNT; +use pocketmine\block\utils\SupportType; use pocketmine\block\VanillaBlocks; use pocketmine\entity\Entity; +use pocketmine\event\block\BlockExplodeEvent; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityExplodeEvent; use pocketmine\item\VanillaItems; use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\Utils; use pocketmine\world\format\SubChunk; use pocketmine\world\particle\HugeExplodeSeedParticle; use pocketmine\world\sound\ExplodeSound; @@ -48,25 +52,36 @@ use function mt_rand; use function sqrt; class Explosion{ + public const DEFAULT_FIRE_CHANCE = 1.0 / 3.0; + private int $rays = 16; public World $world; - /** @var Block[] */ + /** + * @var Block[] + * @phpstan-var array + */ public array $affectedBlocks = []; public float $stepLen = 0.3; + /** @var Block[] */ + private array $fireIgnitions = []; private SubChunkExplorer $subChunkExplorer; public function __construct( public Position $source, public float $radius, - private Entity|Block|null $what = null + private Entity|Block|null $what = null, + private float $fireChance = 0.0 ){ if(!$this->source->isValid()){ throw new \InvalidArgumentException("Position does not have a valid world"); } $this->world = $this->source->getWorld(); - + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } if($radius <= 0){ throw new \InvalidArgumentException("Explosion radius must be greater than 0, got $radius"); } @@ -85,6 +100,7 @@ class Explosion{ $blockFactory = RuntimeBlockStateRegistry::getInstance(); $mRays = $this->rays - 1; + $incendiary = $this->fireChance > 0; for($i = 0; $i < $this->rays; ++$i){ for($j = 0; $j < $this->rays; ++$j){ for($k = 0; $k < $this->rays; ++$k){ @@ -127,7 +143,12 @@ class Explosion{ $_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false); foreach($_block->getAffectedBlocks() as $_affectedBlock){ $_affectedBlockPos = $_affectedBlock->getPosition(); - $this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock; + $posHash = World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z); + $this->affectedBlocks[$posHash] = $_affectedBlock; + + if($incendiary && Utils::getRandomFloat() <= $this->fireChance){ + $this->fireIgnitions[$posHash] = $_affectedBlock; + } } } } @@ -150,13 +171,32 @@ class Explosion{ $yield = min(100, (1 / $this->radius) * 100); if($this->what instanceof Entity){ - $ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield); + $ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield, $this->fireIgnitions); + + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $yield = $ev->getYield(); + $this->affectedBlocks = $ev->getBlockList(); + $this->fireIgnitions = $ev->getIgnitions(); + }elseif($this->what instanceof Block){ + $ev = new BlockExplodeEvent( + $this->what, + $this->source, + $this->affectedBlocks, + $yield, + $this->fireIgnitions, + ); + $ev->call(); if($ev->isCancelled()){ return false; }else{ $yield = $ev->getYield(); - $this->affectedBlocks = $ev->getBlockList(); + $this->affectedBlocks = $ev->getAffectedBlocks(); + $this->fireIgnitions = $ev->getIgnitions(); } } @@ -198,8 +238,9 @@ class Explosion{ $air = VanillaItems::AIR(); $airBlock = VanillaBlocks::AIR(); + $fireBlock = VanillaBlocks::FIRE(); - foreach($this->affectedBlocks as $block){ + foreach($this->affectedBlocks as $hash => $block){ $pos = $block->getPosition(); if($block instanceof TNT){ $block->ignite(mt_rand(10, 30)); @@ -212,7 +253,13 @@ class Explosion{ if(($t = $this->world->getTileAt($pos->x, $pos->y, $pos->z)) !== null){ $t->onBlockDestroyed(); //needed to create drops for inventories } - $this->world->setBlockAt($pos->x, $pos->y, $pos->z, $airBlock); + $targetBlock = + isset($this->fireIgnitions[$hash]) && + $block->getSide(Facing::DOWN)->getSupportType(Facing::UP) === SupportType::FULL ? + $fireBlock : + $airBlock; + + $this->world->setBlockAt($pos->x, $pos->y, $pos->z, $targetBlock); } } @@ -221,4 +268,18 @@ class Explosion{ return true; } + + /** + * Sets a chance between 0 and 1 of creating a fire. + * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. + * + * @param float $fireChance 0 ... 1 + */ + public function setFireChance(float $fireChance) : void{ + Utils::checkFloatNotInfOrNaN("fireChance", $fireChance); + if($fireChance < 0.0 || $fireChance > 1.0){ + throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1."); + } + $this->fireChance = $fireChance; + } } diff --git a/src/world/World.php b/src/world/World.php index 6ed0dab2e..b6ff53972 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -42,6 +42,7 @@ use pocketmine\data\SavedDataLoadingException; use pocketmine\entity\Entity; use pocketmine\entity\EntityFactory; use pocketmine\entity\Location; +use pocketmine\entity\NeverSavedWithChunkEntity; use pocketmine\entity\object\ExperienceOrb; use pocketmine\entity\object\ItemEntity; use pocketmine\event\block\BlockBreakEvent; @@ -94,9 +95,11 @@ use pocketmine\world\format\io\GlobalBlockStateHandlers; use pocketmine\world\format\io\WritableWorldProvider; use pocketmine\world\format\LightArray; use pocketmine\world\format\SubChunk; +use pocketmine\world\generator\executor\AsyncGeneratorExecutor; +use pocketmine\world\generator\executor\GeneratorExecutor; +use pocketmine\world\generator\executor\GeneratorExecutorSetupParameters; +use pocketmine\world\generator\executor\SyncGeneratorExecutor; use pocketmine\world\generator\GeneratorManager; -use pocketmine\world\generator\GeneratorRegisterTask; -use pocketmine\world\generator\GeneratorUnregisterTask; use pocketmine\world\generator\PopulationTask; use pocketmine\world\light\BlockLightUpdate; use pocketmine\world\light\LightPopulationTask; @@ -114,6 +117,7 @@ use function array_keys; use function array_map; use function array_merge; use function array_sum; +use function array_values; use function assert; use function cos; use function count; @@ -168,6 +172,9 @@ class World implements ChunkManager{ public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3; + //TODO: this could probably do with being a lot bigger + private const BLOCK_CACHE_SIZE_CAP = 2048; + /** * @var Player[] entity runtime ID => Player * @phpstan-var array @@ -203,6 +210,7 @@ class World implements ChunkManager{ * @phpstan-var array> */ private array $blockCache = []; + private int $blockCacheSize = 0; /** * @var AxisAlignedBB[][][] chunkHash => [relativeBlockHash => AxisAlignedBB[]] * @phpstan-var array>> @@ -283,6 +291,12 @@ class World implements ChunkManager{ */ private array $chunks = []; + /** + * @var true[] + * @phpstan-var array + */ + private array $knownUngeneratedChunks = []; + /** * @var Vector3[][] chunkHash => [relativeBlockHash => Vector3] * @phpstan-var array> @@ -332,11 +346,7 @@ class World implements ChunkManager{ */ private array $chunkPopulationRequestQueueIndex = []; - /** - * @var true[] - * @phpstan-var array - */ - private array $generatorRegisteredWorkers = []; + private readonly GeneratorExecutor $generatorExecutor; private bool $autoSave = true; @@ -356,9 +366,6 @@ class World implements ChunkManager{ private bool $doingTick = false; - /** @phpstan-var class-string<\pocketmine\world\generator\Generator> */ - private string $generator; - private bool $unloaded = false; /** * @var \Closure[] @@ -371,6 +378,8 @@ class World implements ChunkManager{ private \Logger $logger; + private RuntimeBlockStateRegistry $blockStateRegistry; + /** * @phpstan-return ChunkPosHash */ @@ -484,6 +493,7 @@ class World implements ChunkManager{ $this->displayName = $this->provider->getWorldData()->getName(); $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName"); + $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance(); $this->minY = $this->provider->getWorldMinY(); $this->maxY = $this->provider->getWorldMaxY(); @@ -491,7 +501,23 @@ class World implements ChunkManager{ $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ?? throw new AssumptionFailedError("WorldManager should already have checked that the generator exists"); $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions()); - $this->generator = $generator->getGeneratorClass(); + + $executorSetupParameters = new GeneratorExecutorSetupParameters( + worldMinY: $this->minY, + worldMaxY: $this->maxY, + generatorSeed: $this->getSeed(), + generatorClass: $generator->getGeneratorClass(), + generatorSettings: $this->provider->getWorldData()->getGeneratorOptions() + ); + $this->generatorExecutor = $generator->isFast() ? + new SyncGeneratorExecutor($executorSetupParameters) : + new AsyncGeneratorExecutor( + $this->logger, + $this->workerPool, + $executorSetupParameters, + $this->worldId + ); + $this->chunkPopulationRequestQueue = new \SplQueue(); $this->addOnUnloadCallback(function() : void{ $this->logger->debug("Cancelling unfulfilled generation requests"); @@ -527,17 +553,6 @@ class World implements ChunkManager{ $this->initRandomTickBlocksFromConfig($cfg); $this->timings = new WorldTimings($this); - - $this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{ - if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){ - $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered"); - unset($this->generatorRegisteredWorkers[$workerId]); - } - }); - $workerPool = $this->workerPool; - $this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{ - $workerPool->removeWorkerStartHook($workerStartHook); - }); } private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{ @@ -555,7 +570,7 @@ class World implements ChunkManager{ }catch(BlockStateDeserializeException){ continue; } - $block = RuntimeBlockStateRegistry::getInstance()->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData)); + $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData)); }else{ //TODO: we probably ought to log an error here continue; @@ -566,7 +581,7 @@ class World implements ChunkManager{ } } - foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $state){ + foreach($this->blockStateRegistry->getAllKnownStates() as $state){ $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null; if($dontTickName === null && $state->ticksRandomly()){ $this->randomTickBlocks[$state->getStateId()] = true; @@ -578,21 +593,6 @@ class World implements ChunkManager{ return $this->tickRateTime; } - public function registerGeneratorToWorker(int $worker) : void{ - $this->logger->debug("Registering generator on worker $worker"); - $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker); - $this->generatorRegisteredWorkers[$worker] = true; - } - - public function unregisterGenerator() : void{ - foreach($this->workerPool->getRunningWorkers() as $i){ - if(isset($this->generatorRegisteredWorkers[$i])){ - $this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i); - } - } - $this->generatorRegisteredWorkers = []; - } - public function getServer() : Server{ return $this->server; } @@ -633,6 +633,7 @@ class World implements ChunkManager{ self::getXZ($chunkHash, $chunkX, $chunkZ); $this->unloadChunk($chunkX, $chunkZ, false); } + $this->knownUngeneratedChunks = []; foreach($this->entitiesByChunk as $chunkHash => $entities){ self::getXZ($chunkHash, $chunkX, $chunkZ); @@ -650,10 +651,11 @@ class World implements ChunkManager{ $this->save(); - $this->unregisterGenerator(); + $this->generatorExecutor->shutdown(); $this->provider->close(); $this->blockCache = []; + $this->blockCacheSize = 0; $this->blockCollisionBoxCache = []; $this->unloaded = true; @@ -674,7 +676,6 @@ class World implements ChunkManager{ * Used for broadcasting sounds and particles with specific targets. * * @param Player[] $allowed - * @phpstan-param list $allowed * * @return array */ @@ -1085,7 +1086,6 @@ class World implements ChunkManager{ /** * @param Vector3[] $blocks - * @phpstan-param list $blocks * * @return ClientboundPacket[] * @phpstan-return list @@ -1139,13 +1139,16 @@ class World implements ChunkManager{ public function clearCache(bool $force = false) : void{ if($force){ $this->blockCache = []; + $this->blockCacheSize = 0; $this->blockCollisionBoxCache = []; }else{ - $count = 0; + //Recalculate this when we're asked - blockCacheSize may be higher than the real size + $this->blockCacheSize = 0; foreach($this->blockCache as $list){ - $count += count($list); - if($count > 2048){ + $this->blockCacheSize += count($list); + if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){ $this->blockCache = []; + $this->blockCacheSize = 0; break; } } @@ -1153,7 +1156,7 @@ class World implements ChunkManager{ $count = 0; foreach($this->blockCollisionBoxCache as $list){ $count += count($list); - if($count > 2048){ + if($count > self::BLOCK_CACHE_SIZE_CAP){ //TODO: Is this really the best logic? $this->blockCollisionBoxCache = []; break; @@ -1162,6 +1165,19 @@ class World implements ChunkManager{ } } + private function trimBlockCache() : void{ + $before = $this->blockCacheSize; + //Since PHP maintains key order, earliest in foreach should be the oldest entries + //Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance + foreach($this->blockCache as $chunkHash => $blocks){ + unset($this->blockCache[$chunkHash]); + $this->blockCacheSize -= count($blocks); + if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){ + break; + } + } + } + /** * @return true[] fullID => dummy * @phpstan-return array @@ -1344,7 +1360,7 @@ class World implements ChunkManager{ * TODO: phpstan can't infer these types yet :( * @phpstan-var array $blockLight * @phpstan-var array $skyLight - * @phpstan-var array $heightMap + * @phpstan-var non-empty-list $heightMap */ if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){ return; @@ -1375,7 +1391,7 @@ class World implements ChunkManager{ $entity->onRandomUpdate(); } - $blockFactory = RuntimeBlockStateRegistry::getInstance(); + $blockFactory = $this->blockStateRegistry; foreach($chunk->getSubChunks() as $Y => $subChunk){ if(!$subChunk->isEmptyFast()){ $k = 0; @@ -1436,8 +1452,8 @@ class World implements ChunkManager{ $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData( $chunk->getSubChunks(), $chunk->isPopulated(), - array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk())), - array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()), + array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))), + array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())), ), $chunk->getTerrainDirtyFlags()); $chunk->clearTerrainDirtyFlags(); } @@ -1509,24 +1525,48 @@ class World implements ChunkManager{ $collides = []; + $collisionInfo = $this->blockStateRegistry->collisionInfo; if($targetFirst){ for($z = $minZ; $z <= $maxZ; ++$z){ + $zOverflow = $z === $minZ || $z === $maxZ; for($x = $minX; $x <= $maxX; ++$x){ + $zxOverflow = $zOverflow || $x === $minX || $x === $maxX; for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->getBlockAt($x, $y, $z); - if($block->collidesWithBB($bb)){ - return [$block]; + $overflow = $zxOverflow || $y === $minY || $y === $maxY; + + $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo); + if($overflow ? + $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) : + match ($stateCollisionInfo) { + RuntimeBlockStateRegistry::COLLISION_CUBE => true, + RuntimeBlockStateRegistry::COLLISION_NONE => false, + default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) + } + ){ + return [$this->getBlockAt($x, $y, $z)]; } } } } }else{ + //TODO: duplicated code :( this way is better for performance though for($z = $minZ; $z <= $maxZ; ++$z){ + $zOverflow = $z === $minZ || $z === $maxZ; for($x = $minX; $x <= $maxX; ++$x){ + $zxOverflow = $zOverflow || $x === $minX || $x === $maxX; for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->getBlockAt($x, $y, $z); - if($block->collidesWithBB($bb)){ - $collides[] = $block; + $overflow = $zxOverflow || $y === $minY || $y === $maxY; + + $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo); + if($overflow ? + $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) : + match ($stateCollisionInfo) { + RuntimeBlockStateRegistry::COLLISION_CUBE => true, + RuntimeBlockStateRegistry::COLLISION_NONE => false, + default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) + } + ){ + $collides[] = $this->getBlockAt($x, $y, $z); } } } @@ -1536,23 +1576,64 @@ class World implements ChunkManager{ return $collides; } + /** + * @param int[] $collisionInfo + * @phpstan-param array $collisionInfo + */ + private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{ + if(!$this->isInWorld($x, $y, $z)){ + return RuntimeBlockStateRegistry::COLLISION_NONE; + } + $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE); + if($chunk === null){ + return RuntimeBlockStateRegistry::COLLISION_NONE; + } + $stateId = $chunk + ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE) + ->getBlockStateId( + $x & SubChunk::COORD_MASK, + $y & SubChunk::COORD_MASK, + $z & SubChunk::COORD_MASK + ); + return $collisionInfo[$stateId]; + } + /** * Returns a list of all block AABBs which overlap the full block area at the given coordinates. * This checks a padding of 1 block around the coordinates to account for oversized AABBs of blocks like fences. * Larger AABBs (>= 2 blocks on any axis) are not accounted for. * + * @param int[] $collisionInfo + * @phpstan-param array $collisionInfo + * * @return AxisAlignedBB[] + * @phpstan-return list */ - private function getBlockCollisionBoxesForCell(int $x, int $y, int $z) : array{ - $block = $this->getBlockAt($x, $y, $z); - $boxes = $block->getCollisionBoxes(); + private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{ + $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo); + $boxes = match($stateCollisionInfo){ + RuntimeBlockStateRegistry::COLLISION_NONE => [], + RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)], + default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes() + }; - $cellBB = AxisAlignedBB::one()->offset($x, $y, $z); - foreach(Facing::OFFSET as [$dx, $dy, $dz]){ - $extraBoxes = $this->getBlockAt($x + $dx, $y + $dy, $z + $dz)->getCollisionBoxes(); - foreach($extraBoxes as $extraBox){ - if($extraBox->intersectsWith($cellBB)){ - $boxes[] = $extraBox; + //overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case + if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){ + $cellBB = null; + foreach(Facing::OFFSET as [$dx, $dy, $dz]){ + $offsetX = $x + $dx; + $offsetY = $y + $dy; + $offsetZ = $z + $dz; + $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo); + if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){ + //avoid allocating this unless it's needed + $cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z); + $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes(); + foreach($extraBoxes as $extraBox){ + if($extraBox->intersectsWith($cellBB)){ + $boxes[] = $extraBox; + } + } } } } @@ -1574,13 +1655,15 @@ class World implements ChunkManager{ $collides = []; + $collisionInfo = $this->blockStateRegistry->collisionInfo; + for($z = $minZ; $z <= $maxZ; ++$z){ for($x = $minX; $x <= $maxX; ++$x){ $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE); for($y = $minY; $y <= $maxY; ++$y){ $relativeBlockHash = World::chunkBlockHash($x, $y, $z); - $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z); + $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo); foreach($boxes as $blockBB){ if($blockBB->intersectsWith($bb)){ @@ -1775,7 +1858,7 @@ class World implements ChunkManager{ return; } - $blockFactory = RuntimeBlockStateRegistry::getInstance(); + $blockFactory = $this->blockStateRegistry; $this->timings->doBlockSkyLightUpdates->startTiming(); if($this->skyLightUpdate === null){ $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight); @@ -1894,7 +1977,7 @@ class World implements ChunkManager{ $chunk = $this->chunks[$chunkHash] ?? null; if($chunk !== null){ - $block = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK)); + $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK)); }else{ $addToCache = false; $block = VanillaBlocks::AIR(); @@ -1922,6 +2005,10 @@ class World implements ChunkManager{ if($addToCache && $relativeBlockHash !== null){ $this->blockCache[$chunkHash][$relativeBlockHash] = $block; + + if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){ + $this->trimBlockCache(); + } } return $block; @@ -1954,6 +2041,15 @@ class World implements ChunkManager{ throw new WorldException("Cannot set a block in un-generated terrain"); } + //TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :( + $stateId = $block->getStateId(); + if(!$this->blockStateRegistry->hasStateId($stateId)){ + throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)"); + } + if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){ + throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer"); + } + $this->timings->setBlock->startTiming(); $this->unlockChunk($chunkX, $chunkZ, null); @@ -1968,6 +2064,7 @@ class World implements ChunkManager{ $relativeBlockHash = World::chunkBlockHash($x, $y, $z); unset($this->blockCache[$chunkHash][$relativeBlockHash]); + $this->blockCacheSize--; unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]); //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the //caches for those blocks as well @@ -2015,7 +2112,6 @@ class World implements ChunkManager{ * @phpstan-return list */ public function dropExperience(Vector3 $pos, int $amount) : array{ - /** @var ExperienceOrb[] $orbs */ $orbs = []; foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){ @@ -2204,22 +2300,15 @@ class World implements ChunkManager{ if($item->isNull() || !$item->canBePlaced()){ return false; } - $hand = $item->getBlock($face); - $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z); - if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){ - $blockReplace = $blockClicked; - //TODO: while this mimics the vanilla behaviour with replaceable blocks, we should really pass some other - //value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know about - //the vanilla behaviour. - $face = Facing::UP; - $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z); - }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){ - return false; - } - - $tx = new BlockTransaction($this); - if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){ + //TODO: while passing Facing::UP mimics the vanilla behaviour with replaceable blocks, we should really pass + //some other value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know + //about the vanilla behaviour. + $tx = + $item->getPlacementTransaction($blockClicked, $blockClicked, Facing::UP, $clickVector, $player) ?? + $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player); + if($tx === null){ + //no placement options available return false; } @@ -2263,6 +2352,7 @@ class World implements ChunkManager{ if(!$tx->apply()){ return false; } + $first = true; foreach($tx->getBlocks() as [$x, $y, $z, $_]){ $tile = $this->getTileAt($x, $y, $z); if($tile !== null){ @@ -2270,11 +2360,12 @@ class World implements ChunkManager{ $tile->copyDataFromItem($item); } - $this->getBlockAt($x, $y, $z)->onPostPlace(); - } - - if($playSound){ - $this->addSound($hand->getPosition(), new BlockPlaceSound($hand)); + $placed = $this->getBlockAt($x, $y, $z); + $placed->onPostPlace(); + if($first && $playSound){ + $this->addSound($placed->getPosition(), new BlockPlaceSound($placed)); + } + $first = false; } $item->pop(); @@ -2538,6 +2629,16 @@ class World implements ChunkManager{ } public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + foreach($chunk->getSubChunks() as $subChunk){ + foreach($subChunk->getBlockLayers() as $blockLayer){ + foreach($blockLayer->getPalette() as $blockStateId){ + if(!$this->blockStateRegistry->hasStateId($blockStateId)){ + throw new \InvalidArgumentException("Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)"); + } + } + } + } + $chunkHash = World::chunkHash($chunkX, $chunkZ); $oldChunk = $this->loadChunk($chunkX, $chunkZ); if($oldChunk !== null && $oldChunk !== $chunk){ @@ -2549,7 +2650,7 @@ class World implements ChunkManager{ $localY = $tilePosition->getFloorY(); $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK; - $newBlock = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ)); + $newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ)); $expectedTileClass = $newBlock->getIdInfo()->getTileClass(); if( $expectedTileClass === null || //new block doesn't expect a tile @@ -2570,7 +2671,9 @@ class World implements ChunkManager{ } $this->chunks[$chunkHash] = $chunk; + unset($this->knownUngeneratedChunks[$chunkHash]); + $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []); unset($this->blockCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); @@ -2675,6 +2778,11 @@ class World implements ChunkManager{ throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId()); } } + if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof NeverSavedWithChunkEntity){ + //canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash + //later on. Better we just force all entities to have a save ID, even if it might not be needed. + throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory"); + } $pos = $entity->getPosition()->asVector3(); $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity; $this->entityLastKnownPositions[$entity->getId()] = $pos; @@ -2776,6 +2884,9 @@ class World implements ChunkManager{ if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){ throw new \InvalidArgumentException("Tile position is outside the world bounds"); } + if(!TileFactory::getInstance()->isRegistered($tile::class)){ + throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory"); + } $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE; $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE; @@ -2825,6 +2936,9 @@ class World implements ChunkManager{ if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){ return $this->chunks[$chunkHash]; } + if(isset($this->knownUngeneratedChunks[$chunkHash])){ + return null; + } $this->timings->syncChunkLoad->startTiming(); @@ -2844,6 +2958,7 @@ class World implements ChunkManager{ if($loadedChunkData === null){ $this->timings->syncChunkLoad->stopTiming(); + $this->knownUngeneratedChunks[$chunkHash] = true; return null; } @@ -2855,10 +2970,12 @@ class World implements ChunkManager{ $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity"); } $this->chunks[$chunkHash] = $chunk; + + $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []); unset($this->blockCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]); - $this->initChunk($x, $z, $chunkData); + $this->initChunk($x, $z, $chunkData, $chunk); if(ChunkLoadEvent::hasHandlers()){ (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call(); @@ -2878,12 +2995,14 @@ class World implements ChunkManager{ return $this->chunks[$chunkHash]; } - private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{ + private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, Chunk $chunk) : void{ $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ"); if(count($chunkData->getEntityNBT()) !== 0){ $this->timings->syncChunkLoadEntities->startTiming(); $entityFactory = EntityFactory::getInstance(); + + $deletedEntities = []; foreach($chunkData->getEntityNBT() as $k => $nbt){ try{ $entity = $entityFactory->createFromData($this, $nbt); @@ -2900,18 +3019,23 @@ class World implements ChunkManager{ }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format $saveId = "legacy(" . $saveIdTag->getValue() . ")"; } - $logger->warning("Deleted unknown entity type $saveId"); + $deletedEntities[$saveId] = ($deletedEntities[$saveId] ?? 0) + 1; } //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place //here, because entities currently add themselves to the world } + foreach(Utils::promoteKeys($deletedEntities) as $saveId => $count){ + $logger->warning("Deleted unknown entity type $saveId x$count"); + } $this->timings->syncChunkLoadEntities->stopTiming(); } if(count($chunkData->getTileNBT()) !== 0){ $this->timings->syncChunkLoadTileEntities->startTiming(); $tileFactory = TileFactory::getInstance(); + + $deletedTiles = []; foreach($chunkData->getTileNBT() as $k => $nbt){ try{ $tile = $tileFactory->createFromData($this, $nbt); @@ -2921,7 +3045,8 @@ class World implements ChunkManager{ continue; } if($tile === null){ - $logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", "")); + $saveId = $nbt->getString("id", ""); + $deletedTiles[$saveId] = ($deletedTiles[$saveId] ?? 0) + 1; continue; } @@ -2935,6 +3060,20 @@ class World implements ChunkManager{ }else{ $this->addTile($tile); } + $expectedStateId = $chunk->getBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK); + $actualStateId = $this->getBlock($tilePosition)->getStateId(); + if($expectedStateId !== $actualStateId){ + //state ID was updated by readStateFromWorld - typically because the block pulled some data from the tile + //make sure this is synced to the chunk + //TODO: in the future we should pull tile reading logic out of readStateFromWorld() and do it only + //when the tile is loaded - this would be cleaner and faster + $chunk->setBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK, $actualStateId); + $this->logger->debug("Tile " . $tile::class . " at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z updated block state ID from $expectedStateId to $actualStateId"); + } + } + + foreach(Utils::promoteKeys($deletedTiles) as $saveId => $count){ + $logger->warning("Deleted unknown tile entity type $saveId x$count"); } $this->timings->syncChunkLoadTileEntities->stopTiming(); @@ -2991,8 +3130,8 @@ class World implements ChunkManager{ $this->provider->saveChunk($x, $z, new ChunkData( $chunk->getSubChunks(), $chunk->isPopulated(), - array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk())), - array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()), + array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))), + array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())), ), $chunk->getTerrainDirtyFlags()); }finally{ $this->timings->syncChunkSave->stopTiming(); @@ -3014,6 +3153,7 @@ class World implements ChunkManager{ } unset($this->chunks[$chunkHash]); + $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []); unset($this->blockCache[$chunkHash]); unset($this->blockCollisionBoxCache[$chunkHash]); unset($this->changedBlocks[$chunkHash]); @@ -3054,6 +3194,7 @@ class World implements ChunkManager{ * @phpstan-return Promise */ public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{ + /** @phpstan-var PromiseResolver $resolver */ $resolver = new PromiseResolver(); $spawn ??= $this->getSpawnLocation(); /* @@ -3225,6 +3366,7 @@ class World implements ChunkManager{ private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{ $chunkHash = World::chunkHash($chunkX, $chunkZ); $this->addChunkHashToPopulationRequestQueue($chunkHash); + /** @phpstan-var PromiseResolver $resolver */ $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver(); if($associatedChunkLoader === null){ $temporaryLoader = new class implements ChunkLoader{}; @@ -3370,8 +3512,8 @@ class World implements ChunkManager{ $centerChunk = $this->loadChunk($chunkX, $chunkZ); $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ); - $task = new PopulationTask( - $this->worldId, + + $this->generatorExecutor->populate( $chunkX, $chunkZ, $centerChunk, @@ -3384,15 +3526,6 @@ class World implements ChunkManager{ $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader); } ); - $workerId = $this->workerPool->selectWorker(); - if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){ - $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline"); - unset($this->generatorRegisteredWorkers[$workerId]); - } - if(!isset($this->generatorRegisteredWorkers[$workerId])){ - $this->registerGeneratorToWorker($workerId); - } - $this->workerPool->submitTaskToWorker($task, $workerId); return $resolver->getPromise(); }finally{ diff --git a/src/world/biome/model/BiomeDefinitionEntryData.php b/src/world/biome/model/BiomeDefinitionEntryData.php new file mode 100644 index 000000000..bb63b36e1 --- /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 000000000..f70a77d15 --- /dev/null +++ b/src/world/biome/model/ColorData.php @@ -0,0 +1,41 @@ + + */ protected array $tiles = []; protected HeightArray $heightMap; @@ -210,6 +213,7 @@ class Chunk{ /** * @return Tile[] + * @phpstan-return array */ public function getTiles() : array{ return $this->tiles; @@ -237,6 +241,7 @@ class Chunk{ /** * @return int[] + * @phpstan-return non-empty-list */ public function getHeightMapArray() : array{ return $this->heightMap->getValues(); @@ -244,6 +249,7 @@ class Chunk{ /** * @param int[] $values + * @phpstan-param non-empty-list $values */ public function setHeightMapArray(array $values) : void{ $this->heightMap = new HeightArray($values); diff --git a/src/world/format/HeightArray.php b/src/world/format/HeightArray.php index 27f9cecb7..03094c3c8 100644 --- a/src/world/format/HeightArray.php +++ b/src/world/format/HeightArray.php @@ -36,7 +36,7 @@ final class HeightArray{ /** * @param int[] $values ZZZZXXXX key bit order - * @phpstan-param list $values + * @phpstan-param non-empty-list $values */ public function __construct(array $values){ if(count($values) !== 256){ @@ -66,7 +66,7 @@ final class HeightArray{ /** * @return int[] ZZZZXXXX key bit order - * @phpstan-return list + * @phpstan-return non-empty-list */ public function getValues() : array{ return $this->array->toArray(); diff --git a/src/world/format/SubChunk.php b/src/world/format/SubChunk.php index d8546e7e9..cc6673430 100644 --- a/src/world/format/SubChunk.php +++ b/src/world/format/SubChunk.php @@ -134,11 +134,8 @@ class SubChunk{ foreach($this->blockLayers as $layer){ $layer->collectGarbage(); - foreach($layer->getPalette() as $p){ - if($p !== $this->emptyBlockId){ - $cleanedLayers[] = $layer; - continue 2; - } + if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){ + $cleanedLayers[] = $layer; } } $this->blockLayers = $cleanedLayers; diff --git a/src/world/format/io/BaseWorldProvider.php b/src/world/format/io/BaseWorldProvider.php index 79f6875a4..6fcb8e10b 100644 --- a/src/world/format/io/BaseWorldProvider.php +++ b/src/world/format/io/BaseWorldProvider.php @@ -83,11 +83,11 @@ abstract class BaseWorldProvider implements WorldProvider{ } try{ - $newPalette[$k] = $this->blockStateDeserializer->deserialize($newStateData); + $newPalette[] = $this->blockStateDeserializer->deserialize($newStateData); }catch(BlockStateDeserializeException $e){ //this should never happen anyway - if the upgrader returned an invalid state, we have bigger problems $blockDecodeErrors[] = "Palette offset $k / Failed to deserialize upgraded state $id:$meta: " . $e->getMessage(); - $newPalette[$k] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData()); + $newPalette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData()); } } diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php index 6e18f27ac..a186ec07f 100644 --- a/src/world/format/io/FastChunkSerializer.php +++ b/src/world/format/io/FastChunkSerializer.php @@ -23,8 +23,10 @@ declare(strict_types=1); namespace pocketmine\world\format\io; -use pocketmine\utils\Binary; -use pocketmine\utils\BinaryStream; +use pmmp\encoding\BE; +use pmmp\encoding\Byte; +use pmmp\encoding\ByteBufferReader; +use pmmp\encoding\ByteBufferWriter; use pocketmine\world\format\Chunk; use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\SubChunk; @@ -45,15 +47,15 @@ final class FastChunkSerializer{ //NOOP } - private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array) : void{ + private static function serializePalettedArray(ByteBufferWriter $stream, PalettedBlockArray $array) : void{ $wordArray = $array->getWordArray(); $palette = $array->getPalette(); - $stream->putByte($array->getBitsPerBlock()); - $stream->put($wordArray); + Byte::writeUnsigned($stream, $array->getBitsPerBlock()); + $stream->writeByteArray($wordArray); $serialPalette = pack("L*", ...$palette); - $stream->putInt(strlen($serialPalette)); - $stream->put($serialPalette); + BE::writeUnsignedInt($stream, strlen($serialPalette)); + $stream->writeByteArray($serialPalette); } /** @@ -61,21 +63,20 @@ final class FastChunkSerializer{ * TODO: tiles and entities */ public static function serializeTerrain(Chunk $chunk) : string{ - $stream = new BinaryStream(); - $stream->putByte( - ($chunk->isPopulated() ? self::FLAG_POPULATED : 0) - ); + $stream = new ByteBufferWriter(); + Byte::writeUnsigned($stream, ($chunk->isPopulated() ? self::FLAG_POPULATED : 0)); //subchunks $subChunks = $chunk->getSubChunks(); $count = count($subChunks); - $stream->putByte($count); + Byte::writeUnsigned($stream, $count); foreach($subChunks as $y => $subChunk){ - $stream->putByte($y); - $stream->putInt($subChunk->getEmptyBlockId()); + Byte::writeSigned($stream, $y); + BE::writeUnsignedInt($stream, $subChunk->getEmptyBlockId()); + $layers = $subChunk->getBlockLayers(); - $stream->putByte(count($layers)); + Byte::writeUnsigned($stream, count($layers)); foreach($layers as $blocks){ self::serializePalettedArray($stream, $blocks); } @@ -83,14 +84,15 @@ final class FastChunkSerializer{ } - return $stream->getBuffer(); + return $stream->getData(); } - private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{ - $bitsPerBlock = $stream->getByte(); - $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + private static function deserializePalettedArray(ByteBufferReader $stream) : PalettedBlockArray{ + $bitsPerBlock = Byte::readUnsigned($stream); + $words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + $paletteSize = BE::readUnsignedInt($stream); /** @var int[] $unpackedPalette */ - $unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here + $unpackedPalette = unpack("L*", $stream->readByteArray($paletteSize)); //unpack() will never fail here $palette = array_values($unpackedPalette); return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); @@ -100,21 +102,21 @@ final class FastChunkSerializer{ * Deserializes a fast-serialized chunk */ public static function deserializeTerrain(string $data) : Chunk{ - $stream = new BinaryStream($data); + $stream = new ByteBufferReader($data); - $flags = $stream->getByte(); + $flags = Byte::readUnsigned($stream); $terrainPopulated = (bool) ($flags & self::FLAG_POPULATED); $subChunks = []; - $count = $stream->getByte(); + $count = Byte::readUnsigned($stream); for($subCount = 0; $subCount < $count; ++$subCount){ - $y = Binary::signByte($stream->getByte()); - $airBlockId = $stream->getInt(); + $y = Byte::readSigned($stream); + //TODO: why the heck are we using big-endian here? + $airBlockId = BE::readUnsignedInt($stream); - /** @var PalettedBlockArray[] $layers */ $layers = []; - for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ + for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){ $layers[] = self::deserializePalettedArray($stream); } $biomeArray = self::deserializePalettedArray($stream); diff --git a/src/world/format/io/FormatConverter.php b/src/world/format/io/FormatConverter.php index 1d485afa2..421d707fa 100644 --- a/src/world/format/io/FormatConverter.php +++ b/src/world/format/io/FormatConverter.php @@ -33,6 +33,7 @@ use function basename; use function crc32; use function file_exists; use function floor; +use function flush; use function microtime; use function mkdir; use function random_bytes; @@ -150,6 +151,10 @@ class FormatConverter{ $diff = $time - $thisRound; $thisRound = $time; $this->logger->info("Converted $counter / $count chunks (" . floor($this->chunksPerProgressUpdate / $diff) . " chunks/sec)"); + flush(); + } + if(($counter % (2 ** 16)) === 0){ + $new->doGarbageCollection(); } } $total = microtime(true) - $start; diff --git a/src/world/format/io/GlobalBlockStateHandlers.php b/src/world/format/io/GlobalBlockStateHandlers.php index c1d3934cf..731b15f74 100644 --- a/src/world/format/io/GlobalBlockStateHandlers.php +++ b/src/world/format/io/GlobalBlockStateHandlers.php @@ -26,7 +26,9 @@ namespace pocketmine\world\format\io; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\BlockTypeNames; use pocketmine\data\bedrock\block\convert\BlockObjectToStateSerializer; +use pocketmine\data\bedrock\block\convert\BlockSerializerDeserializerRegistrar; use pocketmine\data\bedrock\block\convert\BlockStateToObjectDeserializer; +use pocketmine\data\bedrock\block\convert\VanillaBlockMappings; use pocketmine\data\bedrock\block\upgrade\BlockDataUpgrader; use pocketmine\data\bedrock\block\upgrade\BlockIdMetaUpgrader; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgrader; @@ -44,20 +46,28 @@ use const pocketmine\BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH; * benefits for now. */ final class GlobalBlockStateHandlers{ - private static ?BlockObjectToStateSerializer $blockStateSerializer = null; - - private static ?BlockStateToObjectDeserializer $blockStateDeserializer = null; - private static ?BlockDataUpgrader $blockDataUpgrader = null; private static ?BlockStateData $unknownBlockStateData = null; + private static ?BlockSerializerDeserializerRegistrar $registrar = null; + + public static function getRegistrar() : BlockSerializerDeserializerRegistrar{ + if(self::$registrar === null){ + $deserializer = new BlockStateToObjectDeserializer(); + $serializer = new BlockObjectToStateSerializer(); + self::$registrar = new BlockSerializerDeserializerRegistrar($deserializer, $serializer); + VanillaBlockMappings::init(self::$registrar); + } + return self::$registrar; + } + public static function getDeserializer() : BlockStateToObjectDeserializer{ - return self::$blockStateDeserializer ??= new BlockStateToObjectDeserializer(); + return self::getRegistrar()->deserializer; } public static function getSerializer() : BlockObjectToStateSerializer{ - return self::$blockStateSerializer ??= new BlockObjectToStateSerializer(); + return self::getRegistrar()->serializer; } public static function getUpgrader() : BlockDataUpgrader{ diff --git a/src/world/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php index b2e079124..68eb6dc81 100644 --- a/src/world/format/io/data/BedrockWorldData.php +++ b/src/world/format/io/data/BedrockWorldData.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\world\format\io\data; +use pocketmine\data\bedrock\WorldDataVersions; use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\NbtDataException; use pocketmine\nbt\tag\CompoundTag; @@ -50,15 +51,9 @@ use function time; class BedrockWorldData extends BaseNbtWorldData{ - public const CURRENT_STORAGE_VERSION = 10; - public const CURRENT_STORAGE_NETWORK_VERSION = 748; - public const CURRENT_CLIENT_VERSION_TARGET = [ - 1, //major - 21, //minor - 40, //patch - 1, //revision - 0 //is beta - ]; + public const CURRENT_STORAGE_VERSION = WorldDataVersions::STORAGE; + public const CURRENT_STORAGE_NETWORK_VERSION = WorldDataVersions::NETWORK; + public const CURRENT_CLIENT_VERSION_TARGET = WorldDataVersions::LAST_OPENED_IN; public const GENERATOR_LIMITED = 0; public const GENERATOR_INFINITE = 1; diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php index dda489d31..3a64f93f6 100644 --- a/src/world/format/io/leveldb/LevelDB.php +++ b/src/world/format/io/leveldb/LevelDB.php @@ -27,6 +27,7 @@ use pocketmine\block\Block; use pocketmine\data\bedrock\BiomeIds; use pocketmine\data\bedrock\block\BlockStateDeserializeException; use pocketmine\data\bedrock\block\convert\UnsupportedBlockStateException; +use pocketmine\data\bedrock\WorldDataVersions; use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\NBT; use pocketmine\nbt\NbtDataException; @@ -35,6 +36,7 @@ use pocketmine\nbt\TreeRoot; use pocketmine\utils\Binary; use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryStream; +use pocketmine\utils\Utils; use pocketmine\VersionInfo; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\BaseWorldProvider; @@ -78,8 +80,8 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ protected const ENTRY_FLAT_WORLD_LAYERS = "game_flatworldlayers"; - protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_21_40; - protected const CURRENT_LEVEL_SUBCHUNK_VERSION = SubChunkVersion::PALETTED_MULTI; + protected const CURRENT_LEVEL_CHUNK_VERSION = WorldDataVersions::CHUNK; + protected const CURRENT_LEVEL_SUBCHUNK_VERSION = WorldDataVersions::SUBCHUNK; private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4; @@ -203,23 +205,29 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt); }catch(BlockStateDeserializeException $e){ //while not ideal, this is not a fatal error - $blockDecodeErrors[] = "Palette offset $i / Upgrade error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString(); + $errorMessage = "Upgrade error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString(); + $blockDecodeErrors[$errorMessage][] = $i; $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData()); continue; } try{ $palette[] = $this->blockStateDeserializer->deserialize($blockStateData); }catch(UnsupportedBlockStateException $e){ - $blockDecodeErrors[] = "Palette offset $i / " . $e->getMessage(); + $blockDecodeErrors[$e->getMessage()][] = $i; $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData()); }catch(BlockStateDeserializeException $e){ - $blockDecodeErrors[] = "Palette offset $i / Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString(); + $errorMessage = "Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString(); + $blockDecodeErrors[$errorMessage][] = $i; $palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData()); } } if(count($blockDecodeErrors) > 0){ - $logger->error("Errors decoding blocks:\n - " . implode("\n - ", $blockDecodeErrors)); + $finalErrors = []; + foreach(Utils::promoteKeys($blockDecodeErrors) as $errorMessage => $paletteOffsets){ + $finalErrors[] = "$errorMessage (palette offsets: " . implode(", ", $paletteOffsets) . ")"; + } + $logger->error("Errors decoding blocks:\n - " . implode("\n - ", $finalErrors)); } //TODO: exceptions @@ -711,7 +719,6 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $nbt = new LittleEndianNbtSerializer(); - /** @var CompoundTag[] $entities */ $entities = []; if(($entityData = $this->db->get($index . ChunkDataKey::ENTITIES)) !== false && $entityData !== ""){ try{ @@ -721,7 +728,6 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ } } - /** @var CompoundTag[] $tiles */ $tiles = []; if(($tileData = $this->db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !== false && $tileData !== ""){ try{ diff --git a/src/world/format/io/region/LegacyAnvilChunkTrait.php b/src/world/format/io/region/LegacyAnvilChunkTrait.php index 6e2f4c8f8..05c79ea63 100644 --- a/src/world/format/io/region/LegacyAnvilChunkTrait.php +++ b/src/world/format/io/region/LegacyAnvilChunkTrait.php @@ -87,12 +87,10 @@ trait LegacyAnvilChunkTrait{ } $subChunks = []; - $subChunksTag = $chunk->getListTag("Sections") ?? []; + $subChunksTag = $chunk->getListTag("Sections", CompoundTag::class) ?? []; foreach($subChunksTag as $subChunk){ - if($subChunk instanceof CompoundTag){ - $y = $subChunk->getByte("Y"); - $subChunks[$y] = $this->deserializeSubChunk($subChunk, clone $biomes3d, new \PrefixedLogger($logger, "Subchunk y=$y")); - } + $y = $subChunk->getByte("Y"); + $subChunks[$y] = $this->deserializeSubChunk($subChunk, clone $biomes3d, new \PrefixedLogger($logger, "Subchunk y=$y")); } for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(!isset($subChunks[$y])){ diff --git a/src/world/format/io/region/RegionWorldProvider.php b/src/world/format/io/region/RegionWorldProvider.php index 75fcfd083..9ce0a35de 100644 --- a/src/world/format/io/region/RegionWorldProvider.php +++ b/src/world/format/io/region/RegionWorldProvider.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace pocketmine\world\format\io\region; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\ByteArrayTag; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; @@ -33,10 +32,8 @@ use pocketmine\world\format\io\exception\CorruptedChunkException; use pocketmine\world\format\io\LoadedChunkData; use pocketmine\world\format\io\WorldData; use Symfony\Component\Filesystem\Path; -use function assert; use function file_exists; use function is_dir; -use function is_int; use function morton2d_encode; use function rename; use function scandir; @@ -60,7 +57,12 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ public static function isValid(string $path) : bool{ if(file_exists(Path::join($path, "level.dat")) && is_dir($regionPath = Path::join($path, "region"))){ - foreach(scandir($regionPath, SCANDIR_SORT_NONE) as $file){ + $files = scandir($regionPath, SCANDIR_SORT_NONE); + if($files === false){ + //we can't tell the type if we don't have read perms + return false; + } + foreach($files as $file){ $extPos = strrpos($file, "."); if($extPos !== false && substr($file, $extPos + 1) === static::getRegionFileExtension()){ //we don't care if other region types exist, we only care if this format is possible @@ -93,10 +95,12 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ } /** - * @param int $regionX reference parameter - * @param int $regionZ reference parameter + * @param int|null $regionX reference parameter + * @param int|null $regionZ reference parameter * @phpstan-param-out int $regionX * @phpstan-param-out int $regionZ + * + * TODO: make this private */ public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ) : void{ $regionX = $chunkX >> 5; @@ -154,24 +158,16 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ /** * @return CompoundTag[] + * @phpstan-return list + * * @throws CorruptedChunkException */ protected static function getCompoundList(string $context, ListTag $list) : array{ - if($list->count() === 0){ //empty lists might have wrong types, we don't care - return []; - } - if($list->getTagType() !== NBT::TAG_Compound){ + $compoundList = $list->cast(CompoundTag::class); + if($compoundList === null){ throw new CorruptedChunkException("Expected TAG_List for '$context'"); } - $result = []; - foreach($list as $tag){ - if(!($tag instanceof CompoundTag)){ - //this should never happen, but it's still possible due to lack of native type safety - throw new CorruptedChunkException("Expected TAG_List for '$context'"); - } - $result[] = $tag; - } - return $result; + return $compoundList->getValue(); } protected static function readFixedSizeByteArray(CompoundTag $chunk, string $tagName, int $length) : string{ @@ -195,7 +191,6 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ public function loadChunk(int $chunkX, int $chunkZ) : ?LoadedChunkData{ $regionX = $regionZ = null; self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ); - assert(is_int($regionX) && is_int($regionZ)); if(!file_exists($this->pathToRegion($regionX, $regionZ))){ return null; @@ -209,6 +204,9 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ return null; } + /** + * @phpstan-return \RegexIterator + */ private function createRegionIterator() : \RegexIterator{ return new \RegexIterator( new \FilesystemIterator( diff --git a/src/world/generator/FlatGeneratorOptions.php b/src/world/generator/FlatGeneratorOptions.php index ab10c8a47..8271ebcbf 100644 --- a/src/world/generator/FlatGeneratorOptions.php +++ b/src/world/generator/FlatGeneratorOptions.php @@ -26,11 +26,12 @@ namespace pocketmine\world\generator; use pocketmine\data\bedrock\BiomeIds; use pocketmine\item\LegacyStringToItemParser; use pocketmine\item\LegacyStringToItemParserException; +use pocketmine\world\World; use function array_map; -use function count; use function explode; use function preg_match; use function preg_match_all; +use const PHP_INT_MAX; /** * @internal @@ -71,12 +72,11 @@ final class FlatGeneratorOptions{ */ public static function parseLayers(string $layers) : array{ $result = []; - $split = array_map('\trim', explode(',', $layers)); + $split = array_map('\trim', explode(',', $layers, limit: World::Y_MAX - World::Y_MIN)); $y = 0; $itemParser = LegacyStringToItemParser::getInstance(); foreach($split as $line){ - preg_match('#^(?:(\d+)[x|*])?(.+)$#', $line, $matches); - if(count($matches) !== 3){ + if(preg_match('#^(?:(\d+)[x|*])?(.+)$#', $line, $matches) !== 1){ throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\""); } @@ -98,7 +98,7 @@ final class FlatGeneratorOptions{ * @throws InvalidGeneratorOptionsException */ public static function parsePreset(string $presetString) : self{ - $preset = explode(";", $presetString); + $preset = explode(";", $presetString, limit: 4); $blocks = $preset[1] ?? ""; $biomeId = (int) ($preset[2] ?? BiomeIds::PLAINS); $optionsString = $preset[3] ?? ""; @@ -111,15 +111,16 @@ final class FlatGeneratorOptions{ $params = true; if($matches[3][$i] !== ""){ $params = []; - $p = explode(" ", $matches[3][$i]); + $p = explode(" ", $matches[3][$i], limit: PHP_INT_MAX); foreach($p as $k){ - $k = explode("=", $k); + //TODO: this should be limited to 2 parts, but 3 preserves old behaviour when given e.g. treecount=20=1 + $k = explode("=", $k, limit: 3); if(isset($k[1])){ $params[$k[0]] = $k[1]; } } } - $options[(string) $option] = $params; + $options[$option] = $params; } return new self($structure, $biomeId, $options); } diff --git a/src/world/generator/GeneratorManager.php b/src/world/generator/GeneratorManager.php index 291ea91de..a1b00480e 100644 --- a/src/world/generator/GeneratorManager.php +++ b/src/world/generator/GeneratorManager.php @@ -50,7 +50,7 @@ final class GeneratorManager{ }catch(InvalidGeneratorOptionsException $e){ return $e; } - }); + }, fast: true); $this->addGenerator(Normal::class, "normal", fn() => null); $this->addAlias("normal", "default"); $this->addGenerator(Nether::class, "nether", fn() => null); @@ -62,6 +62,7 @@ final class GeneratorManager{ * @param string $name Alias for this generator type that can be written in configs * @param \Closure $presetValidator Callback to validate generator options for new worlds * @param bool $overwrite Whether to force overwriting any existing registered generator with the same name + * @param bool $fast Whether this generator is fast enough to run without async tasks * * @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator * @@ -69,7 +70,7 @@ final class GeneratorManager{ * * @throws \InvalidArgumentException */ - public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{ + public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{ Utils::testValidInstance($class, Generator::class); $name = strtolower($name); @@ -77,7 +78,7 @@ final class GeneratorManager{ throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); } - $this->list[$name] = new GeneratorManagerEntry($class, $presetValidator); + $this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast); } /** diff --git a/src/world/generator/GeneratorManagerEntry.php b/src/world/generator/GeneratorManagerEntry.php index 256ed27d5..942f6ee79 100644 --- a/src/world/generator/GeneratorManagerEntry.php +++ b/src/world/generator/GeneratorManagerEntry.php @@ -31,12 +31,15 @@ final class GeneratorManagerEntry{ */ public function __construct( private string $generatorClass, - private \Closure $presetValidator + private \Closure $presetValidator, + private readonly bool $fast ){} /** @phpstan-return class-string */ public function getGeneratorClass() : string{ return $this->generatorClass; } + public function isFast() : bool{ return $this->fast; } + /** * @throws InvalidGeneratorOptionsException */ diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php index bad134324..971349a5b 100644 --- a/src/world/generator/PopulationTask.php +++ b/src/world/generator/PopulationTask.php @@ -27,13 +27,18 @@ use pocketmine\scheduler\AsyncTask; use pocketmine\utils\AssumptionFailedError; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\FastChunkSerializer; -use pocketmine\world\SimpleChunkManager; -use pocketmine\world\World; +use pocketmine\world\generator\executor\ThreadLocalGeneratorContext; use function array_map; use function igbinary_serialize; use function igbinary_unserialize; /** + * @internal + * + * TODO: this should be moved to the executor namespace, but plugins have unfortunately used it directly due to the + * difficulty of regenerating chunks. This should be addressed in the future. + * For the remainder of PM5, we can't relocate this class. + * * @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array $adjacentChunks) : void */ class PopulationTask extends AsyncTask{ @@ -71,8 +76,6 @@ class PopulationTask extends AsyncTask{ 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; @@ -93,21 +96,15 @@ class PopulationTask extends AsyncTask{ $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(); + [$chunk, $chunks] = PopulationUtils::populateChunkWithAdjacents( + $context->getWorldMinY(), + $context->getWorldMaxY(), + $context->getGenerator(), + $this->chunkX, + $this->chunkZ, + $chunk, + $chunks + ); $this->chunk = FastChunkSerializer::serializeTerrain($chunk); @@ -118,18 +115,6 @@ class PopulationTask extends AsyncTask{ $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 diff --git a/src/world/generator/PopulationUtils.php b/src/world/generator/PopulationUtils.php new file mode 100644 index 000000000..84840ee3e --- /dev/null +++ b/src/world/generator/PopulationUtils.php @@ -0,0 +1,74 @@ +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; + } + + /** + * @param Chunk[]|null[] $adjacentChunks + * @phpstan-param array $adjacentChunks + * + * @return Chunk[]|Chunk[][] + * @phpstan-return array{Chunk, array} + */ + public static function populateChunkWithAdjacents(int $minY, int $maxY, Generator $generator, int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks) : array{ + $manager = new SimpleChunkManager($minY, $maxY); + self::setOrGenerateChunk($manager, $generator, $chunkX, $chunkZ, $centerChunk); + + $resultChunks = []; //this is just to keep phpstan's type inference happy + foreach($adjacentChunks as $relativeChunkHash => $c){ + World::getXZ($relativeChunkHash, $relativeX, $relativeZ); + $resultChunks[$relativeChunkHash] = self::setOrGenerateChunk($manager, $generator, $chunkX + $relativeX, $chunkZ + $relativeZ, $c); + } + $adjacentChunks = $resultChunks; + + $generator->populateChunk($manager, $chunkX, $chunkZ); + $centerChunk = $manager->getChunk($chunkX, $chunkZ); + if($centerChunk === null){ + throw new AssumptionFailedError("We just generated this chunk, so it must exist"); + } + $centerChunk->setPopulated(); + return [$centerChunk, $adjacentChunks]; + } +} diff --git a/src/world/generator/executor/AsyncGeneratorExecutor.php b/src/world/generator/executor/AsyncGeneratorExecutor.php new file mode 100644 index 000000000..007ffa5b2 --- /dev/null +++ b/src/world/generator/executor/AsyncGeneratorExecutor.php @@ -0,0 +1,106 @@ + + */ + private array $generatorRegisteredWorkers = []; + + public function __construct( + \Logger $logger, + private readonly AsyncPool $workerPool, + private readonly GeneratorExecutorSetupParameters $setupParameters, + ?int $asyncContextId = null + ){ + $this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor"); + + //TODO: we only allow setting this for PM5 because of PopulationTask uses in plugins + $this->asyncContextId = $asyncContextId ?? self::$nextAsyncContextId++; + + $this->workerStartHook = function(int $workerId) : void{ + if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){ + $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered"); + unset($this->generatorRegisteredWorkers[$workerId]); + } + }; + $this->workerPool->addWorkerStartHook($this->workerStartHook); + } + + private function registerGeneratorToWorker(int $worker) : void{ + $this->logger->debug("Registering generator on worker $worker"); + $this->workerPool->submitTaskToWorker(new AsyncGeneratorRegisterTask($this->setupParameters, $this->asyncContextId), $worker); + $this->generatorRegisteredWorkers[$worker] = true; + } + + private function unregisterGenerator() : void{ + foreach($this->workerPool->getRunningWorkers() as $i){ + if(isset($this->generatorRegisteredWorkers[$i])){ + $this->workerPool->submitTaskToWorker(new AsyncGeneratorUnregisterTask($this->asyncContextId), $i); + } + } + $this->generatorRegisteredWorkers = []; + } + + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{ + $task = new PopulationTask( + $this->asyncContextId, + $chunkX, + $chunkZ, + $centerChunk, + $adjacentChunks, + $onCompletion + ); + $workerId = $this->workerPool->selectWorker(); + if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){ + $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline"); + unset($this->generatorRegisteredWorkers[$workerId]); + } + if(!isset($this->generatorRegisteredWorkers[$workerId])){ + $this->registerGeneratorToWorker($workerId); + } + $this->workerPool->submitTaskToWorker($task, $workerId); + } + + public function shutdown() : void{ + $this->unregisterGenerator(); + $this->workerPool->removeWorkerStartHook($this->workerStartHook); + } +} diff --git a/src/world/generator/GeneratorRegisterTask.php b/src/world/generator/executor/AsyncGeneratorRegisterTask.php similarity index 54% rename from src/world/generator/GeneratorRegisterTask.php rename to src/world/generator/executor/AsyncGeneratorRegisterTask.php index e2e773a35..5bc67834d 100644 --- a/src/world/generator/GeneratorRegisterTask.php +++ b/src/world/generator/executor/AsyncGeneratorRegisterTask.php @@ -21,37 +21,20 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; use pocketmine\scheduler\AsyncTask; -use pocketmine\world\World; -class GeneratorRegisterTask extends AsyncTask{ - public int $seed; - public int $worldId; - public int $worldMinY; - public int $worldMaxY; +class AsyncGeneratorRegisterTask extends AsyncTask{ - /** - * @phpstan-param class-string $generatorClass - */ public function __construct( - World $world, - public string $generatorClass, - public string $generatorSettings - ){ - $this->seed = $world->getSeed(); - $this->worldId = $world->getId(); - $this->worldMinY = $world->getMinY(); - $this->worldMaxY = $world->getMaxY(); - } + private readonly GeneratorExecutorSetupParameters $setupParameters, + private readonly int $contextId + ){} public function onRun() : void{ - /** - * @var Generator $generator - * @see Generator::__construct() - */ - $generator = new $this->generatorClass($this->seed, $this->generatorSettings); - ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId); + $setupParameters = $this->setupParameters; + $generator = $setupParameters->createGenerator(); + ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $setupParameters->worldMinY, $setupParameters->worldMaxY), $this->contextId); } } diff --git a/src/world/generator/GeneratorUnregisterTask.php b/src/world/generator/executor/AsyncGeneratorUnregisterTask.php similarity index 74% rename from src/world/generator/GeneratorUnregisterTask.php rename to src/world/generator/executor/AsyncGeneratorUnregisterTask.php index 41b4cd808..c771903f5 100644 --- a/src/world/generator/GeneratorUnregisterTask.php +++ b/src/world/generator/executor/AsyncGeneratorUnregisterTask.php @@ -21,19 +21,16 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; use pocketmine\scheduler\AsyncTask; -use pocketmine\world\World; -class GeneratorUnregisterTask extends AsyncTask{ - public int $worldId; - - public function __construct(World $world){ - $this->worldId = $world->getId(); - } +class AsyncGeneratorUnregisterTask extends AsyncTask{ + public function __construct( + private readonly int $contextId + ){} public function onRun() : void{ - ThreadLocalGeneratorContext::unregister($this->worldId); + ThreadLocalGeneratorContext::unregister($this->contextId); } } diff --git a/src/world/generator/executor/GeneratorExecutor.php b/src/world/generator/executor/GeneratorExecutor.php new file mode 100644 index 000000000..d3f62d410 --- /dev/null +++ b/src/world/generator/executor/GeneratorExecutor.php @@ -0,0 +1,38 @@ + $adjacentChunks + * @phpstan-param \Closure(Chunk $centerChunk, array $adjacentChunks) : void $onCompletion + */ + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void; + + public function shutdown() : void; + +} diff --git a/src/world/generator/executor/GeneratorExecutorSetupParameters.php b/src/world/generator/executor/GeneratorExecutorSetupParameters.php new file mode 100644 index 000000000..b5fdb7bf9 --- /dev/null +++ b/src/world/generator/executor/GeneratorExecutorSetupParameters.php @@ -0,0 +1,50 @@ + $generatorClass + */ + public function __construct( + public readonly int $worldMinY, + public readonly int $worldMaxY, + public readonly int $generatorSeed, + public readonly string $generatorClass, + public readonly string $generatorSettings, + ){} + + public function createGenerator() : Generator{ + /** + * @var Generator $generator + * @see Generator::__construct() + */ + $generator = new $this->generatorClass($this->generatorSeed, $this->generatorSettings); + return $generator; + } +} diff --git a/src/world/generator/executor/SyncGeneratorExecutor.php b/src/world/generator/executor/SyncGeneratorExecutor.php new file mode 100644 index 000000000..79b5fdd00 --- /dev/null +++ b/src/world/generator/executor/SyncGeneratorExecutor.php @@ -0,0 +1,61 @@ +generator = $setupParameters->createGenerator(); + $this->worldMinY = $setupParameters->worldMinY; + $this->worldMaxY = $setupParameters->worldMaxY; + } + + public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{ + [$centerChunk, $adjacentChunks] = PopulationUtils::populateChunkWithAdjacents( + $this->worldMinY, + $this->worldMaxY, + $this->generator, + $chunkX, + $chunkZ, + $centerChunk, + $adjacentChunks + ); + + $onCompletion($centerChunk, $adjacentChunks); + } + + public function shutdown() : void{ + //NOOP + } +} diff --git a/src/world/generator/ThreadLocalGeneratorContext.php b/src/world/generator/executor/ThreadLocalGeneratorContext.php similarity index 94% rename from src/world/generator/ThreadLocalGeneratorContext.php rename to src/world/generator/executor/ThreadLocalGeneratorContext.php index bcf99882b..bea8bb032 100644 --- a/src/world/generator/ThreadLocalGeneratorContext.php +++ b/src/world/generator/executor/ThreadLocalGeneratorContext.php @@ -21,7 +21,9 @@ declare(strict_types=1); -namespace pocketmine\world\generator; +namespace pocketmine\world\generator\executor; + +use pocketmine\world\generator\Generator; /** * Manages thread-local caches for generators and the things needed to support them diff --git a/src/world/generator/normal/Normal.php b/src/world/generator/normal/Normal.php index 1d4805e16..a440f1e5f 100644 --- a/src/world/generator/normal/Normal.php +++ b/src/world/generator/normal/Normal.php @@ -126,10 +126,10 @@ class Normal extends Generator{ $hash = (int) $hash; $xNoise = $hash >> 20 & 3; $zNoise = $hash >> 22 & 3; - if($xNoise == 3){ + if($xNoise === 3){ $xNoise = 1; } - if($zNoise == 3){ + if($zNoise === 3){ $zNoise = 1; } diff --git a/src/world/light/LightPopulationTask.php b/src/world/light/LightPopulationTask.php index 5aa0ead65..29d957831 100644 --- a/src/world/light/LightPopulationTask.php +++ b/src/world/light/LightPopulationTask.php @@ -44,7 +44,7 @@ class LightPopulationTask extends AsyncTask{ private string $resultBlockLightArrays; /** - * @phpstan-param \Closure(array $blockLight, array $skyLight, array $heightMap) : void $onCompletion + * @phpstan-param \Closure(array $blockLight, array $skyLight, non-empty-list $heightMap) : void $onCompletion */ public function __construct(Chunk $chunk, \Closure $onCompletion){ $this->chunk = FastChunkSerializer::serializeTerrain($chunk); @@ -80,7 +80,10 @@ class LightPopulationTask extends AsyncTask{ } public function onCompletion() : void{ - /** @var int[] $heightMapArray */ + /** + * @var int[] $heightMapArray + * @phpstan-var non-empty-list $heightMapArray + */ $heightMapArray = igbinary_unserialize($this->resultHeightMap); /** @var LightArray[] $skyLightArrays */ @@ -90,7 +93,7 @@ class LightPopulationTask extends AsyncTask{ /** * @var \Closure - * @phpstan-var \Closure(array $blockLight, array $skyLight, array $heightMap>) : void + * @phpstan-var \Closure(array $blockLight, array $skyLight, non-empty-list $heightMap) : void */ $callback = $this->fetchLocal(self::TLS_KEY_COMPLETION_CALLBACK); $callback($blockLightArrays, $skyLightArrays, $heightMapArray); diff --git a/src/world/sound/EntityAttackNoDamageSound.php b/src/world/sound/EntityAttackNoDamageSound.php index 6804c668e..d46340a28 100644 --- a/src/world/sound/EntityAttackNoDamageSound.php +++ b/src/world/sound/EntityAttackNoDamageSound.php @@ -39,7 +39,8 @@ class EntityAttackNoDamageSound implements Sound{ -1, "minecraft:player", false, - false + false, + -1 )]; } } diff --git a/src/world/sound/EntityAttackSound.php b/src/world/sound/EntityAttackSound.php index bf1877444..28be1e62f 100644 --- a/src/world/sound/EntityAttackSound.php +++ b/src/world/sound/EntityAttackSound.php @@ -39,7 +39,8 @@ class EntityAttackSound implements Sound{ -1, "minecraft:player", false, - false + false, + -1 )]; } } diff --git a/src/world/sound/EntityLandSound.php b/src/world/sound/EntityLandSound.php index 998c5ed02..ef25395cf 100644 --- a/src/world/sound/EntityLandSound.php +++ b/src/world/sound/EntityLandSound.php @@ -46,7 +46,8 @@ class EntityLandSound implements Sound{ TypeConverter::getInstance()->getBlockTranslator()->internalIdToNetworkId($this->blockLandedOn->getStateId()), $this->entity::getNetworkTypeId(), false, //TODO: does isBaby have any relevance here? - false + false, + $this->entity->getId() )]; } } diff --git a/src/world/sound/EntityLongFallSound.php b/src/world/sound/EntityLongFallSound.php index e0dabe3a5..5e0186f8e 100644 --- a/src/world/sound/EntityLongFallSound.php +++ b/src/world/sound/EntityLongFallSound.php @@ -42,7 +42,8 @@ class EntityLongFallSound implements Sound{ -1, $this->entity::getNetworkTypeId(), false, //TODO: is isBaby relevant here? - false + false, + $this->entity->getId() )]; } } diff --git a/src/world/sound/EntityShortFallSound.php b/src/world/sound/EntityShortFallSound.php index 8955c3552..d230294d7 100644 --- a/src/world/sound/EntityShortFallSound.php +++ b/src/world/sound/EntityShortFallSound.php @@ -41,7 +41,8 @@ class EntityShortFallSound implements Sound{ -1, $this->entity::getNetworkTypeId(), false, //TODO: does isBaby have any relevance here? - false + false, + -1 )]; } } diff --git a/src/world/sound/FireworkCrackleSound.php b/src/world/sound/FireworkCrackleSound.php new file mode 100644 index 000000000..c0e897d70 --- /dev/null +++ b/src/world/sound/FireworkCrackleSound.php @@ -0,0 +1,35 @@ +volume * 16777215), ":", false, - false + false, + -1 )]; } } diff --git a/tests/phpstan/DummyPluginOwned.php b/tests/phpstan/DummyPluginOwned.php new file mode 100644 index 000000000..b63975dcf --- /dev/null +++ b/tests/phpstan/DummyPluginOwned.php @@ -0,0 +1,28 @@ +, array\\ given\\.$#" + message: '#^Parameter \#1 \$strings of function pocketmine\\build\\server_phar\\preg_quote_array expects array\, array\ given\.$#' + identifier: argument.type count: 1 path: ../../../build/server-phar.php - - message: "#^Do\\-while loop condition is always false\\.$#" + message: '#^Do\-while loop condition is always false\.$#' + identifier: doWhile.alwaysFalse count: 1 path: ../../../src/PocketMine.php - - message: "#^Parameter \\#1 \\$array of static method pocketmine\\\\plugin\\\\PluginGraylist\\:\\:fromArray\\(\\) expects array, mixed given\\.$#" + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 1 path: ../../../src/Server.php - - message: "#^Cannot cast mixed to int\\.$#" + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic + count: 1 + path: ../../../src/Server.php + + - + message: '#^Method pocketmine\\Server\:\:getCommandAliases\(\) should return array\\> but returns array\\>\.$#' + identifier: return.type + count: 1 + path: ../../../src/Server.php + + - + message: '#^Parameter \#1 \$generatorName of closure expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/Server.php + + - + message: '#^Cannot cast mixed to int\.$#' + identifier: cast.int count: 2 path: ../../../src/ServerConfigGroup.php - - message: "#^Cannot cast mixed to string\\.$#" + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 2 path: ../../../src/ServerConfigGroup.php - - message: "#^Cannot access offset 'git' on mixed\\.$#" + message: '#^Cannot access offset ''git'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 path: ../../../src/VersionInfo.php - - message: "#^Method pocketmine\\\\VersionInfo\\:\\:GIT_HASH\\(\\) should return string but returns mixed\\.$#" + message: '#^Static property pocketmine\\VersionInfo\:\:\$gitHash \(string\|null\) does not accept mixed\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/VersionInfo.php - - message: "#^Static property pocketmine\\\\VersionInfo\\:\\:\\$gitHash \\(string\\|null\\) does not accept mixed\\.$#" + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic count: 1 - path: ../../../src/VersionInfo.php + path: ../../../src/block/Block.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Block.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Block.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:setBlockStateId\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\format\\Chunk\:\:setBlockStateId\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Block.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Block.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Cactus.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/block/ChorusFlower.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/block/ChorusFlower.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/block/ChorusFlower.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getRealBlockSkyLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DaylightSensor.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getRealBlockSkyLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DaylightSensor.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getRealBlockSkyLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DaylightSensor.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#1 \\$xDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$xDiff of class pocketmine\\world\\particle\\DragonEggTeleportParticle constructor expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int\\<\\-64, 319\\> given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int\<\-64, 319\> given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#2 \\$yDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$yDiff of class pocketmine\\world\\particle\\DragonEggTeleportParticle constructor expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#3 \\$zDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$zDiff of class pocketmine\\world\\particle\\DragonEggTeleportParticle constructor expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/DragonEgg.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Fire.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getHighestAdjacentFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getHighestAdjacentFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getHighestAdjacentFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/FrostedIce.php - - message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$min of function mt_rand expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$max of function mt_rand expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Grass.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getHighestAdjacentBlockLight\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Ice.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getHighestAdjacentBlockLight\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Ice.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getHighestAdjacentBlockLight\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Ice.php - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Leaves.php - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Leaves.php - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Leaves.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Liquid.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Liquid.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Liquid.php - - message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$min of function mt_rand expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Mycelium.php - - message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$max of function mt_rand expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/Mycelium.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/RedMushroom.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/RedMushroom.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getFullLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/RedMushroom.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/SnowLayer.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/SnowLayer.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/SnowLayer.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/Sugarcane.php - - message: "#^Parameter \\#1 \\$x of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#" + message: '#^Parameter \#1 \$x of class pocketmine\\math\\Vector3 constructor expects float\|int, int\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, int\\|null given\\.$#" + message: '#^Parameter \#2 \$value of method pocketmine\\nbt\\tag\\CompoundTag\:\:setInt\(\) expects int, int\|null given\.$#' + identifier: argument.type count: 4 path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#3 \\$z of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#" + message: '#^Parameter \#3 \$z of class pocketmine\\math\\Vector3 constructor expects float\|int, int\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/tile/Chest.php - - message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairX \\(int\\|null\\) does not accept float\\|int\\.$#" + message: '#^Property pocketmine\\block\\tile\\Chest\:\:\$pairX \(int\|null\) does not accept float\|int\.$#' + identifier: assign.propertyType count: 2 path: ../../../src/block/tile/Chest.php - - message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairZ \\(int\\|null\\) does not accept float\\|int\\.$#" + message: '#^Property pocketmine\\block\\tile\\Chest\:\:\$pairZ \(int\|null\) does not accept float\|int\.$#' + identifier: assign.propertyType count: 2 path: ../../../src/block/tile/Chest.php - - message: "#^Constant pocketmine\\\\block\\\\tile\\\\MobHead\\:\\:TAG_MOUTH_MOVING is unused\\.$#" + message: '#^Constant pocketmine\\block\\tile\\MobHead\:\:TAG_MOUTH_MOVING is unused\.$#' + identifier: classConstant.unused count: 1 path: ../../../src/block/tile/MobHead.php - - message: "#^Constant pocketmine\\\\block\\\\tile\\\\MobHead\\:\\:TAG_MOUTH_TICK_COUNT is unused\\.$#" + message: '#^Constant pocketmine\\block\\tile\\MobHead\:\:TAG_MOUTH_TICK_COUNT is unused\.$#' + identifier: classConstant.unused count: 1 path: ../../../src/block/tile/MobHead.php - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$value of method pocketmine\\nbt\\tag\\CompoundTag\:\:setInt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 3 path: ../../../src/block/tile/Spawnable.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getPotentialLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic + count: 1 + path: ../../../src/block/tile/TileFactory.php + + - + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getPotentialLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/utils/CropGrowthHelper.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getPotentialLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getPotentialLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/utils/CropGrowthHelper.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getPotentialLightAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getPotentialLightAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/block/utils/CropGrowthHelper.php - - message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#" + message: '#^Cannot call method addParticle\(\) on pocketmine\\world\\World\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/command/defaults/ParticleCommand.php - - message: "#^Cannot call method getSeed\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#" + message: '#^Cannot call method getSeed\(\) on pocketmine\\world\\World\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/command/defaults/SeedCommand.php - - message: "#^Cannot call method setSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#" + message: '#^Cannot call method setSpawnLocation\(\) on pocketmine\\world\\World\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/command/defaults/SetWorldSpawnCommand.php - - message: "#^Cannot call method getTime\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#" + message: '#^Cannot call method getTime\(\) on pocketmine\\world\\World\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/command/defaults/TimeCommand.php - - message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#" - count: 2 - path: ../../../src/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function fseek expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function fwrite expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function stream_get_contents expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" + message: '#^Parameter \#1 \$path of static method pocketmine\\utils\\Filesystem\:\:cleanPath\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/crash/CrashDump.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Binary operation "\." between ''Error\: '' and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/crash/CrashDumpRenderer.php + + - + message: '#^Binary operation "\." between ''File\: '' and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/crash/CrashDumpRenderer.php + + - + message: '#^Binary operation "\." between ''Line\: '' and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/crash/CrashDumpRenderer.php + + - + message: '#^Binary operation "\." between ''Type\: '' and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/crash/CrashDumpRenderer.php + + - + message: '#^Parameter \#1 \$faces of method pocketmine\\block\\Vine\:\:setFaces\(\) expects list\<2\|3\|4\|5\>, array\ given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/data/bedrock/block/convert/VanillaBlockMappings.php + + - + message: '#^Parameter \#1 \$blockToItemId of class pocketmine\\data\\bedrock\\item\\BlockItemIdMap constructor expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/data/bedrock/item/BlockItemIdMap.php + + - + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/Living.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/Living.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/Living.php - - message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$x of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/FallingBlock.php - - message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$y of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/FallingBlock.php - - message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#4 \$z of method pocketmine\\block\\Block\:\:position\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/FallingBlock.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/Painting.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/Painting.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/object/Painting.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/projectile/Projectile.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/projectile/Projectile.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/entity/projectile/Projectile.php - - message: "#^Parameter \\#2 \\$recipe of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects pocketmine\\\\crafting\\\\CraftingRecipe, pocketmine\\\\crafting\\\\CraftingRecipe\\|null given\\.$#" + message: '#^Parameter \#2 \$recipe of class pocketmine\\event\\inventory\\CraftItemEvent constructor expects pocketmine\\crafting\\CraftingRecipe, pocketmine\\crafting\\CraftingRecipe\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/inventory/transaction/CraftingTransaction.php - - message: "#^Parameter \\#3 \\$repetitions of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects int, int\\|null given\\.$#" + message: '#^Parameter \#3 \$repetitions of class pocketmine\\event\\inventory\\CraftItemEvent constructor expects int, int\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/inventory/transaction/CraftingTransaction.php - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 - path: ../../../src/item/Item.php + message: '#^Parameter &\$haveItems @param\-out type of method pocketmine\\inventory\\transaction\\InventoryTransaction\:\:matchItems\(\) expects list\, array\, pocketmine\\item\\Item\> given\.$#' + identifier: paramOut.type + count: 1 + path: ../../../src/inventory/transaction/InventoryTransaction.php - - message: "#^Parameter \\#1 \\$buffer of method pocketmine\\\\nbt\\\\BaseNbtSerializer\\:\\:read\\(\\) expects string, mixed given\\.$#" + message: '#^Parameter &\$needItems @param\-out type of method pocketmine\\inventory\\transaction\\InventoryTransaction\:\:matchItems\(\) expects list\, array\, pocketmine\\item\\Item\> given\.$#' + identifier: paramOut.type + count: 1 + path: ../../../src/inventory/transaction/InventoryTransaction.php + + - + message: '#^Parameter \#1 \$buffer of method pocketmine\\nbt\\BaseNbtSerializer\:\:read\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/item/Item.php - - message: "#^Parameter \\#1 \\$string of function base64_decode expects string, mixed given\\.$#" + message: '#^Parameter \#1 \$string of function base64_decode expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/item/Item.php - - message: "#^Parameter \\#1 \\$string of function hex2bin expects string, mixed given\\.$#" + message: '#^Parameter \#1 \$string of function hex2bin expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/item/Item.php - - message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#" + message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/ChunkRequestTask.php - - message: "#^Cannot call method doFirstSpawn\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method doFirstSpawn\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getAttributeMap\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method getAttributeMap\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getLanguage\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method getLanguage\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 4 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getLocation\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method getLocation\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getUsedChunkStatus\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method getUsedChunkStatus\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getUsername\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#" + message: '#^Cannot call method getUsername\(\) on pocketmine\\player\\PlayerInfo\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method getUuid\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#" + message: '#^Cannot call method getUuid\(\) on pocketmine\\player\\PlayerInfo\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method sendData\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method sendData\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method setNoClientPredictions\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#" + message: '#^Cannot call method setNoClientPredictions\(\) on pocketmine\\player\\Player\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Cannot call method syncAll\\(\\) on pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null\\.$#" + message: '#^Cannot call method syncAll\(\) on pocketmine\\network\\mcpe\\InventoryManager\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$clientPub of class pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask constructor expects string, string\\|null given\\.$#" + message: '#^Parameter \#1 \$clientPub of class pocketmine\\network\\mcpe\\encryption\\PrepareEncryptionTask constructor expects string, string\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$for of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:syncAbilities\\(\\) expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#1 \$for of method pocketmine\\network\\mcpe\\NetworkSession\:\:syncAbilities\(\) expects pocketmine\\player\\Player, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\DeathPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#1 \$player of class pocketmine\\network\\mcpe\\handler\\DeathPacketHandler constructor expects pocketmine\\player\\Player, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#1 \$player of class pocketmine\\network\\mcpe\\handler\\InGamePacketHandler constructor expects pocketmine\\player\\Player, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$playerInfo of class pocketmine\\\\event\\\\player\\\\PlayerResourcePackOfferEvent constructor expects pocketmine\\\\player\\\\PlayerInfo, pocketmine\\\\player\\\\PlayerInfo\\|null given\\.$#" + message: '#^Parameter \#1 \$playerInfo of class pocketmine\\event\\player\\PlayerResourcePackOfferEvent constructor expects pocketmine\\player\\PlayerInfo, pocketmine\\player\\PlayerInfo\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#1 \\$target of method pocketmine\\\\command\\\\Command\\:\\:testPermissionSilent\\(\\) expects pocketmine\\\\command\\\\CommandSender, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#1 \$target of method pocketmine\\command\\Command\:\:testPermissionSilent\(\) expects pocketmine\\command\\CommandSender, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#2 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\EntityEventBroadcaster\\:\\:onEntityEffectAdded\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#2 \$entity of method pocketmine\\network\\mcpe\\EntityEventBroadcaster\:\:onEntityEffectAdded\(\) expects pocketmine\\entity\\Living, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#2 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\EntityEventBroadcaster\\:\\:onEntityEffectRemoved\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#2 \$entity of method pocketmine\\network\\mcpe\\EntityEventBroadcaster\:\:onEntityEffectRemoved\(\) expects pocketmine\\entity\\Living, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#2 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\EntityEventBroadcaster\\:\\:syncAttributes\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#2 \$entity of method pocketmine\\network\\mcpe\\EntityEventBroadcaster\:\:syncAttributes\(\) expects pocketmine\\entity\\Living, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#2 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#" + message: '#^Parameter \#2 \$player of class pocketmine\\network\\mcpe\\handler\\PreSpawnPacketHandler constructor expects pocketmine\\player\\Player, pocketmine\\player\\Player\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#2 \\$playerInfo of method pocketmine\\\\Server\\:\\:createPlayer\\(\\) expects pocketmine\\\\player\\\\PlayerInfo, pocketmine\\\\player\\\\PlayerInfo\\|null given\\.$#" + message: '#^Parameter \#2 \$playerInfo of method pocketmine\\Server\:\:createPlayer\(\) expects pocketmine\\player\\PlayerInfo, pocketmine\\player\\PlayerInfo\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#3 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#" + message: '#^Parameter \#3 \$inventoryManager of class pocketmine\\network\\mcpe\\handler\\InGamePacketHandler constructor expects pocketmine\\network\\mcpe\\InventoryManager, pocketmine\\network\\mcpe\\InventoryManager\|null given\.$#' + identifier: argument.type count: 2 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Parameter \\#4 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#" + message: '#^Parameter \#4 \$inventoryManager of class pocketmine\\network\\mcpe\\handler\\PreSpawnPacketHandler constructor expects pocketmine\\network\\mcpe\\InventoryManager, pocketmine\\network\\mcpe\\InventoryManager\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/NetworkSession.php - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\auth\\\\ProcessLoginTask\\:\\:\\$chain \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: ../../../src/network/mcpe/auth/ProcessLoginTask.php - - - - message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#" + message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/network/mcpe/compression/CompressBatchTask.php - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask\\:\\:\\$serverPrivateKey \\(string\\) does not accept string\\|null\\.$#" + message: '#^Property pocketmine\\network\\mcpe\\encryption\\PrepareEncryptionTask\:\:\$serverPrivateKey \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/network/mcpe/encryption/PrepareEncryptionTask.php - - message: "#^Method pocketmine\\\\permission\\\\DefaultPermissions\\:\\:registerPermission\\(\\) should return pocketmine\\\\permission\\\\Permission but returns pocketmine\\\\permission\\\\Permission\\|null\\.$#" + message: '#^Method pocketmine\\permission\\DefaultPermissions\:\:registerPermission\(\) should return pocketmine\\permission\\Permission but returns pocketmine\\permission\\Permission\|null\.$#' + identifier: return.type count: 1 path: ../../../src/permission/DefaultPermissions.php - - message: "#^Parameter \\#1 \\$value of static method pocketmine\\\\permission\\\\PermissionParser\\:\\:defaultFromString\\(\\) expects bool\\|string, mixed given\\.$#" + message: '#^Parameter \#1 \$value of static method pocketmine\\permission\\PermissionParser\:\:defaultFromString\(\) expects bool\|string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/permission/PermissionParser.php - - message: "#^Parameter \\#2 \\$description of class pocketmine\\\\permission\\\\Permission constructor expects pocketmine\\\\lang\\\\Translatable\\|string\\|null, mixed given\\.$#" + message: '#^Parameter \#2 \$description of class pocketmine\\permission\\Permission constructor expects pocketmine\\lang\\Translatable\|string\|null, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/permission/PermissionParser.php - - message: "#^Cannot call method getSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#" + message: '#^Cannot call method getSpawnLocation\(\) on pocketmine\\world\\World\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/player/Player.php - - message: "#^Method pocketmine\\\\player\\\\Player\\:\\:getSpawn\\(\\) should return pocketmine\\\\world\\\\Position but returns pocketmine\\\\world\\\\Position\\|null\\.$#" + message: '#^Method pocketmine\\player\\Player\:\:getSpawn\(\) should return pocketmine\\world\\Position but returns pocketmine\\world\\Position\|null\.$#' + identifier: return.type count: 1 path: ../../../src/player/Player.php - - message: "#^Method pocketmine\\\\plugin\\\\PluginBase\\:\\:getConfig\\(\\) should return pocketmine\\\\utils\\\\Config but returns pocketmine\\\\utils\\\\Config\\|null\\.$#" + message: '#^Method pocketmine\\plugin\\PluginBase\:\:getConfig\(\) should return pocketmine\\utils\\Config but returns pocketmine\\utils\\Config\|null\.$#' + identifier: return.type count: 1 path: ../../../src/plugin/PluginBase.php - - message: "#^Cannot cast mixed to string\\.$#" + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 1 path: ../../../src/plugin/PluginDescription.php - - message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#" + message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$authors \(array\\) does not accept list\\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/plugin/PluginDescription.php - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#" + message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$main \(string\) does not accept mixed\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/plugin/PluginDescription.php - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, mixed given\\.$#" + message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$name \(string\) does not accept mixed\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/plugin/PluginDescription.php - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$main \\(string\\) does not accept mixed\\.$#" + message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$srcNamespacePrefix \(string\) does not accept mixed\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/plugin/PluginDescription.php - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$name \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/plugin/PluginDescription.php - - - - message: "#^Cannot call method addChild\\(\\) on pocketmine\\\\permission\\\\Permission\\|null\\.$#" + message: '#^Cannot call method addChild\(\) on pocketmine\\permission\\Permission\|null\.$#' + identifier: method.nonObject count: 4 path: ../../../src/plugin/PluginManager.php - - message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackSize\\(\\) should return int but returns int\\<0, max\\>\\|false\\.$#" + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic + count: 1 + path: ../../../src/plugin/PluginManager.php + + - + message: '#^Method pocketmine\\resourcepacks\\ZippedResourcePack\:\:getPackSize\(\) should return int but returns int\<0, max\>\|false\.$#' + identifier: return.type count: 1 path: ../../../src/resourcepacks/ZippedResourcePack.php - - message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getSha256\\(\\) should return string but returns string\\|false\\.$#" + message: '#^Property pocketmine\\resourcepacks\\ZippedResourcePack\:\:\$fileResource \(resource\) does not accept resource\|false\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/resourcepacks/ZippedResourcePack.php - - message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$fileResource \\(resource\\) does not accept resource\\|false\\.$#" + message: '#^Property pocketmine\\resourcepacks\\ZippedResourcePack\:\:\$sha256 \(string\|null\) does not accept string\|false\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/resourcepacks/ZippedResourcePack.php - - message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$sha256 \\(string\\|null\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/resourcepacks/ZippedResourcePack.php - - - - message: "#^Property pocketmine\\\\scheduler\\\\BulkCurlTask\\:\\:\\$operations \\(string\\) does not accept string\\|null\\.$#" + message: '#^Property pocketmine\\scheduler\\BulkCurlTask\:\:\$operations \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/scheduler/BulkCurlTask.php - - message: "#^Cannot call method getNextRun\\(\\) on array\\\\>\\|int\\|pocketmine\\\\scheduler\\\\TaskHandler\\\\.$#" + message: '#^Cannot call method getNextRun\(\) on array\\>\|int\|pocketmine\\scheduler\\TaskHandler\\.$#' + identifier: method.nonObject count: 1 path: ../../../src/scheduler/TaskScheduler.php - - message: "#^Cannot access offset string on mixed\\.$#" + message: '#^Possibly invalid array key type mixed\.$#' + identifier: offsetAccess.invalidOffset + count: 1 + path: ../../../src/thread/ThreadManager.php + + - + message: '#^Cannot access offset string on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 2 path: ../../../src/utils/Config.php - - message: "#^Method pocketmine\\\\utils\\\\Config\\:\\:fixYAMLIndexes\\(\\) should return string but returns string\\|null\\.$#" + message: '#^Method pocketmine\\utils\\Config\:\:fixYAMLIndexes\(\) should return string but returns string\|null\.$#' + identifier: return.type count: 1 path: ../../../src/utils/Config.php - - message: "#^Parameter \\#1 \\$config of static method pocketmine\\\\utils\\\\Config\\:\\:writeProperties\\(\\) expects array\\, array\\ given\\.$#" + message: '#^Parameter \#1 \$config of static method pocketmine\\utils\\Config\:\:writeProperties\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 path: ../../../src/utils/Config.php - - message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#" + message: '#^Parameter \#1 \$string of function trim expects string, string\|false given\.$#' + identifier: argument.type count: 1 path: ../../../src/utils/Timezone.php - - message: "#^Cannot cast mixed to string\\.$#" + message: '#^Binary operation "\." between mixed and ''\-\>''\|''\:\:'' results in an error\.$#' + identifier: binaryOp.invalid count: 1 path: ../../../src/utils/Utils.php - - message: "#^Method pocketmine\\\\utils\\\\Utils\\:\\:printable\\(\\) should return string but returns string\\|null\\.$#" + message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: ../../../src/utils/Utils.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 1 path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" + message: '#^Method pocketmine\\utils\\Utils\:\:printable\(\) should return string but returns string\|null\.$#' + identifier: return.type count: 1 path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#2 \\$file of class pocketmine\\\\thread\\\\ThreadCrashInfoFrame constructor expects string\\|null, mixed given\\.$#" + message: '#^Parameter \#1 \$path of static method pocketmine\\utils\\Filesystem\:\:cleanPath\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#3 \\$line of class pocketmine\\\\thread\\\\ThreadCrashInfoFrame constructor expects int, mixed given\\.$#" + message: '#^Parameter \#2 \$file of class pocketmine\\thread\\ThreadCrashInfoFrame constructor expects string\|null, mixed given\.$#' + identifier: argument.type count: 1 path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$line of class pocketmine\\thread\\ThreadCrashInfoFrame constructor expects int, mixed given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/utils/Utils.php + + - + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:setBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:setBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:setBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/Explosion.php - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" + message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable count: 1 path: ../../../src/world/World.php - - message: "#^Cannot access offset 'data' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#" + message: '#^Cannot access offset ''data'' on array\{priority\: int, data\: pocketmine\\math\\Vector3\}\|int\|pocketmine\\math\\Vector3\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 path: ../../../src/world/World.php - - message: "#^Cannot access offset 'priority' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#" + message: '#^Cannot access offset ''priority'' on array\{priority\: int, data\: pocketmine\\math\\Vector3\}\|int\|pocketmine\\math\\Vector3\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 path: ../../../src/world/World.php - - message: "#^Cannot cast mixed to string\\.$#" + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/world/World.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/world/World.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:getTileAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/World.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of method pocketmine\\world\\World\:\:isInWorld\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of static method pocketmine\\world\\World\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/World.php - - message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/world/World.php - - - - message: "#^Method pocketmine\\\\world\\\\biome\\\\BiomeRegistry\\:\\:getBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#" + message: '#^Method pocketmine\\world\\biome\\BiomeRegistry\:\:getBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#' + identifier: return.type count: 1 path: ../../../src/world/biome/BiomeRegistry.php - - message: "#^Method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:getSubChunk\\(\\) should return pocketmine\\\\world\\\\format\\\\SubChunk but returns pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Method pocketmine\\world\\format\\Chunk\:\:getSubChunk\(\) should return pocketmine\\world\\format\\SubChunk but returns pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: return.type count: 1 path: ../../../src/world/format/Chunk.php - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$x of static method pocketmine\\world\\format\\Chunk\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/format/Chunk.php - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of static method pocketmine\\world\\format\\Chunk\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/format/Chunk.php - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#3 \$z of static method pocketmine\\world\\format\\Chunk\:\:blockHash\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/format/Chunk.php - - message: "#^Method pocketmine\\\\world\\\\format\\\\HeightArray\\:\\:get\\(\\) should return int but returns int\\|null\\.$#" + message: '#^Method pocketmine\\world\\format\\HeightArray\:\:get\(\) should return int but returns int\|null\.$#' + identifier: return.type count: 1 path: ../../../src/world/format/HeightArray.php - - message: "#^Only numeric types are allowed in %%, int\\<0, max\\>\\|false given on the left side\\.$#" + message: '#^Only numeric types are allowed in %%, int\<0, max\>\|false given on the left side\.$#' + identifier: mod.leftNonNumeric count: 1 path: ../../../src/world/format/io/region/RegionLoader.php - - message: "#^Parameter \\#2 \\$size of function ftruncate expects int\\<0, max\\>, int given\\.$#" + message: '#^Parameter \#2 \$size of function ftruncate expects int\<0, max\>, int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/format/io/region/RegionLoader.php - - message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/world/format/io/region/RegionWorldProvider.php - - - - message: "#^Cannot access offset 1 on mixed\\.$#" - count: 2 - path: ../../../src/world/format/io/region/RegionWorldProvider.php - - - - message: "#^Cannot access offset 2 on mixed\\.$#" - count: 2 - path: ../../../src/world/format/io/region/RegionWorldProvider.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 4 - path: ../../../src/world/format/io/region/RegionWorldProvider.php - - - - message: "#^Method pocketmine\\\\world\\\\generator\\\\biome\\\\BiomeSelector\\:\\:pickBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#" + message: '#^Method pocketmine\\world\\generator\\biome\\BiomeSelector\:\:pickBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#' + identifier: return.type count: 1 path: ../../../src/world/generator/biome/BiomeSelector.php - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Dynamic new is not allowed\.$#' + identifier: pocketmine.new.dynamic + count: 1 + path: ../../../src/world/generator/executor/GeneratorExecutorSetupParameters.php + + - + message: '#^Cannot call method getBiomeId\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/generator/hell/Nether.php - - message: "#^Offset int does not exist on SplFixedArray\\\\|null\\.$#" - count: 4 - path: ../../../src/world/generator/noise/Noise.php - - - - message: "#^Parameter \\$q0 of static method pocketmine\\\\world\\\\generator\\\\noise\\\\Noise\\:\\:linearLerp\\(\\) expects float, float\\|null given\\.$#" + message: '#^Parameter \$q0 of static method pocketmine\\world\\generator\\noise\\Noise\:\:linearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/generator/noise/Noise.php - - message: "#^Parameter \\$q1 of static method pocketmine\\\\world\\\\generator\\\\noise\\\\Noise\\:\\:linearLerp\\(\\) expects float, float\\|null given\\.$#" + message: '#^Parameter \$q1 of static method pocketmine\\world\\generator\\noise\\Noise\:\:linearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/generator/noise/Noise.php - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method getBiomeId\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/generator/normal/Normal.php - - message: "#^Parameter \\#1 \\$start of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#1 \$start of method pocketmine\\utils\\Random\:\:nextRange\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/generator/object/TallGrass.php - - message: "#^Parameter \\#2 \\$end of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$end of method pocketmine\\utils\\Random\:\:nextRange\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/generator/object/TallGrass.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\ChunkManager\:\:getBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 2 path: ../../../src/world/generator/object/TallGrass.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#" + message: '#^Parameter \#2 \$y of method pocketmine\\world\\ChunkManager\:\:setBlockAt\(\) expects int, float\|int given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/generator/object/TallGrass.php - - message: "#^Cannot call method getBlockLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Cannot call method getBlockLightArray\(\) on pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/BlockLightUpdate.php - - message: "#^Cannot call method getBlockStateId\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Cannot call method getBlockStateId\(\) on pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/BlockLightUpdate.php - - message: "#^Cannot call method getSubChunks\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method getSubChunks\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/BlockLightUpdate.php - - message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultBlockLightArrays \\(string\\) does not accept string\\|null\\.$#" + message: '#^Property pocketmine\\world\\light\\LightPopulationTask\:\:\$resultBlockLightArrays \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/world/light/LightPopulationTask.php - - message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultHeightMap \\(string\\) does not accept string\\|null\\.$#" + message: '#^Property pocketmine\\world\\light\\LightPopulationTask\:\:\$resultHeightMap \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/world/light/LightPopulationTask.php - - message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultSkyLightArrays \\(string\\) does not accept string\\|null\\.$#" + message: '#^Property pocketmine\\world\\light\\LightPopulationTask\:\:\$resultSkyLightArrays \(string\) does not accept string\|null\.$#' + identifier: assign.propertyType count: 1 path: ../../../src/world/light/LightPopulationTask.php - - message: "#^Cannot call method getBlockSkyLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Cannot call method getBlockSkyLightArray\(\) on pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method getBlockStateId\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Cannot call method getBlockStateId\(\) on pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method getHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method getHeightMap\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 6 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method getHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method getHeightMapArray\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method getSubChunk\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method getSubChunk\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method setHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method setHeightMap\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 2 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Cannot call method setHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" + message: '#^Cannot call method setHeightMapArray\(\) on pocketmine\\world\\format\\Chunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMap\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#" + message: '#^Parameter \#1 \$chunk of static method pocketmine\\world\\light\\SkyLightUpdate\:\:recalculateHeightMap\(\) expects pocketmine\\world\\format\\Chunk, pocketmine\\world\\format\\Chunk\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/light/SkyLightUpdate.php - - message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMapColumn\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#" + message: '#^Parameter \#1 \$chunk of static method pocketmine\\world\\light\\SkyLightUpdate\:\:recalculateHeightMapColumn\(\) expects pocketmine\\world\\format\\Chunk, pocketmine\\world\\format\\Chunk\|null given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/light/SkyLightUpdate.php - diff --git a/tests/phpstan/configs/impossible-generics.neon b/tests/phpstan/configs/impossible-generics.neon index b0e67d294..e0b944e69 100644 --- a/tests/phpstan/configs/impossible-generics.neon +++ b/tests/phpstan/configs/impossible-generics.neon @@ -1,12 +1,14 @@ parameters: ignoreErrors: - - message: "#^Method pocketmine\\\\event\\\\RegisteredListener\\:\\:__construct\\(\\) has parameter \\$handler with no signature specified for Closure\\.$#" + message: '#^Method pocketmine\\event\\RegisteredListener\:\:__construct\(\) has parameter \$handler with no signature specified for Closure\.$#' + identifier: missingType.callable count: 1 path: ../../../src/event/RegisteredListener.php - - message: "#^Method pocketmine\\\\event\\\\RegisteredListener\\:\\:getHandler\\(\\) return type has no signature specified for Closure\\.$#" + message: '#^Method pocketmine\\event\\RegisteredListener\:\:getHandler\(\) return type has no signature specified for Closure\.$#' + identifier: missingType.callable count: 1 path: ../../../src/event/RegisteredListener.php diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index ea5e1c62a..65064ab6c 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -1,122 +1,308 @@ parameters: ignoreErrors: - - message: "#^Method pocketmine\\\\block\\\\CakeWithCandle\\:\\:onInteractCandle\\(\\) has parameter \\$returnedItems with no value type specified in iterable type array\\.$#" + message: '#^Access to an undefined property object\:\:\$crashId\.$#' + identifier: property.notFound count: 1 - path: ../../../src/block/CakeWithCandle.php + path: ../../../src/Server.php - - message: "#^Method pocketmine\\\\block\\\\CopperDoor\\:\\:onInteractCopper\\(\\) has parameter \\$returnedItems with no value type specified in iterable type array\\.$#" + message: '#^Access to an undefined property object\:\:\$crashUrl\.$#' + identifier: property.notFound count: 1 - path: ../../../src/block/CopperDoor.php + path: ../../../src/Server.php - - message: "#^Method pocketmine\\\\block\\\\CopperTrapdoor\\:\\:onInteractCopper\\(\\) has parameter \\$returnedItems with no value type specified in iterable type array\\.$#" + message: '#^Access to an undefined property object\:\:\$error\.$#' + identifier: property.notFound count: 1 - path: ../../../src/block/CopperTrapdoor.php + path: ../../../src/Server.php - - message: "#^Method pocketmine\\\\block\\\\DoubleTallGrass\\:\\:traitGetDropsForIncompatibleTool\\(\\) return type has no value type specified in iterable type array\\.$#" + message: '#^Method pocketmine\\block\\DoubleTallGrass\:\:traitGetDropsForIncompatibleTool\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue count: 1 path: ../../../src/block/DoubleTallGrass.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:ACACIA_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:ACACIA_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:BIRCH_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:ACACIA_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:CHERRY_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:BIRCH_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:CRIMSON_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:BIRCH_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:DARK_OAK_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:CHERRY_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:JUNGLE_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:CHERRY_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:MANGROVE_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:CRIMSON_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:OAK_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:CRIMSON_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:SPRUCE_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:DARK_OAK_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:WARPED_SIGN\\(\\)\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:DARK_OAK_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 path: ../../../src/block/VanillaBlocks.php - - message: "#^Call to function assert\\(\\) with false and 'unknown hit type' will always evaluate to false\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:JUNGLE_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod count: 1 - path: ../../../src/entity/projectile/Projectile.php + path: ../../../src/block/VanillaBlocks.php - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\raklib\\\\PthreadsChannelWriter\\:\\:\\$buffer is never read, only written\\.$#" + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:JUNGLE_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:MANGROVE_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:MANGROVE_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:OAK_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:OAK_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:PALE_OAK_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:PALE_OAK_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:SPRUCE_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:SPRUCE_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:WARPED_HANGING_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Creating callable from a non\-native static method pocketmine\\item\\VanillaItems\:\:WARPED_SIGN\(\)\.$#' + identifier: callable.nonNativeMethod + count: 1 + path: ../../../src/block/VanillaBlocks.php + + - + message: '#^Strict comparison using \=\=\= between \*NEVER\* and 5 will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: ../../../src/command/defaults/TeleportCommand.php + + - + message: '#^Method pocketmine\\crafting\\ShapedRecipe\:\:getIngredientMap\(\) should return list\\> but returns array\, non\-empty\-array\, pocketmine\\crafting\\RecipeIngredient\|null\>\>\.$#' + identifier: return.type + count: 1 + path: ../../../src/crafting/ShapedRecipe.php + + - + message: '#^Offset ''name'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset + count: 1 + path: ../../../src/crafting/json/ItemStackData.php + + - + message: '#^Property pocketmine\\crash\\CrashDumpData\:\:\$parameters \(list\\) does not accept array\.$#' + identifier: assign.propertyType + count: 1 + path: ../../../src/crash/CrashDump.php + + - + message: '#^Property pocketmine\\item\\WritableBookBase\:\:\$pages \(list\\) does not accept non\-empty\-array\\.$#' + identifier: assign.propertyType + count: 1 + path: ../../../src/item/WritableBookBase.php + + - + message: '#^Parameter \#3 \$input of class pocketmine\\network\\mcpe\\protocol\\types\\recipe\\ShapedRecipe constructor expects list\\>, array\, non\-empty\-array\, pocketmine\\network\\mcpe\\protocol\\types\\recipe\\RecipeIngredient\>\> given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/network/mcpe/cache/CraftingDataCache.php + + - + message: '#^Parameter \#1 \$states of class pocketmine\\network\\mcpe\\convert\\BlockStateDictionary constructor expects list\, array\, pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\> given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/network/mcpe/convert/BlockStateDictionary.php + + - + message: '#^Cannot access offset ''default'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: ../../../src/network/mcpe/convert/LegacySkinAdapter.php + + - + message: '#^Property pocketmine\\network\\mcpe\\raklib\\PthreadsChannelWriter\:\:\$buffer is never read, only written\.$#' + identifier: property.onlyWritten count: 1 path: ../../../src/network/mcpe/raklib/PthreadsChannelWriter.php - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\raklib\\\\SnoozeAwarePthreadsChannelWriter\\:\\:\\$buffer is never read, only written\\.$#" + message: '#^Property pocketmine\\network\\mcpe\\raklib\\SnoozeAwarePthreadsChannelWriter\:\:\$buffer is never read, only written\.$#' + identifier: property.onlyWritten count: 1 path: ../../../src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php - - message: "#^Dead catch \\- RuntimeException is never thrown in the try block\\.$#" + message: '#^Dead catch \- RuntimeException is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 1 path: ../../../src/plugin/PluginManager.php - - message: "#^Method pocketmine\\\\timings\\\\TimingsHandler\\:\\:lazyGetSet\\(\\) should return pocketmine\\\\utils\\\\ObjectSet\\ but returns pocketmine\\\\utils\\\\ObjectSet\\\\.$#" + message: '#^Binary operation "\." between mixed and ''/''\|''\\\\'' results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/thread/ThreadSafeClassLoader.php + + - + message: '#^Binary operation "\." between mixed and non\-falsy\-string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/thread/ThreadSafeClassLoader.php + + - + message: '#^Method pocketmine\\timings\\TimingsHandler\:\:lazyGetSet\(\) should return pocketmine\\utils\\ObjectSet\ but returns pocketmine\\utils\\ObjectSet\\.$#' + identifier: return.type count: 1 path: ../../../src/timings/TimingsHandler.php - - message: "#^Casting to int something that's already int\\.$#" + message: '#^Parameter &\$where @param\-out type of method pocketmine\\timings\\TimingsHandler\:\:lazyGetSet\(\) expects pocketmine\\utils\\ObjectSet\, pocketmine\\utils\\ObjectSet\ given\.$#' + identifier: paramOut.type + count: 1 + path: ../../../src/timings/TimingsHandler.php + + - + message: '#^Binary operation "\*" between mixed and 3600 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/utils/Timezone.php + + - + message: '#^Binary operation "\*" between mixed and 60 results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/utils/Timezone.php + + - + message: '#^Binary operation "\+" between \(float\|int\) and mixed results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: ../../../src/utils/Timezone.php + + - + message: '#^Property pocketmine\\world\\format\\io\\region\\RegionLoader\:\:\$locationTable \(list\\) does not accept non\-empty\-array\\.$#' + identifier: assign.propertyType + count: 2 + path: ../../../src/world/format/io/region/RegionLoader.php + + - + message: '#^Property pocketmine\\world\\format\\io\\region\\RegionLoader\:\:\$locationTable \(list\\) does not accept non\-empty\-array\, pocketmine\\world\\format\\io\\region\\RegionLocationTableEntry\|null\>\.$#' + identifier: assign.propertyType + count: 3 + path: ../../../src/world/format/io/region/RegionLoader.php + + - + message: '#^Method pocketmine\\world\\format\\io\\region\\RegionWorldProvider\:\:createRegionIterator\(\) should return RegexIterator\ but returns RegexIterator\\>\.$#' + identifier: return.type + count: 1 + path: ../../../src/world/format/io/region/RegionWorldProvider.php + + - + message: '#^Casting to int something that''s already int\.$#' + identifier: cast.useless count: 1 path: ../../../src/world/generator/normal/Normal.php - - message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertFalse\\(\\) with false will always evaluate to true\\.$#" + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertFalse\(\) with false will always evaluate to true\.$#' + identifier: staticMethod.alreadyNarrowedType count: 1 path: ../../phpunit/promise/PromiseTest.php - - message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with false and 'All promise should…' will always evaluate to false\\.$#" + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with false and ''All promise should…'' will always evaluate to false\.$#' + identifier: staticMethod.impossibleType count: 1 path: ../../phpunit/promise/PromiseTest.php - - message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with false will always evaluate to false\\.$#" + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with false will always evaluate to false\.$#' + identifier: staticMethod.impossibleType count: 2 path: ../../phpunit/promise/PromiseTest.php - - - message: "#^Strict comparison using \\=\\=\\= between 0 and 0 will always evaluate to true\\.$#" - count: 1 - path: ../rules/UnsafeForeachArrayOfStringRule.php - diff --git a/tests/phpstan/configs/property-hook-sadness.neon b/tests/phpstan/configs/property-hook-sadness.neon new file mode 100644 index 000000000..b3901993a --- /dev/null +++ b/tests/phpstan/configs/property-hook-sadness.neon @@ -0,0 +1,61 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$enderInventory because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Human.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$hungerManager because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Human.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$inventory because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Human.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$offHandInventory because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Human.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Human\:\:\$xpManager because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Human.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Living\:\:\$armorInventory because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Living.php + + - + message: '#^Cannot unset property pocketmine\\entity\\Living\:\:\$effectManager because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/entity/Living.php + + - + message: '#^Cannot unset property pocketmine\\player\\Player\:\:\$craftingGrid because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/player/Player.php + + - + message: '#^Cannot unset property pocketmine\\player\\Player\:\:\$cursorInventory because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/player/Player.php + + - + message: '#^Cannot unset property pocketmine\\world\\format\\io\\leveldb\\LevelDB\:\:\$db because it might have hooks in a subclass\.$#' + identifier: unset.possiblyHookedProperty + count: 1 + path: ../../../src/world/format/io/leveldb/LevelDB.php diff --git a/tests/phpstan/configs/reflection-class-sadness.neon b/tests/phpstan/configs/reflection-class-sadness.neon new file mode 100644 index 000000000..477a97d41 --- /dev/null +++ b/tests/phpstan/configs/reflection-class-sadness.neon @@ -0,0 +1,31 @@ +parameters: + ignoreErrors: + - + message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\ in PHPDoc tag @param for parameter \$class is redundant, template type T of object of class ReflectionClass has the same variance\.$#' + identifier: generics.callSiteVarianceRedundant + count: 2 + path: ../../phpunit/event/HandlerListManagerTest.php + + - + message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\ in PHPDoc tag @param for parameter \$expect is redundant, template type T of object of class ReflectionClass has the same variance\.$#' + identifier: generics.callSiteVarianceRedundant + count: 1 + path: ../../phpunit/event/HandlerListManagerTest.php + + - + message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\ in PHPDoc tag @return is redundant, template type T of object of class ReflectionClass has the same variance\.$#' + identifier: generics.callSiteVarianceRedundant + count: 3 + path: ../../phpunit/event/HandlerListManagerTest.php + + - + message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\ in PHPDoc tag @var for property pocketmine\\event\\HandlerListManagerTest\:\:\$isValidFunc is redundant, template type T of object of class ReflectionClass has the same variance\.$#' + identifier: generics.callSiteVarianceRedundant + count: 1 + path: ../../phpunit/event/HandlerListManagerTest.php + + - + message: '#^Call\-site variance of covariant pocketmine\\event\\Event in generic type ReflectionClass\ in PHPDoc tag @var for property pocketmine\\event\\HandlerListManagerTest\:\:\$resolveParentFunc is redundant, template type T of object of class ReflectionClass has the same variance\.$#' + identifier: generics.callSiteVarianceRedundant + count: 2 + path: ../../phpunit/event/HandlerListManagerTest.php diff --git a/tests/phpstan/configs/spl-fixed-array-sucks.neon b/tests/phpstan/configs/spl-fixed-array-sucks.neon index daa6361dd..9c5ab915f 100644 --- a/tests/phpstan/configs/spl-fixed-array-sucks.neon +++ b/tests/phpstan/configs/spl-fixed-array-sucks.neon @@ -1,22 +1,55 @@ parameters: ignoreErrors: - - message: "#^Cannot call method collectGarbage\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" + message: '#^Cannot call method collectGarbage\(\) on pocketmine\\world\\format\\SubChunk\|null\.$#' + identifier: method.nonObject count: 1 path: ../../../src/world/format/Chunk.php - - message: "#^Method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:getSubChunks\\(\\) should return array\\ but returns array\\\\.$#" + message: '#^Method pocketmine\\world\\format\\Chunk\:\:getSubChunks\(\) should return array\ but returns array\\.$#' + identifier: return.type count: 1 path: ../../../src/world/format/Chunk.php - - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\)\\: mixed\\)\\|null, Closure\\(pocketmine\\\\world\\\\format\\\\SubChunk\\)\\: pocketmine\\\\world\\\\format\\\\SubChunk given\\.$#" + message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(pocketmine\\world\\format\\SubChunk\|null\)\: mixed\)\|null, Closure\(pocketmine\\world\\format\\SubChunk\)\: pocketmine\\world\\format\\SubChunk given\.$#' + identifier: argument.type count: 1 path: ../../../src/world/format/Chunk.php - - message: "#^Method pocketmine\\\\world\\\\format\\\\HeightArray\\:\\:getValues\\(\\) should return array\\ but returns array\\\\.$#" + message: '#^Method pocketmine\\world\\format\\HeightArray\:\:getValues\(\) should return non\-empty\-list\ but returns array\\.$#' + identifier: return.type count: 1 path: ../../../src/world/format/HeightArray.php + - + message: '#^Offset int might not exist on SplFixedArray\\|null\.$#' + identifier: offsetAccess.notFound + count: 4 + path: ../../../src/world/generator/noise/Noise.php + + - + message: '#^Parameter \$q00 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/world/generator/noise/Noise.php + + - + message: '#^Parameter \$q01 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/world/generator/noise/Noise.php + + - + message: '#^Parameter \$q10 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/world/generator/noise/Noise.php + + - + message: '#^Parameter \$q11 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#' + identifier: argument.type + count: 1 + path: ../../../src/world/generator/noise/Noise.php diff --git a/tests/phpstan/rules/DeprecatedLegacyEnumAccessRule.php b/tests/phpstan/rules/DeprecatedLegacyEnumAccessRule.php index 4fa767022..5753bb628 100644 --- a/tests/phpstan/rules/DeprecatedLegacyEnumAccessRule.php +++ b/tests/phpstan/rules/DeprecatedLegacyEnumAccessRule.php @@ -28,7 +28,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeWithClassName; use pocketmine\utils\LegacyEnumShimTrait; use function sprintf; @@ -51,18 +50,15 @@ final class DeprecatedLegacyEnumAccessRule implements Rule{ $scope->resolveTypeByName($node->class) : $scope->getType($node->class); - if(!$classType instanceof TypeWithClassName){ - return []; - } + $errors = []; + $reflections = $classType->getObjectClassReflections(); + foreach($reflections as $reflection){ + if(!$reflection->hasTraitUse(LegacyEnumShimTrait::class) || !$reflection->implementsInterface(\UnitEnum::class)){ + continue; + } - $reflection = $classType->getClassReflection(); - if($reflection === null || !$reflection->hasTraitUse(LegacyEnumShimTrait::class) || !$reflection->implementsInterface(\UnitEnum::class)){ - return []; - } - - if(!$reflection->hasNativeMethod($caseName)){ - return [ - RuleErrorBuilder::message(sprintf( + if(!$reflection->hasNativeMethod($caseName)){ + $errors[] = RuleErrorBuilder::message(sprintf( 'Use of legacy enum case accessor %s::%s().', $reflection->getName(), $caseName @@ -70,10 +66,11 @@ final class DeprecatedLegacyEnumAccessRule implements Rule{ 'Access the enum constant directly instead (remove the brackets), e.g. %s::%s', $reflection->getName(), $caseName - ))->build() - ]; + ))->identifier('pocketmine.enum.deprecatedAccessor') + ->build(); + } } - return []; + return $errors; } } diff --git a/tests/phpstan/rules/DisallowDynamicNewRule.php b/tests/phpstan/rules/DisallowDynamicNewRule.php new file mode 100644 index 000000000..c25e6a18b --- /dev/null +++ b/tests/phpstan/rules/DisallowDynamicNewRule.php @@ -0,0 +1,55 @@ + + */ +final class DisallowDynamicNewRule implements Rule{ + + public function getNodeType() : string{ + return New_::class; + } + + public function processNode(Node $node, Scope $scope) : array{ + /** @var New_ $node */ + if($node->class instanceof Expr){ + return [ + RuleErrorBuilder::message("Dynamic new is not allowed.") + ->tip("For factories, use closures instead. Closures can implement custom logic, are statically analyzable, and don't restrict the constructor signature.") + ->identifier("pocketmine.new.dynamic") + ->build() + ]; + } + + return []; + } +} diff --git a/tests/phpstan/rules/DisallowEnumComparisonRule.php b/tests/phpstan/rules/DisallowEnumComparisonRule.php index fc5377173..d73cc3972 100644 --- a/tests/phpstan/rules/DisallowEnumComparisonRule.php +++ b/tests/phpstan/rules/DisallowEnumComparisonRule.php @@ -30,7 +30,6 @@ use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -61,7 +60,7 @@ class DisallowEnumComparisonRule implements Rule{ $node instanceof Identical ? '===' : '!==', $leftType->describe(VerbosityLevel::value()), $rightType->describe(VerbosityLevel::value()) - ))->build()]; + ))->identifier('pocketmine.enum.badComparison')->build()]; } return []; } @@ -69,7 +68,7 @@ class DisallowEnumComparisonRule implements Rule{ private function checkForEnumTypes(Type $comparedType) : bool{ //TODO: what we really want to do here is iterate over the contained types, but there's no universal way to //do that. This might break with other circumstances. - if($comparedType instanceof ObjectType){ + if($comparedType->isObject()->yes()){ $types = [$comparedType]; }elseif($comparedType instanceof UnionType){ $types = $comparedType->getTypes(); @@ -77,12 +76,14 @@ class DisallowEnumComparisonRule implements Rule{ return false; } foreach($types as $containedType){ - if(!($containedType instanceof ObjectType)){ + if(!($containedType->isObject()->yes())){ continue; } - $class = $containedType->getClassReflection(); - if($class !== null && $class->hasTraitUse(EnumTrait::class)){ - return true; + $classes = $containedType->getObjectClassReflections(); + foreach($classes as $class){ + if($class->hasTraitUse(EnumTrait::class)){ + return true; + } } } return false; diff --git a/tests/phpstan/rules/DisallowForeachByReferenceRule.php b/tests/phpstan/rules/DisallowForeachByReferenceRule.php index 79124d328..eb6589705 100644 --- a/tests/phpstan/rules/DisallowForeachByReferenceRule.php +++ b/tests/phpstan/rules/DisallowForeachByReferenceRule.php @@ -44,6 +44,7 @@ final class DisallowForeachByReferenceRule implements Rule{ return [ RuleErrorBuilder::message("Foreach by-reference is not allowed, because it has surprising behaviour.") ->tip("If the value variable is used outside of the foreach construct (e.g. in a second foreach), the iterable's contents will be unexpectedly altered.") + ->identifier('pocketmine.foreach.byRef') ->build() ]; } diff --git a/tests/phpstan/rules/ExplodeLimitRule.php b/tests/phpstan/rules/ExplodeLimitRule.php new file mode 100644 index 000000000..4e8a341ad --- /dev/null +++ b/tests/phpstan/rules/ExplodeLimitRule.php @@ -0,0 +1,92 @@ + + */ +final class ExplodeLimitRule implements Rule{ + private ReflectionProvider $reflectionProvider; + + public function __construct( + ReflectionProvider $reflectionProvider + ){ + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType() : string{ + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope) : array{ + if(!$node->name instanceof Name){ + return []; + } + + if(!$this->reflectionProvider->hasFunction($node->name, $scope)){ + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + + if($functionReflection->getName() !== 'explode'){ + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if($normalizedFuncCall === null){ + return []; + } + + $count = count($normalizedFuncCall->getArgs()); + if($count !== 3){ + return [ + RuleErrorBuilder::message('The $limit parameter of explode() must be set to prevent malicious client data wasting resources.') + ->identifier("pocketmine.explode.limit") + ->build() + ]; + } + + return []; + } +} 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 745cf2109..cb463c61d 100644 --- a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php +++ b/tests/phpstan/rules/UnsafeForeachRule.php @@ -41,7 +41,7 @@ use function sprintf; /** * @implements Rule */ -final class UnsafeForeachArrayOfStringRule implements Rule{ +final class UnsafeForeachRule implements Rule{ public function getNodeType() : string{ return Foreach_::class; @@ -73,7 +73,7 @@ final class UnsafeForeachArrayOfStringRule implements Rule{ $benevolentUnionDepth--; return $result; } - if($type instanceof IntegerType && $benevolentUnionDepth === 0){ + if($type instanceof IntegerType){ $expectsIntKeyTypes = true; return $type; } @@ -87,24 +87,31 @@ final class UnsafeForeachArrayOfStringRule implements Rule{ $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)->build() - ]; + ))->identifier('pocketmine.foreach.implicitKeys')->build(); } - return []; + 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 $errors; } } diff --git a/tests/phpstan/stubs/chunkutils2.stub b/tests/phpstan/stubs/chunkutils2.stub new file mode 100644 index 000000000..b23e4a7fd --- /dev/null +++ b/tests/phpstan/stubs/chunkutils2.stub @@ -0,0 +1,15 @@ + $palette + */ + public static function fromData(int $bitsPerBlock, string $wordArray, array $palette): PalettedBlockArray {} + + /** + * @return list + */ + public function getPalette(): array {} +} diff --git a/tests/phpunit/block/BlockTest.php b/tests/phpunit/block/BlockTest.php index 841917787..138a3e4e8 100644 --- a/tests/phpunit/block/BlockTest.php +++ b/tests/phpunit/block/BlockTest.php @@ -24,15 +24,22 @@ declare(strict_types=1); namespace pocketmine\block; use PHPUnit\Framework\TestCase; +use pocketmine\data\bedrock\BedrockDataFiles; +use pocketmine\data\bedrock\block\BlockTypeNames; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; +use pocketmine\world\format\io\GlobalBlockStateHandlers; +use function array_fill_keys; +use function get_debug_type; use function implode; use function is_array; +use function is_float; use function is_int; use function is_string; use function json_decode; use function log; +use function round; use const JSON_THROW_ON_ERROR; class BlockTest extends TestCase{ @@ -94,12 +101,62 @@ class BlockTest extends TestCase{ } } + public function testBlockBreakInfo() : void{ + $propertiesTable = json_decode(Filesystem::fileGetContents(BedrockDataFiles::BLOCK_PROPERTIES_TABLE_JSON), true, 3, JSON_THROW_ON_ERROR); + if(!is_array($propertiesTable)){ + throw new AssumptionFailedError("Block properties table must be an array"); + } + $exceptions = array_fill_keys([ + BlockTypeNames::AIR, + BlockTypeNames::WATER, + BlockTypeNames::FLOWING_WATER, + BlockTypeNames::LAVA, + BlockTypeNames::FLOWING_LAVA, + BlockTypeNames::MANGROVE_LOG, //For some reason ONLY this wood block has blast resistance 2 instead of 10... + ], true); + + $serializer = GlobalBlockStateHandlers::getSerializer(); + $testedBlocks = []; + $hardnessErrors = []; + $blastResistanceErrors = []; + foreach($this->blockFactory->getAllKnownStates() as $block){ + $vanillaId = $serializer->serializeBlock($block)->getName(); + if(isset($exceptions[$vanillaId]) || isset($testedBlocks[$vanillaId])){ + continue; + } + if(!isset($propertiesTable[$vanillaId]) || !is_array($propertiesTable[$vanillaId])){ + throw new AssumptionFailedError("$vanillaId does not exist in the vanilla block properties table or is not an array"); + } + if(!isset($propertiesTable[$vanillaId]["hardness"]) || !is_float($propertiesTable[$vanillaId]["hardness"])){ + throw new AssumptionFailedError("Hardness property is missing for $vanillaId or is not a float value"); + } + if(!isset($propertiesTable[$vanillaId]["blastResistance"]) || !is_float($propertiesTable[$vanillaId]["blastResistance"])){ + throw new AssumptionFailedError("Blast resistance property is missing for $vanillaId or is not a float value"); + } + $testedBlocks[$vanillaId] = true; + + $vanillaHardness = round($propertiesTable[$vanillaId]["hardness"], 5); + $vanillaBlastResistance = round($propertiesTable[$vanillaId]["blastResistance"], 5) * 5; + + $breakInfo = $block->getBreakInfo(); + if($breakInfo->getHardness() !== $vanillaHardness){ + $hardnessErrors[] = "Hardness mismatch for $vanillaId (expected: $vanillaHardness, got " . $breakInfo->getHardness() . ")"; + } + if($breakInfo->getBlastResistance() !== $vanillaBlastResistance){ + $blastResistanceErrors[] = "Blast resistance mismatch for $vanillaId (expected: $vanillaBlastResistance, got " . $breakInfo->getBlastResistance() . ")"; + } + } + self::assertEmpty($hardnessErrors, "Block hardness test failed:\n" . implode("\n", $hardnessErrors)); + self::assertEmpty($blastResistanceErrors, "Block blast resistance test failed:\n" . implode("\n", $blastResistanceErrors)); + } + /** - * @return int[] - * @phpstan-return array + * @return int[][]|string[][] + * @phpstan-return array{array, array} */ public static function computeConsistencyCheckTable(RuntimeBlockStateRegistry $blockStateRegistry) : array{ $newTable = []; + $newTileMap = []; $idNameLookup = []; //if we ever split up block registration into multiple registries (e.g. separating chemistry blocks), @@ -118,36 +175,70 @@ class BlockTest extends TestCase{ } $idName = $idNameLookup[$block->getTypeId()]; $newTable[$idName] = ($newTable[$idName] ?? 0) + 1; - } - return $newTable; + $tileClass = $block->getIdInfo()->getTileClass(); + if($tileClass !== null){ + if(isset($newTileMap[$idName]) && $newTileMap[$idName] !== $tileClass){ + throw new AssumptionFailedError("Tile entity $tileClass for $idName is inconsistent"); + } + $newTileMap[$idName] = $tileClass; + } + } + return [$newTable, $newTileMap]; } /** - * @phpstan-param array $actual + * @phpstan-param array $actualStateCounts + * @phpstan-param array $actualTiles * * @return string[] */ - public static function computeConsistencyCheckDiff(string $expectedFile, array $actual) : array{ - $expected = json_decode(Filesystem::fileGetContents($expectedFile), true, 2, JSON_THROW_ON_ERROR); + public static function computeConsistencyCheckDiff(string $expectedFile, array $actualStateCounts, array $actualTiles) : array{ + $expected = json_decode(Filesystem::fileGetContents($expectedFile), true, 3, JSON_THROW_ON_ERROR); if(!is_array($expected)){ - throw new AssumptionFailedError("Old table should be array"); + throw new AssumptionFailedError("Old table should be array{stateCounts: array, tiles: array}"); + } + $expectedStates = $expected["stateCounts"] ?? []; + $expectedTiles = $expected["tiles"] ?? []; + if(!is_array($expectedStates)){ + throw new AssumptionFailedError("stateCounts should be an array, but have " . get_debug_type($expectedStates)); + } + if(!is_array($expectedTiles)){ + throw new AssumptionFailedError("tiles should be an array, but have " . get_debug_type($expectedTiles)); } $errors = []; - foreach(Utils::promoteKeys($expected) as $typeName => $numStates){ + foreach(Utils::promoteKeys($expectedStates) as $typeName => $numStates){ if(!is_string($typeName) || !is_int($numStates)){ throw new AssumptionFailedError("Old table should be array"); } - if(!isset($actual[$typeName])){ + if(!isset($actualStateCounts[$typeName])){ $errors[] = "Removed block type $typeName ($numStates permutations)"; - }elseif($actual[$typeName] !== $numStates){ - $errors[] = "Block type $typeName permutation count changed: $numStates -> " . $actual[$typeName]; + }elseif($actualStateCounts[$typeName] !== $numStates){ + $errors[] = "Block type $typeName permutation count changed: $numStates -> " . $actualStateCounts[$typeName]; } } - foreach(Utils::stringifyKeys($actual) as $typeName => $numStates){ - if(!isset($expected[$typeName])){ - $errors[] = "Added block type $typeName (" . $actual[$typeName] . " permutations)"; + foreach(Utils::stringifyKeys($actualStateCounts) as $typeName => $numStates){ + if(!isset($expectedStates[$typeName])){ + $errors[] = "Added block type $typeName (" . $actualStateCounts[$typeName] . " permutations)"; + } + } + + foreach(Utils::promoteKeys($expectedTiles) as $typeName => $tile){ + if(!is_string($typeName) || !is_string($tile)){ + throw new AssumptionFailedError("Tile table should be array"); + } + if(isset($actualStateCounts[$typeName])){ + if(!isset($actualTiles[$typeName])){ + $errors[] = "$typeName no longer has a tile"; + }elseif($actualTiles[$typeName] !== $tile){ + $errors[] = "$typeName has changed tile ($tile -> " . $actualTiles[$typeName] . ")"; + } + } + } + foreach(Utils::promoteKeys($actualTiles) as $typeName => $tile){ + if(isset($expectedStates[$typeName]) && !isset($expectedTiles[$typeName])){ + $errors[] = "$typeName has a tile when it previously didn't ($tile)"; } } @@ -155,8 +246,8 @@ class BlockTest extends TestCase{ } public function testConsistency() : void{ - $newTable = self::computeConsistencyCheckTable($this->blockFactory); - $errors = self::computeConsistencyCheckDiff(__DIR__ . '/block_factory_consistency_check.json', $newTable); + [$newTable, $newTileMap] = self::computeConsistencyCheckTable($this->blockFactory); + $errors = self::computeConsistencyCheckDiff(__DIR__ . '/block_factory_consistency_check.json', $newTable, $newTileMap); self::assertEmpty($errors, "Block factory consistency check failed:\n" . implode("\n", $errors)); } diff --git a/tests/phpunit/block/BlockTypeIdsTest.php b/tests/phpunit/block/BlockTypeIdsTest.php index ce21a89ab..cbfc07eaf 100644 --- a/tests/phpunit/block/BlockTypeIdsTest.php +++ b/tests/phpunit/block/BlockTypeIdsTest.php @@ -35,8 +35,12 @@ class BlockTypeIdsTest extends TestCase{ $constants = $reflect->getConstants(); unset($constants['FIRST_UNUSED_BLOCK_ID']); + self::assertNotEmpty($constants, "We should never have zero type IDs"); - self::assertSame($reflect->getConstant('FIRST_UNUSED_BLOCK_ID'), max($constants) + 1, "FIRST_UNUSED_BLOCK_ID must be one higher than the highest fixed type ID"); + $max = max($constants); + self::assertIsInt($max, "Max type ID should always be an integer"); + + self::assertSame($reflect->getConstant('FIRST_UNUSED_BLOCK_ID'), $max + 1, "FIRST_UNUSED_BLOCK_ID must be one higher than the highest fixed type ID"); } public function testNoDuplicates() : void{ diff --git a/tests/phpunit/block/block_factory_consistency_check.json b/tests/phpunit/block/block_factory_consistency_check.json index 79804d8cb..b96607c0b 100644 --- a/tests/phpunit/block/block_factory_consistency_check.json +++ b/tests/phpunit/block/block_factory_consistency_check.json @@ -1,712 +1,864 @@ { - "ACACIA_BUTTON": 12, - "ACACIA_DOOR": 32, - "ACACIA_FENCE": 1, - "ACACIA_FENCE_GATE": 16, - "ACACIA_LEAVES": 4, - "ACACIA_LOG": 6, - "ACACIA_PLANKS": 1, - "ACACIA_PRESSURE_PLATE": 2, - "ACACIA_SAPLING": 2, - "ACACIA_SIGN": 16, - "ACACIA_SLAB": 3, - "ACACIA_STAIRS": 8, - "ACACIA_TRAPDOOR": 16, - "ACACIA_WALL_SIGN": 4, - "ACACIA_WOOD": 6, - "ACTIVATOR_RAIL": 12, - "AIR": 1, - "ALLIUM": 1, - "ALL_SIDED_MUSHROOM_STEM": 1, - "AMETHYST": 1, - "AMETHYST_CLUSTER": 24, - "ANCIENT_DEBRIS": 1, - "ANDESITE": 1, - "ANDESITE_SLAB": 3, - "ANDESITE_STAIRS": 8, - "ANDESITE_WALL": 162, - "ANVIL": 12, - "AZALEA_LEAVES": 4, - "AZURE_BLUET": 1, - "BAMBOO": 12, - "BAMBOO_SAPLING": 2, - "BANNER": 256, - "BARREL": 12, - "BARRIER": 1, - "BASALT": 3, - "BEACON": 1, - "BED": 256, - "BEDROCK": 2, - "BEETROOTS": 8, - "BELL": 16, - "BIG_DRIPLEAF_HEAD": 16, - "BIG_DRIPLEAF_STEM": 4, - "BIRCH_BUTTON": 12, - "BIRCH_DOOR": 32, - "BIRCH_FENCE": 1, - "BIRCH_FENCE_GATE": 16, - "BIRCH_LEAVES": 4, - "BIRCH_LOG": 6, - "BIRCH_PLANKS": 1, - "BIRCH_PRESSURE_PLATE": 2, - "BIRCH_SAPLING": 2, - "BIRCH_SIGN": 16, - "BIRCH_SLAB": 3, - "BIRCH_STAIRS": 8, - "BIRCH_TRAPDOOR": 16, - "BIRCH_WALL_SIGN": 4, - "BIRCH_WOOD": 6, - "BLACKSTONE": 1, - "BLACKSTONE_SLAB": 3, - "BLACKSTONE_STAIRS": 8, - "BLACKSTONE_WALL": 162, - "BLAST_FURNACE": 8, - "BLUE_ICE": 1, - "BLUE_ORCHID": 1, - "BLUE_TORCH": 5, - "BONE_BLOCK": 3, - "BOOKSHELF": 1, - "BREWING_STAND": 8, - "BRICKS": 1, - "BRICK_SLAB": 3, - "BRICK_STAIRS": 8, - "BRICK_WALL": 162, - "BROWN_MUSHROOM": 1, - "BROWN_MUSHROOM_BLOCK": 11, - "BUDDING_AMETHYST": 1, - "CACTUS": 16, - "CAKE": 7, - "CAKE_WITH_CANDLE": 2, - "CAKE_WITH_DYED_CANDLE": 32, - "CALCITE": 1, - "CAMPFIRE": 8, - "CANDLE": 8, - "CARPET": 16, - "CARROTS": 8, - "CARTOGRAPHY_TABLE": 1, - "CARVED_PUMPKIN": 4, - "CAULDRON": 1, - "CAVE_VINES": 104, - "CHAIN": 3, - "CHEMICAL_HEAT": 1, - "CHERRY_BUTTON": 12, - "CHERRY_DOOR": 32, - "CHERRY_FENCE": 1, - "CHERRY_FENCE_GATE": 16, - "CHERRY_LEAVES": 4, - "CHERRY_LOG": 6, - "CHERRY_PLANKS": 1, - "CHERRY_PRESSURE_PLATE": 2, - "CHERRY_SIGN": 16, - "CHERRY_SLAB": 3, - "CHERRY_STAIRS": 8, - "CHERRY_TRAPDOOR": 16, - "CHERRY_WALL_SIGN": 4, - "CHERRY_WOOD": 6, - "CHEST": 4, - "CHISELED_BOOKSHELF": 256, - "CHISELED_COPPER": 8, - "CHISELED_DEEPSLATE": 1, - "CHISELED_NETHER_BRICKS": 1, - "CHISELED_POLISHED_BLACKSTONE": 1, - "CHISELED_QUARTZ": 3, - "CHISELED_RED_SANDSTONE": 1, - "CHISELED_SANDSTONE": 1, - "CHISELED_STONE_BRICKS": 1, - "CHISELED_TUFF": 1, - "CHISELED_TUFF_BRICKS": 1, - "CHORUS_FLOWER": 6, - "CHORUS_PLANT": 1, - "CLAY": 1, - "COAL": 1, - "COAL_ORE": 1, - "COBBLED_DEEPSLATE": 1, - "COBBLED_DEEPSLATE_SLAB": 3, - "COBBLED_DEEPSLATE_STAIRS": 8, - "COBBLED_DEEPSLATE_WALL": 162, - "COBBLESTONE": 1, - "COBBLESTONE_SLAB": 3, - "COBBLESTONE_STAIRS": 8, - "COBBLESTONE_WALL": 162, - "COBWEB": 1, - "COCOA_POD": 12, - "COMPOUND_CREATOR": 4, - "CONCRETE": 16, - "CONCRETE_POWDER": 16, - "COPPER": 8, - "COPPER_BULB": 32, - "COPPER_DOOR": 256, - "COPPER_GRATE": 8, - "COPPER_ORE": 1, - "COPPER_TRAPDOOR": 128, - "CORAL": 10, - "CORAL_BLOCK": 10, - "CORAL_FAN": 20, - "CORNFLOWER": 1, - "CRACKED_DEEPSLATE_BRICKS": 1, - "CRACKED_DEEPSLATE_TILES": 1, - "CRACKED_NETHER_BRICKS": 1, - "CRACKED_POLISHED_BLACKSTONE_BRICKS": 1, - "CRACKED_STONE_BRICKS": 1, - "CRAFTING_TABLE": 1, - "CRIMSON_BUTTON": 12, - "CRIMSON_DOOR": 32, - "CRIMSON_FENCE": 1, - "CRIMSON_FENCE_GATE": 16, - "CRIMSON_HYPHAE": 6, - "CRIMSON_PLANKS": 1, - "CRIMSON_PRESSURE_PLATE": 2, - "CRIMSON_ROOTS": 1, - "CRIMSON_SIGN": 16, - "CRIMSON_SLAB": 3, - "CRIMSON_STAIRS": 8, - "CRIMSON_STEM": 6, - "CRIMSON_TRAPDOOR": 16, - "CRIMSON_WALL_SIGN": 4, - "CRYING_OBSIDIAN": 1, - "CUT_COPPER": 8, - "CUT_COPPER_SLAB": 24, - "CUT_COPPER_STAIRS": 64, - "CUT_RED_SANDSTONE": 1, - "CUT_RED_SANDSTONE_SLAB": 3, - "CUT_SANDSTONE": 1, - "CUT_SANDSTONE_SLAB": 3, - "DANDELION": 1, - "DARK_OAK_BUTTON": 12, - "DARK_OAK_DOOR": 32, - "DARK_OAK_FENCE": 1, - "DARK_OAK_FENCE_GATE": 16, - "DARK_OAK_LEAVES": 4, - "DARK_OAK_LOG": 6, - "DARK_OAK_PLANKS": 1, - "DARK_OAK_PRESSURE_PLATE": 2, - "DARK_OAK_SAPLING": 2, - "DARK_OAK_SIGN": 16, - "DARK_OAK_SLAB": 3, - "DARK_OAK_STAIRS": 8, - "DARK_OAK_TRAPDOOR": 16, - "DARK_OAK_WALL_SIGN": 4, - "DARK_OAK_WOOD": 6, - "DARK_PRISMARINE": 1, - "DARK_PRISMARINE_SLAB": 3, - "DARK_PRISMARINE_STAIRS": 8, - "DAYLIGHT_SENSOR": 32, - "DEAD_BUSH": 1, - "DEEPSLATE": 3, - "DEEPSLATE_BRICKS": 1, - "DEEPSLATE_BRICK_SLAB": 3, - "DEEPSLATE_BRICK_STAIRS": 8, - "DEEPSLATE_BRICK_WALL": 162, - "DEEPSLATE_COAL_ORE": 1, - "DEEPSLATE_COPPER_ORE": 1, - "DEEPSLATE_DIAMOND_ORE": 1, - "DEEPSLATE_EMERALD_ORE": 1, - "DEEPSLATE_GOLD_ORE": 1, - "DEEPSLATE_IRON_ORE": 1, - "DEEPSLATE_LAPIS_LAZULI_ORE": 1, - "DEEPSLATE_REDSTONE_ORE": 2, - "DEEPSLATE_TILES": 1, - "DEEPSLATE_TILE_SLAB": 3, - "DEEPSLATE_TILE_STAIRS": 8, - "DEEPSLATE_TILE_WALL": 162, - "DETECTOR_RAIL": 12, - "DIAMOND": 1, - "DIAMOND_ORE": 1, - "DIORITE": 1, - "DIORITE_SLAB": 3, - "DIORITE_STAIRS": 8, - "DIORITE_WALL": 162, - "DIRT": 3, - "DOUBLE_PITCHER_CROP": 4, - "DOUBLE_TALLGRASS": 2, - "DRAGON_EGG": 1, - "DRIED_KELP": 1, - "DYED_CANDLE": 128, - "DYED_SHULKER_BOX": 16, - "ELEMENT_ACTINIUM": 1, - "ELEMENT_ALUMINUM": 1, - "ELEMENT_AMERICIUM": 1, - "ELEMENT_ANTIMONY": 1, - "ELEMENT_ARGON": 1, - "ELEMENT_ARSENIC": 1, - "ELEMENT_ASTATINE": 1, - "ELEMENT_BARIUM": 1, - "ELEMENT_BERKELIUM": 1, - "ELEMENT_BERYLLIUM": 1, - "ELEMENT_BISMUTH": 1, - "ELEMENT_BOHRIUM": 1, - "ELEMENT_BORON": 1, - "ELEMENT_BROMINE": 1, - "ELEMENT_CADMIUM": 1, - "ELEMENT_CALCIUM": 1, - "ELEMENT_CALIFORNIUM": 1, - "ELEMENT_CARBON": 1, - "ELEMENT_CERIUM": 1, - "ELEMENT_CESIUM": 1, - "ELEMENT_CHLORINE": 1, - "ELEMENT_CHROMIUM": 1, - "ELEMENT_COBALT": 1, - "ELEMENT_CONSTRUCTOR": 4, - "ELEMENT_COPERNICIUM": 1, - "ELEMENT_COPPER": 1, - "ELEMENT_CURIUM": 1, - "ELEMENT_DARMSTADTIUM": 1, - "ELEMENT_DUBNIUM": 1, - "ELEMENT_DYSPROSIUM": 1, - "ELEMENT_EINSTEINIUM": 1, - "ELEMENT_ERBIUM": 1, - "ELEMENT_EUROPIUM": 1, - "ELEMENT_FERMIUM": 1, - "ELEMENT_FLEROVIUM": 1, - "ELEMENT_FLUORINE": 1, - "ELEMENT_FRANCIUM": 1, - "ELEMENT_GADOLINIUM": 1, - "ELEMENT_GALLIUM": 1, - "ELEMENT_GERMANIUM": 1, - "ELEMENT_GOLD": 1, - "ELEMENT_HAFNIUM": 1, - "ELEMENT_HASSIUM": 1, - "ELEMENT_HELIUM": 1, - "ELEMENT_HOLMIUM": 1, - "ELEMENT_HYDROGEN": 1, - "ELEMENT_INDIUM": 1, - "ELEMENT_IODINE": 1, - "ELEMENT_IRIDIUM": 1, - "ELEMENT_IRON": 1, - "ELEMENT_KRYPTON": 1, - "ELEMENT_LANTHANUM": 1, - "ELEMENT_LAWRENCIUM": 1, - "ELEMENT_LEAD": 1, - "ELEMENT_LITHIUM": 1, - "ELEMENT_LIVERMORIUM": 1, - "ELEMENT_LUTETIUM": 1, - "ELEMENT_MAGNESIUM": 1, - "ELEMENT_MANGANESE": 1, - "ELEMENT_MEITNERIUM": 1, - "ELEMENT_MENDELEVIUM": 1, - "ELEMENT_MERCURY": 1, - "ELEMENT_MOLYBDENUM": 1, - "ELEMENT_MOSCOVIUM": 1, - "ELEMENT_NEODYMIUM": 1, - "ELEMENT_NEON": 1, - "ELEMENT_NEPTUNIUM": 1, - "ELEMENT_NICKEL": 1, - "ELEMENT_NIHONIUM": 1, - "ELEMENT_NIOBIUM": 1, - "ELEMENT_NITROGEN": 1, - "ELEMENT_NOBELIUM": 1, - "ELEMENT_OGANESSON": 1, - "ELEMENT_OSMIUM": 1, - "ELEMENT_OXYGEN": 1, - "ELEMENT_PALLADIUM": 1, - "ELEMENT_PHOSPHORUS": 1, - "ELEMENT_PLATINUM": 1, - "ELEMENT_PLUTONIUM": 1, - "ELEMENT_POLONIUM": 1, - "ELEMENT_POTASSIUM": 1, - "ELEMENT_PRASEODYMIUM": 1, - "ELEMENT_PROMETHIUM": 1, - "ELEMENT_PROTACTINIUM": 1, - "ELEMENT_RADIUM": 1, - "ELEMENT_RADON": 1, - "ELEMENT_RHENIUM": 1, - "ELEMENT_RHODIUM": 1, - "ELEMENT_ROENTGENIUM": 1, - "ELEMENT_RUBIDIUM": 1, - "ELEMENT_RUTHENIUM": 1, - "ELEMENT_RUTHERFORDIUM": 1, - "ELEMENT_SAMARIUM": 1, - "ELEMENT_SCANDIUM": 1, - "ELEMENT_SEABORGIUM": 1, - "ELEMENT_SELENIUM": 1, - "ELEMENT_SILICON": 1, - "ELEMENT_SILVER": 1, - "ELEMENT_SODIUM": 1, - "ELEMENT_STRONTIUM": 1, - "ELEMENT_SULFUR": 1, - "ELEMENT_TANTALUM": 1, - "ELEMENT_TECHNETIUM": 1, - "ELEMENT_TELLURIUM": 1, - "ELEMENT_TENNESSINE": 1, - "ELEMENT_TERBIUM": 1, - "ELEMENT_THALLIUM": 1, - "ELEMENT_THORIUM": 1, - "ELEMENT_THULIUM": 1, - "ELEMENT_TIN": 1, - "ELEMENT_TITANIUM": 1, - "ELEMENT_TUNGSTEN": 1, - "ELEMENT_URANIUM": 1, - "ELEMENT_VANADIUM": 1, - "ELEMENT_XENON": 1, - "ELEMENT_YTTERBIUM": 1, - "ELEMENT_YTTRIUM": 1, - "ELEMENT_ZERO": 1, - "ELEMENT_ZINC": 1, - "ELEMENT_ZIRCONIUM": 1, - "EMERALD": 1, - "EMERALD_ORE": 1, - "ENCHANTING_TABLE": 1, - "ENDER_CHEST": 4, - "END_PORTAL_FRAME": 8, - "END_ROD": 6, - "END_STONE": 1, - "END_STONE_BRICKS": 1, - "END_STONE_BRICK_SLAB": 3, - "END_STONE_BRICK_STAIRS": 8, - "END_STONE_BRICK_WALL": 162, - "FAKE_WOODEN_SLAB": 3, - "FARMLAND": 1304, - "FERN": 1, - "FIRE": 16, - "FLETCHING_TABLE": 1, - "FLOWERING_AZALEA_LEAVES": 4, - "FLOWER_POT": 1, - "FROGLIGHT": 9, - "FROSTED_ICE": 4, - "FURNACE": 8, - "GILDED_BLACKSTONE": 1, - "GLASS": 1, - "GLASS_PANE": 1, - "GLAZED_TERRACOTTA": 64, - "GLOWING_ITEM_FRAME": 12, - "GLOWING_OBSIDIAN": 1, - "GLOWSTONE": 1, - "GLOW_LICHEN": 64, - "GOLD": 1, - "GOLD_ORE": 1, - "GRANITE": 1, - "GRANITE_SLAB": 3, - "GRANITE_STAIRS": 8, - "GRANITE_WALL": 162, - "GRASS": 1, - "GRASS_PATH": 1, - "GRAVEL": 1, - "GREEN_TORCH": 5, - "HANGING_ROOTS": 1, - "HARDENED_CLAY": 1, - "HARDENED_GLASS": 1, - "HARDENED_GLASS_PANE": 1, - "HAY_BALE": 3, - "HONEYCOMB": 1, - "HOPPER": 10, - "ICE": 1, - "INFESTED_CHISELED_STONE_BRICK": 1, - "INFESTED_COBBLESTONE": 1, - "INFESTED_CRACKED_STONE_BRICK": 1, - "INFESTED_MOSSY_STONE_BRICK": 1, - "INFESTED_STONE": 1, - "INFESTED_STONE_BRICK": 1, - "INFO_UPDATE": 1, - "INFO_UPDATE2": 1, - "INVISIBLE_BEDROCK": 1, - "IRON": 1, - "IRON_BARS": 1, - "IRON_DOOR": 32, - "IRON_ORE": 1, - "IRON_TRAPDOOR": 16, - "ITEM_FRAME": 12, - "JUKEBOX": 1, - "JUNGLE_BUTTON": 12, - "JUNGLE_DOOR": 32, - "JUNGLE_FENCE": 1, - "JUNGLE_FENCE_GATE": 16, - "JUNGLE_LEAVES": 4, - "JUNGLE_LOG": 6, - "JUNGLE_PLANKS": 1, - "JUNGLE_PRESSURE_PLATE": 2, - "JUNGLE_SAPLING": 2, - "JUNGLE_SIGN": 16, - "JUNGLE_SLAB": 3, - "JUNGLE_STAIRS": 8, - "JUNGLE_TRAPDOOR": 16, - "JUNGLE_WALL_SIGN": 4, - "JUNGLE_WOOD": 6, - "LAB_TABLE": 4, - "LADDER": 4, - "LANTERN": 2, - "LAPIS_LAZULI": 1, - "LAPIS_LAZULI_ORE": 1, - "LARGE_FERN": 2, - "LAVA": 32, - "LAVA_CAULDRON": 6, - "LECTERN": 8, - "LEGACY_STONECUTTER": 1, - "LEVER": 16, - "LIGHT": 16, - "LIGHTNING_ROD": 6, - "LILAC": 2, - "LILY_OF_THE_VALLEY": 1, - "LILY_PAD": 1, - "LIT_PUMPKIN": 4, - "LOOM": 4, - "MAGMA": 1, - "MANGROVE_BUTTON": 12, - "MANGROVE_DOOR": 32, - "MANGROVE_FENCE": 1, - "MANGROVE_FENCE_GATE": 16, - "MANGROVE_LEAVES": 4, - "MANGROVE_LOG": 6, - "MANGROVE_PLANKS": 1, - "MANGROVE_PRESSURE_PLATE": 2, - "MANGROVE_ROOTS": 1, - "MANGROVE_SIGN": 16, - "MANGROVE_SLAB": 3, - "MANGROVE_STAIRS": 8, - "MANGROVE_TRAPDOOR": 16, - "MANGROVE_WALL_SIGN": 4, - "MANGROVE_WOOD": 6, - "MATERIAL_REDUCER": 4, - "MELON": 1, - "MELON_STEM": 40, - "MOB_HEAD": 35, - "MONSTER_SPAWNER": 1, - "MOSSY_COBBLESTONE": 1, - "MOSSY_COBBLESTONE_SLAB": 3, - "MOSSY_COBBLESTONE_STAIRS": 8, - "MOSSY_COBBLESTONE_WALL": 162, - "MOSSY_STONE_BRICKS": 1, - "MOSSY_STONE_BRICK_SLAB": 3, - "MOSSY_STONE_BRICK_STAIRS": 8, - "MOSSY_STONE_BRICK_WALL": 162, - "MUD": 1, - "MUDDY_MANGROVE_ROOTS": 3, - "MUD_BRICKS": 1, - "MUD_BRICK_SLAB": 3, - "MUD_BRICK_STAIRS": 8, - "MUD_BRICK_WALL": 162, - "MUSHROOM_STEM": 1, - "MYCELIUM": 1, - "NETHERITE": 1, - "NETHERRACK": 1, - "NETHER_BRICKS": 1, - "NETHER_BRICK_FENCE": 1, - "NETHER_BRICK_SLAB": 3, - "NETHER_BRICK_STAIRS": 8, - "NETHER_BRICK_WALL": 162, - "NETHER_GOLD_ORE": 1, - "NETHER_PORTAL": 2, - "NETHER_QUARTZ_ORE": 1, - "NETHER_REACTOR_CORE": 1, - "NETHER_WART": 4, - "NETHER_WART_BLOCK": 1, - "NOTE_BLOCK": 1, - "OAK_BUTTON": 12, - "OAK_DOOR": 32, - "OAK_FENCE": 1, - "OAK_FENCE_GATE": 16, - "OAK_LEAVES": 4, - "OAK_LOG": 6, - "OAK_PLANKS": 1, - "OAK_PRESSURE_PLATE": 2, - "OAK_SAPLING": 2, - "OAK_SIGN": 16, - "OAK_SLAB": 3, - "OAK_STAIRS": 8, - "OAK_TRAPDOOR": 16, - "OAK_WALL_SIGN": 4, - "OAK_WOOD": 6, - "OBSIDIAN": 1, - "ORANGE_TULIP": 1, - "OXEYE_DAISY": 1, - "PACKED_ICE": 1, - "PACKED_MUD": 1, - "PEONY": 2, - "PINK_PETALS": 16, - "PINK_TULIP": 1, - "PITCHER_CROP": 3, - "PITCHER_PLANT": 2, - "PODZOL": 1, - "POLISHED_ANDESITE": 1, - "POLISHED_ANDESITE_SLAB": 3, - "POLISHED_ANDESITE_STAIRS": 8, - "POLISHED_BASALT": 3, - "POLISHED_BLACKSTONE": 1, - "POLISHED_BLACKSTONE_BRICKS": 1, - "POLISHED_BLACKSTONE_BRICK_SLAB": 3, - "POLISHED_BLACKSTONE_BRICK_STAIRS": 8, - "POLISHED_BLACKSTONE_BRICK_WALL": 162, - "POLISHED_BLACKSTONE_BUTTON": 12, - "POLISHED_BLACKSTONE_PRESSURE_PLATE": 2, - "POLISHED_BLACKSTONE_SLAB": 3, - "POLISHED_BLACKSTONE_STAIRS": 8, - "POLISHED_BLACKSTONE_WALL": 162, - "POLISHED_DEEPSLATE": 1, - "POLISHED_DEEPSLATE_SLAB": 3, - "POLISHED_DEEPSLATE_STAIRS": 8, - "POLISHED_DEEPSLATE_WALL": 162, - "POLISHED_DIORITE": 1, - "POLISHED_DIORITE_SLAB": 3, - "POLISHED_DIORITE_STAIRS": 8, - "POLISHED_GRANITE": 1, - "POLISHED_GRANITE_SLAB": 3, - "POLISHED_GRANITE_STAIRS": 8, - "POLISHED_TUFF": 1, - "POLISHED_TUFF_SLAB": 3, - "POLISHED_TUFF_STAIRS": 8, - "POLISHED_TUFF_WALL": 162, - "POPPY": 1, - "POTATOES": 8, - "POTION_CAULDRON": 6, - "POWERED_RAIL": 12, - "PRISMARINE": 1, - "PRISMARINE_BRICKS": 1, - "PRISMARINE_BRICKS_SLAB": 3, - "PRISMARINE_BRICKS_STAIRS": 8, - "PRISMARINE_SLAB": 3, - "PRISMARINE_STAIRS": 8, - "PRISMARINE_WALL": 162, - "PUMPKIN": 1, - "PUMPKIN_STEM": 40, - "PURPLE_TORCH": 5, - "PURPUR": 1, - "PURPUR_PILLAR": 3, - "PURPUR_SLAB": 3, - "PURPUR_STAIRS": 8, - "QUARTZ": 1, - "QUARTZ_BRICKS": 1, - "QUARTZ_PILLAR": 3, - "QUARTZ_SLAB": 3, - "QUARTZ_STAIRS": 8, - "RAIL": 10, - "RAW_COPPER": 1, - "RAW_GOLD": 1, - "RAW_IRON": 1, - "REDSTONE": 1, - "REDSTONE_COMPARATOR": 16, - "REDSTONE_LAMP": 2, - "REDSTONE_ORE": 2, - "REDSTONE_REPEATER": 32, - "REDSTONE_TORCH": 10, - "REDSTONE_WIRE": 16, - "RED_MUSHROOM": 1, - "RED_MUSHROOM_BLOCK": 11, - "RED_NETHER_BRICKS": 1, - "RED_NETHER_BRICK_SLAB": 3, - "RED_NETHER_BRICK_STAIRS": 8, - "RED_NETHER_BRICK_WALL": 162, - "RED_SAND": 1, - "RED_SANDSTONE": 1, - "RED_SANDSTONE_SLAB": 3, - "RED_SANDSTONE_STAIRS": 8, - "RED_SANDSTONE_WALL": 162, - "RED_TORCH": 5, - "RED_TULIP": 1, - "REINFORCED_DEEPSLATE": 1, - "RESERVED6": 1, - "ROSE_BUSH": 2, - "SAND": 1, - "SANDSTONE": 1, - "SANDSTONE_SLAB": 3, - "SANDSTONE_STAIRS": 8, - "SANDSTONE_WALL": 162, - "SCULK": 1, - "SEA_LANTERN": 1, - "SEA_PICKLE": 8, - "SHROOMLIGHT": 1, - "SHULKER_BOX": 1, - "SLIME": 1, - "SMALL_DRIPLEAF": 8, - "SMITHING_TABLE": 1, - "SMOKER": 8, - "SMOOTH_BASALT": 1, - "SMOOTH_QUARTZ": 1, - "SMOOTH_QUARTZ_SLAB": 3, - "SMOOTH_QUARTZ_STAIRS": 8, - "SMOOTH_RED_SANDSTONE": 1, - "SMOOTH_RED_SANDSTONE_SLAB": 3, - "SMOOTH_RED_SANDSTONE_STAIRS": 8, - "SMOOTH_SANDSTONE": 1, - "SMOOTH_SANDSTONE_SLAB": 3, - "SMOOTH_SANDSTONE_STAIRS": 8, - "SMOOTH_STONE": 1, - "SMOOTH_STONE_SLAB": 3, - "SNOW": 1, - "SNOW_LAYER": 8, - "SOUL_CAMPFIRE": 8, - "SOUL_FIRE": 1, - "SOUL_LANTERN": 2, - "SOUL_SAND": 1, - "SOUL_SOIL": 1, - "SOUL_TORCH": 5, - "SPONGE": 2, - "SPORE_BLOSSOM": 1, - "SPRUCE_BUTTON": 12, - "SPRUCE_DOOR": 32, - "SPRUCE_FENCE": 1, - "SPRUCE_FENCE_GATE": 16, - "SPRUCE_LEAVES": 4, - "SPRUCE_LOG": 6, - "SPRUCE_PLANKS": 1, - "SPRUCE_PRESSURE_PLATE": 2, - "SPRUCE_SAPLING": 2, - "SPRUCE_SIGN": 16, - "SPRUCE_SLAB": 3, - "SPRUCE_STAIRS": 8, - "SPRUCE_TRAPDOOR": 16, - "SPRUCE_WALL_SIGN": 4, - "SPRUCE_WOOD": 6, - "STAINED_CLAY": 16, - "STAINED_GLASS": 16, - "STAINED_GLASS_PANE": 16, - "STAINED_HARDENED_GLASS": 16, - "STAINED_HARDENED_GLASS_PANE": 16, - "STONE": 1, - "STONECUTTER": 4, - "STONE_BRICKS": 1, - "STONE_BRICK_SLAB": 3, - "STONE_BRICK_STAIRS": 8, - "STONE_BRICK_WALL": 162, - "STONE_BUTTON": 12, - "STONE_PRESSURE_PLATE": 2, - "STONE_SLAB": 3, - "STONE_STAIRS": 8, - "SUGARCANE": 16, - "SUNFLOWER": 2, - "SWEET_BERRY_BUSH": 4, - "TALL_GRASS": 1, - "TINTED_GLASS": 1, - "TNT": 4, - "TORCH": 5, - "TORCHFLOWER": 1, - "TORCHFLOWER_CROP": 2, - "TRAPPED_CHEST": 4, - "TRIPWIRE": 16, - "TRIPWIRE_HOOK": 16, - "TUFF": 1, - "TUFF_BRICKS": 1, - "TUFF_BRICK_SLAB": 3, - "TUFF_BRICK_STAIRS": 8, - "TUFF_BRICK_WALL": 162, - "TUFF_SLAB": 3, - "TUFF_STAIRS": 8, - "TUFF_WALL": 162, - "TWISTING_VINES": 26, - "UNDERWATER_TORCH": 5, - "VINES": 16, - "WALL_BANNER": 64, - "WALL_CORAL_FAN": 40, - "WARPED_BUTTON": 12, - "WARPED_DOOR": 32, - "WARPED_FENCE": 1, - "WARPED_FENCE_GATE": 16, - "WARPED_HYPHAE": 6, - "WARPED_PLANKS": 1, - "WARPED_PRESSURE_PLATE": 2, - "WARPED_ROOTS": 1, - "WARPED_SIGN": 16, - "WARPED_SLAB": 3, - "WARPED_STAIRS": 8, - "WARPED_STEM": 6, - "WARPED_TRAPDOOR": 16, - "WARPED_WALL_SIGN": 4, - "WARPED_WART_BLOCK": 1, - "WATER": 32, - "WATER_CAULDRON": 6, - "WEEPING_VINES": 26, - "WEIGHTED_PRESSURE_PLATE_HEAVY": 16, - "WEIGHTED_PRESSURE_PLATE_LIGHT": 16, - "WHEAT": 8, - "WHITE_TULIP": 1, - "WITHER_ROSE": 1, - "WOOL": 16 + "stateCounts": { + "ACACIA_BUTTON": 12, + "ACACIA_CEILING_CENTER_HANGING_SIGN": 16, + "ACACIA_CEILING_EDGES_HANGING_SIGN": 4, + "ACACIA_DOOR": 32, + "ACACIA_FENCE": 1, + "ACACIA_FENCE_GATE": 16, + "ACACIA_LEAVES": 4, + "ACACIA_LOG": 6, + "ACACIA_PLANKS": 1, + "ACACIA_PRESSURE_PLATE": 2, + "ACACIA_SAPLING": 2, + "ACACIA_SIGN": 16, + "ACACIA_SLAB": 3, + "ACACIA_STAIRS": 8, + "ACACIA_TRAPDOOR": 16, + "ACACIA_WALL_HANGING_SIGN": 4, + "ACACIA_WALL_SIGN": 4, + "ACACIA_WOOD": 6, + "ACTIVATOR_RAIL": 12, + "AIR": 1, + "ALLIUM": 1, + "ALL_SIDED_MUSHROOM_STEM": 1, + "AMETHYST": 1, + "AMETHYST_CLUSTER": 24, + "ANCIENT_DEBRIS": 1, + "ANDESITE": 1, + "ANDESITE_SLAB": 3, + "ANDESITE_STAIRS": 8, + "ANDESITE_WALL": 162, + "ANVIL": 12, + "AZALEA_LEAVES": 4, + "AZURE_BLUET": 1, + "BAMBOO": 12, + "BAMBOO_SAPLING": 2, + "BANNER": 256, + "BARREL": 12, + "BARRIER": 1, + "BASALT": 3, + "BEACON": 1, + "BED": 256, + "BEDROCK": 2, + "BEETROOTS": 8, + "BELL": 16, + "BIG_DRIPLEAF_HEAD": 16, + "BIG_DRIPLEAF_STEM": 4, + "BIRCH_BUTTON": 12, + "BIRCH_CEILING_CENTER_HANGING_SIGN": 16, + "BIRCH_CEILING_EDGES_HANGING_SIGN": 4, + "BIRCH_DOOR": 32, + "BIRCH_FENCE": 1, + "BIRCH_FENCE_GATE": 16, + "BIRCH_LEAVES": 4, + "BIRCH_LOG": 6, + "BIRCH_PLANKS": 1, + "BIRCH_PRESSURE_PLATE": 2, + "BIRCH_SAPLING": 2, + "BIRCH_SIGN": 16, + "BIRCH_SLAB": 3, + "BIRCH_STAIRS": 8, + "BIRCH_TRAPDOOR": 16, + "BIRCH_WALL_HANGING_SIGN": 4, + "BIRCH_WALL_SIGN": 4, + "BIRCH_WOOD": 6, + "BLACKSTONE": 1, + "BLACKSTONE_SLAB": 3, + "BLACKSTONE_STAIRS": 8, + "BLACKSTONE_WALL": 162, + "BLAST_FURNACE": 8, + "BLUE_ICE": 1, + "BLUE_ORCHID": 1, + "BLUE_TORCH": 5, + "BONE_BLOCK": 3, + "BOOKSHELF": 1, + "BREWING_STAND": 8, + "BRICKS": 1, + "BRICK_SLAB": 3, + "BRICK_STAIRS": 8, + "BRICK_WALL": 162, + "BROWN_MUSHROOM": 1, + "BROWN_MUSHROOM_BLOCK": 11, + "BUDDING_AMETHYST": 1, + "CACTUS": 16, + "CAKE": 7, + "CAKE_WITH_CANDLE": 2, + "CAKE_WITH_DYED_CANDLE": 32, + "CALCITE": 1, + "CAMPFIRE": 8, + "CANDLE": 8, + "CARPET": 16, + "CARROTS": 8, + "CARTOGRAPHY_TABLE": 1, + "CARVED_PUMPKIN": 4, + "CAULDRON": 1, + "CAVE_VINES": 104, + "CHAIN": 3, + "CHEMICAL_HEAT": 1, + "CHERRY_BUTTON": 12, + "CHERRY_CEILING_CENTER_HANGING_SIGN": 16, + "CHERRY_CEILING_EDGES_HANGING_SIGN": 4, + "CHERRY_DOOR": 32, + "CHERRY_FENCE": 1, + "CHERRY_FENCE_GATE": 16, + "CHERRY_LEAVES": 4, + "CHERRY_LOG": 6, + "CHERRY_PLANKS": 1, + "CHERRY_PRESSURE_PLATE": 2, + "CHERRY_SIGN": 16, + "CHERRY_SLAB": 3, + "CHERRY_STAIRS": 8, + "CHERRY_TRAPDOOR": 16, + "CHERRY_WALL_HANGING_SIGN": 4, + "CHERRY_WALL_SIGN": 4, + "CHERRY_WOOD": 6, + "CHEST": 4, + "CHISELED_BOOKSHELF": 256, + "CHISELED_COPPER": 8, + "CHISELED_DEEPSLATE": 1, + "CHISELED_NETHER_BRICKS": 1, + "CHISELED_POLISHED_BLACKSTONE": 1, + "CHISELED_QUARTZ": 3, + "CHISELED_RED_SANDSTONE": 1, + "CHISELED_RESIN_BRICKS": 1, + "CHISELED_SANDSTONE": 1, + "CHISELED_STONE_BRICKS": 1, + "CHISELED_TUFF": 1, + "CHISELED_TUFF_BRICKS": 1, + "CHORUS_FLOWER": 6, + "CHORUS_PLANT": 1, + "CLAY": 1, + "COAL": 1, + "COAL_ORE": 1, + "COBBLED_DEEPSLATE": 1, + "COBBLED_DEEPSLATE_SLAB": 3, + "COBBLED_DEEPSLATE_STAIRS": 8, + "COBBLED_DEEPSLATE_WALL": 162, + "COBBLESTONE": 1, + "COBBLESTONE_SLAB": 3, + "COBBLESTONE_STAIRS": 8, + "COBBLESTONE_WALL": 162, + "COBWEB": 1, + "COCOA_POD": 12, + "COMPOUND_CREATOR": 4, + "CONCRETE": 16, + "CONCRETE_POWDER": 16, + "COPPER": 8, + "COPPER_BULB": 32, + "COPPER_DOOR": 256, + "COPPER_GRATE": 8, + "COPPER_ORE": 1, + "COPPER_TRAPDOOR": 128, + "CORAL": 10, + "CORAL_BLOCK": 10, + "CORAL_FAN": 20, + "CORNFLOWER": 1, + "CRACKED_DEEPSLATE_BRICKS": 1, + "CRACKED_DEEPSLATE_TILES": 1, + "CRACKED_NETHER_BRICKS": 1, + "CRACKED_POLISHED_BLACKSTONE_BRICKS": 1, + "CRACKED_STONE_BRICKS": 1, + "CRAFTING_TABLE": 1, + "CRIMSON_BUTTON": 12, + "CRIMSON_CEILING_CENTER_HANGING_SIGN": 16, + "CRIMSON_CEILING_EDGES_HANGING_SIGN": 4, + "CRIMSON_DOOR": 32, + "CRIMSON_FENCE": 1, + "CRIMSON_FENCE_GATE": 16, + "CRIMSON_HYPHAE": 6, + "CRIMSON_PLANKS": 1, + "CRIMSON_PRESSURE_PLATE": 2, + "CRIMSON_ROOTS": 1, + "CRIMSON_SIGN": 16, + "CRIMSON_SLAB": 3, + "CRIMSON_STAIRS": 8, + "CRIMSON_STEM": 6, + "CRIMSON_TRAPDOOR": 16, + "CRIMSON_WALL_HANGING_SIGN": 4, + "CRIMSON_WALL_SIGN": 4, + "CRYING_OBSIDIAN": 1, + "CUT_COPPER": 8, + "CUT_COPPER_SLAB": 24, + "CUT_COPPER_STAIRS": 64, + "CUT_RED_SANDSTONE": 1, + "CUT_RED_SANDSTONE_SLAB": 3, + "CUT_SANDSTONE": 1, + "CUT_SANDSTONE_SLAB": 3, + "DANDELION": 1, + "DARK_OAK_BUTTON": 12, + "DARK_OAK_CEILING_CENTER_HANGING_SIGN": 16, + "DARK_OAK_CEILING_EDGES_HANGING_SIGN": 4, + "DARK_OAK_DOOR": 32, + "DARK_OAK_FENCE": 1, + "DARK_OAK_FENCE_GATE": 16, + "DARK_OAK_LEAVES": 4, + "DARK_OAK_LOG": 6, + "DARK_OAK_PLANKS": 1, + "DARK_OAK_PRESSURE_PLATE": 2, + "DARK_OAK_SAPLING": 2, + "DARK_OAK_SIGN": 16, + "DARK_OAK_SLAB": 3, + "DARK_OAK_STAIRS": 8, + "DARK_OAK_TRAPDOOR": 16, + "DARK_OAK_WALL_HANGING_SIGN": 4, + "DARK_OAK_WALL_SIGN": 4, + "DARK_OAK_WOOD": 6, + "DARK_PRISMARINE": 1, + "DARK_PRISMARINE_SLAB": 3, + "DARK_PRISMARINE_STAIRS": 8, + "DAYLIGHT_SENSOR": 32, + "DEAD_BUSH": 1, + "DEEPSLATE": 3, + "DEEPSLATE_BRICKS": 1, + "DEEPSLATE_BRICK_SLAB": 3, + "DEEPSLATE_BRICK_STAIRS": 8, + "DEEPSLATE_BRICK_WALL": 162, + "DEEPSLATE_COAL_ORE": 1, + "DEEPSLATE_COPPER_ORE": 1, + "DEEPSLATE_DIAMOND_ORE": 1, + "DEEPSLATE_EMERALD_ORE": 1, + "DEEPSLATE_GOLD_ORE": 1, + "DEEPSLATE_IRON_ORE": 1, + "DEEPSLATE_LAPIS_LAZULI_ORE": 1, + "DEEPSLATE_REDSTONE_ORE": 2, + "DEEPSLATE_TILES": 1, + "DEEPSLATE_TILE_SLAB": 3, + "DEEPSLATE_TILE_STAIRS": 8, + "DEEPSLATE_TILE_WALL": 162, + "DETECTOR_RAIL": 12, + "DIAMOND": 1, + "DIAMOND_ORE": 1, + "DIORITE": 1, + "DIORITE_SLAB": 3, + "DIORITE_STAIRS": 8, + "DIORITE_WALL": 162, + "DIRT": 3, + "DOUBLE_PITCHER_CROP": 4, + "DOUBLE_TALLGRASS": 2, + "DRAGON_EGG": 1, + "DRIED_KELP": 1, + "DYED_CANDLE": 128, + "DYED_SHULKER_BOX": 16, + "ELEMENT_ACTINIUM": 1, + "ELEMENT_ALUMINUM": 1, + "ELEMENT_AMERICIUM": 1, + "ELEMENT_ANTIMONY": 1, + "ELEMENT_ARGON": 1, + "ELEMENT_ARSENIC": 1, + "ELEMENT_ASTATINE": 1, + "ELEMENT_BARIUM": 1, + "ELEMENT_BERKELIUM": 1, + "ELEMENT_BERYLLIUM": 1, + "ELEMENT_BISMUTH": 1, + "ELEMENT_BOHRIUM": 1, + "ELEMENT_BORON": 1, + "ELEMENT_BROMINE": 1, + "ELEMENT_CADMIUM": 1, + "ELEMENT_CALCIUM": 1, + "ELEMENT_CALIFORNIUM": 1, + "ELEMENT_CARBON": 1, + "ELEMENT_CERIUM": 1, + "ELEMENT_CESIUM": 1, + "ELEMENT_CHLORINE": 1, + "ELEMENT_CHROMIUM": 1, + "ELEMENT_COBALT": 1, + "ELEMENT_CONSTRUCTOR": 4, + "ELEMENT_COPERNICIUM": 1, + "ELEMENT_COPPER": 1, + "ELEMENT_CURIUM": 1, + "ELEMENT_DARMSTADTIUM": 1, + "ELEMENT_DUBNIUM": 1, + "ELEMENT_DYSPROSIUM": 1, + "ELEMENT_EINSTEINIUM": 1, + "ELEMENT_ERBIUM": 1, + "ELEMENT_EUROPIUM": 1, + "ELEMENT_FERMIUM": 1, + "ELEMENT_FLEROVIUM": 1, + "ELEMENT_FLUORINE": 1, + "ELEMENT_FRANCIUM": 1, + "ELEMENT_GADOLINIUM": 1, + "ELEMENT_GALLIUM": 1, + "ELEMENT_GERMANIUM": 1, + "ELEMENT_GOLD": 1, + "ELEMENT_HAFNIUM": 1, + "ELEMENT_HASSIUM": 1, + "ELEMENT_HELIUM": 1, + "ELEMENT_HOLMIUM": 1, + "ELEMENT_HYDROGEN": 1, + "ELEMENT_INDIUM": 1, + "ELEMENT_IODINE": 1, + "ELEMENT_IRIDIUM": 1, + "ELEMENT_IRON": 1, + "ELEMENT_KRYPTON": 1, + "ELEMENT_LANTHANUM": 1, + "ELEMENT_LAWRENCIUM": 1, + "ELEMENT_LEAD": 1, + "ELEMENT_LITHIUM": 1, + "ELEMENT_LIVERMORIUM": 1, + "ELEMENT_LUTETIUM": 1, + "ELEMENT_MAGNESIUM": 1, + "ELEMENT_MANGANESE": 1, + "ELEMENT_MEITNERIUM": 1, + "ELEMENT_MENDELEVIUM": 1, + "ELEMENT_MERCURY": 1, + "ELEMENT_MOLYBDENUM": 1, + "ELEMENT_MOSCOVIUM": 1, + "ELEMENT_NEODYMIUM": 1, + "ELEMENT_NEON": 1, + "ELEMENT_NEPTUNIUM": 1, + "ELEMENT_NICKEL": 1, + "ELEMENT_NIHONIUM": 1, + "ELEMENT_NIOBIUM": 1, + "ELEMENT_NITROGEN": 1, + "ELEMENT_NOBELIUM": 1, + "ELEMENT_OGANESSON": 1, + "ELEMENT_OSMIUM": 1, + "ELEMENT_OXYGEN": 1, + "ELEMENT_PALLADIUM": 1, + "ELEMENT_PHOSPHORUS": 1, + "ELEMENT_PLATINUM": 1, + "ELEMENT_PLUTONIUM": 1, + "ELEMENT_POLONIUM": 1, + "ELEMENT_POTASSIUM": 1, + "ELEMENT_PRASEODYMIUM": 1, + "ELEMENT_PROMETHIUM": 1, + "ELEMENT_PROTACTINIUM": 1, + "ELEMENT_RADIUM": 1, + "ELEMENT_RADON": 1, + "ELEMENT_RHENIUM": 1, + "ELEMENT_RHODIUM": 1, + "ELEMENT_ROENTGENIUM": 1, + "ELEMENT_RUBIDIUM": 1, + "ELEMENT_RUTHENIUM": 1, + "ELEMENT_RUTHERFORDIUM": 1, + "ELEMENT_SAMARIUM": 1, + "ELEMENT_SCANDIUM": 1, + "ELEMENT_SEABORGIUM": 1, + "ELEMENT_SELENIUM": 1, + "ELEMENT_SILICON": 1, + "ELEMENT_SILVER": 1, + "ELEMENT_SODIUM": 1, + "ELEMENT_STRONTIUM": 1, + "ELEMENT_SULFUR": 1, + "ELEMENT_TANTALUM": 1, + "ELEMENT_TECHNETIUM": 1, + "ELEMENT_TELLURIUM": 1, + "ELEMENT_TENNESSINE": 1, + "ELEMENT_TERBIUM": 1, + "ELEMENT_THALLIUM": 1, + "ELEMENT_THORIUM": 1, + "ELEMENT_THULIUM": 1, + "ELEMENT_TIN": 1, + "ELEMENT_TITANIUM": 1, + "ELEMENT_TUNGSTEN": 1, + "ELEMENT_URANIUM": 1, + "ELEMENT_VANADIUM": 1, + "ELEMENT_XENON": 1, + "ELEMENT_YTTERBIUM": 1, + "ELEMENT_YTTRIUM": 1, + "ELEMENT_ZERO": 1, + "ELEMENT_ZINC": 1, + "ELEMENT_ZIRCONIUM": 1, + "EMERALD": 1, + "EMERALD_ORE": 1, + "ENCHANTING_TABLE": 1, + "ENDER_CHEST": 4, + "END_PORTAL_FRAME": 8, + "END_ROD": 6, + "END_STONE": 1, + "END_STONE_BRICKS": 1, + "END_STONE_BRICK_SLAB": 3, + "END_STONE_BRICK_STAIRS": 8, + "END_STONE_BRICK_WALL": 162, + "FAKE_WOODEN_SLAB": 3, + "FARMLAND": 1304, + "FERN": 1, + "FIRE": 16, + "FLETCHING_TABLE": 1, + "FLOWERING_AZALEA_LEAVES": 4, + "FLOWER_POT": 1, + "FROGLIGHT": 9, + "FROSTED_ICE": 4, + "FURNACE": 8, + "GILDED_BLACKSTONE": 1, + "GLASS": 1, + "GLASS_PANE": 1, + "GLAZED_TERRACOTTA": 64, + "GLOWING_ITEM_FRAME": 12, + "GLOWING_OBSIDIAN": 1, + "GLOWSTONE": 1, + "GLOW_LICHEN": 64, + "GOLD": 1, + "GOLD_ORE": 1, + "GRANITE": 1, + "GRANITE_SLAB": 3, + "GRANITE_STAIRS": 8, + "GRANITE_WALL": 162, + "GRASS": 1, + "GRASS_PATH": 1, + "GRAVEL": 1, + "GREEN_TORCH": 5, + "HANGING_ROOTS": 1, + "HARDENED_CLAY": 1, + "HARDENED_GLASS": 1, + "HARDENED_GLASS_PANE": 1, + "HAY_BALE": 3, + "HONEYCOMB": 1, + "HOPPER": 10, + "ICE": 1, + "INFESTED_CHISELED_STONE_BRICK": 1, + "INFESTED_COBBLESTONE": 1, + "INFESTED_CRACKED_STONE_BRICK": 1, + "INFESTED_MOSSY_STONE_BRICK": 1, + "INFESTED_STONE": 1, + "INFESTED_STONE_BRICK": 1, + "INFO_UPDATE": 1, + "INFO_UPDATE2": 1, + "INVISIBLE_BEDROCK": 1, + "IRON": 1, + "IRON_BARS": 1, + "IRON_DOOR": 32, + "IRON_ORE": 1, + "IRON_TRAPDOOR": 16, + "ITEM_FRAME": 12, + "JUKEBOX": 1, + "JUNGLE_BUTTON": 12, + "JUNGLE_CEILING_CENTER_HANGING_SIGN": 16, + "JUNGLE_CEILING_EDGES_HANGING_SIGN": 4, + "JUNGLE_DOOR": 32, + "JUNGLE_FENCE": 1, + "JUNGLE_FENCE_GATE": 16, + "JUNGLE_LEAVES": 4, + "JUNGLE_LOG": 6, + "JUNGLE_PLANKS": 1, + "JUNGLE_PRESSURE_PLATE": 2, + "JUNGLE_SAPLING": 2, + "JUNGLE_SIGN": 16, + "JUNGLE_SLAB": 3, + "JUNGLE_STAIRS": 8, + "JUNGLE_TRAPDOOR": 16, + "JUNGLE_WALL_HANGING_SIGN": 4, + "JUNGLE_WALL_SIGN": 4, + "JUNGLE_WOOD": 6, + "LAB_TABLE": 4, + "LADDER": 4, + "LANTERN": 2, + "LAPIS_LAZULI": 1, + "LAPIS_LAZULI_ORE": 1, + "LARGE_FERN": 2, + "LAVA": 32, + "LAVA_CAULDRON": 6, + "LECTERN": 8, + "LEGACY_STONECUTTER": 1, + "LEVER": 16, + "LIGHT": 16, + "LIGHTNING_ROD": 6, + "LILAC": 2, + "LILY_OF_THE_VALLEY": 1, + "LILY_PAD": 1, + "LIT_PUMPKIN": 4, + "LOOM": 4, + "MAGMA": 1, + "MANGROVE_BUTTON": 12, + "MANGROVE_CEILING_CENTER_HANGING_SIGN": 16, + "MANGROVE_CEILING_EDGES_HANGING_SIGN": 4, + "MANGROVE_DOOR": 32, + "MANGROVE_FENCE": 1, + "MANGROVE_FENCE_GATE": 16, + "MANGROVE_LEAVES": 4, + "MANGROVE_LOG": 6, + "MANGROVE_PLANKS": 1, + "MANGROVE_PRESSURE_PLATE": 2, + "MANGROVE_ROOTS": 1, + "MANGROVE_SIGN": 16, + "MANGROVE_SLAB": 3, + "MANGROVE_STAIRS": 8, + "MANGROVE_TRAPDOOR": 16, + "MANGROVE_WALL_HANGING_SIGN": 4, + "MANGROVE_WALL_SIGN": 4, + "MANGROVE_WOOD": 6, + "MATERIAL_REDUCER": 4, + "MELON": 1, + "MELON_STEM": 40, + "MOB_HEAD": 35, + "MONSTER_SPAWNER": 1, + "MOSSY_COBBLESTONE": 1, + "MOSSY_COBBLESTONE_SLAB": 3, + "MOSSY_COBBLESTONE_STAIRS": 8, + "MOSSY_COBBLESTONE_WALL": 162, + "MOSSY_STONE_BRICKS": 1, + "MOSSY_STONE_BRICK_SLAB": 3, + "MOSSY_STONE_BRICK_STAIRS": 8, + "MOSSY_STONE_BRICK_WALL": 162, + "MUD": 1, + "MUDDY_MANGROVE_ROOTS": 3, + "MUD_BRICKS": 1, + "MUD_BRICK_SLAB": 3, + "MUD_BRICK_STAIRS": 8, + "MUD_BRICK_WALL": 162, + "MUSHROOM_STEM": 1, + "MYCELIUM": 1, + "NETHERITE": 1, + "NETHERRACK": 1, + "NETHER_BRICKS": 1, + "NETHER_BRICK_FENCE": 1, + "NETHER_BRICK_SLAB": 3, + "NETHER_BRICK_STAIRS": 8, + "NETHER_BRICK_WALL": 162, + "NETHER_GOLD_ORE": 1, + "NETHER_PORTAL": 2, + "NETHER_QUARTZ_ORE": 1, + "NETHER_REACTOR_CORE": 1, + "NETHER_WART": 4, + "NETHER_WART_BLOCK": 1, + "NOTE_BLOCK": 1, + "OAK_BUTTON": 12, + "OAK_CEILING_CENTER_HANGING_SIGN": 16, + "OAK_CEILING_EDGES_HANGING_SIGN": 4, + "OAK_DOOR": 32, + "OAK_FENCE": 1, + "OAK_FENCE_GATE": 16, + "OAK_LEAVES": 4, + "OAK_LOG": 6, + "OAK_PLANKS": 1, + "OAK_PRESSURE_PLATE": 2, + "OAK_SAPLING": 2, + "OAK_SIGN": 16, + "OAK_SLAB": 3, + "OAK_STAIRS": 8, + "OAK_TRAPDOOR": 16, + "OAK_WALL_HANGING_SIGN": 4, + "OAK_WALL_SIGN": 4, + "OAK_WOOD": 6, + "OBSIDIAN": 1, + "OMINOUS_BANNER": 16, + "OMINOUS_WALL_BANNER": 4, + "ORANGE_TULIP": 1, + "OXEYE_DAISY": 1, + "PACKED_ICE": 1, + "PACKED_MUD": 1, + "PALE_OAK_BUTTON": 12, + "PALE_OAK_CEILING_CENTER_HANGING_SIGN": 16, + "PALE_OAK_CEILING_EDGES_HANGING_SIGN": 4, + "PALE_OAK_DOOR": 32, + "PALE_OAK_FENCE": 1, + "PALE_OAK_FENCE_GATE": 16, + "PALE_OAK_LEAVES": 4, + "PALE_OAK_LOG": 6, + "PALE_OAK_PLANKS": 1, + "PALE_OAK_PRESSURE_PLATE": 2, + "PALE_OAK_SIGN": 16, + "PALE_OAK_SLAB": 3, + "PALE_OAK_STAIRS": 8, + "PALE_OAK_TRAPDOOR": 16, + "PALE_OAK_WALL_HANGING_SIGN": 4, + "PALE_OAK_WALL_SIGN": 4, + "PALE_OAK_WOOD": 6, + "PEONY": 2, + "PINK_PETALS": 16, + "PINK_TULIP": 1, + "PITCHER_CROP": 3, + "PITCHER_PLANT": 2, + "PODZOL": 1, + "POLISHED_ANDESITE": 1, + "POLISHED_ANDESITE_SLAB": 3, + "POLISHED_ANDESITE_STAIRS": 8, + "POLISHED_BASALT": 3, + "POLISHED_BLACKSTONE": 1, + "POLISHED_BLACKSTONE_BRICKS": 1, + "POLISHED_BLACKSTONE_BRICK_SLAB": 3, + "POLISHED_BLACKSTONE_BRICK_STAIRS": 8, + "POLISHED_BLACKSTONE_BRICK_WALL": 162, + "POLISHED_BLACKSTONE_BUTTON": 12, + "POLISHED_BLACKSTONE_PRESSURE_PLATE": 2, + "POLISHED_BLACKSTONE_SLAB": 3, + "POLISHED_BLACKSTONE_STAIRS": 8, + "POLISHED_BLACKSTONE_WALL": 162, + "POLISHED_DEEPSLATE": 1, + "POLISHED_DEEPSLATE_SLAB": 3, + "POLISHED_DEEPSLATE_STAIRS": 8, + "POLISHED_DEEPSLATE_WALL": 162, + "POLISHED_DIORITE": 1, + "POLISHED_DIORITE_SLAB": 3, + "POLISHED_DIORITE_STAIRS": 8, + "POLISHED_GRANITE": 1, + "POLISHED_GRANITE_SLAB": 3, + "POLISHED_GRANITE_STAIRS": 8, + "POLISHED_TUFF": 1, + "POLISHED_TUFF_SLAB": 3, + "POLISHED_TUFF_STAIRS": 8, + "POLISHED_TUFF_WALL": 162, + "POPPY": 1, + "POTATOES": 8, + "POTION_CAULDRON": 6, + "POWERED_RAIL": 12, + "PRISMARINE": 1, + "PRISMARINE_BRICKS": 1, + "PRISMARINE_BRICKS_SLAB": 3, + "PRISMARINE_BRICKS_STAIRS": 8, + "PRISMARINE_SLAB": 3, + "PRISMARINE_STAIRS": 8, + "PRISMARINE_WALL": 162, + "PUMPKIN": 1, + "PUMPKIN_STEM": 40, + "PURPLE_TORCH": 5, + "PURPUR": 1, + "PURPUR_PILLAR": 3, + "PURPUR_SLAB": 3, + "PURPUR_STAIRS": 8, + "QUARTZ": 1, + "QUARTZ_BRICKS": 1, + "QUARTZ_PILLAR": 3, + "QUARTZ_SLAB": 3, + "QUARTZ_STAIRS": 8, + "RAIL": 10, + "RAW_COPPER": 1, + "RAW_GOLD": 1, + "RAW_IRON": 1, + "REDSTONE": 1, + "REDSTONE_COMPARATOR": 16, + "REDSTONE_LAMP": 2, + "REDSTONE_ORE": 2, + "REDSTONE_REPEATER": 32, + "REDSTONE_TORCH": 10, + "REDSTONE_WIRE": 16, + "RED_MUSHROOM": 1, + "RED_MUSHROOM_BLOCK": 11, + "RED_NETHER_BRICKS": 1, + "RED_NETHER_BRICK_SLAB": 3, + "RED_NETHER_BRICK_STAIRS": 8, + "RED_NETHER_BRICK_WALL": 162, + "RED_SAND": 1, + "RED_SANDSTONE": 1, + "RED_SANDSTONE_SLAB": 3, + "RED_SANDSTONE_STAIRS": 8, + "RED_SANDSTONE_WALL": 162, + "RED_TORCH": 5, + "RED_TULIP": 1, + "REINFORCED_DEEPSLATE": 1, + "RESERVED6": 1, + "RESIN": 1, + "RESIN_BRICKS": 1, + "RESIN_BRICK_SLAB": 3, + "RESIN_BRICK_STAIRS": 8, + "RESIN_BRICK_WALL": 162, + "RESIN_CLUMP": 64, + "RESPAWN_ANCHOR": 5, + "ROSE_BUSH": 2, + "SAND": 1, + "SANDSTONE": 1, + "SANDSTONE_SLAB": 3, + "SANDSTONE_STAIRS": 8, + "SANDSTONE_WALL": 162, + "SCULK": 1, + "SEA_LANTERN": 1, + "SEA_PICKLE": 8, + "SHROOMLIGHT": 1, + "SHULKER_BOX": 1, + "SLIME": 1, + "SMALL_DRIPLEAF": 8, + "SMITHING_TABLE": 1, + "SMOKER": 8, + "SMOOTH_BASALT": 1, + "SMOOTH_QUARTZ": 1, + "SMOOTH_QUARTZ_SLAB": 3, + "SMOOTH_QUARTZ_STAIRS": 8, + "SMOOTH_RED_SANDSTONE": 1, + "SMOOTH_RED_SANDSTONE_SLAB": 3, + "SMOOTH_RED_SANDSTONE_STAIRS": 8, + "SMOOTH_SANDSTONE": 1, + "SMOOTH_SANDSTONE_SLAB": 3, + "SMOOTH_SANDSTONE_STAIRS": 8, + "SMOOTH_STONE": 1, + "SMOOTH_STONE_SLAB": 3, + "SNOW": 1, + "SNOW_LAYER": 8, + "SOUL_CAMPFIRE": 8, + "SOUL_FIRE": 1, + "SOUL_LANTERN": 2, + "SOUL_SAND": 1, + "SOUL_SOIL": 1, + "SOUL_TORCH": 5, + "SPONGE": 2, + "SPORE_BLOSSOM": 1, + "SPRUCE_BUTTON": 12, + "SPRUCE_CEILING_CENTER_HANGING_SIGN": 16, + "SPRUCE_CEILING_EDGES_HANGING_SIGN": 4, + "SPRUCE_DOOR": 32, + "SPRUCE_FENCE": 1, + "SPRUCE_FENCE_GATE": 16, + "SPRUCE_LEAVES": 4, + "SPRUCE_LOG": 6, + "SPRUCE_PLANKS": 1, + "SPRUCE_PRESSURE_PLATE": 2, + "SPRUCE_SAPLING": 2, + "SPRUCE_SIGN": 16, + "SPRUCE_SLAB": 3, + "SPRUCE_STAIRS": 8, + "SPRUCE_TRAPDOOR": 16, + "SPRUCE_WALL_HANGING_SIGN": 4, + "SPRUCE_WALL_SIGN": 4, + "SPRUCE_WOOD": 6, + "STAINED_CLAY": 16, + "STAINED_GLASS": 16, + "STAINED_GLASS_PANE": 16, + "STAINED_HARDENED_GLASS": 16, + "STAINED_HARDENED_GLASS_PANE": 16, + "STONE": 1, + "STONECUTTER": 4, + "STONE_BRICKS": 1, + "STONE_BRICK_SLAB": 3, + "STONE_BRICK_STAIRS": 8, + "STONE_BRICK_WALL": 162, + "STONE_BUTTON": 12, + "STONE_PRESSURE_PLATE": 2, + "STONE_SLAB": 3, + "STONE_STAIRS": 8, + "SUGARCANE": 16, + "SUNFLOWER": 2, + "SWEET_BERRY_BUSH": 4, + "TALL_GRASS": 1, + "TINTED_GLASS": 1, + "TNT": 4, + "TORCH": 5, + "TORCHFLOWER": 1, + "TORCHFLOWER_CROP": 2, + "TRAPPED_CHEST": 4, + "TRIPWIRE": 16, + "TRIPWIRE_HOOK": 16, + "TUFF": 1, + "TUFF_BRICKS": 1, + "TUFF_BRICK_SLAB": 3, + "TUFF_BRICK_STAIRS": 8, + "TUFF_BRICK_WALL": 162, + "TUFF_SLAB": 3, + "TUFF_STAIRS": 8, + "TUFF_WALL": 162, + "TWISTING_VINES": 26, + "UNDERWATER_TORCH": 5, + "VINES": 16, + "WALL_BANNER": 64, + "WALL_CORAL_FAN": 40, + "WARPED_BUTTON": 12, + "WARPED_CEILING_CENTER_HANGING_SIGN": 16, + "WARPED_CEILING_EDGES_HANGING_SIGN": 4, + "WARPED_DOOR": 32, + "WARPED_FENCE": 1, + "WARPED_FENCE_GATE": 16, + "WARPED_HYPHAE": 6, + "WARPED_PLANKS": 1, + "WARPED_PRESSURE_PLATE": 2, + "WARPED_ROOTS": 1, + "WARPED_SIGN": 16, + "WARPED_SLAB": 3, + "WARPED_STAIRS": 8, + "WARPED_STEM": 6, + "WARPED_TRAPDOOR": 16, + "WARPED_WALL_HANGING_SIGN": 4, + "WARPED_WALL_SIGN": 4, + "WARPED_WART_BLOCK": 1, + "WATER": 32, + "WATER_CAULDRON": 6, + "WEEPING_VINES": 26, + "WEIGHTED_PRESSURE_PLATE_HEAVY": 16, + "WEIGHTED_PRESSURE_PLATE_LIGHT": 16, + "WHEAT": 8, + "WHITE_TULIP": 1, + "WITHER_ROSE": 1, + "WOOL": 16 + }, + "tiles": { + "ACACIA_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "ACACIA_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "ACACIA_SIGN": "pocketmine\\block\\tile\\Sign", + "ACACIA_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "ACACIA_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "BANNER": "pocketmine\\block\\tile\\Banner", + "BARREL": "pocketmine\\block\\tile\\Barrel", + "BEACON": "pocketmine\\block\\tile\\Beacon", + "BED": "pocketmine\\block\\tile\\Bed", + "BELL": "pocketmine\\block\\tile\\Bell", + "BIRCH_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "BIRCH_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "BIRCH_SIGN": "pocketmine\\block\\tile\\Sign", + "BIRCH_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "BIRCH_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "BLAST_FURNACE": "pocketmine\\block\\tile\\BlastFurnace", + "BREWING_STAND": "pocketmine\\block\\tile\\BrewingStand", + "CAMPFIRE": "pocketmine\\block\\tile\\Campfire", + "CAULDRON": "pocketmine\\block\\tile\\Cauldron", + "CHERRY_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CHERRY_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CHERRY_SIGN": "pocketmine\\block\\tile\\Sign", + "CHERRY_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CHERRY_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "CHEST": "pocketmine\\block\\tile\\Chest", + "CHISELED_BOOKSHELF": "pocketmine\\block\\tile\\ChiseledBookshelf", + "CRIMSON_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CRIMSON_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CRIMSON_SIGN": "pocketmine\\block\\tile\\Sign", + "CRIMSON_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "CRIMSON_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "DARK_OAK_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "DARK_OAK_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "DARK_OAK_SIGN": "pocketmine\\block\\tile\\Sign", + "DARK_OAK_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "DARK_OAK_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "DAYLIGHT_SENSOR": "pocketmine\\block\\tile\\DaylightSensor", + "DYED_SHULKER_BOX": "pocketmine\\block\\tile\\ShulkerBox", + "ENCHANTING_TABLE": "pocketmine\\block\\tile\\EnchantTable", + "ENDER_CHEST": "pocketmine\\block\\tile\\EnderChest", + "FLOWER_POT": "pocketmine\\block\\tile\\FlowerPot", + "FURNACE": "pocketmine\\block\\tile\\NormalFurnace", + "GLOWING_ITEM_FRAME": "pocketmine\\block\\tile\\GlowingItemFrame", + "HOPPER": "pocketmine\\block\\tile\\Hopper", + "ITEM_FRAME": "pocketmine\\block\\tile\\ItemFrame", + "JUKEBOX": "pocketmine\\block\\tile\\Jukebox", + "JUNGLE_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "JUNGLE_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "JUNGLE_SIGN": "pocketmine\\block\\tile\\Sign", + "JUNGLE_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "JUNGLE_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "LAVA_CAULDRON": "pocketmine\\block\\tile\\Cauldron", + "LECTERN": "pocketmine\\block\\tile\\Lectern", + "MANGROVE_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "MANGROVE_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "MANGROVE_SIGN": "pocketmine\\block\\tile\\Sign", + "MANGROVE_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "MANGROVE_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "MOB_HEAD": "pocketmine\\block\\tile\\MobHead", + "MONSTER_SPAWNER": "pocketmine\\block\\tile\\MonsterSpawner", + "NOTE_BLOCK": "pocketmine\\block\\tile\\Note", + "OAK_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "OAK_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "OAK_SIGN": "pocketmine\\block\\tile\\Sign", + "OAK_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "OAK_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "OMINOUS_BANNER": "pocketmine\\block\\tile\\Banner", + "OMINOUS_WALL_BANNER": "pocketmine\\block\\tile\\Banner", + "PALE_OAK_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "PALE_OAK_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "PALE_OAK_SIGN": "pocketmine\\block\\tile\\Sign", + "PALE_OAK_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "PALE_OAK_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "POTION_CAULDRON": "pocketmine\\block\\tile\\Cauldron", + "REDSTONE_COMPARATOR": "pocketmine\\block\\tile\\Comparator", + "SHULKER_BOX": "pocketmine\\block\\tile\\ShulkerBox", + "SMOKER": "pocketmine\\block\\tile\\Smoker", + "SOUL_CAMPFIRE": "pocketmine\\block\\tile\\Campfire", + "SPRUCE_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "SPRUCE_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "SPRUCE_SIGN": "pocketmine\\block\\tile\\Sign", + "SPRUCE_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "SPRUCE_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "TRAPPED_CHEST": "pocketmine\\block\\tile\\Chest", + "WALL_BANNER": "pocketmine\\block\\tile\\Banner", + "WARPED_CEILING_CENTER_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "WARPED_CEILING_EDGES_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "WARPED_SIGN": "pocketmine\\block\\tile\\Sign", + "WARPED_WALL_HANGING_SIGN": "pocketmine\\block\\tile\\HangingSign", + "WARPED_WALL_SIGN": "pocketmine\\block\\tile\\Sign", + "WATER_CAULDRON": "pocketmine\\block\\tile\\Cauldron" + } } \ No newline at end of file diff --git a/tests/phpunit/block/regenerate_consistency_check.php b/tests/phpunit/block/regenerate_consistency_check.php index e86f70d70..eb4ccf6c8 100644 --- a/tests/phpunit/block/regenerate_consistency_check.php +++ b/tests/phpunit/block/regenerate_consistency_check.php @@ -28,11 +28,11 @@ require dirname(__DIR__, 3) . '/vendor/autoload.php'; /* This script needs to be re-run after any intentional blockfactory change (adding or removing a block state). */ -$newTable = BlockTest::computeConsistencyCheckTable(RuntimeBlockStateRegistry::getInstance()); +[$newTable, $newTiles] = BlockTest::computeConsistencyCheckTable(RuntimeBlockStateRegistry::getInstance()); $oldTablePath = __DIR__ . '/block_factory_consistency_check.json'; if(file_exists($oldTablePath)){ - $errors = BlockTest::computeConsistencyCheckDiff($oldTablePath, $newTable); + $errors = BlockTest::computeConsistencyCheckDiff($oldTablePath, $newTable, $newTiles); if(count($errors) > 0){ echo count($errors) . " changes detected:\n"; @@ -47,5 +47,6 @@ if(file_exists($oldTablePath)){ } ksort($newTable, SORT_STRING); +ksort($newTiles, SORT_STRING); -file_put_contents($oldTablePath, json_encode($newTable, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); +file_put_contents($oldTablePath, json_encode(["stateCounts" => $newTable, "tiles" => $newTiles], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); diff --git a/tests/phpunit/data/bedrock/block/convert/BlockSerializerDeserializerTest.php b/tests/phpunit/data/bedrock/block/convert/BlockSerializerDeserializerTest.php index a47a9b155..c0f850027 100644 --- a/tests/phpunit/data/bedrock/block/convert/BlockSerializerDeserializerTest.php +++ b/tests/phpunit/data/bedrock/block/convert/BlockSerializerDeserializerTest.php @@ -42,6 +42,8 @@ final class BlockSerializerDeserializerTest extends TestCase{ public function setUp() : void{ $this->deserializer = new BlockStateToObjectDeserializer(); $this->serializer = new BlockObjectToStateSerializer(); + $registrar = new BlockSerializerDeserializerRegistrar($this->deserializer, $this->serializer); + VanillaBlockMappings::init($registrar); } public function testAllKnownBlockStatesSerializableAndDeserializable() : void{ @@ -49,16 +51,21 @@ final class BlockSerializerDeserializerTest extends TestCase{ try{ $blockStateData = $this->serializer->serializeBlock($block); }catch(BlockStateSerializeException $e){ - self::fail($e->getMessage()); + self::fail("Failed to serialize " . $block->getName() . ": " . $e->getMessage()); } try{ $newBlock = $this->deserializer->deserializeBlock($blockStateData); }catch(BlockStateDeserializeException $e){ - self::fail($e->getMessage()); + self::fail("Failed to deserialize " . $blockStateData->getName() . ": " . $e->getMessage() . " with data " . $blockStateData->toNbt()); } - if($block->getTypeId() === BlockTypeIds::POTION_CAULDRON){ - //this pretends to be a water cauldron in the blockstate, and stores its actual data in the blockentity + if(match ($block->getTypeId()) { + BlockTypeIds::POTION_CAULDRON, + BlockTypeIds::OMINOUS_BANNER, + BlockTypeIds::OMINOUS_WALL_BANNER => true, + default => false + }){ + //these pretend to be something else in the blockstate, and the variant switching is done via block entity data continue; } diff --git a/tests/phpunit/event/HandlerListManagerTest.php b/tests/phpunit/event/HandlerListManagerTest.php index c61043dab..5b4f6babc 100644 --- a/tests/phpunit/event/HandlerListManagerTest.php +++ b/tests/phpunit/event/HandlerListManagerTest.php @@ -35,12 +35,12 @@ class HandlerListManagerTest extends TestCase{ /** * @var \Closure - * @phpstan-var \Closure(\ReflectionClass) : bool + * @phpstan-var \Closure(\ReflectionClass) : bool */ private $isValidFunc; /** * @var \Closure - * @phpstan-var \Closure(\ReflectionClass) : ?\ReflectionClass + * @phpstan-var \Closure(\ReflectionClass) : ?\ReflectionClass */ private $resolveParentFunc; @@ -53,7 +53,7 @@ class HandlerListManagerTest extends TestCase{ /** * @return \Generator|mixed[][] - * @phpstan-return \Generator, bool, string}, void, void> + * @phpstan-return \Generator, bool, string}, void, void> */ public static function isValidClassProvider() : \Generator{ yield [new \ReflectionClass(Event::class), false, "event base should not be handleable"]; @@ -65,7 +65,7 @@ class HandlerListManagerTest extends TestCase{ /** * @dataProvider isValidClassProvider * - * @phpstan-param \ReflectionClass $class + * @phpstan-param \ReflectionClass $class */ public function testIsValidClass(\ReflectionClass $class, bool $isValid, string $reason) : void{ self::assertSame($isValid, ($this->isValidFunc)($class), $reason); @@ -73,7 +73,7 @@ class HandlerListManagerTest extends TestCase{ /** * @return \Generator|\ReflectionClass[][] - * @phpstan-return \Generator, \ReflectionClass|null}, void, void> + * @phpstan-return \Generator, \ReflectionClass|null}, void, void> */ public static function resolveParentClassProvider() : \Generator{ yield [new \ReflectionClass(TestConcreteExtendsAllowHandleEvent::class), new \ReflectionClass(TestAbstractAllowHandleEvent::class)]; @@ -85,8 +85,8 @@ class HandlerListManagerTest extends TestCase{ /** * @dataProvider resolveParentClassProvider * - * @phpstan-param \ReflectionClass $class - * @phpstan-param \ReflectionClass|null $expect + * @phpstan-param \ReflectionClass $class + * @phpstan-param \ReflectionClass|null $expect */ public function testResolveParentClass(\ReflectionClass $class, ?\ReflectionClass $expect) : void{ if($expect === null){ diff --git a/tests/phpunit/item/ItemTypeIdsTest.php b/tests/phpunit/item/ItemTypeIdsTest.php index 7336780b3..a30489f07 100644 --- a/tests/phpunit/item/ItemTypeIdsTest.php +++ b/tests/phpunit/item/ItemTypeIdsTest.php @@ -35,8 +35,12 @@ class ItemTypeIdsTest extends TestCase{ $constants = $reflect->getConstants(); unset($constants['FIRST_UNUSED_ITEM_ID']); + self::assertNotEmpty($constants, "We should never have zero type IDs"); - self::assertSame($reflect->getConstant('FIRST_UNUSED_ITEM_ID'), max($constants) + 1, "FIRST_UNUSED_ITEM_ID must be one higher than the highest fixed type ID"); + $max = max($constants); + self::assertIsInt($max, "Max type ID should always be an integer"); + + self::assertSame($reflect->getConstant('FIRST_UNUSED_ITEM_ID'), $max + 1, "FIRST_UNUSED_ITEM_ID must be one higher than the highest fixed type ID"); } public function testNoDuplicates() : void{ diff --git a/tests/phpunit/scheduler/AsyncPoolTest.php b/tests/phpunit/scheduler/AsyncPoolTest.php index 53ec15c12..88985cc39 100644 --- a/tests/phpunit/scheduler/AsyncPoolTest.php +++ b/tests/phpunit/scheduler/AsyncPoolTest.php @@ -71,6 +71,7 @@ class AsyncPoolTest extends TestCase{ } public function testThreadSafeSetResult() : void{ + /** @phpstan-var PromiseResolver> $resolver */ $resolver = new PromiseResolver(); $resolver->getPromise()->onCompletion( function(ThreadSafeArray $result) : void{ diff --git a/tests/phpunit/utils/CloningRegistryTraitTest.php b/tests/phpunit/utils/CloningRegistryTraitTest.php index e3b53ecb5..3a1e6a5e0 100644 --- a/tests/phpunit/utils/CloningRegistryTraitTest.php +++ b/tests/phpunit/utils/CloningRegistryTraitTest.php @@ -47,7 +47,7 @@ final class CloningRegistryTraitTest extends TestCase{ public function testGetAllClone() : void{ $list1 = TestCloningRegistry::getAll(); $list2 = TestCloningRegistry::getAll(); - foreach(Utils::promoteKeys($list1) as $k => $member){ + foreach(Utils::stringifyKeys($list1) as $k => $member){ self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members"); } } diff --git a/tests/phpunit/utils/fixtures/TestTrait.php b/tests/phpunit/utils/fixtures/TestTrait.php index bc32c0cff..3e749c0b1 100644 --- a/tests/phpunit/utils/fixtures/TestTrait.php +++ b/tests/phpunit/utils/fixtures/TestTrait.php @@ -23,6 +23,6 @@ declare(strict_types=1); namespace pocketmine\utils\fixtures; -trait TestTrait{ +trait TestTrait{ // @phpstan-ignore trait.unused } diff --git a/tests/plugins/DevTools b/tests/plugins/DevTools index c6dca357c..a030d39e5 160000 --- a/tests/plugins/DevTools +++ b/tests/plugins/DevTools @@ -1 +1 @@ -Subproject commit c6dca357c7e8a37ce3479a1bedfe849451e072e3 +Subproject commit a030d39e51d267b5cd7e09069844a06910072ae7 diff --git a/tools/blockstate-upgrade-schema-utils.php b/tools/blockstate-upgrade-schema-utils.php index b7a9a4169..80a79bd89 100644 --- a/tools/blockstate-upgrade-schema-utils.php +++ b/tools/blockstate-upgrade-schema-utils.php @@ -51,8 +51,10 @@ use function array_unique; use function array_values; use function count; use function dirname; +use function fclose; use function file_exists; use function file_put_contents; +use function fopen; use function fwrite; use function get_class; use function get_debug_type; @@ -352,6 +354,9 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra * @param string[] $strings */ function findCommonPrefix(array $strings) : string{ + if(count($strings) === 0){ + return ""; + } sort($strings, SORT_STRING); $first = $strings[array_key_first($strings)]; @@ -523,10 +528,12 @@ function processRemappedStates(array $upgradeTable) : array{ } } } + $orderedUnchanged = []; foreach(Utils::stringifyKeys($unchangedStatesByNewName) as $newName => $unchangedStates){ - ksort($unchangedStates); - $unchangedStatesByNewName[$newName] = $unchangedStates; + sort($unchangedStates); + $orderedUnchanged[$newName] = $unchangedStates; } + $unchangedStatesByNewName = $orderedUnchanged; $notFlattenedProperties = []; @@ -656,7 +663,8 @@ function processRemappedStates(array $upgradeTable) : array{ usort($list, function(BlockStateUpgradeSchemaBlockRemap $a, BlockStateUpgradeSchemaBlockRemap $b) : int{ return count($b->oldState) <=> count($a->oldState); }); - return array_values($list); + //usort discards keys, so this is already a list + return $list; } /** @@ -882,6 +890,44 @@ function cmdUpdateAll(array $argv) : int{ return 0; } +/** + * @param string[] $argv + */ +function cmdDumpTable(array $argv) : int{ + $tableFile = $argv[2]; + $outputFile = $argv[3]; + + $output = fopen($outputFile, 'wb'); + if($output === false){ + fwrite(STDERR, "Failed to open output file: $outputFile\n"); + return 1; + } + + $table = loadUpgradeTableFromFile($tableFile, reverse: false); + ksort($table, SORT_STRING); + + foreach(Utils::stringifyKeys($table) as $oldName => $mappings){ + fwrite($output, "---------- MAPPING LIST: $oldName ----------\n"); + foreach($mappings as $mapping){ + $oldNbt = $mapping->old->toVanillaNbt(); + $oldNbt->setInt("version", $mapping->new->getVersion()); + + //intentionally not reused result of toVanillaNbt otherwise output wouldn't include version + fwrite($output, "OLD: " . $mapping->old->toVanillaNbt() . "\n"); + if(!$oldNbt->equals($mapping->new->toVanillaNbt())){ + fwrite($output, "NEW: " . $mapping->new->toVanillaNbt() . "\n"); + }else{ + fwrite($output, "NEW: version bump only (" . $mapping->new->getVersion() . ")\n"); + } + fwrite($output, "-----\n"); + } + } + + fclose($output); + \GlobalLogger::get()->info("Table dump file $outputFile generated successfully."); + return 0; +} + /** * @param string[] $argv */ @@ -890,7 +936,8 @@ function main(array $argv) : int{ "generate" => [["palette upgrade table file", "schema output file"], cmdGenerate(...)], "test" => [["palette upgrade table file", "schema output file"], cmdTest(...)], "update" => [["schema input file", "old palette file", "updated schema output file"], cmdUpdate(...)], - "update-all" => [["schema folder", "path to BlockPaletteArchive"], cmdUpdateAll(...)] + "update-all" => [["schema folder", "path to BlockPaletteArchive"], cmdUpdateAll(...)], + "dump-table" => [["palette upgrade table file", "txt output file"], cmdDumpTable(...)] ]; $selected = $argv[1] ?? null; diff --git a/tools/compact-regions.php b/tools/compact-regions.php index 04ac3f0c9..ab80792d3 100644 --- a/tools/compact-regions.php +++ b/tools/compact-regions.php @@ -76,7 +76,12 @@ function find_regions_recursive(string $dir, array &$files) : void{ in_array(pathinfo($fullPath, PATHINFO_EXTENSION), SUPPORTED_EXTENSIONS, true) && is_file($fullPath) ){ - $files[$fullPath] = filesize($fullPath); + $size = filesize($fullPath); + if($size === false){ + //If we can't get the size of the file, we probably don't have perms to read it, so ignore it + continue; + } + $files[$fullPath] = $size; }elseif(is_dir($fullPath)){ find_regions_recursive($fullPath, $files); } @@ -165,7 +170,8 @@ function main(array $argv) : int{ clearstatcache(); $newSize = 0; foreach(Utils::stringifyKeys($files) as $file => $oldSize){ - $newSize += file_exists($file) ? filesize($file) : 0; + $size = file_exists($file) ? filesize($file) : 0; + $newSize += $size !== false ? $size : 0; } $diff = $currentSize - $newSize; $logger->info("Finished compaction of " . count($files) . " files. Freed " . number_format($diff) . " bytes of space (" . round(($diff / $currentSize) * 100, 2) . "% reduction)."); diff --git a/tools/convert-world.php b/tools/convert-world.php index d4d15ce57..828ccb470 100644 --- a/tools/convert-world.php +++ b/tools/convert-world.php @@ -52,7 +52,7 @@ $writableFormats = array_filter($providerManager->getAvailableProviders(), fn(Wo $requiredOpts = [ "world" => "path to the input world for conversion", "backup" => "path to back up the original files", - "format" => "desired output format (can be one of: " . implode(",", array_keys($writableFormats)) . ")" + "format" => "desired output format (can be one of: " . implode(", ", array_keys($writableFormats)) . ")" ]; $usageMessage = "Options:\n"; foreach($requiredOpts as $_opt => $_desc){ @@ -89,7 +89,7 @@ if(count($oldProviderClasses) === 0){ exit(1); } if(count($oldProviderClasses) > 1){ - fwrite(STDERR, "Ambiguous input world format: matched " . count($oldProviderClasses) . " (" . implode(array_keys($oldProviderClasses)) . ")" . PHP_EOL); + fwrite(STDERR, "Ambiguous input world format: matched " . count($oldProviderClasses) . " (" . implode(", ", array_keys($oldProviderClasses)) . ")" . PHP_EOL); exit(1); } $oldProviderClass = array_shift($oldProviderClasses); diff --git a/tools/generate-bedrock-data-from-packets.php b/tools/generate-bedrock-data-from-packets.php index 2c20e6099..47411701a 100644 --- a/tools/generate-bedrock-data-from-packets.php +++ b/tools/generate-bedrock-data-from-packets.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\tools\generate_bedrock_data_from_packets; +use pmmp\encoding\ByteBufferReader; use pocketmine\crafting\json\FurnaceRecipeData; use pocketmine\crafting\json\ItemStackData; use pocketmine\crafting\json\PotionContainerChangeRecipeData; @@ -35,8 +36,8 @@ use pocketmine\crafting\json\SmithingTrimRecipeData; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\ItemTypeNames; +use pocketmine\inventory\json\CreativeGroupData; use pocketmine\nbt\LittleEndianNbtSerializer; -use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\TreeRoot; @@ -48,15 +49,15 @@ use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket; use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket; use pocketmine\network\mcpe\protocol\CraftingDataPacket; use pocketmine\network\mcpe\protocol\CreativeContentPacket; +use pocketmine\network\mcpe\protocol\ItemRegistryPacket; use pocketmine\network\mcpe\protocol\PacketPool; 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\CreativeContentEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraDataShield; +use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\network\mcpe\protocol\types\recipe\ComplexAliasItemDescriptor; use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe; use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor; @@ -73,6 +74,8 @@ use pocketmine\network\PacketHandlingException; 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; @@ -97,6 +100,7 @@ use function json_encode; 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; @@ -134,6 +138,19 @@ class ParserPacketHandler extends PacketHandler{ return base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($statePropertiesTag))); } + /** + * @param ItemStackData[] $items + */ + private function creativeGroupEntryToJson(CreativeGroupEntry $entry, array $items) : CreativeGroupData{ + $data = new CreativeGroupData(); + + $data->group_name = $entry->getCategoryName(); + $data->group_icon = $entry->getIcon()->getId() === 0 ? null : $this->itemStackToJson($entry->getIcon()); + $data->items = $items; + + return $data; + } + private function itemStackToJson(ItemStack $itemStack) : ItemStackData{ if($itemStack->getId() === 0){ throw new InvalidArgumentException("Cannot serialize a null itemstack"); @@ -173,7 +190,7 @@ class ParserPacketHandler extends PacketHandler{ $rawExtraData = $itemStack->getRawExtraData(); if($rawExtraData !== ""){ - $decoder = PacketSerializer::decoder($rawExtraData, 0); + $decoder = new ByteBufferReader($rawExtraData); $extraData = $itemStringId === ItemTypeNames::SHIELD ? ItemStackExtraDataShield::read($decoder) : ItemStackExtraData::read($decoder); $nbt = $extraData->getNbt(); if($nbt !== null && count($nbt) > 0){ @@ -191,11 +208,18 @@ class ParserPacketHandler extends PacketHandler{ return $data; } - /** - * @return mixed[] - */ - private static function objectToOrderedArray(object $object) : array{ - $result = (array) ($object instanceof \JsonSerializable ? $object->jsonSerialize() : $object); + private static function objectToOrderedArray(object $object) : mixed{ + if($object instanceof \JsonSerializable){ + $result = $object->jsonSerialize(); + if(is_object($result)){ + $result = (array) $result; + }elseif(!is_array($result)){ + return $result; + } + }else{ + $result = (array) $object; + } + ksort($result, SORT_STRING); foreach(Utils::promoteKeys($result) as $property => $value){ @@ -234,31 +258,68 @@ class ParserPacketHandler extends PacketHandler{ } public function handleStartGame(StartGamePacket $packet) : bool{ - $this->itemTypeDictionary = new ItemTypeDictionary($packet->itemTable); - - echo "updating legacy item ID mapping table\n"; - $table = []; - foreach($packet->itemTable as $entry){ - $table[$entry->getStringId()] = [ - "runtime_id" => $entry->getNumericId(), - "component_based" => $entry->isComponentBased() - ]; - } - ksort($table, SORT_STRING); - file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n"); - foreach(Utils::promoteKeys($packet->levelSettings->experiments->getExperiments()) as $name => $experiment){ echo "Experiment \"$name\" is " . ($experiment ? "" : "not ") . "active\n"; } return true; } + public function handleItemRegistry(ItemRegistryPacket $packet) : bool{ + $this->itemTypeDictionary = new ItemTypeDictionary($packet->getEntries()); + + echo "updating legacy item ID mapping table\n"; + $emptyNBT = new CompoundTag(); + $table = []; + foreach($packet->getEntries() as $entry){ + $table[$entry->getStringId()] = [ + "runtime_id" => $entry->getNumericId(), + "component_based" => $entry->isComponentBased(), + "version" => $entry->getVersion(), + ]; + + $componentNBT = $entry->getComponentNbt()->getRoot(); + if(!$componentNBT->equals($emptyNBT)){ + $table[$entry->getStringId()]["component_nbt"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($componentNBT))); + } + } + ksort($table, SORT_STRING); + file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n"); + + echo "updating item registry\n"; + $items = array_map(function(ItemTypeEntry $entry) : mixed{ + return self::objectToOrderedArray($entry); + }, $packet->getEntries()); + file_put_contents($this->bedrockDataPath . '/item_registry.json', json_encode($items, JSON_PRETTY_PRINT) . "\n"); + return true; + } + public function handleCreativeContent(CreativeContentPacket $packet) : bool{ echo "updating creative inventory data\n"; - $items = array_map(function(CreativeContentEntry $entry) : array{ - return self::objectToOrderedArray($this->itemStackToJson($entry->getItem())); - }, $packet->getEntries()); - file_put_contents($this->bedrockDataPath . '/creativeitems.json', json_encode($items, JSON_PRETTY_PRINT) . "\n"); + + $groupItems = []; + foreach($packet->getItems() as $itemEntry){ + $groupItems[$itemEntry->getGroupId()][] = $this->itemStackToJson($itemEntry->getItem()); + } + + static $typeMap = [ + CreativeContentPacket::CATEGORY_CONSTRUCTION => "construction", + CreativeContentPacket::CATEGORY_NATURE => "nature", + CreativeContentPacket::CATEGORY_EQUIPMENT => "equipment", + CreativeContentPacket::CATEGORY_ITEMS => "items", + ]; + + $groupCategories = []; + foreach(Utils::promoteKeys($packet->getGroups()) as $groupId => $group){ + $category = $typeMap[$group->getCategoryId()] ?? throw new PacketHandlingException("Unknown creative category ID " . $group->getCategoryId()); + //FIXME: objectToOrderedArray might mess with the order of groupItems + //this isn't a problem right now because it's a list, but could cause problems in the future + $groupCategories[$category][] = self::objectToOrderedArray($this->creativeGroupEntryToJson($group, $groupItems[$groupId])); + } + + foreach(Utils::promoteKeys($groupCategories) as $category => $categoryGroups){ + file_put_contents($this->bedrockDataPath . '/creative/' . $category . '.json', json_encode($categoryGroups, JSON_PRETTY_PRINT) . "\n"); + } + return true; } @@ -454,7 +515,7 @@ class ParserPacketHandler extends PacketHandler{ //this sorts the data into a canonical order to make diffs between versions reliable //how the data is ordered doesn't matter as long as it's reproducible - foreach($recipes as $_type => $entries){ + foreach(Utils::promoteKeys($recipes) as $_type => $entries){ $_sortedRecipes = []; $_seen = []; foreach($entries as $entry){ @@ -475,10 +536,10 @@ class ParserPacketHandler extends PacketHandler{ } ksort($recipes, SORT_STRING); - foreach($recipes as $type => $entries){ + foreach(Utils::promoteKeys($recipes) as $type => $entries){ echo "$type: " . count($entries) . "\n"; } - foreach($recipes as $type => $entries){ + foreach(Utils::promoteKeys($recipes) as $type => $entries){ file_put_contents(Path::join($recipesPath, $type . '.json'), json_encode($entries, JSON_PRETTY_PRINT) . "\n"); } @@ -492,8 +553,8 @@ class ParserPacketHandler extends PacketHandler{ if(!($tag instanceof CompoundTag)){ throw new AssumptionFailedError(); } - $idList = $tag->getTag("idlist"); - if(!($idList instanceof ListTag) || $idList->getTagType() !== NBT::TAG_Compound){ + $generic = $tag->getTag("idlist"); + if(!($generic instanceof ListTag) || ($idList = $generic->cast(CompoundTag::class)) === null){ echo $tag . "\n"; throw new \RuntimeException("expected TAG_List(\"idlist\") tag inside root TAG_Compound"); } @@ -503,9 +564,6 @@ class ParserPacketHandler extends PacketHandler{ } echo "updating legacy => string entity ID mapping table\n"; $map = []; - /** - * @var CompoundTag $thing - */ foreach($idList as $thing){ $map[$thing->getString("id")] = $thing->getInt("rid"); } @@ -519,34 +577,34 @@ class ParserPacketHandler extends PacketHandler{ public function handleBiomeDefinitionList(BiomeDefinitionListPacket $packet) : bool{ echo "storing biome definitions" . PHP_EOL; - file_put_contents($this->bedrockDataPath . '/biome_definitions_full.nbt', $packet->definitions->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(); - $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); - } - } + $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); } - file_put_contents($this->bedrockDataPath . '/biome_definitions.nbt', (new CacheableNbt($strippedNbt))->getEncodedNbt()); + ksort($definitions, SORT_STRING); + + file_put_contents($this->bedrockDataPath . '/biome_definitions.json', json_encode($definitions, JSON_PRETTY_PRINT) . "\n"); return true; } @@ -571,7 +629,7 @@ function main(array $argv) : int{ } foreach($packets as $lineNum => $line){ - $parts = explode(':', $line); + $parts = explode(':', $line, limit: 3); if(count($parts) !== 2){ fwrite(STDERR, 'Wrong packet format at line ' . ($lineNum + 1) . ', expected read:base64 or write:base64'); return 1; @@ -587,12 +645,13 @@ function main(array $argv) : int{ fwrite(STDERR, "Unknown packet on line " . ($lineNum + 1) . ": " . $parts[1]); continue; } - $serializer = PacketSerializer::decoder($raw, 0); + $serializer = new ByteBufferReader($raw); $pk->decode($serializer); $pk->handle($handler); - if(!$serializer->feof()){ - echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getBuffer()) . " bytes): " . bin2hex($serializer->getRemaining()) . "\n"; + $remaining = strlen($serializer->getData()) - $serializer->getOffset(); + if($remaining > 0){ + echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getData()) . " bytes): " . bin2hex($serializer->readByteArray($remaining)) . "\n"; } } return 0; diff --git a/tools/generate-item-upgrade-schema.php b/tools/generate-item-upgrade-schema.php index 4eee92539..7ad473b23 100644 --- a/tools/generate-item-upgrade-schema.php +++ b/tools/generate-item-upgrade-schema.php @@ -31,10 +31,12 @@ namespace pocketmine\tools\generate_item_upgrade_schema; use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\utils\Filesystem; +use pocketmine\utils\Utils; use Symfony\Component\Filesystem\Path; use function count; use function dirname; use function file_put_contents; +use function fwrite; use function is_array; use function json_decode; use function json_encode; @@ -45,6 +47,7 @@ use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const SCANDIR_SORT_ASCENDING; use const SORT_STRING; +use const STDERR; require dirname(__DIR__) . '/vendor/autoload.php'; @@ -56,7 +59,7 @@ if(count($argv) !== 4){ [, $mappingTableFile, $upgradeSchemasDir, $outputFile] = $argv; $target = json_decode(Filesystem::fileGetContents($mappingTableFile), true, JSON_THROW_ON_ERROR); -if(!is_array($target)){ +if(!is_array($target) || !isset($target["simple"]) || !is_array($target["simple"]) || !isset($target["complex"]) || !is_array($target["complex"])){ \GlobalLogger::get()->error("Invalid mapping table file"); exit(1); } @@ -93,7 +96,7 @@ foreach($files as $file){ $newDiff = []; -foreach($target["simple"] as $oldId => $newId){ +foreach(Utils::promoteKeys($target["simple"]) as $oldId => $newId){ $previousNewId = $merged["simple"][$oldId] ?? null; if( $previousNewId === $newId || //if previous schemas already accounted for this @@ -107,8 +110,12 @@ if(isset($newDiff["renamedIds"])){ ksort($newDiff["renamedIds"], SORT_STRING); } -foreach($target["complex"] as $oldId => $mappings){ - foreach($mappings as $meta => $newId){ +foreach(Utils::promoteKeys($target["complex"]) as $oldId => $mappings){ + if(!is_array($mappings)){ + fwrite(STDERR, "Complex mapping for $oldId is not an array\n"); + exit(1); + } + foreach(Utils::promoteKeys($mappings) as $meta => $newId){ if(($merged["complex"][$oldId][$meta] ?? null) !== $newId){ if($oldId === "minecraft:spawn_egg" && $meta === 130 && ($newId === "minecraft:axolotl_bucket" || $newId === "minecraft:axolotl_spawn_egg")){ //TODO: hack for vanilla bug workaround diff --git a/tools/simulate-chunk-selector.php b/tools/simulate-chunk-selector.php index 0b279268a..3d5e167cf 100644 --- a/tools/simulate-chunk-selector.php +++ b/tools/simulate-chunk-selector.php @@ -53,6 +53,10 @@ use const STR_PAD_LEFT; require dirname(__DIR__) . '/vendor/autoload.php'; +/** + * @phpstan-param positive-int $scale + * @phpstan-param positive-int $radius + */ function newImage(int $scale, int $radius) : \GdImage{ $image = Utils::assumeNotFalse(imagecreatetruecolor($scale * $radius * 2, $scale * $radius * 2)); imagesavealpha($image, true); @@ -149,6 +153,18 @@ if($radius === null){ fwrite(STDERR, "Please specify a radius using --radius\n"); exit(1); } +if($radius < 1){ + fwrite(STDERR, "Radius cannot be less than 1\n"); + exit(1); +} +if($scale < 1){ + fwrite(STDERR, "Scale cannot be less than 1\n"); + exit(1); +} +if($nChunksPerStep < 1){ + fwrite(STDERR, "Chunks per step cannot be less than 1\n"); + exit(1); +} $outputDirectory = null; if(isset($opts["output"])){