mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-08 02:42:58 +00:00
Compare commits
36 Commits
block-posi
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
fdae689f87 | |||
8027c271fa | |||
6abc40bb83 | |||
07cd2e5542 | |||
5b435e783c | |||
1224055f66 | |||
306623e890 | |||
ada3acdba4 | |||
6a1d80e021 | |||
1ff0988e75 | |||
506cfe0362 | |||
fea17fa4a9 | |||
aee358d329 | |||
80899ea72c | |||
42f90e94ff | |||
8f536e6f21 | |||
45482e868d | |||
742aa46b88 | |||
cf1b360a62 | |||
0aa6cde259 | |||
8f8fe948c1 | |||
b10caf7437 | |||
de66d84d29 | |||
636f562bcf | |||
42094e6768 | |||
b341078765 | |||
f7687af337 | |||
ba93665fe7 | |||
6817215683 | |||
67b9d6222d | |||
6f197bc1bb | |||
bba525da02 | |||
ad6d34f1a6 | |||
a8eaa43bc8 | |||
fe7c282052 | |||
45917d495c |
65
.github/workflows/draft-release-from-pr.yml
vendored
65
.github/workflows/draft-release-from-pr.yml
vendored
@ -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 }}"}'
|
13
.github/workflows/draft-release-from-tag.yml
vendored
13
.github/workflows/draft-release-from-tag.yml
vendored
@ -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
|
118
.github/workflows/draft-release.yml
vendored
118
.github/workflows/draft-release.yml
vendored
@ -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."
|
||||
|
@ -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();
|
Submodule build/php updated: 5016e0a3d5...b1eaaa48ec
@ -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';
|
||||
|
@ -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
12
composer.lock
generated
@ -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",
|
||||
|
@ -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.
|
||||
|
103
src/GarbageCollectorManager.php
Normal file
103
src/GarbageCollectorManager.php
Normal 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
305
src/MemoryDump.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
54
src/block/ResinClump.php
Normal 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 [];
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
||||
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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()],
|
||||
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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]));
|
||||
|
@ -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);
|
||||
|
@ -93,6 +93,7 @@ abstract class AsyncTask extends Runnable{
|
||||
|
||||
$this->finished = true;
|
||||
AsyncWorker::getNotifier()->wakeupSleeper();
|
||||
AsyncWorker::maybeCollectCycles();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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{
|
||||
|
@ -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,
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
824
src/world/format/io/leveldb/BaseLevelDB.php
Normal file
824
src/world/format/io/leveldb/BaseLevelDB.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
245
src/world/format/io/leveldb/RegionizedLevelDB.php
Normal file
245
src/world/format/io/leveldb/RegionizedLevelDB.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user