Compare commits

...

36 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
45 changed files with 2121 additions and 1530 deletions

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

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

View File

@ -114,3 +114,16 @@ Released 5th December 2024.
## 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.

12
composer.lock generated
View File

@ -471,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",
@ -488,9 +488,9 @@
"description": "Language resources used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/Language/issues",
"source": "https://github.com/pmmp/Language/tree/2.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",

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.23.2";
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

@ -191,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()
@ -590,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()
@ -673,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()
@ -858,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));
@ -921,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])));
@ -977,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);
@ -988,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));
@ -1123,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));
@ -1179,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",
@ -1312,6 +1333,7 @@ final class VanillaBlocks{
self::registerBlocksR17();
self::registerBlocksR18();
self::registerMudBlocks();
self::registerResinBlocks();
self::registerTuffBlocks();
self::registerCraftingTables();
@ -1359,6 +1381,7 @@ 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), TileSign::class);
self::register($idName("wall_sign"), fn(BID $id) => new WallSign($id, $name . " Wall Sign", $signBreakInfo, $woodType, $signAsItem), TileSign::class);
@ -1583,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));
@ -1690,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));
@ -1728,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

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

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

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

@ -93,6 +93,7 @@ abstract class AsyncTask extends Runnable{
$this->finished = true;
AsyncWorker::getNotifier()->wakeupSleeper();
AsyncWorker::maybeCollectCycles();
}
/**

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

@ -112,6 +112,7 @@
"CHISELED_POLISHED_BLACKSTONE": 1,
"CHISELED_QUARTZ": 3,
"CHISELED_RED_SANDSTONE": 1,
"CHISELED_RESIN_BRICKS": 1,
"CHISELED_SANDSTONE": 1,
"CHISELED_STONE_BRICKS": 1,
"CHISELED_TUFF": 1,
@ -511,6 +512,20 @@
"OXEYE_DAISY": 1,
"PACKED_ICE": 1,
"PACKED_MUD": 1,
"PALE_OAK_BUTTON": 12,
"PALE_OAK_DOOR": 32,
"PALE_OAK_FENCE": 1,
"PALE_OAK_FENCE_GATE": 16,
"PALE_OAK_LEAVES": 4,
"PALE_OAK_LOG": 6,
"PALE_OAK_PLANKS": 1,
"PALE_OAK_PRESSURE_PLATE": 2,
"PALE_OAK_SIGN": 16,
"PALE_OAK_SLAB": 3,
"PALE_OAK_STAIRS": 8,
"PALE_OAK_TRAPDOOR": 16,
"PALE_OAK_WALL_SIGN": 4,
"PALE_OAK_WOOD": 6,
"PEONY": 2,
"PINK_PETALS": 16,
"PINK_TULIP": 1,
@ -594,6 +609,12 @@
"RED_TULIP": 1,
"REINFORCED_DEEPSLATE": 1,
"RESERVED6": 1,
"RESIN": 1,
"RESIN_BRICKS": 1,
"RESIN_BRICK_SLAB": 3,
"RESIN_BRICK_STAIRS": 8,
"RESIN_BRICK_WALL": 162,
"RESIN_CLUMP": 64,
"ROSE_BUSH": 2,
"SAND": 1,
"SANDSTONE": 1,
@ -754,6 +775,8 @@
"NOTE_BLOCK": "pocketmine\\block\\tile\\Note",
"OAK_SIGN": "pocketmine\\block\\tile\\Sign",
"OAK_WALL_SIGN": "pocketmine\\block\\tile\\Sign",
"PALE_OAK_SIGN": "pocketmine\\block\\tile\\Sign",
"PALE_OAK_WALL_SIGN": "pocketmine\\block\\tile\\Sign",
"POTION_CAULDRON": "pocketmine\\block\\tile\\Cauldron",
"REDSTONE_COMPARATOR": "pocketmine\\block\\tile\\Comparator",
"SHULKER_BOX": "pocketmine\\block\\tile\\ShulkerBox",

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