Compare commits

...

60 Commits

Author SHA1 Message Date
fdae689f87 Fixed a series of mistakes that prevented converting from regionized leveldb to a different format 2024-12-23 19:11:59 +00:00
8027c271fa Remove regionized leveldb 32x32 and 64x64
these produce such small file sizes on average that the DB logs would probably take up a significant fraction of the world's footprint.
My gut instinct is that 128 will probably be the sweet spot, as on average it should sit well below the threshold for level 3 compactions, and most worlds would likely fit into a single DB.
256 is probably not worthwhile, but might be worth trying.
2024-12-21 18:47:44 +00:00
6abc40bb83 ... 2024-12-21 18:13:59 +00:00
07cd2e5542 Apparently intdiv() is fucking stupid 2024-12-21 18:10:25 +00:00
5b435e783c stfu 2024-12-21 18:03:41 +00:00
1224055f66 Introduced a custom LevelDB impl with regions and better key format
This new impl (which is not loadable by vanilla) is targeted at very large worlds, which experience significant I/O performance issues due to a variety of issues described in #6580.

Two main changes are made in RegionizedLevelDB:
- First, multiple LevelDBs are used, which cover a fixed NxN segment of terrain, similar to Anvil in Java. However, there's no constraint on these region sizes. Several experimental sizes are supported by default in WorldProviderManager.
- Second, bigEndianLong(morton2d(chunkX, chunkZ)) is used for chunk keys instead of littleEndianInt(chunkX).littleEndianInt(chunkZ). This new scheme has much better cache locality than Mojang's version, which reduces overlap and costly DB compactions.

The following new provider options are available as a result of this change:
- custom-leveldb-regions-32
- custom-leveldb-regions-64
- custom-leveldb-regions-128
- custom-leveldb-regions-256

Smaller sizes will likely be less space-efficient, but will also probably have better performance.
Once a sweet spot is found, a default will be introduced.

Note that the different variations of custom-leveldb-regions-* are not cross-compatible.
Conversion between the different formats is necessary if you want to change formats.
2024-12-21 17:56:09 +00:00
306623e890 FormatConverter: do periodic GC
this reduces the risk of OOM during conversion of large worlds
we probably ought to limit the size of region caches for regionized worlds, but that's a problem for another time.
2024-12-21 17:49:03 +00:00
ada3acdba4 FormatConverter: ensure we don't get stalled due to stdout buffer flood
this can happen due to very noisy outputs during conversion, e.g. if there were many unknown blocks.
2024-12-21 17:49:03 +00:00
6a1d80e021 tools/convert-world: fixed some UI issues 2024-12-21 17:49:02 +00:00
1ff0988e75 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12440895736
2024-12-21 01:22:54 +00:00
506cfe0362 Bump build/php from 5016e0a to b1eaaa4 (#6579)
Bumps [build/php](https://github.com/pmmp/php-build-scripts) from `5016e0a` to `b1eaaa4`.
- [Release notes](https://github.com/pmmp/php-build-scripts/releases)
- [Commits](5016e0a3d5...b1eaaa48ec)

---
updated-dependencies:
- dependency-name: build/php
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-20 10:39:39 +00:00
fea17fa4a9 RakLibServer: disable GC
GC is not required for RakLib as it doesn't generate any unmanaged cycles.
Cycles in general do exist (e.g. Server <-> ServerSession), but these are
explicitly cleaned up, so GC wouldn't have any useful work to do.
2024-12-19 20:33:40 +00:00
aee358d329 This timings handler management is a crap design
This has bitten me on the ass so many times now
2024-12-16 03:11:07 +00:00
80899ea72c Make sure timings are counted under the proper parents 2024-12-15 21:34:16 +00:00
42f90e94ff AsyncWorker now manually triggers GC at the end of each task run, similar to the main thread
this avoids costly GC runs during hot code.
2024-12-15 21:25:32 +00:00
8f536e6f21 always the CS 2024-12-15 20:46:10 +00:00
45482e868d Fixed AsyncWorker GC not getting re-enabled after memory dump
async workers still use automatic GC for now. We should probably switch to manual GC at some point, but it's not a priority right now.
2024-12-15 20:45:51 +00:00
742aa46b88 Separate memory dumping utilities from MemoryManager 2024-12-15 20:44:00 +00:00
cf1b360a62 World: Prevent block cache from getting too big
This has been a long-standing issue since at least 2016, and probably longer.
Heavy use of getBlock(At) could cause the cache to blow up and use all available memory.

Recently, it's become clear that unmanaged cache size is also a problem for GC, because
the large number of objects blows up the GC root buffer. At first, this causes more frequent
GC runs; later, the frequency of GC runs drops, but the performance cost of them goes up
substantially because of the sheer number of objects. We can avoid this by trimming the
cache when we detect that it's exceeded limits.

I've implemented this in such a way that failing to update blockCacheSize in new code
won't have lasting impacts, since the cache count will be recalculated during scheduled
cache cleaning anyway.

Closes #152.
2024-12-15 18:40:32 +00:00
0aa6cde259 Remove stupid MemoryManager settings
No one in their right mind is going to change the defaults for these anyway.

All this crap does is overwhelm users with stuff they don't understand.
Most of this stuff has no business being modified by non-developers anyway.
2024-12-15 16:41:54 +00:00
8f8fe948c1 MemoryManager: Control when cycle garbage collection is run (#6554)
This PR replicates the mechanism by which PHP's own GC is triggered: using a dynamically adjusted threshold based on the number of roots and the number of destroyed cycles. This approach was chosen to minimize behavioural changes.

This currently only applies to the main thread. Doing this for other threads is a bit more complicated (and in the case of RakLib, possibly not necessary anyway).

By doing this, we can get more accurate performance profiling. Instead of GC happening in random pathways and throwing off GC numbers, we trigger it in a predictable place, where timings can record it.

This change may also produce minor performance improvements in code touching lots of objects (such as `CraftingDataPacket` encoding`), which previously might've triggered multiple GC runs within a single tick. Now that GC runs wait for `MemoryManager`, it can touch as many objects as it wants during a tick without paying a performance penalty.

While working on this change I came across a number of issues that should probably be addressed in the future:

1) Objects like Server, World and Player that can't possibly be GC'd repeatedly end up in the GC root buffer because the refcounts fluctuate frequently. Because of the dependency chains in these objects, they all drag each other into GC, causing an almost guaranteed parasitic performance cost to GC. This is discussed in php/php-src#17131, as the proper solution to this is probably generational GC, or perhaps some way to explicitly mark objects to be ignored by GC.
2) World's `blockCache` blows up the GC root threshold due to poor size management. This leads to infrequent, but extremely expensive GC runs due to the sheer number of objects being scanned. We could avoid a lot of this cost by managing caches like this more effectively.
3) StringToItemParser and many of the pocketmine\data classes which make heavy use of closures are afflicted by thousands of reference cycles. This doesn't present a major performance issue in most cases because the cycles are simple, but this could easily be fixed with some simple refactors.
2024-12-15 16:26:39 +00:00
b10caf7437 Remove tool tier of some blocks to match vanilla (#6573) 2024-12-13 21:54:48 +00:00
de66d84d29 Implement new 1.20 and 1.21 records (#6572) 2024-12-13 21:10:34 +03:00
636f562bcf Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12307996607
2024-12-13 01:40:42 +00:00
42094e6768 Implement resin blocks & items (#6571) 2024-12-12 23:21:41 +03:00
b341078765 Implement new pale oak blocks (#6570) 2024-12-12 17:53:52 +03:00
f7687af337 Fixed draft release being created on release publish 2024-12-12 13:44:15 +00:00
ba93665fe7 TextFormat: reduce hella duplicated code in toHTML() 2024-12-10 14:11:11 +00:00
6817215683 TextFormat: Added new material colours for armor trims (#5838)
Unfortunately, these new formatting codes conflict with the Java strikethrough and underline, so we can't support these anymore.

A TextFormat::javaToBedrock() is provided to strip these codes, or (if these formats become supported via different codes) to convert them to Bedrock variants.

Co-authored-by: Dylan T. <dktapps@pmmp.io>
2024-12-10 13:40:03 +00:00
67b9d6222d 5.23.3 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/12240364052
2024-12-09 16:52:50 +00:00
6f197bc1bb 5.23.2 (#6569) 2024-12-09 16:51:41 +00:00
bba525da02 Remove dead PHPStan ignored errors 2024-12-09 16:44:25 +00:00
ad6d34f1a6 Remove legacy make-release script
we no longer use this release workflow, all releases should now be done via pull request
2024-12-09 16:44:07 +00:00
a8eaa43bc8 Recombine release workflows
having two different workflows able to trigger releases is a pain for build number continuity.
perhaps longer term we should source the build number a different way, but these workflows needed restructuring anyway.
2024-12-09 16:36:26 +00:00
fe7c282052 Bump pocketmine/locale-data in the production-patch-updates group (#6568) 2024-12-09 12:03:09 +00:00
45917d495c Fixed CrashDump incorrectly detecting phar core crashes as plugin crashes (#6564)
fixes #6563

Since #6217 was merged, \pocketmine\PATH no longer includes the path of the original phar.
This means that the frame originating from the phar stub would not get its path cleaned up,
leading to it being incorrectly detected as a plugin frame.

We should probably explore better methods of detecting plugin crashes in the future; however
this fix should solve the immediate issue.
2024-12-08 16:52:33 +00:00
1481977f35 Create pr-stale.yml 2024-12-05 20:47:46 +00:00
8efdf501ad 5.23.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/12187209543
2024-12-05 20:05:36 +00:00
2b0daebc2a 5.23.1 (#6562) 2024-12-05 20:04:43 +00:00
6b2da15b80 Fixed signs 2024-12-05 19:58:52 +00:00
2ef02a2c5e Upgraded block consistency check to detect tile changes 2024-12-05 19:57:13 +00:00
15e8895e54 5.23.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/12183301507
2024-12-05 15:52:16 +00:00
ea8f971287 Release 5.23.0 (#6561) 2024-12-05 15:51:13 +00:00
62e1d87f5e Mention internal timings deprecations
plugins shouldn't be using these, but since it's not marked as internal, we can't be sure.
2024-12-05 15:47:34 +00:00
ea068d4907 Update 5.23.md 2024-12-05 15:01:49 +00:00
fa7bc78e7c Prepare 5.23.0 release 2024-12-05 14:29:17 +00:00
0aaf4238a8 more deprecations in line with major-next 2024-12-05 13:02:09 +00:00
35a90d24ec AsyncTask: deprecate progress update related stuff 2024-12-05 12:57:26 +00:00
9a6e258b6c Merge branch 'stable' of github.com:pmmp/PocketMine-MP into minor-next 2024-12-05 10:13:06 +00:00
a1448bfb88 5.22.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/12160926590
2024-12-04 13:38:41 +00:00
ba6828c6bd Release 5.22.0 (Bedrock 1.21.50 support) (#6559)
Co-authored-by: Dylan K. Taylor <dktapps@pmmp.io>
2024-12-04 13:36:52 +00:00
5fc96c393d Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12151373979
2024-12-04 01:39:58 +00:00
2d0321ff02 Switch back to official JsonMapper
the issues that led to the need for a fork have been addressed in the 5.0.0 release.
2024-12-03 15:19:38 +00:00
c56d4d3e3c dependabot: update github actions deps together, monthly 2024-12-03 14:56:22 +00:00
06028aac97 issues: don't recommend forums to get help 2024-12-03 02:07:58 +00:00
950f7ad7a4 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12131296321
2024-12-03 01:39:32 +00:00
49da50659f Bump docker/build-push-action from 6.9.0 to 6.10.0 (#6553) 2024-12-02 16:36:12 +00:00
ae9b4dbb05 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/12111121061
2024-12-02 01:41:01 +00:00
fcef015f32 L link 2024-12-02 00:40:55 +00:00
44771c892d Use PlayerAuthInputPacket::SNEAKING flag to test for sneaking (#6544)
This binds internal sneaking to whether or not the player is currently pressing the shift key, which fixes #5792 and fixes #5903.

However, it does introduce visual issues with sneaking, as explained in #6548. This needs to be worked on separately. For now, it's better we trade 2 functional bugs for 1 visual bug.
2024-12-01 13:14:30 +00:00
65 changed files with 3290 additions and 2358 deletions

View File

@ -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

View File

@ -37,4 +37,7 @@ updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
interval: monthly
groups:
github-actions:
patterns: ["*"]

View File

@ -53,7 +53,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.10.0
with:
push: true
context: ./pocketmine-mp
@ -66,7 +66,7 @@ jobs:
- name: Build image for major tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v6.10.0
with:
push: true
context: ./pocketmine-mp
@ -79,7 +79,7 @@ jobs:
- name: Build image for minor tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v6.10.0
with:
push: true
context: ./pocketmine-mp
@ -92,7 +92,7 @@ jobs:
- name: Build image for latest tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v6.10.0
with:
push: true
context: ./pocketmine-mp

View File

@ -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 }}"}'

View File

@ -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

View File

@ -1,28 +1,85 @@
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
skip:
name: Check whether to ignore this tag
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
php-version: [8.2]
outputs:
draft-url: ${{ steps.create-draft.outputs.html_url }}
version: ${{ steps.get-pm-version.outputs.PM_VERSION }}
skip: ${{ steps.exists.outputs.exists == 'true' }}
steps:
- 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-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 }}"}'
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-20.04
steps:
- uses: actions/checkout@v4
@ -32,7 +89,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@2.31.1
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 }} \
@ -108,12 +173,17 @@ 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 }}).
- name: Post draft release URL on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v3
with:
message: "[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."

29
.github/workflows/pr-stale.yml vendored Normal file
View File

@ -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

View File

@ -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.

View File

@ -1,174 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\build\make_release;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use pocketmine\utils\VersionString;
use pocketmine\VersionInfo;
use function array_keys;
use function array_map;
use function dirname;
use function fgets;
use function file_put_contents;
use function fwrite;
use function getopt;
use function is_string;
use function max;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_pad;
use function strlen;
use function strtolower;
use function system;
use const STDERR;
use const STDIN;
use const STDOUT;
use const STR_PAD_LEFT;
require_once dirname(__DIR__) . '/vendor/autoload.php';
function replaceVersion(string $versionInfoPath, string $newVersion, bool $isDev, string $channel) : void{
$versionInfo = Filesystem::fileGetContents($versionInfoPath);
$versionInfo = preg_replace(
$pattern = '/^([\t ]*public )?const BASE_VERSION = "(\d+)\.(\d+)\.(\d+)(?:-(.*))?";$/m',
'$1const BASE_VERSION = "' . $newVersion . '";',
$versionInfo
);
$versionInfo = preg_replace(
'/^([\t ]*public )?const IS_DEVELOPMENT_BUILD = (?:true|false);$/m',
'$1const IS_DEVELOPMENT_BUILD = ' . ($isDev ? 'true' : 'false') . ';',
$versionInfo
);
$versionInfo = preg_replace(
'/^([\t ]*public )?const BUILD_CHANNEL = ".*";$/m',
'$1const BUILD_CHANNEL = "' . $channel . '";',
$versionInfo
);
file_put_contents($versionInfoPath, $versionInfo);
}
const ACCEPTED_OPTS = [
"current" => "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();

View File

@ -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';

16
changelogs/5.22.md Normal file
View File

@ -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.

129
changelogs/5.23.md Normal file
View File

@ -0,0 +1,129 @@
# 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<Promise<list<string>>>` - 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<list<string>>` - 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.

View File

@ -32,11 +32,11 @@
"ext-zlib": ">=1.2.11",
"composer-runtime-api": "^2.0",
"adhocore/json-comment": "~1.2.0",
"pocketmine/netresearch-jsonmapper": "~v4.4.999",
"netresearch/jsonmapper": "~v5.0.0",
"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",
"pocketmine/bedrock-data": "~2.15.0+bedrock-1.21.50",
"pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50",
"pocketmine/bedrock-protocol": "~35.0.0+bedrock-1.21.50",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0",

155
composer.lock generated
View File

@ -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": "732102eca72dc1d29e7b67dfbce07653",
"packages": [
{
"name": "adhocore/json-comment",
@ -125,6 +125,57 @@
],
"time": "2023-11-29T23:19:16+00:00"
},
{
"name": "netresearch/jsonmapper",
"version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c"
},
"dist": {
"type": "zip",
"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.0.0",
@ -153,16 +204,16 @@
},
{
"name": "pocketmine/bedrock-data",
"version": "2.14.1+bedrock-1.21.40",
"version": "2.15.0+bedrock-1.21.50",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "4a41864ed09613ecec6791e2ae076a8ec7089cc4"
"reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/4a41864ed09613ecec6791e2ae076a8ec7089cc4",
"reference": "4a41864ed09613ecec6791e2ae076a8ec7089cc4",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/6e819f36d781866ce63d2406be2ce7f2d1afd9ad",
"reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad",
"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/bedrock-1.21.50"
},
"time": "2024-11-12T21:36:20+00:00"
"time": "2024-12-04T12:59:12+00:00"
},
{
"name": "pocketmine/bedrock-item-upgrade-schema",
"version": "1.13.1",
"version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git",
"reference": "1cf81305f2ffcf7dde9577c4f16a55c765192b03"
"reference": "9fc7c9bbb558a017395c1cb7dd819c033ee971bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/1cf81305f2ffcf7dde9577c4f16a55c765192b03",
"reference": "1cf81305f2ffcf7dde9577c4f16a55c765192b03",
"url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/9fc7c9bbb558a017395c1cb7dd819c033ee971bb",
"reference": "9fc7c9bbb558a017395c1cb7dd819c033ee971bb",
"shasum": ""
},
"type": "library",
@ -199,22 +250,22 @@
"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.14.0"
},
"time": "2024-11-12T21:33:17+00:00"
"time": "2024-12-04T12:22:49+00:00"
},
{
"name": "pocketmine/bedrock-protocol",
"version": "35.0.0+bedrock-1.21.40",
"version": "35.0.0+bedrock-1.21.50",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459"
"reference": "bd1ec79bae8c88aa984e1c5f0c3313be5ae9b435"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459",
"reference": "6aa7cbeb4a7ec6fa58f9024aeaddad7c5c65a459",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/bd1ec79bae8c88aa984e1c5f0c3313be5ae9b435",
"reference": "bd1ec79bae8c88aa984e1c5f0c3313be5ae9b435",
"shasum": ""
},
"require": {
@ -245,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/35.0.0+bedrock-1.21.40"
"source": "https://github.com/pmmp/BedrockProtocol/tree/35.0.0+bedrock-1.21.50"
},
"time": "2024-10-24T15:45:43+00:00"
"time": "2024-12-04T13:02:00+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -420,16 +471,16 @@
},
{
"name": "pocketmine/locale-data",
"version": "2.22.0",
"version": "2.22.1",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Language.git",
"reference": "aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d"
"reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/Language/zipball/aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d",
"reference": "aed64e9ca92ffbb20788b3b3bb75b60e4f0eae2d",
"url": "https://api.github.com/repos/pmmp/Language/zipball/fa4e377c437391cfcfdedd08eea3a848eabd1b49",
"reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49",
"shasum": ""
},
"type": "library",
@ -437,9 +488,9 @@
"description": "Language resources used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/Language/issues",
"source": "https://github.com/pmmp/Language/tree/2.22.0"
"source": "https://github.com/pmmp/Language/tree/2.22.1"
},
"time": "2024-11-16T13:28:01+00:00"
"time": "2024-12-06T14:44:17+00:00"
},
{
"name": "pocketmine/log",
@ -565,60 +616,6 @@
},
"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"
},
{
"name": "pocketmine/raklib",
"version": "1.1.1",

View File

@ -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.

View File

@ -0,0 +1,103 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine;
use pocketmine\timings\TimingsHandler;
use function gc_collect_cycles;
use function gc_disable;
use function gc_status;
use function hrtime;
use function max;
use function min;
use function number_format;
/**
* Allows threads to manually trigger the cyclic garbage collector using a threshold like PHP's own garbage collector,
* but triggered at a time that suits the thread instead of in random code pathways.
*
* The GC trigger behaviour in this class was adapted from Zend/zend_gc.c as of PHP 8.3.14.
*/
final class GarbageCollectorManager{
//TODO: These values could be adjusted to better suit PM, but for now we just want to mirror PHP GC to minimize
//behavioural changes.
private const GC_THRESHOLD_TRIGGER = 100;
private const GC_THRESHOLD_MAX = 1_000_000_000;
private const GC_THRESHOLD_DEFAULT = 10_001;
private const GC_THRESHOLD_STEP = 10_000;
private int $threshold = self::GC_THRESHOLD_DEFAULT;
private int $collectionTimeTotalNs = 0;
private \Logger $logger;
private TimingsHandler $timings;
public function __construct(
\Logger $logger,
?TimingsHandler $parentTimings,
){
gc_disable();
$this->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->logger->debug("gc_collect_cycles: " . number_format($time) . " ns ($rootsBefore -> $rootsAfter roots, $cycles cycles collected) - total GC time: " . number_format($this->collectionTimeTotalNs) . " ns");
return $cycles;
}
}

305
src/MemoryDump.php Normal file
View File

@ -0,0 +1,305 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine;
use pocketmine\utils\Utils;
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_disable;
use function gc_enable;
use function gc_enabled;
use function get_class;
use function get_declared_classes;
use function get_defined_functions;
use function ini_get;
use function ini_set;
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 mkdir;
use function print_r;
use function spl_object_hash;
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;
final class MemoryDump{
private function __construct(){
//NOOP
}
/**
* Static memory dumper accessible from any thread.
*/
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');
$gcEnabled = gc_enabled();
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);
if($gcEnabled){
gc_enable();
}
}
/**
* @param object[] $objects reference parameter
* @param int[] $refCounts reference parameter
*
* @phpstan-param array<string, object> $objects
* @phpstan-param array<string, int> $refCounts
* @phpstan-param-out array<string, object> $objects
* @phpstan-param-out array<string, int> $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;
}
}

View File

@ -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<string, object> $objects
* @phpstan-param array<string, int> $refCounts
* @phpstan-param-out array<string, object> $objects
* @phpstan-param-out array<string, int> $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);
}
}

View File

@ -282,6 +282,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";

View File

@ -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.23.3";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";

View File

@ -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';

View File

@ -765,8 +765,29 @@ 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 FIRST_UNUSED_BLOCK_ID = 10738;
public const FIRST_UNUSED_BLOCK_ID = 10759;
private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID;

View File

@ -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;

54
src/block/ResinClump.php Normal file
View File

@ -0,0 +1,54 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\MultiAnySupportTrait;
use pocketmine\block\utils\SupportType;
final class ResinClump extends Transparent{
use MultiAnySupportTrait;
public function isSolid() : bool{
return false;
}
public function getSupportType(int $facing) : SupportType{
return SupportType::NONE;
}
public function canBeReplaced() : bool{
return true;
}
/**
* @return int[]
*/
protected function getInitialPlaceFaces(Block $blockReplace) : array{
return $blockReplace instanceof ResinClump ? $blockReplace->faces : [];
}
protected function recalculateCollisionBoxes() : array{
return [];
}
}

View File

@ -54,6 +54,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;
@ -190,6 +191,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()
@ -589,6 +591,20 @@ use function strtolower;
* @method static Flower OXEYE_DAISY()
* @method static PackedIce PACKED_ICE()
* @method static Opaque PACKED_MUD()
* @method static WoodenButton PALE_OAK_BUTTON()
* @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 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 +688,12 @@ 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 DoublePlant ROSE_BUSH()
* @method static Sand SAND()
* @method static Opaque SANDSTONE()
@ -857,12 +879,12 @@ final class VanillaBlocks{
self::register("bedrock", fn(BID $id) => new Bedrock($id, "Bedrock", new Info(BreakInfo::indestructible())));
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));
@ -920,7 +942,7 @@ final class VanillaBlocks{
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])));
@ -976,9 +998,9 @@ final class VanillaBlocks{
$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);
@ -987,7 +1009,7 @@ final class VanillaBlocks{
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("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(5.0));
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));
@ -1122,7 +1144,7 @@ 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));
@ -1178,7 +1200,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",
@ -1311,6 +1333,7 @@ final class VanillaBlocks{
self::registerBlocksR17();
self::registerBlocksR18();
self::registerMudBlocks();
self::registerResinBlocks();
self::registerTuffBlocks();
self::registerCraftingTables();
@ -1358,9 +1381,10 @@ 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);
}
}
@ -1582,7 +1606,7 @@ final class VanillaBlocks{
$prefix = fn(string $thing) => "Polished Blackstone" . ($thing !== "" ? " $thing" : "");
self::register("polished_blackstone", fn(BID $id) => new Opaque($id, $prefix(""), $blackstoneBreakInfo));
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));
@ -1689,9 +1713,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));
@ -1727,6 +1750,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));
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));

View File

@ -53,6 +53,7 @@ enum LeavesType{
case AZALEA;
case FLOWERING_AZALEA;
case CHERRY;
case PALE_OAK;
public function getDisplayName() : string{
return match($this){
@ -65,7 +66,8 @@ enum LeavesType{
self::MANGROVE => "Mangrove",
self::AZALEA => "Azalea",
self::FLOWERING_AZALEA => "Flowering Azalea",
self::CHERRY => "Cherry"
self::CHERRY => "Cherry",
self::PALE_OAK => "Pale Oak",
};
}
}

View File

@ -59,11 +59,15 @@ enum RecordType{
case DISK_CAT;
case DISK_BLOCKS;
case DISK_CHIRP;
case DISK_CREATOR;
case DISK_CREATOR_MUSIC_BOX;
case DISK_FAR;
case DISK_MALL;
case DISK_MELLOHI;
case DISK_OTHERSIDE;
case DISK_PIGSTEP;
case DISK_PRECIPICE;
case DISK_RELIC;
case DISK_STAL;
case DISK_STRAD;
case DISK_WARD;
@ -83,11 +87,15 @@ enum RecordType{
self::DISK_CAT => ["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()],

View File

@ -53,6 +53,7 @@ enum WoodType{
case CRIMSON;
case WARPED;
case CHERRY;
case PALE_OAK;
public function getDisplayName() : string{
return match($this){
@ -66,6 +67,7 @@ enum WoodType{
self::CRIMSON => "Crimson",
self::WARPED => "Warped",
self::CHERRY => "Cherry",
self::PALE_OAK => "Pale Oak",
};
}

View File

@ -45,6 +45,7 @@ final class BedrockDataFiles{
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 REQUIRED_ITEM_LIST_JSON = BEDROCK_DATA_PATH . '/required_item_list.json';

View File

@ -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;
}

View File

@ -93,12 +93,17 @@ final class BlockStateNames{
public const MC_VERTICAL_HALF = "minecraft:vertical_half";
public const MOISTURIZED_AMOUNT = "moisturized_amount";
public const MULTI_FACE_DIRECTION_BITS = "multi_face_direction_bits";
public const NATURAL = "natural";
public const OCCUPIED_BIT = "occupied_bit";
public const OMINOUS = "ominous";
public const OPEN_BIT = "open_bit";
public const ORIENTATION = "orientation";
public const OUTPUT_LIT_BIT = "output_lit_bit";
public const OUTPUT_SUBTRACT_BIT = "output_subtract_bit";
public const PALE_MOSS_CARPET_SIDE_EAST = "pale_moss_carpet_side_east";
public const PALE_MOSS_CARPET_SIDE_NORTH = "pale_moss_carpet_side_north";
public const PALE_MOSS_CARPET_SIDE_SOUTH = "pale_moss_carpet_side_south";
public const PALE_MOSS_CARPET_SIDE_WEST = "pale_moss_carpet_side_west";
public const PERSISTENT_BIT = "persistent_bit";
public const PILLAR_AXIS = "pillar_axis";
public const PORTAL_AXIS = "portal_axis";
@ -116,6 +121,7 @@ final class BlockStateNames{
public const STABILITY_CHECK = "stability_check";
public const STRUCTURE_BLOCK_TYPE = "structure_block_type";
public const SUSPENDED_BIT = "suspended_bit";
public const TIP = "tip";
public const TOGGLE_BIT = "toggle_bit";
public const TORCH_FACING_DIRECTION = "torch_facing_direction";
public const TRIAL_SPAWNER_STATE = "trial_spawner_state";

View File

@ -106,6 +106,22 @@ final class BlockStateStringValues{
public const ORIENTATION_UP_WEST = "up_west";
public const ORIENTATION_WEST_UP = "west_up";
public const PALE_MOSS_CARPET_SIDE_EAST_NONE = "none";
public const PALE_MOSS_CARPET_SIDE_EAST_SHORT = "short";
public const PALE_MOSS_CARPET_SIDE_EAST_TALL = "tall";
public const PALE_MOSS_CARPET_SIDE_NORTH_NONE = "none";
public const PALE_MOSS_CARPET_SIDE_NORTH_SHORT = "short";
public const PALE_MOSS_CARPET_SIDE_NORTH_TALL = "tall";
public const PALE_MOSS_CARPET_SIDE_SOUTH_NONE = "none";
public const PALE_MOSS_CARPET_SIDE_SOUTH_SHORT = "short";
public const PALE_MOSS_CARPET_SIDE_SOUTH_TALL = "tall";
public const PALE_MOSS_CARPET_SIDE_WEST_NONE = "none";
public const PALE_MOSS_CARPET_SIDE_WEST_SHORT = "short";
public const PALE_MOSS_CARPET_SIDE_WEST_TALL = "tall";
public const PILLAR_AXIS_X = "x";
public const PILLAR_AXIS_Y = "y";
public const PILLAR_AXIS_Z = "z";

View File

@ -192,6 +192,7 @@ final class BlockTypeNames{
public const CAVE_VINES_HEAD_WITH_BERRIES = "minecraft:cave_vines_head_with_berries";
public const CHAIN = "minecraft:chain";
public const CHAIN_COMMAND_BLOCK = "minecraft:chain_command_block";
public const CHALKBOARD = "minecraft:chalkboard";
public const CHEMICAL_HEAT = "minecraft:chemical_heat";
public const CHERRY_BUTTON = "minecraft:cherry_button";
public const CHERRY_DOOR = "minecraft:cherry_door";
@ -219,6 +220,7 @@ final class BlockTypeNames{
public const CHISELED_POLISHED_BLACKSTONE = "minecraft:chiseled_polished_blackstone";
public const CHISELED_QUARTZ_BLOCK = "minecraft:chiseled_quartz_block";
public const CHISELED_RED_SANDSTONE = "minecraft:chiseled_red_sandstone";
public const CHISELED_RESIN_BRICKS = "minecraft:chiseled_resin_bricks";
public const CHISELED_SANDSTONE = "minecraft:chiseled_sandstone";
public const CHISELED_STONE_BRICKS = "minecraft:chiseled_stone_bricks";
public const CHISELED_TUFF = "minecraft:chiseled_tuff";
@ -227,6 +229,7 @@ final class BlockTypeNames{
public const CHORUS_PLANT = "minecraft:chorus_plant";
public const CLAY = "minecraft:clay";
public const CLIENT_REQUEST_PLACEHOLDER_BLOCK = "minecraft:client_request_placeholder_block";
public const CLOSED_EYEBLOSSOM = "minecraft:closed_eyeblossom";
public const COAL_BLOCK = "minecraft:coal_block";
public const COAL_ORE = "minecraft:coal_ore";
public const COARSE_DIRT = "minecraft:coarse_dirt";
@ -262,6 +265,7 @@ final class BlockTypeNames{
public const CRACKED_STONE_BRICKS = "minecraft:cracked_stone_bricks";
public const CRAFTER = "minecraft:crafter";
public const CRAFTING_TABLE = "minecraft:crafting_table";
public const CREAKING_HEART = "minecraft:creaking_heart";
public const CREEPER_HEAD = "minecraft:creeper_head";
public const CRIMSON_BUTTON = "minecraft:crimson_button";
public const CRIMSON_DOOR = "minecraft:crimson_door";
@ -831,6 +835,7 @@ final class BlockTypeNames{
public const OBSERVER = "minecraft:observer";
public const OBSIDIAN = "minecraft:obsidian";
public const OCHRE_FROGLIGHT = "minecraft:ochre_froglight";
public const OPEN_EYEBLOSSOM = "minecraft:open_eyeblossom";
public const ORANGE_CANDLE = "minecraft:orange_candle";
public const ORANGE_CANDLE_CAKE = "minecraft:orange_candle_cake";
public const ORANGE_CARPET = "minecraft:orange_carpet";
@ -856,6 +861,26 @@ final class BlockTypeNames{
public const OXIDIZED_DOUBLE_CUT_COPPER_SLAB = "minecraft:oxidized_double_cut_copper_slab";
public const PACKED_ICE = "minecraft:packed_ice";
public const PACKED_MUD = "minecraft:packed_mud";
public const PALE_HANGING_MOSS = "minecraft:pale_hanging_moss";
public const PALE_MOSS_BLOCK = "minecraft:pale_moss_block";
public const PALE_MOSS_CARPET = "minecraft:pale_moss_carpet";
public const PALE_OAK_BUTTON = "minecraft:pale_oak_button";
public const PALE_OAK_DOOR = "minecraft:pale_oak_door";
public const PALE_OAK_DOUBLE_SLAB = "minecraft:pale_oak_double_slab";
public const PALE_OAK_FENCE = "minecraft:pale_oak_fence";
public const PALE_OAK_FENCE_GATE = "minecraft:pale_oak_fence_gate";
public const PALE_OAK_HANGING_SIGN = "minecraft:pale_oak_hanging_sign";
public const PALE_OAK_LEAVES = "minecraft:pale_oak_leaves";
public const PALE_OAK_LOG = "minecraft:pale_oak_log";
public const PALE_OAK_PLANKS = "minecraft:pale_oak_planks";
public const PALE_OAK_PRESSURE_PLATE = "minecraft:pale_oak_pressure_plate";
public const PALE_OAK_SAPLING = "minecraft:pale_oak_sapling";
public const PALE_OAK_SLAB = "minecraft:pale_oak_slab";
public const PALE_OAK_STAIRS = "minecraft:pale_oak_stairs";
public const PALE_OAK_STANDING_SIGN = "minecraft:pale_oak_standing_sign";
public const PALE_OAK_TRAPDOOR = "minecraft:pale_oak_trapdoor";
public const PALE_OAK_WALL_SIGN = "minecraft:pale_oak_wall_sign";
public const PALE_OAK_WOOD = "minecraft:pale_oak_wood";
public const PEARLESCENT_FROGLIGHT = "minecraft:pearlescent_froglight";
public const PEONY = "minecraft:peony";
public const PETRIFIED_OAK_DOUBLE_SLAB = "minecraft:petrified_oak_double_slab";
@ -994,6 +1019,13 @@ final class BlockTypeNames{
public const REINFORCED_DEEPSLATE = "minecraft:reinforced_deepslate";
public const REPEATING_COMMAND_BLOCK = "minecraft:repeating_command_block";
public const RESERVED6 = "minecraft:reserved6";
public const RESIN_BLOCK = "minecraft:resin_block";
public const RESIN_BRICK_DOUBLE_SLAB = "minecraft:resin_brick_double_slab";
public const RESIN_BRICK_SLAB = "minecraft:resin_brick_slab";
public const RESIN_BRICK_STAIRS = "minecraft:resin_brick_stairs";
public const RESIN_BRICK_WALL = "minecraft:resin_brick_wall";
public const RESIN_BRICKS = "minecraft:resin_bricks";
public const RESIN_CLUMP = "minecraft:resin_clump";
public const RESPAWN_ANCHOR = "minecraft:respawn_anchor";
public const ROSE_BUSH = "minecraft:rose_bush";
public const SAND = "minecraft:sand";
@ -1096,6 +1128,8 @@ final class BlockTypeNames{
public const STRIPPED_MANGROVE_WOOD = "minecraft:stripped_mangrove_wood";
public const STRIPPED_OAK_LOG = "minecraft:stripped_oak_log";
public const STRIPPED_OAK_WOOD = "minecraft:stripped_oak_wood";
public const STRIPPED_PALE_OAK_LOG = "minecraft:stripped_pale_oak_log";
public const STRIPPED_PALE_OAK_WOOD = "minecraft:stripped_pale_oak_wood";
public const STRIPPED_SPRUCE_LOG = "minecraft:stripped_spruce_log";
public const STRIPPED_SPRUCE_WOOD = "minecraft:stripped_spruce_wood";
public const STRIPPED_WARPED_HYPHAE = "minecraft:stripped_warped_hyphae";

View File

@ -121,6 +121,7 @@ use pocketmine\block\RedstoneOre;
use pocketmine\block\RedstoneRepeater;
use pocketmine\block\RedstoneTorch;
use pocketmine\block\RedstoneWire;
use pocketmine\block\ResinClump;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\Sapling;
use pocketmine\block\SeaPickle;
@ -704,6 +705,20 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$this->mapSlab(Blocks::OAK_SLAB(), Ids::OAK_SLAB, Ids::OAK_DOUBLE_SLAB);
$this->mapStairs(Blocks::OAK_STAIRS(), Ids::OAK_STAIRS);
$this->map(Blocks::PALE_OAK_BUTTON(), fn(WoodenButton $block) => Helper::encodeButton($block, new Writer(Ids::PALE_OAK_BUTTON)));
$this->map(Blocks::PALE_OAK_DOOR(), fn(WoodenDoor $block) => Helper::encodeDoor($block, new Writer(Ids::PALE_OAK_DOOR)));
$this->map(Blocks::PALE_OAK_FENCE_GATE(), fn(FenceGate $block) => Helper::encodeFenceGate($block, new Writer(Ids::PALE_OAK_FENCE_GATE)));
$this->map(Blocks::PALE_OAK_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => Helper::encodeSimplePressurePlate($block, new Writer(Ids::PALE_OAK_PRESSURE_PLATE)));
$this->map(Blocks::PALE_OAK_SIGN(), fn(FloorSign $block) => Helper::encodeFloorSign($block, new Writer(Ids::PALE_OAK_STANDING_SIGN)));
$this->map(Blocks::PALE_OAK_TRAPDOOR(), fn(WoodenTrapdoor $block) => Helper::encodeTrapdoor($block, new Writer(Ids::PALE_OAK_TRAPDOOR)));
$this->map(Blocks::PALE_OAK_WALL_SIGN(), fn(WallSign $block) => Helper::encodeWallSign($block, new Writer(Ids::PALE_OAK_WALL_SIGN)));
$this->mapLog(Blocks::PALE_OAK_LOG(), Ids::PALE_OAK_LOG, Ids::STRIPPED_PALE_OAK_LOG);
$this->mapLog(Blocks::PALE_OAK_WOOD(), Ids::PALE_OAK_WOOD, Ids::STRIPPED_PALE_OAK_WOOD);
$this->mapSimple(Blocks::PALE_OAK_FENCE(), Ids::PALE_OAK_FENCE);
$this->mapSimple(Blocks::PALE_OAK_PLANKS(), Ids::PALE_OAK_PLANKS);
$this->mapSlab(Blocks::PALE_OAK_SLAB(), Ids::PALE_OAK_SLAB, Ids::PALE_OAK_DOUBLE_SLAB);
$this->mapStairs(Blocks::PALE_OAK_STAIRS(), Ids::PALE_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)));
@ -740,6 +755,7 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$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)));
$this->map(Blocks::PALE_OAK_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::PALE_OAK_LEAVES)));
//legacy mess
$this->map(Blocks::ACACIA_LEAVES(), fn(Leaves $block) => Helper::encodeLeaves($block, new Writer(Ids::ACACIA_LEAVES)));
@ -795,6 +811,7 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$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_RESIN_BRICKS(), Ids::CHISELED_RESIN_BRICKS);
$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);
@ -1036,6 +1053,8 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$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::RESIN(), Ids::RESIN_BLOCK);
$this->mapSimple(Blocks::RESIN_BRICKS(), Ids::RESIN_BRICKS);
$this->mapSimple(Blocks::SAND(), Ids::SAND);
$this->mapSimple(Blocks::SANDSTONE(), Ids::SANDSTONE);
$this->mapSimple(Blocks::SCULK(), Ids::SCULK);
@ -1720,6 +1739,13 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$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->mapSlab(Blocks::RESIN_BRICK_SLAB(), Ids::RESIN_BRICK_SLAB, Ids::RESIN_BRICK_DOUBLE_SLAB);
$this->map(Blocks::RESIN_BRICK_STAIRS(), fn(Stair $block) => Helper::encodeStairs($block, new Writer(Ids::RESIN_BRICK_STAIRS)));
$this->map(Blocks::RESIN_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::RESIN_BRICK_WALL)));
$this->map(Blocks::RESIN_CLUMP(), function(ResinClump $block) : Writer{
return Writer::create(Ids::RESIN_CLUMP)
->writeFacingFlags($block->getFaces());
});
$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);

View File

@ -608,6 +608,20 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$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::PALE_OAK_BUTTON, fn(Reader $in) => Helper::decodeButton(Blocks::PALE_OAK_BUTTON(), $in));
$this->map(Ids::PALE_OAK_DOOR, fn(Reader $in) => Helper::decodeDoor(Blocks::PALE_OAK_DOOR(), $in));
$this->map(Ids::PALE_OAK_FENCE_GATE, fn(Reader $in) => Helper::decodeFenceGate(Blocks::PALE_OAK_FENCE_GATE(), $in));
$this->map(Ids::PALE_OAK_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(Blocks::PALE_OAK_PRESSURE_PLATE(), $in));
$this->map(Ids::PALE_OAK_STANDING_SIGN, fn(Reader $in) => Helper::decodeFloorSign(Blocks::PALE_OAK_SIGN(), $in));
$this->map(Ids::PALE_OAK_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Blocks::PALE_OAK_TRAPDOOR(), $in));
$this->map(Ids::PALE_OAK_WALL_SIGN, fn(Reader $in) => Helper::decodeWallSign(Blocks::PALE_OAK_WALL_SIGN(), $in));
$this->mapLog(Ids::PALE_OAK_LOG, Ids::STRIPPED_PALE_OAK_LOG, fn() => Blocks::PALE_OAK_LOG());
$this->mapLog(Ids::PALE_OAK_WOOD, Ids::STRIPPED_PALE_OAK_WOOD, fn() => Blocks::PALE_OAK_WOOD());
$this->mapSimple(Ids::PALE_OAK_FENCE, fn() => Blocks::PALE_OAK_FENCE());
$this->mapSimple(Ids::PALE_OAK_PLANKS, fn() => Blocks::PALE_OAK_PLANKS());
$this->mapSlab(Ids::PALE_OAK_SLAB, Ids::PALE_OAK_DOUBLE_SLAB, fn() => Blocks::PALE_OAK_SLAB());
$this->mapStairs(Ids::PALE_OAK_STAIRS, fn() => Blocks::PALE_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));
@ -647,6 +661,7 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$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::PALE_OAK_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::PALE_OAK_LEAVES(), $in));
$this->map(Ids::SPRUCE_LEAVES, fn(Reader $in) => Helper::decodeLeaves(Blocks::SPRUCE_LEAVES(), $in));
}
@ -720,6 +735,7 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$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_RESIN_BRICKS, fn() => Blocks::CHISELED_RESIN_BRICKS());
$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());
@ -957,6 +973,8 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$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::RESIN_BLOCK, fn() => Blocks::RESIN());
$this->mapSimple(Ids::RESIN_BRICKS, fn() => Blocks::RESIN_BRICKS());
$this->mapSimple(Ids::SAND, fn() => Blocks::SAND());
$this->mapSimple(Ids::SANDSTONE, fn() => Blocks::SANDSTONE());
$this->mapSimple(Ids::SCULK, fn() => Blocks::SCULK());
@ -1567,6 +1585,10 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
return Blocks::SUGARCANE()
->setAge($in->readBoundedInt(StateNames::AGE, 0, 15));
});
$this->mapSlab(Ids::RESIN_BRICK_SLAB, Ids::RESIN_BRICK_DOUBLE_SLAB, fn() => Blocks::RESIN_BRICK_SLAB());
$this->mapStairs(Ids::RESIN_BRICK_STAIRS, fn() => Blocks::RESIN_BRICK_STAIRS());
$this->map(Ids::RESIN_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RESIN_BRICK_WALL(), $in));
$this->map(Ids::RESIN_CLUMP, fn(Reader $in) => Blocks::RESIN_CLUMP()->setFaces($in->readFacingFlags()));
$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));

View File

@ -151,6 +151,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());
@ -302,11 +303,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());
@ -331,6 +336,7 @@ final class ItemSerializerDeserializerRegistrar{
$this->map1to1Item(Ids::OAK_BOAT, Items::OAK_BOAT());
$this->map1to1Item(Ids::OAK_SIGN, Items::OAK_SIGN());
$this->map1to1Item(Ids::PAINTING, Items::PAINTING());
$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 +360,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());

View File

@ -75,6 +75,7 @@ final class ItemTypeNames{
public const BLEACH = "minecraft:bleach";
public const BLUE_BUNDLE = "minecraft:blue_bundle";
public const BLUE_DYE = "minecraft:blue_dye";
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";
@ -154,6 +155,7 @@ final class ItemTypeNames{
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";
@ -398,6 +400,11 @@ final class ItemTypeNames{
public const ORANGE_DYE = "minecraft:orange_dye";
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";
@ -448,6 +455,7 @@ final class ItemTypeNames{
public const RED_FLOWER = "minecraft:red_flower";
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";

View File

@ -328,8 +328,14 @@ 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 FIRST_UNUSED_ITEM_ID = 20292;
public const FIRST_UNUSED_ITEM_ID = 20298;
private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID;

View File

@ -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,13 @@ 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("rooted_dirt", fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED));
$result->registerBlock("rose", fn() => Blocks::POPPY());
$result->registerBlock("rose_bush", fn() => Blocks::ROSE_BUSH());
@ -1084,6 +1105,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));
@ -1472,11 +1495,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 +1511,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());

View File

@ -243,6 +243,7 @@ use function strtolower;
* @method static Boat OAK_BOAT()
* @method static ItemBlockWallOrFloor OAK_SIGN()
* @method static PaintingItem PAINTING()
* @method static ItemBlockWallOrFloor PALE_OAK_SIGN()
* @method static Item PAPER()
* @method static Item PHANTOM_MEMBRANE()
* @method static PitcherPod PITCHER_POD()
@ -275,17 +276,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()
@ -535,6 +541,7 @@ final class VanillaItems{
});
self::register("oak_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::OAK_SIGN(), Blocks::OAK_WALL_SIGN()));
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("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 +573,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]));

View File

@ -77,6 +77,7 @@ 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;
@ -135,7 +136,7 @@ class InGamePacketHandler extends PacketHandler{
protected ?Vector3 $lastPlayerAuthInputPosition = null;
protected ?float $lastPlayerAuthInputYaw = null;
protected ?float $lastPlayerAuthInputPitch = null;
protected ?int $lastPlayerAuthInputFlags = null;
protected ?BitSet $lastPlayerAuthInputFlags = null;
public bool $forceMoveSync = false;
@ -161,9 +162,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;
}
@ -215,7 +216,10 @@ class InGamePacketHandler extends PacketHandler{
if($inputFlags !== $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 +234,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();
}
}

View File

@ -93,6 +93,7 @@ final class ItemStackResponseBuilder{
$item->getCount(),
$itemStackInfo->getStackId(),
$item->getCustomName(),
$item->getCustomName(),
$item instanceof Durable ? $item->getDamage() : 0,
);
}

View File

@ -37,6 +37,7 @@ 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\resourcepacks\ResourcePack;
use Ramsey\Uuid\Uuid;
use function array_keys;
use function array_map;
use function ceil;
@ -103,7 +104,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 +118,9 @@ class ResourcePacksPacketHandler extends PacketHandler{
resourcePackEntries: $resourcePackEntries,
mustAccept: $this->mustAccept,
hasAddons: false,
hasScripts: false
hasScripts: false,
worldTemplateId: Uuid::fromString(Uuid::NIL),
worldTemplateVersion: ""
));
$this->session->getLogger()->debug("Waiting for client to accept resource packs");
}

View File

@ -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{
@ -82,7 +82,10 @@ class RakLibServer extends Thread{
}
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);

View File

@ -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;

View File

@ -23,6 +23,9 @@ declare(strict_types=1);
namespace pocketmine\plugin;
/**
* @deprecated
*/
interface ResourceProvider{
/**
* Gets an embedded resource on the plugin file.

View File

@ -68,7 +68,10 @@ abstract class AsyncTask extends Runnable{
*/
private static array $threadLocalStorage = [];
/** @phpstan-var ThreadSafeArray<int, string>|null */
/**
* @phpstan-var ThreadSafeArray<int, string>|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.

View File

@ -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{

View File

@ -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,

View File

@ -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 = [];

View File

@ -59,6 +59,16 @@ 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 = "";
private static ?bool $formattingCodes = null;
@ -111,6 +121,16 @@ 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);
}
protected static function getEscapeCodes() : void{
@ -144,15 +164,25 @@ 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);
}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 = $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 +221,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 +243,16 @@ 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,
default => $token,
};
}

View File

@ -63,6 +63,16 @@ 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 COLORS = [
self::BLACK => self::BLACK,
@ -82,19 +92,29 @@ 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,
];
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 +150,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-u])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
if($result === false) throw self::makePcreError();
return $result;
}
@ -144,7 +164,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-u]/u", "", $string));
}
return str_replace("\x1b", "", self::preg_replace("/\x1b[\\(\\][[0-9;\\[\\(]+[Bm]/u", "", $string));
}
@ -155,7 +175,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-u])/u', TextFormat::ESCAPE . '$1', $string);
}
/**
@ -183,6 +203,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 +224,46 @@ abstract class TextFormat{
$newString = "";
$tokens = 0;
foreach(self::tokenize($string) as $token){
switch($token){
case TextFormat::BOLD:
$newString .= "<span style=font-weight:bold>";
++$tokens;
break;
case TextFormat::OBFUSCATED:
//$newString .= "<span style=text-decoration:line-through>";
//++$tokens;
break;
case TextFormat::ITALIC:
$newString .= "<span style=font-style:italic>";
++$tokens;
break;
case TextFormat::UNDERLINE:
$newString .= "<span style=text-decoration:underline>";
++$tokens;
break;
case TextFormat::STRIKETHROUGH:
$newString .= "<span style=text-decoration:line-through>";
++$tokens;
break;
case TextFormat::RESET:
$newString .= str_repeat("</span>", $tokens);
$tokens = 0;
break;
//Colors
case TextFormat::BLACK:
$newString .= "<span style=color:#000>";
++$tokens;
break;
case TextFormat::DARK_BLUE:
$newString .= "<span style=color:#00A>";
++$tokens;
break;
case TextFormat::DARK_GREEN:
$newString .= "<span style=color:#0A0>";
++$tokens;
break;
case TextFormat::DARK_AQUA:
$newString .= "<span style=color:#0AA>";
++$tokens;
break;
case TextFormat::DARK_RED:
$newString .= "<span style=color:#A00>";
++$tokens;
break;
case TextFormat::DARK_PURPLE:
$newString .= "<span style=color:#A0A>";
++$tokens;
break;
case TextFormat::GOLD:
$newString .= "<span style=color:#FA0>";
++$tokens;
break;
case TextFormat::GRAY:
$newString .= "<span style=color:#AAA>";
++$tokens;
break;
case TextFormat::DARK_GRAY:
$newString .= "<span style=color:#555>";
++$tokens;
break;
case TextFormat::BLUE:
$newString .= "<span style=color:#55F>";
++$tokens;
break;
case TextFormat::GREEN:
$newString .= "<span style=color:#5F5>";
++$tokens;
break;
case TextFormat::AQUA:
$newString .= "<span style=color:#5FF>";
++$tokens;
break;
case TextFormat::RED:
$newString .= "<span style=color:#F55>";
++$tokens;
break;
case TextFormat::LIGHT_PURPLE:
$newString .= "<span style=color:#F5F>";
++$tokens;
break;
case TextFormat::YELLOW:
$newString .= "<span style=color:#FF5>";
++$tokens;
break;
case TextFormat::WHITE:
$newString .= "<span style=color:#FFF>";
++$tokens;
break;
case TextFormat::MINECOIN_GOLD:
$newString .= "<span style=color:#dd0>";
++$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::BOLD => "font-weight:bold",
TextFormat::ITALIC => "font-style:italic",
default => null
};
if($formatString !== null){
$newString .= "<span style=\"$formatString\">";
++$tokens;
}elseif($token === TextFormat::RESET){
$newString .= str_repeat("</span>", $tokens);
$tokens = 0;
}else{
$newString .= $token;
}
}

View File

@ -167,6 +167,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<int, Player>
@ -202,6 +205,7 @@ class World implements ChunkManager{
* @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, Block>>
*/
private array $blockCache = [];
private int $blockCacheSize = 0;
/**
* @var AxisAlignedBB[][][] chunkHash => [relativeBlockHash => AxisAlignedBB[]]
* @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, list<AxisAlignedBB>>>
@ -653,6 +657,7 @@ class World implements ChunkManager{
$this->provider->close();
$this->blockCache = [];
$this->blockCacheSize = 0;
$this->blockCollisionBoxCache = [];
$this->unloaded = true;
@ -1138,13 +1143,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;
}
}
@ -1152,7 +1160,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;
@ -1161,6 +1169,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<int, true>
@ -1921,6 +1942,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;
@ -1967,6 +1992,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
@ -2570,6 +2596,7 @@ class World implements ChunkManager{
$this->chunks[$chunkHash] = $chunk;
$this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
unset($this->blockCache[$chunkHash]);
unset($this->blockCollisionBoxCache[$chunkHash]);
unset($this->changedBlocks[$chunkHash]);
@ -2854,6 +2881,8 @@ 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]);
@ -3013,6 +3042,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]);

View File

@ -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;

View File

@ -25,9 +25,11 @@ namespace pocketmine\world\format\io;
use pocketmine\utils\Utils;
use pocketmine\world\format\io\leveldb\LevelDB;
use pocketmine\world\format\io\leveldb\RegionizedLevelDB;
use pocketmine\world\format\io\region\Anvil;
use pocketmine\world\format\io\region\McRegion;
use pocketmine\world\format\io\region\PMAnvil;
use pocketmine\world\WorldCreationOptions;
use function strtolower;
use function trim;
@ -41,10 +43,24 @@ final class WorldProviderManager{
private WritableWorldProviderManagerEntry $default;
public function __construct(){
$leveldb = new WritableWorldProviderManagerEntry(LevelDB::isValid(...), fn(string $path, \Logger $logger) => new LevelDB($path, $logger), LevelDB::generate(...));
$leveldb = new WritableWorldProviderManagerEntry(
LevelDB::isValid(...),
fn(string $path, \Logger $logger) => new LevelDB($path, $logger),
LevelDB::generate(...)
);
$this->default = $leveldb;
$this->addProvider($leveldb, "leveldb");
//any arbitrary size is supported, but powers of 2 are best
//these are the most likely to be useful
foreach([128, 256] as $regionLength){
$this->addProvider(new WritableWorldProviderManagerEntry(
fn(string $path) => RegionizedLevelDB::isValid($path, $regionLength),
fn(string $path, \Logger $logger) => new RegionizedLevelDB($path, $logger, $regionLength),
fn(string $path, string $name, WorldCreationOptions $options) => RegionizedLevelDB::generate($path, $name, $options, $regionLength)
), "custom-leveldb-regions-$regionLength");
}
$this->addProvider(new ReadOnlyWorldProviderManagerEntry(Anvil::isValid(...), fn(string $path, \Logger $logger) => new Anvil($path, $logger)), "anvil");
$this->addProvider(new ReadOnlyWorldProviderManagerEntry(McRegion::isValid(...), fn(string $path, \Logger $logger) => new McRegion($path, $logger)), "mcregion");
$this->addProvider(new ReadOnlyWorldProviderManagerEntry(PMAnvil::isValid(...), fn(string $path, \Logger $logger) => new PMAnvil($path, $logger)), "pmanvil");

View File

@ -0,0 +1,824 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\format\io\leveldb;
use pocketmine\block\Block;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
use pocketmine\data\bedrock\block\convert\UnsupportedBlockStateException;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\VersionInfo;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\BaseWorldProvider;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\ChunkUtils;
use pocketmine\world\format\io\data\BedrockWorldData;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\LoadedChunkData;
use pocketmine\world\format\io\WorldData;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function array_map;
use function array_values;
use function chr;
use function count;
use function defined;
use function extension_loaded;
use function implode;
use function mkdir;
use function ord;
use function str_repeat;
use function strlen;
use function substr;
use function unpack;
abstract class BaseLevelDB extends BaseWorldProvider implements WritableWorldProvider{
protected const FINALISATION_NEEDS_INSTATICKING = 0;
protected const FINALISATION_NEEDS_POPULATION = 1;
protected const FINALISATION_DONE = 2;
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;
private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
private static function checkForLevelDBExtension() : void{
if(!extension_loaded('leveldb')){
throw new UnsupportedWorldFormatException("The leveldb PHP extension is required to use this world format");
}
if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){
throw new UnsupportedWorldFormatException("Given version of php-leveldb doesn't support zlib raw compression");
}
}
public function __construct(string $path, \Logger $logger){
self::checkForLevelDBExtension();
parent::__construct($path, $logger);
}
protected function loadLevelData() : WorldData{
return new BedrockWorldData(Path::join($this->getPath(), "level.dat"));
}
public function getWorldMinY() : int{
return -64;
}
public function getWorldMaxY() : int{
return 320;
}
protected static function baseGenerate(string $path, string $name, WorldCreationOptions $options) : void{
self::checkForLevelDBExtension();
@mkdir($path, 0777, true);
BedrockWorldData::generate($path, $name, $options);
}
/**
* @throws CorruptedChunkException
*/
protected function deserializeBlockPalette(BinaryStream $stream, \Logger $logger) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte() >> 1;
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e);
}
$nbt = new LittleEndianNbtSerializer();
$palette = [];
if($bitsPerBlock === 0){
$paletteSize = 1;
/*
* Due to code copy-paste in a public plugin, some PM4 worlds have 0 bpb palettes with a length prefix.
* This is invalid and does not happen in vanilla.
* These palettes were accepted by PM4 despite being invalid, but PM5 considered them corrupt, causing loss
* of data. Since many users were affected by this, a workaround is therefore necessary to allow PM5 to read
* these worlds without data loss.
*
* References:
* - https://github.com/Refaltor77/CustomItemAPI/issues/68
* - https://github.com/pmmp/PocketMine-MP/issues/5911
*/
$offset = $stream->getOffset();
$byte1 = $stream->getByte();
$stream->setOffset($offset); //reset offset
if($byte1 !== NBT::TAG_Compound){ //normally the first byte would be the NBT of the blockstate
$susLength = $stream->getLInt();
if($susLength !== 1){ //make sure the data isn't complete garbage
throw new CorruptedChunkException("CustomItemAPI borked 0 bpb palette should always have a length of 1");
}
$logger->error("Unexpected palette size for 0 bpb palette");
}
}else{
$paletteSize = $stream->getLInt();
}
$blockDecodeErrors = [];
for($i = 0; $i < $paletteSize; ++$i){
try{
$offset = $stream->getOffset();
$blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
}catch(NbtDataException $e){
//NBT borked, unrecoverable
throw new CorruptedChunkException("Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
}
//TODO: remember data for unknown states so we can implement them later
try{
$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();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
continue;
}
try{
$palette[] = $this->blockStateDeserializer->deserialize($blockStateData);
}catch(UnsupportedBlockStateException $e){
$blockDecodeErrors[] = "Palette offset $i / " . $e->getMessage();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}catch(BlockStateDeserializeException $e){
$blockDecodeErrors[] = "Palette offset $i / Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}
}
if(count($blockDecodeErrors) > 0){
//$logger->error("Errors decoding blocks:\n - " . implode("\n - ", $blockDecodeErrors));
}
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
$stream->putByte($blocks->getBitsPerBlock() << 1);
$stream->put($blocks->getWordArray());
$palette = $blocks->getPalette();
if($blocks->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
$tags = [];
foreach($palette as $p){
$tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
}
$stream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
}
/**
* @throws CorruptedChunkException
*/
private static function getExpected3dBiomesCount(int $chunkVersion) : int{
return match(true){
$chunkVersion >= ChunkVersion::v1_18_30 => 24,
$chunkVersion >= ChunkVersion::v1_18_0_25_beta => 25,
$chunkVersion >= ChunkVersion::v1_18_0_24_beta => 32,
$chunkVersion >= ChunkVersion::v1_18_0_22_beta => 65,
$chunkVersion >= ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs => 32,
default => throw new CorruptedChunkException("Chunk version $chunkVersion should not have 3D biomes")
};
}
/**
* @throws CorruptedChunkException
*/
private static function deserializeBiomePalette(BinaryStream $stream, int $bitsPerBlock) : PalettedBlockArray{
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
}
$palette = [];
$paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
for($i = 0; $i < $paletteSize; ++$i){
$palette[] = $stream->getLInt();
}
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
$stream->putByte($biomes->getBitsPerBlock() << 1);
$stream->put($biomes->getWordArray());
$palette = $biomes->getPalette();
if($biomes->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
foreach($palette as $p){
$stream->putLInt($p);
}
}
/**
* @throws CorruptedChunkException
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private static function deserialize3dBiomes(BinaryStream $stream, int $chunkVersion, \Logger $logger) : array{
$previous = null;
$result = [];
$nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
$expectedCount = self::getExpected3dBiomesCount($chunkVersion);
for($i = 0; $i < $expectedCount; ++$i){
try{
$bitsPerBlock = $stream->getByte() >> 1;
if($bitsPerBlock === 127){
if($previous === null){
throw new CorruptedChunkException("Serialized biome palette $i has no previous palette to copy from");
}
$decoded = clone $previous;
}else{
$decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
}
$previous = $decoded;
if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ //older versions wrote additional superfluous biome palettes
$result[$nextIndex++] = $decoded;
}elseif($stream->feof()){
//not enough padding biome arrays for the given version - this is non-critical since we discard the excess anyway, but this should be logged
$logger->error("Wrong number of 3D biome palettes for this chunk version: expected $expectedCount, but got " . ($i + 1) . " - this is not a problem, but may indicate a corrupted chunk");
break;
}
}catch(BinaryDataException $e){
throw new CorruptedChunkException("Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
}
}
if(!$stream->feof()){
//maybe bad output produced by a third-party conversion tool like Chunker
$logger->error("Unexpected trailing data after 3D biomes data");
}
return $result;
}
/**
* @param SubChunk[] $subChunks
*/
private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
//TODO: the server-side min/max may not coincide with the world storage min/max - we may need additional logic to handle this
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
//TODO: is it worth trying to use the previous palette if it's the same as the current one? vanilla supports
//this, but it's not clear if it's worth the effort to implement.
self::serializeBiomePalette($stream, $subChunks[$y]->getBiomeArray());
}
}
/**
* @phpstan-param-out int $x
* @phpstan-param-out int $y
* @phpstan-param-out int $z
*/
protected static function deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z) : void{
if($chunkVersion >= ChunkVersion::v1_0_0){
$x = ($key >> 12) & 0xf;
$z = ($key >> 8) & 0xf;
$y = $key & 0xff;
}else{ //pre-1.0, 7 bits were used because the build height limit was lower
$x = ($key >> 11) & 0xf;
$z = ($key >> 7) & 0xf;
$y = $key & 0x7f;
}
}
/**
* @return PalettedBlockArray[]
*/
protected function deserializeLegacyExtraData(\LevelDB $db, string $index, int $chunkVersion, \Logger $logger) : array{
if(($extraRawData = $db->get($index . ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA)) === false || $extraRawData === ""){
return [];
}
/** @var PalettedBlockArray[] $extraDataLayers */
$extraDataLayers = [];
$binaryStream = new BinaryStream($extraRawData);
$count = $binaryStream->getLInt();
for($i = 0; $i < $count; ++$i){
$key = $binaryStream->getLInt();
$value = $binaryStream->getLShort();
self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
$ySub = ($fullY >> SubChunk::COORD_BIT_SIZE);
$y = $key & SubChunk::COORD_MASK;
$blockId = $value & 0xff;
$blockData = ($value >> 8) & 0xf;
try{
$blockStateData = $this->blockDataUpgrader->upgradeIntIdMeta($blockId, $blockData);
}catch(BlockStateDeserializeException $e){
//TODO: we could preserve this in case it's supported in the future, but this was historically only
//used for grass anyway, so we probably don't need to care
$logger->error("Failed to upgrade legacy extra block: " . $e->getMessage() . " ($blockId:$blockData)");
continue;
}
//assume this won't throw
$blockStateId = $this->blockStateDeserializer->deserialize($blockStateData);
if(!isset($extraDataLayers[$ySub])){
$extraDataLayers[$ySub] = new PalettedBlockArray(Block::EMPTY_STATE_ID);
}
$extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
}
return $extraDataLayers;
}
private function readVersion(\LevelDB $db, int $chunkX, int $chunkZ) : ?int{
$index = $this->coordsToChunkIndex($chunkX, $chunkZ);
$chunkVersionRaw = $db->get($index . ChunkDataKey::NEW_VERSION);
if($chunkVersionRaw === false){
$chunkVersionRaw = $db->get($index . ChunkDataKey::OLD_VERSION);
if($chunkVersionRaw === false){
return null;
}
}
return ord($chunkVersionRaw);
}
/**
* Deserializes terrain data stored in the 0.9 full-chunk format into subchunks.
*
* @return SubChunk[]
* @phpstan-return array<int, SubChunk>
* @throws CorruptedWorldException
*/
private function deserializeLegacyTerrainData(\LevelDB $db, string $index, int $chunkVersion, \Logger $logger) : array{
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($db, $index, $chunkVersion, $logger);
$legacyTerrain = $db->get($index . ChunkDataKey::LEGACY_TERRAIN);
if($legacyTerrain === false){
throw new CorruptedChunkException("Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
}
$binaryStream = new BinaryStream($legacyTerrain);
try{
$fullIds = $binaryStream->get(32768);
$fullData = $binaryStream->get(16384);
$binaryStream->get(32768); //legacy light info, discard it
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
try{
$binaryStream->get(256); //heightmap, discard it
/** @var int[] $unpackedBiomeArray */
$unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
$biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data in legacy terrain data");
}
$subChunks = [];
for($yy = 0; $yy < 8; ++$yy){
$storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"))];
if(isset($convertedLegacyExtraData[$yy])){
$storages[] = $convertedLegacyExtraData[$yy];
}
$subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
}
//make sure extrapolated biomes get filled in correctly
for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){
if(!isset($subChunks[$yy])){
$subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d);
}
}
return $subChunks;
}
/**
* Deserializes a subchunk stored in the legacy non-paletted format used from 1.0 until 1.2.13.
*/
private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream, int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
try{
$blocks = $binaryStream->get(4096);
$blockData = $binaryStream->get(2048);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if($chunkVersion < ChunkVersion::v1_1_0){
try{
$binaryStream->get(4096); //legacy light info, discard it
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data in legacy subchunk data");
}
}catch(BinaryDataException $e){
$logger->error("Failed to read legacy subchunk light info: " . $e->getMessage());
}
}
$storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)];
if($convertedLegacyExtraData !== null){
$storages[] = $convertedLegacyExtraData;
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
}
/**
* Deserializes subchunk data stored under a subchunk LevelDB key.
*
* @see ChunkDataKey::SUBCHUNK
* @throws CorruptedChunkException
*/
private function deserializeSubChunkData(BinaryStream $binaryStream, int $chunkVersion, int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
switch($subChunkVersion){
case SubChunkVersion::CLASSIC:
case SubChunkVersion::CLASSIC_BUG_2: //these are all identical to version 0, but vanilla respects these so we should also
case SubChunkVersion::CLASSIC_BUG_3:
case SubChunkVersion::CLASSIC_BUG_4:
case SubChunkVersion::CLASSIC_BUG_5:
case SubChunkVersion::CLASSIC_BUG_6:
case SubChunkVersion::CLASSIC_BUG_7:
return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger);
case SubChunkVersion::PALETTED_SINGLE:
$storages = [$this->deserializeBlockPalette($binaryStream, $logger)];
if($convertedLegacyExtraData !== null){
$storages[] = $convertedLegacyExtraData;
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
$storageCount = $binaryStream->getByte();
if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
//height ignored; this seems pointless since this is already in the key anyway
$binaryStream->getByte();
}
$storages = [];
for($k = 0; $k < $storageCount; ++$k){
$storages[] = $this->deserializeBlockPalette($binaryStream, $logger);
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
default:
//this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption
throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion");
}
}
private static function hasOffsetCavesAndCliffsSubChunks(int $chunkVersion) : bool{
return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
}
/**
* Deserializes any subchunks stored under subchunk LevelDB keys, upgrading them to the current format if necessary.
*
* @param PalettedBlockArray[] $convertedLegacyExtraData
* @param PalettedBlockArray[] $biomeArrays
*
* @phpstan-param array<int, PalettedBlockArray> $convertedLegacyExtraData
* @phpstan-param array<int, PalettedBlockArray> $biomeArrays
* @phpstan-param-out bool $hasBeenUpgraded
*
* @return SubChunk[]
* @phpstan-return array<int, SubChunk>
*/
private function deserializeAllSubChunkData(\LevelDB $db, string $index, int $chunkVersion, bool &$hasBeenUpgraded, array $convertedLegacyExtraData, array $biomeArrays, \Logger $logger) : array{
$subChunks = [];
$subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(($data = $db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){
$subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]);
continue;
}
$binaryStream = new BinaryStream($data);
if($binaryStream->feof()){
throw new CorruptedChunkException("Unexpected empty data for subchunk $y");
}
$subChunkVersion = $binaryStream->getByte();
if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
$hasBeenUpgraded = true;
}
$subChunks[$y] = $this->deserializeSubChunkData(
$binaryStream,
$chunkVersion,
$subChunkVersion,
$convertedLegacyExtraData[$y] ?? null,
$biomeArrays[$y],
new \PrefixedLogger($logger, "Subchunk y=$y v$subChunkVersion")
);
}
return $subChunks;
}
/**
* Deserializes any available biome data into an array of paletted biomes. Old 2D biomes are extrapolated to 3D.
*
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private function deserializeBiomeData(\LevelDB $db, string $index, int $chunkVersion, \Logger $logger) : array{
$biomeArrays = [];
if(($maps2d = $db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps2d);
try{
$binaryStream->get(512); //heightmap, discard it
$biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); //never throws
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data after 2D biome data");
}
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = clone $biomes3d;
}
}elseif(($maps3d = $db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps3d);
try{
$binaryStream->get(512);
$biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}else{
$logger->error("Missing biome data, using default ocean biome");
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = new PalettedBlockArray(BiomeIds::OCEAN); //polyfill
}
}
return $biomeArrays;
}
protected function loadChunkFromDB(\LevelDB $db, int $chunkX, int $chunkZ) : ?LoadedChunkData{
$index = $this->coordsToChunkIndex($chunkX, $chunkZ);
$chunkVersion = $this->readVersion($db, $chunkX, $chunkZ);
if($chunkVersion === null){
//TODO: this might be a slightly-corrupted chunk with a missing version field
return null;
}
//TODO: read PM_DATA_VERSION - we'll need it to fix up old chunks
$logger = new \PrefixedLogger($this->logger, "Loading chunk x=$chunkX z=$chunkZ v$chunkVersion");
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
switch($chunkVersion){
case ChunkVersion::v1_21_40:
//TODO: BiomeStates became shorts instead of bytes
case ChunkVersion::v1_18_30:
case ChunkVersion::v1_18_0_25_beta:
case ChunkVersion::v1_18_0_24_unused:
case ChunkVersion::v1_18_0_24_beta:
case ChunkVersion::v1_18_0_22_unused:
case ChunkVersion::v1_18_0_22_beta:
case ChunkVersion::v1_18_0_20_unused:
case ChunkVersion::v1_18_0_20_beta:
case ChunkVersion::v1_17_40_unused:
case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_25_unused:
case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_23_unused:
case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_230_50_unused:
case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_220_50_unused:
case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_210:
case ChunkVersion::v1_16_100_57_beta:
case ChunkVersion::v1_16_100_52_beta:
case ChunkVersion::v1_16_0:
case ChunkVersion::v1_16_0_51_beta:
//TODO: check walls
case ChunkVersion::v1_12_0_unused2:
case ChunkVersion::v1_12_0_unused1:
case ChunkVersion::v1_12_0_4_beta:
case ChunkVersion::v1_11_1:
case ChunkVersion::v1_11_0_4_beta:
case ChunkVersion::v1_11_0_3_beta:
case ChunkVersion::v1_11_0_1_beta:
case ChunkVersion::v1_9_0:
case ChunkVersion::v1_8_0:
case ChunkVersion::v1_2_13:
case ChunkVersion::v1_2_0:
case ChunkVersion::v1_2_0_2_beta:
case ChunkVersion::v1_1_0_converted_from_console:
case ChunkVersion::v1_1_0:
//TODO: check beds
case ChunkVersion::v1_0_0:
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($db, $index, $chunkVersion, $logger);
$biomeArrays = $this->deserializeBiomeData($db, $index, $chunkVersion, $logger);
$subChunks = $this->deserializeAllSubChunkData($db, $index, $chunkVersion, $hasBeenUpgraded, $convertedLegacyExtraData, $biomeArrays, $logger);
break;
case ChunkVersion::v0_9_5:
case ChunkVersion::v0_9_2:
case ChunkVersion::v0_9_0:
$subChunks = $this->deserializeLegacyTerrainData($db, $index, $chunkVersion, $logger);
break;
default:
throw new CorruptedChunkException("don't know how to decode chunk format version $chunkVersion");
}
$nbt = new LittleEndianNbtSerializer();
/** @var CompoundTag[] $entities */
$entities = [];
if(($entityData = $db->get($index . ChunkDataKey::ENTITIES)) !== false && $entityData !== ""){
try{
$entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
}catch(NbtDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}
/** @var CompoundTag[] $tiles */
$tiles = [];
if(($tileData = $db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !== false && $tileData !== ""){
try{
$tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
}catch(NbtDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}
$finalisationChr = $db->get($index . ChunkDataKey::FINALIZATION);
if($finalisationChr !== false){
$finalisation = ord($finalisationChr);
$terrainPopulated = $finalisation === self::FINALISATION_DONE;
}else{ //older versions didn't have this tag
$terrainPopulated = true;
}
//TODO: tile ticks, biome states (?)
return new LoadedChunkData(
data: new ChunkData($subChunks, $terrainPopulated, $entities, $tiles),
upgraded: $hasBeenUpgraded,
fixerFlags: LoadedChunkData::FIXER_FLAG_ALL //TODO: fill this by version rather than just setting all flags
);
}
protected function saveChunkToDB(\LevelDB $db, int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags) : void{
$index = $this->coordsToChunkIndex($chunkX, $chunkZ);
$write = new \LevelDBWriteBatch();
$write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
$write->put($index . ChunkDataKey::PM_DATA_VERSION, Binary::writeLLong(VersionInfo::WORLD_DATA_VERSION));
$subChunks = $chunkData->getSubChunks();
if(($dirtyFlags & Chunk::DIRTY_FLAG_BLOCKS) !== 0){
foreach($subChunks as $y => $subChunk){
$key = $index . ChunkDataKey::SUBCHUNK . chr($y);
if($subChunk->isEmptyAuthoritative()){
$write->delete($key);
}else{
$subStream = new BinaryStream();
$subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
$layers = $subChunk->getBlockLayers();
$subStream->putByte(count($layers));
foreach($layers as $blocks){
$this->serializeBlockPalette($subStream, $blocks);
}
$write->put($key, $subStream->getBuffer());
}
}
}
if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
$stream = new BinaryStream();
$stream->put(str_repeat("\x00", 512)); //fake heightmap
self::serialize3dBiomes($stream, $subChunks);
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
}
//TODO: use this properly
$write->put($index . ChunkDataKey::FINALIZATION, chr($chunkData->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION));
$this->writeTags($chunkData->getTileNBT(), $index . ChunkDataKey::BLOCK_ENTITIES, $write);
$this->writeTags($chunkData->getEntityNBT(), $index . ChunkDataKey::ENTITIES, $write);
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS);
$write->delete($index . ChunkDataKey::LEGACY_TERRAIN);
$db->write($write);
}
/**
* @param CompoundTag[] $targets
*/
private function writeTags(array $targets, string $index, \LevelDBWriteBatch $write) : void{
if(count($targets) > 0){
$nbt = new LittleEndianNbtSerializer();
$write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) => new TreeRoot($tag), $targets)));
}else{
$write->delete($index);
}
}
abstract protected function coordsToChunkIndex(int $chunkX, int $chunkZ) : string;
/**
* @return int[]
* @phpstan-return array{int, int}
*/
abstract protected function coordsFromChunkIndex(string $chunkIndex) : array;
/**
* Returns a generator which yields all the chunks in this database.
*
* @return \Generator|LoadedChunkData[]
* @phpstan-return \Generator<array{int, int}, LoadedChunkData, void, void>
* @throws CorruptedChunkException
*/
protected function getAllChunksFromDB(\LevelDB $db, bool $skipCorrupted, ?\Logger $logger) : \Generator{
foreach($db->getIterator() as $key => $_){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
[$chunkX, $chunkZ] = $this->coordsFromChunkIndex(substr($key, 0, 8));
try{
if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
yield [$chunkX, $chunkZ] => $chunk;
}
}catch(CorruptedChunkException $e){
if(!$skipCorrupted){
throw $e;
}
if($logger !== null){
$logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
}
}
}
}
}
protected function calculateChunkCountInDB(\LevelDB $db) : int{
$count = 0;
foreach($db->getIterator() as $key => $_){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
$count++;
}
}
return $count;
}
}

View File

@ -23,78 +23,24 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\leveldb;
use pocketmine\block\Block;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
use pocketmine\data\bedrock\block\convert\UnsupportedBlockStateException;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\VersionInfo;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\BaseWorldProvider;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\ChunkUtils;
use pocketmine\world\format\io\data\BedrockWorldData;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\LoadedChunkData;
use pocketmine\world\format\io\WorldData;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function array_map;
use function array_values;
use function chr;
use function count;
use function defined;
use function extension_loaded;
use function file_exists;
use function implode;
use function is_dir;
use function mkdir;
use function ord;
use function str_repeat;
use function strlen;
use function substr;
use function trim;
use function unpack;
use const LEVELDB_ZLIB_RAW_COMPRESSION;
class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
protected const FINALISATION_NEEDS_INSTATICKING = 0;
protected const FINALISATION_NEEDS_POPULATION = 1;
protected const FINALISATION_DONE = 2;
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;
private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
class LevelDB extends BaseLevelDB{
protected \LevelDB $db;
private static function checkForLevelDBExtension() : void{
if(!extension_loaded('leveldb')){
throw new UnsupportedWorldFormatException("The leveldb PHP extension is required to use this world format");
}
if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){
throw new UnsupportedWorldFormatException("Given version of php-leveldb doesn't support zlib raw compression");
}
}
/**
* @throws \LevelDBException
*/
@ -106,7 +52,6 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
public function __construct(string $path, \Logger $logger){
self::checkForLevelDBExtension();
parent::__construct($path, $logger);
try{
@ -117,708 +62,53 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
}
protected function loadLevelData() : WorldData{
return new BedrockWorldData(Path::join($this->getPath(), "level.dat"));
}
public function getWorldMinY() : int{
return -64;
}
public function getWorldMaxY() : int{
return 320;
}
public static function isValid(string $path) : bool{
return file_exists(Path::join($path, "level.dat")) && is_dir(Path::join($path, "db"));
}
public static function generate(string $path, string $name, WorldCreationOptions $options) : void{
self::checkForLevelDBExtension();
self::baseGenerate($path, $name, $options);
$dbPath = Path::join($path, "db");
if(!file_exists($dbPath)){
mkdir($dbPath, 0777, true);
}
BedrockWorldData::generate($path, $name, $options);
}
/**
* @throws CorruptedChunkException
*/
protected function deserializeBlockPalette(BinaryStream $stream, \Logger $logger) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte() >> 1;
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e);
}
$nbt = new LittleEndianNbtSerializer();
$palette = [];
if($bitsPerBlock === 0){
$paletteSize = 1;
/*
* Due to code copy-paste in a public plugin, some PM4 worlds have 0 bpb palettes with a length prefix.
* This is invalid and does not happen in vanilla.
* These palettes were accepted by PM4 despite being invalid, but PM5 considered them corrupt, causing loss
* of data. Since many users were affected by this, a workaround is therefore necessary to allow PM5 to read
* these worlds without data loss.
*
* References:
* - https://github.com/Refaltor77/CustomItemAPI/issues/68
* - https://github.com/pmmp/PocketMine-MP/issues/5911
*/
$offset = $stream->getOffset();
$byte1 = $stream->getByte();
$stream->setOffset($offset); //reset offset
if($byte1 !== NBT::TAG_Compound){ //normally the first byte would be the NBT of the blockstate
$susLength = $stream->getLInt();
if($susLength !== 1){ //make sure the data isn't complete garbage
throw new CorruptedChunkException("CustomItemAPI borked 0 bpb palette should always have a length of 1");
}
$logger->error("Unexpected palette size for 0 bpb palette");
}
}else{
$paletteSize = $stream->getLInt();
}
$blockDecodeErrors = [];
for($i = 0; $i < $paletteSize; ++$i){
try{
$offset = $stream->getOffset();
$blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
}catch(NbtDataException $e){
//NBT borked, unrecoverable
throw new CorruptedChunkException("Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
}
//TODO: remember data for unknown states so we can implement them later
try{
$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();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
continue;
}
try{
$palette[] = $this->blockStateDeserializer->deserialize($blockStateData);
}catch(UnsupportedBlockStateException $e){
$blockDecodeErrors[] = "Palette offset $i / " . $e->getMessage();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}catch(BlockStateDeserializeException $e){
$blockDecodeErrors[] = "Palette offset $i / Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}
}
if(count($blockDecodeErrors) > 0){
$logger->error("Errors decoding blocks:\n - " . implode("\n - ", $blockDecodeErrors));
}
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
$stream->putByte($blocks->getBitsPerBlock() << 1);
$stream->put($blocks->getWordArray());
$palette = $blocks->getPalette();
if($blocks->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
$tags = [];
foreach($palette as $p){
$tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
}
$stream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
}
/**
* @throws CorruptedChunkException
*/
private static function getExpected3dBiomesCount(int $chunkVersion) : int{
return match(true){
$chunkVersion >= ChunkVersion::v1_18_30 => 24,
$chunkVersion >= ChunkVersion::v1_18_0_25_beta => 25,
$chunkVersion >= ChunkVersion::v1_18_0_24_beta => 32,
$chunkVersion >= ChunkVersion::v1_18_0_22_beta => 65,
$chunkVersion >= ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs => 32,
default => throw new CorruptedChunkException("Chunk version $chunkVersion should not have 3D biomes")
};
}
/**
* @throws CorruptedChunkException
*/
private static function deserializeBiomePalette(BinaryStream $stream, int $bitsPerBlock) : PalettedBlockArray{
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
}
$palette = [];
$paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
for($i = 0; $i < $paletteSize; ++$i){
$palette[] = $stream->getLInt();
}
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
$stream->putByte($biomes->getBitsPerBlock() << 1);
$stream->put($biomes->getWordArray());
$palette = $biomes->getPalette();
if($biomes->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
foreach($palette as $p){
$stream->putLInt($p);
}
}
/**
* @throws CorruptedChunkException
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private static function deserialize3dBiomes(BinaryStream $stream, int $chunkVersion, \Logger $logger) : array{
$previous = null;
$result = [];
$nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
$expectedCount = self::getExpected3dBiomesCount($chunkVersion);
for($i = 0; $i < $expectedCount; ++$i){
try{
$bitsPerBlock = $stream->getByte() >> 1;
if($bitsPerBlock === 127){
if($previous === null){
throw new CorruptedChunkException("Serialized biome palette $i has no previous palette to copy from");
}
$decoded = clone $previous;
}else{
$decoded = self::deserializeBiomePalette($stream, $bitsPerBlock);
}
$previous = $decoded;
if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ //older versions wrote additional superfluous biome palettes
$result[$nextIndex++] = $decoded;
}elseif($stream->feof()){
//not enough padding biome arrays for the given version - this is non-critical since we discard the excess anyway, but this should be logged
$logger->error("Wrong number of 3D biome palettes for this chunk version: expected $expectedCount, but got " . ($i + 1) . " - this is not a problem, but may indicate a corrupted chunk");
break;
}
}catch(BinaryDataException $e){
throw new CorruptedChunkException("Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
}
}
if(!$stream->feof()){
//maybe bad output produced by a third-party conversion tool like Chunker
$logger->error("Unexpected trailing data after 3D biomes data");
}
return $result;
}
/**
* @param SubChunk[] $subChunks
*/
private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
//TODO: the server-side min/max may not coincide with the world storage min/max - we may need additional logic to handle this
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
//TODO: is it worth trying to use the previous palette if it's the same as the current one? vanilla supports
//this, but it's not clear if it's worth the effort to implement.
self::serializeBiomePalette($stream, $subChunks[$y]->getBiomeArray());
}
}
/**
* @phpstan-param-out int $x
* @phpstan-param-out int $y
* @phpstan-param-out int $z
*/
protected static function deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z) : void{
if($chunkVersion >= ChunkVersion::v1_0_0){
$x = ($key >> 12) & 0xf;
$z = ($key >> 8) & 0xf;
$y = $key & 0xff;
}else{ //pre-1.0, 7 bits were used because the build height limit was lower
$x = ($key >> 11) & 0xf;
$z = ($key >> 7) & 0xf;
$y = $key & 0x7f;
}
}
/**
* @return PalettedBlockArray[]
*/
protected function deserializeLegacyExtraData(string $index, int $chunkVersion, \Logger $logger) : array{
if(($extraRawData = $this->db->get($index . ChunkDataKey::LEGACY_BLOCK_EXTRA_DATA)) === false || $extraRawData === ""){
return [];
}
/** @var PalettedBlockArray[] $extraDataLayers */
$extraDataLayers = [];
$binaryStream = new BinaryStream($extraRawData);
$count = $binaryStream->getLInt();
for($i = 0; $i < $count; ++$i){
$key = $binaryStream->getLInt();
$value = $binaryStream->getLShort();
self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
$ySub = ($fullY >> SubChunk::COORD_BIT_SIZE);
$y = $key & SubChunk::COORD_MASK;
$blockId = $value & 0xff;
$blockData = ($value >> 8) & 0xf;
try{
$blockStateData = $this->blockDataUpgrader->upgradeIntIdMeta($blockId, $blockData);
}catch(BlockStateDeserializeException $e){
//TODO: we could preserve this in case it's supported in the future, but this was historically only
//used for grass anyway, so we probably don't need to care
$logger->error("Failed to upgrade legacy extra block: " . $e->getMessage() . " ($blockId:$blockData)");
continue;
}
//assume this won't throw
$blockStateId = $this->blockStateDeserializer->deserialize($blockStateData);
if(!isset($extraDataLayers[$ySub])){
$extraDataLayers[$ySub] = new PalettedBlockArray(Block::EMPTY_STATE_ID);
}
$extraDataLayers[$ySub]->set($x, $y, $z, $blockStateId);
}
return $extraDataLayers;
}
private function readVersion(int $chunkX, int $chunkZ) : ?int{
$index = self::chunkIndex($chunkX, $chunkZ);
$chunkVersionRaw = $this->db->get($index . ChunkDataKey::NEW_VERSION);
if($chunkVersionRaw === false){
$chunkVersionRaw = $this->db->get($index . ChunkDataKey::OLD_VERSION);
if($chunkVersionRaw === false){
return null;
}
}
return ord($chunkVersionRaw);
}
/**
* Deserializes terrain data stored in the 0.9 full-chunk format into subchunks.
*
* @return SubChunk[]
* @phpstan-return array<int, SubChunk>
* @throws CorruptedWorldException
*/
private function deserializeLegacyTerrainData(string $index, int $chunkVersion, \Logger $logger) : array{
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
$legacyTerrain = $this->db->get($index . ChunkDataKey::LEGACY_TERRAIN);
if($legacyTerrain === false){
throw new CorruptedChunkException("Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
}
$binaryStream = new BinaryStream($legacyTerrain);
try{
$fullIds = $binaryStream->get(32768);
$fullData = $binaryStream->get(16384);
$binaryStream->get(32768); //legacy light info, discard it
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
try{
$binaryStream->get(256); //heightmap, discard it
/** @var int[] $unpackedBiomeArray */
$unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
$biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data in legacy terrain data");
}
$subChunks = [];
for($yy = 0; $yy < 8; ++$yy){
$storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"))];
if(isset($convertedLegacyExtraData[$yy])){
$storages[] = $convertedLegacyExtraData[$yy];
}
$subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
}
//make sure extrapolated biomes get filled in correctly
for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){
if(!isset($subChunks[$yy])){
$subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d);
}
}
return $subChunks;
}
/**
* Deserializes a subchunk stored in the legacy non-paletted format used from 1.0 until 1.2.13.
*/
private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream, int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
try{
$blocks = $binaryStream->get(4096);
$blockData = $binaryStream->get(2048);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if($chunkVersion < ChunkVersion::v1_1_0){
try{
$binaryStream->get(4096); //legacy light info, discard it
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data in legacy subchunk data");
}
}catch(BinaryDataException $e){
$logger->error("Failed to read legacy subchunk light info: " . $e->getMessage());
}
}
$storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)];
if($convertedLegacyExtraData !== null){
$storages[] = $convertedLegacyExtraData;
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
}
/**
* Deserializes subchunk data stored under a subchunk LevelDB key.
*
* @see ChunkDataKey::SUBCHUNK
* @throws CorruptedChunkException
*/
private function deserializeSubChunkData(BinaryStream $binaryStream, int $chunkVersion, int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
switch($subChunkVersion){
case SubChunkVersion::CLASSIC:
case SubChunkVersion::CLASSIC_BUG_2: //these are all identical to version 0, but vanilla respects these so we should also
case SubChunkVersion::CLASSIC_BUG_3:
case SubChunkVersion::CLASSIC_BUG_4:
case SubChunkVersion::CLASSIC_BUG_5:
case SubChunkVersion::CLASSIC_BUG_6:
case SubChunkVersion::CLASSIC_BUG_7:
return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger);
case SubChunkVersion::PALETTED_SINGLE:
$storages = [$this->deserializeBlockPalette($binaryStream, $logger)];
if($convertedLegacyExtraData !== null){
$storages[] = $convertedLegacyExtraData;
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
$storageCount = $binaryStream->getByte();
if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
//height ignored; this seems pointless since this is already in the key anyway
$binaryStream->getByte();
}
$storages = [];
for($k = 0; $k < $storageCount; ++$k){
$storages[] = $this->deserializeBlockPalette($binaryStream, $logger);
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
default:
//this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption
throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion");
}
}
private static function hasOffsetCavesAndCliffsSubChunks(int $chunkVersion) : bool{
return $chunkVersion >= ChunkVersion::v1_16_220_50_unused && $chunkVersion <= ChunkVersion::v1_16_230_50_unused;
}
/**
* Deserializes any subchunks stored under subchunk LevelDB keys, upgrading them to the current format if necessary.
*
* @param PalettedBlockArray[] $convertedLegacyExtraData
* @param PalettedBlockArray[] $biomeArrays
*
* @phpstan-param array<int, PalettedBlockArray> $convertedLegacyExtraData
* @phpstan-param array<int, PalettedBlockArray> $biomeArrays
* @phpstan-param-out bool $hasBeenUpgraded
*
* @return SubChunk[]
* @phpstan-return array<int, SubChunk>
*/
private function deserializeAllSubChunkData(string $index, int $chunkVersion, bool &$hasBeenUpgraded, array $convertedLegacyExtraData, array $biomeArrays, \Logger $logger) : array{
$subChunks = [];
$subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){
$subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]);
continue;
}
$binaryStream = new BinaryStream($data);
if($binaryStream->feof()){
throw new CorruptedChunkException("Unexpected empty data for subchunk $y");
}
$subChunkVersion = $binaryStream->getByte();
if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
$hasBeenUpgraded = true;
}
$subChunks[$y] = $this->deserializeSubChunkData(
$binaryStream,
$chunkVersion,
$subChunkVersion,
$convertedLegacyExtraData[$y] ?? null,
$biomeArrays[$y],
new \PrefixedLogger($logger, "Subchunk y=$y v$subChunkVersion")
);
}
return $subChunks;
}
/**
* Deserializes any available biome data into an array of paletted biomes. Old 2D biomes are extrapolated to 3D.
*
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private function deserializeBiomeData(string $index, int $chunkVersion, \Logger $logger) : array{
$biomeArrays = [];
if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps2d);
try{
$binaryStream->get(512); //heightmap, discard it
$biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); //never throws
if(!$binaryStream->feof()){
$logger->error("Unexpected trailing data after 2D biome data");
}
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = clone $biomes3d;
}
}elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps3d);
try{
$binaryStream->get(512);
$biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}else{
$logger->error("Missing biome data, using default ocean biome");
for($i = Chunk::MIN_SUBCHUNK_INDEX; $i <= Chunk::MAX_SUBCHUNK_INDEX; ++$i){
$biomeArrays[$i] = new PalettedBlockArray(BiomeIds::OCEAN); //polyfill
}
}
return $biomeArrays;
}
/**
* @throws CorruptedChunkException
*/
public function loadChunk(int $chunkX, int $chunkZ) : ?LoadedChunkData{
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
$chunkVersion = $this->readVersion($chunkX, $chunkZ);
if($chunkVersion === null){
//TODO: this might be a slightly-corrupted chunk with a missing version field
return null;
}
//TODO: read PM_DATA_VERSION - we'll need it to fix up old chunks
$logger = new \PrefixedLogger($this->logger, "Loading chunk x=$chunkX z=$chunkZ v$chunkVersion");
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
switch($chunkVersion){
case ChunkVersion::v1_21_40:
//TODO: BiomeStates became shorts instead of bytes
case ChunkVersion::v1_18_30:
case ChunkVersion::v1_18_0_25_beta:
case ChunkVersion::v1_18_0_24_unused:
case ChunkVersion::v1_18_0_24_beta:
case ChunkVersion::v1_18_0_22_unused:
case ChunkVersion::v1_18_0_22_beta:
case ChunkVersion::v1_18_0_20_unused:
case ChunkVersion::v1_18_0_20_beta:
case ChunkVersion::v1_17_40_unused:
case ChunkVersion::v1_17_40_20_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_25_unused:
case ChunkVersion::v1_17_30_25_beta_experimental_caves_cliffs:
case ChunkVersion::v1_17_30_23_unused:
case ChunkVersion::v1_17_30_23_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_230_50_unused:
case ChunkVersion::v1_16_230_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_220_50_unused:
case ChunkVersion::v1_16_220_50_beta_experimental_caves_cliffs:
case ChunkVersion::v1_16_210:
case ChunkVersion::v1_16_100_57_beta:
case ChunkVersion::v1_16_100_52_beta:
case ChunkVersion::v1_16_0:
case ChunkVersion::v1_16_0_51_beta:
//TODO: check walls
case ChunkVersion::v1_12_0_unused2:
case ChunkVersion::v1_12_0_unused1:
case ChunkVersion::v1_12_0_4_beta:
case ChunkVersion::v1_11_1:
case ChunkVersion::v1_11_0_4_beta:
case ChunkVersion::v1_11_0_3_beta:
case ChunkVersion::v1_11_0_1_beta:
case ChunkVersion::v1_9_0:
case ChunkVersion::v1_8_0:
case ChunkVersion::v1_2_13:
case ChunkVersion::v1_2_0:
case ChunkVersion::v1_2_0_2_beta:
case ChunkVersion::v1_1_0_converted_from_console:
case ChunkVersion::v1_1_0:
//TODO: check beds
case ChunkVersion::v1_0_0:
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion, $logger);
$biomeArrays = $this->deserializeBiomeData($index, $chunkVersion, $logger);
$subChunks = $this->deserializeAllSubChunkData($index, $chunkVersion, $hasBeenUpgraded, $convertedLegacyExtraData, $biomeArrays, $logger);
break;
case ChunkVersion::v0_9_5:
case ChunkVersion::v0_9_2:
case ChunkVersion::v0_9_0:
$subChunks = $this->deserializeLegacyTerrainData($index, $chunkVersion, $logger);
break;
default:
throw new CorruptedChunkException("don't know how to decode chunk format version $chunkVersion");
}
$nbt = new LittleEndianNbtSerializer();
/** @var CompoundTag[] $entities */
$entities = [];
if(($entityData = $this->db->get($index . ChunkDataKey::ENTITIES)) !== false && $entityData !== ""){
try{
$entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
}catch(NbtDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}
/** @var CompoundTag[] $tiles */
$tiles = [];
if(($tileData = $this->db->get($index . ChunkDataKey::BLOCK_ENTITIES)) !== false && $tileData !== ""){
try{
$tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
}catch(NbtDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
}
$finalisationChr = $this->db->get($index . ChunkDataKey::FINALIZATION);
if($finalisationChr !== false){
$finalisation = ord($finalisationChr);
$terrainPopulated = $finalisation === self::FINALISATION_DONE;
}else{ //older versions didn't have this tag
$terrainPopulated = true;
}
//TODO: tile ticks, biome states (?)
return new LoadedChunkData(
data: new ChunkData($subChunks, $terrainPopulated, $entities, $tiles),
upgraded: $hasBeenUpgraded,
fixerFlags: LoadedChunkData::FIXER_FLAG_ALL //TODO: fill this by version rather than just setting all flags
);
return $this->loadChunkFromDB($this->db, $chunkX, $chunkZ);
}
public function saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags) : void{
$index = LevelDB::chunkIndex($chunkX, $chunkZ);
$write = new \LevelDBWriteBatch();
$write->put($index . ChunkDataKey::NEW_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
$write->put($index . ChunkDataKey::PM_DATA_VERSION, Binary::writeLLong(VersionInfo::WORLD_DATA_VERSION));
$subChunks = $chunkData->getSubChunks();
if(($dirtyFlags & Chunk::DIRTY_FLAG_BLOCKS) !== 0){
foreach($subChunks as $y => $subChunk){
$key = $index . ChunkDataKey::SUBCHUNK . chr($y);
if($subChunk->isEmptyAuthoritative()){
$write->delete($key);
}else{
$subStream = new BinaryStream();
$subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
$layers = $subChunk->getBlockLayers();
$subStream->putByte(count($layers));
foreach($layers as $blocks){
$this->serializeBlockPalette($subStream, $blocks);
}
$write->put($key, $subStream->getBuffer());
}
}
}
if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
$stream = new BinaryStream();
$stream->put(str_repeat("\x00", 512)); //fake heightmap
self::serialize3dBiomes($stream, $subChunks);
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
}
//TODO: use this properly
$write->put($index . ChunkDataKey::FINALIZATION, chr($chunkData->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION));
$this->writeTags($chunkData->getTileNBT(), $index . ChunkDataKey::BLOCK_ENTITIES, $write);
$this->writeTags($chunkData->getEntityNBT(), $index . ChunkDataKey::ENTITIES, $write);
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOME_COLORS);
$write->delete($index . ChunkDataKey::LEGACY_TERRAIN);
$this->db->write($write);
}
/**
* @param CompoundTag[] $targets
*/
private function writeTags(array $targets, string $index, \LevelDBWriteBatch $write) : void{
if(count($targets) > 0){
$nbt = new LittleEndianNbtSerializer();
$write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) => new TreeRoot($tag), $targets)));
}else{
$write->delete($index);
}
$this->saveChunkToDB($this->db, $chunkX, $chunkZ, $chunkData, $dirtyFlags);
}
public function getDatabase() : \LevelDB{
return $this->db;
}
/**
* @deprecated
*/
public static function chunkIndex(int $chunkX, int $chunkZ) : string{
return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
}
protected function coordsToChunkIndex(int $chunkX, int $chunkZ) : string{
return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ);
}
/**
* @return int[]
* @phpstan-return array{int, int}
*/
protected function coordsFromChunkIndex(string $chunkIndex) : array{
return [Binary::readLInt(substr($chunkIndex, 0, 4)), Binary::readLInt(substr($chunkIndex, 4, 4))];
}
public function doGarbageCollection() : void{
}
@ -828,33 +118,10 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
foreach($this->db->getIterator() as $key => $_){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
$chunkX = Binary::readLInt(substr($key, 0, 4));
$chunkZ = Binary::readLInt(substr($key, 4, 4));
try{
if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
yield [$chunkX, $chunkZ] => $chunk;
}
}catch(CorruptedChunkException $e){
if(!$skipCorrupted){
throw $e;
}
if($logger !== null){
$logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
}
}
}
}
yield from $this->getAllChunksFromDB($this->db, $skipCorrupted, $logger);
}
public function calculateChunkCount() : int{
$count = 0;
foreach($this->db->getIterator() as $key => $_){
if(strlen($key) === 9 && ($key[8] === ChunkDataKey::NEW_VERSION || $key[8] === ChunkDataKey::OLD_VERSION)){
$count++;
}
}
return $count;
return $this->calculateChunkCountInDB($this->db);
}
}

View File

@ -0,0 +1,245 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\format\io\leveldb;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\world\format\io\ChunkData;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use pocketmine\world\format\io\LoadedChunkData;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function array_key_exists;
use function file_exists;
use function floor;
use function is_dir;
use function mkdir;
use function morton2d_decode;
use function morton2d_encode;
use function sprintf;
use function str_contains;
use function time;
use function touch;
use const LEVELDB_ZLIB_RAW_COMPRESSION;
final class RegionizedLevelDB extends BaseLevelDB{
/**
* Any DBs that haven't been accessed for the last 5 minutes will be removed from memory
*/
private const MAX_DB_CACHE_AGE = 5 * 60;
private const DB_DEFAULT_OPTIONS = [
"compression" => LEVELDB_ZLIB_RAW_COMPRESSION,
"block_size" => 64 * 1024 //64KB, big enough for most chunks
];
private static function dbRegionPath(string $base, int $regionLength) : string{
return Path::join($base, "leveldb-regions-$regionLength");
}
public static function isValid(string $path, int $regionLength) : bool{
return file_exists(Path::join($path, "level.dat")) && is_dir(self::dbRegionPath($path, $regionLength));
}
public static function generate(string $path, string $name, WorldCreationOptions $options, int $regionLength) : void{
self::baseGenerate($path, $name, $options);
touch(Path::join($path, 'NOT_BEDROCK_COMPATIBLE.txt'));
@mkdir(self::dbRegionPath($path, $regionLength), 0777, true);
}
/**
* @var \LevelDB[]|null[]
* @phpstan-var array<int, \LevelDB|null>
*/
private array $databases = [];
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $databasesLastUsed = [];
public function __construct(
string $path,
\Logger $logger,
private readonly int $regionLength
){
parent::__construct($path, $logger);
}
protected function coordsFromChunkIndex(string $chunkIndex) : array{
//TODO: these indexes don't need to use long in separated DBs, we could make them smaller and save space
/**
* @var int[] $decoded
* @phpstan-var array{int, int} $decoded
*/
$decoded = morton2d_decode(Binary::readLong($chunkIndex));
return $decoded;
}
protected function coordsToChunkIndex(int $chunkX, int $chunkZ) : string{
return Binary::writeLong(morton2d_encode($chunkX, $chunkZ));
}
protected function getDBPathForRegionCoords(int $regionX, int $regionZ) : string{
return Path::join(self::dbRegionPath($this->path, $this->regionLength), sprintf(
"db.%d.%d",
$regionX,
$regionZ
));
}
/**
* @throws CorruptedChunkException
*/
protected function fetchDBForRegionCoords(int $regionX, int $regionZ, bool $createIfMissing) : ?\LevelDB{
$index = morton2d_encode($regionX, $regionZ);
$db = $this->databases[$index] ?? null;
if(
!array_key_exists($index, $this->databases) || //we haven't tried to fetch this DB yet
($db === null && $createIfMissing) //or we know it doesn't exist and want to create it (for writing)
){
$options = self::DB_DEFAULT_OPTIONS;
$options["create_if_missing"] = $createIfMissing;
$dbPath = $this->getDBPathForRegionCoords($regionX, $regionZ);
try{
$this->databases[$index] = new \LevelDB($dbPath, $options);
}catch(\LevelDBException $e){
//no other way to detect error type :(
if(!str_contains($e->getMessage(), "(create_if_missing is false)")){
throw new CorruptedChunkException("Couldn't open LevelDB region $dbPath: " . $e->getMessage(), 0, $e);
}
//remember that this DB doesn't exist, so we don't have to hit the disk hundreds of times looking for it
$this->databases[$index] = null;
}
}
$this->databasesLastUsed[$index] = time();
return $this->databases[$index];
}
protected function fetchDBForChunkCoords(int $chunkX, int $chunkZ, bool $createIfMissing) : ?\LevelDB{
return $this->fetchDBForRegionCoords(
(int) floor($chunkX / $this->regionLength),
(int) floor($chunkZ / $this->regionLength),
$createIfMissing
);
}
public function loadChunk(int $chunkX, int $chunkZ) : ?LoadedChunkData{
$db = $this->fetchDBForChunkCoords($chunkX, $chunkZ, createIfMissing: false);
return $db !== null ? $this->loadChunkFromDB($db, $chunkX, $chunkZ) : null;
}
public function saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags) : void{
$db = $this->fetchDBForChunkCoords($chunkX, $chunkZ, createIfMissing: true) ??
throw new AssumptionFailedError("We asked fetch to create a DB, it shouldn't return null");
$this->saveChunkToDB($db, $chunkX, $chunkZ, $chunkData, $dirtyFlags);
}
public function doGarbageCollection() : void{
$minLastUsed = time() - self::MAX_DB_CACHE_AGE;
foreach($this->databasesLastUsed as $index => $time){
if($time < $minLastUsed){
//unset will close the DB
unset(
$this->databases[$index],
$this->databasesLastUsed[$index]
);
}
}
}
public function close() : void{
//no explicit actions needed to close DBs
$this->databases = [];
$this->databasesLastUsed = [];
}
private function createRegionIterator() : \RegexIterator{
return new \RegexIterator(
new \FilesystemIterator(
self::dbRegionPath($this->path, $this->regionLength),
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
),
'/\/db\.(-?\d+)\.(-?\d+)$/',
\RegexIterator::GET_MATCH
);
}
public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
$iterator = $this->createRegionIterator();
/** @var string[] $region */
foreach($iterator as $region){
try{
$regionX = (int) $region[1];
$regionZ = (int) $region[2];
$db = $this->fetchDBForRegionCoords($regionX, $regionZ, createIfMissing: false);
if($db === null){
throw new AssumptionFailedError("DB at $region[0] should definitely exist");
}
//TODO: we don't need the DB name coords for now, but we might in the future if the key format is
//changed to be relative
yield from $this->getAllChunksFromDB($db, $skipCorrupted, $logger);
}catch(CorruptedChunkException $e){
//TODO: detect permission errors - although I'm not sure what we could do differently
if(!$skipCorrupted){
throw $e;
}
if($logger !== null){
$logger->error($e->getMessage());
}
}
}
}
public function calculateChunkCount() : int{
$iterator = $this->createRegionIterator();
$total = 0;
/** @var string[] $region */
foreach($iterator as $region){
//TODO: calculateChunkCount has no accounting for corruption errors
$regionX = (int) $region[1];
$regionZ = (int) $region[2];
$db = $this->fetchDBForRegionCoords($regionX, $regionZ, createIfMissing: false);
if($db === null){
throw new AssumptionFailedError("DB at $region[0] should definitely exist");
}
//TODO: we'd need a specialized calculate impl if we change the key length
$total += $this->calculateChunkCountInDB($db);
}
return $total;
}
}

View File

@ -1,10 +1,5 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
count: 2
path: ../../../build/make-release.php
-
message: "#^Parameter \\#1 \\$strings of function pocketmine\\\\build\\\\server_phar\\\\preg_quote_array expects array\\<string\\>, array\\<int, string\\|false\\> given\\.$#"
count: 1
@ -460,26 +455,6 @@ parameters:
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\\.$#"
count: 1

View File

@ -60,6 +60,11 @@ parameters:
count: 1
path: ../../../src/block/VanillaBlocks.php
-
message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:PALE_OAK_SIGN\\(\\)\\.$#"
count: 1
path: ../../../src/block/VanillaBlocks.php
-
message: "#^Creating callable from a non\\-native static method pocketmine\\\\item\\\\VanillaItems\\:\\:SPRUCE_SIGN\\(\\)\\.$#"
count: 1

View File

@ -27,6 +27,7 @@ use PHPUnit\Framework\TestCase;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use function get_debug_type;
use function implode;
use function is_array;
use function is_int;
@ -95,11 +96,12 @@ class BlockTest extends TestCase{
}
/**
* @return int[]
* @phpstan-return array<string, int>
* @return int[][]|string[][]
* @phpstan-return array{array<string, int>, array<string, string>}
*/
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 +120,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<string, int> $actual
* @phpstan-param array<string, int> $actualStateCounts
* @phpstan-param array<string, string> $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<string, int>");
throw new AssumptionFailedError("Old table should be array{stateCounts: array<string, int>, tiles: array<string, string>}");
}
$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<string, int>");
}
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<string, string>");
}
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 +191,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));
}

File diff suppressed because it is too large Load Diff

View File

@ -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));

View File

@ -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);