diff --git a/.gitattributes b/.gitattributes
index bec13fa0a..20ef6a046 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,6 +4,7 @@
*.sh text eol=lf
*.txt text eol=lf
*.properties text eol=lf
+*.neon text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml
index 6c3d25996..71363897c 100644
--- a/.github/workflows/draft-release.yml
+++ b/.github/workflows/draft-release.yml
@@ -35,17 +35,18 @@ jobs:
- name: Install Composer dependencies
run: composer install --no-dev --prefer-dist --no-interaction --ignore-platform-reqs
- - name: Patch VersionInfo
+ - name: Calculate build number
+ id: build-number
run: |
- BUILD_NUMBER=2000+$GITHUB_RUN_NUMBER #to stay above jenkins
+ BUILD_NUMBER=$((2000+$GITHUB_RUN_NUMBER)) #to stay above jenkins
echo "Build number: $BUILD_NUMBER"
- sed -i "s/const BUILD_NUMBER = 0/const BUILD_NUMBER = ${BUILD_NUMBER}/" src/VersionInfo.php
+ echo ::set-output name=BUILD_NUMBER::$BUILD_NUMBER
- name: Minify BedrockData JSON files
- run: php resources/vanilla/.minify_json.php
+ run: php vendor/pocketmine/bedrock-data/.minify_json.php
- name: Build PocketMine-MP.phar
- run: php -dphar.readonly=0 build/server-phar.php --git ${{ github.sha }}
+ run: php -dphar.readonly=0 build/server-phar.php --git ${{ github.sha }} --build ${{ steps.build-number.outputs.BUILD_NUMBER }}
- name: Get PocketMine-MP release version
id: get-pm-version
@@ -56,7 +57,7 @@ jobs:
echo ::set-output name=PM_VERSION_MD::$(php -r 'require "vendor/autoload.php"; echo str_replace(".", "", \pocketmine\VersionInfo::BASE_VERSION);')
- name: Generate build info
- run: php build/generate-build-info-json.php ${{ github.sha }} ${{ steps.get-pm-version.outputs.PM_VERSION }} ${{ github.repository }} > build_info.json
+ run: php build/generate-build-info-json.php ${{ github.sha }} ${{ steps.get-pm-version.outputs.PM_VERSION }} ${{ github.repository }} ${{ steps.build-number.outputs.BUILD_NUMBER }} > build_info.json
- name: Upload release artifacts
uses: actions/upload-artifact@v2
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index db51b5070..274fdcdcf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -13,20 +13,14 @@ jobs:
strategy:
matrix:
image: [ubuntu-20.04]
- php: [8.0.9]
+ php: [8.0.14]
steps:
- - uses: actions/checkout@v2 #needed for build.sh
- - name: Check for PHP build cache
- id: php-build-cache
- uses: actions/cache@v2
+ - name: Build and prepare PHP cache
+ uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636
with:
- path: "./bin"
- key: "php-build-generic-${{ matrix.php }}-${{ matrix.image }}-${{ hashFiles('./tests/gh-actions/build.sh') }}"
-
- - name: Compile PHP
- if: steps.php-build-cache.outputs.cache-hit != 'true'
- run: ./tests/gh-actions/build.sh "${{ matrix.php }}"
+ php-version: ${{ matrix.php }}
+ install-path: "./bin"
phpstan:
name: PHPStan analysis
@@ -37,28 +31,16 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu-20.04]
- php: [8.0.9]
+ php: [8.0.14]
steps:
- uses: actions/checkout@v2
- - name: Restore PHP build cache
- id: php-build-cache
- uses: actions/cache@v2
+ - name: Setup PHP
+ uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636
with:
- path: "./bin"
- key: "php-build-generic-${{ matrix.php }}-${{ matrix.image }}-${{ hashFiles('./tests/gh-actions/build.sh') }}"
-
- - name: Kill build on PHP build cache miss (should never happen)
- if: steps.php-build-cache.outputs.cache-hit != 'true'
- run: exit 1
-
- - name: Install cached PHP's dependencies
- if: steps.php-build-cache.outputs.cache-hit == 'true'
- run: ./tests/gh-actions/install-dependencies.sh
-
- - name: Prefix PHP to PATH
- run: echo "$(pwd)/bin/php7/bin" >> $GITHUB_PATH
+ php-version: ${{ matrix.php }}
+ install-path: "./bin"
- name: Install Composer
run: curl -sS https://getcomposer.org/installer | php
@@ -87,30 +69,16 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu-20.04]
- php: [8.0.9]
+ php: [8.0.14]
steps:
- uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636
with:
- submodules: true
-
- - name: Restore PHP build cache
- id: php-build-cache
- uses: actions/cache@v2
- with:
- path: "./bin"
- key: "php-build-generic-${{ matrix.php }}-${{ matrix.image }}-${{ hashFiles('./tests/gh-actions/build.sh') }}"
-
- - name: Kill build on PHP build cache miss (should never happen)
- if: steps.php-build-cache.outputs.cache-hit != 'true'
- run: exit 1
-
- - name: Install cached PHP's dependencies
- if: steps.php-build-cache.outputs.cache-hit == 'true'
- run: ./tests/gh-actions/install-dependencies.sh
-
- - name: Prefix PHP to PATH
- run: echo "$(pwd)/bin/php7/bin" >> $GITHUB_PATH
+ php-version: ${{ matrix.php }}
+ install-path: "./bin"
- name: Install Composer
run: curl -sS https://getcomposer.org/installer | php
@@ -139,30 +107,18 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu-20.04]
- php: [8.0.9]
+ php: [8.0.14]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- - name: Restore PHP build cache
- id: php-build-cache
- uses: actions/cache@v2
+ - name: Setup PHP
+ uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636
with:
- path: "./bin"
- key: "php-build-generic-${{ matrix.php }}-${{ matrix.image }}-${{ hashFiles('./tests/gh-actions/build.sh') }}"
-
- - name: Kill build on PHP build cache miss (should never happen)
- if: steps.php-build-cache.outputs.cache-hit != 'true'
- run: exit 1
-
- - name: Install cached PHP's dependencies
- if: steps.php-build-cache.outputs.cache-hit == 'true'
- run: ./tests/gh-actions/install-dependencies.sh
-
- - name: Prefix PHP to PATH
- run: echo "$(pwd)/bin/php7/bin" >> $GITHUB_PATH
+ php-version: ${{ matrix.php }}
+ install-path: "./bin"
- name: Install Composer
run: curl -sS https://getcomposer.org/installer | php
@@ -191,30 +147,16 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu-20.04]
- php: [8.0.9]
+ php: [8.0.14]
steps:
- uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636
with:
- submodules: true
-
- - name: Restore PHP build cache
- id: php-build-cache
- uses: actions/cache@v2
- with:
- path: "./bin"
- key: "php-build-generic-${{ matrix.php }}-${{ matrix.image }}-${{ hashFiles('./tests/gh-actions/build.sh') }}"
-
- - name: Kill build on PHP build cache miss (should never happen)
- if: steps.php-build-cache.outputs.cache-hit != 'true'
- run: exit 1
-
- - name: Install cached PHP's dependencies
- if: steps.php-build-cache.outputs.cache-hit == 'true'
- run: ./tests/gh-actions/install-dependencies.sh
-
- - name: Prefix PHP to PATH
- run: echo "$(pwd)/bin/php7/bin" >> $GITHUB_PATH
+ php-version: ${{ matrix.php }}
+ install-path: "./bin"
- name: Install Composer
run: curl -sS https://getcomposer.org/installer | php
@@ -238,11 +180,10 @@ jobs:
- name: Regenerate KnownTranslation APIs
run: php build/generate-known-translation-apis.php
- - name: Run git diff
- run: git diff
-
- - name: Fail job if changes were made
- run: git diff --quiet
+ - name: Verify code is unchanged
+ run: |
+ git diff
+ git diff --quiet
codestyle:
name: Code Style checks
@@ -254,10 +195,10 @@ jobs:
- uses: actions/checkout@v2
- name: Setup PHP and tools
- uses: shivammathur/setup-php@2.9.0
+ uses: shivammathur/setup-php@2.15.0
with:
php-version: 8.0
- tools: php-cs-fixer
+ tools: php-cs-fixer:3.2
- name: Run PHP-CS-Fixer
- run: php-cs-fixer fix --dry-run --diff
+ run: php-cs-fixer fix --dry-run --diff --ansi
diff --git a/.github/workflows/update-php-versions.php b/.github/workflows/update-php-versions.php
index 8b24812c8..0ba7382cf 100644
--- a/.github/workflows/update-php-versions.php
+++ b/.github/workflows/update-php-versions.php
@@ -22,8 +22,6 @@
declare(strict_types=1);
const VERSIONS = [
- "7.3",
- "7.4",
"8.0"
];
diff --git a/.gitmodules b/.gitmodules
index ec6560fd1..0b2349472 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,12 +1,6 @@
-[submodule "resources/locale"]
- path = resources/locale
- url = https://github.com/pmmp/Language.git
[submodule "tests/plugins/DevTools"]
path = tests/plugins/DevTools
url = https://github.com/pmmp/DevTools.git
[submodule "build/php"]
path = build/php
url = https://github.com/pmmp/php-build-scripts.git
-[submodule "resources/vanilla"]
- path = resources/vanilla
- url = https://github.com/pmmp/BedrockData.git
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 659d76748..3fbdf33bb 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -18,6 +18,9 @@ return (new PhpCsFixer\Config)
'array_syntax' => [
'syntax' => 'short'
],
+ 'binary_operator_spaces' => [
+ 'default' => 'single_space'
+ ],
'blank_line_after_namespace' => true,
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => [
@@ -33,12 +36,14 @@ return (new PhpCsFixer\Config)
],
'declare_strict_types' => true,
'elseif' => true,
+ 'fully_qualified_strict_types' => true,
'global_namespace_import' => [
'import_constants' => true,
'import_functions' => true,
'import_classes' => null,
],
'indentation_type' => true,
+ 'logical_operators' => true,
'native_function_invocation' => [
'scope' => 'namespaced',
'include' => ['@all'],
@@ -61,10 +66,20 @@ return (new PhpCsFixer\Config)
],
'sort_algorithm' => 'alpha'
],
+ 'phpdoc_line_span' => [
+ 'property' => 'single',
+ 'method' => null,
+ 'const' => null
+ ],
'phpdoc_trim' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
+ 'return_type_declaration' => [
+ 'space_before' => 'one'
+ ],
+ 'single_blank_line_at_eof' => true,
'single_import_per_statement' => true,
'strict_param' => true,
+ 'unary_operator_spaces' => true,
])
->setFinder($finder)
->setIndent("\t")
diff --git a/BUILDING.md b/BUILDING.md
index 6860967bc..d6e97e05c 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -2,25 +2,24 @@
## Pre-requisites
- A bash shell (git bash is sufficient for Windows)
- [`git`](https://git-scm.com) available in your shell
-- PHP 7.4 or newer available in your shell
+- PHP 8.0 or newer available in your shell
- [`composer`](https://getcomposer.org) available in your shell
## Custom PHP binaries
Because PocketMine-MP requires several non-standard PHP extensions and configuration, PMMP provides scripts to build custom binaries for running PocketMine-MP, as well as prebuilt binaries.
-- [Prebuilt binaries](https://jenkins.pmmp.io/job/PHP-7.4-Aggregate)
+- [Prebuilt binaries](https://jenkins.pmmp.io/job/PHP-8.0-Aggregate)
- [Compile scripts](https://github.com/pmmp/php-build-scripts) are provided as a submodule in the path `build/php`
If you use a custom binary, you'll need to replace `composer` usages in this guide with `path/to/your/php path/to/your/composer.phar`.
## Setting up environment
-1. `git clone --recursive https://github.com/pmmp/PocketMine-MP.git`
+1. `git clone https://github.com/pmmp/PocketMine-MP.git`
2. `composer install`
## Checking out a different branch to build
1. `git checkout `
-2. `git submodule update --init`
-3. Re-run `composer install` to synchronize dependencies.
+2. Re-run `composer install` to synchronize dependencies.
## Optimizing for release builds
1. Add the flags `--no-dev --classmap-authoritative` to your `composer install` command. This will reduce build size and improve autoloading speed.
@@ -34,7 +33,7 @@ There is a bug in PHP that might cause an error which looks like this:
```
Fatal error: Uncaught BadMethodCallException: unable to create temporary file in PocketMine-MP/build/server-phar.php:119
```
-You can work around it by setting `ulimit -n` to some bigger number, e.g. `8192`, or by updating your PHP version to at least 7.4.16 or 8.0.3.
+You can work around it by setting `ulimit -n` to some bigger number, e.g. `8192`, or by updating your PHP version to at least 8.0.3.
## Running PocketMine-MP from source code
Run `src/PocketMine.php` using your preferred PHP binary.
diff --git a/README.md b/README.md
index 3d1787e38..4d376751c 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,13 @@
-
-
+
+
+
+
+
## Getting started
diff --git a/build/generate-build-info-json.php b/build/generate-build-info-json.php
index 6428ce239..dd353fd7c 100644
--- a/build/generate-build-info-json.php
+++ b/build/generate-build-info-json.php
@@ -23,15 +23,15 @@ declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
-if(count($argv) !== 4){
- fwrite(STDERR, "required args: ");
+if(count($argv) !== 5){
+ fwrite(STDERR, "required args: ");
exit(1);
}
echo json_encode([
"php_version" => sprintf("%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION),
"base_version" => \pocketmine\VersionInfo::BASE_VERSION,
- "build" => \pocketmine\VersionInfo::BUILD_NUMBER,
+ "build" => (int) $argv[4],
"is_dev" => \pocketmine\VersionInfo::IS_DEVELOPMENT_BUILD,
"channel" => \pocketmine\VersionInfo::BUILD_CHANNEL,
"git_commit" => $argv[1],
@@ -40,4 +40,4 @@ echo json_encode([
"details_url" => "https://github.com/$argv[3]/releases/tag/$argv[2]",
"download_url" => "https://github.com/$argv[3]/releases/download/$argv[2]/PocketMine-MP.phar",
"source_url" => "https://github.com/$argv[3]/tree/$argv[2]",
-], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
+], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
diff --git a/build/generate-known-translation-apis.php b/build/generate-known-translation-apis.php
index 04dbe37a6..a85cbb1b3 100644
--- a/build/generate-known-translation-apis.php
+++ b/build/generate-known-translation-apis.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\build\generate_known_translation_apis;
use pocketmine\lang\Translatable;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function array_map;
use function count;
@@ -40,6 +41,7 @@ use function preg_match_all;
use function str_replace;
use function strtoupper;
use const INI_SCANNER_RAW;
+use const SORT_NUMERIC;
use const SORT_STRING;
use const STDERR;
@@ -94,13 +96,15 @@ function generate_known_translation_keys(array $languageDefinitions) : void{
/**
* This class contains constants for all the translations known to PocketMine-MP as per the used version of pmmp/Language.
* This class is generated automatically, do NOT modify it by hand.
+ *
+ * @internal
*/
final class KnownTranslationKeys{
HEADER;
ksort($languageDefinitions, SORT_STRING);
- foreach($languageDefinitions as $k => $_){
+ foreach(Utils::stringifyKeys($languageDefinitions) as $k => $_){
echo "\tpublic const ";
echo constantify($k);
echo " = \"" . $k . "\";\n";
@@ -126,6 +130,8 @@ function generate_known_translation_factory(array $languageDefinitions) : void{
* This class contains factory methods for all the translations known to PocketMine-MP as per the used version of
* pmmp/Language.
* This class is generated automatically, do NOT modify it by hand.
+ *
+ * @internal
*/
final class KnownTranslationFactory{
@@ -135,17 +141,22 @@ HEADER;
$parameterRegex = '/{%(.+?)}/';
$translationContainerClass = (new \ReflectionClass(Translatable::class))->getShortName();
- foreach($languageDefinitions as $key => $value){
+ foreach(Utils::stringifyKeys($languageDefinitions) as $key => $value){
$parameters = [];
+ $allParametersPositional = true;
if(preg_match_all($parameterRegex, $value, $matches) > 0){
foreach($matches[1] as $parameterName){
if(is_numeric($parameterName)){
$parameters[$parameterName] = "param$parameterName";
}else{
$parameters[$parameterName] = $parameterName;
+ $allParametersPositional = false;
}
}
}
+ if($allParametersPositional){
+ ksort($parameters, SORT_NUMERIC);
+ }
echo "\tpublic static function " .
functionify($key) .
"(" . implode(", ", array_map(fn(string $paramName) => "$translationContainerClass|string \$$paramName", $parameters)) . ") : $translationContainerClass{\n";
@@ -172,7 +183,7 @@ HEADER;
echo "Done generating KnownTranslationFactory.\n";
}
-$lang = parse_ini_file(Path::join(\pocketmine\RESOURCE_PATH, "locale", "eng.ini"), false, INI_SCANNER_RAW);
+$lang = parse_ini_file(Path::join(\pocketmine\LOCALE_DATA_PATH, "eng.ini"), false, INI_SCANNER_RAW);
if($lang === false){
fwrite(STDERR, "Missing language files!\n");
exit(1);
diff --git a/build/generate-registry-annotations.php b/build/generate-registry-annotations.php
index f7dce3c50..ebc39631b 100644
--- a/build/generate-registry-annotations.php
+++ b/build/generate-registry-annotations.php
@@ -58,7 +58,7 @@ function generateMethodAnnotations(string $namespaceName, array $members) : stri
$memberLines = [];
foreach($members as $name => $member){
$reflect = new \ReflectionClass($member);
- while($reflect !== false and $reflect->isAnonymous()){
+ while($reflect !== false && $reflect->isAnonymous()){
$reflect = $reflect->getParentClass();
}
if($reflect === false){
@@ -91,7 +91,7 @@ foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($argv[1],
throw new \RuntimeException("Failed to get contents of $file");
}
- if(preg_match("/^namespace (.+);$/m", $contents, $matches) !== 1 || preg_match('/^((final|abstract)\s+)?class /m', $contents) !== 1){
+ if(preg_match("/(*ANYCRLF)^namespace (.+);$/m", $contents, $matches) !== 1 || preg_match('/(*ANYCRLF)^((final|abstract)\s+)?class /m', $contents) !== 1){
continue;
}
$shortClassName = basename($file, ".php");
@@ -101,7 +101,7 @@ foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($argv[1],
}
$reflect = new \ReflectionClass($className);
$docComment = $reflect->getDocComment();
- if($docComment === false || preg_match("/^\s*\*\s*@generate-registry-docblock$/m", $docComment) !== 1){
+ if($docComment === false || preg_match("/(*ANYCRLF)^\s*\*\s*@generate-registry-docblock$/m", $docComment) !== 1){
continue;
}
echo "Found registry in $file\n";
@@ -116,4 +116,3 @@ foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($argv[1],
echo "No changes made to file $file\n";
}
}
-
diff --git a/build/make-release.php b/build/make-release.php
index d90623332..d641d44ad 100644
--- a/build/make-release.php
+++ b/build/make-release.php
@@ -23,25 +23,34 @@ declare(strict_types=1);
namespace pocketmine\build\make_release;
+use pocketmine\utils\Utils;
use pocketmine\utils\VersionString;
use pocketmine\VersionInfo;
-use function count;
+use function array_keys;
+use function array_map;
use function dirname;
use function fgets;
use function file_get_contents;
use function file_put_contents;
use function fwrite;
+use function getopt;
+use function is_string;
+use function max;
use function preg_replace;
use function sleep;
use function sprintf;
+use function str_pad;
+use function strlen;
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 = file_get_contents($versionInfoPath);
+ $versionInfo = Utils::assumeNotFalse(file_get_contents($versionInfoPath), $versionInfoPath . " should always exist");
$versionInfo = preg_replace(
$pattern = '/^([\t ]*public )?const BASE_VERSION = "(\d+)\.(\d+)\.(\d+)(?:-(.*))?";$/m',
'$1const BASE_VERSION = "' . $newVersion . '";',
@@ -60,22 +69,46 @@ function replaceVersion(string $versionInfoPath, string $newVersion, bool $isDev
file_put_contents($versionInfoPath, $versionInfo);
}
-/**
- * @param string[] $argv
- * @phpstan-param list $argv
- */
-function main(array $argv) : void{
- if(count($argv) < 2){
- fwrite(STDERR, "Arguments: [release version] [next version]\n");
+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);
}
- if(isset($argv[2])){
- $currentVer = new VersionString($argv[2]);
- }else{
- $currentVer = VersionInfo::VERSION();
+}
+
+function main() : void{
+ $filteredOpts = [];
+ foreach(Utils::stringifyKeys(getopt("", ["current:", "next:", "channel:", "help"])) 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(!is_string($optValue)){
+ fwrite(STDERR, "--$optName expects exactly 1 value\n");
+ exit(1);
+ }
+ $filteredOpts[$optName] = $optValue;
}
- if(isset($argv[3])){
- $nextVer = new VersionString($argv[3]);
+
+ if(isset($filteredOpts["current"])){
+ $currentVer = new VersionString($filteredOpts["current"]);
+ }else{
+ $currentVer = new VersionString(VersionInfo::BASE_VERSION);
+ }
+ if(isset($filteredOpts["next"])){
+ $nextVer = new VersionString($filteredOpts["next"]);
}else{
$nextVer = new VersionString(sprintf(
"%u.%u.%u",
@@ -84,26 +117,29 @@ function main(array $argv) : void{
$currentVer->getPatch() + 1
));
}
+ $channel = $filteredOpts["channel"] ?? VersionInfo::BUILD_CHANNEL;
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);
- system('git add "' . dirname(__DIR__) . '/changelogs"');
+ 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);
}
$versionInfoPath = dirname(__DIR__) . '/src/VersionInfo.php';
- replaceVersion($versionInfoPath, $currentVer->getBaseVersion(), false, $argv[1]);
- system('git commit -m "Release ' . $currentVer->getBaseVersion() . '" --include "' . $versionInfoPath . '"');
- system('git tag ' . $currentVer->getBaseVersion());
- replaceVersion($versionInfoPath, $nextVer->getBaseVersion(), true, "");
- system('git add "' . $versionInfoPath . '"');
- system('git commit -m "' . $nextVer->getBaseVersion() . ' is next" --include "' . $versionInfoPath . '"');
+ 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");
echo "pushing changes in 5 seconds\n";
sleep(5);
- system('git push origin HEAD ' . $currentVer->getBaseVersion());
+ systemWrapper('git push origin HEAD ' . $currentVer->getBaseVersion(), "failed to push changes to remote");
}
-main($argv);
+main();
diff --git a/build/php b/build/php
index ad9cd1fdb..bd329dba0 160000
--- a/build/php
+++ b/build/php
@@ -1 +1 @@
-Subproject commit ad9cd1fdb47742038365ec8ec7e2ed61ba58a068
+Subproject commit bd329dba08242b6ecb99ef7fbf9a0031dcbbb3ff
diff --git a/build/server-phar.php b/build/server-phar.php
index cb3bc0888..2f5aa1050 100644
--- a/build/server-phar.php
+++ b/build/server-phar.php
@@ -134,13 +134,18 @@ function main() : void{
exit(1);
}
- $opts = getopt("", ["out:", "git:"]);
+ $opts = getopt("", ["out:", "git:", "build:"]);
if(isset($opts["git"])){
$gitHash = $opts["git"];
}else{
$gitHash = Git::getRepositoryStatePretty(dirname(__DIR__));
echo "Git hash detected as $gitHash" . PHP_EOL;
}
+ if(isset($opts["build"])){
+ $build = (int) $opts["build"];
+ }else{
+ $build = 0;
+ }
foreach(buildPhar(
$opts["out"] ?? getcwd() . DIRECTORY_SEPARATOR . "PocketMine-MP.phar",
dirname(__DIR__) . DIRECTORY_SEPARATOR,
@@ -150,7 +155,8 @@ function main() : void{
'vendor'
],
[
- 'git' => $gitHash
+ 'git' => $gitHash,
+ 'build' => $build
],
<<<'STUB'
move()` not respecting the given `dx`/`dy`/`dz` and using its own motion instead.
diff --git a/changelogs/3.25.md b/changelogs/3.25.md
new file mode 100644
index 000000000..4c3ab2978
--- /dev/null
+++ b/changelogs/3.25.md
@@ -0,0 +1,38 @@
+**For Minecraft: Bedrock Edition 1.17.40**
+
+### Note about API versions
+Plugins which don't touch the protocol and compatible with any previous 3.x.y version will also run on these releases and do not need API bumps.
+Plugin developers should **only** update their required API to this version if you need the changes in this build.
+
+**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
+
+# 3.25.0
+- Added support for Minecraft: Bedrock Edition 1.17.40.
+- Removed compatibility with earlier versions.
+
+# 3.25.1
+- Fixed autosave bug that caused unmodified chunks to be saved at least once (during the first autosave after they were loaded).
+- `Entity->spawnTo()` now has an additional sanity check for matching worlds (might expose a few new errors in plugins).
+- Fixed a missing field in `CraftRecipeAuto` item stack request type.
+
+# 3.25.2
+- Now analysed using level 9 on PHPStan 1.0.0.
+- `ext-pthreads` v4.0.0 or newer is now required.
+- Fixed crash in `Player->showPlayer()` when the target is not in the same world.
+- `Human->setLifetimeTotalXp()` now limits the maximum value to 2^31.
+- Fixed players, who died in hardcore mode and were unbanned, getting re-banned on next server join.
+
+# 3.25.3
+- Fixed crash when players try to pickup XP while already having max XP.
+- Added a sanity check to `Human->setCurrentTotalXp()` to try and catch an elusive bug that's been appearing in the wild - please get in touch if you know how to reproduce it!
+
+# 3.25.4
+- Fixed a long-standing issue with `Player->removeWindow()` breaking inventory UIs on the client.
+
+# 3.25.5
+- Protocol: Fixed incorrect encoding in `StructureSettings`
+- Fixed reading tags from non-docblock comments in script plugins.
+- Build number is now defined in phar metadata instead of being patched into the source code directly.
+
+# 3.25.6
+- Fixed borked build number in release build of 3.25.5.
diff --git a/changelogs/3.26.md b/changelogs/3.26.md
new file mode 100644
index 000000000..102e68aad
--- /dev/null
+++ b/changelogs/3.26.md
@@ -0,0 +1,32 @@
+**For Minecraft: Bedrock Edition 1.18.0**
+
+### Note about API versions
+Plugins which don't touch the protocol and compatible with any previous 3.x.y version will also run on these releases and do not need API bumps.
+Plugin developers should **only** update their required API to this version if you need the changes in this build.
+
+**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
+
+# 3.26.0
+- Added support for Minecraft: Bedrock Edition 1.18.0.
+- Removed compatibility with earlier versions.
+
+# 3.26.1
+- Fixed a bug in chunk sending that caused double chests to not be paired, signs to be blank, and various other issues.
+
+# 3.26.2
+- Improved error messages shown by `start.cmd`, `start.sh` and `start.ps1` when the PHP binary was not found.
+- The value of PHPRC is now shown when erroring out due to unsatisfied PHP requirements.
+- Removed restriction on the range of valid channels for `auto-updater.channel` in `pocketmine.yml`.
+
+# 3.26.3
+- `PlayerExperienceChangeEvent->setNewProgress()` now performs range checks. This fixes the root of a very old and confusing crash bug which took several years to identify the cause of.
+ - Note that the defective plugin(s) which caused this problem will still cause a server crash, but the plugin responsible will now get blamed correctly.
+
+# 3.26.4
+- Fixed skins appearing black when using RTX resource packs.
+- Fixed chunks containing furnaces in old worlds (pre-2017) being discarded as corrupted.
+ - This was caused by a strict corruption check detecting bad data created by a bug in PocketMine-MP that was fixed in 2017.
+
+# 3.26.5
+- Fixed several denial-of-service attack vectors related to writable book text length and encoding.
+- Fixed several denial-of-service attack vectors related to skin data field lengths.
diff --git a/changelogs/3.27.md b/changelogs/3.27.md
new file mode 100644
index 000000000..58ed8925a
--- /dev/null
+++ b/changelogs/3.27.md
@@ -0,0 +1,15 @@
+**For Minecraft: Bedrock Edition 1.18.0**
+
+### Note about API versions
+Plugins which don't touch the protocol and compatible with any previous 3.x.y version will also run on these releases and do not need API bumps.
+Plugin developers should **only** update their required API to this version if you need the changes in this build.
+
+**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
+
+# 3.27.0
+- Introduced support for protocol encryption.
+ - Encryption is enabled by default.
+ - Fixes login replay attacks.
+ - This may cause some performance degradation.
+ - Encryption can be disabled by setting `network.enable-encryption` to `false` in `pocketmine.yml`. DO NOT do this unless you understand the risks involved.
+- An obsoletion notice has been added to the console during server startup.
\ No newline at end of file
diff --git a/changelogs/4.0-beta.md b/changelogs/4.0-beta.md
new file mode 100644
index 000000000..a08f22428
--- /dev/null
+++ b/changelogs/4.0-beta.md
@@ -0,0 +1,1767 @@
+# 4.0.0-BETA1
+Released 7th September 2021.
+
+This major version features substantial changes throughout the core, including significant API changes, new world format support, performance improvements and a network revamp.
+
+Please note that this is a BETA release and is not finalized. While no significant changes are expected between now and release, the API might still be changed.
+
+Please also note that this changelog is provided on a best-effort basis, and it's possible some changes might not have been mentioned here.
+If you find any omissions, please submit pull requests to add them.
+
+## WARNING
+This is NOT a stable release. PMMP accepts no responsibility or liability for any damages incurred by using this build.
+It should be used for TESTING purposes only.
+
+## Contents
+ * [Core](#core)
+ + [General](#general)
+ + [Configuration](#configuration)
+ + [World handling](#world-handling)
+ - [Interface](#interface)
+ - [Functional](#functional)
+ - [Performance](#performance)
+ + [Logger revamp](#logger-revamp)
+ + [Network](#network)
+ - [Minecraft Bedrock packet encryption](#minecraft-bedrock-packet-encryption)
+ - [Packet receive error handling has been overhauled](#packet-receive-error-handling-has-been-overhauled)
+ - [New packet handler system](#new-packet-handler-system)
+ * [API](#api)
+ + [General](#general-1)
+ + [Changes to `plugin.yml`](#changes-to-pluginyml)
+ - [Permission nesting](#permission-nesting)
+ - [`src-namespace-prefix`](#src-namespace-prefix)
+ + [Block](#block)
+ + [Command](#command)
+ + [Entity](#entity)
+ - [General](#general-2)
+ - [Effect](#effect)
+ - [Removal of runtime entity NBT](#removal-of-runtime-entity-nbt)
+ - [Entity creation](#entity-creation)
+ - [WIP removal of entity network metadata](#wip-removal-of-entity-network-metadata)
+ + [Event](#event)
+ - [Internal event system no longer depends on `Listener`s](#internal-event-system-no-longer-depends-on-listeners)
+ - [Default cancelled handling behaviour has changed](#default-cancelled-handling-behaviour-has-changed)
+ - [`PlayerPreLoginEvent` changes](#playerpreloginevent-changes)
+ - [Other changes](#other-changes)
+ + [Inventory](#inventory)
+ + [Item](#item)
+ - [General](#general-3)
+ - [NBT handling](#nbt-handling)
+ - [Enchantment](#enchantment)
+ + [Lang](#lang)
+ + [Network](#network-1)
+ + [Permission](#permission)
+ + [Player](#player)
+ + [Plugin](#plugin)
+ + [Scheduler](#scheduler)
+ - [Thread-local storage for AsyncTasks](#thread-local-storage-for-asynctasks)
+ - [Other changes](#other-changes-1)
+ + [Server](#server)
+ + [Level / World](#level--world)
+ - [General](#general-4)
+ - [Particles](#particles)
+ - [Sounds](#sounds)
+ + [Utils](#utils)
+ * [Gameplay](#gameplay)
+ + [Blocks](#blocks)
+ + [Items](#items)
+
+Table of contents generated with markdown-toc
+
+## Core
+### General
+- A new "plugin greylist" feature has been introduced, which allows whitelisting or blacklisting plugins from loading.
+- Remote console (RCON) has been removed. The [RconServer](https://github.com/pmmp/RconServer) plugin is provided as a substitute.
+- Spawn protection has been removed. The [BasicSpawnProtection](https://github.com/pmmp/BasicSpawnProtection) plugin is provided as a substitute.
+- CTRL+C signal handling has been removed. The [PcntlSignalHandler](https://github.com/pmmp/PcntlSignalHandler) plugin is provided as a substitute.
+- Player movement anti-cheat has been removed.
+- GS4 Query no longer breaks when disabling RakLib.
+- The `pocketmine_chunkutils` PHP extension has been dropped.
+- New PHP extensions are required by this version:
+ - [chunkutils2](https://github.com/pmmp/ext-chunkutils2)
+ - [morton](https://github.com/pmmp/ext-morton)
+- Many bugs in player respawning have been fixed, including:
+ - Spawning underneath bedrock when spawn position referred to ungenerated terrain
+ - Spawning underneath bedrock on first server join on very slow machines (or when the machine was under very high load)
+ - Spawning inside blocks (or above the ground) when respawning with a custom spawn position set
+ - Player spawn positions sticking to the old location when world spawn position changed - this was because the world spawn at time of player creation was used as the player's custom spawn, so the bug will persist for older player data, but will work as expected for new players.
+- PreProcessor is removed from builds due to high maintenance cost and low benefit. Its usage is now discouraged.
+
+### Commands
+- The `/reload` command has been removed.
+- The `/effect` command no longer supports numeric IDs - it's now required to use names.
+- The `/enchant` command no longer supports numeric IDs - it's now required to use names.
+- Added `/clear` command with functionality equivalent to that of vanilla Minecraft.
+- The following commands' outputs are now localized according to the chosen language settings:
+ - `/gc`
+ - `/status`
+ - `/op`
+ - `/deop`
+
+### Configuration
+- World presets can now be provided as a `preset` key in `pocketmine.yml`, instead of putting them in the `generator` key.
+- The following new options have been added to `pocketmine.yml`:
+ - `chunk-ticking.blocks-per-subchunk-per-tick` (default `3`): Increasing this value will increase the rate at which random block updates happen (e.g. grass growth).
+ - `network.enable-encryption` (default `true`): Controls whether Minecraft network packets are encrypted or not.
+- The following options have been removed from `pocketmine.yml`:
+ - `chunk-ticking.light-updates`: Since lighting is needed for basic vanilla functionality to work, allowing this to be disabled without disabling chunk ticking made no sense. If you don't want light calculation to occur, you can disable chunk ticking altogether.
+ - `player.anti-cheat.allow-movement-cheats`
+
+### World handling
+#### Interface
+- Progress of spawn terrain chunk generation is now logged during initial world creation.
+
+#### Functional
+- Minecraft Bedrock worlds up to 1.12.x are now supported. (1.13+ are still **not supported** due to yet another format change, which is large and requires a lot of work).
+- Automatic conversion of deprecated world formats is now implemented.
+- All formats except `leveldb` have been deprecated. The following world formats will be **automatically converted on load to a new format**:
+ - `mcregion`
+ - `anvil`
+ - `pmanvil`
+- 256 build-height is now supported in all worlds (facilitated by automatic conversion).
+- Extended blocks are now supported (facilitated by automatic conversion).
+- Lighting is no longer stored or loaded from disk - instead, it's calculated on the fly as-needed. This fixes many long-standing bugs.
+
+#### Performance
+- `leveldb` is now the primary supported world format. It is inherently faster than region-based formats thanks to better design.
+- Partial chunk saves (only saving modified subcomponents of chunks) has been implemented. This drastically reduces the amount of data that is usually necessary to write on chunk save, which in turn **drastically reduces the time to complete world saves**. This is possible thanks to the modular design of the `leveldb` world format - this enhancement is not possible with region-based formats.
+- Lighting is no longer guaranteed to be available on every chunk. It's now calculated on the fly as-needed.
+- `/op`, `/deop`, `/whitelist add` and `/whitelist remove` no longer cause player data to be loaded from disk for no reason.
+- Timings now use high-resolution timers provided by `hrtime()` to collect more accurate performance metrics.
+- Z-order curves (morton codes) are now used for block and chunk coordinate hashes. This substantially improves performance in many areas by resolving a hashtable key hash collision performance issue. Affected areas include explosions, light calculation, and more.
+- [`libdeflate`](https://github.com/ebiggers/libdeflate) is now (optionally) used for outbound Minecraft packet compression. It's more than twice as fast as zlib in most cases, providing significant performance boosts to packet broadcasts and overall network performance.
+- Closures are now used for internal event handler calls. This provides a performance improvement of 10-20% over the 3.x system, which had to dynamically resolve callables for every event call.
+
+### Logger revamp
+- Many components now have a dedicated logger which automatically adds [prefixes] to their messages.
+- Main logger now includes milliseconds in timestamps.
+
+### Network
+This version features substantial changes to the network system, improving coherency, reliability and modularity.
+
+#### Minecraft Bedrock packet encryption
+- This fixes replay attacks where hackers steal and replay player logins.
+- A new setting has been added to `pocketmine.yml`: `network.enable-encryption` which is `true` by default.
+
+#### Packet receive error handling has been overhauled
+- Only `BadPacketException` is now caught during packet decode and handling. This requires that all decoding MUST perform proper data error checking.
+ - Throwing a `BadPacketException` from decoding will now cause players to be kicked with the message `Packet processing error`.
+ - The disconnect message includes a random hex ID to help server owners identify the problems reported by their players.
+- Throwing any other exception will now cause a server crash. `Internal server error` has been removed.
+- It is now illegal to send a clientbound packet to the server. Doing so will result in the client being kicked with the message `Unexpected non-serverbound packet`.
+
+#### New packet handler system
+- Packet handlers have been separated from NetworkSession into a dedicated packet handler structure.
+- A network session may have exactly 1 handler at a time, which is mutable and may be replaced at any time. This allows packet handling logic to be broken up into multiple stages:
+ - preventing undefined behaviour when sending wrong packets at the wrong time (they'll now be silently dropped)
+ - allowing the existence of ephemeral state-specific logic (for example stricter resource packs download checks)
+- Packet handlers are now almost entirely absent from `Player` and instead appear in their own dedicated units.
+- Almost all game logic that was previously locked up inside packet handlers in `Player` has been extracted into new API methods. See Player API changes for details.
+
+## API
+### General
+- Most places which previously allowed `callable` now only allow `\Closure`. This is because closures have more consistent behaviour and are more performant.
+- `void` and `?nullable` parameter and return types have been applied in many places.
+- Everything in the `pocketmine\metadata` namespace and related implementations have been removed.
+
+### Changes to `plugin.yml`
+#### Permission nesting
+Permission nesting is no longer supported in `plugin.yml`. Grouping permissions (with defaults) in `plugin.yml` had very confusing and inconsistent behaviour.
+Instead of nesting permission declarations, they should each be declared separately.
+
+_Before_:
+```
+permissions:
+ pmmp:
+ default: op
+ children:
+ pmmp.something:
+ default: op
+ pmmp.somethingElse
+ default: op
+```
+
+_After_:
+```
+permissions:
+ pmmp.something:
+ default: op
+ pmmp.somethingElse
+ default: op
+```
+
+#### `src-namespace-prefix`
+A new directive `src-namespace-prefix` has been introduced. This allows you to get rid of those useless subdirectories in a plugin's structure.
+For example, a plugin whose main was `pmmp\TesterPlugin\Main` used to have to be structured like this:
+```
+|-- plugin.yml
+|-- src/
+ |-- pmmp/
+ |-- TesterPlugin/
+ |-- Main.php
+ |-- SomeOtherClass.php
+ |-- SomeNamespace/
+ |-- SomeNamespacedClass.php
+```
+However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`, now we can get rid of the useless directories and structure it like this instead:
+```
+|-- plugin.yml
+|-- src/
+ |-- Main.php
+ |-- SomeOtherClass.php
+ |-- SomeNamespace/
+ |-- SomeNamespacedClass.php
+```
+
+**Note**: The old structure will also still work just fine. This is not a required change.
+
+### Block
+- A new `VanillaBlocks` class has been added, which contains static methods for creating any currently-known block type. This should be preferred instead of use of `BlockFactory::get()` where constants were used.
+- Blocks now contain their positions instead of extending `Position`. `Block->getPosition()` has been added.
+- Blocks with IDs >= 256 are now supported.
+- Block state and variant metadata have been separated.
+ - Variant is considered an extension of ID and is immutable.
+ - `Block->setDamage()` has been removed.
+ - All blocks now have getters and setters for their appropriate block properties, such as facing, lit/unlit, colour (in some cases), and many more. These should be used instead of metadata.
+- Tile entities are now created and deleted automatically when `World->setBlock()` is used with a block that requires a tile entity.
+- Some tile entities' API has been exposed on their corresponding `Block` classes, with the tile entity classes being deprecated.
+- The `pocketmine\tile` namespace has been relocated to `pocketmine\block\tile`.
+- `Block->recalculateBoundingBox()` and `Block->recalculateCollisionBoxes()` are now expected to return AABBs relative to `0,0,0` instead of their own position.
+- Block break-info has been extracted into a new dynamic `BlockBreakInfo` unit. The following methods have been moved:
+ - `Block->getBlastResistance()` -> `BlockBreakInfo->getBlastResistance()`
+ - `Block->getBreakTime()` -> `BlockBreakInfo->getBreakTime()`
+ - `Block->getHardness()` -> `BlockBreakInfo->getHardness()`
+ - `Block->getToolHarvestLevel()` -> `BlockBreakInfo->getToolHarvestLevel()`
+ - `Block->getToolType()` -> `BlockBreakInfo->getToolType()`
+ - `Block->isBreakable()` -> `BlockBreakInfo->isBreakable()`
+ - `Block->isCompatibleWithTool()` -> `BlockBreakInfo->isToolCompatible()`
+- The following API methods have been added:
+ - `Block->asItem()`: returns an itemstack corresponding to the block
+ - `Block->isSameState()`: returns whether the block is the same as the parameter, including state information
+ - `Block->isSameType()`: returns whether the block is the same as the parameter, without state information
+ - `Block->isFullCube()`
+- The following hooks have been added:
+ - `Block->onAttack()`: called when a player in survival left-clicks the block to try to start breaking it
+ - `Block->onEntityLand()`: called when an entity lands on this block after falling (from any distance)
+ - `Block->onPostPlace()`: called directly after placement in the world, handles things like rail connections and chest pairing
+- The following API methods have been renamed:
+ - `Block->getDamage()` -> `Block->getMeta()`
+ - `Block->onActivate()` -> `Block->onInteract()`
+ - `Block->onEntityCollide()` -> `Block->onEntityInside()`
+- The following API methods have changed signatures:
+ - `Block->onInteract()` now has the signature `onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool`
+ - `Block->getCollisionBoxes()` is now final. Classes should override `recalculateCollisionBoxes()`.
+- The following API methods have been removed:
+ - `Block->canPassThrough()`
+ - `Block->setDamage()`
+ - `Block::get()`: this was superseded by `BlockFactory::get()` a long time ago
+ - `Block->getBoundingBox()`
+- The following classes have been renamed:
+ - `BlockIds` -> `BlockLegacyIds`
+ - `CobblestoneWall` -> `Wall`
+ - `NoteBlock` -> `Note`
+ - `SignPost` -> `Sign`
+ - `StandingBanner` -> `Banner`
+- The following classes have been removed:
+ - `Bricks`
+ - `BurningFurnace`
+ - `CobblestoneStairs`
+ - `Dandelion`
+ - `DoubleSlab`
+ - `DoubleStoneSlab`
+ - `EndStone`
+ - `GlowingRedstoneOre`
+ - `GoldOre`
+ - `Gold`
+ - `IronDoor`
+ - `IronOre`
+ - `IronTrapdoor`
+ - `Iron`
+ - `Lapis`
+ - `NetherBrickFence`
+ - `NetherBrickStairs`
+ - `Obsidian`
+ - `PurpurStairs`
+ - `Purpur`
+ - `QuartzStairs`
+ - `Quartz`
+ - `RedSandstoneStairs`
+ - `RedSandstone`
+ - `SandstoneStairs`
+ - `Sandstone`
+ - `StainedClay`
+ - `StainedGlassPane`
+ - `StainedGlass`
+ - `StoneBrickStairs`
+ - `StoneBricks`
+ - `StoneSlab2`
+ - `StoneSlab`
+ - `Stone`
+ - `WallBanner`
+ - `WallSign`
+ - `Wood2`
+- `BlockToolType` constants have been renamed to remove the `TYPE_` prefix.
+
+### Command
+- The following classes have been removed:
+ - `RemoteConsoleCommandSender`
+- The following API methods have signature changes:
+ - `Command->setPermission()` argument is now mandatory (but still nullable).
+ - `CommandSender->setScreenLineHeight()` argument is now mandatory (but still nullable).
+- Commands with spaces in the name are no longer supported.
+
+### Entity
+#### General
+- `Entity` no longer extends from `Location`. `Entity->getLocation()` and `Entity->getPosition()` should be used instead.
+- Ender inventory has been refactored. It's now split into two parts:
+ - `EnderChestInventory` is a temporary gateway "inventory" that acts as a proxy to the player's ender inventory. It has a `Position` for holder. This is discarded when the player closes the inventory window.
+ - `PlayerEnderInventory` is the storage part. This is stored in `Human` and does not contain any block info.
+ - To open the player's ender inventory, use `Player->setCurrentWindow(new EnderChestInventory($blockPos, $player->getEnderInventory()))`.
+- The following public fields have been removed:
+ - `Entity->chunk`: Entities no longer know which chunk they are in (the `World` now manages this instead).
+ - `Entity->height`: moved to `EntitySizeInfo`; use `Entity->size` instead
+ - `Entity->width`: moved to `EntitySizeInfo`; use `Entity->size` instead
+ - `Entity->eyeHeight`: moved to `EntitySizeInfo`; use `Entity->size` instead
+- The following API methods have been added:
+ - `Entity->getFallDistance()`
+ - `Entity->setFallDistance()`
+ - `ItemEntity->getDespawnDelay()`
+ - `ItemEntity->setDespawnDelay()`
+ - `Living->calculateFallDamage()`: this is `protected`, and may be overridden by subclasses to provide custom damage logic
+ - `Human->getHungerManager()`
+ - `Human->getXpManager()`
+- The following methods have signature changes:
+ - `Entity->entityBaseTick()` is now `protected`.
+ - `Entity->move()` is now `protected`.
+ - `Entity->setPosition()` is now `protected` (use `Entity->teleport()` instead).
+ - `Entity->setPositionAndRotation()` is now `protected` (use `Entity->teleport()` instead).
+ - `Living->knockBack()` now accepts `float, float, float` (the first two parameters have been removed).
+ - `Living->getEffects()` now returns `EffectManager` instead of `Effect[]`.
+- The following classes have been added:
+ - `effect\EffectManager`: contains effect-management functionality extracted from `Living`
+ - `HungerManager`: contains hunger-management functionality extracted from `Human`
+ - `ExperienceManager`: contains XP-management functionality extracted from `Human`
+- The following API methods have been moved / renamed:
+ - `Entity->fall()` -> `Entity->onHitGround()` (and visibility changed to `protected` from `public`)
+ - `Living->removeAllEffects()` -> `EffectManager->clear()`
+ - `Living->removeEffect()` -> `EffectManager->remove()`
+ - `Living->addEffect()` -> `EffectManager->add()`
+ - `Living->getEffect()` -> `EffectManager->get()`
+ - `Living->hasEffect()` -> `EffectManager->has()`
+ - `Living->hasEffects()` -> `EffectManager->hasEffects()`
+ - `Living->getEffects()` -> `EffectManager->all()`
+ - `Human->getFood()` -> `HungerManager->getFood()`
+ - `Human->setFood()` -> `HungerManager->setFood()`
+ - `Human->getMaxFood()` -> `HungerManager->getMaxFood()`
+ - `Human->addFood()` -> `HungerManager->addFood()`
+ - `Human->isHungry()` -> `HungerManager->isHungry()`
+ - `Human->getEnderChestInventory()` -> `Human->getEnderInventory()`
+ - `Human->getSaturation()` -> `HungerManager->getSaturation()`
+ - `Human->setSaturation()` -> `HungerManager->setSaturation()`
+ - `Human->addSaturation()` -> `HungerManager->addSaturation()`
+ - `Human->getExhaustion()` -> `HungerManager->getExhaustion()`
+ - `Human->setExhaustion()` -> `HungerManager->setExhaustion()`
+ - `Human->exhaust()` -> `HungerManager->exhaust()`
+ - `Human->getXpLevel()` -> `ExperienceManager->getXpLevel()`
+ - `Human->setXpLevel()` -> `ExperienceManager->setXpLevel()`
+ - `Human->addXpLevels()` -> `ExperienceManager->addXpLevels()`
+ - `Human->subtractXpLevels()` -> `ExperienceManager->subtractXpLevels()`
+ - `Human->getXpProgress()` -> `ExperienceManager->getXpProgress()`
+ - `Human->setXpProgress()` -> `ExperienceManager->setXpProgress()`
+ - `Human->getRemainderXp()` -> `ExperienceManager->getRemainderXp()`
+ - `Human->getCurrentTotalXp()` -> `ExperienceManager->getCurrentTotalXp()`
+ - `Human->setCurrentTotalXp()` -> `ExperienceManager->setCurrentTotalXp()`
+ - `Human->addXp()` -> `ExperienceManager->addXp()`
+ - `Human->subtractXp()` -> `ExperienceManager->subtractXp()`
+ - `Human->getLifetimeTotalXp()` -> `ExperienceManager->getLifetimeTotalXp()`
+ - `Human->setLifetimeTotalXp()` -> `ExperienceManager->setLifetimeTotalXp()`
+ - `Human->canPickupXp()` -> `ExperienceManager->canPickupXp()`
+ - `Human->onPickupXp()` -> `ExperienceManager->onPickupXp()`
+ - `Human->resetXpCooldown()` -> `ExperienceManager->resetXpCooldown()`
+- The following API methods have been removed:
+ - `Human->getRawUniqueId()`: use `Human->getUniqueId()->toBinary()` instead
+- The following classes have been removed:
+ - `Creature`
+ - `Damageable`
+ - `Monster`
+ - `NPC`
+ - `Rideable`
+ - `Vehicle`
+- `Skin` now throws exceptions on creation if given invalid data.
+
+#### Effect
+- All `Effect` related classes have been moved to the `pocketmine\entity\effect` namespace.
+- Effect functionality embedded in the `Effect` class has been separated out into several classes. The following classes have been added:
+ - `AbsorptionEffect`
+ - `HealthBoostEffect`
+ - `HungerEffect`
+ - `InstantDamageEffect`
+ - `InstantEffect`
+ - `InstantHealthEffect`
+ - `InvisibilityEffect`
+ - `LevitationEffect`
+ - `PoisonEffect`
+ - `RegenerationEffect`
+ - `SaturationEffect`
+ - `SlownessEffect`
+ - `SpeedEffect`
+ - `WitherEffect`
+- `VanillaEffects` class has been added. This exposes all vanilla effect types as static methods, replacing the old `Effect::getEffect()` nastiness.
+ - Example: `Effect::getEffect(Effect::NIGHT_VISION)` can be replaced by `VanillaEffects::NIGHT_VISION()`.
+- Negative effect amplifiers are now explicitly disallowed due to undefined behaviour they created.
+- The boundaries between MCPE effect IDs and PocketMine-MP internals are now more clear.
+ - ID handling is moved to `pocketmine\data\bedrock\EffectIdMap`.
+ - All effect ID constants have been removed from `Effect`. `pocketmine\data\bedrock\EffectIds` if you still need legacy effect IDs for some reason.
+- The following API methods have been moved:
+ - `Effect->getId()` -> `EffectIdMap->toId()`
+ - `Effect::registerEffect()` -> `EffectIdMap->register()`
+ - `Effect::getEffect()` -> `EffectIdMap->fromId()`
+ - `Effect::getEffectByName()` -> `VanillaEffects::fromString()`
+- The following API methods have been added:
+ - `Effect->getRuntimeId()`: this is a **dynamic ID** which can be used for effect type comparisons. **This cannot be stored, so don't use it in configs or NBT!**
+
+#### Removal of runtime entity NBT
+- Entities no longer keep their NBT alive at runtime.
+ - `Entity->namedtag` has been removed.
+ - `Entity->saveNBT()` now returns a newly created `CompoundTag` instead of modifying the previous one in-place.
+ - `Entity->initEntity()` now accepts a `CompoundTag` parameter.
+
+#### Entity creation
+- `Entity::createEntity()` has been removed. It's no longer needed for creating new entities at runtime - just use `new YourEntity` instead.
+- `Entity` subclass constructors can now have any signature, just like a normal class.
+- Loading entities from NBT is now handled by `EntityFactory`. It works quite a bit differently than `Entity::createEntity()` did. Instead of registering `YourEntity::class` to a set of Minecraft save IDs, you now need to provide a callback which will construct an entity when given some NBT and a `World`.
+ - The creation callback is registered using `EntityFactory::register()`.
+ - The creation callback must have the signature `function(World, CompoundTag) : Entity`.
+ - This enables `Entity` subclasses to have any constructor parameters they like.
+ - It also allows requiring that certain data is always provided (for example, it doesn't make much sense to create a `FallingBlock` without specifying what type of block).
+ - Examples:
+ - `ItemEntity` now requires an `Item` in its constructor, so its creation callback decodes the `Item` from the NBT to be passed to the constructor.
+ - `Painting` now requires a `PaintingMotive` in its constructor, so its creation callback decides which `PaintingMotive` to provide based on the NBT it receives.
+ - See `EntityFactory` for more examples.
+- `EntityFactory::register()` (previously `Entity::registerEntity()`) will now throw exceptions on error cases instead of returning `false`.
+- The following API methods have been moved:
+ - `Entity::registerEntity()` -> `EntityFactory::register()`
+- The following classes have changed constructors:
+ - All projectile subclasses now require a `?Entity $thrower` parameter.
+ - `Arrow->__construct()` now requires a `bool $critical` parameter (in addition to the `$thrower` parameter).
+ - `ExperienceOrb->__construct()` now requires a `int $xpValue` parameter.
+ - `FallingBlock->__construct()` now requires a `Block $block` parameter.
+ - `ItemEntity->__construct()` now requires an `Item $item` parameter.
+ - `Painting->__construct()` now requires a `PaintingMotive $motive` parameter.
+ - `SplashPotion->__construct()` now requires a `int $potionId` parameter.
+- The following API methods have been removed:
+ - `Entity::createBaseNBT()`: `new YourEntity` and appropriate API methods should be used instead
+ - `Entity->getSaveId()`
+ - `Entity::getKnownEntityTypes()`
+ - `Entity::createEntity()`: use `new YourEntity` instead (to be reviewed)
+
+#### WIP removal of entity network metadata
+- All network metadata related constants have been removed from the `Entity` class and moved to the protocol layer. It is intended to remove network metadata from the API entirely, but this has not yet been completed.
+ - `Entity::DATA_FLAG_*` constants have been moved to `pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags`.
+ - `Entity::DATA_TYPE_*` constants have been moved to `pocketmine\network\mcpe\protocol\types\entity\EntityMetadataTypes`.
+ - `Entity::DATA_*` constants have been moved to `pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties`.
+- `DataPropertyManager` has been moved to the `pocketmine\network\mcpe\protocol\types\entity` namespace, and as such isn't considered part of the API anymore.
+- Introduced internal `Entity` hook `syncNetworkData()`. This function is expected to synchronize entity properties with the entity's network data set.
+- Internal usage of network metadata sets to store internal entity properties has been removed. Entities are now expected to use regular class properties and synchronize with the network data set as-asked.
+- `Entity->propertyManager` has been renamed to `Entity->networkProperties`.
+- `Entity->getDataPropertyManager()` has been renamed to `Entity->getNetworkProperties()`.
+
+### Event
+#### Internal event system no longer depends on `Listener`s
+- The internal event processing system no longer depends on `Listener` objects. Arbitrary closures can now be used, provided that they satisfy the standard requirements to be a handler.
+ - This change improves performance of event handler calling by approximately 15%. This does not include anything plugins are doing.
+ - The following classes have been removed:
+ - `pocketmine\plugin\EventExecutor`
+ - `pocketmine\plugin\MethodEventExecutor`
+ - `RegisteredListener->__construct()` now requires `Closure` instead of `Listener, EventExecutor` as the leading parameters.
+ - `RegisteredListener->getListener()` has been removed.
+
+#### Default cancelled handling behaviour has changed
+- Handler functions will now **not receive cancelled events by default**. This is a **silent BC break**, i.e. it won't raise errors, but it might cause bugs.
+- `@ignoreCancelled` is now no longer respected.
+- `@handleCancelled` has been added. This allows opting _into_ receiving cancelled events (it's the opposite of `@ignoreCancelled`).
+
+#### `PlayerPreLoginEvent` changes
+- The `Player` object no longer exists at this phase of the login. Instead, a `PlayerInfo` object is provided, along with connection information.
+- Ban, server-full and whitelist checks are now centralized to `PlayerPreLoginEvent`. It's no longer necessary (or possible) to intercept `PlayerKickEvent` to handle these types of disconnects.
+ - Multiple kick reasons may be set to ensure that the player is still removed if there are other reasons for them to be disconnected and one of them is cleared. For example, if a player is banned and the server is full, clearing the ban flag will still cause the player to be disconnected because the server is full.
+ - Plugins may set custom kick reasons. Any custom reason has absolute priority.
+ - If multiple flags are set, the kick message corresponding to the highest priority reason will be shown. The priority (as of this snapshot) is as follows:
+ - Custom (highest priority)
+ - Server full
+ - Whitelisted
+ - Banned
+ - The `PlayerPreLoginEvent::KICK_REASON_PRIORITY` constant contains a list of kick reason priorities, highest first.
+- The following constants have been added:
+ - `PlayerPreLoginEvent::KICK_REASON_PLUGIN`
+ - `PlayerPreLoginEvent::KICK_REASON_SERVER_FULL`
+ - `PlayerPreLoginEvent::KICK_REASON_SERVER_WHITELISTED`
+ - `PlayerPreLoginEvent::KICK_REASON_BANNED`
+ - `PlayerPreLoginEvent::KICK_REASON_PRIORITY`: ordered list of kick reason priorities, highest first
+- The following API methods have been added:
+ - `PlayerPreLoginEvent->clearAllKickReasons()`
+ - `PlayerPreLoginEvent->clearKickReason()`
+ - `PlayerPreLoginEvent->getFinalKickMessage()`: the message to be shown to the player with the current reason list in place
+ - `PlayerPreLoginEvent->getIp()`
+ - `PlayerPreLoginEvent->getKickReasons()`: returns an array of flags indicating kick reasons, must be empty to allow joining
+ - `PlayerPreLoginEvent->getPlayerInfo()`
+ - `PlayerPreLoginEvent->getPort()`
+ - `PlayerPreLoginEvent->isAllowed()`
+ - `PlayerPreLoginEvent->isAuthRequired()`: whether XBL authentication will be enforced
+ - `PlayerPreLoginEvent->isKickReasonSet()`
+ - `PlayerPreLoginEvent->setAuthRequired()`
+ - `PlayerPreLoginEvent->setKickReason()`
+- The following API methods have been changed:
+ - `PlayerPreLoginEvent->getKickMessage()` now has the signature `getKickMessage(int $flag) : ?string`
+- The following API methods have been removed:
+ - `PlayerPreLoginEvent->setKickMessage()`
+ - `PlayerPreLoginEvent->getPlayer()`
+- The following API methods have been moved / renamed:
+ - `InventoryPickupItemEvent->getItem()` -> `InventoryPickupItemEvent->getItemEntity()`
+
+#### Other changes
+- Disconnecting players during events no longer crashes the server (although it might cause other side effects).
+- `PlayerKickEvent` is no longer fired for disconnects that occur before the player completes the initial login sequence (i.e. completing downloading resource packs).
+- Cancellable events must now implement `CancellableTrait` to get the cancellable components needed to satisfy interface requirements. `Event` no longer stubs these methods.
+- `PlayerInteractEvent` is no longer fired when a player activates an item. This fixes the age-old complaint of `PlayerInteractEvent` firing multiple times when interacting once. The following constants have been removed:
+ - `PlayerInteractEvent::LEFT_CLICK_AIR`
+ - `PlayerInteractEvent::RIGHT_CLICK_AIR`
+ - `PlayerInteractEvent::PHYSICAL`
+- The following events have been added:
+ - `PlayerEntityInteractEvent`: player right-clicking (or long-clicking on mobile) on an entity.
+ - `PlayerItemUseEvent`: player activating their held item, for example to throw it.
+ - `BlockTeleportEvent`: block teleporting, for example dragon egg when attacked.
+ - `PlayerDisplayNameChangeEvent`
+ - `EntityItemPickupEvent`: player (or other entity) picks up a dropped item (or arrow). Replaces `InventoryPickupItemEvent` and `InventoryPickupArrowEvent`.
+ - Unlike its predecessors, this event supports changing the destination inventory.
+ - If the destination inventory is `null`, the item will be destroyed. This is usually seen for creative players with full inventories.
+ - `EntityTrampleFarmlandEvent`: mob (or player) jumping on farmland causing it to turn to dirt
+ - `StructureGrowEvent`: called when trees or bamboo grow (or any other multi-block plant structure).
+- The following events have been removed:
+ - `EntityArmorChangeEvent`
+ - `EntityInventoryChangeEvent`
+ - `EntityLevelChangeEvent` - `EntityTeleportEvent` with world checks should be used instead.
+ - `InventoryPickupItemEvent` - use `EntityItemPickupEvent` instead
+ - `InventoryPickupArrowEvent` - use `EntityItemPickupEvent` instead
+ - `NetworkInterfaceCrashEvent`
+ - `PlayerCheatEvent`
+ - `PlayerIllegalMoveEvent`
+- The following API methods have been added:
+ - `EntityDeathEvent->getXpDropAmount()`
+ - `EntityDeathEvent->setXpDropAmount()`
+ - `PlayerDeathEvent->getXpDropAmount()`
+ - `PlayerDeathEvent->setXpDropAmount()`
+- The following API methods have been removed:
+ - `PlayerPreLoginEvent->getPlayer()`
+ - `Cancellable->setCancelled()`: this allows implementors of `Cancellable` to implement their own cancellation mechanisms, such as the complex one in `PlayerPreLoginEvent`
+- The following API methods have been moved:
+ - `Event->isCancelled()` -> `CancellableTrait->isCancelled()`: this was a stub which threw `BadMethodCallException` if the class didn't implement `Cancellable`; now this is simply not available on non-cancellable events
+ - `Event->setCancelled()` has been split into `cancel()` and `uncancel()`, and moved to `CancellableTrait`
+ - `HandlerList::unregisterAll()` -> `HandlerListManager->unregisterAll()`
+ - `HandlerList::getHandlerListFor()` -> `HandlerListManager->getListFor()`
+ - `HandlerList::getHandlerLists()` -> `HandlerListManager->getAll()`
+- The following classes have been moved:
+ - `pocketmine\plugin\RegisteredListener` -> `pocketmine\event\RegisteredListener`
+
+### Inventory
+- All crafting and recipe related classes have been moved to the `pocketmine\crafting` namespace.
+- The following classes have been added:
+ - `CallbackInventoryChangeListener`
+ - `CreativeInventory`: contains the creative functionality previously embedded in `pocketmine\item\Item`, see Item changes for details
+ - `InventoryChangeListener`: allows listening (but not interfering with) events in an inventory.
+ - `PlayerEnderInventory`: represents the pure storage part of the player's ender inventory, without any block information
+ - `transaction\CreateItemAction`
+ - `transaction\DestroyItemAction`
+- The following classes have been renamed / moved:
+ - `ContainerInventory` -> `pocketmine\block\inventory\BlockInventory`
+- The following classes have been moved to the `pocketmine\block\inventory` namespace:
+ - `AnvilInventory`
+ - `ChestInventory`
+ - `DoubleChestInventory`
+ - `EnchantInventory`
+ - `EnderChestInventory`
+ - `FurnaceInventory`
+- The following classes have been removed:
+ - `CustomInventory`
+ - `InventoryEventProcessor`
+ - `Recipe`
+ - `transaction\CreativeInventoryAction`
+- The following API methods have been added:
+ - `Inventory->addChangeListeners()`
+ - `Inventory->getChangeListeners()`
+ - `Inventory->removeChangeListeners()`
+ - `Inventory->swap()`: swaps the contents of two slots
+- The following API methods have been removed:
+ - `BaseInventory->getDefaultSize()`
+ - `BaseInventory->setSize()`
+ - `EnderChestInventory->setHolderPosition()`
+ - `Inventory->close()`
+ - `Inventory->dropContents()`
+ - `Inventory->getName()`
+ - `Inventory->getTitle()`
+ - `Inventory->onSlotChange()`
+ - `Inventory->open()`
+ - `Inventory->sendContents()`
+ - `Inventory->sendSlot()`
+ - `InventoryAction->onExecuteFail()`
+ - `InventoryAction->onExecuteSuccess()`
+ - `PlayerInventory->sendCreativeContents()`
+- The following API methods have signature changes:
+ - `Inventory->clear()` now returns `void` instead of `bool`.
+ - `Inventory->setItem()` now returns `void` instead of `bool`.
+ - `InventoryAction->execute()` now returns `void` instead of `bool`.
+ - `BaseInventory->construct()` no longer accepts a list of items to initialize with.
+- `PlayerInventory->setItemInHand()` now sends the update to viewers of the player.
+
+### Item
+#### General
+- A new `VanillaItems` class has been added, which contains static methods for creating any currently-known item type. This should be preferred instead of use of `ItemFactory::get()` where constants were used.
+- `StringToItemParser` has been added, which allows mapping any string to any item, irrespective of IDs. These mappings are used by `/give` and `/clear`, and are made with custom plugin aliases in mind.
+ - Yes, this means you can finally add your own custom aliases to `/give` without ugly hacks!
+- `LegacyStringToItemParser` has been added, which is a slightly more dynamic (but still inadvisable) replacement for `ItemFactory::fromString()`.
+- `Item->count` is no longer public.
+- The hierarchy of writable books has been changed: `WritableBook` and `WrittenBook` now extend `WritableBookBase`.
+- The following API methods have signature changes:
+ - `WritableBookBase->setPages()` now accepts `WritableBookPage[]` instead of `CompoundTag[]`.
+ - `ItemFactory::get()` no longer accepts `string` for the `tags` parameter.
+ - `ItemFactory::fromString()` no longer accepts a `$multiple` parameter and now only returns `Item`, not `Item|Item[]`.
+- The following methods are now fluent:
+ - `WritableBookBase->setPages()`
+ - `Item->addEnchantment()`
+ - `Item->removeEnchantment()`
+ - `Item->removeEnchantments()`
+ - `Armor->setCustomColor()`
+ - `WrittenBook->setTitle()`
+ - `WrittenBook->setAuthor()`
+ - `WrittenBook->setGeneration()`
+- The following API methods have been removed:
+ - `Item->getNamedTagEntry()`
+ - `Item->removeNamedTagEntry()`
+ - `Item->setDamage()`: "Damage" is now immutable for all items except `Durable` descendents.
+ - `Item->setNamedTagEntry()`
+ - `Item::get()`: this was superseded by `ItemFactory::get()` a long time ago
+ - `Item::fromString()`: this was superseded by `ItemFactory::fromString()` a long time ago
+ - `Item->setCompoundTag()`
+ - `Item->getCompoundTag()`
+ - `Item->hasCompoundTag()`
+ - `Potion::getPotionEffectsById()`
+ - `ProjectileItem->getProjectileEntityType()`
+- The following constants have been removed:
+ - `Potion::ALL` - use `PotionType::getAll()` instead
+ - `Potion::WATER`
+ - `Potion::MUNDANE`
+ - `Potion::LONG_MUNDANE`
+ - `Potion::THICK`
+ - `Potion::AWKWARD`
+ - `Potion::NIGHT_VISION`
+ - `Potion::LONG_NIGHT_VISION`
+ - `Potion::INVISIBILITY`
+ - `Potion::LONG_INVISIBILITY`
+ - `Potion::LEAPING`
+ - `Potion::LONG_LEAPING`
+ - `Potion::STRONG_LEAPING`
+ - `Potion::FIRE_RESISTANCE`
+ - `Potion::LONG_FIRE_RESISTANCE`
+ - `Potion::SWIFTNESS`
+ - `Potion::LONG_SWIFTNESS`
+ - `Potion::STRONG_SWIFTNESS`
+ - `Potion::SLOWNESS`
+ - `Potion::LONG_SLOWNESS`
+ - `Potion::WATER_BREATHING`
+ - `Potion::LONG_WATER_BREATHING`
+ - `Potion::HEALING`
+ - `Potion::STRONG_HEALING`
+ - `Potion::HARMING`
+ - `Potion::STRONG_HARMING`
+ - `Potion::POISON`
+ - `Potion::LONG_POISON`
+ - `Potion::STRONG_POISON`
+ - `Potion::REGENERATION`
+ - `Potion::LONG_REGENERATION`
+ - `Potion::STRONG_REGENERATION`
+ - `Potion::STRENGTH`
+ - `Potion::LONG_STRENGTH`
+ - `Potion::STRONG_STRENGTH`
+ - `Potion::WEAKNESS`
+ - `Potion::LONG_WEAKNESS`
+ - `Potion::WITHER`
+- The following methods have been renamed:
+ - `Item->getDamage()` -> `Item->getMeta()`
+- The following methods have been moved to `pocketmine\inventory\CreativeInventory`:
+ - `Item::addCreativeItem()` -> `CreativeInventory::add()`
+ - `Item::clearCreativeItems()` -> `CreativeInventory::clear()`
+ - `Item::getCreativeItemIndex()` -> `CreativeInventory::getItemIndex()`
+ - `Item::getCreativeItems()` -> `CreativeInventory::getAll()`
+ - `Item::initCreativeItems()` -> `CreativeInventory::init()`
+ - `Item::isCreativeItem()` -> `CreativeInventory::contains()`
+ - `Item::removeCreativeItem()` -> `CreativeInventory::remove()`
+- The following classes have been added:
+ - `ArmorTypeInfo`
+ - `Fertilizer`
+ - `LiquidBucket`
+ - `MilkBucket`
+ - `PotionType`: enum class containing information about vanilla potion types
+ - `WritableBookBase`
+ - `WritableBookPage`
+- The following API methods have been added:
+ - `Armor->getArmorSlot()`
+ - `Item->canStackWith()`: returns whether the two items could be contained in the same inventory slot, ignoring count and stack size limits
+ - `Potion->getType()`: returns a `PotionType` enum object containing information such as the applied effects
+ - `ProjectileItem->createEntity()`: returns a new instance of the projectile entity that will be thrown
+- The following classes have been removed:
+ - `ChainBoots`
+ - `ChainChestplate`
+ - `ChainHelmet`
+ - `ChainLeggings`
+ - `DiamondBoots`
+ - `DiamondChestplate`
+ - `DiamondHelmet`
+ - `DiamondLeggings`
+ - `GoldBoots`
+ - `GoldChestplate`
+ - `GoldHelmet`
+ - `GoldLeggings`
+ - `IronBoots`
+ - `IronChesplate`
+ - `IronHelmet`
+ - `IronLeggings`
+ - `LeatherBoots`
+ - `LeatherCap`
+ - `LeatherPants`
+ - `LeatherTunic`
+
+#### NBT handling
+- Serialized NBT byte array caches are no longer stored on itemstacks. These caches were a premature optimization used for network layer serialization and as such were dependent on the network NBT format.
+- Internal NBT usage has been marginalized. It's no longer necessary to immediately write changes to NBT. The following hooks have been added:
+ - `Item->serializeCompoundTag()`
+ - `Item->deserializeCompoundTag()`
+- It's planned to remove runtime NBT from items completely, but this currently presents unresolved backwards-compatibility problems.
+
+#### Enchantment
+- `VanillaEnchantments` class has been added. This exposes all vanilla enchantment types as static methods, replacing the old `Enchantment::get()` nastiness.
+ - Example: `Enchantment::get(Enchantment::PROTECTION)` is replaced by `VanillaEnchantments::PROTECTION()`
+ - These methods also provide proper type information to static analysers instead of just generic `Enchantment`, making them easier to code with.
+- The boundaries between MCPE enchantment IDs and PocketMine-MP internals are now more clear.
+ - ID handling is moved to `pocketmine\data\bedrock\EnchantmentIdMap`.
+ - All enchantment ID constants have been removed from `Enchantment`. `pocketmine\data\bedrock\EnchantmentIds` if you still need legacy effect IDs for some reason.
+- `Enchantment::RARITY_*` constants were moved to `Rarity` class, and the `RARITY_` prefixes removed.
+- `Enchantment::SLOT_*` constants were moved to `ItemFlags` class, and the `SLOT_` prefixes removed.
+- The following API methods have been moved:
+ - `Enchantment::registerEnchantment()` -> `EnchantmentIdMap->register()`
+ - `Enchantment::getEnchantment()` -> `EnchantmentIdMap->fromId()`
+ - `Enchantment->getId()` -> `EnchantmentIdMap->toId()`
+ - `Enchantment::getEnchantmentByName()` -> `VanillaEnchantments::fromString()`
+- The following API methods have been added:
+ - `Enchantment->getRuntimeId()`: this is a **dynamic ID** which can be used for enchantment type comparisons. **This cannot be stored, so don't use it in configs or NBT!**
+
+### Lang
+- The following classes have been renamed:
+ - `BaseLang` -> `Language`
+ - `TranslationContainer` -> `Translatable`
+- The following classes have been removed:
+ - `TextContainer`
+- The following API methods have been added:
+ - `Translatable->format()`: allows adding formatting (such as color codes) to a translation
+ - `Translatable->prefix()`: allows prefixing formatting
+ - `Translatable->postfix()`: allows postfixing formatting
+- The following API methods have changed signatures:
+ - `Translatable->__construct()` now accepts `array` for parameters, instead of just `list`.
+ - `Translatable->getParameter()` now accepts `int|string` for the index instead of just `int`.
+ - `Translatable->getParameter()` now returns `Translatable|string` instead of just `string`.
+ - `Translatable->getParameters()` now returns `array`.
+- `LanguageNotFoundException` has been added. This is thrown when trying to construct a `Language` which doesn't exist in the server files.
+- `Translatable` no longer discards keys for translation parameters. Previously, only the insertion order was considered.
+- `Translatable` now supports string keys for translation parameters.
+- `Translatable` now supports providing other `Translatable`s as translation parameters.
+- `Language->translateString()` now supports providing `Translatable`s as translation parameters.
+- `Language->translateString()` no longer automatically attempts to translate string parameters. If you want them to be translated, translate them explicitly. This fixes bugs where player chat messages containing translation keys would be unexpectedly translated.
+- `Language->translate()` no longer attempts to translate string parameters of `Translatable` (same rationale as previous point).
+
+### Network
+- The following fields have been removed:
+ - `Network::$BATCH_THRESHOLD`
+- The following classes have been renamed:
+ - `SourceInterface` -> `NetworkInterface`
+ - `AdvancedSourceInterface` -> `AdvancedNetworkInterface`
+- The following classes have been moved:
+ - `CompressBatchedTask` -> `mcpe\CompressBatchTask`
+ - `level\format\io\ChunkRequestTask` -> `mcpe\ChunkRequestTask`
+ - `mcpe\RakLibInterface` -> `mcpe\raklib\RakLibInterface`
+- The following classes have been removed:
+ - `mcpe\PlayerNetworkSessionAdapter`
+- The following methods have been renamed:
+ - `UPnP::PortForward()` -> `UPnP::portForward()`
+ - `UPnP::RemovePortForward()` -> `UPnP::removePortForward()`
+- The following methods have changed signatures:
+ - `UPnP::portForward()` now accepts `string $serviceURL, string $internalIP, int $internalPort, int $externalPort`.
+ - `UPnP::removePortForward()` now accepts `string $serviceURL, int $externalPort`.
+- The following methods have been removed:
+ - `NetworkInterface->putPacket()`
+ - `NetworkInterface->close()`
+ - `NetworkInterface->emergencyShutdown()`
+- `NetworkInterface` now represents a more generic interface to be implemented by any network component, as opposed to specifically a player network interface.
+- Everything under the `rcon` subnamespace has been removed.
+- `upnp\UPnP` has significant changes. It's now a network component instead of a pair of static methods.
+
+### Permission
+- The following new permission nodes have been introduced:
+ - `pocketmine.group.everyone`: granted to everyone by default
+ - `pocketmine.group.operator`: granted to operator players and the console
+ These permission nodes can be assigned and overridden by permission attachments just like any other, which means it's now possible to grant **temporary operator** status which goes away when the player disconnects (or the attachment is removed).
+- Permissions are now always false if they haven't been set explictly, or granted implicitly by another permission.
+- Undefined permissions are now always `false` instead of following the value of `Permission::$DEFAULT_PERMISSION`.
+- Permissions internally no longer have default values. Instead, they are now assigned as a child of one of the `pocketmine.group` permissions:
+ - `true`: add as child to `pocketmine.group.everyone` with value `true`
+ - `false`: do not add to any permission
+ - `op`: add as child to `pocketmine.group.operator` with value `true`
+ - `notop`: add as child to `pocketmine.group.everyone` with value `true`, and to `pocketmine.group.operator` with value `false`
+ However, the `default` key in `plugin.yml` permission definitions continues to be supported.
+- Added `PermissibleDelegateTrait` to reduce boilerplate for users of `PermissibleBase`. This trait is used by `ConsoleCommandSender` and `Player`.
+- The following API methods have been moved:
+ - `Permission::getByName()` -> `PermissionParser::defaultFromString()`
+ - `Permission::loadPermissions()` -> `PermissionParser::loadPermissions()`
+ - `Permission::loadPermission()` -> `PermissionParser::loadPermission()`
+- The following constants have been moved:
+ - `Permission::DEFAULT_FALSE` -> `PermissionParser::DEFAULT_FALSE`
+ - `Permission::DEFAULT_TRUE` -> `PermissionParser::DEFAULT_TRUE`
+ - `Permission::DEFAULT_OP` -> `PermissionParser::DEFAULT_OP`
+ - `Permission::DEFAULT_NOT_OP` -> `PermissionParser::DEFAULT_NOT_OP`
+- The following API methods have been added:
+ - `Permission->addChild()`
+ - `Permission->removeChild()`
+ - `Permissible->getPermissionRecalculationCallbacks()` - allows reacting to changes of permissions, such as new permissions being granted or denied
+ - `Permissible->setBasePermission()` - used for assigning root permissions like `pocketmine.group.operator`; plugins usually shouldn't use this
+ - `Permissible->unsetBasePermission()`
+ - `PermissionAttachmentInfo->getGroupPermissionInfo()` - returns the `PermissionAttachmentInfo` of the permission that caused the current permission value to be set, or null if the permission is explicit
+- The following API methods have been removed:
+ - `Permissible->isOp()`: use `Permissible->hasPermission(DefaultPermissions::ROOT_OPERATOR)` instead, **but you really shouldn't directly depend on a player's op status, add your own permissions instead!**
+ - `Permissible->setOp()`: use `addAttachment($plugin, DefaultPermissions::ROOT_OPERATOR, true)` instead to add, and `removeAttachment()` to remove it (or addAttachment() with false to explicitly deny it, just like any other permission)
+ - `Permission->addParent()`
+ - `Permission->getDefault()`
+ - `Permission->setDefault()`
+ - `PermissionManager->getDefaultPermissions()`
+ - `PermissionManager->recalculatePermissionDefaults()`
+ - `PermissionManager->subscribeToDefaultPerms()`
+ - `PermissionManager->unsubscribeFromDefaultPerms()`
+ - `PermissionManager->getDefaultPermSubscriptions()`
+ - `PermissionAttachment->getPermissible()`
+ - `PermissionAttachmentInfo->getPermissible()`
+- The following fields have been removed:
+ - `Permission::$DEFAULT_PERMISSION`
+- The following API methods have changes:
+ - `PermissionParser::defaultFromString()` now throws `InvalidArgumentException` on unknown values.
+ - `Permission->__construct()` no longer accepts a `$defaultValue` parameter (see notes above about defaults refactor).you should add your permission as a child of `pocketmine.group.everyone` or `pocketmine.group.operator` instead).
+- The following classes have been removed:
+ - `ServerOperator`
+
+### Player
+- The following classes have moved to the new `pocketmine\player` namespace:
+ - `Achievement`
+ - `GameMode`
+ - `IPlayer`
+ - `OfflinePlayer`
+ - `PlayerInfo`
+ - `Player`
+- The following constants have been removed:
+ - `Player::SURVIVAL` - use `GameMode::SURVIVAL()`
+ - `Player::CREATIVE` - use `GameMode::CREATIVE()`
+ - `Player::ADVENTURE` - use `GameMode::ADVENTURE()`
+ - `Player::SPECTATOR` - use `GameMode::SPECTATOR()`
+ - `Player::VIEW` - use `GameMode::SPECTATOR()`
+- (almost) all packet handlers have been removed from `Player`. They are now encapsulated within the network layer.
+- `Player->getSpawn()` no longer returns the world's safe spawn if the player's spawn position isn't set. Returning the safe spawn at the time of call made no sense, because it might not have been safe when actually used. You should pass the result of this function to `World->getSafeSpawn()` to get a safe spawn position instead.
+- The following API methods have been added:
+ - `Player->attackBlock()`: attack (left click) the target block, e.g. to start destroying it (survival)
+ - `Player->attackEntity()`: melee-attack (left click) the target entity (if within range)
+ - `Player->breakBlock()`: destroy the target block in the current world (immediately)
+ - `Player->consumeHeldItem()`: consume the previously activated item, e.g. eating food
+ - `Player->continueBreakBlock()`: punch the target block during destruction in survival, advancing break animation and creating particles
+ - `Player->getItemCooldownExpiry()`: returns the tick on which the player's cooldown for a given item expires
+ - `Player->hasFiniteResources()`
+ - `Player->interactBlock()`: interact (right click) the target block in the current world
+ - `Player->interactEntity()`: interact (right click) the target entity, e.g. to apply a nametag (not implemented yet)
+ - `Player->pickBlock()`: picks (mousewheel click) the target block in the current world
+ - `Player->releaseHeldItem()`: release the previously activated item, e.g. shooting a bow
+ - `Player->selectHotbarSlot()`: select the specified hotbar slot
+ - `Player->stopBreakBlock()`: cease attacking a previously attacked block
+ - `Player->toggleFlight()`: tries to start / stop flying (fires events, may be cancelled)
+ - `Player->updateNextPosition()`: sets the player's next attempted move location (fires events, may be cancelled)
+ - `Player->useHeldItem()`: activate the held item, e.g. throwing a snowball
+ - `Player->getSaveData()`: returns save data generated on the fly
+- The following API methods have been removed:
+ - `Player->addActionBarMessage()`: replaced by `sendActionBarMessage()`
+ - `Player->addSubTitle()`: replaced by `sendSubTitle()`
+ - `Player->addTitle()`: replaced by `sendTitle()`
+ - `Player->getAddress()`: replaced by `NetworkSession->getIp()`
+ - `Player->getPing()`: moved to `NetworkSession`
+ - `Player->getPort()`: moved to `NetworkSession`
+ - `Player->updatePing()`: moved to `NetworkSession`
+ - `Player->dataPacket()`: replaced by `NetworkSession->sendDataPacket()`
+ - `Player->sendDataPacket()`: replaced by `NetworkSession->sendDataPacket()`
+ - `Player->updateNextPosition()`: use `Player->handleMovement()` instead
+ - `IPlayer->isWhitelisted()`: use `Server->isWhitelisted()` instead
+ - `IPlayer->setWhitelisted()`: use `Server->setWhitelisted()` instead
+ - `IPlayer->isBanned()`: this was unreliable because it only checked name bans and didn't account for plugin custom ban systems. Use `Server->getNameBans()->isBanned()` and `Server->getIPBans()->isBanned()` instead.
+ - `IPlayer->setBanned()`: use `Server` APIs instead
+ - `IPlayer->isOp()`: use `Server` APIs instead
+ - `IPlayer->setOp()`: use `Server` APIs instead
+
+### Plugin
+- API version checks are now more strict. It is no longer legal to declare multiple minimum versions on the same major version. Doing so will now cause the plugin to fail to load with the message `Multiple minimum API versions found for some major versions`.
+- `plugin.yml` YAML commands loading is now internalized inside `PluginBase`.
+- `PluginManager->registerEvent()` now has a simpler signature: `registerEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false)`. The provided closure must accept the specified event class as its only parameter. See [Event API changes](#event) for more details.
+- The following classes have been removed:
+ - `PluginLogger`
+- The following constants have been removed:
+ - `PluginLoadOrder::STARTUP` - use `PluginEnableOrder::STARTUP()`
+ - `PluginLoadOrder::POSTWORLD` - use `PluginEnableOrder::POSTWORLD()`
+- The following interface requirements have been removed:
+ - `Plugin->onEnable()`: this is now internalized inside `PluginBase`
+ - `Plugin->onDisable()`: same as above
+ - `Plugin->onLoad()`: same as above
+ - `Plugin->getServer()` is no longer required to be implemented. It's implemented in `PluginBase` for convenience.
+ - `Plugin->isDisabled()` was removed (use `Plugin->isEnabled()` instead).
+ - `Plugin` no longer extends `CommandExecutor`. This means that `Plugin` implementations don't need to implement `onCommand()` anymore.
+- The following hook methods have changed visibility:
+ - `PluginBase->onEnable()` changed from `public` to `protected`
+ - `PluginBase->onDisable()` changed from `public` to `protected`
+ - `PluginBase->onLoad()` changed from `public` to `protected`
+- The following hook methods have been renamed:
+ - `Plugin->setEnabled()` -> `Plugin->onEnableStateChange()`. This change was made to force plugin developers misusing this hook to stop, and to give it a name that better describes what it does.
+- The following (deprecated) API methods have been removed:
+ - `PluginManager->callEvent()`: use `Event->call()` instead
+ - `PluginManager->addPermission()`: use `PermissionManager` instead
+ - `PluginManager->getDefaultPermSubscriptions()`: use `PermissionManager` instead
+ - `PluginManager->getDefaultPermissions()`: use `PermissionManager` instead
+ - `PluginManager->getPermission()`: use `PermissionManager` instead
+ - `PluginManager->getPermissionSubscriptions()`: use `PermissionManager` instead
+ - `PluginManager->getPermissions()`: use `PermissionManager` instead
+ - `PluginManager->recalculatePermissionDefaults()`: use `PermissionManager` instead
+ - `PluginManager->removePermission()`: use `PermissionManager` instead
+ - `PluginManager->subscribeToDefaultPerms()`: use `PermissionManager` instead
+ - `PluginManager->subscribeToPermission()`: use `PermissionManager` instead
+ - `PluginManager->unsubscribeFromDefaultPerms()`: use `PermissionManager` instead
+ - `PluginManager->unsubscribeFromPermission()`: use `PermissionManager` instead
+- It is no longer permitted to throw exceptions from `PluginBase->onEnable()` or `PluginBase->onLoad()`. Doing so will now cause the server to crash.
+
+### Scheduler
+#### Thread-local storage for AsyncTasks
+- TLS has been completely rewritten in this release to be self contained, more robust and easier to use.
+- Now behaves more like simple properties. `storeLocal()` writes, `fetchLocal()` reads.
+- Self-contained and doesn't depend on the async pool to clean up after it.
+- Values are automatically removed from storage when the `AsyncTask` is garbage-collected, just like a regular property.
+- Supports storing multiple values, differentiated by string names.
+- `fetchLocal()` can now be used multiple times. It no longer deletes the stored value.
+- The following classes have been removed:
+ - `FileWriteTask`
+- The following methods have been removed:
+ - `AsyncTask->peekLocal()`: use `fetchLocal()` instead
+- The following methods have signature changes:
+ - `AsyncTask->storeLocal()` now has the signature `storeLocal(string $key, mixed $complexData) : void`
+ - `AsyncTask->fetchLocal()` now has the signature `fetchLocal(string $key) : mixed`
+
+#### Other changes
+- `AsyncPool` uses a new, significantly more performant algorithm for task collection.
+- `BulkCurlTask` has had the `$complexData` constructor parameter removed.
+- `BulkCurlTask->__construct()` now accepts `BulkCurlTaskOperation[]` instead of `mixed[]`.
+- Added `CancelTaskException`, which can be thrown from `Task::onRun()` to cancel a task (especially useful for `ClosureTask`).
+- `pocketmine\Collectable` has been removed, and is no longer extended by `AsyncTask`.
+- The following hooks have been added:
+ - `AsyncTask->onError()`: called on the main thread when an uncontrolled error was detected in the async task, such as a memory failure
+- The following hooks have signature changes:
+ - `AsyncTask->onCompletion()` no longer accepts a `Server` parameter, and has a `void` return type.
+ - `AsyncTask->onProgressUpdate()` no longer accepts a `Server` parameter, and has a `void` return type.
+- The following API methods have been removed:
+ - `AsyncTask->getFromThreadStore()`: use `AsyncTask->worker->getFromThreadStore()`
+ - `AsyncTask->removeFromThreadStore()`: use `AsyncTask->worker->removeFromThreadStore()`
+ - `AsyncTask->saveToThreadStore()`: use `AsyncTask->worker->saveToThreadStore()`
+
+### Server
+- New chat broadcasting APIs have been implemented, which don't depend on the permission system.
+ - The following API methods have been added:
+ - `subscribeToBroadcastChannel()` - allows subscribing a `CommandSender` to receive chat messages (and other message types) on any channel
+ - `unsubscribeFromBroadcastChannel()`
+ - `unsubscribeFromAllBroadcastChannels()`
+ - `getBroadcastChannelSubscribers()`
+ - Giving `Player` any `pocketmine.broadcast.*` permissions will cause them to automatically subscribe to the corresponding broadcast channel (and removing them will unsubscribe it).
+ - It's now possible to create and subscribe to custom broadcast channels without using permissions.
+ - However, `Player`s may automatically unsubscribe themselves from the builtin broadcast channels if they don't have the proper permissions.
+ - Automatic subscribe/unsubscribe from custom broadcast channels can be implemented using the new `Permissible` permission recalculation callbacks API.
+- The following API methods have been removed:
+ - `reloadWhitelist()`
+ - `getLevelMetadata()`
+ - `getPlayerMetadata()`
+ - `getEntityMetadata()`
+ - `getDefaultGamemode()`
+ - `getLoggedInPlayers()`
+ - `onPlayerLogout()`
+ - `addPlayer()`
+ - `removePlayer()`
+ - `reload()`
+ - `getSpawnRadius()`
+ - `enablePlugin()`
+ - `disablePlugin()`
+ - `getGamemodeString()` - replaced by `pocketmine\player\GameMode->getTranslationKey()`
+ - `getGamemodeName()` - replaced by `pocketmine\player\GameMode->name()`
+ - `getGamemodeFromString()` - replaced by `GameMode::fromString()`
+ - `broadcast()` - use `broadcastMessage()` instead
+- The following API methods have changed:
+ - `getOfflinePlayerData()` no longer creates data when it doesn't exist.
+- The following API methods have been renamed:
+ - `getPlayer()` -> `getPlayerByPrefix()` (consider using `getPlayerExact()` instead where possible)
+
+### Level / World
+#### General
+- All references to `Level` in the context of "world" have been changed to `World`.
+ - The `pocketmine\level` namespace has been renamed to `pocketmine\world`
+ - All classes containing the world `Level` in the name in the "world" context have been changed to `World`.
+ - `Position->getLevel()` has been renamed to `Position->getWorld()`, and `Position->level` has been renamed to `Position->world`.
+- Extracted a `WorldManager` unit from `Server`
+ - `Server->findEntity()` -> `WorldManager->findEntity()`
+ - `Server->generateLevel()` -> `WorldManager->generateWorld()`
+ - `Server->getAutoSave()` -> `WorldManager->getAutoSave()`
+ - `Server->getDefaultLevel()` -> `WorldManager->getDefaultWorld()`
+ - `Server->getLevel()` -> `WorldManager->getWorld()`
+ - `Server->getLevelByName()` -> `WorldManager->getWorldByName()`
+ - `Server->getLevels()` -> `WorldManager->getWorlds()`
+ - `Server->isLevelGenerated()` -> `WorldManager->isWorldGenerated()`
+ - `Server->isLevelLoaded()` -> `WorldManager->isWorldLoaded()`
+ - `Server->loadLevel()` -> `WorldManager->loadWorld()`
+ - `WorldManager->loadWorld()` may convert worlds if requested (the `$autoUpgrade` parameter must be provided).
+ - `Server->setAutoSave()` -> `WorldManager->setAutoSave()`
+ - `Server->setDefaultLevel()` -> `WorldManager->setDefaultWorld()`
+ - `Server->unloadLevel()` -> `WorldManager->unloadWorld()`
+- Added `WorldManager->getAutoSaveTicks()` and `WorldManager->setAutoSaveTicks()` to allow controlling the autosave interval.
+- The following classes have been added:
+ - `BlockTransaction`: allows creating batch commits of block changes with validation conditions - if any block can't be applied, the whole transaction fails to apply.
+ - `ChunkListenerNoOpTrait`: contains default no-op stubs for chunk listener implementations
+ - `ChunkListener`: interface allowing subscribing to events happening on a given chunk
+ - `TickingChunkLoader`: a `ChunkLoader` specialization that allows ticking chunks
+- `ChunkLoader` no longer requires implementing `getX()` and `getZ()`.
+- `ChunkLoader` no longer causes chunks to get random updates. If this behaviour is needed, implement `TickingChunkLoader`.
+- The following classes have been renamed:
+ - `pocketmine\world\utils\SubChunkIteratorManager` -> `pocketmine\world\utils\SubChunkExplorer`
+- The following API methods have been added:
+ - `World->registerChunkListener()`
+ - `World->unregisterChunkListener()`
+ - `World->getBlockAt()` (accepts int x/y/z instead of Vector3, faster for some use cases)
+ - `World->setBlockAt()` (accepts int x/y/z instead of Vector3, faster for some use cases)
+ - `Chunk->isDirty()` (replacement for `Chunk->hasChanged()`)
+ - `Chunk->getDirtyFlag()` (more granular component-based chunk dirty-flagging, used to avoid saving unmodified parts of the chunk)
+ - `Chunk->setDirty()`
+ - `Chunk->setDirtyFlag()`
+- The following API methods have been removed:
+ - `ChunkLoader->getLoaderId()` (now object ID is used)
+ - `ChunkLoader->isLoaderActive()`
+ - `ChunkLoader->getPosition()`
+ - `ChunkLoader->getLevel()`
+ - `Chunk->fastSerialize()` (use `FastChunkSerializer::serialize()` instead)
+ - `Chunk->getBlockData()`
+ - `Chunk->getBlockDataColumn()`
+ - `Chunk->getBlockId()`
+ - `Chunk->getBlockIdColumn()`
+ - `Chunk->getBlockLight()`
+ - `Chunk->getBlockLightColumn()`
+ - `Chunk->getBlockSkyLight()`
+ - `Chunk->getBlockSkyLightColumn()`
+ - `Chunk->getMaxY()`
+ - `Chunk->getSubChunkSendCount()` (this was specialized for protocol usage)
+ - `Chunk->getX()`
+ - `Chunk->getZ()`
+ - `Chunk->hasChanged()` (use `Chunk->isDirty()` or `Chunk->getDirtyFlag()` instead)
+ - `Chunk->isGenerated()`
+ - `Chunk->networkSerialize()` (see `ChunkSerializer` in the `network\mcpe\serializer` package)
+ - `Chunk->populateSkyLight()` (use `SkyLightUpdate->recalculateChunk()` instead)
+ - `Chunk->recalculateHeightMap()` (moved to `SkyLightUpdate`)
+ - `Chunk->recalculateHeightMapColumn()` (moved to `SkyLightUpdate`)
+ - `Chunk->setAllBlockLight()`
+ - `Chunk->setAllBlockSkyLight()`
+ - `Chunk->setBlock()`
+ - `Chunk->setBlockData()`
+ - `Chunk->setBlockId()`
+ - `Chunk->setBlockLight()`
+ - `Chunk->setBlockSkyLight()`
+ - `Chunk->setChanged()` (use `Chunk->setDirty()` or `Chunk->setDirtyFlag()` instead)
+ - `Chunk->setGenerated()`
+ - `Chunk->setX()`
+ - `Chunk->setZ()`
+ - `Chunk::fastDeserialize()` (use `FastChunkSerializer::deserialize()` instead)
+ - `World->isFullBlock()`
+ - `World->getFullBlock()`
+ - `World->getBlockIdAt()`
+ - `World->setBlockIdAt()`
+ - `World->getBlockDataAt()`
+ - `World->setBlockDataAt()`
+ - `World->setBlockLightAt()`
+ - `World->setBlockSkyLightAt()`
+ - `World->getBlockSkyLightAt()` (use `World->getRealBlockSkyLightAt()` or `World->getPotentialBlockSkyLightAt()`, depending on use-case)
+ - `World->getHeightMap()` (misleading name, only actually useful for sky light calculation - you probably want `getHighestBlockAt()` instead)
+ - `World->setHeightMap()` (misleading name, only actually useful for sky light calculation)
+ - `World->getChunkEntities()`
+ - `World->getChunkTiles()`
+ - `World->getTileById()`
+ - `World->checkSpawnProtection()`
+ - `World->updateBlockLight()`
+ - `World->updateSkyLight()`
+ - `World->isFullBlock()` (use `Block->isFullCube()` instead)
+ - `World->sendBlocks()`
+ - `World->sendTime()`
+ - `World->addGlobalPacket()`
+ - `World->broadcastGlobalPacket()`
+ - `World->addChunkPacket()`
+ - `World->broadcastLevelSoundEvent()`
+ - `World->broadcastLevelEvent()`
+ - `World->getTickRate()`
+ - `World->setTickRate()`
+ - `World::generateChunkLoaderId()`
+- The following API methods have changed signatures:
+ - `World->addParticle()` now has the signature `addParticle(Vector3 $pos, Particle $particle, ?Player[] $players = null) : void`
+ - `World->addSound()` now has the signature `addSound(?Vector3 $pos, Sound $sound, ?Player[] $players = null) : void`
+ - `World->getRandomTickedBlocks()` now returns `bool[]` instead of `SplFixedArray`.
+ - `World->addRandomTickedBlock()` now accepts `Block` instead of `int, int`.
+ - `World->removeRandomTickedBlock()` now accepts `Block` instead of `int, int`.
+ - `World->setBlock()` has had the `$direct` parameter removed.
+ - `World->loadChunk()` now returns `?Chunk`, and the `$create` parameter has been removed.
+ - `World->getChunk()` no longer accepts a `$create` parameter.
+ - `World->updateAllLight()` now accepts `int, int, int` instead of `Vector3`.
+ - `ChunkManager->setChunk()` (and its notable implementations in `World` and `SimpleChunkManager`) no longer accepts NULL for the `$chunk` parameter.
+ - `Chunk->__construct()` now has the signature `array $subChunks, ?list $entities, ?list $tiles, ?BiomeArray $biomeArray, ?HeightArray $heightArray`.
+ - `Chunk->getSubChunk()` now returns `SubChunk` instead of `SubChunkInterface|null` (and throws `InvalidArgumentException` on out-of-bounds coordinates).
+ - `Chunk->setSubChunk()` no longer accepts `SubChunkInterface`, and the `$allowEmpty` parameter has been removed.
+ - `WorldManager->generateWorld()` (previously `Server->generateWorld()`) now accepts `WorldCreationOptions` instead of `int $seed, class-string $generator, mixed[] $options`.
+- The following API methods have been renamed / moved:
+ - `Level->getChunks()` -> `World->getLoadedChunks()`
+ - `Level->getCollisionCubes()` -> `World->getCollisionBoxes()`
+ - `World->getName()` -> `World->getDisplayName()`
+ - `World->populateChunk()` has been split into `World->requestChunkPopulation()` and `World->orderChunkPopulation()`.
+- The following API methods have changed behaviour:
+ - `World->getChunk()` no longer tries to load chunks from disk. If the chunk is not already in memory, null is returned. (This behaviour now properly matches other `ChunkManager` implementations.)
+ - `World->getHighestBlockAt()` now returns `null` instead of `-1` if the target X/Z column contains no blocks.
+ - The following methods now throw `WorldException` when targeting ungenerated terrain:
+ - `World->getSafeSpawn()` (previously it just silently returned the input position)
+ - `World->getHighestBlockAt()` (previously it returned -1)
+ - `World->loadChunk()` no longer creates an empty chunk when the target chunk doesn't exist on disk.
+ - `World->setChunk()` now fires `ChunkLoadEvent` and `ChunkListener->onChunkLoaded()` when replacing a chunk that didn't previously exist.
+ - `World->useBreakOn()` now returns `false` when the target position is in an ungenerated or unloaded chunk (or chunk locked for generation).
+ - `World->useItemOn()` now returns `false` when the target position is in an ungenerated or unloaded chunk (or chunk locked for generation).
+- A `ChunkListener` interface has been extracted from `ChunkLoader`. The following methods have been moved:
+ - `ChunkLoader->onBlockChanged()` -> `ChunkListener->onBlockChanged()`
+ - `ChunkLoader->onChunkChanged()` -> `ChunkListener->onChunkChanged()`
+ - `ChunkLoader->onChunkLoaded()` -> `ChunkListener->onChunkLoaded()`
+ - `ChunkLoader->onChunkPopulated()` -> `ChunkListener->onChunkPopulated()`
+ - `ChunkLoader->onChunkUnloaded()` -> `ChunkListener->onChunkUnloaded()`
+- `Location` has been moved to `pocketmine\entity\Location`.
+
+#### Particles
+- `DestroyBlockParticle` has been renamed to `BlockBreakParticle` for consistency.
+- `DustParticle->__construct()` now accepts a `pocketmine\color\Color` object instead of `r, g, b, a`.
+- `pocketmine\world\particle\Particle` no longer extends `pocketmine\math\Vector3`, and has been converted to an interface.
+- Added the following `Particle` classes:
+ - `DragonEggTeleportParticle`
+ - `PunchBlockParticle`
+
+#### Sounds
+- `pocketmine\world\sound\Sound` no longer extends `pocketmine\math\Vector3`, and has been converted to an interface.
+- `Sound->encode()` now accepts `?\pocketmine\math\Vector3`. `NULL` may be passed for sounds which are global.
+- Added the following classes:
+ - `ArrowHitSound`
+ - `BlockBreakSound`
+ - `BlockPlaceSound`
+ - `BowShootSound`
+ - `BucketEmptyLavaSound`
+ - `BucketEmptyWaterSound`
+ - `BucketFillLavaSound`
+ - `BucketFillWaterSound`
+ - `ChestCloseSound`
+ - `ChestOpenSound`
+ - `EnderChestCloseSound`
+ - `EnderChestOpenSound`
+ - `ExplodeSound`
+ - `FlintSteelSound`
+ - `ItemBreakSound`
+ - `NoteInstrument`
+ - `NoteSound`
+ - `PaintingPlaceSound`
+ - `PotionSplashSound`
+ - `RedstonePowerOffSound`
+ - `RedstonePowerOnSound`
+ - `ThrowSound`
+ - `XpCollectSound`
+ - `XpLevelUpSound`
+
+### Utils
+- The `Color` class was removed. It's now found as `pocketmine\color\Color` in the [`pocketmine/color`](https://github.com/pmmp/Color) package.
+- The `UUID` class was removed. [`ramsey/uuid`](https://github.com/ramsey/uuid) version 4.1 is now used instead.
+ - `UUID::fromData()` can be replaced by `Ramsey\Uuid\Uuid::uuid3()`
+ - `UUID::fromRandom()` can be replaced by `Ramsey\Uuid\Uuid::uuid4()`
+ - `UUID::fromBinary()` can be replaced by `Ramsey\Uuid\Uuid::fromBytes()` (use `Ramsey\Uuid\Uuid::isValid()` to check validity)
+ - `UUID::toBinary()` is replaced by `Ramsey\Uuid\UuidInterface::getBytes()`
+ - See the documentation at https://uuid.ramsey.dev/en/latest/introduction.html for more information.
+- `Terminal::hasFormattingCodes()` no longer auto-detects the availability of formatting codes. Instead it's necessary to use `Terminal::init()` with no parameters to initialize, or `true` or `false` to override.
+- `Config->save()` no longer catches exceptions thrown during emitting to disk.
+- The following new classes have been added:
+ - `InternetException`
+ - `Internet`
+ - `Process`
+- The following API methods have been added:
+ - `Config->getPath()`: returns the path to the config on disk
+ - `Terminal::write()`: emits a Minecraft-formatted text line without newline
+ - `Terminal::writeLine()`: emits a Minecraft-formatted text line with newline
+ - `Utils::recursiveUnlink()`: recursively deletes a directory and its contents
+- The following API class constants have been added:
+ - `TextFormat::COLORS`: lists all known color codes
+ - `TextFormat::FORMATS`: lists all known formatting codes (e.g. italic, bold). (`RESET` is not included because it _removes_ formats, rather than adding them.)
+- The following deprecated API redirects have been removed:
+ - `Utils::execute()`: moved to `Process`
+ - `Utils::getIP()`: moved to `Internet`
+ - `Utils::getMemoryUsage()`: moved to `Process`
+ - `Utils::getRealMemoryUsage()`: moved to `Process`
+ - `Utils::getThreadCount()`: moved to `Process`
+ - `Utils::getURL()`: moved to `Internet`
+ - `Utils::kill()`: moved to `Process`
+ - `Utils::postURL()`: moved to `Internet`
+ - `Utils::simpleCurl()`: moved to `Internet`
+- The following API fields have been removed / hidden:
+ - `Utils::$ip`
+ - `Utils::$online`
+ - `Utils::$os`
+- The following API methods have signature changes:
+ - `Internet::simpleCurl()` now requires a `Closure` for its `onSuccess` parameter instead of `callable`.
+- The following API methods have been removed:
+ - `TextFormat::toJSON()`
+ - `Utils::getCallableIdentifier()`
+
+## Gameplay
+### Blocks
+- Implemented the following blocks:
+ - bamboo
+ - bamboo sapling
+ - barrel
+ - barrier
+ - blast furnace
+ - blue ice
+ - carved pumpkin
+ - coral block
+ - daylight sensor
+ - dried kelp
+ - elements (from Minecraft: Education Edition)
+ - hard (stained and unstained) glass (from Minecraft: Education Edition)
+ - hard (stained and unstained) glass pane (from Minecraft: Education Edition)
+ - jukebox
+ - note block
+ - red, green, blue and purple torches (from Minecraft: Education Edition)
+ - sea pickle
+ - slime
+ - smoker
+ - underwater torches (from Minecraft: Education Edition)
+ - additional wood variants of the following:
+ - buttons
+ - pressure plates
+ - signs
+ - trapdoors
+ - stairs of the following materials:
+ - andesite (smooth and natural)
+ - diorite (smooth and natural)
+ - end stone
+ - end stone brick
+ - granite (smooth and natural)
+ - mossy cobblestone
+ - prismarine (natural, dark and bricks)
+ - red nether brick
+ - red sandstone (and variants)
+ - stone-like slabs of many variants
+- Non-player entities now bounce when falling on beds.
+- Players and mobs now receive reduced fall damage when falling on beds.
+
+### Items
+- Implemented the following items:
+ - records
+ - compounds (from Minecraft: Education Edition)
+ - black, brown, blue and white dyes
+
+### Inventory
+- Implemented offhand inventory.
+- Block-picking is now supported in survival mode.
+- Block picking behaviour now matches vanilla (no longer overwrites held item, jumps to existing item where possible).
+- Armor can now be equipped by right-clicking while holding it.
+
+# 4.0.0-BETA2
+Released 10th September 2021.
+
+## General
+- ext-chunkutils 0.3.x is now required.
+- Reduced memory usage of light storage after garbage collection.
+- Reduced memory usage of uniform subchunks which only contain 1 type of block.
+- The title bar no longer displays heap memory usage numbers (nobody seemed to know that was what it was, anyway).
+- `/status` no longer displays heap memory usage numbers.
+- `start.sh` no longer specifically mentions PHP 7 when erroring because of a missing PHP binary.
+- Debug messages are now logged when reach distance checks prevent players from doing something.
+
+## Fixes
+- Fixed players getting disconnected when using `/tp` to ungenerated chunks (or when teleported by players).
+- Fixed a potential memory leak in block updating when chunks are unloaded.
+- Fixed `Living->lookAt()` not taking eye height into account.
+- Fixed grass replacing itself when placing grass (and consuming inventory items).
+- Fixed players taking fall damage when falling next to a wall.
+
+## API changes
+- The following API methods have been added:
+ - `World->getChunkEntities()`
+ - `World->notifyNeighbourBlockUpdate()`
+- The following API methods have been removed:
+ - `Chunk->getEntities()`
+ - `Chunk->getSavableEntities()`
+ - `Chunk->addEntity()`
+ - `Chunk->removeEntity()`
+- The following classes have been added:
+ - `pocketmine\inventory\transaction\TransactionBuilderInventory`: facilitates building `InventoryTransaction`s using standard `Inventory` API methods
+- The following class constants have been added:
+ - `Chunk::EDGE_LENGTH`
+ - `Chunk::COORD_BIT_SIZE`
+ - `Chunk::COORD_MASK`
+ - `SubChunk::EDGE_LENGTH`
+ - `SubChunk::COORD_BIT_SIZE`
+ - `SubChunk::COORD_MASK`
+
+# 4.0.0-BETA3
+
+
+## General
+- Added support for Minecraft: Bedrock Edition 1.17.30.
+- Dropped support for Minecraft: Bedrock Edition 1.17.1x.
+- `tools/convert-world.php` now writes errors to stderr and sets the proper exit code.
+- Explosions now use the standard mechanism for processing block updates. Previously, it used a special mechanism due to prohibitively poor efficiency of the standard algorithm. Since these inefficiencies have now been addressed, explosions can now be consistent with everything else, with minimal performance impact.
+- Command usage strings are no longer automatically translated (use `Translatable` instead of bare string keys).
+- Command description strings are no longer automatically translated (use `Translatable` instead of bare string keys).
+
+## Fixes
+- `ItemFactory->isRegistered()` no longer crashes when given negative item IDs.
+- Furnaces now continue to operate after reloading the chunk they were contained in.
+- Fixed being unable to reconnect for 10 seconds after disconnecting in some cases.
+
+## API changes
+- The following API methods have been added:
+ - `Liquid->getMinAdjacentSourcesToFormSource()`: returns how many adjacent source blocks of the same liquid must be present in order for the current block to become a source itself
+ - `Player->getPlayerInfo()`
+- `Liquid` minimum-cost flow calculation code has been extracted to `MinimumCostFlowCalculator`.
+
+# 4.0.0-BETA4
+Released 6th October 2021.
+
+## General
+- Improved performance of lighting calculation by avoiding copies of useless data from the main thread.
+- Resource pack loading now accepts `dependencies` and `capabilities` fields in the `manifest.json` (although it doesn't currently check them).
+- `/help` is now localized according to language set in `server.properties`.
+- Various messages related to world loading, generation and conversion are now localized according to the language set in `server.properties`.
+- Compasses now point to the correct (current) world's spawn point after teleporting players to a different world. Previously, they would continue to point to the spawn of the world that the player initially spawned in.
+- The `--bootstrap` CLI option has been removed.
+- RakLib 0.14.2 is now required. This fixes the following issues:
+ - Fixed incorrect handling of sessions on client disconnect (leading to timeout debug messages).
+ - Fixed transferring players often not working correctly.
+ - Fixed disconnect screens sometimes not displaying.
+
+## Fixes
+- Fixed server crash when UPnP encounters an error.
+- Fixed server crash when clients sent items with bad NBT in inventory transactions.
+- Fixed server crash when loading a plugin with legacy nested permission structure (now the plugin will fail to load, but the server won't crash).
+- Fixed server crash when using `/give` with bad NBT on any item.
+- Fixed server crash when loading a plugin with improperly formatted API version (now the plugin will fail to load, but the server won't crash).
+- Fixed server crash when changing player gamemode during `PlayerLoginEvent`.
+- Incorrect structure of `commands` in plugin manifest is now detected earlier and handled more gracefully.
+- Fixed console reader subprocess lingering on server crash on Windows and screwing up the terminal.
+- Fixed `Player` object memory leak when kicking players during `PlayerLoginEvent` (this caused the `World` to report warnings about leaked entities on shutdown).
+- Fixed `Projectile->move()` ignoring the `dx`/`dy`/`dz` parameters given and using its own `motion` instead.
+- Fixed `BlockFactory->get()` erroneously accepting meta values of `16`.
+- Fixed `Block->isSameState()` false negatives for some types of slabs.
+- Fixed being unable to place slabs of the same type on top of each other to create double slabs.
+
+## API changes
+- Plugin commands in `plugin.yml` must now declare `permission` for each command. Previously, the `permission` key was optional, causing anyone to be able to use the command.
+ - This behaviour was removed because of the potential for security issues - a typo in `plugin.yml` could lead to dangerous functionality being exposed to everyone.
+ - If you want to make a command that everyone can use, declare a permission with a `default` of `true` and assign it to the command.
+- Plugin permissions in `plugin.yml` must now declare `default` for each permission. Previously, the `default` key was optional, causing the permission to silently be denied to everyone (PM4) or granted to ops implicitly (PM3).
+
+### Block
+- Added the following classes:
+ - `pocketmine\block\utils\LeverFacing`
+- Added the following API methods:
+ - `pocketmine\block\Lever->isActivated()`
+ - `pocketmine\block\Lever->setActivated()`
+ - `pocketmine\block\Lever->getFacing()`
+ - `pocketmine\block\Lever->setFacing()`
+
+### World
+- The following API methods have signature changes:
+ - `Chunk->getSubChunks()` now returns `array` instead of `SplFixedArray`.
+- The following API methods have been removed:
+ - `FastChunkSerializer::serialize()`
+ - `FastChunkSerializer::deserialize()`
+- The following API methods have been added:
+ - `FastChunkSerializer::serializeTerrain()`: serializes blocks and biomes only
+ - `FastChunkSerializer::deserializeTerrain()`: deserializes the output of `serializeTerrain()`
+
+### Utils
+- The following API methods have signature changes:
+ - `Process::kill()` now requires an additional `bool $subprocesses` parameter.
+
+# 4.0.0-BETA5
+Released 12th October 2021.
+
+## General
+- Exception log format has been changed. Now, exception info is logged in one big block message. This saves space on the console and improves readability, as well as reducing the ability for bad `ThreadedLoggerAttachment`s to break exception output.
+- Log messages are now pushed to `server.log` before calling logger attachment, instead of after. This fixes messages not being written to disk when an error occurs in a logger attachment.
+- Improved startup performance when loading many plugins.
+- The `worlds` config in `pocketmine.yml` no longer supports attaching the generator settings to the `generator` key (use the `preset` key instead).
+- Using an unknown generator in `server.properties` or `pocketmine.yml` will now cause a failure to generate the world.
+- Using invalid/incorrect world generator options (presets) in `server.properties` or `pocketmine.yml` will now cause a failure to generate the world.
+- Generator options of existing worlds are now validated before loading them. If they are invalid, the server will fail to load them.
+- Several more log messages have been localized, including plugin loading errors.
+
+## Fixes
+- Fixed server crash when using `/give` to give an item by ID which doesn't exist in Minecraft.
+- Fixed server crash when boolean `server.properties` options were given an integer value (e.g. `0` or `1` instead of `false` or `true`).
+- Fixed stats reporting checking for a nonexistent `pocketmine.yml` setting.
+- Fixed use of commands without the proper permission sending a message `commands.generic.permission` instead of the proper message.
+- Fixed entities set on fire appearing to stay on fire, although not taking any damage.
+- Fixed a duplicate `MB` suffix on the `Memory freed` output of `/gc`.
+- Fixed the server attempting to generate a world if it failed to load.
+
+## API
+### Block
+- The following API methods have been renamed:
+ - `Block->getPositionOffset()` -> `Block->getModelPositionOffset()`.
+
+### Event
+- `@handleCancelled` PhpDoc annotation can no longer be used on event handlers for non-cancellable events.
+- The following API methods have been added:
+ - `StructureGrowEvent->getPlayer()`
+
+### Inventory
+- The following API methods have been added:
+ - `Inventory->getAddableItemQuantity()`
+
+### Scheduler
+- `ClosureTask` now permits closures without an explicit return type (useful for arrow functions).
+
+### Utils
+- The following API methods have been added:
+ - `Config::parseProperties()`
+ - `Config::writeProperties()`
+ - `Config::parseList()`
+ - `Config::writeList()`
+
+### World
+- The following API methods have signature changes:
+ - `GeneratorManager->registerGenerator()` now requires a `\Closure $presetValidator` parameter. This is used to check generator options of worlds and configs before attempting to use them.
+
+# 4.0.0-BETA6
+Released 19th October 2021.
+
+## General
+- Added support for Minecraft: Bedrock Edition 1.17.40.
+- Removed support for earlier versions.
+- CTRL+C signal handling has been restored, and is now supported on Windows. Pressing CTRL+C while the server is running will behave as if the `/stop` command was invoked.
+- Added a script `tools/generate-permission-doc.php` to generate a Markdown file with a list of all permissions and their relationships. In the future, this will be used to maintain the official documentation, but plugin developers might find it useful for their own purposes too.
+- [`respect/validation`](https://packagist.org/packages/respect/validation) is no longer a core dependency.
+
+## Fixes
+- Fixed server crash when using `/give` to give an item with a too-large item ID, or `/clear` to clear an item that does not exist.
+ - Now, `LegacyStringToItemParser` is used exclusively, and numeric IDs are no longer parsed.
+
+## Gameplay
+- Picking up some items from a dropped stack of items is now supported. This fixes various bugs with being unable to pick up items with an almost-full inventory.
+
+# 4.0.0-BETA7
+Released 28th October 2021.
+
+## General
+- Phar plugins are now able to depend on folder plugins loaded by DevTools.
+- Now uses [`pocketmine/bedrock-protocol@58c53a259e819a076bf8fe875d2a012da7d19d65`](https://github.com/pmmp/BedrockProtocol/tree/58c53a259e819a076bf8fe875d2a012da7d19d65). This version features significant changes, including:
+ - Standardisation of various field names (e.g. `eid` -> `actorRuntimeId`, `evid` -> `eventId`)
+ - Rename of `entity` related fields to `actor` where appropriate (e.g. `entityRuntimeId` -> `actorRuntimeId`)
+ - Block position `x`/`y`/`z` fields replaced by `BlockPosition`
+ - Static `::create()` functions for all packets, which ensure that fields can't be forgotten
+
+## Fixes
+- Fixed server crash when clients send itemstacks with unmappable dynamic item IDs.
+- Fixed server crash on invalid ItemStackRequest action types.
+- Fixed autosave bug that caused unmodified chunks to be saved at least once (during the first autosave after they were loaded).
+- Fixed `ConsoleReaderThread` returning strings with newlines still on the end.
+- Fixed changes made to adjacent chunks in `ChunkPopulateEvent` (e.g. setting blocks) sometimes getting overwritten.
+
+## API
+### Event
+- `PlayerCreationEvent` now verifies that the player class set is instantiable - this ensures that plugins get properly blamed for breaking things.
+
+### World
+- `World->generateChunkCallback()` has been specialized for use by `PopulationTask`. This allows fixing various inconsistencies involving `ChunkPopulateEvent` (e.g. modifications to adjacent chunks in `ChunkPopulationEvent` might be wiped out, if the population of the target chunk modified the adjacent chunk).
+ - It now accepts `Chunk $centerChunk, array $adjacentChunks` (instead of `?Chunk $chunk`).
+ - It's no longer expected to be used by plugins - plugins should be using `World->setChunk()` anyway.
+- `Chunk->getModificationCounter()` has been added. This is a number starting from `0` when the `Chunk` object is first created (unless overridden by the constructor). It's incremented every time blocks or biomes are changed in the chunk. It resets after the chunk is unloaded and reloaded.
+- The following API methods have changed signatures:
+ - `Sound->encode()` no longer accepts `null` for the position.
+ - `Chunk->__construct()`: removed `HeightArray $heightMap` parameter, added `bool $terrainPopulated` and `int $modificationCounter` parameters.
+
+### Plugin
+- `PluginManager->loadPlugins()` now accepts paths to files as well as directories, in which case it will load only the plugin found in the target file.
+- The following API methods have been removed:
+ - `PluginManager->loadPlugin()`: use `PluginManager->loadPlugins()` instead
+
+# 4.0.0-BETA8
+Released 29th October 2021.
+
+## General
+- Chunk packet caches are now cleared by the memory manager on low memory.
+- `Entity->spawnTo()` now has an additional sanity check for matching worlds (might expose a few new errors in plugins).
+- [`pocketmine/math` 0.4.0](https://github.com/pmmp/Math/releases/tag/0.4.0) is now used. Please see its release notes for changes.
+
+## Fixes
+- Zlib raw check for LevelDB is now done directly on startup, avoiding crashes when later trying to load worlds.
+- Fixed tiles and entities getting deleted from adjacent chunks during chunk population.
+- Fixed players being unable to open their inventories more than once.
+- Fixed entities not getting updated when a nearby chunk is replaced (e.g. dropped items would float in midair if the ground was lower than before)
+
+## API
+### World
+- `World::setChunk()` has the following changes:
+ - `$deleteEntitiesAndTiles` parameter has been removed.
+ - Entities are no longer deleted on chunk replacement.
+ - Tiles are no longer deleted on chunk replacement, unless one of the following conditions is met:
+ - the target block in the new chunk doesn't expect a tile
+ - the target block in the new chunk expects a different type of tile (responsibility of the plugin developer to create the new tile)
+ - there's already a tile in the target chunk which conflicts with the old one
+- `Location::__construct()` has the following changes:
+ - `world` parameter is now 4th instead of last.
+ - All parameters are now mandatory.
+- Reverted addition of chunk modification counters in previous beta.
+- `Chunk::DIRTY_FLAG_TERRAIN` has been renamed to `Chunk::DIRTY_FLAG_BLOCKS`.
+
+# 4.0.0-BETA9
+Released 2nd November 2021.
+
+## General
+- Now analysed using level 9 on PHPStan 1.0.0.
+- `ext-pthreads` v4.0.0 or newer is now required.
+- `resources/vanilla` submodule has been removed. BedrockData is now included via Composer dependency [`pocketmine/bedrock-data`](https://packagist.org/packages/pocketmine/bedrock-data).
+- `pocketmine/spl` Composer dependency has been dropped.
+- The following Composer dependency versions are now required:
+ - [`pocketmine/bedrock-protocol` v5.0.0](https://github.com/pmmp/BedrockProtocol/tree/5.0.0+bedrock-1.17.40) features substantial changes to its API compared to 3.0.1, which was used in 4.0.0-BETA8. Please see its [release notes](https://github.com/pmmp/BedrockData/releases/tag/5.0.0+bedrock-1.17.40).
+ - [`pocketmine/log` v0.4.0](https://github.com/pmmp/Log/tree/0.4.0) removes the `LoggerAttachment` interface and replaces logger attachment objects with closures.
+ - [`pocketmine/log-pthreads` v0.4.0](https://github.com/pmmp/LogPthreads/tree/0.4.0)
+ - [`pocketmine/classloader` v0.2.0](https://github.com/pmmp/ClassLoader/tree/0.2.0)
+- A noisy debug message in `World->updateAllLight()` has been removed.
+
+## API
+### Entity
+- `Human->setLifetimeTotalXp()` now limits the maximum value to 2^31.
+
+### Event
+- `BlockGrowEvent` is now called when cocoa pods grow.
+
+### Item
+- Added `Releasable->canStartUsingItem()`.
+
+### Network
+- Added `NetworkInterfaceStartException`, which may be thrown by `Network->registerInterface()` and `NetworkInterface->start()`.
+
+### Player
+- `SurvivalBlockBreakHandler::createIfNecessary()` has been removed.
+- `SurvivalBlockBreakHandler->__construct()` is now public.
+- `UsedChunkStatus::REQUESTED()` has been renamed to `REQUESTED_SENDING`.
+- `UsedChunkStatus::REQUESTED_GENERATION()` has been added.
+
+### Utils
+- `Promise` API has changed:
+ - Promise-related classes have been moved to `pocketmine\promise` namespace.
+ - It's now split into `Promise` and `PromiseResolver`.
+ - `PromiseResolver` provides only `resolve()` and `reject()`. It should be used by callbacks to resolve a promise.
+ - `Promise` now provides only `onCompletion()` and `isResolved()` APIs. This should be given to consumers to allow them to handle the result of the async operation.
+ - `PromiseResolver` must not be created directly. Use `new PromiseResolver` and `PromiseResolver->getPromise()`.
+
+### World
+- Improved performance of `setBlock()` by around 35% when the `$update` parameter is set to `true`.
+- Improved performance of `setBlock()` by around 30% when the `$update` parameter is set to `false`.
+- `World->generateChunkCallback()` is no longer exposed to public API.
+- `World->getAdjacentChunks()` now returns an array indexed using `World::chunkHash()`, where the `x` and `z` components are the relative offsets from the target chunk (range -1 to +1).
+- `World->lockChunk()` now requires `ChunkLockId $lockId` parameter.
+- `World->unlockChunk()` now requires a `?ChunkLockId $lockId` parameter. If a non-null lockID is given, the lock on the chunk will only be removed if it matches the given lockID.
+- `World->unlockChunk()` now returns `bool` instead of `void` (to signal whether unlocking succeded or not).
+- Added `ChunkLockId` class.
+
+## Fixes
+### World
+- Fixed server crash when tiles with colliding positions are loaded from saved data. Now, an error is logged, but the server won't crash.
+- Fixed server crash when liquids and other items flow into terrain locked for population. Now, an advisory locking mechanism is used, and population results will be discarded and recalculated if modifications are detected.
+- Fixed various crashes that could occur if a chunk was flagged with `setPopulated(true)` after a promise had already been created for its population.
+- Fixed `AssumptionFailedError` in `PopulationTask` when workers previously used for generation are shutdown, and then restarted on the fly by a generation request.
+- Fixed assertion failure in `World->drainPopulationRequestQueue()` when requesting, cancelling and then re-requesting generation of a chunk while the generator was busy.
+- Fixed generation potentially getting stuck if a population request was cancelled while the population task was running (failure to remove locks from used chunks).
+- Fixed `World->requestChunkPopulation()` not taking into account that the target chunk may already be populated. This caused a variety of strange bugs and performance issues.
+- Fixed potential memory leak caused by `World->unregisterChunkListenerFromAll()` not taking players into account.
+- Fixed debug spam of `chunk has no loaders registered` messages during chunk generation.
+
+### Other fixes
+- Fixed server crash when unable to bind to the desired port. Now, the server will show an error and gracefully stop without a crashdump instead.
+- Fixed server crash in `Player->showPlayer()` when the target is not in the same world.
+- Fixed players, who died in hardcore mode and were unbanned, getting re-banned on next server join.
+- Fixed cake block desync when attempting to eat in creative (eating in creative is not yet supported, but the block rollback was missing).
+- Fixed players being able to eat items more quickly by dropping them while eating.
+- Fixed arrows getting added to creative players' inventories when picked up.
+- Fixed players re-requesting the same ungenerated chunks multiple times before they were sent.
+- Fixed commands not working in some cases after using some control sequences on the console.
+
+# 4.0.0-BETA10
+Released 2nd November 2021.
+
+## Fixes
+- Fixed an issue with BedrockData JSON minification which broke the release build of 4.0.0-BETA9.
+
+# 4.0.0-BETA11
+Released 6th November 2021.
+
+## General
+- `resources/locale` submodule has been removed. Language files are now included via Composer dependency [`pocketmine/locale-data`](https://packagist.org/packages/pocketmine/locale-data).
+ - This means it's now possible to run a server from git sources without cloning submodules :)
+ - All remaining submodules (DevTools, build/php) are non-essential for building and running a server.
+- Added a tool `tools/simulate-chunk-sending.php` to visualise the behaviour of `ChunkSelector`.
+
+## Fixes
+- Fixed server crash on saving when player XP has reached int32 max (XP is now capped, similar to Java Edition).
+- Fixed another edge case in chunk generation that led to assertion failures.
+- Fixed server crash when finding a list of `TAG_Float` for entity positions instead of `TAG_Double`.
+- Fixed fast eating when picking up items.
+- Fixed terrain being invisible for a long time when teleporting into ungenerated terrain.
+- Fixed weird chunk loading when teleporting into ungenerated terrain (sometimes farther chunks would render before closer ones, leaving holes in the map temporarily).
+- Fixed players re-requesting chunks when turning their heads or jumping.
+- Fixed bonemeal sometimes being consumed even when cancelling `BlockGrowEvent` and `StructureGrowEvent`.
+
+## API
+### Event
+- Added `PlayerEmoteEvent`.
+
+### Gameplay
+- Chunks are now sent in proper circles. This improves the experience when flying quickly parallel to X or Z axis, since now more chunks in front of the player will load sooner.
+- Added support for emotes.
+
+# 4.0.0-BETA12
+Released 9th November 2021.
+
+## General
+- Introduced support for connecting via IPv6.
+ - PHP binary used must now always be built with IPv6 support, even if IPv6 is disabled. This is because RakNet may still send link-local IPv6 loopback addresses in connection packets even when only using IPv4.
+ - The default port for IPv6 is `19133` (similar to Bedrock Dedicated Server).
+ - Port `19133` is used by default so that Minecraft Bedrock can detect IPv6 servers on LAN.
+ - GS4 Query is supported on both IPv4 and IPv6 according to `server.properties` settings.
+ - The following `server.properties` settings are influential:
+ - `enable-ipv6`: `on` by default. Disabling this completely disables IPv6 support.
+ - `server-ipv6`: `::` by default (equivalent to "any IP", like `0.0.0.0` for IPv4). Most users shouldn't need to change this setting, and it doesn't appear in `server.properties` by default.
+ - `server-portv6`: `19133` by default. You may run both IPv4 and IPv6 on the same port.
+- Various internal changes have been made to prepare for negative Y axis support (upcoming 1.18).
+
+## Fixes
+- Fixed resource packs not applying.
+- Fixed inventory windows being unopenable after dying with inventory windows open.
+- Fixed plugins being able to alter other plugins' permission defaults by redeclaring them in the `plugin.yml`.
+
+## API
+### Block
+- `VanillaBlocks::fromString()` has been removed.
+- Added `CraftingTableInventory`. This exclusively represents a crafting table's 3x3 crafting grid.
+
+### Crafting
+- `CraftingGrid` is now abstract.
+- Removed `CraftingGrid->getHolder()`.
+- The constructor of `CraftingGrid` no longer accepts a `Player` parameter.
+
+### Entity
+#### Effect
+- `Effect->__construct()` once again accepts an `int $defaultDuration` parameter.
+- Removed `VanillaEffects::fromString()`.
+- Added `StringToEffectParser`
+ - Supports custom aliases!
+ - This is used by `/effect` to provide name support.
+
+### Event
+- `InventoryOpenEvent` is now fired when a player opens a crafting table's UI.
+- `InventoryCloseEvent` is now fired when a player closes a crafting table's UI.
+- `PlayerDropItemEvent` will now prevent the drops from force-closing of the following inventories:
+ - anvil
+ - enchanting table
+ - loom
+
+### Inventory
+- Added `TemporaryInventory`. This should be implemented by any inventory whose contents should be evacuated when closing.
+- Added `PlayerCraftingInventory`. This exclusively represents the player's own 2x2 crafting grid.
+
+### Item
+- Removed `VanillaItems::fromString()`
+ - Obsoleted by the far superior, much more dynamic, and plugin-customizable `StringToItemParser`.
+ - `StringToItemParser` allows mapping strings to closure callbacks, allowing you to create aliases for `/give` for any item, including custom ones.
+
+#### Enchantment
+- Removed `VanillaEnchantments::fromString()`.
+- Added `StringToEnchantmentParser`
+ - Supports custom aliases!
+ - This is used by `/enchant` to provide name support.
+
+### Player
+- Removed `Player->setCraftingGrid()`. To open a 3x3 crafting grid to a player, use `setCurrentWindow(new CraftingTableInventory)`.
+
+### Server
+- Added the following API methods:
+ - `Server->getIpV6()`
+ - `Server->getPortV6()`
+
+# 4.0.0-BETA13
+Released 25th November 2021.
+
+## General
+- Improved error messages when a plugin command name or alias contains an illegal character.
+- Fixed script plugins reading tags from non-docblocks before the actual docblock.
+- Added a sanity check to `Human->setCurrentTotalXp()` to try and catch an elusive bug that's been appearing in the wild - please get in touch if you know how to reproduce it!
+- Updated `BUILDING.md` to reflect the fact that submodules are no longer required to build or run the server.
+
+## Internals
+- Crashdump rendering has been separated from crashdump data generation. This allows rendering crashdumps from existing JSON data.
+- Direct iteration of arrays with string keys is now disallowed by a custom PHPStan rule. This is because numeric strings are casted to integers when used as array keys, which produces a variety of unexpected behaviour particularly for iteration.
+ - To iterate on arrays with string keys, `Utils::stringifyKeys()` must now be used.
+
+## Fixes
+- Fixed various crashes involving bad entity data saved on disk (e.g. an item entity with invalid item would crash the server).
+- Fixed various crashes involving arrays with numeric string keys.
+- Fixed crash when players try to pickup XP while already having max XP.
+- Fixed ungenerated chunks saved on disk by old versions of PocketMine-MP (before 3.0.0) being treated as corrupted during world conversion (they are now ignored instead).
+- Fixed misleading corruption error message when saved chunks have missing NBT tags.
+
+## Gameplay
+- Fixed `/setworldspawn` setting incorrect positions based on player position when in negative coordinates.
+- `/setworldspawn` now accepts relative coordinates when used as a player.
+- Added some extra aliases for `/give` and `/clear`: `chipped_anvil`, `coarse_dirt`, `damaged_anvil`, `dark_oak_standing_sign`, `jungle_wood_stairs`, `jungle_wooden_stairs` and `oak_standing_sign`.
+ - Some of these were added for quality-of-life, others were added just to be consistent.
+- Fixed explosions dropping incorrect items when destroying blocks with tile data (e.g. banners, beds).
+- Fixed the bounding box of skulls when mounted on a wall.
+- Fixed podzol dropping itself when mined (instead of dirt).
+
+## API
+### Entity
+- `Projectile->move()` is now protected, like its parent.
+
+### Utils
+- `Utils::parseDocComment()` now allows `-` in tag names.
+
+# 4.0.0-BETA14
+Released 30th November 2021.
+
+## General
+- The server will now log an EMERGENCY-level message when `forceShutdown()` is used for any other reason than a graceful shutdown.
+- The server will now attempt to translate invalid blocks to valid equivalents when loading chunks. This fixes many issues with `update!` blocks appearing in worlds, particularly ghost structures (these would appear when world editors previously erased some blocks by setting their IDs but not metadata).
+
+## Fixes
+- Fixed `ConsoleReaderThread` spawning many zombie processes when running a server inside a Docker container.
+
+# 4.0.0-BETA15
+Released 30th November 2021.
+
+## General
+- Added support for Minecraft: Bedrock 1.18.0
+- Removed support for earlier versions.
diff --git a/changelogs/4.0.md b/changelogs/4.0.md
index 644550657..ec6f7fa60 100644
--- a/changelogs/4.0.md
+++ b/changelogs/4.0.md
@@ -1,111 +1,176 @@
-# 4.0.0-BETA1
-Released 7th September 2021.
+**For Minecraft: Bedrock Edition 1.18.0**
+
+# 4.0.0
+Released 1st December 2021.
This major version features substantial changes throughout the core, including significant API changes, new world format support, performance improvements and a network revamp.
-Please note that this is a BETA release and is not finalized. While no significant changes are expected between now and release, the API might still be changed.
-
Please also note that this changelog is provided on a best-effort basis, and it's possible some changes might not have been mentioned here.
If you find any omissions, please submit pull requests to add them.
-## WARNING
-This is NOT a stable release. PMMP accepts no responsibility or liability for any damages incurred by using this build.
-It should be used for TESTING purposes only.
-
-## Contents
- * [Core](#core)
- + [General](#general)
- + [Configuration](#configuration)
- + [World handling](#world-handling)
- - [Interface](#interface)
- - [Functional](#functional)
- - [Performance](#performance)
- + [Logger revamp](#logger-revamp)
- + [Network](#network)
- - [Minecraft Bedrock packet encryption](#minecraft-bedrock-packet-encryption)
- - [Packet receive error handling has been overhauled](#packet-receive-error-handling-has-been-overhauled)
- - [New packet handler system](#new-packet-handler-system)
- * [API](#api)
- + [General](#general-1)
- + [Changes to `plugin.yml`](#changes-to-pluginyml)
- - [Permission nesting](#permission-nesting)
- - [`src-namespace-prefix`](#src-namespace-prefix)
- + [Block](#block)
- + [Command](#command)
- + [Entity](#entity)
- - [General](#general-2)
- - [Effect](#effect)
- - [Removal of runtime entity NBT](#removal-of-runtime-entity-nbt)
- - [Entity creation](#entity-creation)
- - [WIP removal of entity network metadata](#wip-removal-of-entity-network-metadata)
- + [Event](#event)
- - [Internal event system no longer depends on `Listener`s](#internal-event-system-no-longer-depends-on-listeners)
- - [Default cancelled handling behaviour has changed](#default-cancelled-handling-behaviour-has-changed)
- - [`PlayerPreLoginEvent` changes](#playerpreloginevent-changes)
- - [Other changes](#other-changes)
- + [Inventory](#inventory)
- + [Item](#item)
- - [General](#general-3)
- - [NBT handling](#nbt-handling)
- - [Enchantment](#enchantment)
- + [Lang](#lang)
- + [Network](#network-1)
- + [Permission](#permission)
- + [Player](#player)
- + [Plugin](#plugin)
- + [Scheduler](#scheduler)
- - [Thread-local storage for AsyncTasks](#thread-local-storage-for-asynctasks)
- - [Other changes](#other-changes-1)
- + [Server](#server)
- + [Level / World](#level--world)
- - [General](#general-4)
- - [Particles](#particles)
- - [Sounds](#sounds)
- + [Utils](#utils)
- * [Gameplay](#gameplay)
- + [Blocks](#blocks)
- + [Items](#items)
+* [Core](#core)
+ + [General](#general)
+ + [Dependency changes](#dependency-changes)
+ + [Performance](#performance)
+ + [Tools](#tools)
+ + [Commands](#commands)
+ + [Configuration](#configuration)
+ + [World handling](#world-handling)
+ - [Interface](#interface)
+ - [Functional](#functional)
+ - [Performance](#performance-1)
+ + [Logging](#logging)
+ + [Network](#network)
+ - [Performance](#performance-2)
+ - [Minecraft Bedrock packet encryption](#minecraft-bedrock-packet-encryption)
+ - [Error handling](#error-handling)
+ - [New packet handler system](#new-packet-handler-system)
+ + [Plugin loading](#plugin-loading)
+ + [Internals](#internals)
+* [API](#api)
+ + [General](#general-1)
+ + [Changes to `plugin.yml`](#changes-to-pluginyml)
+ - [Permission nesting](#permission-nesting)
+ - [`src-namespace-prefix`](#src-namespace-prefix)
+ + [Other changes](#other-changes)
+ + [Block](#block)
+ + [Command](#command)
+ + [Entity](#entity)
+ - [General](#general-2)
+ - [Effect](#effect)
+ - [Removal of runtime entity NBT](#removal-of-runtime-entity-nbt)
+ - [Entity creation](#entity-creation)
+ - [WIP removal of entity network metadata](#wip-removal-of-entity-network-metadata)
+ + [Event](#event)
+ - [Internal event system no longer depends on `Listener`s](#internal-event-system-no-longer-depends-on-listeners)
+ - [Default cancelled handling behaviour has changed](#default-cancelled-handling-behaviour-has-changed)
+ - [`PlayerPreLoginEvent` changes](#playerpreloginevent-changes)
+ - [Other changes](#other-changes-1)
+ + [Inventory](#inventory)
+ + [Item](#item)
+ - [General](#general-3)
+ - [NBT handling](#nbt-handling)
+ - [Enchantment](#enchantment)
+ + [Lang](#lang)
+ + [Network](#network-1)
+ + [Permission](#permission)
+ + [Player](#player)
+ + [Plugin](#plugin)
+ + [Promise](#promise)
+ + [Scheduler](#scheduler)
+ - [Thread-local storage for AsyncTasks](#thread-local-storage-for-asynctasks)
+ - [Other AsyncTask changes](#other-asynctask-changes)
+ - [Non-AsyncTask changes](#non-asynctask-changes)
+ + [Server](#server)
+ + [Level / World](#level--world)
+ - [General](#general-4)
+ - [Particles](#particles)
+ - [Sounds](#sounds)
+ + [Utils](#utils)
+* [Gameplay](#gameplay)
+ + [World loading](#world-loading)
+ + [Blocks](#blocks)
+ + [Items](#items)
+ + [Inventory](#inventory-1)
+ + [Misc](#misc)
Table of contents generated with markdown-toc
## Core
### General
-- A new "plugin greylist" feature has been introduced, which allows whitelisting or blacklisting plugins from loading.
- Remote console (RCON) has been removed. The [RconServer](https://github.com/pmmp/RconServer) plugin is provided as a substitute.
- Spawn protection has been removed. The [BasicSpawnProtection](https://github.com/pmmp/BasicSpawnProtection) plugin is provided as a substitute.
-- CTRL+C signal handling has been removed. The [PcntlSignalHandler](https://github.com/pmmp/PcntlSignalHandler) plugin is provided as a substitute.
- Player movement anti-cheat has been removed.
- GS4 Query no longer breaks when disabling RakLib.
-- The `pocketmine_chunkutils` PHP extension has been dropped.
-- New PHP extensions are required by this version:
- - [chunkutils2](https://github.com/pmmp/ext-chunkutils2)
- - [morton](https://github.com/pmmp/ext-morton)
-- Many bugs in player respawning have been fixed, including:
- - Spawning underneath bedrock when spawn position referred to ungenerated terrain
- - Spawning underneath bedrock on first server join on very slow machines (or when the machine was under very high load)
- - Spawning inside blocks (or above the ground) when respawning with a custom spawn position set
- - Player spawn positions sticking to the old location when world spawn position changed - this was because the world spawn at time of player creation was used as the player's custom spawn, so the bug will persist for older player data, but will work as expected for new players.
- PreProcessor is removed from builds due to high maintenance cost and low benefit. Its usage is now discouraged.
+- The title bar no longer displays heap memory usage numbers (nobody seemed to know that was what it was, anyway).
+- `start.sh` no longer specifically mentions PHP 7 when erroring because of a missing PHP binary.
+- The `--bootstrap` CLI option has been removed.
+- Introduced support for connecting via IPv6:
+ - PHP binary used must now always be built with IPv6 support, even if IPv6 is disabled. This is because RakNet may still send link-local IPv6 loopback addresses in connection packets even when only using IPv4.
+ - Port `19133` is used for IPv6 by default so that Minecraft Bedrock can detect IPv6 servers on LAN.
+ - GS4 Query is supported on both IPv4 and IPv6 according to `server.properties` settings.
+ - New `server.properties` options `enable-ipv6`, `server-ipv6`, `server-portv6` have been added (see below).
+
+### Dependency changes
+- The `pocketmine_chunkutils` PHP extension has been dropped.
+- IPv6 support in PHP is now mandatory.
+- New PHP extensions are required by this version:
+ - [crypto](https://github.com/bukka/php-crypto)
+ - [chunkutils2](https://github.com/pmmp/ext-chunkutils2)
+ - [gmp](https://www.php.net/manual/en/book.gmp.php)
+ - [igbinary](https://github.com/igbinary/igbinary)
+ - [leveldb](https://github.com/pmmp/php-leveldb) (must be built with [pmmp/leveldb](https://github.com/pmmp/leveldb/tree/mojang-compatible))
+ - [morton](https://github.com/pmmp/ext-morton)
+- The following Composer dependency versions have changed (**PLEASE READ, dependency API changes are not mentioned in this changelog!**):
+ - `pocketmine/bedrock-protocol` has been added at [7.0.0](https://github.com/pmmp/BedrockProtocol/releases/tag/7.0.0+bedrock-1.18.0).
+ - `pocketmine/classloader` has been updated from 0.1.0 to [0.2.0](https://github.com/pmmp/ClassLoader/releases/tag/0.2.0) (**significant API changes, new features**).
+ - `pocketmine/color` has been added at [0.2.0](https://github.com/pmmp/Color/releases/tag/0.2.0).
+ - `pocketmine/errorhandler` has been added at [0.3.0](https://github.com/pmmp/ErrorHandler/releases/tag/0.3.0).
+ - `pocketmine/locale-data` has been added at [2.0.16](https://github.com/pmmp/Language/releases/tag/2.0.16).
+ - `pocketmine/log` has been updated from 0.2.0 to [0.4.0](https://github.com/pmmp/Log/releases/tag/0.4.0) (**minor API changes**, see also [0.3.0](https://github.com/pmmp/Log/releases/tag/0.3.0)).
+ - `pocketmine/nbt` has been updated from 0.2.18 to [0.3.0](https://github.com/pmmp/NBT/releases/tag/0.3.0) (**significant API changes**).
+ - `pocketmine/raklib` has been updated from 0.12.7 to [0.14.2](https://github.com/pmmp/RakLib/releases/tag/0.14.2) (**significant API changes**, see also [0.13.0](https://github.com/pmmp/RakLib/releases/tag/0.13.0)).
+ - `pocketmine/raklib-ipc` has been added at [0.1.0](https://github.com/pmmp/RakLibIpc/releases/tag/0.1.0) (components extracted from RakLib).
+ - `pocketmine/snooze` has been updated from 0.1.0 to [0.3.0](https://github.com/pmmp/Snooze/releases/tag/0.3.0) (**minor API changes**, see also [0.2.0](https://github.com/pmmp/Snooze/releases/tag/0.2.0)).
+ - `pocketmine/spl` has been dropped.
+- Minecraft Bedrock protocol is now developed in a separate repository, [pmmp/BedrockProtocol](https://github.com/pmmp/BedrockProtocol).
+ - It has significant changes compared to PM3. Please read its changelogs.
+- The following submodules have been removed:
+ - `resources/vanilla`: BedrockData is now included via Composer dependency [`pocketmine/bedrock-data`](https://packagist.org/packages/pocketmine/bedrock-data).
+ - `resources/locale`: Language files are now included via Composer dependency [`pocketmine/locale-data`](https://packagist.org/packages/pocketmine/locale-data).
+ - This means it's now possible to run a server from git sources without cloning submodules :)
+ - All remaining submodules (DevTools, build/php) are non-essential for building and running a server.
+
+### Performance
+- `/op`, `/deop`, `/whitelist add` and `/whitelist remove` no longer cause player data to be loaded from disk for no reason.
+- Timings now use high-resolution timers provided by `hrtime()` to collect more accurate performance metrics.
+- Closures are now used for internal event handler calls. This provides a performance improvement of 10-20% over the 3.x system, which had to dynamically resolve callables for every event call.
+- Improved startup performance when loading many plugins.
+- See more in the [Worlds / Performance](#performance-2) and [Network / Performance](#performance-3) sections.
+
+### Tools
+Some new scripts have been added in the `tools/` directory of the repository. These scripts may use the PocketMine-MP core library, but are intended to be run standalone.
+
+ - `convert-world.php`: allows converting a world to a new format without a running server
+ - `compact-regions.php`: repacks files in legacy Region worlds to clean up wasted disk space
+ - `generate-permission-doc.php`: generates a Markdown document of all core permissions (see [example](https://gist.github.com/dktapps/eed6d6a4571f01b676236bf9ff2779b2))
+ - `simulate-chunk-selector.php`: generates a series of images to visualize the behaviour of the chunk sending algorithm; these images can then be stitched into video using a tool such as [ffmpeg](https://ffmpeg.org/)
### Commands
-- The `/reload` command has been removed.
- The `/effect` command no longer supports numeric IDs - it's now required to use names.
- The `/enchant` command no longer supports numeric IDs - it's now required to use names.
+- The `/give` command no longer permits giving items with invalid NBT (e.g. incorrect types). Previously, this was the cause of random server crashes when using items on PM3.
+- The `/give` command now supports many new aliases like in Java, e.g. it's now possible to do `/give someone bonemeal` or `/give someone lapis_lazuli` instead of using legacy id:metadata.
+- The `/help` command is now localized according to language set in `server.properties`.
+- The `/reload` command has been removed.
+- The `/setworldspawn` command now accepts relative coordinates when used as a player.
+- The `/status` command no longer displays heap memory usage numbers.
- Added `/clear` command with functionality equivalent to that of vanilla Minecraft.
- The following commands' outputs are now localized according to the chosen language settings:
- `/gc`
- `/status`
- `/op`
- `/deop`
+- Fixed use of commands without the proper permission sending a message `commands.generic.permission` instead of the proper message.
+- Fixed commands not working in some cases after using some control sequences on the console.
+- Fixed `/setworldspawn` setting incorrect positions based on player position when in negative coordinates.
### Configuration
- World presets can now be provided as a `preset` key in `pocketmine.yml`, instead of putting them in the `generator` key.
+- The `worlds` config no longer supports attaching the generator settings to the `generator` key (use the `preset` key instead).
+- Using an unknown generator in `server.properties` or `pocketmine.yml` will now cause a failure to generate the world.
+- Using invalid/incorrect world generator options (presets) in `server.properties` or `pocketmine.yml` will now cause a failure to generate the world.
- The following new options have been added to `pocketmine.yml`:
- `chunk-ticking.blocks-per-subchunk-per-tick` (default `3`): Increasing this value will increase the rate at which random block updates happen (e.g. grass growth).
- `network.enable-encryption` (default `true`): Controls whether Minecraft network packets are encrypted or not.
- The following options have been removed from `pocketmine.yml`:
- - `chunk-ticking.light-updates`: Since lighting is needed for basic vanilla functionality to work, allowing this to be disabled without disabling chunk ticking made no sense. If you don't want light calculation to occur, you can disable chunk ticking altogether.
+ - `chunk-ticking.light-updates`: Since lighting is needed for basic vanilla functionality to work, allowing this to be disabled without disabling chunk ticking made no sense. If you don't want light calculation to occur, you can disable chunk ticking altogether by setting `chunk-ticking.per-tick` to `0` in `pocketmine.yml`.
- `player.anti-cheat.allow-movement-cheats`
+- The following new options have been added to `server.properties`:
+ - `enable-ipv6`: `on` by default. Disabling this completely disables IPv6 support.
+ - `server-ipv6`: `::` by default (equivalent to "any IP", like `0.0.0.0` for IPv4). Most users shouldn't need to change this setting, and it doesn't appear in `server.properties` by default.
+ - `server-portv6`: `19133` by default. You may run both IPv4 and IPv6 on the same port, but since Bedrock scans on 19133 by default, PM also uses the same.
### World handling
#### Interface
@@ -118,37 +183,52 @@ It should be used for TESTING purposes only.
- `mcregion`
- `anvil`
- `pmanvil`
+- Generator options of existing worlds are now validated before loading them. If they are invalid, the server will fail to load them.
+- Fixed the server attempting to generate a world if it failed to load.
- 256 build-height is now supported in all worlds (facilitated by automatic conversion).
- Extended blocks are now supported (facilitated by automatic conversion).
+- The server will now attempt to translate invalid blocks to valid equivalents when loading chunks. This fixes many issues with `update!` blocks appearing in worlds, particularly ghost structures (these would appear when world editors previously erased some blocks by setting their IDs but not metadata).
- Lighting is no longer stored or loaded from disk - instead, it's calculated on the fly as-needed. This fixes many long-standing bugs.
+- Explosions now use the standard mechanism for processing block updates. Previously, it used a special mechanism due to prohibitively poor efficiency of the standard algorithm. Since these inefficiencies have now been addressed, explosions can now be consistent with everything else, with minimal performance impact.
+- Fixed debug spam of `chunk has no loaders registered` messages during chunk generation.
+- Various cases of corrupted data in chunks will now cause an error to be logged instead of a server crash. This includes tiles with colliding positions and tiles in incorrect, non-loaded chunks.
+- Fixed players re-requesting the same ungenerated chunks multiple times before they were sent.
+- Fixed players re-requesting chunks when turning their heads or jumping.
#### Performance
- `leveldb` is now the primary supported world format. It is inherently faster than region-based formats thanks to better design.
- Partial chunk saves (only saving modified subcomponents of chunks) has been implemented. This drastically reduces the amount of data that is usually necessary to write on chunk save, which in turn **drastically reduces the time to complete world saves**. This is possible thanks to the modular design of the `leveldb` world format - this enhancement is not possible with region-based formats.
- Lighting is no longer guaranteed to be available on every chunk. It's now calculated on the fly as-needed.
-- `/op`, `/deop`, `/whitelist add` and `/whitelist remove` no longer cause player data to be loaded from disk for no reason.
-- Timings now use high-resolution timers provided by `hrtime()` to collect more accurate performance metrics.
- Z-order curves (morton codes) are now used for block and chunk coordinate hashes. This substantially improves performance in many areas by resolving a hashtable key hash collision performance issue. Affected areas include explosions, light calculation, and more.
-- [`libdeflate`](https://github.com/ebiggers/libdeflate) is now (optionally) used for outbound Minecraft packet compression. It's more than twice as fast as zlib in most cases, providing significant performance boosts to packet broadcasts and overall network performance.
-- Closures are now used for internal event handler calls. This provides a performance improvement of 10-20% over the 3.x system, which had to dynamically resolve callables for every event call.
+- Improved performance of `World->setBlock()` by around 35% when the `$update` parameter is set to `true`.
+- Improved performance of `World->setBlock()` by around 30% when the `$update` parameter is set to `false`.
-### Logger revamp
+### Logging
- Many components now have a dedicated logger which automatically adds [prefixes] to their messages.
- Main logger now includes milliseconds in timestamps.
+- Debug messages are now logged when reach distance checks prevent players from doing something.
+- Various messages related to world loading/generation/conversion and plugin loading errors are now localized according to the language set in `server.properties`.
+- Exception log format has been changed. Now, exception info is logged in one big block message. This saves space on the console and improves readability, as well as reducing the ability for bad `ThreadedLoggerAttachment`s to break exception output.
+- Improved error messages when a plugin command name or alias contains an illegal character.
+- The server will now log an EMERGENCY-level message when `Server->forceShutdown()` is used for any other reason than a graceful shutdown.
### Network
This version features substantial changes to the network system, improving coherency, reliability and modularity.
+#### Performance
+- [`libdeflate`](https://github.com/ebiggers/libdeflate) is now (optionally) used for outbound Minecraft packet compression. It's more than twice as fast as zlib in most cases, providing significant performance boosts to packet broadcasts and overall network performance.
+
#### Minecraft Bedrock packet encryption
- This fixes replay attacks where hackers steal and replay player logins.
- A new setting has been added to `pocketmine.yml`: `network.enable-encryption` which is `true` by default.
-#### Packet receive error handling has been overhauled
+#### Error handling
- Only `BadPacketException` is now caught during packet decode and handling. This requires that all decoding MUST perform proper data error checking.
- Throwing a `BadPacketException` from decoding will now cause players to be kicked with the message `Packet processing error`.
- The disconnect message includes a random hex ID to help server owners identify the problems reported by their players.
- Throwing any other exception will now cause a server crash. `Internal server error` has been removed.
- It is now illegal to send a clientbound packet to the server. Doing so will result in the client being kicked with the message `Unexpected non-serverbound packet`.
+- Fixed server crash when unable to bind to the desired port. Now, the server will show an error and gracefully stop without a crashdump instead.
#### New packet handler system
- Packet handlers have been separated from NetworkSession into a dedicated packet handler structure.
@@ -158,6 +238,17 @@ This version features substantial changes to the network system, improving coher
- Packet handlers are now almost entirely absent from `Player` and instead appear in their own dedicated units.
- Almost all game logic that was previously locked up inside packet handlers in `Player` has been extracted into new API methods. See Player API changes for details.
+### Plugin loading
+- Phar plugins are now able to depend on folder plugins loaded by DevTools.
+- A new "plugin greylist" feature has been introduced, which allows whitelisting or blacklisting plugins from loading. See `plugin_list.yml`.
+
+### Internals
+- The `pocketmine` subdirectory has been removed from `src`. [PSR-4 autoloading is now used thanks to Composer](https://github.com/pmmp/PocketMine-MP/blob/4.0.0/composer.json#L63).
+- Crashdump rendering has been separated from crashdump data generation. This allows rendering crashdumps from existing JSON data.
+- Direct iteration of arrays with string keys is now disallowed by a custom PHPStan rule. This is because numeric strings are casted to integers when used as array keys, which produces a variety of unexpected behaviour particularly for iteration.
+ - To iterate on arrays with string keys, `Utils::stringifyKeys()` must now be used.
+- Fixed various crashes involving arrays with numeric string keys.
+
## API
### General
- Most places which previously allowed `callable` now only allow `\Closure`. This is because closures have more consistent behaviour and are more performant.
@@ -215,14 +306,26 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
**Note**: The old structure will also still work just fine. This is not a required change.
+### Other changes
+- Incorrect structure of `commands` is now detected earlier and handled more gracefully.
+- Commands must now declare `permission` for each command. Previously, the `permission` key was optional, causing anyone to be able to use the command.
+ - This behaviour was removed because of the potential for security issues - a typo in `plugin.yml` could lead to dangerous functionality being exposed to everyone.
+ - If you want to make a command that everyone can use, declare a permission with a `default` of `true` and assign it to the command.
+- Permissions must now declare `default` for each permission. Previously, the `default` key was optional, causing the permission to silently be denied to everyone (PM4) or granted to ops implicitly (PM3).
+
### Block
- A new `VanillaBlocks` class has been added, which contains static methods for creating any currently-known block type. This should be preferred instead of use of `BlockFactory::get()` where constants were used.
+- `BlockFactory` is now a singleton, and its methods are no longer static. `BlockFactory::whatever()` should be replaced with `BlockFactory::getInstance()->whatever()`.
+- `BlockFactory->get()` is now **deprecated**.
+ - For most cases, `VanillaBlocks::WHATEVER_BLOCK()` should fill your needs.
+ - `BlockFactory` should now only be used for loading old save data from, for example, a database, config or a world save.
+ - To refer to blocks by name, consider using `StringToItemParser` to accept names instead of IDs.
- Blocks now contain their positions instead of extending `Position`. `Block->getPosition()` has been added.
- Blocks with IDs >= 256 are now supported.
- Block state and variant metadata have been separated.
- Variant is considered an extension of ID and is immutable.
- `Block->setDamage()` has been removed.
- - All blocks now have getters and setters for their appropriate block properties, such as facing, lit/unlit, colour (in some cases), and many more. These should be used instead of metadata.
+- All blocks now have getters and setters for their appropriate block properties, such as facing, lit/unlit, colour (in some cases), and many more. These should be used instead of metadata.
- Tile entities are now created and deleted automatically when `World->setBlock()` is used with a block that requires a tile entity.
- Some tile entities' API has been exposed on their corresponding `Block` classes, with the tile entity classes being deprecated.
- The `pocketmine\tile` namespace has been relocated to `pocketmine\block\tile`.
@@ -237,9 +340,11 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Block->isCompatibleWithTool()` -> `BlockBreakInfo->isToolCompatible()`
- The following API methods have been added:
- `Block->asItem()`: returns an itemstack corresponding to the block
+ - `Block->getModelPositionOffset()`: used to offset the bounding box of blocks like bamboo based on coordinates
- `Block->isSameState()`: returns whether the block is the same as the parameter, including state information
- `Block->isSameType()`: returns whether the block is the same as the parameter, without state information
- `Block->isFullCube()`
+ - `Liquid->getMinAdjacentSourcesToFormSource()`: returns how many adjacent source blocks of the same liquid must be present in order for the current block to become a source itself
- The following hooks have been added:
- `Block->onAttack()`: called when a player in survival left-clicks the block to try to start breaking it
- `Block->onEntityLand()`: called when an entity lands on this block after falling (from any distance)
@@ -256,12 +361,16 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Block->setDamage()`
- `Block::get()`: this was superseded by `BlockFactory::get()` a long time ago
- `Block->getBoundingBox()`
+- The following classes have been added:
+ - `inventory\CraftingTableInventory`: represents a crafting table's 3x3 crafting grid
+ - `utils\LeverFacing`
+ - `utils\MinimumFlowCostCalculator`: encapsulates flow calculation logic previously locked inside `Liquid`.
- The following classes have been renamed:
- `BlockIds` -> `BlockLegacyIds`
- `CobblestoneWall` -> `Wall`
- `NoteBlock` -> `Note`
- - `SignPost` -> `Sign`
- - `StandingBanner` -> `Banner`
+ - `SignPost` -> `FloorSign`
+ - `StandingBanner` -> `FloorBanner`
- The following classes have been removed:
- `Bricks`
- `BurningFurnace`
@@ -308,11 +417,22 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- The following API methods have signature changes:
- `Command->setPermission()` argument is now mandatory (but still nullable).
- `CommandSender->setScreenLineHeight()` argument is now mandatory (but still nullable).
+ - `Command->getDescription()` now returns `Translatable|string`.
+ - `Command->getUsage()` now returns `Translatable|string`.
+ - `Command->setDescription()` now accepts `Translatable|string`.
+ - `Command->setUsage()` now accepts `Translatable|string`.
+- `Command->setPermission()` now throws an exception if given a string containing non-existing permissions. Previously, it would silently default to allowing ops to use the command, which may not have been desired.
+ - This is usually caused by a typo or forgotten permission declaration.
- Commands with spaces in the name are no longer supported.
+- Command usage strings and description strings are no longer automatically translated (use `Translatable` instead of bare string keys).
### Entity
#### General
- `Entity` no longer extends from `Location`. `Entity->getLocation()` and `Entity->getPosition()` should be used instead.
+- Ender inventory has been refactored. It's now split into two parts:
+ - `EnderChestInventory` is a temporary gateway "inventory" that acts as a proxy to the player's ender inventory. It has a `Position` for holder. This is discarded when the player closes the inventory window.
+ - `PlayerEnderInventory` is the storage part. This is stored in `Human` and does not contain any block info.
+ - To open the player's ender inventory, use `Player->setCurrentWindow(new EnderChestInventory($blockPos, $player->getEnderInventory()))`.
- The following public fields have been removed:
- `Entity->chunk`: Entities no longer know which chunk they are in (the `World` now manages this instead).
- `Entity->height`: moved to `EntitySizeInfo`; use `Entity->size` instead
@@ -329,8 +449,11 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- The following methods have signature changes:
- `Entity->entityBaseTick()` is now `protected`.
- `Entity->move()` is now `protected`.
+ - `Entity->setPosition()` is now `protected` (use `Entity->teleport()` instead).
+ - `Entity->setPositionAndRotation()` is now `protected` (use `Entity->teleport()` instead).
- `Living->knockBack()` now accepts `float, float, float` (the first two parameters have been removed).
- `Living->getEffects()` now returns `EffectManager` instead of `Effect[]`.
+ - `Location->__construct()` now accepts `?World $world` in the 4th parameter, and all parameters are now mandatory.
- The following classes have been added:
- `effect\EffectManager`: contains effect-management functionality extracted from `Living`
- `HungerManager`: contains hunger-management functionality extracted from `Human`
@@ -349,6 +472,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Human->getMaxFood()` -> `HungerManager->getMaxFood()`
- `Human->addFood()` -> `HungerManager->addFood()`
- `Human->isHungry()` -> `HungerManager->isHungry()`
+ - `Human->getEnderChestInventory()` -> `Human->getEnderInventory()`
- `Human->getSaturation()` -> `HungerManager->getSaturation()`
- `Human->setSaturation()` -> `HungerManager->setSaturation()`
- `Human->addSaturation()` -> `HungerManager->addSaturation()`
@@ -372,7 +496,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Human->onPickupXp()` -> `ExperienceManager->onPickupXp()`
- `Human->resetXpCooldown()` -> `ExperienceManager->resetXpCooldown()`
- The following API methods have been removed:
- - `Human->getRawUniqueId()`: use `Human->getUniqueId()->toBinary()` instead
+ - `Human->getRawUniqueId()`: use `Human->getUniqueId()->getBytes()` instead
- The following classes have been removed:
- `Creature`
- `Damageable`
@@ -381,6 +505,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Rideable`
- `Vehicle`
- `Skin` now throws exceptions on creation if given invalid data.
+- Fixed `Living->lookAt()` not taking eye height into account.
#### Effect
- All `Effect` related classes have been moved to the `pocketmine\entity\effect` namespace.
@@ -409,9 +534,10 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Effect->getId()` -> `EffectIdMap->toId()`
- `Effect::registerEffect()` -> `EffectIdMap->register()`
- `Effect::getEffect()` -> `EffectIdMap->fromId()`
- - `Effect::getEffectByName()` -> `VanillaEffects::fromString()`
-- The following API methods have been added:
- - `Effect->getRuntimeId()`: this is a **dynamic ID** which can be used for effect type comparisons. **This cannot be stored, so don't use it in configs or NBT!**
+ - `Effect::getEffectByName()` -> `StringToEffectParser->parse()`
+- Added `StringToEffectParser` singleton:
+ - Supports custom aliases!
+ - This is used by `/effect` to provide name support.
#### Removal of runtime entity NBT
- Entities no longer keep their NBT alive at runtime.
@@ -423,17 +549,18 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Entity::createEntity()` has been removed. It's no longer needed for creating new entities at runtime - just use `new YourEntity` instead.
- `Entity` subclass constructors can now have any signature, just like a normal class.
- Loading entities from NBT is now handled by `EntityFactory`. It works quite a bit differently than `Entity::createEntity()` did. Instead of registering `YourEntity::class` to a set of Minecraft save IDs, you now need to provide a callback which will construct an entity when given some NBT and a `World`.
- - The creation callback is registered using `EntityFactory::register()`.
- - The creation callback must have the signature `function(World, CompoundTag) : Entity`.
+ - `EntityFactory` is a singleton. You can get its instance by using `EntityFactory::getInstance()`.
+ - Creation callbacks are registered using `EntityFactory->register()`.
+ - Creation callbacks must have the signature `function(World, CompoundTag) : Entity`.
- This enables `Entity` subclasses to have any constructor parameters they like.
- It also allows requiring that certain data is always provided (for example, it doesn't make much sense to create a `FallingBlock` without specifying what type of block).
- Examples:
- `ItemEntity` now requires an `Item` in its constructor, so its creation callback decodes the `Item` from the NBT to be passed to the constructor.
- `Painting` now requires a `PaintingMotive` in its constructor, so its creation callback decides which `PaintingMotive` to provide based on the NBT it receives.
- See `EntityFactory` for more examples.
-- `EntityFactory::register()` (previously `Entity::registerEntity()`) will now throw exceptions on error cases instead of returning `false`.
+- `EntityFactory->register()` (previously `Entity::registerEntity()`) will now throw exceptions on error cases instead of returning `false`.
- The following API methods have been moved:
- - `Entity::registerEntity()` -> `EntityFactory::register()`
+ - `Entity::registerEntity()` -> `EntityFactory->register()`
- The following classes have changed constructors:
- All projectile subclasses now require a `?Entity $thrower` parameter.
- `Arrow->__construct()` now requires a `bool $critical` parameter (in addition to the `$thrower` parameter).
@@ -473,6 +600,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- Handler functions will now **not receive cancelled events by default**. This is a **silent BC break**, i.e. it won't raise errors, but it might cause bugs.
- `@ignoreCancelled` is now no longer respected.
- `@handleCancelled` has been added. This allows opting _into_ receiving cancelled events (it's the opposite of `@ignoreCancelled`).
+ - `@handleCancelled` may not be used on non-cancellable events (an exception will be thrown during registration).
#### `PlayerPreLoginEvent` changes
- The `Player` object no longer exists at this phase of the login. Instead, a `PlayerInfo` object is provided, along with connection information.
@@ -514,28 +642,39 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
#### Other changes
- Disconnecting players during events no longer crashes the server (although it might cause other side effects).
-- `PlayerKickEvent` is no longer fired for disconnects that occur before the player completes the initial login sequence (i.e. completing downloading resource packs).
- Cancellable events must now implement `CancellableTrait` to get the cancellable components needed to satisfy interface requirements. `Event` no longer stubs these methods.
- `PlayerInteractEvent` is no longer fired when a player activates an item. This fixes the age-old complaint of `PlayerInteractEvent` firing multiple times when interacting once. The following constants have been removed:
- `PlayerInteractEvent::LEFT_CLICK_AIR`
- `PlayerInteractEvent::RIGHT_CLICK_AIR`
- `PlayerInteractEvent::PHYSICAL`
- The following events have been added:
- - `PlayerEntityInteractEvent`: player right-clicking (or long-clicking on mobile) on an entity.
- - `PlayerItemUseEvent`: player activating their held item, for example to throw it.
- `BlockTeleportEvent`: block teleporting, for example dragon egg when attacked.
- - `PlayerDisplayNameChangeEvent`
- `EntityItemPickupEvent`: player (or other entity) picks up a dropped item (or arrow). Replaces `InventoryPickupItemEvent` and `InventoryPickupArrowEvent`.
- Unlike its predecessors, this event supports changing the destination inventory.
- If the destination inventory is `null`, the item will be destroyed. This is usually seen for creative players with full inventories.
- `EntityTrampleFarmlandEvent`: mob (or player) jumping on farmland causing it to turn to dirt
+ - `PlayerDisplayNameChangeEvent`
+ - `PlayerEmoteEvent`
+ - `PlayerEntityInteractEvent`: player right-clicking (or long-clicking on mobile) on an entity.
+ - `PlayerItemUseEvent`: player activating their held item, for example to throw it.
- `StructureGrowEvent`: called when trees or bamboo grow (or any other multi-block plant structure).
+- The following events have changed behaviour:
+ - Bone meal is now consistently never consumed when `BlockGrowEvent` or `StructureGrowEvent` is cancelled.
+ - `BlockGrowEvent` is now called when cocoa pods grow.
+ - `ChunkPopulateEvent` is now called after all adjacent chunks modified during population have been updated. This fixes issues with modifications made in the event sometimes disappearing.
+ - `InventoryOpenEvent` is now fired when a player opens a crafting table's UI.
+ - `InventoryCloseEvent` is now fired when a player closes a crafting table's UI.
+ - `PlayerDropItemEvent` will now prevent the drops from force-closing of the following inventories:
+ - anvil
+ - enchanting table
+ - loom
+ - `PlayerKickEvent` is no longer fired for disconnects that occur before the player completes the initial login sequence (i.e. completing downloading resource packs).
- The following events have been removed:
- `EntityArmorChangeEvent`
- `EntityInventoryChangeEvent`
- `EntityLevelChangeEvent` - `EntityTeleportEvent` with world checks should be used instead.
- - `InventoryPickupItemEvent` - use `EntityItemPickupEvent` instead
- `InventoryPickupArrowEvent` - use `EntityItemPickupEvent` instead
+ - `InventoryPickupItemEvent` - use `EntityItemPickupEvent` instead
- `NetworkInterfaceCrashEvent`
- `PlayerCheatEvent`
- `PlayerIllegalMoveEvent`
@@ -553,6 +692,8 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `HandlerList::unregisterAll()` -> `HandlerListManager->unregisterAll()`
- `HandlerList::getHandlerListFor()` -> `HandlerListManager->getListFor()`
- `HandlerList::getHandlerLists()` -> `HandlerListManager->getAll()`
+- The following API methods have changed behaviour:
+ - `PlayerCreationEvent->setPlayerClass()` now verifies that the player class set is instantiable.
- The following classes have been moved:
- `pocketmine\plugin\RegisteredListener` -> `pocketmine\event\RegisteredListener`
@@ -562,8 +703,12 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `CallbackInventoryChangeListener`
- `CreativeInventory`: contains the creative functionality previously embedded in `pocketmine\item\Item`, see Item changes for details
- `InventoryChangeListener`: allows listening (but not interfering with) events in an inventory.
+ - `PlayerCraftingInventory`: represents the player's own 2x2 crafting grid.
+ - `PlayerEnderInventory`: represents the pure storage part of the player's ender inventory, without any block information
+ - `TemporaryInventory`: interface which should be implemented by any inventory whose contents should be evacuated when closing.
- `transaction\CreateItemAction`
- `transaction\DestroyItemAction`
+ - `transaction\TransactionBuilderInventory`: facilitates building `InventoryTransaction`s using standard `Inventory` API methods
- The following classes have been renamed / moved:
- `ContainerInventory` -> `pocketmine\block\inventory\BlockInventory`
- The following classes have been moved to the `pocketmine\block\inventory` namespace:
@@ -583,9 +728,12 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Inventory->getChangeListeners()`
- `Inventory->removeChangeListeners()`
- `Inventory->swap()`: swaps the contents of two slots
+ - `Inventory->getAddableItemQuantity()`: returns how many items from the given stack can be added to the inventory, used for partial pickups of itemstacks with a full inventory
- The following API methods have been removed:
- `BaseInventory->getDefaultSize()`
- `BaseInventory->setSize()`
+ - `CraftingGrid->getHolder()`
+ - `EnderChestInventory->setHolderPosition()`
- `Inventory->close()`
- `Inventory->dropContents()`
- `Inventory->getName()`
@@ -598,24 +746,28 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `InventoryAction->onExecuteSuccess()`
- `PlayerInventory->sendCreativeContents()`
- The following API methods have signature changes:
+ - `BaseInventory->__construct()` no longer accepts a list of items to initialize with.
+ - `CraftingGrid->__construct()` no longer accepts a `Player` parameter.
- `Inventory->clear()` now returns `void` instead of `bool`.
- `Inventory->setItem()` now returns `void` instead of `bool`.
- `InventoryAction->execute()` now returns `void` instead of `bool`.
- - `BaseInventory->construct()` no longer accepts a list of items to initialize with.
- `PlayerInventory->setItemInHand()` now sends the update to viewers of the player.
+- `CraftingGrid` is now abstract.
### Item
#### General
- A new `VanillaItems` class has been added, which contains static methods for creating any currently-known item type. This should be preferred instead of use of `ItemFactory::get()` where constants were used.
-- `StringToItemParser` has been added, which allows mapping any string to any item, irrespective of IDs. These mappings are used by `/give` and `/clear`, and are made with custom plugin aliases in mind.
+- `StringToItemParser` singleton has been added:
+ - This allows mapping any string to any item, irrespective of IDs
+ - These mappings are used by `/give` and `/clear`, and are made with custom plugin aliases in mind.
- Yes, this means you can finally add your own custom aliases to `/give` without ugly hacks!
-- `LegacyStringToItemParser` has been added, which is a slightly more dynamic (but still inadvisable) replacement for `ItemFactory::fromString()`.
+- `LegacyStringToItemParser` singleton has been added. This supports id:meta parsing in the same way that `ItemFactory::fromString()` used to, but its use is discouraged.
+- `ItemFactory` is now a singleton instead of static class, and its remaining methods are no longer static. You can get its instance by `ItemFactory::getInstance()`.
- `Item->count` is no longer public.
- The hierarchy of writable books has been changed: `WritableBook` and `WrittenBook` now extend `WritableBookBase`.
- The following API methods have signature changes:
- `WritableBookBase->setPages()` now accepts `WritableBookPage[]` instead of `CompoundTag[]`.
- - `ItemFactory::get()` no longer accepts `string` for the `tags` parameter.
- - `ItemFactory::fromString()` no longer accepts a `$multiple` parameter and now only returns `Item`, not `Item|Item[]`.
+ - `ItemFactory->get()` no longer accepts `string` for the `tags` parameter.
- The following methods are now fluent:
- `WritableBookBase->setPages()`
- `Item->addEnchantment()`
@@ -630,8 +782,9 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Item->removeNamedTagEntry()`
- `Item->setDamage()`: "Damage" is now immutable for all items except `Durable` descendents.
- `Item->setNamedTagEntry()`
- - `Item::get()`: this was superseded by `ItemFactory::get()` a long time ago
- - `Item::fromString()`: this was superseded by `ItemFactory::fromString()` a long time ago
+ - `Item::get()`: prefer `VanillaItems` or `StringToItemParser` if possible; use `ItemFactory->get()` if you have no other choice
+ - `Item::fromString()`: use `StringToItemParser->parse()` or `LegacyStringToItemParser->parse()` instead
+ - `ItemFactory::fromString()`
- `Item->setCompoundTag()`
- `Item->getCompoundTag()`
- `Item->hasCompoundTag()`
@@ -683,7 +836,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Item::clearCreativeItems()` -> `CreativeInventory::clear()`
- `Item::getCreativeItemIndex()` -> `CreativeInventory::getItemIndex()`
- `Item::getCreativeItems()` -> `CreativeInventory::getAll()`
- - `Item::initCreativeItems()` -> `CreativeInventory::init()`
+ - `Item::initCreativeItems()` -> `CreativeInventory::reset()`
- `Item::isCreativeItem()` -> `CreativeInventory::contains()`
- `Item::removeCreativeItem()` -> `CreativeInventory::remove()`
- The following classes have been added:
@@ -692,6 +845,8 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `LiquidBucket`
- `MilkBucket`
- `PotionType`: enum class containing information about vanilla potion types
+ - `Releasable`: this interface is implemented by items like bows which have a "release" action
+ - `StringToItemParser`: allows converting string IDs into any item, used by `/give` and `/clear`
- `WritableBookBase`
- `WritableBookPage`
- The following API methods have been added:
@@ -733,7 +888,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- Example: `Enchantment::get(Enchantment::PROTECTION)` is replaced by `VanillaEnchantments::PROTECTION()`
- These methods also provide proper type information to static analysers instead of just generic `Enchantment`, making them easier to code with.
- The boundaries between MCPE enchantment IDs and PocketMine-MP internals are now more clear.
- - ID handling is moved to `pocketmine\data\bedrock\EnchantmentIdMap`.
+ - ID handling is moved to `pocketmine\data\bedrock\EnchantmentIdMap` singleton.
- All enchantment ID constants have been removed from `Enchantment`. `pocketmine\data\bedrock\EnchantmentIds` if you still need legacy effect IDs for some reason.
- `Enchantment::RARITY_*` constants were moved to `Rarity` class, and the `RARITY_` prefixes removed.
- `Enchantment::SLOT_*` constants were moved to `ItemFlags` class, and the `SLOT_` prefixes removed.
@@ -741,9 +896,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Enchantment::registerEnchantment()` -> `EnchantmentIdMap->register()`
- `Enchantment::getEnchantment()` -> `EnchantmentIdMap->fromId()`
- `Enchantment->getId()` -> `EnchantmentIdMap->toId()`
- - `Enchantment::getEnchantmentByName()` -> `VanillaEnchantments::fromString()`
-- The following API methods have been added:
- - `Enchantment->getRuntimeId()`: this is a **dynamic ID** which can be used for enchantment type comparisons. **This cannot be stored, so don't use it in configs or NBT!**
+ - `Enchantment::getEnchantmentByName()` -> `StringToEnchantmentParser->parse()`
### Lang
- The following classes have been renamed:
@@ -771,6 +924,8 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
### Network
- The following fields have been removed:
- `Network::$BATCH_THRESHOLD`
+- The following classes have been added:
+ - `NetworkInterfaceStartException`: this may be thrown by `Network->registerInterface()` and `NetworkInterface->start()` to cause a graceful failure without crashing - this should be used when, for example, you are unable to bind a port
- The following classes have been renamed:
- `SourceInterface` -> `NetworkInterface`
- `AdvancedSourceInterface` -> `AdvancedNetworkInterface`
@@ -846,13 +1001,15 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `ServerOperator`
### Player
-- The following classes have moved to the new `pocketmine\player` namespace:
+- The following classes have been added/moved to the new `pocketmine\player` namespace:
- `Achievement`
- `GameMode`
- `IPlayer`
- `OfflinePlayer`
- `PlayerInfo`
- `Player`
+ - `SurvivalBlockBreakHandler`: handles cracking animation, sounds and particles when mining a block in creative
+ - `UsedChunkStatus`: enum used internally by the chunk sending system
- The following constants have been removed:
- `Player::SURVIVAL` - use `GameMode::SURVIVAL()`
- `Player::CREATIVE` - use `GameMode::CREATIVE()`
@@ -867,35 +1024,46 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Player->breakBlock()`: destroy the target block in the current world (immediately)
- `Player->consumeHeldItem()`: consume the previously activated item, e.g. eating food
- `Player->continueBreakBlock()`: punch the target block during destruction in survival, advancing break animation and creating particles
+ - `Player->getCurrentWindow()`: returns the inventory window the player is currently viewing, or null if they aren't viewing an inventory
- `Player->getItemCooldownExpiry()`: returns the tick on which the player's cooldown for a given item expires
+ - `Player->getLowerCaseName()`: use `strtolower(Player->getName())` instead
+ - `Player->getPlayerInfo()`: returns a `PlayerInfo` object containing various metadata about the player
+ - `Player->getSaveData()`: returns save data generated on the fly
- `Player->hasFiniteResources()`
- `Player->interactBlock()`: interact (right click) the target block in the current world
- `Player->interactEntity()`: interact (right click) the target entity, e.g. to apply a nametag (not implemented yet)
- `Player->pickBlock()`: picks (mousewheel click) the target block in the current world
- `Player->releaseHeldItem()`: release the previously activated item, e.g. shooting a bow
+ - `Player->removeCurrentWindow()`: removes the inventory window the player is currently viewing, if any
- `Player->selectHotbarSlot()`: select the specified hotbar slot
+ - `Player->setCurrentWindow()`: sets the inventory the player is currently viewing
- `Player->stopBreakBlock()`: cease attacking a previously attacked block
- `Player->toggleFlight()`: tries to start / stop flying (fires events, may be cancelled)
- `Player->updateNextPosition()`: sets the player's next attempted move location (fires events, may be cancelled)
- `Player->useHeldItem()`: activate the held item, e.g. throwing a snowball
- - `Player->getSaveData()`: returns save data generated on the fly
- The following API methods have been removed:
+ - `IPlayer->isBanned()`: this was unreliable because it only checked name bans and didn't account for plugin custom ban systems. Use `Server->getNameBans()->isBanned()` and `Server->getIPBans()->isBanned()` instead.
+ - `IPlayer->isOp()`: use `Server` APIs instead
+ - `IPlayer->isWhitelisted()`: use `Server->isWhitelisted()` instead
+ - `IPlayer->setBanned()`: use `Server` APIs instead
+ - `IPlayer->setOp()`: use `Server` APIs instead
+ - `IPlayer->setWhitelisted()`: use `Server->setWhitelisted()` instead
- `Player->addActionBarMessage()`: replaced by `sendActionBarMessage()`
- `Player->addSubTitle()`: replaced by `sendSubTitle()`
- `Player->addTitle()`: replaced by `sendTitle()`
+ - `Player->addWindow()`: use `Player->setCurrentWindow()` instead
+ - `Player->dataPacket()`: replaced by `NetworkSession->sendDataPacket()`
- `Player->getAddress()`: replaced by `NetworkSession->getIp()`
- `Player->getPing()`: moved to `NetworkSession`
- `Player->getPort()`: moved to `NetworkSession`
- - `Player->updatePing()`: moved to `NetworkSession`
- - `Player->dataPacket()`: replaced by `NetworkSession->sendDataPacket()`
+ - `Player->getWindow()`: use `Player->getCurrentWindow()` instead
+ - `Player->getWindowId()`
+ - `Player->removeAllWindows()`
+ - `Player->removeWindow()`: use `Player->removeCurrentWindow()` instead
- `Player->sendDataPacket()`: replaced by `NetworkSession->sendDataPacket()`
+ - `Player->setCraftingGrid()`: crafting tables now work the same way as other containers; use `Player->setCurrentWindow()`
- `Player->updateNextPosition()`: use `Player->handleMovement()` instead
- - `IPlayer->isWhitelisted()`: use `Server->isWhitelisted()` instead
- - `IPlayer->setWhitelisted()`: use `Server->setWhitelisted()` instead
- - `IPlayer->isBanned()`: this was unreliable because it only checked name bans and didn't account for plugin custom ban systems. Use `Server->getNameBans()->isBanned()` and `Server->getIPBans()->isBanned()` instead.
- - `IPlayer->setBanned()`: use `Server` APIs instead
- - `IPlayer->isOp()`: use `Server` APIs instead
- - `IPlayer->setOp()`: use `Server` APIs instead
+ - `Player->updatePing()`: moved to `NetworkSession`
### Plugin
- API version checks are now more strict. It is no longer legal to declare multiple minimum versions on the same major version. Doing so will now cause the plugin to fail to load with the message `Multiple minimum API versions found for some major versions`.
@@ -919,22 +1087,33 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `PluginBase->onLoad()` changed from `public` to `protected`
- The following hook methods have been renamed:
- `Plugin->setEnabled()` -> `Plugin->onEnableStateChange()`. This change was made to force plugin developers misusing this hook to stop, and to give it a name that better describes what it does.
-- The following (deprecated) API methods have been removed:
- - `PluginManager->callEvent()`: use `Event->call()` instead
+- The following API methods have been removed:
- `PluginManager->addPermission()`: use `PermissionManager` instead
+ - `PluginManager->callEvent()`: use `Event->call()` instead
- `PluginManager->getDefaultPermSubscriptions()`: use `PermissionManager` instead
- `PluginManager->getDefaultPermissions()`: use `PermissionManager` instead
- `PluginManager->getPermission()`: use `PermissionManager` instead
- `PluginManager->getPermissionSubscriptions()`: use `PermissionManager` instead
- `PluginManager->getPermissions()`: use `PermissionManager` instead
+ - `PluginManager->loadPlugin()`: use `PluginManager->loadPlugins()` instead
- `PluginManager->recalculatePermissionDefaults()`: use `PermissionManager` instead
- `PluginManager->removePermission()`: use `PermissionManager` instead
- `PluginManager->subscribeToDefaultPerms()`: use `PermissionManager` instead
- `PluginManager->subscribeToPermission()`: use `PermissionManager` instead
- `PluginManager->unsubscribeFromDefaultPerms()`: use `PermissionManager` instead
- `PluginManager->unsubscribeFromPermission()`: use `PermissionManager` instead
+- The following API methods have changed behaviour:
+ - `PluginManager->loadPlugins()` now accepts paths to files as well as directories, in which case it will load only the plugin found in the target file.
- It is no longer permitted to throw exceptions from `PluginBase->onEnable()` or `PluginBase->onLoad()`. Doing so will now cause the server to crash.
+### Promise
+A very basic in-house implementation of Promises has been added. This is currently used for handling world generation requests.
+
+- `PromiseResolver` is created by the creator of the task. The task should call `PromiseResolver->resolve()` when the result is ready.
+- `Promise` can be obtained by using `PromiseResolver->getPromise()` and should be returned to API consumers.
+
+Please note that this was not written with plugins in mind and its API may change in a future version.
+
### Scheduler
#### Thread-local storage for AsyncTasks
- TLS has been completely rewritten in this release to be self contained, more robust and easier to use.
@@ -951,11 +1130,10 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `AsyncTask->storeLocal()` now has the signature `storeLocal(string $key, mixed $complexData) : void`
- `AsyncTask->fetchLocal()` now has the signature `fetchLocal(string $key) : mixed`
-#### Other changes
+#### Other AsyncTask changes
- `AsyncPool` uses a new, significantly more performant algorithm for task collection.
- `BulkCurlTask` has had the `$complexData` constructor parameter removed.
- `BulkCurlTask->__construct()` now accepts `BulkCurlTaskOperation[]` instead of `mixed[]`.
-- Added `CancelTaskException`, which can be thrown from `Task::onRun()` to cancel a task (especially useful for `ClosureTask`).
- `pocketmine\Collectable` has been removed, and is no longer extended by `AsyncTask`.
- The following hooks have been added:
- `AsyncTask->onError()`: called on the main thread when an uncontrolled error was detected in the async task, such as a memory failure
@@ -967,39 +1145,47 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `AsyncTask->removeFromThreadStore()`: use `AsyncTask->worker->removeFromThreadStore()`
- `AsyncTask->saveToThreadStore()`: use `AsyncTask->worker->saveToThreadStore()`
+#### Non-AsyncTask changes
+- Added `CancelTaskException`, which can be thrown from `Task->onRun()` to cancel a task (especially useful for `ClosureTask`).
+- The `$currentTick` parameter of `Task->onRun()` has been removed (use `Server->getTick()` instead if needed).
+- Callables given to `ClosureTask` are no longer required to declare a `void` typehint (useful for arrow functions).
+
### Server
- New chat broadcasting APIs have been implemented, which don't depend on the permission system.
- The following API methods have been added:
- - `subscribeToBroadcastChannel()` - allows subscribing a `CommandSender` to receive chat messages (and other message types) on any channel
- - `unsubscribeFromBroadcastChannel()`
- - `unsubscribeFromAllBroadcastChannels()`
- - `getBroadcastChannelSubscribers()`
+ - `Server->subscribeToBroadcastChannel()` - allows subscribing a `CommandSender` to receive chat messages (and other message types) on any channel
+ - `Server->unsubscribeFromBroadcastChannel()`
+ - `Server->unsubscribeFromAllBroadcastChannels()`
+ - `Server->getBroadcastChannelSubscribers()`
- Giving `Player` any `pocketmine.broadcast.*` permissions will cause them to automatically subscribe to the corresponding broadcast channel (and removing them will unsubscribe it).
- It's now possible to create and subscribe to custom broadcast channels without using permissions.
- However, `Player`s may automatically unsubscribe themselves from the builtin broadcast channels if they don't have the proper permissions.
- Automatic subscribe/unsubscribe from custom broadcast channels can be implemented using the new `Permissible` permission recalculation callbacks API.
+- The following API methods have been added:
+ - `Server->getIpV6()`
+ - `Server->getPortV6()`
- The following API methods have been removed:
- - `reloadWhitelist()`
- - `getLevelMetadata()`
- - `getPlayerMetadata()`
- - `getEntityMetadata()`
- - `getDefaultGamemode()`
- - `getLoggedInPlayers()`
- - `onPlayerLogout()`
- - `addPlayer()`
- - `removePlayer()`
- - `reload()`
- - `getSpawnRadius()`
- - `enablePlugin()`
- - `disablePlugin()`
- - `getGamemodeString()` - replaced by `pocketmine\player\GameMode->getTranslationKey()`
- - `getGamemodeName()` - replaced by `pocketmine\player\GameMode->name()`
- - `getGamemodeFromString()` - replaced by `GameMode::fromString()`
- - `broadcast()` - use `broadcastMessage()` instead
+ - `Server->reloadWhitelist()`
+ - `Server->getLevelMetadata()`
+ - `Server->getPlayerMetadata()`
+ - `Server->getEntityMetadata()`
+ - `Server->getDefaultGamemode()`
+ - `Server->getLoggedInPlayers()`
+ - `Server->onPlayerLogout()`
+ - `Server->addPlayer()`
+ - `Server->removePlayer()`
+ - `Server->reload()`
+ - `Server->getSpawnRadius()`
+ - `Server->enablePlugin()`
+ - `Server->disablePlugin()`
+ - `Server->getGamemodeString()` - replaced by `pocketmine\player\GameMode->getTranslationKey()`
+ - `Server->getGamemodeName()` - replaced by `pocketmine\player\GameMode->name()`
+ - `Server->getGamemodeFromString()` - replaced by `GameMode::fromString()`
+ - `Server->broadcast()` - use `Server->broadcastMessage()` instead
- The following API methods have changed:
- - `getOfflinePlayerData()` no longer creates data when it doesn't exist.
+ - `Server->getOfflinePlayerData()` no longer creates data when it doesn't exist.
- The following API methods have been renamed:
- - `getPlayer()` -> `getPlayerByPrefix()` (consider using `getPlayerExact()` instead where possible)
+ - `Server->getPlayer()` -> `Server->getPlayerByPrefix()` (consider using `Server->getPlayerExact()` instead where possible)
### Level / World
#### General
@@ -1022,17 +1208,35 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Server->setAutoSave()` -> `WorldManager->setAutoSave()`
- `Server->setDefaultLevel()` -> `WorldManager->setDefaultWorld()`
- `Server->unloadLevel()` -> `WorldManager->unloadWorld()`
-- Added `WorldManager->getAutoSaveTicks()` and `WorldManager->setAutoSaveTicks()` to allow controlling the autosave interval.
+- The following static classes have been un-static-ified and converted to singletons (use `Whatever::getInstance()->method()` instead of `Whatever::method()`):
+ - `GeneratorManager`
+ - `WorldProviderManager`
- The following classes have been added:
- `BlockTransaction`: allows creating batch commits of block changes with validation conditions - if any block can't be applied, the whole transaction fails to apply.
- `ChunkListenerNoOpTrait`: contains default no-op stubs for chunk listener implementations
- `ChunkListener`: interface allowing subscribing to events happening on a given chunk
+ - `ChunkLockId`: used by `World->lockChunk()` and `World->unlockChunk()`
- `TickingChunkLoader`: a `ChunkLoader` specialization that allows ticking chunks
+ - `format\io\FastChunkSerializer`: provides methods to encode a chunk for transmitting to another thread
+ - `WorldCreationOptions`: used for passing world generator options to `WorldManager->generateWorld()`
- `ChunkLoader` no longer requires implementing `getX()` and `getZ()`.
- `ChunkLoader` no longer causes chunks to get random updates. If this behaviour is needed, implement `TickingChunkLoader`.
- The following classes have been renamed:
- `pocketmine\world\utils\SubChunkIteratorManager` -> `pocketmine\world\utils\SubChunkExplorer`
+- The following class constants have been added:
+ - `Chunk::COORD_BIT_SIZE`
+ - `Chunk::COORD_MASK`
+ - `Chunk::DIRTY_FLAG_BLOCKS`
+ - `Chunk::DIRTY_FLAG_TERRAIN`
+ - `Chunk::EDGE_LENGTH`
+ - `SubChunk::COORD_BIT_SIZE`
+ - `SubChunk::COORD_MASK`
+ - `SubChunk::EDGE_LENGTH`
+ - `World::Y_MIN`
- The following API methods have been added:
+ - `WorldManager->getAutoSaveTicks()`
+ - `WorldManager->setAutoSaveTicks()`
+ - `World->notifyNeighbourBlockUpdate()`
- `World->registerChunkListener()`
- `World->unregisterChunkListener()`
- `World->getBlockAt()` (accepts int x/y/z instead of Vector3, faster for some use cases)
@@ -1041,12 +1245,9 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Chunk->getDirtyFlag()` (more granular component-based chunk dirty-flagging, used to avoid saving unmodified parts of the chunk)
- `Chunk->setDirty()`
- `Chunk->setDirtyFlag()`
-- The following API methods have been removed:
- - `ChunkLoader->getLoaderId()` (now object ID is used)
- - `ChunkLoader->isLoaderActive()`
- - `ChunkLoader->getPosition()`
- - `ChunkLoader->getLevel()`
- - `Chunk->fastSerialize()` (use `FastChunkSerializer::serialize()` instead)
+- The following API methods have been removed from the public API:
+ - `Chunk->addEntity()`
+ - `Chunk->fastSerialize()` (use `FastChunkSerializer::serializeTerrain()` instead)
- `Chunk->getBlockData()`
- `Chunk->getBlockDataColumn()`
- `Chunk->getBlockId()`
@@ -1055,7 +1256,9 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Chunk->getBlockLightColumn()`
- `Chunk->getBlockSkyLight()`
- `Chunk->getBlockSkyLightColumn()`
+ - `Chunk->getEntities()`
- `Chunk->getMaxY()`
+ - `Chunk->getSavableEntities()`
- `Chunk->getSubChunkSendCount()` (this was specialized for protocol usage)
- `Chunk->getX()`
- `Chunk->getZ()`
@@ -1065,6 +1268,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Chunk->populateSkyLight()` (use `SkyLightUpdate->recalculateChunk()` instead)
- `Chunk->recalculateHeightMap()` (moved to `SkyLightUpdate`)
- `Chunk->recalculateHeightMapColumn()` (moved to `SkyLightUpdate`)
+ - `Chunk->removeEntity()`
- `Chunk->setAllBlockLight()`
- `Chunk->setAllBlockSkyLight()`
- `Chunk->setBlock()`
@@ -1076,63 +1280,82 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Chunk->setGenerated()`
- `Chunk->setX()`
- `Chunk->setZ()`
- - `Chunk::fastDeserialize()` (use `FastChunkSerializer::deserialize()` instead)
- - `World->isFullBlock()`
- - `World->getFullBlock()`
- - `World->getBlockIdAt()`
- - `World->setBlockIdAt()`
- - `World->getBlockDataAt()`
- - `World->setBlockDataAt()`
- - `World->setBlockLightAt()`
- - `World->setBlockSkyLightAt()`
- - `World->getBlockSkyLightAt()` (use `World->getRealBlockSkyLightAt()` or `World->getPotentialBlockSkyLightAt()`, depending on use-case)
- - `World->getHeightMap()` (misleading name, only actually useful for sky light calculation - you probably want `getHighestBlockAt()` instead)
- - `World->setHeightMap()` (misleading name, only actually useful for sky light calculation)
- - `World->getChunkEntities()`
- - `World->getChunkTiles()`
- - `World->getTileById()`
+ - `Chunk::fastDeserialize()` (use `FastChunkSerializer::deserializeTerrain()` instead)
+ - `ChunkLoader->getLevel()`
+ - `ChunkLoader->getLoaderId()` (now object ID is used)
+ - `ChunkLoader->getPosition()`
+ - `ChunkLoader->isLoaderActive()`
+ - `World->addChunkPacket()`
+ - `World->addGlobalPacket()`
+ - `World->broadcastGlobalPacket()`
+ - `World->broadcastLevelEvent()`
+ - `World->broadcastLevelSoundEvent()`
- `World->checkSpawnProtection()`
- - `World->updateBlockLight()`
- - `World->updateSkyLight()`
+ - `World->generateChunkCallback()`
+ - `World->getBlockDataAt()`
+ - `World->getBlockIdAt()`
+ - `World->getBlockSkyLightAt()` (use `World->getRealBlockSkyLightAt()` or `World->getPotentialBlockSkyLightAt()`, depending on use-case)
+ - `World->getChunkTiles()`
+ - `World->getFullBlock()`
+ - `World->getHeightMap()` (misleading name, only actually useful for sky light calculation - you probably want `getHighestBlockAt()` instead)
+ - `World->getTickRate()`
+ - `World->getTileById()`
+ - `World->isFullBlock()`
- `World->isFullBlock()` (use `Block->isFullCube()` instead)
- `World->sendBlocks()`
- `World->sendTime()`
- - `World->addGlobalPacket()`
- - `World->broadcastGlobalPacket()`
- - `World->addChunkPacket()`
- - `World->broadcastLevelSoundEvent()`
- - `World->broadcastLevelEvent()`
- - `World->getTickRate()`
+ - `World->setBlockDataAt()`
+ - `World->setBlockIdAt()`
+ - `World->setBlockLightAt()`
+ - `World->setBlockSkyLightAt()`
+ - `World->setHeightMap()` (misleading name, only actually useful for sky light calculation)
- `World->setTickRate()`
+ - `World->updateBlockLight()`
+ - `World->updateSkyLight()`
- `World::generateChunkLoaderId()`
- The following API methods have changed signatures:
+ - `Chunk->__construct()` now has the signature `array $subChunks, BiomeArray $biomeIds, bool $terrainPopulated`.
+ - `Chunk->getSubChunk()` now returns `SubChunk` instead of `SubChunkInterface|null` (and throws `InvalidArgumentException` on out-of-bounds coordinates).
+ - `Chunk->getSubChunks()` now returns `array` instead of `SplFixedArray`.
+ - `Chunk->setSubChunk()` no longer accepts `SubChunkInterface`, and the `$allowEmpty` parameter has been removed.
+ - `ChunkManager->setChunk()` (and its notable implementations in `World` and `SimpleChunkManager`) no longer accepts NULL for the `$chunk` parameter.
+ - `GeneratorManager->registerGenerator()` now requires a `\Closure $presetValidator` parameter. This is used to check generator options of worlds and configs before attempting to use them.
+ - `Position->__construct()` now requires the `$world` parameter (it's no longer optional).
- `World->addParticle()` now has the signature `addParticle(Vector3 $pos, Particle $particle, ?Player[] $players = null) : void`
- - `World->addSound()` now has the signature `addSound(?Vector3 $pos, Sound $sound, ?Player[] $players = null) : void`
- - `World->getRandomTickedBlocks()` now returns `bool[]` instead of `SplFixedArray`.
- `World->addRandomTickedBlock()` now accepts `Block` instead of `int, int`.
+ - `World->addSound()` now has the signature `addSound(?Vector3 $pos, Sound $sound, ?Player[] $players = null) : void`
+ - `World->getChunk()` no longer accepts a `$create` parameter.
+ - `World->getRandomTickedBlocks()` now returns `bool[]` instead of `SplFixedArray`.
+ - `World->loadChunk()` now returns `?Chunk`, and the `$create` parameter has been removed.
- `World->removeRandomTickedBlock()` now accepts `Block` instead of `int, int`.
- `World->setBlock()` has had the `$direct` parameter removed.
- - `World->loadChunk()` now returns `?Chunk`, and the `$create` parameter has been removed.
- - `World->getChunk()` no longer accepts a `$create` parameter.
+ - `World->setChunks()` no longer accepts a `$deleteEntitiesAndTiles` parameter.
- `World->updateAllLight()` now accepts `int, int, int` instead of `Vector3`.
- - `ChunkManager->setChunk()` (and its notable implementations in `World` and `SimpleChunkManager`) no longer accepts NULL for the `$chunk` parameter.
- - `Chunk->__construct()` now has the signature `array $subChunks, ?list $entities, ?list $tiles, ?BiomeArray $biomeArray, ?HeightArray $heightArray`.
- - `Chunk->getSubChunk()` now returns `SubChunk` instead of `SubChunkInterface|null` (and throws `InvalidArgumentException` on out-of-bounds coordinates).
- - `Chunk->setSubChunk()` no longer accepts `SubChunkInterface`, and the `$allowEmpty` parameter has been removed.
- - `WorldManager->generateWorld()` (previously `Server->generateWorld()`) now accepts `WorldCreationOptions` instead of `int $seed, class-string $generator, mixed[] $options`.
+ - `WorldManager->generateWorld()` (previously `Server->generateWorld()`) now accepts `WorldCreationOptions` instead of `int $seed, class-string $generator, mixed[] $options`
+ - `World->lockChunk()` now requires `ChunkLockId $lockId` parameter.
+ - `World->unlockChunk()` now requires a `?ChunkLockId $lockId` parameter. If a non-null lockID is given, the lock on the chunk will only be removed if it matches the given lockID.
+ - `World->unlockChunk()` now returns `bool` instead of `void` (to signal whether unlocking succeded or not).
- The following API methods have been renamed / moved:
- - `Level->getChunks()` -> `World->getLoadedChunks()`
- - `Level->getCollisionCubes()` -> `World->getCollisionBoxes()`
+ - `World->getChunks()` -> `World->getLoadedChunks()`
+ - `World->getCollisionCubes()` -> `World->getCollisionBoxes()`
- `World->getName()` -> `World->getDisplayName()`
- `World->populateChunk()` has been split into `World->requestChunkPopulation()` and `World->orderChunkPopulation()`.
- The following API methods have changed behaviour:
+ - `World->getAdjacentChunks()` now returns an array indexed using `World::chunkHash()`, where the `x` and `z` components are the relative offsets from the target chunk (range -1 to +1).
- `World->getChunk()` no longer tries to load chunks from disk. If the chunk is not already in memory, null is returned. (This behaviour now properly matches other `ChunkManager` implementations.)
- `World->getHighestBlockAt()` now returns `null` instead of `-1` if the target X/Z column contains no blocks.
- The following methods now throw `WorldException` when targeting ungenerated terrain:
- `World->getSafeSpawn()` (previously it just silently returned the input position)
- `World->getHighestBlockAt()` (previously it returned -1)
- `World->loadChunk()` no longer creates an empty chunk when the target chunk doesn't exist on disk.
- - `World->setChunk()` now fires `ChunkLoadEvent` and `ChunkListener->onChunkLoaded()` when replacing a chunk that didn't previously exist.
+ - `World->setChunk()` has the following behavioural changes:
+ - Now fires `ChunkLoadEvent` and `ChunkListener->onChunkLoaded()` when replacing a chunk that didn't previously exist.
+ - Now updates entities in the replaced chunk and its neighbours. This fixes bugs such as paintings not dropping and dropped items floating in midair if the ground was lower than before.
+ - Entities are no longer deleted on chunk replacement.
+ - Tiles are no longer deleted on chunk replacement, unless one of the following conditions is met:
+ - the target block in the new chunk doesn't expect a tile
+ - the target block in the new chunk expects a different type of tile (responsibility of the plugin developer to create the new tile)
+ - there's already a tile in the target chunk which conflicts with the old one
- `World->useBreakOn()` now returns `false` when the target position is in an ungenerated or unloaded chunk (or chunk locked for generation).
- `World->useItemOn()` now returns `false` when the target position is in an ungenerated or unloaded chunk (or chunk locked for generation).
- A `ChunkListener` interface has been extracted from `ChunkLoader`. The following methods have been moved:
@@ -1145,7 +1368,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
#### Particles
- `DestroyBlockParticle` has been renamed to `BlockBreakParticle` for consistency.
-- `DustParticle->__construct()` now accepts a `pocketmine\utils\Color` object instead of `r, g, b, a`.
+- `DustParticle->__construct()` now accepts a `pocketmine\color\Color` object instead of `r, g, b, a`.
- `pocketmine\world\particle\Particle` no longer extends `pocketmine\math\Vector3`, and has been converted to an interface.
- Added the following `Particle` classes:
- `DragonEggTeleportParticle`
@@ -1153,7 +1376,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
#### Sounds
- `pocketmine\world\sound\Sound` no longer extends `pocketmine\math\Vector3`, and has been converted to an interface.
-- `Sound->encode()` now accepts `?\pocketmine\math\Vector3`. `NULL` may be passed for sounds which are global.
+- `Sound->encode()` now accepts `pocketmine\math\Vector3`.
- Added the following classes:
- `ArrowHitSound`
- `BlockBreakSound`
@@ -1187,7 +1410,7 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `UUID::fromRandom()` can be replaced by `Ramsey\Uuid\Uuid::uuid4()`
- `UUID::fromBinary()` can be replaced by `Ramsey\Uuid\Uuid::fromBytes()` (use `Ramsey\Uuid\Uuid::isValid()` to check validity)
- `UUID::toBinary()` is replaced by `Ramsey\Uuid\UuidInterface::getBytes()`
- - See the documentation at https://uuid.ramsey.dev/en/latest/introduction.html for more information.
+ - See the [documentation for `ramsey/uuid`](https://uuid.ramsey.dev/en/latest/introduction.html) for more information.
- `Terminal::hasFormattingCodes()` no longer auto-detects the availability of formatting codes. Instead it's necessary to use `Terminal::init()` with no parameters to initialize, or `true` or `false` to override.
- `Config->save()` no longer catches exceptions thrown during emitting to disk.
- The following new classes have been added:
@@ -1196,6 +1419,10 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Process`
- The following API methods have been added:
- `Config->getPath()`: returns the path to the config on disk
+ - `Config::parseList()`: parses a list of entries like `ops.txt` into an array
+ - `Config::parseProperties()`: parses a properties file like `server.properties` into an array
+ - `Config::writeList()`
+ - `Config::writeProperties()`
- `Terminal::write()`: emits a Minecraft-formatted text line without newline
- `Terminal::writeLine()`: emits a Minecraft-formatted text line with newline
- `Utils::recursiveUnlink()`: recursively deletes a directory and its contents
@@ -1218,11 +1445,23 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- `Utils::$os`
- The following API methods have signature changes:
- `Internet::simpleCurl()` now requires a `Closure` for its `onSuccess` parameter instead of `callable`.
+ - `Process::kill()` now requires an additional `bool $subprocesses` parameter.
+- The following API methods have behavioural changes:
+ - `Utils::parseDocComment()` now allows `-` in tag names.
- The following API methods have been removed:
- `TextFormat::toJSON()`
- `Utils::getCallableIdentifier()`
+- `MainLogger` now pushes log messages to `server.log` before calling logger attachments. This fixes messages not being written to disk when an uncaught error is thrown from a logger attachment.
## Gameplay
+### World loading
+- Chunks are now sent in proper circles. This improves the experience when flying quickly parallel to X or Z axis, since now more chunks in front of the player will load sooner.
+- Many bugs in player respawning have been fixed, including:
+ - Spawning underneath bedrock when spawn position referred to ungenerated terrain
+ - Spawning underneath bedrock on first server join on very slow machines (or when the machine was under very high load)
+ - Spawning inside blocks (or above the ground) when respawning with a custom spawn position set
+ - Player spawn positions sticking to the old location when world spawn position changed - this was because the world spawn at time of player creation was used as the player's custom spawn, so the bug will persist for older player data, but will work as expected for new players.
+
### Blocks
- Implemented the following blocks:
- bamboo
@@ -1263,72 +1502,128 @@ However, if we add `src-namespace-prefix: pmmp\TesterPlugin` to the `plugin.yml`
- stone-like slabs of many variants
- Non-player entities now bounce when falling on beds.
- Players and mobs now receive reduced fall damage when falling on beds.
+- Fixed cake block desync when attempting to eat in creative (eating in creative is not yet supported, but the block rollback was missing).
+- Fixed the bounding box of skulls when mounted on a wall.
+- Fixed podzol dropping itself when mined (instead of dirt).
### Items
- Implemented the following items:
- records
- compounds (from Minecraft: Education Edition)
- black, brown, blue and white dyes
+- Compasses now point to the correct (current) world's spawn point after teleporting players to a different world. Previously, they would continue to point to the spawn of the world that the player initially spawned in.
### Inventory
- Implemented offhand inventory.
+- Block-picking is now supported in survival mode.
+- Block picking behaviour now matches vanilla (no longer overwrites held item, jumps to existing item where possible).
+- Armor can now be equipped by right-clicking while holding it.
+- Picking up some items from a dropped stack of items is now supported. This fixes various bugs with being unable to pick up items with an almost-full inventory.
+- Fixed arrows getting added to creative players' inventories when picked up.
-# 4.0.0-BETA2
-Released 10th September 2021.
+### Misc
+- Added support for emotes.
+
+# 4.0.1
+Released 9th December 2021.
## General
-- ext-chunkutils 0.3.x is now required.
-- Reduced memory usage of light storage after garbage collection.
-- Reduced memory usage of uniform subchunks which only contain 1 type of block.
-- The title bar no longer displays heap memory usage numbers (nobody seemed to know that was what it was, anyway).
-- `/status` no longer displays heap memory usage numbers.
-- `start.sh` no longer specifically mentions PHP 7 when erroring because of a missing PHP binary.
-- Debug messages are now logged when reach distance checks prevent players from doing something.
+- Added a script `tools/ping-server.php`. This was sitting in my workspace for many years.
+- `Minecraft network interface running` messages are no longer shown if RakLib was prevented from starting.
## Fixes
-- Fixed players getting disconnected when using `/tp` to ungenerated chunks (or when teleported by players).
-- Fixed a potential memory leak in block updating when chunks are unloaded.
-- Fixed `Living->lookAt()` not taking eye height into account.
-- Fixed grass replacing itself when placing grass (and consuming inventory items).
-- Fixed players taking fall damage when falling next to a wall.
+### Core
+- Fixed server crash when `FallingBlock` has invalid block data that it can't understand.
+- Fixed server crash when loading chunks containing tiles outside the world bounds.
+- Fixed server crash when loading LevelDB chunks containing blockstates which are invalid or not yet supported - they are now treated as corrupted instead.
+- Fixed `level.dat` becoming corrupted by world saves when the disk is full - now it will still fail to save, but it will leave the original data intact. Previously it would destroy the data and leave behind an empty file.
+- Fixed configs becoming corrupted when saved when the disk is full - now they'll still fail to save, but the original file will remain intact.
-## API changes
-- The following API methods have been added:
- - `World->getChunkEntities()`
- - `World->notifyNeighbourBlockUpdate()`
-- The following API methods have been removed:
- - `Chunk->getEntities()`
- - `Chunk->getSavableEntities()`
- - `Chunk->addEntity()`
- - `Chunk->removeEntity()`
-- The following classes have been added:
- - `pocketmine\inventory\transaction\TransactionBuilderInventory`: facilitates building `InventoryTransaction`s using standard `Inventory` API methods
-- The following class constants have been added:
- - `Chunk::EDGE_LENGTH`
- - `Chunk::COORD_BIT_SIZE`
- - `Chunk::COORD_MASK`
- - `SubChunk::EDGE_LENGTH`
- - `SubChunk::COORD_BIT_SIZE`
- - `SubChunk::COORD_MASK`
+### API
+- Fixed mistakes in the 4.0.0 changelog:
+ - Removal of `Player->getLowerCaseName()` is now mentioned.
+ - `CreativeInventory::reset()` is the successor to `Item::initCreativeItems()`, not `CreativeInventory::init()`.
+ - Note that the changelog when viewing from the 4.0.0 GitHub release will remain the same; only the changelog in the current repo will be different.
+- `Config->save()` will no longer write empty data to the file when using JSON and the data fails to encode - an exception will be thrown instead.
+- `StringToItemParser` now returns the correct items for `bamboo`, `shulker_box`, `stone_slab`, `stone_stairs` and `tall_grass`.
+- `StringToItemParser` now recognizes `slime` and `slime_block` (these were previously missing).
-# 4.0.0-BETA3
+# 4.0.2
+Released 12th December 2021.
+## Fixes
+### Core
+- Fixed server crash when loading written books containing pages with invalid UTF-8 characters - the invalid characters are now scrubbed.
+- Fixed server crash when root type of `plugin.yml` is valid, but not an array.
+- Fixed ConsoleReader crash due OPcache ASLR issue - it's not clear what caused this, but OPcache is not needed in the subprocess anyway.
+- Fixed backslashes getting stripped from unquoted command arguments - these were only supposed to be stripped from quoted arguments, to allow escaping of quotes.
+- `build/generate-known-translation-apis.php` now sorts numerically-indexed arguments into ascending order, irrespective of the order they appear in the original string.
+
+### API
+- `KnownTranslationKeys` and `KnownTranslationFactory` are now marked `@internal`.
+- `ItemEntity` now clones the itemstack passed to its constructor, fixing various confusing mutability issues.
+- `PlayerExperienceChangeEvent->setNewProgress()` now performs range checks. This fixes the root of a very old and confusing crash bug which took several years to identify the cause of.
+ - Note that the defective plugin(s) which caused this problem will still cause a server crash, but the plugin responsible will now get blamed correctly.
+- `GeneratorManager->addGenerator()` now consistently converts the given alias to lowercase. Due to a bug, it previously didn't do this if the `$overwrite` parameter was set to `true`, causing a range of confusing bugs.
+
+# 4.0.3
+Released 16th December 2021.
+
+## Fixes
+- Fixed `/dumpmemory` crashing when encountering uninitialized typed properties.
+- Fixed all chunks containing furnaces being treated as corrupted in worlds older than 2017.
+ - This was caused by a strict corruption check detecting bad data created by a bug in PocketMine-MP that was fixed in 2017.
+- Fixed player arm swing animation not being shown when attacks were cancelled by attack cooldown.
+- Fixed being unable to use `/deop` to de-op a player whose name appeared in `ops.txt` with uppercase letters in it.
+- Added a check for valid tile class in `BlockIdentifier`.
+
+# 4.0.4
+Released 1st January 2022.
## General
-- Added support for Minecraft: Bedrock Edition 1.17.30.
-- Dropped support for Minecraft: Bedrock Edition 1.17.1x.
-- `tools/convert-world.php` now writes errors to stderr and sets the proper exit code.
-- Explosions now use the standard mechanism for processing block updates. Previously, it used a special mechanism due to prohibitively poor efficiency of the standard algorithm. Since these inefficiencies have now been addressed, explosions can now be consistent with everything else, with minimal performance impact.
-- Command usage strings are no longer automatically translated (use `Translatable` instead of bare string keys).
-- Command description strings are no longer automatically translated (use `Translatable` instead of bare string keys).
+- Improved performance of loading chests and other containers from world saves.
+- Improved performance of loading player inventories from saved data.
## Fixes
-- `ItemFactory->isRegistered()` no longer crashes when given negative item IDs.
-- Furnaces now continue to operate after reloading the chunk they were contained in.
-- Fixed being unable to reconnect for 10 seconds after disconnecting in some cases.
+- Fixed a crash that could occur when a chunk failed to be prepared for chunk sending.
+- Fixed fall damage when sprinting down stairs.
+- Fixed message length limit for chat (now 512 instead of 255, and accounts for UTF-8).
+- Fixed incorrect message being displayed when trying to sleep in a bed which is too far away.
+- Fixed missing space between `Kicked by admin.` and `Reason` when using `/kick` to kick a player.
+- Fixed client-side performance issue of entities with very large scale.
-## API changes
-- The following API methods have been added:
- - `Liquid->getMinAdjacentSourcesToFormSource()`: returns how many adjacent source blocks of the same liquid must be present in order for the current block to become a source itself
- - `Player->getPlayerInfo()`
-- `Liquid` minimum-cost flow calculation code has been extracted to `MinimumCostFlowCalculator`.
+# 4.0.5
+Released 4th January 2022.
+
+## Fixes
+- Fixed several denial-of-service attack vectors related to writable book text length and encoding.
+- Fixed several denial-of-service attack vectors related to skin data field lengths.
+- Fixed food bar desync when cancelling `PlayerItemConsumeEvent` in a plugin.
+- Fixed compass needles not updating when the world spawn is changed.
+
+# 4.0.6
+Released 13th January 2022.
+
+## Fixes
+- Fixed server crash on invalid facing values provided by the client when placing or breaking blocks.
+- Fixed documentation link to AsyncTask in Worker.
+
+# 4.0.7
+Released 21st January 2022.
+
+## General
+- Max nesting of form responses is now limited to 2.
+- Updated outdated documentation of `PlayerInteractEvent`.
+
+## Fixes
+- Fixed server crash on invalid JSON provided by the client in `ModalFormResponsePacket`.
+- Fixed ender pearls teleporting players when thrown by a player directly against a wall when cancelling `ProjectileLaunchEvent`.
+- Fixed collision box of brewing stand.
+- Fixed break times and tool types of bamboo, nether wart blocks, infested stone and leaves.
+
+# 4.0.8
+Released 25th January 2022.
+
+## Fixes
+- Fixed ender chest not dropping itself when mined with a Silk Touch pickaxe.
+- The correct amount of fall damage is now taken when falling from a height onto hay bales.
diff --git a/changelogs/4.1.md b/changelogs/4.1.md
new file mode 100644
index 000000000..56829b49d
--- /dev/null
+++ b/changelogs/4.1.md
@@ -0,0 +1,166 @@
+**For Minecraft: Bedrock Edition 1.18.0**
+
+### Note about API versions
+Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
+Plugin developers should **only** update their required API to this version if you need the changes in this build.
+
+**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
+
+# 4.1.0-BETA1
+Released 22nd January 2022.
+
+## General
+- Game mode names (e.g. `survival`, `creative`) may now be used for the `gamemode` property in `server.properties`.
+- Increased default maximum render distance to 16 chunks. Players with a render distance smaller than this will notice no difference.
+- The setup wizard now prompts for a maximum render distance value.
+- The setup wizard now prompts for an IPv6 port selection. Previously it would always use 19133.
+- `chunk-ticking.disable-block-ticking` now accepts block names like those used in the `/give` command.
+- The `/clear` command now behaves more like vanilla:
+ - The order of inventories is now the same as Bedrock.
+ - The cursor and offhand inventories are now cleared if necessary.
+
+## Technical
+- `PlayerAuthInputPacket` is now used instead of `MovePlayerPacket` for processing movements. This improves position and rotation accuracy.
+- `&&` and `||` are now always used instead of `and` and `or`.
+- New version of `pocketmine/errorhandler` is used by this version, adding support for `ErrorToExceptionHandler::trap()`. This enables reliably capturing `E_WARNING` and `E_NOTICE` from functions such as `yaml_parse()` and friends.
+- New dependency versions are required by this version:
+ - `pocketmine/bedrock-protocol` has been updated from 7.1.0 to [7.3.0](https://github.com/pmmp/BedrockProtocol/releases/tag/7.3.0%2Bbedrock-1.18.0).
+ - `pocketmine/errorhandler` has been updated from 0.3.0 to [0.6.0](https://github.com/pmmp/ErrorHandler/releases/tag/0.6.0).
+
+## API
+### Block
+- The following classes have been added:
+ - `Lectern`
+ - `Pumpkin`
+- The following public API methods have been added:
+ - `Block->getTypeId() : int` - returns an integer which uniquely identifies the block type, ignoring things like facing, colour etc.
+ - `VanillaBlocks::LECTERN()`
+
+### Entity
+- The following classes have been added:
+ - `animation\ItemEntityStackSizeChangeAnimation`
+- The following public API methods have been added:
+ - `object\ItemEntity->isMergeable(object\ItemEntity $other) : bool`
+ - `object\ItemEntity->setStackSize(int $size) : void`
+ - `object\ItemEntity->tryMergeInto(object\ItemEntity $other) : bool`
+ - `ExperienceManager->canAttractXpOrbs() : bool`
+ - `ExperienceManager->setCanAttractXpOrbs(bool $v = true) : void`
+ - `Entity->getSize() : EntitySizeInfo`
+ - `Living->isGliding() : bool`
+ - `Living->isSwimming() : bool`
+ - `Living->setGliding(bool $value = true) : void`
+ - `Living->setSwimming(bool $value = true) : void`
+- The following protected API methods have been added:
+ - `Entity->getBlocksIntersected(float $inset) : \Generator`
+
+### Event
+- `BlockSpreadEvent` is now called when fire spreads to the positions of blocks it burns away.
+- `BlockFormEvent` is now called when concrete powder turns into concrete due to contact with water.
+- The following classes have been added:
+ - `BlockMeltEvent` - called when ice or snow melts
+ - `ChestPairEvent` - called when two chests try to form a pair
+ - `PlayerToggleGlideEvent` - called when a player starts or stops gliding
+ - `PlayerToggleSwimEvent` - called when a player starts or stops swimming
+
+### Item
+- The following public API methods have been added:
+ - `SplashPotion->getType() : PotionType`
+ - `VanillaItems::AIR()`
+- The following API methods have been deprecated:
+ - `ItemFactory::air()` - use `VanillaItems::AIR()` instead
+
+### Player
+- The following public API methods have been added:
+ - `Player->hasBlockCollision() : bool`
+ - `Player->setHasBlockCollision(bool $value)` - allows controlling spectator-like no-clip behaviour without changing game mode
+ - `Player->toggleSwim(bool $swim) : bool` - called by the network system when the client tries to start/stop swimming
+ - `Player->toggleGlide(bool $glide) : bool` - called by the network system when the client tries to start/stop gliding
+
+### Server
+- The following public API constants have been added:
+ - `Server::DEFAULT_SERVER_NAME`
+ - `Server::DEFAULT_MAX_PLAYERS`
+ - `Server::DEFAULT_PORT_IPV4`
+ - `Server::DEFAULT_PORT_IPV6`
+ - `Server::DEFAULT_MAX_VIEW_DISTANCE`
+
+### Utils
+- Config parsing errors are now always represented by `ConfigLoadException` and include the path to the file in the message.
+- Added `TextFormat::MINECOIN_GOLD`, and support for it to the various `TextFormat` methods.
+- The following public API methods have been added:
+ - `Utils::assumeNotFalse()` - static analysis crutch to silence PHPStan errors without using `ignoreErrors` or `@phpstan-ignore-line`, which are both too coarse.
+- The following public API properties have been added:
+ - `Terminal::$COLOR_MINECOIN_GOLD`
+- The following classes have been added:
+ - `ConfigLoadException`
+- Fixed `Random->nextSignedInt()` to actually return a signed int. Previously it would return any integer value between 0 and 4,294,957,295.
+- Fixed `Random->nextSignedFloat()` to return a float between `-1.0` and `1.0`. Previously it would return any value between `0.0` and `2.0`.
+- `VersionString->getNumber()` output is now structured differently to fix overflow issues caused by the old format.
+
+### World
+- The following classes have been added:
+ - `sound\ItemUseOnBlockSound`
+ - `sound\LecternPlaceBookSound`
+
+## Gameplay
+### Blocks
+- Fire now spreads.
+- Implemented lectern blocks.
+- Added missing sounds for hoeing grass and dirt.
+- Added missing sounds for using a shovel on grass to create grass path.
+- Pumpkins can now be carved using shears.
+
+### Items
+- Dropped items of the same type now merge with each other.
+
+### Misc
+- Implemented player swimming.
+
+# 4.1.0-BETA2
+Released 27th January 2022.
+
+## API
+### Block
+- The following API methods have been added:
+ - `utils\BrewingStandSlot->getSlotNumber() : int`
+ - `utils\FurnaceType->getCookSound() : Sound`
+- The following API constants have been added:
+ - `tile\BrewingStand::BREW_TIME_TICKS`
+
+### Crafting
+- The following API methods have been added:
+ - `CraftingManager->getPotionContainerChangeRecipes() : array>`
+ - `CraftingManager->getPotionTypeRecipes() : array>`
+ - `CraftingManager->registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void`
+ - `CraftingManager->registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void`
+- The following classes have been added:
+ - `BrewingRecipe`
+ - `PotionContainerChangeRecipe`
+ - `PotionTypeRecipe`
+
+### Event
+- The following classes have been added:
+ - `BrewItemEvent` - called when a brewing stand finishes brewing potions; this is called up to 3 times (once for each brewing slot, as needed)
+ - `BrewingFuelUseEvent` - called when a brewing stand consumes blaze powder
+ - `PlayerViewDistanceChangeEvent` - called whenever a player alters their render distance or requests one for the first time when connecting
+
+### World
+#### Sound
+- The following classes have been added:
+ - `BlastFurnaceSound` - the sound made by a blast furnace during smelting
+ - `FurnaceSound` - the sound made by a regular furnace during cooking or smelting
+ - `PotionFinishBrewingSound` - the sound made by a brewing stand when a potion finishes being brewed
+ - `SmokerSound` - the sound made by a smoker during cooking
+
+## Gameplay
+### Blocks
+- Brewing stands can now be used for brewing potions.
+- The visual appearance of a brewing stand now updates correctly when the contents of its inventory changes (adding/removing potions).
+- Added missing sounds for furnace, blast furnace and smoker.
+- Fixed ender chest not dropping itself when mined with a Silk Touch pickaxe.
+- Cobwebs now drop themselves when mined using shears.
+- The correct amount of fall damage is now taken when falling from a height onto hay bales.
+- Fixed block updating bug introduced by beta1 which caused crops and other plants to never grow.
+
+### Misc
+- Added a workaround for client hitbox size bug after swimming which caused the player to be able to fit into one-block-tall gaps.
diff --git a/composer.json b/composer.json
index 12ff5f393..d36d57b93 100644
--- a/composer.json
+++ b/composer.json
@@ -7,7 +7,7 @@
"require": {
"php": "^8.0",
"php-64bit": "*",
- "ext-chunkutils2": "^0.3.0",
+ "ext-chunkutils2": "^0.3.1",
"ext-crypto": "^0.3.1",
"ext-ctype": "*",
"ext-curl": "*",
@@ -22,7 +22,7 @@
"ext-openssl": "*",
"ext-pcre": "*",
"ext-phar": "*",
- "ext-pthreads": "~3.2.0",
+ "ext-pthreads": "^4.0",
"ext-reflection": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
@@ -34,28 +34,28 @@
"adhocore/json-comment": "^1.1",
"fgrosse/phpasn1": "^2.3",
"netresearch/jsonmapper": "^4.0",
- "pocketmine/bedrock-protocol": "2.0.0+bedrock1.17.30",
+ "pocketmine/bedrock-data": "~1.5.0+bedrock-1.18.0",
+ "pocketmine/bedrock-protocol": "~7.3.0+bedrock-1.18.0",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
- "pocketmine/classloader": "dev-master",
+ "pocketmine/classloader": "^0.2.0",
"pocketmine/color": "^0.2.0",
- "pocketmine/errorhandler": "^0.3.0",
- "pocketmine/log": "^0.3.0",
- "pocketmine/log-pthreads": "^0.2.0",
- "pocketmine/math": "^0.3.0",
- "pocketmine/nbt": "^0.3.0",
- "pocketmine/raklib": "^0.14.0",
+ "pocketmine/errorhandler": "^0.6.0",
+ "pocketmine/locale-data": "~2.4.2",
+ "pocketmine/log": "^0.4.0",
+ "pocketmine/log-pthreads": "^0.4.0",
+ "pocketmine/math": "^0.4.0",
+ "pocketmine/nbt": "^0.3.2",
+ "pocketmine/raklib": "^0.14.2",
"pocketmine/raklib-ipc": "^0.1.0",
"pocketmine/snooze": "^0.3.0",
- "pocketmine/spl": "dev-master",
"ramsey/uuid": "^4.1",
- "respect/validation": "^2.0",
"webmozart/path-util": "^2.3"
},
"require-dev": {
- "phpstan/phpstan": "0.12.98",
- "phpstan/phpstan-phpunit": "^0.12.6",
- "phpstan/phpstan-strict-rules": "^0.12.2",
+ "phpstan/phpstan": "1.3.3",
+ "phpstan/phpstan-phpunit": "^1.0.0",
+ "phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.2"
},
"autoload": {
@@ -79,7 +79,7 @@
"sort-packages": true
},
"scripts": {
- "make-devtools": "@php -dphar.readonly=0 tests/plugins/DevTools/src/DevTools/ConsoleScript.php --make tests/plugins/DevTools --out plugins/DevTools.phar",
+ "make-devtools": "@php -dphar.readonly=0 tests/plugins/DevTools/src/ConsoleScript.php --make tests/plugins/DevTools --out plugins/DevTools.phar",
"make-server": [
"@composer install --no-dev --classmap-authoritative --ignore-platform-reqs",
"@php -dphar.readonly=0 build/server-phar.php"
diff --git a/composer.lock b/composer.lock
index 667f2f983..9634345d6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "bd5d7fc81a75739bfd2ef04b38220b84",
+ "content-hash": "9aa2f11ba68d00423732973554fafb20",
"packages": [
{
"name": "adhocore/json-comment",
@@ -63,16 +63,16 @@
},
{
"name": "brick/math",
- "version": "0.9.2",
+ "version": "0.9.3",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0"
+ "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/dff976c2f3487d42c1db75a3b180e2b9f0e72ce0",
- "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0",
+ "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae",
+ "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae",
"shasum": ""
},
"require": {
@@ -82,7 +82,7 @@
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
- "vimeo/psalm": "4.3.2"
+ "vimeo/psalm": "4.9.2"
},
"type": "library",
"autoload": {
@@ -107,36 +107,40 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.9.2"
+ "source": "https://github.com/brick/math/tree/0.9.3"
},
"funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/brick/math",
"type": "tidelift"
}
],
- "time": "2021-01-20T22:51:39+00:00"
+ "time": "2021-08-15T20:50:18+00:00"
},
{
"name": "fgrosse/phpasn1",
- "version": "v2.3.0",
+ "version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/fgrosse/PHPASN1.git",
- "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e"
+ "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/20299033c35f4300eb656e7e8e88cf52d1d6694e",
- "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e",
+ "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296",
+ "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296",
"shasum": ""
},
"require": {
- "php": ">=7.0.0"
+ "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0"
},
"require-dev": {
- "phpunit/phpunit": "~6.3",
- "satooshi/php-coveralls": "~2.0"
+ "php-coveralls/php-coveralls": "~2.0",
+ "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0"
},
"suggest": {
"ext-bcmath": "BCmath is the fallback extension for big integer calculations",
@@ -188,9 +192,9 @@
],
"support": {
"issues": "https://github.com/fgrosse/PHPASN1/issues",
- "source": "https://github.com/fgrosse/PHPASN1/tree/v2.3.0"
+ "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0"
},
- "time": "2021-04-24T19:01:55+00:00"
+ "time": "2021-12-11T12:41:06+00:00"
},
{
"name": "netresearch/jsonmapper",
@@ -244,33 +248,59 @@
"time": "2020-12-01T19:48:11+00:00"
},
{
- "name": "pocketmine/bedrock-protocol",
- "version": "2.0.0+bedrock1.17.30",
+ "name": "pocketmine/bedrock-data",
+ "version": "1.5.0+bedrock-1.18.0",
"source": {
"type": "git",
- "url": "https://github.com/pmmp/BedrockProtocol.git",
- "reference": "faff7da904e68f69b1a9128956dac3122e87308a"
+ "url": "https://github.com/pmmp/BedrockData.git",
+ "reference": "482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/faff7da904e68f69b1a9128956dac3122e87308a",
- "reference": "faff7da904e68f69b1a9128956dac3122e87308a",
+ "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c",
+ "reference": "482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c",
+ "shasum": ""
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0"
+ ],
+ "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
+ "support": {
+ "issues": "https://github.com/pmmp/BedrockData/issues",
+ "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.18.0"
+ },
+ "time": "2021-11-30T18:30:46+00:00"
+ },
+ {
+ "name": "pocketmine/bedrock-protocol",
+ "version": "7.3.0+bedrock-1.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pmmp/BedrockProtocol.git",
+ "reference": "418b4dbaa6720b6c6c4385a4d321d9c0b3dbf14b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/418b4dbaa6720b6c6c4385a4d321d9c0b3dbf14b",
+ "reference": "418b4dbaa6720b6c6c4385a4d321d9c0b3dbf14b",
"shasum": ""
},
"require": {
"ext-json": "*",
"netresearch/jsonmapper": "^4.0",
- "php": "^7.4 || ^8.0",
+ "php": "^8.0",
"pocketmine/binaryutils": "^0.2.0",
"pocketmine/color": "^0.2.0",
- "pocketmine/math": "^0.3.0",
+ "pocketmine/math": "^0.3.0 || ^0.4.0",
"pocketmine/nbt": "^0.3.0",
"ramsey/uuid": "^4.1"
},
"require-dev": {
- "phpstan/phpstan": "0.12.99",
- "phpstan/phpstan-phpunit": "^0.12.21",
- "phpstan/phpstan-strict-rules": "^0.12.10",
+ "phpstan/phpstan": "1.3.1",
+ "phpstan/phpstan-phpunit": "^1.0.0",
+ "phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.5"
},
"type": "library",
@@ -286,22 +316,22 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
- "source": "https://github.com/pmmp/BedrockProtocol/tree/bedrock-1.17.30"
+ "source": "https://github.com/pmmp/BedrockProtocol/tree/7.3.0+bedrock-1.18.0"
},
- "time": "2021-09-21T23:25:51+00:00"
+ "time": "2022-01-06T20:44:27+00:00"
},
{
"name": "pocketmine/binaryutils",
- "version": "0.2.1",
+ "version": "0.2.4",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BinaryUtils.git",
- "reference": "8cd078e2426f8100331f2d73bef10f481dad6cde"
+ "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/8cd078e2426f8100331f2d73bef10f481dad6cde",
- "reference": "8cd078e2426f8100331f2d73bef10f481dad6cde",
+ "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a",
+ "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a",
"shasum": ""
},
"require": {
@@ -310,8 +340,10 @@
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "0.12.85",
- "phpstan/phpstan-strict-rules": "^0.12.4"
+ "phpstan/phpstan": "1.3.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.0.0",
+ "phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
@@ -326,9 +358,9 @@
"description": "Classes and methods for conveniently handling binary data",
"support": {
"issues": "https://github.com/pmmp/BinaryUtils/issues",
- "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.1"
+ "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.4"
},
- "time": "2021-05-30T19:42:57+00:00"
+ "time": "2022-01-12T18:06:33+00:00"
},
{
"name": "pocketmine/callback-validator",
@@ -382,31 +414,31 @@
},
{
"name": "pocketmine/classloader",
- "version": "dev-master",
+ "version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/ClassLoader.git",
- "reference": "80226e0917be79ac3230606113e25134a31e6a85"
+ "reference": "49ea303993efdfb39cd302e2156d50aa78209e78"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/80226e0917be79ac3230606113e25134a31e6a85",
- "reference": "80226e0917be79ac3230606113e25134a31e6a85",
+ "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/49ea303993efdfb39cd302e2156d50aa78209e78",
+ "reference": "49ea303993efdfb39cd302e2156d50aa78209e78",
"shasum": ""
},
"require": {
- "ext-pthreads": "~3.2.0",
+ "ext-pthreads": "~3.2.0 || ^4.0",
"ext-reflection": "*",
- "php": "^7.2 || ^8.0"
+ "php": "^8.0"
},
"conflict": {
"pocketmine/spl": "<0.4"
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "0.12.80",
+ "phpstan/phpstan": "0.12.99",
"phpstan/phpstan-strict-rules": "^0.12.4",
- "phpunit/phpunit": "^8.5 || ^9.5"
+ "phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
@@ -421,9 +453,9 @@
"description": "Ad-hoc autoloading components used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/ClassLoader/issues",
- "source": "https://github.com/pmmp/ClassLoader/tree/master"
+ "source": "https://github.com/pmmp/ClassLoader/tree/0.2.0"
},
- "time": "2021-05-29T23:09:32+00:00"
+ "time": "2021-11-01T20:17:27+00:00"
},
{
"name": "pocketmine/color",
@@ -465,24 +497,25 @@
},
{
"name": "pocketmine/errorhandler",
- "version": "0.3.0",
+ "version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/ErrorHandler.git",
- "reference": "ec742b209e8056bbe855069c4eff94c9734ea19b"
+ "reference": "dae214a04348b911e8219ebf125ff1c5589cc878"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/ec742b209e8056bbe855069c4eff94c9734ea19b",
- "reference": "ec742b209e8056bbe855069c4eff94c9734ea19b",
+ "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/dae214a04348b911e8219ebf125ff1c5589cc878",
+ "reference": "dae214a04348b911e8219ebf125ff1c5589cc878",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0"
+ "php": "^8.0"
},
"require-dev": {
- "phpstan/phpstan": "0.12.75",
- "phpstan/phpstan-strict-rules": "^0.12.2"
+ "phpstan/phpstan": "0.12.99",
+ "phpstan/phpstan-strict-rules": "^0.12.2",
+ "phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
@@ -497,22 +530,45 @@
"description": "Utilities to handle nasty PHP E_* errors in a usable way",
"support": {
"issues": "https://github.com/pmmp/ErrorHandler/issues",
- "source": "https://github.com/pmmp/ErrorHandler/tree/0.3.0"
+ "source": "https://github.com/pmmp/ErrorHandler/tree/0.6.0"
},
- "time": "2021-02-12T18:56:22+00:00"
+ "time": "2022-01-08T21:05:46+00:00"
},
{
- "name": "pocketmine/log",
- "version": "0.3.0",
+ "name": "pocketmine/locale-data",
+ "version": "2.4.3",
"source": {
"type": "git",
- "url": "https://github.com/pmmp/Log.git",
- "reference": "03ab1316da0b1978a7a1c8dd73e1c2a973cb62ec"
+ "url": "https://github.com/pmmp/Language.git",
+ "reference": "4d0b081f1a79407e087968ea76aaf330db6ea2b5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/Log/zipball/03ab1316da0b1978a7a1c8dd73e1c2a973cb62ec",
- "reference": "03ab1316da0b1978a7a1c8dd73e1c2a973cb62ec",
+ "url": "https://api.github.com/repos/pmmp/Language/zipball/4d0b081f1a79407e087968ea76aaf330db6ea2b5",
+ "reference": "4d0b081f1a79407e087968ea76aaf330db6ea2b5",
+ "shasum": ""
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "description": "Language resources used by PocketMine-MP",
+ "support": {
+ "issues": "https://github.com/pmmp/Language/issues",
+ "source": "https://github.com/pmmp/Language/tree/2.4.3"
+ },
+ "time": "2022-01-25T23:18:24+00:00"
+ },
+ {
+ "name": "pocketmine/log",
+ "version": "0.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pmmp/Log.git",
+ "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pmmp/Log/zipball/e6c912c0f9055c81d23108ec2d179b96f404c043",
+ "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043",
"shasum": ""
},
"require": {
@@ -522,7 +578,7 @@
"pocketmine/spl": "<0.4"
},
"require-dev": {
- "phpstan/phpstan": "0.12.80",
+ "phpstan/phpstan": "0.12.88",
"phpstan/phpstan-strict-rules": "^0.12.2"
},
"type": "library",
@@ -538,28 +594,28 @@
"description": "Logging components used by PocketMine-MP and related projects",
"support": {
"issues": "https://github.com/pmmp/Log/issues",
- "source": "https://github.com/pmmp/Log/tree/0.3.0"
+ "source": "https://github.com/pmmp/Log/tree/0.4.0"
},
- "time": "2021-05-18T21:00:49+00:00"
+ "time": "2021-06-18T19:08:09+00:00"
},
{
"name": "pocketmine/log-pthreads",
- "version": "0.2.0",
+ "version": "0.4.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/LogPthreads.git",
- "reference": "6be3445c48c62eba3922f987f000bb20c81d161f"
+ "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/6be3445c48c62eba3922f987f000bb20c81d161f",
- "reference": "6be3445c48c62eba3922f987f000bb20c81d161f",
+ "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/61f709e8cf36bcc24e4efe02acded680a1ce23cd",
+ "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd",
"shasum": ""
},
"require": {
- "ext-pthreads": "~3.2.0",
+ "ext-pthreads": "~3.2.0 || ^4.0",
"php": "^7.4 || ^8.0",
- "pocketmine/log": "^0.2.0 || ^0.3.0"
+ "pocketmine/log": "^0.4.0"
},
"conflict": {
"pocketmine/spl": "<0.4"
@@ -582,33 +638,33 @@
"description": "Logging components specialized for pthreads used by PocketMine-MP and related projects",
"support": {
"issues": "https://github.com/pmmp/LogPthreads/issues",
- "source": "https://github.com/pmmp/LogPthreads/tree/0.2.0"
+ "source": "https://github.com/pmmp/LogPthreads/tree/0.4.0"
},
- "time": "2021-05-18T22:15:28+00:00"
+ "time": "2021-11-01T21:42:09+00:00"
},
{
"name": "pocketmine/math",
- "version": "0.3.0",
+ "version": "0.4.2",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Math.git",
- "reference": "83ec067b12c066fc61d9fb129daf7e61ef3b1d63"
+ "reference": "aacc3759a508a69dfa5bc4dfa770ab733c5c94bf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/Math/zipball/83ec067b12c066fc61d9fb129daf7e61ef3b1d63",
- "reference": "83ec067b12c066fc61d9fb129daf7e61ef3b1d63",
+ "url": "https://api.github.com/repos/pmmp/Math/zipball/aacc3759a508a69dfa5bc4dfa770ab733c5c94bf",
+ "reference": "aacc3759a508a69dfa5bc4dfa770ab733c5c94bf",
"shasum": ""
},
"require": {
- "php": "^7.4 || ^8.0",
+ "php": "^8.0",
"php-64bit": "*"
},
"require-dev": {
- "irstea/phpunit-shim": "^8.5 || ^9.5",
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "0.12.90",
- "phpstan/phpstan-strict-rules": "^0.12.4"
+ "phpstan/phpstan": "1.2.0",
+ "phpstan/phpstan-strict-rules": "^1.0",
+ "phpunit/phpunit": "^8.5 || ^9.5"
},
"type": "library",
"autoload": {
@@ -623,22 +679,22 @@
"description": "PHP library containing math related code used in PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/Math/issues",
- "source": "https://github.com/pmmp/Math/tree/0.3.0"
+ "source": "https://github.com/pmmp/Math/tree/0.4.2"
},
- "time": "2021-07-14T18:39:31+00:00"
+ "time": "2021-12-05T01:15:17+00:00"
},
{
"name": "pocketmine/nbt",
- "version": "0.3.0",
+ "version": "0.3.2",
"source": {
"type": "git",
"url": "https://github.com/pmmp/NBT.git",
- "reference": "98c4a04b55a915e18f83d3b0c9beb24a71abcd31"
+ "reference": "3e0d9ef6b6c5fb45e3745a121296e75631b3eefe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/NBT/zipball/98c4a04b55a915e18f83d3b0c9beb24a71abcd31",
- "reference": "98c4a04b55a915e18f83d3b0c9beb24a71abcd31",
+ "url": "https://api.github.com/repos/pmmp/NBT/zipball/3e0d9ef6b6c5fb45e3745a121296e75631b3eefe",
+ "reference": "3e0d9ef6b6c5fb45e3745a121296e75631b3eefe",
"shasum": ""
},
"require": {
@@ -647,10 +703,10 @@
"pocketmine/binaryutils": "^0.2.0"
},
"require-dev": {
- "irstea/phpunit-shim": "^9.5",
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "0.12.85",
- "phpstan/phpstan-strict-rules": "^0.12.4"
+ "phpstan/phpstan": "1.2.0",
+ "phpstan/phpstan-strict-rules": "^1.0",
+ "phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
@@ -665,22 +721,22 @@
"description": "PHP library for working with Named Binary Tags",
"support": {
"issues": "https://github.com/pmmp/NBT/issues",
- "source": "https://github.com/pmmp/NBT/tree/0.3.0"
+ "source": "https://github.com/pmmp/NBT/tree/0.3.2"
},
- "time": "2021-05-18T15:46:33+00:00"
+ "time": "2021-12-16T01:02:37+00:00"
},
{
"name": "pocketmine/raklib",
- "version": "0.14.0",
+ "version": "0.14.3",
"source": {
"type": "git",
"url": "https://github.com/pmmp/RakLib.git",
- "reference": "ed27bfd83f4de5ff32f71ec7611a66c4857a82ce"
+ "reference": "4798576fec0364266dce23b368a7fec5e5de7927"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/RakLib/zipball/ed27bfd83f4de5ff32f71ec7611a66c4857a82ce",
- "reference": "ed27bfd83f4de5ff32f71ec7611a66c4857a82ce",
+ "url": "https://api.github.com/repos/pmmp/RakLib/zipball/4798576fec0364266dce23b368a7fec5e5de7927",
+ "reference": "4798576fec0364266dce23b368a7fec5e5de7927",
"shasum": ""
},
"require": {
@@ -692,8 +748,8 @@
"pocketmine/log": "^0.3.0 || ^0.4.0"
},
"require-dev": {
- "phpstan/phpstan": "0.12.99",
- "phpstan/phpstan-strict-rules": "^0.12.2"
+ "phpstan/phpstan": "1.3.3",
+ "phpstan/phpstan-strict-rules": "^1.0"
},
"type": "library",
"autoload": {
@@ -708,9 +764,9 @@
"description": "A RakNet server implementation written in PHP",
"support": {
"issues": "https://github.com/pmmp/RakLib/issues",
- "source": "https://github.com/pmmp/RakLib/tree/0.14.0"
+ "source": "https://github.com/pmmp/RakLib/tree/0.14.3"
},
- "time": "2021-09-20T21:53:31+00:00"
+ "time": "2022-01-10T21:29:48+00:00"
},
{
"name": "pocketmine/raklib-ipc",
@@ -755,25 +811,25 @@
},
{
"name": "pocketmine/snooze",
- "version": "0.3.0",
+ "version": "0.3.1",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Snooze.git",
- "reference": "fe5b1dbf0d6267da882d1f67924772bd93db833d"
+ "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pmmp/Snooze/zipball/fe5b1dbf0d6267da882d1f67924772bd93db833d",
- "reference": "fe5b1dbf0d6267da882d1f67924772bd93db833d",
+ "url": "https://api.github.com/repos/pmmp/Snooze/zipball/0ac8fc2a781c419a1f64ebca4d5835028f59e29b",
+ "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b",
"shasum": ""
},
"require": {
- "ext-pthreads": ">=3.1.7dev",
+ "ext-pthreads": "~3.2.0 || ^4.0",
"php-64bit": "^7.3 || ^8.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "0.12.88",
+ "phpstan/phpstan": "0.12.99",
"phpstan/phpstan-strict-rules": "^0.12.4"
},
"type": "library",
@@ -789,63 +845,27 @@
"description": "Thread notification management library for code using the pthreads extension",
"support": {
"issues": "https://github.com/pmmp/Snooze/issues",
- "source": "https://github.com/pmmp/Snooze/tree/0.3.0"
+ "source": "https://github.com/pmmp/Snooze/tree/0.3.1"
},
- "time": "2021-06-13T13:57:47+00:00"
- },
- {
- "name": "pocketmine/spl",
- "version": "dev-master",
- "source": {
- "type": "git",
- "url": "https://github.com/pmmp/SPL.git",
- "reference": "b7a8904f912c1f6d38ad867ff1120614ccb80171"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/pmmp/SPL/zipball/b7a8904f912c1f6d38ad867ff1120614ccb80171",
- "reference": "b7a8904f912c1f6d38ad867ff1120614ccb80171",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "require-dev": {
- "phpstan/phpstan": "^0.12.8"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "./src"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-3.0"
- ],
- "description": "Standard library files required by PocketMine-MP and related projects",
- "support": {
- "issues": "https://github.com/pmmp/SPL/issues",
- "source": "https://github.com/pmmp/SPL/tree/master"
- },
- "time": "2021-01-15T15:19:34+00:00"
+ "time": "2021-11-01T20:50:08+00:00"
},
{
"name": "ramsey/collection",
- "version": "1.1.3",
+ "version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/ramsey/collection.git",
- "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1"
+ "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
- "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+ "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8"
+ "php": "^7.3 || ^8",
+ "symfony/polyfill-php81": "^1.23"
},
"require-dev": {
"captainhook/captainhook": "^5.3",
@@ -855,6 +875,7 @@
"hamcrest/hamcrest-php": "^2",
"jangregor/phpstan-prophecy": "^0.8",
"mockery/mockery": "^1.3",
+ "phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1",
"phpstan/phpstan": "^0.12.32",
"phpstan/phpstan-mockery": "^0.12.5",
@@ -882,7 +903,7 @@
"homepage": "https://benramsey.com"
}
],
- "description": "A PHP 7.2+ library for representing and manipulating collections.",
+ "description": "A PHP library for representing and manipulating collections.",
"keywords": [
"array",
"collection",
@@ -893,7 +914,7 @@
],
"support": {
"issues": "https://github.com/ramsey/collection/issues",
- "source": "https://github.com/ramsey/collection/tree/1.1.3"
+ "source": "https://github.com/ramsey/collection/tree/1.2.2"
},
"funding": [
{
@@ -905,28 +926,29 @@
"type": "tidelift"
}
],
- "time": "2021-01-21T17:40:04+00:00"
+ "time": "2021-10-10T03:01:02+00:00"
},
{
"name": "ramsey/uuid",
- "version": "4.2.1",
+ "version": "4.2.3",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
- "reference": "fe665a03df4f056aa65af552a96e1976df8c8dae"
+ "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/uuid/zipball/fe665a03df4f056aa65af552a96e1976df8c8dae",
- "reference": "fe665a03df4f056aa65af552a96e1976df8c8dae",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
+ "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
"shasum": ""
},
"require": {
"brick/math": "^0.8 || ^0.9",
"ext-json": "*",
- "php": "^7.2 || ^8",
+ "php": "^7.2 || ^8.0",
"ramsey/collection": "^1.0",
- "symfony/polyfill-ctype": "^1.8"
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-php80": "^1.14"
},
"replace": {
"rhumsaa/uuid": "self.version"
@@ -990,7 +1012,7 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
- "source": "https://github.com/ramsey/uuid/tree/4.2.1"
+ "source": "https://github.com/ramsey/uuid/tree/4.2.3"
},
"funding": [
{
@@ -1002,149 +1024,28 @@
"type": "tidelift"
}
],
- "time": "2021-08-11T01:06:55+00:00"
- },
- {
- "name": "respect/stringifier",
- "version": "0.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/Respect/Stringifier.git",
- "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
- "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "require-dev": {
- "friendsofphp/php-cs-fixer": "^2.8",
- "malukenho/docheader": "^0.1.7",
- "phpunit/phpunit": "^6.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Respect\\Stringifier\\": "src/"
- },
- "files": [
- "src/stringify.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Respect/Stringifier Contributors",
- "homepage": "https://github.com/Respect/Stringifier/graphs/contributors"
- }
- ],
- "description": "Converts any value to a string",
- "homepage": "http://respect.github.io/Stringifier/",
- "keywords": [
- "respect",
- "stringifier",
- "stringify"
- ],
- "support": {
- "issues": "https://github.com/Respect/Stringifier/issues",
- "source": "https://github.com/Respect/Stringifier/tree/0.2.0"
- },
- "time": "2017-12-29T19:39:25+00:00"
- },
- {
- "name": "respect/validation",
- "version": "2.2.3",
- "source": {
- "type": "git",
- "url": "https://github.com/Respect/Validation.git",
- "reference": "4c21a7ffc9a4915673cb2c2843963919e664e627"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Respect/Validation/zipball/4c21a7ffc9a4915673cb2c2843963919e664e627",
- "reference": "4c21a7ffc9a4915673cb2c2843963919e664e627",
- "shasum": ""
- },
- "require": {
- "php": "^7.3 || ^8.0",
- "respect/stringifier": "^0.2.0",
- "symfony/polyfill-mbstring": "^1.2"
- },
- "require-dev": {
- "egulias/email-validator": "^3.0",
- "malukenho/docheader": "^0.1",
- "mikey179/vfsstream": "^1.6",
- "phpstan/phpstan": "^0.12",
- "phpstan/phpstan-deprecation-rules": "^0.12",
- "phpstan/phpstan-phpunit": "^0.12",
- "phpunit/phpunit": "^9.3",
- "psr/http-message": "^1.0",
- "respect/coding-standard": "^3.0",
- "squizlabs/php_codesniffer": "^3.5",
- "symfony/validator": "^3.0||^4.0",
- "zendframework/zend-validator": "^2.1"
- },
- "suggest": {
- "egulias/email-validator": "Strict (RFC compliant) email validation",
- "ext-bcmath": "Arbitrary Precision Mathematics",
- "ext-fileinfo": "File Information",
- "ext-mbstring": "Multibyte String Functions",
- "symfony/validator": "Use Symfony validator through Respect\\Validation",
- "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Respect\\Validation\\": "library/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Respect/Validation Contributors",
- "homepage": "https://github.com/Respect/Validation/graphs/contributors"
- }
- ],
- "description": "The most awesome validation engine ever created for PHP",
- "homepage": "http://respect.github.io/Validation/",
- "keywords": [
- "respect",
- "validation",
- "validator"
- ],
- "support": {
- "issues": "https://github.com/Respect/Validation/issues",
- "source": "https://github.com/Respect/Validation/tree/2.2.3"
- },
- "time": "2021-03-19T14:12:45+00:00"
+ "time": "2021-09-25T23:10:38+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.23.0",
+ "version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
+ "reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
- "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
+ "reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
+ "provide": {
+ "ext-ctype": "*"
+ },
"suggest": {
"ext-ctype": "For best performance"
},
@@ -1189,7 +1090,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0"
},
"funding": [
{
@@ -1205,28 +1106,25 @@
"type": "tidelift"
}
],
- "time": "2021-02-19T12:13:01+00:00"
+ "time": "2021-10-20T20:35:02+00:00"
},
{
- "name": "symfony/polyfill-mbstring",
- "version": "v1.23.0",
+ "name": "symfony/polyfill-php80",
+ "version": "v1.24.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
- "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9",
+ "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
- "suggest": {
- "ext-mbstring": "For best performance"
- },
"type": "library",
"extra": {
"branch-alias": {
@@ -1239,10 +1137,96 @@
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
+ "Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-09-13T13:58:33+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php81",
+ "version": "v1.24.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+ "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.23-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -1259,17 +1243,16 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill for the Mbstring extension",
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
- "mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.24.0"
},
"funding": [
{
@@ -1285,7 +1268,7 @@
"type": "tidelift"
}
],
- "time": "2021-05-27T09:27:20+00:00"
+ "time": "2021-09-13T13:58:11+00:00"
},
{
"name": "webmozart/assert",
@@ -1393,6 +1376,7 @@
"issues": "https://github.com/webmozart/path-util/issues",
"source": "https://github.com/webmozart/path-util/tree/2.3.0"
},
+ "abandoned": "symfony/filesystem",
"time": "2015-12-17T08:42:14+00:00"
}
],
@@ -1483,9 +1467,6 @@
"require": {
"php": "^7.1 || ^8.0"
},
- "replace": {
- "myclabs/deep-copy": "self.version"
- },
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
@@ -1526,16 +1507,16 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.12.0",
+ "version": "v4.13.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "6608f01670c3cc5079e18c1dab1104e002579143"
+ "reference": "210577fe3cf7badcc5814d99455df46564f3c077"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143",
- "reference": "6608f01670c3cc5079e18c1dab1104e002579143",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077",
+ "reference": "210577fe3cf7badcc5814d99455df46564f3c077",
"shasum": ""
},
"require": {
@@ -1576,9 +1557,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2"
},
- "time": "2021-07-21T10:44:31+00:00"
+ "time": "2021-11-30T19:35:32+00:00"
},
{
"name": "phar-io/manifest",
@@ -1746,16 +1727,16 @@
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "5.2.2",
+ "version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
+ "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
- "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
+ "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
"shasum": ""
},
"require": {
@@ -1766,7 +1747,8 @@
"webmozart/assert": "^1.9.1"
},
"require-dev": {
- "mockery/mockery": "~1.3.2"
+ "mockery/mockery": "~1.3.2",
+ "psalm/phar": "^4.8"
},
"type": "library",
"extra": {
@@ -1796,22 +1778,22 @@
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
},
- "time": "2020-09-03T19:13:55+00:00"
+ "time": "2021-10-19T17:43:47+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "1.4.0",
+ "version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+ "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
- "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706",
+ "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706",
"shasum": ""
},
"require": {
@@ -1819,7 +1801,8 @@
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
- "ext-tokenizer": "*"
+ "ext-tokenizer": "*",
+ "psalm/phar": "^4.8"
},
"type": "library",
"extra": {
@@ -1845,39 +1828,39 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
- "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0"
},
- "time": "2020-09-17T18:55:26+00:00"
+ "time": "2022-01-04T19:58:01+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "1.13.0",
+ "version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+ "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
- "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
+ "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2",
- "php": "^7.2 || ~8.0, <8.1",
+ "php": "^7.2 || ~8.0, <8.2",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
- "phpspec/phpspec": "^6.0",
+ "phpspec/phpspec": "^6.0 || ^7.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.11.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
@@ -1912,22 +1895,22 @@
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
- "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+ "source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
},
- "time": "2021-03-17T13:42:18+00:00"
+ "time": "2021-12-08T12:19:24+00:00"
},
{
"name": "phpstan/phpstan",
- "version": "0.12.98",
+ "version": "1.3.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00"
+ "reference": "151a51f6149855785fbd883e79768c0abc96b75f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3bb7cc246c057405dd5e290c3ecc62ab51d57e00",
- "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/151a51f6149855785fbd883e79768c0abc96b75f",
+ "reference": "151a51f6149855785fbd883e79768c0abc96b75f",
"shasum": ""
},
"require": {
@@ -1943,7 +1926,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "0.12-dev"
+ "dev-master": "1.3-dev"
}
},
"autoload": {
@@ -1958,7 +1941,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
- "source": "https://github.com/phpstan/phpstan/tree/0.12.98"
+ "source": "https://github.com/phpstan/phpstan/tree/1.3.3"
},
"funding": [
{
@@ -1978,38 +1961,39 @@
"type": "tidelift"
}
],
- "time": "2021-09-02T12:33:01+00:00"
+ "time": "2022-01-07T09:49:03+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
- "version": "0.12.22",
+ "version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
- "reference": "7c01ef93bf128b4ac8bdad38c54b2a4fd6b0b3cc"
+ "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/7c01ef93bf128b4ac8bdad38c54b2a4fd6b0b3cc",
- "reference": "7c01ef93bf128b4ac8bdad38c54b2a4fd6b0b3cc",
+ "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3",
+ "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
- "phpstan/phpstan": "^0.12.92"
+ "phpstan/phpstan": "^1.0"
},
"conflict": {
"phpunit/phpunit": "<7.0"
},
"require-dev": {
+ "nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
- "phpstan/phpstan-strict-rules": "^0.12.6",
+ "phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
- "dev-master": "0.12-dev"
+ "dev-master": "1.0-dev"
},
"phpstan": {
"includes": [
@@ -2030,37 +2014,38 @@
"description": "PHPUnit extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
- "source": "https://github.com/phpstan/phpstan-phpunit/tree/0.12.22"
+ "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0"
},
- "time": "2021-08-12T10:53:43+00:00"
+ "time": "2021-10-14T08:03:54+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
- "version": "0.12.11",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
- "reference": "2b72e8e17d2034145f239126e876e5fb659675e2"
+ "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2b72e8e17d2034145f239126e876e5fb659675e2",
- "reference": "2b72e8e17d2034145f239126e876e5fb659675e2",
+ "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/e12d55f74a8cca18c6e684c6450767e055ba7717",
+ "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
- "phpstan/phpstan": "^0.12.96"
+ "phpstan/phpstan": "^1.2.0"
},
"require-dev": {
+ "nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
- "phpstan/phpstan-phpunit": "^0.12.16",
+ "phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
- "dev-master": "0.12-dev"
+ "dev-master": "1.0-dev"
},
"phpstan": {
"includes": [
@@ -2080,29 +2065,29 @@
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
- "source": "https://github.com/phpstan/phpstan-strict-rules/tree/0.12.11"
+ "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.1.0"
},
- "time": "2021-08-21T11:36:27+00:00"
+ "time": "2021-11-18T09:30:29+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.6",
+ "version": "9.2.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "f6293e1b30a2354e8428e004689671b83871edde"
+ "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
- "reference": "f6293e1b30a2354e8428e004689671b83871edde",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687",
+ "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^4.10.2",
+ "nikic/php-parser": "^4.13.0",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@@ -2151,7 +2136,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10"
},
"funding": [
{
@@ -2159,20 +2144,20 @@
"type": "github"
}
],
- "time": "2021-03-28T07:26:59+00:00"
+ "time": "2021-12-05T09:12:13+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "3.0.5",
+ "version": "3.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
- "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
"shasum": ""
},
"require": {
@@ -2211,7 +2196,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5"
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
},
"funding": [
{
@@ -2219,7 +2204,7 @@
"type": "github"
}
],
- "time": "2020-09-28T05:57:25+00:00"
+ "time": "2021-12-02T12:48:52+00:00"
},
{
"name": "phpunit/php-invoker",
@@ -2404,16 +2389,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.5.9",
+ "version": "9.5.13",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b"
+ "reference": "597cb647654ede35e43b137926dfdfef0fb11743"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
- "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/597cb647654ede35e43b137926dfdfef0fb11743",
+ "reference": "597cb647654ede35e43b137926dfdfef0fb11743",
"shasum": ""
},
"require": {
@@ -2429,7 +2414,7 @@
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpspec/prophecy": "^1.12.1",
- "phpunit/php-code-coverage": "^9.2.3",
+ "phpunit/php-code-coverage": "^9.2.7",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
"phpunit/php-text-template": "^2.0.3",
@@ -2491,11 +2476,11 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.13"
},
"funding": [
{
- "url": "https://phpunit.de/donate.html",
+ "url": "https://phpunit.de/sponsors.html",
"type": "custom"
},
{
@@ -2503,7 +2488,7 @@
"type": "github"
}
],
- "time": "2021-08-31T06:47:40+00:00"
+ "time": "2022-01-24T07:33:35+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -2934,16 +2919,16 @@
},
{
"name": "sebastian/exporter",
- "version": "4.0.3",
+ "version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65"
+ "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65",
- "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9",
+ "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9",
"shasum": ""
},
"require": {
@@ -2992,14 +2977,14 @@
}
],
"description": "Provides the functionality to export PHP variables for visualization",
- "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4"
},
"funding": [
{
@@ -3007,7 +2992,7 @@
"type": "github"
}
],
- "time": "2020-09-28T05:24:23+00:00"
+ "time": "2021-11-11T14:18:36+00:00"
},
{
"name": "sebastian/global-state",
@@ -3358,7 +3343,6 @@
"type": "github"
}
],
- "abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{
@@ -3523,16 +3507,13 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": {
- "pocketmine/classloader": 20,
- "pocketmine/spl": 20
- },
+ "stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.0",
"php-64bit": "*",
- "ext-chunkutils2": "^0.3.0",
+ "ext-chunkutils2": "^0.3.1",
"ext-crypto": "^0.3.1",
"ext-ctype": "*",
"ext-curl": "*",
@@ -3547,7 +3528,7 @@
"ext-openssl": "*",
"ext-pcre": "*",
"ext-phar": "*",
- "ext-pthreads": "~3.2.0",
+ "ext-pthreads": "^4.0",
"ext-reflection": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
@@ -3561,5 +3542,5 @@
"platform-overrides": {
"php": "8.0.0"
},
- "plugin-api-version": "2.1.0"
+ "plugin-api-version": "2.2.0"
}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 762eaefc9..9ebc98502 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -1,13 +1,9 @@
includes:
- tests/phpstan/configs/actual-problems.neon
- - tests/phpstan/configs/check-explicit-mixed-baseline.neon
- tests/phpstan/configs/gc-hacks.neon
- tests/phpstan/configs/impossible-generics.neon
- - tests/phpstan/configs/l7-baseline.neon
- - tests/phpstan/configs/l8-baseline.neon
- tests/phpstan/configs/php-bugs.neon
- tests/phpstan/configs/phpstan-bugs.neon
- - tests/phpstan/configs/pthreads-bugs.neon
- tests/phpstan/configs/runtime-type-checks.neon
- tests/phpstan/configs/spl-fixed-array-sucks.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
@@ -16,11 +12,11 @@ includes:
rules:
- pocketmine\phpstan\rules\DisallowEnumComparisonRule
+ - pocketmine\phpstan\rules\UnsafeForeachArrayOfStringRule
# - pocketmine\phpstan\rules\ThreadedSupportedTypesRule
parameters:
- level: 8
- checkExplicitMixed: true
+ level: 9
checkMissingCallableSignature: true
treatPhpDocTypesAsCertain: false
bootstrapFiles:
@@ -38,6 +34,9 @@ parameters:
- tests/phpunit
- tests/plugins/TesterPlugin
- tools
+ excludePaths:
+ analyseAndScan:
+ - build/php
dynamicConstantNames:
- pocketmine\VersionInfo::IS_DEVELOPMENT_BUILD
- pocketmine\DEBUG
@@ -54,5 +53,5 @@ parameters:
#variadics don't work for this - mixed probably shouldn't work either, but for now it does
#what we actually need is something that accepts an infinite number of parameters, but in the absence of that,
#we'll just fill it with 10 - it's very unlikely to encounter a callable with 10 parameters anyway.
- anyCallable: 'callable(mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed) : mixed'
- anyClosure: '\Closure(mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed) : mixed'
+ anyCallable: 'callable(never, never, never, never, never, never, never, never, never, never) : mixed'
+ anyClosure: '\Closure(never, never, never, never, never, never, never, never, never, never) : mixed'
diff --git a/resources/item_from_string_bc_map.json b/resources/item_from_string_bc_map.json
index 0edc1e7df..c5c502e9a 100644
--- a/resources/item_from_string_bc_map.json
+++ b/resources/item_from_string_bc_map.json
@@ -1,4 +1,691 @@
{
+ "-2": -2,
+ "-3": -3,
+ "-4": -4,
+ "-5": -5,
+ "-6": -6,
+ "-7": -7,
+ "-8": -8,
+ "-9": -9,
+ "-10": -10,
+ "-11": -11,
+ "-12": -12,
+ "-13": -13,
+ "-14": -14,
+ "-15": -15,
+ "-16": -16,
+ "-17": -17,
+ "-18": -18,
+ "-19": -19,
+ "-20": -20,
+ "-21": -21,
+ "-22": -22,
+ "-23": -23,
+ "-24": -24,
+ "-25": -25,
+ "-26": -26,
+ "-27": -27,
+ "-28": -28,
+ "-29": -29,
+ "-30": -30,
+ "-31": -31,
+ "-32": -32,
+ "-33": -33,
+ "-34": -34,
+ "-35": -35,
+ "-36": -36,
+ "-37": -37,
+ "-38": -38,
+ "-39": -39,
+ "-40": -40,
+ "-41": -41,
+ "-42": -42,
+ "-43": -43,
+ "-44": -44,
+ "-45": -45,
+ "-46": -46,
+ "-47": -47,
+ "-48": -48,
+ "-49": -49,
+ "-50": -50,
+ "-51": -51,
+ "-52": -52,
+ "-53": -53,
+ "-54": -54,
+ "-55": -55,
+ "-56": -56,
+ "-57": -57,
+ "-58": -58,
+ "-59": -59,
+ "-60": -60,
+ "-61": -61,
+ "-62": -62,
+ "-63": -63,
+ "-64": -64,
+ "-65": -65,
+ "-66": -66,
+ "-67": -67,
+ "-68": -68,
+ "-69": -69,
+ "-70": -70,
+ "-71": -71,
+ "-72": -72,
+ "-73": -73,
+ "-74": -74,
+ "-75": -75,
+ "-76": -76,
+ "-77": -77,
+ "-78": -78,
+ "-79": -79,
+ "-80": -80,
+ "-81": -81,
+ "-82": -82,
+ "-83": -83,
+ "-84": -84,
+ "-85": -85,
+ "-86": -86,
+ "-87": -87,
+ "-88": -88,
+ "-89": -89,
+ "-90": -90,
+ "-91": -91,
+ "-92": -92,
+ "-93": -93,
+ "-94": -94,
+ "-95": -95,
+ "-96": -96,
+ "-97": -97,
+ "-98": -98,
+ "-99": -99,
+ "-100": -100,
+ "-101": -101,
+ "-102": -102,
+ "-103": -103,
+ "-104": -104,
+ "-105": -105,
+ "-106": -106,
+ "-107": -107,
+ "-108": -108,
+ "-109": -109,
+ "-110": -110,
+ "-111": -111,
+ "-112": -112,
+ "-113": -113,
+ "-114": -114,
+ "-115": -115,
+ "-116": -116,
+ "-117": -117,
+ "-118": -118,
+ "-119": -119,
+ "-120": -120,
+ "-121": -121,
+ "-122": -122,
+ "-123": -123,
+ "-124": -124,
+ "-125": -125,
+ "-126": -126,
+ "-127": -127,
+ "-128": -128,
+ "-129": -129,
+ "-130": -130,
+ "-131": -131,
+ "-132": -132,
+ "-133": -133,
+ "-134": -134,
+ "-135": -135,
+ "-136": -136,
+ "-137": -137,
+ "-138": -138,
+ "-139": -139,
+ "-140": -140,
+ "-141": -141,
+ "-142": -142,
+ "-143": -143,
+ "-144": -144,
+ "-145": -145,
+ "-146": -146,
+ "-147": -147,
+ "-148": -148,
+ "-149": -149,
+ "-150": -150,
+ "-151": -151,
+ "-152": -152,
+ "-153": -153,
+ "-154": -154,
+ "-155": -155,
+ "-156": -156,
+ "-157": -157,
+ "-159": -159,
+ "-160": -160,
+ "-161": -161,
+ "-162": -162,
+ "-163": -163,
+ "-164": -164,
+ "-165": -165,
+ "-166": -166,
+ "-167": -167,
+ "-168": -168,
+ "-169": -169,
+ "-170": -170,
+ "-171": -171,
+ "-172": -172,
+ "-173": -173,
+ "-174": -174,
+ "-175": -175,
+ "-176": -176,
+ "-177": -177,
+ "-178": -178,
+ "-179": -179,
+ "-180": -180,
+ "-181": -181,
+ "-182": -182,
+ "-183": -183,
+ "-184": -184,
+ "-185": -185,
+ "-186": -186,
+ "-187": -187,
+ "-188": -188,
+ "-189": -189,
+ "-190": -190,
+ "-191": -191,
+ "-192": -192,
+ "-193": -193,
+ "-194": -194,
+ "-195": -195,
+ "-196": -196,
+ "-197": -197,
+ "-198": -198,
+ "-199": -199,
+ "-200": -200,
+ "-201": -201,
+ "-202": -202,
+ "-203": -203,
+ "-204": -204,
+ "-206": -206,
+ "-207": -207,
+ "-208": -208,
+ "-209": -209,
+ "-210": -210,
+ "-211": -211,
+ "-213": -213,
+ "-214": -214,
+ "0": 0,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "7": 7,
+ "8": 8,
+ "9": 9,
+ "10": 10,
+ "11": 11,
+ "12": 12,
+ "13": 13,
+ "14": 14,
+ "15": 15,
+ "16": 16,
+ "17": 17,
+ "18": 18,
+ "19": 19,
+ "20": 20,
+ "21": 21,
+ "22": 22,
+ "23": 23,
+ "24": 24,
+ "25": 25,
+ "26": 26,
+ "27": 27,
+ "28": 28,
+ "29": 29,
+ "30": 30,
+ "31": 31,
+ "32": 32,
+ "33": 33,
+ "34": 34,
+ "35": 35,
+ "36": 36,
+ "37": 37,
+ "38": 38,
+ "39": 39,
+ "40": 40,
+ "41": 41,
+ "42": 42,
+ "43": 43,
+ "44": 44,
+ "45": 45,
+ "46": 46,
+ "47": 47,
+ "48": 48,
+ "49": 49,
+ "50": 50,
+ "51": 51,
+ "52": 52,
+ "53": 53,
+ "54": 54,
+ "55": 55,
+ "56": 56,
+ "57": 57,
+ "58": 58,
+ "59": 59,
+ "60": 60,
+ "61": 61,
+ "62": 62,
+ "63": 63,
+ "64": 64,
+ "65": 65,
+ "66": 66,
+ "67": 67,
+ "68": 68,
+ "69": 69,
+ "70": 70,
+ "71": 71,
+ "72": 72,
+ "73": 73,
+ "74": 74,
+ "75": 75,
+ "76": 76,
+ "77": 77,
+ "78": 78,
+ "79": 79,
+ "80": 80,
+ "81": 81,
+ "82": 82,
+ "83": 83,
+ "84": 84,
+ "85": 85,
+ "86": 86,
+ "87": 87,
+ "88": 88,
+ "89": 89,
+ "90": 90,
+ "91": 91,
+ "92": 92,
+ "93": 93,
+ "94": 94,
+ "95": 95,
+ "96": 96,
+ "97": 97,
+ "98": 98,
+ "99": 99,
+ "100": 100,
+ "101": 101,
+ "102": 102,
+ "103": 103,
+ "104": 104,
+ "105": 105,
+ "106": 106,
+ "107": 107,
+ "108": 108,
+ "109": 109,
+ "110": 110,
+ "111": 111,
+ "112": 112,
+ "113": 113,
+ "114": 114,
+ "115": 115,
+ "116": 116,
+ "117": 117,
+ "118": 118,
+ "119": 119,
+ "120": 120,
+ "121": 121,
+ "122": 122,
+ "123": 123,
+ "124": 124,
+ "125": 125,
+ "126": 126,
+ "127": 127,
+ "128": 128,
+ "129": 129,
+ "130": 130,
+ "131": 131,
+ "132": 132,
+ "133": 133,
+ "134": 134,
+ "135": 135,
+ "136": 136,
+ "137": 137,
+ "138": 138,
+ "139": 139,
+ "140": 140,
+ "141": 141,
+ "142": 142,
+ "143": 143,
+ "144": 144,
+ "145": 145,
+ "146": 146,
+ "147": 147,
+ "148": 148,
+ "149": 149,
+ "150": 150,
+ "151": 151,
+ "152": 152,
+ "153": 153,
+ "154": 154,
+ "155": 155,
+ "156": 156,
+ "157": 157,
+ "158": 158,
+ "159": 159,
+ "160": 160,
+ "161": 161,
+ "162": 162,
+ "163": 163,
+ "164": 164,
+ "165": 165,
+ "166": 166,
+ "167": 167,
+ "168": 168,
+ "169": 169,
+ "170": 170,
+ "171": 171,
+ "172": 172,
+ "173": 173,
+ "174": 174,
+ "175": 175,
+ "176": 176,
+ "177": 177,
+ "178": 178,
+ "179": 179,
+ "180": 180,
+ "181": 181,
+ "182": 182,
+ "183": 183,
+ "184": 184,
+ "185": 185,
+ "186": 186,
+ "187": 187,
+ "188": 188,
+ "189": 189,
+ "190": 190,
+ "191": 191,
+ "192": 192,
+ "193": 193,
+ "194": 194,
+ "195": 195,
+ "196": 196,
+ "197": 197,
+ "198": 198,
+ "199": 199,
+ "200": 200,
+ "201": 201,
+ "202": 202,
+ "203": 203,
+ "204": 204,
+ "205": 205,
+ "206": 206,
+ "207": 207,
+ "208": 208,
+ "209": 209,
+ "213": 213,
+ "214": 214,
+ "215": 215,
+ "216": 216,
+ "218": 218,
+ "219": 219,
+ "220": 220,
+ "221": 221,
+ "222": 222,
+ "223": 223,
+ "224": 224,
+ "225": 225,
+ "226": 226,
+ "227": 227,
+ "228": 228,
+ "229": 229,
+ "231": 231,
+ "232": 232,
+ "233": 233,
+ "234": 234,
+ "235": 235,
+ "236": 236,
+ "237": 237,
+ "238": 238,
+ "239": 239,
+ "240": 240,
+ "241": 241,
+ "243": 243,
+ "244": 244,
+ "245": 245,
+ "246": 246,
+ "247": 247,
+ "248": 248,
+ "249": 249,
+ "250": 250,
+ "251": 251,
+ "252": 252,
+ "253": 253,
+ "254": 254,
+ "255": 255,
+ "256": 256,
+ "257": 257,
+ "258": 258,
+ "259": 259,
+ "260": 260,
+ "261": 261,
+ "262": 262,
+ "263": 263,
+ "264": 264,
+ "265": 265,
+ "266": 266,
+ "267": 267,
+ "268": 268,
+ "269": 269,
+ "270": 270,
+ "271": 271,
+ "272": 272,
+ "273": 273,
+ "274": 274,
+ "275": 275,
+ "276": 276,
+ "277": 277,
+ "278": 278,
+ "279": 279,
+ "280": 280,
+ "281": 281,
+ "282": 282,
+ "283": 283,
+ "284": 284,
+ "285": 285,
+ "286": 286,
+ "287": 287,
+ "288": 288,
+ "289": 289,
+ "290": 290,
+ "291": 291,
+ "292": 292,
+ "293": 293,
+ "294": 294,
+ "295": 295,
+ "296": 296,
+ "297": 297,
+ "298": 298,
+ "299": 299,
+ "300": 300,
+ "301": 301,
+ "302": 302,
+ "303": 303,
+ "304": 304,
+ "305": 305,
+ "306": 306,
+ "307": 307,
+ "308": 308,
+ "309": 309,
+ "310": 310,
+ "311": 311,
+ "312": 312,
+ "313": 313,
+ "314": 314,
+ "315": 315,
+ "316": 316,
+ "317": 317,
+ "318": 318,
+ "319": 319,
+ "320": 320,
+ "321": 321,
+ "322": 322,
+ "323": 323,
+ "324": 324,
+ "325": 325,
+ "328": 328,
+ "329": 329,
+ "330": 330,
+ "331": 331,
+ "332": 332,
+ "333": 333,
+ "334": 334,
+ "335": 335,
+ "336": 336,
+ "337": 337,
+ "338": 338,
+ "339": 339,
+ "340": 340,
+ "341": 341,
+ "342": 342,
+ "344": 344,
+ "345": 345,
+ "346": 346,
+ "347": 347,
+ "348": 348,
+ "349": 349,
+ "350": 350,
+ "351": 351,
+ "352": 352,
+ "353": 353,
+ "354": 354,
+ "355": 355,
+ "356": 356,
+ "357": 357,
+ "358": 358,
+ "359": 359,
+ "360": 360,
+ "361": 361,
+ "362": 362,
+ "363": 363,
+ "364": 364,
+ "365": 365,
+ "366": 366,
+ "367": 367,
+ "368": 368,
+ "369": 369,
+ "370": 370,
+ "371": 371,
+ "372": 372,
+ "373": 373,
+ "374": 374,
+ "375": 375,
+ "376": 376,
+ "377": 377,
+ "378": 378,
+ "379": 379,
+ "380": 380,
+ "381": 381,
+ "382": 382,
+ "383": 383,
+ "384": 384,
+ "385": 385,
+ "386": 386,
+ "387": 387,
+ "388": 388,
+ "389": 389,
+ "390": 390,
+ "391": 391,
+ "392": 392,
+ "393": 393,
+ "394": 394,
+ "395": 395,
+ "396": 396,
+ "397": 397,
+ "398": 398,
+ "399": 399,
+ "400": 400,
+ "401": 401,
+ "402": 402,
+ "403": 403,
+ "404": 404,
+ "405": 405,
+ "406": 406,
+ "407": 407,
+ "408": 408,
+ "409": 409,
+ "410": 410,
+ "411": 411,
+ "412": 412,
+ "413": 413,
+ "414": 414,
+ "415": 415,
+ "416": 416,
+ "417": 417,
+ "418": 418,
+ "419": 419,
+ "420": 420,
+ "421": 421,
+ "422": 422,
+ "423": 423,
+ "424": 424,
+ "425": 425,
+ "426": 426,
+ "427": 427,
+ "428": 428,
+ "429": 429,
+ "430": 430,
+ "431": 431,
+ "432": 432,
+ "433": 433,
+ "434": 434,
+ "437": 437,
+ "438": 438,
+ "441": 441,
+ "442": 442,
+ "443": 443,
+ "444": 444,
+ "445": 445,
+ "446": 446,
+ "447": 447,
+ "448": 448,
+ "449": 449,
+ "450": 450,
+ "451": 451,
+ "452": 452,
+ "453": 453,
+ "455": 455,
+ "457": 457,
+ "458": 458,
+ "459": 459,
+ "460": 460,
+ "461": 461,
+ "462": 462,
+ "463": 463,
+ "464": 464,
+ "465": 465,
+ "466": 466,
+ "467": 467,
+ "468": 468,
+ "469": 469,
+ "470": 470,
+ "471": 471,
+ "472": 472,
+ "473": 473,
+ "474": 474,
+ "475": 475,
+ "476": 476,
+ "477": 477,
+ "499": 499,
+ "500": 500,
+ "501": 501,
+ "502": 502,
+ "503": 503,
+ "504": 504,
+ "505": 505,
+ "506": 506,
+ "507": 507,
+ "508": 508,
+ "509": 509,
+ "510": 510,
+ "511": 511,
+ "513": 513,
"acacia_button": -140,
"acacia_door": 430,
"acacia_door_block": 196,
@@ -226,27 +913,16 @@
"egg": 344,
"element_0": 36,
"element_1": -12,
+ "element_2": -13,
+ "element_3": -14,
+ "element_4": -15,
+ "element_5": -16,
+ "element_6": -17,
+ "element_7": -18,
+ "element_8": -19,
+ "element_9": -20,
"element_10": -21,
- "element_100": -111,
- "element_101": -112,
- "element_102": -113,
- "element_103": -114,
- "element_104": -115,
- "element_105": -116,
- "element_106": -117,
- "element_107": -118,
- "element_108": -119,
- "element_109": -120,
"element_11": -22,
- "element_110": -121,
- "element_111": -122,
- "element_112": -123,
- "element_113": -124,
- "element_114": -125,
- "element_115": -126,
- "element_116": -127,
- "element_117": -128,
- "element_118": -129,
"element_12": -23,
"element_13": -24,
"element_14": -25,
@@ -255,7 +931,6 @@
"element_17": -28,
"element_18": -29,
"element_19": -30,
- "element_2": -13,
"element_20": -31,
"element_21": -32,
"element_22": -33,
@@ -266,7 +941,6 @@
"element_27": -38,
"element_28": -39,
"element_29": -40,
- "element_3": -14,
"element_30": -41,
"element_31": -42,
"element_32": -43,
@@ -277,7 +951,6 @@
"element_37": -48,
"element_38": -49,
"element_39": -50,
- "element_4": -15,
"element_40": -51,
"element_41": -52,
"element_42": -53,
@@ -288,7 +961,6 @@
"element_47": -58,
"element_48": -59,
"element_49": -60,
- "element_5": -16,
"element_50": -61,
"element_51": -62,
"element_52": -63,
@@ -299,7 +971,6 @@
"element_57": -68,
"element_58": -69,
"element_59": -70,
- "element_6": -17,
"element_60": -71,
"element_61": -72,
"element_62": -73,
@@ -310,7 +981,6 @@
"element_67": -78,
"element_68": -79,
"element_69": -80,
- "element_7": -18,
"element_70": -81,
"element_71": -82,
"element_72": -83,
@@ -321,7 +991,6 @@
"element_77": -88,
"element_78": -89,
"element_79": -90,
- "element_8": -19,
"element_80": -91,
"element_81": -92,
"element_82": -93,
@@ -332,7 +1001,6 @@
"element_87": -98,
"element_88": -99,
"element_89": -100,
- "element_9": -20,
"element_90": -101,
"element_91": -102,
"element_92": -103,
@@ -343,6 +1011,25 @@
"element_97": -108,
"element_98": -109,
"element_99": -110,
+ "element_100": -111,
+ "element_101": -112,
+ "element_102": -113,
+ "element_103": -114,
+ "element_104": -115,
+ "element_105": -116,
+ "element_106": -117,
+ "element_107": -118,
+ "element_108": -119,
+ "element_109": -120,
+ "element_110": -121,
+ "element_111": -122,
+ "element_112": -123,
+ "element_113": -124,
+ "element_114": -125,
+ "element_115": -126,
+ "element_116": -127,
+ "element_117": -128,
+ "element_118": -129,
"elytra": 444,
"emerald": 388,
"emerald_block": 133,
@@ -532,8 +1219,8 @@
"leather_pants": 300,
"leather_tunic": 299,
"leave": 18,
- "leaves": 18,
"leave2": 161,
+ "leaves": 18,
"leaves2": 161,
"lectern": -194,
"lever": 69,
diff --git a/resources/locale b/resources/locale
deleted file mode 160000
index 4a322da43..000000000
--- a/resources/locale
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 4a322da43eb9beef727bd36be3d6b641b8316591
diff --git a/resources/pocketmine.yml b/resources/pocketmine.yml
index 3cb7ce430..7d0a337a7 100644
--- a/resources/pocketmine.yml
+++ b/resources/pocketmine.yml
@@ -108,7 +108,7 @@ player:
verify-xuid: true
level-settings:
- #The default format that levels will use when created
+ #The default format that worlds will use when created
default-format: leveldb
chunk-sending:
@@ -128,7 +128,9 @@ chunk-ticking:
blocks-per-subchunk-per-tick: 3
#IDs of blocks not to perform random ticking on.
disable-block-ticking:
- #- 2 # grass
+ #- grass
+ #- ice
+ #- fire
chunk-generation:
#Max. amount of chunks in the waiting queue to be populated
@@ -176,7 +178,7 @@ aliases:
#savestop: [save-all, stop]
worlds:
- #These settings will override the generator set in server.properties and allows loading multiple levels
+ #These settings will override the generator set in server.properties and allows loading multiple worlds
#Example:
#world:
# seed: 404
diff --git a/resources/vanilla b/resources/vanilla
deleted file mode 160000
index 19569dd72..000000000
--- a/resources/vanilla
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 19569dd729970e161a24b574b41c06a5e064ab81
diff --git a/src/CoreConstants.php b/src/CoreConstants.php
index bf88306f1..46e00a2a3 100644
--- a/src/CoreConstants.php
+++ b/src/CoreConstants.php
@@ -35,3 +35,6 @@ define('pocketmine\_CORE_CONSTANTS_INCLUDED', true);
define('pocketmine\PATH', dirname(__DIR__) . '/');
define('pocketmine\RESOURCE_PATH', dirname(__DIR__) . '/resources/');
+define('pocketmine\BEDROCK_DATA_PATH', dirname(__DIR__) . '/vendor/pocketmine/bedrock-data/');
+define('pocketmine\LOCALE_DATA_PATH', dirname(__DIR__) . '/vendor/pocketmine/locale-data/');
+define('pocketmine\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__) . '/vendor/autoload.php');
diff --git a/src/CrashDump.php b/src/CrashDump.php
deleted file mode 100644
index 983ecd4a8..000000000
--- a/src/CrashDump.php
+++ /dev/null
@@ -1,379 +0,0 @@
-
- */
- private $data = [];
- /** @var string */
- private $encodedData = "";
- /** @var string */
- private $path;
-
- public function __construct(Server $server){
- $this->time = microtime(true);
- $this->server = $server;
-
- $crashPath = Path::join($this->server->getDataPath(), "crashdumps");
- if(!is_dir($crashPath)){
- mkdir($crashPath);
- }
- $this->path = Path::join($crashPath, date("D_M_j-H.i.s-T_Y", (int) $this->time) . ".log");
- $fp = @fopen($this->path, "wb");
- if(!is_resource($fp)){
- throw new \RuntimeException("Could not create Crash Dump");
- }
- $this->fp = $fp;
- $this->data["format_version"] = self::FORMAT_VERSION;
- $this->data["time"] = $this->time;
- $this->data["uptime"] = $this->time - $this->server->getStartTime();
- $this->addLine($this->server->getName() . " Crash Dump " . date("D M j H:i:s T Y", (int) $this->time));
- $this->addLine();
- $this->baseCrash();
- $this->generalData();
- $this->pluginsData();
-
- $this->extraData();
-
- $this->encodeData();
-
- fclose($this->fp);
- }
-
- public function getPath() : string{
- return $this->path;
- }
-
- public function getEncodedData() : string{
- return $this->encodedData;
- }
-
- /**
- * @return mixed[]
- * @phpstan-return array
- */
- public function getData() : array{
- return $this->data;
- }
-
- private function encodeData() : void{
- $this->addLine();
- $this->addLine("----------------------REPORT THE DATA BELOW THIS LINE-----------------------");
- $this->addLine();
- $this->addLine("===BEGIN CRASH DUMP===");
- $json = json_encode($this->data, JSON_UNESCAPED_SLASHES);
- if($json === false){
- throw new \RuntimeException("Failed to encode crashdump JSON: " . json_last_error_msg());
- }
- $zlibEncoded = zlib_encode($json, ZLIB_ENCODING_DEFLATE, 9);
- if($zlibEncoded === false) throw new AssumptionFailedError("ZLIB compression failed");
- $this->encodedData = $zlibEncoded;
- foreach(str_split(base64_encode($this->encodedData), 76) as $line){
- $this->addLine($line);
- }
- $this->addLine("===END CRASH DUMP===");
- }
-
- private function pluginsData() : void{
- if($this->server->getPluginManager() instanceof PluginManager){
- $this->addLine();
- $this->addLine("Loaded plugins:");
- $this->data["plugins"] = [];
- $plugins = $this->server->getPluginManager()->getPlugins();
- ksort($plugins, SORT_STRING);
- foreach($plugins as $p){
- $d = $p->getDescription();
- $this->data["plugins"][$d->getName()] = [
- "name" => $d->getName(),
- "version" => $d->getVersion(),
- "authors" => $d->getAuthors(),
- "api" => $d->getCompatibleApis(),
- "enabled" => $p->isEnabled(),
- "depends" => $d->getDepend(),
- "softDepends" => $d->getSoftDepend(),
- "main" => $d->getMain(),
- "load" => mb_strtoupper($d->getOrder()->name()),
- "website" => $d->getWebsite()
- ];
- $this->addLine($d->getName() . " " . $d->getVersion() . " by " . implode(", ", $d->getAuthors()) . " for API(s) " . implode(", ", $d->getCompatibleApis()));
- }
- }
- }
-
- private function extraData() : void{
- global $argv;
-
- if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-settings", true)){
- $this->data["parameters"] = (array) $argv;
- if(($serverDotProperties = @file_get_contents(Path::join($this->server->getDataPath(), "server.properties"))) !== false){
- $this->data["server.properties"] = preg_replace("#^rcon\\.password=(.*)$#m", "rcon.password=******", $serverDotProperties);
- }else{
- $this->data["server.properties"] = $serverDotProperties;
- }
- if(($pocketmineDotYml = @file_get_contents(Path::join($this->server->getDataPath(), "pocketmine.yml"))) !== false){
- $this->data["pocketmine.yml"] = $pocketmineDotYml;
- }else{
- $this->data["pocketmine.yml"] = "";
- }
- }else{
- $this->data["pocketmine.yml"] = "";
- $this->data["server.properties"] = "";
- $this->data["parameters"] = [];
- }
- $extensions = [];
- foreach(get_loaded_extensions() as $ext){
- $extensions[$ext] = phpversion($ext);
- }
- $this->data["extensions"] = $extensions;
-
- if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-phpinfo", true)){
- ob_start();
- phpinfo();
- $this->data["phpinfo"] = ob_get_contents();
- ob_end_clean();
- }
- }
-
- private function baseCrash() : void{
- global $lastExceptionError, $lastError;
-
- if(isset($lastExceptionError)){
- $error = $lastExceptionError;
- }else{
- $error = error_get_last();
- if($error === null){
- throw new \RuntimeException("Crash error information missing - did something use exit()?");
- }
- $error["trace"] = Utils::currentTrace(3); //Skipping CrashDump->baseCrash, CrashDump->construct, Server->crashDump
- $error["fullFile"] = $error["file"];
- $error["file"] = Filesystem::cleanPath($error["file"]);
- try{
- $error["type"] = ErrorTypeToStringMap::get($error["type"]);
- }catch(\InvalidArgumentException $e){
- //pass
- }
- if(($pos = strpos($error["message"], "\n")) !== false){
- $error["message"] = substr($error["message"], 0, $pos);
- }
- }
-
- if(isset($lastError)){
- if(isset($lastError["trace"])){
- $lastError["trace"] = Utils::printableTrace($lastError["trace"]);
- }
- $this->data["lastError"] = $lastError;
- }
-
- $this->data["error"] = $error;
- unset($this->data["error"]["fullFile"]);
- unset($this->data["error"]["trace"]);
- $this->addLine("Error: " . $error["message"]);
- $this->addLine("File: " . $error["file"]);
- $this->addLine("Line: " . $error["line"]);
- $this->addLine("Type: " . $error["type"]);
-
- $this->data["plugin_involvement"] = self::PLUGIN_INVOLVEMENT_NONE;
- if(!$this->determinePluginFromFile($error["fullFile"], true)){ //fatal errors won't leave any stack trace
- foreach($error["trace"] as $frame){
- if(!isset($frame["file"])){
- continue; //PHP core
- }
- if($this->determinePluginFromFile($frame["file"], false)){
- break;
- }
- }
- }
-
- $this->addLine();
- $this->addLine("Code:");
- $this->data["code"] = [];
-
- if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-code", true) and file_exists($error["fullFile"])){
- $file = @file($error["fullFile"], FILE_IGNORE_NEW_LINES);
- if($file !== false){
- for($l = max(0, $error["line"] - 10); $l < $error["line"] + 10 and isset($file[$l]); ++$l){
- $this->addLine("[" . ($l + 1) . "] " . $file[$l]);
- $this->data["code"][$l + 1] = $file[$l];
- }
- }
- }
-
- $this->addLine();
- $this->addLine("Backtrace:");
- foreach(($this->data["trace"] = Utils::printableTrace($error["trace"])) as $line){
- $this->addLine($line);
- }
- $this->addLine();
- }
-
- private function determinePluginFromFile(string $filePath, bool $crashFrame) : bool{
- $frameCleanPath = Filesystem::cleanPath($filePath);
- if(strpos($frameCleanPath, Filesystem::CLEAN_PATH_SRC_PREFIX) !== 0){
- $this->addLine();
- if($crashFrame){
- $this->addLine("THIS CRASH WAS CAUSED BY A PLUGIN");
- $this->data["plugin_involvement"] = self::PLUGIN_INVOLVEMENT_DIRECT;
- }else{
- $this->addLine("A PLUGIN WAS INVOLVED IN THIS CRASH");
- $this->data["plugin_involvement"] = self::PLUGIN_INVOLVEMENT_INDIRECT;
- }
-
- if(file_exists($filePath)){
- $reflection = new \ReflectionClass(PluginBase::class);
- $file = $reflection->getProperty("file");
- $file->setAccessible(true);
- foreach($this->server->getPluginManager()->getPlugins() as $plugin){
- $filePath = Filesystem::cleanPath($file->getValue($plugin));
- if(strpos($frameCleanPath, $filePath) === 0){
- $this->data["plugin"] = $plugin->getName();
- $this->addLine("BAD PLUGIN: " . $plugin->getDescription()->getFullName());
- break;
- }
- }
- }
- return true;
- }
- return false;
- }
-
- private function generalData() : void{
- $version = VersionInfo::VERSION();
- $composerLibraries = [];
- foreach(InstalledVersions::getInstalledPackages() as $package){
- $composerLibraries[$package] = sprintf(
- "%s@%s",
- InstalledVersions::getPrettyVersion($package) ?? "unknown",
- InstalledVersions::getReference($package) ?? "unknown"
- );
- }
-
- $this->data["general"] = [];
- $this->data["general"]["name"] = $this->server->getName();
- $this->data["general"]["base_version"] = VersionInfo::BASE_VERSION;
- $this->data["general"]["build"] = VersionInfo::BUILD_NUMBER;
- $this->data["general"]["is_dev"] = VersionInfo::IS_DEVELOPMENT_BUILD;
- $this->data["general"]["protocol"] = ProtocolInfo::CURRENT_PROTOCOL;
- $this->data["general"]["git"] = VersionInfo::GIT_HASH();
- $this->data["general"]["uname"] = php_uname("a");
- $this->data["general"]["php"] = phpversion();
- $this->data["general"]["zend"] = zend_version();
- $this->data["general"]["php_os"] = PHP_OS;
- $this->data["general"]["os"] = Utils::getOS();
- $this->data["general"]["composer_libraries"] = $composerLibraries;
- $this->addLine($this->server->getName() . " version: " . $version->getFullVersion(true) . " [Protocol " . ProtocolInfo::CURRENT_PROTOCOL . "]");
- $this->addLine("Git commit: " . VersionInfo::GIT_HASH());
- $this->addLine("uname -a: " . php_uname("a"));
- $this->addLine("PHP Version: " . phpversion());
- $this->addLine("Zend version: " . zend_version());
- $this->addLine("OS : " . PHP_OS . ", " . Utils::getOS());
- $this->addLine("Composer libraries: ");
- foreach($composerLibraries as $library => $libraryVersion){
- $this->addLine("- $library $libraryVersion");
- }
- }
-
- /**
- * @param string $line
- */
- public function addLine($line = "") : void{
- fwrite($this->fp, $line . PHP_EOL);
- }
-
- /**
- * @param string $str
- */
- public function add($str) : void{
- fwrite($this->fp, $str);
- }
-}
diff --git a/src/MemoryManager.php b/src/MemoryManager.php
index dbcd80092..b24853317 100644
--- a/src/MemoryManager.php
+++ b/src/MemoryManager.php
@@ -24,10 +24,10 @@ declare(strict_types=1);
namespace pocketmine;
use pocketmine\event\server\LowMemoryEvent;
+use pocketmine\network\mcpe\cache\ChunkCache;
use pocketmine\scheduler\DumpWorkerMemoryTask;
use pocketmine\scheduler\GarbageCollectionTask;
use pocketmine\timings\Timings;
-use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Process;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
@@ -64,6 +64,7 @@ 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;
@@ -167,14 +168,14 @@ class MemoryManager{
}
public function canUseChunkCache() : bool{
- return !$this->lowMemory or !$this->lowMemDisableChunkCache;
+ return !$this->lowMemory || !$this->lowMemDisableChunkCache;
}
/**
* Returns the allowed chunk radius based on the current memory usage.
*/
public function getViewDistance(int $distance) : int{
- return ($this->lowMemory and $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
+ return ($this->lowMemory && $this->lowMemChunkRadiusOverride > 0) ? min($this->lowMemChunkRadiusOverride, $distance) : $distance;
}
/**
@@ -187,6 +188,7 @@ class MemoryManager{
foreach($this->server->getWorldManager()->getWorlds() as $world){
$world->clearCache(true);
}
+ ChunkCache::pruneCaches();
}
if($this->lowMemChunkGC){
@@ -212,18 +214,18 @@ class MemoryManager{
public function check() : void{
Timings::$memoryManager->startTiming();
- if(($this->memoryLimit > 0 or $this->globalMemoryLimit > 0) and ++$this->checkTicker >= $this->checkRate){
+ if(($this->memoryLimit > 0 || $this->globalMemoryLimit > 0) && ++$this->checkTicker >= $this->checkRate){
$this->checkTicker = 0;
$memory = Process::getAdvancedMemoryUsage();
$trigger = false;
- if($this->memoryLimit > 0 and $memory[0] > $this->memoryLimit){
+ if($this->memoryLimit > 0 && $memory[0] > $this->memoryLimit){
$trigger = 0;
- }elseif($this->globalMemoryLimit > 0 and $memory[1] > $this->globalMemoryLimit){
+ }elseif($this->globalMemoryLimit > 0 && $memory[1] > $this->globalMemoryLimit){
$trigger = 1;
}
if($trigger !== false){
- if($this->lowMemory and $this->continuousTrigger){
+ if($this->lowMemory && $this->continuousTrigger){
if(++$this->continuousTriggerTicker >= $this->continuousTriggerRate){
$this->continuousTriggerTicker = 0;
$this->trigger($memory[$trigger], $this->memoryLimit, $trigger > 0, ++$this->continuousTriggerCount);
@@ -238,7 +240,7 @@ class MemoryManager{
}
}
- if($this->garbageCollectionPeriod > 0 and ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
+ if($this->garbageCollectionPeriod > 0 && ++$this->garbageCollectionTicker >= $this->garbageCollectionPeriod){
$this->garbageCollectionTicker = 0;
$this->triggerGarbageCollector();
}
@@ -289,8 +291,7 @@ class MemoryManager{
* @param mixed $startingObject
*/
public static function dumpMemory($startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger) : void{
- $hardLimit = ini_get('memory_limit');
- if($hardLimit === false) throw new AssumptionFailedError("memory_limit INI directive should always exist");
+ $hardLimit = Utils::assumeNotFalse(ini_get('memory_limit'), "memory_limit INI directive should always exist");
ini_set('memory_limit', '-1');
gc_disable();
@@ -298,7 +299,7 @@ class MemoryManager{
mkdir($outputFolder, 0777, true);
}
- $obData = fopen(Path::join($outputFolder, "objects.js"), "wb+");
+ $obData = Utils::assumeNotFalse(fopen(Path::join($outputFolder, "objects.js"), "wb+"));
$objects = [];
@@ -316,13 +317,16 @@ class MemoryManager{
$reflection = new \ReflectionClass($className);
$staticProperties[$className] = [];
foreach($reflection->getProperties() as $property){
- if(!$property->isStatic() or $property->getDeclaringClass()->getName() !== $className){
+ if(!$property->isStatic() || $property->getDeclaringClass()->getName() !== $className){
continue;
}
if(!$property->isPublic()){
$property->setAccessible(true);
}
+ if(!$property->isInitialized()){
+ continue;
+ }
$staticCount++;
$staticProperties[$className][$property->getName()] = self::continueDump($property->getValue(), $objects, $refCounts, 0, $maxNesting, $maxStringSize);
@@ -347,38 +351,36 @@ class MemoryManager{
}
}
- file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
+ 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");
- if(isset($GLOBALS)){ //This might be null if we're on a different thread
- $globalVariables = [];
- $globalCount = 0;
+ $globalVariables = [];
+ $globalCount = 0;
- $ignoredGlobals = [
- 'GLOBALS' => true,
- '_SERVER' => true,
- '_REQUEST' => true,
- '_POST' => true,
- '_GET' => true,
- '_FILES' => true,
- '_ENV' => true,
- '_COOKIE' => true,
- '_SESSION' => true
- ];
+ $ignoredGlobals = [
+ 'GLOBALS' => true,
+ '_SERVER' => true,
+ '_REQUEST' => true,
+ '_POST' => true,
+ '_GET' => true,
+ '_FILES' => true,
+ '_ENV' => true,
+ '_COOKIE' => true,
+ '_SESSION' => true
+ ];
- foreach($GLOBALS as $varName => $value){
- if(isset($ignoredGlobals[$varName])){
- continue;
- }
-
- $globalCount++;
- $globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize);
+ foreach(Utils::stringifyKeys($GLOBALS) as $varName => $value){
+ if(isset($ignoredGlobals[$varName])){
+ continue;
}
- file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
- $logger->info("Wrote $globalCount global variables");
+ $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);
@@ -391,7 +393,7 @@ class MemoryManager{
$functionStaticVarsCount += count($vars);
}
}
- file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
+ 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);
@@ -448,13 +450,16 @@ class MemoryManager{
if(!$property->isPublic()){
$property->setAccessible(true);
}
+ 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) . "\n");
+ fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n");
}
}while($continue);
@@ -463,11 +468,11 @@ class MemoryManager{
fclose($obData);
- file_put_contents(Path::join($outputFolder, "serverEntry.js"), json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
- file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
+ 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));
+ file_put_contents(Path::join($outputFolder, "instanceCounts.js"), json_encode($instanceCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$logger->info("Finished!");
diff --git a/src/PocketMine.php b/src/PocketMine.php
index afac6fd8d..9cb8eedf8 100644
--- a/src/PocketMine.php
+++ b/src/PocketMine.php
@@ -32,13 +32,16 @@ namespace pocketmine {
use pocketmine\utils\ServerKiller;
use pocketmine\utils\Terminal;
use pocketmine\utils\Timezone;
+ use pocketmine\utils\Utils;
use pocketmine\wizard\SetupWizard;
use Webmozart\PathUtil\Path;
+ use function defined;
use function extension_loaded;
+ use function getcwd;
use function phpversion;
use function preg_match;
use function preg_quote;
- use function strpos;
+ use function realpath;
use function version_compare;
require_once __DIR__ . '/VersionInfo.php';
@@ -111,21 +114,22 @@ namespace pocketmine {
}
}
- if(extension_loaded("pthreads")){
- $pthreads_version = phpversion("pthreads");
+ if(($pthreads_version = phpversion("pthreads")) !== false){
if(substr_count($pthreads_version, ".") < 2){
$pthreads_version = "0.$pthreads_version";
}
- if(version_compare($pthreads_version, "3.2.0") < 0){
- $messages[] = "pthreads >= 3.2.0 is required, while you have $pthreads_version.";
+ if(version_compare($pthreads_version, "4.0.0") < 0 || version_compare($pthreads_version, "5.0.0") > 0){
+ $messages[] = "pthreads ^4.0.0 is required, while you have $pthreads_version.";
}
}
- if(extension_loaded("leveldb")){
- $leveldb_version = phpversion("leveldb");
+ if(($leveldb_version = phpversion("leveldb")) !== false){
if(version_compare($leveldb_version, "0.2.1") < 0){
$messages[] = "php-leveldb >= 0.2.1 is required, while you have $leveldb_version.";
}
+ if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){
+ $messages[] = "Given version of php-leveldb doesn't support ZLIB_RAW compression (use https://github.com/pmmp/php-leveldb)";
+ }
}
$chunkutils2_version = phpversion("chunkutils2");
@@ -142,6 +146,10 @@ namespace pocketmine {
$messages[] = "The native PocketMine extension is no longer supported.";
}
+ if(!defined('AF_INET6')){
+ $messages[] = "IPv6 support is required, but your PHP binary was built without IPv6 support.";
+ }
+
return $messages;
}
@@ -205,6 +213,8 @@ JIT_WARNING
}
critical_error("PHP binary used: " . $binary);
critical_error("Loaded php.ini: " . (($file = php_ini_loaded_file()) !== false ? $file : "none"));
+ $phprc = getenv("PHPRC");
+ critical_error("Value of PHPRC environment variable: " . ($phprc === false ? "" : $phprc));
critical_error("Please recompile PHP with the needed configuration, or refer to the installation instructions at http://pmmp.rtfd.io/en/rtfd/installation.html.");
echo PHP_EOL;
exit(1);
@@ -214,20 +224,13 @@ JIT_WARNING
error_reporting(-1);
set_ini_entries();
- $opts = getopt("", ["bootstrap:"]);
- if(isset($opts["bootstrap"])){
- $bootstrap = ($real = realpath($opts["bootstrap"])) !== false ? $real : $opts["bootstrap"];
- }else{
- $bootstrap = dirname(__FILE__, 2) . '/vendor/autoload.php';
- }
-
- if($bootstrap === false or !is_file($bootstrap)){
+ $bootstrap = dirname(__FILE__, 2) . '/vendor/autoload.php';
+ if(!is_file($bootstrap)){
critical_error("Composer autoloader not found at " . $bootstrap);
critical_error("Please install/update Composer dependencies or use provided builds.");
exit(1);
}
- define('pocketmine\COMPOSER_AUTOLOADER_PATH', $bootstrap);
- require_once(\pocketmine\COMPOSER_AUTOLOADER_PATH);
+ require_once($bootstrap);
$composerGitHash = InstalledVersions::getReference('pocketmine/pocketmine-mp');
if($composerGitHash !== null){
@@ -250,8 +253,9 @@ JIT_WARNING
$opts = getopt("", ["data:", "plugins:", "no-wizard", "enable-ansi", "disable-ansi"]);
- $dataPath = isset($opts["data"]) ? $opts["data"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR;
- $pluginPath = isset($opts["plugins"]) ? $opts["plugins"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR . "plugins" . DIRECTORY_SEPARATOR;
+ $cwd = Utils::assumeNotFalse(realpath(Utils::assumeNotFalse(getcwd())));
+ $dataPath = isset($opts["data"]) ? $opts["data"] . DIRECTORY_SEPARATOR : $cwd . DIRECTORY_SEPARATOR;
+ $pluginPath = isset($opts["plugins"]) ? $opts["plugins"] . DIRECTORY_SEPARATOR : $cwd . DIRECTORY_SEPARATOR . "plugins" . DIRECTORY_SEPARATOR;
Filesystem::addCleanedPath($pluginPath, Filesystem::CLEAN_PATH_PLUGINS_PREFIX);
if(!file_exists($dataPath)){
@@ -283,7 +287,7 @@ JIT_WARNING
$exitCode = 0;
do{
- if(!file_exists(Path::join($dataPath, "server.properties")) and !isset($opts["no-wizard"])){
+ if(!file_exists(Path::join($dataPath, "server.properties")) && !isset($opts["no-wizard"])){
$installer = new SetupWizard($dataPath);
if(!$installer->run()){
$exitCode = -1;
@@ -307,7 +311,7 @@ JIT_WARNING
if(ThreadManager::getInstance()->stopAll() > 0){
$logger->debug("Some threads could not be stopped, performing a force-kill");
- Process::kill(Process::pid());
+ Process::kill(Process::pid(), true);
}
}while(false);
diff --git a/src/Server.php b/src/Server.php
index f76235d57..8b484a9f1 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -34,12 +34,14 @@ use pocketmine\console\ConsoleCommandSender;
use pocketmine\console\ConsoleReaderThread;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\CraftingManagerFromDataHelper;
-use pocketmine\data\java\GameModeIdMap;
+use pocketmine\crash\CrashDump;
+use pocketmine\crash\CrashDumpRenderer;
use pocketmine\entity\EntityDataHelper;
use pocketmine\entity\Location;
use pocketmine\event\HandlerListManager;
use pocketmine\event\player\PlayerCreationEvent;
use pocketmine\event\player\PlayerDataSaveEvent;
+use pocketmine\event\player\PlayerLoginEvent;
use pocketmine\event\server\CommandEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\event\server\QueryRegenerateEvent;
@@ -63,6 +65,7 @@ use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
use pocketmine\network\mcpe\raklib\RakLibInterface;
use pocketmine\network\Network;
+use pocketmine\network\NetworkInterfaceStartException;
use pocketmine\network\query\DedicatedQueryNetworkInterface;
use pocketmine\network\query\QueryHandler;
use pocketmine\network\query\QueryInfo;
@@ -80,6 +83,8 @@ use pocketmine\plugin\PluginGraylist;
use pocketmine\plugin\PluginManager;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\ScriptPluginLoader;
+use pocketmine\promise\Promise;
+use pocketmine\promise\PromiseResolver;
use pocketmine\resourcepacks\ResourcePackManager;
use pocketmine\scheduler\AsyncPool;
use pocketmine\snooze\SleeperHandler;
@@ -96,7 +101,7 @@ use pocketmine\utils\MainLogger;
use pocketmine\utils\NotCloneable;
use pocketmine\utils\NotSerializable;
use pocketmine\utils\Process;
-use pocketmine\utils\Promise;
+use pocketmine\utils\SignalHandler;
use pocketmine\utils\Terminal;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
@@ -105,26 +110,29 @@ use pocketmine\world\format\io\WorldProviderManager;
use pocketmine\world\format\io\WritableWorldProviderManagerEntry;
use pocketmine\world\generator\Generator;
use pocketmine\world\generator\GeneratorManager;
+use pocketmine\world\generator\InvalidGeneratorOptionsException;
use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
use pocketmine\world\WorldManager;
use Ramsey\Uuid\UuidInterface;
use Webmozart\PathUtil\Path;
-use function array_shift;
use function array_sum;
use function base64_encode;
use function cli_set_process_title;
use function copy;
use function count;
-use function explode;
+use function date;
+use function fclose;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
+use function fopen;
use function get_class;
-use function implode;
use function ini_set;
use function is_array;
+use function is_dir;
+use function is_resource;
use function is_string;
use function json_decode;
use function max;
@@ -169,6 +177,12 @@ class Server{
public const BROADCAST_CHANNEL_ADMINISTRATIVE = "pocketmine.broadcast.admin";
public const BROADCAST_CHANNEL_USERS = "pocketmine.broadcast.user";
+ public const DEFAULT_SERVER_NAME = VersionInfo::NAME . " Server";
+ public const DEFAULT_MAX_PLAYERS = 20;
+ public const DEFAULT_PORT_IPV4 = 19132;
+ public const DEFAULT_PORT_IPV6 = 19133;
+ public const DEFAULT_MAX_VIEW_DISTANCE = 16;
+
private static ?Server $instance = null;
private SleeperHandler $tickSleeper;
@@ -251,6 +265,8 @@ class Server{
/** @var Player[] */
private array $playerList = [];
+ private SignalHandler $signalHandler;
+
/**
* @var CommandSender[][]
* @phpstan-var array>
@@ -313,11 +329,15 @@ class Server{
}
public function getPort() : int{
- return $this->configGroup->getConfigInt("server-port", 19132);
+ return $this->configGroup->getConfigInt("server-port", self::DEFAULT_PORT_IPV4);
+ }
+
+ public function getPortV6() : int{
+ return $this->configGroup->getConfigInt("server-portv6", self::DEFAULT_PORT_IPV6);
}
public function getViewDistance() : int{
- return max(2, $this->configGroup->getConfigInt("view-distance", 8));
+ return max(2, $this->configGroup->getConfigInt("view-distance", self::DEFAULT_MAX_VIEW_DISTANCE));
}
/**
@@ -332,12 +352,17 @@ class Server{
return $str !== "" ? $str : "0.0.0.0";
}
+ public function getIpV6() : string{
+ $str = $this->configGroup->getConfigString("server-ipv6");
+ return $str !== "" ? $str : "::";
+ }
+
public function getServerUniqueId() : UuidInterface{
return $this->serverID;
}
public function getGamemode() : GameMode{
- return GameModeIdMap::getInstance()->fromId($this->configGroup->getConfigInt("gamemode", 0)) ?? GameMode::SURVIVAL();
+ return GameMode::fromString($this->configGroup->getConfigString("gamemode", GameMode::SURVIVAL()->name())) ?? GameMode::SURVIVAL();
}
public function getForceGamemode() : bool{
@@ -360,7 +385,7 @@ class Server{
}
public function getMotd() : string{
- return $this->configGroup->getConfigString("motd", VersionInfo::NAME . " Server");
+ return $this->configGroup->getConfigString("motd", self::DEFAULT_SERVER_NAME);
}
public function getLoader() : \DynamicClassLoader{
@@ -517,9 +542,10 @@ class Server{
if(!$ev->isCancelled()){
Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
$nbt = new BigEndianNbtSerializer();
+ $contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly");
try{
- file_put_contents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP));
- }catch(\ErrorException $e){
+ Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents);
+ }catch(\RuntimeException $e){
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
$this->logger->logException($e);
}
@@ -535,7 +561,7 @@ class Server{
$ev->call();
$class = $ev->getPlayerClass();
- if($offlinePlayerData !== null and ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString("Level", ""))) !== null){
+ if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString("Level", ""))) !== null){
$playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
$spawn = $playerPos->asVector3();
}else{
@@ -546,11 +572,11 @@ class Server{
$playerPos = null;
$spawn = $world->getSpawnLocation();
}
- $playerPromise = new Promise();
+ $playerPromiseResolver = new PromiseResolver();
$world->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
- function() use ($playerPromise, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{
+ function() use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{
if(!$session->isConnected()){
- $playerPromise->reject();
+ $playerPromiseResolver->reject();
return;
}
@@ -570,16 +596,16 @@ class Server{
if(!$player->hasPlayedBefore()){
$player->onGround = true; //TODO: this hack is needed for new players in-air ticks - they don't get detected as on-ground until they move
}
- $playerPromise->resolve($player);
+ $playerPromiseResolver->resolve($player);
},
- static function() use ($playerPromise, $session) : void{
+ static function() use ($playerPromiseResolver, $session) : void{
if($session->isConnected()){
$session->disconnect("Spawn terrain generation failed");
}
- $playerPromise->reject();
+ $playerPromiseResolver->reject();
}
);
- return $playerPromise;
+ return $playerPromiseResolver->getPromise();
}
/**
@@ -670,7 +696,13 @@ class Server{
}
public function removeOp(string $name) : void{
- $this->operators->remove(strtolower($name));
+ $lowercaseName = strtolower($name);
+ foreach($this->operators->getAll() as $operatorName => $_){
+ $operatorName = (string) $operatorName;
+ if($lowercaseName === strtolower($operatorName)){
+ $this->operators->remove($operatorName);
+ }
+ }
if(($player = $this->getPlayerExact($name)) !== null){
$player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR);
@@ -689,7 +721,7 @@ class Server{
}
public function isWhitelisted(string $name) : bool{
- return !$this->hasWhitelist() or $this->operators->exists($name, true) or $this->whitelist->exists($name, true);
+ return !$this->hasWhitelist() || $this->operators->exists($name, true) || $this->whitelist->exists($name, true);
}
public function isOp(string $name) : bool{
@@ -735,7 +767,7 @@ class Server{
public function __construct(\DynamicClassLoader $autoloader, \AttachableThreadedLogger $logger, string $dataPath, string $pluginPath){
if(self::$instance !== null){
- throw new \InvalidStateException("Only one server instance can exist at once");
+ throw new \LogicException("Only one server instance can exist at once");
}
self::$instance = $this;
$this->startTime = microtime(true);
@@ -744,6 +776,11 @@ class Server{
$this->autoloader = $autoloader;
$this->logger = $logger;
+ $this->signalHandler = new SignalHandler(function() : void{
+ $this->logger->info("Received signal interrupt, stopping the server");
+ $this->shutdown();
+ });
+
try{
foreach([
$dataPath,
@@ -762,7 +799,7 @@ class Server{
$this->logger->info("Loading server configuration");
$pocketmineYmlPath = Path::join($this->dataPath, "pocketmine.yml");
if(!file_exists($pocketmineYmlPath)){
- $content = file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml"));
+ $content = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml")), "Missing required resource file");
if(VersionInfo::IS_DEVELOPMENT_BUILD){
$content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content);
}
@@ -772,11 +809,13 @@ class Server{
$this->configGroup = new ServerConfigGroup(
new Config($pocketmineYmlPath, Config::YAML, []),
new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES, [
- "motd" => VersionInfo::NAME . " Server",
- "server-port" => 19132,
+ "motd" => self::DEFAULT_SERVER_NAME,
+ "server-port" => self::DEFAULT_PORT_IPV4,
+ "server-portv6" => self::DEFAULT_PORT_IPV6,
+ "enable-ipv6" => true,
"white-list" => false,
- "max-players" => 20,
- "gamemode" => 0,
+ "max-players" => self::DEFAULT_MAX_PLAYERS,
+ "gamemode" => GameMode::SURVIVAL()->name(),
"force-gamemode" => false,
"hardcore" => false,
"pvp" => true,
@@ -787,7 +826,7 @@ class Server{
"level-type" => "DEFAULT",
"enable-query" => true,
"auto-save" => true,
- "view-distance" => 8,
+ "view-distance" => self::DEFAULT_MAX_VIEW_DISTANCE,
"xbox-auth" => true,
"language" => "eng"
])
@@ -856,7 +895,7 @@ class Server{
}
$netCompressionLevel = $this->configGroup->getPropertyInt("network.compression-level", 6);
- if($netCompressionLevel < 1 or $netCompressionLevel > 9){
+ if($netCompressionLevel < 1 || $netCompressionLevel > 9){
$this->logger->warning("Invalid network compression level $netCompressionLevel set, setting to default 6");
$netCompressionLevel = 6;
}
@@ -873,7 +912,7 @@ class Server{
$bannedTxt = Path::join($this->dataPath, "banned.txt");
$bannedPlayersTxt = Path::join($this->dataPath, "banned-players.txt");
- if(file_exists($bannedTxt) and !file_exists($bannedPlayersTxt)){
+ if(file_exists($bannedTxt) && !file_exists($bannedPlayersTxt)){
@rename($bannedTxt, $bannedPlayersTxt);
}
@touch($bannedPlayersTxt);
@@ -884,7 +923,7 @@ class Server{
$this->banByIP = new BanList($bannedIpsTxt);
$this->banByIP->load();
- $this->maxPlayers = $this->configGroup->getConfigInt("max-players", 20);
+ $this->maxPlayers = $this->configGroup->getConfigInt("max-players", self::DEFAULT_MAX_PLAYERS);
$this->onlineMode = $this->configGroup->getConfigBool("xbox-auth", true);
if($this->onlineMode){
@@ -895,7 +934,7 @@ class Server{
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
}
- if($this->configGroup->getConfigBool("hardcore", false) and $this->getDifficulty() < World::DIFFICULTY_HARD){
+ if($this->configGroup->getConfigBool("hardcore", false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
$this->configGroup->setConfigInt("difficulty", World::DIFFICULTY_HARD);
}
@@ -923,7 +962,7 @@ class Server{
$this->commandMap = new SimpleCommandMap($this);
- $this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\RESOURCE_PATH, "vanilla", "recipes.json"));
+ $this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes.json"));
$this->resourceManager = new ResourcePackManager(Path::join($this->getDataPath(), "resource_packs"), $this->logger);
@@ -945,7 +984,7 @@ class Server{
$providerManager = new WorldProviderManager();
if(
- ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString("level-settings.default-format", ""))) !== null and
+ ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString("level-settings.default-format", ""))) !== null &&
$format instanceof WritableWorldProviderManagerEntry
){
$providerManager->setDefault($format);
@@ -966,91 +1005,17 @@ class Server{
$this->pluginManager->loadPlugins($this->pluginPath);
$this->enablePlugins(PluginEnableOrder::STARTUP());
- foreach((array) $this->configGroup->getProperty("worlds", []) as $name => $options){
- if($options === null){
- $options = [];
- }elseif(!is_array($options)){
- continue;
- }
- if(!$this->worldManager->loadWorld($name, true)){
- $creationOptions = WorldCreationOptions::create();
- //TODO: error checking
-
- if(isset($options["generator"])){
- $generatorOptions = explode(":", $options["generator"]);
- $creationOptions->setGeneratorClass(GeneratorManager::getInstance()->getGenerator(array_shift($generatorOptions)));
- if(count($generatorOptions) > 0){
- $creationOptions->setGeneratorOptions(implode(":", $generatorOptions));
- }
- }
- if(isset($options["difficulty"]) && is_string($options["difficulty"])){
- $creationOptions->setDifficulty(World::getDifficultyFromString($options["difficulty"]));
- }
- if(isset($options["preset"]) && is_string($options["preset"])){
- $creationOptions->setGeneratorOptions($options["preset"]);
- }
- if(isset($options["seed"])){
- $convertedSeed = Generator::convertSeed((string) ($options["seed"] ?? ""));
- if($convertedSeed !== null){
- $creationOptions->setSeed($convertedSeed);
- }
- }
-
- $this->worldManager->generateWorld($name, $creationOptions);
- }
+ if(!$this->startupPrepareWorlds()){
+ return;
}
-
- if($this->worldManager->getDefaultWorld() === null){
- $default = $this->configGroup->getConfigString("level-name", "world");
- if(trim($default) == ""){
- $this->getLogger()->warning("level-name cannot be null, using default");
- $default = "world";
- $this->configGroup->setConfigString("level-name", "world");
- }
- if(!$this->worldManager->loadWorld($default, true)){
- $creationOptions = WorldCreationOptions::create()
- ->setGeneratorClass(GeneratorManager::getInstance()->getGenerator($this->configGroup->getConfigString("level-type")))
- ->setGeneratorOptions($this->configGroup->getConfigString("generator-settings"));
- $convertedSeed = Generator::convertSeed($this->configGroup->getConfigString("level-seed"));
- if($convertedSeed !== null){
- $creationOptions->setSeed($convertedSeed);
- }
- $this->worldManager->generateWorld($default, $creationOptions);
- }
-
- $world = $this->worldManager->getWorldByName($default);
- if($world === null){
- $this->getLogger()->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
- $this->forceShutdown();
-
- return;
- }
- $this->worldManager->setDefaultWorld($world);
- }
-
$this->enablePlugins(PluginEnableOrder::POSTWORLD());
- $useQuery = $this->configGroup->getConfigBool("enable-query", true);
- if(!$this->network->registerInterface(new RakLibInterface($this)) && $useQuery){
- //RakLib would normally handle the transport for Query packets
- //if it's not registered we need to make sure Query still works
- $this->network->registerInterface(new DedicatedQueryNetworkInterface($this->getIp(), $this->getPort(), new \PrefixedLogger($this->logger, "Dedicated Query Interface")));
- }
- $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_networkStart($this->getIp(), (string) $this->getPort())));
-
- if($useQuery){
- $this->network->registerRawPacketHandler(new QueryHandler($this));
+ if(!$this->startupPrepareNetworkInterfaces()){
+ $this->forceShutdown();
+ return;
}
- foreach($this->getIPBans()->getEntries() as $entry){
- $this->network->blockAddress($entry->getName(), -1);
- }
-
- if($this->configGroup->getPropertyBool("network.upnp-forwarding", false)){
- $this->network->registerInterface(new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
- }
-
- if($this->configGroup->getPropertyBool("settings.send-usage", true)){
+ if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){
$this->sendUsageTicker = 6000;
$this->sendUsage(SendUsageTask::TYPE_OPEN);
}
@@ -1085,6 +1050,153 @@ class Server{
}
}
+ private function startupPrepareWorlds() : bool{
+ $getGenerator = function(string $generatorName, string $generatorOptions, string $worldName) : ?string{
+ $generatorEntry = GeneratorManager::getInstance()->getGenerator($generatorName);
+ if($generatorEntry === null){
+ $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
+ $worldName,
+ KnownTranslationFactory::pocketmine_level_unknownGenerator($generatorName)
+ )));
+ return null;
+ }
+ try{
+ $generatorEntry->validateGeneratorOptions($generatorOptions);
+ }catch(InvalidGeneratorOptionsException $e){
+ $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
+ $worldName,
+ KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions($generatorOptions, $generatorName, $e->getMessage())
+ )));
+ return null;
+ }
+ return $generatorEntry->getGeneratorClass();
+ };
+
+ foreach((array) $this->configGroup->getProperty("worlds", []) as $name => $options){
+ if($options === null){
+ $options = [];
+ }elseif(!is_array($options)){
+ continue;
+ }
+ if(!$this->worldManager->loadWorld($name, true) && !$this->worldManager->isWorldGenerated($name)){
+ $creationOptions = WorldCreationOptions::create();
+ //TODO: error checking
+
+ $generatorName = $options["generator"] ?? "default";
+ $generatorOptions = isset($options["preset"]) && is_string($options["preset"]) ? $options["preset"] : "";
+
+ $generatorClass = $getGenerator($generatorName, $generatorOptions, $name);
+ if($generatorClass === null){
+ continue;
+ }
+ $creationOptions->setGeneratorClass($generatorClass);
+ $creationOptions->setGeneratorOptions($generatorOptions);
+
+ if(isset($options["difficulty"]) && is_string($options["difficulty"])){
+ $creationOptions->setDifficulty(World::getDifficultyFromString($options["difficulty"]));
+ }
+
+ if(isset($options["seed"])){
+ $convertedSeed = Generator::convertSeed((string) ($options["seed"] ?? ""));
+ if($convertedSeed !== null){
+ $creationOptions->setSeed($convertedSeed);
+ }
+ }
+
+ $this->worldManager->generateWorld($name, $creationOptions);
+ }
+ }
+
+ if($this->worldManager->getDefaultWorld() === null){
+ $default = $this->configGroup->getConfigString("level-name", "world");
+ if(trim($default) == ""){
+ $this->getLogger()->warning("level-name cannot be null, using default");
+ $default = "world";
+ $this->configGroup->setConfigString("level-name", "world");
+ }
+ if(!$this->worldManager->loadWorld($default, true) && !$this->worldManager->isWorldGenerated($default)){
+ $generatorName = $this->configGroup->getConfigString("level-type");
+ $generatorOptions = $this->configGroup->getConfigString("generator-settings");
+ $generatorClass = $getGenerator($generatorName, $generatorOptions, $default);
+ if($generatorClass !== null){
+ $creationOptions = WorldCreationOptions::create()
+ ->setGeneratorClass($generatorClass)
+ ->setGeneratorOptions($generatorOptions);
+ $convertedSeed = Generator::convertSeed($this->configGroup->getConfigString("level-seed"));
+ if($convertedSeed !== null){
+ $creationOptions->setSeed($convertedSeed);
+ }
+ $this->worldManager->generateWorld($default, $creationOptions);
+ }
+ }
+
+ $world = $this->worldManager->getWorldByName($default);
+ if($world === null){
+ $this->getLogger()->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
+ $this->forceShutdown();
+
+ return false;
+ }
+ $this->worldManager->setDefaultWorld($world);
+ }
+
+ return true;
+ }
+
+ private function startupPrepareConnectableNetworkInterfaces(string $ip, int $port, bool $ipV6, bool $useQuery) : bool{
+ $prettyIp = $ipV6 ? "[$ip]" : $ip;
+ try{
+ $rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6));
+ }catch(NetworkInterfaceStartException $e){
+ $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
+ $ip,
+ (string) $port,
+ $e->getMessage()
+ )));
+ return false;
+ }
+ if($rakLibRegistered){
+ $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_networkStart($prettyIp, (string) $port)));
+ }
+ if($useQuery){
+ if(!$rakLibRegistered){
+ //RakLib would normally handle the transport for Query packets
+ //if it's not registered we need to make sure Query still works
+ $this->network->registerInterface(new DedicatedQueryNetworkInterface($ip, $port, $ipV6, new \PrefixedLogger($this->logger, "Dedicated Query Interface")));
+ }
+ $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_query_running($prettyIp, (string) $port)));
+ }
+ return true;
+ }
+
+ private function startupPrepareNetworkInterfaces() : bool{
+ $useQuery = $this->configGroup->getConfigBool("enable-query", true);
+
+ if(
+ !$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery) ||
+ (
+ $this->configGroup->getConfigBool("enable-ipv6", true) &&
+ !$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery)
+ )
+ ){
+ return false;
+ }
+
+ if($useQuery){
+ $this->network->registerRawPacketHandler(new QueryHandler($this));
+ }
+
+ foreach($this->getIPBans()->getEntries() as $entry){
+ $this->network->blockAddress($entry->getName(), -1);
+ }
+
+ if($this->configGroup->getPropertyBool("network.upnp-forwarding", false)){
+ $this->network->registerInterface(new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
+ }
+
+ return true;
+ }
+
/**
* Subscribes to a particular message broadcast channel.
* The channel ID can be any arbitrary string.
@@ -1109,7 +1221,7 @@ class Server{
* Unsubscribes from all broadcast channels.
*/
public function unsubscribeFromAllBroadcastChannels(CommandSender $subscriber) : void{
- foreach($this->broadcastSubscribers as $channelId => $recipients){
+ foreach(Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){
$this->unsubscribeFromBroadcastChannel($channelId, $subscriber);
}
}
@@ -1269,7 +1381,7 @@ class Server{
public function enablePlugins(PluginEnableOrder $type) : void{
foreach($this->pluginManager->getPlugins() as $plugin){
- if(!$plugin->isEnabled() and $plugin->getDescription()->getOrder()->equals($type)){
+ if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder()->equals($type)){
$this->pluginManager->enablePlugin($plugin);
}
}
@@ -1293,20 +1405,17 @@ class Server{
$commandLine = $ev->getCommand();
}
- if($this->commandMap->dispatch($sender, $commandLine)){
- return true;
- }
-
- $sender->sendMessage(KnownTranslationFactory::commands_generic_notFound()->prefix(TextFormat::RED));
-
- return false;
+ return $this->commandMap->dispatch($sender, $commandLine);
}
/**
* Shuts the server down correctly
*/
public function shutdown() : void{
- $this->isRunning = false;
+ if($this->isRunning){
+ $this->isRunning = false;
+ $this->signalHandler->unregister();
+ }
}
public function forceShutdown() : void{
@@ -1318,6 +1427,9 @@ class Server{
echo "\x1b]0;\x07";
}
+ if($this->isRunning){
+ $this->logger->emergency("Forcing server shutdown");
+ }
try{
if(!$this->isRunning()){
$this->sendUsage(SendUsageTask::TYPE_CLOSE);
@@ -1371,7 +1483,7 @@ class Server{
}catch(\Throwable $e){
$this->logger->logException($e);
$this->logger->emergency("Crashed while crashing, killing process");
- @Process::kill(Process::pid());
+ @Process::kill(Process::pid(), true);
}
}
@@ -1416,6 +1528,25 @@ class Server{
$this->crashDump();
}
+ private function writeCrashDumpFile(CrashDump $dump) : string{
+ $crashFolder = Path::join($this->getDataPath(), "crashdumps");
+ if(!is_dir($crashFolder)){
+ mkdir($crashFolder);
+ }
+ $crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
+
+ $fp = @fopen($crashDumpPath, "wb");
+ if(!is_resource($fp)){
+ throw new \RuntimeException("Unable to open new file to generate crashdump");
+ }
+ $writer = new CrashDumpRenderer($fp, $dump->getData());
+ $writer->renderHumanReadable();
+ $dump->encodeData($writer);
+
+ fclose($fp);
+ return $crashDumpPath;
+ }
+
public function crashDump() : void{
while(@ob_end_flush()){}
if(!$this->isRunning){
@@ -1430,34 +1561,37 @@ class Server{
ini_set("memory_limit", '-1'); //Fix error dump not dumped on memory problems
try{
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_create()));
- $dump = new CrashDump($this);
+ $dump = new CrashDump($this, $this->pluginManager ?? null);
- $this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_submit($dump->getPath())));
+ $crashDumpPath = $this->writeCrashDumpFile($dump);
+
+ $this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_submit($crashDumpPath)));
if($this->configGroup->getPropertyBool("auto-report.enabled", true)){
$report = true;
$stamp = Path::join($this->getDataPath(), "crashdumps", ".last_crash");
$crashInterval = 120; //2 minutes
- if(file_exists($stamp) and !($report = (filemtime($stamp) + $crashInterval < time()))){
+ if(($lastReportTime = @filemtime($stamp)) !== false && $lastReportTime + $crashInterval >= time()){
+ $report = false;
$this->logger->debug("Not sending crashdump due to last crash less than $crashInterval seconds ago");
}
@touch($stamp); //update file timestamp
- $plugin = $dump->getData()["plugin"];
- if(is_string($plugin)){
+ $plugin = $dump->getData()->plugin;
+ if($plugin !== ""){
$p = $this->pluginManager->getPlugin($plugin);
- if($p instanceof Plugin and !($p->getPluginLoader() instanceof PharPluginLoader)){
+ if($p instanceof Plugin && !($p->getPluginLoader() instanceof PharPluginLoader)){
$this->logger->debug("Not sending crashdump due to caused by non-phar plugin");
$report = false;
}
}
- if($dump->getData()["error"]["type"] === \ParseError::class){
+ if($dump->getData()->error["type"] === \ParseError::class){
$report = false;
}
- if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false or VersionInfo::GIT_HASH() === str_repeat("00", 20)){
+ if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false || VersionInfo::GIT_HASH() === str_repeat("00", 20)){
$this->logger->debug("Not sending crashdump due to locally modified");
$report = false; //Don't send crashdumps for locally modified builds
}
@@ -1472,8 +1606,8 @@ class Server{
"reportPaste" => base64_encode($dump->getEncodedData())
], 10, [], $postUrlError);
- if($reply !== null and ($data = json_decode($reply->getBody())) !== null){
- if(isset($data->crashId) and isset($data->crashUrl)){
+ if($reply !== null && ($data = json_decode($reply->getBody())) !== null){
+ if(isset($data->crashId) && isset($data->crashUrl)){
$reportId = $data->crashId;
$reportUrl = $data->crashUrl;
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId)));
@@ -1501,7 +1635,7 @@ class Server{
echo "--- Waiting $spacing seconds to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL;
sleep($spacing);
}
- @Process::kill(Process::pid());
+ @Process::kill(Process::pid(), true);
exit(1);
}
@@ -1527,7 +1661,28 @@ class Server{
}
}
- public function addOnlinePlayer(Player $player) : void{
+ public function addOnlinePlayer(Player $player) : bool{
+ $ev = new PlayerLoginEvent($player, "Plugin reason");
+ $ev->call();
+ if($ev->isCancelled() || !$player->isConnected()){
+ $player->disconnect($ev->getKickMessage());
+
+ return false;
+ }
+
+ $session = $player->getNetworkSession();
+ $position = $player->getPosition();
+ $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
+ TextFormat::AQUA . $player->getName() . TextFormat::WHITE,
+ $session->getIp(),
+ (string) $session->getPort(),
+ (string) $player->getId(),
+ $position->getWorld()->getDisplayName(),
+ (string) round($position->x, 4),
+ (string) round($position->y, 4),
+ (string) round($position->z, 4)
+ )));
+
foreach($this->playerList as $p){
$p->getNetworkSession()->onPlayerAdded($player);
}
@@ -1537,6 +1692,8 @@ class Server{
if($this->sendUsageTicker > 0){
$this->uniquePlayers[$rawUUID] = $rawUUID;
}
+
+ return true;
}
public function removeOnlinePlayer(Player $player) : void{
@@ -1636,7 +1793,7 @@ class Server{
$this->network->getBandwidthTracker()->rotateAverageHistory();
}
- if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){
+ if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
$this->sendUsageTicker = 6000;
$this->sendUsage(SendUsageTask::TYPE_STATUS);
}
diff --git a/src/ServerConfigGroup.php b/src/ServerConfigGroup.php
index 1965e0d7f..f69990cf8 100644
--- a/src/ServerConfigGroup.php
+++ b/src/ServerConfigGroup.php
@@ -27,6 +27,8 @@ use pocketmine\utils\Config;
use function array_key_exists;
use function getopt;
use function is_bool;
+use function is_int;
+use function is_string;
use function strtolower;
final class ServerConfigGroup{
@@ -110,16 +112,20 @@ final class ServerConfigGroup{
}else{
$value = $this->serverProperties->exists($variable) ? $this->serverProperties->get($variable) : $defaultValue;
}
-
if(is_bool($value)){
return $value;
}
- switch(strtolower($value)){
- case "on":
- case "true":
- case "1":
- case "yes":
- return true;
+ if(is_int($value)){
+ return $value !== 0;
+ }
+ if(is_string($value)){
+ switch(strtolower($value)){
+ case "on":
+ case "true":
+ case "1":
+ case "yes":
+ return true;
+ }
}
return false;
diff --git a/src/VersionInfo.php b/src/VersionInfo.php
index dedf0f3ae..4848849e0 100644
--- a/src/VersionInfo.php
+++ b/src/VersionInfo.php
@@ -25,14 +25,15 @@ namespace pocketmine;
use pocketmine\utils\Git;
use pocketmine\utils\VersionString;
+use function is_array;
+use function is_int;
use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
- public const BASE_VERSION = "4.0.0-BETA4";
+ public const BASE_VERSION = "4.1.0-BETA3";
public const IS_DEVELOPMENT_BUILD = true;
- public const BUILD_NUMBER = 0;
- public const BUILD_CHANNEL = "";
+ public const BUILD_CHANNEL = "beta";
private function __construct(){
//NOOP
@@ -61,12 +62,29 @@ final class VersionInfo{
return self::$gitHash;
}
+ private static ?int $buildNumber = null;
+
+ public static function BUILD_NUMBER() : int{
+ if(self::$buildNumber === null){
+ self::$buildNumber = 0;
+ if(\Phar::running(true) !== ""){
+ $phar = new \Phar(\Phar::running(false));
+ $meta = $phar->getMetadata();
+ if(is_array($meta) && isset($meta["build"]) && is_int($meta["build"])){
+ self::$buildNumber = $meta["build"];
+ }
+ }
+ }
+
+ return self::$buildNumber;
+ }
+
/** @var VersionString|null */
private static $fullVersion = null;
public static function VERSION() : VersionString{
if(self::$fullVersion === null){
- self::$fullVersion = new VersionString(self::BASE_VERSION, self::IS_DEVELOPMENT_BUILD, self::BUILD_NUMBER);
+ self::$fullVersion = new VersionString(self::BASE_VERSION, self::IS_DEVELOPMENT_BUILD, self::BUILD_NUMBER());
}
return self::$fullVersion;
}
diff --git a/src/block/Bamboo.php b/src/block/Bamboo.php
index fc5182cfb..7fdf226a0 100644
--- a/src/block/Bamboo.php
+++ b/src/block/Bamboo.php
@@ -115,7 +115,7 @@ class Bamboo extends Transparent{
return 12 + (self::getOffsetSeed($x, 0, $z) % 5);
}
- public function getPositionOffset() : ?Vector3{
+ public function getModelPositionOffset() : ?Vector3{
$seed = self::getOffsetSeed($this->position->getFloorX(), 0, $this->position->getFloorZ());
$retX = (($seed % 12) + 1) / 16;
$retZ = ((($seed >> 8) % 12) + 1) / 16;
@@ -145,12 +145,12 @@ class Bamboo extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($item instanceof Fertilizer){
$top = $this->seekToTop();
- if($top->grow(self::getMaxHeight($top->position->getFloorX(), $top->position->getFloorZ()), mt_rand(1, 2))){
+ if($top->grow(self::getMaxHeight($top->position->getFloorX(), $top->position->getFloorZ()), mt_rand(1, 2), $player)){
$item->pop();
return true;
}
}elseif($item instanceof ItemBamboo){
- if($this->seekToTop()->grow(PHP_INT_MAX, 1)){
+ if($this->seekToTop()->grow(PHP_INT_MAX, 1, $player)){
$item->pop();
return true;
}
@@ -160,12 +160,12 @@ class Bamboo extends Transparent{
public function onNearbyBlockChange() : void{
$below = $this->position->getWorld()->getBlock($this->position->down());
- if(!$this->canBeSupportedBy($below) and !$below->isSameType($this)){
+ if(!$this->canBeSupportedBy($below) && !$below->isSameType($this)){
$this->position->getWorld()->useBreakOn($this->position);
}
}
- private function grow(int $maxHeight, int $growAmount) : bool{
+ private function grow(int $maxHeight, int $growAmount, ?Player $player) : bool{
$world = $this->position->getWorld();
if(!$world->getBlock($this->position->up())->canBeReplaced()){
return false;
@@ -212,7 +212,7 @@ class Bamboo extends Transparent{
$tx->addBlock($this->position->subtract(0, $idx - $growAmount, 0), $newBlock);
}
- $ev = new StructureGrowEvent($this, $tx);
+ $ev = new StructureGrowEvent($this, $tx, $player);
$ev->call();
if($ev->isCancelled()){
return false;
@@ -229,7 +229,7 @@ class Bamboo extends Transparent{
$world = $this->position->getWorld();
if($this->ready){
$this->ready = false;
- if($world->getFullLight($this->position) < 9 || !$this->grow(self::getMaxHeight($this->position->getFloorX(), $this->position->getFloorZ()), 1)){
+ if($world->getFullLight($this->position) < 9 || !$this->grow(self::getMaxHeight($this->position->getFloorX(), $this->position->getFloorZ()), 1, null)){
$world->setBlock($this->position, $this);
}
}elseif($world->getBlock($this->position->up())->canBeReplaced()){
diff --git a/src/block/BambooSapling.php b/src/block/BambooSapling.php
index b617236ed..3da1e593a 100644
--- a/src/block/BambooSapling.php
+++ b/src/block/BambooSapling.php
@@ -73,7 +73,7 @@ final class BambooSapling extends Flowable{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($item instanceof Fertilizer || $item instanceof ItemBamboo){
- if($this->grow()){
+ if($this->grow($player)){
$item->pop();
return true;
}
@@ -87,7 +87,7 @@ final class BambooSapling extends Flowable{
}
}
- private function grow() : bool{
+ private function grow(?Player $player) : bool{
$world = $this->position->getWorld();
if(!$world->getBlock($this->position->up())->canBeReplaced()){
return false;
@@ -98,7 +98,7 @@ final class BambooSapling extends Flowable{
$tx->addBlock($this->position, $bamboo)
->addBlock($this->position->up(), (clone $bamboo)->setLeafSize(Bamboo::SMALL_LEAVES));
- $ev = new StructureGrowEvent($this, $tx);
+ $ev = new StructureGrowEvent($this, $tx, $player);
$ev->call();
if($ev->isCancelled()){
return false;
@@ -115,7 +115,7 @@ final class BambooSapling extends Flowable{
$world = $this->position->getWorld();
if($this->ready){
$this->ready = false;
- if($world->getFullLight($this->position) < 9 || !$this->grow()){
+ if($world->getFullLight($this->position) < 9 || !$this->grow(null)){
$world->setBlock($this->position, $this);
}
}elseif($world->getBlock($this->position->up())->canBeReplaced()){
diff --git a/src/block/BaseBanner.php b/src/block/BaseBanner.php
index 0f7d55ae5..64e767897 100644
--- a/src/block/BaseBanner.php
+++ b/src/block/BaseBanner.php
@@ -130,7 +130,7 @@ abstract class BaseBanner extends Transparent{
public function getDropsForCompatibleTool(Item $item) : array{
$drop = $this->asItem();
- if($drop instanceof ItemBanner and count($this->patterns) > 0){
+ if($drop instanceof ItemBanner && count($this->patterns) > 0){
$drop->setPatterns($this->patterns);
}
@@ -139,7 +139,7 @@ abstract class BaseBanner extends Transparent{
public function getPickedItem(bool $addUserData = false) : Item{
$result = $this->asItem();
- if($addUserData and $result instanceof ItemBanner and count($this->patterns) > 0){
+ if($addUserData && $result instanceof ItemBanner && count($this->patterns) > 0){
$result->setPatterns($this->patterns);
}
return $result;
diff --git a/src/block/BaseCoral.php b/src/block/BaseCoral.php
index f3b37988a..71b1a5880 100644
--- a/src/block/BaseCoral.php
+++ b/src/block/BaseCoral.php
@@ -24,34 +24,17 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\CoralType;
+use pocketmine\block\utils\CoralTypeTrait;
use pocketmine\item\Item;
abstract class BaseCoral extends Transparent{
-
- protected CoralType $coralType;
- protected bool $dead = false;
+ use CoralTypeTrait;
public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){
parent::__construct($idInfo, $name, $breakInfo);
$this->coralType = CoralType::TUBE();
}
- public function getCoralType() : CoralType{ return $this->coralType; }
-
- /** @return $this */
- public function setCoralType(CoralType $coralType) : self{
- $this->coralType = $coralType;
- return $this;
- }
-
- public function isDead() : bool{ return $this->dead; }
-
- /** @return $this */
- public function setDead(bool $dead) : self{
- $this->dead = $dead;
- return $this;
- }
-
public function onNearbyBlockChange() : void{
if(!$this->dead){
$world = $this->position->getWorld();
diff --git a/src/block/BaseRail.php b/src/block/BaseRail.php
index bf117cd7c..0344cdba8 100644
--- a/src/block/BaseRail.php
+++ b/src/block/BaseRail.php
@@ -101,7 +101,7 @@ abstract class BaseRail extends Flowable{
}
if(
- $other instanceof BaseRail and
+ $other instanceof BaseRail &&
in_array($otherConnection, $other->getCurrentShapeConnections(), true)
){
$connections[] = $connection;
@@ -179,7 +179,7 @@ abstract class BaseRail extends Flowable{
$otherSide |= RailConnectionInfo::FLAG_ASCEND;
}
- if(!($other instanceof BaseRail) or count($otherConnections = $other->getConnectedDirections()) >= 2){
+ if(!($other instanceof BaseRail) || count($otherConnections = $other->getConnectedDirections()) >= 2){
//we can only connect to a rail that has less than 2 connections
continue;
}
@@ -224,7 +224,7 @@ abstract class BaseRail extends Flowable{
$this->position->getWorld()->useBreakOn($this->position);
}else{
foreach($this->getCurrentShapeConnections() as $connection){
- if(($connection & RailConnectionInfo::FLAG_ASCEND) !== 0 and $this->getSide($connection & ~RailConnectionInfo::FLAG_ASCEND)->isTransparent()){
+ if(($connection & RailConnectionInfo::FLAG_ASCEND) !== 0 && $this->getSide($connection & ~RailConnectionInfo::FLAG_ASCEND)->isTransparent()){
$this->position->getWorld()->useBreakOn($this->position);
break;
}
diff --git a/src/block/BaseSign.php b/src/block/BaseSign.php
index 586eee6a1..42e6bf178 100644
--- a/src/block/BaseSign.php
+++ b/src/block/BaseSign.php
@@ -37,8 +37,6 @@ use function assert;
use function strlen;
abstract class BaseSign extends Transparent{
- //TODO: conditionally useless properties, find a way to fix
-
protected SignText $text;
protected ?int $editorEntityRuntimeId = null;
diff --git a/src/block/Bed.php b/src/block/Bed.php
index fe419547a..fd566bca3 100644
--- a/src/block/Bed.php
+++ b/src/block/Bed.php
@@ -120,7 +120,7 @@ class Bed extends Transparent{
public function getOtherHalf() : ?Bed{
$other = $this->getSide($this->getOtherHalfSide());
- if($other instanceof Bed and $other->head !== $this->head and $other->facing === $this->facing){
+ if($other instanceof Bed && $other->head !== $this->head && $other->facing === $this->facing){
return $other;
}
@@ -135,17 +135,17 @@ class Bed extends Transparent{
$player->sendMessage(TextFormat::GRAY . "This bed is incomplete");
return true;
- }elseif($playerPos->distanceSquared($this->position) > 4 and $playerPos->distanceSquared($other->position) > 4){
+ }elseif($playerPos->distanceSquared($this->position) > 4 && $playerPos->distanceSquared($other->position) > 4){
$player->sendMessage(KnownTranslationFactory::tile_bed_tooFar()->prefix(TextFormat::GRAY));
return true;
}
$time = $this->position->getWorld()->getTimeOfDay();
- $isNight = ($time >= World::TIME_NIGHT and $time < World::TIME_SUNRISE);
+ $isNight = ($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE);
if(!$isNight){
- $player->sendMessage(KnownTranslationFactory::tile_bed_tooFar()->prefix(TextFormat::GRAY));
+ $player->sendMessage(KnownTranslationFactory::tile_bed_noSleep()->prefix(TextFormat::GRAY));
return true;
}
@@ -166,7 +166,7 @@ class Bed extends Transparent{
}
public function onNearbyBlockChange() : void{
- if(($other = $this->getOtherHalf()) !== null and $other->occupied !== $this->occupied){
+ if(($other = $this->getOtherHalf()) !== null && $other->occupied !== $this->occupied){
$this->occupied = $other->occupied;
$this->position->getWorld()->setBlock($this->position, $this);
}
@@ -186,7 +186,7 @@ class Bed extends Transparent{
$this->facing = $player !== null ? $player->getHorizontalFacing() : Facing::NORTH;
$next = $this->getSide($this->getOtherHalfSide());
- if($next->canBeReplaced() and !$next->getSide(Facing::DOWN)->isTransparent()){
+ if($next->canBeReplaced() && !$next->getSide(Facing::DOWN)->isTransparent()){
$nextState = clone $this;
$nextState->head = true;
$tx->addBlock($blockReplace->position, $this)->addBlock($next->position, $nextState);
diff --git a/src/block/Bell.php b/src/block/Bell.php
index 1cffd606b..2b33610b6 100644
--- a/src/block/Bell.php
+++ b/src/block/Bell.php
@@ -37,8 +37,6 @@ use pocketmine\world\BlockTransaction;
use pocketmine\world\sound\BellRingSound;
final class Bell extends Transparent{
- private const BELL_RINGING_REPEAT_TICKS = 20;
-
use HorizontalFacingTrait;
private BellAttachmentType $attachmentType;
diff --git a/src/block/Block.php b/src/block/Block.php
index 7006db1ca..ef2ba2b81 100644
--- a/src/block/Block.php
+++ b/src/block/Block.php
@@ -148,14 +148,14 @@ class Block{
$tileType = $this->idInfo->getTileClass();
$oldTile = $this->position->getWorld()->getTile($this->position);
if($oldTile !== null){
- if($tileType === null or !($oldTile instanceof $tileType)){
+ if($tileType === null || !($oldTile instanceof $tileType)){
$oldTile->close();
$oldTile = null;
}elseif($oldTile instanceof Spawnable){
$oldTile->setDirty(); //destroy old network cache
}
}
- if($oldTile === null and $tileType !== null){
+ if($oldTile === null && $tileType !== null){
/**
* @var Tile $tile
* @see Tile::__construct()
@@ -166,20 +166,28 @@ class Block{
}
/**
- * Returns whether the given block has an equivalent type to this one. This compares base legacy ID and variant.
+ * Returns a type ID that identifies this type of block. This does not include information like facing, colour,
+ * powered/unpowered, etc.
+ */
+ public function getTypeId() : int{
+ return ($this->idInfo->getBlockId() << Block::INTERNAL_METADATA_BITS) | $this->idInfo->getVariant();
+ }
+
+ /**
+ * Returns whether the given block has an equivalent type to this one. This compares the type IDs.
*
* Note: This ignores additional IDs used to represent additional states. This means that, for example, a lit
* furnace and unlit furnace are considered the same type.
*/
public function isSameType(Block $other) : bool{
- return $this->idInfo->getBlockId() === $other->idInfo->getBlockId() and $this->idInfo->getVariant() === $other->idInfo->getVariant();
+ return $this->getTypeId() === $other->getTypeId();
}
/**
* Returns whether the given block has the same type and properties as this block.
*/
public function isSameState(Block $other) : bool{
- return $this->isSameType($other) and $this->writeStateToMeta() === $other->writeStateToMeta();
+ return $this->getFullId() === $other->getFullId();
}
/**
@@ -353,7 +361,7 @@ class Block{
*/
public function getDrops(Item $item) : array{
if($this->breakInfo->isToolCompatible($item)){
- if($this->isAffectedBySilkTouch() and $item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){
+ if($this->isAffectedBySilkTouch() && $item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){
return $this->getSilkTouchDrops($item);
}
@@ -394,7 +402,7 @@ class Block{
* Returns how much XP will be dropped by breaking this block with the given item.
*/
public function getXpDropForTool(Item $item) : int{
- if($item->hasEnchantment(VanillaEnchantments::SILK_TOUCH()) or !$this->breakInfo->isToolCompatible($item)){
+ if($item->hasEnchantment(VanillaEnchantments::SILK_TOUCH()) || !$this->breakInfo->isToolCompatible($item)){
return 0;
}
@@ -493,7 +501,7 @@ class Block{
return $this->position->getWorld()->getBlock($this->position->getSide($side, $step));
}
- throw new \InvalidStateException("Block does not have a valid world");
+ throw new \LogicException("Block does not have a valid world");
}
/**
@@ -577,7 +585,7 @@ class Block{
final public function getCollisionBoxes() : array{
if($this->collisionBoxes === null){
$this->collisionBoxes = $this->recalculateCollisionBoxes();
- $extraOffset = $this->getPositionOffset();
+ $extraOffset = $this->getModelPositionOffset();
$offset = $extraOffset !== null ? $this->position->addVector($extraOffset) : $this->position;
foreach($this->collisionBoxes as $bb){
$bb->offset($offset->x, $offset->y, $offset->z);
@@ -588,10 +596,10 @@ class Block{
}
/**
- * Returns an additional fractional vector to shift the block's effective position by based on the current position.
+ * Returns an additional fractional vector to shift the block model's position by based on the current position.
* Used to randomize position of things like bamboo canes and tall grass.
*/
- public function getPositionOffset() : ?Vector3{
+ public function getModelPositionOffset() : ?Vector3{
return null;
}
@@ -605,7 +613,7 @@ class Block{
public function isFullCube() : bool{
$bb = $this->getCollisionBoxes();
- return count($bb) === 1 and $bb[0]->getAverageEdgeLength() >= 1 and $bb[0]->isCube();
+ return count($bb) === 1 && $bb[0]->getAverageEdgeLength() >= 1 && $bb[0]->isCube();
}
public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{
diff --git a/src/block/BlockBreakInfo.php b/src/block/BlockBreakInfo.php
index 0290b02ab..184195aee 100644
--- a/src/block/BlockBreakInfo.php
+++ b/src/block/BlockBreakInfo.php
@@ -119,8 +119,8 @@ class BlockBreakInfo{
return false;
}
- return $this->toolType === BlockToolType::NONE or $this->toolHarvestLevel === 0 or (
- ($this->toolType & $tool->getBlockToolType()) !== 0 and $tool->getBlockToolHarvestLevel() >= $this->toolHarvestLevel);
+ return $this->toolType === BlockToolType::NONE || $this->toolHarvestLevel === 0 || (
+ ($this->toolType & $tool->getBlockToolType()) !== 0 && $tool->getBlockToolHarvestLevel() >= $this->toolHarvestLevel);
}
/**
diff --git a/src/block/BlockFactory.php b/src/block/BlockFactory.php
index ee2345f82..14d37d72d 100644
--- a/src/block/BlockFactory.php
+++ b/src/block/BlockFactory.php
@@ -43,6 +43,7 @@ use pocketmine\block\tile\FlowerPot as TileFlowerPot;
use pocketmine\block\tile\Hopper as TileHopper;
use pocketmine\block\tile\ItemFrame as TileItemFrame;
use pocketmine\block\tile\Jukebox as TileJukebox;
+use pocketmine\block\tile\Lectern as TileLectern;
use pocketmine\block\tile\MonsterSpawner as TileMonsterSpawner;
use pocketmine\block\tile\NormalFurnace as TileNormalFurnace;
use pocketmine\block\tile\Note as TileNote;
@@ -75,6 +76,12 @@ class BlockFactory{
*/
private $fullList;
+ /**
+ * @var \SplFixedArray|int[]
+ * @phpstan-var \SplFixedArray
+ */
+ private \SplFixedArray $mappedStateIds;
+
/**
* @var \SplFixedArray|int[]
* @phpstan-var \SplFixedArray
@@ -98,6 +105,7 @@ class BlockFactory{
public function __construct(){
$this->fullList = new \SplFixedArray(1024 << Block::INTERNAL_METADATA_BITS);
+ $this->mappedStateIds = new \SplFixedArray(1024 << Block::INTERNAL_METADATA_BITS);
$this->light = \SplFixedArray::fromArray(array_fill(0, 1024 << Block::INTERNAL_METADATA_BITS, 0));
$this->lightFilter = \SplFixedArray::fromArray(array_fill(0, 1024 << Block::INTERNAL_METADATA_BITS, 1));
@@ -105,364 +113,412 @@ class BlockFactory{
$this->blastResistance = \SplFixedArray::fromArray(array_fill(0, 1024 << Block::INTERNAL_METADATA_BITS, 0.0));
$railBreakInfo = new BlockBreakInfo(0.7);
- $this->register(new ActivatorRail(new BID(Ids::ACTIVATOR_RAIL, 0), "Activator Rail", $railBreakInfo));
- $this->register(new Air(new BID(Ids::AIR, 0), "Air", BlockBreakInfo::indestructible(-1.0)));
- $this->register(new Anvil(new BID(Ids::ANVIL, 0), "Anvil", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 6000.0)));
- $this->register(new Bamboo(new BID(Ids::BAMBOO, 0), "Bamboo", new BlockBreakInfo(2.0 /* 1.0 in PC */, BlockToolType::AXE)));
- $this->register(new BambooSapling(new BID(Ids::BAMBOO_SAPLING, 0), "Bamboo Sapling", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new ActivatorRail(new BID(Ids::ACTIVATOR_RAIL, 0), "Activator Rail", $railBreakInfo));
+ $this->registerAllMeta(new Air(new BID(Ids::AIR, 0), "Air", BlockBreakInfo::indestructible(-1.0)));
+ $this->registerAllMeta(new Anvil(new BID(Ids::ANVIL, 0), "Anvil", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 6000.0)));
+ $this->registerAllMeta(new Bamboo(new BID(Ids::BAMBOO, 0), "Bamboo", new class(2.0 /* 1.0 in PC */, BlockToolType::AXE) extends BlockBreakInfo{
+ public function getBreakTime(Item $item) : float{
+ if($item->getBlockToolType() === BlockToolType::SWORD){
+ return 0.0;
+ }
+ return parent::getBreakTime($item);
+ }
+ }));
+ $this->registerAllMeta(new BambooSapling(new BID(Ids::BAMBOO_SAPLING, 0), "Bamboo Sapling", BlockBreakInfo::instant()));
$bannerBreakInfo = new BlockBreakInfo(1.0, BlockToolType::AXE);
- $this->register(new FloorBanner(new BID(Ids::STANDING_BANNER, 0, ItemIds::BANNER, TileBanner::class), "Banner", $bannerBreakInfo));
- $this->register(new WallBanner(new BID(Ids::WALL_BANNER, 0, ItemIds::BANNER, TileBanner::class), "Wall Banner", $bannerBreakInfo));
- $this->register(new Barrel(new BID(Ids::BARREL, 0, null, TileBarrel::class), "Barrel", new BlockBreakInfo(2.5, BlockToolType::AXE)));
- $this->register(new Transparent(new BID(Ids::BARRIER, 0), "Barrier", BlockBreakInfo::indestructible()));
- $this->register(new Beacon(new BID(Ids::BEACON, 0, null, TileBeacon::class), "Beacon", new BlockBreakInfo(3.0)));
- $this->register(new Bed(new BID(Ids::BED_BLOCK, 0, ItemIds::BED, TileBed::class), "Bed Block", new BlockBreakInfo(0.2)));
- $this->register(new Bedrock(new BID(Ids::BEDROCK, 0), "Bedrock", BlockBreakInfo::indestructible()));
+ $this->registerAllMeta(new FloorBanner(new BID(Ids::STANDING_BANNER, 0, ItemIds::BANNER, TileBanner::class), "Banner", $bannerBreakInfo));
+ $this->registerAllMeta(new WallBanner(new BID(Ids::WALL_BANNER, 0, ItemIds::BANNER, TileBanner::class), "Wall Banner", $bannerBreakInfo));
+ $this->registerAllMeta(new Barrel(new BID(Ids::BARREL, 0, null, TileBarrel::class), "Barrel", new BlockBreakInfo(2.5, BlockToolType::AXE)));
+ $this->registerAllMeta(new Transparent(new BID(Ids::BARRIER, 0), "Barrier", BlockBreakInfo::indestructible()));
+ $this->registerAllMeta(new Beacon(new BID(Ids::BEACON, 0, null, TileBeacon::class), "Beacon", new BlockBreakInfo(3.0)));
+ $this->registerAllMeta(new Bed(new BID(Ids::BED_BLOCK, 0, ItemIds::BED, TileBed::class), "Bed Block", new BlockBreakInfo(0.2)));
+ $this->registerAllMeta(new Bedrock(new BID(Ids::BEDROCK, 0), "Bedrock", BlockBreakInfo::indestructible()));
- $this->register(new Beetroot(new BID(Ids::BEETROOT_BLOCK, 0), "Beetroot Block", BlockBreakInfo::instant()));
- $this->register(new Bell(new BID(Ids::BELL, 0, null, TileBell::class), "Bell", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new BlueIce(new BID(Ids::BLUE_ICE, 0), "Blue Ice", new BlockBreakInfo(2.8, BlockToolType::PICKAXE)));
- $this->register(new BoneBlock(new BID(Ids::BONE_BLOCK, 0), "Bone Block", new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Bookshelf(new BID(Ids::BOOKSHELF, 0), "Bookshelf", new BlockBreakInfo(1.5, BlockToolType::AXE)));
- $this->register(new BrewingStand(new BID(Ids::BREWING_STAND_BLOCK, 0, ItemIds::BREWING_STAND, TileBrewingStand::class), "Brewing Stand", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Beetroot(new BID(Ids::BEETROOT_BLOCK, 0), "Beetroot Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Bell(new BID(Ids::BELL, 0, null, TileBell::class), "Bell", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new BlueIce(new BID(Ids::BLUE_ICE, 0), "Blue Ice", new BlockBreakInfo(2.8, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new BoneBlock(new BID(Ids::BONE_BLOCK, 0), "Bone Block", new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Bookshelf(new BID(Ids::BOOKSHELF, 0), "Bookshelf", new BlockBreakInfo(1.5, BlockToolType::AXE)));
+ $this->registerAllMeta(new BrewingStand(new BID(Ids::BREWING_STAND_BLOCK, 0, ItemIds::BREWING_STAND, TileBrewingStand::class), "Brewing Stand", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
$bricksBreakInfo = new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register(new Stair(new BID(Ids::BRICK_STAIRS, 0), "Brick Stairs", $bricksBreakInfo));
- $this->register(new Opaque(new BID(Ids::BRICK_BLOCK, 0), "Bricks", $bricksBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::BRICK_STAIRS, 0), "Brick Stairs", $bricksBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::BRICK_BLOCK, 0), "Bricks", $bricksBreakInfo));
- $this->register(new BrownMushroom(new BID(Ids::BROWN_MUSHROOM, 0), "Brown Mushroom", BlockBreakInfo::instant()));
- $this->register(new Cactus(new BID(Ids::CACTUS, 0), "Cactus", new BlockBreakInfo(0.4)));
- $this->register(new Cake(new BID(Ids::CAKE_BLOCK, 0, ItemIds::CAKE), "Cake", new BlockBreakInfo(0.5)));
- $this->register(new Carrot(new BID(Ids::CARROTS, 0), "Carrot Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new BrownMushroom(new BID(Ids::BROWN_MUSHROOM, 0), "Brown Mushroom", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Cactus(new BID(Ids::CACTUS, 0), "Cactus", new BlockBreakInfo(0.4)));
+ $this->registerAllMeta(new Cake(new BID(Ids::CAKE_BLOCK, 0, ItemIds::CAKE), "Cake", new BlockBreakInfo(0.5)));
+ $this->registerAllMeta(new Carrot(new BID(Ids::CARROTS, 0), "Carrot Block", BlockBreakInfo::instant()));
$chestBreakInfo = new BlockBreakInfo(2.5, BlockToolType::AXE);
- $this->register(new Chest(new BID(Ids::CHEST, 0, null, TileChest::class), "Chest", $chestBreakInfo));
- $this->register(new Clay(new BID(Ids::CLAY_BLOCK, 0), "Clay Block", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
- $this->register(new Coal(new BID(Ids::COAL_BLOCK, 0), "Coal Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0)));
- $this->register(new CoalOre(new BID(Ids::COAL_ORE, 0), "Coal Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Chest(new BID(Ids::CHEST, 0, null, TileChest::class), "Chest", $chestBreakInfo));
+ $this->registerAllMeta(new Clay(new BID(Ids::CLAY_BLOCK, 0), "Clay Block", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Coal(new BID(Ids::COAL_BLOCK, 0), "Coal Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0)));
+ $this->registerAllMeta(new CoalOre(new BID(Ids::COAL_ORE, 0), "Coal Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
$cobblestoneBreakInfo = new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register($cobblestone = new Opaque(new BID(Ids::COBBLESTONE, 0), "Cobblestone", $cobblestoneBreakInfo));
- $infestedStoneBreakInfo = new BlockBreakInfo(0.75);
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_COBBLESTONE), "Infested Cobblestone", $infestedStoneBreakInfo, $cobblestone));
- $this->register(new Opaque(new BID(Ids::MOSSY_COBBLESTONE, 0), "Mossy Cobblestone", $cobblestoneBreakInfo));
- $this->register(new Stair(new BID(Ids::COBBLESTONE_STAIRS, 0), "Cobblestone Stairs", $cobblestoneBreakInfo));
- $this->register(new Stair(new BID(Ids::MOSSY_COBBLESTONE_STAIRS, 0), "Mossy Cobblestone Stairs", $cobblestoneBreakInfo));
+ $this->registerAllMeta($cobblestone = new Opaque(new BID(Ids::COBBLESTONE, 0), "Cobblestone", $cobblestoneBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::MOSSY_COBBLESTONE, 0), "Mossy Cobblestone", $cobblestoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::COBBLESTONE_STAIRS, 0), "Cobblestone Stairs", $cobblestoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::MOSSY_COBBLESTONE_STAIRS, 0), "Mossy Cobblestone Stairs", $cobblestoneBreakInfo));
- $this->register(new Cobweb(new BID(Ids::COBWEB, 0), "Cobweb", new BlockBreakInfo(4.0, BlockToolType::SWORD | BlockToolType::SHEARS, 1)));
- $this->register(new CocoaBlock(new BID(Ids::COCOA, 0), "Cocoa Block", new BlockBreakInfo(0.2, BlockToolType::AXE, 0, 15.0)));
- $this->register(new CoralBlock(new BID(Ids::CORAL_BLOCK, 0), "Coral Block", new BlockBreakInfo(7.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new CraftingTable(new BID(Ids::CRAFTING_TABLE, 0), "Crafting Table", new BlockBreakInfo(2.5, BlockToolType::AXE)));
- $this->register(new DaylightSensor(new BIDFlattened(Ids::DAYLIGHT_DETECTOR, [Ids::DAYLIGHT_DETECTOR_INVERTED], 0, null, TileDaylightSensor::class), "Daylight Sensor", new BlockBreakInfo(0.2, BlockToolType::AXE)));
- $this->register(new DeadBush(new BID(Ids::DEADBUSH, 0), "Dead Bush", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)));
- $this->register(new DetectorRail(new BID(Ids::DETECTOR_RAIL, 0), "Detector Rail", $railBreakInfo));
+ $this->registerAllMeta(new Cobweb(new BID(Ids::COBWEB, 0), "Cobweb", new BlockBreakInfo(4.0, BlockToolType::SWORD | BlockToolType::SHEARS, 1)));
+ $this->registerAllMeta(new CocoaBlock(new BID(Ids::COCOA, 0), "Cocoa Block", new BlockBreakInfo(0.2, BlockToolType::AXE, 0, 15.0)));
+ $this->registerAllMeta(new CoralBlock(new BID(Ids::CORAL_BLOCK, 0), "Coral Block", new BlockBreakInfo(7.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new CraftingTable(new BID(Ids::CRAFTING_TABLE, 0), "Crafting Table", new BlockBreakInfo(2.5, BlockToolType::AXE)));
+ $this->registerAllMeta(new DaylightSensor(new BIDFlattened(Ids::DAYLIGHT_DETECTOR, [Ids::DAYLIGHT_DETECTOR_INVERTED], 0, null, TileDaylightSensor::class), "Daylight Sensor", new BlockBreakInfo(0.2, BlockToolType::AXE)));
+ $this->registerAllMeta(new DeadBush(new BID(Ids::DEADBUSH, 0), "Dead Bush", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)));
+ $this->registerAllMeta(new DetectorRail(new BID(Ids::DETECTOR_RAIL, 0), "Detector Rail", $railBreakInfo));
- $this->register(new Opaque(new BID(Ids::DIAMOND_BLOCK, 0), "Diamond Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
- $this->register(new DiamondOre(new BID(Ids::DIAMOND_ORE, 0), "Diamond Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
- $this->register(new Dirt(new BID(Ids::DIRT, 0), "Dirt", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
- $this->register(new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_SUNFLOWER), "Sunflower", BlockBreakInfo::instant()));
- $this->register(new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_LILAC), "Lilac", BlockBreakInfo::instant()));
- $this->register(new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_ROSE_BUSH), "Rose Bush", BlockBreakInfo::instant()));
- $this->register(new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_PEONY), "Peony", BlockBreakInfo::instant()));
- $this->register(new DoubleTallGrass(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_TALLGRASS), "Double Tallgrass", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)));
- $this->register(new DoubleTallGrass(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_LARGE_FERN), "Large Fern", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)));
- $this->register(new DragonEgg(new BID(Ids::DRAGON_EGG, 0), "Dragon Egg", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new DriedKelp(new BID(Ids::DRIED_KELP_BLOCK, 0), "Dried Kelp Block", new BlockBreakInfo(0.5, BlockToolType::NONE, 0, 12.5)));
- $this->register(new Opaque(new BID(Ids::EMERALD_BLOCK, 0), "Emerald Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
- $this->register(new EmeraldOre(new BID(Ids::EMERALD_ORE, 0), "Emerald Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
- $this->register(new EnchantingTable(new BID(Ids::ENCHANTING_TABLE, 0, null, TileEnchantingTable::class), "Enchanting Table", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 6000.0)));
- $this->register(new EndPortalFrame(new BID(Ids::END_PORTAL_FRAME, 0), "End Portal Frame", BlockBreakInfo::indestructible()));
- $this->register(new EndRod(new BID(Ids::END_ROD, 0), "End Rod", BlockBreakInfo::instant()));
- $this->register(new Opaque(new BID(Ids::END_STONE, 0), "End Stone", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 45.0)));
+ $this->registerAllMeta(new Opaque(new BID(Ids::DIAMOND_BLOCK, 0), "Diamond Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
+ $this->registerAllMeta(new DiamondOre(new BID(Ids::DIAMOND_ORE, 0), "Diamond Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
+ $this->registerAllMeta(new Dirt(new BID(Ids::DIRT, 0), "Dirt", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(
+ new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_SUNFLOWER), "Sunflower", BlockBreakInfo::instant()),
+ new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_LILAC), "Lilac", BlockBreakInfo::instant()),
+ new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_ROSE_BUSH), "Rose Bush", BlockBreakInfo::instant()),
+ new DoublePlant(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_PEONY), "Peony", BlockBreakInfo::instant()),
+ new DoubleTallGrass(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_TALLGRASS), "Double Tallgrass", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)),
+ new DoubleTallGrass(new BID(Ids::DOUBLE_PLANT, Meta::DOUBLE_PLANT_LARGE_FERN), "Large Fern", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)),
+ );
+ $this->registerAllMeta(new DragonEgg(new BID(Ids::DRAGON_EGG, 0), "Dragon Egg", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new DriedKelp(new BID(Ids::DRIED_KELP_BLOCK, 0), "Dried Kelp Block", new BlockBreakInfo(0.5, BlockToolType::NONE, 0, 12.5)));
+ $this->registerAllMeta(new Opaque(new BID(Ids::EMERALD_BLOCK, 0), "Emerald Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
+ $this->registerAllMeta(new EmeraldOre(new BID(Ids::EMERALD_ORE, 0), "Emerald Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
+ $this->registerAllMeta(new EnchantingTable(new BID(Ids::ENCHANTING_TABLE, 0, null, TileEnchantingTable::class), "Enchanting Table", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 6000.0)));
+ $this->registerAllMeta(new EndPortalFrame(new BID(Ids::END_PORTAL_FRAME, 0), "End Portal Frame", BlockBreakInfo::indestructible()));
+ $this->registerAllMeta(new EndRod(new BID(Ids::END_ROD, 0), "End Rod", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Opaque(new BID(Ids::END_STONE, 0), "End Stone", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 45.0)));
$endBrickBreakInfo = new BlockBreakInfo(0.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 4.0);
- $this->register(new Opaque(new BID(Ids::END_BRICKS, 0), "End Stone Bricks", $endBrickBreakInfo));
- $this->register(new Stair(new BID(Ids::END_BRICK_STAIRS, 0), "End Stone Brick Stairs", $endBrickBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::END_BRICKS, 0), "End Stone Bricks", $endBrickBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::END_BRICK_STAIRS, 0), "End Stone Brick Stairs", $endBrickBreakInfo));
- $this->register(new EnderChest(new BID(Ids::ENDER_CHEST, 0, null, TileEnderChest::class), "Ender Chest", new BlockBreakInfo(22.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 3000.0)));
- $this->register(new Farmland(new BID(Ids::FARMLAND, 0), "Farmland", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
- $this->register(new Fire(new BID(Ids::FIRE, 0), "Fire Block", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::DANDELION, 0), "Dandelion", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_ALLIUM), "Allium", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_AZURE_BLUET), "Azure Bluet", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_BLUE_ORCHID), "Blue Orchid", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_CORNFLOWER), "Cornflower", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_LILY_OF_THE_VALLEY), "Lily of the Valley", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_ORANGE_TULIP), "Orange Tulip", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_OXEYE_DAISY), "Oxeye Daisy", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_PINK_TULIP), "Pink Tulip", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_POPPY), "Poppy", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_RED_TULIP), "Red Tulip", BlockBreakInfo::instant()));
- $this->register(new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_WHITE_TULIP), "White Tulip", BlockBreakInfo::instant()));
-
- $flowerPot = new FlowerPot(new BID(Ids::FLOWER_POT_BLOCK, 0, ItemIds::FLOWER_POT, TileFlowerPot::class), "Flower Pot", BlockBreakInfo::instant());
- $this->register($flowerPot);
- for($meta = 1; $meta < 16; ++$meta){
- $this->remap(Ids::FLOWER_POT_BLOCK, $meta, $flowerPot);
- }
- $this->register(new FrostedIce(new BID(Ids::FROSTED_ICE, 0), "Frosted Ice", new BlockBreakInfo(2.5, BlockToolType::PICKAXE)));
- $this->register(new Furnace(new BIDFlattened(Ids::FURNACE, [Ids::LIT_FURNACE], 0, null, TileNormalFurnace::class), "Furnace", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Furnace(new BIDFlattened(Ids::BLAST_FURNACE, [Ids::LIT_BLAST_FURNACE], 0, null, TileBlastFurnace::class), "Blast Furnace", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Furnace(new BIDFlattened(Ids::SMOKER, [Ids::LIT_SMOKER], 0, null, TileSmoker::class), "Smoker", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new EnderChest(new BID(Ids::ENDER_CHEST, 0, null, TileEnderChest::class), "Ender Chest", new BlockBreakInfo(22.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 3000.0)));
+ $this->registerAllMeta(new Farmland(new BID(Ids::FARMLAND, 0), "Farmland", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Fire(new BID(Ids::FIRE, 0), "Fire Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new FletchingTable(new BID(Ids::FLETCHING_TABLE, 0), "Fletching Table", new BlockBreakInfo(2.5, BlockToolType::AXE, 0, 2.5)));
+ $this->registerAllMeta(new Flower(new BID(Ids::DANDELION, 0), "Dandelion", BlockBreakInfo::instant()));
+ $this->registerAllMeta(
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_POPPY), "Poppy", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_ALLIUM), "Allium", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_AZURE_BLUET), "Azure Bluet", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_BLUE_ORCHID), "Blue Orchid", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_CORNFLOWER), "Cornflower", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_LILY_OF_THE_VALLEY), "Lily of the Valley", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_ORANGE_TULIP), "Orange Tulip", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_OXEYE_DAISY), "Oxeye Daisy", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_PINK_TULIP), "Pink Tulip", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_RED_TULIP), "Red Tulip", BlockBreakInfo::instant()),
+ new Flower(new BID(Ids::RED_FLOWER, Meta::FLOWER_WHITE_TULIP), "White Tulip", BlockBreakInfo::instant()),
+ );
+ $this->registerAllMeta(new FlowerPot(new BID(Ids::FLOWER_POT_BLOCK, 0, ItemIds::FLOWER_POT, TileFlowerPot::class), "Flower Pot", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new FrostedIce(new BID(Ids::FROSTED_ICE, 0), "Frosted Ice", new BlockBreakInfo(2.5, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new Furnace(new BIDFlattened(Ids::FURNACE, [Ids::LIT_FURNACE], 0, null, TileNormalFurnace::class), "Furnace", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Furnace(new BIDFlattened(Ids::BLAST_FURNACE, [Ids::LIT_BLAST_FURNACE], 0, null, TileBlastFurnace::class), "Blast Furnace", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Furnace(new BIDFlattened(Ids::SMOKER, [Ids::LIT_SMOKER], 0, null, TileSmoker::class), "Smoker", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
$glassBreakInfo = new BlockBreakInfo(0.3);
- $this->register(new Glass(new BID(Ids::GLASS, 0), "Glass", $glassBreakInfo));
- $this->register(new GlassPane(new BID(Ids::GLASS_PANE, 0), "Glass Pane", $glassBreakInfo));
- $this->register(new GlowingObsidian(new BID(Ids::GLOWINGOBSIDIAN, 0), "Glowing Obsidian", new BlockBreakInfo(10.0, BlockToolType::PICKAXE, ToolTier::DIAMOND()->getHarvestLevel(), 50.0)));
- $this->register(new Glowstone(new BID(Ids::GLOWSTONE, 0), "Glowstone", new BlockBreakInfo(0.3, BlockToolType::PICKAXE)));
- $this->register(new Opaque(new BID(Ids::GOLD_BLOCK, 0), "Gold Block", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
- $this->register(new Opaque(new BID(Ids::GOLD_ORE, 0), "Gold Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
+ $this->registerAllMeta(new Glass(new BID(Ids::GLASS, 0), "Glass", $glassBreakInfo));
+ $this->registerAllMeta(new GlassPane(new BID(Ids::GLASS_PANE, 0), "Glass Pane", $glassBreakInfo));
+ $this->registerAllMeta(new GlowingObsidian(new BID(Ids::GLOWINGOBSIDIAN, 0), "Glowing Obsidian", new BlockBreakInfo(10.0, BlockToolType::PICKAXE, ToolTier::DIAMOND()->getHarvestLevel(), 50.0)));
+ $this->registerAllMeta(new Glowstone(new BID(Ids::GLOWSTONE, 0), "Glowstone", new BlockBreakInfo(0.3, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new Opaque(new BID(Ids::GOLD_BLOCK, 0), "Gold Block", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel(), 30.0)));
+ $this->registerAllMeta(new Opaque(new BID(Ids::GOLD_ORE, 0), "Gold Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
$grassBreakInfo = new BlockBreakInfo(0.6, BlockToolType::SHOVEL);
- $this->register(new Grass(new BID(Ids::GRASS, 0), "Grass", $grassBreakInfo));
- $this->register(new GrassPath(new BID(Ids::GRASS_PATH, 0), "Grass Path", $grassBreakInfo));
- $this->register(new Gravel(new BID(Ids::GRAVEL, 0), "Gravel", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Grass(new BID(Ids::GRASS, 0), "Grass", $grassBreakInfo));
+ $this->registerAllMeta(new GrassPath(new BID(Ids::GRASS_PATH, 0), "Grass Path", $grassBreakInfo));
+ $this->registerAllMeta(new Gravel(new BID(Ids::GRAVEL, 0), "Gravel", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
$hardenedClayBreakInfo = new BlockBreakInfo(1.25, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 21.0);
- $this->register(new HardenedClay(new BID(Ids::HARDENED_CLAY, 0), "Hardened Clay", $hardenedClayBreakInfo));
+ $this->registerAllMeta(new HardenedClay(new BID(Ids::HARDENED_CLAY, 0), "Hardened Clay", $hardenedClayBreakInfo));
$hardenedGlassBreakInfo = new BlockBreakInfo(10.0);
- $this->register(new HardenedGlass(new BID(Ids::HARD_GLASS, 0), "Hardened Glass", $hardenedGlassBreakInfo));
- $this->register(new HardenedGlassPane(new BID(Ids::HARD_GLASS_PANE, 0), "Hardened Glass Pane", $hardenedGlassBreakInfo));
- $this->register(new HayBale(new BID(Ids::HAY_BALE, 0), "Hay Bale", new BlockBreakInfo(0.5)));
- $this->register(new Hopper(new BID(Ids::HOPPER_BLOCK, 0, ItemIds::HOPPER, TileHopper::class), "Hopper", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 15.0)));
- $this->register(new Ice(new BID(Ids::ICE, 0), "Ice", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new HardenedGlass(new BID(Ids::HARD_GLASS, 0), "Hardened Glass", $hardenedGlassBreakInfo));
+ $this->registerAllMeta(new HardenedGlassPane(new BID(Ids::HARD_GLASS_PANE, 0), "Hardened Glass Pane", $hardenedGlassBreakInfo));
+ $this->registerAllMeta(new HayBale(new BID(Ids::HAY_BALE, 0), "Hay Bale", new BlockBreakInfo(0.5)));
+ $this->registerAllMeta(new Hopper(new BID(Ids::HOPPER_BLOCK, 0, ItemIds::HOPPER, TileHopper::class), "Hopper", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 15.0)));
+ $this->registerAllMeta(new Ice(new BID(Ids::ICE, 0), "Ice", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
$updateBlockBreakInfo = new BlockBreakInfo(1.0);
- $this->register(new Opaque(new BID(Ids::INFO_UPDATE, 0), "update!", $updateBlockBreakInfo));
- $this->register(new Opaque(new BID(Ids::INFO_UPDATE2, 0), "ate!upd", $updateBlockBreakInfo));
- $this->register(new Transparent(new BID(Ids::INVISIBLEBEDROCK, 0), "Invisible Bedrock", BlockBreakInfo::indestructible()));
+ $this->registerAllMeta(new Opaque(new BID(Ids::INFO_UPDATE, 0), "update!", $updateBlockBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::INFO_UPDATE2, 0), "ate!upd", $updateBlockBreakInfo));
+ $this->registerAllMeta(new Transparent(new BID(Ids::INVISIBLEBEDROCK, 0), "Invisible Bedrock", BlockBreakInfo::indestructible()));
$ironBreakInfo = new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel(), 30.0);
- $this->register(new Opaque(new BID(Ids::IRON_BLOCK, 0), "Iron Block", $ironBreakInfo));
- $this->register(new Thin(new BID(Ids::IRON_BARS, 0), "Iron Bars", $ironBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::IRON_BLOCK, 0), "Iron Block", $ironBreakInfo));
+ $this->registerAllMeta(new Thin(new BID(Ids::IRON_BARS, 0), "Iron Bars", $ironBreakInfo));
$ironDoorBreakInfo = new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 25.0);
- $this->register(new Door(new BID(Ids::IRON_DOOR_BLOCK, 0, ItemIds::IRON_DOOR), "Iron Door", $ironDoorBreakInfo));
- $this->register(new Trapdoor(new BID(Ids::IRON_TRAPDOOR, 0), "Iron Trapdoor", $ironDoorBreakInfo));
- $this->register(new Opaque(new BID(Ids::IRON_ORE, 0), "Iron Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
- $this->register(new ItemFrame(new BID(Ids::FRAME_BLOCK, 0, ItemIds::FRAME, TileItemFrame::class), "Item Frame", new BlockBreakInfo(0.25)));
- $this->register(new Jukebox(new BID(Ids::JUKEBOX, 0, ItemIds::JUKEBOX, TileJukebox::class), "Jukebox", new BlockBreakInfo(0.8, BlockToolType::AXE))); //TODO: in PC the hardness is 2.0, not 0.8, unsure if this is a MCPE bug or not
- $this->register(new Ladder(new BID(Ids::LADDER, 0), "Ladder", new BlockBreakInfo(0.4, BlockToolType::AXE)));
- $this->register(new Lantern(new BID(Ids::LANTERN, 0), "Lantern", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Opaque(new BID(Ids::LAPIS_BLOCK, 0), "Lapis Lazuli Block", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
- $this->register(new LapisOre(new BID(Ids::LAPIS_ORE, 0), "Lapis Lazuli Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
- $this->register(new Lava(new BIDFlattened(Ids::FLOWING_LAVA, [Ids::STILL_LAVA], 0), "Lava", BlockBreakInfo::indestructible(500.0)));
- $this->register(new Lever(new BID(Ids::LEVER, 0), "Lever", new BlockBreakInfo(0.5)));
- $this->register(new Loom(new BID(Ids::LOOM, 0), "Loom", new BlockBreakInfo(2.5, BlockToolType::AXE)));
- $this->register(new Magma(new BID(Ids::MAGMA, 0), "Magma Block", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Melon(new BID(Ids::MELON_BLOCK, 0), "Melon Block", new BlockBreakInfo(1.0, BlockToolType::AXE)));
- $this->register(new MelonStem(new BID(Ids::MELON_STEM, 0, ItemIds::MELON_SEEDS), "Melon Stem", BlockBreakInfo::instant()));
- $this->register(new MonsterSpawner(new BID(Ids::MOB_SPAWNER, 0, null, TileMonsterSpawner::class), "Monster Spawner", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Mycelium(new BID(Ids::MYCELIUM, 0), "Mycelium", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Door(new BID(Ids::IRON_DOOR_BLOCK, 0, ItemIds::IRON_DOOR), "Iron Door", $ironDoorBreakInfo));
+ $this->registerAllMeta(new Trapdoor(new BID(Ids::IRON_TRAPDOOR, 0), "Iron Trapdoor", $ironDoorBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::IRON_ORE, 0), "Iron Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
+ $this->registerAllMeta(new ItemFrame(new BID(Ids::FRAME_BLOCK, 0, ItemIds::FRAME, TileItemFrame::class), "Item Frame", new BlockBreakInfo(0.25)));
+ $this->registerAllMeta(new Jukebox(new BID(Ids::JUKEBOX, 0, ItemIds::JUKEBOX, TileJukebox::class), "Jukebox", new BlockBreakInfo(0.8, BlockToolType::AXE))); //TODO: in PC the hardness is 2.0, not 0.8, unsure if this is a MCPE bug or not
+ $this->registerAllMeta(new Ladder(new BID(Ids::LADDER, 0), "Ladder", new BlockBreakInfo(0.4, BlockToolType::AXE)));
+ $this->registerAllMeta(new Lantern(new BID(Ids::LANTERN, 0), "Lantern", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Opaque(new BID(Ids::LAPIS_BLOCK, 0), "Lapis Lazuli Block", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
+ $this->registerAllMeta(new LapisOre(new BID(Ids::LAPIS_ORE, 0), "Lapis Lazuli Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::STONE()->getHarvestLevel())));
+ $this->registerAllMeta(new Lava(new BIDFlattened(Ids::FLOWING_LAVA, [Ids::STILL_LAVA], 0), "Lava", BlockBreakInfo::indestructible(500.0)));
+ $this->registerAllMeta(new Lectern(new BID(Ids::LECTERN, 0, ItemIds::LECTERN, TileLectern::class), "Lectern", new BlockBreakInfo(2.0, BlockToolType::AXE)));
+ $this->registerAllMeta(new Lever(new BID(Ids::LEVER, 0), "Lever", new BlockBreakInfo(0.5)));
+ $this->registerAllMeta(new Loom(new BID(Ids::LOOM, 0), "Loom", new BlockBreakInfo(2.5, BlockToolType::AXE)));
+ $this->registerAllMeta(new Magma(new BID(Ids::MAGMA, 0), "Magma Block", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Melon(new BID(Ids::MELON_BLOCK, 0), "Melon Block", new BlockBreakInfo(1.0, BlockToolType::AXE)));
+ $this->registerAllMeta(new MelonStem(new BID(Ids::MELON_STEM, 0, ItemIds::MELON_SEEDS), "Melon Stem", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new MonsterSpawner(new BID(Ids::MOB_SPAWNER, 0, null, TileMonsterSpawner::class), "Monster Spawner", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Mycelium(new BID(Ids::MYCELIUM, 0), "Mycelium", new BlockBreakInfo(0.6, BlockToolType::SHOVEL)));
$netherBrickBreakInfo = new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register(new Opaque(new BID(Ids::NETHER_BRICK_BLOCK, 0), "Nether Bricks", $netherBrickBreakInfo));
- $this->register(new Opaque(new BID(Ids::RED_NETHER_BRICK, 0), "Red Nether Bricks", $netherBrickBreakInfo));
- $this->register(new Fence(new BID(Ids::NETHER_BRICK_FENCE, 0), "Nether Brick Fence", $netherBrickBreakInfo));
- $this->register(new Stair(new BID(Ids::NETHER_BRICK_STAIRS, 0), "Nether Brick Stairs", $netherBrickBreakInfo));
- $this->register(new Stair(new BID(Ids::RED_NETHER_BRICK_STAIRS, 0), "Red Nether Brick Stairs", $netherBrickBreakInfo));
- $this->register(new NetherPortal(new BID(Ids::PORTAL, 0), "Nether Portal", BlockBreakInfo::indestructible(0.0)));
- $this->register(new NetherQuartzOre(new BID(Ids::NETHER_QUARTZ_ORE, 0), "Nether Quartz Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new NetherReactor(new BID(Ids::NETHERREACTOR, 0), "Nether Reactor Core", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Opaque(new BID(Ids::NETHER_WART_BLOCK, 0), "Nether Wart Block", new BlockBreakInfo(1.0)));
- $this->register(new NetherWartPlant(new BID(Ids::NETHER_WART_PLANT, 0, ItemIds::NETHER_WART), "Nether Wart", BlockBreakInfo::instant()));
- $this->register(new Netherrack(new BID(Ids::NETHERRACK, 0), "Netherrack", new BlockBreakInfo(0.4, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Note(new BID(Ids::NOTEBLOCK, 0, null, TileNote::class), "Note Block", new BlockBreakInfo(0.8, BlockToolType::AXE)));
- $this->register(new Opaque(new BID(Ids::OBSIDIAN, 0), "Obsidian", new BlockBreakInfo(35.0 /* 50 in PC */, BlockToolType::PICKAXE, ToolTier::DIAMOND()->getHarvestLevel(), 6000.0)));
- $this->register(new PackedIce(new BID(Ids::PACKED_ICE, 0), "Packed Ice", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
- $this->register(new Podzol(new BID(Ids::PODZOL, 0), "Podzol", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
- $this->register(new Potato(new BID(Ids::POTATOES, 0), "Potato Block", BlockBreakInfo::instant()));
- $this->register(new PoweredRail(new BID(Ids::GOLDEN_RAIL, Meta::RAIL_STRAIGHT_NORTH_SOUTH), "Powered Rail", $railBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::NETHER_BRICK_BLOCK, 0), "Nether Bricks", $netherBrickBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::RED_NETHER_BRICK, 0), "Red Nether Bricks", $netherBrickBreakInfo));
+ $this->registerAllMeta(new Fence(new BID(Ids::NETHER_BRICK_FENCE, 0), "Nether Brick Fence", $netherBrickBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::NETHER_BRICK_STAIRS, 0), "Nether Brick Stairs", $netherBrickBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::RED_NETHER_BRICK_STAIRS, 0), "Red Nether Brick Stairs", $netherBrickBreakInfo));
+ $this->registerAllMeta(new NetherPortal(new BID(Ids::PORTAL, 0), "Nether Portal", BlockBreakInfo::indestructible(0.0)));
+ $this->registerAllMeta(new NetherQuartzOre(new BID(Ids::NETHER_QUARTZ_ORE, 0), "Nether Quartz Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new NetherReactor(new BID(Ids::NETHERREACTOR, 0), "Nether Reactor Core", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Opaque(new BID(Ids::NETHER_WART_BLOCK, 0), "Nether Wart Block", new BlockBreakInfo(1.0, BlockToolType::HOE)));
+ $this->registerAllMeta(new NetherWartPlant(new BID(Ids::NETHER_WART_PLANT, 0, ItemIds::NETHER_WART), "Nether Wart", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Netherrack(new BID(Ids::NETHERRACK, 0), "Netherrack", new BlockBreakInfo(0.4, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Note(new BID(Ids::NOTEBLOCK, 0, null, TileNote::class), "Note Block", new BlockBreakInfo(0.8, BlockToolType::AXE)));
+ $this->registerAllMeta(new Opaque(new BID(Ids::OBSIDIAN, 0), "Obsidian", new BlockBreakInfo(35.0 /* 50 in PC */, BlockToolType::PICKAXE, ToolTier::DIAMOND()->getHarvestLevel(), 6000.0)));
+ $this->registerAllMeta(new PackedIce(new BID(Ids::PACKED_ICE, 0), "Packed Ice", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new Podzol(new BID(Ids::PODZOL, 0), "Podzol", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Potato(new BID(Ids::POTATOES, 0), "Potato Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new PoweredRail(new BID(Ids::GOLDEN_RAIL, 0), "Powered Rail", $railBreakInfo));
$prismarineBreakInfo = new BlockBreakInfo(1.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register(new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_BRICKS), "Prismarine Bricks", $prismarineBreakInfo));
- $this->register(new Stair(new BID(Ids::PRISMARINE_BRICKS_STAIRS, 0), "Prismarine Bricks Stairs", $prismarineBreakInfo));
- $this->register(new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_DARK), "Dark Prismarine", $prismarineBreakInfo));
- $this->register(new Stair(new BID(Ids::DARK_PRISMARINE_STAIRS, 0), "Dark Prismarine Stairs", $prismarineBreakInfo));
- $this->register(new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_NORMAL), "Prismarine", $prismarineBreakInfo));
- $this->register(new Stair(new BID(Ids::PRISMARINE_STAIRS, 0), "Prismarine Stairs", $prismarineBreakInfo));
+ $this->registerAllMeta(
+ new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_NORMAL), "Prismarine", $prismarineBreakInfo),
+ new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_DARK), "Dark Prismarine", $prismarineBreakInfo),
+ new Opaque(new BID(Ids::PRISMARINE, Meta::PRISMARINE_BRICKS), "Prismarine Bricks", $prismarineBreakInfo)
+ );
+ $this->registerAllMeta(new Stair(new BID(Ids::PRISMARINE_BRICKS_STAIRS, 0), "Prismarine Bricks Stairs", $prismarineBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::DARK_PRISMARINE_STAIRS, 0), "Dark Prismarine Stairs", $prismarineBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::PRISMARINE_STAIRS, 0), "Prismarine Stairs", $prismarineBreakInfo));
$pumpkinBreakInfo = new BlockBreakInfo(1.0, BlockToolType::AXE);
- $this->register($pumpkin = new Opaque(new BID(Ids::PUMPKIN, 0), "Pumpkin", $pumpkinBreakInfo));
- for($i = 1; $i <= 3; ++$i){
- $this->remap(Ids::PUMPKIN, $i, $pumpkin);
- }
- $this->register(new CarvedPumpkin(new BID(Ids::CARVED_PUMPKIN, 0), "Carved Pumpkin", $pumpkinBreakInfo));
- $this->register(new LitPumpkin(new BID(Ids::JACK_O_LANTERN, 0), "Jack o'Lantern", $pumpkinBreakInfo));
+ $this->registerAllMeta(new Pumpkin(new BID(Ids::PUMPKIN, 0), "Pumpkin", $pumpkinBreakInfo));
+ $this->registerAllMeta(new CarvedPumpkin(new BID(Ids::CARVED_PUMPKIN, 0), "Carved Pumpkin", $pumpkinBreakInfo));
+ $this->registerAllMeta(new LitPumpkin(new BID(Ids::JACK_O_LANTERN, 0), "Jack o'Lantern", $pumpkinBreakInfo));
- $this->register(new PumpkinStem(new BID(Ids::PUMPKIN_STEM, 0, ItemIds::PUMPKIN_SEEDS), "Pumpkin Stem", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new PumpkinStem(new BID(Ids::PUMPKIN_STEM, 0, ItemIds::PUMPKIN_SEEDS), "Pumpkin Stem", BlockBreakInfo::instant()));
$purpurBreakInfo = new BlockBreakInfo(1.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register(new Opaque(new BID(Ids::PURPUR_BLOCK, Meta::PURPUR_NORMAL), "Purpur Block", $purpurBreakInfo));
- $this->register(new SimplePillar(new BID(Ids::PURPUR_BLOCK, Meta::PURPUR_PILLAR), "Purpur Pillar", $purpurBreakInfo));
- $this->register(new Stair(new BID(Ids::PURPUR_STAIRS, 0), "Purpur Stairs", $purpurBreakInfo));
+ $this->registerAllMeta(
+ new Opaque(new BID(Ids::PURPUR_BLOCK, Meta::PURPUR_NORMAL), "Purpur Block", $purpurBreakInfo),
+ new SimplePillar(new BID(Ids::PURPUR_BLOCK, Meta::PURPUR_PILLAR), "Purpur Pillar", $purpurBreakInfo)
+ );
+ $this->registerAllMeta(new Stair(new BID(Ids::PURPUR_STAIRS, 0), "Purpur Stairs", $purpurBreakInfo));
$quartzBreakInfo = new BlockBreakInfo(0.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
- $this->register(new Opaque(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_NORMAL), "Quartz Block", $quartzBreakInfo));
- $this->register(new Stair(new BID(Ids::QUARTZ_STAIRS, 0), "Quartz Stairs", $quartzBreakInfo));
- $this->register(new SimplePillar(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_CHISELED), "Chiseled Quartz Block", $quartzBreakInfo));
- $this->register(new SimplePillar(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_PILLAR), "Quartz Pillar", $quartzBreakInfo));
- $this->register(new Opaque(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_SMOOTH), "Smooth Quartz Block", $quartzBreakInfo)); //TODO: this has axis rotation in 1.9, unsure if a bug (https://bugs.mojang.com/browse/MCPE-39074)
- $this->register(new Stair(new BID(Ids::SMOOTH_QUARTZ_STAIRS, 0), "Smooth Quartz Stairs", $quartzBreakInfo));
+ $this->registerAllMeta(
+ new Opaque(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_NORMAL), "Quartz Block", $quartzBreakInfo),
+ new SimplePillar(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_CHISELED), "Chiseled Quartz Block", $quartzBreakInfo),
+ new SimplePillar(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_PILLAR), "Quartz Pillar", $quartzBreakInfo),
+ new Opaque(new BID(Ids::QUARTZ_BLOCK, Meta::QUARTZ_SMOOTH), "Smooth Quartz Block", $quartzBreakInfo) //TODO: we may need to account for the fact this previously incorrectly had axis
+ );
+ $this->registerAllMeta(new Stair(new BID(Ids::QUARTZ_STAIRS, 0), "Quartz Stairs", $quartzBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::SMOOTH_QUARTZ_STAIRS, 0), "Smooth Quartz Stairs", $quartzBreakInfo));
- $this->register(new Rail(new BID(Ids::RAIL, 0), "Rail", $railBreakInfo));
- $this->register(new RedMushroom(new BID(Ids::RED_MUSHROOM, 0), "Red Mushroom", BlockBreakInfo::instant()));
- $this->register(new Redstone(new BID(Ids::REDSTONE_BLOCK, 0), "Redstone Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0)));
- $this->register(new RedstoneComparator(new BIDFlattened(Ids::UNPOWERED_COMPARATOR, [Ids::POWERED_COMPARATOR], 0, ItemIds::COMPARATOR, TileComparator::class), "Redstone Comparator", BlockBreakInfo::instant()));
- $this->register(new RedstoneLamp(new BIDFlattened(Ids::REDSTONE_LAMP, [Ids::LIT_REDSTONE_LAMP], 0), "Redstone Lamp", new BlockBreakInfo(0.3)));
- $this->register(new RedstoneOre(new BIDFlattened(Ids::REDSTONE_ORE, [Ids::LIT_REDSTONE_ORE], 0), "Redstone Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
- $this->register(new RedstoneRepeater(new BIDFlattened(Ids::UNPOWERED_REPEATER, [Ids::POWERED_REPEATER], 0, ItemIds::REPEATER), "Redstone Repeater", BlockBreakInfo::instant()));
- $this->register(new RedstoneTorch(new BIDFlattened(Ids::REDSTONE_TORCH, [Ids::UNLIT_REDSTONE_TORCH], 0), "Redstone Torch", BlockBreakInfo::instant()));
- $this->register(new RedstoneWire(new BID(Ids::REDSTONE_WIRE, 0, ItemIds::REDSTONE), "Redstone", BlockBreakInfo::instant()));
- $this->register(new Reserved6(new BID(Ids::RESERVED6, 0), "reserved6", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Rail(new BID(Ids::RAIL, 0), "Rail", $railBreakInfo));
+ $this->registerAllMeta(new RedMushroom(new BID(Ids::RED_MUSHROOM, 0), "Red Mushroom", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Redstone(new BID(Ids::REDSTONE_BLOCK, 0), "Redstone Block", new BlockBreakInfo(5.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0)));
+ $this->registerAllMeta(new RedstoneComparator(new BIDFlattened(Ids::UNPOWERED_COMPARATOR, [Ids::POWERED_COMPARATOR], 0, ItemIds::COMPARATOR, TileComparator::class), "Redstone Comparator", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new RedstoneLamp(new BIDFlattened(Ids::REDSTONE_LAMP, [Ids::LIT_REDSTONE_LAMP], 0), "Redstone Lamp", new BlockBreakInfo(0.3)));
+ $this->registerAllMeta(new RedstoneOre(new BIDFlattened(Ids::REDSTONE_ORE, [Ids::LIT_REDSTONE_ORE], 0), "Redstone Ore", new BlockBreakInfo(3.0, BlockToolType::PICKAXE, ToolTier::IRON()->getHarvestLevel())));
+ $this->registerAllMeta(new RedstoneRepeater(new BIDFlattened(Ids::UNPOWERED_REPEATER, [Ids::POWERED_REPEATER], 0, ItemIds::REPEATER), "Redstone Repeater", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new RedstoneTorch(new BIDFlattened(Ids::REDSTONE_TORCH, [Ids::UNLIT_REDSTONE_TORCH], 0), "Redstone Torch", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new RedstoneWire(new BID(Ids::REDSTONE_WIRE, 0, ItemIds::REDSTONE), "Redstone", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Reserved6(new BID(Ids::RESERVED6, 0), "reserved6", BlockBreakInfo::instant()));
$sandBreakInfo = new BlockBreakInfo(0.5, BlockToolType::SHOVEL);
- $this->register(new Sand(new BID(Ids::SAND, 0), "Sand", $sandBreakInfo));
- $this->register(new Sand(new BID(Ids::SAND, 1), "Red Sand", $sandBreakInfo));
- $this->register(new SeaLantern(new BID(Ids::SEALANTERN, 0), "Sea Lantern", new BlockBreakInfo(0.3)));
- $this->register(new SeaPickle(new BID(Ids::SEA_PICKLE, 0), "Sea Pickle", BlockBreakInfo::instant()));
- $this->register(new Skull(new BID(Ids::MOB_HEAD_BLOCK, 0, ItemIds::SKULL, TileSkull::class), "Mob Head", new BlockBreakInfo(1.0)));
- $this->register(new Slime(new BID(Ids::SLIME, 0), "Slime Block", BlockBreakInfo::instant()));
- $this->register(new Snow(new BID(Ids::SNOW, 0), "Snow Block", new BlockBreakInfo(0.2, BlockToolType::SHOVEL, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new SnowLayer(new BID(Ids::SNOW_LAYER, 0), "Snow Layer", new BlockBreakInfo(0.1, BlockToolType::SHOVEL, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new SoulSand(new BID(Ids::SOUL_SAND, 0), "Soul Sand", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
- $this->register(new Sponge(new BID(Ids::SPONGE, 0), "Sponge", new BlockBreakInfo(0.6, BlockToolType::HOE)));
+ $this->registerAllMeta(
+ new Sand(new BID(Ids::SAND, 0), "Sand", $sandBreakInfo),
+ new Sand(new BID(Ids::SAND, 1), "Red Sand", $sandBreakInfo)
+ );
+ $this->registerAllMeta(new SeaLantern(new BID(Ids::SEALANTERN, 0), "Sea Lantern", new BlockBreakInfo(0.3)));
+ $this->registerAllMeta(new SeaPickle(new BID(Ids::SEA_PICKLE, 0), "Sea Pickle", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Skull(new BID(Ids::MOB_HEAD_BLOCK, 0, ItemIds::SKULL, TileSkull::class), "Mob Head", new BlockBreakInfo(1.0)));
+ $this->registerAllMeta(new Slime(new BID(Ids::SLIME, 0), "Slime Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Snow(new BID(Ids::SNOW, 0), "Snow Block", new BlockBreakInfo(0.2, BlockToolType::SHOVEL, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new SnowLayer(new BID(Ids::SNOW_LAYER, 0), "Snow Layer", new BlockBreakInfo(0.1, BlockToolType::SHOVEL, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new SoulSand(new BID(Ids::SOUL_SAND, 0), "Soul Sand", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Sponge(new BID(Ids::SPONGE, 0), "Sponge", new BlockBreakInfo(0.6, BlockToolType::HOE)));
$shulkerBoxBreakInfo = new BlockBreakInfo(2, BlockToolType::PICKAXE);
- $this->register(new ShulkerBox(new BID(Ids::UNDYED_SHULKER_BOX, 0, null, TileShulkerBox::class), "Shulker Box", $shulkerBoxBreakInfo));
+ $this->registerAllMeta(new ShulkerBox(new BID(Ids::UNDYED_SHULKER_BOX, 0, null, TileShulkerBox::class), "Shulker Box", $shulkerBoxBreakInfo));
$stoneBreakInfo = new BlockBreakInfo(1.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register($stone = new class(new BID(Ids::STONE, Meta::STONE_NORMAL), "Stone", $stoneBreakInfo) extends Opaque{
- public function getDropsForCompatibleTool(Item $item) : array{
- return [VanillaBlocks::COBBLESTONE()->asItem()];
- }
+ $this->registerAllMeta(
+ $stone = new class(new BID(Ids::STONE, Meta::STONE_NORMAL), "Stone", $stoneBreakInfo) extends Opaque{
+ public function getDropsForCompatibleTool(Item $item) : array{
+ return [VanillaBlocks::COBBLESTONE()->asItem()];
+ }
- public function isAffectedBySilkTouch() : bool{
- return true;
- }
- });
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE), "Infested Stone", $infestedStoneBreakInfo, $stone));
- $this->register(new Stair(new BID(Ids::NORMAL_STONE_STAIRS, 0), "Stone Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::SMOOTH_STONE, 0), "Smooth Stone", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_ANDESITE), "Andesite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::ANDESITE_STAIRS, 0), "Andesite Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_DIORITE), "Diorite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::DIORITE_STAIRS, 0), "Diorite Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_GRANITE), "Granite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::GRANITE_STAIRS, 0), "Granite Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_ANDESITE), "Polished Andesite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::POLISHED_ANDESITE_STAIRS, 0), "Polished Andesite Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_DIORITE), "Polished Diorite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::POLISHED_DIORITE_STAIRS, 0), "Polished Diorite Stairs", $stoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_GRANITE), "Polished Granite", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::POLISHED_GRANITE_STAIRS, 0), "Polished Granite Stairs", $stoneBreakInfo));
- $this->register(new Stair(new BID(Ids::STONE_BRICK_STAIRS, 0), "Stone Brick Stairs", $stoneBreakInfo));
- $this->register($chiseledStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_CHISELED), "Chiseled Stone Bricks", $stoneBreakInfo));
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_CHISELED), "Infested Chiseled Stone Brick", $infestedStoneBreakInfo, $chiseledStoneBrick));
- $this->register($crackedStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_CRACKED), "Cracked Stone Bricks", $stoneBreakInfo));
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_CRACKED), "Infested Cracked Stone Brick", $infestedStoneBreakInfo, $crackedStoneBrick));
- $this->register($mossyStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_MOSSY), "Mossy Stone Bricks", $stoneBreakInfo));
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_MOSSY), "Infested Mossy Stone Brick", $infestedStoneBreakInfo, $mossyStoneBrick));
- $this->register(new Stair(new BID(Ids::MOSSY_STONE_BRICK_STAIRS, 0), "Mossy Stone Brick Stairs", $stoneBreakInfo));
- $this->register($stoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_NORMAL), "Stone Bricks", $stoneBreakInfo));
- $this->register(new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK), "Infested Stone Brick", $infestedStoneBreakInfo, $stoneBrick));
- $this->register(new StoneButton(new BID(Ids::STONE_BUTTON, 0), "Stone Button", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
- $this->register(new StonePressurePlate(new BID(Ids::STONE_PRESSURE_PLATE, 0), "Stone Pressure Plate", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ public function isAffectedBySilkTouch() : bool{
+ return true;
+ }
+ },
+ new Opaque(new BID(Ids::STONE, Meta::STONE_ANDESITE), "Andesite", $stoneBreakInfo),
+ new Opaque(new BID(Ids::STONE, Meta::STONE_DIORITE), "Diorite", $stoneBreakInfo),
+ new Opaque(new BID(Ids::STONE, Meta::STONE_GRANITE), "Granite", $stoneBreakInfo),
+ new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_ANDESITE), "Polished Andesite", $stoneBreakInfo),
+ new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_DIORITE), "Polished Diorite", $stoneBreakInfo),
+ new Opaque(new BID(Ids::STONE, Meta::STONE_POLISHED_GRANITE), "Polished Granite", $stoneBreakInfo)
+ );
+ $this->registerAllMeta(
+ $stoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_NORMAL), "Stone Bricks", $stoneBreakInfo),
+ $mossyStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_MOSSY), "Mossy Stone Bricks", $stoneBreakInfo),
+ $crackedStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_CRACKED), "Cracked Stone Bricks", $stoneBreakInfo),
+ $chiseledStoneBrick = new Opaque(new BID(Ids::STONEBRICK, Meta::STONE_BRICK_CHISELED), "Chiseled Stone Bricks", $stoneBreakInfo)
+ );
+ $infestedStoneBreakInfo = new BlockBreakInfo(0.75, BlockToolType::PICKAXE);
+ $this->registerAllMeta(
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE), "Infested Stone", $infestedStoneBreakInfo, $stone),
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK), "Infested Stone Brick", $infestedStoneBreakInfo, $stoneBrick),
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_COBBLESTONE), "Infested Cobblestone", $infestedStoneBreakInfo, $cobblestone),
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_MOSSY), "Infested Mossy Stone Brick", $infestedStoneBreakInfo, $mossyStoneBrick),
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_CRACKED), "Infested Cracked Stone Brick", $infestedStoneBreakInfo, $crackedStoneBrick),
+ new InfestedStone(new BID(Ids::MONSTER_EGG, Meta::INFESTED_STONE_BRICK_CHISELED), "Infested Chiseled Stone Brick", $infestedStoneBreakInfo, $chiseledStoneBrick)
+ );
+ $this->registerAllMeta(new Stair(new BID(Ids::NORMAL_STONE_STAIRS, 0), "Stone Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Opaque(new BID(Ids::SMOOTH_STONE, 0), "Smooth Stone", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::ANDESITE_STAIRS, 0), "Andesite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::DIORITE_STAIRS, 0), "Diorite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::GRANITE_STAIRS, 0), "Granite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::POLISHED_ANDESITE_STAIRS, 0), "Polished Andesite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::POLISHED_DIORITE_STAIRS, 0), "Polished Diorite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::POLISHED_GRANITE_STAIRS, 0), "Polished Granite Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::STONE_BRICK_STAIRS, 0), "Stone Brick Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::MOSSY_STONE_BRICK_STAIRS, 0), "Mossy Stone Brick Stairs", $stoneBreakInfo));
+ $this->registerAllMeta(new StoneButton(new BID(Ids::STONE_BUTTON, 0), "Stone Button", new BlockBreakInfo(0.5, BlockToolType::PICKAXE)));
+ $this->registerAllMeta(new StonePressurePlate(new BID(Ids::STONE_PRESSURE_PLATE, 0), "Stone Pressure Plate", new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
//TODO: in the future this won't be the same for all the types
$stoneSlabBreakInfo = new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_BRICK), "Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_COBBLESTONE), "Cobblestone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_FAKE_WOODEN), "Fake Wooden", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_NETHER_BRICK), "Nether Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_QUARTZ), "Quartz", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_SANDSTONE), "Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_SMOOTH_STONE), "Smooth Stone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(1, Meta::STONE_SLAB_STONE_BRICK), "Stone Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_DARK_PRISMARINE), "Dark Prismarine", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_MOSSY_COBBLESTONE), "Mossy Cobblestone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_PRISMARINE), "Prismarine", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_PRISMARINE_BRICKS), "Prismarine Bricks", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_PURPUR), "Purpur", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_RED_NETHER_BRICK), "Red Nether Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_RED_SANDSTONE), "Red Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(2, Meta::STONE_SLAB2_SMOOTH_SANDSTONE), "Smooth Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_ANDESITE), "Andesite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_DIORITE), "Diorite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_END_STONE_BRICK), "End Stone Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_GRANITE), "Granite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_POLISHED_ANDESITE), "Polished Andesite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_POLISHED_DIORITE), "Polished Diorite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_POLISHED_GRANITE), "Polished Granite", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(3, Meta::STONE_SLAB3_SMOOTH_RED_SANDSTONE), "Smooth Red Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(4, Meta::STONE_SLAB4_CUT_RED_SANDSTONE), "Cut Red Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(4, Meta::STONE_SLAB4_CUT_SANDSTONE), "Cut Sandstone", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(4, Meta::STONE_SLAB4_MOSSY_STONE_BRICK), "Mossy Stone Brick", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(4, Meta::STONE_SLAB4_SMOOTH_QUARTZ), "Smooth Quartz", $stoneSlabBreakInfo));
- $this->registerSlabWithDoubleHighBitsRemapping(new Slab(BlockLegacyIdHelper::getStoneSlabIdentifier(4, Meta::STONE_SLAB4_STONE), "Stone", $stoneSlabBreakInfo));
- $this->register(new Opaque(new BID(Ids::STONECUTTER, 0), "Stonecutter", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new Sugarcane(new BID(Ids::REEDS_BLOCK, 0, ItemIds::REEDS), "Sugarcane", BlockBreakInfo::instant()));
- $this->register(new TNT(new BID(Ids::TNT, 0), "TNT", BlockBreakInfo::instant()));
- $fern = new TallGrass(new BID(Ids::TALLGRASS, Meta::TALLGRASS_FERN), "Fern", BlockBreakInfo::instant(BlockToolType::SHEARS, 1));
- $this->register($fern);
- $this->remap(Ids::TALLGRASS, 0, $fern);
- $this->remap(Ids::TALLGRASS, 3, $fern);
- $this->register(new TallGrass(new BID(Ids::TALLGRASS, Meta::TALLGRASS_NORMAL), "Tall Grass", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)));
- $this->register(new Torch(new BID(Ids::COLORED_TORCH_BP, 0), "Blue Torch", BlockBreakInfo::instant()));
- $this->register(new Torch(new BID(Ids::COLORED_TORCH_BP, 8), "Purple Torch", BlockBreakInfo::instant()));
- $this->register(new Torch(new BID(Ids::COLORED_TORCH_RG, 0), "Red Torch", BlockBreakInfo::instant()));
- $this->register(new Torch(new BID(Ids::COLORED_TORCH_RG, 8), "Green Torch", BlockBreakInfo::instant()));
- $this->register(new Torch(new BID(Ids::TORCH, 0), "Torch", BlockBreakInfo::instant()));
- $this->register(new TrappedChest(new BID(Ids::TRAPPED_CHEST, 0, null, TileChest::class), "Trapped Chest", $chestBreakInfo));
- $this->register(new Tripwire(new BID(Ids::TRIPWIRE, 0, ItemIds::STRING), "Tripwire", BlockBreakInfo::instant()));
- $this->register(new TripwireHook(new BID(Ids::TRIPWIRE_HOOK, 0), "Tripwire Hook", BlockBreakInfo::instant()));
- $this->register(new UnderwaterTorch(new BID(Ids::UNDERWATER_TORCH, 0), "Underwater Torch", BlockBreakInfo::instant()));
- $this->register(new Vine(new BID(Ids::VINE, 0), "Vines", new BlockBreakInfo(0.2, BlockToolType::AXE)));
- $this->register(new Water(new BIDFlattened(Ids::FLOWING_WATER, [Ids::STILL_WATER], 0), "Water", BlockBreakInfo::indestructible(500.0)));
- $this->register(new WaterLily(new BID(Ids::LILY_PAD, 0), "Lily Pad", new BlockBreakInfo(0.6)));
+ $getStoneSlabId = static fn(int $stoneSlabId, int $meta) => BlockLegacyIdHelper::getStoneSlabIdentifier($stoneSlabId, $meta);
+ foreach([
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_BRICK), "Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_COBBLESTONE), "Cobblestone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_FAKE_WOODEN), "Fake Wooden", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_NETHER_BRICK), "Nether Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_QUARTZ), "Quartz", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_SANDSTONE), "Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_SMOOTH_STONE), "Smooth Stone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(1, Meta::STONE_SLAB_STONE_BRICK), "Stone Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_DARK_PRISMARINE), "Dark Prismarine", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_MOSSY_COBBLESTONE), "Mossy Cobblestone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_PRISMARINE), "Prismarine", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_PRISMARINE_BRICKS), "Prismarine Bricks", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_PURPUR), "Purpur", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_RED_NETHER_BRICK), "Red Nether Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_RED_SANDSTONE), "Red Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(2, Meta::STONE_SLAB2_SMOOTH_SANDSTONE), "Smooth Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_ANDESITE), "Andesite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_DIORITE), "Diorite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_END_STONE_BRICK), "End Stone Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_GRANITE), "Granite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_POLISHED_ANDESITE), "Polished Andesite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_POLISHED_DIORITE), "Polished Diorite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_POLISHED_GRANITE), "Polished Granite", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(3, Meta::STONE_SLAB3_SMOOTH_RED_SANDSTONE), "Smooth Red Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(4, Meta::STONE_SLAB4_CUT_RED_SANDSTONE), "Cut Red Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(4, Meta::STONE_SLAB4_CUT_SANDSTONE), "Cut Sandstone", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(4, Meta::STONE_SLAB4_MOSSY_STONE_BRICK), "Mossy Stone Brick", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(4, Meta::STONE_SLAB4_SMOOTH_QUARTZ), "Smooth Quartz", $stoneSlabBreakInfo),
+ new Slab($getStoneSlabId(4, Meta::STONE_SLAB4_STONE), "Stone", $stoneSlabBreakInfo),
+ ] as $slabType){
+ $this->registerSlabWithDoubleHighBitsRemapping($slabType);
+ }
+
+ $this->registerAllMeta(new Opaque(new BID(Ids::STONECUTTER, 0), "Stonecutter", new BlockBreakInfo(3.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new Sugarcane(new BID(Ids::REEDS_BLOCK, 0, ItemIds::REEDS), "Sugarcane", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new SweetBerryBush(new BID(Ids::SWEET_BERRY_BUSH, 0, ItemIds::SWEET_BERRIES), "Sweet Berry Bush", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new TNT(new BID(Ids::TNT, 0), "TNT", BlockBreakInfo::instant()));
+ $this->registerAllMeta(
+ new TallGrass(new BID(Ids::TALLGRASS, Meta::TALLGRASS_FERN), "Fern", BlockBreakInfo::instant(BlockToolType::SHEARS, 1)),
+ new TallGrass(new BID(Ids::TALLGRASS, Meta::TALLGRASS_NORMAL), "Tall Grass", BlockBreakInfo::instant(BlockToolType::SHEARS, 1))
+ );
+ $this->registerAllMeta(
+ new Torch(new BID(Ids::COLORED_TORCH_BP, 0), "Blue Torch", BlockBreakInfo::instant()),
+ new Torch(new BID(Ids::COLORED_TORCH_BP, 8), "Purple Torch", BlockBreakInfo::instant())
+ );
+ $this->registerAllMeta(
+ new Torch(new BID(Ids::COLORED_TORCH_RG, 0), "Red Torch", BlockBreakInfo::instant()),
+ new Torch(new BID(Ids::COLORED_TORCH_RG, 8), "Green Torch", BlockBreakInfo::instant())
+ );
+ $this->registerAllMeta(new Torch(new BID(Ids::TORCH, 0), "Torch", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new TrappedChest(new BID(Ids::TRAPPED_CHEST, 0, null, TileChest::class), "Trapped Chest", $chestBreakInfo));
+ $this->registerAllMeta(new Tripwire(new BID(Ids::TRIPWIRE, 0, ItemIds::STRING), "Tripwire", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new TripwireHook(new BID(Ids::TRIPWIRE_HOOK, 0), "Tripwire Hook", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new UnderwaterTorch(new BID(Ids::UNDERWATER_TORCH, 0), "Underwater Torch", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new Vine(new BID(Ids::VINE, 0), "Vines", new BlockBreakInfo(0.2, BlockToolType::AXE)));
+ $this->registerAllMeta(new Water(new BIDFlattened(Ids::FLOWING_WATER, [Ids::STILL_WATER], 0), "Water", BlockBreakInfo::indestructible(500.0)));
+ $this->registerAllMeta(new WaterLily(new BID(Ids::LILY_PAD, 0), "Lily Pad", BlockBreakInfo::instant()));
$weightedPressurePlateBreakInfo = new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
- $this->register(new WeightedPressurePlateHeavy(new BID(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE, 0), "Weighted Pressure Plate Heavy", $weightedPressurePlateBreakInfo));
- $this->register(new WeightedPressurePlateLight(new BID(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE, 0), "Weighted Pressure Plate Light", $weightedPressurePlateBreakInfo));
- $this->register(new Wheat(new BID(Ids::WHEAT_BLOCK, 0), "Wheat Block", BlockBreakInfo::instant()));
+ $this->registerAllMeta(new WeightedPressurePlateHeavy(new BID(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE, 0), "Weighted Pressure Plate Heavy", $weightedPressurePlateBreakInfo));
+ $this->registerAllMeta(new WeightedPressurePlateLight(new BID(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE, 0), "Weighted Pressure Plate Light", $weightedPressurePlateBreakInfo));
+ $this->registerAllMeta(new Wheat(new BID(Ids::WHEAT_BLOCK, 0), "Wheat Block", BlockBreakInfo::instant()));
$planksBreakInfo = new BlockBreakInfo(2.0, BlockToolType::AXE, 0, 15.0);
- $leavesBreakInfo = new BlockBreakInfo(0.2, BlockToolType::SHEARS);
+ $leavesBreakInfo = new class(0.2, BlockToolType::HOE) extends BlockBreakInfo{
+ public function getBreakTime(Item $item) : float{
+ if($item->getBlockToolType() === BlockToolType::SHEARS){
+ return 0.0;
+ }
+ return parent::getBreakTime($item);
+ }
+ };
$signBreakInfo = new BlockBreakInfo(1.0, BlockToolType::AXE);
$logBreakInfo = new BlockBreakInfo(2.0, BlockToolType::AXE);
$woodenDoorBreakInfo = new BlockBreakInfo(3.0, BlockToolType::AXE, 0, 15.0);
$woodenButtonBreakInfo = new BlockBreakInfo(0.5, BlockToolType::AXE);
$woodenPressurePlateBreakInfo = new BlockBreakInfo(0.5, BlockToolType::AXE);
+
+ $planks = [];
+ $saplings = [];
+ $fences = [];
+ $leaves = [];
+ $allSidedLogs = [];
foreach(TreeType::getAll() as $treeType){
$magicNumber = $treeType->getMagicNumber();
$name = $treeType->getDisplayName();
- $this->register(new Planks(new BID(Ids::PLANKS, $magicNumber), $name . " Planks", $planksBreakInfo));
- $this->register(new Sapling(new BID(Ids::SAPLING, $magicNumber), $name . " Sapling", BlockBreakInfo::instant(), $treeType));
- $this->register(new WoodenFence(new BID(Ids::FENCE, $magicNumber), $name . " Fence", $planksBreakInfo));
+ $planks[] = new Planks(new BID(Ids::PLANKS, $magicNumber), $name . " Planks", $planksBreakInfo);
+ $saplings[] = new Sapling(new BID(Ids::SAPLING, $magicNumber), $name . " Sapling", BlockBreakInfo::instant(), $treeType);
+ $fences[] = new WoodenFence(new BID(Ids::FENCE, $magicNumber), $name . " Fence", $planksBreakInfo);
$this->registerSlabWithDoubleHighBitsRemapping(new WoodenSlab(new BIDFlattened(Ids::WOODEN_SLAB, [Ids::DOUBLE_WOODEN_SLAB], $magicNumber), $name, $planksBreakInfo));
//TODO: find a better way to deal with this split
- $this->register(new Leaves(new BID($magicNumber >= 4 ? Ids::LEAVES2 : Ids::LEAVES, $magicNumber & 0x03), $name . " Leaves", $leavesBreakInfo, $treeType));
+ $leaves[] = new Leaves(new BID($magicNumber >= 4 ? Ids::LEAVES2 : Ids::LEAVES, $magicNumber & 0x03), $name . " Leaves", $leavesBreakInfo, $treeType);
+
$this->register(new Log(new BID($magicNumber >= 4 ? Ids::LOG2 : Ids::LOG, $magicNumber & 0x03), $name . " Log", $logBreakInfo, $treeType, false));
-
$wood = new Wood(new BID(Ids::WOOD, $magicNumber), $name . " Wood", $logBreakInfo, $treeType, false);
- $this->register($wood);
$this->remap($magicNumber >= 4 ? Ids::LOG2 : Ids::LOG, ($magicNumber & 0x03) | 0b1100, $wood);
- $this->register(new Wood(new BID(Ids::WOOD, $magicNumber | BlockLegacyMetadata::WOOD_FLAG_STRIPPED), "Stripped $name Wood", $logBreakInfo, $treeType, true));
- $this->register(new Log(BlockLegacyIdHelper::getStrippedLogIdentifier($treeType), "Stripped " . $name . " Log", $logBreakInfo, $treeType, true));
- $this->register(new FenceGate(BlockLegacyIdHelper::getWoodenFenceIdentifier($treeType), $name . " Fence Gate", $planksBreakInfo));
- $this->register(new WoodenStairs(BlockLegacyIdHelper::getWoodenStairsIdentifier($treeType), $name . " Stairs", $planksBreakInfo));
- $this->register(new WoodenDoor(BlockLegacyIdHelper::getWoodenDoorIdentifier($treeType), $name . " Door", $woodenDoorBreakInfo));
+ $allSidedLogs[] = $wood;
+ $allSidedLogs[] = new Wood(new BID(Ids::WOOD, $magicNumber | BlockLegacyMetadata::WOOD_FLAG_STRIPPED), "Stripped $name Wood", $logBreakInfo, $treeType, true);
- $this->register(new WoodenButton(BlockLegacyIdHelper::getWoodenButtonIdentifier($treeType), $name . " Button", $woodenButtonBreakInfo));
- $this->register(new WoodenPressurePlate(BlockLegacyIdHelper::getWoodenPressurePlateIdentifier($treeType), $name . " Pressure Plate", $woodenPressurePlateBreakInfo));
- $this->register(new WoodenTrapdoor(BlockLegacyIdHelper::getWoodenTrapdoorIdentifier($treeType), $name . " Trapdoor", $woodenDoorBreakInfo));
+ $this->registerAllMeta(new Log(BlockLegacyIdHelper::getStrippedLogIdentifier($treeType), "Stripped " . $name . " Log", $logBreakInfo, $treeType, true));
+ $this->registerAllMeta(new FenceGate(BlockLegacyIdHelper::getWoodenFenceIdentifier($treeType), $name . " Fence Gate", $planksBreakInfo));
+ $this->registerAllMeta(new WoodenStairs(BlockLegacyIdHelper::getWoodenStairsIdentifier($treeType), $name . " Stairs", $planksBreakInfo));
+ $this->registerAllMeta(new WoodenDoor(BlockLegacyIdHelper::getWoodenDoorIdentifier($treeType), $name . " Door", $woodenDoorBreakInfo));
- $this->register(new FloorSign(BlockLegacyIdHelper::getWoodenFloorSignIdentifier($treeType), $name . " Sign", $signBreakInfo));
- $this->register(new WallSign(BlockLegacyIdHelper::getWoodenWallSignIdentifier($treeType), $name . " Wall Sign", $signBreakInfo));
+ $this->registerAllMeta(new WoodenButton(BlockLegacyIdHelper::getWoodenButtonIdentifier($treeType), $name . " Button", $woodenButtonBreakInfo));
+ $this->registerAllMeta(new WoodenPressurePlate(BlockLegacyIdHelper::getWoodenPressurePlateIdentifier($treeType), $name . " Pressure Plate", $woodenPressurePlateBreakInfo));
+ $this->registerAllMeta(new WoodenTrapdoor(BlockLegacyIdHelper::getWoodenTrapdoorIdentifier($treeType), $name . " Trapdoor", $woodenDoorBreakInfo));
+
+ $this->registerAllMeta(new FloorSign(BlockLegacyIdHelper::getWoodenFloorSignIdentifier($treeType), $name . " Sign", $signBreakInfo));
+ $this->registerAllMeta(new WallSign(BlockLegacyIdHelper::getWoodenWallSignIdentifier($treeType), $name . " Wall Sign", $signBreakInfo));
}
+ $this->registerAllMeta(...$planks);
+ $this->registerAllMeta(...$saplings);
+ $this->registerAllMeta(...$fences);
+ $this->registerAllMeta(...$leaves);
+ $this->registerAllMeta(...$allSidedLogs);
static $sandstoneTypes = [
Meta::SANDSTONE_NORMAL => "",
@@ -471,32 +527,36 @@ class BlockFactory{
Meta::SANDSTONE_SMOOTH => "Smooth "
];
$sandstoneBreakInfo = new BlockBreakInfo(0.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
- $this->register(new Stair(new BID(Ids::RED_SANDSTONE_STAIRS, 0), "Red Sandstone Stairs", $sandstoneBreakInfo));
- $this->register(new Stair(new BID(Ids::SMOOTH_RED_SANDSTONE_STAIRS, 0), "Smooth Red Sandstone Stairs", $sandstoneBreakInfo));
- $this->register(new Stair(new BID(Ids::SANDSTONE_STAIRS, 0), "Sandstone Stairs", $sandstoneBreakInfo));
- $this->register(new Stair(new BID(Ids::SMOOTH_SANDSTONE_STAIRS, 0), "Smooth Sandstone Stairs", $sandstoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::RED_SANDSTONE_STAIRS, 0), "Red Sandstone Stairs", $sandstoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::SMOOTH_RED_SANDSTONE_STAIRS, 0), "Smooth Red Sandstone Stairs", $sandstoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::SANDSTONE_STAIRS, 0), "Sandstone Stairs", $sandstoneBreakInfo));
+ $this->registerAllMeta(new Stair(new BID(Ids::SMOOTH_SANDSTONE_STAIRS, 0), "Smooth Sandstone Stairs", $sandstoneBreakInfo));
+ $sandstones = [];
+ $redSandstones = [];
foreach($sandstoneTypes as $variant => $prefix){
- $this->register(new Opaque(new BID(Ids::SANDSTONE, $variant), $prefix . "Sandstone", $sandstoneBreakInfo));
- $this->register(new Opaque(new BID(Ids::RED_SANDSTONE, $variant), $prefix . "Red Sandstone", $sandstoneBreakInfo));
+ $sandstones[] = new Opaque(new BID(Ids::SANDSTONE, $variant), $prefix . "Sandstone", $sandstoneBreakInfo);
+ $redSandstones[] = new Opaque(new BID(Ids::RED_SANDSTONE, $variant), $prefix . "Red Sandstone", $sandstoneBreakInfo);
}
+ $this->registerAllMeta(...$sandstones);
+ $this->registerAllMeta(...$redSandstones);
$glazedTerracottaBreakInfo = new BlockBreakInfo(1.4, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
foreach(DyeColor::getAll() as $color){
$coloredName = function(string $name) use($color) : string{
return $color->getDisplayName() . " " . $name;
};
- $this->register(new GlazedTerracotta(BlockLegacyIdHelper::getGlazedTerracottaIdentifier($color), $coloredName("Glazed Terracotta"), $glazedTerracottaBreakInfo));
+ $this->registerAllMeta(new GlazedTerracotta(BlockLegacyIdHelper::getGlazedTerracottaIdentifier($color), $coloredName("Glazed Terracotta"), $glazedTerracottaBreakInfo));
}
- $this->register(new DyedShulkerBox(new BID(Ids::SHULKER_BOX, 0, null, TileShulkerBox::class), "Dyed Shulker Box", $shulkerBoxBreakInfo));
- $this->register(new StainedGlass(new BID(Ids::STAINED_GLASS, 0), "Stained Glass", $glassBreakInfo));
- $this->register(new StainedGlassPane(new BID(Ids::STAINED_GLASS_PANE, 0), "Stained Glass Pane", $glassBreakInfo));
- $this->register(new StainedHardenedClay(new BID(Ids::STAINED_CLAY, 0), "Stained Clay", $hardenedClayBreakInfo));
- $this->register(new StainedHardenedGlass(new BID(Ids::HARD_STAINED_GLASS, 0), "Stained Hardened Glass", $hardenedGlassBreakInfo));
- $this->register(new StainedHardenedGlassPane(new BID(Ids::HARD_STAINED_GLASS_PANE, 0), "Stained Hardened Glass Pane", $hardenedGlassBreakInfo));
- $this->register(new Carpet(new BID(Ids::CARPET, 0), "Carpet", new BlockBreakInfo(0.1)));
- $this->register(new Concrete(new BID(Ids::CONCRETE, 0), "Concrete", new BlockBreakInfo(1.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
- $this->register(new ConcretePowder(new BID(Ids::CONCRETE_POWDER, 0), "Concrete Powder", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
- $this->register(new Wool(new BID(Ids::WOOL, 0), "Wool", new class(0.8, BlockToolType::SHEARS) extends BlockBreakInfo{
+ $this->registerAllMeta(new DyedShulkerBox(new BID(Ids::SHULKER_BOX, 0, null, TileShulkerBox::class), "Dyed Shulker Box", $shulkerBoxBreakInfo));
+ $this->registerAllMeta(new StainedGlass(new BID(Ids::STAINED_GLASS, 0), "Stained Glass", $glassBreakInfo));
+ $this->registerAllMeta(new StainedGlassPane(new BID(Ids::STAINED_GLASS_PANE, 0), "Stained Glass Pane", $glassBreakInfo));
+ $this->registerAllMeta(new StainedHardenedClay(new BID(Ids::STAINED_CLAY, 0), "Stained Clay", $hardenedClayBreakInfo));
+ $this->registerAllMeta(new StainedHardenedGlass(new BID(Ids::HARD_STAINED_GLASS, 0), "Stained Hardened Glass", $hardenedGlassBreakInfo));
+ $this->registerAllMeta(new StainedHardenedGlassPane(new BID(Ids::HARD_STAINED_GLASS_PANE, 0), "Stained Hardened Glass Pane", $hardenedGlassBreakInfo));
+ $this->registerAllMeta(new Carpet(new BID(Ids::CARPET, 0), "Carpet", new BlockBreakInfo(0.1)));
+ $this->registerAllMeta(new Concrete(new BID(Ids::CONCRETE, 0), "Concrete", new BlockBreakInfo(1.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel())));
+ $this->registerAllMeta(new ConcretePowder(new BID(Ids::CONCRETE_POWDER, 0), "Concrete Powder", new BlockBreakInfo(0.5, BlockToolType::SHOVEL)));
+ $this->registerAllMeta(new Wool(new BID(Ids::WOOL, 0), "Wool", new class(0.8, BlockToolType::SHEARS) extends BlockBreakInfo{
public function getBreakTime(Item $item) : float{
$time = parent::getBreakTime($item);
if($item->getBlockToolType() === BlockToolType::SHEARS){
@@ -509,44 +569,48 @@ class BlockFactory{
//TODO: in the future these won't all have the same hardness; they only do now because of the old metadata crap
$wallBreakInfo = new BlockBreakInfo(2.0, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel(), 30.0);
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_ANDESITE), "Andesite Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_BRICK), "Brick Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_DIORITE), "Diorite Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_END_STONE_BRICK), "End Stone Brick Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_GRANITE), "Granite Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_MOSSY_STONE_BRICK), "Mossy Stone Brick Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_MOSSY_COBBLESTONE), "Mossy Cobblestone Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_NETHER_BRICK), "Nether Brick Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_COBBLESTONE), "Cobblestone Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_PRISMARINE), "Prismarine Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_RED_NETHER_BRICK), "Red Nether Brick Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_RED_SANDSTONE), "Red Sandstone Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_SANDSTONE), "Sandstone Wall", $wallBreakInfo));
- $this->register(new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_STONE_BRICK), "Stone Brick Wall", $wallBreakInfo));
+ $this->registerAllMeta(
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_COBBLESTONE), "Cobblestone Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_ANDESITE), "Andesite Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_BRICK), "Brick Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_DIORITE), "Diorite Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_END_STONE_BRICK), "End Stone Brick Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_GRANITE), "Granite Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_MOSSY_STONE_BRICK), "Mossy Stone Brick Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_MOSSY_COBBLESTONE), "Mossy Cobblestone Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_NETHER_BRICK), "Nether Brick Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_PRISMARINE), "Prismarine Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_RED_NETHER_BRICK), "Red Nether Brick Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_RED_SANDSTONE), "Red Sandstone Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_SANDSTONE), "Sandstone Wall", $wallBreakInfo),
+ new Wall(new BID(Ids::COBBLESTONE_WALL, Meta::WALL_STONE_BRICK), "Stone Brick Wall", $wallBreakInfo),
+ );
$this->registerElements();
$chemistryTableBreakInfo = new BlockBreakInfo(2.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
- $this->register(new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_COMPOUND_CREATOR), "Compound Creator", $chemistryTableBreakInfo));
- $this->register(new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_ELEMENT_CONSTRUCTOR), "Element Constructor", $chemistryTableBreakInfo));
- $this->register(new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_LAB_TABLE), "Lab Table", $chemistryTableBreakInfo));
- $this->register(new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_MATERIAL_REDUCER), "Material Reducer", $chemistryTableBreakInfo));
+ $this->registerAllMeta(
+ new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_COMPOUND_CREATOR), "Compound Creator", $chemistryTableBreakInfo),
+ new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_ELEMENT_CONSTRUCTOR), "Element Constructor", $chemistryTableBreakInfo),
+ new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_LAB_TABLE), "Lab Table", $chemistryTableBreakInfo),
+ new ChemistryTable(new BID(Ids::CHEMISTRY_TABLE, Meta::CHEMISTRY_MATERIAL_REDUCER), "Material Reducer", $chemistryTableBreakInfo)
+ );
- $this->register(new ChemicalHeat(new BID(Ids::CHEMICAL_HEAT, 0), "Heat Block", $chemistryTableBreakInfo));
+ $this->registerAllMeta(new ChemicalHeat(new BID(Ids::CHEMICAL_HEAT, 0), "Heat Block", $chemistryTableBreakInfo));
$this->registerMushroomBlocks();
- $this->register(new Coral(
+ $this->registerAllMeta(new Coral(
new BID(Ids::CORAL, 0),
"Coral",
BlockBreakInfo::instant(),
));
- $this->register(new FloorCoralFan(
+ $this->registerAllMeta(new FloorCoralFan(
new BlockIdentifierFlattened(Ids::CORAL_FAN, [Ids::CORAL_FAN_DEAD], 0, ItemIds::CORAL_FAN),
"Coral Fan",
BlockBreakInfo::instant(),
));
- $this->register(new WallCoralFan(
+ $this->registerAllMeta(new WallCoralFan(
new BlockIdentifierFlattened(Ids::CORAL_FAN_HANG, [Ids::CORAL_FAN_HANG2, Ids::CORAL_FAN_HANG3], 0, ItemIds::CORAL_FAN),
"Wall Coral Fan",
BlockBreakInfo::instant(),
@@ -567,12 +631,10 @@ class BlockFactory{
//TODO: minecraft:dropper
//TODO: minecraft:end_gateway
//TODO: minecraft:end_portal
- //TODO: minecraft:fletching_table
//TODO: minecraft:grindstone
//TODO: minecraft:jigsaw
//TODO: minecraft:kelp
//TODO: minecraft:lava_cauldron
- //TODO: minecraft:lectern
//TODO: minecraft:movingBlock
//TODO: minecraft:observer
//TODO: minecraft:piston
@@ -584,7 +646,6 @@ class BlockFactory{
//TODO: minecraft:sticky_piston
//TODO: minecraft:stonecutter_block
//TODO: minecraft:structure_block
- $this->register(new SweetBerryBush(new BID(Ids::SWEET_BERRY_BUSH, 0, ItemIds::SWEET_BERRIES), "Sweet Berry Bush", BlockBreakInfo::instant()));
//TODO: minecraft:turtle_egg
//endregion
@@ -740,126 +801,155 @@ class BlockFactory{
private function registerElements() : void{
$instaBreak = BlockBreakInfo::instant();
- $this->register(new Opaque(new BID(Ids::ELEMENT_0, 0), "???", $instaBreak));
+ $this->registerAllMeta(new Opaque(new BID(Ids::ELEMENT_0, 0), "???", $instaBreak));
- $this->register(new Element(new BID(Ids::ELEMENT_1, 0), "Hydrogen", $instaBreak, "h", 1, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_2, 0), "Helium", $instaBreak, "he", 2, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_3, 0), "Lithium", $instaBreak, "li", 3, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_4, 0), "Beryllium", $instaBreak, "be", 4, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_5, 0), "Boron", $instaBreak, "b", 5, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_6, 0), "Carbon", $instaBreak, "c", 6, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_7, 0), "Nitrogen", $instaBreak, "n", 7, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_8, 0), "Oxygen", $instaBreak, "o", 8, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_9, 0), "Fluorine", $instaBreak, "f", 9, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_10, 0), "Neon", $instaBreak, "ne", 10, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_11, 0), "Sodium", $instaBreak, "na", 11, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_12, 0), "Magnesium", $instaBreak, "mg", 12, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_13, 0), "Aluminum", $instaBreak, "al", 13, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_14, 0), "Silicon", $instaBreak, "si", 14, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_15, 0), "Phosphorus", $instaBreak, "p", 15, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_16, 0), "Sulfur", $instaBreak, "s", 16, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_17, 0), "Chlorine", $instaBreak, "cl", 17, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_18, 0), "Argon", $instaBreak, "ar", 18, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_19, 0), "Potassium", $instaBreak, "k", 19, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_20, 0), "Calcium", $instaBreak, "ca", 20, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_21, 0), "Scandium", $instaBreak, "sc", 21, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_22, 0), "Titanium", $instaBreak, "ti", 22, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_23, 0), "Vanadium", $instaBreak, "v", 23, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_24, 0), "Chromium", $instaBreak, "cr", 24, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_25, 0), "Manganese", $instaBreak, "mn", 25, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_26, 0), "Iron", $instaBreak, "fe", 26, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_27, 0), "Cobalt", $instaBreak, "co", 27, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_28, 0), "Nickel", $instaBreak, "ni", 28, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_29, 0), "Copper", $instaBreak, "cu", 29, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_30, 0), "Zinc", $instaBreak, "zn", 30, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_31, 0), "Gallium", $instaBreak, "ga", 31, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_32, 0), "Germanium", $instaBreak, "ge", 32, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_33, 0), "Arsenic", $instaBreak, "as", 33, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_34, 0), "Selenium", $instaBreak, "se", 34, 5));
- $this->register(new Element(new BID(Ids::ELEMENT_35, 0), "Bromine", $instaBreak, "br", 35, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_36, 0), "Krypton", $instaBreak, "kr", 36, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_37, 0), "Rubidium", $instaBreak, "rb", 37, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_38, 0), "Strontium", $instaBreak, "sr", 38, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_39, 0), "Yttrium", $instaBreak, "y", 39, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_40, 0), "Zirconium", $instaBreak, "zr", 40, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_41, 0), "Niobium", $instaBreak, "nb", 41, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_42, 0), "Molybdenum", $instaBreak, "mo", 42, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_43, 0), "Technetium", $instaBreak, "tc", 43, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_44, 0), "Ruthenium", $instaBreak, "ru", 44, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_45, 0), "Rhodium", $instaBreak, "rh", 45, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_46, 0), "Palladium", $instaBreak, "pd", 46, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_47, 0), "Silver", $instaBreak, "ag", 47, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_48, 0), "Cadmium", $instaBreak, "cd", 48, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_49, 0), "Indium", $instaBreak, "in", 49, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_50, 0), "Tin", $instaBreak, "sn", 50, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_51, 0), "Antimony", $instaBreak, "sb", 51, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_52, 0), "Tellurium", $instaBreak, "te", 52, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_53, 0), "Iodine", $instaBreak, "i", 53, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_54, 0), "Xenon", $instaBreak, "xe", 54, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_55, 0), "Cesium", $instaBreak, "cs", 55, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_56, 0), "Barium", $instaBreak, "ba", 56, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_57, 0), "Lanthanum", $instaBreak, "la", 57, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_58, 0), "Cerium", $instaBreak, "ce", 58, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_59, 0), "Praseodymium", $instaBreak, "pr", 59, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_60, 0), "Neodymium", $instaBreak, "nd", 60, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_61, 0), "Promethium", $instaBreak, "pm", 61, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_62, 0), "Samarium", $instaBreak, "sm", 62, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_63, 0), "Europium", $instaBreak, "eu", 63, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_64, 0), "Gadolinium", $instaBreak, "gd", 64, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_65, 0), "Terbium", $instaBreak, "tb", 65, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_66, 0), "Dysprosium", $instaBreak, "dy", 66, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_67, 0), "Holmium", $instaBreak, "ho", 67, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_68, 0), "Erbium", $instaBreak, "er", 68, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_69, 0), "Thulium", $instaBreak, "tm", 69, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_70, 0), "Ytterbium", $instaBreak, "yb", 70, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_71, 0), "Lutetium", $instaBreak, "lu", 71, 8));
- $this->register(new Element(new BID(Ids::ELEMENT_72, 0), "Hafnium", $instaBreak, "hf", 72, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_73, 0), "Tantalum", $instaBreak, "ta", 73, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_74, 0), "Tungsten", $instaBreak, "w", 74, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_75, 0), "Rhenium", $instaBreak, "re", 75, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_76, 0), "Osmium", $instaBreak, "os", 76, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_77, 0), "Iridium", $instaBreak, "ir", 77, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_78, 0), "Platinum", $instaBreak, "pt", 78, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_79, 0), "Gold", $instaBreak, "au", 79, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_80, 0), "Mercury", $instaBreak, "hg", 80, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_81, 0), "Thallium", $instaBreak, "tl", 81, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_82, 0), "Lead", $instaBreak, "pb", 82, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_83, 0), "Bismuth", $instaBreak, "bi", 83, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_84, 0), "Polonium", $instaBreak, "po", 84, 4));
- $this->register(new Element(new BID(Ids::ELEMENT_85, 0), "Astatine", $instaBreak, "at", 85, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_86, 0), "Radon", $instaBreak, "rn", 86, 7));
- $this->register(new Element(new BID(Ids::ELEMENT_87, 0), "Francium", $instaBreak, "fr", 87, 0));
- $this->register(new Element(new BID(Ids::ELEMENT_88, 0), "Radium", $instaBreak, "ra", 88, 1));
- $this->register(new Element(new BID(Ids::ELEMENT_89, 0), "Actinium", $instaBreak, "ac", 89, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_90, 0), "Thorium", $instaBreak, "th", 90, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_91, 0), "Protactinium", $instaBreak, "pa", 91, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_92, 0), "Uranium", $instaBreak, "u", 92, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_93, 0), "Neptunium", $instaBreak, "np", 93, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_94, 0), "Plutonium", $instaBreak, "pu", 94, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_95, 0), "Americium", $instaBreak, "am", 95, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_96, 0), "Curium", $instaBreak, "cm", 96, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_97, 0), "Berkelium", $instaBreak, "bk", 97, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_98, 0), "Californium", $instaBreak, "cf", 98, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_99, 0), "Einsteinium", $instaBreak, "es", 99, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_100, 0), "Fermium", $instaBreak, "fm", 100, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_101, 0), "Mendelevium", $instaBreak, "md", 101, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_102, 0), "Nobelium", $instaBreak, "no", 102, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_103, 0), "Lawrencium", $instaBreak, "lr", 103, 9));
- $this->register(new Element(new BID(Ids::ELEMENT_104, 0), "Rutherfordium", $instaBreak, "rf", 104, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_105, 0), "Dubnium", $instaBreak, "db", 105, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_106, 0), "Seaborgium", $instaBreak, "sg", 106, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_107, 0), "Bohrium", $instaBreak, "bh", 107, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_108, 0), "Hassium", $instaBreak, "hs", 108, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_109, 0), "Meitnerium", $instaBreak, "mt", 109, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_110, 0), "Darmstadtium", $instaBreak, "ds", 110, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_111, 0), "Roentgenium", $instaBreak, "rg", 111, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_112, 0), "Copernicium", $instaBreak, "cn", 112, 2));
- $this->register(new Element(new BID(Ids::ELEMENT_113, 0), "Nihonium", $instaBreak, "nh", 113, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_114, 0), "Flerovium", $instaBreak, "fl", 114, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_115, 0), "Moscovium", $instaBreak, "mc", 115, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_116, 0), "Livermorium", $instaBreak, "lv", 116, 3));
- $this->register(new Element(new BID(Ids::ELEMENT_117, 0), "Tennessine", $instaBreak, "ts", 117, 6));
- $this->register(new Element(new BID(Ids::ELEMENT_118, 0), "Oganesson", $instaBreak, "og", 118, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_1, 0), "Hydrogen", $instaBreak, "h", 1, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_2, 0), "Helium", $instaBreak, "he", 2, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_3, 0), "Lithium", $instaBreak, "li", 3, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_4, 0), "Beryllium", $instaBreak, "be", 4, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_5, 0), "Boron", $instaBreak, "b", 5, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_6, 0), "Carbon", $instaBreak, "c", 6, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_7, 0), "Nitrogen", $instaBreak, "n", 7, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_8, 0), "Oxygen", $instaBreak, "o", 8, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_9, 0), "Fluorine", $instaBreak, "f", 9, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_10, 0), "Neon", $instaBreak, "ne", 10, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_11, 0), "Sodium", $instaBreak, "na", 11, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_12, 0), "Magnesium", $instaBreak, "mg", 12, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_13, 0), "Aluminum", $instaBreak, "al", 13, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_14, 0), "Silicon", $instaBreak, "si", 14, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_15, 0), "Phosphorus", $instaBreak, "p", 15, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_16, 0), "Sulfur", $instaBreak, "s", 16, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_17, 0), "Chlorine", $instaBreak, "cl", 17, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_18, 0), "Argon", $instaBreak, "ar", 18, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_19, 0), "Potassium", $instaBreak, "k", 19, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_20, 0), "Calcium", $instaBreak, "ca", 20, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_21, 0), "Scandium", $instaBreak, "sc", 21, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_22, 0), "Titanium", $instaBreak, "ti", 22, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_23, 0), "Vanadium", $instaBreak, "v", 23, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_24, 0), "Chromium", $instaBreak, "cr", 24, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_25, 0), "Manganese", $instaBreak, "mn", 25, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_26, 0), "Iron", $instaBreak, "fe", 26, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_27, 0), "Cobalt", $instaBreak, "co", 27, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_28, 0), "Nickel", $instaBreak, "ni", 28, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_29, 0), "Copper", $instaBreak, "cu", 29, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_30, 0), "Zinc", $instaBreak, "zn", 30, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_31, 0), "Gallium", $instaBreak, "ga", 31, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_32, 0), "Germanium", $instaBreak, "ge", 32, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_33, 0), "Arsenic", $instaBreak, "as", 33, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_34, 0), "Selenium", $instaBreak, "se", 34, 5));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_35, 0), "Bromine", $instaBreak, "br", 35, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_36, 0), "Krypton", $instaBreak, "kr", 36, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_37, 0), "Rubidium", $instaBreak, "rb", 37, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_38, 0), "Strontium", $instaBreak, "sr", 38, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_39, 0), "Yttrium", $instaBreak, "y", 39, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_40, 0), "Zirconium", $instaBreak, "zr", 40, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_41, 0), "Niobium", $instaBreak, "nb", 41, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_42, 0), "Molybdenum", $instaBreak, "mo", 42, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_43, 0), "Technetium", $instaBreak, "tc", 43, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_44, 0), "Ruthenium", $instaBreak, "ru", 44, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_45, 0), "Rhodium", $instaBreak, "rh", 45, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_46, 0), "Palladium", $instaBreak, "pd", 46, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_47, 0), "Silver", $instaBreak, "ag", 47, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_48, 0), "Cadmium", $instaBreak, "cd", 48, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_49, 0), "Indium", $instaBreak, "in", 49, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_50, 0), "Tin", $instaBreak, "sn", 50, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_51, 0), "Antimony", $instaBreak, "sb", 51, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_52, 0), "Tellurium", $instaBreak, "te", 52, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_53, 0), "Iodine", $instaBreak, "i", 53, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_54, 0), "Xenon", $instaBreak, "xe", 54, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_55, 0), "Cesium", $instaBreak, "cs", 55, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_56, 0), "Barium", $instaBreak, "ba", 56, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_57, 0), "Lanthanum", $instaBreak, "la", 57, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_58, 0), "Cerium", $instaBreak, "ce", 58, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_59, 0), "Praseodymium", $instaBreak, "pr", 59, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_60, 0), "Neodymium", $instaBreak, "nd", 60, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_61, 0), "Promethium", $instaBreak, "pm", 61, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_62, 0), "Samarium", $instaBreak, "sm", 62, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_63, 0), "Europium", $instaBreak, "eu", 63, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_64, 0), "Gadolinium", $instaBreak, "gd", 64, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_65, 0), "Terbium", $instaBreak, "tb", 65, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_66, 0), "Dysprosium", $instaBreak, "dy", 66, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_67, 0), "Holmium", $instaBreak, "ho", 67, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_68, 0), "Erbium", $instaBreak, "er", 68, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_69, 0), "Thulium", $instaBreak, "tm", 69, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_70, 0), "Ytterbium", $instaBreak, "yb", 70, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_71, 0), "Lutetium", $instaBreak, "lu", 71, 8));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_72, 0), "Hafnium", $instaBreak, "hf", 72, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_73, 0), "Tantalum", $instaBreak, "ta", 73, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_74, 0), "Tungsten", $instaBreak, "w", 74, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_75, 0), "Rhenium", $instaBreak, "re", 75, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_76, 0), "Osmium", $instaBreak, "os", 76, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_77, 0), "Iridium", $instaBreak, "ir", 77, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_78, 0), "Platinum", $instaBreak, "pt", 78, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_79, 0), "Gold", $instaBreak, "au", 79, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_80, 0), "Mercury", $instaBreak, "hg", 80, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_81, 0), "Thallium", $instaBreak, "tl", 81, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_82, 0), "Lead", $instaBreak, "pb", 82, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_83, 0), "Bismuth", $instaBreak, "bi", 83, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_84, 0), "Polonium", $instaBreak, "po", 84, 4));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_85, 0), "Astatine", $instaBreak, "at", 85, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_86, 0), "Radon", $instaBreak, "rn", 86, 7));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_87, 0), "Francium", $instaBreak, "fr", 87, 0));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_88, 0), "Radium", $instaBreak, "ra", 88, 1));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_89, 0), "Actinium", $instaBreak, "ac", 89, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_90, 0), "Thorium", $instaBreak, "th", 90, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_91, 0), "Protactinium", $instaBreak, "pa", 91, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_92, 0), "Uranium", $instaBreak, "u", 92, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_93, 0), "Neptunium", $instaBreak, "np", 93, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_94, 0), "Plutonium", $instaBreak, "pu", 94, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_95, 0), "Americium", $instaBreak, "am", 95, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_96, 0), "Curium", $instaBreak, "cm", 96, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_97, 0), "Berkelium", $instaBreak, "bk", 97, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_98, 0), "Californium", $instaBreak, "cf", 98, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_99, 0), "Einsteinium", $instaBreak, "es", 99, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_100, 0), "Fermium", $instaBreak, "fm", 100, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_101, 0), "Mendelevium", $instaBreak, "md", 101, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_102, 0), "Nobelium", $instaBreak, "no", 102, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_103, 0), "Lawrencium", $instaBreak, "lr", 103, 9));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_104, 0), "Rutherfordium", $instaBreak, "rf", 104, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_105, 0), "Dubnium", $instaBreak, "db", 105, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_106, 0), "Seaborgium", $instaBreak, "sg", 106, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_107, 0), "Bohrium", $instaBreak, "bh", 107, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_108, 0), "Hassium", $instaBreak, "hs", 108, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_109, 0), "Meitnerium", $instaBreak, "mt", 109, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_110, 0), "Darmstadtium", $instaBreak, "ds", 110, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_111, 0), "Roentgenium", $instaBreak, "rg", 111, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_112, 0), "Copernicium", $instaBreak, "cn", 112, 2));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_113, 0), "Nihonium", $instaBreak, "nh", 113, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_114, 0), "Flerovium", $instaBreak, "fl", 114, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_115, 0), "Moscovium", $instaBreak, "mc", 115, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_116, 0), "Livermorium", $instaBreak, "lv", 116, 3));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_117, 0), "Tennessine", $instaBreak, "ts", 117, 6));
+ $this->registerAllMeta(new Element(new BID(Ids::ELEMENT_118, 0), "Oganesson", $instaBreak, "og", 118, 7));
+ }
+
+ /**
+ * Claims the whole metadata range (0-15) for all IDs associated with this block. Any unregistered states will be
+ * mapped to the default (provided) state.
+ *
+ * This should only be used when this block type has sole ownership of an ID. For IDs which contain multiple block
+ * types (variants), the regular register() method should be used instead.
+ */
+ private function registerAllMeta(Block $default, Block ...$additional) : void{
+ $ids = [];
+ $this->register($default);
+ foreach($default->getIdInfo()->getAllBlockIds() as $id){
+ $ids[$id] = $id;
+ }
+ foreach($additional as $block){
+ $this->register($block);
+ foreach($block->getIdInfo()->getAllBlockIds() as $id){
+ $ids[$id] = $id;
+ }
+ }
+
+ foreach($ids as $id){
+ for($meta = 0; $meta < 1 << Block::INTERNAL_METADATA_BITS; ++$meta){
+ if(!$this->isRegistered($id, $meta)){
+ $this->remap($id, $meta, $default);
+ }
+ }
+ }
}
private function registerSlabWithDoubleHighBitsRemapping(Slab $block) : void{
@@ -891,7 +981,7 @@ class BlockFactory{
}
foreach($block->getIdInfo()->getAllBlockIds() as $id){
- if(!$override and $this->isRegistered($id, $variant)){
+ if(!$override && $this->isRegistered($id, $variant)){
throw new \InvalidArgumentException("Block registration $id:$variant conflicts with an existing block");
}
@@ -900,7 +990,7 @@ class BlockFactory{
continue;
}
- if(!$override and $this->isRegistered($id, $m)){
+ if(!$override && $this->isRegistered($id, $m)){
throw new \InvalidArgumentException("Block registration " . get_class($block) . " has states which conflict with other blocks");
}
@@ -938,6 +1028,7 @@ class BlockFactory{
private function fillStaticArrays(int $index, Block $block) : void{
$this->fullList[$index] = $block;
+ $this->mappedStateIds[$index] = $block->getFullId();
$this->light[$index] = $block->getLightLevel();
$this->lightFilter[$index] = min(15, $block->getLightFilter() + 1); //opacity plus 1 standard light filter
$this->blocksDirectSkyLight[$index] = $block->blocksDirectSkyLight();
@@ -952,7 +1043,7 @@ class BlockFactory{
* Deserializes a block from the provided legacy ID and legacy meta.
*/
public function get(int $id, int $meta) : Block{
- if($meta < 0 or $meta >= (1 << Block::INTERNAL_METADATA_BITS)){
+ if($meta < 0 || $meta >= (1 << Block::INTERNAL_METADATA_BITS)){
throw new \InvalidArgumentException("Block meta value $meta is out of bounds");
}
@@ -978,7 +1069,7 @@ class BlockFactory{
*/
public function isRegistered(int $id, int $meta = 0) : bool{
$b = $this->fullList[($id << Block::INTERNAL_METADATA_BITS) | $meta];
- return $b !== null and !($b instanceof UnknownBlock);
+ return $b !== null && !($b instanceof UnknownBlock);
}
/**
@@ -987,4 +1078,12 @@ class BlockFactory{
public function getAllKnownStates() : array{
return array_filter($this->fullList->toArray(), function(?Block $v) : bool{ return $v !== null; });
}
+
+ /**
+ * Returns the ID of the state mapped to the given state ID.
+ * Used to correct invalid blockstates found in loaded chunks.
+ */
+ public function getMappedStateId(int $fullState) : int{
+ return $this->mappedStateIds[$fullState] ?? $fullState;
+ }
}
diff --git a/src/block/BlockIdentifier.php b/src/block/BlockIdentifier.php
index 5ea45c90b..b69037ad0 100644
--- a/src/block/BlockIdentifier.php
+++ b/src/block/BlockIdentifier.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\tile\Tile;
+use pocketmine\utils\Utils;
class BlockIdentifier{
@@ -40,6 +41,10 @@ class BlockIdentifier{
$this->blockId = $blockId;
$this->variant = $variant;
$this->itemId = $itemId;
+
+ if($tileClass !== null){
+ Utils::testValidInstance($tileClass, Tile::class);
+ }
$this->tileClass = $tileClass;
}
diff --git a/src/block/BlockLegacyMetadata.php b/src/block/BlockLegacyMetadata.php
index c9faa5ce7..5078654ab 100644
--- a/src/block/BlockLegacyMetadata.php
+++ b/src/block/BlockLegacyMetadata.php
@@ -142,6 +142,8 @@ final class BlockLegacyMetadata{
public const LEAVES_FLAG_NO_DECAY = 0x04;
public const LEAVES_FLAG_CHECK_DECAY = 0x08;
+ public const LECTERN_FLAG_POWERED = 0x04;
+
public const LEVER_FLAG_POWERED = 0x08;
public const LIQUID_FLAG_FALLING = 0x08;
diff --git a/src/block/BrewingStand.php b/src/block/BrewingStand.php
index 8d631453f..fd763a3f0 100644
--- a/src/block/BrewingStand.php
+++ b/src/block/BrewingStand.php
@@ -26,6 +26,9 @@ namespace pocketmine\block;
use pocketmine\block\tile\BrewingStand as TileBrewingStand;
use pocketmine\block\utils\BrewingStandSlot;
use pocketmine\item\Item;
+use pocketmine\math\Axis;
+use pocketmine\math\AxisAlignedBB;
+use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use function array_key_exists;
@@ -67,6 +70,19 @@ class BrewingStand extends Transparent{
return 0b111;
}
+ protected function recalculateCollisionBoxes() : array{
+ return [
+ //bottom slab part - in PC this is also inset on X/Z by 1/16, but Bedrock sucks
+ AxisAlignedBB::one()->trim(Facing::UP, 7 / 8),
+
+ //center post
+ AxisAlignedBB::one()
+ ->squash(Axis::X, 7 / 16)
+ ->squash(Axis::Z, 7 / 16)
+ ->trim(Facing::UP, 1 / 8)
+ ];
+ }
+
public function hasSlot(BrewingStandSlot $slot) : bool{
return array_key_exists($slot->id(), $this->slots);
}
@@ -100,7 +116,7 @@ class BrewingStand extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player instanceof Player){
$stand = $this->position->getWorld()->getTile($this->position);
- if($stand instanceof TileBrewingStand and $stand->canOpenWith($item->getCustomName())){
+ if($stand instanceof TileBrewingStand && $stand->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($stand->getInventory());
}
}
@@ -109,6 +125,24 @@ class BrewingStand extends Transparent{
}
public function onScheduledUpdate() : void{
- //TODO
+ $brewing = $this->position->getWorld()->getTile($this->position);
+ if($brewing instanceof TileBrewingStand){
+ if($brewing->onUpdate()){
+ $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1);
+ }
+
+ $changed = false;
+ foreach(BrewingStandSlot::getAll() as $slot){
+ $occupied = !$brewing->getInventory()->isSlotEmpty($slot->getSlotNumber());
+ if($occupied !== $this->hasSlot($slot)){
+ $this->setSlot($slot, $occupied);
+ $changed = true;
+ }
+ }
+
+ if($changed){
+ $this->position->getWorld()->setBlock($this->position, $this);
+ }
+ }
}
}
diff --git a/src/block/Cactus.php b/src/block/Cactus.php
index 56ee7017f..2802c4966 100644
--- a/src/block/Cactus.php
+++ b/src/block/Cactus.php
@@ -82,7 +82,7 @@ class Cactus extends Transparent{
public function onNearbyBlockChange() : void{
$down = $this->getSide(Facing::DOWN);
- if($down->getId() !== BlockLegacyIds::SAND and !$down->isSameType($this)){
+ if($down->getId() !== BlockLegacyIds::SAND && !$down->isSameType($this)){
$this->position->getWorld()->useBreakOn($this->position);
}else{
foreach(Facing::HORIZONTAL as $side){
@@ -129,7 +129,7 @@ class Cactus extends Transparent{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$down = $this->getSide(Facing::DOWN);
- if($down->getId() === BlockLegacyIds::SAND or $down->isSameType($this)){
+ if($down->getId() === BlockLegacyIds::SAND || $down->isSameType($this)){
foreach(Facing::HORIZONTAL as $side){
if($this->getSide($side)->isSolid()){
return false;
diff --git a/src/block/Cake.php b/src/block/Cake.php
index 75030c659..e9f82943f 100644
--- a/src/block/Cake.php
+++ b/src/block/Cake.php
@@ -94,8 +94,7 @@ class Cake extends Transparent implements FoodSource{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player !== null){
- $player->consumeObject($this);
- return true;
+ return $player->consumeObject($this);
}
return false;
diff --git a/src/block/Chest.php b/src/block/Chest.php
index c89b3dde7..c270246e5 100644
--- a/src/block/Chest.php
+++ b/src/block/Chest.php
@@ -26,6 +26,7 @@ namespace pocketmine\block;
use pocketmine\block\tile\Chest as TileChest;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\NormalHorizontalFacingInMetadataTrait;
+use pocketmine\event\block\ChestPairEvent;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
@@ -47,17 +48,21 @@ class Chest extends Transparent{
public function onPostPlace() : void{
$tile = $this->position->getWorld()->getTile($this->position);
if($tile instanceof TileChest){
- foreach([
- Facing::rotateY($this->facing, true),
- Facing::rotateY($this->facing, false)
- ] as $side){
+ foreach([false, true] as $clockwise){
+ $side = Facing::rotateY($this->facing, $clockwise);
$c = $this->getSide($side);
- if($c instanceof Chest and $c->isSameType($this) and $c->facing === $this->facing){
- $pair = $this->position->getWorld()->getTile($c->position);
- if($pair instanceof TileChest and !$pair->isPaired()){
- $pair->pairWith($tile);
- $tile->pairWith($pair);
- break;
+ if($c instanceof Chest && $c->isSameType($this) && $c->facing === $this->facing){
+ $world = $this->position->getWorld();
+ $pair = $world->getTile($c->position);
+ if($pair instanceof TileChest && !$pair->isPaired()){
+ [$left, $right] = $clockwise ? [$c, $this] : [$this, $c];
+ $ev = new ChestPairEvent($left, $right);
+ $ev->call();
+ if(!$ev->isCancelled() && $world->getBlock($this->position)->isSameType($this) && $world->getBlock($c->position)->isSameType($c)){
+ $pair->pairWith($tile);
+ $tile->pairWith($pair);
+ break;
+ }
}
}
}
@@ -70,8 +75,8 @@ class Chest extends Transparent{
$chest = $this->position->getWorld()->getTile($this->position);
if($chest instanceof TileChest){
if(
- !$this->getSide(Facing::UP)->isTransparent() or
- (($pair = $chest->getPair()) !== null and !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) or
+ !$this->getSide(Facing::UP)->isTransparent() ||
+ (($pair = $chest->getPair()) !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) ||
!$chest->canOpenWith($item->getCustomName())
){
return true;
diff --git a/src/block/Cobweb.php b/src/block/Cobweb.php
index 680af1595..785db721d 100644
--- a/src/block/Cobweb.php
+++ b/src/block/Cobweb.php
@@ -39,6 +39,9 @@ class Cobweb extends Flowable{
}
public function getDropsForCompatibleTool(Item $item) : array{
+ if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){
+ return [$this->asItem()];
+ }
return [
VanillaItems::STRING()
];
diff --git a/src/block/CocoaBlock.php b/src/block/CocoaBlock.php
index 82b514172..ef19356e8 100644
--- a/src/block/CocoaBlock.php
+++ b/src/block/CocoaBlock.php
@@ -26,6 +26,7 @@ namespace pocketmine\block;
use pocketmine\block\utils\BlockDataSerializer;
use pocketmine\block\utils\HorizontalFacingTrait;
use pocketmine\block\utils\TreeType;
+use pocketmine\event\block\BlockGrowEvent;
use pocketmine\item\Fertilizer;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
@@ -85,7 +86,7 @@ class CocoaBlock extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if(Facing::axis($face) !== Axis::Y and $this->canAttachTo($blockClicked)){
+ if(Facing::axis($face) !== Axis::Y && $this->canAttachTo($blockClicked)){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
@@ -94,10 +95,7 @@ class CocoaBlock extends Transparent{
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($this->age < 2 and $item instanceof Fertilizer){
- $this->age++;
- $this->position->getWorld()->setBlock($this->position, $this);
-
+ if($item instanceof Fertilizer && $this->grow()){
$item->pop();
return true;
@@ -117,12 +115,25 @@ class CocoaBlock extends Transparent{
}
public function onRandomTick() : void{
- if($this->age < 2 and mt_rand(1, 5) === 1){
- $this->age++;
- $this->position->getWorld()->setBlock($this->position, $this);
+ if(mt_rand(1, 5) === 1){
+ $this->grow();
}
}
+ private function grow() : bool{
+ if($this->age < 2){
+ $block = clone $this;
+ $block->age++;
+ $ev = new BlockGrowEvent($this, $block);
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ return true;
+ }
+ }
+ return false;
+ }
+
public function getDropsForCompatibleTool(Item $item) : array{
return [
VanillaItems::COCOA_BEANS()->setCount($this->age === 2 ? mt_rand(2, 3) : 1)
diff --git a/src/block/ConcretePowder.php b/src/block/ConcretePowder.php
index b3c4c51ea..6503faf72 100644
--- a/src/block/ConcretePowder.php
+++ b/src/block/ConcretePowder.php
@@ -27,6 +27,7 @@ use pocketmine\block\utils\ColorInMetadataTrait;
use pocketmine\block\utils\DyeColor;
use pocketmine\block\utils\Fallable;
use pocketmine\block\utils\FallableTrait;
+use pocketmine\event\block\BlockFormEvent;
use pocketmine\math\Facing;
class ConcretePowder extends Opaque implements Fallable{
@@ -42,7 +43,11 @@ class ConcretePowder extends Opaque implements Fallable{
public function onNearbyBlockChange() : void{
if(($block = $this->checkAdjacentWater()) !== null){
- $this->position->getWorld()->setBlock($this->position, $block);
+ $ev = new BlockFormEvent($this, $block);
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ }
}else{
$this->startFalling();
}
diff --git a/src/block/CoralBlock.php b/src/block/CoralBlock.php
index 5399e5804..f06172be5 100644
--- a/src/block/CoralBlock.php
+++ b/src/block/CoralBlock.php
@@ -24,15 +24,14 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\CoralType;
+use pocketmine\block\utils\CoralTypeTrait;
use pocketmine\block\utils\InvalidBlockStateException;
use pocketmine\data\bedrock\CoralTypeIdMap;
use pocketmine\item\Item;
use function mt_rand;
final class CoralBlock extends Opaque{
-
- private CoralType $coralType;
- private bool $dead = false;
+ use CoralTypeTrait;
public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){
$this->coralType = CoralType::TUBE();
@@ -60,22 +59,6 @@ final class CoralBlock extends Opaque{
return 0b1111;
}
- public function getCoralType() : CoralType{ return $this->coralType; }
-
- /** @return $this */
- public function setCoralType(CoralType $coralType) : self{
- $this->coralType = $coralType;
- return $this;
- }
-
- public function isDead() : bool{ return $this->dead; }
-
- /** @return $this */
- public function setDead(bool $dead) : self{
- $this->dead = $dead;
- return $this;
- }
-
public function onNearbyBlockChange() : void{
if(!$this->dead){
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(40, 200));
diff --git a/src/block/CraftingTable.php b/src/block/CraftingTable.php
index a3d1dc47f..a35e13e5b 100644
--- a/src/block/CraftingTable.php
+++ b/src/block/CraftingTable.php
@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
-use pocketmine\crafting\CraftingGrid;
+use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
@@ -32,7 +32,7 @@ class CraftingTable extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player instanceof Player){
- $player->setCraftingGrid(new CraftingGrid($player, CraftingGrid::SIZE_BIG));
+ $player->setCurrentWindow(new CraftingTableInventory($this->position));
}
return true;
diff --git a/src/block/Crops.php b/src/block/Crops.php
index 47cb33897..054fed254 100644
--- a/src/block/Crops.php
+++ b/src/block/Crops.php
@@ -69,7 +69,7 @@ abstract class Crops extends Flowable{
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($this->age < 7 and $item instanceof Fertilizer){
+ if($this->age < 7 && $item instanceof Fertilizer){
$block = clone $this;
$block->age += mt_rand(2, 5);
if($block->age > 7){
@@ -80,10 +80,9 @@ abstract class Crops extends Flowable{
$ev->call();
if(!$ev->isCancelled()){
$this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ $item->pop();
}
- $item->pop();
-
return true;
}
@@ -101,7 +100,7 @@ abstract class Crops extends Flowable{
}
public function onRandomTick() : void{
- if($this->age < 7 and mt_rand(0, 2) === 1){
+ if($this->age < 7 && mt_rand(0, 2) === 1){
$block = clone $this;
++$block->age;
$ev = new BlockGrowEvent($this, $block);
diff --git a/src/block/Dirt.php b/src/block/Dirt.php
index 29df9f545..390098216 100644
--- a/src/block/Dirt.php
+++ b/src/block/Dirt.php
@@ -28,6 +28,7 @@ use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
+use pocketmine\world\sound\ItemUseOnBlockSound;
class Dirt extends Opaque{
@@ -58,9 +59,12 @@ class Dirt extends Opaque{
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($face === Facing::UP and $item instanceof Hoe){
+ if($face === Facing::UP && $item instanceof Hoe){
$item->applyDamage(1);
- $this->position->getWorld()->setBlock($this->position, $this->coarse ? VanillaBlocks::DIRT() : VanillaBlocks::FARMLAND());
+
+ $newBlock = $this->coarse ? VanillaBlocks::DIRT() : VanillaBlocks::FARMLAND();
+ $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock));
+ $this->position->getWorld()->setBlock($this->position, $newBlock);
return true;
}
diff --git a/src/block/Door.php b/src/block/Door.php
index 74a9f1b78..a52e51d9d 100644
--- a/src/block/Door.php
+++ b/src/block/Door.php
@@ -72,7 +72,7 @@ class Door extends Transparent{
//copy door properties from other half
$other = $this->getSide($this->top ? Facing::DOWN : Facing::UP);
- if($other instanceof Door and $other->isSameType($this)){
+ if($other instanceof Door && $other->isSameType($this)){
if($this->top){
$this->facing = $other->facing;
$this->open = $other->open;
@@ -129,7 +129,7 @@ class Door extends Transparent{
if($face === Facing::UP){
$blockUp = $this->getSide(Facing::UP);
$blockDown = $this->getSide(Facing::DOWN);
- if(!$blockUp->canBeReplaced() or $blockDown->isTransparent()){
+ if(!$blockUp->canBeReplaced() || $blockDown->isTransparent()){
return false;
}
@@ -140,7 +140,7 @@ class Door extends Transparent{
$next = $this->getSide(Facing::rotateY($this->facing, false));
$next2 = $this->getSide(Facing::rotateY($this->facing, true));
- if($next->isSameType($this) or (!$next2->isTransparent() and $next->isTransparent())){ //Door hinge
+ if($next->isSameType($this) || (!$next2->isTransparent() && $next->isTransparent())){ //Door hinge
$this->hingeRight = true;
}
@@ -158,7 +158,7 @@ class Door extends Transparent{
$this->open = !$this->open;
$other = $this->getSide($this->top ? Facing::DOWN : Facing::UP);
- if($other instanceof Door and $other->isSameType($this)){
+ if($other instanceof Door && $other->isSameType($this)){
$other->open = $this->open;
$this->position->getWorld()->setBlock($other->position, $other);
}
diff --git a/src/block/DoublePlant.php b/src/block/DoublePlant.php
index 00d361832..155a8e59d 100644
--- a/src/block/DoublePlant.php
+++ b/src/block/DoublePlant.php
@@ -55,7 +55,7 @@ class DoublePlant extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$id = $blockReplace->getSide(Facing::DOWN)->getId();
- if(($id === BlockLegacyIds::GRASS or $id === BlockLegacyIds::DIRT) and $blockReplace->getSide(Facing::UP)->canBeReplaced()){
+ if(($id === BlockLegacyIds::GRASS || $id === BlockLegacyIds::DIRT) && $blockReplace->getSide(Facing::UP)->canBeReplaced()){
$top = clone $this;
$top->top = true;
$tx->addBlock($blockReplace->position, $this)->addBlock($blockReplace->position->getSide(Facing::UP), $top);
@@ -72,14 +72,14 @@ class DoublePlant extends Flowable{
$other = $this->getSide($this->top ? Facing::DOWN : Facing::UP);
return (
- $other instanceof DoublePlant and
- $other->isSameType($this) and
+ $other instanceof DoublePlant &&
+ $other->isSameType($this) &&
$other->top !== $this->top
);
}
public function onNearbyBlockChange() : void{
- if(!$this->isValidHalfPlant() or (!$this->top and $this->getSide(Facing::DOWN)->isTransparent())){
+ if(!$this->isValidHalfPlant() || (!$this->top && $this->getSide(Facing::DOWN)->isTransparent())){
$this->position->getWorld()->useBreakOn($this->position);
}
}
diff --git a/src/block/DoubleTallGrass.php b/src/block/DoubleTallGrass.php
index 6fee40e32..f3d98b265 100644
--- a/src/block/DoubleTallGrass.php
+++ b/src/block/DoubleTallGrass.php
@@ -34,7 +34,7 @@ class DoubleTallGrass extends DoublePlant{
}
public function getDropsForIncompatibleTool(Item $item) : array{
- if($this->top and mt_rand(0, 7) === 0){
+ if($this->top && mt_rand(0, 7) === 0){
return [VanillaItems::WHEAT_SEEDS()];
}
return [];
diff --git a/src/block/EndRod.php b/src/block/EndRod.php
index 39c28a097..18fd438b5 100644
--- a/src/block/EndRod.php
+++ b/src/block/EndRod.php
@@ -46,7 +46,7 @@ class EndRod extends Flowable{
}
public function readStateFromData(int $id, int $stateMeta) : void{
- if($stateMeta !== 0 and $stateMeta !== 1){
+ if($stateMeta !== 0 && $stateMeta !== 1){
$stateMeta ^= 1;
}
@@ -59,7 +59,7 @@ class EndRod extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$this->facing = $face;
- if($blockClicked instanceof EndRod and $blockClicked->facing === $this->facing){
+ if($blockClicked instanceof EndRod && $blockClicked->facing === $this->facing){
$this->facing = Facing::opposite($face);
}
diff --git a/src/block/EnderChest.php b/src/block/EnderChest.php
index e37632008..6f6acc39a 100644
--- a/src/block/EnderChest.php
+++ b/src/block/EnderChest.php
@@ -52,7 +52,7 @@ class EnderChest extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player instanceof Player){
$enderChest = $this->position->getWorld()->getTile($this->position);
- if($enderChest instanceof TileEnderChest and $this->getSide(Facing::UP)->isTransparent()){
+ if($enderChest instanceof TileEnderChest && $this->getSide(Facing::UP)->isTransparent()){
$enderChest->setViewerCount($enderChest->getViewerCount() + 1);
$player->setCurrentWindow(new EnderChestInventory($this->position, $player->getEnderInventory()));
}
@@ -66,4 +66,8 @@ class EnderChest extends Transparent{
VanillaBlocks::OBSIDIAN()->asItem()->setCount(8)
];
}
+
+ public function isAffectedBySilkTouch() : bool{
+ return true;
+ }
}
diff --git a/src/block/Fence.php b/src/block/Fence.php
index 0ee3cdaf1..cbcdd62e9 100644
--- a/src/block/Fence.php
+++ b/src/block/Fence.php
@@ -41,7 +41,7 @@ class Fence extends Transparent{
foreach(Facing::HORIZONTAL as $facing){
$block = $this->getSide($facing);
- if($block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent())){
+ if($block instanceof static || $block instanceof FenceGate || ($block->isSolid() && !$block->isTransparent())){
$this->connections[$facing] = true;
}else{
unset($this->connections[$facing]);
@@ -61,7 +61,7 @@ class Fence extends Transparent{
$connectWest = isset($this->connections[Facing::WEST]);
$connectEast = isset($this->connections[Facing::EAST]);
- if($connectWest or $connectEast){
+ if($connectWest || $connectEast){
//X axis (west/east)
$bbs[] = AxisAlignedBB::one()
->squash(Axis::Z, $inset)
@@ -73,7 +73,7 @@ class Fence extends Transparent{
$connectNorth = isset($this->connections[Facing::NORTH]);
$connectSouth = isset($this->connections[Facing::SOUTH]);
- if($connectNorth or $connectSouth){
+ if($connectNorth || $connectSouth){
//Z axis (north/south)
$bbs[] = AxisAlignedBB::one()
->squash(Axis::X, $inset)
diff --git a/src/block/FenceGate.php b/src/block/FenceGate.php
index 32d79eba3..4255ee5ef 100644
--- a/src/block/FenceGate.php
+++ b/src/block/FenceGate.php
@@ -80,7 +80,7 @@ class FenceGate extends Transparent{
private function checkInWall() : bool{
return (
- $this->getSide(Facing::rotateY($this->facing, false)) instanceof Wall or
+ $this->getSide(Facing::rotateY($this->facing, false)) instanceof Wall ||
$this->getSide(Facing::rotateY($this->facing, true)) instanceof Wall
);
}
@@ -105,7 +105,7 @@ class FenceGate extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$this->open = !$this->open;
- if($this->open and $player !== null){
+ if($this->open && $player !== null){
$playerFacing = $player->getHorizontalFacing();
if($playerFacing === Facing::opposite($this->facing)){
$this->facing = $playerFacing;
diff --git a/src/block/Fire.php b/src/block/Fire.php
index 82c08bfe0..1b73882b0 100644
--- a/src/block/Fire.php
+++ b/src/block/Fire.php
@@ -27,11 +27,16 @@ use pocketmine\block\utils\BlockDataSerializer;
use pocketmine\entity\Entity;
use pocketmine\entity\projectile\Arrow;
use pocketmine\event\block\BlockBurnEvent;
+use pocketmine\event\block\BlockSpreadEvent;
use pocketmine\event\entity\EntityCombustByBlockEvent;
use pocketmine\event\entity\EntityDamageByBlockEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\item\Item;
use pocketmine\math\Facing;
+use pocketmine\world\format\Chunk;
+use pocketmine\world\World;
+use function intdiv;
+use function max;
use function min;
use function mt_rand;
@@ -94,7 +99,7 @@ class Fire extends Flowable{
}
public function onNearbyBlockChange() : void{
- if(!$this->getSide(Facing::DOWN)->isSolid() and !$this->hasAdjacentFlammableBlocks()){
+ if(!$this->getSide(Facing::DOWN)->isSolid() && !$this->hasAdjacentFlammableBlocks()){
$this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR());
}else{
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40));
@@ -109,7 +114,7 @@ class Fire extends Flowable{
$down = $this->getSide(Facing::DOWN);
$result = null;
- if($this->age < 15 and mt_rand(0, 2) === 0){
+ if($this->age < 15 && mt_rand(0, 2) === 0){
$this->age++;
$result = $this;
}
@@ -118,13 +123,13 @@ class Fire extends Flowable{
if(!$down->burnsForever()){
//TODO: check rain
if($this->age === 15){
- if(!$down->isFlammable() and mt_rand(0, 3) === 3){ //1/4 chance to extinguish
+ if(!$down->isFlammable() && mt_rand(0, 3) === 3){ //1/4 chance to extinguish
$canSpread = false;
$result = VanillaBlocks::AIR();
}
}elseif(!$this->hasAdjacentFlammableBlocks()){
$canSpread = false;
- if(!$down->isSolid() or $this->age > 3){
+ if(!$down->isSolid() || $this->age > 3){
$result = VanillaBlocks::AIR();
}
}
@@ -137,17 +142,8 @@ class Fire extends Flowable{
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40));
if($canSpread){
- //TODO: raise upper bound for chance in humid biomes
-
- foreach($this->getHorizontalSides() as $side){
- $this->burnBlock($side, 300);
- }
-
- //vanilla uses a 250 upper bound here, but I don't think they intended to increase the chance of incineration
- $this->burnBlock($this->getSide(Facing::UP), 350);
- $this->burnBlock($this->getSide(Facing::DOWN), 350);
-
- //TODO: fire spread
+ $this->burnBlocksAround();
+ $this->spreadFire();
}
}
@@ -165,6 +161,18 @@ class Fire extends Flowable{
return false;
}
+ private function burnBlocksAround() : void{
+ //TODO: raise upper bound for chance in humid biomes
+
+ foreach($this->getHorizontalSides() as $side){
+ $this->burnBlock($side, 300);
+ }
+
+ //vanilla uses a 250 upper bound here, but I don't think they intended to increase the chance of incineration
+ $this->burnBlock($this->getSide(Facing::UP), 350);
+ $this->burnBlock($this->getSide(Facing::DOWN), 350);
+ }
+
private function burnBlock(Block $block, int $chanceBound) : void{
if(mt_rand(0, $chanceBound) < $block->getFlammability()){
$ev = new BlockBurnEvent($block, $this);
@@ -172,14 +180,85 @@ class Fire extends Flowable{
if(!$ev->isCancelled()){
$block->onIncinerate();
+ $spreadedFire = false;
if(mt_rand(0, $this->age + 9) < 5){ //TODO: check rain
$fire = clone $this;
$fire->age = min(15, $fire->age + (mt_rand(0, 4) >> 2));
- $this->position->getWorld()->setBlock($block->position, $fire);
- }else{
+ $spreadedFire = $this->spreadBlock($block, $fire);
+ }
+ if(!$spreadedFire){
$this->position->getWorld()->setBlock($block->position, VanillaBlocks::AIR());
}
}
}
}
+
+ private function spreadFire() : void{
+ $world = $this->position->getWorld();
+ $difficultyChanceIncrease = $world->getDifficulty() * 7;
+ $ageDivisor = $this->age + 30;
+
+ for($y = -1; $y <= 4; ++$y){
+ $targetY = $y + (int) $this->position->y;
+ if($targetY < World::Y_MIN || $targetY >= World::Y_MAX){
+ continue;
+ }
+ //Higher blocks have a lower chance of catching fire
+ $randomBound = 100 + ($y > 1 ? ($y - 1) * 100 : 0);
+
+ for($z = -1; $z <= 1; ++$z){
+ $targetZ = $z + (int) $this->position->z;
+ for($x = -1; $x <= 1; ++$x){
+ if($x === 0 && $y === 0 && $z === 0){
+ continue;
+ }
+ $targetX = $x + (int) $this->position->x;
+ if(!$world->isInWorld($targetX, $targetY, $targetZ)){
+ continue;
+ }
+
+ if(!$world->isChunkLoaded($targetX >> Chunk::COORD_BIT_SIZE, $targetZ >> Chunk::COORD_BIT_SIZE)){
+ continue;
+ }
+ $block = $world->getBlockAt($targetX, $targetY, $targetZ);
+ if($block->getId() !== BlockLegacyIds::AIR){
+ continue;
+ }
+
+ //TODO: fire can't spread if it's raining in any horizontally adjacent block, or the current one
+
+ $encouragement = 0;
+ foreach($block->position->sides() as $vector3){
+ if($world->isInWorld($vector3->x, $vector3->y, $vector3->z)){
+ $encouragement = max($encouragement, $world->getBlockAt($vector3->x, $vector3->y, $vector3->z)->getFlameEncouragement());
+ }
+ }
+
+ if($encouragement <= 0){
+ continue;
+ }
+
+ $maxChance = intdiv($encouragement + 40 + $difficultyChanceIncrease, $ageDivisor);
+ //TODO: max chance is lowered by half in humid biomes
+
+ if($maxChance > 0 && mt_rand(0, $randomBound - 1) <= $maxChance){
+ $new = clone $this;
+ $new->age = min(15, $this->age + (mt_rand(0, 4) >> 2));
+ $this->spreadBlock($block, $new);
+ }
+ }
+ }
+ }
+ }
+
+ private function spreadBlock(Block $block, Block $newState) : bool{
+ $ev = new BlockSpreadEvent($block, $this, $newState);
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $block->position->getWorld()->setBlock($block->position, $ev->getNewState());
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/block/FletchingTable.php b/src/block/FletchingTable.php
new file mode 100644
index 000000000..64a71ad97
--- /dev/null
+++ b/src/block/FletchingTable.php
@@ -0,0 +1,30 @@
+getSide(Facing::DOWN);
- if($down->getId() === BlockLegacyIds::GRASS or $down->getId() === BlockLegacyIds::DIRT or $down->getId() === BlockLegacyIds::FARMLAND){
+ if($down->getId() === BlockLegacyIds::GRASS || $down->getId() === BlockLegacyIds::DIRT || $down->getId() === BlockLegacyIds::FARMLAND){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
diff --git a/src/block/FlowerPot.php b/src/block/FlowerPot.php
index 79e8e80b1..6dc21eb44 100644
--- a/src/block/FlowerPot.php
+++ b/src/block/FlowerPot.php
@@ -69,7 +69,7 @@ class FlowerPot extends Flowable{
/** @return $this */
public function setPlant(?Block $plant) : self{
- if($plant === null or $plant instanceof Air){
+ if($plant === null || $plant instanceof Air){
$this->plant = null;
}else{
$this->plant = clone $plant;
@@ -83,12 +83,12 @@ class FlowerPot extends Flowable{
}
return
- $block instanceof Cactus or
- $block instanceof DeadBush or
- $block instanceof Flower or
- $block instanceof RedMushroom or
- $block instanceof Sapling or
- ($block instanceof TallGrass and $block->getIdInfo()->getVariant() === BlockLegacyMetadata::TALLGRASS_FERN); //TODO: clean up
+ $block instanceof Cactus ||
+ $block instanceof DeadBush ||
+ $block instanceof Flower ||
+ $block instanceof RedMushroom ||
+ $block instanceof Sapling ||
+ ($block instanceof TallGrass && $block->getIdInfo()->getVariant() === BlockLegacyMetadata::TALLGRASS_FERN); //TODO: clean up
//TODO: bamboo
}
diff --git a/src/block/FrostedIce.php b/src/block/FrostedIce.php
index 269832d49..217263e92 100644
--- a/src/block/FrostedIce.php
+++ b/src/block/FrostedIce.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\BlockDataSerializer;
+use pocketmine\event\block\BlockMeltEvent;
use function mt_rand;
class FrostedIce extends Ice{
@@ -62,7 +63,7 @@ class FrostedIce extends Ice{
}
public function onRandomTick() : void{
- if((!$this->checkAdjacentBlocks(4) or mt_rand(0, 2) === 0) and
+ if((!$this->checkAdjacentBlocks(4) || mt_rand(0, 2) === 0) &&
$this->position->getWorld()->getHighestAdjacentFullLightAt($this->position->x, $this->position->y, $this->position->z) >= 12 - $this->age){
if($this->tryMelt()){
foreach($this->getAllSides() as $block){
@@ -84,11 +85,11 @@ class FrostedIce extends Ice{
$found = 0;
for($x = -1; $x <= 1; ++$x){
for($z = -1; $z <= 1; ++$z){
- if($x === 0 and $z === 0){
+ if($x === 0 && $z === 0){
continue;
}
if(
- $this->position->getWorld()->getBlockAt($this->position->x + $x, $this->position->y, $this->position->z + $z) instanceof FrostedIce and
+ $this->position->getWorld()->getBlockAt($this->position->x + $x, $this->position->y, $this->position->z + $z) instanceof FrostedIce &&
++$found >= $requirement
){
return true;
@@ -105,7 +106,11 @@ class FrostedIce extends Ice{
*/
private function tryMelt() : bool{
if($this->age >= 3){
- $this->position->getWorld()->useBreakOn($this->position);
+ $ev = new BlockMeltEvent($this, VanillaBlocks::WATER());
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ }
return true;
}
diff --git a/src/block/Furnace.php b/src/block/Furnace.php
index c49ffa35b..ff96d0b27 100644
--- a/src/block/Furnace.php
+++ b/src/block/Furnace.php
@@ -29,6 +29,7 @@ use pocketmine\block\utils\NormalHorizontalFacingInMetadataTrait;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
+use function mt_rand;
class Furnace extends Opaque{
use FacesOppositePlacingPlayerTrait;
@@ -73,7 +74,7 @@ class Furnace extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player instanceof Player){
$furnace = $this->position->getWorld()->getTile($this->position);
- if($furnace instanceof TileFurnace and $furnace->canOpenWith($item->getCustomName())){
+ if($furnace instanceof TileFurnace && $furnace->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($furnace->getInventory());
}
}
@@ -83,7 +84,10 @@ class Furnace extends Opaque{
public function onScheduledUpdate() : void{
$furnace = $this->position->getWorld()->getTile($this->position);
- if($furnace instanceof TileFurnace and $furnace->onUpdate()){
+ if($furnace instanceof TileFurnace && $furnace->onUpdate()){
+ if(mt_rand(1, 60) === 1){ //in vanilla this is between 1 and 5 seconds; try to average about 3
+ $this->position->getWorld()->addSound($this->position, $furnace->getFurnaceType()->getCookSound());
+ }
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); //TODO: check this
}
}
diff --git a/src/block/Grass.php b/src/block/Grass.php
index e6a128850..82d42c55f 100644
--- a/src/block/Grass.php
+++ b/src/block/Grass.php
@@ -33,6 +33,7 @@ use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\Random;
use pocketmine\world\generator\object\TallGrass as TallGrassObject;
+use pocketmine\world\sound\ItemUseOnBlockSound;
use function mt_rand;
class Grass extends Opaque{
@@ -53,7 +54,7 @@ class Grass extends Opaque{
public function onRandomTick() : void{
$lightAbove = $this->position->getWorld()->getFullLightAt($this->position->x, $this->position->y + 1, $this->position->z);
- if($lightAbove < 4 and $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + 1, $this->position->z)->getLightFilter() >= 2){
+ if($lightAbove < 4 && $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + 1, $this->position->z)->getLightFilter() >= 2){
//grass dies
$ev = new BlockSpreadEvent($this, $this, VanillaBlocks::DIRT());
$ev->call();
@@ -69,9 +70,9 @@ class Grass extends Opaque{
$b = $this->position->getWorld()->getBlockAt($x, $y, $z);
if(
- !($b instanceof Dirt) or
- $b->isCoarse() or
- $this->position->getWorld()->getFullLightAt($x, $y + 1, $z) < 4 or
+ !($b instanceof Dirt) ||
+ $b->isCoarse() ||
+ $this->position->getWorld()->getFullLightAt($x, $y + 1, $z) < 4 ||
$this->position->getWorld()->getBlockAt($x, $y + 1, $z)->getLightFilter() >= 2
){
continue;
@@ -97,12 +98,16 @@ class Grass extends Opaque{
return true;
}elseif($item instanceof Hoe){
$item->applyDamage(1);
- $this->position->getWorld()->setBlock($this->position, VanillaBlocks::FARMLAND());
+ $newBlock = VanillaBlocks::FARMLAND();
+ $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock));
+ $this->position->getWorld()->setBlock($this->position, $newBlock);
return true;
- }elseif($item instanceof Shovel and $this->getSide(Facing::UP)->getId() === BlockLegacyIds::AIR){
+ }elseif($item instanceof Shovel && $this->getSide(Facing::UP)->getId() === BlockLegacyIds::AIR){
$item->applyDamage(1);
- $this->position->getWorld()->setBlock($this->position, VanillaBlocks::GRASS_PATH());
+ $newBlock = VanillaBlocks::GRASS_PATH();
+ $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new ItemUseOnBlockSound($newBlock));
+ $this->position->getWorld()->setBlock($this->position, $newBlock);
return true;
}
diff --git a/src/block/HayBale.php b/src/block/HayBale.php
index abf25e8a1..92df9a5fb 100644
--- a/src/block/HayBale.php
+++ b/src/block/HayBale.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\PillarRotationInMetadataTrait;
+use pocketmine\entity\Entity;
class HayBale extends Opaque{
use PillarRotationInMetadataTrait;
@@ -35,4 +36,9 @@ class HayBale extends Opaque{
public function getFlammability() : int{
return 20;
}
+
+ public function onEntityLand(Entity $entity) : ?float{
+ $entity->fallDistance *= 0.2;
+ return null;
+ }
}
diff --git a/src/block/Ice.php b/src/block/Ice.php
index c1156fffa..eb7d6a1df 100644
--- a/src/block/Ice.php
+++ b/src/block/Ice.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
+use pocketmine\event\block\BlockMeltEvent;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
use pocketmine\player\Player;
@@ -38,7 +39,7 @@ class Ice extends Transparent{
}
public function onBreak(Item $item, ?Player $player = null) : bool{
- if(($player === null or $player->isSurvival()) and !$item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){
+ if(($player === null || $player->isSurvival()) && !$item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){
$this->position->getWorld()->setBlock($this->position, VanillaBlocks::WATER());
return true;
}
@@ -51,7 +52,11 @@ class Ice extends Transparent{
public function onRandomTick() : void{
if($this->position->getWorld()->getHighestAdjacentBlockLight($this->position->x, $this->position->y, $this->position->z) >= 12){
- $this->position->getWorld()->useBreakOn($this->position);
+ $ev = new BlockMeltEvent($this, VanillaBlocks::WATER());
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ }
}
}
diff --git a/src/block/ItemFrame.php b/src/block/ItemFrame.php
index aff9f6566..270938d75 100644
--- a/src/block/ItemFrame.php
+++ b/src/block/ItemFrame.php
@@ -86,7 +86,7 @@ class ItemFrame extends Flowable{
/** @return $this */
public function setFramedItem(?Item $item) : self{
- if($item === null or $item->isNull()){
+ if($item === null || $item->isNull()){
$this->framedItem = null;
$this->itemRotation = 0;
}else{
@@ -161,7 +161,7 @@ class ItemFrame extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($face === Facing::DOWN or $face === Facing::UP or !$blockClicked->isSolid()){
+ if($face === Facing::DOWN || $face === Facing::UP || !$blockClicked->isSolid()){
return false;
}
@@ -172,7 +172,7 @@ class ItemFrame extends Flowable{
public function getDropsForCompatibleTool(Item $item) : array{
$drops = parent::getDropsForCompatibleTool($item);
- if($this->framedItem !== null and lcg_value() <= $this->itemDropChance){
+ if($this->framedItem !== null && lcg_value() <= $this->itemDropChance){
$drops[] = clone $this->framedItem;
}
diff --git a/src/block/Ladder.php b/src/block/Ladder.php
index f76a28b6f..bd4de7e0f 100644
--- a/src/block/Ladder.php
+++ b/src/block/Ladder.php
@@ -65,7 +65,7 @@ class Ladder extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if(!$blockClicked->isTransparent() and Facing::axis($face) !== Axis::Y){
+ if(!$blockClicked->isTransparent() && Facing::axis($face) !== Axis::Y){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
diff --git a/src/block/Lantern.php b/src/block/Lantern.php
index 4bd2f5918..516374a4d 100644
--- a/src/block/Lantern.php
+++ b/src/block/Lantern.php
@@ -77,11 +77,11 @@ class Lantern extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if(!$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->up())) and !$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->down()))){
+ if(!$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->up())) && !$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->down()))){
return false;
}
- $this->hanging = ($face === Facing::DOWN or !$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->down())));
+ $this->hanging = ($face === Facing::DOWN || !$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->down())));
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
diff --git a/src/block/Lava.php b/src/block/Lava.php
index 06ee5fd81..729393193 100644
--- a/src/block/Lava.php
+++ b/src/block/Lava.php
@@ -91,8 +91,6 @@ class Lava extends Liquid{
}
public function onEntityInside(Entity $entity) : bool{
- $entity->fallDistance *= 0.5;
-
$ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_LAVA, 4);
$entity->attack($ev);
diff --git a/src/block/Leaves.php b/src/block/Leaves.php
index 03c99936e..7ca55c6dc 100644
--- a/src/block/Leaves.php
+++ b/src/block/Leaves.php
@@ -96,7 +96,7 @@ class Leaves extends Transparent{
return true;
}
- if($block->getId() === $this->getId() and $distance <= 4){
+ if($block->getId() === $this->getId() && $distance <= 4){
foreach(Facing::ALL as $side){
if($this->findLog($pos->getSide($side), $visited, $distance + 1)){
return true;
@@ -108,7 +108,7 @@ class Leaves extends Transparent{
}
public function onNearbyBlockChange() : void{
- if(!$this->noDecay and !$this->checkDecay){
+ if(!$this->noDecay && !$this->checkDecay){
$this->checkDecay = true;
$this->position->getWorld()->setBlock($this->position, $this, false);
}
@@ -119,10 +119,10 @@ class Leaves extends Transparent{
}
public function onRandomTick() : void{
- if(!$this->noDecay and $this->checkDecay){
+ if(!$this->noDecay && $this->checkDecay){
$ev = new LeavesDecayEvent($this);
$ev->call();
- if($ev->isCancelled() or $this->findLog($this->position)){
+ if($ev->isCancelled() || $this->findLog($this->position)){
$this->checkDecay = false;
$this->position->getWorld()->setBlock($this->position, $this, false);
}else{
@@ -145,7 +145,7 @@ class Leaves extends Transparent{
if(mt_rand(1, 20) === 1){ //Saplings
$drops[] = ItemFactory::getInstance()->get(ItemIds::SAPLING, $this->treeType->getMagicNumber());
}
- if(($this->treeType->equals(TreeType::OAK()) or $this->treeType->equals(TreeType::DARK_OAK())) and mt_rand(1, 200) === 1){ //Apples
+ if(($this->treeType->equals(TreeType::OAK()) || $this->treeType->equals(TreeType::DARK_OAK())) && mt_rand(1, 200) === 1){ //Apples
$drops[] = VanillaItems::APPLE();
}
diff --git a/src/block/Lectern.php b/src/block/Lectern.php
new file mode 100644
index 000000000..d56b73c63
--- /dev/null
+++ b/src/block/Lectern.php
@@ -0,0 +1,166 @@
+facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03);
+ $this->producingSignal = ($stateMeta & BlockLegacyMetadata::LECTERN_FLAG_POWERED) !== 0;
+ }
+
+ public function writeStateToMeta() : int{
+ return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing) | ($this->producingSignal ? BlockLegacyMetadata::LECTERN_FLAG_POWERED : 0);
+ }
+
+ public function readStateFromWorld() : void{
+ parent::readStateFromWorld();
+ $tile = $this->position->getWorld()->getTile($this->position);
+ if($tile instanceof TileLectern){
+ $this->viewedPage = $tile->getViewedPage();
+ $this->book = $tile->getBook();
+ }
+ }
+
+ public function writeStateToWorld() : void{
+ parent::writeStateToWorld();
+ $tile = $this->position->getWorld()->getTile($this->position);
+ if($tile instanceof TileLectern){
+ $tile->setViewedPage($this->viewedPage);
+ $tile->setBook($this->book);
+ }
+ }
+
+ public function getStateBitmask() : int{
+ return 0b111;
+ }
+
+ public function getFlammability() : int{
+ return 30;
+ }
+
+ public function getDrops(Item $item) : array{
+ $drops = parent::getDrops($item);
+ if($this->book !== null){
+ $drops[] = clone $this->book;
+ }
+
+ return $drops;
+ }
+
+ protected function recalculateCollisionBoxes() : array{
+ return [AxisAlignedBB::one()->trim(Facing::UP, 0.1)];
+ }
+
+ public function isProducingSignal() : bool{ return $this->producingSignal; }
+
+ /** @return $this */
+ public function setProducingSignal(bool $producingSignal) : self{
+ $this->producingSignal = $producingSignal;
+ return $this;
+ }
+
+ public function getViewedPage() : int{
+ return $this->viewedPage;
+ }
+
+ /** @return $this */
+ public function setViewedPage(int $viewedPage) : self{
+ $this->viewedPage = $viewedPage;
+ return $this;
+ }
+
+ public function getBook() : ?WritableBookBase{
+ return $this->book !== null ? clone $this->book : null;
+ }
+
+ /** @return $this */
+ public function setBook(?WritableBookBase $book) : self{
+ $this->book = $book !== null && !$book->isNull() ? (clone $book)->setCount(1) : null;
+ $this->viewedPage = 0;
+ return $this;
+ }
+
+ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
+ if($this->book === null && $item instanceof WritableBookBase){
+ $this->position->getWorld()->setBlock($this->position, $this->setBook($item));
+ $this->position->getWorld()->addSound($this->position, new LecternPlaceBookSound());
+ $item->pop();
+ }
+ return true;
+ }
+
+ public function onAttack(Item $item, int $face, ?Player $player = null) : bool{
+ if($this->book !== null){
+ $this->position->getWorld()->dropItem($this->position->up(), $this->book);
+ $this->position->getWorld()->setBlock($this->position, $this->setBook(null));
+ }
+ return false;
+ }
+
+ public function onPageTurn(int $newPage) : bool{
+ if($newPage === $this->viewedPage){
+ return true;
+ }
+ if($this->book === null || $newPage >= count($this->book->getPages()) || $newPage < 0){
+ return false;
+ }
+
+ $this->viewedPage = $newPage;
+ if(!$this->producingSignal){
+ $this->producingSignal = true;
+ $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1);
+ }
+
+ $this->position->getWorld()->setBlock($this->position, $this);
+
+ return true;
+ }
+
+ public function onScheduledUpdate() : void{
+ if($this->producingSignal){
+ $this->producingSignal = false;
+ $this->position->getWorld()->setBlock($this->position, $this);
+ }
+ }
+}
diff --git a/src/block/Liquid.php b/src/block/Liquid.php
index 6e3c8df35..46e9c487d 100644
--- a/src/block/Liquid.php
+++ b/src/block/Liquid.php
@@ -131,7 +131,7 @@ abstract class Liquid extends Transparent{
abstract public function getBucketEmptySound() : Sound;
public function isSource() : bool{
- return !$this->falling and $this->decay === 0;
+ return !$this->falling && $this->decay === 0;
}
/**
@@ -154,7 +154,7 @@ abstract class Liquid extends Transparent{
}
protected function getEffectiveFlowDecay(Block $block) : int{
- if(!($block instanceof Liquid) or !$block->isSameType($this)){
+ if(!($block instanceof Liquid) || !$block->isSameType($this)){
return -1;
}
@@ -279,7 +279,7 @@ abstract class Liquid extends Transparent{
$newDecay = $smallestFlowDecay + $multiplier;
$falling = false;
- if($newDecay >= 8 or $smallestFlowDecay < 0){
+ if($newDecay >= 8 || $smallestFlowDecay < 0){
$newDecay = -1;
}
@@ -290,14 +290,14 @@ abstract class Liquid extends Transparent{
$minAdjacentSources = $this->getMinAdjacentSourcesToFormSource();
if($minAdjacentSources !== null && $this->adjacentSources >= $minAdjacentSources){
$bottomBlock = $world->getBlockAt($this->position->x, $this->position->y - 1, $this->position->z);
- if($bottomBlock->isSolid() or ($bottomBlock instanceof Liquid and $bottomBlock->isSameType($this) and $bottomBlock->isSource())){
+ if($bottomBlock->isSolid() || ($bottomBlock instanceof Liquid && $bottomBlock->isSameType($this) && $bottomBlock->isSource())){
$newDecay = 0;
$falling = false;
}
}
- if($falling !== $this->falling or (!$falling and $newDecay !== $this->decay)){
- if(!$falling and $newDecay < 0){
+ if($falling !== $this->falling || (!$falling && $newDecay !== $this->decay)){
+ if(!$falling && $newDecay < 0){
$world->setBlock($this->position, VanillaBlocks::AIR());
return;
}
@@ -312,7 +312,7 @@ abstract class Liquid extends Transparent{
$this->flowIntoBlock($bottomBlock, 0, true);
- if($this->isSource() or !$bottomBlock->canBeFlowedInto()){
+ if($this->isSource() || !$bottomBlock->canBeFlowedInto()){
if($this->falling){
$adjacentDecay = 1; //falling liquid behaves like source block
}else{
@@ -331,7 +331,7 @@ abstract class Liquid extends Transparent{
}
protected function flowIntoBlock(Block $block, int $newFlowDecay, bool $falling) : void{
- if($this->canFlowInto($block) and !($block instanceof Liquid)){
+ if($this->canFlowInto($block) && !($block instanceof Liquid)){
$new = clone $this;
$new->falling = $falling;
$new->decay = $falling ? 0 : $newFlowDecay;
@@ -350,7 +350,7 @@ abstract class Liquid extends Transparent{
/** @phpstan-impure */
private function getSmallestFlowDecay(Block $block, int $decay) : int{
- if(!($block instanceof Liquid) or !$block->isSameType($this)){
+ if(!($block instanceof Liquid) || !$block->isSameType($this)){
return $decay;
}
@@ -381,8 +381,8 @@ abstract class Liquid extends Transparent{
protected function canFlowInto(Block $block) : bool{
return
- $this->position->getWorld()->isInWorld($block->position->x, $block->position->y, $block->position->z) and
- $block->canBeFlowedInto() and
- !($block instanceof Liquid and $block->isSource()); //TODO: I think this should only be liquids of the same type
+ $this->position->getWorld()->isInWorld($block->position->x, $block->position->y, $block->position->z) &&
+ $block->canBeFlowedInto() &&
+ !($block instanceof Liquid && $block->isSource()); //TODO: I think this should only be liquids of the same type
}
}
diff --git a/src/block/Magma.php b/src/block/Magma.php
index e698a6431..fbff583aa 100644
--- a/src/block/Magma.php
+++ b/src/block/Magma.php
@@ -39,7 +39,7 @@ class Magma extends Opaque{
}
public function onEntityInside(Entity $entity) : bool{
- if($entity instanceof Living and !$entity->isSneaking()){
+ if($entity instanceof Living && !$entity->isSneaking()){
$ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, 1);
$entity->attack($ev);
}
diff --git a/src/block/NetherPortal.php b/src/block/NetherPortal.php
index b8c73d0a7..5ba1f0d41 100644
--- a/src/block/NetherPortal.php
+++ b/src/block/NetherPortal.php
@@ -53,7 +53,7 @@ class NetherPortal extends Transparent{
* @return $this
*/
public function setAxis(int $axis) : self{
- if($axis !== Axis::X and $axis !== Axis::Z){
+ if($axis !== Axis::X && $axis !== Axis::Z){
throw new \InvalidArgumentException("Invalid axis");
}
$this->axis = $axis;
diff --git a/src/block/NetherWartPlant.php b/src/block/NetherWartPlant.php
index 5809c8a4e..c6b441915 100644
--- a/src/block/NetherWartPlant.php
+++ b/src/block/NetherWartPlant.php
@@ -79,7 +79,7 @@ class NetherWartPlant extends Flowable{
}
public function onRandomTick() : void{
- if($this->age < 3 and mt_rand(0, 10) === 0){ //Still growing
+ if($this->age < 3 && mt_rand(0, 10) === 0){ //Still growing
$block = clone $this;
$block->age++;
$ev = new BlockGrowEvent($this, $block);
diff --git a/src/block/Note.php b/src/block/Note.php
index d8ff22402..1dc24733a 100644
--- a/src/block/Note.php
+++ b/src/block/Note.php
@@ -59,7 +59,7 @@ class Note extends Opaque{
/** @return $this */
public function setPitch(int $pitch) : self{
- if($pitch < self::MIN_PITCH or $pitch > self::MAX_PITCH){
+ if($pitch < self::MIN_PITCH || $pitch > self::MAX_PITCH){
throw new \InvalidArgumentException("Pitch must be in range " . self::MIN_PITCH . " - " . self::MAX_PITCH);
}
$this->pitch = $pitch;
diff --git a/src/block/Podzol.php b/src/block/Podzol.php
index 8da535c54..0eeba12c8 100644
--- a/src/block/Podzol.php
+++ b/src/block/Podzol.php
@@ -23,6 +23,13 @@ declare(strict_types=1);
namespace pocketmine\block;
+use pocketmine\item\Item;
+
class Podzol extends Opaque{
+ public function getDropsForCompatibleTool(Item $item) : array{
+ return [
+ VanillaBlocks::DIRT()->asItem()
+ ];
+ }
}
diff --git a/src/block/Pumpkin.php b/src/block/Pumpkin.php
new file mode 100644
index 000000000..835c68f31
--- /dev/null
+++ b/src/block/Pumpkin.php
@@ -0,0 +1,45 @@
+applyDamage(1);
+ $this->position->getWorld()->setBlock($this->position, VanillaBlocks::CARVED_PUMPKIN()->setFacing($face));
+ $this->position->getWorld()->dropItem($this->position->add(0.5, 0.5, 0.5), VanillaItems::PUMPKIN_SEEDS()->setCount(1));
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/block/RedstoneComparator.php b/src/block/RedstoneComparator.php
index 764676953..95471bd98 100644
--- a/src/block/RedstoneComparator.php
+++ b/src/block/RedstoneComparator.php
@@ -57,7 +57,7 @@ class RedstoneComparator extends Flowable{
public function readStateFromData(int $id, int $stateMeta) : void{
$this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03);
$this->isSubtractMode = ($stateMeta & BlockLegacyMetadata::REDSTONE_COMPARATOR_FLAG_SUBTRACT) !== 0;
- $this->powered = ($id === $this->idInfoFlattened->getSecondId() or ($stateMeta & BlockLegacyMetadata::REDSTONE_COMPARATOR_FLAG_POWERED) !== 0);
+ $this->powered = ($id === $this->idInfoFlattened->getSecondId() || ($stateMeta & BlockLegacyMetadata::REDSTONE_COMPARATOR_FLAG_POWERED) !== 0);
}
public function writeStateToMeta() : int{
diff --git a/src/block/Sapling.php b/src/block/Sapling.php
index cb9d36b02..9709032c5 100644
--- a/src/block/Sapling.php
+++ b/src/block/Sapling.php
@@ -68,7 +68,7 @@ class Sapling extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$down = $this->getSide(Facing::DOWN);
- if($down->getId() === BlockLegacyIds::GRASS or $down->getId() === BlockLegacyIds::DIRT or $down->getId() === BlockLegacyIds::FARMLAND){
+ if($down->getId() === BlockLegacyIds::GRASS || $down->getId() === BlockLegacyIds::DIRT || $down->getId() === BlockLegacyIds::FARMLAND){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
@@ -76,9 +76,7 @@ class Sapling extends Flowable{
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($item instanceof Fertilizer){
- $this->grow();
-
+ if($item instanceof Fertilizer && $this->grow($player)){
$item->pop();
return true;
@@ -98,9 +96,9 @@ class Sapling extends Flowable{
}
public function onRandomTick() : void{
- if($this->position->getWorld()->getFullLightAt($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) >= 8 and mt_rand(1, 7) === 1){
+ if($this->position->getWorld()->getFullLightAt($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) >= 8 && mt_rand(1, 7) === 1){
if($this->ready){
- $this->grow();
+ $this->grow(null);
}else{
$this->ready = true;
$this->position->getWorld()->setBlock($this->position, $this);
@@ -108,21 +106,20 @@ class Sapling extends Flowable{
}
}
- private function grow() : void{
+ private function grow(?Player $player) : bool{
$random = new Random(mt_rand());
$tree = TreeFactory::get($random, $this->treeType);
$transaction = $tree?->getBlockTransaction($this->position->getWorld(), $this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ(), $random);
if($transaction === null){
- return;
+ return false;
}
- $ev = new StructureGrowEvent($this, $transaction);
+ $ev = new StructureGrowEvent($this, $transaction, $player);
$ev->call();
- if($ev->isCancelled()){
- return;
+ if(!$ev->isCancelled()){
+ return $transaction->apply();
}
-
- $transaction->apply();
+ return false;
}
public function getFuelTime() : int{
diff --git a/src/block/SeaPickle.php b/src/block/SeaPickle.php
index 9110dc22c..fb931f004 100644
--- a/src/block/SeaPickle.php
+++ b/src/block/SeaPickle.php
@@ -83,12 +83,12 @@ class SeaPickle extends Transparent{
public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{
//TODO: proper placement logic (needs a supporting face below)
- return ($blockReplace instanceof SeaPickle and $blockReplace->count < 4) or parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock);
+ return ($blockReplace instanceof SeaPickle && $blockReplace->count < 4) || parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock);
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$this->underwater = false; //TODO: implement this once we have new water logic in place
- if($blockReplace instanceof SeaPickle and $blockReplace->count < 4){
+ if($blockReplace instanceof SeaPickle && $blockReplace->count < 4){
$this->count = $blockReplace->count + 1;
}
diff --git a/src/block/ShulkerBox.php b/src/block/ShulkerBox.php
index a81e5009d..18cd38ff8 100644
--- a/src/block/ShulkerBox.php
+++ b/src/block/ShulkerBox.php
@@ -91,7 +91,7 @@ class ShulkerBox extends Opaque{
$shulker = $this->position->getWorld()->getTile($this->position);
if($shulker instanceof TileShulkerBox){
if(
- $this->getSide($this->facing)->getId() !== BlockLegacyIds::AIR or
+ $this->getSide($this->facing)->getId() !== BlockLegacyIds::AIR ||
!$shulker->canOpenWith($item->getCustomName())
){
return true;
diff --git a/src/block/Skull.php b/src/block/Skull.php
index 6ee2e77f4..b403924cf 100644
--- a/src/block/Skull.php
+++ b/src/block/Skull.php
@@ -124,8 +124,14 @@ class Skull extends Flowable{
* @return AxisAlignedBB[]
*/
protected function recalculateCollisionBoxes() : array{
- //TODO: different bounds depending on attached face
- return [AxisAlignedBB::one()->contract(0.25, 0, 0.25)->trim(Facing::UP, 0.5)];
+ $collisionBox = AxisAlignedBB::one()->contract(0.25, 0, 0.25)->trim(Facing::UP, 0.5);
+ return match($this->facing){
+ Facing::NORTH => [$collisionBox->offset(0, 0.25, 0.25)],
+ Facing::SOUTH => [$collisionBox->offset(0, 0.25, -0.25)],
+ Facing::WEST => [$collisionBox->offset(0.25, 0.25, 0)],
+ Facing::EAST => [$collisionBox->offset(-0.25, 0.25, 0)],
+ default => [$collisionBox]
+ };
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
@@ -134,7 +140,7 @@ class Skull extends Flowable{
}
$this->facing = $face;
- if($player !== null and $face === Facing::UP){
+ if($player !== null && $face === Facing::UP){
$this->rotation = ((int) floor(($player->getLocation()->getYaw() * 16 / 360) + 0.5)) & 0xf;
}
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
diff --git a/src/block/Slab.php b/src/block/Slab.php
index 610c6ee8c..1bddeba94 100644
--- a/src/block/Slab.php
+++ b/src/block/Slab.php
@@ -90,11 +90,11 @@ class Slab extends Transparent{
return true;
}
- if($blockReplace instanceof Slab and !$blockReplace->slabType->equals(SlabType::DOUBLE()) and $blockReplace->isSameType($this)){
+ if($blockReplace instanceof Slab && !$blockReplace->slabType->equals(SlabType::DOUBLE()) && $blockReplace->isSameType($this)){
if($blockReplace->slabType->equals(SlabType::TOP())){ //Trying to combine with top slab
- return $clickVector->y <= 0.5 or (!$isClickedBlock and $face === Facing::UP);
+ return $clickVector->y <= 0.5 || (!$isClickedBlock && $face === Facing::UP);
}else{
- return $clickVector->y >= 0.5 or (!$isClickedBlock and $face === Facing::DOWN);
+ return $clickVector->y >= 0.5 || (!$isClickedBlock && $face === Facing::DOWN);
}
}
@@ -102,9 +102,9 @@ class Slab extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($blockReplace instanceof Slab and !$blockReplace->slabType->equals(SlabType::DOUBLE()) and $blockReplace->isSameType($this) and (
- ($blockReplace->slabType->equals(SlabType::TOP()) and ($clickVector->y <= 0.5 or $face === Facing::UP)) or
- ($blockReplace->slabType->equals(SlabType::BOTTOM()) and ($clickVector->y >= 0.5 or $face === Facing::DOWN))
+ if($blockReplace instanceof Slab && !$blockReplace->slabType->equals(SlabType::DOUBLE()) && $blockReplace->isSameType($this) && (
+ ($blockReplace->slabType->equals(SlabType::TOP()) && ($clickVector->y <= 0.5 || $face === Facing::UP)) ||
+ ($blockReplace->slabType->equals(SlabType::BOTTOM()) && ($clickVector->y >= 0.5 || $face === Facing::DOWN))
)){
//Clicked in empty half of existing slab
$this->slabType = SlabType::DOUBLE();
diff --git a/src/block/SnowLayer.php b/src/block/SnowLayer.php
index c901e9d0c..015733a76 100644
--- a/src/block/SnowLayer.php
+++ b/src/block/SnowLayer.php
@@ -26,6 +26,7 @@ namespace pocketmine\block;
use pocketmine\block\utils\BlockDataSerializer;
use pocketmine\block\utils\Fallable;
use pocketmine\block\utils\FallableTrait;
+use pocketmine\event\block\BlockMeltEvent;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\math\AxisAlignedBB;
@@ -77,7 +78,7 @@ class SnowLayer extends Flowable implements Fallable{
}
private function canBeSupportedBy(Block $b) : bool{
- return $b->isSolid() or ($b instanceof SnowLayer and $b->isSameType($this) and $b->layers === 8);
+ return $b->isSolid() || ($b instanceof SnowLayer && $b->isSameType($this) && $b->layers === 8);
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
@@ -100,7 +101,11 @@ class SnowLayer extends Flowable implements Fallable{
public function onRandomTick() : void{
if($this->position->getWorld()->getBlockLightAt($this->position->x, $this->position->y, $this->position->z) >= 12){
- $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR(), false);
+ $ev = new BlockMeltEvent($this, VanillaBlocks::AIR());
+ $ev->call();
+ if(!$ev->isCancelled()){
+ $this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ }
}
}
diff --git a/src/block/Stair.php b/src/block/Stair.php
index b4dd2c65a..39bf4ab3a 100644
--- a/src/block/Stair.php
+++ b/src/block/Stair.php
@@ -96,9 +96,9 @@ class Stair extends Transparent{
->trim(Facing::opposite($topStepFace), 0.5)
->trim(Facing::opposite($this->facing), 0.5);
- if($this->shape->equals(StairShape::OUTER_LEFT()) or $this->shape->equals(StairShape::OUTER_RIGHT())){
+ if($this->shape->equals(StairShape::OUTER_LEFT()) || $this->shape->equals(StairShape::OUTER_RIGHT())){
$topStep->trim(Facing::rotateY($this->facing, $this->shape->equals(StairShape::OUTER_LEFT())), 0.5);
- }elseif($this->shape->equals(StairShape::INNER_LEFT()) or $this->shape->equals(StairShape::INNER_RIGHT())){
+ }elseif($this->shape->equals(StairShape::INNER_LEFT()) || $this->shape->equals(StairShape::INNER_RIGHT())){
//add an extra cube
$bbs[] = AxisAlignedBB::one()
->trim(Facing::opposite($topStepFace), 0.5)
@@ -114,8 +114,8 @@ class Stair extends Transparent{
private function getPossibleCornerFacing(bool $oppositeFacing) : ?int{
$side = $this->getSide($oppositeFacing ? Facing::opposite($this->facing) : $this->facing);
return (
- $side instanceof Stair and
- $side->upsideDown === $this->upsideDown and
+ $side instanceof Stair &&
+ $side->upsideDown === $this->upsideDown &&
Facing::axis($side->facing) !== Facing::axis($this->facing) //perpendicular
) ? $side->facing : null;
}
@@ -124,7 +124,7 @@ class Stair extends Transparent{
if($player !== null){
$this->facing = $player->getHorizontalFacing();
}
- $this->upsideDown = (($clickVector->y > 0.5 and $face !== Facing::UP) or $face === Facing::DOWN);
+ $this->upsideDown = (($clickVector->y > 0.5 && $face !== Facing::UP) || $face === Facing::DOWN);
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
diff --git a/src/block/Stem.php b/src/block/Stem.php
index 5904736f7..e223b18be 100644
--- a/src/block/Stem.php
+++ b/src/block/Stem.php
@@ -53,7 +53,7 @@ abstract class Stem extends Crops{
$side = $this->getSide(Facing::HORIZONTAL[array_rand(Facing::HORIZONTAL)]);
$d = $side->getSide(Facing::DOWN);
- if($side->getId() === BlockLegacyIds::AIR and ($d->getId() === BlockLegacyIds::FARMLAND or $d->getId() === BlockLegacyIds::GRASS or $d->getId() === BlockLegacyIds::DIRT)){
+ if($side->getId() === BlockLegacyIds::AIR && ($d->getId() === BlockLegacyIds::FARMLAND || $d->getId() === BlockLegacyIds::GRASS || $d->getId() === BlockLegacyIds::DIRT)){
$ev = new BlockGrowEvent($side, $grow);
$ev->call();
if(!$ev->isCancelled()){
diff --git a/src/block/Sugarcane.php b/src/block/Sugarcane.php
index eadf9c1ff..d2c13308a 100644
--- a/src/block/Sugarcane.php
+++ b/src/block/Sugarcane.php
@@ -48,7 +48,8 @@ class Sugarcane extends Flowable{
return 0b1111;
}
- private function grow() : void{
+ private function grow() : bool{
+ $grew = false;
for($y = 1; $y < 3; ++$y){
if(!$this->position->getWorld()->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){
break;
@@ -61,12 +62,14 @@ class Sugarcane extends Flowable{
break;
}
$this->position->getWorld()->setBlock($b->position, $ev->getNewState());
+ $grew = true;
}else{
break;
}
}
$this->age = 0;
$this->position->getWorld()->setBlock($this->position, $this);
+ return $grew;
}
public function getAge() : int{ return $this->age; }
@@ -82,12 +85,10 @@ class Sugarcane extends Flowable{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($item instanceof Fertilizer){
- if(!$this->getSide(Facing::DOWN)->isSameType($this)){
- $this->grow();
+ if(!$this->getSide(Facing::DOWN)->isSameType($this) && $this->grow()){
+ $item->pop();
}
- $item->pop();
-
return true;
}
@@ -96,7 +97,7 @@ class Sugarcane extends Flowable{
public function onNearbyBlockChange() : void{
$down = $this->getSide(Facing::DOWN);
- if($down->isTransparent() and !$down->isSameType($this)){
+ if($down->isTransparent() && !$down->isSameType($this)){
$this->position->getWorld()->useBreakOn($this->position);
}
}
@@ -120,7 +121,7 @@ class Sugarcane extends Flowable{
$down = $this->getSide(Facing::DOWN);
if($down->isSameType($this)){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
- }elseif($down->getId() === BlockLegacyIds::GRASS or $down->getId() === BlockLegacyIds::DIRT or $down->getId() === BlockLegacyIds::SAND or $down->getId() === BlockLegacyIds::PODZOL){
+ }elseif($down->getId() === BlockLegacyIds::GRASS || $down->getId() === BlockLegacyIds::DIRT || $down->getId() === BlockLegacyIds::SAND || $down->getId() === BlockLegacyIds::PODZOL){
foreach(Facing::HORIZONTAL as $side){
if($down->getSide($side) instanceof Water){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
diff --git a/src/block/SweetBerryBush.php b/src/block/SweetBerryBush.php
index efb35845e..4983bfea4 100644
--- a/src/block/SweetBerryBush.php
+++ b/src/block/SweetBerryBush.php
@@ -99,9 +99,9 @@ class SweetBerryBush extends Flowable{
if(!$ev->isCancelled()){
$this->position->getWorld()->setBlock($this->position, $ev->getNewState());
+ $item->pop();
}
- $item->pop();
}elseif(($dropAmount = $this->getBerryDropAmount()) > 0){
$this->position->getWorld()->setBlock($this->position, $this->setAge(self::STAGE_BUSH_NO_BERRIES));
$this->position->getWorld()->dropItem($this->position, $this->asItem()->setCount($dropAmount));
@@ -135,7 +135,7 @@ class SweetBerryBush extends Flowable{
}
public function onRandomTick() : void{
- if($this->age < self::STAGE_MATURE and mt_rand(0, 2) === 1){
+ if($this->age < self::STAGE_MATURE && mt_rand(0, 2) === 1){
$block = clone $this;
++$block->age;
$ev = new BlockGrowEvent($this, $block);
diff --git a/src/block/TNT.php b/src/block/TNT.php
index 27383e996..5af0b5267 100644
--- a/src/block/TNT.php
+++ b/src/block/TNT.php
@@ -86,7 +86,7 @@ class TNT extends Opaque{
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($item instanceof FlintSteel or $item->hasEnchantment(VanillaEnchantments::FIRE_ASPECT())){
+ if($item instanceof FlintSteel || $item->hasEnchantment(VanillaEnchantments::FIRE_ASPECT())){
if($item instanceof Durable){
$item->applyDamage(1);
}
@@ -102,7 +102,7 @@ class TNT extends Opaque{
}
public function onEntityInside(Entity $entity) : bool{
- if($entity instanceof Arrow and $entity->isOnFire()){
+ if($entity instanceof Arrow && $entity->isOnFire()){
$this->ignite();
return false;
}
diff --git a/src/block/TallGrass.php b/src/block/TallGrass.php
index 7f5daf731..b77a828fe 100644
--- a/src/block/TallGrass.php
+++ b/src/block/TallGrass.php
@@ -39,7 +39,7 @@ class TallGrass extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$down = $this->getSide(Facing::DOWN)->getId();
- if($down === BlockLegacyIds::GRASS or $down === BlockLegacyIds::DIRT){
+ if($down === BlockLegacyIds::GRASS || $down === BlockLegacyIds::DIRT){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
diff --git a/src/block/Thin.php b/src/block/Thin.php
index 3f3dc86e7..567e4e86c 100644
--- a/src/block/Thin.php
+++ b/src/block/Thin.php
@@ -37,7 +37,7 @@ class Thin extends Transparent{
foreach(Facing::HORIZONTAL as $facing){
$side = $this->getSide($facing);
- if($side instanceof Thin or $side->isFullCube()){
+ if($side instanceof Thin || $side->isFullCube()){
$this->connections[$facing] = true;
}else{
unset($this->connections[$facing]);
@@ -51,7 +51,7 @@ class Thin extends Transparent{
/** @var AxisAlignedBB[] $bbs */
$bbs = [];
- if(isset($this->connections[Facing::WEST]) or isset($this->connections[Facing::EAST])){
+ if(isset($this->connections[Facing::WEST]) || isset($this->connections[Facing::EAST])){
$bb = AxisAlignedBB::one()->squash(Axis::Z, $inset);
if(!isset($this->connections[Facing::WEST])){
@@ -62,7 +62,7 @@ class Thin extends Transparent{
$bbs[] = $bb;
}
- if(isset($this->connections[Facing::NORTH]) or isset($this->connections[Facing::SOUTH])){
+ if(isset($this->connections[Facing::NORTH]) || isset($this->connections[Facing::SOUTH])){
$bb = AxisAlignedBB::one()->squash(Axis::X, $inset);
if(!isset($this->connections[Facing::NORTH])){
diff --git a/src/block/Torch.php b/src/block/Torch.php
index 292bfbb18..cb5bf8fb8 100644
--- a/src/block/Torch.php
+++ b/src/block/Torch.php
@@ -66,16 +66,16 @@ class Torch extends Flowable{
$below = $this->getSide(Facing::DOWN);
$face = Facing::opposite($this->facing);
- if($this->getSide($face)->isTransparent() and !($face === Facing::DOWN and ($below->getId() === BlockLegacyIds::FENCE or $below->getId() === BlockLegacyIds::COBBLESTONE_WALL))){
+ if($this->getSide($face)->isTransparent() && !($face === Facing::DOWN && ($below->getId() === BlockLegacyIds::FENCE || $below->getId() === BlockLegacyIds::COBBLESTONE_WALL))){
$this->position->getWorld()->useBreakOn($this->position);
}
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if($blockClicked->canBeReplaced() and !$blockClicked->getSide(Facing::DOWN)->isTransparent()){
+ if($blockClicked->canBeReplaced() && !$blockClicked->getSide(Facing::DOWN)->isTransparent()){
$this->facing = Facing::UP;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
- }elseif($face !== Facing::DOWN and (!$blockClicked->isTransparent() or ($face === Facing::UP and ($blockClicked->getId() === BlockLegacyIds::FENCE or $blockClicked->getId() === BlockLegacyIds::COBBLESTONE_WALL)))){
+ }elseif($face !== Facing::DOWN && (!$blockClicked->isTransparent() || ($face === Facing::UP && ($blockClicked->getId() === BlockLegacyIds::FENCE || $blockClicked->getId() === BlockLegacyIds::COBBLESTONE_WALL)))){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}else{
diff --git a/src/block/Trapdoor.php b/src/block/Trapdoor.php
index 6f9d7b34c..08cb4b200 100644
--- a/src/block/Trapdoor.php
+++ b/src/block/Trapdoor.php
@@ -82,7 +82,7 @@ class Trapdoor extends Transparent{
if($player !== null){
$this->facing = Facing::opposite($player->getHorizontalFacing());
}
- if(($clickVector->y > 0.5 and $face !== Facing::UP) or $face === Facing::DOWN){
+ if(($clickVector->y > 0.5 && $face !== Facing::UP) || $face === Facing::DOWN){
$this->top = true;
}
diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php
index a1b39b023..7d1c4a1e6 100644
--- a/src/block/VanillaBlocks.php
+++ b/src/block/VanillaBlocks.php
@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\utils\CloningRegistryTrait;
-use function assert;
/**
* This doc-block is generated automatically, do not modify it manually.
@@ -300,6 +299,7 @@ use function assert;
* @method static Farmland FARMLAND()
* @method static TallGrass FERN()
* @method static Fire FIRE()
+ * @method static FletchingTable FLETCHING_TABLE()
* @method static FlowerPot FLOWER_POT()
* @method static FrostedIce FROSTED_ICE()
* @method static Furnace FURNACE()
@@ -363,6 +363,7 @@ use function assert;
* @method static LapisOre LAPIS_LAZULI_ORE()
* @method static DoubleTallGrass LARGE_FERN()
* @method static Lava LAVA()
+ * @method static Lectern LECTERN()
* @method static Opaque LEGACY_STONECUTTER()
* @method static Lever LEVER()
* @method static GlazedTerracotta LIGHT_BLUE_GLAZED_TERRACOTTA()
@@ -445,7 +446,7 @@ use function assert;
* @method static Slab PRISMARINE_SLAB()
* @method static Stair PRISMARINE_STAIRS()
* @method static Wall PRISMARINE_WALL()
- * @method static Opaque PUMPKIN()
+ * @method static Pumpkin PUMPKIN()
* @method static PumpkinStem PUMPKIN_STEM()
* @method static GlazedTerracotta PURPLE_GLAZED_TERRACOTTA()
* @method static Torch PURPLE_TORCH()
@@ -580,12 +581,6 @@ final class VanillaBlocks{
self::_registryRegister($name, $block);
}
- public static function fromString(string $name) : Block{
- $result = self::_registryFromString($name);
- assert($result instanceof Block);
- return $result;
- }
-
/**
* @return Block[]
*/
@@ -866,6 +861,7 @@ final class VanillaBlocks{
self::register("farmland", $factory->get(60, 0));
self::register("fern", $factory->get(31, 2));
self::register("fire", $factory->get(51, 0));
+ self::register("fletching_table", $factory->get(456, 0));
self::register("flower_pot", $factory->get(140, 0));
self::register("frosted_ice", $factory->get(207, 0));
self::register("furnace", $factory->get(61, 2));
@@ -929,6 +925,7 @@ final class VanillaBlocks{
self::register("lapis_lazuli_ore", $factory->get(21, 0));
self::register("large_fern", $factory->get(175, 3));
self::register("lava", $factory->get(10, 0));
+ self::register("lectern", $factory->get(449, 0));
self::register("legacy_stonecutter", $factory->get(245, 0));
self::register("lever", $factory->get(69, 0));
self::register("light_blue_glazed_terracotta", $factory->get(223, 2));
diff --git a/src/block/Vine.php b/src/block/Vine.php
index b9331bc40..16f684846 100644
--- a/src/block/Vine.php
+++ b/src/block/Vine.php
@@ -116,7 +116,7 @@ class Vine extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
- if(!$blockClicked->isSolid() or Facing::axis($face) === Axis::Y){
+ if(!$blockClicked->isSolid() || Facing::axis($face) === Axis::Y){
return false;
}
@@ -134,7 +134,7 @@ class Vine extends Flowable{
$supportedFaces = $up instanceof Vine ? array_intersect_key($this->faces, $up->faces) : [];
foreach($this->faces as $face){
- if(!isset($supportedFaces[$face]) and !$this->getSide($face)->isSolid()){
+ if(!isset($supportedFaces[$face]) && !$this->getSide($face)->isSolid()){
unset($this->faces[$face]);
$changed = true;
}
diff --git a/src/block/Wall.php b/src/block/Wall.php
index 5a1323d14..6c827d190 100644
--- a/src/block/Wall.php
+++ b/src/block/Wall.php
@@ -37,7 +37,7 @@ class Wall extends Transparent{
foreach(Facing::HORIZONTAL as $facing){
$block = $this->getSide($facing);
- if($block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent())){
+ if($block instanceof static || $block instanceof FenceGate || ($block->isSolid() && !$block->isTransparent())){
$this->connections[$facing] = $facing;
}else{
unset($this->connections[$facing]);
@@ -57,10 +57,10 @@ class Wall extends Transparent{
$inset = 0.25;
if(
- !$this->up and //if there is a block on top, it stays as a post
+ !$this->up && //if there is a block on top, it stays as a post
(
- ($north and $south and !$west and !$east) or
- (!$north and !$south and $west and $east)
+ ($north && $south && !$west && !$east) ||
+ (!$north && !$south && $west && $east)
)
){
//If connected to two sides on the same axis but not any others, AND there is not a block on top, there is no post and the wall is thinner
diff --git a/src/block/inventory/AnimatedBlockInventoryTrait.php b/src/block/inventory/AnimatedBlockInventoryTrait.php
index e0bb9fee0..c80fced3a 100644
--- a/src/block/inventory/AnimatedBlockInventoryTrait.php
+++ b/src/block/inventory/AnimatedBlockInventoryTrait.php
@@ -47,7 +47,7 @@ trait AnimatedBlockInventoryTrait{
public function onOpen(Player $who) : void{
parent::onOpen($who);
- if($this->getHolder()->isValid() and $this->getViewerCount() === 1){
+ if($this->getHolder()->isValid() && $this->getViewerCount() === 1){
//TODO: this crap really shouldn't be managed by the inventory
$this->animateBlock(true);
$this->getHolder()->getWorld()->addSound($this->getHolder()->add(0.5, 0.5, 0.5), $this->getOpenSound());
@@ -57,7 +57,7 @@ trait AnimatedBlockInventoryTrait{
abstract protected function animateBlock(bool $isOpen) : void;
public function onClose(Player $who) : void{
- if($this->getHolder()->isValid() and $this->getViewerCount() === 1){
+ if($this->getHolder()->isValid() && $this->getViewerCount() === 1){
//TODO: this crap really shouldn't be managed by the inventory
$this->animateBlock(false);
$this->getHolder()->getWorld()->addSound($this->getHolder()->add(0.5, 0.5, 0.5), $this->getCloseSound());
diff --git a/src/block/inventory/AnvilInventory.php b/src/block/inventory/AnvilInventory.php
index 2e875d3ff..1def8f913 100644
--- a/src/block/inventory/AnvilInventory.php
+++ b/src/block/inventory/AnvilInventory.php
@@ -24,10 +24,10 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
-use pocketmine\player\Player;
+use pocketmine\inventory\TemporaryInventory;
use pocketmine\world\Position;
-class AnvilInventory extends SimpleInventory implements BlockInventory{
+class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
public const SLOT_INPUT = 0;
@@ -37,13 +37,4 @@ class AnvilInventory extends SimpleInventory implements BlockInventory{
$this->holder = $holder;
parent::__construct(2);
}
-
- public function onClose(Player $who) : void{
- parent::onClose($who);
-
- foreach($this->getContents() as $item){
- $who->dropItem($item);
- }
- $this->clearAll();
- }
}
diff --git a/src/block/inventory/ChestInventory.php b/src/block/inventory/ChestInventory.php
index 1f8538e4b..b7594b232 100644
--- a/src/block/inventory/ChestInventory.php
+++ b/src/block/inventory/ChestInventory.php
@@ -25,6 +25,7 @@ namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\world\Position;
use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
@@ -50,6 +51,6 @@ class ChestInventory extends SimpleInventory implements BlockInventory{
$holder = $this->getHolder();
//event ID is always 1 for a chest
- $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(1, $isOpen ? 1 : 0, $holder->asVector3()));
+ $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
}
diff --git a/src/block/inventory/CraftingTableInventory.php b/src/block/inventory/CraftingTableInventory.php
new file mode 100644
index 000000000..3659a1f5d
--- /dev/null
+++ b/src/block/inventory/CraftingTableInventory.php
@@ -0,0 +1,37 @@
+holder = $holder;
+ parent::__construct(CraftingGrid::SIZE_BIG);
+ }
+}
diff --git a/src/block/inventory/EnchantInventory.php b/src/block/inventory/EnchantInventory.php
index aafc90caf..91bfcab5e 100644
--- a/src/block/inventory/EnchantInventory.php
+++ b/src/block/inventory/EnchantInventory.php
@@ -24,10 +24,10 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
-use pocketmine\player\Player;
+use pocketmine\inventory\TemporaryInventory;
use pocketmine\world\Position;
-class EnchantInventory extends SimpleInventory implements BlockInventory{
+class EnchantInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
public const SLOT_INPUT = 0;
@@ -37,13 +37,4 @@ class EnchantInventory extends SimpleInventory implements BlockInventory{
$this->holder = $holder;
parent::__construct(2);
}
-
- public function onClose(Player $who) : void{
- parent::onClose($who);
-
- foreach($this->getContents() as $item){
- $who->dropItem($item);
- }
- $this->clearAll();
- }
}
diff --git a/src/block/inventory/EnderChestInventory.php b/src/block/inventory/EnderChestInventory.php
index 6887219c6..b9ff1832e 100644
--- a/src/block/inventory/EnderChestInventory.php
+++ b/src/block/inventory/EnderChestInventory.php
@@ -28,6 +28,7 @@ use pocketmine\inventory\DelegateInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\PlayerEnderInventory;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\player\Player;
use pocketmine\world\Position;
use pocketmine\world\sound\EnderChestCloseSound;
@@ -74,7 +75,7 @@ class EnderChestInventory extends DelegateInventory implements BlockInventory{
$holder = $this->getHolder();
//event ID is always 1 for a chest
- $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(1, $isOpen ? 1 : 0, $holder->asVector3()));
+ $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
public function onClose(Player $who) : void{
diff --git a/src/block/inventory/LoomInventory.php b/src/block/inventory/LoomInventory.php
index 24830329a..27a8f2dbc 100644
--- a/src/block/inventory/LoomInventory.php
+++ b/src/block/inventory/LoomInventory.php
@@ -24,10 +24,10 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
-use pocketmine\player\Player;
+use pocketmine\inventory\TemporaryInventory;
use pocketmine\world\Position;
-final class LoomInventory extends SimpleInventory implements BlockInventory{
+final class LoomInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
public const SLOT_BANNER = 0;
@@ -38,13 +38,4 @@ final class LoomInventory extends SimpleInventory implements BlockInventory{
$this->holder = $holder;
parent::__construct($size);
}
-
- public function onClose(Player $who) : void{
- parent::onClose($who);
-
- foreach($this->getContents() as $item){
- $who->dropItem($item);
- }
- $this->clearAll();
- }
}
diff --git a/src/block/inventory/ShulkerBoxInventory.php b/src/block/inventory/ShulkerBoxInventory.php
index 3e4edcdd7..cace49652 100644
--- a/src/block/inventory/ShulkerBoxInventory.php
+++ b/src/block/inventory/ShulkerBoxInventory.php
@@ -27,6 +27,7 @@ use pocketmine\block\BlockLegacyIds;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\world\Position;
use pocketmine\world\sound\ShulkerBoxCloseSound;
use pocketmine\world\sound\ShulkerBoxOpenSound;
@@ -59,6 +60,6 @@ class ShulkerBoxInventory extends SimpleInventory implements BlockInventory{
$holder = $this->getHolder();
//event ID is always 1 for a chest
- $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(1, $isOpen ? 1 : 0, $holder->asVector3()));
+ $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
}
diff --git a/src/block/tile/Bell.php b/src/block/tile/Bell.php
index b7e5eadde..bc2ab29fe 100644
--- a/src/block/tile/Bell.php
+++ b/src/block/tile/Bell.php
@@ -27,6 +27,7 @@ use pocketmine\block\utils\BlockDataSerializer;
use pocketmine\math\Facing;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\BlockActorDataPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
final class Bell extends Spawnable{
@@ -81,6 +82,6 @@ final class Bell extends Spawnable{
$nbt->setByte(self::TAG_RINGING, 1);
$nbt->setInt(self::TAG_DIRECTION, BlockDataSerializer::writeLegacyHorizontalFacing($bellHitFace));
$nbt->setInt(self::TAG_TICKS, 0);
- return BlockActorDataPacket::create($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ(), new CacheableNbt($nbt));
+ return BlockActorDataPacket::create(BlockPosition::fromVector3($this->position), new CacheableNbt($nbt));
}
}
diff --git a/src/block/tile/BrewingStand.php b/src/block/tile/BrewingStand.php
index 450a52125..e008c7fda 100644
--- a/src/block/tile/BrewingStand.php
+++ b/src/block/tile/BrewingStand.php
@@ -24,16 +24,29 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\BrewingStandInventory;
+use pocketmine\crafting\BrewingRecipe;
+use pocketmine\event\block\BrewingFuelUseEvent;
+use pocketmine\event\block\BrewItemEvent;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
+use pocketmine\item\Item;
+use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
+use pocketmine\network\mcpe\protocol\ContainerSetDataPacket;
+use pocketmine\player\Player;
+use pocketmine\world\sound\PotionFinishBrewingSound;
use pocketmine\world\World;
+use function array_map;
+use function count;
class BrewingStand extends Spawnable implements Container, Nameable{
-
+ use NameableTrait {
+ addAdditionalSpawnData as addNameSpawnData;
+ }
use ContainerTrait;
- use NameableTrait;
+
+ public const BREW_TIME_TICKS = 400; // Brew time in ticks
private const TAG_BREW_TIME = "BrewTime"; //TAG_Short
private const TAG_BREW_TIME_PE = "CookTime"; //TAG_Short
@@ -41,15 +54,11 @@ class BrewingStand extends Spawnable implements Container, Nameable{
private const TAG_REMAINING_FUEL_TIME = "Fuel"; //TAG_Byte
private const TAG_REMAINING_FUEL_TIME_PE = "FuelAmount"; //TAG_Short
- /** @var BrewingStandInventory */
- private $inventory;
+ private BrewingStandInventory $inventory;
- /** @var int */
- private $brewTime = 0;
- /** @var int */
- private $maxFuelTime = 0;
- /** @var int */
- private $remainingFuelTime = 0;
+ private int $brewTime = 0;
+ private int $maxFuelTime = 0;
+ private int $remainingFuelTime = 0;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
@@ -83,6 +92,14 @@ class BrewingStand extends Spawnable implements Container, Nameable{
$nbt->setShort(self::TAG_REMAINING_FUEL_TIME_PE, $this->remainingFuelTime);
}
+ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
+ $this->addNameSpawnData($nbt);
+
+ $nbt->setShort(self::TAG_BREW_TIME_PE, $this->brewTime);
+ $nbt->setShort(self::TAG_MAX_FUEL_TIME, $this->maxFuelTime);
+ $nbt->setShort(self::TAG_REMAINING_FUEL_TIME_PE, $this->remainingFuelTime);
+ }
+
public function getDefaultName() : string{
return "Brewing Stand";
}
@@ -108,4 +125,135 @@ class BrewingStand extends Spawnable implements Container, Nameable{
public function getRealInventory(){
return $this->inventory;
}
+
+ private function checkFuel(Item $item) : void{
+ $ev = new BrewingFuelUseEvent($this);
+ if(!$item->equals(VanillaItems::BLAZE_POWDER(), true, false)){
+ $ev->cancel();
+ }
+
+ $ev->call();
+ if($ev->isCancelled()){
+ return;
+ }
+
+ $item->pop();
+ $this->inventory->setItem(BrewingStandInventory::SLOT_FUEL, $item);
+
+ $this->maxFuelTime = $this->remainingFuelTime = $ev->getFuelTime();
+ }
+
+ /**
+ * @return BrewingRecipe[]
+ * @phpstan-return array
+ */
+ private function getBrewableRecipes() : array{
+ if($this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT)->isNull()){
+ return [];
+ }
+
+ $recipes = [];
+ foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){
+ $input = $this->inventory->getItem($slot);
+ if($input->isNull()){
+ continue;
+ }
+
+ if(($recipe = $this->position->getWorld()->getServer()->getCraftingManager()->matchBrewingRecipe($input, $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT))) !== null){
+ $recipes[$slot] = $recipe;
+ }
+ }
+
+ return $recipes;
+ }
+
+ public function onUpdate() : bool{
+ if($this->closed){
+ return false;
+ }
+
+ $this->timings->startTiming();
+
+ $prevBrewTime = $this->brewTime;
+ $prevRemainingFuelTime = $this->remainingFuelTime;
+ $prevMaxFuelTime = $this->maxFuelTime;
+
+ $ret = false;
+
+ $fuel = $this->inventory->getItem(BrewingStandInventory::SLOT_FUEL);
+ $ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT);
+
+ $recipes = $this->getBrewableRecipes();
+ $canBrew = count($recipes) !== 0;
+
+ if($this->remainingFuelTime <= 0 && $canBrew){
+ $this->checkFuel($fuel);
+ }
+
+ if($this->remainingFuelTime > 0){
+ if($canBrew){
+ if($this->brewTime === 0){
+ $this->brewTime = self::BREW_TIME_TICKS;
+ --$this->remainingFuelTime;
+ }
+
+ --$this->brewTime;
+
+ if($this->brewTime <= 0){
+ $anythingBrewed = false;
+ foreach($recipes as $slot => $recipe){
+ $input = $this->inventory->getItem($slot);
+ $output = $recipe->getResultFor($input);
+ if($output === null){
+ continue;
+ }
+
+ $ev = new BrewItemEvent($this, $slot, $input, $output, $recipe);
+ $ev->call();
+ if($ev->isCancelled()){
+ continue;
+ }
+
+ $this->inventory->setItem($slot, $ev->getResult());
+ $anythingBrewed = true;
+ }
+
+ if($anythingBrewed){
+ $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new PotionFinishBrewingSound());
+ }
+
+ $ingredient->pop();
+ $this->inventory->setItem(BrewingStandInventory::SLOT_INGREDIENT, $ingredient);
+
+ $this->brewTime = 0;
+ }else{
+ $ret = true;
+ }
+ }else{
+ $this->brewTime = 0;
+ }
+ }else{
+ $this->brewTime = $this->remainingFuelTime = $this->maxFuelTime = 0;
+ }
+
+ $viewers = array_map(fn(Player $p) => $p->getNetworkSession()->getInvManager(), $this->inventory->getViewers());
+ foreach($viewers as $v){
+ if($v === null){
+ continue;
+ }
+ if($prevBrewTime !== $this->brewTime){
+ $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_BREW_TIME, $this->brewTime);
+ }
+ if($prevRemainingFuelTime !== $this->remainingFuelTime){
+ $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_FUEL_AMOUNT, $this->remainingFuelTime);
+ }
+ if($prevMaxFuelTime !== $this->maxFuelTime){
+ $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_FUEL_TOTAL, $this->maxFuelTime);
+ }
+ }
+
+ $this->timings->stopTiming();
+
+ return $ret;
+ }
}
diff --git a/src/block/tile/Chest.php b/src/block/tile/Chest.php
index 67534f123..614d7f0cd 100644
--- a/src/block/tile/Chest.php
+++ b/src/block/tile/Chest.php
@@ -60,12 +60,12 @@ class Chest extends Spawnable implements Container, Nameable{
}
public function readSaveData(CompoundTag $nbt) : void{
- if(($pairXTag = $nbt->getTag(self::TAG_PAIRX)) instanceof IntTag and ($pairZTag = $nbt->getTag(self::TAG_PAIRZ)) instanceof IntTag){
+ if(($pairXTag = $nbt->getTag(self::TAG_PAIRX)) instanceof IntTag && ($pairZTag = $nbt->getTag(self::TAG_PAIRZ)) instanceof IntTag){
$pairX = $pairXTag->getValue();
$pairZ = $pairZTag->getValue();
if(
- ($this->position->x === $pairX and abs($this->position->z - $pairZ) === 1) or
- ($this->position->z === $pairZ and abs($this->position->x - $pairX) === 1)
+ ($this->position->x === $pairX && abs($this->position->z - $pairZ) === 1) ||
+ ($this->position->z === $pairZ && abs($this->position->x - $pairX) === 1)
){
$this->pairX = $pairX;
$this->pairZ = $pairZ;
@@ -100,7 +100,7 @@ class Chest extends Spawnable implements Container, Nameable{
$this->inventory->removeAllViewers();
if($this->doubleInventory !== null){
- if($this->isPaired() and $this->position->getWorld()->isChunkLoaded($this->pairX >> Chunk::COORD_BIT_SIZE, $this->pairZ >> Chunk::COORD_BIT_SIZE)){
+ if($this->isPaired() && $this->position->getWorld()->isChunkLoaded($this->pairX >> Chunk::COORD_BIT_SIZE, $this->pairZ >> Chunk::COORD_BIT_SIZE)){
$this->doubleInventory->removeAllViewers();
if(($pair = $this->getPair()) !== null){
$pair->doubleInventory = null;
@@ -122,7 +122,7 @@ class Chest extends Spawnable implements Container, Nameable{
* @return ChestInventory|DoubleChestInventory
*/
public function getInventory(){
- if($this->isPaired() and $this->doubleInventory === null){
+ if($this->isPaired() && $this->doubleInventory === null){
$this->checkPairing();
}
return $this->doubleInventory instanceof DoubleChestInventory ? $this->doubleInventory : $this->inventory;
@@ -136,7 +136,7 @@ class Chest extends Spawnable implements Container, Nameable{
}
protected function checkPairing() : void{
- if($this->isPaired() and !$this->position->getWorld()->isInLoadedTerrain(new Vector3($this->pairX, $this->position->y, $this->pairZ))){
+ if($this->isPaired() && !$this->position->getWorld()->isInLoadedTerrain(new Vector3($this->pairX, $this->position->y, $this->pairZ))){
//paired to a tile in an unloaded chunk
$this->doubleInventory = null;
@@ -167,7 +167,7 @@ class Chest extends Spawnable implements Container, Nameable{
}
public function isPaired() : bool{
- return $this->pairX !== null and $this->pairZ !== null;
+ return $this->pairX !== null && $this->pairZ !== null;
}
public function getPair() : ?Chest{
@@ -182,7 +182,7 @@ class Chest extends Spawnable implements Container, Nameable{
}
public function pairWith(Chest $tile) : bool{
- if($this->isPaired() or $tile->isPaired()){
+ if($this->isPaired() || $tile->isPaired()){
return false;
}
diff --git a/src/block/tile/ContainerTrait.php b/src/block/tile/ContainerTrait.php
index ae8cc8ea7..2abe134f2 100644
--- a/src/block/tile/ContainerTrait.php
+++ b/src/block/tile/ContainerTrait.php
@@ -48,11 +48,14 @@ trait ContainerTrait{
$inventory = $this->getRealInventory();
$listeners = $inventory->getListeners()->toArray();
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
- $inventory->clearAll();
+
+ $newContents = [];
/** @var CompoundTag $itemNBT */
foreach($inventoryTag as $itemNBT){
- $inventory->setItem($itemNBT->getByte("Slot"), Item::nbtDeserialize($itemNBT));
+ $newContents[$itemNBT->getByte("Slot")] = Item::nbtDeserialize($itemNBT);
}
+ $inventory->setContents($newContents);
+
$inventory->getListeners()->add(...$listeners);
}
@@ -78,7 +81,7 @@ trait ContainerTrait{
* @see Container::canOpenWith()
*/
public function canOpenWith(string $key) : bool{
- return $this->lock === null or $this->lock === $key;
+ return $this->lock === null || $this->lock === $key;
}
/**
diff --git a/src/block/tile/FlowerPot.php b/src/block/tile/FlowerPot.php
index f44e73e37..221453ae6 100644
--- a/src/block/tile/FlowerPot.php
+++ b/src/block/tile/FlowerPot.php
@@ -42,7 +42,7 @@ class FlowerPot extends Spawnable{
private $plant = null;
public function readSaveData(CompoundTag $nbt) : void{
- if(($itemIdTag = $nbt->getTag(self::TAG_ITEM)) instanceof ShortTag and ($itemMetaTag = $nbt->getTag(self::TAG_ITEM_DATA)) instanceof IntTag){
+ if(($itemIdTag = $nbt->getTag(self::TAG_ITEM)) instanceof ShortTag && ($itemMetaTag = $nbt->getTag(self::TAG_ITEM_DATA)) instanceof IntTag){
try{
$this->setPlant(BlockFactory::getInstance()->get($itemIdTag->getValue(), $itemMetaTag->getValue()));
}catch(\InvalidArgumentException $e){
@@ -65,7 +65,7 @@ class FlowerPot extends Spawnable{
}
public function setPlant(?Block $plant) : void{
- if($plant === null or $plant instanceof Air){
+ if($plant === null || $plant instanceof Air){
$this->plant = null;
}else{
$this->plant = clone $plant;
diff --git a/src/block/tile/Furnace.php b/src/block/tile/Furnace.php
index b527317d6..21b05f504 100644
--- a/src/block/tile/Furnace.php
+++ b/src/block/tile/Furnace.php
@@ -132,14 +132,14 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
$this->maxFuelTime = $this->remainingFuelTime = $ev->getBurnTime();
$this->onStartSmelting();
- if($this->remainingFuelTime > 0 and $ev->isBurning()){
+ if($this->remainingFuelTime > 0 && $ev->isBurning()){
$this->inventory->setFuel($fuel->getFuelResidue());
}
}
protected function onStartSmelting() : void{
$block = $this->getBlock();
- if($block instanceof BlockFurnace and !$block->isLit()){
+ if($block instanceof BlockFurnace && !$block->isLit()){
$block->setLit(true);
$this->position->getWorld()->setBlock($block->getPosition(), $block);
}
@@ -147,7 +147,7 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
protected function onStopSmelting() : void{
$block = $this->getBlock();
- if($block instanceof BlockFurnace and $block->isLit()){
+ if($block instanceof BlockFurnace && $block->isLit()){
$block->setLit(false);
$this->position->getWorld()->setBlock($block->getPosition(), $block);
}
@@ -175,16 +175,16 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
$furnaceType = $this->getFurnaceType();
$smelt = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($raw);
- $canSmelt = ($smelt instanceof FurnaceRecipe and $raw->getCount() > 0 and (($smelt->getResult()->equals($product) and $product->getCount() < $product->getMaxStackSize()) or $product->isNull()));
+ $canSmelt = ($smelt instanceof FurnaceRecipe && $raw->getCount() > 0 && (($smelt->getResult()->equals($product) && $product->getCount() < $product->getMaxStackSize()) || $product->isNull()));
- if($this->remainingFuelTime <= 0 and $canSmelt and $fuel->getFuelTime() > 0 and $fuel->getCount() > 0){
+ if($this->remainingFuelTime <= 0 && $canSmelt && $fuel->getFuelTime() > 0 && $fuel->getCount() > 0){
$this->checkFuel($fuel);
}
if($this->remainingFuelTime > 0){
--$this->remainingFuelTime;
- if($smelt instanceof FurnaceRecipe and $canSmelt){
+ if($smelt instanceof FurnaceRecipe && $canSmelt){
++$this->cookTime;
if($this->cookTime >= $furnaceType->getCookDurationTicks()){
diff --git a/src/block/tile/ItemFrame.php b/src/block/tile/ItemFrame.php
index 32d55f201..f4f679e7e 100644
--- a/src/block/tile/ItemFrame.php
+++ b/src/block/tile/ItemFrame.php
@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\World;
@@ -46,7 +46,7 @@ class ItemFrame extends Spawnable{
private $itemDropChance = 1.0;
public function __construct(World $world, Vector3 $pos){
- $this->item = ItemFactory::air();
+ $this->item = VanillaItems::AIR();
parent::__construct($world, $pos);
}
@@ -73,10 +73,10 @@ class ItemFrame extends Spawnable{
}
public function setItem(?Item $item) : void{
- if($item !== null and !$item->isNull()){
+ if($item !== null && !$item->isNull()){
$this->item = clone $item;
}else{
- $this->item = ItemFactory::air();
+ $this->item = VanillaItems::AIR();
}
}
diff --git a/src/block/tile/Lectern.php b/src/block/tile/Lectern.php
new file mode 100644
index 000000000..00e4a1072
--- /dev/null
+++ b/src/block/tile/Lectern.php
@@ -0,0 +1,87 @@
+viewedPage = $nbt->getInt(self::TAG_PAGE, 0);
+ if(($itemTag = $nbt->getCompoundTag(self::TAG_BOOK)) !== null){
+ $book = Item::nbtDeserialize($itemTag);
+ if($book instanceof WritableBookBase && !$book->isNull()){
+ $this->book = $book;
+ }
+ }
+ }
+
+ protected function writeSaveData(CompoundTag $nbt) : void{
+ $nbt->setByte(self::TAG_HAS_BOOK, $this->book !== null ? 1 : 0);
+ $nbt->setInt(self::TAG_PAGE, $this->viewedPage);
+ if($this->book !== null){
+ $nbt->setTag(self::TAG_BOOK, $this->book->nbtSerialize());
+ $nbt->setInt(self::TAG_TOTAL_PAGES, count($this->book->getPages()));
+ }
+ }
+
+ public function getViewedPage() : int{
+ return $this->viewedPage;
+ }
+
+ public function setViewedPage(int $viewedPage) : void{
+ $this->viewedPage = $viewedPage;
+ }
+
+ public function getBook() : ?WritableBookBase{
+ return $this->book !== null ? clone $this->book : null;
+ }
+
+ public function setBook(?WritableBookBase $book) : void{
+ $this->book = $book !== null && !$book->isNull() ? clone $book : null;
+ }
+
+ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
+ $nbt->setByte(self::TAG_HAS_BOOK, $this->book !== null ? 1 : 0);
+ $nbt->setInt(self::TAG_PAGE, $this->viewedPage);
+ if($this->book !== null){
+ $nbt->setTag(self::TAG_BOOK, $this->book->nbtSerialize());
+ $nbt->setInt(self::TAG_TOTAL_PAGES, count($this->book->getPages()));
+ }
+ }
+}
diff --git a/src/block/tile/Note.php b/src/block/tile/Note.php
index 5157f7436..30c252593 100644
--- a/src/block/tile/Note.php
+++ b/src/block/tile/Note.php
@@ -34,7 +34,7 @@ class Note extends Tile{
private $pitch = 0;
public function readSaveData(CompoundTag $nbt) : void{
- if(($pitch = $nbt->getByte("note", $this->pitch)) > BlockNote::MIN_PITCH and $pitch <= BlockNote::MAX_PITCH){
+ if(($pitch = $nbt->getByte("note", $this->pitch)) > BlockNote::MIN_PITCH && $pitch <= BlockNote::MAX_PITCH){
$this->pitch = $pitch;
}
}
@@ -48,7 +48,7 @@ class Note extends Tile{
}
public function setPitch(int $pitch) : void{
- if($pitch < BlockNote::MIN_PITCH or $pitch > BlockNote::MAX_PITCH){
+ if($pitch < BlockNote::MIN_PITCH || $pitch > BlockNote::MAX_PITCH){
throw new \InvalidArgumentException("Pitch must be in range " . BlockNote::MIN_PITCH . " - " . BlockNote::MAX_PITCH);
}
$this->pitch = $pitch;
diff --git a/src/block/tile/Skull.php b/src/block/tile/Skull.php
index 4db9ebd93..8ff40898d 100644
--- a/src/block/tile/Skull.php
+++ b/src/block/tile/Skull.php
@@ -59,7 +59,7 @@ class Skull extends Spawnable{
}
}
$rotation = $nbt->getByte(self::TAG_ROT, 0);
- if($rotation >= 0 and $rotation <= 15){
+ if($rotation >= 0 && $rotation <= 15){
$this->skullRotation = $rotation;
}
}
diff --git a/src/block/tile/TileFactory.php b/src/block/tile/TileFactory.php
index 9c51050fe..10c09ad55 100644
--- a/src/block/tile/TileFactory.php
+++ b/src/block/tile/TileFactory.php
@@ -23,8 +23,9 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\math\Vector3;
-use pocketmine\nbt\NbtDataException;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
@@ -66,6 +67,7 @@ final class TileFactory{
$this->register(Hopper::class, ["Hopper", "minecraft:hopper"]);
$this->register(ItemFrame::class, ["ItemFrame"]); //this is an entity in PC
$this->register(Jukebox::class, ["Jukebox", "RecordPlayer", "minecraft:jukebox"]);
+ $this->register(Lectern::class, ["Lectern", "minecraft:lectern"]);
$this->register(MonsterSpawner::class, ["MobSpawner", "minecraft:mob_spawner"]);
$this->register(Note::class, ["Music", "minecraft:noteblock"]);
$this->register(ShulkerBox::class, ["ShulkerBox", "minecraft:shulker_box"]);
@@ -84,7 +86,6 @@ final class TileFactory{
//TODO: EndGateway
//TODO: EndPortal
//TODO: JigsawBlock
- //TODO: Lectern
//TODO: MovingBlock
//TODO: NetherReactor
//TODO: PistonArm
@@ -112,21 +113,25 @@ final class TileFactory{
/**
* @internal
- * @throws NbtDataException
+ * @throws SavedDataLoadingException
*/
public function createFromData(World $world, CompoundTag $nbt) : ?Tile{
- $type = $nbt->getString(Tile::TAG_ID, "");
- if(!isset($this->knownTiles[$type])){
- return null;
+ try{
+ $type = $nbt->getString(Tile::TAG_ID, "");
+ if(!isset($this->knownTiles[$type])){
+ return null;
+ }
+ $class = $this->knownTiles[$type];
+ assert(is_a($class, Tile::class, true));
+ /**
+ * @var Tile $tile
+ * @see Tile::__construct()
+ */
+ $tile = new $class($world, new Vector3($nbt->getInt(Tile::TAG_X), $nbt->getInt(Tile::TAG_Y), $nbt->getInt(Tile::TAG_Z)));
+ $tile->readSaveData($nbt);
+ }catch(NbtException $e){
+ throw new SavedDataLoadingException($e->getMessage(), 0, $e);
}
- $class = $this->knownTiles[$type];
- assert(is_a($class, Tile::class, true));
- /**
- * @var Tile $tile
- * @see Tile::__construct()
- */
- $tile = new $class($world, new Vector3($nbt->getInt(Tile::TAG_X), $nbt->getInt(Tile::TAG_Y), $nbt->getInt(Tile::TAG_Z)));
- $tile->readSaveData($nbt);
return $tile;
}
diff --git a/src/block/utils/BlockDataSerializer.php b/src/block/utils/BlockDataSerializer.php
index 0c0c8005b..4bf5e9b75 100644
--- a/src/block/utils/BlockDataSerializer.php
+++ b/src/block/utils/BlockDataSerializer.php
@@ -150,7 +150,7 @@ final class BlockDataSerializer{
}
public static function readBoundedInt(string $name, int $v, int $min, int $max) : int{
- if($v < $min or $v > $max){
+ if($v < $min || $v > $max){
throw new InvalidBlockStateException("$name should be in range $min - $max, got $v");
}
return $v;
diff --git a/src/block/utils/BrewingStandSlot.php b/src/block/utils/BrewingStandSlot.php
index c5b14af1b..37182693e 100644
--- a/src/block/utils/BrewingStandSlot.php
+++ b/src/block/utils/BrewingStandSlot.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block\utils;
+use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\utils\EnumTrait;
/**
@@ -36,13 +37,24 @@ use pocketmine\utils\EnumTrait;
* @method static BrewingStandSlot SOUTHWEST()
*/
final class BrewingStandSlot{
- use EnumTrait;
+ use EnumTrait {
+ __construct as Enum___construct;
+ }
protected static function setup() : void{
self::registerAll(
- new self("east"),
- new self("northwest"),
- new self("southwest")
+ new self("east", BrewingStandInventory::SLOT_BOTTLE_LEFT),
+ new self("northwest", BrewingStandInventory::SLOT_BOTTLE_MIDDLE),
+ new self("southwest", BrewingStandInventory::SLOT_BOTTLE_RIGHT)
);
}
+
+ private function __construct(string $enumName, private int $slotNumber){
+ $this->Enum___construct($enumName);
+ }
+
+ /**
+ * Returns the brewing stand inventory slot number associated with this visual slot.
+ */
+ public function getSlotNumber() : int{ return $this->slotNumber; }
}
diff --git a/src/block/utils/CoralTypeTrait.php b/src/block/utils/CoralTypeTrait.php
new file mode 100644
index 000000000..33a37bc55
--- /dev/null
+++ b/src/block/utils/CoralTypeTrait.php
@@ -0,0 +1,46 @@
+coralType; }
+
+ /** @return $this */
+ public function setCoralType(CoralType $coralType) : self{
+ $this->coralType = $coralType;
+ return $this;
+ }
+
+ public function isDead() : bool{ return $this->dead; }
+
+ /** @return $this */
+ public function setDead(bool $dead) : self{
+ $this->dead = $dead;
+ return $this;
+ }
+}
diff --git a/src/block/utils/FallableTrait.php b/src/block/utils/FallableTrait.php
index 33f71a890..6afcc52bf 100644
--- a/src/block/utils/FallableTrait.php
+++ b/src/block/utils/FallableTrait.php
@@ -50,7 +50,7 @@ trait FallableTrait{
public function onNearbyBlockChange() : void{
$pos = $this->getPosition();
$down = $pos->getWorld()->getBlock($pos->getSide(Facing::DOWN));
- if($down->getId() === BlockLegacyIds::AIR or $down instanceof Liquid or $down instanceof Fire){
+ if($down->getId() === BlockLegacyIds::AIR || $down instanceof Liquid || $down instanceof Fire){
$pos->getWorld()->setBlock($pos, VanillaBlocks::AIR());
$block = $this;
diff --git a/src/block/utils/MinimumCostFlowCalculator.php b/src/block/utils/MinimumCostFlowCalculator.php
index 95e845791..e299caaa3 100644
--- a/src/block/utils/MinimumCostFlowCalculator.php
+++ b/src/block/utils/MinimumCostFlowCalculator.php
@@ -55,7 +55,7 @@ final class MinimumCostFlowCalculator{
$cost = 1000;
foreach(Facing::HORIZONTAL as $j){
- if($j === $originOpposite or $j === $lastOpposite){
+ if($j === $originOpposite || $j === $lastOpposite){
continue;
}
diff --git a/src/block/utils/RecordType.php b/src/block/utils/RecordType.php
index 093663d0b..e497d419c 100644
--- a/src/block/utils/RecordType.php
+++ b/src/block/utils/RecordType.php
@@ -25,7 +25,7 @@ namespace pocketmine\block\utils;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
-use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
use pocketmine\utils\EnumTrait;
/**
@@ -54,18 +54,18 @@ final class RecordType{
protected static function setup() : void{
self::registerAll(
- new RecordType("disk_13", "C418 - 13", LevelSoundEventPacket::SOUND_RECORD_13, KnownTranslationFactory::item_record_13_desc()),
- new RecordType("disk_cat", "C418 - cat", LevelSoundEventPacket::SOUND_RECORD_CAT, KnownTranslationFactory::item_record_cat_desc()),
- new RecordType("disk_blocks", "C418 - blocks", LevelSoundEventPacket::SOUND_RECORD_BLOCKS, KnownTranslationFactory::item_record_blocks_desc()),
- new RecordType("disk_chirp", "C418 - chirp", LevelSoundEventPacket::SOUND_RECORD_CHIRP, KnownTranslationFactory::item_record_chirp_desc()),
- new RecordType("disk_far", "C418 - far", LevelSoundEventPacket::SOUND_RECORD_FAR, KnownTranslationFactory::item_record_far_desc()),
- new RecordType("disk_mall", "C418 - mall", LevelSoundEventPacket::SOUND_RECORD_MALL, KnownTranslationFactory::item_record_mall_desc()),
- new RecordType("disk_mellohi", "C418 - mellohi", LevelSoundEventPacket::SOUND_RECORD_MELLOHI, KnownTranslationFactory::item_record_mellohi_desc()),
- new RecordType("disk_stal", "C418 - stal", LevelSoundEventPacket::SOUND_RECORD_STAL, KnownTranslationFactory::item_record_stal_desc()),
- new RecordType("disk_strad", "C418 - strad", LevelSoundEventPacket::SOUND_RECORD_STRAD, KnownTranslationFactory::item_record_strad_desc()),
- new RecordType("disk_ward", "C418 - ward", LevelSoundEventPacket::SOUND_RECORD_WARD, KnownTranslationFactory::item_record_ward_desc()),
- new RecordType("disk_11", "C418 - 11", LevelSoundEventPacket::SOUND_RECORD_11, KnownTranslationFactory::item_record_11_desc()),
- new RecordType("disk_wait", "C418 - wait", LevelSoundEventPacket::SOUND_RECORD_WAIT, KnownTranslationFactory::item_record_wait_desc())
+ new RecordType("disk_13", "C418 - 13", LevelSoundEvent::RECORD_13, KnownTranslationFactory::item_record_13_desc()),
+ new RecordType("disk_cat", "C418 - cat", LevelSoundEvent::RECORD_CAT, KnownTranslationFactory::item_record_cat_desc()),
+ new RecordType("disk_blocks", "C418 - blocks", LevelSoundEvent::RECORD_BLOCKS, KnownTranslationFactory::item_record_blocks_desc()),
+ new RecordType("disk_chirp", "C418 - chirp", LevelSoundEvent::RECORD_CHIRP, KnownTranslationFactory::item_record_chirp_desc()),
+ new RecordType("disk_far", "C418 - far", LevelSoundEvent::RECORD_FAR, KnownTranslationFactory::item_record_far_desc()),
+ new RecordType("disk_mall", "C418 - mall", LevelSoundEvent::RECORD_MALL, KnownTranslationFactory::item_record_mall_desc()),
+ new RecordType("disk_mellohi", "C418 - mellohi", LevelSoundEvent::RECORD_MELLOHI, KnownTranslationFactory::item_record_mellohi_desc()),
+ new RecordType("disk_stal", "C418 - stal", LevelSoundEvent::RECORD_STAL, KnownTranslationFactory::item_record_stal_desc()),
+ new RecordType("disk_strad", "C418 - strad", LevelSoundEvent::RECORD_STRAD, KnownTranslationFactory::item_record_strad_desc()),
+ new RecordType("disk_ward", "C418 - ward", LevelSoundEvent::RECORD_WARD, KnownTranslationFactory::item_record_ward_desc()),
+ new RecordType("disk_11", "C418 - 11", LevelSoundEvent::RECORD_11, KnownTranslationFactory::item_record_11_desc()),
+ new RecordType("disk_wait", "C418 - wait", LevelSoundEvent::RECORD_WAIT, KnownTranslationFactory::item_record_wait_desc())
//TODO: Lena Raine - Pigstep
);
}
diff --git a/src/block/utils/SignText.php b/src/block/utils/SignText.php
index 753e25bb3..4c7eeeb02 100644
--- a/src/block/utils/SignText.php
+++ b/src/block/utils/SignText.php
@@ -89,7 +89,7 @@ class SignText{
if(!is_int($index)){
throw new \InvalidArgumentException("Index must be an integer");
}
- if($index < 0 or $index >= self::LINE_COUNT){
+ if($index < 0 || $index >= self::LINE_COUNT){
throw new \InvalidArgumentException("Line index is out of bounds");
}
}
diff --git a/src/command/Command.php b/src/command/Command.php
index ff08620f1..6b1d6ab33 100644
--- a/src/command/Command.php
+++ b/src/command/Command.php
@@ -117,7 +117,7 @@ abstract class Command{
}
if($this->permissionMessage === null){
- $target->sendMessage(KnownTranslationFactory::commands_generic_permission()->prefix(TextFormat::RED));
+ $target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->prefix(TextFormat::RED));
}elseif($this->permissionMessage !== ""){
$target->sendMessage(str_replace("", $permission ?? $this->permission, $this->permissionMessage));
}
@@ -127,7 +127,7 @@ abstract class Command{
public function testPermissionSilent(CommandSender $target, ?string $permission = null) : bool{
$permission ??= $this->permission;
- if($permission === null or $permission === ""){
+ if($permission === null || $permission === ""){
return true;
}
@@ -182,7 +182,7 @@ abstract class Command{
}
private function allowChangesFrom(CommandMap $commandMap) : bool{
- return $this->commandMap === null or $this->commandMap === $commandMap;
+ return $this->commandMap === null || $this->commandMap === $commandMap;
}
public function isRegistered() : bool{
@@ -235,7 +235,7 @@ abstract class Command{
$result = KnownTranslationFactory::chat_type_admin($source->getName(), $message);
$colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC);
- if($sendToSource and !($source instanceof ConsoleCommandSender)){
+ if($sendToSource && !($source instanceof ConsoleCommandSender)){
$source->sendMessage($message);
}
diff --git a/src/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php
index f6e11f5e5..968c10012 100644
--- a/src/command/FormattedCommandAlias.php
+++ b/src/command/FormattedCommandAlias.php
@@ -71,7 +71,7 @@ class FormattedCommandAlias extends Command{
$index = strpos($formatString, '$');
while($index !== false){
$start = $index;
- if($index > 0 and $formatString[$start - 1] === "\\"){
+ if($index > 0 && $formatString[$start - 1] === "\\"){
$formatString = substr($formatString, 0, $start - 1) . substr($formatString, $start);
$index = strpos($formatString, '$', $index);
continue;
@@ -88,7 +88,7 @@ class FormattedCommandAlias extends Command{
$argStart = $index;
- while($index < strlen($formatString) and self::inRange(ord($formatString[$index]) - 48, 0, 9)){
+ while($index < strlen($formatString) && self::inRange(ord($formatString[$index]) - 48, 0, 9)){
++$index;
}
@@ -106,19 +106,19 @@ class FormattedCommandAlias extends Command{
$rest = false;
- if($index < strlen($formatString) and $formatString[$index] === "-"){
+ if($index < strlen($formatString) && $formatString[$index] === "-"){
$rest = true;
++$index;
}
$end = $index;
- if($required and $position >= count($args)){
+ if($required && $position >= count($args)){
throw new \InvalidArgumentException("Missing required argument " . ($position + 1));
}
$replacement = "";
- if($rest and $position < count($args)){
+ if($rest && $position < count($args)){
for($i = $position, $c = count($args); $i < $c; ++$i){
if($i !== $position){
$replacement .= " ";
@@ -141,6 +141,6 @@ class FormattedCommandAlias extends Command{
}
private static function inRange(int $i, int $j, int $k) : bool{
- return $i >= $j and $i <= $k;
+ return $i >= $j && $i <= $k;
}
}
diff --git a/src/command/PluginCommand.php b/src/command/PluginCommand.php
index 2453f4025..634978006 100644
--- a/src/command/PluginCommand.php
+++ b/src/command/PluginCommand.php
@@ -53,7 +53,7 @@ final class PluginCommand extends Command implements PluginOwned{
$success = $this->executor->onCommand($sender, $this, $commandLabel, $args);
- if(!$success and $this->usageMessage !== ""){
+ if(!$success && $this->usageMessage !== ""){
throw new InvalidCommandSyntaxException();
}
diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php
index 638e10740..116f59c23 100644
--- a/src/command/SimpleCommandMap.php
+++ b/src/command/SimpleCommandMap.php
@@ -67,6 +67,7 @@ use pocketmine\command\defaults\WhitelistCommand;
use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\Server;
+use pocketmine\utils\TextFormat;
use function array_shift;
use function count;
use function explode;
@@ -182,11 +183,11 @@ class SimpleCommandMap implements CommandMap{
private function registerAlias(Command $command, bool $isAlias, string $fallbackPrefix, string $label) : bool{
$this->knownCommands[$fallbackPrefix . ":" . $label] = $command;
- if(($command instanceof VanillaCommand or $isAlias) and isset($this->knownCommands[$label])){
+ if(($command instanceof VanillaCommand || $isAlias) && isset($this->knownCommands[$label])){
return false;
}
- if(isset($this->knownCommands[$label]) and $this->knownCommands[$label]->getLabel() === $label){
+ if(isset($this->knownCommands[$label]) && $this->knownCommands[$label]->getLabel() === $label){
return false;
}
@@ -205,31 +206,28 @@ class SimpleCommandMap implements CommandMap{
foreach($matches[0] as $k => $_){
for($i = 1; $i <= 2; ++$i){
if($matches[$i][$k] !== ""){
- $args[$k] = stripslashes($matches[$i][$k]);
+ $args[$k] = $i === 1 ? stripslashes($matches[$i][$k]) : $matches[$i][$k];
break;
}
}
}
+
$sentCommandLabel = array_shift($args);
- if($sentCommandLabel === null){
- return false;
- }
- $target = $this->getCommand($sentCommandLabel);
- if($target === null){
- return false;
+ if($sentCommandLabel !== null && ($target = $this->getCommand($sentCommandLabel)) !== null){
+ $target->timings->startTiming();
+
+ try{
+ $target->execute($sender, $sentCommandLabel, $args);
+ }catch(InvalidCommandSyntaxException $e){
+ $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage())));
+ }finally{
+ $target->timings->stopTiming();
+ }
+ return true;
}
- $target->timings->startTiming();
-
- try{
- $target->execute($sender, $sentCommandLabel, $args);
- }catch(InvalidCommandSyntaxException $e){
- $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage())));
- }finally{
- $target->timings->stopTiming();
- }
-
- return true;
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->prefix(TextFormat::RED));
+ return false;
}
public function clearCommands() : void{
diff --git a/src/command/defaults/BanIpCommand.php b/src/command/defaults/BanIpCommand.php
index fca99eff6..d6e1584b9 100644
--- a/src/command/defaults/BanIpCommand.php
+++ b/src/command/defaults/BanIpCommand.php
@@ -32,7 +32,7 @@ use pocketmine\player\Player;
use function array_shift;
use function count;
use function implode;
-use function preg_match;
+use function inet_pton;
class BanIpCommand extends VanillaCommand{
@@ -57,7 +57,7 @@ class BanIpCommand extends VanillaCommand{
$value = array_shift($args);
$reason = implode(" ", $args);
- if(preg_match("/^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$/", $value)){
+ if(inet_pton($value) !== false){
$this->processIPBan($value, $sender, $reason);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_banip_success($value));
diff --git a/src/command/defaults/ClearCommand.php b/src/command/defaults/ClearCommand.php
index 59bee1e5b..237f3d108 100644
--- a/src/command/defaults/ClearCommand.php
+++ b/src/command/defaults/ClearCommand.php
@@ -26,6 +26,8 @@ namespace pocketmine\command\defaults;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
+use pocketmine\inventory\Inventory;
+use pocketmine\item\Item;
use pocketmine\item\LegacyStringToItemParser;
use pocketmine\item\LegacyStringToItemParserException;
use pocketmine\item\StringToItemParser;
@@ -33,9 +35,9 @@ use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
-use function array_merge;
use function count;
use function implode;
+use function min;
class ClearCommand extends VanillaCommand{
@@ -57,7 +59,6 @@ class ClearCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
- $target = null;
if(isset($args[0])){
$target = $sender->getServer()->getPlayerByPrefix($args[0]);
if($target === null){
@@ -77,14 +78,14 @@ class ClearCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
- $item = null;
+ $targetItem = null;
$maxCount = -1;
if(isset($args[1])){
try{
- $item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]);
+ $targetItem = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]);
if(isset($args[2])){
- $item->setCount($maxCount = $this->getInteger($sender, $args[2], 0));
+ $targetItem->setCount($maxCount = $this->getInteger($sender, $args[2], -1));
}
}catch(LegacyStringToItemParserException $e){
//vanilla checks this at argument parsing layer, can't come up with a better alternative
@@ -93,14 +94,19 @@ class ClearCommand extends VanillaCommand{
}
}
- //checking players inventory for all the items matching the criteria
- if($item !== null and $maxCount === 0){
- $count = 0;
- $contents = array_merge($target->getInventory()->all($item), $target->getArmorInventory()->all($item));
- foreach($contents as $content){
- $count += $content->getCount();
- }
+ /**
+ * @var Inventory[] $inventories - This is the order that vanilla would clear items in.
+ */
+ $inventories = [
+ $target->getInventory(),
+ $target->getCursorInventory(),
+ $target->getArmorInventory(),
+ $target->getOffHandInventory()
+ ];
+ // Checking player's inventory for all the items matching the criteria
+ if($targetItem !== null && $maxCount === 0){
+ $count = $this->countItems($inventories, $targetItem);
if($count > 0){
$sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count));
}else{
@@ -110,65 +116,59 @@ class ClearCommand extends VanillaCommand{
return true;
}
- $cleared = 0;
-
- //clear everything from the targets inventory
- if($item === null){
- $contents = array_merge($target->getInventory()->getContents(), $target->getArmorInventory()->getContents());
- foreach($contents as $content){
- $cleared += $content->getCount();
+ $clearedCount = 0;
+ if($targetItem === null){
+ // Clear all items from the inventories
+ $clearedCount += $this->countItems($inventories, null);
+ foreach($inventories as $inventory){
+ $inventory->clearAll();
}
-
- $target->getInventory()->clearAll();
- $target->getArmorInventory()->clearAll();
- //TODO: should the cursor inv be cleared?
}else{
- //clear the item from targets inventory irrelevant of the count
+ // Clear the item from target's inventory irrelevant of the count
if($maxCount === -1){
- if(($slot = $target->getArmorInventory()->first($item)) !== -1){
- $cleared++;
- $target->getArmorInventory()->clear($slot);
- }
-
- foreach($target->getInventory()->all($item) as $index => $i){
- $cleared += $i->getCount();
- $target->getInventory()->clear($index);
+ $clearedCount += $this->countItems($inventories, $targetItem);
+ foreach($inventories as $inventory){
+ $inventory->remove($targetItem);
}
}else{
- //clear only the given amount of that particular item from targets inventory
- if(($slot = $target->getArmorInventory()->first($item)) !== -1){
- $cleared++;
- $maxCount--;
- $target->getArmorInventory()->clear($slot);
- }
-
- if($maxCount > 0){
- foreach($target->getInventory()->all($item) as $index => $i){
- if($i->getCount() >= $maxCount){
- $i->pop($maxCount);
- $cleared += $maxCount;
- $target->getInventory()->setItem($index, $i);
- break;
- }
+ // Clear the item from target's inventory up to maxCount
+ foreach($inventories as $inventory){
+ foreach($inventory->all($targetItem) as $index => $item){
+ // The count to reduce from the item and max count
+ $reductionCount = min($item->getCount(), $maxCount);
+ $item->pop($reductionCount);
+ $clearedCount += $reductionCount;
+ $inventory->setItem($index, $item);
+ $maxCount -= $reductionCount;
if($maxCount <= 0){
- break;
+ break 2;
}
-
- $cleared += $i->getCount();
- $maxCount -= $i->getCount();
- $target->getInventory()->clear($index);
}
}
}
}
- if($cleared > 0){
- Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $cleared));
+ if($clearedCount > 0){
+ Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount));
}else{
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED));
}
return true;
}
+
+ /**
+ * @param Inventory[] $inventories
+ */
+ protected function countItems(array $inventories, ?Item $target) : int{
+ $count = 0;
+ foreach($inventories as $inventory){
+ $contents = $target !== null ? $inventory->all($target) : $inventory->getContents();
+ foreach($contents as $item){
+ $count += $item->getCount();
+ }
+ }
+ return $count;
+ }
}
diff --git a/src/command/defaults/DefaultGamemodeCommand.php b/src/command/defaults/DefaultGamemodeCommand.php
index f7752319e..1f0b29aa3 100644
--- a/src/command/defaults/DefaultGamemodeCommand.php
+++ b/src/command/defaults/DefaultGamemodeCommand.php
@@ -25,7 +25,6 @@ namespace pocketmine\command\defaults;
use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
-use pocketmine\data\java\GameModeIdMap;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\GameMode;
@@ -53,11 +52,11 @@ class DefaultGamemodeCommand extends VanillaCommand{
$gameMode = GameMode::fromString($args[0]);
if($gameMode === null){
- $sender->sendMessage("Unknown game mode");
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_unknown($args[0]));
return true;
}
- $sender->getServer()->getConfigGroup()->setConfigInt("gamemode", GameModeIdMap::getInstance()->toId($gameMode));
+ $sender->getServer()->getConfigGroup()->setConfigString("gamemode", $gameMode->name());
$sender->sendMessage(KnownTranslationFactory::commands_defaultgamemode_success($gameMode->getTranslatableName()));
return true;
}
diff --git a/src/command/defaults/EffectCommand.php b/src/command/defaults/EffectCommand.php
index 84fa8585e..fe2db7d2d 100644
--- a/src/command/defaults/EffectCommand.php
+++ b/src/command/defaults/EffectCommand.php
@@ -26,7 +26,7 @@ namespace pocketmine\command\defaults;
use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\entity\effect\EffectInstance;
-use pocketmine\entity\effect\VanillaEffects;
+use pocketmine\entity\effect\StringToEffectParser;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\Limits;
@@ -69,9 +69,8 @@ class EffectCommand extends VanillaCommand{
return true;
}
- try{
- $effect = VanillaEffects::fromString($args[1]);
- }catch(\InvalidArgumentException $e){
+ $effect = StringToEffectParser::getInstance()->parse($args[1]);
+ if($effect === null){
$sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->prefix(TextFormat::RED));
return true;
}
@@ -97,7 +96,7 @@ class EffectCommand extends VanillaCommand{
$visible = true;
if(count($args) >= 5){
$v = strtolower($args[4]);
- if($v === "on" or $v === "true" or $v === "t" or $v === "1"){
+ if($v === "on" || $v === "true" || $v === "t" || $v === "1"){
$visible = false;
}
}
diff --git a/src/command/defaults/EnchantCommand.php b/src/command/defaults/EnchantCommand.php
index 08e959a4f..3e53a55fb 100644
--- a/src/command/defaults/EnchantCommand.php
+++ b/src/command/defaults/EnchantCommand.php
@@ -26,7 +26,7 @@ namespace pocketmine\command\defaults;
use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\item\enchantment\EnchantmentInstance;
-use pocketmine\item\enchantment\VanillaEnchantments;
+use pocketmine\item\enchantment\StringToEnchantmentParser;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\TextFormat;
@@ -66,9 +66,8 @@ class EnchantCommand extends VanillaCommand{
return true;
}
- try{
- $enchantment = VanillaEnchantments::fromString($args[1]);
- }catch(\InvalidArgumentException $e){
+ $enchantment = StringToEnchantmentParser::getInstance()->parse($args[1]);
+ if($enchantment === null){
$sender->sendMessage(KnownTranslationFactory::commands_enchant_notFound($args[1]));
return true;
}
diff --git a/src/command/defaults/GamemodeCommand.php b/src/command/defaults/GamemodeCommand.php
index e01150875..86658de6d 100644
--- a/src/command/defaults/GamemodeCommand.php
+++ b/src/command/defaults/GamemodeCommand.php
@@ -55,7 +55,7 @@ class GamemodeCommand extends VanillaCommand{
$gameMode = GameMode::fromString($args[0]);
if($gameMode === null){
- $sender->sendMessage("Unknown game mode");
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_unknown($args[0]));
return true;
}
@@ -74,7 +74,7 @@ class GamemodeCommand extends VanillaCommand{
$target->setGamemode($gameMode);
if(!$gameMode->equals($target->getGamemode())){
- $sender->sendMessage("Game mode change for " . $target->getName() . " failed!");
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_failure($target->getName()));
}else{
if($target === $sender){
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_gamemode_success_self($gameMode->getTranslatableName()));
diff --git a/src/command/defaults/GarbageCollectorCommand.php b/src/command/defaults/GarbageCollectorCommand.php
index 1081ac52e..2e85ed093 100644
--- a/src/command/defaults/GarbageCollectorCommand.php
+++ b/src/command/defaults/GarbageCollectorCommand.php
@@ -68,7 +68,7 @@ class GarbageCollectorCommand extends VanillaCommand{
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_entities(TextFormat::RED . number_format($entitiesCollected))->prefix(TextFormat::GOLD));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_cycles(TextFormat::RED . number_format($cyclesCollected))->prefix(TextFormat::GOLD));
- $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_memoryFreed(TextFormat::RED . number_format(round((($memory - memory_get_usage()) / 1024) / 1024, 2), 2) . " MB")->prefix(TextFormat::GOLD));
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_memoryFreed(TextFormat::RED . number_format(round((($memory - memory_get_usage()) / 1024) / 1024, 2), 2))->prefix(TextFormat::GOLD));
return true;
}
}
diff --git a/src/command/defaults/GiveCommand.php b/src/command/defaults/GiveCommand.php
index 34814f207..8fd5c4298 100644
--- a/src/command/defaults/GiveCommand.php
+++ b/src/command/defaults/GiveCommand.php
@@ -32,6 +32,7 @@ use pocketmine\item\StringToItemParser;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\nbt\JsonNbtParser;
use pocketmine\nbt\NbtDataException;
+use pocketmine\nbt\NbtException;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\TextFormat;
use function array_slice;
@@ -86,7 +87,12 @@ class GiveCommand extends VanillaCommand{
return true;
}
- $item->setNamedTag($tags);
+ try{
+ $item->setNamedTag($tags);
+ }catch(NbtException $e){
+ $sender->sendMessage(KnownTranslationFactory::commands_give_tagError($e->getMessage()));
+ return true;
+ }
}
//TODO: overflow
diff --git a/src/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php
index 77bc98d0e..f16049a69 100644
--- a/src/command/defaults/HelpCommand.php
+++ b/src/command/defaults/HelpCommand.php
@@ -105,17 +105,20 @@ class HelpCommand extends VanillaCommand{
$lang = $sender->getLanguage();
$description = $cmd->getDescription();
$descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description;
- $message = TextFormat::YELLOW . "--------- " . TextFormat::WHITE . " Help: /" . $cmd->getName() . TextFormat::YELLOW . " ---------\n";
- $message .= TextFormat::GOLD . "Description: " . TextFormat::WHITE . $descriptionString . "\n";
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($commandName)
+ ->format(TextFormat::YELLOW . "--------- " . TextFormat::WHITE, TextFormat::YELLOW . " ---------"));
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::WHITE . $descriptionString)
+ ->prefix(TextFormat::GOLD));
+
$usage = $cmd->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage;
- $message .= TextFormat::GOLD . "Usage: " . TextFormat::WHITE . implode("\n" . TextFormat::WHITE, explode("\n", $usageString)) . "\n";
- $sender->sendMessage($message);
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::WHITE . implode("\n" . TextFormat::WHITE, explode("\n", $usageString)))
+ ->prefix(TextFormat::GOLD));
return true;
}
}
- $sender->sendMessage(TextFormat::RED . "No help for " . strtolower($commandName));
+ $sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->prefix(TextFormat::RED));
return true;
}
diff --git a/src/command/defaults/KickCommand.php b/src/command/defaults/KickCommand.php
index a341ca64b..7952c522e 100644
--- a/src/command/defaults/KickCommand.php
+++ b/src/command/defaults/KickCommand.php
@@ -59,7 +59,7 @@ class KickCommand extends VanillaCommand{
$reason = trim(implode(" ", $args));
if(($player = $sender->getServer()->getPlayerByPrefix($name)) instanceof Player){
- $player->kick("Kicked by admin." . ($reason !== "" ? "Reason: " . $reason : ""));
+ $player->kick("Kicked by admin." . ($reason !== "" ? " Reason: " . $reason : ""));
if($reason !== ""){
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kick_success_reason($player->getName(), $reason));
}else{
diff --git a/src/command/defaults/ListCommand.php b/src/command/defaults/ListCommand.php
index a19130e3f..c451877ae 100644
--- a/src/command/defaults/ListCommand.php
+++ b/src/command/defaults/ListCommand.php
@@ -52,7 +52,7 @@ class ListCommand extends VanillaCommand{
$playerNames = array_map(function(Player $player) : string{
return $player->getName();
}, array_filter($sender->getServer()->getOnlinePlayers(), function(Player $player) use ($sender) : bool{
- return !($sender instanceof Player) or $sender->canSee($player);
+ return !($sender instanceof Player) || $sender->canSee($player);
}));
sort($playerNames, SORT_STRING);
diff --git a/src/command/defaults/PardonIpCommand.php b/src/command/defaults/PardonIpCommand.php
index 395ba1d93..f0b2c9aa5 100644
--- a/src/command/defaults/PardonIpCommand.php
+++ b/src/command/defaults/PardonIpCommand.php
@@ -29,7 +29,7 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use function count;
-use function preg_match;
+use function inet_pton;
class PardonIpCommand extends VanillaCommand{
@@ -52,7 +52,7 @@ class PardonIpCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
- if(preg_match("/^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$/", $args[0])){
+ if(inet_pton($args[0]) !== false){
$sender->getServer()->getIPBans()->remove($args[0]);
$sender->getServer()->getNetwork()->unblockAddress($args[0]);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_unbanip_success($args[0]));
diff --git a/src/command/defaults/ParticleCommand.php b/src/command/defaults/ParticleCommand.php
index ee7af129a..072ca9dc8 100644
--- a/src/command/defaults/ParticleCommand.php
+++ b/src/command/defaults/ParticleCommand.php
@@ -181,12 +181,12 @@ class ParticleCommand extends VanillaCommand{
case "slime":
return new ItemBreakParticle(VanillaItems::SLIMEBALL());
case "itembreak":
- if($data !== null and $data !== 0){
+ if($data !== null && $data !== 0){
return new ItemBreakParticle(ItemFactory::getInstance()->get($data));
}
break;
case "terrain":
- if($data !== null and $data !== 0){
+ if($data !== null && $data !== 0){
return new TerrainParticle(BlockFactory::getInstance()->get($data, 0));
}
break;
diff --git a/src/command/defaults/SetWorldSpawnCommand.php b/src/command/defaults/SetWorldSpawnCommand.php
index 9bd37db98..96582a349 100644
--- a/src/command/defaults/SetWorldSpawnCommand.php
+++ b/src/command/defaults/SetWorldSpawnCommand.php
@@ -31,8 +31,8 @@ use pocketmine\math\Vector3;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
+use pocketmine\world\World;
use function count;
-use function round;
class SetWorldSpawnCommand extends VanillaCommand{
@@ -54,22 +54,32 @@ class SetWorldSpawnCommand extends VanillaCommand{
if($sender instanceof Player){
$location = $sender->getPosition();
$world = $location->getWorld();
- $pos = $location->asVector3()->round();
+ $pos = $location->asVector3()->floor();
}else{
$sender->sendMessage(TextFormat::RED . "You can only perform this command as a player");
return true;
}
}elseif(count($args) === 3){
- $world = $sender->getServer()->getWorldManager()->getDefaultWorld();
- $pos = new Vector3($this->getInteger($sender, $args[0]), $this->getInteger($sender, $args[1]), $this->getInteger($sender, $args[2]));
+ if($sender instanceof Player){
+ $base = $sender->getPosition();
+ $world = $base->getWorld();
+ }else{
+ $base = new Vector3(0.0, 0.0, 0.0);
+ $world = $sender->getServer()->getWorldManager()->getDefaultWorld();
+ }
+ $pos = (new Vector3(
+ $this->getRelativeDouble($base->x, $sender, $args[0]),
+ $this->getRelativeDouble($base->y, $sender, $args[1], World::Y_MIN, World::Y_MAX),
+ $this->getRelativeDouble($base->z, $sender, $args[2]),
+ ))->floor();
}else{
throw new InvalidCommandSyntaxException();
}
$world->setSpawnLocation($pos);
- Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_setworldspawn_success((string) round($pos->x, 2), (string) round($pos->y, 2), (string) round($pos->z, 2)));
+ Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_setworldspawn_success((string) $pos->x, (string) $pos->y, (string) $pos->z));
return true;
}
diff --git a/src/command/defaults/TeleportCommand.php b/src/command/defaults/TeleportCommand.php
index 1ece20f30..4748dc2d3 100644
--- a/src/command/defaults/TeleportCommand.php
+++ b/src/command/defaults/TeleportCommand.php
@@ -32,6 +32,7 @@ use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\TextFormat;
+use pocketmine\world\World;
use function array_shift;
use function count;
use function round;
@@ -111,9 +112,9 @@ class TeleportCommand extends VanillaCommand{
}
$x = $this->getRelativeDouble($base->x, $sender, $targetArgs[0]);
- $y = $this->getRelativeDouble($base->y, $sender, $targetArgs[1], 0, 256);
+ $y = $this->getRelativeDouble($base->y, $sender, $targetArgs[1], World::Y_MIN, World::Y_MAX);
$z = $this->getRelativeDouble($base->z, $sender, $targetArgs[2]);
- $targetLocation = new Location($x, $y, $z, $yaw, $pitch, $base->getWorld());
+ $targetLocation = new Location($x, $y, $z, $base->getWorld(), $yaw, $pitch);
$subject->teleport($targetLocation);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_tp_success_coordinates(
diff --git a/src/command/defaults/TimingsCommand.php b/src/command/defaults/TimingsCommand.php
index 26839b312..13c95b5bd 100644
--- a/src/command/defaults/TimingsCommand.php
+++ b/src/command/defaults/TimingsCommand.php
@@ -34,6 +34,7 @@ use pocketmine\scheduler\BulkCurlTaskOperation;
use pocketmine\timings\TimingsHandler;
use pocketmine\utils\InternetException;
use pocketmine\utils\InternetRequestResult;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function count;
use function fclose;
@@ -102,10 +103,10 @@ class TimingsCommand extends VanillaCommand{
if($mode === "reset"){
TimingsHandler::reload();
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_reset());
- }elseif($mode === "merged" or $mode === "report" or $paste){
+ }elseif($mode === "merged" || $mode === "report" || $paste){
$timings = "";
if($paste){
- $fileTimings = fopen("php://temp", "r+b");
+ $fileTimings = Utils::assumeNotFalse(fopen("php://temp", "r+b"), "Opening php://temp should never fail");
}else{
$index = 0;
$timingFolder = Path::join($sender->getServer()->getDataPath(), "timings");
@@ -153,7 +154,7 @@ class TimingsCommand extends VanillaCommand{
)],
function(array $results) use ($sender, $host) : void{
/** @phpstan-var array $results */
- if($sender instanceof Player and !$sender->isOnline()){ // TODO replace with a more generic API method for checking availability of CommandSender
+ if($sender instanceof Player && !$sender->isOnline()){ // TODO replace with a more generic API method for checking availability of CommandSender
return;
}
$result = $results[0];
diff --git a/src/console/ConsoleCommandSender.php b/src/console/ConsoleCommandSender.php
index 45ae2f5ab..3879ab016 100644
--- a/src/console/ConsoleCommandSender.php
+++ b/src/console/ConsoleCommandSender.php
@@ -78,7 +78,7 @@ class ConsoleCommandSender implements CommandSender{
}
public function setScreenLineHeight(?int $height) : void{
- if($height !== null and $height < 1){
+ if($height !== null && $height < 1){
throw new \InvalidArgumentException("Line height must be at least 1");
}
$this->lineHeight = $height;
diff --git a/src/console/ConsoleReader.php b/src/console/ConsoleReader.php
index 9da61cb6d..ac7b6f644 100644
--- a/src/console/ConsoleReader.php
+++ b/src/console/ConsoleReader.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\console;
+use pocketmine\utils\Utils;
use function fclose;
use function fgets;
use function fopen;
@@ -44,7 +45,7 @@ final class ConsoleReader{
fclose($this->stdin);
}
- $this->stdin = fopen("php://stdin", "r");
+ $this->stdin = Utils::assumeNotFalse(fopen("php://stdin", "r"), "Opening stdin should never fail");
}
/**
@@ -60,11 +61,10 @@ final class ConsoleReader{
if(($count = stream_select($r, $w, $e, 0, 200000)) === 0){ //nothing changed in 200000 microseconds
return null;
}elseif($count === false){ //stream error
- $this->initStdin();
+ return null;
}
if(($raw = fgets($this->stdin)) === false){ //broken pipe or EOF
- $this->initStdin();
usleep(200000); //prevent CPU waste if it's end of pipe
return null; //loop back round
}
diff --git a/src/console/ConsoleReaderChildProcess.php b/src/console/ConsoleReaderChildProcess.php
index 3879ecdff..509d0daca 100644
--- a/src/console/ConsoleReaderChildProcess.php
+++ b/src/console/ConsoleReaderChildProcess.php
@@ -46,7 +46,10 @@ if($socket === false){
$consoleReader = new ConsoleReader();
while(!feof($socket)){
$line = $consoleReader->readLine();
- if($line !== null){
- fwrite($socket, $line . "\n");
+ if(@fwrite($socket, ($line ?? "") . "\n") === false){
+ //Always send even if there's no line, to check if the parent is alive
+ //If the parent process was terminated forcibly, it won't close the connection properly, so feof() will return
+ //false even though the connection is actually broken. However, fwrite() will fail.
+ break;
}
}
diff --git a/src/console/ConsoleReaderThread.php b/src/console/ConsoleReaderThread.php
index f1cca9bb3..c9938e05f 100644
--- a/src/console/ConsoleReaderThread.php
+++ b/src/console/ConsoleReaderThread.php
@@ -26,11 +26,13 @@ namespace pocketmine\console;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\thread\Thread;
use pocketmine\utils\AssumptionFailedError;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function base64_encode;
use function fgets;
use function fopen;
use function preg_replace;
+use function proc_close;
use function proc_open;
use function proc_terminate;
use function sprintf;
@@ -39,6 +41,7 @@ use function stream_socket_accept;
use function stream_socket_get_name;
use function stream_socket_server;
use function stream_socket_shutdown;
+use function trim;
use const PHP_BINARY;
use const STREAM_SHUT_RDWR;
@@ -79,21 +82,18 @@ final class ConsoleReaderThread extends Thread{
if($server === false){
throw new \RuntimeException("Failed to open console reader socket server");
}
- $address = stream_socket_get_name($server, false);
- if($address === false) throw new AssumptionFailedError("stream_socket_get_name() shouldn't return false here");
+ $address = Utils::assumeNotFalse(stream_socket_get_name($server, false), "stream_socket_get_name() shouldn't return false here");
//Windows sucks, and likes to corrupt UTF-8 file paths when they travel to the subprocess, so we base64 encode
//the path to avoid the problem. This is an abysmally shitty hack, but here we are :(
- $sub = proc_open(
- [PHP_BINARY, '-r', sprintf('require base64_decode("%s", true);', base64_encode(Path::join(__DIR__, 'ConsoleReaderChildProcess.php'))), $address],
+ $sub = Utils::assumeNotFalse(proc_open(
+ [PHP_BINARY, '-dopcache.enable_cli=0', '-r', sprintf('require base64_decode("%s", true);', base64_encode(Path::join(__DIR__, 'ConsoleReaderChildProcess.php'))), $address],
[
2 => fopen("php://stderr", "w"),
],
$pipes
- );
- if($sub === false){
- throw new AssumptionFailedError("Something has gone horribly wrong");
- }
+ ), "Something has gone horribly wrong");
+
$client = stream_socket_accept($server, 15);
if($client === false){
throw new AssumptionFailedError("stream_socket_accept() returned false");
@@ -113,7 +113,12 @@ final class ConsoleReaderThread extends Thread{
break;
}
- $buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $command);
+ $command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
+ $command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
+ if($command === ""){
+ continue;
+ }
+ $buffer[] = $command;
if($notifier !== null){
$notifier->wakeupSleeper();
}
@@ -124,6 +129,7 @@ final class ConsoleReaderThread extends Thread{
//gets stuck in a blocking fgets() read because stream_select() is a hunk of junk (hence the separate process in
//the first place).
proc_terminate($sub);
+ proc_close($sub);
stream_socket_shutdown($client, STREAM_SHUT_RDWR);
}
diff --git a/src/crafting/BrewingRecipe.php b/src/crafting/BrewingRecipe.php
new file mode 100644
index 000000000..e903f0c3c
--- /dev/null
+++ b/src/crafting/BrewingRecipe.php
@@ -0,0 +1,30 @@
+holder = $holder;
+ public function __construct(int $gridWidth){
$this->gridWidth = $gridWidth;
parent::__construct($this->getGridWidth() ** 2);
}
@@ -63,13 +59,6 @@ class CraftingGrid extends SimpleInventory{
$this->seekRecipeBounds();
}
- /**
- * @return Player
- */
- public function getHolder(){
- return $this->holder;
- }
-
private function seekRecipeBounds() : void{
$minX = PHP_INT_MAX;
$maxX = 0;
@@ -107,11 +96,11 @@ class CraftingGrid extends SimpleInventory{
* Returns the item at offset x,y, offset by where the starts of the recipe rectangle are.
*/
public function getIngredient(int $x, int $y) : Item{
- if($this->startX !== null and $this->startY !== null){
+ if($this->startX !== null && $this->startY !== null){
return $this->getItem(($y + $this->startY) * $this->gridWidth + ($x + $this->startX));
}
- throw new \InvalidStateException("No ingredients found in grid");
+ throw new \LogicException("No ingredients found in grid");
}
/**
diff --git a/src/crafting/CraftingManager.php b/src/crafting/CraftingManager.php
index 32b6b7717..ba690c844 100644
--- a/src/crafting/CraftingManager.php
+++ b/src/crafting/CraftingManager.php
@@ -45,6 +45,18 @@ class CraftingManager{
*/
protected $furnaceRecipeManagers;
+ /**
+ * @var PotionTypeRecipe[][]
+ * @phpstan-var array>
+ */
+ protected $potionTypeRecipes = [];
+
+ /**
+ * @var PotionContainerChangeRecipe[][]
+ * @phpstan-var array>
+ */
+ protected $potionContainerChangeRecipes = [];
+
/**
* @var ObjectSet
* @phpstan-var ObjectSet<\Closure() : void>
@@ -140,6 +152,22 @@ class CraftingManager{
return $this->furnaceRecipeManagers[$furnaceType->id()];
}
+ /**
+ * @return PotionTypeRecipe[][]
+ * @phpstan-return array>
+ */
+ public function getPotionTypeRecipes() : array{
+ return $this->potionTypeRecipes;
+ }
+
+ /**
+ * @return PotionContainerChangeRecipe[][]
+ * @phpstan-return array>
+ */
+ public function getPotionContainerChangeRecipes() : array{
+ return $this->potionContainerChangeRecipes;
+ }
+
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
@@ -156,6 +184,25 @@ class CraftingManager{
}
}
+ public function registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void{
+ $input = $recipe->getInput();
+ $ingredient = $recipe->getIngredient();
+ $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe;
+
+ foreach($this->recipeRegisteredCallbacks as $callback){
+ $callback();
+ }
+ }
+
+ public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void{
+ $ingredient = $recipe->getIngredient();
+ $this->potionContainerChangeRecipes[$recipe->getInputItemId()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe;
+
+ foreach($this->recipeRegisteredCallbacks as $callback){
+ $callback();
+ }
+ }
+
/**
* @param Item[] $outputs
*/
@@ -206,4 +253,11 @@ class CraftingManager{
}
}
}
+
+ public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingRecipe{
+ return $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . $ingredient->getMeta()] ??
+ $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":?"] ??
+ $this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":" . $ingredient->getMeta()] ??
+ $this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":?"] ?? null;
+ }
}
diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php
index 6660bb602..b5c485ab2 100644
--- a/src/crafting/CraftingManagerFromDataHelper.php
+++ b/src/crafting/CraftingManagerFromDataHelper.php
@@ -25,6 +25,7 @@ namespace pocketmine\crafting;
use pocketmine\item\Item;
use pocketmine\utils\AssumptionFailedError;
+use pocketmine\utils\Utils;
use function array_map;
use function file_get_contents;
use function is_array;
@@ -33,7 +34,7 @@ use function json_decode;
final class CraftingManagerFromDataHelper{
public static function make(string $filePath) : CraftingManager{
- $recipes = json_decode(file_get_contents($filePath), true);
+ $recipes = json_decode(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file"), true);
if(!is_array($recipes)){
throw new AssumptionFailedError("recipes.json root should contain a map of recipe types");
}
@@ -76,6 +77,20 @@ final class CraftingManagerFromDataHelper{
Item::jsonDeserialize($recipe["input"]))
);
}
+ foreach($recipes["potion_type"] as $recipe){
+ $result->registerPotionTypeRecipe(new PotionTypeRecipe(
+ Item::jsonDeserialize($recipe["input"]),
+ Item::jsonDeserialize($recipe["ingredient"]),
+ Item::jsonDeserialize($recipe["output"])
+ ));
+ }
+ foreach($recipes["potion_container_change"] as $recipe){
+ $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe(
+ $recipe["input_item_id"],
+ Item::jsonDeserialize($recipe["ingredient"]),
+ $recipe["output_item_id"]
+ ));
+ }
return $result;
}
diff --git a/src/crafting/FurnaceRecipeManager.php b/src/crafting/FurnaceRecipeManager.php
index 9cb9c6eef..15bdee2fe 100644
--- a/src/crafting/FurnaceRecipeManager.php
+++ b/src/crafting/FurnaceRecipeManager.php
@@ -65,4 +65,4 @@ final class FurnaceRecipeManager{
public function match(Item $input) : ?FurnaceRecipe{
return $this->furnaceRecipes[$input->getId() . ":" . $input->getMeta()] ?? $this->furnaceRecipes[$input->getId() . ":?"] ?? null;
}
-}
\ No newline at end of file
+}
diff --git a/src/crafting/FurnaceType.php b/src/crafting/FurnaceType.php
index 889150c45..079066b97 100644
--- a/src/crafting/FurnaceType.php
+++ b/src/crafting/FurnaceType.php
@@ -24,6 +24,10 @@ declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\utils\EnumTrait;
+use pocketmine\world\sound\BlastFurnaceSound;
+use pocketmine\world\sound\FurnaceSound;
+use pocketmine\world\sound\SmokerSound;
+use pocketmine\world\sound\Sound;
/**
* This doc-block is generated automatically, do not modify it manually.
@@ -42,15 +46,17 @@ final class FurnaceType{
protected static function setup() : void{
self::registerAll(
- new self("furnace", 200),
- new self("blast_furnace", 100),
- new self("smoker", 100),
+ new self("furnace", 200, new FurnaceSound()),
+ new self("blast_furnace", 100, new BlastFurnaceSound()),
+ new self("smoker", 100, new SmokerSound()),
);
}
- private function __construct(string $enumName, private int $cookDurationTicks){
+ private function __construct(string $enumName, private int $cookDurationTicks, private Sound $cookSound){
$this->Enum___construct($enumName);
}
public function getCookDurationTicks() : int{ return $this->cookDurationTicks; }
+
+ public function getCookSound() : Sound{ return $this->cookSound; }
}
diff --git a/src/crafting/PotionContainerChangeRecipe.php b/src/crafting/PotionContainerChangeRecipe.php
new file mode 100644
index 000000000..4f4ec2cde
--- /dev/null
+++ b/src/crafting/PotionContainerChangeRecipe.php
@@ -0,0 +1,54 @@
+ingredient = clone $ingredient;
+ }
+
+ public function getInputItemId() : int{
+ return $this->inputItemId;
+ }
+
+ public function getIngredient() : Item{
+ return clone $this->ingredient;
+ }
+
+ public function getOutputItemId() : int{
+ return $this->outputItemId;
+ }
+
+ public function getResultFor(Item $input) : ?Item{
+ return $input->getId() === $this->getInputItemId() ? ItemFactory::getInstance()->get($this->getOutputItemId(), $input->getMeta()) : null;
+ }
+}
diff --git a/src/crafting/PotionTypeRecipe.php b/src/crafting/PotionTypeRecipe.php
new file mode 100644
index 000000000..aa1604d6f
--- /dev/null
+++ b/src/crafting/PotionTypeRecipe.php
@@ -0,0 +1,55 @@
+input = clone $input;
+ $this->ingredient = clone $ingredient;
+ $this->output = clone $output;
+ }
+
+ public function getInput() : Item{
+ return clone $this->input;
+ }
+
+ public function getIngredient() : Item{
+ return clone $this->ingredient;
+ }
+
+ public function getOutput() : Item{
+ return clone $this->output;
+ }
+
+ public function getResultFor(Item $input) : ?Item{
+ return $input->equals($this->input, true, false) ? $this->getOutput() : null;
+ }
+}
diff --git a/src/crafting/ShapedRecipe.php b/src/crafting/ShapedRecipe.php
index e346f22ad..fc02224a9 100644
--- a/src/crafting/ShapedRecipe.php
+++ b/src/crafting/ShapedRecipe.php
@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\utils\Utils;
use function array_values;
use function count;
@@ -62,14 +62,14 @@ class ShapedRecipe implements CraftingRecipe{
*/
public function __construct(array $shape, array $ingredients, array $results){
$this->height = count($shape);
- if($this->height > 3 or $this->height <= 0){
+ if($this->height > 3 || $this->height <= 0){
throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 rows, not $this->height");
}
$shape = array_values($shape);
$this->width = strlen($shape[0]);
- if($this->width > 3 or $this->width <= 0){
+ if($this->width > 3 || $this->width <= 0){
throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 columns, not $this->width");
}
@@ -79,7 +79,7 @@ class ShapedRecipe implements CraftingRecipe{
}
for($x = 0; $x < $this->width; ++$x){
- if($row[$x] !== ' ' and !isset($ingredients[$row[$x]])){
+ if($row[$x] !== ' ' && !isset($ingredients[$row[$x]])){
throw new \InvalidArgumentException("No item specified for symbol '" . $row[$x] . "'");
}
}
@@ -155,7 +155,7 @@ class ShapedRecipe implements CraftingRecipe{
public function getIngredient(int $x, int $y) : Item{
$exists = $this->ingredientList[$this->shape[$y][$x]] ?? null;
- return $exists !== null ? clone $exists : ItemFactory::air();
+ return $exists !== null ? clone $exists : VanillaItems::AIR();
}
/**
@@ -172,7 +172,7 @@ class ShapedRecipe implements CraftingRecipe{
$given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $x, $y);
$required = $this->getIngredient($x, $y);
- if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasNamedTag()) or $required->getCount() > $given->getCount()){
+ if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasNamedTag()) || $required->getCount() > $given->getCount()){
return false;
}
}
@@ -182,10 +182,10 @@ class ShapedRecipe implements CraftingRecipe{
}
public function matchesCraftingGrid(CraftingGrid $grid) : bool{
- if($this->width !== $grid->getRecipeWidth() or $this->height !== $grid->getRecipeHeight()){
+ if($this->width !== $grid->getRecipeWidth() || $this->height !== $grid->getRecipeHeight()){
return false;
}
- return $this->matchInputMap($grid, false) or $this->matchInputMap($grid, true);
+ return $this->matchInputMap($grid, false) || $this->matchInputMap($grid, true);
}
}
diff --git a/src/crafting/ShapelessRecipe.php b/src/crafting/ShapelessRecipe.php
index 195b917fa..4ef3bfc1a 100644
--- a/src/crafting/ShapelessRecipe.php
+++ b/src/crafting/ShapelessRecipe.php
@@ -85,7 +85,7 @@ class ShapelessRecipe implements CraftingRecipe{
foreach($this->ingredients as $needItem){
foreach($input as $j => $haveItem){
- if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasNamedTag()) and $haveItem->getCount() >= $needItem->getCount()){
+ if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasNamedTag()) && $haveItem->getCount() >= $needItem->getCount()){
unset($input[$j]);
continue 2;
}
diff --git a/src/crash/CrashDump.php b/src/crash/CrashDump.php
new file mode 100644
index 000000000..395f11a3f
--- /dev/null
+++ b/src/crash/CrashDump.php
@@ -0,0 +1,288 @@
+server = $server;
+ $this->pluginManager = $pluginManager;
+
+ $this->data = new CrashDumpData();
+ $this->data->format_version = self::FORMAT_VERSION;
+ $this->data->time = $now;
+ $this->data->uptime = $now - $this->server->getStartTime();
+
+ $this->baseCrash();
+ $this->generalData();
+ $this->pluginsData();
+
+ $this->extraData();
+
+ $json = json_encode($this->data, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
+ $this->encodedData = Utils::assumeNotFalse(zlib_encode($json, ZLIB_ENCODING_DEFLATE, 9), "ZLIB compression failed");
+ }
+
+ public function getEncodedData() : string{
+ return $this->encodedData;
+ }
+
+ public function getData() : CrashDumpData{
+ return $this->data;
+ }
+
+ public function encodeData(CrashDumpRenderer $renderer) : void{
+ $renderer->addLine();
+ $renderer->addLine("----------------------REPORT THE DATA BELOW THIS LINE-----------------------");
+ $renderer->addLine();
+ $renderer->addLine("===BEGIN CRASH DUMP===");
+ foreach(str_split(base64_encode($this->encodedData), 76) as $line){
+ $renderer->addLine($line);
+ }
+ $renderer->addLine("===END CRASH DUMP===");
+ }
+
+ private function pluginsData() : void{
+ if($this->pluginManager !== null){
+ $plugins = $this->pluginManager->getPlugins();
+ ksort($plugins, SORT_STRING);
+ foreach($plugins as $p){
+ $d = $p->getDescription();
+ $this->data->plugins[$d->getName()] = new CrashDumpDataPluginEntry(
+ name: $d->getName(),
+ version: $d->getVersion(),
+ authors: $d->getAuthors(),
+ api: $d->getCompatibleApis(),
+ enabled: $p->isEnabled(),
+ depends: $d->getDepend(),
+ softDepends: $d->getSoftDepend(),
+ main: $d->getMain(),
+ load: mb_strtoupper($d->getOrder()->name()),
+ website: $d->getWebsite()
+ );
+ }
+ }
+ }
+
+ private function extraData() : void{
+ global $argv;
+
+ if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-settings", true)){
+ $this->data->parameters = (array) $argv;
+ if(($serverDotProperties = @file_get_contents(Path::join($this->server->getDataPath(), "server.properties"))) !== false){
+ $this->data->serverDotProperties = preg_replace("#^rcon\\.password=(.*)$#m", "rcon.password=******", $serverDotProperties) ?? throw new AssumptionFailedError("Pattern is valid");
+ }
+ if(($pocketmineDotYml = @file_get_contents(Path::join($this->server->getDataPath(), "pocketmine.yml"))) !== false){
+ $this->data->pocketmineDotYml = $pocketmineDotYml;
+ }
+ }
+ $extensions = [];
+ foreach(get_loaded_extensions() as $ext){
+ $version = phpversion($ext);
+ $extensions[$ext] = $version !== false ? $version : "**UNKNOWN**";
+ }
+ $this->data->extensions = $extensions;
+
+ if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-phpinfo", true)){
+ ob_start();
+ phpinfo();
+ $this->data->phpinfo = ob_get_contents(); // @phpstan-ignore-line
+ ob_end_clean();
+ }
+ }
+
+ private function baseCrash() : void{
+ global $lastExceptionError, $lastError;
+
+ if(isset($lastExceptionError)){
+ $error = $lastExceptionError;
+ }else{
+ $error = error_get_last();
+ if($error === null){
+ throw new \RuntimeException("Crash error information missing - did something use exit()?");
+ }
+ $error["trace"] = Utils::currentTrace(3); //Skipping CrashDump->baseCrash, CrashDump->construct, Server->crashDump
+ $error["fullFile"] = $error["file"];
+ $error["file"] = Filesystem::cleanPath($error["file"]);
+ try{
+ $error["type"] = ErrorTypeToStringMap::get($error["type"]);
+ }catch(\InvalidArgumentException $e){
+ //pass
+ }
+ if(($pos = strpos($error["message"], "\n")) !== false){
+ $error["message"] = substr($error["message"], 0, $pos);
+ }
+ }
+
+ if(isset($lastError)){
+ if(isset($lastError["trace"])){
+ $lastError["trace"] = Utils::printableTrace($lastError["trace"]);
+ }
+ $this->data->lastError = $lastError;
+ }
+
+ $this->data->error = $error;
+ unset($this->data->error["fullFile"]);
+ unset($this->data->error["trace"]);
+
+ $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_NONE;
+ if(!$this->determinePluginFromFile($error["fullFile"], true)){ //fatal errors won't leave any stack trace
+ foreach($error["trace"] as $frame){
+ if(!isset($frame["file"])){
+ continue; //PHP core
+ }
+ if($this->determinePluginFromFile($frame["file"], false)){
+ break;
+ }
+ }
+ }
+
+ if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-code", true) && file_exists($error["fullFile"])){
+ $file = @file($error["fullFile"], FILE_IGNORE_NEW_LINES);
+ if($file !== false){
+ for($l = max(0, $error["line"] - 10); $l < $error["line"] + 10 && isset($file[$l]); ++$l){
+ $this->data->code[$l + 1] = $file[$l];
+ }
+ }
+ }
+
+ $this->data->trace = Utils::printableTrace($error["trace"]);
+ }
+
+ private function determinePluginFromFile(string $filePath, bool $crashFrame) : bool{
+ $frameCleanPath = Filesystem::cleanPath($filePath);
+ if(strpos($frameCleanPath, Filesystem::CLEAN_PATH_SRC_PREFIX) !== 0){
+ if($crashFrame){
+ $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_DIRECT;
+ }else{
+ $this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_INDIRECT;
+ }
+
+ if(file_exists($filePath)){
+ $reflection = new \ReflectionClass(PluginBase::class);
+ $file = $reflection->getProperty("file");
+ $file->setAccessible(true);
+ foreach($this->server->getPluginManager()->getPlugins() as $plugin){
+ $filePath = Filesystem::cleanPath($file->getValue($plugin));
+ if(strpos($frameCleanPath, $filePath) === 0){
+ $this->data->plugin = $plugin->getName();
+ break;
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private function generalData() : void{
+ $composerLibraries = [];
+ foreach(InstalledVersions::getInstalledPackages() as $package){
+ $composerLibraries[$package] = sprintf(
+ "%s@%s",
+ InstalledVersions::getPrettyVersion($package) ?? "unknown",
+ InstalledVersions::getReference($package) ?? "unknown"
+ );
+ }
+
+ $this->data->general = new CrashDumpDataGeneral(
+ name: $this->server->getName(),
+ base_version: VersionInfo::BASE_VERSION,
+ build: VersionInfo::BUILD_NUMBER(),
+ is_dev: VersionInfo::IS_DEVELOPMENT_BUILD,
+ protocol: ProtocolInfo::CURRENT_PROTOCOL,
+ git: VersionInfo::GIT_HASH(),
+ uname: php_uname("a"),
+ php: PHP_VERSION,
+ zend: zend_version(),
+ php_os: PHP_OS,
+ os: Utils::getOS(),
+ composer_libraries: $composerLibraries,
+ );
+ }
+}
diff --git a/src/crash/CrashDumpData.php b/src/crash/CrashDumpData.php
new file mode 100644
index 000000000..295517b0a
--- /dev/null
+++ b/src/crash/CrashDumpData.php
@@ -0,0 +1,84 @@
+
+ */
+ public array $plugins = [];
+
+ /** @var string[] */
+ public array $parameters = [];
+
+ public string $serverDotProperties = "";
+
+ public string $pocketmineDotYml = "";
+
+ /**
+ * @var string[]
+ * @phpstan-var array
+ */
+ public array $extensions = [];
+
+ public string $phpinfo = "";
+
+ public CrashDumpDataGeneral $general;
+
+ /**
+ * @return mixed[]
+ */
+ public function jsonSerialize() : array{
+ $result = (array) $this;
+ unset($result["serverDotProperties"]);
+ unset($result["pocketmineDotYml"]);
+ $result["pocketmine.yml"] = $this->pocketmineDotYml;
+ $result["server.properties"] = $this->serverDotProperties;
+ return $result;
+ }
+}
diff --git a/src/crash/CrashDumpDataGeneral.php b/src/crash/CrashDumpDataGeneral.php
new file mode 100644
index 000000000..0f7188364
--- /dev/null
+++ b/src/crash/CrashDumpDataGeneral.php
@@ -0,0 +1,46 @@
+ $composer_libraries
+ */
+ public function __construct(
+ public string $name,
+ public string $base_version,
+ public int $build,
+ public bool $is_dev,
+ public int $protocol,
+ public string $git,
+ public string $uname,
+ public string $php,
+ public string $zend,
+ public string $php_os,
+ public string $os,
+ public array $composer_libraries,
+ ){}
+}
diff --git a/src/crash/CrashDumpDataPluginEntry.php b/src/crash/CrashDumpDataPluginEntry.php
new file mode 100644
index 000000000..661c56ca2
--- /dev/null
+++ b/src/crash/CrashDumpDataPluginEntry.php
@@ -0,0 +1,45 @@
+addLine($this->data->general->name . " Crash Dump " . date("D M j H:i:s T Y", (int) $this->data->time));
+ $this->addLine();
+
+ $this->addLine("Error: " . $this->data->error["message"]);
+ $this->addLine("File: " . $this->data->error["file"]);
+ $this->addLine("Line: " . $this->data->error["line"]);
+ $this->addLine("Type: " . $this->data->error["type"]);
+
+ if($this->data->plugin_involvement !== CrashDump::PLUGIN_INVOLVEMENT_NONE){
+ $this->addLine();
+ $this->addLine(match($this->data->plugin_involvement){
+ CrashDump::PLUGIN_INVOLVEMENT_DIRECT => "THIS CRASH WAS CAUSED BY A PLUGIN",
+ CrashDump::PLUGIN_INVOLVEMENT_INDIRECT => "A PLUGIN WAS INVOLVED IN THIS CRASH",
+ default => "Unknown plugin involvement!"
+ });
+ }
+ if($this->data->plugin !== ""){
+ $this->addLine("BAD PLUGIN: " . $this->data->plugin);
+ }
+
+ $this->addLine();
+ $this->addLine("Code:");
+
+ foreach($this->data->code as $lineNumber => $line){
+ $this->addLine("[$lineNumber] $line");
+ }
+
+ $this->addLine();
+ $this->addLine("Backtrace:");
+ foreach($this->data->trace as $line){
+ $this->addLine($line);
+ }
+ $this->addLine();
+
+ $version = new VersionString($this->data->general->base_version, $this->data->general->is_dev, $this->data->general->build);
+
+ $this->addLine($this->data->general->name . " version: " . $version->getFullVersion(true) . " [Protocol " . $this->data->general->protocol . "]");
+ $this->addLine("Git commit: " . $this->data->general->git);
+ $this->addLine("uname -a: " . $this->data->general->uname);
+ $this->addLine("PHP Version: " . $this->data->general->php);
+ $this->addLine("Zend version: " . $this->data->general->zend);
+ $this->addLine("OS: " . $this->data->general->php_os . ", " . $this->data->general->os);
+ $this->addLine("Composer libraries: ");
+ foreach(Utils::stringifyKeys($this->data->general->composer_libraries) as $library => $libraryVersion){
+ $this->addLine("- $library $libraryVersion");
+ }
+
+ if(count($this->data->plugins) > 0){
+ $this->addLine();
+ $this->addLine("Loaded plugins:");
+ foreach($this->data->plugins as $p){
+ $this->addLine($p->name . " " . $p->version . " by " . implode(", ", $p->authors) . " for API(s) " . implode(", ", $p->api));
+ }
+ }
+ }
+
+ public function addLine(string $line = "") : void{
+ fwrite($this->fp, $line . PHP_EOL);
+ }
+}
diff --git a/src/data/SavedDataLoadingException.php b/src/data/SavedDataLoadingException.php
new file mode 100644
index 000000000..03dfa392b
--- /dev/null
+++ b/src/data/SavedDataLoadingException.php
@@ -0,0 +1,28 @@
+ $legacyId){
- if(!is_string($stringId) or !is_int($legacyId)){
+ if(!is_string($stringId) || !is_int($legacyId)){
throw new AssumptionFailedError("ID map should have string keys and int values");
}
$this->legacyToString[$legacyId] = $stringId;
diff --git a/src/entity/Attribute.php b/src/entity/Attribute.php
index 589a5da47..3f9bc7faf 100644
--- a/src/entity/Attribute.php
+++ b/src/entity/Attribute.php
@@ -65,7 +65,7 @@ class Attribute{
protected $desynchronized = true;
public function __construct(string $id, float $minValue, float $maxValue, float $defaultValue, bool $shouldSend = true){
- if($minValue > $maxValue or $defaultValue > $maxValue or $defaultValue < $minValue){
+ if($minValue > $maxValue || $defaultValue > $maxValue || $defaultValue < $minValue){
throw new \InvalidArgumentException("Invalid ranges: min value: $minValue, max value: $maxValue, $defaultValue: $defaultValue");
}
$this->id = $id;
@@ -123,7 +123,7 @@ class Attribute{
* @return $this
*/
public function setDefaultValue(float $defaultValue){
- if($defaultValue > $this->getMaxValue() or $defaultValue < $this->getMinValue()){
+ if($defaultValue > $this->getMaxValue() || $defaultValue < $this->getMinValue()){
throw new \InvalidArgumentException("Default $defaultValue is outside the range " . $this->getMinValue() . " - " . $this->getMaxValue());
}
@@ -146,7 +146,7 @@ class Attribute{
* @return $this
*/
public function setValue(float $value, bool $fit = false, bool $forceSend = false){
- if($value > $this->getMaxValue() or $value < $this->getMinValue()){
+ if($value > $this->getMaxValue() || $value < $this->getMinValue()){
if(!$fit){
throw new \InvalidArgumentException("Value $value is outside the range " . $this->getMinValue() . " - " . $this->getMaxValue());
}
@@ -172,7 +172,7 @@ class Attribute{
}
public function isDesynchronized() : bool{
- return $this->shouldSend and $this->desynchronized;
+ return $this->shouldSend && $this->desynchronized;
}
public function markSynchronized(bool $synced = true) : void{
diff --git a/src/entity/AttributeMap.php b/src/entity/AttributeMap.php
index 0a4a11d3d..cc427d929 100644
--- a/src/entity/AttributeMap.php
+++ b/src/entity/AttributeMap.php
@@ -49,7 +49,7 @@ class AttributeMap{
*/
public function needSend() : array{
return array_filter($this->attributes, function(Attribute $attribute) : bool{
- return $attribute->isSyncable() and $attribute->isDesynchronized();
+ return $attribute->isSyncable() && $attribute->isDesynchronized();
});
}
}
diff --git a/src/entity/Entity.php b/src/entity/Entity.php
index b1680fdb2..417253b98 100644
--- a/src/entity/Entity.php
+++ b/src/entity/Entity.php
@@ -227,9 +227,9 @@ abstract class Entity{
$this->location = $location->asLocation();
assert(
- !is_nan($this->location->x) and !is_infinite($this->location->x) and
- !is_nan($this->location->y) and !is_infinite($this->location->y) and
- !is_nan($this->location->z) and !is_infinite($this->location->z)
+ !is_nan($this->location->x) && !is_infinite($this->location->x) &&
+ !is_nan($this->location->y) && !is_infinite($this->location->y) &&
+ !is_nan($this->location->z) && !is_infinite($this->location->z)
);
$this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0);
@@ -304,12 +304,8 @@ abstract class Entity{
if($value <= 0){
throw new \InvalidArgumentException("Scale must be greater than 0");
}
- $this->size = $this->getInitialSizeInfo()->scale($value);
-
$this->scale = $value;
-
- $this->recalculateBoundingBox();
- $this->networkPropertiesDirty = true;
+ $this->setSize($this->getInitialSizeInfo()->scale($value));
}
public function getBoundingBox() : AxisAlignedBB{
@@ -329,6 +325,16 @@ abstract class Entity{
);
}
+ public function getSize() : EntitySizeInfo{
+ return $this->size;
+ }
+
+ protected function setSize(EntitySizeInfo $size) : void{
+ $this->size = $size;
+ $this->recalculateBoundingBox();
+ $this->networkPropertiesDirty = true;
+ }
+
public function isImmobile() : bool{
return $this->immobile;
}
@@ -584,7 +590,7 @@ abstract class Entity{
$this->health = 0;
}
}
- }elseif($amount <= $this->getMaxHealth() or $amount < $this->health){
+ }elseif($amount <= $this->getMaxHealth() || $amount < $this->health){
$this->health = $amount;
}else{
$this->health = $this->getMaxHealth();
@@ -635,13 +641,13 @@ abstract class Entity{
$this->checkBlockIntersections();
- if($this->location->y <= -16 and $this->isAlive()){
+ if($this->location->y <= World::Y_MIN - 16 && $this->isAlive()){
$ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10);
$this->attack($ev);
$hasUpdate = true;
}
- if($this->isOnFire() and $this->doOnFireTick($tickDiff)){
+ if($this->isOnFire() && $this->doOnFireTick($tickDiff)){
$hasUpdate = true;
}
@@ -677,14 +683,16 @@ abstract class Entity{
* @throws \InvalidArgumentException
*/
public function setFireTicks(int $fireTicks) : void{
- if($fireTicks < 0 or $fireTicks > 0x7fff){
+ if($fireTicks < 0 || $fireTicks > 0x7fff){
throw new \InvalidArgumentException("Fire ticks must be in range 0 ... " . 0x7fff . ", got $fireTicks");
}
$this->fireTicks = $fireTicks;
+ $this->networkPropertiesDirty = true;
}
public function extinguish() : void{
$this->fireTicks = 0;
+ $this->networkPropertiesDirty = true;
}
public function isFireProof() : bool{
@@ -692,13 +700,13 @@ abstract class Entity{
}
protected function doOnFireTick(int $tickDiff = 1) : bool{
- if($this->isFireProof() and $this->fireTicks > 1){
+ if($this->isFireProof() && $this->fireTicks > 1){
$this->fireTicks = 1;
}else{
$this->fireTicks -= $tickDiff;
}
- if(($this->fireTicks % 20 === 0) or $tickDiff > 20){
+ if(($this->fireTicks % 20 === 0) || $tickDiff > 20){
$this->dealFireDamage();
}
@@ -720,7 +728,7 @@ abstract class Entity{
}
public function canCollideWith(Entity $entity) : bool{
- return !$this->justCreated and $entity !== $this;
+ return !$this->justCreated && $entity !== $this;
}
public function canBeCollidedWith() : bool{
@@ -740,13 +748,13 @@ abstract class Entity{
$this->setImmobile($still);
}
- if($teleport or $diffPosition > 0.0001 or $diffRotation > 1.0 or (!$wasStill and $still)){
+ if($teleport || $diffPosition > 0.0001 || $diffRotation > 1.0 || (!$wasStill && $still)){
$this->lastLocation = $this->location->asLocation();
$this->broadcastMovement($teleport);
}
- if($diffMotion > 0.0025 or $wasStill !== $still){ //0.05 ** 2
+ if($diffMotion > 0.0025 || $wasStill !== $still){ //0.05 ** 2
$this->lastMotion = clone $this->motion;
$this->broadcastMotion();
@@ -830,10 +838,10 @@ abstract class Entity{
$diffZ = $z - $floorZ;
if($world->getBlockAt($floorX, $floorY, $floorZ)->isSolid()){
- $westNonSolid = !$world->getBlockAt($floorX - 1, $floorY, $floorZ)->isSolid();
- $eastNonSolid = !$world->getBlockAt($floorX + 1, $floorY, $floorZ)->isSolid();
- $downNonSolid = !$world->getBlockAt($floorX, $floorY - 1, $floorZ)->isSolid();
- $upNonSolid = !$world->getBlockAt($floorX, $floorY + 1, $floorZ)->isSolid();
+ $westNonSolid = !$world->getBlockAt($floorX - 1, $floorY, $floorZ)->isSolid();
+ $eastNonSolid = !$world->getBlockAt($floorX + 1, $floorY, $floorZ)->isSolid();
+ $downNonSolid = !$world->getBlockAt($floorX, $floorY - 1, $floorZ)->isSolid();
+ $upNonSolid = !$world->getBlockAt($floorX, $floorY + 1, $floorZ)->isSolid();
$northNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ - 1)->isSolid();
$southNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ + 1)->isSolid();
@@ -845,27 +853,27 @@ abstract class Entity{
$direction = Facing::WEST;
}
- if($eastNonSolid and 1 - $diffX < $limit){
+ if($eastNonSolid && 1 - $diffX < $limit){
$limit = 1 - $diffX;
$direction = Facing::EAST;
}
- if($downNonSolid and $diffY < $limit){
+ if($downNonSolid && $diffY < $limit){
$limit = $diffY;
$direction = Facing::DOWN;
}
- if($upNonSolid and 1 - $diffY < $limit){
+ if($upNonSolid && 1 - $diffY < $limit){
$limit = 1 - $diffY;
$direction = Facing::UP;
}
- if($northNonSolid and $diffZ < $limit){
+ if($northNonSolid && $diffZ < $limit){
$limit = $diffZ;
$direction = Facing::NORTH;
}
- if($southNonSolid and 1 - $diffZ < $limit){
+ if($southNonSolid && 1 - $diffZ < $limit){
$direction = Facing::SOUTH;
}
@@ -895,13 +903,13 @@ abstract class Entity{
$angle += 360.0;
}
- if((0 <= $angle and $angle < 45) or (315 <= $angle and $angle < 360)){
+ if((0 <= $angle && $angle < 45) || (315 <= $angle && $angle < 360)){
return Facing::SOUTH;
}
- if(45 <= $angle and $angle < 135){
+ if(45 <= $angle && $angle < 135){
return Facing::WEST;
}
- if(135 <= $angle and $angle < 225){
+ if(135 <= $angle && $angle < 225){
return Facing::NORTH;
}
@@ -956,7 +964,7 @@ abstract class Entity{
abs($this->motion->z) <= self::MOTION_THRESHOLD ? 0 : null
);
- if($this->motion->x != 0 or $this->motion->y != 0 or $this->motion->z != 0){
+ if($this->motion->x != 0 || $this->motion->y != 0 || $this->motion->z != 0){
$this->move($this->motion->x, $this->motion->y, $this->motion->z);
}
@@ -972,13 +980,13 @@ abstract class Entity{
$this->timings->stopTiming();
//if($this->isStatic())
- return ($hasUpdate or $this->hasMovementUpdate());
+ return ($hasUpdate || $this->hasMovementUpdate());
//return !($this instanceof Player);
}
final public function scheduleUpdate() : void{
if($this->closed){
- throw new \InvalidStateException("Cannot schedule update on garbage entity " . get_class($this));
+ throw new \LogicException("Cannot schedule update on garbage entity " . get_class($this));
}
$this->getWorld()->updateEntities[$this->id] = $this;
}
@@ -1011,10 +1019,10 @@ abstract class Entity{
*/
public function hasMovementUpdate() : bool{
return (
- $this->forceMovementUpdate or
- $this->motion->x != 0 or
- $this->motion->y != 0 or
- $this->motion->z != 0 or
+ $this->forceMovementUpdate ||
+ $this->motion->x != 0 ||
+ $this->motion->y != 0 ||
+ $this->motion->z != 0 ||
!$this->onGround
);
}
@@ -1087,7 +1095,7 @@ abstract class Entity{
public function isInsideOfSolid() : bool{
$block = $this->getWorld()->getBlockAt((int) floor($this->location->x), (int) floor($y = ($this->location->y + $this->getEyeHeight())), (int) floor($this->location->z));
- return $block->isSolid() and !$block->isTransparent() and $block->collidesWithBB($this->getBoundingBox());
+ return $block->isSolid() && !$block->isTransparent() && $block->collidesWithBB($this->getBoundingBox());
}
protected function move(float $dx, float $dy, float $dz) : void{
@@ -1106,7 +1114,7 @@ abstract class Entity{
$moveBB = clone $this->boundingBox;
- assert(abs($dx) <= 20 and abs($dy) <= 20 and abs($dz) <= 20, "Movement distance is excessive: dx=$dx, dy=$dy, dz=$dz");
+ assert(abs($dx) <= 20 && abs($dy) <= 20 && abs($dz) <= 20, "Movement distance is excessive: dx=$dx, dy=$dy, dz=$dz");
$list = $this->getWorld()->getCollisionBoxes($this, $moveBB->addCoord($dx, $dy, $dz), false);
@@ -1116,7 +1124,7 @@ abstract class Entity{
$moveBB->offset(0, $dy, 0);
- $fallingFlag = ($this->onGround or ($dy != $wantedY and $wantedY < 0));
+ $fallingFlag = ($this->onGround || ($dy != $wantedY && $wantedY < 0));
foreach($list as $bb){
$dx = $bb->calculateXOffset($moveBB, $dx);
@@ -1130,7 +1138,7 @@ abstract class Entity{
$moveBB->offset(0, 0, $dz);
- if($this->stepHeight > 0 and $fallingFlag and ($wantedX != $dx or $wantedZ != $dz)){
+ if($this->stepHeight > 0 && $fallingFlag && ($wantedX != $dx || $wantedZ != $dz)){
$cx = $dx;
$cy = $dy;
$cz = $dz;
@@ -1183,9 +1191,9 @@ abstract class Entity{
($this->boundingBox->minX + $this->boundingBox->maxX) / 2,
$this->boundingBox->minY - $this->ySize,
($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2,
+ $this->location->world,
$this->location->yaw,
- $this->location->pitch,
- $this->location->world
+ $this->location->pitch
);
$this->getWorld()->onEntityMoved($this);
@@ -1206,9 +1214,33 @@ abstract class Entity{
protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{
$this->isCollidedVertically = $wantedY != $dy;
- $this->isCollidedHorizontally = ($wantedX != $dx or $wantedZ != $dz);
- $this->isCollided = ($this->isCollidedHorizontally or $this->isCollidedVertically);
- $this->onGround = ($wantedY != $dy and $wantedY < 0);
+ $this->isCollidedHorizontally = ($wantedX != $dx || $wantedZ != $dz);
+ $this->isCollided = ($this->isCollidedHorizontally || $this->isCollidedVertically);
+ $this->onGround = ($wantedY != $dy && $wantedY < 0);
+ }
+
+ /**
+ * Yields all the blocks whose full-cube areas are intersected by the entity's AABB.
+ *
+ * @phpstan-return \Generator
+ */
+ protected function getBlocksIntersected(float $inset) : \Generator{
+ $minX = (int) floor($this->boundingBox->minX + $inset);
+ $minY = (int) floor($this->boundingBox->minY + $inset);
+ $minZ = (int) floor($this->boundingBox->minZ + $inset);
+ $maxX = (int) floor($this->boundingBox->maxX - $inset);
+ $maxY = (int) floor($this->boundingBox->maxY - $inset);
+ $maxZ = (int) floor($this->boundingBox->maxZ - $inset);
+
+ $world = $this->getWorld();
+
+ for($z = $minZ; $z <= $maxZ; ++$z){
+ for($x = $minX; $x <= $maxX; ++$x){
+ for($y = $minY; $y <= $maxY; ++$y){
+ yield $world->getBlockAt($x, $y, $z);
+ }
+ }
+ }
}
/**
@@ -1216,27 +1248,12 @@ abstract class Entity{
*/
protected function getBlocksAroundWithEntityInsideActions() : array{
if($this->blocksAround === null){
- $inset = 0.001; //Offset against floating-point errors
-
- $minX = (int) floor($this->boundingBox->minX + $inset);
- $minY = (int) floor($this->boundingBox->minY + $inset);
- $minZ = (int) floor($this->boundingBox->minZ + $inset);
- $maxX = (int) floor($this->boundingBox->maxX - $inset);
- $maxY = (int) floor($this->boundingBox->maxY - $inset);
- $maxZ = (int) floor($this->boundingBox->maxZ - $inset);
-
$this->blocksAround = [];
- $world = $this->getWorld();
-
- for($z = $minZ; $z <= $maxZ; ++$z){
- for($x = $minX; $x <= $maxX; ++$x){
- for($y = $minY; $y <= $maxY; ++$y){
- $block = $world->getBlockAt($x, $y, $z);
- if($block->hasEntityCollision()){
- $this->blocksAround[] = $block;
- }
- }
+ $inset = 0.001; //Offset against floating-point errors
+ foreach($this->getBlocksIntersected($inset) as $block){
+ if($block->hasEntityCollision()){
+ $this->blocksAround[] = $block;
}
}
}
@@ -1416,20 +1433,21 @@ abstract class Entity{
* Called by spawnTo() to send whatever packets needed to spawn the entity to the client.
*/
protected function sendSpawnPacket(Player $player) : void{
- $pk = new AddActorPacket();
- $pk->entityRuntimeId = $this->getId();
- $pk->type = static::getNetworkTypeId();
- $pk->position = $this->location->asVector3();
- $pk->motion = $this->getMotion();
- $pk->yaw = $this->location->yaw;
- $pk->headYaw = $this->location->yaw; //TODO
- $pk->pitch = $this->location->pitch;
- $pk->attributes = array_map(function(Attribute $attr) : NetworkAttribute{
- return new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue());
- }, $this->attributeMap->getAll());
- $pk->metadata = $this->getAllNetworkData();
-
- $player->getNetworkSession()->sendDataPacket($pk);
+ $player->getNetworkSession()->sendDataPacket(AddActorPacket::create(
+ $this->getId(), //TODO: actor unique ID
+ $this->getId(),
+ static::getNetworkTypeId(),
+ $this->location->asVector3(),
+ $this->getMotion(),
+ $this->location->pitch,
+ $this->location->yaw,
+ $this->location->yaw, //TODO: head yaw
+ array_map(function(Attribute $attr) : NetworkAttribute{
+ return new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue());
+ }, $this->attributeMap->getAll()),
+ $this->getAllNetworkData(),
+ [] //TODO: entity links
+ ));
}
public function spawnTo(Player $player) : void{
@@ -1437,7 +1455,7 @@ abstract class Entity{
//TODO: this will cause some visible lag during chunk resends; if the player uses a spawn egg in a chunk, the
//created entity won't be visible until after the resend arrives. However, this is better than possibly crashing
//the player by sending them entities too early.
- if(!isset($this->hasSpawned[$id]) and $player->hasReceivedChunk($this->location->getFloorX() >> Chunk::COORD_BIT_SIZE, $this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
+ if(!isset($this->hasSpawned[$id]) && $player->getWorld() === $this->getWorld() && $player->hasReceivedChunk($this->location->getFloorX() >> Chunk::COORD_BIT_SIZE, $this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
$this->hasSpawned[$id] = $player;
$this->sendSpawnPacket($player);
@@ -1586,8 +1604,8 @@ abstract class Entity{
protected function syncNetworkData(EntityMetadataCollection $properties) : void{
$properties->setByte(EntityMetadataProperties::ALWAYS_SHOW_NAMETAG, $this->alwaysShowNameTag ? 1 : 0);
- $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_HEIGHT, $this->size->getHeight());
- $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_WIDTH, $this->size->getWidth());
+ $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_HEIGHT, $this->size->getHeight() / $this->scale);
+ $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_WIDTH, $this->size->getWidth() / $this->scale);
$properties->setFloat(EntityMetadataProperties::SCALE, $this->scale);
$properties->setLong(EntityMetadataProperties::LEAD_HOLDER_EID, -1);
$properties->setLong(EntityMetadataProperties::OWNER_EID, $this->ownerId ?? -1);
@@ -1596,7 +1614,7 @@ abstract class Entity{
$properties->setString(EntityMetadataProperties::SCORE_TAG, $this->scoreTag);
$properties->setByte(EntityMetadataProperties::COLOR, 0);
- $properties->setGenericFlag(EntityMetadataFlags::AFFECTED_BY_GRAVITY, true);
+ $properties->setGenericFlag(EntityMetadataFlags::AFFECTED_BY_GRAVITY, $this->gravityEnabled);
$properties->setGenericFlag(EntityMetadataFlags::CAN_CLIMB, $this->canClimb);
$properties->setGenericFlag(EntityMetadataFlags::CAN_SHOW_NAMETAG, $this->nameTagVisible);
$properties->setGenericFlag(EntityMetadataFlags::HAS_COLLISION, true);
diff --git a/src/entity/EntityDataHelper.php b/src/entity/EntityDataHelper.php
index f58fef743..f01a10bb7 100644
--- a/src/entity/EntityDataHelper.php
+++ b/src/entity/EntityDataHelper.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\entity;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\math\Vector3;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\CompoundTag;
@@ -38,34 +39,40 @@ final class EntityDataHelper{
//NOOP
}
+ /**
+ * @throws SavedDataLoadingException
+ */
public static function parseLocation(CompoundTag $nbt, World $world) : Location{
$pos = self::parseVec3($nbt, "Pos", false);
$yawPitch = $nbt->getTag("Rotation");
- if(!($yawPitch instanceof ListTag) or $yawPitch->getTagType() !== NBT::TAG_Float){
- throw new \UnexpectedValueException("'Rotation' should be a List");
+ if(!($yawPitch instanceof ListTag) || $yawPitch->getTagType() !== NBT::TAG_Float){
+ throw new SavedDataLoadingException("'Rotation' should be a List");
}
/** @var FloatTag[] $values */
$values = $yawPitch->getValue();
if(count($values) !== 2){
- throw new \UnexpectedValueException("Expected exactly 2 entries for 'Rotation'");
+ throw new SavedDataLoadingException("Expected exactly 2 entries for 'Rotation'");
}
return Location::fromObject($pos, $world, $values[0]->getValue(), $values[1]->getValue());
}
+ /**
+ * @throws SavedDataLoadingException
+ */
public static function parseVec3(CompoundTag $nbt, string $tagName, bool $optional) : Vector3{
$pos = $nbt->getTag($tagName);
- if($pos === null and $optional){
+ if($pos === null && $optional){
return new Vector3(0, 0, 0);
}
- if(!($pos instanceof ListTag) or $pos->getTagType() !== NBT::TAG_Double){
- throw new \UnexpectedValueException("'$tagName' should be a List");
+ if(!($pos instanceof ListTag) || ($pos->getTagType() !== NBT::TAG_Double && $pos->getTagType() !== NBT::TAG_Float)){
+ throw new SavedDataLoadingException("'$tagName' should be a List or List");
}
- /** @var DoubleTag[] $values */
+ /** @var DoubleTag[]|FloatTag[] $values */
$values = $pos->getValue();
if(count($values) !== 3){
- throw new \UnexpectedValueException("Expected exactly 3 entries in '$tagName' tag");
+ throw new SavedDataLoadingException("Expected exactly 3 entries in '$tagName' tag");
}
return new Vector3($values[0]->getValue(), $values[1]->getValue(), $values[2]->getValue());
}
diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php
index 809d32341..0010f0ace 100644
--- a/src/entity/EntityFactory.php
+++ b/src/entity/EntityFactory.php
@@ -30,6 +30,7 @@ use pocketmine\block\BlockFactory;
use pocketmine\data\bedrock\EntityLegacyIds;
use pocketmine\data\bedrock\PotionTypeIdMap;
use pocketmine\data\bedrock\PotionTypeIds;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\object\ExperienceOrb;
use pocketmine\entity\object\FallingBlock;
use pocketmine\entity\object\ItemEntity;
@@ -45,7 +46,7 @@ use pocketmine\entity\projectile\SplashPotion;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
-use pocketmine\nbt\NbtDataException;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
@@ -113,12 +114,12 @@ final class EntityFactory{
$this->register(ItemEntity::class, function(World $world, CompoundTag $nbt) : ItemEntity{
$itemTag = $nbt->getCompoundTag("Item");
if($itemTag === null){
- throw new \UnexpectedValueException("Expected \"Item\" NBT tag not found");
+ throw new SavedDataLoadingException("Expected \"Item\" NBT tag not found");
}
$item = Item::nbtDeserialize($itemTag);
if($item->isNull()){
- throw new \UnexpectedValueException("Item is invalid");
+ throw new SavedDataLoadingException("Item is invalid");
}
return new ItemEntity(EntityDataHelper::parseLocation($nbt, $world), $item, $nbt);
}, ['Item', 'minecraft:item'], EntityLegacyIds::ITEM);
@@ -126,7 +127,7 @@ final class EntityFactory{
$this->register(Painting::class, function(World $world, CompoundTag $nbt) : Painting{
$motive = PaintingMotive::getMotiveByName($nbt->getString("Motive"));
if($motive === null){
- throw new \UnexpectedValueException("Unknown painting motive");
+ throw new SavedDataLoadingException("Unknown painting motive");
}
$blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ"));
if(($directionTag = $nbt->getTag("Direction")) instanceof ByteTag){
@@ -134,7 +135,7 @@ final class EntityFactory{
}elseif(($facingTag = $nbt->getTag("Facing")) instanceof ByteTag){
$facing = Painting::DATA_TO_FACING[$facingTag->getValue()] ?? Facing::NORTH;
}else{
- throw new \UnexpectedValueException("Missing facing info");
+ throw new SavedDataLoadingException("Missing facing info");
}
return new Painting(EntityDataHelper::parseLocation($nbt, $world), $blockIn, $facing, $motive, $nbt);
@@ -151,7 +152,7 @@ final class EntityFactory{
$this->register(SplashPotion::class, function(World $world, CompoundTag $nbt) : SplashPotion{
$potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort("PotionId", PotionTypeIds::WATER));
if($potionType === null){
- throw new \UnexpectedValueException("No such potion type");
+ throw new SavedDataLoadingException("No such potion type");
}
return new SplashPotion(EntityDataHelper::parseLocation($nbt, $world), null, $potionType, $nbt);
}, ['ThrownPotion', 'minecraft:potion', 'thrownpotion'], EntityLegacyIds::SPLASH_POTION);
@@ -222,25 +223,28 @@ final class EntityFactory{
/**
* Creates an entity from data stored on a chunk.
*
- * @throws \RuntimeException
- * @throws NbtDataException
+ * @throws SavedDataLoadingException
* @internal
*/
public function createFromData(World $world, CompoundTag $nbt) : ?Entity{
- $saveId = $nbt->getTag("id") ?? $nbt->getTag("identifier");
- $func = null;
- if($saveId instanceof StringTag){
- $func = $this->creationFuncs[$saveId->getValue()] ?? null;
- }elseif($saveId instanceof IntTag){ //legacy MCPE format
- $func = $this->creationFuncs[$saveId->getValue() & 0xff] ?? null;
- }
- if($func === null){
- return null;
- }
- /** @var Entity $entity */
- $entity = $func($world, $nbt);
+ try{
+ $saveId = $nbt->getTag("id") ?? $nbt->getTag("identifier");
+ $func = null;
+ if($saveId instanceof StringTag){
+ $func = $this->creationFuncs[$saveId->getValue()] ?? null;
+ }elseif($saveId instanceof IntTag){ //legacy MCPE format
+ $func = $this->creationFuncs[$saveId->getValue() & 0xff] ?? null;
+ }
+ if($func === null){
+ return null;
+ }
+ /** @var Entity $entity */
+ $entity = $func($world, $nbt);
- return $entity;
+ return $entity;
+ }catch(NbtException $e){
+ throw new SavedDataLoadingException($e->getMessage(), 0, $e);
+ }
}
public function injectSaveId(string $class, CompoundTag $saveData) : void{
diff --git a/src/entity/ExperienceManager.php b/src/entity/ExperienceManager.php
index 7f4d46511..4ed1c5486 100644
--- a/src/entity/ExperienceManager.php
+++ b/src/entity/ExperienceManager.php
@@ -27,6 +27,7 @@ use pocketmine\entity\utils\ExperienceUtils;
use pocketmine\event\player\PlayerExperienceChangeEvent;
use pocketmine\item\Durable;
use pocketmine\item\enchantment\VanillaEnchantments;
+use pocketmine\utils\Limits;
use pocketmine\world\sound\XpCollectSound;
use pocketmine\world\sound\XpLevelUpSound;
use function array_rand;
@@ -48,6 +49,9 @@ class ExperienceManager{
/** @var int */
private $totalXp = 0;
+ /** @var bool */
+ private $canAttractXpOrbs = true;
+
/** @var int */
private $xpCooldown = 0;
@@ -141,7 +145,9 @@ class ExperienceManager{
public function setCurrentTotalXp(int $amount) : bool{
$newLevel = ExperienceUtils::getLevelFromXp($amount);
- return $this->setXpAndProgress((int) $newLevel, $newLevel - ((int) $newLevel));
+ $xpLevel = (int) $newLevel;
+ $xpProgress = $newLevel - (int) $newLevel;
+ return $this->setXpAndProgress($xpLevel, $xpProgress);
}
/**
@@ -151,6 +157,7 @@ class ExperienceManager{
* @param bool $playSound Whether to play level-up and XP gained sounds.
*/
public function addXp(int $amount, bool $playSound = true) : bool{
+ $amount = min($amount, Limits::INT32_MAX - $this->totalXp);
$oldLevel = $this->getXpLevel();
$oldTotal = $this->getCurrentTotalXp();
@@ -223,8 +230,8 @@ class ExperienceManager{
* score when they die. (TODO: add this when MCPE supports it)
*/
public function setLifetimeTotalXp(int $amount) : void{
- if($amount < 0){
- throw new \InvalidArgumentException("XP must be greater than 0");
+ if($amount < 0 || $amount > Limits::INT32_MAX){
+ throw new \InvalidArgumentException("XP must be greater than 0 and less than " . Limits::INT32_MAX);
}
$this->totalXp = $amount;
@@ -245,14 +252,14 @@ class ExperienceManager{
/** @var Durable[] $equipment */
$equipment = [];
- if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){
+ if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
$equipment[$mainHandIndex] = $item;
}
- if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){
+ if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
$equipment[$offHandIndex] = $item;
}
foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){
- if($armorItem instanceof Durable and $armorItem->hasEnchantment(VanillaEnchantments::MENDING())){
+ if($armorItem instanceof Durable && $armorItem->hasEnchantment(VanillaEnchantments::MENDING())){
$equipment[$k] = $armorItem;
}
}
@@ -290,4 +297,12 @@ class ExperienceManager{
$this->xpCooldown = max(0, $this->xpCooldown - $tickDiff);
}
}
+
+ public function canAttractXpOrbs() : bool{
+ return $this->canAttractXpOrbs;
+ }
+
+ public function setCanAttractXpOrbs(bool $v = true) : void{
+ $this->canAttractXpOrbs = $v;
+ }
}
diff --git a/src/entity/Human.php b/src/entity/Human.php
index 4768422ae..56da6570e 100644
--- a/src/entity/Human.php
+++ b/src/entity/Human.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\entity;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\animation\TotemUseAnimation;
use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
@@ -47,8 +48,10 @@ use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
+use pocketmine\network\mcpe\protocol\AdventureSettingsPacket;
use pocketmine\network\mcpe\protocol\PlayerListPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
+use pocketmine\network\mcpe\protocol\types\DeviceOS;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\network\mcpe\protocol\types\entity\StringMetadataProperty;
@@ -102,12 +105,12 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
/**
* @throws InvalidSkinException
- * @throws \UnexpectedValueException
+ * @throws SavedDataLoadingException
*/
public static function parseSkinNBT(CompoundTag $nbt) : Skin{
$skinTag = $nbt->getCompoundTag("Skin");
if($skinTag === null){
- throw new \UnexpectedValueException("Missing skin data");
+ throw new SavedDataLoadingException("Missing skin data");
}
return new Skin( //this throws if the skin is invalid
$skinTag->getString("Name"),
@@ -145,7 +148,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
*/
public function sendSkin(?array $targets = null) : void{
$this->server->broadcastPackets($targets ?? $this->hasSpawned, [
- PlayerSkinPacket::create($this->getUniqueId(), SkinAdapterSingleton::get()->toSkinData($this->skin))
+ PlayerSkinPacket::create($this->getUniqueId(), "", "", SkinAdapterSingleton::get()->toSkinData($this->skin))
]);
}
@@ -163,7 +166,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
public function consumeObject(Consumable $consumable) : bool{
- if($consumable instanceof FoodSource && $consumable->requiresHunger() and !$this->hungerManager->isHungry()){
+ if($consumable instanceof FoodSource && $consumable->requiresHunger() && !$this->hungerManager->isHungry()){
return false;
}
@@ -214,6 +217,19 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
}
+ /**
+ * @param Item[] $items
+ * @phpstan-param array $items
+ */
+ private static function populateInventoryFromListTag(Inventory $inventory, array $items) : void{
+ $listeners = $inventory->getListeners()->toArray();
+ $inventory->getListeners()->clear();
+
+ $inventory->setContents($items);
+
+ $inventory->getListeners()->add(...$listeners);
+ }
+
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
@@ -244,25 +260,23 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$inventoryTag = $nbt->getListTag("Inventory");
if($inventoryTag !== null){
- $armorListeners = $this->armorInventory->getListeners()->toArray();
- $this->armorInventory->getListeners()->clear();
- $inventoryListeners = $this->inventory->getListeners()->toArray();
- $this->inventory->getListeners()->clear();
+ $inventoryItems = [];
+ $armorInventoryItems = [];
/** @var CompoundTag $item */
foreach($inventoryTag as $i => $item){
$slot = $item->getByte("Slot");
- if($slot >= 0 and $slot < 9){ //Hotbar
+ if($slot >= 0 && $slot < 9){ //Hotbar
//Old hotbar saving stuff, ignore it
- }elseif($slot >= 100 and $slot < 104){ //Armor
- $this->armorInventory->setItem($slot - 100, Item::nbtDeserialize($item));
- }elseif($slot >= 9 and $slot < $this->inventory->getSize() + 9){
- $this->inventory->setItem($slot - 9, Item::nbtDeserialize($item));
+ }elseif($slot >= 100 && $slot < 104){ //Armor
+ $armorInventoryItems[$slot - 100] = Item::nbtDeserialize($item);
+ }elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){
+ $inventoryItems[$slot - 9] = Item::nbtDeserialize($item);
}
}
- $this->armorInventory->getListeners()->add(...$armorListeners);
- $this->inventory->getListeners()->add(...$inventoryListeners);
+ self::populateInventoryFromListTag($this->inventory, $inventoryItems);
+ self::populateInventoryFromListTag($this->armorInventory, $armorInventoryItems);
}
$offHand = $nbt->getCompoundTag("OffHandItem");
if($offHand !== null){
@@ -276,10 +290,13 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$enderChestInventoryTag = $nbt->getListTag("EnderChestInventory");
if($enderChestInventoryTag !== null){
+ $enderChestInventoryItems = [];
+
/** @var CompoundTag $item */
foreach($enderChestInventoryTag as $i => $item){
- $this->enderInventory->setItem($item->getByte("Slot"), Item::nbtDeserialize($item));
+ $enderChestInventoryItems[$item->getByte("Slot")] = Item::nbtDeserialize($item);
}
+ self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
}
$this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0));
@@ -323,8 +340,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
parent::applyDamageModifiers($source);
$type = $source->getCause();
- if($type !== EntityDamageEvent::CAUSE_SUICIDE and $type !== EntityDamageEvent::CAUSE_VOID
- and ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){
+ if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
+ && ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){
$compensation = $this->getHealth() - $source->getFinalDamage() - 1;
if($compensation < 0){
@@ -444,17 +461,24 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$player->getNetworkSession()->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), SkinAdapterSingleton::get()->toSkinData($this->skin))]));
}
- $pk = new AddPlayerPacket();
- $pk->uuid = $this->getUniqueId();
- $pk->username = $this->getName();
- $pk->entityRuntimeId = $this->getId();
- $pk->position = $this->location->asVector3();
- $pk->motion = $this->getMotion();
- $pk->yaw = $this->location->yaw;
- $pk->pitch = $this->location->pitch;
- $pk->item = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->getInventory()->getItemInHand()));
- $pk->metadata = $this->getAllNetworkData();
- $player->getNetworkSession()->sendDataPacket($pk);
+ $player->getNetworkSession()->sendDataPacket(AddPlayerPacket::create(
+ $this->getUniqueId(),
+ $this->getName(),
+ $this->getId(), //TODO: actor unique ID
+ $this->getId(),
+ "",
+ $this->location->asVector3(),
+ $this->getMotion(),
+ $this->location->pitch,
+ $this->location->yaw,
+ $this->location->yaw, //TODO: head yaw
+ ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->getInventory()->getItemInHand())),
+ $this->getAllNetworkData(),
+ AdventureSettingsPacket::create(0, 0, 0, 0, 0, $this->getId()), //TODO
+ [], //TODO: entity links
+ "", //device ID (we intentionally don't send this - secvuln)
+ DeviceOS::UNKNOWN //we intentionally don't send this (secvuln)
+ ));
//TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately
$this->sendData([$player], [EntityMetadataProperties::NAMETAG => new StringMetadataProperty($this->getNameTag())]);
diff --git a/src/entity/HungerManager.php b/src/entity/HungerManager.php
index f0e0e9d8e..4b95751a0 100644
--- a/src/entity/HungerManager.php
+++ b/src/entity/HungerManager.php
@@ -182,7 +182,7 @@ class HungerManager{
}
public function tick(int $tickDiff = 1) : void{
- if(!$this->entity->isAlive() or !$this->enabled){
+ if(!$this->entity->isAlive() || !$this->enabled){
return;
}
$food = $this->getFood();
@@ -194,12 +194,12 @@ class HungerManager{
$this->foodTickTimer = 0;
}
- if($difficulty === World::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){
+ if($difficulty === World::DIFFICULTY_PEACEFUL && $this->foodTickTimer % 10 === 0){
if($food < $this->getMaxFood()){
$this->addFood(1.0);
$food = $this->getFood();
}
- if($this->foodTickTimer % 20 === 0 and $health < $this->entity->getMaxHealth()){
+ if($this->foodTickTimer % 20 === 0 && $health < $this->entity->getMaxHealth()){
$this->entity->heal(new EntityRegainHealthEvent($this->entity, 1, EntityRegainHealthEvent::CAUSE_SATURATION));
}
}
@@ -211,7 +211,7 @@ class HungerManager{
$this->exhaust(3.0, PlayerExhaustEvent::CAUSE_HEALTH_REGEN);
}
}elseif($food <= 0){
- if(($difficulty === World::DIFFICULTY_EASY and $health > 10) or ($difficulty === World::DIFFICULTY_NORMAL and $health > 1) or $difficulty === World::DIFFICULTY_HARD){
+ if(($difficulty === World::DIFFICULTY_EASY && $health > 10) || ($difficulty === World::DIFFICULTY_NORMAL && $health > 1) || $difficulty === World::DIFFICULTY_HARD){
$this->entity->attack(new EntityDamageEvent($this->entity, EntityDamageEvent::CAUSE_STARVATION, 1));
}
}
diff --git a/src/entity/InvalidSkinException.php b/src/entity/InvalidSkinException.php
index 65ad5cf17..43aef8f10 100644
--- a/src/entity/InvalidSkinException.php
+++ b/src/entity/InvalidSkinException.php
@@ -25,4 +25,4 @@ namespace pocketmine\entity;
final class InvalidSkinException extends \InvalidArgumentException{
-}
\ No newline at end of file
+}
diff --git a/src/entity/Living.php b/src/entity/Living.php
index 401bd051d..889e55fa2 100644
--- a/src/entity/Living.php
+++ b/src/entity/Living.php
@@ -116,6 +116,10 @@ abstract class Living extends Entity{
protected $sprinting = false;
/** @var bool */
protected $sneaking = false;
+ /** @var bool */
+ protected $gliding = false;
+ /** @var bool */
+ protected $swimming = false;
abstract public function getName() : string;
@@ -183,7 +187,7 @@ abstract class Living extends Entity{
$wasAlive = $this->isAlive();
parent::setHealth($amount);
$this->healthAttr->setValue(ceil($this->getHealth()), true);
- if($this->isAlive() and !$wasAlive){
+ if($this->isAlive() && !$wasAlive){
$this->broadcastAnimation(new RespawnAnimation($this));
}
}
@@ -227,6 +231,37 @@ abstract class Living extends Entity{
}
}
+ public function isGliding() : bool{
+ return $this->gliding;
+ }
+
+ public function setGliding(bool $value = true) : void{
+ $this->gliding = $value;
+ $this->networkPropertiesDirty = true;
+ $this->recalculateSize();
+ }
+
+ public function isSwimming() : bool{
+ return $this->swimming;
+ }
+
+ public function setSwimming(bool $value = true) : void{
+ $this->swimming = $value;
+ $this->networkPropertiesDirty = true;
+ $this->recalculateSize();
+ }
+
+ private function recalculateSize() : void{
+ $size = $this->getInitialSizeInfo();
+ if($this->isSwimming() || $this->isGliding()){
+ $width = $size->getWidth();
+ //we don't actually know an appropriate eye height for a swimming mob, but 2/3 should be good enough.
+ $this->setSize((new EntitySizeInfo($width, $width, $width * 2 / 3))->scale($this->getScale()));
+ }else{
+ $this->setSize($size->scale($this->getScale()));
+ }
+ }
+
public function getMovementSpeed() : float{
return $this->moveSpeedAttr->getValue();
}
@@ -372,7 +407,7 @@ abstract class Living extends Entity{
* to effects or armour.
*/
public function applyDamageModifiers(EntityDamageEvent $source) : void{
- if($this->lastDamageCause !== null and $this->attackTime > 0){
+ if($this->lastDamageCause !== null && $this->attackTime > 0){
if($this->lastDamageCause->getBaseDamage() >= $source->getBaseDamage()){
$source->cancel();
}
@@ -384,7 +419,7 @@ abstract class Living extends Entity{
}
$cause = $source->getCause();
- if(($resistance = $this->effectManager->get(VanillaEffects::RESISTANCE())) !== null and $cause !== EntityDamageEvent::CAUSE_VOID and $cause !== EntityDamageEvent::CAUSE_SUICIDE){
+ if(($resistance = $this->effectManager->get(VanillaEffects::RESISTANCE())) !== null && $cause !== EntityDamageEvent::CAUSE_VOID && $cause !== EntityDamageEvent::CAUSE_SUICIDE){
$source->setModifier(-$source->getFinalDamage() * min(1, 0.2 * $resistance->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE);
}
@@ -408,10 +443,10 @@ abstract class Living extends Entity{
$this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(EntityDamageEvent::MODIFIER_ABSORPTION)));
$this->damageArmor($source->getBaseDamage());
- if($source instanceof EntityDamageByEntityEvent and ($attacker = $source->getDamager()) !== null){
+ if($source instanceof EntityDamageByEntityEvent && ($attacker = $source->getDamager()) !== null){
$damage = 0;
foreach($this->armorInventory->getContents() as $k => $item){
- if($item instanceof Armor and ($thornsLevel = $item->getEnchantmentLevel(VanillaEnchantments::THORNS())) > 0){
+ if($item instanceof Armor && ($thornsLevel = $item->getEnchantmentLevel(VanillaEnchantments::THORNS())) > 0){
if(mt_rand(0, 99) < $thornsLevel * 15){
$this->damageItem($item, 3);
$damage += ($thornsLevel > 10 ? $thornsLevel - 10 : 1 + mt_rand(0, 3));
@@ -458,10 +493,10 @@ abstract class Living extends Entity{
$source->cancel();
}
- if($this->effectManager->has(VanillaEffects::FIRE_RESISTANCE()) and (
+ if($this->effectManager->has(VanillaEffects::FIRE_RESISTANCE()) && (
$source->getCause() === EntityDamageEvent::CAUSE_FIRE
- or $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK
- or $source->getCause() === EntityDamageEvent::CAUSE_LAVA
+ || $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK
+ || $source->getCause() === EntityDamageEvent::CAUSE_LAVA
)
){
$source->cancel();
@@ -469,8 +504,8 @@ abstract class Living extends Entity{
$this->applyDamageModifiers($source);
- if($source instanceof EntityDamageByEntityEvent and (
- $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION or
+ if($source instanceof EntityDamageByEntityEvent && (
+ $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION ||
$source->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION)
){
//TODO: knockback should not just apply for entity damage sources
@@ -607,7 +642,7 @@ abstract class Living extends Entity{
if(!$this->canBreathe()){
$this->setBreathing(false);
- if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 or
+ if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 ||
lcg_value() <= (1 / ($respirationLevel + 1))
){
$ticks -= $tickDiff;
@@ -637,7 +672,7 @@ abstract class Living extends Entity{
* Returns whether the entity can currently breathe.
*/
public function canBreathe() : bool{
- return $this->effectManager->has(VanillaEffects::WATER_BREATHING()) or $this->effectManager->has(VanillaEffects::CONDUIT_POWER()) or !$this->isUnderwater();
+ return $this->effectManager->has(VanillaEffects::WATER_BREATHING()) || $this->effectManager->has(VanillaEffects::CONDUIT_POWER()) || !$this->isUnderwater();
}
/**
@@ -732,7 +767,7 @@ abstract class Living extends Entity{
$block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z);
$blocks[$nextIndex++] = $block;
- if($maxLength !== 0 and count($blocks) > $maxLength){
+ if($maxLength !== 0 && count($blocks) > $maxLength){
array_shift($blocks);
--$nextIndex;
}
@@ -773,14 +808,17 @@ abstract class Living extends Entity{
public function lookAt(Vector3 $target) : void{
$horizontal = sqrt(($target->x - $this->location->x) ** 2 + ($target->z - $this->location->z) ** 2);
$vertical = $target->y - ($this->location->y + $this->getEyeHeight());
- $this->location->pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down
+ $pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down
$xDist = $target->x - $this->location->x;
$zDist = $target->z - $this->location->z;
- $this->location->yaw = atan2($zDist, $xDist) / M_PI * 180 - 90;
- if($this->location->yaw < 0){
- $this->location->yaw += 360.0;
+
+ $yaw = atan2($zDist, $xDist) / M_PI * 180 - 90;
+ if($yaw < 0){
+ $yaw += 360.0;
}
+
+ $this->setRotation($yaw, $pitch);
}
protected function sendSpawnPacket(Player $player) : void{
@@ -800,6 +838,8 @@ abstract class Living extends Entity{
$properties->setGenericFlag(EntityMetadataFlags::BREATHING, $this->breathing);
$properties->setGenericFlag(EntityMetadataFlags::SNEAKING, $this->sneaking);
$properties->setGenericFlag(EntityMetadataFlags::SPRINTING, $this->sprinting);
+ $properties->setGenericFlag(EntityMetadataFlags::GLIDING, $this->gliding);
+ $properties->setGenericFlag(EntityMetadataFlags::SWIMMING, $this->swimming);
}
protected function onDispose() : void{
diff --git a/src/entity/Location.php b/src/entity/Location.php
index 156062a41..2ebbb2661 100644
--- a/src/entity/Location.php
+++ b/src/entity/Location.php
@@ -34,7 +34,7 @@ class Location extends Position{
/** @var float */
public $pitch;
- public function __construct(float $x, float $y, float $z, float $yaw = 0.0, float $pitch = 0.0, ?World $world = null){
+ public function __construct(float $x, float $y, float $z, ?World $world, float $yaw, float $pitch){
$this->yaw = $yaw;
$this->pitch = $pitch;
parent::__construct($x, $y, $z, $world);
@@ -44,14 +44,14 @@ class Location extends Position{
* @return Location
*/
public static function fromObject(Vector3 $pos, ?World $world, float $yaw = 0.0, float $pitch = 0.0){
- return new Location($pos->x, $pos->y, $pos->z, $yaw, $pitch, $world ?? (($pos instanceof Position) ? $pos->world : null));
+ return new Location($pos->x, $pos->y, $pos->z, $world ?? (($pos instanceof Position) ? $pos->world : null), $yaw, $pitch);
}
/**
* Return a Location instance
*/
public function asLocation() : Location{
- return new Location($this->x, $this->y, $this->z, $this->yaw, $this->pitch, $this->world);
+ return new Location($this->x, $this->y, $this->z, $this->world, $this->yaw, $this->pitch);
}
public function getYaw() : float{
@@ -68,7 +68,7 @@ class Location extends Position{
public function equals(Vector3 $v) : bool{
if($v instanceof Location){
- return parent::equals($v) and $v->yaw == $this->yaw and $v->pitch == $this->pitch;
+ return parent::equals($v) && $v->yaw == $this->yaw && $v->pitch == $this->pitch;
}
return parent::equals($v);
}
diff --git a/src/entity/Skin.php b/src/entity/Skin.php
index 0542bd75a..786a6e711 100644
--- a/src/entity/Skin.php
+++ b/src/entity/Skin.php
@@ -24,11 +24,13 @@ declare(strict_types=1);
namespace pocketmine\entity;
use Ahc\Json\Comment as CommentedJsonDecoder;
+use pocketmine\utils\Limits;
use function implode;
use function in_array;
use function json_encode;
use function json_last_error_msg;
use function strlen;
+use const JSON_THROW_ON_ERROR;
final class Skin{
public const ACCEPTED_SKIN_SIZES = [
@@ -48,7 +50,17 @@ final class Skin{
/** @var string */
private $geometryData;
+ private static function checkLength(string $string, string $name, int $maxLength) : void{
+ if(strlen($string) > $maxLength){
+ throw new InvalidSkinException("$name must be at most $maxLength bytes, but have " . strlen($string) . " bytes");
+ }
+ }
+
public function __construct(string $skinId, string $skinData, string $capeData = "", string $geometryName = "", string $geometryData = ""){
+ self::checkLength($skinId, "Skin ID", Limits::INT16_MAX);
+ self::checkLength($geometryName, "Geometry name", Limits::INT16_MAX);
+ self::checkLength($geometryData, "Geometry data", Limits::INT32_MAX);
+
if($skinId === ""){
throw new InvalidSkinException("Skin ID must not be empty");
}
@@ -56,7 +68,7 @@ final class Skin{
if(!in_array($len, self::ACCEPTED_SKIN_SIZES, true)){
throw new InvalidSkinException("Invalid skin data size $len bytes (allowed sizes: " . implode(", ", self::ACCEPTED_SKIN_SIZES) . ")");
}
- if($capeData !== "" and strlen($capeData) !== 8192){
+ if($capeData !== "" && strlen($capeData) !== 8192){
throw new InvalidSkinException("Invalid cape data size " . strlen($capeData) . " bytes (must be exactly 8192 bytes)");
}
@@ -73,7 +85,7 @@ final class Skin{
* Not only that, they are pretty-printed.
* TODO: find out what model crap can be safely dropped from the packet (unless it gets fixed first)
*/
- $geometryData = json_encode($decodedGeometry);
+ $geometryData = json_encode($decodedGeometry, JSON_THROW_ON_ERROR);
}
$this->skinId = $skinId;
diff --git a/src/entity/Squid.php b/src/entity/Squid.php
index 1010c5d78..39e2a3637 100644
--- a/src/entity/Squid.php
+++ b/src/entity/Squid.php
@@ -95,7 +95,7 @@ class Squid extends WaterAnimal{
if($this->isAlive()){
- if($this->location->y > 62 and $this->swimDirection !== null){
+ if($this->location->y > 62 && $this->swimDirection !== null){
$this->swimDirection = $this->swimDirection->withComponents(null, -0.5, null);
}
@@ -113,8 +113,10 @@ class Squid extends WaterAnimal{
}
$f = sqrt(($this->motion->x ** 2) + ($this->motion->z ** 2));
- $this->location->yaw = (-atan2($this->motion->x, $this->motion->z) * 180 / M_PI);
- $this->location->pitch = (-atan2($f, $this->motion->y) * 180 / M_PI);
+ $this->setRotation(
+ -atan2($this->motion->x, $this->motion->z) * 180 / M_PI,
+ -atan2($f, $this->motion->y) * 180 / M_PI
+ );
}
return $hasUpdate;
diff --git a/src/entity/Villager.php b/src/entity/Villager.php
index d43e4faf9..7e1c081cc 100644
--- a/src/entity/Villager.php
+++ b/src/entity/Villager.php
@@ -57,7 +57,7 @@ class Villager extends Living implements Ageable{
/** @var int $profession */
$profession = $nbt->getInt("Profession", self::PROFESSION_FARMER);
- if($profession > 4 or $profession < 0){
+ if($profession > 4 || $profession < 0){
$profession = self::PROFESSION_FARMER;
}
diff --git a/src/entity/animation/ArmSwingAnimation.php b/src/entity/animation/ArmSwingAnimation.php
index c309472bf..8871415a2 100644
--- a/src/entity/animation/ArmSwingAnimation.php
+++ b/src/entity/animation/ArmSwingAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Living;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class ArmSwingAnimation implements Animation{
@@ -37,7 +38,7 @@ final class ArmSwingAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->entity->getId(), ActorEventPacket::ARM_SWING, 0)
+ ActorEventPacket::create($this->entity->getId(), ActorEvent::ARM_SWING, 0)
];
}
}
diff --git a/src/entity/animation/ArrowShakeAnimation.php b/src/entity/animation/ArrowShakeAnimation.php
index 925b93d3a..256c6142b 100644
--- a/src/entity/animation/ArrowShakeAnimation.php
+++ b/src/entity/animation/ArrowShakeAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\projectile\Arrow;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
class ArrowShakeAnimation implements Animation{
@@ -40,7 +41,7 @@ class ArrowShakeAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->arrow->getId(), ActorEventPacket::ARROW_SHAKE, $this->durationInTicks)
+ ActorEventPacket::create($this->arrow->getId(), ActorEvent::ARROW_SHAKE, $this->durationInTicks)
];
}
}
diff --git a/src/entity/animation/ConsumingItemAnimation.php b/src/entity/animation/ConsumingItemAnimation.php
index 4f3e34ba4..b32f118bb 100644
--- a/src/entity/animation/ConsumingItemAnimation.php
+++ b/src/entity/animation/ConsumingItemAnimation.php
@@ -27,6 +27,7 @@ use pocketmine\entity\Human;
use pocketmine\item\Item;
use pocketmine\network\mcpe\convert\ItemTranslator;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class ConsumingItemAnimation implements Animation{
@@ -45,7 +46,7 @@ final class ConsumingItemAnimation implements Animation{
[$netId, $netData] = ItemTranslator::getInstance()->toNetworkId($this->item->getId(), $this->item->getMeta());
return [
//TODO: need to check the data values
- ActorEventPacket::create($this->human->getId(), ActorEventPacket::EATING_ITEM, ($netId << 16) | $netData)
+ ActorEventPacket::create($this->human->getId(), ActorEvent::EATING_ITEM, ($netId << 16) | $netData)
];
}
}
diff --git a/src/entity/animation/DeathAnimation.php b/src/entity/animation/DeathAnimation.php
index 35dab4597..a3a924413 100644
--- a/src/entity/animation/DeathAnimation.php
+++ b/src/entity/animation/DeathAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Living;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class DeathAnimation implements Animation{
@@ -37,7 +38,7 @@ final class DeathAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->entity->getId(), ActorEventPacket::DEATH_ANIMATION, 0)
+ ActorEventPacket::create($this->entity->getId(), ActorEvent::DEATH_ANIMATION, 0)
];
}
}
diff --git a/src/entity/animation/HurtAnimation.php b/src/entity/animation/HurtAnimation.php
index 3ba5ebb8c..355b49d47 100644
--- a/src/entity/animation/HurtAnimation.php
+++ b/src/entity/animation/HurtAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Living;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class HurtAnimation implements Animation{
@@ -37,7 +38,7 @@ final class HurtAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->entity->getId(), ActorEventPacket::HURT_ANIMATION, 0)
+ ActorEventPacket::create($this->entity->getId(), ActorEvent::HURT_ANIMATION, 0)
];
}
}
diff --git a/src/entity/animation/ItemEntityStackSizeChangeAnimation.php b/src/entity/animation/ItemEntityStackSizeChangeAnimation.php
new file mode 100644
index 000000000..ecc943c8c
--- /dev/null
+++ b/src/entity/animation/ItemEntityStackSizeChangeAnimation.php
@@ -0,0 +1,40 @@
+itemEntity->getId(), ActorEvent::ITEM_ENTITY_MERGE, $this->newStackSize)];
+ }
+}
diff --git a/src/entity/animation/RespawnAnimation.php b/src/entity/animation/RespawnAnimation.php
index defb26cf7..e8466e26e 100644
--- a/src/entity/animation/RespawnAnimation.php
+++ b/src/entity/animation/RespawnAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Living;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class RespawnAnimation implements Animation{
@@ -37,7 +38,7 @@ final class RespawnAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->entity->getId(), ActorEventPacket::RESPAWN, 0)
+ ActorEventPacket::create($this->entity->getId(), ActorEvent::RESPAWN, 0)
];
}
}
diff --git a/src/entity/animation/SquidInkCloudAnimation.php b/src/entity/animation/SquidInkCloudAnimation.php
index fa3a9fd6b..6ae728a79 100644
--- a/src/entity/animation/SquidInkCloudAnimation.php
+++ b/src/entity/animation/SquidInkCloudAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Squid;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class SquidInkCloudAnimation implements Animation{
@@ -37,7 +38,7 @@ final class SquidInkCloudAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->squid->getId(), ActorEventPacket::SQUID_INK_CLOUD, 0)
+ ActorEventPacket::create($this->squid->getId(), ActorEvent::SQUID_INK_CLOUD, 0)
];
}
}
diff --git a/src/entity/animation/TotemUseAnimation.php b/src/entity/animation/TotemUseAnimation.php
index 11eb68e46..ec0d3b12c 100644
--- a/src/entity/animation/TotemUseAnimation.php
+++ b/src/entity/animation/TotemUseAnimation.php
@@ -25,6 +25,7 @@ namespace pocketmine\entity\animation;
use pocketmine\entity\Human;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
final class TotemUseAnimation implements Animation{
@@ -38,7 +39,7 @@ final class TotemUseAnimation implements Animation{
public function encode() : array{
return [
- ActorEventPacket::create($this->human->getId(), ActorEventPacket::CONSUME_TOTEM, 0)
+ ActorEventPacket::create($this->human->getId(), ActorEvent::CONSUME_TOTEM, 0)
];
}
}
diff --git a/src/entity/effect/Effect.php b/src/entity/effect/Effect.php
index b5c385c6f..3d1bd4c7c 100644
--- a/src/entity/effect/Effect.php
+++ b/src/entity/effect/Effect.php
@@ -38,12 +38,14 @@ class Effect{
* @param Translatable|string $name Translation key used for effect name
* @param Color $color Color of bubbles given by this effect
* @param bool $bad Whether the effect is harmful
+ * @param int $defaultDuration
* @param bool $hasBubbles Whether the effect has potion bubbles. Some do not (e.g. Instant Damage has its own particles instead of bubbles)
*/
public function __construct(
protected Translatable|string $name,
protected Color $color,
protected bool $bad = false,
+ private int $defaultDuration = 600,
protected bool $hasBubbles = true
){}
@@ -73,7 +75,7 @@ class Effect{
* Returns the default duration (in ticks) this effect will apply for if a duration is not specified.
*/
public function getDefaultDuration() : int{
- return 600;
+ return $this->defaultDuration;
}
/**
diff --git a/src/entity/effect/EffectInstance.php b/src/entity/effect/EffectInstance.php
index c908c7ec7..566ca1e5e 100644
--- a/src/entity/effect/EffectInstance.php
+++ b/src/entity/effect/EffectInstance.php
@@ -77,7 +77,7 @@ class EffectInstance{
* @return $this
*/
public function setDuration(int $duration) : EffectInstance{
- if($duration < 0 or $duration > Limits::INT32_MAX){
+ if($duration < 0 || $duration > Limits::INT32_MAX){
throw new \InvalidArgumentException("Effect duration must be in range 0 - " . Limits::INT32_MAX . ", got $duration");
}
$this->duration = $duration;
@@ -118,7 +118,7 @@ class EffectInstance{
* @return $this
*/
public function setAmplifier(int $amplifier) : EffectInstance{
- if($amplifier < 0 or $amplifier > 255){
+ if($amplifier < 0 || $amplifier > 255){
throw new \InvalidArgumentException("Amplifier must be in range 0 - 255, got $amplifier");
}
$this->amplifier = $amplifier;
diff --git a/src/entity/effect/EffectManager.php b/src/entity/effect/EffectManager.php
index 17bdb8947..ee561229f 100644
--- a/src/entity/effect/EffectManager.php
+++ b/src/entity/effect/EffectManager.php
@@ -91,7 +91,7 @@ class EffectManager{
$ev = new EntityEffectRemoveEvent($this->entity, $effect);
$ev->call();
if($ev->isCancelled()){
- if($hasExpired and !$ev->getEffect()->hasExpired()){ //altered duration of an expired effect to make it not get removed
+ if($hasExpired && !$ev->getEffect()->hasExpired()){ //altered duration of an expired effect to make it not get removed
foreach($this->effectAddHooks as $hook){
$hook($ev->getEffect(), true);
}
@@ -140,7 +140,7 @@ class EffectManager{
$oldEffect = $this->effects[$index];
if(
abs($effect->getAmplifier()) < $oldEffect->getAmplifier()
- or (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) and $effect->getDuration() < $oldEffect->getDuration())
+ || (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration())
){
$cancelled = true;
}
@@ -180,7 +180,7 @@ class EffectManager{
$colors = [];
$ambient = true;
foreach($this->effects as $effect){
- if($effect->isVisible() and $effect->getType()->hasBubbles()){
+ if($effect->isVisible() && $effect->getType()->hasBubbles()){
$level = $effect->getEffectLevel();
$color = $effect->getColor();
for($i = 0; $i < $level; ++$i){
diff --git a/src/entity/effect/InstantEffect.php b/src/entity/effect/InstantEffect.php
index 653925918..ac5852e2f 100644
--- a/src/entity/effect/InstantEffect.php
+++ b/src/entity/effect/InstantEffect.php
@@ -23,10 +23,13 @@ declare(strict_types=1);
namespace pocketmine\entity\effect;
+use pocketmine\color\Color;
+use pocketmine\lang\Translatable;
+
abstract class InstantEffect extends Effect{
- public function getDefaultDuration() : int{
- return 1;
+ public function __construct(Translatable|string $name, Color $color, bool $bad = false, bool $hasBubbles = true){
+ parent::__construct($name, $color, $bad, 1, $hasBubbles);
}
public function canTick(EffectInstance $instance) : bool{
diff --git a/src/entity/effect/PoisonEffect.php b/src/entity/effect/PoisonEffect.php
index 99dffb3af..166be76c6 100644
--- a/src/entity/effect/PoisonEffect.php
+++ b/src/entity/effect/PoisonEffect.php
@@ -34,8 +34,8 @@ class PoisonEffect extends Effect{
/** @var bool */
private $fatal;
- public function __construct(Translatable|string $name, Color $color, bool $isBad = false, bool $hasBubbles = true, bool $fatal = false){
- parent::__construct($name, $color, $isBad, $hasBubbles);
+ public function __construct(Translatable|string $name, Color $color, bool $isBad = false, int $defaultDuration = 600, bool $hasBubbles = true, bool $fatal = false){
+ parent::__construct($name, $color, $isBad, $defaultDuration, $hasBubbles);
$this->fatal = $fatal;
}
@@ -47,7 +47,7 @@ class PoisonEffect extends Effect{
}
public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{
- if($entity->getHealth() > 1 or $this->fatal){
+ if($entity->getHealth() > 1 || $this->fatal){
$ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, 1);
$entity->attack($ev);
}
diff --git a/src/entity/effect/StringToEffectParser.php b/src/entity/effect/StringToEffectParser.php
new file mode 100644
index 000000000..2eb8731b9
--- /dev/null
+++ b/src/entity/effect/StringToEffectParser.php
@@ -0,0 +1,73 @@
+
+ */
+final class StringToEffectParser extends StringToTParser{
+ use SingletonTrait;
+
+ private static function make() : self{
+ $result = new self;
+
+ $result->register("absorption", fn() => VanillaEffects::ABSORPTION());
+ $result->register("blindness", fn() => VanillaEffects::BLINDNESS());
+ $result->register("conduit_power", fn() => VanillaEffects::CONDUIT_POWER());
+ $result->register("fatal_poison", fn() => VanillaEffects::FATAL_POISON());
+ $result->register("fire_resistance", fn() => VanillaEffects::FIRE_RESISTANCE());
+ $result->register("haste", fn() => VanillaEffects::HASTE());
+ $result->register("health_boost", fn() => VanillaEffects::HEALTH_BOOST());
+ $result->register("hunger", fn() => VanillaEffects::HUNGER());
+ $result->register("instant_damage", fn() => VanillaEffects::INSTANT_DAMAGE());
+ $result->register("instant_health", fn() => VanillaEffects::INSTANT_HEALTH());
+ $result->register("invisibility", fn() => VanillaEffects::INVISIBILITY());
+ $result->register("jump_boost", fn() => VanillaEffects::JUMP_BOOST());
+ $result->register("levitation", fn() => VanillaEffects::LEVITATION());
+ $result->register("mining_fatigue", fn() => VanillaEffects::MINING_FATIGUE());
+ $result->register("nausea", fn() => VanillaEffects::NAUSEA());
+ $result->register("night_vision", fn() => VanillaEffects::NIGHT_VISION());
+ $result->register("poison", fn() => VanillaEffects::POISON());
+ $result->register("regeneration", fn() => VanillaEffects::REGENERATION());
+ $result->register("resistance", fn() => VanillaEffects::RESISTANCE());
+ $result->register("saturation", fn() => VanillaEffects::SATURATION());
+ $result->register("slowness", fn() => VanillaEffects::SLOWNESS());
+ $result->register("speed", fn() => VanillaEffects::SPEED());
+ $result->register("strength", fn() => VanillaEffects::STRENGTH());
+ $result->register("water_breathing", fn() => VanillaEffects::WATER_BREATHING());
+ $result->register("weakness", fn() => VanillaEffects::WEAKNESS());
+ $result->register("wither", fn() => VanillaEffects::WITHER());
+
+ return $result;
+ }
+
+ public function parse(string $input) : ?Effect{
+ return parent::parse($input);
+ }
+}
diff --git a/src/entity/effect/VanillaEffects.php b/src/entity/effect/VanillaEffects.php
index a447613d4..d382a9f60 100644
--- a/src/entity/effect/VanillaEffects.php
+++ b/src/entity/effect/VanillaEffects.php
@@ -26,7 +26,6 @@ namespace pocketmine\entity\effect;
use pocketmine\color\Color;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\utils\RegistryTrait;
-use function assert;
/**
* This doc-block is generated automatically, do not modify it manually.
@@ -69,7 +68,7 @@ final class VanillaEffects{
//TODO: bad_omen
self::register("blindness", new Effect(KnownTranslationFactory::potion_blindness(), new Color(0x1f, 0x1f, 0x23), true));
self::register("conduit_power", new Effect(KnownTranslationFactory::potion_conduitPower(), new Color(0x1d, 0xc2, 0xd1)));
- self::register("fatal_poison", new PoisonEffect(KnownTranslationFactory::potion_poison(), new Color(0x4e, 0x93, 0x31), true, true, true));
+ self::register("fatal_poison", new PoisonEffect(KnownTranslationFactory::potion_poison(), new Color(0x4e, 0x93, 0x31), true, 600, true, true));
self::register("fire_resistance", new Effect(KnownTranslationFactory::potion_fireResistance(), new Color(0xe4, 0x9a, 0x3a)));
self::register("haste", new Effect(KnownTranslationFactory::potion_digSpeed(), new Color(0xd9, 0xc0, 0x43)));
self::register("health_boost", new HealthBoostEffect(KnownTranslationFactory::potion_healthBoost(), new Color(0xf8, 0x7d, 0x23)));
@@ -85,7 +84,7 @@ final class VanillaEffects{
self::register("poison", new PoisonEffect(KnownTranslationFactory::potion_poison(), new Color(0x4e, 0x93, 0x31), true));
self::register("regeneration", new RegenerationEffect(KnownTranslationFactory::potion_regeneration(), new Color(0xcd, 0x5c, 0xab)));
self::register("resistance", new Effect(KnownTranslationFactory::potion_resistance(), new Color(0x99, 0x45, 0x3a)));
- self::register("saturation", new SaturationEffect(KnownTranslationFactory::potion_saturation(), new Color(0xf8, 0x24, 0x23), false));
+ self::register("saturation", new SaturationEffect(KnownTranslationFactory::potion_saturation(), new Color(0xf8, 0x24, 0x23)));
//TODO: slow_falling
self::register("slowness", new SlownessEffect(KnownTranslationFactory::potion_moveSlowdown(), new Color(0x5a, 0x6c, 0x81), true));
self::register("speed", new SpeedEffect(KnownTranslationFactory::potion_moveSpeed(), new Color(0x7c, 0xaf, 0xc6)));
@@ -109,10 +108,4 @@ final class VanillaEffects{
$result = self::_registryGetAll();
return $result;
}
-
- public static function fromString(string $name) : Effect{
- $result = self::_registryFromString($name);
- assert($result instanceof Effect);
- return $result;
- }
}
diff --git a/src/entity/object/ExperienceOrb.php b/src/entity/object/ExperienceOrb.php
index 8011a6a47..274abf379 100644
--- a/src/entity/object/ExperienceOrb.php
+++ b/src/entity/object/ExperienceOrb.php
@@ -166,7 +166,7 @@ class ExperienceOrb extends Entity{
}
$currentTarget = $this->getTargetPlayer();
- if($currentTarget !== null and (!$currentTarget->isAlive() or $currentTarget->location->distanceSquared($this->location) > self::MAX_TARGET_DISTANCE ** 2)){
+ if($currentTarget !== null && (!$currentTarget->isAlive() || !$currentTarget->getXpManager()->canAttractXpOrbs() || $currentTarget->location->distanceSquared($this->location) > self::MAX_TARGET_DISTANCE ** 2)){
$currentTarget = null;
}
@@ -174,7 +174,7 @@ class ExperienceOrb extends Entity{
if($currentTarget === null){
$newTarget = $this->getWorld()->getNearestEntity($this->location, self::MAX_TARGET_DISTANCE, Human::class);
- if($newTarget instanceof Human and !($newTarget instanceof Player and $newTarget->isSpectator())){
+ if($newTarget instanceof Human && !($newTarget instanceof Player && $newTarget->isSpectator()) && $newTarget->getXpManager()->canAttractXpOrbs()){
$currentTarget = $newTarget;
}
}
@@ -194,7 +194,7 @@ class ExperienceOrb extends Entity{
$this->motion = $this->motion->addVector($vector->normalize()->multiply(0.2 * (1 - sqrt($distance)) ** 2));
}
- if($currentTarget->getXpManager()->canPickupXp() and $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){
+ if($currentTarget->getXpManager()->canPickupXp() && $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){
$this->flagForDespawn();
$currentTarget->getXpManager()->onPickupXp($this->getXpValue());
diff --git a/src/entity/object/FallingBlock.php b/src/entity/object/FallingBlock.php
index 5376b866e..cb2551349 100644
--- a/src/entity/object/FallingBlock.php
+++ b/src/entity/object/FallingBlock.php
@@ -26,6 +26,7 @@ namespace pocketmine\entity\object;
use pocketmine\block\Block;
use pocketmine\block\BlockFactory;
use pocketmine\block\utils\Fallable;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Location;
@@ -71,7 +72,7 @@ class FallingBlock extends Entity{
}
if($blockId === 0){
- throw new \UnexpectedValueException("Missing block info from NBT");
+ throw new SavedDataLoadingException("Missing block info from NBT");
}
$damage = $nbt->getByte("Data", 0);
@@ -111,11 +112,11 @@ class FallingBlock extends Entity{
$blockTarget = $this->block->tickFalling();
}
- if($this->onGround or $blockTarget !== null){
+ if($this->onGround || $blockTarget !== null){
$this->flagForDespawn();
$block = $world->getBlock($pos);
- if(!$block->canBeReplaced() or !$world->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()) or ($this->onGround and abs($this->location->y - $this->location->getFloorY()) > 0.001)){
+ if(!$block->canBeReplaced() || !$world->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()) || ($this->onGround && abs($this->location->y - $this->location->getFloorY()) > 0.001)){
//FIXME: anvils are supposed to destroy torches
$world->dropItem($this->location, $this->block->asItem());
}else{
diff --git a/src/entity/object/ItemEntity.php b/src/entity/object/ItemEntity.php
index 7de765e65..73d67a192 100644
--- a/src/entity/object/ItemEntity.php
+++ b/src/entity/object/ItemEntity.php
@@ -23,11 +23,13 @@ declare(strict_types=1);
namespace pocketmine\entity\object;
+use pocketmine\entity\animation\ItemEntityStackSizeChangeAnimation;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Location;
use pocketmine\event\entity\EntityItemPickupEvent;
use pocketmine\event\entity\ItemDespawnEvent;
+use pocketmine\event\entity\ItemMergeEvent;
use pocketmine\event\entity\ItemSpawnEvent;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
@@ -43,6 +45,7 @@ class ItemEntity extends Entity{
public static function getNetworkTypeId() : string{ return EntityIds::ITEM; }
+ public const MERGE_CHECK_PERIOD = 2; //0.1 seconds
public const DEFAULT_DESPAWN_DELAY = 6000; //5 minutes
public const NEVER_DESPAWN = -1;
public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY; //max value storable by mojang NBT :(
@@ -68,7 +71,7 @@ class ItemEntity extends Entity{
if($item->isNull()){
throw new \InvalidArgumentException("Item entity must have a non-air item with a count of at least 1");
}
- $this->item = $item;
+ $this->item = clone $item;
parent::__construct($location, $nbt);
}
@@ -100,11 +103,32 @@ class ItemEntity extends Entity{
$hasUpdate = parent::entityBaseTick($tickDiff);
- if(!$this->isFlaggedForDespawn() and $this->pickupDelay !== self::NEVER_DESPAWN){ //Infinite delay
+ if(!$this->isFlaggedForDespawn() && $this->pickupDelay !== self::NEVER_DESPAWN){ //Infinite delay
$this->pickupDelay -= $tickDiff;
if($this->pickupDelay < 0){
$this->pickupDelay = 0;
}
+ if($this->hasMovementUpdate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
+ $mergeable = [$this]; //in case the merge target ends up not being this
+ $mergeTarget = $this;
+ foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
+ if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
+ continue;
+ }
+
+ if($entity->isMergeable($this)){
+ $mergeable[] = $entity;
+ if($entity->item->getCount() > $mergeTarget->item->getCount()){
+ $mergeTarget = $entity;
+ }
+ }
+ }
+ foreach($mergeable as $itemEntity){
+ if($itemEntity !== $mergeTarget){
+ $itemEntity->tryMergeInto($mergeTarget);
+ }
+ }
+ }
$this->despawnDelay -= $tickDiff;
if($this->despawnDelay <= 0){
@@ -122,6 +146,37 @@ class ItemEntity extends Entity{
return $hasUpdate;
}
+ /**
+ * Returns whether this item entity can merge with the given one.
+ */
+ public function isMergeable(ItemEntity $entity) : bool{
+ $item = $entity->item;
+ return $entity !== $this && $entity->pickupDelay !== self::NEVER_DESPAWN && $item->canStackWith($this->item) && $item->getCount() + $this->item->getCount() <= $item->getMaxStackSize();
+ }
+
+ /**
+ * Attempts to merge this item entity into the given item entity. Returns true if it was successful.
+ */
+ public function tryMergeInto(ItemEntity $consumer) : bool{
+ if(!$this->isMergeable($consumer)){
+ return false;
+ }
+
+ $ev = new ItemMergeEvent($this, $consumer);
+ $ev->call();
+
+ if($ev->isCancelled()){
+ return false;
+ }
+
+ $consumer->setStackSize($consumer->item->getCount() + $this->item->getCount());
+ $this->flagForDespawn();
+ $consumer->pickupDelay = max($consumer->pickupDelay, $this->pickupDelay);
+ $consumer->despawnDelay = max($consumer->despawnDelay, $this->despawnDelay);
+
+ return true;
+ }
+
protected function tryChangeMovement() : void{
$this->checkObstruction($this->location->x, $this->location->y, $this->location->z);
parent::tryChangeMovement();
@@ -183,7 +238,7 @@ class ItemEntity extends Entity{
* @throws \InvalidArgumentException
*/
public function setDespawnDelay(int $despawnDelay) : void{
- if(($despawnDelay < 0 or $despawnDelay > self::MAX_DESPAWN_DELAY) and $despawnDelay !== self::NEVER_DESPAWN){
+ if(($despawnDelay < 0 || $despawnDelay > self::MAX_DESPAWN_DELAY) && $despawnDelay !== self::NEVER_DESPAWN){
throw new \InvalidArgumentException("Despawn ticker must be in range 0 ... " . self::MAX_DESPAWN_DELAY . " or " . self::NEVER_DESPAWN . ", got $despawnDelay");
}
$this->despawnDelay = $despawnDelay;
@@ -206,14 +261,23 @@ class ItemEntity extends Entity{
}
protected function sendSpawnPacket(Player $player) : void{
- $pk = new AddItemActorPacket();
- $pk->entityRuntimeId = $this->getId();
- $pk->position = $this->location->asVector3();
- $pk->motion = $this->getMotion();
- $pk->item = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->getItem()));
- $pk->metadata = $this->getAllNetworkData();
+ $player->getNetworkSession()->sendDataPacket(AddItemActorPacket::create(
+ $this->getId(), //TODO: entity unique ID
+ $this->getId(),
+ ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->getItem())),
+ $this->location->asVector3(),
+ $this->getMotion(),
+ $this->getAllNetworkData(),
+ false //TODO: I have no idea what this is needed for, but right now we don't support fishing anyway
+ ));
+ }
- $player->getNetworkSession()->sendDataPacket($pk);
+ public function setStackSize(int $newCount) : void{
+ if($newCount <= 0){
+ throw new \InvalidArgumentException("Stack size must be at least 1");
+ }
+ $this->item->setCount($newCount);
+ $this->broadcastAnimation(new ItemEntityStackSizeChangeAnimation($this, $newCount));
}
public function getOffsetPosition(Vector3 $vector3) : Vector3{
@@ -227,13 +291,13 @@ class ItemEntity extends Entity{
$item = $this->getItem();
$playerInventory = match(true){
- $player->getOffHandInventory()->getItem(0)->canStackWith($item) and $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
- $player->getInventory()->canAddItem($item) => $player->getInventory(),
+ $player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
+ $player->getInventory()->getAddableItemQuantity($item) > 0 => $player->getInventory(),
default => null
};
$ev = new EntityItemPickupEvent($player, $this, $item, $playerInventory);
- if($player->hasFiniteResources() and $playerInventory === null){
+ if($player->hasFiniteResources() && $playerInventory === null){
$ev->cancel();
}
@@ -246,7 +310,12 @@ class ItemEntity extends Entity{
$viewer->getNetworkSession()->onPlayerPickUpItem($player, $this);
}
- $ev->getInventory()?->addItem($ev->getItem());
+ $inventory = $ev->getInventory();
+ if($inventory !== null){
+ foreach($inventory->addItem($ev->getItem()) as $remains){
+ $this->getWorld()->dropItem($this->location, $remains, new Vector3(0, 0, 0));
+ }
+ }
$this->flagForDespawn();
}
}
diff --git a/src/entity/object/Painting.php b/src/entity/object/Painting.php
index 4769df15d..ff59de043 100644
--- a/src/entity/object/Painting.php
+++ b/src/entity/object/Painting.php
@@ -107,7 +107,7 @@ class Painting extends Entity{
if($this->lastDamageCause instanceof EntityDamageByEntityEvent){
$killer = $this->lastDamageCause->getDamager();
- if($killer instanceof Player and !$killer->hasFiniteResources()){
+ if($killer instanceof Player && !$killer->hasFiniteResources()){
$drops = false;
}
}
@@ -149,17 +149,17 @@ class Painting extends Entity{
}
protected function sendSpawnPacket(Player $player) : void{
- $pk = new AddPaintingPacket();
- $pk->entityRuntimeId = $this->getId();
- $pk->position = new Vector3(
- ($this->boundingBox->minX + $this->boundingBox->maxX) / 2,
- ($this->boundingBox->minY + $this->boundingBox->maxY) / 2,
- ($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2
- );
- $pk->direction = self::FACING_TO_DATA[$this->facing];
- $pk->title = $this->motive->getName();
-
- $player->getNetworkSession()->sendDataPacket($pk);
+ $player->getNetworkSession()->sendDataPacket(AddPaintingPacket::create(
+ $this->getId(), //TODO: entity unique ID
+ $this->getId(),
+ new Vector3(
+ ($this->boundingBox->minX + $this->boundingBox->maxX) / 2,
+ ($this->boundingBox->minY + $this->boundingBox->maxY) / 2,
+ ($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2
+ ),
+ self::FACING_TO_DATA[$this->facing],
+ $this->motive->getName()
+ ));
}
/**
@@ -212,7 +212,7 @@ class Painting extends Entity{
$pos = $startPos->getSide($rotatedFace, $w)->getSide(Facing::UP, $h);
$block = $world->getBlockAt($pos->x, $pos->y, $pos->z);
- if($block->isSolid() or !$block->getSide($oppositeSide)->isSolid()){
+ if($block->isSolid() || !$block->getSide($oppositeSide)->isSolid()){
return false;
}
}
diff --git a/src/entity/object/PrimedTNT.php b/src/entity/object/PrimedTNT.php
index f3ed921b8..70d58dafa 100644
--- a/src/entity/object/PrimedTNT.php
+++ b/src/entity/object/PrimedTNT.php
@@ -58,7 +58,7 @@ class PrimedTNT extends Entity implements Explosive{
}
public function setFuse(int $fuse) : void{
- if($fuse < 0 or $fuse > 32767){
+ if($fuse < 0 || $fuse > 32767){
throw new \InvalidArgumentException("Fuse must be in the range 0-32767");
}
$this->fuse = $fuse;
@@ -112,7 +112,7 @@ class PrimedTNT extends Entity implements Explosive{
}
}
- return $hasUpdate or $this->fuse >= 0;
+ return $hasUpdate || $this->fuse >= 0;
}
public function explode() : void{
diff --git a/src/entity/projectile/Arrow.php b/src/entity/projectile/Arrow.php
index 5fbef6486..47924c8cb 100644
--- a/src/entity/projectile/Arrow.php
+++ b/src/entity/projectile/Arrow.php
@@ -175,16 +175,17 @@ class Arrow extends Projectile{
$item = VanillaItems::ARROW();
$playerInventory = match(true){
- $player->getOffHandInventory()->getItem(0)->canStackWith($item) and $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
+ !$player->hasFiniteResources() => null, //arrows are not picked up in creative
+ $player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
$player->getInventory()->canAddItem($item) => $player->getInventory(),
default => null
};
$ev = new EntityItemPickupEvent($player, $this, $item, $playerInventory);
- if($player->hasFiniteResources() and $playerInventory === null){
+ if($player->hasFiniteResources() && $playerInventory === null){
$ev->cancel();
}
- if($this->pickupMode === self::PICKUP_NONE or ($this->pickupMode === self::PICKUP_CREATIVE and !$player->isCreative())){
+ if($this->pickupMode === self::PICKUP_NONE || ($this->pickupMode === self::PICKUP_CREATIVE && !$player->isCreative())){
$ev->cancel();
}
diff --git a/src/entity/projectile/Projectile.php b/src/entity/projectile/Projectile.php
index 9201b6060..1f652bda4 100644
--- a/src/entity/projectile/Projectile.php
+++ b/src/entity/projectile/Projectile.php
@@ -77,36 +77,32 @@ abstract class Projectile extends Entity{
$this->setHealth(1);
$this->damage = $nbt->getDouble("damage", $this->damage);
- do{
- $blockPos = null;
- $blockId = null;
- $blockData = null;
-
- if(($tileXTag = $nbt->getTag("tileX")) instanceof IntTag and ($tileYTag = $nbt->getTag("tileY")) instanceof IntTag and ($tileZTag = $nbt->getTag("tileZ")) instanceof IntTag){
+ (function() use ($nbt) : void{
+ if(($tileXTag = $nbt->getTag("tileX")) instanceof IntTag && ($tileYTag = $nbt->getTag("tileY")) instanceof IntTag && ($tileZTag = $nbt->getTag("tileZ")) instanceof IntTag){
$blockPos = new Vector3($tileXTag->getValue(), $tileYTag->getValue(), $tileZTag->getValue());
}else{
- break;
+ return;
}
if(($blockIdTag = $nbt->getTag("blockId")) instanceof IntTag){
$blockId = $blockIdTag->getValue();
}else{
- break;
+ return;
}
if(($blockDataTag = $nbt->getTag("blockData")) instanceof ByteTag){
$blockData = $blockDataTag->getValue();
}else{
- break;
+ return;
}
$this->blockHit = BlockFactory::getInstance()->get($blockId, $blockData);
$this->blockHit->position($this->getWorld(), $blockPos->getFloorX(), $blockPos->getFloorY(), $blockPos->getFloorZ());
- }while(false);
+ })();
}
public function canCollideWith(Entity $entity) : bool{
- return $entity instanceof Living and !$this->onGround;
+ return $entity instanceof Living && !$this->onGround;
}
public function canBeCollidedWith() : bool{
@@ -159,7 +155,7 @@ abstract class Projectile extends Entity{
}
public function onNearbyBlockChange() : void{
- if($this->blockHit !== null and $this->getWorld()->isInLoadedTerrain($this->blockHit->getPosition()) and !$this->blockHit->isSameState($this->getWorld()->getBlock($this->blockHit->getPosition()))){
+ if($this->blockHit !== null && $this->getWorld()->isInLoadedTerrain($this->blockHit->getPosition()) && !$this->blockHit->isSameState($this->getWorld()->getBlock($this->blockHit->getPosition()))){
$this->blockHit = null;
}
@@ -167,16 +163,16 @@ abstract class Projectile extends Entity{
}
public function hasMovementUpdate() : bool{
- return $this->blockHit === null and parent::hasMovementUpdate();
+ return $this->blockHit === null && parent::hasMovementUpdate();
}
- public function move(float $dx, float $dy, float $dz) : void{
+ protected function move(float $dx, float $dy, float $dz) : void{
$this->blocksAround = null;
Timings::$entityMove->startTiming();
$start = $this->location->asVector3();
- $end = $start->addVector($this->motion);
+ $end = $start->add($dx, $dy, $dz);
$blockHit = null;
$entityHit = null;
@@ -198,7 +194,7 @@ abstract class Projectile extends Entity{
$newDiff = $end->subtractVector($start);
foreach($this->getWorld()->getCollidingEntities($this->boundingBox->addCoord($newDiff->x, $newDiff->y, $newDiff->z)->expand(1, 1, 1), $this) as $entity){
- if($entity->getId() === $this->getOwningEntityId() and $this->ticksLived < 5){
+ if($entity->getId() === $this->getOwningEntityId() && $this->ticksLived < 5){
continue;
}
@@ -257,8 +253,10 @@ abstract class Projectile extends Entity{
//recompute angles...
$f = sqrt(($this->motion->x ** 2) + ($this->motion->z ** 2));
- $this->location->yaw = (atan2($this->motion->x, $this->motion->z) * 180 / M_PI);
- $this->location->pitch = (atan2($this->motion->y, $f) * 180 / M_PI);
+ $this->setRotation(
+ atan2($this->motion->x, $this->motion->z) * 180 / M_PI,
+ atan2($this->motion->y, $f) * 180 / M_PI
+ );
}
$this->getWorld()->onEntityMoved($this);
diff --git a/src/entity/projectile/SplashPotion.php b/src/entity/projectile/SplashPotion.php
index a7ecfa47a..364ded7c2 100644
--- a/src/entity/projectile/SplashPotion.php
+++ b/src/entity/projectile/SplashPotion.php
@@ -99,14 +99,14 @@ class SplashPotion extends Throwable{
if($hasEffects){
if(!$this->willLinger()){
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){
- if($entity instanceof Living and $entity->isAlive()){
+ if($entity instanceof Living && $entity->isAlive()){
$distanceSquared = $entity->getEyePos()->distanceSquared($this->location);
if($distanceSquared > 16){ //4 blocks
continue;
}
$distanceMultiplier = 1 - (sqrt($distanceSquared) / 4);
- if($event instanceof ProjectileHitEntityEvent and $entity === $event->getEntityHit()){
+ if($event instanceof ProjectileHitEntityEvent && $entity === $event->getEntityHit()){
$distanceMultiplier = 1.0;
}
@@ -129,7 +129,7 @@ class SplashPotion extends Throwable{
}else{
//TODO: lingering potions
}
- }elseif($event instanceof ProjectileHitBlockEvent and $this->getPotionType()->equals(PotionType::WATER())){
+ }elseif($event instanceof ProjectileHitBlockEvent && $this->getPotionType()->equals(PotionType::WATER())){
$blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace());
if($blockIn->getId() === BlockLegacyIds::FIRE){
diff --git a/src/event/HandlerList.php b/src/event/HandlerList.php
index b452a382c..71b159ca5 100644
--- a/src/event/HandlerList.php
+++ b/src/event/HandlerList.php
@@ -47,7 +47,7 @@ class HandlerList{
*/
public function register(RegisteredListener $listener) : void{
if(isset($this->handlerSlots[$listener->getPriority()][spl_object_id($listener)])){
- throw new \InvalidStateException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
+ throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
}
$this->handlerSlots[$listener->getPriority()][spl_object_id($listener)] = $listener;
}
@@ -65,11 +65,11 @@ class HandlerList{
* @param RegisteredListener|Listener|Plugin $object
*/
public function unregister($object) : void{
- if($object instanceof Plugin or $object instanceof Listener){
+ if($object instanceof Plugin || $object instanceof Listener){
foreach($this->handlerSlots as $priority => $list){
foreach($list as $hash => $listener){
- if(($object instanceof Plugin and $listener->getPlugin() === $object)
- or ($object instanceof Listener and (new \ReflectionFunction($listener->getHandler()))->getClosureThis() === $object) //this doesn't even need to be a listener :D
+ if(($object instanceof Plugin && $listener->getPlugin() === $object)
+ || ($object instanceof Listener && (new \ReflectionFunction($listener->getHandler()))->getClosureThis() === $object) //this doesn't even need to be a listener :D
){
unset($this->handlerSlots[$priority][$hash]);
}
diff --git a/src/event/HandlerListManager.php b/src/event/HandlerListManager.php
index 0859b1e5e..864b639d7 100644
--- a/src/event/HandlerListManager.php
+++ b/src/event/HandlerListManager.php
@@ -45,7 +45,7 @@ class HandlerListManager{
* @param Plugin|Listener|RegisteredListener|null $object
*/
public function unregisterAll($object = null) : void{
- if($object instanceof Listener or $object instanceof Plugin or $object instanceof RegisteredListener){
+ if($object instanceof Listener || $object instanceof Plugin || $object instanceof RegisteredListener){
foreach($this->allLists as $h){
$h->unregister($object);
}
diff --git a/src/event/RegisteredListener.php b/src/event/RegisteredListener.php
index cc22a0511..d0732bc99 100644
--- a/src/event/RegisteredListener.php
+++ b/src/event/RegisteredListener.php
@@ -68,7 +68,7 @@ class RegisteredListener{
}
public function callEvent(Event $event) : void{
- if($event instanceof Cancellable and $event->isCancelled() and !$this->isHandlingCancelled()){
+ if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){
return;
}
$this->timings->startTiming();
diff --git a/src/event/block/BlockBreakEvent.php b/src/event/block/BlockBreakEvent.php
index 2540ecec4..e11a71ebc 100644
--- a/src/event/block/BlockBreakEvent.php
+++ b/src/event/block/BlockBreakEvent.php
@@ -103,8 +103,6 @@ class BlockBreakEvent extends BlockEvent implements Cancellable{
/**
* Variadic hack for easy array member type enforcement.
- *
- * @param Item ...$drops
*/
public function setDropsVariadic(Item ...$drops) : void{
$this->blockDrops = $drops;
diff --git a/src/event/block/BlockItemPickupEvent.php b/src/event/block/BlockItemPickupEvent.php
index 242840538..9b41b8730 100644
--- a/src/event/block/BlockItemPickupEvent.php
+++ b/src/event/block/BlockItemPickupEvent.php
@@ -57,4 +57,4 @@ class BlockItemPickupEvent extends BlockEvent implements Cancellable{
public function setInventory(?Inventory $inventory) : void{
$this->inventory = $inventory;
}
-}
\ No newline at end of file
+}
diff --git a/src/event/block/BlockMeltEvent.php b/src/event/block/BlockMeltEvent.php
new file mode 100644
index 000000000..a3b2f8fc7
--- /dev/null
+++ b/src/event/block/BlockMeltEvent.php
@@ -0,0 +1,31 @@
+getBlock());
+ }
+
+ public function getBrewingStand() : BrewingStand{
+ return $this->brewingStand;
+ }
+
+ /**
+ * Returns which slot of the brewing stand's inventory the potion is in.
+ */
+ public function getSlot() : int{
+ return $this->slot;
+ }
+
+ public function getInput() : Item{
+ return clone $this->input;
+ }
+
+ public function getResult() : Item{
+ return clone $this->result;
+ }
+
+ public function setResult(Item $result) : void{
+ $this->result = clone $result;
+ }
+
+ public function getRecipe() : BrewingRecipe{
+ return $this->recipe;
+ }
+}
diff --git a/src/event/block/BrewingFuelUseEvent.php b/src/event/block/BrewingFuelUseEvent.php
new file mode 100644
index 000000000..25b2c105c
--- /dev/null
+++ b/src/event/block/BrewingFuelUseEvent.php
@@ -0,0 +1,64 @@
+getBlock());
+ }
+
+ public function getBrewingStand() : BrewingStand{
+ return $this->brewingStand;
+ }
+
+ /**
+ * Returns how many times the fuel can be used for potion brewing before it runs out.
+ */
+ public function getFuelTime() : int{
+ return $this->fuelTime;
+ }
+
+ /**
+ * Sets how many times the fuel can be used for potion brewing before it runs out.
+ */
+ public function setFuelTime(int $fuelTime) : void{
+ if($fuelTime <= 0){
+ throw new \InvalidArgumentException("Fuel time must be positive");
+ }
+ $this->fuelTime = $fuelTime;
+ }
+}
diff --git a/src/event/block/ChestPairEvent.php b/src/event/block/ChestPairEvent.php
new file mode 100644
index 000000000..5343edf6f
--- /dev/null
+++ b/src/event/block/ChestPairEvent.php
@@ -0,0 +1,42 @@
+left; }
+
+ public function getRight() : Chest{ return $this->right; }
+}
diff --git a/src/event/block/StructureGrowEvent.php b/src/event/block/StructureGrowEvent.php
index 88fd54ae8..6dc462d7c 100644
--- a/src/event/block/StructureGrowEvent.php
+++ b/src/event/block/StructureGrowEvent.php
@@ -7,6 +7,7 @@ namespace pocketmine\event\block;
use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
+use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
/**
@@ -17,13 +18,23 @@ class StructureGrowEvent extends BlockEvent implements Cancellable{
use CancellableTrait;
private BlockTransaction $transaction;
+ private ?Player $player;
- public function __construct(Block $block, BlockTransaction $transaction){
+ public function __construct(Block $block, BlockTransaction $transaction, ?Player $player){
parent::__construct($block);
$this->transaction = $transaction;
+ $this->player = $player;
}
public function getTransaction() : BlockTransaction{
return $this->transaction;
}
-}
\ No newline at end of file
+
+ /**
+ * It returns the player which grows the structure.
+ * It returns null when the structure grows by itself.
+ */
+ public function getPlayer() : ?Player{
+ return $this->player;
+ }
+}
diff --git a/src/event/entity/EntityEffectRemoveEvent.php b/src/event/entity/EntityEffectRemoveEvent.php
index afd2ef631..35e59023c 100644
--- a/src/event/entity/EntityEffectRemoveEvent.php
+++ b/src/event/entity/EntityEffectRemoveEvent.php
@@ -29,7 +29,7 @@ namespace pocketmine\event\entity;
class EntityEffectRemoveEvent extends EntityEffectEvent{
public function cancel() : void{
if($this->getEffect()->getDuration() <= 0){
- throw new \InvalidStateException("Removal of expired effects cannot be cancelled");
+ throw new \LogicException("Removal of expired effects cannot be cancelled");
}
parent::cancel();
}
diff --git a/src/event/entity/EntityShootBowEvent.php b/src/event/entity/EntityShootBowEvent.php
index 32f4f4d70..5a871f282 100644
--- a/src/event/entity/EntityShootBowEvent.php
+++ b/src/event/entity/EntityShootBowEvent.php
@@ -39,7 +39,7 @@ class EntityShootBowEvent extends EntityEvent implements Cancellable{
/** @var Item */
private $bow;
- /** @var Projectile */
+ /** @var Entity */
private $projectile;
/** @var float */
private $force;
diff --git a/src/event/entity/ItemMergeEvent.php b/src/event/entity/ItemMergeEvent.php
new file mode 100644
index 000000000..4b8b7af7c
--- /dev/null
+++ b/src/event/entity/ItemMergeEvent.php
@@ -0,0 +1,52 @@
+
+ */
+class ItemMergeEvent extends EntityEvent implements Cancellable{
+ use CancellableTrait;
+
+ public function __construct(
+ ItemEntity $entity,
+ protected ItemEntity $target
+ ){
+ $this->entity = $entity;
+ }
+
+ /**
+ * Returns the merge destination.
+ */
+ public function getTarget() : ItemEntity{
+ return $this->target;
+ }
+
+}
diff --git a/src/event/player/PlayerCreationEvent.php b/src/event/player/PlayerCreationEvent.php
index d4056ee12..013fcdb2a 100644
--- a/src/event/player/PlayerCreationEvent.php
+++ b/src/event/player/PlayerCreationEvent.php
@@ -26,6 +26,7 @@ namespace pocketmine\event\player;
use pocketmine\event\Event;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\player\Player;
+use pocketmine\utils\Utils;
use function is_a;
/**
@@ -96,10 +97,7 @@ class PlayerCreationEvent extends Event{
* @phpstan-param class-string $class
*/
public function setPlayerClass($class) : void{
- if(!is_a($class, $this->baseClass, true)){
- throw new \RuntimeException("Class $class must extend " . $this->baseClass);
- }
-
+ Utils::testValidInstance($class, $this->baseClass);
$this->playerClass = $class;
}
}
diff --git a/src/event/player/PlayerDisplayNameChangeEvent.php b/src/event/player/PlayerDisplayNameChangeEvent.php
index 27de577c8..8956ca2c4 100644
--- a/src/event/player/PlayerDisplayNameChangeEvent.php
+++ b/src/event/player/PlayerDisplayNameChangeEvent.php
@@ -45,4 +45,4 @@ class PlayerDisplayNameChangeEvent extends PlayerEvent{
public function getNewName() : string{
return $this->newName;
}
-}
\ No newline at end of file
+}
diff --git a/src/event/player/PlayerEmoteEvent.php b/src/event/player/PlayerEmoteEvent.php
new file mode 100644
index 000000000..b10a3171e
--- /dev/null
+++ b/src/event/player/PlayerEmoteEvent.php
@@ -0,0 +1,51 @@
+player = $player;
+ }
+
+ public function getEmoteId() : string{
+ return $this->emoteId;
+ }
+
+ public function setEmoteId(string $emoteId) : void{
+ $this->emoteId = $emoteId;
+ }
+
+}
diff --git a/src/event/player/PlayerExperienceChangeEvent.php b/src/event/player/PlayerExperienceChangeEvent.php
index f1569d79d..0ca195bdc 100644
--- a/src/event/player/PlayerExperienceChangeEvent.php
+++ b/src/event/player/PlayerExperienceChangeEvent.php
@@ -82,6 +82,9 @@ class PlayerExperienceChangeEvent extends EntityEvent implements Cancellable{
}
public function setNewProgress(?float $newProgress) : void{
+ if($newProgress < 0.0 || $newProgress > 1.0){
+ throw new \InvalidArgumentException("XP progress must be in range 0-1");
+ }
$this->newProgress = $newProgress;
}
}
diff --git a/src/event/player/PlayerInteractEvent.php b/src/event/player/PlayerInteractEvent.php
index f67b72d9c..b76a8226c 100644
--- a/src/event/player/PlayerInteractEvent.php
+++ b/src/event/player/PlayerInteractEvent.php
@@ -31,7 +31,8 @@ use pocketmine\math\Vector3;
use pocketmine\player\Player;
/**
- * Called when a player interacts or touches a block (including air?)
+ * Called when a player interacts or touches a block.
+ * This is called for both left click (start break) and right click (use).
*/
class PlayerInteractEvent extends PlayerEvent implements Cancellable{
use CancellableTrait;
diff --git a/src/event/player/PlayerToggleGlideEvent.php b/src/event/player/PlayerToggleGlideEvent.php
new file mode 100644
index 000000000..c8baabd9f
--- /dev/null
+++ b/src/event/player/PlayerToggleGlideEvent.php
@@ -0,0 +1,40 @@
+player = $player;
+ }
+
+ public function isGliding() : bool{
+ return $this->isGliding;
+ }
+}
diff --git a/src/event/player/PlayerToggleSwimEvent.php b/src/event/player/PlayerToggleSwimEvent.php
new file mode 100644
index 000000000..382b3c687
--- /dev/null
+++ b/src/event/player/PlayerToggleSwimEvent.php
@@ -0,0 +1,40 @@
+player = $player;
+ }
+
+ public function isSwimming() : bool{
+ return $this->isSwimming;
+ }
+}
diff --git a/src/event/player/PlayerViewDistanceChangeEvent.php b/src/event/player/PlayerViewDistanceChangeEvent.php
new file mode 100644
index 000000000..0048672ab
--- /dev/null
+++ b/src/event/player/PlayerViewDistanceChangeEvent.php
@@ -0,0 +1,55 @@
+player = $player;
+ $this->oldDistance = $oldDistance;
+ $this->newDistance = $newDistance;
+ }
+
+ /**
+ * Returns the new view radius, measured in chunks.
+ */
+ public function getNewDistance() : int{
+ return $this->newDistance;
+ }
+
+ /**
+ * Returns the old view radius, measured in chunks.
+ * A value of -1 means that the player has just connected and did not have a view distance before this event.
+ */
+ public function getOldDistance() : int{
+ return $this->oldDistance;
+ }
+}
diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php
index 4d79d7d94..5700923ac 100644
--- a/src/inventory/BaseInventory.php
+++ b/src/inventory/BaseInventory.php
@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
use pocketmine\utils\ObjectSet;
use function array_slice;
@@ -89,7 +89,7 @@ abstract class BaseInventory implements Inventory{
public function setItem(int $index, Item $item) : void{
if($item->isNull()){
- $item = ItemFactory::air();
+ $item = VanillaItems::AIR();
}else{
$item = clone $item;
}
@@ -146,7 +146,7 @@ abstract class BaseInventory implements Inventory{
$checkTags = $exact || $item->hasNamedTag();
foreach($this->getContents() as $index => $i){
- if($item->equals($i, $checkDamage, $checkTags) and ($i->getCount() === $count or (!$exact and $i->getCount() > $count))){
+ if($item->equals($i, $checkDamage, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){
return $index;
}
}
@@ -169,6 +169,10 @@ abstract class BaseInventory implements Inventory{
}
public function canAddItem(Item $item) : bool{
+ return $this->getAddableItemQuantity($item) === $item->getCount();
+ }
+
+ public function getAddableItemQuantity(Item $item) : int{
$count = $item->getCount();
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
$slot = $this->getItem($i);
@@ -181,11 +185,11 @@ abstract class BaseInventory implements Inventory{
}
if($count <= 0){
- return true;
+ return $item->getCount();
}
}
- return false;
+ return $item->getCount() - $count;
}
public function addItem(Item ...$slots) : array{
@@ -220,7 +224,7 @@ abstract class BaseInventory implements Inventory{
$emptySlots[] = $i;
}
- if($slot->canStackWith($item) and $item->getCount() < $item->getMaxStackSize()){
+ if($slot->canStackWith($item) && $item->getCount() < $item->getMaxStackSize()){
$amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize());
if($amount > 0){
$slot->setCount($slot->getCount() - $amount);
@@ -286,7 +290,7 @@ abstract class BaseInventory implements Inventory{
}
public function clear(int $index) : void{
- $this->setItem($index, ItemFactory::air());
+ $this->setItem($index, VanillaItems::AIR());
}
public function clearAll() : void{
@@ -363,7 +367,7 @@ abstract class BaseInventory implements Inventory{
}
public function slotExists(int $slot) : bool{
- return $slot >= 0 and $slot < $this->getSize();
+ return $slot >= 0 && $slot < $this->getSize();
}
public function getListeners() : ObjectSet{
diff --git a/src/inventory/CreativeInventory.php b/src/inventory/CreativeInventory.php
index 99ad5f60e..e71966f44 100644
--- a/src/inventory/CreativeInventory.php
+++ b/src/inventory/CreativeInventory.php
@@ -37,7 +37,7 @@ final class CreativeInventory{
private $creative = [];
private function __construct(){
- $creativeItems = json_decode(file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "vanilla", "creativeitems.json")), true);
+ $creativeItems = json_decode(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "creativeitems.json")), true);
foreach($creativeItems as $data){
$item = Item::jsonDeserialize($data);
diff --git a/src/inventory/Inventory.php b/src/inventory/Inventory.php
index 0a5e3b766..8eef250b1 100644
--- a/src/inventory/Inventory.php
+++ b/src/inventory/Inventory.php
@@ -52,8 +52,6 @@ interface Inventory{
*
* Returns the Items that did not fit.
*
- * @param Item ...$slots
- *
* @return Item[]
*/
public function addItem(Item ...$slots) : array;
@@ -63,12 +61,15 @@ interface Inventory{
*/
public function canAddItem(Item $item) : bool;
+ /**
+ * Returns how many items from the given itemstack can be added to this inventory.
+ */
+ public function getAddableItemQuantity(Item $item) : int;
+
/**
* Removes the given Item from the inventory.
* It will return the Items that couldn't be removed.
*
- * @param Item ...$slots
- *
* @return Item[]
*/
public function removeItem(Item ...$slots) : array;
diff --git a/src/inventory/PlayerCraftingInventory.php b/src/inventory/PlayerCraftingInventory.php
new file mode 100644
index 000000000..0c02eb47b
--- /dev/null
+++ b/src/inventory/PlayerCraftingInventory.php
@@ -0,0 +1,36 @@
+holder; }
+}
diff --git a/src/inventory/PlayerCursorInventory.php b/src/inventory/PlayerCursorInventory.php
index 9cd948c58..7c219fd00 100644
--- a/src/inventory/PlayerCursorInventory.php
+++ b/src/inventory/PlayerCursorInventory.php
@@ -25,7 +25,7 @@ namespace pocketmine\inventory;
use pocketmine\player\Player;
-class PlayerCursorInventory extends SimpleInventory{
+class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{
/** @var Player */
protected $holder;
diff --git a/src/inventory/PlayerInventory.php b/src/inventory/PlayerInventory.php
index 253ddfdea..1da30d2e9 100644
--- a/src/inventory/PlayerInventory.php
+++ b/src/inventory/PlayerInventory.php
@@ -49,7 +49,7 @@ class PlayerInventory extends SimpleInventory{
}
public function isHotbarSlot(int $slot) : bool{
- return $slot >= 0 and $slot <= $this->getHotbarSize();
+ return $slot >= 0 && $slot <= $this->getHotbarSize();
}
/**
diff --git a/src/inventory/SimpleInventory.php b/src/inventory/SimpleInventory.php
index f05b527a2..28b7d8b47 100644
--- a/src/inventory/SimpleInventory.php
+++ b/src/inventory/SimpleInventory.php
@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
/**
* This class provides a complete implementation of a regular inventory.
@@ -49,7 +49,7 @@ class SimpleInventory extends BaseInventory{
}
public function getItem(int $index) : Item{
- return $this->slots[$index] !== null ? clone $this->slots[$index] : ItemFactory::air();
+ return $this->slots[$index] !== null ? clone $this->slots[$index] : VanillaItems::AIR();
}
/**
@@ -62,7 +62,7 @@ class SimpleInventory extends BaseInventory{
if($slot !== null){
$contents[$i] = clone $slot;
}elseif($includeEmpty){
- $contents[$i] = ItemFactory::air();
+ $contents[$i] = VanillaItems::AIR();
}
}
@@ -71,10 +71,10 @@ class SimpleInventory extends BaseInventory{
protected function internalSetContents(array $items) : void{
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
- if(!isset($items[$i])){
- $this->clear($i);
+ if(!isset($items[$i]) || $items[$i]->isNull()){
+ $this->slots[$i] = null;
}else{
- $this->setItem($i, $items[$i]);
+ $this->slots[$i] = clone $items[$i];
}
}
}
diff --git a/src/inventory/TemporaryInventory.php b/src/inventory/TemporaryInventory.php
new file mode 100644
index 000000000..b2905dd8b
--- /dev/null
+++ b/src/inventory/TemporaryInventory.php
@@ -0,0 +1,28 @@
+ $txItem){
- if($txItem->equals($recipeItem, !$wildcards or !$recipeItem->hasAnyDamageValue(), !$wildcards or $recipeItem->hasNamedTag())){
+ if($txItem->equals($recipeItem, !$wildcards || !$recipeItem->hasAnyDamageValue(), !$wildcards || $recipeItem->hasNamedTag())){
$haveCount += $txItem->getCount();
unset($txItems[$j]);
}
diff --git a/src/inventory/transaction/TransactionBuilderInventory.php b/src/inventory/transaction/TransactionBuilderInventory.php
index 8888b4a45..58efacd16 100644
--- a/src/inventory/transaction/TransactionBuilderInventory.php
+++ b/src/inventory/transaction/TransactionBuilderInventory.php
@@ -27,7 +27,7 @@ use pocketmine\inventory\BaseInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
/**
* This class facilitates generating SlotChangeActions to build an inventory transaction.
@@ -62,7 +62,7 @@ final class TransactionBuilderInventory extends BaseInventory{
protected function internalSetItem(int $index, Item $item) : void{
if(!$item->equalsExact($this->actualInventory->getItem($index))){
- $this->changedSlots[$index] = $item->isNull() ? ItemFactory::air() : clone $item;
+ $this->changedSlots[$index] = $item->isNull() ? VanillaItems::AIR() : clone $item;
}
}
diff --git a/src/inventory/transaction/action/CreateItemAction.php b/src/inventory/transaction/action/CreateItemAction.php
index dbe02798e..a7a28b47e 100644
--- a/src/inventory/transaction/action/CreateItemAction.php
+++ b/src/inventory/transaction/action/CreateItemAction.php
@@ -26,7 +26,7 @@ namespace pocketmine\inventory\transaction\action;
use pocketmine\inventory\CreativeInventory;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
/**
@@ -36,7 +36,7 @@ use pocketmine\player\Player;
class CreateItemAction extends InventoryAction{
public function __construct(Item $sourceItem){
- parent::__construct($sourceItem, ItemFactory::air());
+ parent::__construct($sourceItem, VanillaItems::AIR());
}
public function validate(Player $source) : void{
diff --git a/src/inventory/transaction/action/DestroyItemAction.php b/src/inventory/transaction/action/DestroyItemAction.php
index 6edd30af2..a623d5700 100644
--- a/src/inventory/transaction/action/DestroyItemAction.php
+++ b/src/inventory/transaction/action/DestroyItemAction.php
@@ -25,7 +25,7 @@ namespace pocketmine\inventory\transaction\action;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
/**
@@ -35,7 +35,7 @@ use pocketmine\player\Player;
class DestroyItemAction extends InventoryAction{
public function __construct(Item $targetItem){
- parent::__construct(ItemFactory::air(), $targetItem);
+ parent::__construct(VanillaItems::AIR(), $targetItem);
}
public function validate(Player $source) : void{
diff --git a/src/inventory/transaction/action/DropItemAction.php b/src/inventory/transaction/action/DropItemAction.php
index 2e20d5f93..62db5abac 100644
--- a/src/inventory/transaction/action/DropItemAction.php
+++ b/src/inventory/transaction/action/DropItemAction.php
@@ -26,7 +26,7 @@ namespace pocketmine\inventory\transaction\action;
use pocketmine\event\player\PlayerDropItemEvent;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
+use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
/**
@@ -35,7 +35,7 @@ use pocketmine\player\Player;
class DropItemAction extends InventoryAction{
public function __construct(Item $targetItem){
- parent::__construct(ItemFactory::air(), $targetItem);
+ parent::__construct(VanillaItems::AIR(), $targetItem);
}
public function validate(Player $source) : void{
diff --git a/src/item/Armor.php b/src/item/Armor.php
index 39ced43e3..cd3d6bd55 100644
--- a/src/item/Armor.php
+++ b/src/item/Armor.php
@@ -96,7 +96,7 @@ class Armor extends Durable{
foreach($this->getEnchantments() as $enchantment){
$type = $enchantment->getType();
- if($type instanceof ProtectionEnchantment and $type->isApplicable($event)){
+ if($type instanceof ProtectionEnchantment && $type->isApplicable($event)){
$epf += $type->getProtectionFactor($enchantment->getLevel());
}
}
@@ -110,7 +110,7 @@ class Armor extends Durable{
$chance = 1 / ($unbreakingLevel + 1);
for($i = 0; $i < $amount; ++$i){
- if(mt_rand(1, 100) > 60 and lcg_value() > $chance){ //unbreaking only applies to armor 40% of the time at best
+ if(mt_rand(1, 100) > 60 && lcg_value() > $chance){ //unbreaking only applies to armor 40% of the time at best
$negated++;
}
}
diff --git a/src/item/Bow.php b/src/item/Bow.php
index fd6364e84..c74b91f4f 100644
--- a/src/item/Bow.php
+++ b/src/item/Bow.php
@@ -52,7 +52,7 @@ class Bow extends Tool implements Releasable{
default => null
};
- if($player->hasFiniteResources() and $inventory === null){
+ if($player->hasFiniteResources() && $inventory === null){
return ItemUseResult::FAIL();
}
@@ -85,7 +85,7 @@ class Bow extends Tool implements Releasable{
}
$ev = new EntityShootBowEvent($player, $this, $entity, $baseForce * 3);
- if($baseForce < 0.1 or $diff < 5 or $player->isSpectator()){
+ if($baseForce < 0.1 || $diff < 5 || $player->isSpectator()){
$ev->cancel();
}
@@ -123,4 +123,8 @@ class Bow extends Tool implements Releasable{
return ItemUseResult::SUCCESS();
}
+
+ public function canStartUsingItem(Player $player) : bool{
+ return !$player->hasFiniteResources() || $player->getOffHandInventory()->contains($arrow = VanillaItems::ARROW()) || $player->getInventory()->contains($arrow);
+ }
}
diff --git a/src/item/Bucket.php b/src/item/Bucket.php
index b7fd2a19f..3d6c818a1 100644
--- a/src/item/Bucket.php
+++ b/src/item/Bucket.php
@@ -38,7 +38,7 @@ class Bucket extends Item{
public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{
//TODO: move this to generic placement logic
- if($blockClicked instanceof Liquid and $blockClicked->isSource()){
+ if($blockClicked instanceof Liquid && $blockClicked->isSource()){
$stack = clone $this;
$stack->pop();
diff --git a/src/item/ChorusFruit.php b/src/item/ChorusFruit.php
index 7cf621e67..51364b15b 100644
--- a/src/item/ChorusFruit.php
+++ b/src/item/ChorusFruit.php
@@ -61,7 +61,7 @@ class ChorusFruit extends Food{
$y = mt_rand($minY, $maxY);
$z = mt_rand($minZ, $maxZ);
- while($y >= 0 and !$world->getBlockAt($x, $y, $z)->isSolid()){
+ while($y >= 0 && !$world->getBlockAt($x, $y, $z)->isSolid()){
$y--;
}
if($y < 0){
@@ -70,7 +70,7 @@ class ChorusFruit extends Food{
$blockUp = $world->getBlockAt($x, $y + 1, $z);
$blockUp2 = $world->getBlockAt($x, $y + 2, $z);
- if($blockUp->isSolid() or $blockUp instanceof Liquid or $blockUp2->isSolid() or $blockUp2 instanceof Liquid){
+ if($blockUp->isSolid() || $blockUp instanceof Liquid || $blockUp2->isSolid() || $blockUp2 instanceof Liquid){
continue;
}
diff --git a/src/item/Durable.php b/src/item/Durable.php
index 004525808..9d4a2d044 100644
--- a/src/item/Durable.php
+++ b/src/item/Durable.php
@@ -62,7 +62,7 @@ abstract class Durable extends Item{
* @return bool if any damage was applied to the item
*/
public function applyDamage(int $amount) : bool{
- if($this->isUnbreakable() or $this->isBroken()){
+ if($this->isUnbreakable() || $this->isBroken()){
return false;
}
@@ -81,7 +81,7 @@ abstract class Durable extends Item{
}
public function setDamage(int $damage) : Item{
- if($damage < 0 or $damage > $this->getMaxDurability()){
+ if($damage < 0 || $damage > $this->getMaxDurability()){
throw new \InvalidArgumentException("Damage must be in range 0 - " . $this->getMaxDurability());
}
$this->damage = $damage;
diff --git a/src/item/Food.php b/src/item/Food.php
index ff737e329..e58924a30 100644
--- a/src/item/Food.php
+++ b/src/item/Food.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\entity\Living;
+use pocketmine\player\Player;
abstract class Food extends Item implements FoodSourceItem{
public function requiresHunger() : bool{
@@ -31,7 +32,7 @@ abstract class Food extends Item implements FoodSourceItem{
}
public function getResidue() : Item{
- return ItemFactory::air();
+ return VanillaItems::AIR();
}
public function getAdditionalEffects() : array{
@@ -41,4 +42,8 @@ abstract class Food extends Item implements FoodSourceItem{
public function onConsume(Living $consumer) : void{
}
+
+ public function canStartUsingItem(Player $player) : bool{
+ return !$this->requiresHunger() || $player->getHungerManager()->isHungry();
+ }
}
diff --git a/src/item/Item.php b/src/item/Item.php
index b28bf9066..a1b2a6eab 100644
--- a/src/item/Item.php
+++ b/src/item/Item.php
@@ -31,12 +31,14 @@ use pocketmine\block\BlockBreakInfo;
use pocketmine\block\BlockToolType;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\EnchantmentIdMap;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\Entity;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\math\Vector3;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtDataException;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\nbt\tag\ShortTag;
@@ -240,6 +242,7 @@ class Item implements \JsonSerializable{
* Sets the Item's NBT from the supplied CompoundTag object.
*
* @return $this
+ * @throws NbtException
*/
public function setNamedTag(CompoundTag $tag) : Item{
if($tag->getCount() === 0){
@@ -255,6 +258,7 @@ class Item implements \JsonSerializable{
/**
* Removes the Item's NBT.
* @return $this
+ * @throws NbtException
*/
public function clearNamedTag() : Item{
$this->nbt = new CompoundTag();
@@ -262,6 +266,9 @@ class Item implements \JsonSerializable{
return $this;
}
+ /**
+ * @throws NbtException
+ */
protected function deserializeCompoundTag(CompoundTag $tag) : void{
$this->customName = "";
$this->lore = [];
@@ -270,7 +277,7 @@ class Item implements \JsonSerializable{
if($display !== null){
$this->customName = $display->getString(self::TAG_DISPLAY_NAME, $this->customName);
$lore = $display->getListTag(self::TAG_DISPLAY_LORE);
- if($lore !== null and $lore->getTagType() === NBT::TAG_String){
+ if($lore !== null && $lore->getTagType() === NBT::TAG_String){
/** @var StringTag $t */
foreach($lore as $t){
$this->lore[] = $t->getValue();
@@ -280,7 +287,7 @@ class Item implements \JsonSerializable{
$this->removeEnchantments();
$enchantments = $tag->getListTag(self::TAG_ENCH);
- if($enchantments !== null and $enchantments->getTagType() === NBT::TAG_Compound){
+ if($enchantments !== null && $enchantments->getTagType() === NBT::TAG_Compound){
/** @var CompoundTag $enchantment */
foreach($enchantments as $enchantment){
$magicNumber = $enchantment->getShort("id", -1);
@@ -405,7 +412,7 @@ class Item implements \JsonSerializable{
}
public function isNull() : bool{
- return $this->count <= 0 or $this->getId() === ItemIds::AIR;
+ return $this->count <= 0 || $this->getId() === ItemIds::AIR;
}
/**
@@ -561,9 +568,9 @@ class Item implements \JsonSerializable{
* @param bool $checkCompound Whether to verify that the items' NBT match.
*/
final public function equals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{
- return $this->getId() === $item->getId() and
- (!$checkDamage or $this->getMeta() === $item->getMeta()) and
- (!$checkCompound or $this->getNamedTag()->equals($item->getNamedTag()));
+ return $this->getId() === $item->getId() &&
+ (!$checkDamage || $this->getMeta() === $item->getMeta()) &&
+ (!$checkCompound || $this->getNamedTag()->equals($item->getNamedTag()));
}
/**
@@ -577,7 +584,7 @@ class Item implements \JsonSerializable{
* Returns whether the specified item stack has the same ID, damage, NBT and count as this item stack.
*/
final public function equalsExact(Item $other) : bool{
- return $this->equals($other, true, true) and $this->count === $other->count;
+ return $this->equals($other, true, true) && $this->count === $other->count;
}
final public function __toString() : string{
@@ -665,9 +672,11 @@ class Item implements \JsonSerializable{
/**
* Deserializes an Item from an NBT CompoundTag
+ * @throws NbtException
+ * @throws SavedDataLoadingException
*/
public static function nbtDeserialize(CompoundTag $tag) : Item{
- if($tag->getTag("id") === null or $tag->getTag("Count") === null){
+ if($tag->getTag("id") === null || $tag->getTag("Count") === null){
return ItemFactory::getInstance()->get(0);
}
@@ -682,11 +691,11 @@ class Item implements \JsonSerializable{
$item = LegacyStringToItemParser::getInstance()->parse($idTag->getValue() . ":$meta");
}catch(LegacyStringToItemParserException $e){
//TODO: improve error handling
- return ItemFactory::air();
+ return VanillaItems::AIR();
}
$item->setCount($count);
}else{
- throw new \InvalidArgumentException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given");
+ throw new SavedDataLoadingException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given");
}
$itemNBT = $tag->getCompoundTag("tag");
diff --git a/src/item/ItemEnchantmentHandlingTrait.php b/src/item/ItemEnchantmentHandlingTrait.php
index 927509a41..9ba344139 100644
--- a/src/item/ItemEnchantmentHandlingTrait.php
+++ b/src/item/ItemEnchantmentHandlingTrait.php
@@ -42,7 +42,7 @@ trait ItemEnchantmentHandlingTrait{
public function hasEnchantment(Enchantment $enchantment, int $level = -1) : bool{
$id = spl_object_id($enchantment);
- return isset($this->enchantments[$id]) and ($level === -1 or $this->enchantments[$id]->getLevel() === $level);
+ return isset($this->enchantments[$id]) && ($level === -1 || $this->enchantments[$id]->getLevel() === $level);
}
public function getEnchantment(Enchantment $enchantment) : ?EnchantmentInstance{
@@ -54,7 +54,7 @@ trait ItemEnchantmentHandlingTrait{
*/
public function removeEnchantment(Enchantment $enchantment, int $level = -1) : self{
$instance = $this->getEnchantment($enchantment);
- if($instance !== null and ($level === -1 or $instance->getLevel() === $level)){
+ if($instance !== null && ($level === -1 || $instance->getLevel() === $level)){
unset($this->enchantments[spl_object_id($enchantment)]);
}
diff --git a/src/item/ItemFactory.php b/src/item/ItemFactory.php
index 5fb7438a1..dbceea1b0 100644
--- a/src/item/ItemFactory.php
+++ b/src/item/ItemFactory.php
@@ -41,6 +41,7 @@ use pocketmine\entity\Villager;
use pocketmine\entity\Zombie;
use pocketmine\inventory\ArmorInventory;
use pocketmine\math\Vector3;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\utils\SingletonTrait;
use pocketmine\world\World;
@@ -251,6 +252,7 @@ class ItemFactory{
$this->register(new Steak(new ItemIdentifier(ItemIds::STEAK, 0), "Steak"));
$this->register(new Stick(new ItemIdentifier(ItemIds::STICK, 0), "Stick"));
$this->register(new StringItem(new ItemIdentifier(ItemIds::STRING, 0), "String"));
+ $this->register(new SweetBerries(new ItemIdentifier(ItemIds::SWEET_BERRIES, 0), "Sweet Berries"));
$this->register(new Totem(new ItemIdentifier(ItemIds::TOTEM, 0), "Totem of Undying"));
$this->register(new WheatSeeds(new ItemIdentifier(ItemIds::WHEAT_SEEDS, 0), "Wheat Seeds"));
$this->register(new WritableBook(new ItemIdentifier(ItemIds::WRITABLE_BOOK, 0), "Book & Quill"));
@@ -326,7 +328,6 @@ class ItemFactory{
//TODO: minecraft:shield
//TODO: minecraft:sparkler
//TODO: minecraft:spawn_egg
- $this->register(new SweetBerries(new ItemIdentifier(ItemIds::SWEET_BERRIES, 0), "Sweet Berries"));
//TODO: minecraft:tnt_minecart
//TODO: minecraft:trident
//TODO: minecraft:turtle_helmet
@@ -417,7 +418,7 @@ class ItemFactory{
$id = $item->getId();
$variant = $item->getMeta();
- if(!$override and $this->isRegistered($id, $variant)){
+ if(!$override && $this->isRegistered($id, $variant)){
throw new \RuntimeException("Trying to overwrite an already registered item");
}
@@ -444,6 +445,7 @@ class ItemFactory{
* Deserializes an item from the provided legacy ID, legacy meta, count and NBT.
*
* @throws \InvalidArgumentException
+ * @throws NbtException
*/
public function get(int $id, int $meta = 0, int $count = 1, ?CompoundTag $tags = null) : Item{
/** @var Item|null $item */
@@ -451,7 +453,7 @@ class ItemFactory{
if($meta !== -1){
if(isset($this->list[$offset = self::getListOffset($id, $meta)])){
$item = clone $this->list[$offset];
- }elseif(isset($this->list[$zero = self::getListOffset($id, 0)]) and $this->list[$zero] instanceof Durable){
+ }elseif(isset($this->list[$zero = self::getListOffset($id, 0)]) && $this->list[$zero] instanceof Durable){
if($meta <= $this->list[$zero]->getMaxDurability()){
$item = clone $this->list[$zero];
$item->setDamage($meta);
@@ -476,6 +478,10 @@ class ItemFactory{
return $item;
}
+ /**
+ * @deprecated
+ * @see VanillaItems::AIR()
+ */
public static function air() : Item{
return self::getInstance()->get(ItemIds::AIR, 0, 0);
}
@@ -492,7 +498,7 @@ class ItemFactory{
}
private static function getListOffset(int $id, int $variant) : int{
- if($id < -0x8000 or $id > 0x7fff){
+ if($id < -0x8000 || $id > 0x7fff){
throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff);
}
return (($id & 0xffff) << 16) | ($variant & 0xffff);
diff --git a/src/item/ItemIdentifier.php b/src/item/ItemIdentifier.php
index 0242706f4..414cd5e0e 100644
--- a/src/item/ItemIdentifier.php
+++ b/src/item/ItemIdentifier.php
@@ -31,7 +31,7 @@ final class ItemIdentifier{
private $meta;
public function __construct(int $id, int $meta){
- if($id < -0x8000 or $id > 0x7fff){ //signed short range
+ if($id < -0x8000 || $id > 0x7fff){ //signed short range
throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff);
}
$this->id = $id;
diff --git a/src/item/LegacyStringToItemParser.php b/src/item/LegacyStringToItemParser.php
index 9edb37c19..3065cb1bd 100644
--- a/src/item/LegacyStringToItemParser.php
+++ b/src/item/LegacyStringToItemParser.php
@@ -25,13 +25,13 @@ namespace pocketmine\item;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function explode;
use function file_get_contents;
use function is_array;
use function is_int;
use function is_numeric;
-use function is_string;
use function json_decode;
use function str_replace;
use function strtolower;
@@ -56,15 +56,14 @@ final class LegacyStringToItemParser{
private static function make() : self{
$result = new self(ItemFactory::getInstance());
- $mappingsRaw = @file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'item_from_string_bc_map.json'));
- if($mappingsRaw === false) throw new AssumptionFailedError("Missing required resource file");
+ $mappingsRaw = Utils::assumeNotFalse(@file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'item_from_string_bc_map.json')), "Missing required resource file");
$mappings = json_decode($mappingsRaw, true);
if(!is_array($mappings)) throw new AssumptionFailedError("Invalid mappings format, expected array");
foreach($mappings as $name => $id){
- if(!is_string($name) or !is_int($id)) throw new AssumptionFailedError("Invalid mappings format, expected string keys and int values");
- $result->addMapping($name, $id);
+ if(!is_int($id)) throw new AssumptionFailedError("Invalid mappings format, expected int values");
+ $result->addMapping((string) $name, $id);
}
return $result;
@@ -84,6 +83,14 @@ final class LegacyStringToItemParser{
$this->map[$alias] = $id;
}
+ /**
+ * @return int[]
+ * @phpstan-return array
+ */
+ public function getMappings() : array{
+ return $this->map;
+ }
+
/**
* Tries to parse the specified string into Item types.
*
@@ -106,9 +113,7 @@ final class LegacyStringToItemParser{
throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value");
}
- if(is_numeric($b[0])){
- $item = $this->itemFactory->get((int) $b[0], $meta);
- }elseif(isset($this->map[strtolower($b[0])])){
+ if(isset($this->map[strtolower($b[0])])){
$item = $this->itemFactory->get($this->map[strtolower($b[0])], $meta);
}else{
throw new LegacyStringToItemParserException("Unable to resolve \"" . $input . "\" to a valid item");
diff --git a/src/item/MilkBucket.php b/src/item/MilkBucket.php
index 4419a510f..f8c8b3ddf 100644
--- a/src/item/MilkBucket.php
+++ b/src/item/MilkBucket.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\entity\Living;
+use pocketmine\player\Player;
class MilkBucket extends Item implements ConsumableItem{
@@ -42,4 +43,8 @@ class MilkBucket extends Item implements ConsumableItem{
public function onConsume(Living $consumer) : void{
$consumer->getEffects()->clear();
}
+
+ public function canStartUsingItem(Player $player) : bool{
+ return true;
+ }
}
diff --git a/src/item/Potion.php b/src/item/Potion.php
index 5b2bbc17e..5be216e96 100644
--- a/src/item/Potion.php
+++ b/src/item/Potion.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\entity\Living;
+use pocketmine\player\Player;
class Potion extends Item implements ConsumableItem{
@@ -52,4 +53,8 @@ class Potion extends Item implements ConsumableItem{
public function getResidue() : Item{
return VanillaItems::GLASS_BOTTLE();
}
+
+ public function canStartUsingItem(Player $player) : bool{
+ return true;
+ }
}
diff --git a/src/item/Releasable.php b/src/item/Releasable.php
index 07e77cf28..acb9d65c8 100644
--- a/src/item/Releasable.php
+++ b/src/item/Releasable.php
@@ -23,9 +23,13 @@ declare(strict_types=1);
namespace pocketmine\item;
+use pocketmine\player\Player;
+
/**
* Interface implemented by objects that can be used.
*/
interface Releasable{
+ public function canStartUsingItem(Player $player) : bool;
+
}
diff --git a/src/item/SplashPotion.php b/src/item/SplashPotion.php
index 1b338a96b..e25cbf843 100644
--- a/src/item/SplashPotion.php
+++ b/src/item/SplashPotion.php
@@ -37,6 +37,8 @@ class SplashPotion extends ProjectileItem{
$this->potionType = $potionType;
}
+ public function getType() : PotionType{ return $this->potionType; }
+
public function getMaxStackSize() : int{
return 1;
}
diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php
index 700ada9bf..843beae49 100644
--- a/src/item/StringToItemParser.php
+++ b/src/item/StringToItemParser.php
@@ -29,25 +29,16 @@ use pocketmine\block\utils\DyeColor;
use pocketmine\block\utils\SlabType;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\SingletonTrait;
-use function array_keys;
-use function str_replace;
-use function strtolower;
-use function trim;
+use pocketmine\utils\StringToTParser;
/**
* Handles parsing items from strings. This is used to interpret names from the /give command (and others).
- * Custom aliases may be registered.
- * Note that the aliases should be user-friendly, i.e. easily readable and writable.
+ *
+ * @phpstan-extends StringToTParser-
*/
-final class StringToItemParser{
+final class StringToItemParser extends StringToTParser{
use SingletonTrait;
- /**
- * @var \Closure[]
- * @phpstan-var array
- */
- private array $callbackMap = [];
-
private static function make() : self{
$result = new self;
@@ -106,7 +97,7 @@ final class StringToItemParser{
$result->registerBlock("anvil", fn() => VanillaBlocks::ANVIL());
$result->registerBlock("ateupd_block", fn() => VanillaBlocks::INFO_UPDATE2());
$result->registerBlock("azure_bluet", fn() => VanillaBlocks::AZURE_BLUET());
- $result->registerBlock("bamboo", fn() => VanillaBlocks::BAMBOO_SAPLING());
+ $result->registerBlock("bamboo", fn() => VanillaBlocks::BAMBOO());
$result->registerBlock("bamboo_sapling", fn() => VanillaBlocks::BAMBOO_SAPLING());
$result->registerBlock("banner", fn() => VanillaBlocks::BANNER());
$result->registerBlock("barrel", fn() => VanillaBlocks::BARREL());
@@ -167,6 +158,7 @@ final class StringToItemParser{
$result->registerBlock("chemical_heat", fn() => VanillaBlocks::CHEMICAL_HEAT());
$result->registerBlock("chemistry_table", fn() => VanillaBlocks::COMPOUND_CREATOR());
$result->registerBlock("chest", fn() => VanillaBlocks::CHEST());
+ $result->registerBlock("chipped_anvil", fn() => VanillaBlocks::ANVIL()->setDamage(1));
$result->registerBlock("chiseled_quartz", fn() => VanillaBlocks::CHISELED_QUARTZ());
$result->registerBlock("chiseled_red_sandstone", fn() => VanillaBlocks::CHISELED_RED_SANDSTONE());
$result->registerBlock("chiseled_sandstone", fn() => VanillaBlocks::CHISELED_SANDSTONE());
@@ -174,6 +166,7 @@ final class StringToItemParser{
$result->registerBlock("clay_block", fn() => VanillaBlocks::CLAY());
$result->registerBlock("coal_block", fn() => VanillaBlocks::COAL());
$result->registerBlock("coal_ore", fn() => VanillaBlocks::COAL_ORE());
+ $result->registerBlock("coarse_dirt", fn() => VanillaBlocks::DIRT()->setCoarse(true));
$result->registerBlock("cobble", fn() => VanillaBlocks::COBBLESTONE());
$result->registerBlock("cobble_stairs", fn() => VanillaBlocks::COBBLESTONE_STAIRS());
$result->registerBlock("cobble_wall", fn() => VanillaBlocks::COBBLESTONE_WALL());
@@ -209,6 +202,7 @@ final class StringToItemParser{
$result->registerBlock("cut_sandstone", fn() => VanillaBlocks::CUT_SANDSTONE());
$result->registerBlock("cut_sandstone_slab", fn() => VanillaBlocks::CUT_SANDSTONE_SLAB());
$result->registerBlock("cyan_glazed_terracotta", fn() => VanillaBlocks::CYAN_GLAZED_TERRACOTTA());
+ $result->registerBlock("damaged_anvil", fn() => VanillaBlocks::ANVIL()->setDamage(2));
$result->registerBlock("dandelion", fn() => VanillaBlocks::DANDELION());
$result->registerBlock("dark_oak_button", fn() => VanillaBlocks::DARK_OAK_BUTTON());
$result->registerBlock("dark_oak_door", fn() => VanillaBlocks::DARK_OAK_DOOR());
@@ -223,6 +217,7 @@ final class StringToItemParser{
$result->registerBlock("dark_oak_sign", fn() => VanillaBlocks::DARK_OAK_SIGN());
$result->registerBlock("dark_oak_slab", fn() => VanillaBlocks::DARK_OAK_SLAB());
$result->registerBlock("dark_oak_stairs", fn() => VanillaBlocks::DARK_OAK_STAIRS());
+ $result->registerBlock("dark_oak_standing_sign", fn() => VanillaBlocks::DARK_OAK_SIGN());
$result->registerBlock("dark_oak_trapdoor", fn() => VanillaBlocks::DARK_OAK_TRAPDOOR());
$result->registerBlock("dark_oak_wall_sign", fn() => VanillaBlocks::DARK_OAK_WALL_SIGN());
$result->registerBlock("dark_oak_wood", fn() => VanillaBlocks::DARK_OAK_WOOD());
@@ -530,6 +525,7 @@ final class StringToItemParser{
$result->registerBlock("fence_gate_spruce", fn() => VanillaBlocks::SPRUCE_FENCE_GATE());
$result->registerBlock("fern", fn() => VanillaBlocks::FERN());
$result->registerBlock("fire", fn() => VanillaBlocks::FIRE());
+ $result->registerBlock("fletching_table", fn() => VanillaBlocks::FLETCHING_TABLE());
$result->registerBlock("flower_pot", fn() => VanillaBlocks::FLOWER_POT());
$result->registerBlock("flower_pot_block", fn() => VanillaBlocks::FLOWER_POT());
$result->registerBlock("flowing_lava", fn() => VanillaBlocks::LAVA());
@@ -617,6 +613,8 @@ final class StringToItemParser{
$result->registerBlock("jungle_trapdoor", fn() => VanillaBlocks::JUNGLE_TRAPDOOR());
$result->registerBlock("jungle_wall_sign", fn() => VanillaBlocks::JUNGLE_WALL_SIGN());
$result->registerBlock("jungle_wood", fn() => VanillaBlocks::JUNGLE_WOOD());
+ $result->registerBlock("jungle_wood_stairs", fn() => VanillaBlocks::JUNGLE_STAIRS());
+ $result->registerBlock("jungle_wooden_stairs", fn() => VanillaBlocks::JUNGLE_STAIRS());
$result->registerBlock("lab_table", fn() => VanillaBlocks::LAB_TABLE());
$result->registerBlock("ladder", fn() => VanillaBlocks::LADDER());
$result->registerBlock("lantern", fn() => VanillaBlocks::LANTERN());
@@ -630,6 +628,7 @@ final class StringToItemParser{
$result->registerBlock("leave2", fn() => VanillaBlocks::ACACIA_LEAVES());
$result->registerBlock("leaves", fn() => VanillaBlocks::OAK_LEAVES());
$result->registerBlock("leaves2", fn() => VanillaBlocks::ACACIA_LEAVES());
+ $result->registerBlock("lectern", fn() => VanillaBlocks::LECTERN());
$result->registerBlock("legacy_stonecutter", fn() => VanillaBlocks::LEGACY_STONECUTTER());
$result->registerBlock("lever", fn() => VanillaBlocks::LEVER());
$result->registerBlock("light_blue_glazed_terracotta", fn() => VanillaBlocks::LIGHT_BLUE_GLAZED_TERRACOTTA());
@@ -703,6 +702,7 @@ final class StringToItemParser{
$result->registerBlock("oak_sign", fn() => VanillaBlocks::OAK_SIGN());
$result->registerBlock("oak_slab", fn() => VanillaBlocks::OAK_SLAB());
$result->registerBlock("oak_stairs", fn() => VanillaBlocks::OAK_STAIRS());
+ $result->registerBlock("oak_standing_sign", fn() => VanillaBlocks::OAK_SIGN());
$result->registerBlock("oak_trapdoor", fn() => VanillaBlocks::OAK_TRAPDOOR());
$result->registerBlock("oak_wall_sign", fn() => VanillaBlocks::OAK_WALL_SIGN());
$result->registerBlock("oak_wood", fn() => VanillaBlocks::OAK_WOOD());
@@ -799,13 +799,15 @@ final class StringToItemParser{
$result->registerBlock("sea_lantern", fn() => VanillaBlocks::SEA_LANTERN());
$result->registerBlock("sea_pickle", fn() => VanillaBlocks::SEA_PICKLE());
$result->registerBlock("sealantern", fn() => VanillaBlocks::SEA_LANTERN());
- $result->registerBlock("shulker_box", fn() => VanillaBlocks::DYED_SHULKER_BOX());
+ $result->registerBlock("shulker_box", fn() => VanillaBlocks::SHULKER_BOX());
$result->registerBlock("sign", fn() => VanillaBlocks::OAK_SIGN());
$result->registerBlock("sign_post", fn() => VanillaBlocks::OAK_SIGN());
$result->registerBlock("silver_glazed_terracotta", fn() => VanillaBlocks::LIGHT_GRAY_GLAZED_TERRACOTTA());
$result->registerBlock("skull_block", fn() => VanillaBlocks::MOB_HEAD());
$result->registerBlock("slab", fn() => VanillaBlocks::SMOOTH_STONE_SLAB());
$result->registerBlock("slabs", fn() => VanillaBlocks::SMOOTH_STONE_SLAB());
+ $result->registerBlock("slime", fn() => VanillaBlocks::SLIME());
+ $result->registerBlock("slime_block", fn() => VanillaBlocks::SLIME());
$result->registerBlock("smoker", fn() => VanillaBlocks::SMOKER());
$result->registerBlock("smooth_quartz", fn() => VanillaBlocks::SMOOTH_QUARTZ());
$result->registerBlock("smooth_quartz_slab", fn() => VanillaBlocks::SMOOTH_QUARTZ_SLAB());
@@ -860,11 +862,11 @@ final class StringToItemParser{
$result->registerBlock("stone_bricks", fn() => VanillaBlocks::STONE_BRICKS());
$result->registerBlock("stone_button", fn() => VanillaBlocks::STONE_BUTTON());
$result->registerBlock("stone_pressure_plate", fn() => VanillaBlocks::STONE_PRESSURE_PLATE());
- $result->registerBlock("stone_slab", fn() => VanillaBlocks::SMOOTH_STONE_SLAB());
+ $result->registerBlock("stone_slab", fn() => VanillaBlocks::STONE_SLAB());
$result->registerBlock("stone_slab2", fn() => VanillaBlocks::RED_SANDSTONE_SLAB());
$result->registerBlock("stone_slab3", fn() => VanillaBlocks::END_STONE_BRICK_SLAB());
$result->registerBlock("stone_slab4", fn() => VanillaBlocks::MOSSY_STONE_BRICK_SLAB());
- $result->registerBlock("stone_stairs", fn() => VanillaBlocks::COBBLESTONE_STAIRS());
+ $result->registerBlock("stone_stairs", fn() => VanillaBlocks::STONE_STAIRS());
$result->registerBlock("stone_wall", fn() => VanillaBlocks::COBBLESTONE_WALL());
$result->registerBlock("stonebrick", fn() => VanillaBlocks::STONE_BRICKS());
$result->registerBlock("stonecutter", fn() => VanillaBlocks::LEGACY_STONECUTTER());
@@ -886,7 +888,7 @@ final class StringToItemParser{
$result->registerBlock("sugarcane_block", fn() => VanillaBlocks::SUGARCANE());
$result->registerBlock("sunflower", fn() => VanillaBlocks::SUNFLOWER());
$result->registerBlock("sweet_berry_bush", fn() => VanillaBlocks::SWEET_BERRY_BUSH());
- $result->registerBlock("tall_grass", fn() => VanillaBlocks::FERN());
+ $result->registerBlock("tall_grass", fn() => VanillaBlocks::TALL_GRASS());
$result->registerBlock("tallgrass", fn() => VanillaBlocks::FERN());
$result->registerBlock("terracotta", fn() => VanillaBlocks::STAINED_CLAY());
$result->registerBlock("tnt", fn() => VanillaBlocks::TNT());
@@ -1324,41 +1326,12 @@ final class StringToItemParser{
return $result;
}
- /** @phpstan-param \Closure(string $input) : Item $callback */
- public function register(string $alias, \Closure $callback) : void{
- $key = $this->reprocess($alias);
- if(isset($this->callbackMap[$key])){
- throw new \InvalidArgumentException("Alias \"$key\" is already registered");
- }
- $this->callbackMap[$key] = $callback;
- }
-
/** @phpstan-param \Closure(string $input) : Block $callback */
public function registerBlock(string $alias, \Closure $callback) : void{
$this->register($alias, fn(string $input) => $callback($input)->asItem());
}
- /** @phpstan-param \Closure(string $input) : Item $callback */
- public function override(string $alias, \Closure $callback) : void{
- $this->callbackMap[$this->reprocess($alias)] = $callback;
- }
-
- /** Tries to parse the specified string into an item. */
public function parse(string $input) : ?Item{
- $key = $this->reprocess($input);
- if(isset($this->callbackMap[$key])){
- return ($this->callbackMap[$key])($input);
- }
-
- return null;
- }
-
- protected function reprocess(string $input) : string{
- return strtolower(str_replace([" ", "minecraft:"], ["_", ""], trim($input)));
- }
-
- /** @return string[] */
- public function getKnownAliases() : array{
- return array_keys($this->callbackMap);
+ return parent::parse($input);
}
}
diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php
index f9ab3ade7..79676945e 100644
--- a/src/item/VanillaItems.php
+++ b/src/item/VanillaItems.php
@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\utils\CloningRegistryTrait;
-use function assert;
/**
* This doc-block is generated automatically, do not modify it manually.
@@ -33,6 +32,7 @@ use function assert;
* @generate-registry-docblock
*
* @method static Boat ACACIA_BOAT()
+ * @method static ItemBlock AIR()
* @method static Apple APPLE()
* @method static Arrow ARROW()
* @method static Potion AWKWARD_POTION()
@@ -381,12 +381,6 @@ final class VanillaItems{
self::_registryRegister($name, $item);
}
- public static function fromString(string $name) : Item{
- $result = self::_registryFromString($name);
- assert($result instanceof Item);
- return $result;
- }
-
/**
* @return Item[]
*/
@@ -399,6 +393,8 @@ final class VanillaItems{
protected static function setup() : void{
$factory = ItemFactory::getInstance();
+ self::register("air", $factory->get(ItemIds::AIR, 0, 0));
+
self::register("acacia_boat", $factory->get(333, 4));
self::register("apple", $factory->get(260));
self::register("arrow", $factory->get(262));
diff --git a/src/item/WritableBookBase.php b/src/item/WritableBookBase.php
index b19de34ba..db0b37574 100644
--- a/src/item/WritableBookBase.php
+++ b/src/item/WritableBookBase.php
@@ -31,6 +31,7 @@ use function array_push;
use function array_slice;
use function array_values;
use function count;
+use function mb_scrub;
abstract class WritableBookBase extends Item{
public const TAG_PAGES = "pages"; //TAG_List
@@ -168,12 +169,12 @@ abstract class WritableBookBase extends Item{
if($pages->getTagType() === NBT::TAG_Compound){ //PE format
/** @var CompoundTag $page */
foreach($pages as $page){
- $this->pages[] = new WritableBookPage($page->getString(self::TAG_PAGE_TEXT), $page->getString(self::TAG_PAGE_PHOTONAME, ""));
+ $this->pages[] = new WritableBookPage(mb_scrub($page->getString(self::TAG_PAGE_TEXT), 'UTF-8'), $page->getString(self::TAG_PAGE_PHOTONAME, ""));
}
}elseif($pages->getTagType() === NBT::TAG_String){ //PC format
/** @var StringTag $page */
foreach($pages as $page){
- $this->pages[] = new WritableBookPage($page->getValue());
+ $this->pages[] = new WritableBookPage(mb_scrub($page->getValue(), 'UTF-8'));
}
}
}
diff --git a/src/item/WritableBookPage.php b/src/item/WritableBookPage.php
index 62ebbcf50..95d5a6258 100644
--- a/src/item/WritableBookPage.php
+++ b/src/item/WritableBookPage.php
@@ -23,17 +23,32 @@ declare(strict_types=1);
namespace pocketmine\item;
+use pocketmine\utils\Limits;
use pocketmine\utils\Utils;
+use function sprintf;
+use function strlen;
class WritableBookPage{
+ public const PAGE_LENGTH_HARD_LIMIT_BYTES = Limits::INT16_MAX;
+ public const PHOTO_NAME_LENGTH_HARD_LIMIT_BYTES = Limits::INT16_MAX;
/** @var string */
private $text;
/** @var string */
private $photoName;
+ /**
+ * @throws \InvalidArgumentException
+ */
+ private static function checkLength(string $string, string $name, int $maxLength) : void{
+ if(strlen($string) > $maxLength){
+ throw new \InvalidArgumentException(sprintf("$name must be at most %d bytes, but have %d bytes", $maxLength, strlen($string)));
+ }
+ }
+
public function __construct(string $text, string $photoName = ""){
- //TODO: data validation
+ self::checkLength($text, "Text", self::PAGE_LENGTH_HARD_LIMIT_BYTES);
+ self::checkLength($photoName, "Photo name", self::PHOTO_NAME_LENGTH_HARD_LIMIT_BYTES);
Utils::checkUTF8($text);
$this->text = $text;
$this->photoName = $photoName;
diff --git a/src/item/WrittenBook.php b/src/item/WrittenBook.php
index f356b1cd6..129dfec3b 100644
--- a/src/item/WrittenBook.php
+++ b/src/item/WrittenBook.php
@@ -24,7 +24,10 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\nbt\tag\CompoundTag;
+use pocketmine\utils\Limits;
use pocketmine\utils\Utils;
+use function sprintf;
+use function strlen;
class WrittenBook extends WritableBookBase{
@@ -62,7 +65,7 @@ class WrittenBook extends WritableBookBase{
* @return $this
*/
public function setGeneration(int $generation) : self{
- if($generation < 0 or $generation > 3){
+ if($generation < 0 || $generation > 3){
throw new \InvalidArgumentException("Generation \"$generation\" is out of range");
}
@@ -85,6 +88,9 @@ class WrittenBook extends WritableBookBase{
* @return $this
*/
public function setAuthor(string $authorName) : self{
+ if(strlen($authorName) > Limits::INT16_MAX){
+ throw new \InvalidArgumentException(sprintf("Author must be at most %d bytes, but have %d bytes", Limits::INT16_MAX, strlen($authorName)));
+ }
Utils::checkUTF8($authorName);
$this->author = $authorName;
return $this;
@@ -103,6 +109,9 @@ class WrittenBook extends WritableBookBase{
* @return $this
*/
public function setTitle(string $title) : self{
+ if(strlen($title) > Limits::INT16_MAX){
+ throw new \InvalidArgumentException(sprintf("Title must be at most %d bytes, but have %d bytes", Limits::INT16_MAX, strlen($title)));
+ }
Utils::checkUTF8($title);
$this->title = $title;
return $this;
diff --git a/src/item/enchantment/ProtectionEnchantment.php b/src/item/enchantment/ProtectionEnchantment.php
index 30312ce34..b6f6a91be 100644
--- a/src/item/enchantment/ProtectionEnchantment.php
+++ b/src/item/enchantment/ProtectionEnchantment.php
@@ -66,6 +66,6 @@ class ProtectionEnchantment extends Enchantment{
* Returns whether this enchantment type offers protection from the specified damage source's cause.
*/
public function isApplicable(EntityDamageEvent $event) : bool{
- return $this->applicableDamageTypes === null or isset($this->applicableDamageTypes[$event->getCause()]);
+ return $this->applicableDamageTypes === null || isset($this->applicableDamageTypes[$event->getCause()]);
}
}
diff --git a/src/item/enchantment/StringToEnchantmentParser.php b/src/item/enchantment/StringToEnchantmentParser.php
new file mode 100644
index 000000000..104cc3686
--- /dev/null
+++ b/src/item/enchantment/StringToEnchantmentParser.php
@@ -0,0 +1,66 @@
+
+ */
+final class StringToEnchantmentParser extends StringToTParser{
+ use SingletonTrait;
+
+ private static function make() : self{
+ $result = new self;
+
+ $result->register("blast_protection", fn() => VanillaEnchantments::BLAST_PROTECTION());
+ $result->register("efficiency", fn() => VanillaEnchantments::EFFICIENCY());
+ $result->register("feather_falling", fn() => VanillaEnchantments::FEATHER_FALLING());
+ $result->register("fire_aspect", fn() => VanillaEnchantments::FIRE_ASPECT());
+ $result->register("fire_protection", fn() => VanillaEnchantments::FIRE_PROTECTION());
+ $result->register("flame", fn() => VanillaEnchantments::FLAME());
+ $result->register("infinity", fn() => VanillaEnchantments::INFINITY());
+ $result->register("knockback", fn() => VanillaEnchantments::KNOCKBACK());
+ $result->register("mending", fn() => VanillaEnchantments::MENDING());
+ $result->register("power", fn() => VanillaEnchantments::POWER());
+ $result->register("projectile_protection", fn() => VanillaEnchantments::PROJECTILE_PROTECTION());
+ $result->register("protection", fn() => VanillaEnchantments::PROTECTION());
+ $result->register("punch", fn() => VanillaEnchantments::PUNCH());
+ $result->register("respiration", fn() => VanillaEnchantments::RESPIRATION());
+ $result->register("sharpness", fn() => VanillaEnchantments::SHARPNESS());
+ $result->register("silk_touch", fn() => VanillaEnchantments::SILK_TOUCH());
+ $result->register("thorns", fn() => VanillaEnchantments::THORNS());
+ $result->register("unbreaking", fn() => VanillaEnchantments::UNBREAKING());
+ $result->register("vanishing", fn() => VanillaEnchantments::VANISHING());
+
+ return $result;
+ }
+
+ public function parse(string $input) : ?Enchantment{
+ return parent::parse($input);
+ }
+}
diff --git a/src/item/enchantment/VanillaEnchantments.php b/src/item/enchantment/VanillaEnchantments.php
index 37425bca1..f42039160 100644
--- a/src/item/enchantment/VanillaEnchantments.php
+++ b/src/item/enchantment/VanillaEnchantments.php
@@ -113,10 +113,4 @@ final class VanillaEnchantments{
$result = self::_registryGetAll();
return $result;
}
-
- public static function fromString(string $name) : Enchantment{
- /** @var Enchantment $result */
- $result = self::_registryFromString($name);
- return $result;
- }
}
diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php
index 62577ee16..d3b262d79 100644
--- a/src/lang/KnownTranslationFactory.php
+++ b/src/lang/KnownTranslationFactory.php
@@ -27,6 +27,8 @@ namespace pocketmine\lang;
* This class contains factory methods for all the translations known to PocketMine-MP as per the used version of
* pmmp/Language.
* This class is generated automatically, do NOT modify it by hand.
+ *
+ * @internal
*/
final class KnownTranslationFactory{
public static function ability_flight() : Translatable{
@@ -242,10 +244,10 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::COMMANDS_ENCHANT_USAGE, []);
}
- public static function commands_gamemode_success_other(Translatable|string $param1, Translatable|string $param0) : Translatable{
+ public static function commands_gamemode_success_other(Translatable|string $param0, Translatable|string $param1) : Translatable{
return new Translatable(KnownTranslationKeys::COMMANDS_GAMEMODE_SUCCESS_OTHER, [
- 1 => $param1,
0 => $param0,
+ 1 => $param1,
]);
}
@@ -573,6 +575,12 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_USAGE, []);
}
+ public static function death_attack_anvil(Translatable|string $param0) : Translatable{
+ return new Translatable(KnownTranslationKeys::DEATH_ATTACK_ANVIL, [
+ 0 => $param0,
+ ]);
+ }
+
public static function death_attack_arrow(Translatable|string $param0, Translatable|string $param1) : Translatable{
return new Translatable(KnownTranslationKeys::DEATH_ATTACK_ARROW, [
0 => $param0,
@@ -909,6 +917,12 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::GAMEMODE_INFO, []);
}
+ public static function gamemode_options(Translatable|string $param0) : Translatable{
+ return new Translatable(KnownTranslationKeys::GAMEMODE_OPTIONS, [
+ 0 => $param0,
+ ]);
+ }
+
public static function invalid_port() : Translatable{
return new Translatable(KnownTranslationKeys::INVALID_PORT, []);
}
@@ -1103,6 +1117,18 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ENCHANT_DESCRIPTION, []);
}
+ public static function pocketmine_command_error_permission(Translatable|string $commandName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ERROR_PERMISSION, [
+ "commandName" => $commandName,
+ ]);
+ }
+
+ public static function pocketmine_command_error_playerNotFound(Translatable|string $playerName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ERROR_PLAYERNOTFOUND, [
+ "playerName" => $playerName,
+ ]);
+ }
+
public static function pocketmine_command_exception(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_EXCEPTION, [
0 => $param0,
@@ -1115,6 +1141,18 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GAMEMODE_DESCRIPTION, []);
}
+ public static function pocketmine_command_gamemode_failure(Translatable|string $playerName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GAMEMODE_FAILURE, [
+ "playerName" => $playerName,
+ ]);
+ }
+
+ public static function pocketmine_command_gamemode_unknown(Translatable|string $gameModeName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GAMEMODE_UNKNOWN, [
+ "gameModeName" => $gameModeName,
+ ]);
+ }
+
public static function pocketmine_command_gc_chunks(Translatable|string $chunksCollected) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_CHUNKS, [
"chunksCollected" => $chunksCollected,
@@ -1159,6 +1197,24 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_HELP_DESCRIPTION, []);
}
+ public static function pocketmine_command_help_specificCommand_description(Translatable|string $description) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_DESCRIPTION, [
+ "description" => $description,
+ ]);
+ }
+
+ public static function pocketmine_command_help_specificCommand_header(Translatable|string $commandName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_HEADER, [
+ "commandName" => $commandName,
+ ]);
+ }
+
+ public static function pocketmine_command_help_specificCommand_usage(Translatable|string $usage) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_USAGE, [
+ "usage" => $usage,
+ ]);
+ }
+
public static function pocketmine_command_kick_description() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_KICK_DESCRIPTION, []);
}
@@ -1179,6 +1235,13 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ME_DESCRIPTION, []);
}
+ public static function pocketmine_command_notFound(Translatable|string $commandName, Translatable|string $helpCommand) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_NOTFOUND, [
+ "commandName" => $commandName,
+ "helpCommand" => $helpCommand,
+ ]);
+ }
+
public static function pocketmine_command_op_description() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_OP_DESCRIPTION, []);
}
@@ -1490,6 +1553,25 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_level_conversion_finish(Translatable|string $worldName, Translatable|string $backupPath) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_CONVERSION_FINISH, [
+ "worldName" => $worldName,
+ "backupPath" => $backupPath,
+ ]);
+ }
+
+ public static function pocketmine_level_conversion_start(Translatable|string $worldName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_CONVERSION_START, [
+ "worldName" => $worldName,
+ ]);
+ }
+
+ public static function pocketmine_level_corrupted(Translatable|string $details) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_CORRUPTED, [
+ "details" => $details,
+ ]);
+ }
+
public static function pocketmine_level_defaultError() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_DEFAULTERROR, []);
}
@@ -1501,6 +1583,14 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_level_invalidGeneratorOptions(Translatable|string $preset, Translatable|string $generatorName, Translatable|string $details) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_INVALIDGENERATOROPTIONS, [
+ "preset" => $preset,
+ "generatorName" => $generatorName,
+ "details" => $details,
+ ]);
+ }
+
public static function pocketmine_level_loadError(Translatable|string $param0, Translatable|string $param1) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_LOADERROR, [
0 => $param0,
@@ -1520,16 +1610,36 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_level_spawnTerrainGenerationProgress(Translatable|string $done, Translatable|string $total, Translatable|string $percentageDone) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_SPAWNTERRAINGENERATIONPROGRESS, [
+ "done" => $done,
+ "total" => $total,
+ "percentageDone" => $percentageDone,
+ ]);
+ }
+
public static function pocketmine_level_unknownFormat() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_UNKNOWNFORMAT, []);
}
+ public static function pocketmine_level_unknownGenerator(Translatable|string $generatorName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_UNKNOWNGENERATOR, [
+ "generatorName" => $generatorName,
+ ]);
+ }
+
public static function pocketmine_level_unloading(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_UNLOADING, [
0 => $param0,
]);
}
+ public static function pocketmine_level_unsupportedFormat(Translatable|string $details) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_UNSUPPORTEDFORMAT, [
+ "details" => $details,
+ ]);
+ }
+
public static function pocketmine_player_invalidEntity(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLAYER_INVALIDENTITY, [
0 => $param0,
@@ -1564,10 +1674,11 @@ final class KnownTranslationFactory{
]);
}
- public static function pocketmine_plugin_aliasError(Translatable|string $param0, Translatable|string $param1) : Translatable{
+ public static function pocketmine_plugin_aliasError(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_ALIASERROR, [
0 => $param0,
1 => $param1,
+ 2 => $param2,
]);
}
@@ -1577,14 +1688,21 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_plugin_badDataFolder(Translatable|string $dataFolder) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_BADDATAFOLDER, [
+ "dataFolder" => $dataFolder,
+ ]);
+ }
+
public static function pocketmine_plugin_circularDependency() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_CIRCULARDEPENDENCY, []);
}
- public static function pocketmine_plugin_commandError(Translatable|string $param0, Translatable|string $param1) : Translatable{
+ public static function pocketmine_plugin_commandError(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_COMMANDERROR, [
0 => $param0,
1 => $param1,
+ 2 => $param2,
]);
}
@@ -1602,23 +1720,42 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_plugin_disallowedByBlacklist() : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DISALLOWEDBYBLACKLIST, []);
+ }
+
+ public static function pocketmine_plugin_disallowedByWhitelist() : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DISALLOWEDBYWHITELIST, []);
+ }
+
public static function pocketmine_plugin_duplicateError(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DUPLICATEERROR, [
0 => $param0,
]);
}
+ public static function pocketmine_plugin_duplicatePermissionError(Translatable|string $permissionName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DUPLICATEPERMISSIONERROR, [
+ "permissionName" => $permissionName,
+ ]);
+ }
+
+ public static function pocketmine_plugin_emptyExtensionVersionConstraint(Translatable|string $constraintIndex, Translatable|string $extensionName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_EMPTYEXTENSIONVERSIONCONSTRAINT, [
+ "constraintIndex" => $constraintIndex,
+ "extensionName" => $extensionName,
+ ]);
+ }
+
public static function pocketmine_plugin_enable(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_ENABLE, [
0 => $param0,
]);
}
- public static function pocketmine_plugin_fileError(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{
- return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_FILEERROR, [
- 0 => $param0,
- 1 => $param1,
- 2 => $param2,
+ public static function pocketmine_plugin_extensionNotLoaded(Translatable|string $extensionName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_EXTENSIONNOTLOADED, [
+ "extensionName" => $extensionName,
]);
}
@@ -1634,6 +1771,14 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_plugin_incompatibleExtensionVersion(Translatable|string $extensionVersion, Translatable|string $extensionName, Translatable|string $pluginRequirement) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INCOMPATIBLEEXTENSIONVERSION, [
+ "extensionVersion" => $extensionVersion,
+ "extensionName" => $extensionName,
+ "pluginRequirement" => $pluginRequirement,
+ ]);
+ }
+
public static function pocketmine_plugin_incompatibleOS(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INCOMPATIBLEOS, [
0 => $param0,
@@ -1652,6 +1797,25 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_plugin_invalidAPI(Translatable|string $apiVersion) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INVALIDAPI, [
+ "apiVersion" => $apiVersion,
+ ]);
+ }
+
+ public static function pocketmine_plugin_invalidExtensionVersionConstraint(Translatable|string $versionConstraint, Translatable|string $extensionName) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INVALIDEXTENSIONVERSIONCONSTRAINT, [
+ "versionConstraint" => $versionConstraint,
+ "extensionName" => $extensionName,
+ ]);
+ }
+
+ public static function pocketmine_plugin_invalidManifest(Translatable|string $details) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INVALIDMANIFEST, [
+ "details" => $details,
+ ]);
+ }
+
public static function pocketmine_plugin_load(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_LOAD, [
0 => $param0,
@@ -1665,6 +1829,16 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_plugin_mainClassNotFound() : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_MAINCLASSNOTFOUND, []);
+ }
+
+ public static function pocketmine_plugin_mainClassWrongType(Translatable|string $pluginInterface) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_MAINCLASSWRONGTYPE, [
+ "pluginInterface" => $pluginInterface,
+ ]);
+ }
+
public static function pocketmine_plugin_restrictedName() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_RESTRICTEDNAME, []);
}
@@ -1792,6 +1966,34 @@ final class KnownTranslationFactory{
]);
}
+ public static function pocketmine_server_networkStartFailed(Translatable|string $ipAddress, Translatable|string $port, Translatable|string $errorMessage) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_NETWORKSTARTFAILED, [
+ "ipAddress" => $ipAddress,
+ "port" => $port,
+ "errorMessage" => $errorMessage,
+ ]);
+ }
+
+ public static function pocketmine_server_obsolete_warning1(Translatable|string $param0, Translatable|string $param1) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_OBSOLETE_WARNING1, [
+ 0 => $param0,
+ 1 => $param1,
+ ]);
+ }
+
+ public static function pocketmine_server_obsolete_warning2(Translatable|string $param0, Translatable|string $param1) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_OBSOLETE_WARNING2, [
+ 0 => $param0,
+ 1 => $param1,
+ ]);
+ }
+
+ public static function pocketmine_server_obsolete_warning3(Translatable|string $param0) : Translatable{
+ return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_OBSOLETE_WARNING3, [
+ 0 => $param0,
+ ]);
+ }
+
public static function pocketmine_server_query_running(Translatable|string $param0, Translatable|string $param1) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_QUERY_RUNNING, [
0 => $param0,
@@ -1917,6 +2119,10 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POTION_SATURATION, []);
}
+ public static function potion_slowFalling() : Translatable{
+ return new Translatable(KnownTranslationKeys::POTION_SLOWFALLING, []);
+ }
+
public static function potion_waterBreathing() : Translatable{
return new Translatable(KnownTranslationKeys::POTION_WATERBREATHING, []);
}
@@ -1945,6 +2151,14 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::SERVER_PORT, []);
}
+ public static function server_port_v4() : Translatable{
+ return new Translatable(KnownTranslationKeys::SERVER_PORT_V4, []);
+ }
+
+ public static function server_port_v6() : Translatable{
+ return new Translatable(KnownTranslationKeys::SERVER_PORT_V6, []);
+ }
+
public static function server_properties() : Translatable{
return new Translatable(KnownTranslationKeys::SERVER_PROPERTIES, []);
}
@@ -1969,6 +2183,10 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::TILE_BED_TOOFAR, []);
}
+ public static function view_distance() : Translatable{
+ return new Translatable(KnownTranslationKeys::VIEW_DISTANCE, []);
+ }
+
public static function welcome_to_pocketmine(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::WELCOME_TO_POCKETMINE, [
0 => $param0,
diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php
index 5c3d5bfa3..1e488e2b8 100644
--- a/src/lang/KnownTranslationKeys.php
+++ b/src/lang/KnownTranslationKeys.php
@@ -26,6 +26,8 @@ namespace pocketmine\lang;
/**
* This class contains constants for all the translations known to PocketMine-MP as per the used version of pmmp/Language.
* This class is generated automatically, do NOT modify it by hand.
+ *
+ * @internal
*/
final class KnownTranslationKeys{
public const ABILITY_FLIGHT = "ability.flight";
@@ -128,6 +130,7 @@ final class KnownTranslationKeys{
public const COMMANDS_WHITELIST_REMOVE_SUCCESS = "commands.whitelist.remove.success";
public const COMMANDS_WHITELIST_REMOVE_USAGE = "commands.whitelist.remove.usage";
public const COMMANDS_WHITELIST_USAGE = "commands.whitelist.usage";
+ public const DEATH_ATTACK_ANVIL = "death.attack.anvil";
public const DEATH_ATTACK_ARROW = "death.attack.arrow";
public const DEATH_ATTACK_ARROW_ITEM = "death.attack.arrow.item";
public const DEATH_ATTACK_CACTUS = "death.attack.cactus";
@@ -200,6 +203,7 @@ final class KnownTranslationKeys{
public const GAMEMODE_SPECTATOR = "gameMode.spectator";
public const GAMEMODE_SURVIVAL = "gameMode.survival";
public const GAMEMODE_INFO = "gamemode_info";
+ public const GAMEMODE_OPTIONS = "gamemode_options";
public const INVALID_PORT = "invalid_port";
public const IP_CONFIRM = "ip_confirm";
public const IP_GET = "ip_get";
@@ -243,8 +247,12 @@ final class KnownTranslationKeys{
public const POCKETMINE_COMMAND_DIFFICULTY_DESCRIPTION = "pocketmine.command.difficulty.description";
public const POCKETMINE_COMMAND_EFFECT_DESCRIPTION = "pocketmine.command.effect.description";
public const POCKETMINE_COMMAND_ENCHANT_DESCRIPTION = "pocketmine.command.enchant.description";
+ public const POCKETMINE_COMMAND_ERROR_PERMISSION = "pocketmine.command.error.permission";
+ public const POCKETMINE_COMMAND_ERROR_PLAYERNOTFOUND = "pocketmine.command.error.playerNotFound";
public const POCKETMINE_COMMAND_EXCEPTION = "pocketmine.command.exception";
public const POCKETMINE_COMMAND_GAMEMODE_DESCRIPTION = "pocketmine.command.gamemode.description";
+ public const POCKETMINE_COMMAND_GAMEMODE_FAILURE = "pocketmine.command.gamemode.failure";
+ public const POCKETMINE_COMMAND_GAMEMODE_UNKNOWN = "pocketmine.command.gamemode.unknown";
public const POCKETMINE_COMMAND_GC_CHUNKS = "pocketmine.command.gc.chunks";
public const POCKETMINE_COMMAND_GC_CYCLES = "pocketmine.command.gc.cycles";
public const POCKETMINE_COMMAND_GC_DESCRIPTION = "pocketmine.command.gc.description";
@@ -254,11 +262,15 @@ final class KnownTranslationKeys{
public const POCKETMINE_COMMAND_GIVE_DESCRIPTION = "pocketmine.command.give.description";
public const POCKETMINE_COMMAND_GIVE_USAGE = "pocketmine.command.give.usage";
public const POCKETMINE_COMMAND_HELP_DESCRIPTION = "pocketmine.command.help.description";
+ public const POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_DESCRIPTION = "pocketmine.command.help.specificCommand.description";
+ public const POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_HEADER = "pocketmine.command.help.specificCommand.header";
+ public const POCKETMINE_COMMAND_HELP_SPECIFICCOMMAND_USAGE = "pocketmine.command.help.specificCommand.usage";
public const POCKETMINE_COMMAND_KICK_DESCRIPTION = "pocketmine.command.kick.description";
public const POCKETMINE_COMMAND_KILL_DESCRIPTION = "pocketmine.command.kill.description";
public const POCKETMINE_COMMAND_KILL_USAGE = "pocketmine.command.kill.usage";
public const POCKETMINE_COMMAND_LIST_DESCRIPTION = "pocketmine.command.list.description";
public const POCKETMINE_COMMAND_ME_DESCRIPTION = "pocketmine.command.me.description";
+ public const POCKETMINE_COMMAND_NOTFOUND = "pocketmine.command.notFound";
public const POCKETMINE_COMMAND_OP_DESCRIPTION = "pocketmine.command.op.description";
public const POCKETMINE_COMMAND_PARTICLE_DESCRIPTION = "pocketmine.command.particle.description";
public const POCKETMINE_COMMAND_PARTICLE_USAGE = "pocketmine.command.particle.usage";
@@ -324,33 +336,51 @@ final class KnownTranslationKeys{
public const POCKETMINE_LEVEL_AMBIGUOUSFORMAT = "pocketmine.level.ambiguousFormat";
public const POCKETMINE_LEVEL_BACKGROUNDGENERATION = "pocketmine.level.backgroundGeneration";
public const POCKETMINE_LEVEL_BADDEFAULTFORMAT = "pocketmine.level.badDefaultFormat";
+ public const POCKETMINE_LEVEL_CONVERSION_FINISH = "pocketmine.level.conversion.finish";
+ public const POCKETMINE_LEVEL_CONVERSION_START = "pocketmine.level.conversion.start";
+ public const POCKETMINE_LEVEL_CORRUPTED = "pocketmine.level.corrupted";
public const POCKETMINE_LEVEL_DEFAULTERROR = "pocketmine.level.defaultError";
public const POCKETMINE_LEVEL_GENERATIONERROR = "pocketmine.level.generationError";
+ public const POCKETMINE_LEVEL_INVALIDGENERATOROPTIONS = "pocketmine.level.invalidGeneratorOptions";
public const POCKETMINE_LEVEL_LOADERROR = "pocketmine.level.loadError";
public const POCKETMINE_LEVEL_NOTFOUND = "pocketmine.level.notFound";
public const POCKETMINE_LEVEL_PREPARING = "pocketmine.level.preparing";
+ public const POCKETMINE_LEVEL_SPAWNTERRAINGENERATIONPROGRESS = "pocketmine.level.spawnTerrainGenerationProgress";
public const POCKETMINE_LEVEL_UNKNOWNFORMAT = "pocketmine.level.unknownFormat";
+ public const POCKETMINE_LEVEL_UNKNOWNGENERATOR = "pocketmine.level.unknownGenerator";
public const POCKETMINE_LEVEL_UNLOADING = "pocketmine.level.unloading";
+ public const POCKETMINE_LEVEL_UNSUPPORTEDFORMAT = "pocketmine.level.unsupportedFormat";
public const POCKETMINE_PLAYER_INVALIDENTITY = "pocketmine.player.invalidEntity";
public const POCKETMINE_PLAYER_INVALIDMOVE = "pocketmine.player.invalidMove";
public const POCKETMINE_PLAYER_LOGIN = "pocketmine.player.logIn";
public const POCKETMINE_PLAYER_LOGOUT = "pocketmine.player.logOut";
public const POCKETMINE_PLUGIN_ALIASERROR = "pocketmine.plugin.aliasError";
public const POCKETMINE_PLUGIN_AMBIGUOUSMINAPI = "pocketmine.plugin.ambiguousMinAPI";
+ public const POCKETMINE_PLUGIN_BADDATAFOLDER = "pocketmine.plugin.badDataFolder";
public const POCKETMINE_PLUGIN_CIRCULARDEPENDENCY = "pocketmine.plugin.circularDependency";
public const POCKETMINE_PLUGIN_COMMANDERROR = "pocketmine.plugin.commandError";
public const POCKETMINE_PLUGIN_DEPRECATEDEVENT = "pocketmine.plugin.deprecatedEvent";
public const POCKETMINE_PLUGIN_DISABLE = "pocketmine.plugin.disable";
+ public const POCKETMINE_PLUGIN_DISALLOWEDBYBLACKLIST = "pocketmine.plugin.disallowedByBlacklist";
+ public const POCKETMINE_PLUGIN_DISALLOWEDBYWHITELIST = "pocketmine.plugin.disallowedByWhitelist";
public const POCKETMINE_PLUGIN_DUPLICATEERROR = "pocketmine.plugin.duplicateError";
+ public const POCKETMINE_PLUGIN_DUPLICATEPERMISSIONERROR = "pocketmine.plugin.duplicatePermissionError";
+ public const POCKETMINE_PLUGIN_EMPTYEXTENSIONVERSIONCONSTRAINT = "pocketmine.plugin.emptyExtensionVersionConstraint";
public const POCKETMINE_PLUGIN_ENABLE = "pocketmine.plugin.enable";
- public const POCKETMINE_PLUGIN_FILEERROR = "pocketmine.plugin.fileError";
+ public const POCKETMINE_PLUGIN_EXTENSIONNOTLOADED = "pocketmine.plugin.extensionNotLoaded";
public const POCKETMINE_PLUGIN_GENERICLOADERROR = "pocketmine.plugin.genericLoadError";
public const POCKETMINE_PLUGIN_INCOMPATIBLEAPI = "pocketmine.plugin.incompatibleAPI";
+ public const POCKETMINE_PLUGIN_INCOMPATIBLEEXTENSIONVERSION = "pocketmine.plugin.incompatibleExtensionVersion";
public const POCKETMINE_PLUGIN_INCOMPATIBLEOS = "pocketmine.plugin.incompatibleOS";
public const POCKETMINE_PLUGIN_INCOMPATIBLEPHPVERSION = "pocketmine.plugin.incompatiblePhpVersion";
public const POCKETMINE_PLUGIN_INCOMPATIBLEPROTOCOL = "pocketmine.plugin.incompatibleProtocol";
+ public const POCKETMINE_PLUGIN_INVALIDAPI = "pocketmine.plugin.invalidAPI";
+ public const POCKETMINE_PLUGIN_INVALIDEXTENSIONVERSIONCONSTRAINT = "pocketmine.plugin.invalidExtensionVersionConstraint";
+ public const POCKETMINE_PLUGIN_INVALIDMANIFEST = "pocketmine.plugin.invalidManifest";
public const POCKETMINE_PLUGIN_LOAD = "pocketmine.plugin.load";
public const POCKETMINE_PLUGIN_LOADERROR = "pocketmine.plugin.loadError";
+ public const POCKETMINE_PLUGIN_MAINCLASSNOTFOUND = "pocketmine.plugin.mainClassNotFound";
+ public const POCKETMINE_PLUGIN_MAINCLASSWRONGTYPE = "pocketmine.plugin.mainClassWrongType";
public const POCKETMINE_PLUGIN_RESTRICTEDNAME = "pocketmine.plugin.restrictedName";
public const POCKETMINE_PLUGIN_SPACESDISCOURAGED = "pocketmine.plugin.spacesDiscouraged";
public const POCKETMINE_PLUGIN_UNKNOWNDEPENDENCY = "pocketmine.plugin.unknownDependency";
@@ -375,6 +405,10 @@ final class KnownTranslationKeys{
public const POCKETMINE_SERVER_INFO_EXTENDED = "pocketmine.server.info.extended";
public const POCKETMINE_SERVER_LICENSE = "pocketmine.server.license";
public const POCKETMINE_SERVER_NETWORKSTART = "pocketmine.server.networkStart";
+ public const POCKETMINE_SERVER_NETWORKSTARTFAILED = "pocketmine.server.networkStartFailed";
+ public const POCKETMINE_SERVER_OBSOLETE_WARNING1 = "pocketmine.server.obsolete.warning1";
+ public const POCKETMINE_SERVER_OBSOLETE_WARNING2 = "pocketmine.server.obsolete.warning2";
+ public const POCKETMINE_SERVER_OBSOLETE_WARNING3 = "pocketmine.server.obsolete.warning3";
public const POCKETMINE_SERVER_QUERY_RUNNING = "pocketmine.server.query.running";
public const POCKETMINE_SERVER_START = "pocketmine.server.start";
public const POCKETMINE_SERVER_STARTFINISHED = "pocketmine.server.startFinished";
@@ -404,6 +438,7 @@ final class KnownTranslationKeys{
public const POTION_REGENERATION = "potion.regeneration";
public const POTION_RESISTANCE = "potion.resistance";
public const POTION_SATURATION = "potion.saturation";
+ public const POTION_SLOWFALLING = "potion.slowFalling";
public const POTION_WATERBREATHING = "potion.waterBreathing";
public const POTION_WEAKNESS = "potion.weakness";
public const POTION_WITHER = "potion.wither";
@@ -411,12 +446,15 @@ final class KnownTranslationKeys{
public const QUERY_WARNING1 = "query_warning1";
public const QUERY_WARNING2 = "query_warning2";
public const SERVER_PORT = "server_port";
+ public const SERVER_PORT_V4 = "server_port_v4";
+ public const SERVER_PORT_V6 = "server_port_v6";
public const SERVER_PROPERTIES = "server_properties";
public const SETTING_UP_SERVER_NOW = "setting_up_server_now";
public const SKIP_INSTALLER = "skip_installer";
public const TILE_BED_NOSLEEP = "tile.bed.noSleep";
public const TILE_BED_OCCUPIED = "tile.bed.occupied";
public const TILE_BED_TOOFAR = "tile.bed.tooFar";
+ public const VIEW_DISTANCE = "view_distance";
public const WELCOME_TO_POCKETMINE = "welcome_to_pocketmine";
public const WHITELIST_ENABLE = "whitelist_enable";
public const WHITELIST_INFO = "whitelist_info";
diff --git a/src/lang/Language.php b/src/lang/Language.php
index f3e966704..bce7c5c69 100644
--- a/src/lang/Language.php
+++ b/src/lang/Language.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\lang;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function array_filter;
use function array_map;
@@ -52,7 +53,7 @@ class Language{
*/
public static function getLanguageList(string $path = "") : array{
if($path === ""){
- $path = Path::join(\pocketmine\RESOURCE_PATH, "locale");
+ $path = \pocketmine\LOCALE_DATA_PATH;
}
if(is_dir($path)){
@@ -101,7 +102,7 @@ class Language{
$this->langName = strtolower($lang);
if($path === null){
- $path = Path::join(\pocketmine\RESOURCE_PATH, "locale");
+ $path = \pocketmine\LOCALE_DATA_PATH;
}
$this->lang = self::loadLang($path, $this->langName);
@@ -123,7 +124,7 @@ class Language{
protected static function loadLang(string $path, string $languageCode) : array{
$file = Path::join($path, $languageCode . ".ini");
if(file_exists($file)){
- return array_map('\stripcslashes', parse_ini_file($file, false, INI_SCANNER_RAW));
+ return array_map('\stripcslashes', Utils::assumeNotFalse(parse_ini_file($file, false, INI_SCANNER_RAW), "Missing or inaccessible required resource files"));
}
throw new LanguageNotFoundException("Language \"$languageCode\" not found");
@@ -134,7 +135,7 @@ class Language{
*/
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null) : string{
$baseText = $this->get($str);
- $baseText = $this->parseTranslation(($onlyPrefix === null or strpos($str, $onlyPrefix) === 0) ? $baseText : $str, $onlyPrefix);
+ $baseText = $this->parseTranslation(($onlyPrefix === null || strpos($str, $onlyPrefix) === 0) ? $baseText : $str, $onlyPrefix);
foreach($params as $i => $p){
$replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p;
@@ -175,14 +176,14 @@ class Language{
if($replaceString !== null){
$ord = ord($c);
if(
- ($ord >= 0x30 and $ord <= 0x39) // 0-9
- or ($ord >= 0x41 and $ord <= 0x5a) // A-Z
- or ($ord >= 0x61 and $ord <= 0x7a) or // a-z
- $c === "." or $c === "-"
+ ($ord >= 0x30 && $ord <= 0x39) // 0-9
+ || ($ord >= 0x41 && $ord <= 0x5a) // A-Z
+ || ($ord >= 0x61 && $ord <= 0x7a) || // a-z
+ $c === "." || $c === "-"
){
$replaceString .= $c;
}else{
- if(($t = $this->internalGet(substr($replaceString, 1))) !== null and ($onlyPrefix === null or strpos($replaceString, $onlyPrefix) === 1)){
+ if(($t = $this->internalGet(substr($replaceString, 1))) !== null && ($onlyPrefix === null || strpos($replaceString, $onlyPrefix) === 1)){
$newString .= $t;
}else{
$newString .= $replaceString;
@@ -203,7 +204,7 @@ class Language{
}
if($replaceString !== null){
- if(($t = $this->internalGet(substr($replaceString, 1))) !== null and ($onlyPrefix === null or strpos($replaceString, $onlyPrefix) === 1)){
+ if(($t = $this->internalGet(substr($replaceString, 1))) !== null && ($onlyPrefix === null || strpos($replaceString, $onlyPrefix) === 1)){
$newString .= $t;
}else{
$newString .= $replaceString;
diff --git a/src/network/Network.php b/src/network/Network.php
index 22c1b2f39..2502809b1 100644
--- a/src/network/Network.php
+++ b/src/network/Network.php
@@ -91,6 +91,9 @@ class Network{
$this->sessionManager->tick();
}
+ /**
+ * @throws NetworkInterfaceStartException
+ */
public function registerInterface(NetworkInterface $interface) : bool{
$ev = new NetworkInterfaceRegisterEvent($interface);
$ev->call();
@@ -188,7 +191,7 @@ class Network{
}
public function processRawPacket(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : void{
- if(isset($this->bannedIps[$address]) and time() < $this->bannedIps[$address]){
+ if(isset($this->bannedIps[$address]) && time() < $this->bannedIps[$address]){
$this->logger->debug("Dropped raw packet from banned address $address $port");
return;
}
diff --git a/src/network/NetworkInterface.php b/src/network/NetworkInterface.php
index 146a7351c..a6beddbae 100644
--- a/src/network/NetworkInterface.php
+++ b/src/network/NetworkInterface.php
@@ -33,6 +33,7 @@ interface NetworkInterface{
/**
* Performs actions needed to start the interface after it is registered.
+ * @throws NetworkInterfaceStartException
*/
public function start() : void;
diff --git a/src/network/NetworkInterfaceStartException.php b/src/network/NetworkInterfaceStartException.php
new file mode 100644
index 000000000..d3a563825
--- /dev/null
+++ b/src/network/NetworkInterfaceStartException.php
@@ -0,0 +1,32 @@
+compressor = $compressor;
- $this->chunk = FastChunkSerializer::serializeWithoutLight($chunk);
+ $this->chunk = FastChunkSerializer::serializeTerrain($chunk);
$this->chunkX = $chunkX;
$this->chunkZ = $chunkZ;
$this->tiles = ChunkSerializer::serializeTiles($chunk);
@@ -68,11 +68,11 @@ class ChunkRequestTask extends AsyncTask{
}
public function onRun() : void{
- $chunk = FastChunkSerializer::deserialize($this->chunk);
- $subCount = ChunkSerializer::getSubChunkCount($chunk);
+ $chunk = FastChunkSerializer::deserializeTerrain($this->chunk);
+ $subCount = ChunkSerializer::getSubChunkCount($chunk) + ChunkSerializer::LOWER_PADDING_SIZE;
$encoderContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
$payload = ChunkSerializer::serializeFullChunk($chunk, RuntimeBlockMapping::getInstance(), $encoderContext, $this->tiles);
- $this->setResult($this->compressor->compress(PacketBatch::fromPackets($encoderContext, LevelChunkPacket::withoutCache($this->chunkX, $this->chunkZ, $subCount, $payload))->getBuffer()));
+ $this->setResult($this->compressor->compress(PacketBatch::fromPackets($encoderContext, LevelChunkPacket::create($this->chunkX, $this->chunkZ, $subCount, null, $payload))->getBuffer()));
}
public function onError() : void{
diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php
index 67931b7d0..e7e53b4d3 100644
--- a/src/network/mcpe/InventoryManager.php
+++ b/src/network/mcpe/InventoryManager.php
@@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\BlockInventory;
use pocketmine\block\inventory\BrewingStandInventory;
+use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\FurnaceInventory;
use pocketmine\block\inventory\HopperInventory;
@@ -45,6 +46,7 @@ use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
@@ -65,9 +67,7 @@ class InventoryManager{
//these IDs are used for 1.16 to restore 1.14ish crafting & inventory behaviour; since they don't seem to have any
//effect on the behaviour of inventory transactions I don't currently plan to integrate these into the main system.
private const RESERVED_WINDOW_ID_RANGE_START = ContainerIds::LAST - 10;
- private const RESERVED_WINDOW_ID_RANGE_END = ContainerIds::LAST;
- public const HARDCODED_CRAFTING_GRID_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 1;
- public const HARDCODED_INVENTORY_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 2;
+ private const HARDCODED_INVENTORY_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 2;
/** @var Player */
private $player;
@@ -79,6 +79,15 @@ class InventoryManager{
/** @var int */
private $lastInventoryNetworkId = ContainerIds::FIRST;
+ /**
+ * TODO: HACK! This tracks GUIs for inventories that the server considers "always open" so that the client can't
+ * open them twice. (1.16 hack)
+ * @var true[]
+ * @phpstan-var array
+ * @internal
+ */
+ protected $openHardcodedWindows = [];
+
/**
* @var Item[][]
* @phpstan-var array>
@@ -129,7 +138,7 @@ class InventoryManager{
public function onTransactionStart(InventoryTransaction $tx) : void{
foreach($tx->getActions() as $action){
- if($action instanceof SlotChangeAction and ($windowId = $this->getWindowId($action->getInventory())) !== null){
+ if($action instanceof SlotChangeAction && ($windowId = $this->getWindowId($action->getInventory())) !== null){
//in some cases the inventory might not have a window ID, but still be referenced by a transaction (e.g. crafting grid changes), so we can't unconditionally record the change here or we might leak things
$this->initiatedSlotChanges[$windowId][$action->getSlot()] = $action->getTargetItem();
}
@@ -150,7 +159,7 @@ class InventoryManager{
return;
}
}
- throw new \UnsupportedOperationException("Unsupported inventory type");
+ throw new \LogicException("Unsupported inventory type");
}
/** @phpstan-return ObjectSet */
@@ -164,31 +173,42 @@ class InventoryManager{
//TODO: we should be using some kind of tagging system to identify the types. Instanceof is flaky especially
//if the class isn't final, not to mention being inflexible.
if($inv instanceof BlockInventory){
- switch(true){
- case $inv instanceof LoomInventory:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::LOOM, $inv->getHolder())];
- case $inv instanceof FurnaceInventory:
- return match($inv->getFurnaceType()->id()){
- FurnaceType::FURNACE()->id() => [ContainerOpenPacket::blockInvVec3($id, WindowTypes::FURNACE, $inv->getHolder())],
- FurnaceType::BLAST_FURNACE()->id() => [ContainerOpenPacket::blockInvVec3($id, WindowTypes::BLAST_FURNACE, $inv->getHolder())],
- FurnaceType::SMOKER()->id() => [ContainerOpenPacket::blockInvVec3($id, WindowTypes::SMOKER, $inv->getHolder())],
+ $blockPosition = BlockPosition::fromVector3($inv->getHolder());
+ $windowType = match(true){
+ $inv instanceof LoomInventory => WindowTypes::LOOM,
+ $inv instanceof FurnaceInventory => match($inv->getFurnaceType()->id()){
+ FurnaceType::FURNACE()->id() => WindowTypes::FURNACE,
+ FurnaceType::BLAST_FURNACE()->id() => WindowTypes::BLAST_FURNACE,
+ FurnaceType::SMOKER()->id() => WindowTypes::SMOKER,
default => throw new AssumptionFailedError("Unreachable")
- };
- case $inv instanceof EnchantInventory:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::ENCHANTMENT, $inv->getHolder())];
- case $inv instanceof BrewingStandInventory:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::BREWING_STAND, $inv->getHolder())];
- case $inv instanceof AnvilInventory:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::ANVIL, $inv->getHolder())];
- case $inv instanceof HopperInventory:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::HOPPER, $inv->getHolder())];
- default:
- return [ContainerOpenPacket::blockInvVec3($id, WindowTypes::CONTAINER, $inv->getHolder())];
- }
+ },
+ $inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT,
+ $inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND,
+ $inv instanceof AnvilInventory => WindowTypes::ANVIL,
+ $inv instanceof HopperInventory => WindowTypes::HOPPER,
+ $inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH,
+ default => WindowTypes::CONTAINER
+ };
+ return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)];
}
return null;
}
+ public function onClientOpenMainInventory() : void{
+ $id = self::HARDCODED_INVENTORY_WINDOW_ID;
+ if(!isset($this->openHardcodedWindows[$id])){
+ //TODO: HACK! this restores 1.14ish behaviour, but this should be able to be listened to and
+ //controlled by plugins. However, the player is always a subscriber to their own inventory so it
+ //doesn't integrate well with the regular container system right now.
+ $this->openHardcodedWindows[$id] = true;
+ $this->session->sendDataPacket(ContainerOpenPacket::entityInv(
+ InventoryManager::HARDCODED_INVENTORY_WINDOW_ID,
+ WindowTypes::INVENTORY,
+ $this->player->getId()
+ ));
+ }
+ }
+
public function onCurrentWindowRemove() : void{
if(isset($this->windowMap[$this->lastInventoryNetworkId])){
$this->remove($this->lastInventoryNetworkId);
@@ -197,16 +217,18 @@ class InventoryManager{
}
public function onClientRemoveWindow(int $id) : void{
- if($id >= self::RESERVED_WINDOW_ID_RANGE_START && $id <= self::RESERVED_WINDOW_ID_RANGE_END){
- //TODO: HACK! crafting grid & main inventory currently use these fake IDs
- return;
- }
- if($id === $this->lastInventoryNetworkId){
+ if(isset($this->openHardcodedWindows[$id])){
+ unset($this->openHardcodedWindows[$id]);
+ }elseif($id === $this->lastInventoryNetworkId){
$this->remove($id);
$this->player->removeCurrentWindow();
}else{
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId");
}
+
+ //Always send this, even if no window matches. If we told the client to close a window, it will behave as if it
+ //initiated the close and expect an ack.
+ $this->session->sendDataPacket(ContainerClosePacket::create($id, false));
}
public function syncSlot(Inventory $inventory, int $slot) : void{
@@ -214,7 +236,7 @@ class InventoryManager{
if($windowId !== null){
$currentItem = $inventory->getItem($slot);
$clientSideItem = $this->initiatedSlotChanges[$windowId][$slot] ?? null;
- if($clientSideItem === null or !$clientSideItem->equalsExact($currentItem)){
+ if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
if($windowId === ContainerIds::OFFHAND){
//TODO: HACK!
@@ -279,6 +301,7 @@ class InventoryManager{
$this->player->getId(),
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())),
$selected,
+ $selected,
ContainerIds::INVENTORY
));
$this->clientSelectedHotbarSlot = $selected;
diff --git a/src/network/mcpe/JwtUtils.php b/src/network/mcpe/JwtUtils.php
index 06d5cdfee..7a0a706c8 100644
--- a/src/network/mcpe/JwtUtils.php
+++ b/src/network/mcpe/JwtUtils.php
@@ -27,6 +27,7 @@ use FG\ASN1\Exception\ParserException;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\Sequence;
use pocketmine\utils\AssumptionFailedError;
+use pocketmine\utils\Utils;
use function base64_decode;
use function base64_encode;
use function count;
@@ -185,11 +186,7 @@ final class JwtUtils{
}
public static function emitDerPublicKey(\OpenSSLAsymmetricKey $opensslKey) : string{
- $details = openssl_pkey_get_details($opensslKey);
- if($details === false){
- throw new AssumptionFailedError("Failed to get details from OpenSSL key resource");
- }
-
+ $details = Utils::assumeNotFalse(openssl_pkey_get_details($opensslKey), "Failed to get details from OpenSSL key resource");
/** @var string $pemKey */
$pemKey = $details['key'];
if(preg_match("@^-----BEGIN[A-Z\d ]+PUBLIC KEY-----\n([A-Za-z\d+/\n]+)\n-----END[A-Z\d ]+PUBLIC KEY-----\n$@", $pemKey, $matches) === 1){
diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php
index 45d6daaa6..3c97db45e 100644
--- a/src/network/mcpe/NetworkSession.php
+++ b/src/network/mcpe/NetworkSession.php
@@ -62,6 +62,7 @@ use pocketmine\network\mcpe\protocol\AvailableCommandsPacket;
use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\DisconnectPacket;
+use pocketmine\network\mcpe\protocol\EmotePacket;
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
use pocketmine\network\mcpe\protocol\MobEffectPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
@@ -88,6 +89,7 @@ use pocketmine\network\mcpe\protocol\SetTitlePacket;
use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
use pocketmine\network\mcpe\protocol\TextPacket;
use pocketmine\network\mcpe\protocol\TransferPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\command\CommandData;
use pocketmine\network\mcpe\protocol\types\command\CommandEnum;
use pocketmine\network\mcpe\protocol\types\command\CommandParameter;
@@ -109,6 +111,7 @@ use pocketmine\player\UsedChunkStatus;
use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use pocketmine\timings\Timings;
+use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
@@ -121,12 +124,12 @@ use function count;
use function get_class;
use function in_array;
use function json_encode;
-use function json_last_error_msg;
use function strlen;
use function strtolower;
use function substr;
use function time;
use function ucfirst;
+use const JSON_THROW_ON_ERROR;
class NetworkSession{
private \PrefixedLogger $logger;
@@ -234,6 +237,10 @@ class NetworkSession{
return;
}
$this->player = $player;
+ if(!$this->server->addOnlinePlayer($player)){
+ return;
+ }
+
$this->invManager = new InventoryManager($this->player, $this);
$effectManager = $this->player->getEffects();
@@ -343,6 +350,10 @@ class NetworkSession{
try{
foreach($stream->getPackets($this->packetPool, $this->packetSerializerContext, 500) as [$packet, $buffer]){
+ if($packet === null){
+ $this->logger->debug("Unknown packet: " . base64_encode($buffer));
+ throw new PacketHandlingException("Unknown packet received");
+ }
try{
$this->handleDataPacket($packet, $buffer);
}catch(PacketHandlingException $e){
@@ -381,7 +392,7 @@ class NetworkSession{
$ev = new DataPacketReceiveEvent($this, $packet);
$ev->call();
- if(!$ev->isCancelled() and ($this->handler === null or !$packet->handle($this->handler))){
+ if(!$ev->isCancelled() && ($this->handler === null || !$packet->handle($this->handler))){
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
}
}finally{
@@ -391,7 +402,7 @@ class NetworkSession{
public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{
//Basic safety restriction. TODO: improve this
- if(!$this->loggedIn and !$packet->canBeSentBeforeLogin()){
+ if(!$this->loggedIn && !$packet->canBeSentBeforeLogin()){
throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getDisplayName() . " too early");
}
@@ -442,6 +453,8 @@ class NetworkSession{
}
}
+ public function getPacketSerializerContext() : PacketSerializerContext{ return $this->packetSerializerContext; }
+
public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; }
public function getCompressor() : Compressor{
@@ -460,7 +473,7 @@ class NetworkSession{
}else{
$this->compressedQueue->enqueue($payload);
$payload->onResolve(function(CompressBatchPromise $payload) : void{
- if($this->connected and $this->compressedQueue->bottom() === $payload){
+ if($this->connected && $this->compressedQueue->bottom() === $payload){
$this->compressedQueue->dequeue(); //result unused
$this->sendEncoded($payload->getResult());
@@ -494,7 +507,7 @@ class NetworkSession{
* @phpstan-param \Closure() : void $func
*/
private function tryDisconnect(\Closure $func, string $reason) : void{
- if($this->connected and !$this->disconnectGuard){
+ if($this->connected && !$this->disconnectGuard){
$this->disconnectGuard = true;
$func();
$this->disconnectGuard = false;
@@ -525,8 +538,6 @@ class NetworkSession{
/**
* Instructs the remote client to connect to a different server.
- *
- * @throws \UnsupportedOperationException
*/
public function transfer(string $ip, int $port, string $reason = "transfer") : void{
$this->tryDisconnect(function() use ($ip, $port, $reason) : void{
@@ -552,7 +563,7 @@ class NetworkSession{
*/
private function doServerDisconnect(string $reason, bool $notify = true) : void{
if($notify){
- $this->sendDataPacket($reason === "" ? DisconnectPacket::silent() : DisconnectPacket::message($reason), true);
+ $this->sendDataPacket(DisconnectPacket::create($reason !== "" ? $reason : null), true);
}
$this->sender->close($notify ? $reason : "");
@@ -575,7 +586,7 @@ class NetworkSession{
return;
}
if($error === null){
- if($authenticated and !($this->info instanceof XboxLivePlayerInfo)){
+ if($authenticated && !($this->info instanceof XboxLivePlayerInfo)){
$error = "Expected XUID but none found";
}elseif($clientPubKey === null){
$error = "Missing client public key"; //failsafe
@@ -622,7 +633,7 @@ class NetworkSession{
continue;
}
$info = $existingSession->getPlayerInfo();
- if($info !== null and ($info->getUsername() === $this->info->getUsername() or $info->getUuid()->equals($this->info->getUuid()))){
+ if($info !== null && ($info->getUsername() === $this->info->getUsername() || $info->getUuid()->equals($this->info->getUuid()))){
if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() : "")){
return;
}
@@ -704,7 +715,7 @@ class NetworkSession{
public function onServerDeath() : void{
if($this->handler instanceof InGamePacketHandler){ //TODO: this is a bad fix for pre-spawn death, this shouldn't be reachable at all at this stage :(
- $this->setHandler(new DeathPacketHandler($this->player, $this));
+ $this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError()));
}
}
@@ -722,16 +733,17 @@ class NetworkSession{
$yaw = $yaw ?? $location->getYaw();
$pitch = $pitch ?? $location->getPitch();
- $pk = new MovePlayerPacket();
- $pk->entityRuntimeId = $this->player->getId();
- $pk->position = $this->player->getOffsetPosition($pos);
- $pk->pitch = $pitch;
- $pk->headYaw = $yaw;
- $pk->yaw = $yaw;
- $pk->mode = $mode;
- $pk->onGround = $this->player->onGround;
-
- $this->sendDataPacket($pk);
+ $this->sendDataPacket(MovePlayerPacket::simple(
+ $this->player->getId(),
+ $this->player->getOffsetPosition($pos),
+ $pitch,
+ $yaw,
+ $yaw, //TODO: head yaw
+ $mode,
+ $this->player->onGround,
+ 0, //TODO: riding entity ID
+ 0 //TODO: tick
+ ));
if($this->handler instanceof InGamePacketHandler){
$this->handler->forceMoveSync = true;
@@ -744,18 +756,25 @@ class NetworkSession{
}
public function syncViewAreaCenterPoint(Vector3 $newPos, int $viewDistance) : void{
- $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create($newPos->getFloorX(), $newPos->getFloorY(), $newPos->getFloorZ(), $viewDistance * 16)); //blocks, not chunks >.>
+ $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create(BlockPosition::fromVector3($newPos), $viewDistance * 16)); //blocks, not chunks >.>
}
public function syncPlayerSpawnPoint(Position $newSpawn) : void{
- [$x, $y, $z] = [$newSpawn->getFloorX(), $newSpawn->getFloorY(), $newSpawn->getFloorZ()];
- $this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($x, $y, $z, DimensionIds::OVERWORLD, $x, $y, $z));
+ $newSpawnBlockPosition = BlockPosition::fromVector3($newSpawn);
+ //TODO: respawn causing block position (bed, respawn anchor)
+ $this->sendDataPacket(SetSpawnPositionPacket::playerSpawn($newSpawnBlockPosition, DimensionIds::OVERWORLD, $newSpawnBlockPosition));
+ }
+
+ public function syncWorldSpawnPoint(Position $newSpawn) : void{
+ $this->sendDataPacket(SetSpawnPositionPacket::worldSpawn(BlockPosition::fromVector3($newSpawn), DimensionIds::OVERWORLD));
}
public function syncGameMode(GameMode $mode, bool $isRollback = false) : void{
$this->sendDataPacket(SetPlayerGameTypePacket::create(TypeConverter::getInstance()->coreGameModeToProtocol($mode)));
- $this->syncAdventureSettings($this->player);
- if(!$isRollback){
+ if($this->player !== null){
+ $this->syncAdventureSettings($this->player);
+ }
+ if(!$isRollback && $this->invManager !== null){
$this->invManager->syncCreative();
}
}
@@ -764,22 +783,25 @@ class NetworkSession{
* TODO: make this less specialized
*/
public function syncAdventureSettings(Player $for) : void{
- $pk = new AdventureSettingsPacket();
+ $isOp = $for->hasPermission(DefaultPermissions::ROOT_OPERATOR);
+ $pk = AdventureSettingsPacket::create(
+ 0,
+ $isOp ? AdventureSettingsPacket::PERMISSION_OPERATOR : AdventureSettingsPacket::PERMISSION_NORMAL,
+ 0,
+ $isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
+ 0,
+ $for->getId()
+ );
$pk->setFlag(AdventureSettingsPacket::WORLD_IMMUTABLE, $for->isSpectator());
$pk->setFlag(AdventureSettingsPacket::NO_PVP, $for->isSpectator());
$pk->setFlag(AdventureSettingsPacket::AUTO_JUMP, $for->hasAutoJump());
$pk->setFlag(AdventureSettingsPacket::ALLOW_FLIGHT, $for->getAllowFlight());
- $pk->setFlag(AdventureSettingsPacket::NO_CLIP, $for->isSpectator());
+ $pk->setFlag(AdventureSettingsPacket::NO_CLIP, !$for->hasBlockCollision());
$pk->setFlag(AdventureSettingsPacket::FLYING, $for->isFlying());
//TODO: permission flags
- $isOp = $for->hasPermission(DefaultPermissions::ROOT_OPERATOR);
- $pk->commandPermission = ($isOp ? AdventureSettingsPacket::PERMISSION_OPERATOR : AdventureSettingsPacket::PERMISSION_NORMAL);
- $pk->playerPermission = ($isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER);
- $pk->entityUniqueId = $for->getId();
-
$this->sendDataPacket($pk);
}
@@ -816,9 +838,9 @@ class NetworkSession{
}
public function syncAvailableCommands() : void{
- $pk = new AvailableCommandsPacket();
+ $commandData = [];
foreach($this->server->getCommandMap()->getCommands() as $name => $command){
- if(isset($pk->commandData[$command->getName()]) or $command->getName() === "help" or !$command->testPermissionSilent($this->player)){
+ if(isset($commandData[$command->getName()]) || $command->getName() === "help" || !$command->testPermissionSilent($this->player)){
continue;
}
@@ -845,10 +867,10 @@ class NetworkSession{
]
);
- $pk->commandData[$command->getName()] = $data;
+ $commandData[$command->getName()] = $data;
}
- $this->sendDataPacket($pk);
+ $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));
}
public function onRawChatMessage(string $message) : void{
@@ -878,11 +900,7 @@ class NetworkSession{
}
public function onFormSent(int $id, Form $form) : bool{
- $formData = json_encode($form);
- if($formData === false){
- throw new \InvalidArgumentException("Failed to encode form JSON: " . json_last_error_msg());
- }
- return $this->sendDataPacket(ModalFormRequestPacket::create($id, $formData));
+ return $this->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($form, JSON_THROW_ON_ERROR)));
}
/**
@@ -902,11 +920,11 @@ class NetworkSession{
return;
}
$currentWorld = $this->player->getLocation()->getWorld();
- if($world !== $currentWorld or ($status = $this->player->getUsedChunkStatus($chunkX, $chunkZ)) === null){
+ if($world !== $currentWorld || ($status = $this->player->getUsedChunkStatus($chunkX, $chunkZ)) === null){
$this->logger->debug("Tried to send no-longer-active chunk $chunkX $chunkZ in world " . $world->getFolderName());
return;
}
- if(!$status->equals(UsedChunkStatus::REQUESTED())){
+ if(!$status->equals(UsedChunkStatus::REQUESTED_SENDING())){
//TODO: make this an error
//this could be triggered due to the shitty way that chunk resends are handled
//right now - not because of the spammy re-requesting, but because the chunk status reverts
@@ -933,8 +951,8 @@ class NetworkSession{
$world = $this->player->getWorld();
$this->syncWorldTime($world->getTime());
$this->syncWorldDifficulty($world->getDifficulty());
+ $this->syncWorldSpawnPoint($world->getSpawnLocation());
//TODO: weather needs to be synced here (when implemented)
- //TODO: world spawn needs to be synced here
}
}
@@ -956,12 +974,12 @@ class NetworkSession{
public function onMobMainHandItemChange(Human $mob) : void{
//TODO: we could send zero for slot here because remote players don't need to know which slot was selected
$inv = $mob->getInventory();
- $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())), $inv->getHeldItemIndex(), ContainerIds::INVENTORY));
+ $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())), $inv->getHeldItemIndex(), $inv->getHeldItemIndex(), ContainerIds::INVENTORY));
}
public function onMobOffHandItemChange(Human $mob) : void{
$inv = $mob->getOffHandInventory();
- $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))), 0, ContainerIds::OFFHAND));
+ $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))), 0, 0, ContainerIds::OFFHAND));
}
public function onMobArmorChange(Living $mob) : void{
@@ -1023,6 +1041,10 @@ class NetworkSession{
$this->sendDataPacket(SetTitlePacket::setAnimationTimes($fadeIn, $stay, $fadeOut));
}
+ public function onEmote(Player $from, string $emoteId) : void{
+ $this->sendDataPacket(EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER));
+ }
+
public function tick() : void{
if($this->info === null){
if(time() >= $this->connectTime + 10){
diff --git a/src/network/mcpe/StandardPacketBroadcaster.php b/src/network/mcpe/StandardPacketBroadcaster.php
index 1442a5bbf..4da9b0951 100644
--- a/src/network/mcpe/StandardPacketBroadcaster.php
+++ b/src/network/mcpe/StandardPacketBroadcaster.php
@@ -23,10 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
-use pocketmine\network\mcpe\compression\Compressor;
-use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
-use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\Server;
use function spl_object_id;
@@ -40,35 +37,40 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
}
public function broadcastPackets(array $recipients, array $packets) : void{
- //TODO: we should be using session-specific serializer contexts for this
- $stream = PacketBatch::fromPackets(new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary()), ...$packets);
-
- /** @var Compressor[] $compressors */
+ $buffers = [];
$compressors = [];
- /** @var NetworkSession[][] $compressorTargets */
- $compressorTargets = [];
+ $targetMap = [];
foreach($recipients as $recipient){
- $compressor = $recipient->getCompressor();
- $compressorId = spl_object_id($compressor);
+ $serializerContext = $recipient->getPacketSerializerContext();
+ $bufferId = spl_object_id($serializerContext);
+ if(!isset($buffers[$bufferId])){
+ $buffers[$bufferId] = PacketBatch::fromPackets($serializerContext, ...$packets);
+ }
+
//TODO: different compressors might be compatible, it might not be necessary to split them up by object
- $compressors[$compressorId] = $compressor;
- $compressorTargets[$compressorId][] = $recipient;
+ $compressor = $recipient->getCompressor();
+ $compressors[spl_object_id($compressor)] = $compressor;
+
+ $targetMap[$bufferId][spl_object_id($compressor)][] = $recipient;
}
- foreach($compressors as $compressorId => $compressor){
- if(!$compressor->willCompress($stream->getBuffer())){
- foreach($compressorTargets[$compressorId] as $target){
- foreach($packets as $pk){
- $target->addToSendBuffer($pk);
+ foreach($targetMap as $bufferId => $compressorMap){
+ $buffer = $buffers[$bufferId];
+ foreach($compressorMap as $compressorId => $compressorTargets){
+ $compressor = $compressors[$compressorId];
+ if(!$compressor->willCompress($buffer->getBuffer())){
+ foreach($compressorTargets as $target){
+ foreach($packets as $pk){
+ $target->addToSendBuffer($pk);
+ }
+ }
+ }else{
+ $promise = $this->server->prepareBatch($buffer, $compressor);
+ foreach($compressorTargets as $target){
+ $target->queueCompressed($promise);
}
- }
- }else{
- $promise = $this->server->prepareBatch($stream, $compressor);
- foreach($compressorTargets[$compressorId] as $target){
- $target->queueCompressed($promise);
}
}
}
-
}
}
diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php
index c0e537c64..940dd8bad 100644
--- a/src/network/mcpe/auth/ProcessLoginTask.php
+++ b/src/network/mcpe/auth/ProcessLoginTask.php
@@ -33,7 +33,6 @@ use function base64_decode;
use function igbinary_serialize;
use function igbinary_unserialize;
use function openssl_error_string;
-use function openssl_free_key;
use function time;
class ProcessLoginTask extends AsyncTask{
@@ -158,8 +157,6 @@ class ProcessLoginTask extends AsyncTask{
throw new VerifyLoginException($e->getMessage(), 0, $e);
}
- @openssl_free_key($signingKeyOpenSSL);
-
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
$this->authenticated = true; //we're signed into xbox live
}
@@ -177,11 +174,11 @@ class ProcessLoginTask extends AsyncTask{
}
$time = time();
- if(isset($claims->nbf) and $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
+ if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY);
}
- if(isset($claims->exp) and $claims->exp < $time - self::CLOCK_DRIFT_MAX){
+ if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE);
}
diff --git a/src/network/mcpe/cache/ChunkCache.php b/src/network/mcpe/cache/ChunkCache.php
index 269451f49..f8107e356 100644
--- a/src/network/mcpe/cache/ChunkCache.php
+++ b/src/network/mcpe/cache/ChunkCache.php
@@ -36,9 +36,6 @@ use function strlen;
/**
* This class is used by the current MCPE protocol system to store cached chunk packets for fast resending.
- *
- * TODO: make MemoryManager aware of this so the cache can be destroyed when memory is low
- * TODO: this needs a hook for world unloading
*/
class ChunkCache implements ChunkListener{
/** @var self[][] */
@@ -69,6 +66,19 @@ class ChunkCache implements ChunkListener{
return self::$instances[$worldId][$compressorId];
}
+ public static function pruneCaches() : void{
+ foreach(self::$instances as $compressorMap){
+ foreach($compressorMap as $chunkCache){
+ foreach($chunkCache->caches as $chunkHash => $promise){
+ if($promise->hasResult()){
+ //Do not clear promises that are not yet fulfilled; they will have requesters waiting on them
+ unset($chunkCache->caches[$chunkHash]);
+ }
+ }
+ }
+ }
+ }
+
/** @var World */
private $world;
/** @var Compressor */
@@ -118,10 +128,12 @@ class ChunkCache implements ChunkListener{
$chunk,
$this->caches[$chunkHash],
$this->compressor,
- function() use ($chunkX, $chunkZ) : void{
+ function() use ($chunkHash, $chunkX, $chunkZ) : void{
$this->world->getLogger()->error("Failed preparing chunk $chunkX $chunkZ, retrying");
- $this->restartPendingRequest($chunkX, $chunkZ);
+ if(isset($this->caches[$chunkHash])){
+ $this->restartPendingRequest($chunkX, $chunkZ);
+ }
}
)
);
@@ -148,7 +160,7 @@ class ChunkCache implements ChunkListener{
private function restartPendingRequest(int $chunkX, int $chunkZ) : void{
$chunkHash = World::chunkHash($chunkX, $chunkZ);
$existing = $this->caches[$chunkHash] ?? null;
- if($existing === null or $existing->hasResult()){
+ if($existing === null || $existing->hasResult()){
throw new \InvalidArgumentException("Restart can only be applied to unresolved promises");
}
$existing->cancel();
diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php
index a87629a26..2b2d1968e 100644
--- a/src/network/mcpe/cache/CraftingDataCache.php
+++ b/src/network/mcpe/cache/CraftingDataCache.php
@@ -26,12 +26,15 @@ namespace pocketmine\network\mcpe\cache;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\FurnaceType;
use pocketmine\item\Item;
+use pocketmine\network\mcpe\convert\ItemTranslator;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\recipe\CraftingRecipeBlockName;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe as ProtocolFurnaceRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipeBlockName;
+use pocketmine\network\mcpe\protocol\types\recipe\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe;
+use pocketmine\network\mcpe\protocol\types\recipe\PotionTypeRecipe as ProtocolPotionTypeRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
@@ -71,15 +74,14 @@ final class CraftingDataCache{
*/
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
Timings::$craftingDataCacheRebuild->startTiming();
- $pk = new CraftingDataPacket();
- $pk->cleanRecipes = true;
$counter = 0;
$nullUUID = Uuid::fromString(Uuid::NIL);
$converter = TypeConverter::getInstance();
+ $recipesWithTypeIds = [];
foreach($manager->getShapelessRecipes() as $list){
foreach($list as $recipe){
- $pk->entries[] = new ProtocolShapelessRecipe(
+ $recipesWithTypeIds[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt(++$counter),
array_map(function(Item $item) use ($converter) : RecipeIngredient{
@@ -104,7 +106,7 @@ final class CraftingDataCache{
$inputs[$row][$column] = $converter->coreItemStackToRecipeIngredient($recipe->getIngredient($column, $row));
}
}
- $pk->entries[] = $r = new ProtocolShapedRecipe(
+ $recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
CraftingDataPacket::ENTRY_SHAPED,
Binary::writeInt(++$counter),
$inputs,
@@ -128,7 +130,7 @@ final class CraftingDataCache{
};
foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){
$input = $converter->coreItemStackToNet($recipe->getInput());
- $pk->entries[] = new ProtocolFurnaceRecipe(
+ $recipesWithTypeIds[] = new ProtocolFurnaceRecipe(
CraftingDataPacket::ENTRY_FURNACE_DATA,
$input->getId(),
$input->getMeta(),
@@ -138,7 +140,39 @@ final class CraftingDataCache{
}
}
+ $potionTypeRecipes = [];
+ foreach($manager->getPotionTypeRecipes() as $recipes){
+ foreach($recipes as $recipe){
+ $input = $converter->coreItemStackToNet($recipe->getInput());
+ $ingredient = $converter->coreItemStackToNet($recipe->getIngredient());
+ $output = $converter->coreItemStackToNet($recipe->getOutput());
+ $potionTypeRecipes[] = new ProtocolPotionTypeRecipe(
+ $input->getId(),
+ $input->getMeta(),
+ $ingredient->getId(),
+ $ingredient->getMeta(),
+ $output->getId(),
+ $output->getMeta()
+ );
+ }
+ }
+
+ $potionContainerChangeRecipes = [];
+ $itemTranslator = ItemTranslator::getInstance();
+ foreach($manager->getPotionContainerChangeRecipes() as $recipes){
+ foreach($recipes as $recipe){
+ $input = $itemTranslator->toNetworkId($recipe->getInputItemId(), 0);
+ $ingredient = $itemTranslator->toNetworkId($recipe->getIngredient()->getId(), 0);
+ $output = $itemTranslator->toNetworkId($recipe->getOutputItemId(), 0);
+ $potionContainerChangeRecipes[] = new ProtocolPotionContainerChangeRecipe(
+ $input[0],
+ $ingredient[0],
+ $output[0]
+ );
+ }
+ }
+
Timings::$craftingDataCacheRebuild->stopTiming();
- return $pk;
+ return CraftingDataPacket::create($recipesWithTypeIds, $potionTypeRecipes, $potionContainerChangeRecipes, [], true);
}
}
diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php
index 901fe875c..39b8ea774 100644
--- a/src/network/mcpe/cache/StaticPacketCache.php
+++ b/src/network/mcpe/cache/StaticPacketCache.php
@@ -50,8 +50,8 @@ class StaticPacketCache{
private static function make() : self{
return new self(
- BiomeDefinitionListPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\RESOURCE_PATH, 'vanilla', 'biome_definitions.nbt'))),
- AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\RESOURCE_PATH, 'vanilla', 'entity_identifiers.nbt')))
+ BiomeDefinitionListPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'biome_definitions.nbt'))),
+ AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'entity_identifiers.nbt')))
);
}
diff --git a/src/network/mcpe/compression/CompressBatchPromise.php b/src/network/mcpe/compression/CompressBatchPromise.php
index 8fa8b12ae..c62388f6a 100644
--- a/src/network/mcpe/compression/CompressBatchPromise.php
+++ b/src/network/mcpe/compression/CompressBatchPromise.php
@@ -59,7 +59,7 @@ class CompressBatchPromise{
public function resolve(string $result) : void{
if(!$this->cancelled){
if($this->result !== null){
- throw new \InvalidStateException("Cannot resolve promise more than once");
+ throw new \LogicException("Cannot resolve promise more than once");
}
$this->result = $result;
foreach($this->callbacks as $callback){
@@ -80,7 +80,7 @@ class CompressBatchPromise{
public function getResult() : string{
$this->checkCancelled();
if($this->result === null){
- throw new \InvalidStateException("Promise has not yet been resolved");
+ throw new \LogicException("Promise has not yet been resolved");
}
return $this->result;
}
@@ -95,7 +95,7 @@ class CompressBatchPromise{
public function cancel() : void{
if($this->hasResult()){
- throw new \InvalidStateException("Cannot cancel a resolved promise");
+ throw new \LogicException("Cannot cancel a resolved promise");
}
$this->cancelled = true;
}
diff --git a/src/network/mcpe/compression/Compressor.php b/src/network/mcpe/compression/Compressor.php
index bbc8fac4f..50e8ab609 100644
--- a/src/network/mcpe/compression/Compressor.php
+++ b/src/network/mcpe/compression/Compressor.php
@@ -33,4 +33,4 @@ interface Compressor{
public function decompress(string $payload) : string;
public function compress(string $payload) : string;
-}
\ No newline at end of file
+}
diff --git a/src/network/mcpe/compression/DecompressionException.php b/src/network/mcpe/compression/DecompressionException.php
index dfaf7e50e..8cff0e6d2 100644
--- a/src/network/mcpe/compression/DecompressionException.php
+++ b/src/network/mcpe/compression/DecompressionException.php
@@ -25,4 +25,4 @@ namespace pocketmine\network\mcpe\compression;
final class DecompressionException extends \RuntimeException{
-}
\ No newline at end of file
+}
diff --git a/src/network/mcpe/compression/ZlibCompressor.php b/src/network/mcpe/compression/ZlibCompressor.php
index b5bd33bcb..a489aa153 100644
--- a/src/network/mcpe/compression/ZlibCompressor.php
+++ b/src/network/mcpe/compression/ZlibCompressor.php
@@ -23,8 +23,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\compression;
-use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
+use pocketmine\utils\Utils;
use function function_exists;
use function libdeflate_deflate_compress;
use function strlen;
@@ -60,7 +60,7 @@ final class ZlibCompressor implements Compressor{
}
public function willCompress(string $data) : bool{
- return $this->threshold > -1 and strlen($data) >= $this->threshold;
+ return $this->threshold > -1 && strlen($data) >= $this->threshold;
}
/**
@@ -75,9 +75,7 @@ final class ZlibCompressor implements Compressor{
}
private static function zlib_encode(string $data, int $level) : string{
- $result = zlib_encode($data, ZLIB_ENCODING_RAW, $level);
- if($result === false) throw new AssumptionFailedError("ZLIB compression failed");
- return $result;
+ return Utils::assumeNotFalse(zlib_encode($data, ZLIB_ENCODING_RAW, $level), "ZLIB compression failed");
}
public function compress(string $payload) : string{
diff --git a/src/network/mcpe/convert/GlobalItemTypeDictionary.php b/src/network/mcpe/convert/GlobalItemTypeDictionary.php
index c1537fa7c..61510e590 100644
--- a/src/network/mcpe/convert/GlobalItemTypeDictionary.php
+++ b/src/network/mcpe/convert/GlobalItemTypeDictionary.php
@@ -27,6 +27,7 @@ use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function file_get_contents;
use function is_array;
@@ -39,8 +40,7 @@ final class GlobalItemTypeDictionary{
use SingletonTrait;
private static function make() : self{
- $data = file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'vanilla', 'required_item_list.json'));
- if($data === false) throw new AssumptionFailedError("Missing required resource file");
+ $data = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'required_item_list.json')), "Missing required resource file");
$table = json_decode($data, true);
if(!is_array($table)){
throw new AssumptionFailedError("Invalid item list format");
diff --git a/src/network/mcpe/convert/ItemTranslator.php b/src/network/mcpe/convert/ItemTranslator.php
index d1a7c3a58..d00a6f90c 100644
--- a/src/network/mcpe/convert/ItemTranslator.php
+++ b/src/network/mcpe/convert/ItemTranslator.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
+use pocketmine\data\bedrock\LegacyItemIdToStringIdMap;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function array_key_exists;
use function file_get_contents;
@@ -65,21 +67,13 @@ final class ItemTranslator{
private $complexNetToCoreMapping = [];
private static function make() : self{
- $data = file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'vanilla', 'r16_to_current_item_map.json'));
- if($data === false) throw new AssumptionFailedError("Missing required resource file");
+ $data = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'r16_to_current_item_map.json')), "Missing required resource file");
$json = json_decode($data, true);
- if(!is_array($json) or !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){
+ if(!is_array($json) || !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){
throw new AssumptionFailedError("Invalid item table format");
}
- $legacyStringToIntMapRaw = file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'vanilla', 'item_id_map.json'));
- if($legacyStringToIntMapRaw === false){
- throw new AssumptionFailedError("Missing required resource file");
- }
- $legacyStringToIntMap = json_decode($legacyStringToIntMapRaw, true);
- if(!is_array($legacyStringToIntMap)){
- throw new AssumptionFailedError("Invalid mapping table format");
- }
+ $legacyStringToIntMap = LegacyItemIdToStringIdMap::getInstance();
/** @phpstan-var array $simpleMappings */
$simpleMappings = [];
@@ -87,13 +81,14 @@ final class ItemTranslator{
if(!is_string($oldId) || !is_string($newId)){
throw new AssumptionFailedError("Invalid item table format");
}
- if(!isset($legacyStringToIntMap[$oldId])){
+ $intId = $legacyStringToIntMap->stringToLegacy($oldId);
+ if($intId === null){
//new item without a fixed legacy ID - we can't handle this right now
continue;
}
- $simpleMappings[$newId] = $legacyStringToIntMap[$oldId];
+ $simpleMappings[$newId] = $intId;
}
- foreach($legacyStringToIntMap as $stringId => $intId){
+ foreach(Utils::stringifyKeys($legacyStringToIntMap->getStringToLegacyMap()) as $stringId => $intId){
if(isset($simpleMappings[$stringId])){
throw new \UnexpectedValueException("Old ID $stringId collides with new ID");
}
@@ -110,7 +105,12 @@ final class ItemTranslator{
if(!is_numeric($meta) || !is_string($newId)){
throw new AssumptionFailedError("Invalid item table format");
}
- $complexMappings[$newId] = [$legacyStringToIntMap[$oldId], (int) $meta];
+ $intId = $legacyStringToIntMap->stringToLegacy($oldId);
+ if($intId === null){
+ //new item without a fixed legacy ID - we can't handle this right now
+ continue;
+ }
+ $complexMappings[$newId] = [$intId, (int) $meta];
}
}
@@ -142,10 +142,10 @@ final class ItemTranslator{
}
/**
- * @return int[]
- * @phpstan-return array{int, int}
+ * @return int[]|null
+ * @phpstan-return array{int, int}|null
*/
- public function toNetworkId(int $internalId, int $internalMeta) : array{
+ public function toNetworkIdQuiet(int $internalId, int $internalMeta) : ?array{
if($internalMeta === -1){
$internalMeta = 0x7fff;
}
@@ -156,17 +156,27 @@ final class ItemTranslator{
return [$this->simpleCoreToNetMapping[$internalId], $internalMeta];
}
- throw new \InvalidArgumentException("Unmapped ID/metadata combination $internalId:$internalMeta");
+ return null;
}
/**
* @return int[]
* @phpstan-return array{int, int}
*/
+ public function toNetworkId(int $internalId, int $internalMeta) : array{
+ return $this->toNetworkIdQuiet($internalId, $internalMeta) ??
+ throw new \InvalidArgumentException("Unmapped ID/metadata combination $internalId:$internalMeta");
+ }
+
+ /**
+ * @return int[]
+ * @phpstan-return array{int, int}
+ * @throws TypeConversionException
+ */
public function fromNetworkId(int $networkId, int $networkMeta, ?bool &$isComplexMapping = null) : array{
if(isset($this->complexNetToCoreMapping[$networkId])){
if($networkMeta !== 0){
- throw new \UnexpectedValueException("Unexpected non-zero network meta on complex item mapping");
+ throw new TypeConversionException("Unexpected non-zero network meta on complex item mapping");
}
$isComplexMapping = true;
return $this->complexNetToCoreMapping[$networkId];
@@ -175,12 +185,13 @@ final class ItemTranslator{
if(isset($this->simpleNetToCoreMapping[$networkId])){
return [$this->simpleNetToCoreMapping[$networkId], $networkMeta];
}
- throw new \UnexpectedValueException("Unmapped network ID/metadata combination $networkId:$networkMeta");
+ throw new TypeConversionException("Unmapped network ID/metadata combination $networkId:$networkMeta");
}
/**
* @return int[]
* @phpstan-return array{int, int}
+ * @throws TypeConversionException
*/
public function fromNetworkIdWithWildcardHandling(int $networkId, int $networkMeta) : array{
$isComplexMapping = false;
diff --git a/src/network/mcpe/convert/LegacySkinAdapter.php b/src/network/mcpe/convert/LegacySkinAdapter.php
index 31d4ae92b..60bb29d57 100644
--- a/src/network/mcpe/convert/LegacySkinAdapter.php
+++ b/src/network/mcpe/convert/LegacySkinAdapter.php
@@ -31,9 +31,9 @@ use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
-use function json_last_error_msg;
use function random_bytes;
use function str_repeat;
+use const JSON_THROW_ON_ERROR;
class LegacySkinAdapter implements SkinAdapter{
@@ -44,14 +44,10 @@ class LegacySkinAdapter implements SkinAdapter{
if($geometryName === ""){
$geometryName = "geometry.humanoid.custom";
}
- $resourcePatch = json_encode(["geometry" => ["default" => $geometryName]]);
- if($resourcePatch === false){
- throw new \RuntimeException("json_encode() failed: " . json_last_error_msg());
- }
return new SkinData(
$skin->getSkinId(),
"", //TODO: playfab ID
- $resourcePatch,
+ json_encode(["geometry" => ["default" => $geometryName]], JSON_THROW_ON_ERROR),
SkinImage::fromLegacy($skin->getSkinData()), [],
$capeImage,
$skin->getGeometryData()
diff --git a/src/network/mcpe/convert/RuntimeBlockMapping.php b/src/network/mcpe/convert/RuntimeBlockMapping.php
index 45affea44..75835be0f 100644
--- a/src/network/mcpe/convert/RuntimeBlockMapping.php
+++ b/src/network/mcpe/convert/RuntimeBlockMapping.php
@@ -30,8 +30,8 @@ use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
-use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function file_get_contents;
@@ -49,11 +49,11 @@ final class RuntimeBlockMapping{
private $bedrockKnownStates;
private function __construct(){
- $canonicalBlockStatesFile = file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "vanilla", "canonical_block_states.nbt"));
- if($canonicalBlockStatesFile === false){
- throw new AssumptionFailedError("Missing required resource file");
- }
- $stream = PacketSerializer::decoder($canonicalBlockStatesFile, 0, new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary()));
+ $stream = PacketSerializer::decoder(
+ Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt")), "Missing required resource file"),
+ 0,
+ new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
+ );
$list = [];
while(!$stream->feof()){
$list[] = $stream->getNbtCompoundRoot();
@@ -67,7 +67,11 @@ final class RuntimeBlockMapping{
$legacyIdMap = LegacyBlockIdToStringIdMap::getInstance();
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
$legacyStateMap = [];
- $legacyStateMapReader = PacketSerializer::decoder(file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "vanilla", "r12_to_current_block_map.bin")), 0, new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary()));
+ $legacyStateMapReader = PacketSerializer::decoder(
+ Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "r12_to_current_block_map.bin")), "Missing required resource file"),
+ 0,
+ new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
+ );
$nbtReader = new NetworkNbtSerializer();
while(!$legacyStateMapReader->feof()){
$id = $legacyStateMapReader->getString();
diff --git a/src/network/mcpe/convert/TypeConversionException.php b/src/network/mcpe/convert/TypeConversionException.php
new file mode 100644
index 000000000..fb24fefd3
--- /dev/null
+++ b/src/network/mcpe/convert/TypeConversionException.php
@@ -0,0 +1,35 @@
+getMessage(), 0, $previous);
+ }
+}
diff --git a/src/network/mcpe/convert/TypeConverter.php b/src/network/mcpe/convert/TypeConverter.php
index c523eac10..d03918efd 100644
--- a/src/network/mcpe/convert/TypeConverter.php
+++ b/src/network/mcpe/convert/TypeConverter.php
@@ -24,10 +24,9 @@ namespace pocketmine\network\mcpe\convert;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\inventory\AnvilInventory;
+use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\LoomInventory;
-use pocketmine\crafting\CraftingGrid;
-use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction;
@@ -37,6 +36,7 @@ use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
use pocketmine\item\ItemIds;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\network\mcpe\InventoryManager;
@@ -50,13 +50,13 @@ use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
-use function array_key_exists;
class TypeConverter{
use SingletonTrait;
private const DAMAGE_TAG = "Damage"; //TAG_Int
private const DAMAGE_TAG_CONFLICT_RESOLUTION = "___Damage_ProtocolCollisionResolution___";
+ private const PM_ID_TAG = "___Id___";
private const PM_META_TAG = "___Meta___";
/** @var int */
@@ -142,26 +142,40 @@ class TypeConverter{
}
$isBlockItem = $itemStack->getId() < 256;
- if($itemStack instanceof Durable and $itemStack->getDamage() > 0){
- if($nbt !== null){
- if(($existing = $nbt->getTag(self::DAMAGE_TAG)) !== null){
- $nbt->removeTag(self::DAMAGE_TAG);
- $nbt->setTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION, $existing);
- }
- }else{
- $nbt = new CompoundTag();
- }
- $nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage());
- }elseif($isBlockItem && $itemStack->getMeta() !== 0){
- //TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
- //client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
- //client-side. Aside from being very annoying, this also breaks various server-side behaviours.
+
+ $idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($itemStack->getId(), $itemStack->getMeta());
+ if($idMeta === null){
+ //Display unmapped items as INFO_UPDATE, but stick something in their NBT to make sure they don't stack with
+ //other unmapped items.
+ [$id, $meta] = ItemTranslator::getInstance()->toNetworkId(ItemIds::INFO_UPDATE, 0);
if($nbt === null){
$nbt = new CompoundTag();
}
+ $nbt->setInt(self::PM_ID_TAG, $itemStack->getId());
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
+ }else{
+ [$id, $meta] = $idMeta;
+
+ if($itemStack instanceof Durable && $itemStack->getDamage() > 0){
+ if($nbt !== null){
+ if(($existing = $nbt->getTag(self::DAMAGE_TAG)) !== null){
+ $nbt->removeTag(self::DAMAGE_TAG);
+ $nbt->setTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION, $existing);
+ }
+ }else{
+ $nbt = new CompoundTag();
+ }
+ $nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage());
+ }elseif($isBlockItem && $itemStack->getMeta() !== 0){
+ //TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
+ //client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
+ //client-side. Aside from being very annoying, this also breaks various server-side behaviours.
+ if($nbt === null){
+ $nbt = new CompoundTag();
+ }
+ $nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
+ }
}
- [$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), $itemStack->getMeta());
$blockRuntimeId = 0;
if($isBlockItem){
@@ -183,6 +197,9 @@ class TypeConverter{
);
}
+ /**
+ * @throws TypeConversionException
+ */
public function netItemStackToCore(ItemStack $itemStack) : Item{
if($itemStack->getId() === 0){
return ItemFactory::getInstance()->get(ItemIds::AIR, 0, 0);
@@ -193,14 +210,16 @@ class TypeConverter{
if($compound !== null){
$compound = clone $compound;
+ if(($idTag = $compound->getTag(self::PM_ID_TAG)) instanceof IntTag){
+ $id = $idTag->getValue();
+ $compound->removeTag(self::PM_ID_TAG);
+ }
if(($damageTag = $compound->getTag(self::DAMAGE_TAG)) instanceof IntTag){
$meta = $damageTag->getValue();
$compound->removeTag(self::DAMAGE_TAG);
if(($conflicted = $compound->getTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION)) !== null){
$compound->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION);
$compound->setTag(self::DAMAGE_TAG, $conflicted);
- }elseif($compound->count() === 0){
- $compound = null;
}
}elseif(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
@@ -208,74 +227,70 @@ class TypeConverter{
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
$meta = $metaTag->getValue();
$compound->removeTag(self::PM_META_TAG);
- if($compound->count() === 0){
- $compound = null;
- }
+ }
+ if($compound->count() === 0){
+ $compound = null;
}
}
- return ItemFactory::getInstance()->get(
- $id,
- $meta,
- $itemStack->getCount(),
- $compound
- );
+ try{
+ return ItemFactory::getInstance()->get(
+ $id,
+ $meta,
+ $itemStack->getCount(),
+ $compound
+ );
+ }catch(NbtException $e){
+ throw TypeConversionException::wrap($e, "Bad itemstack NBT data");
+ }
}
/**
- * @param int[] $test
- * @phpstan-param array $test
- * @phpstan-param \Closure(Inventory) : bool $c
- * @phpstan-return array{int, Inventory}
- */
- protected function mapUIInventory(int $slot, array $test, ?Inventory $inventory, \Closure $c) : ?array{
- if($inventory === null){
- return null;
- }
- if(array_key_exists($slot, $test) && $c($inventory)){
- return [$test[$slot], $inventory];
- }
- return null;
- }
-
- /**
- * @throws \UnexpectedValueException
+ * @throws TypeConversionException
*/
public function createInventoryAction(NetworkInventoryAction $action, Player $player, InventoryManager $inventoryManager) : ?InventoryAction{
if($action->oldItem->getItemStack()->equals($action->newItem->getItemStack())){
//filter out useless noise in 1.13
return null;
}
- $old = $this->netItemStackToCore($action->oldItem->getItemStack());
- $new = $this->netItemStackToCore($action->newItem->getItemStack());
+ try{
+ $old = $this->netItemStackToCore($action->oldItem->getItemStack());
+ }catch(TypeConversionException $e){
+ throw TypeConversionException::wrap($e, "Inventory action: oldItem");
+ }
+ try{
+ $new = $this->netItemStackToCore($action->newItem->getItemStack());
+ }catch(TypeConversionException $e){
+ throw TypeConversionException::wrap($e, "Inventory action: newItem");
+ }
switch($action->sourceType){
case NetworkInventoryAction::SOURCE_CONTAINER:
- if($action->windowId === ContainerIds::UI and $action->inventorySlot > 0){
+ $window = null;
+ if($action->windowId === ContainerIds::UI && $action->inventorySlot > 0){
if($action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
return null; //useless noise
}
$pSlot = $action->inventorySlot;
- $craftingGrid = $player->getCraftingGrid();
- $mapped =
- $this->mapUIInventory($pSlot, UIInventorySlotOffset::CRAFTING2X2_INPUT, $craftingGrid,
- function(Inventory $i) : bool{ return $i instanceof CraftingGrid && $i->getGridWidth() === CraftingGrid::SIZE_SMALL; }) ??
- $this->mapUIInventory($pSlot, UIInventorySlotOffset::CRAFTING3X3_INPUT, $craftingGrid,
- function(Inventory $i) : bool{ return $i instanceof CraftingGrid && $i->getGridWidth() === CraftingGrid::SIZE_BIG; });
- if($mapped === null){
- $current = $player->getCurrentWindow();
- $mapped =
- $this->mapUIInventory($pSlot, UIInventorySlotOffset::ANVIL, $current,
- function(Inventory $i) : bool{ return $i instanceof AnvilInventory; }) ??
- $this->mapUIInventory($pSlot, UIInventorySlotOffset::ENCHANTING_TABLE, $current,
- function(Inventory $i) : bool{ return $i instanceof EnchantInventory; }) ??
- $this->mapUIInventory($pSlot, UIInventorySlotOffset::LOOM, $current,
- fn(Inventory $i) => $i instanceof LoomInventory);
+ $slot = UIInventorySlotOffset::CRAFTING2X2_INPUT[$pSlot] ?? null;
+ if($slot !== null){
+ $window = $player->getCraftingGrid();
+ }elseif(($current = $player->getCurrentWindow()) !== null){
+ $slotMap = match(true){
+ $current instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
+ $current instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
+ $current instanceof LoomInventory => UIInventorySlotOffset::LOOM,
+ $current instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
+ default => null
+ };
+ if($slotMap !== null){
+ $window = $current;
+ $slot = $slotMap[$pSlot] ?? null;
+ }
}
- if($mapped === null){
- throw new \UnexpectedValueException("Unmatched UI inventory slot offset $pSlot");
+ if($slot === null){
+ throw new TypeConversionException("Unmatched UI inventory slot offset $pSlot");
}
- [$slot, $window] = $mapped;
}else{
$window = $inventoryManager->getWindow($action->windowId);
$slot = $action->inventorySlot;
@@ -284,10 +299,10 @@ class TypeConverter{
return new SlotChangeAction($window, $slot, $old, $new);
}
- throw new \UnexpectedValueException("No open container with window ID $action->windowId");
+ throw new TypeConversionException("No open container with window ID $action->windowId");
case NetworkInventoryAction::SOURCE_WORLD:
if($action->inventorySlot !== NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
- throw new \UnexpectedValueException("Only expecting drop-item world actions from the client!");
+ throw new TypeConversionException("Only expecting drop-item world actions from the client!");
}
return new DropItemAction($new);
@@ -298,7 +313,7 @@ class TypeConverter{
case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_CREATE_ITEM:
return new CreateItemAction($old);
default:
- throw new \UnexpectedValueException("Unexpected creative action type $action->inventorySlot");
+ throw new TypeConversionException("Unexpected creative action type $action->inventorySlot");
}
case NetworkInventoryAction::SOURCE_TODO:
@@ -310,9 +325,9 @@ class TypeConverter{
}
//TODO: more stuff
- throw new \UnexpectedValueException("No open container with window ID $action->windowId");
+ throw new TypeConversionException("No open container with window ID $action->windowId");
default:
- throw new \UnexpectedValueException("Unknown inventory source type $action->sourceType");
+ throw new TypeConversionException("Unknown inventory source type $action->sourceType");
}
}
}
diff --git a/src/network/mcpe/encryption/EncryptionUtils.php b/src/network/mcpe/encryption/EncryptionUtils.php
index 0ad2ebfee..0aec6b445 100644
--- a/src/network/mcpe/encryption/EncryptionUtils.php
+++ b/src/network/mcpe/encryption/EncryptionUtils.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\encryption;
use pocketmine\network\mcpe\JwtUtils;
+use pocketmine\utils\Utils;
use function base64_encode;
use function bin2hex;
use function gmp_init;
@@ -49,7 +50,7 @@ final class EncryptionUtils{
}
public static function generateKey(\GMP $secret, string $salt) : string{
- return openssl_digest($salt . hex2bin(str_pad(gmp_strval($secret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true);
+ return Utils::assumeNotFalse(openssl_digest($salt . hex2bin(str_pad(gmp_strval($secret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true));
}
public static function generateServerHandshakeJwt(\OpenSSLAsymmetricKey $serverPriv, string $salt) : string{
diff --git a/src/network/mcpe/encryption/PrepareEncryptionTask.php b/src/network/mcpe/encryption/PrepareEncryptionTask.php
index 75b3b1f1c..7b2bee0e8 100644
--- a/src/network/mcpe/encryption/PrepareEncryptionTask.php
+++ b/src/network/mcpe/encryption/PrepareEncryptionTask.php
@@ -29,7 +29,6 @@ use pocketmine\utils\AssumptionFailedError;
use function igbinary_serialize;
use function igbinary_unserialize;
use function openssl_error_string;
-use function openssl_free_key;
use function openssl_pkey_get_details;
use function openssl_pkey_new;
use function random_bytes;
@@ -78,9 +77,6 @@ class PrepareEncryptionTask extends AsyncTask{
$salt = random_bytes(16);
$this->aesKey = EncryptionUtils::generateKey($sharedSecret, $salt);
$this->handshakeJwt = EncryptionUtils::generateServerHandshakeJwt($serverPriv, $salt);
-
- @openssl_free_key($serverPriv);
- @openssl_free_key($clientPub);
}
public function onCompletion() : void{
diff --git a/src/network/mcpe/handler/DeathPacketHandler.php b/src/network/mcpe/handler/DeathPacketHandler.php
index db4cbaa7d..bc56729c9 100644
--- a/src/network/mcpe/handler/DeathPacketHandler.php
+++ b/src/network/mcpe/handler/DeathPacketHandler.php
@@ -23,9 +23,12 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
+use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
+use pocketmine\network\mcpe\protocol\ContainerClosePacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\RespawnPacket;
+use pocketmine\network\mcpe\protocol\types\PlayerAction;
use pocketmine\player\Player;
class DeathPacketHandler extends PacketHandler{
@@ -34,10 +37,12 @@ class DeathPacketHandler extends PacketHandler{
private $player;
/** @var NetworkSession */
private $session;
+ private InventoryManager $inventoryManager;
- public function __construct(Player $player, NetworkSession $session){
+ public function __construct(Player $player, NetworkSession $session, InventoryManager $inventoryManager){
$this->player = $player;
$this->session = $session;
+ $this->inventoryManager = $inventoryManager;
}
public function setUp() : void{
@@ -49,7 +54,7 @@ class DeathPacketHandler extends PacketHandler{
}
public function handlePlayerAction(PlayerActionPacket $packet) : bool{
- if($packet->action === PlayerActionPacket::ACTION_RESPAWN){
+ if($packet->action === PlayerAction::RESPAWN){
$this->player->respawn();
return true;
}
@@ -57,6 +62,11 @@ class DeathPacketHandler extends PacketHandler{
return false;
}
+ public function handleContainerClose(ContainerClosePacket $packet) : bool{
+ $this->inventoryManager->onClientRemoveWindow($packet->windowId);
+ return true;
+ }
+
public function handleRespawn(RespawnPacket $packet) : bool{
if($packet->respawnState === RespawnPacket::CLIENT_READY_TO_SPAWN){
$this->session->sendDataPacket(RespawnPacket::create(
diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php
index c36510d02..97d45ee89 100644
--- a/src/network/mcpe/handler/InGamePacketHandler.php
+++ b/src/network/mcpe/handler/InGamePacketHandler.php
@@ -25,9 +25,10 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\block\BaseSign;
use pocketmine\block\ItemFrame;
+use pocketmine\block\Lectern;
use pocketmine\block\utils\SignText;
-use pocketmine\crafting\CraftingGrid;
use pocketmine\entity\animation\ConsumingItemAnimation;
+use pocketmine\entity\Attribute;
use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerEditBookEvent;
use pocketmine\inventory\transaction\action\InventoryAction;
@@ -37,11 +38,14 @@ use pocketmine\inventory\transaction\TransactionException;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\VanillaItems;
use pocketmine\item\WritableBook;
+use pocketmine\item\WritableBookPage;
use pocketmine\item\WrittenBook;
+use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
+use pocketmine\network\mcpe\convert\TypeConversionException;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
@@ -56,12 +60,13 @@ use pocketmine\network\mcpe\protocol\BossEventPacket;
use pocketmine\network\mcpe\protocol\CommandBlockUpdatePacket;
use pocketmine\network\mcpe\protocol\CommandRequestPacket;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
-use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
use pocketmine\network\mcpe\protocol\CraftingEventPacket;
+use pocketmine\network\mcpe\protocol\EmotePacket;
use pocketmine\network\mcpe\protocol\InteractPacket;
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
use pocketmine\network\mcpe\protocol\LabTablePacket;
+use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacketV1;
use pocketmine\network\mcpe\protocol\MapInfoRequestPacket;
@@ -71,6 +76,7 @@ use pocketmine\network\mcpe\protocol\ModalFormResponsePacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\NetworkStackLatencyPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
+use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
use pocketmine\network\mcpe\protocol\PlayerHotbarPacket;
use pocketmine\network\mcpe\protocol\PlayerInputPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
@@ -82,6 +88,10 @@ use pocketmine\network\mcpe\protocol\ShowCreditsPacket;
use pocketmine\network\mcpe\protocol\SpawnExperienceOrbPacket;
use pocketmine\network\mcpe\protocol\SubClientLoginPacket;
use pocketmine\network\mcpe\protocol\TextPacket;
+use pocketmine\network\mcpe\protocol\types\ActorEvent;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
+use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
+use pocketmine\network\mcpe\protocol\types\entity\FloatMetadataProperty;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
@@ -90,33 +100,42 @@ use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\network\mcpe\protocol\types\inventory\UseItemOnEntityTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData;
-use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
+use pocketmine\network\mcpe\protocol\types\PlayerAction;
+use pocketmine\network\mcpe\protocol\types\PlayerAuthInputFlags;
+use pocketmine\network\mcpe\protocol\types\PlayerBlockActionStopBreak;
+use pocketmine\network\mcpe\protocol\types\PlayerBlockActionWithBlockInfo;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
-use function array_key_exists;
+use pocketmine\utils\Limits;
+use pocketmine\utils\TextFormat;
+use pocketmine\world\format\Chunk;
use function array_push;
use function base64_encode;
use function count;
use function fmod;
use function implode;
+use function in_array;
use function is_infinite;
use function is_nan;
use function json_decode;
use function json_encode;
-use function json_last_error_msg;
use function max;
+use function mb_strlen;
use function microtime;
use function preg_match;
+use function sprintf;
use function strlen;
use function strpos;
use function substr;
use function trim;
+use const JSON_THROW_ON_ERROR;
/**
* This handler handles packets related to general gameplay.
*/
class InGamePacketHandler extends PacketHandler{
+ private const MAX_FORM_RESPONSE_DEPTH = 2; //modal/simple will be 1, custom forms 2 - they will never contain anything other than string|int|float|bool|null
/** @var Player */
private $player;
@@ -134,15 +153,6 @@ class InGamePacketHandler extends PacketHandler{
/** @var bool */
public $forceMoveSync = false;
- /**
- * TODO: HACK! This tracks GUIs for inventories that the server considers "always open" so that the client can't
- * open them twice. (1.16 hack)
- * @var true[]
- * @phpstan-var array
- * @internal
- */
- protected $openHardcodedWindows = [];
-
private InventoryManager $inventoryManager;
public function __construct(Player $player, NetworkSession $session, InventoryManager $inventoryManager){
@@ -160,16 +170,31 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleMovePlayer(MovePlayerPacket $packet) : bool{
- $rawPos = $packet->position;
- foreach([$rawPos->x, $rawPos->y, $rawPos->z, $packet->yaw, $packet->headYaw, $packet->pitch] as $float){
+ //The client sends this every time it lands on the ground, even when using PlayerAuthInputPacket.
+ //Silence the debug spam that this causes.
+ return true;
+ }
+
+ private function resolveOnOffInputFlags(PlayerAuthInputPacket $packet, int $startFlag, int $stopFlag) : ?bool{
+ $enabled = $packet->hasFlag($startFlag);
+ if($enabled !== $packet->hasFlag($stopFlag)){
+ return $enabled;
+ }
+ //neither flag was set, or both were set
+ return null;
+ }
+
+ public function handlePlayerAuthInput(PlayerAuthInputPacket $packet) : bool{
+ $rawPos = $packet->getPosition();
+ foreach([$rawPos->x, $rawPos->y, $rawPos->z, $packet->getYaw(), $packet->getHeadYaw(), $packet->getPitch()] as $float){
if(is_infinite($float) || is_nan($float)){
$this->session->getLogger()->debug("Invalid movement received, contains NAN/INF components");
return false;
}
}
- $yaw = fmod($packet->yaw, 360);
- $pitch = fmod($packet->pitch, 360);
+ $yaw = fmod($packet->getYaw(), 360);
+ $pitch = fmod($packet->getPitch(), 360);
if($yaw < 0){
$yaw += 360;
}
@@ -177,9 +202,9 @@ class InGamePacketHandler extends PacketHandler{
$this->player->setRotation($yaw, $pitch);
$curPos = $this->player->getLocation();
- $newPos = $packet->position->round(4)->subtract(0, 1.62, 0);
+ $newPos = $rawPos->round(4)->subtract(0, 1.62, 0);
- if($this->forceMoveSync and $newPos->distanceSquared($curPos) > 1){ //Tolerate up to 1 block to avoid problems with client-sided physics when spawning in blocks
+ if($this->forceMoveSync && $newPos->distanceSquared($curPos) > 1){ //Tolerate up to 1 block to avoid problems with client-sided physics when spawning in blocks
$this->session->getLogger()->debug("Got outdated pre-teleport movement, received " . $newPos . ", expected " . $curPos);
//Still getting movements from before teleport, ignore them
return false;
@@ -188,9 +213,57 @@ class InGamePacketHandler extends PacketHandler{
// Once we get a movement within a reasonable distance, treat it as a teleport ACK and remove position lock
$this->forceMoveSync = false;
+ $sneaking = $this->resolveOnOffInputFlags($packet, PlayerAuthInputFlags::START_SNEAKING, PlayerAuthInputFlags::STOP_SNEAKING);
+ $sprinting = $this->resolveOnOffInputFlags($packet, PlayerAuthInputFlags::START_SPRINTING, PlayerAuthInputFlags::STOP_SPRINTING);
+ $swimming = $this->resolveOnOffInputFlags($packet, PlayerAuthInputFlags::START_SWIMMING, PlayerAuthInputFlags::STOP_SWIMMING);
+ $gliding = $this->resolveOnOffInputFlags($packet, PlayerAuthInputFlags::START_GLIDING, PlayerAuthInputFlags::STOP_GLIDING);
+ $mismatch =
+ ($sneaking !== null && !$this->player->toggleSneak($sneaking)) |
+ ($sprinting !== null && !$this->player->toggleSprint($sprinting)) |
+ ($swimming !== null && !$this->player->toggleSwim($swimming)) |
+ ($gliding !== null && !$this->player->toggleGlide($gliding));
+ if((bool) $mismatch){
+ $this->player->sendData([$this->player]);
+ }elseif($packet->hasFlag(PlayerAuthInputFlags::STOP_SWIMMING) || $packet->hasFlag(PlayerAuthInputFlags::STOP_GLIDING)){
+ //TODO: HACK! workaround for a client bug where the AABB doesn't change back properly when stopping swimming or gliding
+ $this->player->sendData([$this->player], [
+ EntityMetadataProperties::BOUNDING_BOX_HEIGHT => new FloatMetadataProperty($this->player->getSize()->getHeight())
+ ]);
+ }
+
+ if($packet->hasFlag(PlayerAuthInputFlags::START_JUMPING)){
+ $this->player->jump();
+ }
+
+ //TODO: this packet has WAYYYYY more useful information that we're not using
$this->player->handleMovement($newPos);
- return true;
+ $packetHandled = true;
+
+ $useItemTransaction = $packet->getItemInteractionData();
+ if($useItemTransaction !== null && !$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
+ $packetHandled = false;
+ $this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
+ }
+
+ $blockActions = $packet->getBlockActions();
+ if($blockActions !== null){
+ foreach($blockActions as $k => $blockAction){
+ $actionHandled = false;
+ if($blockAction instanceof PlayerBlockActionStopBreak){
+ $actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), new BlockPosition(0, 0, 0), Facing::DOWN);
+ }elseif($blockAction instanceof PlayerBlockActionWithBlockInfo){
+ $actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), $blockAction->getBlockPosition(), $blockAction->getFace());
+ }
+
+ if(!$actionHandled){
+ $packetHandled = false;
+ $this->session->getLogger()->debug("Unhandled player block action at offset $k in PlayerAuthInputPacket");
+ }
+ }
+ }
+
+ return $packetHandled;
}
public function handleLevelSoundEventPacketV1(LevelSoundEventPacketV1 $packet) : bool{
@@ -198,14 +271,14 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleActorEvent(ActorEventPacket $packet) : bool{
- if($packet->entityRuntimeId !== $this->player->getId()){
+ if($packet->actorRuntimeId !== $this->player->getId()){
//TODO HACK: EATING_ITEM is sent back to the server when the server sends it for other players (1.14 bug, maybe earlier)
- return $packet->event === ActorEventPacket::EATING_ITEM;
+ return $packet->actorRuntimeId === ActorEvent::EATING_ITEM;
}
- $this->player->doCloseInventory();
+ $this->player->removeCurrentWindow();
- switch($packet->event){
- case ActorEventPacket::EATING_ITEM: //TODO: ignore this and handle it server-side
+ switch($packet->eventId){
+ case ActorEvent::EATING_ITEM: //TODO: ignore this and handle it server-side
$item = $this->player->getInventory()->getItemInHand();
if($item->isNull()){
return false;
@@ -251,11 +324,11 @@ class InGamePacketHandler extends PacketHandler{
foreach($data->getActions() as $networkInventoryAction){
if(
(
- $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO and (
- $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT or
+ $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO && (
+ $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT ||
$networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT
)
- ) or (
+ ) || (
$this->craftingTransaction !== null &&
!$networkInventoryAction->oldItem->getItemStack()->equals($networkInventoryAction->newItem->getItemStack()) &&
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER &&
@@ -271,8 +344,8 @@ class InGamePacketHandler extends PacketHandler{
if($action !== null){
$actions[] = $action;
}
- }catch(\UnexpectedValueException $e){
- $this->session->getLogger()->debug("Unhandled inventory action: " . $e->getMessage());
+ }catch(TypeConversionException $e){
+ $this->session->getLogger()->debug("Error unpacking inventory action: " . $e->getMessage());
return false;
}
}
@@ -294,6 +367,7 @@ class InGamePacketHandler extends PacketHandler{
//all of the parts before we can execute it
return true;
}
+ $this->player->setUsingItem(false);
try{
$this->inventoryManager->onTransactionStart($this->craftingTransaction);
$this->craftingTransaction->execute();
@@ -304,14 +378,6 @@ class InGamePacketHandler extends PacketHandler{
foreach($this->craftingTransaction->getInventories() as $inventory){
$this->inventoryManager->syncContents($inventory);
}
- /*
- * TODO: HACK!
- * we can't resend the contents of the crafting window, so we force the client to close it instead.
- * So people don't whine about messy desync issues when someone cancels CraftItemEvent, or when a crafting
- * transaction goes wrong.
- */
- $this->session->sendDataPacket(ContainerClosePacket::create(InventoryManager::HARDCODED_CRAFTING_GRID_WINDOW_ID, true));
-
return false;
}finally{
$this->craftingTransaction = null;
@@ -329,6 +395,7 @@ class InGamePacketHandler extends PacketHandler{
return true;
}
+ $this->player->setUsingItem(false);
$transaction = new InventoryTransaction($this->player, $actions);
$this->inventoryManager->onTransactionStart($transaction);
try{
@@ -355,12 +422,12 @@ class InGamePacketHandler extends PacketHandler{
switch($data->getActionType()){
case UseItemTransactionData::ACTION_CLICK_BLOCK:
//TODO: start hack for client spam bug
- $clickPos = $data->getClickPos();
- $spamBug = ($this->lastRightClickData !== null and
- microtime(true) - $this->lastRightClickTime < 0.1 and //100ms
- $this->lastRightClickData->getPlayerPos()->distanceSquared($data->getPlayerPos()) < 0.00001 and
- $this->lastRightClickData->getBlockPos()->equals($data->getBlockPos()) and
- $this->lastRightClickData->getClickPos()->distanceSquared($clickPos) < 0.00001 //signature spam bug has 0 distance, but allow some error
+ $clickPos = $data->getClickPosition();
+ $spamBug = ($this->lastRightClickData !== null &&
+ microtime(true) - $this->lastRightClickTime < 0.1 && //100ms
+ $this->lastRightClickData->getPlayerPosition()->distanceSquared($data->getPlayerPosition()) < 0.00001 &&
+ $this->lastRightClickData->getBlockPosition()->equals($data->getBlockPosition()) &&
+ $this->lastRightClickData->getClickPosition()->distanceSquared($clickPos) < 0.00001 //signature spam bug has 0 distance, but allow some error
);
//get rid of continued spam if the player clicks and holds right-click
$this->lastRightClickData = $data;
@@ -370,32 +437,26 @@ class InGamePacketHandler extends PacketHandler{
}
//TODO: end hack for client spam bug
- $blockPos = $data->getBlockPos();
- if(!$this->player->interactBlock($blockPos, $data->getFace(), $clickPos)){
- $this->onFailedBlockAction($blockPos, $data->getFace());
- }elseif(
- !array_key_exists($windowId = InventoryManager::HARDCODED_CRAFTING_GRID_WINDOW_ID, $this->openHardcodedWindows) &&
- $this->player->getCraftingGrid()->getGridWidth() === CraftingGrid::SIZE_BIG
- ){
- //TODO: HACK! crafting grid doesn't fit very well into the current PM container system, so this hack
- //allows it to carry on working approximately the same way as it did in 1.14
- $this->openHardcodedWindows[$windowId] = true;
- $this->session->sendDataPacket(ContainerOpenPacket::blockInvVec3(
- InventoryManager::HARDCODED_CRAFTING_GRID_WINDOW_ID,
- WindowTypes::WORKBENCH,
- $blockPos
- ));
+ self::validateFacing($data->getFace());
+
+ $blockPos = $data->getBlockPosition();
+ $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
+ if(!$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos)){
+ $this->onFailedBlockAction($vBlockPos, $data->getFace());
}
return true;
case UseItemTransactionData::ACTION_BREAK_BLOCK:
- $blockPos = $data->getBlockPos();
- if(!$this->player->breakBlock($blockPos)){
- $this->onFailedBlockAction($blockPos, null);
+ $blockPos = $data->getBlockPosition();
+ $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
+ if(!$this->player->breakBlock($vBlockPos)){
+ $this->onFailedBlockAction($vBlockPos, null);
}
return true;
case UseItemTransactionData::ACTION_CLICK_AIR:
if($this->player->isUsingItem()){
if(!$this->player->consumeHeldItem()){
+ $hungerAttr = $this->player->getAttributeMap()->get(Attribute::HUNGER) ?? throw new AssumptionFailedError();
+ $hungerAttr->markSynchronized(false);
$this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex());
}
return true;
@@ -409,6 +470,15 @@ class InGamePacketHandler extends PacketHandler{
return false;
}
+ /**
+ * @throws PacketHandlingException
+ */
+ private static function validateFacing(int $facing) : void{
+ if(!in_array($facing, Facing::ALL, true)){
+ throw new PacketHandlingException("Invalid facing value $facing");
+ }
+ }
+
/**
* Internal function used to execute rollbacks when an action fails on a block.
*/
@@ -431,7 +501,7 @@ class InGamePacketHandler extends PacketHandler{
}
private function handleUseItemOnEntityTransaction(UseItemOnEntityTransactionData $data) : bool{
- $target = $this->player->getWorld()->getEntity($data->getEntityRuntimeId());
+ $target = $this->player->getWorld()->getEntity($data->getActorRuntimeId());
if($target === null){
return false;
}
@@ -441,7 +511,7 @@ class InGamePacketHandler extends PacketHandler{
//TODO: use transactiondata for rollbacks here
switch($data->getActionType()){
case UseItemOnEntityTransactionData::ACTION_INTERACT:
- if(!$this->player->interactEntity($target, $data->getClickPos())){
+ if(!$this->player->interactEntity($target, $data->getClickPosition())){
$this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex());
}
return true;
@@ -497,30 +567,19 @@ class InGamePacketHandler extends PacketHandler{
//TODO: implement handling for this where it matters
return true;
}
- $target = $this->player->getWorld()->getEntity($packet->target);
+ $target = $this->player->getWorld()->getEntity($packet->targetActorRuntimeId);
if($target === null){
return false;
}
- if(
- $packet->action === InteractPacket::ACTION_OPEN_INVENTORY && $target === $this->player &&
- !array_key_exists($windowId = InventoryManager::HARDCODED_INVENTORY_WINDOW_ID, $this->openHardcodedWindows)
- ){
- //TODO: HACK! this restores 1.14ish behaviour, but this should be able to be listened to and
- //controlled by plugins. However, the player is always a subscriber to their own inventory so it
- //doesn't integrate well with the regular container system right now.
- $this->openHardcodedWindows[$windowId] = true;
- $this->session->sendDataPacket(ContainerOpenPacket::entityInv(
- InventoryManager::HARDCODED_INVENTORY_WINDOW_ID,
- WindowTypes::INVENTORY,
- $this->player->getId()
- ));
+ if($packet->action === InteractPacket::ACTION_OPEN_INVENTORY && $target === $this->player){
+ $this->inventoryManager->onClientOpenMainInventory();
return true;
}
return false; //TODO
}
public function handleBlockPickRequest(BlockPickRequestPacket $packet) : bool{
- return $this->player->pickBlock(new Vector3($packet->blockX, $packet->blockY, $packet->blockZ), $packet->addUserData);
+ return $this->player->pickBlock(new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()), $packet->addUserData);
}
public function handleActorPickRequest(ActorPickRequestPacket $packet) : bool{
@@ -528,67 +587,42 @@ class InGamePacketHandler extends PacketHandler{
}
public function handlePlayerAction(PlayerActionPacket $packet) : bool{
- $pos = new Vector3($packet->x, $packet->y, $packet->z);
+ return $this->handlePlayerActionFromData($packet->action, $packet->blockPosition, $packet->face);
+ }
- switch($packet->action){
- case PlayerActionPacket::ACTION_START_BREAK:
- if(!$this->player->attackBlock($pos, $packet->face)){
- $this->onFailedBlockAction($pos, $packet->face);
+ private function handlePlayerActionFromData(int $action, BlockPosition $blockPosition, int $face) : bool{
+ $pos = new Vector3($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ());
+
+ switch($action){
+ case PlayerAction::START_BREAK:
+ self::validateFacing($face);
+ if(!$this->player->attackBlock($pos, $face)){
+ $this->onFailedBlockAction($pos, $face);
}
break;
- case PlayerActionPacket::ACTION_ABORT_BREAK:
- case PlayerActionPacket::ACTION_STOP_BREAK:
+ case PlayerAction::ABORT_BREAK:
+ case PlayerAction::STOP_BREAK:
$this->player->stopBreakBlock($pos);
break;
- case PlayerActionPacket::ACTION_START_SLEEPING:
+ case PlayerAction::START_SLEEPING:
//unused
break;
- case PlayerActionPacket::ACTION_STOP_SLEEPING:
+ case PlayerAction::STOP_SLEEPING:
$this->player->stopSleep();
break;
- case PlayerActionPacket::ACTION_JUMP:
- $this->player->jump();
- return true;
- case PlayerActionPacket::ACTION_START_SPRINT:
- if(!$this->player->toggleSprint(true)){
- $this->player->sendData([$this->player]);
- }
- return true;
- case PlayerActionPacket::ACTION_STOP_SPRINT:
- if(!$this->player->toggleSprint(false)){
- $this->player->sendData([$this->player]);
- }
- return true;
- case PlayerActionPacket::ACTION_START_SNEAK:
- if(!$this->player->toggleSneak(true)){
- $this->player->sendData([$this->player]);
- }
- return true;
- case PlayerActionPacket::ACTION_STOP_SNEAK:
- if(!$this->player->toggleSneak(false)){
- $this->player->sendData([$this->player]);
- }
- return true;
- case PlayerActionPacket::ACTION_START_GLIDE:
- case PlayerActionPacket::ACTION_STOP_GLIDE:
- break; //TODO
- case PlayerActionPacket::ACTION_CRACK_BREAK:
- $this->player->continueBreakBlock($pos, $packet->face);
+ case PlayerAction::CRACK_BREAK:
+ self::validateFacing($face);
+ $this->player->continueBreakBlock($pos, $face);
break;
- case PlayerActionPacket::ACTION_START_SWIMMING:
- break; //TODO
- case PlayerActionPacket::ACTION_STOP_SWIMMING:
- //TODO: handle this when it doesn't spam every damn tick (yet another spam bug!!)
+ case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now)
break;
- case PlayerActionPacket::ACTION_INTERACT_BLOCK: //TODO: ignored (for now)
- break;
- case PlayerActionPacket::ACTION_CREATIVE_PLAYER_DESTROY_BLOCK:
+ case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//TODO: do we need to handle this?
break;
default:
- $this->session->getLogger()->debug("Unhandled/unknown player action type " . $packet->action);
+ $this->session->getLogger()->debug("Unhandled/unknown player action type " . $action);
return false;
}
@@ -606,15 +640,7 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleContainerClose(ContainerClosePacket $packet) : bool{
- $this->player->doCloseInventory();
-
- if(array_key_exists($packet->windowId, $this->openHardcodedWindows)){
- unset($this->openHardcodedWindows[$packet->windowId]);
- }else{
- $this->inventoryManager->onClientRemoveWindow($packet->windowId);
- }
-
- $this->session->sendDataPacket(ContainerClosePacket::create($packet->windowId, false));
+ $this->inventoryManager->onClientRemoveWindow($packet->windowId);
return true;
}
@@ -627,7 +653,7 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{
- if($packet->entityUniqueId !== $this->player->getId()){
+ if($packet->targetActorUniqueId !== $this->player->getId()){
return false; //TODO: operators can change other people's permissions using this
}
@@ -647,13 +673,13 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleBlockActorData(BlockActorDataPacket $packet) : bool{
- $pos = new Vector3($packet->x, $packet->y, $packet->z);
+ $pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
if($pos->distanceSquared($this->player->getLocation()) > 10000){
return false;
}
$block = $this->player->getLocation()->getWorld()->getBlock($pos);
- $nbt = $packet->namedtag->getRoot();
+ $nbt = $packet->nbt->getRoot();
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
if($block instanceof BaseSign){
@@ -677,7 +703,7 @@ class InGamePacketHandler extends PacketHandler{
return true;
}
- $this->session->getLogger()->debug("Invalid sign update data: " . base64_encode($packet->namedtag->getEncodedNbt()));
+ $this->session->getLogger()->debug("Invalid sign update data: " . base64_encode($packet->nbt->getEncodedNbt()));
}
return false;
@@ -711,9 +737,10 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleItemFrameDropItem(ItemFrameDropItemPacket $packet) : bool{
- $block = $this->player->getWorld()->getBlockAt($packet->x, $packet->y, $packet->z);
- if($block instanceof ItemFrame and $block->getFramedItem() !== null){
- return $this->player->attackBlock(new Vector3($packet->x, $packet->y, $packet->z), $block->getFacing());
+ $blockPosition = $packet->blockPosition;
+ $block = $this->player->getWorld()->getBlockAt($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ());
+ if($block instanceof ItemFrame && $block->getFramedItem() !== null){
+ return $this->player->attackBlock(new Vector3($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ()), $block->getFacing());
}
return false;
}
@@ -751,6 +778,24 @@ class InGamePacketHandler extends PacketHandler{
return false; //TODO
}
+ /**
+ * @throws PacketHandlingException
+ */
+ private function checkBookText(string $string, string $fieldName, int $softLimit, int $hardLimit, bool &$cancel) : string{
+ if(strlen($string) > $hardLimit){
+ throw new PacketHandlingException(sprintf("Book %s must be at most %d bytes, but have %d bytes", $fieldName, $hardLimit, strlen($string)));
+ }
+
+ $result = TextFormat::clean($string, false);
+ //strlen() is O(1), mb_strlen() is O(n)
+ if(strlen($result) > $softLimit * 4 || mb_strlen($result, 'UTF-8') > $softLimit){
+ $cancel = true;
+ $this->session->getLogger()->debug("Cancelled book edit due to $fieldName exceeded soft limit of $softLimit chars");
+ }
+
+ return $result;
+ }
+
public function handleBookEdit(BookEditPacket $packet) : bool{
//TODO: break this up into book API things
$oldBook = $this->player->getInventory()->getItem($packet->inventorySlot);
@@ -760,10 +805,11 @@ class InGamePacketHandler extends PacketHandler{
$newBook = clone $oldBook;
$modifiedPages = [];
-
+ $cancel = false;
switch($packet->type){
case BookEditPacket::TYPE_REPLACE_PAGE:
- $newBook->setPageText($packet->pageNumber, $packet->text);
+ $text = self::checkBookText($packet->text, "page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
+ $newBook->setPageText($packet->pageNumber, $text);
$modifiedPages[] = $packet->pageNumber;
break;
case BookEditPacket::TYPE_ADD_PAGE:
@@ -772,7 +818,8 @@ class InGamePacketHandler extends PacketHandler{
//TODO: the client can send insert-before actions on trailing client-side pages which cause odd behaviour on the server
return false;
}
- $newBook->insertPage($packet->pageNumber, $packet->text);
+ $text = self::checkBookText($packet->text, "page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
+ $newBook->insertPage($packet->pageNumber, $text);
$modifiedPages[] = $packet->pageNumber;
break;
case BookEditPacket::TYPE_DELETE_PAGE:
@@ -783,7 +830,7 @@ class InGamePacketHandler extends PacketHandler{
$modifiedPages[] = $packet->pageNumber;
break;
case BookEditPacket::TYPE_SWAP_PAGES:
- if(!$newBook->pageExists($packet->pageNumber) or !$newBook->pageExists($packet->secondaryPageNumber)){
+ if(!$newBook->pageExists($packet->pageNumber) || !$newBook->pageExists($packet->secondaryPageNumber)){
//the client will create pages on its own without telling us until it tries to switch them
$newBook->addPage(max($packet->pageNumber, $packet->secondaryPageNumber));
}
@@ -791,18 +838,46 @@ class InGamePacketHandler extends PacketHandler{
$modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber];
break;
case BookEditPacket::TYPE_SIGN_BOOK:
- /** @var WrittenBook $newBook */
+ $title = self::checkBookText($packet->title, "title", 16, Limits::INT16_MAX, $cancel);
+ //this one doesn't have a limit in vanilla, so we have to improvise
+ $author = self::checkBookText($packet->author, "author", 256, Limits::INT16_MAX, $cancel);
+
$newBook = VanillaItems::WRITTEN_BOOK()
->setPages($oldBook->getPages())
- ->setAuthor($packet->author)
- ->setTitle($packet->title)
+ ->setAuthor($author)
+ ->setTitle($title)
->setGeneration(WrittenBook::GENERATION_ORIGINAL);
break;
default:
return false;
}
- $event = new PlayerEditBookEvent($this->player, $oldBook, $newBook, $packet->type, $modifiedPages);
+ //for redundancy, in case of protocol changes, we don't want to pass these directly
+ $action = match($packet->type){
+ BookEditPacket::TYPE_REPLACE_PAGE => PlayerEditBookEvent::ACTION_REPLACE_PAGE,
+ BookEditPacket::TYPE_ADD_PAGE => PlayerEditBookEvent::ACTION_ADD_PAGE,
+ BookEditPacket::TYPE_DELETE_PAGE => PlayerEditBookEvent::ACTION_DELETE_PAGE,
+ BookEditPacket::TYPE_SWAP_PAGES => PlayerEditBookEvent::ACTION_SWAP_PAGES,
+ BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK,
+ default => throw new AssumptionFailedError("We already filtered unknown types in the switch above")
+ };
+
+ /*
+ * Plugins may have created books with more than 50 pages; we allow plugins to do this, but not players.
+ * Don't allow the page count to grow past 50, but allow deleting, swapping or altering text of existing pages.
+ */
+ $oldPageCount = count($oldBook->getPages());
+ $newPageCount = count($newBook->getPages());
+ if(($newPageCount > $oldPageCount && $newPageCount > 50)){
+ $this->session->getLogger()->debug("Cancelled book edit due to adding too many pages (new page count would be $newPageCount)");
+ $cancel = true;
+ }
+
+ $event = new PlayerEditBookEvent($this->player, $oldBook, $newBook, $action, $modifiedPages);
+ if($cancel){
+ $event->cancel();
+ }
+
$event->call();
if($event->isCancelled()){
return true;
@@ -830,7 +905,7 @@ class InGamePacketHandler extends PacketHandler{
$newParts = [];
$inQuotes = false;
for($i = 0, $len = strlen($raw); $i <= $len; ++$i){
- if($i === $len or ($raw[$i] === "," and !$inQuotes)){
+ if($i === $len || ($raw[$i] === "," && !$inQuotes)){
$part = substr($raw, $lastComma + 1, $i - ($lastComma + 1));
if(trim($part) === ""){ //regular parts will have quotes or something else that makes them non-empty
$part = '""';
@@ -851,14 +926,18 @@ class InGamePacketHandler extends PacketHandler{
}
$fixed = "[" . implode(",", $newParts) . "]";
- if(($ret = json_decode($fixed, $assoc)) === null){
- throw new \InvalidArgumentException("Failed to fix JSON: " . json_last_error_msg() . "(original: $json, modified: $fixed)");
+ try{
+ return json_decode($fixed, $assoc, self::MAX_FORM_RESPONSE_DEPTH, JSON_THROW_ON_ERROR);
+ }catch(\JsonException $e){
+ throw PacketHandlingException::wrap($e, "Failed to fix JSON (original: $json, modified: $fixed)");
}
-
- return $ret;
}
- return json_decode($json, $assoc);
+ try{
+ return json_decode($json, $assoc, self::MAX_FORM_RESPONSE_DEPTH, JSON_THROW_ON_ERROR);
+ }catch(\JsonException $e){
+ throw PacketHandlingException::wrap($e);
+ }
}
public function handleServerSettingsRequest(ServerSettingsRequestPacket $packet) : bool{
@@ -869,6 +948,31 @@ class InGamePacketHandler extends PacketHandler{
return false; //TODO
}
+ public function handleLecternUpdate(LecternUpdatePacket $packet) : bool{
+ if($packet->dropBook){
+ //Drop book is handled with an interact event on use item transaction
+ return true;
+ }
+
+ $pos = $packet->blockPosition;
+ $chunkX = $pos->getX() >> Chunk::COORD_BIT_SIZE;
+ $chunkZ = $pos->getZ() >> Chunk::COORD_BIT_SIZE;
+ $world = $this->player->getWorld();
+ if(!$world->isChunkLoaded($chunkX, $chunkZ) || $world->isChunkLocked($chunkX, $chunkZ)){
+ return false;
+ }
+
+ $lectern = $world->getBlockAt($pos->getX(), $pos->getY(), $pos->getZ());
+ if($lectern instanceof Lectern && $this->player->canInteract($lectern->getPosition(), 15)){
+ if(!$lectern->onPageTurn($packet->page)){
+ $this->onFailedBlockAction($lectern->getPosition(), null);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
public function handleNetworkStackLatency(NetworkStackLatencyPacket $packet) : bool{
return true; //TODO: implement this properly - this is here to silence debug spam from MCPE dev builds
}
@@ -882,4 +986,9 @@ class InGamePacketHandler extends PacketHandler{
*/
return true;
}
+
+ public function handleEmote(EmotePacket $packet) : bool{
+ $this->player->emote($packet->getEmoteId());
+ return true;
+ }
}
diff --git a/src/network/mcpe/handler/LoginPacketHandler.php b/src/network/mcpe/handler/LoginPacketHandler.php
index 3ff77e2c4..f2ff2e0df 100644
--- a/src/network/mcpe/handler/LoginPacketHandler.php
+++ b/src/network/mcpe/handler/LoginPacketHandler.php
@@ -145,7 +145,7 @@ class LoginPacketHandler extends PacketHandler{
if(!$this->server->isWhitelisted($playerInfo->getUsername())){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_WHITELISTED, "Server is whitelisted");
}
- if($this->server->getNameBans()->isBanned($playerInfo->getUsername()) or $this->server->getIPBans()->isBanned($this->session->getIp())){
+ if($this->server->getNameBans()->isBanned($playerInfo->getUsername()) || $this->server->getIPBans()->isBanned($this->session->getIp())){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_BANNED, "You are banned");
}
diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php
index 59d204d68..626f7a3eb 100644
--- a/src/network/mcpe/handler/PreSpawnPacketHandler.php
+++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php
@@ -31,9 +31,11 @@ use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\BoolGameRule;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\network\mcpe\protocol\types\Experiments;
+use pocketmine\network\mcpe\protocol\types\LevelSettings;
use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings;
use pocketmine\network\mcpe\protocol\types\PlayerMovementType;
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
@@ -64,39 +66,47 @@ class PreSpawnPacketHandler extends PacketHandler{
}
public function setUp() : void{
- $spawnPosition = $this->player->getSpawn();
$location = $this->player->getLocation();
- $pk = new StartGamePacket();
- $pk->entityUniqueId = $this->player->getId();
- $pk->entityRuntimeId = $this->player->getId();
- $pk->playerGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode());
- $pk->playerPosition = $this->player->getOffsetPosition($location);
- $pk->pitch = $location->pitch;
- $pk->yaw = $location->yaw;
- $pk->seed = -1;
- $pk->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
- $pk->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
- $pk->difficulty = $location->getWorld()->getDifficulty();
- $pk->spawnX = $spawnPosition->getFloorX();
- $pk->spawnY = $spawnPosition->getFloorY();
- $pk->spawnZ = $spawnPosition->getFloorZ();
- $pk->hasAchievementsDisabled = true;
- $pk->time = $location->getWorld()->getTime();
- $pk->eduEditionOffer = 0;
- $pk->rainLevel = 0; //TODO: implement these properly
- $pk->lightningLevel = 0;
- $pk->commandsEnabled = true;
- $pk->gameRules = [
+ $levelSettings = new LevelSettings();
+ $levelSettings->seed = -1;
+ $levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
+ $levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
+ $levelSettings->difficulty = $location->getWorld()->getDifficulty();
+ $levelSettings->spawnPosition = BlockPosition::fromVector3($location->getWorld()->getSpawnLocation());
+ $levelSettings->hasAchievementsDisabled = true;
+ $levelSettings->time = $location->getWorld()->getTime();
+ $levelSettings->eduEditionOffer = 0;
+ $levelSettings->rainLevel = 0; //TODO: implement these properly
+ $levelSettings->lightningLevel = 0;
+ $levelSettings->commandsEnabled = true;
+ $levelSettings->gameRules = [
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
];
- $pk->experiments = new Experiments([], false);
- $pk->levelId = "";
- $pk->worldName = $this->server->getMotd();
- $pk->itemTable = GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(); //TODO: check if this is actually needed
- $pk->playerMovementSettings = new PlayerMovementSettings(PlayerMovementType::LEGACY, 0, false);
- $pk->serverSoftwareVersion = sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true));
- $this->session->sendDataPacket($pk);
+ $levelSettings->experiments = new Experiments([], false);
+
+ $this->session->sendDataPacket(StartGamePacket::create(
+ $this->player->getId(),
+ $this->player->getId(),
+ TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
+ $this->player->getOffsetPosition($location),
+ $location->pitch,
+ $location->yaw,
+ $levelSettings,
+ "",
+ $this->server->getMotd(),
+ "",
+ false,
+ new PlayerMovementSettings(PlayerMovementType::SERVER_AUTHORITATIVE_V1, 0, false),
+ 0,
+ 0,
+ "",
+ false,
+ sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
+ [],
+ 0,
+ GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries()
+ ));
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php
index 5b6d1d7a5..356a95a76 100644
--- a/src/network/mcpe/handler/ResourcePacksPacketHandler.php
+++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php
@@ -25,6 +25,7 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\network\mcpe\NetworkSession;
+use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\ResourcePackChunkDataPacket;
use pocketmine\network\mcpe\protocol\ResourcePackChunkRequestPacket;
use pocketmine\network\mcpe\protocol\ResourcePackClientResponsePacket;
@@ -34,6 +35,7 @@ use pocketmine\network\mcpe\protocol\ResourcePackStackPacket;
use pocketmine\network\mcpe\protocol\types\Experiments;
use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackInfoEntry;
use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackStackEntry;
+use pocketmine\network\mcpe\protocol\types\resourcepacks\ResourcePackType;
use pocketmine\resourcepacks\ResourcePack;
use pocketmine\resourcepacks\ResourcePackManager;
use function array_map;
@@ -113,7 +115,9 @@ class ResourcePacksPacketHandler extends PacketHandler{
self::PACK_CHUNK_SIZE,
(int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE),
$pack->getPackSize(),
- $pack->getSha256()
+ $pack->getSha256(),
+ false,
+ ResourcePackType::RESOURCES //TODO: this might be an addon (not behaviour pack), needed to properly support client-side custom items
));
}
$this->session->getLogger()->debug("Player requested download of " . count($packet->packIds) . " resource packs");
@@ -130,7 +134,7 @@ class ResourcePacksPacketHandler extends PacketHandler{
//we don't force here, because it doesn't have user-facing effects
//but it does have an annoying side-effect when true: it makes
//the client remove its own non-server-supplied resource packs.
- $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], false, new Experiments([], false)));
+ $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], false, ProtocolInfo::MINECRAFT_VERSION_NETWORK, new Experiments([], false)));
$this->session->getLogger()->debug("Applying resource pack stack");
break;
case ResourcePackClientResponsePacket::STATUS_COMPLETED:
@@ -159,7 +163,7 @@ class ResourcePacksPacketHandler extends PacketHandler{
}
$offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE;
- if($offset < 0 or $offset >= $pack->getPackSize()){
+ if($offset < 0 || $offset >= $pack->getPackSize()){
$this->disconnectWithError("Invalid out-of-bounds request for chunk $packet->chunkIndex of $packet->packId: offset $offset, file size " . $pack->getPackSize());
return false;
}
diff --git a/src/network/mcpe/raklib/RakLibInterface.php b/src/network/mcpe/raklib/RakLibInterface.php
index 9653a0d4e..5d34ad352 100644
--- a/src/network/mcpe/raklib/RakLibInterface.php
+++ b/src/network/mcpe/raklib/RakLibInterface.php
@@ -32,11 +32,12 @@ use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\StandardPacketBroadcaster;
use pocketmine\network\Network;
+use pocketmine\network\NetworkInterfaceStartException;
use pocketmine\network\PacketHandlingException;
use pocketmine\Server;
use pocketmine\snooze\SleeperNotifier;
-use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
+use raklib\generic\SocketException;
use raklib\protocol\EncapsulatedPacket;
use raklib\protocol\PacketReliability;
use raklib\server\ipc\RakLibToUserThreadMessageReceiver;
@@ -87,7 +88,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
/** @var PacketBroadcaster */
private $broadcaster;
- public function __construct(Server $server){
+ public function __construct(Server $server, string $ip, int $port, bool $ipV6){
$this->server = $server;
$this->rakServerId = mt_rand(0, PHP_INT_MAX);
@@ -100,7 +101,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$this->server->getLogger(),
$mainToThreadBuffer,
$threadToMainBuffer,
- new InternetAddress($this->server->getIp(), $this->server->getPort(), 4),
+ new InternetAddress($ip, $port, $ipV6 ? 6 : 4),
$this->rakServerId,
$this->server->getConfigGroup()->getPropertyInt("network.max-mtu-size", 1492),
self::MCPE_RAKNET_PROTOCOL_VERSION,
@@ -121,7 +122,11 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
while($this->eventReceiver->handle($this));
});
$this->server->getLogger()->debug("Waiting for RakLib to start...");
- $this->rakLib->startAndWait();
+ try{
+ $this->rakLib->startAndWait();
+ }catch(SocketException $e){
+ throw new NetworkInterfaceStartException($e->getMessage(), 0, $e);
+ }
$this->server->getLogger()->debug("RakLib booted successfully");
}
@@ -133,7 +138,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
if(!$this->rakLib->isRunning()){
$e = $this->rakLib->getCrashInfo();
if($e !== null){
- throw new \RuntimeException("RakLib crashed: $e");
+ throw new \RuntimeException("RakLib crashed: " . $e->makePrettyMessage());
}
throw new \Exception("RakLib Thread crashed without crash information");
}
@@ -175,7 +180,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
public function onPacketReceive(int $sessionId, string $packet) : void{
if(isset($this->sessions[$sessionId])){
- if($packet === "" or $packet[0] !== self::MCPE_RAKNET_PACKET_ID){
+ if($packet === "" || $packet[0] !== self::MCPE_RAKNET_PACKET_ID){
$this->sessions[$sessionId]->getLogger()->debug("Non-FE packet received: " . base64_encode($packet));
return;
}
@@ -192,10 +197,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$logger->error("Bad packet (error ID $errorId): " . $e->getMessage());
//intentionally doesn't use logException, we don't want spammy packet error traces to appear in release mode
- $logger->debug("Origin: " . Filesystem::cleanPath($e->getFile()) . "(" . $e->getLine() . ")");
- foreach(Utils::printableTrace($e->getTrace()) as $frame){
- $logger->debug($frame);
- }
+ $logger->debug(implode("\n", Utils::printableExceptionInfo($e)));
$session->disconnect("Packet processing error (Error ID: $errorId)");
$this->interface->blockAddress($address, 5);
}
diff --git a/src/network/mcpe/raklib/RakLibServer.php b/src/network/mcpe/raklib/RakLibServer.php
index 7591475ee..e7ee44d2c 100644
--- a/src/network/mcpe/raklib/RakLibServer.php
+++ b/src/network/mcpe/raklib/RakLibServer.php
@@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\raklib;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\thread\Thread;
use raklib\generic\Socket;
+use raklib\generic\SocketException;
use raklib\server\ipc\RakLibToUserThreadMessageSender;
use raklib\server\ipc\UserToRakLibThreadMessageReceiver;
use raklib\server\Server;
@@ -68,7 +69,7 @@ class RakLibServer extends Thread{
/** @var SleeperNotifier */
protected $mainThreadNotifier;
- /** @var string|null */
+ /** @var RakLibThreadCrashInfo|null */
public $crashInfo = null;
public function __construct(
@@ -102,24 +103,24 @@ class RakLibServer extends Thread{
* @return void
*/
public function shutdownHandler(){
- if($this->cleanShutdown !== true){
+ if($this->cleanShutdown !== true && $this->crashInfo === null){
$error = error_get_last();
if($error !== null){
$this->logger->emergency("Fatal error: " . $error["message"] . " in " . $error["file"] . " on line " . $error["line"]);
- $this->setCrashInfo($error['message']);
+ $this->setCrashInfo(RakLibThreadCrashInfo::fromLastErrorInfo($error));
}else{
$this->logger->emergency("RakLib shutdown unexpectedly");
}
}
}
- public function getCrashInfo() : ?string{
+ public function getCrashInfo() : ?RakLibThreadCrashInfo{
return $this->crashInfo;
}
- private function setCrashInfo(string $info) : void{
- $this->synchronized(function(string $info) : void{
+ private function setCrashInfo(RakLibThreadCrashInfo $info) : void{
+ $this->synchronized(function(RakLibThreadCrashInfo $info) : void{
$this->crashInfo = $info;
$this->notify();
}, $info);
@@ -128,11 +129,15 @@ class RakLibServer extends Thread{
public function startAndWait(int $options = PTHREADS_INHERIT_NONE) : void{
$this->start($options);
$this->synchronized(function() : void{
- while(!$this->ready and $this->crashInfo === null){
+ while(!$this->ready && $this->crashInfo === null){
$this->wait();
}
- if($this->crashInfo !== null){
- throw new \RuntimeException("RakLib failed to start: $this->crashInfo");
+ $crashInfo = $this->crashInfo;
+ if($crashInfo !== null){
+ if($crashInfo->getClass() === SocketException::class){
+ throw new SocketException($crashInfo->getMessage());
+ }
+ throw new \RuntimeException("RakLib failed to start: " . $crashInfo->makePrettyMessage());
}
});
}
@@ -145,7 +150,12 @@ class RakLibServer extends Thread{
register_shutdown_function([$this, "shutdownHandler"]);
- $socket = new Socket($this->address);
+ try{
+ $socket = new Socket($this->address);
+ }catch(SocketException $e){
+ $this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
+ return;
+ }
$manager = new Server(
$this->serverId,
$this->logger,
@@ -166,7 +176,7 @@ class RakLibServer extends Thread{
$manager->waitShutdown();
$this->cleanShutdown = true;
}catch(\Throwable $e){
- $this->setCrashInfo($e->getMessage());
+ $this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
$this->logger->logException($e);
}
}
diff --git a/src/network/mcpe/raklib/RakLibThreadCrashInfo.php b/src/network/mcpe/raklib/RakLibThreadCrashInfo.php
new file mode 100644
index 000000000..c58f28e3f
--- /dev/null
+++ b/src/network/mcpe/raklib/RakLibThreadCrashInfo.php
@@ -0,0 +1,62 @@
+getMessage(), $e->getFile(), $e->getLine());
+ }
+
+ /**
+ * @phpstan-param array{message: string, file: string, line: int} $info
+ */
+ public static function fromLastErrorInfo(array $info) : self{
+ return new self(null, $info["message"], $info["file"], $info["line"]);
+ }
+
+ /** @return string|null */
+ public function getClass() : ?string{ return $this->class; }
+
+ public function getMessage() : string{ return $this->message; }
+
+ public function getFile() : string{ return $this->file; }
+
+ public function getLine() : int{ return $this->line; }
+
+ public function makePrettyMessage() : string{
+ return sprintf("%s: \"%s\" in %s on line %d", $this->class ?? "Fatal error", $this->message, Filesystem::cleanPath($this->file), $this->line);
+ }
+}
diff --git a/src/network/mcpe/serializer/ChunkSerializer.php b/src/network/mcpe/serializer/ChunkSerializer.php
index fe8253e4c..0fe5d891a 100644
--- a/src/network/mcpe/serializer/ChunkSerializer.php
+++ b/src/network/mcpe/serializer/ChunkSerializer.php
@@ -24,6 +24,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\serializer;
use pocketmine\block\tile\Spawnable;
+use pocketmine\data\bedrock\BiomeIds;
+use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
@@ -32,10 +34,14 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk;
+use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
+use function chr;
use function count;
+use function str_repeat;
final class ChunkSerializer{
+ public const LOWER_PADDING_SIZE = 4;
private function __construct(){
//NOOP
@@ -46,8 +52,8 @@ final class ChunkSerializer{
* Chunks are sent in a stack, so every chunk below the top non-empty one must be sent.
*/
public static function getSubChunkCount(Chunk $chunk) : int{
- for($count = $chunk->getSubChunks()->count(); $count > 0; --$count){
- if($chunk->getSubChunk($count - 1)->isEmptyFast()){
+ for($y = Chunk::MAX_SUBCHUNK_INDEX, $count = count($chunk->getSubChunks()); $y >= Chunk::MIN_SUBCHUNK_INDEX; --$y, --$count){
+ if($chunk->getSubChunk($y)->isEmptyFast()){
continue;
}
return $count;
@@ -58,11 +64,22 @@ final class ChunkSerializer{
public static function serializeFullChunk(Chunk $chunk, RuntimeBlockMapping $blockMapper, PacketSerializerContext $encoderContext, ?string $tiles = null) : string{
$stream = PacketSerializer::encoder($encoderContext);
+
+ //TODO: HACK! fill in fake subchunks to make up for the new negative space client-side
+ for($y = 0; $y < self::LOWER_PADDING_SIZE; $y++){
+ $stream->putByte(8); //subchunk version 8
+ $stream->putByte(0); //0 layers - client will treat this as all-air
+ }
+
$subChunkCount = self::getSubChunkCount($chunk);
- for($y = 0; $y < $subChunkCount; ++$y){
+ for($y = Chunk::MIN_SUBCHUNK_INDEX, $writtenCount = 0; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){
self::serializeSubChunk($chunk->getSubChunk($y), $blockMapper, $stream, false);
}
- $stream->put($chunk->getBiomeIdArray());
+
+ //TODO: right now we don't support 3D natively, so we just 3Dify our 2D biomes so they fill the column
+ $encodedBiomePalette = self::serializeBiomesAsPalette($chunk);
+ $stream->put(str_repeat($encodedBiomePalette, 25));
+
$stream->putByte(0); //border block array count
//Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client.
@@ -116,4 +133,39 @@ final class ChunkSerializer{
return $stream->getBuffer();
}
+
+ private static function serializeBiomesAsPalette(Chunk $chunk) : string{
+ $biomeIdMap = LegacyBiomeIdToStringIdMap::getInstance();
+ $biomePalette = new PalettedBlockArray($chunk->getBiomeId(0, 0));
+ for($x = 0; $x < 16; ++$x){
+ for($z = 0; $z < 16; ++$z){
+ $biomeId = $chunk->getBiomeId($x, $z);
+ if($biomeIdMap->legacyToString($biomeId) === null){
+ //make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
+ $biomeId = BiomeIds::OCEAN;
+ }
+ for($y = 0; $y < 16; ++$y){
+ $biomePalette->set($x, $y, $z, $biomeId);
+ }
+ }
+ }
+
+ $biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
+ $encodedBiomePalette =
+ chr(($biomePaletteBitsPerBlock << 1) | 1) . //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
+ $biomePalette->getWordArray();
+
+ //these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
+ //but since we know they are always unsigned, we can avoid the extra fcall overhead of
+ //zigzag and just shift directly.
+ $biomePaletteArray = $biomePalette->getPalette();
+ if($biomePaletteBitsPerBlock !== 0){
+ $encodedBiomePalette .= Binary::writeUnsignedVarInt(count($biomePaletteArray) << 1);
+ }
+ foreach($biomePaletteArray as $p){
+ $encodedBiomePalette .= Binary::writeUnsignedVarInt($p << 1);
+ }
+
+ return $encodedBiomePalette;
+ }
}
diff --git a/src/network/query/DedicatedQueryNetworkInterface.php b/src/network/query/DedicatedQueryNetworkInterface.php
index d742c9e53..70a021a5a 100644
--- a/src/network/query/DedicatedQueryNetworkInterface.php
+++ b/src/network/query/DedicatedQueryNetworkInterface.php
@@ -34,11 +34,14 @@ use function socket_recvfrom;
use function socket_select;
use function socket_sendto;
use function socket_set_nonblock;
+use function socket_set_option;
use function socket_strerror;
use function strlen;
use function time;
use function trim;
use const AF_INET;
+use const IPPROTO_IPV6;
+use const IPV6_V6ONLY;
use const PHP_INT_MAX;
use const SOCK_DGRAM;
use const SOCKET_EADDRINUSE;
@@ -74,15 +77,18 @@ final class DedicatedQueryNetworkInterface implements AdvancedNetworkInterface{
/** @var string[] */
private $rawPacketPatterns = [];
- public function __construct(string $ip, int $port, \Logger $logger){
+ public function __construct(string $ip, int $port, bool $ipV6, \Logger $logger){
$this->ip = $ip;
$this->port = $port;
$this->logger = $logger;
- $socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+ $socket = @socket_create($ipV6 ? AF_INET6 : AF_INET, SOCK_DGRAM, SOL_UDP);
if($socket === false){
throw new \RuntimeException("Failed to create socket");
}
+ if($ipV6){
+ socket_set_option($socket, IPPROTO_IPV6, IPV6_V6ONLY, 1); //disable linux's cool but annoying ipv4-over-ipv6 network stack
+ }
$this->socket = $socket;
}
diff --git a/src/network/query/QueryHandler.php b/src/network/query/QueryHandler.php
index 1ec8a2277..06b3c5ab0 100644
--- a/src/network/query/QueryHandler.php
+++ b/src/network/query/QueryHandler.php
@@ -27,7 +27,6 @@ declare(strict_types=1);
*/
namespace pocketmine\network\query;
-use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\RawPacketHandler;
use pocketmine\Server;
@@ -57,8 +56,6 @@ class QueryHandler implements RawPacketHandler{
public function __construct(Server $server){
$this->server = $server;
$this->logger = new \PrefixedLogger($this->server->getLogger(), "Query Handler");
- $addr = $this->server->getIp();
- $port = $this->server->getPort();
/*
The Query protocol is built on top of the existing Minecraft PE UDP network stack.
@@ -71,7 +68,6 @@ class QueryHandler implements RawPacketHandler{
$this->regenerateToken();
$this->lastToken = $this->token;
- $this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_query_running($addr, (string) $port)));
}
public function getPattern() : string{
@@ -108,7 +104,7 @@ class QueryHandler implements RawPacketHandler{
return true;
case self::STATISTICS: //Stat
$token = $stream->getInt();
- if($token !== ($t1 = self::getTokenString($this->token, $address)) and $token !== ($t2 = self::getTokenString($this->lastToken, $address))){
+ if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){
$this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2");
return true;
diff --git a/src/network/query/QueryInfo.php b/src/network/query/QueryInfo.php
index b6cb199ca..5275edba7 100644
--- a/src/network/query/QueryInfo.php
+++ b/src/network/query/QueryInfo.php
@@ -204,7 +204,7 @@ final class QueryInfo{
$query = "";
$plist = $this->server_engine;
- if(count($this->plugins) > 0 and $this->listPlugins){
+ if(count($this->plugins) > 0 && $this->listPlugins){
$plist .= ":";
foreach($this->plugins as $p){
$d = $p->getDescription();
@@ -233,7 +233,7 @@ final class QueryInfo{
$query .= $key . "\x00" . $value . "\x00";
}
- foreach($this->extraData as $key => $value){
+ foreach(Utils::stringifyKeys($this->extraData) as $key => $value){
$query .= $key . "\x00" . $value . "\x00";
}
diff --git a/src/network/upnp/UPnP.php b/src/network/upnp/UPnP.php
index c2ed90f6e..1c14cead8 100644
--- a/src/network/upnp/UPnP.php
+++ b/src/network/upnp/UPnP.php
@@ -55,8 +55,8 @@ declare(strict_types=1);
*/
namespace pocketmine\network\upnp;
-use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Internet;
+use pocketmine\utils\Utils;
use function count;
use function libxml_use_internal_errors;
use function parse_url;
@@ -99,13 +99,8 @@ class UPnP{
* @throws UPnPException
*/
public static function getServiceUrl() : string{
- $socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
- if($socket === false){
- throw new AssumptionFailedError("Socket error: " . trim(socket_strerror(socket_last_error())));
- }
- if(!@socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => 3, "usec" => 0])){
- throw new AssumptionFailedError("Socket error: " . trim(socket_strerror(socket_last_error($socket))));
- }
+ $socket = Utils::assumeNotFalse(@socket_create(AF_INET, SOCK_DGRAM, SOL_UDP), fn() => "Socket error: " . trim(socket_strerror(socket_last_error())));
+ Utils::assumeNotFalse(@socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => 3, "usec" => 0]), "Socket error: " . trim(socket_strerror(socket_last_error($socket))));
$contents =
"M-SEARCH * HTTP/1.1\r\n" .
"MX: 2\r\n" .
@@ -171,17 +166,14 @@ class UPnP{
}
libxml_use_internal_errors($defaultInternalError);
$root->registerXPathNamespace("upnp", "urn:schemas-upnp-org:device-1-0");
- $xpathResult = $root->xpath(
+ $xpathResult = Utils::assumeNotFalse($root->xpath(
'//upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:InternetGatewayDevice:1"]' .
'/upnp:deviceList/upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:WANDevice:1"]' .
'/upnp:deviceList/upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:WANConnectionDevice:1"]' .
'/upnp:serviceList/upnp:service[upnp:serviceType="urn:schemas-upnp-org:service:WANIPConnection:1"]' .
'/upnp:controlURL'
- );
- if($xpathResult === false){
- //this should be an array of 0 if there is no matching elements; false indicates a problem with the query itself
- throw new AssumptionFailedError("xpath query should not error here");
- }
+ ), "xpath query is borked");
+
if(count($xpathResult) === 0){
throw new UPnPException("Your router does not support portforwarding");
}
diff --git a/src/network/upnp/UPnPNetworkInterface.php b/src/network/upnp/UPnPNetworkInterface.php
index f261cc401..45e8a1171 100644
--- a/src/network/upnp/UPnPNetworkInterface.php
+++ b/src/network/upnp/UPnPNetworkInterface.php
@@ -25,6 +25,7 @@ namespace pocketmine\network\upnp;
use pocketmine\network\NetworkInterface;
use pocketmine\utils\Internet;
+use pocketmine\utils\InternetException;
final class UPnPNetworkInterface implements NetworkInterface{
@@ -50,12 +51,12 @@ final class UPnPNetworkInterface implements NetworkInterface{
public function start() : void{
$this->logger->info("Attempting to portforward...");
- $this->serviceURL = UPnP::getServiceUrl();
try{
+ $this->serviceURL = UPnP::getServiceUrl();
UPnP::portForward($this->serviceURL, Internet::getInternalIP(), $this->port, $this->port);
$this->logger->info("Forwarded $this->ip:$this->port to external port $this->port");
- }catch(UPnPException $e){
+ }catch(UPnPException | InternetException $e){
$this->logger->error("UPnP portforward failed: " . $e->getMessage());
}
}
diff --git a/src/permission/BanEntry.php b/src/permission/BanEntry.php
index 63858d39c..d0d200eac 100644
--- a/src/permission/BanEntry.php
+++ b/src/permission/BanEntry.php
@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\permission;
-use pocketmine\utils\AssumptionFailedError;
+use pocketmine\utils\Utils;
use function array_shift;
use function count;
use function explode;
@@ -143,8 +143,7 @@ class BanEntry{
private static function parseDate(string $date) : \DateTime{
$datetime = \DateTime::createFromFormat(self::$format, $date);
if(!($datetime instanceof \DateTime)){
- $lastErrors = \DateTime::getLastErrors();
- if($lastErrors === false) throw new AssumptionFailedError("DateTime::getLastErrors() should not be returning false in here");
+ $lastErrors = Utils::assumeNotFalse(\DateTime::getLastErrors(), "DateTime::getLastErrors() should not be returning false in here");
throw new \RuntimeException("Corrupted date/time: " . implode(", ", $lastErrors["errors"]));
}
@@ -169,7 +168,7 @@ class BanEntry{
}
if(count($parts) > 0){
$expire = trim(array_shift($parts));
- if($expire !== "" and strtolower($expire) !== "forever"){
+ if($expire !== "" && strtolower($expire) !== "forever"){
$entry->setExpires(self::parseDate($expire));
}
}
diff --git a/src/permission/DefaultPermissions.php b/src/permission/DefaultPermissions.php
index 371a970db..3145fda58 100644
--- a/src/permission/DefaultPermissions.php
+++ b/src/permission/DefaultPermissions.php
@@ -51,65 +51,56 @@ abstract class DefaultPermissions{
self::registerPermission(new Permission(DefaultPermissionNames::BROADCAST_ADMIN, "Allows the user to receive administrative broadcasts"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::BROADCAST_USER, "Allows the user to receive user broadcasts"), [$everyoneRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ADD, "Allows the user to add a player to the server whitelist"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_REMOVE, "Allows the user to remove a player from the server whitelist"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_RELOAD, "Allows the user to reload the server whitelist"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ENABLE, "Allows the user to enable the server whitelist"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_DISABLE, "Allows the user to disable the server whitelist"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_LIST, "Allows the user to list all players on the server whitelist"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_PLAYER, "Allows the user to ban players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_IP, "Allows the user to ban IP addresses"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_LIST, "Allows the user to list banned players"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER, "Allows the user to unban players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_IP, "Allows the user to unban IP addresses"), [$operatorRoot]);
-
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_PLAYER, "Allows the user to ban players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_OTHER, "Allows the user to clear inventory of other players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_SELF, "Allows the user to clear their own inventory"), [$everyoneRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DEFAULTGAMEMODE, "Allows the user to change the default gamemode"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DIFFICULTY, "Allows the user to change the game difficulty"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DUMPMEMORY, "Allows the user to dump memory contents"), [$consoleRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_EFFECT, "Allows the user to give/take potion effects"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ENCHANT, "Allows the user to enchant items"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GAMEMODE, "Allows the user to change the gamemode of players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GC, "Allows the user to fire garbage collection tasks"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GIVE, "Allows the user to give items to players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_HELP, "Allows the user to view the help menu"), [$everyoneRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KICK, "Allows the user to kick players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_OTHER, "Allows the user to kill other players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_SELF, "Allows the user to commit suicide"), [$everyoneRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_LIST, "Allows the user to list all online players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ME, "Allows the user to perform a chat action"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_OP_GIVE, "Allows the user to give a player operator status"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_OP_TAKE, "Allows the user to take a player's operator status"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_ENABLE, "Allows the user to enable automatic saving"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PARTICLE, "Allows the user to create particle effects"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PLUGINS, "Allows the user to view the list of plugins"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_DISABLE, "Allows the user to disable automatic saving"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_ENABLE, "Allows the user to enable automatic saving"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_PERFORM, "Allows the user to perform a manual save"), [$operatorRoot]);
-
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAY, "Allows the user to talk as the console"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SEED, "Allows the user to view the seed of the world"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SETWORLDSPAWN, "Allows the user to change the world spawn"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SPAWNPOINT, "Allows the user to change player's spawnpoint"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STATUS, "Allows the user to view the server performance"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STOP, "Allows the user to stop the server"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELEPORT, "Allows the user to teleport players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELL, "Allows the user to privately message another player"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_ADD, "Allows the user to fast-forward time"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_QUERY, "Allows the user query the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_SET, "Allows the user to change the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_START, "Allows the user to restart the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_STOP, "Allows the user to stop the time"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_QUERY, "Allows the user query the time"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_SELF, "Allows the user to commit suicide"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_OTHER, "Allows the user to kill other players"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_SELF, "Allows the user to clear their own inventory"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_OTHER, "Allows the user to clear inventory of other players"), [$operatorRoot]);
-
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ME, "Allows the user to perform a chat action"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELL, "Allows the user to privately message another player"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAY, "Allows the user to talk as the console"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GIVE, "Allows the user to give items to players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_EFFECT, "Allows the user to give/take potion effects"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ENCHANT, "Allows the user to enchant items"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PARTICLE, "Allows the user to create particle effects"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELEPORT, "Allows the user to teleport players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KICK, "Allows the user to kick players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STOP, "Allows the user to stop the server"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_LIST, "Allows the user to list all online players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_HELP, "Allows the user to view the help menu"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PLUGINS, "Allows the user to view the list of plugins"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_VERSION, "Allows the user to view the version of the server"), [$everyoneRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GAMEMODE, "Allows the user to change the gamemode of players"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DEFAULTGAMEMODE, "Allows the user to change the default gamemode"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SEED, "Allows the user to view the seed of the world"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STATUS, "Allows the user to view the server performance"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GC, "Allows the user to fire garbage collection tasks"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DUMPMEMORY, "Allows the user to dump memory contents"), [$consoleRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIMINGS, "Allows the user to records timings for all plugin events"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SPAWNPOINT, "Allows the user to change player's spawnpoint"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SETWORLDSPAWN, "Allows the user to change the world spawn"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TRANSFERSERVER, "Allows the user to transfer self to another server"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIMINGS, "Allows the user to record timings to analyse server performance"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TITLE, "Allows the user to send a title to the specified player"), [$operatorRoot]);
- self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DIFFICULTY, "Allows the user to change the game difficulty"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TRANSFERSERVER, "Allows the user to transfer self to another server"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_IP, "Allows the user to unban IP addresses"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER, "Allows the user to unban players"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_VERSION, "Allows the user to view the version of the server"), [$everyoneRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ADD, "Allows the user to add a player to the server whitelist"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_DISABLE, "Allows the user to disable the server whitelist"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ENABLE, "Allows the user to enable the server whitelist"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_LIST, "Allows the user to list all players on the server whitelist"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_RELOAD, "Allows the user to reload the server whitelist"), [$operatorRoot]);
+ self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_REMOVE, "Allows the user to remove a player from the server whitelist"), [$operatorRoot]);
}
}
diff --git a/src/permission/PermissibleInternal.php b/src/permission/PermissibleInternal.php
index c9484085e..23d0b3b0c 100644
--- a/src/permission/PermissibleInternal.php
+++ b/src/permission/PermissibleInternal.php
@@ -27,6 +27,7 @@ use pocketmine\plugin\Plugin;
use pocketmine\plugin\PluginException;
use pocketmine\timings\Timings;
use pocketmine\utils\ObjectSet;
+use pocketmine\utils\Utils;
use function count;
use function spl_object_id;
@@ -113,7 +114,7 @@ class PermissibleInternal implements Permissible{
$result = new PermissionAttachment($plugin);
$this->attachments[spl_object_id($result)] = $result;
- if($name !== null and $value !== null){
+ if($name !== null && $value !== null){
$result->setPermission($name, $value);
}
@@ -143,10 +144,10 @@ class PermissibleInternal implements Permissible{
$oldPermissions = $this->permissions;
$this->permissions = [];
- foreach($this->rootPermissions as $name => $isGranted){
+ foreach(Utils::stringifyKeys($this->rootPermissions) as $name => $isGranted){
$perm = $permManager->getPermission($name);
if($perm === null){
- throw new \InvalidStateException("Unregistered root permission $name");
+ throw new \LogicException("Unregistered root permission $name");
}
$this->permissions[$name] = new PermissionAttachmentInfo($name, null, $isGranted, null);
$permManager->subscribeToPermission($name, $this);
@@ -187,11 +188,12 @@ class PermissibleInternal implements Permissible{
}
/**
- * @param bool[] $children
+ * @param bool[] $children
+ * @phpstan-param array $children
*/
private function calculateChildPermissions(array $children, bool $invert, ?PermissionAttachment $attachment, ?PermissionAttachmentInfo $parent) : void{
$permManager = PermissionManager::getInstance();
- foreach($children as $name => $v){
+ foreach(Utils::stringifyKeys($children) as $name => $v){
$perm = $permManager->getPermission($name);
$value = ($v xor $invert);
$this->permissions[$name] = new PermissionAttachmentInfo($name, $attachment, $value, $parent);
diff --git a/src/permission/PermissionParser.php b/src/permission/PermissionParser.php
index 7115a5536..8aab762cf 100644
--- a/src/permission/PermissionParser.php
+++ b/src/permission/PermissionParser.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\permission;
+use pocketmine\utils\Utils;
use function is_bool;
use function strtolower;
@@ -55,7 +56,7 @@ class PermissionParser{
/**
* @param bool|string $value
*
- * @throws \InvalidArgumentException
+ * @throws PermissionParserException
*/
public static function defaultFromString($value) : string{
if(is_bool($value)){
@@ -70,7 +71,7 @@ class PermissionParser{
return self::DEFAULT_STRING_MAP[$lower];
}
- throw new \InvalidArgumentException("Unknown permission default name \"$value\"");
+ throw new PermissionParserException("Unknown permission default name \"$value\"");
}
/**
@@ -79,17 +80,18 @@ class PermissionParser{
*
* @return Permission[][]
* @phpstan-return array>
+ * @throws PermissionParserException
*/
public static function loadPermissions(array $data, string $default = self::DEFAULT_FALSE) : array{
$result = [];
- foreach($data as $name => $entry){
+ foreach(Utils::stringifyKeys($data) as $name => $entry){
$desc = null;
if(isset($entry["default"])){
$default = PermissionParser::defaultFromString($entry["default"]);
}
if(isset($entry["children"])){
- throw new \InvalidArgumentException("Nested permission declarations are no longer supported. Declare each permission separately.");
+ throw new PermissionParserException("Nested permission declarations are no longer supported. Declare each permission separately.");
}
if(isset($entry["description"])){
diff --git a/src/permission/PermissionParserException.php b/src/permission/PermissionParserException.php
new file mode 100644
index 000000000..a0231f805
--- /dev/null
+++ b/src/permission/PermissionParserException.php
@@ -0,0 +1,31 @@
+
*/
public function selectChunks(int $radius, int $centerX, int $centerZ) : \Generator{
- $radiusSquared = $radius ** 2;
+ for($subRadius = 0; $subRadius < $radius; $subRadius++){
+ $subRadiusSquared = $subRadius ** 2;
+ $nextSubRadiusSquared = ($subRadius + 1) ** 2;
+ $minX = (int) ($subRadius / M_SQRT2);
- for($x = 0; $x < $radius; ++$x){
- for($z = 0; $z <= $x; ++$z){
- if(($x ** 2 + $z ** 2) > $radiusSquared){
- break; //skip to next band
- }
+ $lastZ = 0;
- //If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be.
+ for($x = $subRadius; $x >= $minX; --$x){
+ for($z = $lastZ; $z <= $x; ++$z){
+ $distanceSquared = ($x ** 2 + $z ** 2);
+ if($distanceSquared < $subRadiusSquared){
+ continue;
+ }elseif($distanceSquared >= $nextSubRadiusSquared){
+ break; //skip to next X
+ }
- /* Top right quadrant */
- yield World::chunkHash($centerX + $x, $centerZ + $z);
- /* Top left quadrant */
- yield World::chunkHash($centerX - $x - 1, $centerZ + $z);
- /* Bottom right quadrant */
- yield World::chunkHash($centerX + $x, $centerZ - $z - 1);
- /* Bottom left quadrant */
- yield World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
+ $lastZ = $z;
+ //If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be.
- if($x !== $z){
- /* Top right quadrant mirror */
- yield World::chunkHash($centerX + $z, $centerZ + $x);
- /* Top left quadrant mirror */
- yield World::chunkHash($centerX - $z - 1, $centerZ + $x);
- /* Bottom right quadrant mirror */
- yield World::chunkHash($centerX + $z, $centerZ - $x - 1);
- /* Bottom left quadrant mirror */
- yield World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
+ /* Top right quadrant */
+ yield World::chunkHash($centerX + $x, $centerZ + $z);
+ /* Top left quadrant */
+ yield World::chunkHash($centerX - $x - 1, $centerZ + $z);
+ /* Bottom right quadrant */
+ yield World::chunkHash($centerX + $x, $centerZ - $z - 1);
+ /* Bottom left quadrant */
+ yield World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
+
+ if($x !== $z){
+ /* Top right quadrant mirror */
+ yield World::chunkHash($centerX + $z, $centerZ + $x);
+ /* Top left quadrant mirror */
+ yield World::chunkHash($centerX - $z - 1, $centerZ + $x);
+ /* Bottom right quadrant mirror */
+ yield World::chunkHash($centerX + $z, $centerZ - $x - 1);
+ /* Bottom left quadrant mirror */
+ yield World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
+ }
}
}
}
diff --git a/src/player/OfflinePlayer.php b/src/player/OfflinePlayer.php
index a90047b5d..1a9955413 100644
--- a/src/player/OfflinePlayer.php
+++ b/src/player/OfflinePlayer.php
@@ -43,11 +43,11 @@ class OfflinePlayer implements IPlayer{
}
public function getFirstPlayed() : ?int{
- return ($this->namedtag !== null and ($firstPlayedTag = $this->namedtag->getTag("firstPlayed")) instanceof LongTag) ? $firstPlayedTag->getValue() : null;
+ return ($this->namedtag !== null && ($firstPlayedTag = $this->namedtag->getTag("firstPlayed")) instanceof LongTag) ? $firstPlayedTag->getValue() : null;
}
public function getLastPlayed() : ?int{
- return ($this->namedtag !== null and ($lastPlayedTag = $this->namedtag->getTag("lastPlayed")) instanceof LongTag) ? $lastPlayedTag->getValue() : null;
+ return ($this->namedtag !== null && ($lastPlayedTag = $this->namedtag->getTag("lastPlayed")) instanceof LongTag) ? $lastPlayedTag->getValue() : null;
}
public function hasPlayedBefore() : bool{
diff --git a/src/player/Player.php b/src/player/Player.php
index 14a6b205a..7cf4388d1 100644
--- a/src/player/Player.php
+++ b/src/player/Player.php
@@ -53,6 +53,7 @@ use pocketmine\event\player\PlayerChatEvent;
use pocketmine\event\player\PlayerCommandPreprocessEvent;
use pocketmine\event\player\PlayerDeathEvent;
use pocketmine\event\player\PlayerDisplayNameChangeEvent;
+use pocketmine\event\player\PlayerEmoteEvent;
use pocketmine\event\player\PlayerEntityInteractEvent;
use pocketmine\event\player\PlayerExhaustEvent;
use pocketmine\event\player\PlayerGameModeChangeEvent;
@@ -63,18 +64,23 @@ use pocketmine\event\player\PlayerItemUseEvent;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerJumpEvent;
use pocketmine\event\player\PlayerKickEvent;
-use pocketmine\event\player\PlayerLoginEvent;
use pocketmine\event\player\PlayerMoveEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\event\player\PlayerRespawnEvent;
use pocketmine\event\player\PlayerToggleFlightEvent;
+use pocketmine\event\player\PlayerToggleGlideEvent;
use pocketmine\event\player\PlayerToggleSneakEvent;
use pocketmine\event\player\PlayerToggleSprintEvent;
+use pocketmine\event\player\PlayerToggleSwimEvent;
use pocketmine\event\player\PlayerTransferEvent;
+use pocketmine\event\player\PlayerViewDistanceChangeEvent;
use pocketmine\form\Form;
use pocketmine\form\FormValidationException;
+use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
+use pocketmine\inventory\PlayerCraftingInventory;
use pocketmine\inventory\PlayerCursorInventory;
+use pocketmine\inventory\TemporaryInventory;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\TransactionBuilderInventory;
@@ -98,6 +104,7 @@ use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\AnimatePacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\SetActorMotionPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
@@ -130,10 +137,10 @@ use function floor;
use function get_class;
use function is_int;
use function max;
+use function mb_strlen;
use function microtime;
use function min;
use function preg_match;
-use function round;
use function spl_object_id;
use function sqrt;
use function strlen;
@@ -154,6 +161,18 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
private const MOVES_PER_TICK = 2;
private const MOVE_BACKLOG_SIZE = 100 * self::MOVES_PER_TICK; //100 ticks backlog (5 seconds)
+ /** Max length of a chat message (UTF-8 codepoints, not bytes) */
+ private const MAX_CHAT_CHAR_LENGTH = 512;
+ /**
+ * Max length of a chat message in bytes. This is a theoretical maximum (if every character was 4 bytes).
+ * Since mb_strlen() is O(n), it gets very slow with large messages. Checking byte length with strlen() is O(1) and
+ * is a useful heuristic to filter out oversized messages.
+ */
+ private const MAX_CHAT_BYTE_LENGTH = self::MAX_CHAT_CHAR_LENGTH * 4;
+ private const MAX_REACH_DISTANCE_CREATIVE = 13;
+ private const MAX_REACH_DISTANCE_SURVIVAL = 7;
+ private const MAX_REACH_DISTANCE_ENTITY_INTERACTION = 8;
+
/**
* Validates the given username.
*/
@@ -164,7 +183,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$lname = strtolower($name);
$len = strlen($name);
- return $lname !== "rcon" and $lname !== "console" and $len >= 1 and $len <= 16 and preg_match("/[^A-Za-z0-9_ ]/", $name) === 0;
+ return $lname !== "rcon" && $lname !== "console" && $len >= 1 && $len <= 16 && preg_match("/[^A-Za-z0-9_ ]/", $name) === 0;
}
protected ?NetworkSession $networkSession;
@@ -181,7 +200,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/** @var Inventory[] */
protected array $permanentWindows = [];
protected PlayerCursorInventory $cursorInventory;
- protected CraftingGrid $craftingGrid;
+ protected PlayerCraftingInventory $craftingGrid;
protected int $messageCounter = 2;
@@ -194,6 +213,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @phpstan-var array
*/
protected array $usedChunks = [];
+ /**
+ * @var true[]
+ * @phpstan-var array
+ */
+ private array $activeChunkGenerationRequests = [];
/**
* @var true[] chunkHash => dummy
* @phpstan-var array
@@ -226,6 +250,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
//TODO: Abilities
protected bool $autoJump = true;
protected bool $allowFlight = false;
+ protected bool $blockCollision = true;
protected bool $flying = false;
protected ?int $lineHeight = null;
@@ -235,6 +260,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/** @var int[] ID => ticks map */
protected array $usedItemsCooldown = [];
+ private int $lastEmoteTick = 0;
+
protected int $formIdCounter = 0;
/** @var Form[] */
protected array $forms = [];
@@ -272,32 +299,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$world = $spawnLocation->getWorld();
//load the spawn chunk so we can see the terrain
- $world->registerChunkLoader($this->chunkLoader, $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE, true);
- $world->registerChunkListener($this, $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE);
- $this->usedChunks[World::chunkHash($spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE)] = UsedChunkStatus::NEEDED();
+ $xSpawnChunk = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
+ $zSpawnChunk = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
+ $world->registerChunkLoader($this->chunkLoader, $xSpawnChunk, $zSpawnChunk, true);
+ $world->registerChunkListener($this, $xSpawnChunk, $zSpawnChunk);
+ $this->usedChunks[World::chunkHash($xSpawnChunk, $zSpawnChunk)] = UsedChunkStatus::NEEDED();
parent::__construct($spawnLocation, $this->playerInfo->getSkin(), $namedtag);
-
- $ev = new PlayerLoginEvent($this, "Plugin reason");
- $ev->call();
- if($ev->isCancelled() or !$this->isConnected()){
- $this->disconnect($ev->getKickMessage());
-
- return;
- }
-
- $this->server->getLogger()->info($this->getServer()->getLanguage()->translate(KnownTranslationFactory::pocketmine_player_logIn(
- TextFormat::AQUA . $this->username . TextFormat::WHITE,
- $session->getIp(),
- (string) $session->getPort(),
- (string) $this->id,
- $this->getWorld()->getDisplayName(),
- (string) round($this->location->x, 4),
- (string) round($this->location->y, 4),
- (string) round($this->location->z, 4)
- )));
-
- $this->server->addOnlinePlayer($this);
}
protected function initHumanData(CompoundTag $nbt) : void{
@@ -308,10 +316,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
parent::initEntity($nbt);
$this->addDefaultWindows();
+ $this->inventory->getListeners()->add(new CallbackInventoryListener(
+ function(Inventory $unused, int $slot) : void{
+ if($slot === $this->inventory->getHeldItemIndex()){
+ $this->setUsingItem(false);
+ }
+ },
+ function() : void{
+ $this->setUsingItem(false);
+ }
+ ));
+
$this->firstPlayed = $nbt->getLong("firstPlayed", $now = (int) (microtime(true) * 1000));
$this->lastPlayed = $nbt->getLong("lastPlayed", $now);
- if(!$this->server->getForceGamemode() and ($gameModeTag = $nbt->getTag("playerGameType")) instanceof IntTag){
+ if(!$this->server->getForceGamemode() && ($gameModeTag = $nbt->getTag("playerGameType")) instanceof IntTag){
$this->internalSetGameMode(GameModeIdMap::getInstance()->fromId($gameModeTag->getValue()) ?? GameMode::SURVIVAL()); //TODO: bad hack here to avoid crashes on corrupted data
}else{
$this->internalSetGameMode($this->server->getGamemode());
@@ -384,14 +403,27 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function setAllowFlight(bool $value) : void{
- $this->allowFlight = $value;
- $this->getNetworkSession()->syncAdventureSettings($this);
+ if($this->allowFlight !== $value){
+ $this->allowFlight = $value;
+ $this->getNetworkSession()->syncAdventureSettings($this);
+ }
}
public function getAllowFlight() : bool{
return $this->allowFlight;
}
+ public function setHasBlockCollision(bool $value) : void{
+ if($this->blockCollision !== $value){
+ $this->blockCollision = $value;
+ $this->getNetworkSession()->syncAdventureSettings($this);
+ }
+ }
+
+ public function hasBlockCollision() : bool{
+ return $this->blockCollision;
+ }
+
public function setFlying(bool $value) : void{
if($this->flying !== $value){
$this->flying = $value;
@@ -405,8 +437,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function setAutoJump(bool $value) : void{
- $this->autoJump = $value;
- $this->getNetworkSession()->syncAdventureSettings($this);
+ if($this->autoJump !== $value){
+ $this->autoJump = $value;
+ $this->getNetworkSession()->syncAdventureSettings($this);
+ }
}
public function hasAutoJump() : bool{
@@ -414,7 +448,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function spawnTo(Player $player) : void{
- if($this->isAlive() and $player->isAlive() and $player->getWorld() === $this->getWorld() and $player->canSee($this) and !$this->isSpectator()){
+ if($this->isAlive() && $player->isAlive() && $player->canSee($this) && !$this->isSpectator()){
parent::spawnTo($player);
}
}
@@ -428,7 +462,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function setScreenLineHeight(?int $height) : void{
- if($height !== null and $height < 1){
+ if($height !== null && $height < 1){
throw new \InvalidArgumentException("Line height must be at least 1");
}
$this->lineHeight = $height;
@@ -461,7 +495,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function canBeCollidedWith() : bool{
- return !$this->isSpectator() and parent::canBeCollidedWith();
+ return !$this->isSpectator() && parent::canBeCollidedWith();
}
public function resetFallDistance() : void{
@@ -474,7 +508,14 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function setViewDistance(int $distance) : void{
- $this->viewDistance = $this->server->getAllowedViewDistance($distance);
+ $newViewDistance = $this->server->getAllowedViewDistance($distance);
+
+ if($newViewDistance !== $this->viewDistance){
+ $ev = new PlayerViewDistanceChangeEvent($this, $this->viewDistance, $newViewDistance);
+ $ev->call();
+ }
+
+ $this->viewDistance = $newViewDistance;
$this->spawnThreshold = (int) (min($this->viewDistance, $this->server->getConfigGroup()->getPropertyInt("chunk-sending.spawn-radius", 4)) ** 2 * M_PI);
@@ -490,12 +531,12 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function isConnected() : bool{
- return $this->networkSession !== null and $this->networkSession->isConnected();
+ return $this->networkSession !== null && $this->networkSession->isConnected();
}
public function getNetworkSession() : NetworkSession{
if($this->networkSession === null){
- throw new \InvalidStateException("Player is not connected");
+ throw new \LogicException("Player is not connected");
}
return $this->networkSession;
}
@@ -648,6 +689,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$this->getNetworkSession()->stopUsingChunk($x, $z);
unset($this->usedChunks[$index]);
+ unset($this->activeChunkGenerationRequests[$index]);
}
$world->unregisterChunkLoader($this->chunkLoader, $x, $z);
$world->unregisterChunkListener($this, $x, $z);
@@ -665,7 +707,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function spawnEntitiesOnChunk(int $chunkX, int $chunkZ) : void{
foreach($this->getWorld()->getChunkEntities($chunkX, $chunkZ) as $entity){
- if($entity !== $this and !$entity->isFlaggedForDespawn()){
+ if($entity !== $this && !$entity->isFlaggedForDespawn()){
$entity->spawnTo($this);
}
}
@@ -684,19 +726,23 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$count = 0;
$world = $this->getWorld();
+
+ $limit = $this->chunksPerTick - count($this->activeChunkGenerationRequests);
foreach($this->loadQueue as $index => $distance){
- if($count >= $this->chunksPerTick){
+ if($count >= $limit){
break;
}
$X = null;
$Z = null;
World::getXZ($index, $X, $Z);
- assert(is_int($X) and is_int($Z));
+ assert(is_int($X) && is_int($Z));
++$count;
- $this->usedChunks[$index] = UsedChunkStatus::NEEDED();
+ $this->usedChunks[$index] = UsedChunkStatus::REQUESTED_GENERATION();
+ $this->activeChunkGenerationRequests[$index] = true;
+ unset($this->loadQueue[$index]);
$this->getWorld()->registerChunkLoader($this->chunkLoader, $X, $Z, true);
$this->getWorld()->registerChunkListener($this, $X, $Z);
@@ -705,15 +751,14 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if(!$this->isConnected() || !isset($this->usedChunks[$index]) || $world !== $this->getWorld()){
return;
}
- if(!$this->usedChunks[$index]->equals(UsedChunkStatus::NEEDED())){
- //TODO: make this an error
- //we may have added multiple completion handlers, since the Player keeps re-requesting chunks
- //it doesn't have yet (a relic from the old system, but also currently relied on for chunk resends).
- //in this event, make sure we don't try to send the chunk multiple times.
+ if(!$this->usedChunks[$index]->equals(UsedChunkStatus::REQUESTED_GENERATION())){
+ //We may have previously requested this, decided we didn't want it, and then decided we did want
+ //it again, all before the generation request got executed. In that case, the promise would have
+ //multiple callbacks for this player. In that case, only the first one matters.
return;
}
- unset($this->loadQueue[$index]);
- $this->usedChunks[$index] = UsedChunkStatus::REQUESTED();
+ unset($this->activeChunkGenerationRequests[$index]);
+ $this->usedChunks[$index] = UsedChunkStatus::REQUESTED_SENDING();
$this->getNetworkSession()->startUsingChunk($X, $Z, function() use ($X, $Z, $index) : void{
$this->usedChunks[$index] = UsedChunkStatus::SENT();
@@ -780,7 +825,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->getHealth() <= 0){
$this->logger->debug("Quit while dead, forcing respawn");
- $this->respawn();
+ $this->actuallyRespawn();
}
}
@@ -789,7 +834,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* This is based on factors including the player's current render radius and current position.
*/
protected function orderChunks() : void{
- if(!$this->isConnected() or $this->viewDistance === -1){
+ if(!$this->isConnected() || $this->viewDistance === -1){
return;
}
@@ -803,7 +848,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->location->getFloorX() >> Chunk::COORD_BIT_SIZE,
$this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE
) as $hash){
- if(!isset($this->usedChunks[$hash]) or $this->usedChunks[$hash]->equals(UsedChunkStatus::NEEDED())){
+ if(!isset($this->usedChunks[$hash]) || $this->usedChunks[$hash]->equals(UsedChunkStatus::NEEDED())){
$newOrder[$hash] = true;
}
unset($unloadChunks[$hash]);
@@ -815,7 +860,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$this->loadQueue = $newOrder;
- if(count($this->loadQueue) > 0 or count($unloadChunks) > 0){
+ if(count($this->loadQueue) > 0 || count($unloadChunks) > 0){
$this->chunkLoader->setCurrentLocation($this->location);
$this->getNetworkSession()->syncViewAreaCenterPoint($this->location, $this->viewDistance);
}
@@ -851,14 +896,14 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
*/
public function hasReceivedChunk(int $chunkX, int $chunkZ) : bool{
$status = $this->usedChunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
- return $status !== null and $status->equals(UsedChunkStatus::SENT());
+ return $status !== null && $status->equals(UsedChunkStatus::SENT());
}
/**
* Ticks the chunk-requesting mechanism.
*/
public function doChunkRequests() : void{
- if($this->nextChunkOrderRun !== PHP_INT_MAX and $this->nextChunkOrderRun-- <= 0){
+ if($this->nextChunkOrderRun !== PHP_INT_MAX && $this->nextChunkOrderRun-- <= 0){
$this->nextChunkOrderRun = PHP_INT_MAX;
$this->orderChunks();
}
@@ -882,7 +927,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function hasValidCustomSpawn() : bool{
- return $this->spawnPosition !== null and $this->spawnPosition->isValid();
+ return $this->spawnPosition !== null && $this->spawnPosition->isValid();
}
/**
@@ -964,6 +1009,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->isSpectator()){
$this->setFlying(true);
+ $this->setHasBlockCollision(false);
$this->setSilent();
$this->onGround = false;
@@ -974,6 +1020,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->isSurvival()){
$this->setFlying(false);
}
+ $this->setHasBlockCollision(true);
$this->setSilent(false);
$this->checkGroundState(0, 0, 0, 0, 0, 0);
}
@@ -1012,7 +1059,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @param bool $literal whether a literal check should be performed
*/
public function isSurvival(bool $literal = false) : bool{
- return $this->gamemode->equals(GameMode::SURVIVAL()) or (!$literal and $this->gamemode->equals(GameMode::ADVENTURE()));
+ return $this->gamemode->equals(GameMode::SURVIVAL()) || (!$literal && $this->gamemode->equals(GameMode::ADVENTURE()));
}
/**
@@ -1022,7 +1069,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @param bool $literal whether a literal check should be performed
*/
public function isCreative(bool $literal = false) : bool{
- return $this->gamemode->equals(GameMode::CREATIVE()) or (!$literal and $this->gamemode->equals(GameMode::SPECTATOR()));
+ return $this->gamemode->equals(GameMode::CREATIVE()) || (!$literal && $this->gamemode->equals(GameMode::SPECTATOR()));
}
/**
@@ -1032,7 +1079,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @param bool $literal whether a literal check should be performed
*/
public function isAdventure(bool $literal = false) : bool{
- return $this->gamemode->equals(GameMode::ADVENTURE()) or (!$literal and $this->gamemode->equals(GameMode::SPECTATOR()));
+ return $this->gamemode->equals(GameMode::ADVENTURE()) || (!$literal && $this->gamemode->equals(GameMode::SPECTATOR()));
}
public function isSpectator() : bool{
@@ -1043,7 +1090,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* TODO: make this a dynamic ability instead of being hardcoded
*/
public function hasFiniteResources() : bool{
- return $this->gamemode->equals(GameMode::SURVIVAL()) or $this->gamemode->equals(GameMode::ADVENTURE());
+ return $this->gamemode->equals(GameMode::SURVIVAL()) || $this->gamemode->equals(GameMode::ADVENTURE());
}
public function isFireProof() : bool{
@@ -1074,6 +1121,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$bb->minY = $this->location->y - 0.2;
$bb->maxY = $this->location->y + 0.2;
+ //we're already at the new position at this point; check if there are blocks we might have landed on between
+ //the old and new positions (running down stairs necessitates this)
+ $bb = $bb->addCoord(-$dx, -$dy, -$dz);
+
$this->onGround = $this->isCollided = count($this->getWorld()->getCollisionBlocks($bb, true)) > 0;
}
}
@@ -1086,7 +1137,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(1, 0.5, 1), $this) as $entity){
$entity->scheduleUpdate();
- if(!$entity->isAlive() or $entity->isFlaggedForDespawn()){
+ if(!$entity->isAlive() || $entity->isFlaggedForDespawn()){
continue;
}
@@ -1137,7 +1188,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->nextChunkOrderRun = 0;
}
- if(!$revert and $distanceSquared != 0){
+ if(!$revert && $distanceSquared != 0){
$dx = $newPos->x - $this->location->x;
$dy = $newPos->y - $this->location->y;
$dz = $newPos->z - $this->location->z;
@@ -1166,7 +1217,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$delta = $to->distanceSquared($from);
$deltaAngle = abs($this->lastLocation->yaw - $to->yaw) + abs($this->lastLocation->pitch - $to->pitch);
- if($delta > 0.0001 or $deltaAngle > 1.0){
+ if($delta > 0.0001 || $deltaAngle > 1.0){
$ev = new PlayerMoveEvent($this, $from, $to);
$ev->call();
@@ -1184,16 +1235,18 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->lastLocation = $to;
$this->broadcastMovement();
- $distance = sqrt((($from->x - $to->x) ** 2) + (($from->z - $to->z) ** 2));
- //TODO: check swimming (adds 0.015 exhaustion in MCPE)
- if($this->isSprinting()){
- $this->hungerManager->exhaust(0.1 * $distance, PlayerExhaustEvent::CAUSE_SPRINTING);
- }else{
- $this->hungerManager->exhaust(0.01 * $distance, PlayerExhaustEvent::CAUSE_WALKING);
- }
+ $horizontalDistanceTravelled = sqrt((($from->x - $to->x) ** 2) + (($from->z - $to->z) ** 2));
+ if($horizontalDistanceTravelled > 0){
+ //TODO: check swimming (adds 0.015 exhaustion in MCPE)
+ if($this->isSprinting()){
+ $this->hungerManager->exhaust(0.1 * $horizontalDistanceTravelled, PlayerExhaustEvent::CAUSE_SPRINTING);
+ }else{
+ $this->hungerManager->exhaust(0.01 * $horizontalDistanceTravelled, PlayerExhaustEvent::CAUSE_WALKING);
+ }
- if($this->nextChunkOrderRun > 20){
- $this->nextChunkOrderRun = 20;
+ if($this->nextChunkOrderRun > 20){
+ $this->nextChunkOrderRun = 20;
+ }
}
}
@@ -1246,7 +1299,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->lastUpdate = $currentTick;
- if(!$this->isAlive() and $this->spawned){
+ if(!$this->isAlive() && $this->spawned){
$this->onDeathUpdate($tickDiff);
return true;
}
@@ -1266,13 +1319,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->entityBaseTick($tickDiff);
Timings::$entityBaseTick->stopTiming();
- if(!$this->isSpectator() and $this->isAlive()){
+ if(!$this->isSpectator() && $this->isAlive()){
Timings::$playerCheckNearEntities->startTiming();
$this->checkNearEntities();
Timings::$playerCheckNearEntities->stopTiming();
}
- if($this->blockBreakHandler !== null and !$this->blockBreakHandler->update()){
+ if($this->blockBreakHandler !== null && !$this->blockBreakHandler->update()){
$this->blockBreakHandler = null;
}
}
@@ -1283,7 +1336,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function canBreathe() : bool{
- return $this->isCreative() or parent::canBreathe();
+ return $this->isCreative() || parent::canBreathe();
}
/**
@@ -1308,11 +1361,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* as a command.
*/
public function chat(string $message) : bool{
- $this->doCloseInventory();
+ $this->removeCurrentWindow();
$message = TextFormat::clean($message, false);
foreach(explode("\n", $message) as $messagePart){
- if(trim($messagePart) !== "" and strlen($messagePart) <= 255 and $this->messageCounter-- > 0){
+ if(trim($messagePart) !== "" && strlen($messagePart) <= self::MAX_CHAT_BYTE_LENGTH && mb_strlen($messagePart, 'UTF-8') <= self::MAX_CHAT_CHAR_LENGTH && $this->messageCounter-- > 0){
if(strpos($messagePart, './') === 0){
$messagePart = substr($messagePart, 1);
}
@@ -1372,7 +1425,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$oldItem = clone $item;
$ev = new PlayerItemUseEvent($this, $item, $directionVector);
- if($this->hasItemCooldown($item) or $this->isSpectator()){
+ if($this->hasItemCooldown($item) || $this->isSpectator()){
$ev->cancel();
}
@@ -1388,14 +1441,14 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$this->resetItemCooldown($item);
- if($this->hasFiniteResources() and !$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){
+ if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
$this->inventory->setItemInHand($item);
}
- $this->setUsingItem($item instanceof Releasable);
+ $this->setUsingItem($item instanceof Releasable && $item->canStartUsingItem($this));
return true;
}
@@ -1416,7 +1469,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$ev->call();
- if($ev->isCancelled() or !$this->consumeObject($slot)){
+ if($ev->isCancelled() || !$this->consumeObject($slot)){
return false;
}
@@ -1443,7 +1496,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
public function releaseHeldItem() : bool{
try{
$item = $this->inventory->getItemInHand();
- if(!$this->isUsingItem() or $this->hasItemCooldown($item)){
+ if(!$this->isUsingItem() || $this->hasItemCooldown($item)){
return false;
}
@@ -1452,7 +1505,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$result = $item->onReleaseUsing($this);
if($result->equals(ItemUseResult::SUCCESS())){
$this->resetItemCooldown($item);
- if(!$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){
+ if(!$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
@@ -1477,7 +1530,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$ev = new PlayerBlockPickEvent($this, $block, $item);
$existingSlot = $this->inventory->first($item);
- if($existingSlot === -1 and $this->hasFiniteResources()){
+ if($existingSlot === -1 && $this->hasFiniteResources()){
$ev->cancel();
}
$ev->call();
@@ -1538,22 +1591,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return true;
}
- if(!$this->isCreative()){
- $this->blockBreakHandler = SurvivalBlockBreakHandler::createIfNecessary($this, $pos, $target, $face, 16);
+ if(!$this->isCreative() && !$block->getBreakInfo()->breaksInstantly()){
+ $this->blockBreakHandler = new SurvivalBlockBreakHandler($this, $pos, $target, $face, 16);
}
return true;
}
public function continueBreakBlock(Vector3 $pos, int $face) : void{
- if($this->blockBreakHandler !== null and $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
- //TODO: check the targeted block matches the one we're told to target
+ if($this->blockBreakHandler !== null && $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
$this->blockBreakHandler->setTargetedFace($face);
}
}
public function stopBreakBlock(Vector3 $pos) : void{
- if($this->blockBreakHandler !== null and $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
+ if($this->blockBreakHandler !== null && $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){
$this->blockBreakHandler = null;
}
}
@@ -1564,15 +1616,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @return bool if the block was successfully broken, false if a rollback needs to take place.
*/
public function breakBlock(Vector3 $pos) : bool{
- $this->doCloseInventory();
+ $this->removeCurrentWindow();
- if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? 13 : 7)){
+ if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$this->stopBreakBlock($pos);
$item = $this->inventory->getItemInHand();
$oldItem = clone $item;
if($this->getWorld()->useBreakOn($pos, $item, $this, true)){
- if($this->hasFiniteResources() and !$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){
+ if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
@@ -1596,12 +1648,12 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
public function interactBlock(Vector3 $pos, int $face, Vector3 $clickOffset) : bool{
$this->setUsingItem(false);
- if($this->canInteract($pos->add(0.5, 0.5, 0.5), 13)){
+ if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$item = $this->inventory->getItemInHand(); //this is a copy of the real item
$oldItem = clone $item;
if($this->getWorld()->useItemOn($pos, $item, $face, $clickOffset, $this, true)){
- if($this->hasFiniteResources() and !$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){
+ if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
@@ -1626,7 +1678,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if(!$entity->isAlive()){
return false;
}
- if($entity instanceof ItemEntity or $entity instanceof Arrow){
+ if($entity instanceof ItemEntity || $entity instanceof Arrow){
$this->logger->debug("Attempted to attack non-attackable entity " . get_class($entity));
return false;
}
@@ -1635,10 +1687,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$oldItem = clone $heldItem;
$ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints());
- if(!$this->canInteract($entity->getLocation(), 8)){
+ if(!$this->canInteract($entity->getLocation(), self::MAX_REACH_DISTANCE_ENTITY_INTERACTION)){
$this->logger->debug("Cancelled attack of entity " . $entity->getId() . " due to not currently being interactable");
$ev->cancel();
- }elseif($this->isSpectator() or ($entity instanceof Player and !$this->server->getConfigGroup()->getConfigBool("pvp"))){
+ }elseif($this->isSpectator() || ($entity instanceof Player && !$this->server->getConfigGroup()->getConfigBool("pvp"))){
$ev->cancel();
}
@@ -1647,28 +1699,28 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$meleeEnchantments = [];
foreach($heldItem->getEnchantments() as $enchantment){
$type = $enchantment->getType();
- if($type instanceof MeleeWeaponEnchantment and $type->isApplicableTo($entity)){
+ if($type instanceof MeleeWeaponEnchantment && $type->isApplicableTo($entity)){
$meleeEnchantmentDamage += $type->getDamageBonus($enchantment->getLevel());
$meleeEnchantments[] = $enchantment;
}
}
$ev->setModifier($meleeEnchantmentDamage, EntityDamageEvent::MODIFIER_WEAPON_ENCHANTMENTS);
- if(!$this->isSprinting() and !$this->isFlying() and $this->fallDistance > 0 and !$this->effectManager->has(VanillaEffects::BLINDNESS()) and !$this->isUnderwater()){
+ if(!$this->isSprinting() && !$this->isFlying() && $this->fallDistance > 0 && !$this->effectManager->has(VanillaEffects::BLINDNESS()) && !$this->isUnderwater()){
$ev->setModifier($ev->getFinalDamage() / 2, EntityDamageEvent::MODIFIER_CRITICAL);
}
$entity->attack($ev);
+ $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$soundPos = $entity->getPosition()->add(0, $entity->size->getHeight() / 2, 0);
if($ev->isCancelled()){
$this->getWorld()->addSound($soundPos, new EntityAttackNoDamageSound());
return false;
}
- $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$this->getWorld()->addSound($soundPos, new EntityAttackSound());
- if($ev->getModifier(EntityDamageEvent::MODIFIER_CRITICAL) > 0 and $entity instanceof Living){
+ if($ev->getModifier(EntityDamageEvent::MODIFIER_CRITICAL) > 0 && $entity instanceof Living){
$entity->broadcastAnimation(new CriticalHitAnimation($entity));
}
@@ -1681,7 +1733,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->isAlive()){
//reactive damage like thorns might cause us to be killed by attacking another mob, which
//would mean we'd already have dropped the inventory by the time we reached here
- if($heldItem->onAttackEntity($entity) and $this->hasFiniteResources() and $oldItem->equalsExact($this->inventory->getItemInHand())){ //always fire the hook, even if we are survival
+ if($heldItem->onAttackEntity($entity) && $this->hasFiniteResources() && $oldItem->equalsExact($this->inventory->getItemInHand())){ //always fire the hook, even if we are survival
if($heldItem instanceof Durable && $heldItem->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
@@ -1700,7 +1752,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
public function interactEntity(Entity $entity, Vector3 $clickPos) : bool{
$ev = new PlayerEntityInteractEvent($this, $entity, $clickPos);
- if(!$this->canInteract($entity->getLocation(), 8)){
+ if(!$this->canInteract($entity->getLocation(), self::MAX_REACH_DISTANCE_ENTITY_INTERACTION)){
$this->logger->debug("Cancelled interaction with entity " . $entity->getId() . " due to not currently being interactable");
$ev->cancel();
}
@@ -1714,6 +1766,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function toggleSprint(bool $sprint) : bool{
+ if($sprint === $this->sprinting){
+ return true;
+ }
$ev = new PlayerToggleSprintEvent($this, $sprint);
$ev->call();
if($ev->isCancelled()){
@@ -1724,6 +1779,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function toggleSneak(bool $sneak) : bool{
+ if($sneak === $this->sneaking){
+ return true;
+ }
$ev = new PlayerToggleSneakEvent($this, $sneak);
$ev->call();
if($ev->isCancelled()){
@@ -1734,6 +1792,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function toggleFlight(bool $fly) : bool{
+ if($fly === $this->flying){
+ return true;
+ }
$ev = new PlayerToggleFlightEvent($this, $fly);
if(!$this->allowFlight){
$ev->cancel();
@@ -1742,12 +1803,51 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($ev->isCancelled()){
return false;
}
- //don't use setFlying() here, to avoid feedback loops - TODO: get rid of this hack
- $this->flying = $fly;
- $this->resetFallDistance();
+ $this->setFlying($fly);
return true;
}
+ public function toggleGlide(bool $glide) : bool{
+ if($glide === $this->gliding){
+ return true;
+ }
+ $ev = new PlayerToggleGlideEvent($this, $glide);
+ $ev->call();
+ if($ev->isCancelled()){
+ return false;
+ }
+ $this->setGliding($glide);
+ return true;
+ }
+
+ public function toggleSwim(bool $swim) : bool{
+ if($swim === $this->swimming){
+ return true;
+ }
+ $ev = new PlayerToggleSwimEvent($this, $swim);
+ $ev->call();
+ if($ev->isCancelled()){
+ return false;
+ }
+ $this->setSwimming($swim);
+ return true;
+ }
+
+ public function emote(string $emoteId) : void{
+ $currentTick = $this->server->getTick();
+ if($currentTick - $this->lastEmoteTick > 5){
+ $this->lastEmoteTick = $currentTick;
+ $event = new PlayerEmoteEvent($this, $emoteId);
+ $event->call();
+ if(!$event->isCancelled()){
+ $emoteId = $event->getEmoteId();
+ foreach($this->getViewers() as $player){
+ $player->getNetworkSession()->onEmote($this, $emoteId);
+ }
+ }
+ }
+ }
+
/**
* Drops an item on the ground in front of the player.
*/
@@ -1807,7 +1907,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @param int $fadeOut Title fade-out time in ticks.
*/
public function setTitleDuration(int $fadeIn, int $stay, int $fadeOut) : void{
- if($fadeIn >= 0 and $stay >= 0 and $fadeOut >= 0){
+ if($fadeIn >= 0 && $stay >= 0 && $fadeOut >= 0){
$this->getNetworkSession()->onTitleDuration($fadeIn, $stay, $fadeOut);
}
}
@@ -1962,13 +2062,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
*/
public function onPostDisconnect(string $reason, Translatable|string|null $quitMessage) : void{
if($this->isConnected()){
- throw new \InvalidStateException("Player is still connected");
+ throw new \LogicException("Player is still connected");
}
//prevent the player receiving their own disconnect message
$this->server->unsubscribeFromAllBroadcastChannels($this);
- $this->doCloseInventory();
+ $this->removeCurrentWindow();
$ev = new PlayerQuitEvent($this, $quitMessage ?? $this->getLeaveMessage(), $reason);
$ev->call();
@@ -2081,7 +2181,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function onDeath() : void{
//Crafting grid must always be evacuated even if keep-inventory is true. This dumps the contents into the
//main inventory and drops the rest on the ground.
- $this->doCloseInventory();
+ $this->removeCurrentWindow();
$ev = new PlayerDeathEvent($this, $this->getDrops(), $this->getXpDropAmount(), null);
$ev->call();
@@ -2121,10 +2221,6 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function respawn() : void{
- if($this->respawnLocked){
- return;
- }
- $this->respawnLocked = true;
if($this->server->isHardcore()){
if($this->kick("You have been banned because you died in hardcore mode")){ //this allows plugins to prevent the ban by cancelling PlayerKickEvent
$this->server->getNameBans()->addBan($this->getName(), "Died in hardcore mode");
@@ -2132,6 +2228,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return;
}
+ $this->actuallyRespawn();
+ }
+
+ protected function actuallyRespawn() : void{
+ if($this->respawnLocked){
+ return;
+ }
+ $this->respawnLocked = true;
+
$this->logger->debug("Waiting for spawn terrain generation for respawn");
$spawn = $this->getSpawn();
$spawn->getWorld()->orderChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
@@ -2189,11 +2294,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
if($this->isCreative()
- and $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE
- and $source->getCause() !== EntityDamageEvent::CAUSE_VOID
+ && $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE
+ && $source->getCause() !== EntityDamageEvent::CAUSE_VOID
){
$source->cancel();
- }elseif($this->allowFlight and $source->getCause() === EntityDamageEvent::CAUSE_FALL){
+ }elseif($this->allowFlight && $source->getCause() === EntityDamageEvent::CAUSE_FALL){
$source->cancel();
}
@@ -2206,7 +2311,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$properties->setGenericFlag(EntityMetadataFlags::ACTION, $this->startAction > -1);
$properties->setPlayerFlag(PlayerMetadataFlags::SLEEP, $this->sleeping !== null);
- $properties->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, $this->sleeping ?? new Vector3(0, 0, 0));
+ $properties->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, $this->sleeping !== null ? BlockPosition::fromVector3($this->sleeping) : new BlockPosition(0, 0, 0));
}
public function sendData(?array $targets, ?array $data = null) : void{
@@ -2218,7 +2323,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function broadcastAnimation(Animation $animation, ?array $targets = null) : void{
- if($this->spawned and $targets === null){
+ if($this->spawned && $targets === null){
$targets = $this->getViewers();
$targets[] = $this;
}
@@ -2275,7 +2380,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function addDefaultWindows() : void{
$this->cursorInventory = new PlayerCursorInventory($this);
- $this->craftingGrid = new CraftingGrid($this, CraftingGrid::SIZE_SMALL);
+ $this->craftingGrid = new PlayerCraftingInventory($this);
$this->addPermanentInventories($this->inventory, $this->armorInventory, $this->cursorInventory, $this->offHandInventory);
@@ -2290,17 +2395,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return $this->craftingGrid;
}
- public function setCraftingGrid(CraftingGrid $grid) : void{
- $this->craftingGrid = $grid;
- }
-
/**
* @internal Called to clean up crafting grid and cursor inventory when it is detected that the player closed their
* inventory.
*/
- public function doCloseInventory() : void{
- /** @var Inventory[] $inventories */
+ private function doCloseInventory() : void{
$inventories = [$this->craftingGrid, $this->cursorInventory];
+ if($this->currentWindow instanceof TemporaryInventory){
+ $inventories[] = $this->currentWindow;
+ }
$transaction = new InventoryTransaction($this);
$mainInventoryTransactionBuilder = new TransactionBuilderInventory($this->inventory);
@@ -2337,10 +2440,6 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e);
}
}
-
- if($this->craftingGrid->getGridWidth() > CraftingGrid::SIZE_SMALL){
- $this->craftingGrid = new CraftingGrid($this, CraftingGrid::SIZE_SMALL);
- }
}
/**
@@ -2377,6 +2476,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function removeCurrentWindow() : void{
+ $this->doCloseInventory();
if($this->currentWindow !== null){
(new InventoryCloseEvent($this->currentWindow, $this))->call();
diff --git a/src/player/SurvivalBlockBreakHandler.php b/src/player/SurvivalBlockBreakHandler.php
index 3992e8fa5..197d31215 100644
--- a/src/player/SurvivalBlockBreakHandler.php
+++ b/src/player/SurvivalBlockBreakHandler.php
@@ -28,6 +28,7 @@ use pocketmine\entity\animation\ArmSwingAnimation;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
use pocketmine\world\particle\BlockPunchParticle;
use pocketmine\world\sound\BlockPunchSound;
use function abs;
@@ -58,7 +59,7 @@ final class SurvivalBlockBreakHandler{
/** @var float */
private $breakProgress = 0;
- private function __construct(Player $player, Vector3 $blockPos, Block $block, int $targetedFace, int $maxPlayerDistance, int $fxTickInterval = self::DEFAULT_FX_INTERVAL_TICKS){
+ public function __construct(Player $player, Vector3 $blockPos, Block $block, int $targetedFace, int $maxPlayerDistance, int $fxTickInterval = self::DEFAULT_FX_INTERVAL_TICKS){
$this->player = $player;
$this->blockPos = $blockPos;
$this->block = $block;
@@ -70,19 +71,11 @@ final class SurvivalBlockBreakHandler{
if($this->breakSpeed > 0){
$this->player->getWorld()->broadcastPacketToViewers(
$this->blockPos,
- LevelEventPacket::create(LevelEventPacket::EVENT_BLOCK_START_BREAK, (int) (65535 * $this->breakSpeed), $this->blockPos)
+ LevelEventPacket::create(LevelEvent::BLOCK_START_BREAK, (int) (65535 * $this->breakSpeed), $this->blockPos)
);
}
}
- public static function createIfNecessary(Player $player, Vector3 $blockPos, Block $block, int $targetedFace, int $maxPlayerDistance, int $fxTickInterval = self::DEFAULT_FX_INTERVAL_TICKS) : ?self{
- $breakInfo = $block->getBreakInfo();
- if(!$breakInfo->breaksInstantly()){
- return new self($player, $blockPos, $block, $targetedFace, $maxPlayerDistance, $fxTickInterval);
- }
- return null;
- }
-
/**
* Returns the calculated break speed as percentage progress per game tick.
*/
@@ -100,8 +93,7 @@ final class SurvivalBlockBreakHandler{
}
public function update() : bool{
- if(
- $this->player->getPosition()->distanceSquared($this->blockPos->add(0.5, 0.5, 0.5)) > $this->maxPlayerDistance ** 2){
+ if($this->player->getPosition()->distanceSquared($this->blockPos->add(0.5, 0.5, 0.5)) > $this->maxPlayerDistance ** 2){
return false;
}
@@ -113,7 +105,7 @@ final class SurvivalBlockBreakHandler{
$this->breakProgress += $this->breakSpeed;
- if(($this->fxTicker++ % $this->fxTickInterval) === 0 and $this->breakProgress < 1){
+ if(($this->fxTicker++ % $this->fxTickInterval) === 0 && $this->breakProgress < 1){
$this->player->getWorld()->addParticle($this->blockPos, new BlockPunchParticle($this->block, $this->targetedFace));
$this->player->getWorld()->addSound($this->blockPos, new BlockPunchSound($this->block));
$this->player->broadcastAnimation(new ArmSwingAnimation($this->player), $this->player->getViewers());
@@ -147,7 +139,7 @@ final class SurvivalBlockBreakHandler{
if($this->player->getWorld()->isInLoadedTerrain($this->blockPos)){
$this->player->getWorld()->broadcastPacketToViewers(
$this->blockPos,
- LevelEventPacket::create(LevelEventPacket::EVENT_BLOCK_STOP_BREAK, 0, $this->blockPos)
+ LevelEventPacket::create(LevelEvent::BLOCK_STOP_BREAK, 0, $this->blockPos)
);
}
}
diff --git a/src/player/UsedChunkStatus.php b/src/player/UsedChunkStatus.php
index bbdb9a061..fa8227628 100644
--- a/src/player/UsedChunkStatus.php
+++ b/src/player/UsedChunkStatus.php
@@ -32,7 +32,8 @@ use pocketmine\utils\EnumTrait;
* @generate-registry-docblock
*
* @method static UsedChunkStatus NEEDED()
- * @method static UsedChunkStatus REQUESTED()
+ * @method static UsedChunkStatus REQUESTED_GENERATION()
+ * @method static UsedChunkStatus REQUESTED_SENDING()
* @method static UsedChunkStatus SENT()
*/
final class UsedChunkStatus{
@@ -41,7 +42,8 @@ final class UsedChunkStatus{
protected static function setup() : void{
self::registerAll(
new self("NEEDED"),
- new self("REQUESTED"),
+ new self("REQUESTED_GENERATION"),
+ new self("REQUESTED_SENDING"),
new self("SENT")
);
}
diff --git a/src/plugin/ApiVersion.php b/src/plugin/ApiVersion.php
index 5a2bb035b..3842c1af5 100644
--- a/src/plugin/ApiVersion.php
+++ b/src/plugin/ApiVersion.php
@@ -53,7 +53,7 @@ final class ApiVersion{
continue;
}
- if($version->getMinor() === $myVersion->getMinor() and $version->getPatch() > $myVersion->getPatch()){ //If the plugin requires bug fixes in patches, being backwards compatible
+ if($version->getMinor() === $myVersion->getMinor() && $version->getPatch() > $myVersion->getPatch()){ //If the plugin requires bug fixes in patches, being backwards compatible
continue;
}
}
diff --git a/src/plugin/PharPluginLoader.php b/src/plugin/PharPluginLoader.php
index d42fa0b5e..e036099e8 100644
--- a/src/plugin/PharPluginLoader.php
+++ b/src/plugin/PharPluginLoader.php
@@ -41,7 +41,7 @@ class PharPluginLoader implements PluginLoader{
public function canLoadPlugin(string $path) : bool{
$ext = ".phar";
- return is_file($path) and substr($path, -strlen($ext)) === $ext;
+ return is_file($path) && substr($path, -strlen($ext)) === $ext;
}
/**
diff --git a/src/plugin/PluginBase.php b/src/plugin/PluginBase.php
index ac7b0036f..fdf47d39c 100644
--- a/src/plugin/PluginBase.php
+++ b/src/plugin/PluginBase.php
@@ -32,16 +32,13 @@ use pocketmine\scheduler\TaskScheduler;
use pocketmine\Server;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Config;
+use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use function count;
use function dirname;
use function fclose;
use function file_exists;
use function fopen;
-use function gettype;
-use function is_array;
-use function is_bool;
-use function is_string;
use function mkdir;
use function rtrim;
use function stream_copy_to_stream;
@@ -166,50 +163,39 @@ abstract class PluginBase implements Plugin, CommandExecutor{
private function registerYamlCommands() : void{
$pluginCmds = [];
- foreach($this->getDescription()->getCommands() as $key => $data){
+ foreach(Utils::stringifyKeys($this->getDescription()->getCommands()) as $key => $data){
if(strpos($key, ":") !== false){
- $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->getDescription()->getFullName())));
+ $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->getDescription()->getFullName(), ":")));
continue;
}
- if(is_array($data)){ //TODO: error out if it isn't
- $newCmd = new PluginCommand($key, $this, $this);
- if(isset($data["description"])){
- $newCmd->setDescription($data["description"]);
- }
- if(isset($data["usage"])){
- $newCmd->setUsage($data["usage"]);
- }
-
- if(isset($data["aliases"]) and is_array($data["aliases"])){
- $aliasList = [];
- foreach($data["aliases"] as $alias){
- if(strpos($alias, ":") !== false){
- $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_aliasError($alias, $this->getDescription()->getFullName())));
- continue;
- }
- $aliasList[] = $alias;
- }
-
- $newCmd->setAliases($aliasList);
- }
-
- if(isset($data["permission"])){
- if(is_bool($data["permission"])){
- $newCmd->setPermission($data["permission"] ? "true" : "false");
- }elseif(is_string($data["permission"])){
- $newCmd->setPermission($data["permission"]);
- }else{
- $this->logger->error("Permission must be a string, " . gettype($data["permission"]) . " given for command $key");
- }
- }
-
- if(isset($data["permission-message"])){
- $newCmd->setPermissionMessage($data["permission-message"]);
- }
-
- $pluginCmds[] = $newCmd;
+ $newCmd = new PluginCommand($key, $this, $this);
+ if(($description = $data->getDescription()) !== null){
+ $newCmd->setDescription($description);
}
+
+ if(($usageMessage = $data->getUsageMessage()) !== null){
+ $newCmd->setUsage($usageMessage);
+ }
+
+ $aliasList = [];
+ foreach($data->getAliases() as $alias){
+ if(strpos($alias, ":") !== false){
+ $this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_aliasError($alias, $this->getDescription()->getFullName(), ":")));
+ continue;
+ }
+ $aliasList[] = $alias;
+ }
+
+ $newCmd->setAliases($aliasList);
+
+ $newCmd->setPermission($data->getPermission());
+
+ if(($permissionDeniedMessage = $data->getPermissionDeniedMessage()) !== null){
+ $newCmd->setPermissionMessage($permissionDeniedMessage);
+ }
+
+ $pluginCmds[] = $newCmd;
}
if(count($pluginCmds) > 0){
@@ -223,11 +209,11 @@ abstract class PluginBase implements Plugin, CommandExecutor{
*/
public function getCommand(string $name){
$command = $this->getServer()->getPluginCommand($name);
- if($command === null or $command->getOwningPlugin() !== $this){
+ if($command === null || $command->getOwningPlugin() !== $this){
$command = $this->getServer()->getPluginCommand(strtolower($this->description->getName()) . ":" . $name);
}
- if($command instanceof PluginOwned and $command->getOwningPlugin() === $this){
+ if($command instanceof PluginOwned && $command->getOwningPlugin() === $this){
return $command;
}else{
return null;
@@ -268,7 +254,7 @@ abstract class PluginBase implements Plugin, CommandExecutor{
mkdir(dirname($out), 0755, true);
}
- if(file_exists($out) and !$replace){
+ if(file_exists($out) && !$replace){
return false;
}
diff --git a/src/plugin/PluginDescription.php b/src/plugin/PluginDescription.php
index 9ae463093..0bd7587bb 100644
--- a/src/plugin/PluginDescription.php
+++ b/src/plugin/PluginDescription.php
@@ -25,16 +25,15 @@ namespace pocketmine\plugin;
use pocketmine\permission\Permission;
use pocketmine\permission\PermissionParser;
+use pocketmine\permission\PermissionParserException;
use function array_map;
use function array_values;
+use function get_debug_type;
use function is_array;
-use function phpversion;
+use function is_string;
use function preg_match;
use function str_replace;
use function stripos;
-use function strlen;
-use function substr;
-use function version_compare;
use function yaml_parse;
class PluginDescription{
@@ -42,77 +41,81 @@ class PluginDescription{
* @var mixed[]
* @phpstan-var array
*/
- private $map;
+ private array $map;
- /** @var string */
- private $name;
- /** @var string */
- private $main;
+ private string $name;
+ private string $main;
private string $srcNamespacePrefix = "";
/** @var string[] */
- private $api;
+ private array $api;
/** @var int[] */
- private $compatibleMcpeProtocols = [];
+ private array $compatibleMcpeProtocols = [];
/** @var string[] */
- private $compatibleOperatingSystems = [];
+ private array $compatibleOperatingSystems = [];
/**
* @var string[][]
* @phpstan-var array>
*/
- private $extensions = [];
+ private array $extensions = [];
/** @var string[] */
- private $depend = [];
+ private array $depend = [];
/** @var string[] */
- private $softDepend = [];
+ private array $softDepend = [];
/** @var string[] */
- private $loadBefore = [];
- /** @var string */
- private $version;
+ private array $loadBefore = [];
+ private string $version;
/**
- * @var mixed[][]
- * @phpstan-var array>
+ * @var PluginDescriptionCommandEntry[]
+ * @phpstan-var array
*/
- private $commands = [];
- /** @var string */
- private $description = "";
+ private array $commands = [];
+ private string $description = "";
/** @var string[] */
- private $authors = [];
- /** @var string */
- private $website = "";
- /** @var string */
- private $prefix = "";
- /** @var PluginEnableOrder */
- private $order;
+ private array $authors = [];
+ private string $website = "";
+ private string $prefix = "";
+ private PluginEnableOrder $order;
/**
* @var Permission[][]
* @phpstan-var array>
*/
- private $permissions = [];
+ private array $permissions = [];
/**
* @param string|mixed[] $yamlString
*/
public function __construct($yamlString){
- $this->loadMap(!is_array($yamlString) ? yaml_parse($yamlString) : $yamlString);
+ if(is_string($yamlString)){
+ $map = yaml_parse($yamlString);
+ if($map === false){
+ throw new PluginDescriptionParseException("YAML parsing error in plugin manifest");
+ }
+ if(!is_array($map)){
+ throw new PluginDescriptionParseException("Invalid structure of plugin manifest, expected array but have " . get_debug_type($map));
+ }
+ }else{
+ $map = $yamlString;
+ }
+ $this->loadMap($map);
}
/**
* @param mixed[] $plugin
- * @throws PluginException
+ * @throws PluginDescriptionParseException
*/
private function loadMap(array $plugin) : void{
$this->map = $plugin;
$this->name = $plugin["name"];
if(preg_match('/^[A-Za-z0-9 _.-]+$/', $this->name) === 0){
- throw new PluginException("Invalid Plugin name");
+ throw new PluginDescriptionParseException("Invalid Plugin name");
}
$this->name = str_replace(" ", "_", $this->name);
$this->version = (string) $plugin["version"];
$this->main = $plugin["main"];
if(stripos($this->main, "pocketmine\\") === 0){
- throw new PluginException("Invalid Plugin main, cannot start within the PocketMine namespace");
+ throw new PluginDescriptionParseException("Invalid Plugin main, cannot start within the PocketMine namespace");
}
$this->srcNamespacePrefix = $plugin["src-namespace-prefix"] ?? "";
@@ -121,8 +124,25 @@ class PluginDescription{
$this->compatibleMcpeProtocols = array_map("\intval", (array) ($plugin["mcpe-protocol"] ?? []));
$this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin["os"] ?? []));
- if(isset($plugin["commands"]) and is_array($plugin["commands"])){
- $this->commands = $plugin["commands"];
+ if(isset($plugin["commands"]) && is_array($plugin["commands"])){
+ foreach($plugin["commands"] as $commandName => $commandData){
+ if(!is_string($commandName)){
+ throw new PluginDescriptionParseException("Invalid Plugin commands, key must be the name of the command");
+ }
+ if(!is_array($commandData)){
+ throw new PluginDescriptionParseException("Command $commandName has invalid properties");
+ }
+ if(!isset($commandData["permission"]) || !is_string($commandData["permission"])){
+ throw new PluginDescriptionParseException("Command $commandName does not have a valid permission set");
+ }
+ $this->commands[$commandName] = new PluginDescriptionCommandEntry(
+ $commandData["description"] ?? null,
+ $commandData["usage"] ?? null,
+ $commandData["aliases"] ?? [],
+ $commandData["permission"],
+ $commandData["permission-message"] ?? null
+ );
+ }
}
if(isset($plugin["depend"])){
@@ -153,7 +173,7 @@ class PluginDescription{
if(isset($plugin["load"])){
$order = PluginEnableOrder::fromString($plugin["load"]);
if($order === null){
- throw new PluginException("Invalid Plugin \"load\"");
+ throw new PluginDescriptionParseException("Invalid Plugin \"load\"");
}
$this->order = $order;
}else{
@@ -175,7 +195,11 @@ class PluginDescription{
}
if(isset($plugin["permissions"])){
- $this->permissions = PermissionParser::loadPermissions($plugin["permissions"]);
+ try{
+ $this->permissions = PermissionParser::loadPermissions($plugin["permissions"]);
+ }catch(PermissionParserException $e){
+ throw new PluginDescriptionParseException("Invalid Plugin \"permissions\": " . $e->getMessage(), 0, $e);
+ }
}
}
@@ -216,8 +240,8 @@ class PluginDescription{
}
/**
- * @return mixed[][]
- * @phpstan-return array>
+ * @return PluginDescriptionCommandEntry[]
+ * @phpstan-return array
*/
public function getCommands() : array{
return $this->commands;
@@ -231,40 +255,6 @@ class PluginDescription{
return $this->extensions;
}
- /**
- * Checks if the current PHP runtime has the extensions required by the plugin.
- *
- * @throws PluginException if there are required extensions missing or have incompatible version, or if the version constraint cannot be parsed
- */
- public function checkRequiredExtensions() : void{
- foreach($this->extensions as $name => $versionConstrs){
- $gotVersion = phpversion($name);
- if($gotVersion === false){
- throw new PluginException("Required extension $name not loaded");
- }
-
- foreach($versionConstrs as $constr){ // versionConstrs_loop
- if($constr === "*"){
- continue;
- }
- if($constr === ""){
- throw new PluginException("One of the extension version constraints of $name is empty. Consider quoting the version string in plugin.yml");
- }
- foreach(["<=", "le", "<>", "!=", "ne", "<", "lt", "==", "=", "eq", ">=", "ge", ">", "gt"] as $comparator){
- // warning: the > character should be quoted in YAML
- if(substr($constr, 0, strlen($comparator)) === $comparator){
- $version = substr($constr, strlen($comparator));
- if(!version_compare($gotVersion, $version, $comparator)){
- throw new PluginException("Required extension $name has an incompatible version ($gotVersion not $constr)");
- }
- continue 2; // versionConstrs_loop
- }
- }
- throw new PluginException("Error parsing version constraint: $constr");
- }
- }
- }
-
/**
* @return string[]
*/
diff --git a/src/plugin/PluginDescriptionCommandEntry.php b/src/plugin/PluginDescriptionCommandEntry.php
new file mode 100644
index 000000000..b9b618e8a
--- /dev/null
+++ b/src/plugin/PluginDescriptionCommandEntry.php
@@ -0,0 +1,53 @@
+ $aliases
+ */
+ public function __construct(
+ private ?string $description,
+ private ?string $usageMessage,
+ private array $aliases,
+ private string $permission,
+ private ?string $permissionDeniedMessage,
+ ){}
+
+ public function getDescription() : ?string{ return $this->description; }
+
+ public function getUsageMessage() : ?string{ return $this->usageMessage; }
+
+ /**
+ * @return string[]
+ * @phpstan-return list
+ */
+ public function getAliases() : array{ return $this->aliases; }
+
+ public function getPermission() : string{ return $this->permission; }
+
+ public function getPermissionDeniedMessage() : ?string{ return $this->permissionDeniedMessage; }
+}
diff --git a/src/plugin/PluginDescriptionParseException.php b/src/plugin/PluginDescriptionParseException.php
new file mode 100644
index 000000000..ad98342f0
--- /dev/null
+++ b/src/plugin/PluginDescriptionParseException.php
@@ -0,0 +1,31 @@
+setName('plugin_list.yml');
- try{
- $validator->assert($array);
- }catch(NestedValidationException $e){
- throw new \InvalidArgumentException($e->getFullMessage(), 0, $e);
+ if(!isset($array["mode"]) || ($array["mode"] !== "whitelist" && $array["mode"] !== "blacklist")){
+ throw new \InvalidArgumentException("\"mode\" must be set");
}
- return new PluginGraylist($array["plugins"], $array["mode"] === 'whitelist');
+ $isWhitelist = match($array["mode"]){
+ "whitelist" => true,
+ "blacklist" => false,
+ default => throw new \InvalidArgumentException("\"mode\" must be either \"whitelist\" or \"blacklist\"")
+ };
+ $plugins = [];
+ if(isset($array["plugins"])){
+ if(!is_array($array["plugins"])){
+ throw new \InvalidArgumentException("\"plugins\" must be an array");
+ }
+ foreach($array["plugins"] as $k => $v){
+ if(!is_string($v) && !is_int($v) && !is_float($v)){
+ throw new \InvalidArgumentException("\"plugins\" contains invalid element at position $k");
+ }
+ $plugins[] = (string) $v;
+ }
+ }
+ return new PluginGraylist($plugins, $isWhitelist);
}
/**
diff --git a/src/plugin/PluginLoadTriage.php b/src/plugin/PluginLoadTriage.php
new file mode 100644
index 000000000..c9c4fff77
--- /dev/null
+++ b/src/plugin/PluginLoadTriage.php
@@ -0,0 +1,42 @@
+
+ */
+ public $plugins = [];
+ /**
+ * @var string[][]
+ * @phpstan-var array>
+ */
+ public $dependencies = [];
+ /**
+ * @var string[][]
+ * @phpstan-var array>
+ */
+ public $softDependencies = [];
+}
diff --git a/src/plugin/PluginLoadTriageEntry.php b/src/plugin/PluginLoadTriageEntry.php
new file mode 100644
index 000000000..2c8643b1d
--- /dev/null
+++ b/src/plugin/PluginLoadTriageEntry.php
@@ -0,0 +1,42 @@
+file; }
+
+ public function getLoader() : PluginLoader{ return $this->loader; }
+
+ public function getDescription() : PluginDescription{ return $this->description; }
+}
diff --git a/src/plugin/PluginLoadabilityChecker.php b/src/plugin/PluginLoadabilityChecker.php
new file mode 100644
index 000000000..216f70129
--- /dev/null
+++ b/src/plugin/PluginLoadabilityChecker.php
@@ -0,0 +1,113 @@
+getName();
+ if(stripos($name, "pocketmine") !== false || stripos($name, "minecraft") !== false || stripos($name, "mojang") !== false){
+ return KnownTranslationFactory::pocketmine_plugin_restrictedName();
+ }
+
+ foreach($description->getCompatibleApis() as $api){
+ if(!VersionString::isValidBaseVersion($api)){
+ return KnownTranslationFactory::pocketmine_plugin_invalidAPI($api);
+ }
+ }
+
+ if(!ApiVersion::isCompatible($this->apiVersion, $description->getCompatibleApis())){
+ return KnownTranslationFactory::pocketmine_plugin_incompatibleAPI(implode(", ", $description->getCompatibleApis()));
+ }
+
+ $ambiguousVersions = ApiVersion::checkAmbiguousVersions($description->getCompatibleApis());
+ if(count($ambiguousVersions) > 0){
+ return KnownTranslationFactory::pocketmine_plugin_ambiguousMinAPI(implode(", ", $ambiguousVersions));
+ }
+
+ if(count($description->getCompatibleOperatingSystems()) > 0 && !in_array(Utils::getOS(), $description->getCompatibleOperatingSystems(), true)) {
+ return KnownTranslationFactory::pocketmine_plugin_incompatibleOS(implode(", ", $description->getCompatibleOperatingSystems()));
+ }
+
+ if(count($pluginMcpeProtocols = $description->getCompatibleMcpeProtocols()) > 0){
+ $serverMcpeProtocols = [ProtocolInfo::CURRENT_PROTOCOL];
+ if(count(array_intersect($pluginMcpeProtocols, $serverMcpeProtocols)) === 0){
+ return KnownTranslationFactory::pocketmine_plugin_incompatibleProtocol(implode(", ", $pluginMcpeProtocols));
+ }
+ }
+
+ foreach(Utils::stringifyKeys($description->getRequiredExtensions()) as $extensionName => $versionConstrs){
+ if(!extension_loaded($extensionName)){
+ return KnownTranslationFactory::pocketmine_plugin_extensionNotLoaded($extensionName);
+ }
+ $gotVersion = phpversion($extensionName);
+ if($gotVersion === false){
+ //extensions may set NULL as the extension version, in which case phpversion() may return false
+ $gotVersion = "**UNKNOWN**";
+ }
+
+ foreach($versionConstrs as $k => $constr){ // versionConstrs_loop
+ if($constr === "*"){
+ continue;
+ }
+ if($constr === ""){
+ return KnownTranslationFactory::pocketmine_plugin_emptyExtensionVersionConstraint(extensionName: $extensionName, constraintIndex: "$k");
+ }
+ foreach(["<=", "le", "<>", "!=", "ne", "<", "lt", "==", "=", "eq", ">=", "ge", ">", "gt"] as $comparator){
+ // warning: the > character should be quoted in YAML
+ if(substr($constr, 0, strlen($comparator)) === $comparator){
+ $version = substr($constr, strlen($comparator));
+ if(!version_compare($gotVersion, $version, $comparator)){
+ return KnownTranslationFactory::pocketmine_plugin_incompatibleExtensionVersion(extensionName: $extensionName, extensionVersion: $gotVersion, pluginRequirement: $constr);
+ }
+ continue 2; // versionConstrs_loop
+ }
+ }
+ return KnownTranslationFactory::pocketmine_plugin_invalidExtensionVersionConstraint(extensionName: $extensionName, versionConstraint: $constr);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/plugin/PluginLoader.php b/src/plugin/PluginLoader.php
index 2bf603dde..7e4787fa6 100644
--- a/src/plugin/PluginLoader.php
+++ b/src/plugin/PluginLoader.php
@@ -40,6 +40,7 @@ interface PluginLoader{
/**
* Gets the PluginDescription from the file
+ * @throws PluginDescriptionParseException
*/
public function getPluginDescription(string $file) : ?PluginDescription;
diff --git a/src/plugin/PluginLogger.php b/src/plugin/PluginLogger.php
index ba310563b..1a1ab9cdb 100644
--- a/src/plugin/PluginLogger.php
+++ b/src/plugin/PluginLogger.php
@@ -25,16 +25,28 @@ namespace pocketmine\plugin;
use function spl_object_id;
+/**
+ * @phpstan-import-type LoggerAttachment from \AttachableLogger
+ */
class PluginLogger extends \PrefixedLogger implements \AttachableLogger{
- /** @var \LoggerAttachment[] */
+ /**
+ * @var \Closure[]
+ * @phpstan-var LoggerAttachment[]
+ */
private $attachments = [];
- public function addAttachment(\LoggerAttachment $attachment){
+ /**
+ * @phpstan-param LoggerAttachment $attachment
+ */
+ public function addAttachment(\Closure $attachment){
$this->attachments[spl_object_id($attachment)] = $attachment;
}
- public function removeAttachment(\LoggerAttachment $attachment){
+ /**
+ * @phpstan-param LoggerAttachment $attachment
+ */
+ public function removeAttachment(\Closure $attachment){
unset($this->attachments[spl_object_id($attachment)]);
}
@@ -49,7 +61,7 @@ class PluginLogger extends \PrefixedLogger implements \AttachableLogger{
public function log($level, $message){
parent::log($level, $message);
foreach($this->attachments as $attachment){
- $attachment->log($level, $message);
+ $attachment($level, $message);
}
}
}
diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php
index c646a4dff..529fe5bbe 100644
--- a/src/plugin/PluginManager.php
+++ b/src/plugin/PluginManager.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\plugin;
+use pocketmine\event\Cancellable;
use pocketmine\event\Event;
use pocketmine\event\EventPriority;
use pocketmine\event\HandlerListManager;
@@ -32,7 +33,6 @@ use pocketmine\event\plugin\PluginDisableEvent;
use pocketmine\event\plugin\PluginEnableEvent;
use pocketmine\event\RegisteredListener;
use pocketmine\lang\KnownTranslationFactory;
-use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\permission\DefaultPermissions;
use pocketmine\permission\PermissionManager;
use pocketmine\permission\PermissionParser;
@@ -41,7 +41,9 @@ use pocketmine\timings\TimingsHandler;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
-use function array_intersect;
+use function array_diff_key;
+use function array_key_exists;
+use function array_keys;
use function array_merge;
use function class_exists;
use function count;
@@ -49,16 +51,17 @@ use function dirname;
use function file_exists;
use function get_class;
use function implode;
-use function in_array;
use function is_a;
use function is_array;
use function is_dir;
+use function is_file;
use function is_string;
use function is_subclass_of;
use function iterator_to_array;
use function mkdir;
+use function realpath;
use function shuffle;
-use function stripos;
+use function sprintf;
use function strpos;
use function strtolower;
@@ -76,6 +79,8 @@ class PluginManager{
/** @var Plugin[] */
protected $enabledPlugins = [];
+ private bool $loadPluginsGuard = false;
+
/**
* @var PluginLoader[]
* @phpstan-var array, PluginLoader>
@@ -127,103 +132,96 @@ class PluginManager{
return Path::join(dirname($pluginPath), $pluginName);
}
- /**
- * @param PluginLoader[] $loaders
- */
- public function loadPlugin(string $path, ?array $loaders = null) : ?Plugin{
- foreach($loaders ?? $this->fileAssociations as $loader){
- if($loader->canLoadPlugin($path)){
- $description = $loader->getPluginDescription($path);
- if($description instanceof PluginDescription){
- $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_load($description->getFullName())));
- try{
- $description->checkRequiredExtensions();
- }catch(PluginException $ex){
- $this->server->getLogger()->error($ex->getMessage());
- return null;
- }
+ private function internalLoadPlugin(string $path, PluginLoader $loader, PluginDescription $description) : ?Plugin{
+ $language = $this->server->getLanguage();
+ $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_load($description->getFullName())));
- $dataFolder = $this->getDataDirectory($path, $description->getName());
- if(file_exists($dataFolder) and !is_dir($dataFolder)){
- $this->server->getLogger()->error("Projected dataFolder '" . $dataFolder . "' for " . $description->getName() . " exists and is not a directory");
- return null;
- }
- if(!file_exists($dataFolder)){
- mkdir($dataFolder, 0777, true);
- }
+ $dataFolder = $this->getDataDirectory($path, $description->getName());
+ if(file_exists($dataFolder) && !is_dir($dataFolder)){
+ $this->server->getLogger()->error($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $description->getName(),
+ KnownTranslationFactory::pocketmine_plugin_badDataFolder($dataFolder)
+ )));
+ return null;
+ }
+ if(!file_exists($dataFolder)){
+ mkdir($dataFolder, 0777, true);
+ }
- $prefixed = $loader->getAccessProtocol() . $path;
- $loader->loadPlugin($prefixed);
+ $prefixed = $loader->getAccessProtocol() . $path;
+ $loader->loadPlugin($prefixed);
- $mainClass = $description->getMain();
- if(!class_exists($mainClass, true)){
- $this->server->getLogger()->error("Main class for plugin " . $description->getName() . " not found");
- return null;
- }
- if(!is_a($mainClass, Plugin::class, true)){
- $this->server->getLogger()->error("Main class for plugin " . $description->getName() . " is not an instance of " . Plugin::class);
- return null;
- }
+ $mainClass = $description->getMain();
+ if(!class_exists($mainClass, true)){
+ $this->server->getLogger()->error($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $description->getName(),
+ KnownTranslationFactory::pocketmine_plugin_mainClassNotFound()
+ )));
+ return null;
+ }
+ if(!is_a($mainClass, Plugin::class, true)){
+ $this->server->getLogger()->error($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $description->getName(),
+ KnownTranslationFactory::pocketmine_plugin_mainClassWrongType(Plugin::class)
+ )));
+ return null;
+ }
- $permManager = PermissionManager::getInstance();
- $opRoot = $permManager->getPermission(DefaultPermissions::ROOT_OPERATOR);
- $everyoneRoot = $permManager->getPermission(DefaultPermissions::ROOT_USER);
- foreach($description->getPermissions() as $default => $perms){
- foreach($perms as $perm){
- $permManager->addPermission($perm);
- switch($default){
- case PermissionParser::DEFAULT_TRUE:
- $everyoneRoot->addChild($perm->getName(), true);
- break;
- case PermissionParser::DEFAULT_OP:
- $opRoot->addChild($perm->getName(), true);
- break;
- case PermissionParser::DEFAULT_NOT_OP:
- //TODO: I don't think anyone uses this, and it currently relies on some magic inside PermissibleBase
- //to ensure that the operator override actually applies.
- //Explore getting rid of this.
- //The following grants this permission to anyone who has the "everyone" root permission.
- //However, if the operator root node (which has higher priority) is present, the
- //permission will be denied instead.
- $everyoneRoot->addChild($perm->getName(), true);
- $opRoot->addChild($perm->getName(), false);
- break;
- default:
- break;
- }
- }
- }
-
- /**
- * @var Plugin $plugin
- * @see Plugin::__construct()
- */
- $plugin = new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed, new DiskResourceProvider($prefixed . "/resources/"));
- $this->plugins[$plugin->getDescription()->getName()] = $plugin;
-
- return $plugin;
+ $permManager = PermissionManager::getInstance();
+ foreach($description->getPermissions() as $permsGroup){
+ foreach($permsGroup as $perm){
+ if($permManager->getPermission($perm->getName()) !== null){
+ $this->server->getLogger()->error($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $description->getName(),
+ KnownTranslationFactory::pocketmine_plugin_duplicatePermissionError($perm->getName())
+ )));
+ return null;
+ }
+ }
+ }
+ $opRoot = $permManager->getPermission(DefaultPermissions::ROOT_OPERATOR);
+ $everyoneRoot = $permManager->getPermission(DefaultPermissions::ROOT_USER);
+ foreach(Utils::stringifyKeys($description->getPermissions()) as $default => $perms){
+ foreach($perms as $perm){
+ $permManager->addPermission($perm);
+ switch($default){
+ case PermissionParser::DEFAULT_TRUE:
+ $everyoneRoot->addChild($perm->getName(), true);
+ break;
+ case PermissionParser::DEFAULT_OP:
+ $opRoot->addChild($perm->getName(), true);
+ break;
+ case PermissionParser::DEFAULT_NOT_OP:
+ //TODO: I don't think anyone uses this, and it currently relies on some magic inside PermissibleBase
+ //to ensure that the operator override actually applies.
+ //Explore getting rid of this.
+ //The following grants this permission to anyone who has the "everyone" root permission.
+ //However, if the operator root node (which has higher priority) is present, the
+ //permission will be denied instead.
+ $everyoneRoot->addChild($perm->getName(), true);
+ $opRoot->addChild($perm->getName(), false);
+ break;
+ default:
+ break;
}
}
}
- return null;
+ /**
+ * @var Plugin $plugin
+ * @see Plugin::__construct()
+ */
+ $plugin = new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed, new DiskResourceProvider($prefixed . "/resources/"));
+ $this->plugins[$plugin->getDescription()->getName()] = $plugin;
+
+ return $plugin;
}
/**
* @param string[]|null $newLoaders
* @phpstan-param list> $newLoaders
- *
- * @return Plugin[]
*/
- public function loadPlugins(string $directory, ?array $newLoaders = null) : array{
- if(!is_dir($directory)){
- return [];
- }
-
- $plugins = [];
- $loadedPlugins = [];
- $dependencies = [];
- $softDependencies = [];
+ private function triagePlugins(string $path, PluginLoadTriage $triage, ?array $newLoaders = null) : void{
if(is_array($newLoaders)){
$loaders = [];
foreach($newLoaders as $key){
@@ -235,8 +233,17 @@ class PluginManager{
$loaders = $this->fileAssociations;
}
- $files = iterator_to_array(new \FilesystemIterator($directory, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
- shuffle($files); //this prevents plugins implicitly relying on the filesystem name order when they should be using dependency properties
+ if(is_dir($path)){
+ $files = iterator_to_array(new \FilesystemIterator($path, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
+ shuffle($files); //this prevents plugins implicitly relying on the filesystem name order when they should be using dependency properties
+ }elseif(is_file($path)){
+ $realPath = Utils::assumeNotFalse(realpath($path), "realpath() should not return false on an accessible, existing file");
+ $files = [$realPath];
+ }else{
+ return;
+ }
+
+ $loadabilityChecker = new PluginLoadabilityChecker($this->server->getApiVersion());
foreach($loaders as $loader){
foreach($files as $file){
if(!is_string($file)) throw new AssumptionFailedError("FilesystemIterator current should be string when using CURRENT_AS_PATHNAME");
@@ -245,8 +252,14 @@ class PluginManager{
}
try{
$description = $loader->getPluginDescription($file);
+ }catch(PluginDescriptionParseException $e){
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $file,
+ KnownTranslationFactory::pocketmine_plugin_invalidManifest($e->getMessage())
+ )));
+ continue;
}catch(\RuntimeException $e){ //TODO: more specific exception handling
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_fileError($file, $directory, $e->getMessage())));
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($file, $e->getMessage())));
$this->server->getLogger()->logException($e);
continue;
}
@@ -255,142 +268,165 @@ class PluginManager{
}
$name = $description->getName();
- if(stripos($name, "pocketmine") !== false or stripos($name, "minecraft") !== false or stripos($name, "mojang") !== false){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, KnownTranslationFactory::pocketmine_plugin_restrictedName())));
+
+ if(($loadabilityError = $loadabilityChecker->check($description)) !== null){
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError)));
continue;
}
- if(strpos($name, " ") !== false){
- $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
- }
- if(isset($plugins[$name]) or $this->getPlugin($name) instanceof Plugin){
+ if(isset($triage->plugins[$name]) || $this->getPlugin($name) instanceof Plugin){
$this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_duplicateError($name)));
continue;
}
- if(!ApiVersion::isCompatible($this->server->getApiVersion(), $description->getCompatibleApis())){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
- $name,
- KnownTranslationFactory::pocketmine_plugin_incompatibleAPI(implode(", ", $description->getCompatibleApis()))
- )));
- continue;
- }
- $ambiguousVersions = ApiVersion::checkAmbiguousVersions($description->getCompatibleApis());
- if(count($ambiguousVersions) > 0){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
- $name,
- KnownTranslationFactory::pocketmine_plugin_ambiguousMinAPI(implode(", ", $ambiguousVersions))
- )));
- continue;
+ if(strpos($name, " ") !== false){
+ $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
}
- if(count($description->getCompatibleOperatingSystems()) > 0 and !in_array(Utils::getOS(), $description->getCompatibleOperatingSystems(), true)) {
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
- $name,
- KnownTranslationFactory::pocketmine_plugin_incompatibleOS(implode(", ", $description->getCompatibleOperatingSystems()))
- )));
- continue;
- }
-
- if(count($pluginMcpeProtocols = $description->getCompatibleMcpeProtocols()) > 0){
- $serverMcpeProtocols = [ProtocolInfo::CURRENT_PROTOCOL];
- if(count(array_intersect($pluginMcpeProtocols, $serverMcpeProtocols)) === 0){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
- $name,
- KnownTranslationFactory::pocketmine_plugin_incompatibleProtocol(implode(", ", $pluginMcpeProtocols))
- )));
- continue;
- }
- }
-
- if($this->graylist !== null and !$this->graylist->isAllowed($name)){
+ if($this->graylist !== null && !$this->graylist->isAllowed($name)){
$this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
$name,
- "Disallowed by graylist"
+ $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist()
)));
continue;
}
- $plugins[$name] = $file;
- $softDependencies[$name] = array_merge($softDependencies[$name] ?? [], $description->getSoftDepend());
- $dependencies[$name] = $description->getDepend();
+ $triage->plugins[$name] = new PluginLoadTriageEntry($file, $loader, $description);
+
+ $triage->softDependencies[$name] = array_merge($triage->softDependencies[$name] ?? [], $description->getSoftDepend());
+ $triage->dependencies[$name] = $description->getDepend();
foreach($description->getLoadBefore() as $before){
- if(isset($softDependencies[$before])){
- $softDependencies[$before][] = $name;
+ if(isset($triage->softDependencies[$before])){
+ $triage->softDependencies[$before][] = $name;
}else{
- $softDependencies[$before] = [$name];
+ $triage->softDependencies[$before] = [$name];
}
}
}
}
+ }
- while(count($plugins) > 0){
+ /**
+ * @param string[][] $dependencyLists
+ * @param Plugin[] $loadedPlugins
+ */
+ private function checkDepsForTriage(string $pluginName, string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{
+ if(isset($dependencyLists[$pluginName])){
+ foreach($dependencyLists[$pluginName] as $key => $dependency){
+ if(isset($loadedPlugins[$dependency]) || $this->getPlugin($dependency) instanceof Plugin){
+ $this->server->getLogger()->debug("Successfully resolved $dependencyType dependency \"$dependency\" for plugin \"$pluginName\"");
+ unset($dependencyLists[$pluginName][$key]);
+ }elseif(array_key_exists($dependency, $triage->plugins)){
+ $this->server->getLogger()->debug("Deferring resolution of $dependencyType dependency \"$dependency\" for plugin \"$pluginName\" (found but not loaded yet)");
+ }
+ }
+
+ if(count($dependencyLists[$pluginName]) === 0){
+ unset($dependencyLists[$pluginName]);
+ }
+ }
+ }
+
+ /**
+ * @return Plugin[]
+ */
+ public function loadPlugins(string $path) : array{
+ if($this->loadPluginsGuard){
+ throw new \LogicException(__METHOD__ . "() cannot be called from within itself");
+ }
+ $this->loadPluginsGuard = true;
+
+ $triage = new PluginLoadTriage();
+ $this->triagePlugins($path, $triage);
+
+ $loadedPlugins = [];
+
+ while(count($triage->plugins) > 0){
$loadedThisLoop = 0;
- foreach($plugins as $name => $file){
- if(isset($dependencies[$name])){
- foreach($dependencies[$name] as $key => $dependency){
- if(isset($loadedPlugins[$dependency]) or $this->getPlugin($dependency) instanceof Plugin){
- unset($dependencies[$name][$key]);
- }elseif(!isset($plugins[$dependency])){
- $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
- $name,
- KnownTranslationFactory::pocketmine_plugin_unknownDependency($dependency)
- )));
- unset($plugins[$name]);
- continue 2;
- }
- }
+ foreach(Utils::stringifyKeys($triage->plugins) as $name => $entry){
+ $this->checkDepsForTriage($name, "hard", $triage->dependencies, $loadedPlugins, $triage);
+ $this->checkDepsForTriage($name, "soft", $triage->softDependencies, $loadedPlugins, $triage);
- if(count($dependencies[$name]) === 0){
- unset($dependencies[$name]);
- }
- }
-
- if(isset($softDependencies[$name])){
- foreach($softDependencies[$name] as $key => $dependency){
- if(isset($loadedPlugins[$dependency]) or $this->getPlugin($dependency) instanceof Plugin){
- $this->server->getLogger()->debug("Successfully resolved soft dependency \"$dependency\" for plugin \"$name\"");
- unset($softDependencies[$name][$key]);
- }elseif(!isset($plugins[$dependency])){
- //this dependency is never going to be resolved, so don't bother trying
- $this->server->getLogger()->debug("Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\"");
- unset($softDependencies[$name][$key]);
- }else{
- $this->server->getLogger()->debug("Deferring resolution of soft dependency \"$dependency\" for plugin \"$name\" (found but not loaded yet)");
- }
- }
-
- if(count($softDependencies[$name]) === 0){
- unset($softDependencies[$name]);
- }
- }
-
- if(!isset($dependencies[$name]) and !isset($softDependencies[$name])){
- unset($plugins[$name]);
+ if(!isset($triage->dependencies[$name]) && !isset($triage->softDependencies[$name])){
+ unset($triage->plugins[$name]);
$loadedThisLoop++;
- if(($plugin = $this->loadPlugin($file, $loaders)) instanceof Plugin){
+
+ $oldRegisteredLoaders = $this->fileAssociations;
+ if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof Plugin){
$loadedPlugins[$name] = $plugin;
- }else{
- $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_genericLoadError($name)));
+ $diffLoaders = [];
+ foreach($this->fileAssociations as $k => $loader){
+ if(!array_key_exists($k, $oldRegisteredLoaders)){
+ $diffLoaders[] = $k;
+ }
+ }
+ if(count($diffLoaders) !== 0){
+ $this->server->getLogger()->debug("Plugin $name registered a new plugin loader during load, scanning for new plugins");
+ $plugins = $triage->plugins;
+ $this->triagePlugins($path, $triage, $diffLoaders);
+ $diffPlugins = array_diff_key($triage->plugins, $plugins);
+ $this->server->getLogger()->debug("Re-triage found plugins: " . implode(", ", array_keys($diffPlugins)));
+ }
}
}
}
if($loadedThisLoop === 0){
//No plugins loaded :(
- foreach($plugins as $name => $file){
+
+ //check for skippable soft dependencies first, in case the dependents could resolve hard dependencies
+ foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
+ if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){
+ foreach($triage->softDependencies[$name] as $k => $dependency){
+ if($this->getPlugin($dependency) === null && !array_key_exists($dependency, $triage->plugins)){
+ $this->server->getLogger()->debug("Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\"");
+ unset($triage->softDependencies[$name][$k]);
+ }
+ }
+ if(count($triage->softDependencies[$name]) === 0){
+ unset($triage->softDependencies[$name]);
+ continue 2; //go back to the top and try again
+ }
+ }
+ }
+
+ foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
+ if(isset($triage->dependencies[$name])){
+ $unknownDependencies = [];
+
+ foreach($triage->dependencies[$name] as $k => $dependency){
+ if($this->getPlugin($dependency) === null && !array_key_exists($dependency, $triage->plugins)){
+ //assume that the plugin is never going to be loaded
+ //by this point all soft dependencies have been ignored if they were able to be, so
+ //there's no chance of this dependency ever being resolved
+ $unknownDependencies[$dependency] = $dependency;
+ }
+ }
+
+ if(count($unknownDependencies) > 0){
+ $this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
+ $name,
+ KnownTranslationFactory::pocketmine_plugin_unknownDependency(implode(", ", $unknownDependencies))
+ )));
+ unset($triage->plugins[$name]);
+ }
+ }
+ }
+
+ foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
$this->server->getLogger()->critical($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, KnownTranslationFactory::pocketmine_plugin_circularDependency())));
}
- $plugins = [];
+ break;
}
}
+ $this->loadPluginsGuard = false;
return $loadedPlugins;
}
public function isPluginEnabled(Plugin $plugin) : bool{
- return isset($this->plugins[$plugin->getDescription()->getName()]) and $plugin->isEnabled();
+ return isset($this->plugins[$plugin->getDescription()->getName()]) && $plugin->isEnabled();
}
public function enablePlugin(Plugin $plugin) : void{
@@ -449,7 +485,7 @@ class PluginManager{
* @phpstan-return class-string|null
*/
private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
- if($method->isStatic() or !$method->getDeclaringClass()->implementsInterface(Listener::class)){
+ if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
return null;
}
$tags = Utils::parseDocComment((string) $method->getDocComment());
@@ -506,6 +542,14 @@ class PluginManager{
$handleCancelled = false;
if(isset($tags[ListenerMethodTags::HANDLE_CANCELLED])){
+ if(!is_a($eventClass, Cancellable::class, true)){
+ throw new PluginException(sprintf(
+ "Event handler %s() declares @%s for non-cancellable event of type %s",
+ Utils::getNiceClosureName($handlerClosure),
+ ListenerMethodTags::HANDLE_CANCELLED,
+ $eventClass
+ ));
+ }
switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
case "true":
case "":
diff --git a/src/plugin/ScriptPluginLoader.php b/src/plugin/ScriptPluginLoader.php
index ea9d4c22d..ccf4c8a01 100644
--- a/src/plugin/ScriptPluginLoader.php
+++ b/src/plugin/ScriptPluginLoader.php
@@ -23,13 +23,14 @@ declare(strict_types=1);
namespace pocketmine\plugin;
+use pocketmine\utils\Utils;
+use function count;
use function file;
+use function implode;
use function is_file;
-use function preg_match;
use function strlen;
use function strpos;
use function substr;
-use function trim;
use const FILE_IGNORE_NEW_LINES;
use const FILE_SKIP_EMPTY_LINES;
@@ -41,7 +42,7 @@ class ScriptPluginLoader implements PluginLoader{
public function canLoadPlugin(string $path) : bool{
$ext = ".php";
- return is_file($path) and substr($path, -strlen($ext)) === $ext;
+ return is_file($path) && substr($path, -strlen($ext)) === $ext;
}
/**
@@ -60,30 +61,27 @@ class ScriptPluginLoader implements PluginLoader{
return null;
}
- $data = [];
-
$insideHeader = false;
+
+ $docCommentLines = [];
foreach($content as $line){
- if(!$insideHeader and strpos($line, "/**") !== false){
- $insideHeader = true;
- }
-
- if(preg_match("/^[ \t]+\\*[ \t]+@([a-zA-Z]+)([ \t]+(.*))?$/", $line, $matches) > 0){
- $key = $matches[1];
- $content = trim($matches[3] ?? "");
-
- if($key === "notscript"){
- return null;
+ if(!$insideHeader){
+ if(strpos($line, "/**") !== false){
+ $insideHeader = true;
+ }else{
+ continue;
}
-
- $data[$key] = $content;
}
- if($insideHeader and strpos($line, "*/") !== false){
+ $docCommentLines[] = $line;
+
+ if(strpos($line, "*/") !== false){
break;
}
}
- if($insideHeader){
+
+ $data = Utils::parseDocComment(implode("\n", $docCommentLines));
+ if(count($data) !== 0){
return new PluginDescription($data);
}
diff --git a/src/promise/Promise.php b/src/promise/Promise.php
new file mode 100644
index 000000000..5b036cdd3
--- /dev/null
+++ b/src/promise/Promise.php
@@ -0,0 +1,55 @@
+promise()
+ * @see PromiseResolver
+ * @phpstan-param PromiseSharedData $shared
+ */
+ public function __construct(private PromiseSharedData $shared){}
+
+ /**
+ * @phpstan-param \Closure(TValue) : void $onSuccess
+ * @phpstan-param \Closure() : void $onFailure
+ */
+ public function onCompletion(\Closure $onSuccess, \Closure $onFailure) : void{
+ if($this->shared->resolved){
+ $this->shared->result === null ? $onFailure() : $onSuccess($this->shared->result);
+ }else{
+ $this->shared->onSuccess[spl_object_id($onSuccess)] = $onSuccess;
+ $this->shared->onFailure[spl_object_id($onFailure)] = $onFailure;
+ }
+ }
+
+ public function isResolved() : bool{
+ return $this->shared->resolved;
+ }
+}
diff --git a/src/promise/PromiseResolver.php b/src/promise/PromiseResolver.php
new file mode 100644
index 000000000..aca3b6b89
--- /dev/null
+++ b/src/promise/PromiseResolver.php
@@ -0,0 +1,75 @@
+ */
+ private PromiseSharedData $shared;
+ /** @phpstan-var Promise */
+ private Promise $promise;
+
+ public function __construct(){
+ $this->shared = new PromiseSharedData();
+ $this->promise = new Promise($this->shared);
+ }
+
+ /**
+ * @param mixed $value
+ * @phpstan-param TValue $value
+ */
+ public function resolve($value) : void{
+ if($this->shared->resolved){
+ throw new \LogicException("Promise has already been resolved/rejected");
+ }
+ $this->shared->resolved = true;
+ $this->shared->result = $value;
+ foreach($this->shared->onSuccess as $c){
+ $c($value);
+ }
+ $this->shared->onSuccess = [];
+ $this->shared->onFailure = [];
+ }
+
+ public function reject() : void{
+ if($this->shared->resolved){
+ throw new \LogicException("Promise has already been resolved/rejected");
+ }
+ $this->shared->resolved = true;
+ foreach($this->shared->onFailure as $c){
+ $c();
+ }
+ $this->shared->onSuccess = [];
+ $this->shared->onFailure = [];
+ }
+
+ /**
+ * @phpstan-return Promise
+ */
+ public function getPromise() : Promise{
+ return $this->promise;
+ }
+}
diff --git a/src/promise/PromiseSharedData.php b/src/promise/PromiseSharedData.php
new file mode 100644
index 000000000..bccf56cc2
--- /dev/null
+++ b/src/promise/PromiseSharedData.php
@@ -0,0 +1,51 @@
+
+ */
+ public array $onSuccess = [];
+
+ /**
+ * @var \Closure[]
+ * @phpstan-var array
+ */
+ public array $onFailure = [];
+
+ public bool $resolved = false;
+
+ /**
+ * @var mixed
+ * @phpstan-var TValue|null
+ */
+ public $result = null;
+}
diff --git a/src/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php
index 08b9a90ae..657df537d 100644
--- a/src/resourcepacks/ResourcePackManager.php
+++ b/src/resourcepacks/ResourcePackManager.php
@@ -32,6 +32,8 @@ use function file_exists;
use function gettype;
use function is_array;
use function is_dir;
+use function is_float;
+use function is_int;
use function is_string;
use function mkdir;
use function strtolower;
@@ -81,10 +83,11 @@ class ResourcePackManager{
}
foreach($resourceStack as $pos => $pack){
- if(!is_string($pack)){
+ if(!is_string($pack) && !is_int($pack) && !is_float($pack)){
$logger->critical("Found invalid entry in resource pack list at offset $pos of type " . gettype($pack));
continue;
}
+ $pack = (string) $pack;
try{
$packPath = Path::join($this->path, $pack);
if(!file_exists($packPath)){
diff --git a/src/resourcepacks/ZippedResourcePack.php b/src/resourcepacks/ZippedResourcePack.php
index 9f2593c84..347a197dd 100644
--- a/src/resourcepacks/ZippedResourcePack.php
+++ b/src/resourcepacks/ZippedResourcePack.php
@@ -25,6 +25,7 @@ namespace pocketmine\resourcepacks;
use Ahc\Json\Comment as CommentedJsonDecoder;
use pocketmine\resourcepacks\json\Manifest;
+use pocketmine\utils\Utils;
use function assert;
use function fclose;
use function feof;
@@ -73,9 +74,9 @@ class ZippedResourcePack implements ResourcePack{
$manifestPath = null;
$manifestIdx = null;
for($i = 0; $i < $archive->numFiles; ++$i){
- $name = $archive->getNameIndex($i);
+ $name = Utils::assumeNotFalse($archive->getNameIndex($i), "This index should be valid");
if(
- ($manifestPath === null or strlen($name) < strlen($manifestPath)) and
+ ($manifestPath === null || strlen($name) < strlen($manifestPath)) &&
preg_match('#.*/manifest.json$#', $name) === 1
){
$manifestPath = $name;
@@ -145,7 +146,7 @@ class ZippedResourcePack implements ResourcePack{
}
public function getSha256(bool $cached = true) : string{
- if($this->sha256 === null or !$cached){
+ if($this->sha256 === null || !$cached){
$this->sha256 = hash_file("sha256", $this->path, true);
}
return $this->sha256;
@@ -156,6 +157,6 @@ class ZippedResourcePack implements ResourcePack{
if(feof($this->fileResource)){
throw new \InvalidArgumentException("Requested a resource pack chunk with invalid start offset");
}
- return fread($this->fileResource, $length);
+ return Utils::assumeNotFalse(fread($this->fileResource, $length), "Already checked that we're not at EOF");
}
}
diff --git a/src/resourcepacks/json/Manifest.php b/src/resourcepacks/json/Manifest.php
index 31c68973e..cd6f1c913 100644
--- a/src/resourcepacks/json/Manifest.php
+++ b/src/resourcepacks/json/Manifest.php
@@ -40,4 +40,10 @@ final class Manifest{
public array $modules;
public ?ManifestMetadata $metadata = null;
+
+ /** @var string[] */
+ public ?array $capabilities = null;
+
+ /** @var ManifestDependencyEntry[] */
+ public ?array $dependencies = null;
}
diff --git a/src/resourcepacks/json/ManifestDependencyEntry.php b/src/resourcepacks/json/ManifestDependencyEntry.php
new file mode 100644
index 000000000..f7dd76abc
--- /dev/null
+++ b/src/resourcepacks/json/ManifestDependencyEntry.php
@@ -0,0 +1,37 @@
+= $this->size){
+ if($worker < 0 || $worker >= $this->size){
throw new \InvalidArgumentException("Invalid worker $worker");
}
if($task->isSubmitted()){
@@ -197,7 +197,7 @@ class AsyncPool{
}
}
}
- if($worker === null or ($minUsage > 0 and count($this->workers) < $this->size)){
+ if($worker === null || ($minUsage > 0 && count($this->workers) < $this->size)){
//select a worker to start on the fly
for($i = 0; $i < $this->size; ++$i){
if(!isset($this->workers[$i])){
@@ -297,7 +297,7 @@ class AsyncPool{
$ret = 0;
$time = time();
foreach($this->taskQueues as $i => $queue){
- if((!isset($this->workerLastUsed[$i]) or $this->workerLastUsed[$i] + 300 < $time) and $queue->isEmpty()){
+ if((!isset($this->workerLastUsed[$i]) || $this->workerLastUsed[$i] + 300 < $time) && $queue->isEmpty()){
$this->workers[$i]->quit();
$this->eventLoop->removeNotifier($this->workers[$i]->getNotifier());
unset($this->workers[$i], $this->taskQueues[$i], $this->workerLastUsed[$i]);
diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php
index 2f1bedc6b..ea70364b9 100644
--- a/src/scheduler/AsyncTask.php
+++ b/src/scheduler/AsyncTask.php
@@ -93,7 +93,7 @@ abstract class AsyncTask extends \Threaded{
}
public function isCrashed() : bool{
- return $this->crashed or $this->isTerminated();
+ return $this->crashed || $this->isTerminated();
}
/**
@@ -101,7 +101,7 @@ abstract class AsyncTask extends \Threaded{
* because it is not true prior to task execution.
*/
public function isFinished() : bool{
- return $this->finished or $this->isCrashed();
+ return $this->finished || $this->isCrashed();
}
public function hasResult() : bool{
@@ -236,7 +236,7 @@ abstract class AsyncTask extends \Threaded{
*/
protected function fetchLocal(string $key){
$id = spl_object_id($this);
- if(self::$threadLocalStorage === null or !isset(self::$threadLocalStorage[$id][$key])){
+ if(self::$threadLocalStorage === null || !isset(self::$threadLocalStorage[$id][$key])){
throw new \InvalidArgumentException("No matching thread-local data found on this thread");
}
@@ -245,7 +245,7 @@ abstract class AsyncTask extends \Threaded{
final public function __destruct(){
$this->reallyDestruct();
- if(self::$threadLocalStorage !== null and isset(self::$threadLocalStorage[$h = spl_object_id($this)])){
+ if(self::$threadLocalStorage !== null && isset(self::$threadLocalStorage[$h = spl_object_id($this)])){
unset(self::$threadLocalStorage[$h]);
if(self::$threadLocalStorage->count() === 0){
self::$threadLocalStorage = null;
diff --git a/src/scheduler/AsyncWorker.php b/src/scheduler/AsyncWorker.php
index eedb7ea5c..3dc8c2e68 100644
--- a/src/scheduler/AsyncWorker.php
+++ b/src/scheduler/AsyncWorker.php
@@ -92,7 +92,7 @@ class AsyncWorker extends Worker{
*/
public function saveToThreadStore(string $identifier, $value) : void{
if(\Thread::getCurrentThread() !== $this){
- throw new \InvalidStateException("Thread-local data can only be stored in the thread context");
+ throw new \LogicException("Thread-local data can only be stored in the thread context");
}
self::$store[$identifier] = $value;
}
@@ -109,7 +109,7 @@ class AsyncWorker extends Worker{
*/
public function getFromThreadStore(string $identifier){
if(\Thread::getCurrentThread() !== $this){
- throw new \InvalidStateException("Thread-local data can only be fetched in the thread context");
+ throw new \LogicException("Thread-local data can only be fetched in the thread context");
}
return self::$store[$identifier] ?? null;
}
@@ -119,7 +119,7 @@ class AsyncWorker extends Worker{
*/
public function removeFromThreadStore(string $identifier) : void{
if(\Thread::getCurrentThread() !== $this){
- throw new \InvalidStateException("Thread-local data can only be removed in the thread context");
+ throw new \LogicException("Thread-local data can only be removed in the thread context");
}
unset(self::$store[$identifier]);
}
diff --git a/src/scheduler/ClosureTask.php b/src/scheduler/ClosureTask.php
index 45e30212e..74bf85a00 100644
--- a/src/scheduler/ClosureTask.php
+++ b/src/scheduler/ClosureTask.php
@@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\scheduler;
+use DaveRandom\CallbackValidator\CallbackType;
+use DaveRandom\CallbackValidator\ReturnType;
use pocketmine\utils\Utils;
/**
@@ -49,7 +51,7 @@ class ClosureTask extends Task{
* @phpstan-param \Closure() : void $closure
*/
public function __construct(\Closure $closure){
- Utils::validateCallableSignature(function() : void{}, $closure);
+ Utils::validateCallableSignature(new CallbackType(new ReturnType()), $closure);
$this->closure = $closure;
}
diff --git a/src/scheduler/Task.php b/src/scheduler/Task.php
index 0c4ea4469..4ad4c7e39 100644
--- a/src/scheduler/Task.php
+++ b/src/scheduler/Task.php
@@ -42,7 +42,7 @@ abstract class Task{
}
final public function setHandler(?TaskHandler $taskHandler) : void{
- if($this->taskHandler === null or $taskHandler === null){
+ if($this->taskHandler === null || $taskHandler === null){
$this->taskHandler = $taskHandler;
}
}
diff --git a/src/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php
index 22317573d..f66bd857a 100644
--- a/src/scheduler/TaskScheduler.php
+++ b/src/scheduler/TaskScheduler.php
@@ -88,12 +88,9 @@ class TaskScheduler{
return $this->tasks->contains($task);
}
- /**
- * @throws \InvalidStateException
- */
private function addTask(Task $task, int $delay, int $period) : TaskHandler{
if(!$this->enabled){
- throw new \InvalidStateException("Tried to schedule task to disabled scheduler");
+ throw new \LogicException("Tried to schedule task to disabled scheduler");
}
if($delay <= 0){
@@ -153,6 +150,6 @@ class TaskScheduler{
}
private function isReady(int $currentTick) : bool{
- return !$this->queue->isEmpty() and $this->queue->current()->getNextRun() <= $currentTick;
+ return !$this->queue->isEmpty() && $this->queue->current()->getNextRun() <= $currentTick;
}
}
diff --git a/src/stats/SendUsageTask.php b/src/stats/SendUsageTask.php
index d108ca94a..2064caf08 100644
--- a/src/stats/SendUsageTask.php
+++ b/src/stats/SendUsageTask.php
@@ -27,7 +27,6 @@ use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\player\Player;
use pocketmine\scheduler\AsyncTask;
use pocketmine\Server;
-use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Internet;
use pocketmine\utils\Process;
use pocketmine\utils\Utils;
@@ -37,11 +36,11 @@ use function array_map;
use function array_values;
use function count;
use function json_encode;
-use function json_last_error_msg;
use function md5;
use function microtime;
use function php_uname;
use function strlen;
+use const JSON_THROW_ON_ERROR;
use const PHP_VERSION;
class SendUsageTask extends AsyncTask{
@@ -123,9 +122,7 @@ class SendUsageTask extends AsyncTask{
];
//This anonymizes the user ids so they cannot be reversed to the original
- foreach($playerList as $k => $v){
- $playerList[$k] = md5($v);
- }
+ $playerList = array_map('md5', $playerList);
$players = array_map(function(Player $p) : string{ return md5($p->getUniqueId()->getBytes()); }, $server->getOnlinePlayers());
@@ -152,9 +149,7 @@ class SendUsageTask extends AsyncTask{
}
$this->endpoint = $endpoint . "api/post";
- $data = json_encode($data/*, JSON_PRETTY_PRINT*/);
- if($data === false) throw new AssumptionFailedError("Statistics JSON should never fail to encode: " . json_last_error_msg());
- $this->data = $data;
+ $this->data = json_encode($data, /*JSON_PRETTY_PRINT |*/ JSON_THROW_ON_ERROR);
}
public function onRun() : void{
diff --git a/src/thread/ThreadManager.php b/src/thread/ThreadManager.php
index 56f36c8f0..2b1ca58cc 100644
--- a/src/thread/ThreadManager.php
+++ b/src/thread/ThreadManager.php
@@ -45,7 +45,7 @@ class ThreadManager extends \Volatile{
* @param Worker|Thread $thread
*/
public function add($thread) : void{
- if($thread instanceof Thread or $thread instanceof Worker){
+ if($thread instanceof Thread || $thread instanceof Worker){
$this[spl_object_id($thread)] = $thread;
}
}
@@ -54,7 +54,7 @@ class ThreadManager extends \Volatile{
* @param Worker|Thread $thread
*/
public function remove($thread) : void{
- if($thread instanceof Thread or $thread instanceof Worker){
+ if($thread instanceof Thread || $thread instanceof Worker){
unset($this[spl_object_id($thread)]);
}
}
diff --git a/src/thread/Worker.php b/src/thread/Worker.php
index e710787cc..e504165ff 100644
--- a/src/thread/Worker.php
+++ b/src/thread/Worker.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\thread;
+use pocketmine\scheduler\AsyncTask;
use const PTHREADS_INHERIT_NONE;
/**
diff --git a/src/updater/UpdateCheckTask.php b/src/updater/UpdateCheckTask.php
index 6b571b2fe..fb308eec3 100644
--- a/src/updater/UpdateCheckTask.php
+++ b/src/updater/UpdateCheckTask.php
@@ -53,7 +53,7 @@ class UpdateCheckTask extends AsyncTask{
if($response !== null){
$response = json_decode($response->getBody(), true);
if(is_array($response)){
- if(isset($response["error"]) and is_string($response["error"])){
+ if(isset($response["error"]) && is_string($response["error"])){
$this->error = $response["error"];
}else{
$mapper = new \JsonMapper();
diff --git a/src/updater/UpdateChecker.php b/src/updater/UpdateChecker.php
index f7459a1ce..9c530dedb 100644
--- a/src/updater/UpdateChecker.php
+++ b/src/updater/UpdateChecker.php
@@ -68,9 +68,9 @@ class UpdateChecker{
$this->showConsoleUpdate();
}
}else{
- if(!VersionInfo::IS_DEVELOPMENT_BUILD and $this->getChannel() !== "stable"){
+ if(!VersionInfo::IS_DEVELOPMENT_BUILD && $this->getChannel() !== "stable"){
$this->showChannelSuggestionStable();
- }elseif(VersionInfo::IS_DEVELOPMENT_BUILD and $this->getChannel() === "stable"){
+ }elseif(VersionInfo::IS_DEVELOPMENT_BUILD && $this->getChannel() === "stable"){
$this->showChannelSuggestionBeta();
}
}
@@ -153,6 +153,8 @@ class UpdateChecker{
if($currentVersion->getBuild() > 0 && $currentVersion->compare($newVersion) > 0){
$this->updateInfo = $updateInfo;
+ }else{
+ $this->logger->debug("API reported version is an older version or the same version (" . $newVersion->getFullVersion() . "), not showing notification");
}
}
@@ -160,12 +162,7 @@ class UpdateChecker{
* Returns the channel used for update checking (stable, beta, dev)
*/
public function getChannel() : string{
- $channel = strtolower($this->server->getConfigGroup()->getPropertyString("auto-updater.preferred-channel", "stable"));
- if($channel !== "stable" and $channel !== "beta" and $channel !== "alpha" and $channel !== "development"){
- $channel = "stable";
- }
-
- return $channel;
+ return strtolower($this->server->getConfigGroup()->getPropertyString("auto-updater.preferred-channel", "stable"));
}
/**
diff --git a/src/utils/Config.php b/src/utils/Config.php
index 841b7f860..f15bfe1e5 100644
--- a/src/utils/Config.php
+++ b/src/utils/Config.php
@@ -23,8 +23,10 @@ declare(strict_types=1);
namespace pocketmine\utils;
+use pocketmine\errorhandler\ErrorToExceptionHandler;
use Webmozart\PathUtil\Path;
use function array_change_key_case;
+use function array_fill_keys;
use function array_keys;
use function array_shift;
use function count;
@@ -32,7 +34,7 @@ use function date;
use function explode;
use function file_exists;
use function file_get_contents;
-use function file_put_contents;
+use function get_debug_type;
use function implode;
use function is_array;
use function is_bool;
@@ -52,6 +54,7 @@ use function yaml_parse;
use const CASE_LOWER;
use const JSON_BIGINT_AS_STRING;
use const JSON_PRETTY_PRINT;
+use const JSON_THROW_ON_ERROR;
/**
* Config Class for simple config manipulation of multiple formats.
@@ -143,8 +146,7 @@ class Config{
* @param mixed[] $default
* @phpstan-param array $default
*
- * @throws \InvalidArgumentException if config type could not be auto-detected
- * @throws \InvalidStateException if config type is invalid
+ * @throws \InvalidArgumentException if config type is invalid or could not be auto-detected
*/
private function load(string $file, int $type = Config::DETECT, array $default = []) : void{
$this->file = $file;
@@ -167,28 +169,42 @@ class Config{
if($content === false){
throw new \RuntimeException("Unable to load config file");
}
- $config = null;
switch($this->type){
case Config::PROPERTIES:
- $config = $this->parseProperties($content);
+ $config = self::parseProperties($content);
break;
case Config::JSON:
- $config = json_decode($content, true);
+ try{
+ $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
+ }catch(\JsonException $e){
+ throw ConfigLoadException::wrap($this->file, $e);
+ }
break;
case Config::YAML:
$content = self::fixYAMLIndexes($content);
- $config = yaml_parse($content);
+ try{
+ $config = ErrorToExceptionHandler::trap(fn() => yaml_parse($content));
+ }catch(\ErrorException $e){
+ throw ConfigLoadException::wrap($this->file, $e);
+ }
break;
case Config::SERIALIZED:
- $config = unserialize($content);
+ try{
+ $config = ErrorToExceptionHandler::trap(fn() => unserialize($content));
+ }catch(\ErrorException $e){
+ throw ConfigLoadException::wrap($this->file, $e);
+ }
break;
case Config::ENUM:
- $config = self::parseList($content);
+ $config = array_fill_keys(self::parseList($content), true);
break;
default:
- throw new \InvalidStateException("Config type is unknown");
+ throw new \InvalidArgumentException("Invalid config type specified");
}
- $this->config = is_array($config) ? $config : $default;
+ if(!is_array($config)){
+ throw new ConfigLoadException("Failed to load config $this->file: Expected array for base type, but got " . get_debug_type($config));
+ }
+ $this->config = $config;
if($this->fillDefaults($default, $this->config) > 0){
$this->save();
}
@@ -204,17 +220,15 @@ class Config{
/**
* Flushes the config to disk in the appropriate format.
- *
- * @throws \InvalidStateException if config type is not valid
*/
public function save() : void{
$content = null;
switch($this->type){
case Config::PROPERTIES:
- $content = $this->writeProperties();
+ $content = self::writeProperties($this->config);
break;
case Config::JSON:
- $content = json_encode($this->config, $this->jsonOptions);
+ $content = json_encode($this->config, $this->jsonOptions | JSON_THROW_ON_ERROR);
break;
case Config::YAML:
$content = yaml_emit($this->config, YAML_UTF8_ENCODING);
@@ -223,13 +237,13 @@ class Config{
$content = serialize($this->config);
break;
case Config::ENUM:
- $content = implode("\r\n", array_keys($this->config));
+ $content = self::writeList(array_keys($this->config));
break;
default:
- throw new \InvalidStateException("Config type is unknown, has not been set or not detected");
+ throw new AssumptionFailedError("Config type is unknown, has not been set or not detected");
}
- file_put_contents($this->file, $content);
+ Filesystem::safeFilePutContents($this->file, $content);
$this->changed = false;
}
@@ -343,14 +357,14 @@ class Config{
$this->config[$base] = [];
}
- $base =& $this->config[$base];
+ $base = &$this->config[$base];
while(count($vars) > 0){
$baseKey = array_shift($vars);
if(!isset($base[$baseKey])){
$base[$baseKey] = [];
}
- $base =& $base[$baseKey];
+ $base = &$base[$baseKey];
}
$base = $value;
@@ -379,7 +393,7 @@ class Config{
while(count($vars) > 0){
$baseKey = array_shift($vars);
- if(is_array($base) and isset($base[$baseKey])){
+ if(is_array($base) && isset($base[$baseKey])){
$base = $base[$baseKey];
}else{
return $default;
@@ -395,14 +409,14 @@ class Config{
$vars = explode(".", $key);
- $currentNode =& $this->config;
+ $currentNode = &$this->config;
while(count($vars) > 0){
$nodeName = array_shift($vars);
if(isset($currentNode[$nodeName])){
if(count($vars) === 0){ //final node
unset($currentNode[$nodeName]);
}elseif(is_array($currentNode[$nodeName])){
- $currentNode =& $currentNode[$nodeName];
+ $currentNode = &$currentNode[$nodeName];
}
}else{
break;
@@ -427,7 +441,7 @@ class Config{
public function set($k, $v = true) : void{
$this->config[$k] = $v;
$this->changed = true;
- foreach($this->nestedCache as $nestedKey => $nvalue){
+ foreach(Utils::stringifyKeys($this->nestedCache) as $nestedKey => $nvalue){
if(substr($nestedKey, 0, strlen($k) + 1) === ($k . ".")){
unset($this->nestedCache[$nestedKey]);
}
@@ -489,9 +503,9 @@ class Config{
*/
private function fillDefaults(array $default, &$data) : int{
$changed = 0;
- foreach($default as $k => $v){
+ foreach(Utils::stringifyKeys($default) as $k => $v){
if(is_array($v)){
- if(!isset($data[$k]) or !is_array($data[$k])){
+ if(!isset($data[$k]) || !is_array($data[$k])){
$data[$k] = [];
}
$changed += $this->fillDefaults($v, $data[$k]);
@@ -509,28 +523,38 @@ class Config{
}
/**
- * @return true[]
- * @phpstan-return array
+ * @return string[]
+ * @phpstan-return list
*/
- private static function parseList(string $content) : array{
+ public static function parseList(string $content) : array{
$result = [];
foreach(explode("\n", trim(str_replace("\r\n", "\n", $content))) as $v){
$v = trim($v);
- if($v == ""){
+ if($v === ""){
continue;
}
- $result[$v] = true;
+ $result[] = $v;
}
return $result;
}
- private function writeProperties() : string{
+ /**
+ * @param string[] $entries
+ * @phpstan-param list $entries
+ */
+ public static function writeList(array $entries) : string{
+ return implode("\n", $entries);
+ }
+
+ /**
+ * @param string[]|int[]|float[]|bool[] $config
+ * @phpstan-param array $config
+ */
+ public static function writeProperties(array $config) : string{
$content = "#Properties Config file\r\n#" . date("D M j H:i:s T Y") . "\r\n";
- foreach($this->config as $k => $v){
+ foreach(Utils::stringifyKeys($config) as $k => $v){
if(is_bool($v)){
$v = $v ? "on" : "off";
- }elseif(is_array($v)){
- $v = implode(";", $v);
}
$content .= $k . "=" . $v . "\r\n";
}
@@ -539,9 +563,10 @@ class Config{
}
/**
- * @return mixed[]
+ * @return string[]|int[]|float[]|bool[]
+ * @phpstan-return array
*/
- private function parseProperties(string $content) : array{
+ public static function parseProperties(string $content) : array{
$result = [];
if(preg_match_all('/^\s*([a-zA-Z0-9\-_\.]+)[ \t]*=([^\r\n]*)/um', $content, $matches) > 0){ //false or 0 matches
foreach($matches[1] as $i => $k){
@@ -557,11 +582,15 @@ class Config{
case "no":
$v = false;
break;
+ default:
+ $v = match($v){
+ (string) ((int) $v) => (int) $v,
+ (string) ((float) $v) => (float) $v,
+ default => $v,
+ };
+ break;
}
- if(isset($result[$k])){
- \GlobalLogger::get()->debug("[Config] Repeated property " . $k . " on file " . $this->file);
- }
- $result[$k] = $v;
+ $result[(string) $k] = $v;
}
}
diff --git a/src/utils/ConfigLoadException.php b/src/utils/ConfigLoadException.php
new file mode 100644
index 000000000..a4be1da3d
--- /dev/null
+++ b/src/utils/ConfigLoadException.php
@@ -0,0 +1,31 @@
+getMessage(), 0, $e);
+ }
+}
diff --git a/src/utils/Filesystem.php b/src/utils/Filesystem.php
index 9a89976e6..56d45790e 100644
--- a/src/utils/Filesystem.php
+++ b/src/utils/Filesystem.php
@@ -23,12 +23,14 @@ declare(strict_types=1);
namespace pocketmine\utils;
+use pocketmine\errorhandler\ErrorToExceptionHandler;
use Webmozart\PathUtil\Path;
use function copy;
use function dirname;
use function fclose;
use function fflush;
use function file_exists;
+use function file_put_contents;
use function flock;
use function fopen;
use function ftruncate;
@@ -40,6 +42,7 @@ use function ltrim;
use function mkdir;
use function preg_match;
use function realpath;
+use function rename;
use function rmdir;
use function rtrim;
use function scandir;
@@ -76,10 +79,9 @@ final class Filesystem{
public static function recursiveUnlink(string $dir) : void{
if(is_dir($dir)){
- $objects = scandir($dir, SCANDIR_SORT_NONE);
- if($objects === false) throw new AssumptionFailedError("scandir() shouldn't return false when is_dir() returns true");
+ $objects = Utils::assumeNotFalse(scandir($dir, SCANDIR_SORT_NONE), "scandir() shouldn't return false when is_dir() returns true");
foreach($objects as $object){
- if($object !== "." and $object !== ".."){
+ if($object !== "." && $object !== ".."){
$fullObject = Path::join($dir, $object);
if(is_dir($fullObject)){
self::recursiveUnlink($fullObject);
@@ -109,8 +111,12 @@ final class Filesystem{
//if the parent dir doesn't exist, the user most likely made a mistake
throw new \RuntimeException("The parent directory of $destination does not exist, or is not a directory");
}
- if(!@mkdir($destination) && !is_dir($destination)){
- throw new \RuntimeException("Failed to create output directory $destination (permission denied?)");
+ try{
+ ErrorToExceptionHandler::trap(fn() => mkdir($destination));
+ }catch(\ErrorException $e){
+ if(!is_dir($destination)){
+ throw new \RuntimeException("Failed to create output directory $destination: " . $e->getMessage());
+ }
}
}
self::recursiveCopyInternal($origin, $destination);
@@ -124,8 +130,7 @@ final class Filesystem{
}
mkdir($destination); //TODO: access permissions?
}
- $objects = scandir($origin, SCANDIR_SORT_NONE);
- if($objects === false) throw new AssumptionFailedError("scandir() shouldn't return false when is_dir() returns true");
+ $objects = Utils::assumeNotFalse(scandir($origin, SCANDIR_SORT_NONE));
foreach($objects as $object){
if($object === "." || $object === ".."){
continue;
@@ -163,7 +168,8 @@ final class Filesystem{
$result = str_replace([DIRECTORY_SEPARATOR, ".php", "phar://"], ["/", "", ""], $path);
//remove relative paths
- foreach(self::$cleanedPaths as $cleanPath => $replacement){
+ //this should probably never have integer keys, but it's safer than making PHPStan ignore it
+ foreach(Utils::stringifyKeys(self::$cleanedPaths) as $cleanPath => $replacement){
$cleanPath = rtrim(str_replace([DIRECTORY_SEPARATOR, "phar://"], ["/", ""], $cleanPath), "/");
if(strpos($result, $cleanPath) === 0){
$result = ltrim(str_replace($cleanPath, $replacement, $result), "/");
@@ -189,7 +195,7 @@ final class Filesystem{
//wait for a shared lock to avoid race conditions if two servers started at the same time - this makes sure the
//other server wrote its PID and released exclusive lock before we get our lock
flock($resource, LOCK_SH);
- $pid = stream_get_contents($resource);
+ $pid = Utils::assumeNotFalse(stream_get_contents($resource), "This is a known valid file resource, at worst we should receive an empty string");
if(preg_match('/^\d+$/', $pid) === 1){
return (int) $pid;
}
@@ -220,4 +226,73 @@ final class Filesystem{
@unlink($lockFilePath);
}
}
+
+ /**
+ * Wrapper around file_put_contents() which writes to a temporary file before overwriting the original. If the disk
+ * is full, writing to the temporary file will fail before the original file is modified, leaving it untouched.
+ *
+ * This is necessary because file_put_contents() destroys the data currently in the file if it fails to write the
+ * new contents.
+ *
+ * @param resource|null $context Context to pass to file_put_contents
+ *
+ * @throws \RuntimeException if the operation failed for any reason
+ */
+ public static function safeFilePutContents(string $fileName, string $contents, int $flags = 0, $context = null) : void{
+ $directory = dirname($fileName);
+ if(!is_dir($directory)){
+ throw new \RuntimeException("Target directory path does not exist or is not a directory");
+ }
+ if(is_dir($fileName)){
+ throw new \RuntimeException("Target file path already exists and is not a file");
+ }
+
+ $counter = 0;
+ do{
+ //we don't care about overwriting any preexisting tmpfile but we can't write if a directory is already here
+ $temporaryFileName = $fileName . ".$counter.tmp";
+ $counter++;
+ }while(is_dir($temporaryFileName));
+
+ try{
+ ErrorToExceptionHandler::trap(fn() => $context !== null ?
+ file_put_contents($temporaryFileName, $contents, $flags, $context) :
+ file_put_contents($temporaryFileName, $contents, $flags)
+ );
+ }catch(\ErrorException $filePutContentsException){
+ $context !== null ?
+ @unlink($temporaryFileName, $context) :
+ @unlink($temporaryFileName);
+ throw new \RuntimeException("Failed to write to temporary file $temporaryFileName: " . $filePutContentsException->getMessage(), 0, $filePutContentsException);
+ }
+
+ $renameTemporaryFileResult = $context !== null ?
+ @rename($temporaryFileName, $fileName, $context) :
+ @rename($temporaryFileName, $fileName);
+ if(!$renameTemporaryFileResult){
+ /*
+ * The following code works around a bug in Windows where rename() will periodically decide to give us a
+ * spurious "Access is denied (code: 5)" error. As far as I could determine, the fault comes from Windows
+ * itself, but since I couldn't reliably reproduce the issue it's very hard to debug.
+ *
+ * The following code can be used to test. Usually it will fail anywhere before 100,000 iterations.
+ *
+ * for($i = 0; $i < 10_000_000; ++$i){
+ * file_put_contents('ops.txt.0.tmp', 'some data ' . $i, 0);
+ * if(!rename('ops.txt.0.tmp', 'ops.txt')){
+ * throw new \Error("something weird happened");
+ * }
+ * }
+ */
+ try{
+ ErrorToExceptionHandler::trap(fn() => $context !== null ?
+ copy($temporaryFileName, $fileName, $context) :
+ copy($temporaryFileName, $fileName)
+ );
+ }catch(\ErrorException $copyException){
+ throw new \RuntimeException("Failed to move temporary file contents into target file: " . $copyException->getMessage(), 0, $copyException);
+ }
+ @unlink($temporaryFileName);
+ }
+ }
}
diff --git a/src/utils/Git.php b/src/utils/Git.php
index dd83eb033..65142330b 100644
--- a/src/utils/Git.php
+++ b/src/utils/Git.php
@@ -39,8 +39,8 @@ final class Git{
* @param bool $dirty reference parameter, set to whether the repo has local changes
*/
public static function getRepositoryState(string $dir, bool &$dirty) : ?string{
- if(Process::execute("git -C \"$dir\" rev-parse HEAD", $out) === 0 and $out !== false and strlen($out = trim($out)) === 40){
- if(Process::execute("git -C \"$dir\" diff --quiet") === 1 or Process::execute("git -C \"$dir\" diff --cached --quiet") === 1){ //Locally-modified
+ if(Process::execute("git -C \"$dir\" rev-parse HEAD", $out) === 0 && $out !== false && strlen($out = trim($out)) === 40){
+ if(Process::execute("git -C \"$dir\" diff --quiet") === 1 || Process::execute("git -C \"$dir\" diff --cached --quiet") === 1){ //Locally-modified
$dirty = true;
}
return $out;
diff --git a/src/utils/Internet.php b/src/utils/Internet.php
index 36dc96fc2..ff0c3ca21 100644
--- a/src/utils/Internet.php
+++ b/src/utils/Internet.php
@@ -32,6 +32,7 @@ use function curl_getinfo;
use function curl_init;
use function curl_setopt_array;
use function explode;
+use function is_int;
use function is_string;
use function preg_match;
use function socket_close;
@@ -79,7 +80,7 @@ class Internet{
public static function getIP(bool $force = false){
if(!self::$online){
return false;
- }elseif(self::$ip !== false and !$force){
+ }elseif(self::$ip !== false && !$force){
return self::$ip;
}
@@ -89,22 +90,22 @@ class Internet{
}
$ip = self::getURL("http://checkip.dyndns.org/");
- if($ip !== null and preg_match('#Current IP Address\: ([0-9a-fA-F\:\.]*)#', trim(strip_tags($ip->getBody())), $matches) > 0){
+ if($ip !== null && preg_match('#Current IP Address\: ([0-9a-fA-F\:\.]*)#', trim(strip_tags($ip->getBody())), $matches) > 0){
return self::$ip = $matches[1];
}
$ip = self::getURL("http://www.checkip.org/");
- if($ip !== null and preg_match('#">([0-9a-fA-F\:\.]*)#', $ip->getBody(), $matches) > 0){
+ if($ip !== null && preg_match('#">([0-9a-fA-F\:\.]*)#', $ip->getBody(), $matches) > 0){
return self::$ip = $matches[1];
}
$ip = self::getURL("http://checkmyip.org/");
- if($ip !== null and preg_match('#Your IP address is ([0-9a-fA-F\:\.]*)#', $ip->getBody(), $matches) > 0){
+ if($ip !== null && preg_match('#Your IP address is ([0-9a-fA-F\:\.]*)#', $ip->getBody(), $matches) > 0){
return self::$ip = $matches[1];
}
$ip = self::getURL("http://ifconfig.me/ip");
- if($ip !== null and ($addr = trim($ip->getBody())) != ""){
+ if($ip !== null && ($addr = trim($ip->getBody())) != ""){
return self::$ip = $addr;
}
@@ -218,8 +219,8 @@ class Internet{
throw new InternetException(curl_error($ch));
}
if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set");
- /** @var int $httpCode */
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if(!is_int($httpCode)) throw new AssumptionFailedError("curl_getinfo(CURLINFO_HTTP_CODE) always returns int");
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($raw, 0, $headerSize);
$body = substr($raw, $headerSize);
diff --git a/src/utils/MainLogger.php b/src/utils/MainLogger.php
index ebce6012e..415f59b2c 100644
--- a/src/utils/MainLogger.php
+++ b/src/utils/MainLogger.php
@@ -24,14 +24,10 @@ declare(strict_types=1);
namespace pocketmine\utils;
use LogLevel;
-use pocketmine\errorhandler\ErrorTypeToStringMap;
use pocketmine\thread\Thread;
use pocketmine\thread\Worker;
-use function get_class;
-use function is_int;
-use function preg_replace;
+use function implode;
use function sprintf;
-use function trim;
use const PHP_EOL;
use const PTHREADS_INHERIT_NONE;
@@ -119,7 +115,7 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{
}
public function debug($message, bool $force = false){
- if(!$this->logDebug and !$force){
+ if(!$this->logDebug && !$force){
return;
}
$this->send($message, \LogLevel::DEBUG, "DEBUG", TextFormat::GRAY);
@@ -136,44 +132,11 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{
* @return void
*/
public function logException(\Throwable $e, $trace = null){
- if($trace === null){
- $trace = $e->getTrace();
- }
-
- $this->buffer(function() use ($e, $trace) : void{
- $this->critical(self::printExceptionMessage($e));
- foreach(Utils::printableTrace($trace) as $line){
- $this->critical($line);
- }
- for($prev = $e->getPrevious(); $prev !== null; $prev = $prev->getPrevious()){
- $this->critical("Previous: " . self::printExceptionMessage($prev));
- foreach(Utils::printableTrace($prev->getTrace()) as $line){
- $this->critical(" " . $line);
- }
- }
- });
+ $this->critical(implode("\n", Utils::printableExceptionInfo($e, $trace)));
$this->syncFlushBuffer();
}
- private static function printExceptionMessage(\Throwable $e) : string{
- $errstr = preg_replace('/\s+/', ' ', trim($e->getMessage()));
-
- $errno = $e->getCode();
- if(is_int($errno)){
- try{
- $errno = ErrorTypeToStringMap::get($errno);
- }catch(\InvalidArgumentException $ex){
- //pass
- }
- }
-
- $errfile = Filesystem::cleanPath($e->getFile());
- $errline = $e->getLine();
-
- return get_class($e) . ": \"$errstr\" ($errno) in \"$errfile\" at line $errline";
- }
-
public function log($level, $message){
switch($level){
case LogLevel::EMERGENCY:
@@ -230,7 +193,7 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{
$thread = \Thread::getCurrentThread();
if($thread === null){
$threadName = $this->mainThreadName . " thread";
- }elseif($thread instanceof Thread or $thread instanceof Worker){
+ }elseif($thread instanceof Thread || $thread instanceof Worker){
$threadName = $thread->getThreadName() . " thread";
}else{
$threadName = (new \ReflectionClass($thread))->getShortName() . " thread";
@@ -244,12 +207,11 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{
$this->synchronized(function() use ($message, $level, $time) : void{
Terminal::writeLine($message);
+ $this->logWriterThread->write($time->format("Y-m-d") . " " . TextFormat::clean($message) . PHP_EOL);
foreach($this->attachments as $attachment){
- $attachment->call($level, $message);
+ $attachment->log($level, $message);
}
-
- $this->logWriterThread->write($time->format("Y-m-d") . " " . TextFormat::clean($message) . PHP_EOL);
});
}
diff --git a/src/utils/Process.php b/src/utils/Process.php
index f433300fe..503bba7c5 100644
--- a/src/utils/Process.php
+++ b/src/utils/Process.php
@@ -56,7 +56,7 @@ final class Process{
$reserved = memory_get_usage();
$VmSize = null;
$VmRSS = null;
- if(Utils::getOS() === Utils::OS_LINUX or Utils::getOS() === Utils::OS_ANDROID){
+ if(Utils::getOS() === Utils::OS_LINUX || Utils::getOS() === Utils::OS_ANDROID){
$status = @file_get_contents("/proc/self/status");
if($status === false) throw new AssumptionFailedError("/proc/self/status should always be accessible");
@@ -94,7 +94,7 @@ final class Process{
$stack = 0;
$heap = 0;
- if(Utils::getOS() === Utils::OS_LINUX or Utils::getOS() === Utils::OS_ANDROID){
+ if(Utils::getOS() === Utils::OS_LINUX || Utils::getOS() === Utils::OS_ANDROID){
$mappings = @file("/proc/self/maps");
if($mappings === false) throw new AssumptionFailedError("/proc/self/maps should always be accessible");
foreach($mappings as $line){
@@ -112,7 +112,7 @@ final class Process{
}
public static function getThreadCount() : int{
- if(Utils::getOS() === Utils::OS_LINUX or Utils::getOS() === Utils::OS_ANDROID){
+ if(Utils::getOS() === Utils::OS_LINUX || Utils::getOS() === Utils::OS_ANDROID){
$status = @file_get_contents("/proc/self/status");
if($status === false) throw new AssumptionFailedError("/proc/self/status should always be accessible");
if(preg_match("/Threads:[ \t]+([0-9]+)/", $status, $matches) > 0){
@@ -125,18 +125,21 @@ final class Process{
return count(ThreadManager::getInstance()->getAll()) + 2; //MainLogger + Main Thread
}
- public static function kill(int $pid) : void{
+ public static function kill(int $pid, bool $subprocesses) : void{
$logger = \GlobalLogger::get();
if($logger instanceof MainLogger){
$logger->syncFlushBuffer();
}
switch(Utils::getOS()){
case Utils::OS_WINDOWS:
- exec("taskkill.exe /F /PID $pid > NUL");
+ exec("taskkill.exe /F " . ($subprocesses ? "/T " : "") . "/PID $pid > NUL 2> NUL");
break;
case Utils::OS_MACOS:
case Utils::OS_LINUX:
default:
+ if($subprocesses){
+ $pid = -$pid;
+ }
if(function_exists("posix_kill")){
posix_kill($pid, 9); //SIGKILL
}else{
diff --git a/src/utils/Promise.php b/src/utils/Promise.php
deleted file mode 100644
index 4ccfc4bc5..000000000
--- a/src/utils/Promise.php
+++ /dev/null
@@ -1,93 +0,0 @@
-
- */
- private array $onSuccess = [];
-
- /**
- * @var \Closure[]
- * @phpstan-var array
- */
- private array $onFailure = [];
-
- private bool $resolved = false;
-
- /**
- * @var mixed
- * @phpstan-var TValue|null
- */
- private $result = null;
-
- /**
- * @phpstan-param \Closure(TValue) : void $onSuccess
- * @phpstan-param \Closure() : void $onFailure
- */
- public function onCompletion(\Closure $onSuccess, \Closure $onFailure) : void{
- if($this->resolved){
- $this->result === null ? $onFailure() : $onSuccess($this->result);
- }else{
- $this->onSuccess[spl_object_id($onSuccess)] = $onSuccess;
- $this->onFailure[spl_object_id($onFailure)] = $onFailure;
- }
- }
-
- /**
- * @param mixed $value
- * @phpstan-param TValue $value
- */
- public function resolve($value) : void{
- if($this->resolved){
- throw new \InvalidStateException("Promise has already been resolved/rejected");
- }
- $this->resolved = true;
- $this->result = $value;
- foreach($this->onSuccess as $c){
- $c($value);
- }
- $this->onSuccess = [];
- $this->onFailure = [];
- }
-
- public function reject() : void{
- if($this->resolved){
- throw new \InvalidStateException("Promise has already been resolved/rejected");
- }
- $this->resolved = true;
- foreach($this->onFailure as $c){
- $c();
- }
- $this->onSuccess = [];
- $this->onFailure = [];
- }
-}
diff --git a/src/utils/Random.php b/src/utils/Random.php
index 81598ac24..3c7f6cb0d 100644
--- a/src/utils/Random.php
+++ b/src/utils/Random.php
@@ -94,7 +94,7 @@ class Random{
$this->z = $this->w;
$this->w = ($this->w ^ (($this->w >> 19) & 0x7fffffff) ^ ($t ^ (($t >> 8) & 0x7fffffff))) & 0xffffffff;
- return $this->w;
+ return Binary::signInt($this->w);
}
/**
diff --git a/src/utils/RegistryTrait.php b/src/utils/RegistryTrait.php
index 31c2116f1..185dfefb3 100644
--- a/src/utils/RegistryTrait.php
+++ b/src/utils/RegistryTrait.php
@@ -37,11 +37,11 @@ trait RegistryTrait{
* @throws \InvalidArgumentException
*/
private static function _registryRegister(string $name, object $member) : void{
- $name = mb_strtoupper($name);
- if(isset(self::$members[$name])){
- throw new \InvalidArgumentException("\"$name\" is already reserved");
+ $upperName = mb_strtoupper($name);
+ if(isset(self::$members[$upperName])){
+ throw new \InvalidArgumentException("\"$upperName\" is already reserved");
}
- self::$members[mb_strtoupper($name)] = $member;
+ self::$members[$upperName] = $member;
}
/**
@@ -68,11 +68,11 @@ trait RegistryTrait{
*/
private static function _registryFromString(string $name) : object{
self::checkInit();
- $name = mb_strtoupper($name);
- if(!isset(self::$members[$name])){
- throw new \InvalidArgumentException("No such registry member: " . self::class . "::" . $name);
+ $upperName = mb_strtoupper($name);
+ if(!isset(self::$members[$upperName])){
+ throw new \InvalidArgumentException("No such registry member: " . self::class . "::" . $upperName);
}
- return self::preprocessMember(self::$members[$name]);
+ return self::preprocessMember(self::$members[$upperName]);
}
protected static function preprocessMember(object $member) : object{
diff --git a/src/utils/ServerKiller.php b/src/utils/ServerKiller.php
index 78d83f654..24f8eb37a 100644
--- a/src/utils/ServerKiller.php
+++ b/src/utils/ServerKiller.php
@@ -50,7 +50,7 @@ class ServerKiller extends Thread{
});
if(time() - $start >= $this->time){
echo "\nTook too long to stop, server was killed forcefully!\n";
- @Process::kill(Process::pid());
+ @Process::kill(Process::pid(), true);
}
}
diff --git a/src/utils/SignalHandler.php b/src/utils/SignalHandler.php
new file mode 100644
index 000000000..de81a2b67
--- /dev/null
+++ b/src/utils/SignalHandler.php
@@ -0,0 +1,80 @@
+interruptCallback = $interruptCallback;
+
+ if(function_exists('sapi_windows_set_ctrl_handler')){
+ sapi_windows_set_ctrl_handler($this->interruptCallback = function(int $signo) use ($interruptCallback) : void{
+ if($signo === PHP_WINDOWS_EVENT_CTRL_C || $signo === PHP_WINDOWS_EVENT_CTRL_BREAK){
+ $interruptCallback();
+ }
+ });
+ }elseif(function_exists('pcntl_signal')){
+ foreach([
+ SIGTERM,
+ SIGINT,
+ SIGHUP
+ ] as $signal){
+ pcntl_signal($signal, $this->interruptCallback = fn(int $signo) => $interruptCallback());
+ }
+ pcntl_async_signals(true);
+ }else{
+ //no supported signal handlers :(
+ }
+ }
+
+ public function unregister() : void{
+ if(function_exists('sapi_windows_set_ctrl_handler')){
+ sapi_windows_set_ctrl_handler($this->interruptCallback, false);
+ }elseif(function_exists('pcntl_signal')){
+ foreach([
+ SIGTERM,
+ SIGINT,
+ SIGHUP
+ ] as $signal){
+ pcntl_signal($signal, SIG_DFL);
+ }
+ }
+ }
+}
diff --git a/src/utils/SingletonTrait.php b/src/utils/SingletonTrait.php
index 99e30e41c..ee63cbb9c 100644
--- a/src/utils/SingletonTrait.php
+++ b/src/utils/SingletonTrait.php
@@ -45,4 +45,4 @@ trait SingletonTrait{
public static function reset() : void{
self::$instance = null;
}
-}
\ No newline at end of file
+}
diff --git a/src/utils/StringToTParser.php b/src/utils/StringToTParser.php
new file mode 100644
index 000000000..5320f2b58
--- /dev/null
+++ b/src/utils/StringToTParser.php
@@ -0,0 +1,82 @@
+
+ */
+ private array $callbackMap = [];
+
+ /** @phpstan-param \Closure(string $input) : T $callback */
+ public function register(string $alias, \Closure $callback) : void{
+ $key = $this->reprocess($alias);
+ if(isset($this->callbackMap[$key])){
+ throw new \InvalidArgumentException("Alias \"$key\" is already registered");
+ }
+ $this->callbackMap[$key] = $callback;
+ }
+
+ /** @phpstan-param \Closure(string $input) : T $callback */
+ public function override(string $alias, \Closure $callback) : void{
+ $this->callbackMap[$this->reprocess($alias)] = $callback;
+ }
+
+ /**
+ * Tries to parse the specified string into an enchantment.
+ * @phpstan-return T|null
+ */
+ public function parse(string $input){
+ $key = $this->reprocess($input);
+ if(isset($this->callbackMap[$key])){
+ return ($this->callbackMap[$key])($input);
+ }
+
+ return null;
+ }
+
+ protected function reprocess(string $input) : string{
+ return strtolower(str_replace([" ", "minecraft:"], ["_", ""], trim($input)));
+ }
+
+ /** @return string[]|int[] */
+ public function getKnownAliases() : array{
+ return array_keys($this->callbackMap);
+ }
+}
diff --git a/src/utils/Terminal.php b/src/utils/Terminal.php
index 4280f94d8..955abc660 100644
--- a/src/utils/Terminal.php
+++ b/src/utils/Terminal.php
@@ -58,13 +58,14 @@ abstract class Terminal{
public static string $COLOR_LIGHT_PURPLE = "";
public static string $COLOR_YELLOW = "";
public static string $COLOR_WHITE = "";
+ public static string $COLOR_MINECOIN_GOLD = "";
/** @var bool|null */
private static $formattingCodes = null;
public static function hasFormattingCodes() : bool{
if(self::$formattingCodes === null){
- throw new \InvalidStateException("Formatting codes have not been initialized");
+ throw new \LogicException("Formatting codes have not been initialized");
}
return self::$formattingCodes;
}
@@ -73,10 +74,10 @@ abstract class Terminal{
$stdout = fopen("php://stdout", "w");
if($stdout === false) throw new AssumptionFailedError("Opening php://stdout should never fail");
$result = (
- stream_isatty($stdout) and //STDOUT isn't being piped
+ stream_isatty($stdout) && //STDOUT isn't being piped
(
- getenv('TERM') !== false or //Console says it supports colours
- (function_exists('sapi_windows_vt100_support') and sapi_windows_vt100_support($stdout)) //we're on windows and have vt100 support
+ getenv('TERM') !== false || //Console says it supports colours
+ (function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support($stdout)) //we're on windows and have vt100 support
)
);
fclose($stdout);
@@ -110,6 +111,7 @@ abstract class Terminal{
self::$COLOR_LIGHT_PURPLE = $color(207);
self::$COLOR_YELLOW = $color(227);
self::$COLOR_WHITE = $color(231);
+ self::$COLOR_MINECOIN_GOLD = $color(184);
}
protected static function getEscapeCodes() : void{
@@ -142,11 +144,12 @@ abstract class Terminal{
self::$COLOR_LIGHT_PURPLE = $colors >= 256 ? $setaf(207) : $setaf(13);
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);
}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 = $setaf(3);
+ 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);
@@ -210,6 +213,7 @@ abstract class Terminal{
TextFormat::LIGHT_PURPLE => Terminal::$COLOR_LIGHT_PURPLE,
TextFormat::YELLOW => Terminal::$COLOR_YELLOW,
TextFormat::WHITE => Terminal::$COLOR_WHITE,
+ TextFormat::MINECOIN_GOLD => Terminal::$COLOR_MINECOIN_GOLD,
default => $token,
};
}
diff --git a/src/utils/TextFormat.php b/src/utils/TextFormat.php
index 66189078c..f784f5dd2 100644
--- a/src/utils/TextFormat.php
+++ b/src/utils/TextFormat.php
@@ -62,6 +62,7 @@ abstract class TextFormat{
public const LIGHT_PURPLE = TextFormat::ESCAPE . "d";
public const YELLOW = TextFormat::ESCAPE . "e";
public const WHITE = TextFormat::ESCAPE . "f";
+ public const MINECOIN_GOLD = TextFormat::ESCAPE . "g";
public const COLORS = [
self::BLACK => self::BLACK,
@@ -80,6 +81,7 @@ abstract class TextFormat{
self::LIGHT_PURPLE => self::LIGHT_PURPLE,
self::YELLOW => self::YELLOW,
self::WHITE => self::WHITE,
+ self::MINECOIN_GOLD => self::MINECOIN_GOLD,
];
public const OBFUSCATED = TextFormat::ESCAPE . "k";
@@ -128,7 +130,7 @@ abstract class TextFormat{
* @return string[]
*/
public static function tokenize(string $string) : array{
- $result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-fk-or])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+ $result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-gk-or])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
if($result === false) throw self::makePcreError();
return $result;
}
@@ -142,7 +144,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-fk-or]/u", "", $string));
+ $string = str_replace(TextFormat::ESCAPE, "", self::preg_replace("/" . TextFormat::ESCAPE . "[0-9a-gk-or]/u", "", $string));
}
return str_replace("\x1b", "", self::preg_replace("/\x1b[\\(\\][[0-9;\\[\\(]+[Bm]/u", "", $string));
}
@@ -153,7 +155,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-fk-or])/u', TextFormat::ESCAPE . '$1', $string);
+ return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-gk-or])/u', TextFormat::ESCAPE . '$1', $string);
}
/**
@@ -254,6 +256,10 @@ abstract class TextFormat{
$newString .= "";
++$tokens;
break;
+ case TextFormat::MINECOIN_GOLD:
+ $newString .= "";
+ ++$tokens;
+ break;
default:
$newString .= $token;
break;
diff --git a/src/utils/Timezone.php b/src/utils/Timezone.php
index 9f16da98f..95358e704 100644
--- a/src/utils/Timezone.php
+++ b/src/utils/Timezone.php
@@ -55,7 +55,7 @@ abstract class Timezone{
}
public static function init() : void{
- $timezone = ini_get("date.timezone");
+ $timezone = Utils::assumeNotFalse(ini_get("date.timezone"), "date.timezone should always be set in ini");
if($timezone !== ""){
/*
* This is here so that people don't come to us complaining and fill up the issue tracker when they put
@@ -77,7 +77,7 @@ abstract class Timezone{
}
}
- if(($timezone = self::detectSystemTimezone()) !== false and date_default_timezone_set($timezone)){
+ if(($timezone = self::detectSystemTimezone()) !== false && date_default_timezone_set($timezone)){
//Success! Timezone has already been set and validated in the if statement.
//This here is just for redundancy just in case some program wants to read timezone data from the ini.
ini_set("date.timezone", $timezone);
@@ -85,9 +85,11 @@ abstract class Timezone{
}
if(($response = Internet::getURL("http://ip-api.com/json")) !== null //If system timezone detection fails or timezone is an invalid value.
- and is_array($ip_geolocation_data = json_decode($response->getBody(), true))
- and $ip_geolocation_data['status'] !== 'fail'
- and date_default_timezone_set($ip_geolocation_data['timezone'])
+ && is_array($ip_geolocation_data = json_decode($response->getBody(), true))
+ && isset($ip_geolocation_data['status'])
+ && $ip_geolocation_data['status'] !== 'fail'
+ && is_string($ip_geolocation_data['timezone'])
+ && date_default_timezone_set($ip_geolocation_data['timezone'])
){
//Again, for redundancy.
ini_set("date.timezone", $ip_geolocation_data['timezone']);
@@ -148,7 +150,7 @@ abstract class Timezone{
// RHEL / CentOS
$data = @parse_ini_file('/etc/sysconfig/clock');
- if($data !== false and isset($data['ZONE']) and is_string($data['ZONE'])){
+ if($data !== false && isset($data['ZONE']) && is_string($data['ZONE'])){
return trim($data['ZONE']);
}
@@ -163,7 +165,7 @@ abstract class Timezone{
return self::parseOffset($offset);
case Utils::OS_MACOS:
$filename = @readlink('/etc/localtime');
- if($filename !== false and strpos($filename, '/usr/share/zoneinfo/') === 0){
+ if($filename !== false && strpos($filename, '/usr/share/zoneinfo/') === 0){
$timezone = substr($filename, 20);
return trim($timezone);
}
diff --git a/src/utils/Utils.php b/src/utils/Utils.php
index 72192e6ee..0b772ae64 100644
--- a/src/utils/Utils.php
+++ b/src/utils/Utils.php
@@ -28,6 +28,7 @@ declare(strict_types=1);
namespace pocketmine\utils;
use DaveRandom\CallbackValidator\CallbackType;
+use pocketmine\errorhandler\ErrorTypeToStringMap;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function array_combine;
@@ -46,13 +47,17 @@ use function file;
use function file_exists;
use function file_get_contents;
use function function_exists;
+use function get_class;
use function get_current_user;
use function get_loaded_extensions;
use function getenv;
use function gettype;
use function implode;
+use function interface_exists;
+use function is_a;
use function is_array;
use function is_bool;
+use function is_int;
use function is_object;
use function is_string;
use function mb_check_encoding;
@@ -66,6 +71,7 @@ use function preg_grep;
use function preg_match;
use function preg_match_all;
use function preg_replace;
+use function shell_exec;
use function spl_object_id;
use function str_pad;
use function str_split;
@@ -183,7 +189,7 @@ final class Utils{
* @param string $extra optional, additional data to identify the machine
*/
public static function getMachineUniqueId(string $extra = "") : UuidInterface{
- if(self::$serverUniqueId !== null and $extra === ""){
+ if(self::$serverUniqueId !== null && $extra === ""){
return self::$serverUniqueId;
}
@@ -228,7 +234,7 @@ final class Utils{
}elseif($os === Utils::OS_ANDROID){
$machine .= @file_get_contents("/system/build.prop");
}elseif($os === Utils::OS_MACOS){
- $machine .= `system_profiler SPHardwareDataType | grep UUID`;
+ $machine .= shell_exec("system_profiler SPHardwareDataType | grep UUID");
}
$data = $machine . PHP_MAXPATHLEN;
$data .= PHP_INT_MAX;
@@ -259,7 +265,7 @@ final class Utils{
* Other => other
*/
public static function getOS(bool $recalculate = false) : string{
- if(self::$os === null or $recalculate){
+ if(self::$os === null || $recalculate){
$uname = php_uname("s");
if(stripos($uname, "Darwin") !== false){
if(strpos(php_uname("m"), "iP") === 0){
@@ -267,7 +273,7 @@ final class Utils{
}else{
self::$os = self::OS_MACOS;
}
- }elseif(stripos($uname, "Win") !== false or $uname === "Msys"){
+ }elseif(stripos($uname, "Win") !== false || $uname === "Msys"){
self::$os = self::OS_WINDOWS;
}elseif(stripos($uname, "Linux") !== false){
if(@file_exists("/system/build.prop")){
@@ -275,7 +281,7 @@ final class Utils{
}else{
self::$os = self::OS_LINUX;
}
- }elseif(stripos($uname, "BSD") !== false or $uname === "DragonFly"){
+ }elseif(stripos($uname, "BSD") !== false || $uname === "DragonFly"){
self::$os = self::OS_BSD;
}else{
self::$os = self::OS_UNKNOWN;
@@ -288,7 +294,7 @@ final class Utils{
public static function getCoreCount(bool $recalculate = false) : int{
static $processors = 0;
- if($processors > 0 and !$recalculate){
+ if($processors > 0 && !$recalculate){
return $processors;
}else{
$processors = 0;
@@ -311,7 +317,7 @@ final class Utils{
break;
case Utils::OS_BSD:
case Utils::OS_MACOS:
- $processors = (int) `sysctl -n hw.ncpu`;
+ $processors = (int) shell_exec("sysctl -n hw.ncpu");
break;
case Utils::OS_WINDOWS:
$processors = (int) getenv("NUMBER_OF_PROCESSORS");
@@ -384,6 +390,49 @@ final class Utils{
return -1;
}
+ private static function printableExceptionMessage(\Throwable $e) : string{
+ $errstr = preg_replace('/\s+/', ' ', trim($e->getMessage()));
+
+ $errno = $e->getCode();
+ if(is_int($errno)){
+ try{
+ $errno = ErrorTypeToStringMap::get($errno);
+ }catch(\InvalidArgumentException $ex){
+ //pass
+ }
+ }
+
+ $errfile = Filesystem::cleanPath($e->getFile());
+ $errline = $e->getLine();
+
+ return get_class($e) . ": \"$errstr\" ($errno) in \"$errfile\" at line $errline";
+ }
+
+ /**
+ * @param mixed[] $trace
+ * @return string[]
+ */
+ public static function printableExceptionInfo(\Throwable $e, $trace = null) : array{
+ if($trace === null){
+ $trace = $e->getTrace();
+ }
+
+ $lines = [self::printableExceptionMessage($e)];
+ $lines[] = "--- Stack trace ---";
+ foreach(Utils::printableTrace($trace) as $line){
+ $lines[] = " " . $line;
+ }
+ for($prev = $e->getPrevious(); $prev !== null; $prev = $prev->getPrevious()){
+ $lines[] = "--- Previous ---";
+ $lines[] = self::printableExceptionMessage($prev);
+ foreach(Utils::printableTrace($prev->getTrace()) as $line){
+ $lines[] = " " . $line;
+ }
+ }
+ $lines[] = "--- End of exception information ---";
+ return $lines;
+ }
+
/**
* @param mixed[][] $trace
* @phpstan-param list> $trace
@@ -394,7 +443,7 @@ final class Utils{
$messages = [];
for($i = 0; isset($trace[$i]); ++$i){
$params = "";
- if(isset($trace[$i]["args"]) or isset($trace[$i]["params"])){
+ if(isset($trace[$i]["args"]) || isset($trace[$i]["params"])){
if(isset($trace[$i]["args"])){
$args = $trace[$i]["args"];
}else{
@@ -417,7 +466,7 @@ final class Utils{
return gettype($value) . " " . Utils::printable((string) $value);
}, $args));
}
- $messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" or $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")";
+ $messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")";
}
return $messages;
}
@@ -454,10 +503,7 @@ final class Utils{
*/
public static function parseDocComment(string $docComment) : array{
$rawDocComment = substr($docComment, 3, -2); //remove the opening and closing markers
- if($rawDocComment === false){ //usually empty doc comment, but this is safer and statically analysable
- return [];
- }
- preg_match_all('/(*ANYCRLF)^[\t ]*(?:\* )?@([a-zA-Z]+)(?:[\t ]+(.+?))?[\t ]*$/m', $rawDocComment, $matches);
+ preg_match_all('/(*ANYCRLF)^[\t ]*(?:\* )?@([a-zA-Z\-]+)(?:[\t ]+(.+?))?[\t ]*$/m', $rawDocComment, $matches);
return array_combine($matches[1], $matches[2]);
}
@@ -467,18 +513,20 @@ final class Utils{
* @phpstan-param class-string $baseName
*/
public static function testValidInstance(string $className, string $baseName) : void{
+ $baseInterface = false;
if(!class_exists($baseName)){
- throw new \InvalidArgumentException("Base class $baseName does not exist");
+ if(!interface_exists($baseName)){
+ throw new \InvalidArgumentException("Base class $baseName does not exist");
+ }
+ $baseInterface = true;
}
if(!class_exists($className)){
- throw new \InvalidArgumentException("Class $className does not exist");
+ throw new \InvalidArgumentException("Class $className does not exist or is not a class");
+ }
+ if(!is_a($className, $baseName, true)){
+ throw new \InvalidArgumentException("Class $className does not " . ($baseInterface ? "implement" : "extend") . " $baseName");
}
- $base = new \ReflectionClass($baseName);
$class = new \ReflectionClass($className);
-
- if(!$class->isSubclassOf($baseName)){
- throw new \InvalidArgumentException("Class $className does not " . ($base->isInterface() ? "implement" : "extend") . " " . $baseName);
- }
if(!$class->isInstantiable()){
throw new \InvalidArgumentException("Class $className cannot be constructed");
}
@@ -488,17 +536,20 @@ final class Utils{
* Verifies that the given callable is compatible with the desired signature. Throws a TypeError if they are
* incompatible.
*
- * @param callable $signature Dummy callable with the required parameters and return type
- * @param callable $subject Callable to check the signature of
- * @phpstan-param anyCallable $signature
- * @phpstan-param anyCallable $subject
+ * @param callable|CallbackType $signature Dummy callable with the required parameters and return type
+ * @param callable $subject Callable to check the signature of
+ * @phpstan-param anyCallable|CallbackType $signature
+ * @phpstan-param anyCallable $subject
*
* @throws \DaveRandom\CallbackValidator\InvalidCallbackException
* @throws \TypeError
*/
- public static function validateCallableSignature(callable $signature, callable $subject) : void{
- if(!($sigType = CallbackType::createFromCallable($signature))->isSatisfiedBy($subject)){
- throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($subject) . "` must be compatible with `" . $sigType . "`");
+ public static function validateCallableSignature(callable|CallbackType $signature, callable $subject) : void{
+ if(!($signature instanceof CallbackType)){
+ $signature = CallbackType::createFromCallable($signature);
+ }
+ if(!$signature->isSatisfiedBy($subject)){
+ throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($subject) . "` must be compatible with `" . $signature . "`");
}
}
@@ -517,9 +568,38 @@ final class Utils{
}
}
+ /**
+ * Generator which forces array keys to string during iteration.
+ * This is necessary because PHP has an anti-feature where it casts numeric string keys to integers, leading to
+ * various crashes.
+ *
+ * @phpstan-template TKeyType of string
+ * @phpstan-template TValueType
+ * @phpstan-param array $array
+ * @phpstan-return \Generator
+ */
+ public static function stringifyKeys(array $array) : \Generator{
+ foreach($array as $key => $value){ // @phpstan-ignore-line - this is where we fix the stupid bullshit with array keys :)
+ yield (string) $key => $value;
+ }
+ }
+
public static function checkUTF8(string $string) : void{
if(!mb_check_encoding($string, 'UTF-8')){
throw new \InvalidArgumentException("Text must be valid UTF-8");
}
}
+
+ /**
+ * @phpstan-template TValue
+ * @phpstan-param TValue|false $value
+ * @phpstan-param string|\Closure() : string $context
+ * @phpstan-return TValue
+ */
+ public static function assumeNotFalse(mixed $value, \Closure|string $context = "This should never be false") : mixed{
+ if($value === false){
+ throw new AssumptionFailedError("Assumption failure: " . (is_string($context) ? $context : $context()) . " (THIS IS A BUG)");
+ }
+ return $value;
+ }
}
diff --git a/src/utils/VersionString.php b/src/utils/VersionString.php
index 1dd666c9f..0b2aed47a 100644
--- a/src/utils/VersionString.php
+++ b/src/utils/VersionString.php
@@ -63,8 +63,12 @@ class VersionString{
$this->suffix = $matches[4] ?? "";
}
+ public static function isValidBaseVersion(string $baseVersion) : bool{
+ return preg_match('/^\d+\.\d+\.\d+(?:-(.*))?$/', $baseVersion, $matches) === 1;
+ }
+
public function getNumber() : int{
- return (($this->major << 9) | ($this->minor << 5) | $this->patch);
+ return (($this->major * 1_000_000) + ($this->minor * 1_000) + $this->patch);
}
public function getBaseVersion() : string{
@@ -75,7 +79,7 @@ class VersionString{
$retval = $this->baseVersion;
if($this->development){
$retval .= "+dev";
- if($build and $this->build > 0){
+ if($build && $this->build > 0){
$retval .= "." . $this->build;
}
}
diff --git a/src/wizard/SetupWizard.php b/src/wizard/SetupWizard.php
index bb1b8de26..ae0ecb17d 100644
--- a/src/wizard/SetupWizard.php
+++ b/src/wizard/SetupWizard.php
@@ -31,10 +31,13 @@ use pocketmine\data\java\GameModeIdMap;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException;
+use pocketmine\lang\Translatable;
use pocketmine\player\GameMode;
+use pocketmine\Server;
use pocketmine\utils\Config;
use pocketmine\utils\Internet;
use pocketmine\utils\InternetException;
+use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use Webmozart\PathUtil\Path;
use function fgets;
@@ -45,9 +48,9 @@ use const PHP_EOL;
use const STDIN;
class SetupWizard{
- public const DEFAULT_NAME = VersionInfo::NAME . " Server";
- public const DEFAULT_PORT = 19132;
- public const DEFAULT_PLAYERS = 20;
+ public const DEFAULT_NAME = Server::DEFAULT_SERVER_NAME;
+ public const DEFAULT_PORT = Server::DEFAULT_PORT_IPV4;
+ public const DEFAULT_PLAYERS = Server::DEFAULT_MAX_PLAYERS;
/** @var Language */
private $lang;
@@ -69,7 +72,7 @@ class SetupWizard{
}
$this->message("Please select a language");
- foreach($langs as $short => $native){
+ foreach(Utils::stringifyKeys($langs) as $short => $native){
$this->writeLine(" $native => $short");
}
@@ -139,6 +142,18 @@ LICENSE;
$this->message($this->lang->translate(KnownTranslationFactory::server_properties()));
}
+ private function askPort(Translatable $prompt, int $default) : int{
+ while(true){
+ $port = (int) $this->getInput($this->lang->translate($prompt), (string) $default);
+ if($port <= 0 || $port > 65535){
+ $this->error($this->lang->translate(KnownTranslationFactory::invalid_port()));
+ continue;
+ }
+
+ return $port;
+ }
+ }
+
private function generateBaseConfig() : void{
$config = new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES);
@@ -147,26 +162,20 @@ LICENSE;
$this->message($this->lang->translate(KnownTranslationFactory::port_warning()));
- do{
- $port = (int) $this->getInput($this->lang->translate(KnownTranslationFactory::server_port()), (string) self::DEFAULT_PORT);
- if($port <= 0 or $port > 65535){
- $this->error($this->lang->translate(KnownTranslationFactory::invalid_port()));
- continue;
- }
-
- break;
- }while(true);
- $config->set("server-port", $port);
+ $config->set("server-port", $this->askPort(KnownTranslationFactory::server_port_v4(), Server::DEFAULT_PORT_IPV4));
+ $config->set("server-portv6", $this->askPort(KnownTranslationFactory::server_port_v6(), Server::DEFAULT_PORT_IPV6));
$this->message($this->lang->translate(KnownTranslationFactory::gamemode_info()));
do{
- $gamemode = (int) $this->getInput($this->lang->translate(KnownTranslationFactory::default_gamemode()), (string) GameModeIdMap::getInstance()->toId(GameMode::SURVIVAL()));
- }while($gamemode < 0 or $gamemode > 3);
- $config->set("gamemode", $gamemode);
+ $gamemode = GameModeIdMap::getInstance()->fromId((int) $this->getInput($this->lang->translate(KnownTranslationFactory::default_gamemode()), (string) GameModeIdMap::getInstance()->toId(GameMode::SURVIVAL())));
+ }while($gamemode === null);
+ $config->set("gamemode", $gamemode->name());
$config->set("max-players", (int) $this->getInput($this->lang->translate(KnownTranslationFactory::max_players()), (string) self::DEFAULT_PLAYERS));
+ $config->set("view-distance", (int) $this->getInput($this->lang->translate(KnownTranslationFactory::view_distance()), (string) Server::DEFAULT_MAX_VIEW_DISTANCE));
+
$config->save();
}
@@ -255,7 +264,7 @@ LICENSE;
private function getInput(string $message, string $default = "", string $options = "") : string{
$message = "[?] " . $message;
- if($options !== "" or $default !== ""){
+ if($options !== "" || $default !== ""){
$message .= " (" . ($options === "" ? $default : $options) . ")";
}
$message .= ": ";
diff --git a/src/world/ChunkLockId.php b/src/world/ChunkLockId.php
new file mode 100644
index 000000000..9931aa089
--- /dev/null
+++ b/src/world/ChunkLockId.php
@@ -0,0 +1,38 @@
+rays; ++$i){
for($j = 0; $j < $this->rays; ++$j){
for($k = 0; $k < $this->rays; ++$k){
- if($i === 0 or $i === $mRays or $j === 0 or $j === $mRays or $k === 0 or $k === $mRays){
+ if($i === 0 || $i === $mRays || $j === 0 || $j === $mRays || $k === 0 || $k === $mRays){
//this could be written as new Vector3(...)->normalize()->multiply(stepLen), but we're avoiding Vector3 for performance here
[$shiftX, $shiftY, $shiftZ] = [$i / $mRays * 2 - 1, $j / $mRays * 2 - 1, $k / $mRays * 2 - 1];
$len = sqrt($shiftX ** 2 + $shiftY ** 2 + $shiftZ ** 2);
@@ -134,8 +134,7 @@ class Explosion{
$blastForce -= ($blastResistance / 5 + 0.3) * $this->stepLen;
if($blastForce > 0){
if(!isset($this->affectedBlocks[World::blockHash($vBlockX, $vBlockY, $vBlockZ)])){
- $_block = $blockFactory->fromFullBlock($state);
- $_block->position($this->world, $vBlockX, $vBlockY, $vBlockZ);
+ $_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false);
foreach($_block->getAffectedBlocks() as $_affectedBlock){
$_affectedBlockPos = $_affectedBlock->getPosition();
$this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock;
@@ -209,7 +208,7 @@ class Explosion{
}
}
- $air = ItemFactory::air();
+ $air = VanillaItems::AIR();
$airBlock = VanillaBlocks::AIR();
foreach($this->affectedBlocks as $block){
diff --git a/src/world/Position.php b/src/world/Position.php
index 82df931e0..5339adbec 100644
--- a/src/world/Position.php
+++ b/src/world/Position.php
@@ -39,7 +39,7 @@ class Position extends Vector3{
*/
public function __construct($x, $y, $z, ?World $world){
parent::__construct($x, $y, $z);
- if($world !== null and !$world->isLoaded()){
+ if($world !== null && !$world->isLoaded()){
throw new \InvalidArgumentException("Specified world has been unloaded and cannot be used");
}
@@ -77,7 +77,7 @@ class Position extends Vector3{
* Checks if this object has a valid reference to a loaded world
*/
public function isValid() : bool{
- if($this->world !== null and !$this->world->isLoaded()){
+ if($this->world !== null && !$this->world->isLoaded()){
$this->world = null;
return false;
@@ -98,12 +98,12 @@ class Position extends Vector3{
}
public function __toString(){
- return "Position(level=" . ($this->isValid() ? $this->getWorld()->getDisplayName() : "null") . ",x=" . $this->x . ",y=" . $this->y . ",z=" . $this->z . ")";
+ return "Position(world=" . ($this->isValid() ? $this->getWorld()->getDisplayName() : "null") . ",x=" . $this->x . ",y=" . $this->y . ",z=" . $this->z . ")";
}
public function equals(Vector3 $v) : bool{
if($v instanceof Position){
- return parent::equals($v) and $v->world === $this->world;
+ return parent::equals($v) && $v->world === $this->world;
}
return parent::equals($v);
}
diff --git a/src/world/SimpleChunkManager.php b/src/world/SimpleChunkManager.php
index 105412bb5..1b2605f33 100644
--- a/src/world/SimpleChunkManager.php
+++ b/src/world/SimpleChunkManager.php
@@ -81,9 +81,9 @@ class SimpleChunkManager implements ChunkManager{
public function isInWorld(int $x, int $y, int $z) : bool{
return (
- $x <= Limits::INT32_MAX and $x >= Limits::INT32_MIN and
- $y < $this->maxY and $y >= $this->minY and
- $z <= Limits::INT32_MAX and $z >= Limits::INT32_MIN
+ $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
+ $y < $this->maxY && $y >= $this->minY &&
+ $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
);
}
}
diff --git a/src/world/World.php b/src/world/World.php
index 05ff4dcd8..6c2a6a9e5 100644
--- a/src/world/World.php
+++ b/src/world/World.php
@@ -36,6 +36,7 @@ use pocketmine\block\tile\TileFactory;
use pocketmine\block\UnknownBlock;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
+use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\Entity;
use pocketmine\entity\EntityFactory;
use pocketmine\entity\Location;
@@ -51,26 +52,29 @@ use pocketmine\event\world\ChunkUnloadEvent;
use pocketmine\event\world\SpawnChangeEvent;
use pocketmine\event\world\WorldSaveEvent;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
use pocketmine\item\ItemUseResult;
use pocketmine\item\LegacyStringToItemParser;
+use pocketmine\item\StringToItemParser;
+use pocketmine\item\VanillaItems;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Vector3;
-use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\BlockActorDataPacket;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
+use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\UpdateBlockPacket;
use pocketmine\player\Player;
+use pocketmine\promise\Promise;
+use pocketmine\promise\PromiseResolver;
use pocketmine\scheduler\AsyncPool;
use pocketmine\Server;
+use pocketmine\ServerConfigGroup;
use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Limits;
-use pocketmine\utils\Promise;
use pocketmine\utils\ReversePriorityQueue;
use pocketmine\world\biome\Biome;
use pocketmine\world\biome\BiomeRegistry;
@@ -93,7 +97,6 @@ use pocketmine\world\sound\BlockPlaceSound;
use pocketmine\world\sound\Sound;
use pocketmine\world\utils\SubChunkExplorer;
use function abs;
-use function array_fill_keys;
use function array_filter;
use function array_key_exists;
use function array_map;
@@ -116,6 +119,7 @@ use function morton2d_encode;
use function morton3d_decode;
use function morton3d_encode;
use function mt_rand;
+use function preg_match;
use function spl_object_id;
use function strtolower;
use function trim;
@@ -247,13 +251,13 @@ class World implements ChunkManager{
/** @var bool[] */
private $activeChunkPopulationTasks = [];
- /** @var bool[] */
+ /** @var ChunkLockId[] */
private $chunkLock = [];
/** @var int */
private $maxConcurrentChunkPopulationTasks = 2;
/**
- * @var Promise[] chunkHash => promise
- * @phpstan-var array>
+ * @var PromiseResolver[] chunkHash => promise
+ * @phpstan-var array>
*/
private array $chunkPopulationRequestMap = [];
/**
@@ -261,6 +265,12 @@ class World implements ChunkManager{
* @phpstan-var \SplQueue
*/
private \SplQueue $chunkPopulationRequestQueue;
+ /**
+ * @var true[] chunkHash => dummy
+ * @phpstan-var array
+ */
+ private array $chunkPopulationRequestQueueIndex = [];
+
/** @var bool[] */
private $generatorRegisteredWorkers = [];
@@ -323,7 +333,7 @@ class World implements ChunkManager{
private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
- private const BLOCKHASH_XZ_EXTRA_BITS = 6;
+ private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
@@ -411,8 +421,10 @@ class World implements ChunkManager{
$this->maxY = $this->provider->getWorldMaxY();
$this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
- $this->generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator(), true);
- //TODO: validate generator options
+ $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
+ throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
+ $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
+ $this->generator = $generator->getGeneratorClass();
$this->chunkPopulationRequestQueue = new \SplQueue();
$this->addOnUnloadCallback(function() : void{
$this->logger->debug("Cancelling unfulfilled generation requests");
@@ -443,13 +455,7 @@ class World implements ChunkManager{
$this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt("chunk-ticking.blocks-per-subchunk-per-tick", self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
$this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt("chunk-generation.population-queue-size", 2);
- $dontTickBlocks = array_fill_keys($cfg->getProperty("chunk-ticking.disable-block-ticking", []), true);
-
- foreach(BlockFactory::getInstance()->getAllKnownStates() as $state){
- if(!isset($dontTickBlocks[$state->getId()]) and $state->ticksRandomly()){
- $this->randomTickBlocks[$state->getFullId()] = true;
- }
- }
+ $this->initRandomTickBlocksFromConfig($cfg);
$this->timings = new WorldTimings($this);
@@ -465,6 +471,34 @@ class World implements ChunkManager{
});
}
+ private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
+ $dontTickBlocks = [];
+ $parser = StringToItemParser::getInstance();
+ foreach($cfg->getProperty("chunk-ticking.disable-block-ticking", []) as $name){
+ $name = (string) $name;
+ $item = $parser->parse($name);
+ if($item !== null){
+ $block = $item->getBlock();
+ }elseif(preg_match("/^-?\d+$/", $name) === 1){
+ $block = BlockFactory::getInstance()->get((int) $name, 0);
+ }else{
+ //TODO: we probably ought to log an error here
+ continue;
+ }
+
+ if($block->getId() !== BlockLegacyIds::AIR){
+ $dontTickBlocks[$block->getTypeId()] = $name;
+ }
+ }
+
+ foreach(BlockFactory::getInstance()->getAllKnownStates() as $state){
+ $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
+ if($dontTickName === null && $state->ticksRandomly()){
+ $this->randomTickBlocks[$state->getFullId()] = true;
+ }
+ }
+ }
+
public function getTickRateTime() : float{
return $this->tickRateTime;
}
@@ -512,7 +546,7 @@ class World implements ChunkManager{
*/
public function onUnload() : void{
if($this->unloaded){
- throw new \InvalidStateException("Tried to close a world which is already closed");
+ throw new \LogicException("Tried to close a world which is already closed");
}
foreach($this->unloadCallbacks as $callback){
@@ -728,14 +762,9 @@ class World implements ChunkManager{
* Unregisters a chunk listener from all chunks it is listening on in this World.
*/
public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
- $id = spl_object_id($listener);
foreach($this->chunkListeners as $hash => $listeners){
- if(isset($listeners[$id])){
- unset($this->chunkListeners[$hash][$id]);
- if(count($this->chunkListeners[$hash]) === 0){
- unset($this->chunkListeners[$hash]);
- }
- }
+ World::getXZ($hash, $chunkX, $chunkZ);
+ $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
}
}
@@ -750,8 +779,6 @@ class World implements ChunkManager{
/**
* @internal
- *
- * @param Player ...$targets If empty, will send to all players in the world.
*/
public function sendTime(Player ...$targets) : void{
if(count($targets) === 0){
@@ -771,7 +798,7 @@ class World implements ChunkManager{
*/
public function doTick(int $currentTick) : void{
if($this->unloaded){
- throw new \InvalidStateException("Attempted to tick a world which has been closed");
+ throw new \LogicException("Attempted to tick a world which has been closed");
}
$this->timings->doTick->startTiming();
@@ -812,7 +839,7 @@ class World implements ChunkManager{
$this->timings->scheduledBlockUpdates->startTiming();
//Delayed updates
- while($this->scheduledBlockUpdateQueue->count() > 0 and $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
+ while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
/** @var Vector3 $vec */
$vec = $this->scheduledBlockUpdateQueue->extract()["data"];
unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
@@ -851,7 +878,7 @@ class World implements ChunkManager{
//Update entities that need update
Timings::$tickEntity->startTiming();
foreach($this->updateEntities as $id => $entity){
- if($entity->isClosed() or !$entity->onUpdate($currentTick)){
+ if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
unset($this->updateEntities[$id]);
}
if($entity->isFlaggedForDespawn()){
@@ -891,7 +918,7 @@ class World implements ChunkManager{
}
- if($this->sleepTicks > 0 and --$this->sleepTicks <= 0){
+ if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
$this->checkSleep();
}
@@ -922,7 +949,7 @@ class World implements ChunkManager{
if($resetTime){
$time = $this->getTimeOfDay();
- if($time >= World::TIME_NIGHT and $time < World::TIME_SUNRISE){
+ if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
$this->setTime($this->getTime() + World::TIME_FULL - $time);
foreach($this->getPlayers() as $p){
@@ -944,17 +971,25 @@ class World implements ChunkManager{
public function createBlockUpdatePackets(array $blocks) : array{
$packets = [];
+ $blockMapping = RuntimeBlockMapping::getInstance();
+
foreach($blocks as $b){
if(!($b instanceof Vector3)){
throw new \TypeError("Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
}
$fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
- $packets[] = UpdateBlockPacket::create($b->x, $b->y, $b->z, RuntimeBlockMapping::getInstance()->toRuntimeId($fullBlock->getFullId()));
+ $blockPosition = BlockPosition::fromVector3($b);
+ $packets[] = UpdateBlockPacket::create(
+ $blockPosition,
+ $blockMapping->toRuntimeId($fullBlock->getFullId()),
+ UpdateBlockPacket::FLAG_NETWORK,
+ UpdateBlockPacket::DATA_LAYER_NORMAL
+ );
$tile = $this->getTileAt($b->x, $b->y, $b->z);
if($tile instanceof Spawnable){
- $packets[] = BlockActorDataPacket::create($b->x, $b->y, $b->z, $tile->getSerializedSpawnCompound());
+ $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
}
}
@@ -995,7 +1030,7 @@ class World implements ChunkManager{
}
private function tickChunks() : void{
- if($this->chunksPerTick <= 0 or count($this->tickingLoaders) === 0){
+ if($this->chunksPerTick <= 0 || count($this->tickingLoaders) === 0){
return;
}
@@ -1016,7 +1051,7 @@ class World implements ChunkManager{
$dx = mt_rand(-$randRange, $randRange);
$dz = mt_rand(-$randRange, $randRange);
$hash = World::chunkHash($dx + $chunkX, $dz + $chunkZ);
- if(!isset($chunkTickList[$hash]) and isset($this->chunks[$hash]) and $this->isChunkTickable($dx + $chunkX, $dz + $chunkZ)){
+ if(!isset($chunkTickList[$hash]) && isset($this->chunks[$hash]) && $this->isChunkTickable($dx + $chunkX, $dz + $chunkZ)){
$chunkTickList[$hash] = true;
}
}
@@ -1096,6 +1131,7 @@ class World implements ChunkManager{
$entity->onRandomUpdate();
}
+ $blockFactory = BlockFactory::getInstance();
foreach($chunk->getSubChunks() as $Y => $subChunk){
if(!$subChunk->isEmptyFast()){
$k = 0;
@@ -1112,8 +1148,7 @@ class World implements ChunkManager{
$state = $subChunk->getFullBlock($x, $y, $z);
if(isset($this->randomTickBlocks[$state])){
- /** @var Block $block */
- $block = BlockFactory::getInstance()->fromFullBlock($state);
+ $block = $blockFactory->fromFullBlock($state);
$block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
$block->onRandomTick();
}
@@ -1131,7 +1166,7 @@ class World implements ChunkManager{
public function save(bool $force = false) : bool{
- if(!$this->getAutoSave() and !$force){
+ if(!$this->getAutoSave() && !$force){
return false;
}
@@ -1167,8 +1202,8 @@ class World implements ChunkManager{
*/
public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
if(
- !$this->isInWorld($pos->x, $pos->y, $pos->z) or
- (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) and $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
+ !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
+ (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
){
return;
}
@@ -1403,7 +1438,6 @@ class World implements ChunkManager{
public function updateAllLight(int $x, int $y, int $z) : void{
if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
- $this->logger->debug("Skipped runtime light update of x=$x,y=$y,z=$z because the target area has not received base light calculation");
return;
}
@@ -1488,9 +1522,9 @@ class World implements ChunkManager{
public function isInWorld(int $x, int $y, int $z) : bool{
return (
- $x <= Limits::INT32_MAX and $x >= Limits::INT32_MIN and
- $y < $this->maxY and $y >= $this->minY and
- $z <= Limits::INT32_MAX and $z >= Limits::INT32_MIN
+ $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
+ $y < $this->maxY && $y >= $this->minY &&
+ $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
);
}
@@ -1524,7 +1558,7 @@ class World implements ChunkManager{
if($this->isInWorld($x, $y, $z)){
$relativeBlockHash = World::chunkBlockHash($x, $y, $z);
- if($cached and isset($this->blockCache[$chunkHash][$relativeBlockHash])){
+ if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
return $this->blockCache[$chunkHash][$relativeBlockHash];
}
@@ -1554,7 +1588,7 @@ class World implements ChunkManager{
$dynamicStateRead = false;
}
- if($addToCache and $relativeBlockHash !== null){
+ if($addToCache && $relativeBlockHash !== null){
$this->blockCache[$chunkHash][$relativeBlockHash] = $block;
}
@@ -1584,24 +1618,21 @@ class World implements ChunkManager{
}
$chunkX = $x >> Chunk::COORD_BIT_SIZE;
$chunkZ = $z >> Chunk::COORD_BIT_SIZE;
- if($this->isChunkLocked($chunkX, $chunkZ)){
- throw new WorldException("Terrain is locked for generation/population");
- }
if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
throw new WorldException("Cannot set a block in un-generated terrain");
}
$this->timings->setBlock->startTiming();
- $oldBlock = $this->getBlockAt($x, $y, $z, true, false);
+ $this->unlockChunk($chunkX, $chunkZ, null);
$block = clone $block;
$block->position($this, $x, $y, $z);
$block->writeStateToWorld();
- $pos = $block->getPosition();
+ $pos = new Vector3($x, $y, $z);
- $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
+ $chunkHash = World::chunkHash($chunkX, $chunkZ);
$relativeBlockHash = World::chunkBlockHash($x, $y, $z);
unset($this->blockCache[$chunkHash][$relativeBlockHash]);
@@ -1611,14 +1642,12 @@ class World implements ChunkManager{
}
$this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
- foreach($this->getChunkListeners($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE) as $listener){
+ foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
$listener->onBlockChanged($pos);
}
if($update){
- if($oldBlock->getLightFilter() !== $block->getLightFilter() or $oldBlock->getLightLevel() !== $block->getLightLevel()){
- $this->updateAllLight($x, $y, $z);
- }
+ $this->updateAllLight($x, $y, $z);
$this->tryAddToNeighbourUpdateQueue($pos);
foreach($pos->sides() as $side){
$this->tryAddToNeighbourUpdateQueue($side);
@@ -1682,27 +1711,27 @@ class World implements ChunkManager{
$affectedBlocks = $target->getAffectedBlocks();
if($item === null){
- $item = ItemFactory::air();
+ $item = VanillaItems::AIR();
}
$drops = [];
- if($player === null or $player->hasFiniteResources()){
+ if($player === null || $player->hasFiniteResources()){
$drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
}
$xpDrop = 0;
- if($player !== null and $player->hasFiniteResources()){
+ if($player !== null && $player->hasFiniteResources()){
$xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
}
if($player !== null){
$ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
- if($target instanceof Air or ($player->isSurvival() and !$target->getBreakInfo()->isBreakable()) or $player->isSpectator()){
+ if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
$ev->cancel();
}
- if($player->isAdventure(true) and !$ev->isCancelled()){
+ if($player->isAdventure(true) && !$ev->isCancelled()){
$canBreak = false;
$itemParser = LegacyStringToItemParser::getInstance();
foreach($item->getCanDestroy() as $v){
@@ -1801,7 +1830,7 @@ class World implements ChunkManager{
$ev->call();
if(!$ev->isCancelled()){
- if((!$player->isSneaking() or $item->isNull()) and $blockClicked->onInteract($item, $face, $clickVector, $player)){
+ if((!$player->isSneaking() || $item->isNull()) && $blockClicked->onInteract($item, $face, $clickVector, $player)){
return true;
}
@@ -1816,7 +1845,7 @@ class World implements ChunkManager{
return true;
}
- if($item->isNull() or !$item->canBePlaced()){
+ if($item->isNull() || !$item->canBePlaced()){
return false;
}
$hand = $item->getBlock($face);
@@ -1849,7 +1878,7 @@ class World implements ChunkManager{
$ev->cancel();
}
- if($player->isAdventure(true) and !$ev->isCancelled()){
+ if($player->isAdventure(true) && !$ev->isCancelled()){
$canPlace = false;
$itemParser = LegacyStringToItemParser::getInstance();
foreach($item->getCanPlaceOn() as $v){
@@ -1914,9 +1943,9 @@ class World implements ChunkManager{
public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
$nearby = [];
- if($entity === null or $entity->canCollide){
+ if($entity === null || $entity->canCollide){
foreach($this->getNearbyEntities($bb, $entity) as $ent){
- if($ent->canBeCollidedWith() and ($entity === null or $entity->canCollideWith($ent))){
+ if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
$nearby[] = $ent;
}
}
@@ -1944,7 +1973,7 @@ class World implements ChunkManager{
continue;
}
foreach($this->getChunkEntities($x, $z) as $ent){
- if($ent !== $entity and $ent->boundingBox->intersectsWith($bb)){
+ if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
$nearby[] = $ent;
}
}
@@ -1987,7 +2016,7 @@ class World implements ChunkManager{
continue;
}
foreach($this->getChunkEntities($x, $z) as $entity){
- if(!($entity instanceof $entityType) or $entity->isFlaggedForDespawn() or (!$includeDead and !$entity->isAlive())){
+ if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
continue;
}
$distSq = $entity->getPosition()->distanceSquared($pos);
@@ -2042,10 +2071,7 @@ class World implements ChunkManager{
public function setBiomeId(int $x, int $z, int $biomeId) : void{
$chunkX = $x >> Chunk::COORD_BIT_SIZE;
$chunkZ = $z >> Chunk::COORD_BIT_SIZE;
- if($this->isChunkLocked($chunkX, $chunkZ)){
- //the changes would be overwritten when the generation finishes
- throw new WorldException("Chunk is currently locked for async generation/population");
- }
+ $this->unlockChunk($chunkX, $chunkZ, null);
if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
$chunk->setBiomeId($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
}else{
@@ -2082,121 +2108,100 @@ class World implements ChunkManager{
/**
* Returns the chunks adjacent to the specified chunk.
*
- * @return (Chunk|null)[]
+ * @return Chunk[]|null[]
*/
public function getAdjacentChunks(int $x, int $z) : array{
$result = [];
- for($xx = 0; $xx <= 2; ++$xx){
- for($zz = 0; $zz <= 2; ++$zz){
- $i = $zz * 3 + $xx;
- if($i === 4){
+ for($xx = -1; $xx <= 1; ++$xx){
+ for($zz = -1; $zz <= 1; ++$zz){
+ if($xx === 0 && $zz === 0){
continue; //center chunk
}
- $result[$i] = $this->loadChunk($x + $xx - 1, $z + $zz - 1);
+ $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
}
}
return $result;
}
- public function lockChunk(int $chunkX, int $chunkZ) : void{
+ /**
+ * Flags a chunk as locked, usually for async modification.
+ *
+ * This is an **advisory lock**. This means that the lock does **not** prevent the chunk from being modified on the
+ * main thread, such as by setBlock() or setBiomeId(). However, you can use it to detect when such modifications
+ * have taken place - unlockChunk() with the same lockID will fail and return false if this happens.
+ *
+ * This is used internally by the generation system to ensure that two PopulationTasks don't try to modify the same
+ * chunk at the same time. Generation will respect these locks and won't try to do generation of chunks over which
+ * a lock is held.
+ *
+ * WARNING: Be sure to release all locks once you're done with them, or you WILL have problems with terrain not
+ * being generated.
+ */
+ public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
$chunkHash = World::chunkHash($chunkX, $chunkZ);
if(isset($this->chunkLock[$chunkHash])){
throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
}
- $this->chunkLock[$chunkHash] = true;
+ $this->chunkLock[$chunkHash] = $lockId;
}
- public function unlockChunk(int $chunkX, int $chunkZ) : void{
- unset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
+ /**
+ * Unlocks a chunk previously locked by lockChunk().
+ *
+ * You must provide the same lockID as provided to lockChunk().
+ * If a null lockID is given, any existing lock will be removed from the chunk, regardless of who owns it.
+ *
+ * Returns true if unlocking was successful, false otherwise.
+ */
+ public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
+ $chunkHash = World::chunkHash($chunkX, $chunkZ);
+ if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
+ unset($this->chunkLock[$chunkHash]);
+ return true;
+ }
+ return false;
}
+ /**
+ * Returns whether anyone currently has a lock on the chunk at the given coordinates.
+ * You should check this to make sure that population tasks aren't currently modifying chunks that you want to use
+ * in async tasks.
+ */
public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
}
- private function drainPopulationRequestQueue() : void{
- $failed = [];
- while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
- $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
- World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
- if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
- assert(!isset($this->activeChunkPopulationTasks[$nextChunkHash]), "Population for chunk $nextChunkX $nextChunkZ already running");
- $this->orderChunkPopulation($nextChunkX, $nextChunkZ, null);
- if(!isset($this->activeChunkPopulationTasks[$nextChunkHash])){
- $failed[] = $nextChunkHash;
- }
- }
- }
-
- //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
- //queue because it would result in an infinite loop
- foreach($failed as $hash){
- $this->chunkPopulationRequestQueue->enqueue($hash);
- }
- }
-
- public function generateChunkCallback(int $x, int $z, ?Chunk $chunk) : void{
- Timings::$generationCallback->startTiming();
- if(isset($this->chunkPopulationRequestMap[$index = World::chunkHash($x, $z)]) && isset($this->activeChunkPopulationTasks[$index])){
- if($chunk === null){
- throw new AssumptionFailedError("Primary chunk should never be NULL");
- }
- for($xx = -1; $xx <= 1; ++$xx){
- for($zz = -1; $zz <= 1; ++$zz){
- $this->unlockChunk($x + $xx, $z + $zz);
- }
- }
-
- $oldChunk = $this->loadChunk($x, $z);
- $this->setChunk($x, $z, $chunk, false);
- if(($oldChunk === null or !$oldChunk->isPopulated()) and $chunk->isPopulated()){
- (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
-
- foreach($this->getChunkListeners($x, $z) as $listener){
- $listener->onChunkPopulated($x, $z, $chunk);
- }
- }
- unset($this->activeChunkPopulationTasks[$index]);
- $promise = $this->chunkPopulationRequestMap[$index];
- unset($this->chunkPopulationRequestMap[$index]);
- $promise->resolve($chunk);
-
- $this->drainPopulationRequestQueue();
- }elseif($this->isChunkLocked($x, $z)){
- $this->unlockChunk($x, $z);
- if($chunk !== null){
- $this->setChunk($x, $z, $chunk, false);
- }
- $this->drainPopulationRequestQueue();
- }elseif($chunk !== null){
- $this->setChunk($x, $z, $chunk, false);
- }
- Timings::$generationCallback->stopTiming();
- }
-
- /**
- * @param bool $deleteEntitiesAndTiles Whether to delete entities and tiles on the old chunk, or transfer them to the new one
- */
- public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk, bool $deleteEntitiesAndTiles = true) : void{
+ public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
$chunkHash = World::chunkHash($chunkX, $chunkZ);
$oldChunk = $this->loadChunk($chunkX, $chunkZ);
- if($oldChunk !== null and $oldChunk !== $chunk){
- if($deleteEntitiesAndTiles){
- foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
- if(!($entity instanceof Player)){
- $entity->close();
- }
- }
- foreach($oldChunk->getTiles() as $tile){
- $tile->close();
- }
- }else{
- foreach($oldChunk->getTiles() as $tile){
- $chunk->addTile($tile);
- $oldChunk->removeTile($tile);
+ if($oldChunk !== null && $oldChunk !== $chunk){
+ $deletedTiles = 0;
+ $transferredTiles = 0;
+ foreach($oldChunk->getTiles() as $oldTile){
+ $tilePosition = $oldTile->getPosition();
+ $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
+ $localY = $tilePosition->getFloorY();
+ $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
+
+ $newBlock = BlockFactory::getInstance()->fromFullBlock($chunk->getFullBlock($localX, $localY, $localZ));
+ $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
+ if(
+ $expectedTileClass === null || //new block doesn't expect a tile
+ !($oldTile instanceof $expectedTileClass) || //new block expects a different tile
+ (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== null && $newTile !== $oldTile) //new chunk already has a different tile
+ ){
+ $oldTile->close();
+ $deletedTiles++;
+ }else{
+ $transferredTiles++;
+ $chunk->addTile($oldTile);
+ $oldChunk->removeTile($oldTile);
}
}
+ if($deletedTiles > 0 || $transferredTiles > 0){
+ $this->logger->debug("Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
+ }
}
$this->chunks[$chunkHash] = $chunk;
@@ -2220,6 +2225,14 @@ class World implements ChunkManager{
$listener->onChunkChanged($chunkX, $chunkZ, $chunk);
}
}
+
+ for($cX = -1; $cX <= 1; ++$cX){
+ for($cZ = -1; $cZ <= 1; ++$cZ){
+ foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
+ $entity->onNearbyBlockChange();
+ }
+ }
+ }
}
/**
@@ -2269,6 +2282,11 @@ class World implements ChunkManager{
$previousSpawn = $this->getSpawnLocation();
$this->provider->getWorldData()->setSpawn($pos);
(new SpawnChangeEvent($this, $previousSpawn))->call();
+
+ $location = Position::fromObject($pos, $this);
+ foreach($this->players as $player){
+ $player->getNetworkSession()->syncWorldSpawnPoint($location);
+ }
}
/**
@@ -2289,10 +2307,6 @@ class World implements ChunkManager{
}
}
$pos = $entity->getPosition()->asVector3();
- $chunk = $this->getOrLoadChunkAtPosition($pos);
- if($chunk === null){
- throw new \InvalidArgumentException("Cannot add an Entity in an ungenerated chunk");
- }
$this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
$this->entityLastKnownPositions[$entity->getId()] = $pos;
@@ -2388,6 +2402,9 @@ class World implements ChunkManager{
if(!$pos->isValid() || $pos->getWorld() !== $this){
throw new \InvalidArgumentException("Invalid Tile world");
}
+ if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
+ throw new \InvalidArgumentException("Tile position is outside the world bounds");
+ }
$chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
$chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
@@ -2395,7 +2412,7 @@ class World implements ChunkManager{
if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
$this->chunks[$hash]->addTile($tile);
}else{
- throw new \InvalidStateException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
+ throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
}
//delegate tile ticking to the corresponding block
@@ -2424,7 +2441,7 @@ class World implements ChunkManager{
}
public function isChunkInUse(int $x, int $z) : bool{
- return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) and count($this->chunkLoaders[$index]) > 0;
+ return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
}
/**
@@ -2432,8 +2449,6 @@ class World implements ChunkManager{
* returned directly.
*
* @return Chunk|null the requested chunk, or null on failure.
- *
- * @throws \InvalidStateException
*/
public function loadChunk(int $x, int $z) : ?Chunk{
if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
@@ -2482,15 +2497,37 @@ class World implements ChunkManager{
}
private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
+ $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
+
+ $this->timings->syncChunkLoadFixInvalidBlocks->startTiming();
+ $blockFactory = BlockFactory::getInstance();
+ $invalidBlocks = 0;
+ foreach($chunkData->getChunk()->getSubChunks() as $subChunk){
+ foreach($subChunk->getBlockLayers() as $blockLayer){
+ foreach($blockLayer->getPalette() as $blockStateId){
+ $mappedStateId = $blockFactory->getMappedStateId($blockStateId);
+ if($mappedStateId !== $blockStateId){
+ $blockLayer->replaceAll($blockStateId, $mappedStateId);
+ $invalidBlocks++;
+ }
+ }
+ }
+ }
+ if($invalidBlocks > 0){
+ $logger->debug("Fixed $invalidBlocks invalid blockstates");
+ $chunkData->getChunk()->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS, true);
+ }
+ $this->timings->syncChunkLoadFixInvalidBlocks->stopTiming();
+
if(count($chunkData->getEntityNBT()) !== 0){
$this->timings->syncChunkLoadEntities->startTiming();
$entityFactory = EntityFactory::getInstance();
foreach($chunkData->getEntityNBT() as $k => $nbt){
try{
$entity = $entityFactory->createFromData($this, $nbt);
- }catch(NbtDataException $e){
- $this->getLogger()->error("Chunk $chunkX $chunkZ: Bad entity data at list position $k: " . $e->getMessage());
- $this->getLogger()->logException($e);
+ }catch(SavedDataLoadingException $e){
+ $logger->error("Bad entity data at list position $k: " . $e->getMessage());
+ $logger->logException($e);
continue;
}
if($entity === null){
@@ -2501,7 +2538,7 @@ class World implements ChunkManager{
}elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
$saveId = "legacy(" . $saveIdTag->getValue() . ")";
}
- $this->getLogger()->warning("Chunk $chunkX $chunkZ: Deleted unknown entity type $saveId");
+ $logger->warning("Deleted unknown entity type $saveId");
}
//TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
//here, because entities currently add themselves to the world
@@ -2516,15 +2553,23 @@ class World implements ChunkManager{
foreach($chunkData->getTileNBT() as $k => $nbt){
try{
$tile = $tileFactory->createFromData($this, $nbt);
- }catch(NbtDataException $e){
- $this->getLogger()->error("Chunk $chunkX $chunkZ: Bad tile entity data at list position $k: " . $e->getMessage());
- $this->getLogger()->logException($e);
+ }catch(SavedDataLoadingException $e){
+ $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
+ $logger->logException($e);
continue;
}
if($tile === null){
- $this->getLogger()->warning("Chunk $chunkX $chunkZ: Deleted unknown tile entity type " . $nbt->getString("id", ""));
- }elseif(!$this->isChunkLoaded($tile->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE, $tile->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
- $this->logger->error("Chunk $chunkX $chunkZ: Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
+ $logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", ""));
+ continue;
+ }
+
+ $tilePosition = $tile->getPosition();
+ if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
+ $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
+ }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
+ $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
+ }elseif($this->getTile($tilePosition) !== null){
+ $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
}else{
$this->addTile($tile);
}
@@ -2539,7 +2584,7 @@ class World implements ChunkManager{
}
public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
- if(($safe and $this->isChunkInUse($x, $z)) or $this->isSpawnChunk($x, $z)){
+ if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
return false;
}
@@ -2553,7 +2598,7 @@ class World implements ChunkManager{
}
public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
- if($safe and $this->isChunkInUse($x, $z)){
+ if($safe && $this->isChunkInUse($x, $z)){
return false;
}
@@ -2576,7 +2621,7 @@ class World implements ChunkManager{
return false;
}
- if($trySave and $this->getAutoSave()){
+ if($trySave && $this->getAutoSave()){
$this->timings->syncChunkSave->startTiming();
try{
$this->provider->saveChunk($x, $z, new ChunkData(
@@ -2625,14 +2670,14 @@ class World implements ChunkManager{
$spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
$spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
- return abs($X - $spawnX) <= 1 and abs($Z - $spawnZ) <= 1;
+ return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
}
/**
* @throws WorldException if the terrain is not generated
*/
public function getSafeSpawn(?Vector3 $spawn = null) : Position{
- if(!($spawn instanceof Vector3) or $spawn->y < 1){
+ if(!($spawn instanceof Vector3) || $spawn->y < 1){
$spawn = $this->getSpawnLocation();
}
@@ -2657,7 +2702,7 @@ class World implements ChunkManager{
}
}
- for(; $y >= $this->minY and $y < $max; ++$y){
+ for(; $y >= $this->minY && $y < $max; ++$y){
if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
@@ -2743,7 +2788,7 @@ class World implements ChunkManager{
}
public function setDifficulty(int $difficulty) : void{
- if($difficulty < 0 or $difficulty > 3){
+ if($difficulty < 0 || $difficulty > 3){
throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
}
$this->provider->getWorldData()->setDifficulty($difficulty);
@@ -2753,22 +2798,80 @@ class World implements ChunkManager{
}
}
+ private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
+ if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
+ $this->chunkPopulationRequestQueue->enqueue($chunkHash);
+ $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
+ }
+ }
+
/**
* @phpstan-return Promise
*/
private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
$chunkHash = World::chunkHash($chunkX, $chunkZ);
- $this->chunkPopulationRequestQueue->enqueue($chunkHash);
- $promise = $this->chunkPopulationRequestMap[$chunkHash] = new Promise();
+ $this->addChunkHashToPopulationRequestQueue($chunkHash);
+ $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
if($associatedChunkLoader === null){
$temporaryLoader = new class implements ChunkLoader{};
$this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
- $promise->onCompletion(
+ $resolver->getPromise()->onCompletion(
fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
static function() : void{}
);
}
- return $promise;
+ return $resolver->getPromise();
+ }
+
+ private function drainPopulationRequestQueue() : void{
+ $failed = [];
+ while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
+ $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
+ unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
+ World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
+ if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
+ assert(!isset($this->activeChunkPopulationTasks[$nextChunkHash]), "Population for chunk $nextChunkX $nextChunkZ already running");
+ if(
+ !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
+ !isset($this->activeChunkPopulationTasks[$nextChunkHash])
+ ){
+ $failed[] = $nextChunkHash;
+ }
+ }
+ }
+
+ //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
+ //queue because it would result in an infinite loop
+ foreach($failed as $hash){
+ $this->addChunkHashToPopulationRequestQueue($hash);
+ }
+ }
+
+ /**
+ * Checks if a chunk needs to be populated, and whether it's ready to do so.
+ * @return bool[]|PromiseResolver[]|null[]
+ * @phpstan-return array{?PromiseResolver, bool}
+ */
+ private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
+ $chunkHash = World::chunkHash($chunkX, $chunkZ);
+ $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
+ if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
+ //generation is already running
+ return [$resolver, false];
+ }
+
+ $temporaryChunkLoader = new class implements ChunkLoader{};
+ $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
+ $chunk = $this->loadChunk($chunkX, $chunkZ);
+ $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
+ if($chunk !== null && $chunk->isPopulated()){
+ //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
+ $resolver ??= new PromiseResolver();
+ unset($this->chunkPopulationRequestMap[$chunkHash]);
+ $resolver->resolve($chunk);
+ return [$resolver, false];
+ }
+ return [$resolver, true];
}
/**
@@ -2783,17 +2886,16 @@ class World implements ChunkManager{
* @phpstan-return Promise
*/
public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
- $chunkHash = World::chunkHash($chunkX, $chunkZ);
- $promise = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
- if($promise !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
- //generation is already running
- return $promise;
+ [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
+ if(!$proceedWithPopulation){
+ return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
}
+
if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
//too many chunks are already generating; delay resolution of the request until later
- return $promise ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
+ return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
}
- return $this->orderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader);
+ return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
}
/**
@@ -2806,53 +2908,146 @@ class World implements ChunkManager{
*
* @phpstan-return Promise
*/
- public function orderChunkPopulation(int $x, int $z, ?ChunkLoader $associatedChunkLoader) : Promise{
- $index = World::chunkHash($x, $z);
- $promise = $this->chunkPopulationRequestMap[$index] ?? null;
- if($promise !== null && isset($this->activeChunkPopulationTasks[$index])){
- //generation is already running
- return $promise;
+ public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
+ [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
+ if(!$proceedWithPopulation){
+ return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
}
+
+ return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
+ }
+
+ /**
+ * @phpstan-param PromiseResolver|null $resolver
+ * @phpstan-return Promise
+ */
+ private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
+ $chunkHash = World::chunkHash($chunkX, $chunkZ);
+
+ Timings::$population->startTiming();
+
for($xx = -1; $xx <= 1; ++$xx){
for($zz = -1; $zz <= 1; ++$zz){
- if($this->isChunkLocked($x + $xx, $z + $zz)){
+ if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
//chunk is already in use by another generation request; queue the request for later
- return $promise ?? $this->enqueuePopulationRequest($x, $z, $associatedChunkLoader);
+ return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
}
}
}
- $chunk = $this->loadChunk($x, $z);
- if($chunk === null || !$chunk->isPopulated()){
- Timings::$population->startTiming();
-
- $this->activeChunkPopulationTasks[$index] = true;
- if($promise === null){
- $promise = new Promise();
- $this->chunkPopulationRequestMap[$index] = $promise;
- }
-
- for($xx = -1; $xx <= 1; ++$xx){
- for($zz = -1; $zz <= 1; ++$zz){
- $this->lockChunk($x + $xx, $z + $zz);
- }
- }
-
- $task = new PopulationTask($this, $x, $z, $chunk);
- $workerId = $this->workerPool->selectWorker();
- if(!isset($this->generatorRegisteredWorkers[$workerId])){
- $this->registerGeneratorToWorker($workerId);
- }
- $this->workerPool->submitTaskToWorker($task, $workerId);
-
- Timings::$population->stopTiming();
- return $promise;
+ $this->activeChunkPopulationTasks[$chunkHash] = true;
+ if($resolver === null){
+ $resolver = new PromiseResolver();
+ $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
}
- //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
- $result = new Promise();
- $result->resolve($chunk);
- return $result;
+ $chunkPopulationLockId = new ChunkLockId();
+
+ $temporaryChunkLoader = new class implements ChunkLoader{};
+ for($xx = -1; $xx <= 1; ++$xx){
+ for($zz = -1; $zz <= 1; ++$zz){
+ $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
+ $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
+ }
+ }
+
+ $centerChunk = $this->loadChunk($chunkX, $chunkZ);
+ $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
+ $task = new PopulationTask(
+ $this->worldId,
+ $chunkX,
+ $chunkZ,
+ $centerChunk,
+ $adjacentChunks,
+ function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
+ if(!$this->isLoaded()){
+ return;
+ }
+
+ $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
+ }
+ );
+ $workerId = $this->workerPool->selectWorker();
+ if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
+ $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
+ unset($this->generatorRegisteredWorkers[$workerId]);
+ }
+ if(!isset($this->generatorRegisteredWorkers[$workerId])){
+ $this->registerGeneratorToWorker($workerId);
+ }
+ $this->workerPool->submitTaskToWorker($task, $workerId);
+
+ Timings::$population->stopTiming();
+ return $resolver->getPromise();
+ }
+
+ /**
+ * @param Chunk[] $adjacentChunks chunkHash => chunk
+ * @phpstan-param array $adjacentChunks
+ */
+ private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
+ Timings::$generationCallback->startTiming();
+
+ $dirtyChunks = 0;
+ for($xx = -1; $xx <= 1; ++$xx){
+ for($zz = -1; $zz <= 1; ++$zz){
+ $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
+ if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
+ $dirtyChunks++;
+ }
+ }
+ }
+
+ $index = World::chunkHash($x, $z);
+ if(!isset($this->chunkPopulationRequestMap[$index])){
+ $this->logger->debug("Discarding population result for chunk x=$x,z=$z - promise was already broken");
+ unset($this->activeChunkPopulationTasks[$index]);
+ }elseif(isset($this->activeChunkPopulationTasks[$index])){
+ if($dirtyChunks === 0){
+ $oldChunk = $this->loadChunk($x, $z);
+ $this->setChunk($x, $z, $chunk);
+
+ foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
+ World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
+ if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
+ throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
+ }
+ $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
+ }
+
+ if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
+ (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
+
+ foreach($this->getChunkListeners($x, $z) as $listener){
+ $listener->onChunkPopulated($x, $z, $chunk);
+ }
+ }
+ }else{
+ $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
+ }
+
+ //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
+ //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
+ //another PopulationTask. We don't want that because we're here processing the results.
+ //We can't remove the promise from the array before setting the chunks in the world because that would lead
+ //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
+ //middle.
+ unset($this->activeChunkPopulationTasks[$index]);
+
+ if($dirtyChunks === 0){
+ $promise = $this->chunkPopulationRequestMap[$index];
+ unset($this->chunkPopulationRequestMap[$index]);
+ $promise->resolve($chunk);
+ }else{
+ //request failed, stick it back on the queue
+ //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
+ //don't need to be added a second time.
+ $this->addChunkHashToPopulationRequestQueue($index);
+ }
+
+ $this->drainPopulationRequestQueue();
+ }
+ Timings::$generationCallback->stopTiming();
}
public function doChunkGarbageCollection() : void{
diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php
index 452e5a125..65fe54726 100644
--- a/src/world/WorldManager.php
+++ b/src/world/WorldManager.php
@@ -38,6 +38,7 @@ use pocketmine\world\format\io\FormatConverter;
use pocketmine\world\format\io\WorldProviderManager;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\generator\GeneratorManager;
+use pocketmine\world\generator\InvalidGeneratorOptionsException;
use Webmozart\PathUtil\Path;
use function array_keys;
use function array_shift;
@@ -50,6 +51,7 @@ use function iterator_to_array;
use function microtime;
use function round;
use function sprintf;
+use function strval;
use function trim;
class WorldManager{
@@ -102,7 +104,7 @@ class WorldManager{
* it only affects the server on runtime
*/
public function setDefaultWorld(?World $world) : void{
- if($world === null or ($this->isWorldLoaded($world->getFolderName()) and $world !== $this->defaultWorld)){
+ if($world === null || ($this->isWorldLoaded($world->getFolderName()) && $world !== $this->defaultWorld)){
$this->defaultWorld = $world;
}
}
@@ -132,7 +134,7 @@ class WorldManager{
* @throws \InvalidArgumentException
*/
public function unloadWorld(World $world, bool $forceUnload = false) : bool{
- if($world === $this->getDefaultWorld() and !$forceUnload){
+ if($world === $this->getDefaultWorld() && !$forceUnload){
throw new \InvalidArgumentException("The default world cannot be unloaded while running, please switch worlds.");
}
if($world->isDoingTick()){
@@ -140,13 +142,13 @@ class WorldManager{
}
$ev = new WorldUnloadEvent($world);
- if($world === $this->defaultWorld and !$forceUnload){
+ if($world === $this->defaultWorld && !$forceUnload){
$ev->cancel();
}
$ev->call();
- if(!$forceUnload and $ev->isCancelled()){
+ if(!$forceUnload && $ev->isCancelled()){
return false;
}
@@ -157,7 +159,7 @@ class WorldManager{
$safeSpawn = null;
}
foreach($world->getPlayers() as $player){
- if($world === $this->defaultWorld or $safeSpawn === null){
+ if($world === $this->defaultWorld || $safeSpawn === null){
$player->disconnect("Forced default world unload");
}else{
$player->teleport($safeSpawn);
@@ -207,28 +209,50 @@ class WorldManager{
try{
$provider = $providerClass->fromPath($path);
}catch(CorruptedWorldException $e){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError($name, "Corruption detected: " . $e->getMessage())));
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
+ $name,
+ KnownTranslationFactory::pocketmine_level_corrupted($e->getMessage())
+ )));
return false;
}catch(UnsupportedWorldFormatException $e){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError($name, "Unsupported format: " . $e->getMessage())));
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
+ $name,
+ KnownTranslationFactory::pocketmine_level_unsupportedFormat($e->getMessage())
+ )));
+ return false;
+ }
+
+ $generatorEntry = GeneratorManager::getInstance()->getGenerator($provider->getWorldData()->getGenerator());
+ if($generatorEntry === null){
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
+ $name,
+ KnownTranslationFactory::pocketmine_level_unknownGenerator($provider->getWorldData()->getGenerator())
+ )));
return false;
}
try{
- GeneratorManager::getInstance()->getGenerator($provider->getWorldData()->getGenerator(), true);
- }catch(\InvalidArgumentException $e){
- $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError($name, "Unknown generator \"" . $provider->getWorldData()->getGenerator() . "\"")));
+ $generatorEntry->validateGeneratorOptions($provider->getWorldData()->getGeneratorOptions());
+ }catch(InvalidGeneratorOptionsException $e){
+ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
+ $name,
+ KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions(
+ $provider->getWorldData()->getGeneratorOptions(),
+ $provider->getWorldData()->getGenerator(),
+ $e->getMessage()
+ )
+ )));
return false;
}
if(!($provider instanceof WritableWorldProvider)){
if(!$autoUpgrade){
throw new UnsupportedWorldFormatException("World \"$name\" is in an unsupported format and needs to be upgraded");
}
- $this->server->getLogger()->notice("Upgrading world \"$name\" to new format. This may take a while.");
+ $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_start($name)));
$converter = new FormatConverter($provider, $this->providerManager->getDefault(), Path::join($this->server->getDataPath(), "backups", "worlds"), $this->server->getLogger());
$provider = $converter->execute();
- $this->server->getLogger()->notice("Upgraded world \"$name\" to new format successfully. Backed up pre-conversion world at " . $converter->getBackupPath());
+ $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_finish($name, $converter->getBackupPath())));
}
$world = new World($this->server, $name, $provider, $this->server->getAsyncPool());
@@ -247,7 +271,7 @@ class WorldManager{
* @throws \InvalidArgumentException
*/
public function generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration = true) : bool{
- if(trim($name) === "" or $this->isWorldGenerated($name)){
+ if(trim($name) === "" || $this->isWorldGenerated($name)){
return false;
}
@@ -282,7 +306,7 @@ class WorldManager{
$oldProgress = (int) floor(($done / $total) * 100);
$newProgress = (int) floor((++$done / $total) * 100);
if(intdiv($oldProgress, 10) !== intdiv($newProgress, 10) || $done === $total || $done === 1){
- $world->getLogger()->info("Generating spawn terrain chunks: $done / $total ($newProgress%)");
+ $world->getLogger()->info($world->getServer()->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_spawnTerrainGenerationProgress(strval($done), strval($total), strval($newProgress))));
}
},
static function() : void{
@@ -341,7 +365,7 @@ class WorldManager{
}
}
- if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){
+ if($this->autoSave && ++$this->autoSaveTicker >= $this->autoSaveTicks){
$this->autoSaveTicker = 0;
$this->server->getLogger()->debug("[Auto Save] Saving worlds...");
$start = microtime(true);
diff --git a/src/world/WorldTimings.php b/src/world/WorldTimings.php
index 2ace79317..874a5f5fd 100644
--- a/src/world/WorldTimings.php
+++ b/src/world/WorldTimings.php
@@ -45,6 +45,7 @@ class WorldTimings{
public TimingsHandler $syncChunkLoad;
public TimingsHandler $syncChunkLoadData;
+ public TimingsHandler $syncChunkLoadFixInvalidBlocks;
public TimingsHandler $syncChunkLoadEntities;
public TimingsHandler $syncChunkLoadTileEntities;
public TimingsHandler $syncChunkSave;
@@ -64,14 +65,15 @@ class WorldTimings{
$this->entityTick = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Tick Entities");
Timings::init(); //make sure the timers we want are available
- $this->syncChunkSend = new TimingsHandler("** " . $name . "Player Send Chunks", Timings::$playerChunkSend);
- $this->syncChunkSendPrepare = new TimingsHandler("** " . $name . "Player Send Chunk Prepare", Timings::$playerChunkSend);
+ $this->syncChunkSend = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Player Send Chunks", Timings::$playerChunkSend);
+ $this->syncChunkSendPrepare = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Player Send Chunk Prepare", Timings::$playerChunkSend);
- $this->syncChunkLoad = new TimingsHandler("** " . $name . "Chunk Load", Timings::$worldLoad);
- $this->syncChunkLoadData = new TimingsHandler("** " . $name . "Chunk Load - Data");
- $this->syncChunkLoadEntities = new TimingsHandler("** " . $name . "Chunk Load - Entities");
- $this->syncChunkLoadTileEntities = new TimingsHandler("** " . $name . "Chunk Load - TileEntities");
- $this->syncChunkSave = new TimingsHandler("** " . $name . "Chunk Save", Timings::$worldSave);
+ $this->syncChunkLoad = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load", Timings::$worldLoad);
+ $this->syncChunkLoadData = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Data");
+ $this->syncChunkLoadFixInvalidBlocks = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Fix Invalid Blocks");
+ $this->syncChunkLoadEntities = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Entities");
+ $this->syncChunkLoadTileEntities = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - TileEntities");
+ $this->syncChunkSave = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Save", Timings::$worldSave);
$this->doTick = new TimingsHandler($name . "World Tick");
}
diff --git a/src/world/format/BiomeArray.php b/src/world/format/BiomeArray.php
index 30a5d2cb0..ed45e88fc 100644
--- a/src/world/format/BiomeArray.php
+++ b/src/world/format/BiomeArray.php
@@ -48,7 +48,7 @@ final class BiomeArray{
}
private static function idx(int $x, int $z) : int{
- if($x < 0 or $x >= 16 or $z < 0 or $z >= 16){
+ if($x < 0 || $x >= 16 || $z < 0 || $z >= 16){
throw new \InvalidArgumentException("x and z must be in the range 0-15");
}
return ($z << 4) | $x;
@@ -59,7 +59,7 @@ final class BiomeArray{
}
public function set(int $x, int $z, int $biomeId) : void{
- if($biomeId < 0 or $biomeId >= 256){
+ if($biomeId < 0 || $biomeId >= 256){
throw new \InvalidArgumentException("Biome ID must be in the range 0-255");
}
$this->payload[self::idx($x, $z)] = chr($biomeId);
diff --git a/src/world/format/Chunk.php b/src/world/format/Chunk.php
index 3691b4dbd..6a8dad59c 100644
--- a/src/world/format/Chunk.php
+++ b/src/world/format/Chunk.php
@@ -29,15 +29,15 @@ namespace pocketmine\world\format;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\block\tile\Tile;
-use pocketmine\data\bedrock\BiomeIds;
-use function array_fill;
use function array_map;
class Chunk{
- public const DIRTY_FLAG_TERRAIN = 1 << 0;
+ public const DIRTY_FLAG_BLOCKS = 1 << 0;
public const DIRTY_FLAG_BIOMES = 1 << 3;
- public const MAX_SUBCHUNKS = 16;
+ public const MIN_SUBCHUNK_INDEX = 0;
+ public const MAX_SUBCHUNK_INDEX = 15;
+ public const MAX_SUBCHUNKS = self::MAX_SUBCHUNK_INDEX - self::MIN_SUBCHUNK_INDEX + 1;
public const EDGE_LENGTH = SubChunk::EDGE_LENGTH;
public const COORD_BIT_SIZE = SubChunk::COORD_BIT_SIZE;
@@ -69,16 +69,18 @@ class Chunk{
/**
* @param SubChunk[] $subChunks
*/
- public function __construct(array $subChunks = [], ?BiomeArray $biomeIds = null, ?HeightArray $heightMap = null){
+ public function __construct(array $subChunks, BiomeArray $biomeIds, bool $terrainPopulated){
$this->subChunks = new \SplFixedArray(Chunk::MAX_SUBCHUNKS);
foreach($this->subChunks as $y => $null){
- $this->subChunks[$y] = $subChunks[$y] ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
+ $this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
}
- $val = ($this->subChunks->getSize() * SubChunk::EDGE_LENGTH);
- $this->heightMap = $heightMap ?? new HeightArray(array_fill(0, 256, $val));
- $this->biomeIds = $biomeIds ?? BiomeArray::fill(BiomeIds::OCEAN);
+ $val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH;
+ $this->heightMap = HeightArray::fill($val); //TODO: what about lazily initializing this?
+ $this->biomeIds = $biomeIds;
+
+ $this->terrainPopulated = $terrainPopulated;
}
/**
@@ -106,7 +108,7 @@ class Chunk{
*/
public function setFullBlock(int $x, int $y, int $z, int $block) : void{
$this->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)->setFullBlock($x, $y & SubChunk::COORD_MASK, $z, $block);
- $this->terrainDirtyFlags |= self::DIRTY_FLAG_TERRAIN;
+ $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS;
}
/**
@@ -118,7 +120,7 @@ class Chunk{
* @return int|null 0-255, or null if there are no blocks in the column
*/
public function getHighestBlockAt(int $x, int $z) : ?int{
- for($y = $this->subChunks->count() - 1; $y >= 0; --$y){
+ for($y = self::MAX_SUBCHUNK_INDEX; $y >= self::MIN_SUBCHUNK_INDEX; --$y){
$height = $this->getSubChunk($y)->getHighestBlockAt($x, $z);
if($height !== null){
return $height | ($y << SubChunk::COORD_BIT_SIZE);
@@ -186,7 +188,7 @@ class Chunk{
public function setPopulated(bool $value = true) : void{
$this->terrainPopulated = $value;
- $this->terrainDirtyFlags |= self::DIRTY_FLAG_TERRAIN;
+ $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS;
}
public function addTile(Tile $tile) : void{
@@ -195,7 +197,7 @@ class Chunk{
}
$pos = $tile->getPosition();
- if(isset($this->tiles[$index = Chunk::blockHash($pos->x, $pos->y, $pos->z)]) and $this->tiles[$index] !== $tile){
+ if(isset($this->tiles[$index = Chunk::blockHash($pos->x, $pos->y, $pos->z)]) && $this->tiles[$index] !== $tile){
throw new \InvalidArgumentException("Another tile is already at this location");
}
$this->tiles[$index] = $tile;
@@ -280,30 +282,34 @@ class Chunk{
}
public function getSubChunk(int $y) : SubChunk{
- if($y < 0 || $y >= $this->subChunks->getSize()){
+ if($y < self::MIN_SUBCHUNK_INDEX || $y > self::MAX_SUBCHUNK_INDEX){
throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y");
}
- return $this->subChunks[$y];
+ return $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX];
}
/**
* Sets a subchunk in the chunk index
*/
public function setSubChunk(int $y, ?SubChunk $subChunk) : void{
- if($y < 0 or $y >= $this->subChunks->getSize()){
+ if($y < self::MIN_SUBCHUNK_INDEX || $y > self::MAX_SUBCHUNK_INDEX){
throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y");
}
- $this->subChunks[$y] = $subChunk ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
- $this->setTerrainDirtyFlag(self::DIRTY_FLAG_TERRAIN, true);
+ $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []);
+ $this->setTerrainDirtyFlag(self::DIRTY_FLAG_BLOCKS, true);
}
/**
- * @return \SplFixedArray|SubChunk[]
- * @phpstan-return \SplFixedArray
+ * @return SubChunk[]
+ * @phpstan-return array
*/
- public function getSubChunks() : \SplFixedArray{
- return $this->subChunks;
+ public function getSubChunks() : array{
+ $result = [];
+ foreach($this->subChunks as $yOffset => $subChunk){
+ $result[$yOffset + self::MIN_SUBCHUNK_INDEX] = $subChunk;
+ }
+ return $result;
}
/**
diff --git a/src/world/format/HeightArray.php b/src/world/format/HeightArray.php
index 54c8e19f3..88df7299b 100644
--- a/src/world/format/HeightArray.php
+++ b/src/world/format/HeightArray.php
@@ -50,7 +50,7 @@ final class HeightArray{
}
private static function idx(int $x, int $z) : int{
- if($x < 0 or $x >= 16 or $z < 0 or $z >= 16){
+ if($x < 0 || $x >= 16 || $z < 0 || $z >= 16){
throw new \InvalidArgumentException("x and z must be in the range 0-15");
}
return ($z << 4) | $x;
diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php
index ca6d4ea50..c8ab53a73 100644
--- a/src/world/format/io/FastChunkSerializer.php
+++ b/src/world/format/io/FastChunkSerializer.php
@@ -23,11 +23,10 @@ declare(strict_types=1);
namespace pocketmine\world\format\io;
+use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
-use pocketmine\world\format\HeightArray;
-use pocketmine\world\format\LightArray;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function array_values;
@@ -41,34 +40,25 @@ use function unpack;
* The serialization format **is not intended for permanent storage** and may change without warning.
*/
final class FastChunkSerializer{
- private const FLAG_GENERATED = 1 << 0;
private const FLAG_POPULATED = 1 << 1;
- private const FLAG_HAS_LIGHT = 1 << 2;
private function __construct(){
//NOOP
}
- public static function serializeWithoutLight(Chunk $chunk) : string{
- return self::serialize($chunk, false);
- }
-
/**
* Fast-serializes the chunk for passing between threads
* TODO: tiles and entities
*/
- public static function serialize(Chunk $chunk, bool $includeLight = true) : string{
- $includeLight = $includeLight && $chunk->isLightPopulated() === true;
-
+ public static function serializeTerrain(Chunk $chunk) : string{
$stream = new BinaryStream();
$stream->putByte(
- ($includeLight ? self::FLAG_HAS_LIGHT : 0) |
($chunk->isPopulated() ? self::FLAG_POPULATED : 0)
);
//subchunks
$subChunks = $chunk->getSubChunks();
- $count = $subChunks->count();
+ $count = count($subChunks);
$stream->putByte($count);
foreach($subChunks as $y => $subChunk){
@@ -86,18 +76,10 @@ final class FastChunkSerializer{
$stream->putInt(strlen($serialPalette));
$stream->put($serialPalette);
}
-
- if($includeLight){
- $stream->put($subChunk->getBlockSkyLightArray()->getData());
- $stream->put($subChunk->getBlockLightArray()->getData());
- }
}
//biomes
$stream->put($chunk->getBiomeIdArray());
- if($includeLight){
- $stream->put(pack("S*", ...$chunk->getHeightMapArray()));
- }
return $stream->getBuffer();
}
@@ -105,19 +87,17 @@ final class FastChunkSerializer{
/**
* Deserializes a fast-serialized chunk
*/
- public static function deserialize(string $data) : Chunk{
+ public static function deserializeTerrain(string $data) : Chunk{
$stream = new BinaryStream($data);
$flags = $stream->getByte();
- $lightPopulated = (bool) ($flags & self::FLAG_HAS_LIGHT);
$terrainPopulated = (bool) ($flags & self::FLAG_POPULATED);
$subChunks = [];
- $heightMap = null;
$count = $stream->getByte();
for($subCount = 0; $subCount < $count; ++$subCount){
- $y = $stream->getByte();
+ $y = Binary::signByte($stream->getByte());
$airBlockId = $stream->getInt();
/** @var PalettedBlockArray[] $layers */
@@ -131,23 +111,11 @@ final class FastChunkSerializer{
$layers[] = PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
- $subChunks[$y] = new SubChunk(
- $airBlockId, $layers, $lightPopulated ? new LightArray($stream->get(2048)) : null, $lightPopulated ? new LightArray($stream->get(2048)) : null
- );
+ $subChunks[$y] = new SubChunk($airBlockId, $layers);
}
$biomeIds = new BiomeArray($stream->get(256));
- if($lightPopulated){
- /** @var int[] $unpackedHeightMap */
- $unpackedHeightMap = unpack("S*", $stream->get(512)); //unpack() will never fail here
- $heightMap = new HeightArray(array_values($unpackedHeightMap));
- }
- $chunk = new Chunk($subChunks, $biomeIds, $heightMap);
- $chunk->setPopulated($terrainPopulated);
- $chunk->setLightPopulated($lightPopulated);
- $chunk->clearTerrainDirtyFlags();
-
- return $chunk;
+ return new Chunk($subChunks, $biomeIds, $terrainPopulated);
}
}
diff --git a/src/world/format/io/FormatConverter.php b/src/world/format/io/FormatConverter.php
index 5ee6ea3a8..2599415c4 100644
--- a/src/world/format/io/FormatConverter.php
+++ b/src/world/format/io/FormatConverter.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\format\io;
use pocketmine\utils\Filesystem;
use pocketmine\world\generator\GeneratorManager;
+use pocketmine\world\generator\normal\Normal;
use pocketmine\world\WorldCreationOptions;
use Webmozart\PathUtil\Path;
use function basename;
@@ -112,7 +113,9 @@ class FormatConverter{
Filesystem::recursiveUnlink($convertedOutput);
}
$this->newProvider->generate($convertedOutput, $data->getName(), WorldCreationOptions::create()
- ->setGeneratorClass(GeneratorManager::getInstance()->getGenerator($data->getGenerator()))
+ //TODO: defaulting to NORMAL here really isn't very good behaviour, but it's consistent with what we already
+ //did previously; besides, WorldManager checks for unknown generators before this is reached anyway.
+ ->setGeneratorClass(GeneratorManager::getInstance()->getGenerator($data->getGenerator())?->getGeneratorClass() ?? Normal::class)
->setGeneratorOptions($data->getGeneratorOptions())
->setSeed($data->getSeed())
->setSpawnPosition($data->getSpawn())
diff --git a/src/world/format/io/WorldProviderManager.php b/src/world/format/io/WorldProviderManager.php
index 001f92913..db67c6221 100644
--- a/src/world/format/io/WorldProviderManager.php
+++ b/src/world/format/io/WorldProviderManager.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io;
+use pocketmine\utils\Utils;
use pocketmine\world\format\io\leveldb\LevelDB;
use pocketmine\world\format\io\region\Anvil;
use pocketmine\world\format\io\region\McRegion;
@@ -62,7 +63,7 @@ final class WorldProviderManager{
public function addProvider(WorldProviderManagerEntry $providerEntry, string $name, bool $overwrite = false) : void{
$name = strtolower($name);
- if(!$overwrite and isset($this->providers[$name])){
+ if(!$overwrite && isset($this->providers[$name])){
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}
@@ -77,7 +78,7 @@ final class WorldProviderManager{
*/
public function getMatchingProviders(string $path) : array{
$result = [];
- foreach($this->providers as $alias => $providerEntry){
+ foreach(Utils::stringifyKeys($this->providers) as $alias => $providerEntry){
if($providerEntry->isValid($path)){
$result[$alias] = $providerEntry;
}
diff --git a/src/world/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php
index e59c066d5..32fde0439 100644
--- a/src/world/format/io/data/BedrockWorldData.php
+++ b/src/world/format/io/data/BedrockWorldData.php
@@ -31,6 +31,7 @@ use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\utils\Binary;
+use pocketmine\utils\Filesystem;
use pocketmine\utils\Limits;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
@@ -39,6 +40,7 @@ use pocketmine\world\generator\Generator;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
+use Webmozart\PathUtil\Path;
use function file_get_contents;
use function file_put_contents;
use function strlen;
@@ -101,7 +103,7 @@ class BedrockWorldData extends BaseNbtWorldData{
$nbt = new LittleEndianNbtSerializer();
$buffer = $nbt->write(new TreeRoot($worldData));
- file_put_contents($path . "level.dat", Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
+ file_put_contents(Path::join($path, "level.dat"), Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
}
protected function load() : CompoundTag{
@@ -164,7 +166,7 @@ class BedrockWorldData extends BaseNbtWorldData{
$nbt = new LittleEndianNbtSerializer();
$buffer = $nbt->write(new TreeRoot($this->compoundTag));
- file_put_contents($this->dataPath, Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
+ Filesystem::safeFilePutContents($this->dataPath, Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
}
public function getDifficulty() : int{
diff --git a/src/world/format/io/data/JavaWorldData.php b/src/world/format/io/data/JavaWorldData.php
index 356579979..e3cb20b6c 100644
--- a/src/world/format/io/data/JavaWorldData.php
+++ b/src/world/format/io/data/JavaWorldData.php
@@ -29,10 +29,13 @@ use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\FloatTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\TreeRoot;
+use pocketmine\utils\Filesystem;
+use pocketmine\utils\Utils;
use pocketmine\world\format\io\exception\CorruptedWorldException;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
+use Webmozart\PathUtil\Path;
use function ceil;
use function file_get_contents;
use function file_put_contents;
@@ -68,7 +71,7 @@ class JavaWorldData extends BaseNbtWorldData{
$nbt = new BigEndianNbtSerializer();
$buffer = zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $worldData))), ZLIB_ENCODING_GZIP);
- file_put_contents($path . "level.dat", $buffer);
+ file_put_contents(Path::join($path, "level.dat"), $buffer);
}
protected function load() : CompoundTag{
@@ -109,8 +112,8 @@ class JavaWorldData extends BaseNbtWorldData{
public function save() : void{
$nbt = new BigEndianNbtSerializer();
- $buffer = zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $this->compoundTag))), ZLIB_ENCODING_GZIP);
- file_put_contents($this->dataPath, $buffer);
+ $buffer = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $this->compoundTag))), ZLIB_ENCODING_GZIP));
+ Filesystem::safeFilePutContents($this->dataPath, $buffer);
}
public function getDifficulty() : int{
diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php
index 6713818ac..91d367fb0 100644
--- a/src/world/format/io/leveldb/LevelDB.php
+++ b/src/world/format/io/leveldb/LevelDB.php
@@ -25,9 +25,11 @@ namespace pocketmine\world\format\io\leveldb;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
+use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
+use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
@@ -142,7 +144,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
public static function isValid(string $path) : bool{
- return file_exists(Path::join($path, "level.dat")) and is_dir(Path::join($path, "db"));
+ 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{
@@ -156,6 +158,9 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
BedrockWorldData::generate($path, $name, $options);
}
+ /**
+ * @throws CorruptedChunkException
+ */
protected function deserializePaletted(BinaryStream $stream) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte() >> 1;
@@ -168,18 +173,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$palette = [];
$idMap = LegacyBlockIdToStringIdMap::getInstance();
for($i = 0, $paletteSize = $stream->getLInt(); $i < $paletteSize; ++$i){
- $offset = $stream->getOffset();
- $tag = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
- $stream->setOffset($offset);
+ try{
+ $offset = $stream->getOffset();
- $id = $idMap->stringToLegacy($tag->getString("name")) ?? BlockLegacyIds::INFO_UPDATE;
- $data = $tag->getShort("val");
- if($id === BlockLegacyIds::AIR){
- //TODO: quick and dirty hack for artifacts left behind by broken world editors
- //we really need a proper state fixer, but this is a pressing issue.
- $data = 0;
+ $tag = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
+ $stream->setOffset($offset);
+
+ $id = $idMap->stringToLegacy($tag->getString("name")) ?? BlockLegacyIds::INFO_UPDATE;
+ $data = $tag->getShort("val");
+ $palette[] = ($id << Block::INTERNAL_METADATA_BITS) | $data;
+ }catch(NbtException $e){
+ throw new CorruptedChunkException("Invalid blockstate NBT at offset $i in paletted storage: " . $e->getMessage(), 0, $e);
}
- $palette[] = ($id << Block::INTERNAL_METADATA_BITS) | $data;
}
//TODO: exceptions
@@ -202,7 +207,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
* @return PalettedBlockArray[]
*/
protected function deserializeLegacyExtraData(string $index, int $chunkVersion) : array{
- if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) === false or $extraRawData === ""){
+ if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) === false || $extraRawData === ""){
return [];
}
@@ -265,7 +270,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
case 3: //MCPE 1.0
$convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion);
- for($y = 0; $y < Chunk::MAX_SUBCHUNKS; ++$y){
+ for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(($data = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y))) === false){
continue;
}
@@ -386,7 +391,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/** @var CompoundTag[] $entities */
$entities = [];
- if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false and $entityData !== ""){
+ if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false && $entityData !== ""){
try{
$entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData));
}catch(NbtDataException $e){
@@ -396,7 +401,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/** @var CompoundTag[] $tiles */
$tiles = [];
- if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false and $tileData !== ""){
+ if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false && $tileData !== ""){
try{
$tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData));
}catch(NbtDataException $e){
@@ -404,20 +409,22 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
}
}
- $chunk = new Chunk(
- $subChunks,
- $biomeArray
- );
-
- //TODO: tile ticks, biome states (?)
-
$finalisationChr = $this->db->get($index . self::TAG_STATE_FINALISATION);
if($finalisationChr !== false){
$finalisation = ord($finalisationChr);
- $chunk->setPopulated($finalisation === self::FINALISATION_DONE);
+ $terrainPopulated = $finalisation === self::FINALISATION_DONE;
}else{ //older versions didn't have this tag
- $chunk->setPopulated();
+ $terrainPopulated = true;
}
+
+ //TODO: tile ticks, biome states (?)
+
+ $chunk = new Chunk(
+ $subChunks,
+ $biomeArray ?? BiomeArray::fill(BiomeIds::OCEAN), //TODO: maybe missing biomes should be an error?
+ $terrainPopulated
+ );
+
if($hasBeenUpgraded){
$chunk->setTerrainDirty(); //trigger rewriting chunk to disk if it was converted from an older format
}
@@ -433,7 +440,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$write->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION));
$chunk = $chunkData->getChunk();
- if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_TERRAIN)){
+ if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS)){
$subChunks = $chunk->getSubChunks();
foreach($subChunks as $y => $subChunk){
$key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y);
@@ -520,7 +527,7 @@ 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 and substr($key, -1) === self::TAG_VERSION){
+ if(strlen($key) === 9 && substr($key, -1) === self::TAG_VERSION){
$chunkX = Binary::readLInt(substr($key, 0, 4));
$chunkZ = Binary::readLInt(substr($key, 4, 4));
try{
@@ -542,7 +549,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
public function calculateChunkCount() : int{
$count = 0;
foreach($this->db->getIterator() as $key => $_){
- if(strlen($key) === 9 and substr($key, -1) === self::TAG_VERSION){
+ if(strlen($key) === 9 && substr($key, -1) === self::TAG_VERSION){
$count++;
}
}
diff --git a/src/world/format/io/region/LegacyAnvilChunkTrait.php b/src/world/format/io/region/LegacyAnvilChunkTrait.php
index 831f02b18..7debb29ba 100644
--- a/src/world/format/io/region/LegacyAnvilChunkTrait.php
+++ b/src/world/format/io/region/LegacyAnvilChunkTrait.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
+use pocketmine\data\bedrock\BiomeIds;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\ByteArrayTag;
@@ -50,7 +51,7 @@ trait LegacyAnvilChunkTrait{
/**
* @throws CorruptedChunkException
*/
- protected function deserializeChunk(string $data) : ChunkData{
+ protected function deserializeChunk(string $data) : ?ChunkData{
$decompressed = @zlib_decode($data);
if($decompressed === false){
throw new CorruptedChunkException("Failed to decompress chunk NBT");
@@ -86,15 +87,16 @@ trait LegacyAnvilChunkTrait{
$biomeArray = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomeArray = $makeBiomeArray($biomesTag->getValue());
+ }else{
+ $biomeArray = BiomeArray::fill(BiomeIds::OCEAN);
}
- $result = new Chunk(
- $subChunks,
- $biomeArray
- );
- $result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
return new ChunkData(
- $result,
+ new Chunk(
+ $subChunks,
+ $biomeArray,
+ $chunk->getByte("TerrainPopulated", 0) !== 0
+ ),
($entitiesTag = $chunk->getTag("Entities")) instanceof ListTag ? self::getCompoundList("Entities", $entitiesTag) : [],
($tilesTag = $chunk->getTag("TileEntities")) instanceof ListTag ? self::getCompoundList("TileEntities", $tilesTag) : [],
);
diff --git a/src/world/format/io/region/McRegion.php b/src/world/format/io/region/McRegion.php
index aab62a3db..bf1597364 100644
--- a/src/world/format/io/region/McRegion.php
+++ b/src/world/format/io/region/McRegion.php
@@ -25,9 +25,11 @@ namespace pocketmine\world\format\io\region;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
+use pocketmine\data\bedrock\BiomeIds;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\ByteArrayTag;
+use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntArrayTag;
use pocketmine\nbt\tag\ListTag;
@@ -44,7 +46,7 @@ class McRegion extends RegionWorldProvider{
/**
* @throws CorruptedChunkException
*/
- protected function deserializeChunk(string $data) : ChunkData{
+ protected function deserializeChunk(string $data) : ?ChunkData{
$decompressed = @zlib_decode($data);
if($decompressed === false){
throw new CorruptedChunkException("Failed to decompress chunk NBT");
@@ -60,6 +62,14 @@ class McRegion extends RegionWorldProvider{
throw new CorruptedChunkException("'Level' key is missing from chunk NBT");
}
+ $legacyGeneratedTag = $chunk->getTag("TerrainGenerated");
+ if($legacyGeneratedTag instanceof ByteTag && $legacyGeneratedTag->getValue() === 0){
+ //In legacy PM before 3.0, PM used to save MCRegion chunks even when they weren't generated. In these cases
+ //(we'll see them in old worlds), some of the tags which we expect to always be present, will be missing.
+ //If TerrainGenerated (PM-specific tag from the olden days) is false, toss the chunk data and don't bother
+ //trying to read it.
+ return null;
+ }
$subChunks = [];
$fullIds = self::readFixedSizeByteArray($chunk, "Blocks", 32768);
$fullData = self::readFixedSizeByteArray($chunk, "Data", 16384);
@@ -80,12 +90,16 @@ class McRegion extends RegionWorldProvider{
$biomeIds = $makeBiomeArray(ChunkUtils::convertBiomeColors($biomeColorsTag->getValue())); //Convert back to original format
}elseif(($biomesTag = $chunk->getTag("Biomes")) instanceof ByteArrayTag){
$biomeIds = $makeBiomeArray($biomesTag->getValue());
+ }else{
+ $biomeIds = BiomeArray::fill(BiomeIds::OCEAN);
}
- $result = new Chunk($subChunks, $biomeIds);
- $result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0);
return new ChunkData(
- $result,
+ new Chunk(
+ $subChunks,
+ $biomeIds,
+ $chunk->getByte("TerrainPopulated", 0) !== 0
+ ),
($entitiesTag = $chunk->getTag("Entities")) instanceof ListTag ? self::getCompoundList("Entities", $entitiesTag) : [],
($tilesTag = $chunk->getTag("TileEntities")) instanceof ListTag ? self::getCompoundList("TileEntities", $tilesTag) : [],
);
diff --git a/src/world/format/io/region/RegionGarbageMap.php b/src/world/format/io/region/RegionGarbageMap.php
index a3dd8e542..9b0373449 100644
--- a/src/world/format/io/region/RegionGarbageMap.php
+++ b/src/world/format/io/region/RegionGarbageMap.php
@@ -95,7 +95,7 @@ final class RegionGarbageMap{
/** @var int|null $prevIndex */
$prevIndex = null;
foreach($this->entries as $k => $entry){
- if($prevIndex !== null and $this->entries[$prevIndex]->getLastSector() + 1 === $entry->getFirstSector()){
+ if($prevIndex !== null && $this->entries[$prevIndex]->getLastSector() + 1 === $entry->getFirstSector()){
//this SHOULD overwrite the previous index and not appear at the end
$this->entries[$prevIndex] = new RegionLocationTableEntry(
$this->entries[$prevIndex]->getFirstSector(),
diff --git a/src/world/format/io/region/RegionLoader.php b/src/world/format/io/region/RegionLoader.php
index dd50f6c38..449052454 100644
--- a/src/world/format/io/region/RegionLoader.php
+++ b/src/world/format/io/region/RegionLoader.php
@@ -169,7 +169,7 @@ class RegionLoader{
}
$compression = $stream->getByte();
- if($compression !== self::COMPRESSION_ZLIB and $compression !== self::COMPRESSION_GZIP){
+ if($compression !== self::COMPRESSION_ZLIB && $compression !== self::COMPRESSION_GZIP){
throw new CorruptedChunkException("Invalid compression type (got $compression, expected " . self::COMPRESSION_ZLIB . " or " . self::COMPRESSION_GZIP . ")");
}
@@ -192,7 +192,7 @@ class RegionLoader{
$endGarbage = $this->garbageTable->end();
$nextSector = $this->nextSector;
- for(; $endGarbage !== null and $endGarbage->getLastSector() + 1 === $nextSector; $endGarbage = $this->garbageTable->end()){
+ for(; $endGarbage !== null && $endGarbage->getLastSector() + 1 === $nextSector; $endGarbage = $this->garbageTable->end()){
$nextSector = $endGarbage->getFirstSector();
$this->garbageTable->remove($endGarbage);
}
@@ -267,7 +267,7 @@ class RegionLoader{
* @throws \InvalidArgumentException
*/
protected static function getChunkOffset(int $x, int $z) : int{
- if($x < 0 or $x > 31 or $z < 0 or $z > 31){
+ if($x < 0 || $x > 31 || $z < 0 || $z > 31){
throw new \InvalidArgumentException("Invalid chunk position in region, expected x/z in range 0-31, got x=$x, z=$z");
}
return $x | ($z << 5);
@@ -298,7 +298,7 @@ class RegionLoader{
fseek($this->filePointer, 0);
$headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH);
- if($headerRaw === false or strlen($headerRaw) !== self::REGION_HEADER_LENGTH){
+ if($headerRaw === false || strlen($headerRaw) !== self::REGION_HEADER_LENGTH){
throw new CorruptedRegionException("Corrupted region header (unexpected end of file)");
}
@@ -311,7 +311,7 @@ class RegionLoader{
$sectorCount = $index & 0xff;
$timestamp = $data[$i + 1025];
- if($offset === 0 or $sectorCount === 0){
+ if($offset === 0 || $sectorCount === 0){
$this->locationTable[$i] = null;
}elseif($offset >= self::FIRST_SECTOR){
$this->bumpNextFreeSector($this->locationTable[$i] = new RegionLocationTableEntry($offset, $sectorCount, $timestamp));
diff --git a/src/world/format/io/region/RegionLocationTableEntry.php b/src/world/format/io/region/RegionLocationTableEntry.php
index 8a7fdfaeb..c3c68be87 100644
--- a/src/world/format/io/region/RegionLocationTableEntry.php
+++ b/src/world/format/io/region/RegionLocationTableEntry.php
@@ -38,7 +38,7 @@ class RegionLocationTableEntry{
* @throws \InvalidArgumentException
*/
public function __construct(int $firstSector, int $sectorCount, int $timestamp){
- if($firstSector < 0 or $firstSector >= 2 ** 24){
+ if($firstSector < 0 || $firstSector >= 2 ** 24){
throw new \InvalidArgumentException("Start sector must be positive, got $firstSector");
}
$this->firstSector = $firstSector;
@@ -79,10 +79,10 @@ class RegionLocationTableEntry{
$entry2Last = $entry2->getLastSector();
return (
- ($entry2->firstSector >= $entry1->firstSector and $entry2->firstSector <= $entry1Last) or
- ($entry2Last >= $entry1->firstSector and $entry2Last <= $entry1Last)
+ ($entry2->firstSector >= $entry1->firstSector && $entry2->firstSector <= $entry1Last) ||
+ ($entry2Last >= $entry1->firstSector && $entry2Last <= $entry1Last)
);
};
- return $overlapCheck($this, $other) or $overlapCheck($other, $this);
+ return $overlapCheck($this, $other) || $overlapCheck($other, $this);
}
}
diff --git a/src/world/format/io/region/RegionWorldProvider.php b/src/world/format/io/region/RegionWorldProvider.php
index 3ac9132f6..25fcf65b3 100644
--- a/src/world/format/io/region/RegionWorldProvider.php
+++ b/src/world/format/io/region/RegionWorldProvider.php
@@ -59,7 +59,7 @@ abstract class RegionWorldProvider extends BaseWorldProvider{
abstract protected static function getPcWorldFormatVersion() : int;
public static function isValid(string $path) : bool{
- if(file_exists(Path::join($path, "level.dat")) and is_dir($regionPath = Path::join($path, "region"))){
+ if(file_exists(Path::join($path, "level.dat")) && is_dir($regionPath = Path::join($path, "region"))){
foreach(scandir($regionPath, SCANDIR_SORT_NONE) as $file){
$extPos = strrpos($file, ".");
if($extPos !== false && substr($file, $extPos + 1) === static::getRegionFileExtension()){
@@ -146,7 +146,7 @@ abstract class RegionWorldProvider extends BaseWorldProvider{
/**
* @throws CorruptedChunkException
*/
- abstract protected function deserializeChunk(string $data) : ChunkData;
+ abstract protected function deserializeChunk(string $data) : ?ChunkData;
/**
* @return CompoundTag[]
@@ -173,6 +173,9 @@ abstract class RegionWorldProvider extends BaseWorldProvider{
protected static function readFixedSizeByteArray(CompoundTag $chunk, string $tagName, int $length) : string{
$tag = $chunk->getTag($tagName);
if(!($tag instanceof ByteArrayTag)){
+ if($tag === null){
+ throw new CorruptedChunkException("'$tagName' key is missing from chunk NBT");
+ }
throw new CorruptedChunkException("Expected TAG_ByteArray for '$tagName'");
}
$data = $tag->getValue();
@@ -188,7 +191,7 @@ abstract class RegionWorldProvider extends BaseWorldProvider{
public function loadChunk(int $chunkX, int $chunkZ) : ?ChunkData{
$regionX = $regionZ = null;
self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
- assert(is_int($regionX) and is_int($regionZ));
+ assert(is_int($regionX) && is_int($regionZ));
if(!file_exists($this->pathToRegion($regionX, $regionZ))){
return null;
diff --git a/src/world/generator/Flat.php b/src/world/generator/Flat.php
index 1aba3ae13..06dadc517 100644
--- a/src/world/generator/Flat.php
+++ b/src/world/generator/Flat.php
@@ -24,47 +24,32 @@ declare(strict_types=1);
namespace pocketmine\world\generator;
use pocketmine\block\VanillaBlocks;
-use pocketmine\item\LegacyStringToItemParser;
-use pocketmine\item\LegacyStringToItemParserException;
use pocketmine\world\ChunkManager;
+use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\SubChunk;
use pocketmine\world\generator\object\OreType;
use pocketmine\world\generator\populator\Ore;
use pocketmine\world\generator\populator\Populator;
-use function array_map;
use function count;
-use function explode;
-use function preg_match;
-use function preg_match_all;
class Flat extends Generator{
+
/** @var Chunk */
private $chunk;
/** @var Populator[] */
private $populators = [];
- /**
- * @var int[]
- * @phpstan-var array
- */
- private $structure;
- /** @var int */
- private $biome;
- /**
- * @var mixed[]
- * @phpstan-var array
- */
- private array $options = [];
+ private FlatGeneratorOptions $options;
/**
* @throws InvalidGeneratorOptionsException
*/
public function __construct(int $seed, string $preset){
parent::__construct($seed, $preset !== "" ? $preset : "2;bedrock,2xdirt,grass;1;");
- $this->parsePreset();
+ $this->options = FlatGeneratorOptions::parsePreset($this->preset);
- if(isset($this->options["decoration"])){
+ if(isset($this->options->getExtraOptions()["decoration"])){
$ores = new Ore();
$stone = VanillaBlocks::STONE();
$ores->setOreTypes([
@@ -83,76 +68,15 @@ class Flat extends Generator{
$this->generateBaseChunk();
}
- /**
- * @return int[]
- * @phpstan-return array
- *
- * @throws InvalidGeneratorOptionsException
- */
- public static function parseLayers(string $layers) : array{
- $result = [];
- $split = array_map('\trim', explode(',', $layers));
- $y = 0;
- $itemParser = LegacyStringToItemParser::getInstance();
- foreach($split as $line){
- preg_match('#^(?:(\d+)[x|*])?(.+)$#', $line, $matches);
- if(count($matches) !== 3){
- throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\"");
- }
-
- $cnt = $matches[1] !== "" ? (int) $matches[1] : 1;
- try{
- $b = $itemParser->parse($matches[2])->getBlock();
- }catch(LegacyStringToItemParserException $e){
- throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\": " . $e->getMessage(), 0, $e);
- }
- for($cY = $y, $y += $cnt; $cY < $y; ++$cY){
- $result[$cY] = $b->getFullId();
- }
- }
-
- return $result;
- }
-
- protected function parsePreset() : void{
- $preset = explode(";", $this->preset);
- $blocks = $preset[1] ?? "";
- $this->biome = (int) ($preset[2] ?? 1);
- $options = $preset[3] ?? "";
- $this->structure = self::parseLayers($blocks);
-
- //TODO: more error checking
- preg_match_all('#(([0-9a-z_]{1,})\(?([0-9a-z_ =:]{0,})\)?),?#', $options, $matches);
- foreach($matches[2] as $i => $option){
- $params = true;
- if($matches[3][$i] !== ""){
- $params = [];
- $p = explode(" ", $matches[3][$i]);
- foreach($p as $k){
- $k = explode("=", $k);
- if(isset($k[1])){
- $params[$k[0]] = $k[1];
- }
- }
- }
- $this->options[$option] = $params;
- }
- }
-
protected function generateBaseChunk() : void{
- $this->chunk = new Chunk();
+ $this->chunk = new Chunk([], BiomeArray::fill($this->options->getBiomeId()), false);
- for($Z = 0; $Z < Chunk::EDGE_LENGTH; ++$Z){
- for($X = 0; $X < Chunk::EDGE_LENGTH; ++$X){
- $this->chunk->setBiomeId($X, $Z, $this->biome);
- }
- }
-
- $count = count($this->structure);
+ $structure = $this->options->getStructure();
+ $count = count($structure);
for($sy = 0; $sy < $count; $sy += SubChunk::EDGE_LENGTH){
$subchunk = $this->chunk->getSubChunk($sy >> SubChunk::COORD_BIT_SIZE);
- for($y = 0; $y < SubChunk::EDGE_LENGTH and isset($this->structure[$y | $sy]); ++$y){
- $id = $this->structure[$y | $sy];
+ for($y = 0; $y < SubChunk::EDGE_LENGTH && isset($structure[$y | $sy]); ++$y){
+ $id = $structure[$y | $sy];
for($Z = 0; $Z < SubChunk::EDGE_LENGTH; ++$Z){
for($X = 0; $X < SubChunk::EDGE_LENGTH; ++$X){
diff --git a/src/world/generator/FlatGeneratorOptions.php b/src/world/generator/FlatGeneratorOptions.php
new file mode 100644
index 000000000..dee3ee78f
--- /dev/null
+++ b/src/world/generator/FlatGeneratorOptions.php
@@ -0,0 +1,127 @@
+ $structure
+ * @phpstan-param array|true> $extraOptions
+ */
+ public function __construct(
+ private array $structure,
+ private int $biomeId,
+ private array $extraOptions = []
+ ){}
+
+ /**
+ * @return int[]
+ * @phpstan-return array
+ */
+ public function getStructure() : array{ return $this->structure; }
+
+ public function getBiomeId() : int{ return $this->biomeId; }
+
+ /**
+ * @return mixed[]
+ * @phpstan-return array|true>
+ */
+ public function getExtraOptions() : array{ return $this->extraOptions; }
+
+ /**
+ * @return int[]
+ * @phpstan-return array
+ *
+ * @throws InvalidGeneratorOptionsException
+ */
+ public static function parseLayers(string $layers) : array{
+ $result = [];
+ $split = array_map('\trim', explode(',', $layers));
+ $y = 0;
+ $itemParser = LegacyStringToItemParser::getInstance();
+ foreach($split as $line){
+ preg_match('#^(?:(\d+)[x|*])?(.+)$#', $line, $matches);
+ if(count($matches) !== 3){
+ throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\"");
+ }
+
+ $cnt = $matches[1] !== "" ? (int) $matches[1] : 1;
+ try{
+ $b = $itemParser->parse($matches[2])->getBlock();
+ }catch(LegacyStringToItemParserException $e){
+ throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\": " . $e->getMessage(), 0, $e);
+ }
+ for($cY = $y, $y += $cnt; $cY < $y; ++$cY){
+ $result[$cY] = $b->getFullId();
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @throws InvalidGeneratorOptionsException
+ */
+ public static function parsePreset(string $presetString) : self{
+ $preset = explode(";", $presetString);
+ $blocks = $preset[1] ?? "";
+ $biomeId = (int) ($preset[2] ?? BiomeIds::PLAINS);
+ $optionsString = $preset[3] ?? "";
+ $structure = self::parseLayers($blocks);
+
+ $options = [];
+ //TODO: more error checking
+ preg_match_all('#(([0-9a-z_]{1,})\(?([0-9a-z_ =:]{0,})\)?),?#', $optionsString, $matches);
+ foreach($matches[2] as $i => $option){
+ $params = true;
+ if($matches[3][$i] !== ""){
+ $params = [];
+ $p = explode(" ", $matches[3][$i]);
+ foreach($p as $k){
+ $k = explode("=", $k);
+ if(isset($k[1])){
+ $params[$k[0]] = $k[1];
+ }
+ }
+ }
+ $options[(string) $option] = $params;
+ }
+ return new self($structure, $biomeId, $options);
+ }
+
+}
diff --git a/src/world/generator/GeneratorManager.php b/src/world/generator/GeneratorManager.php
index 58811f05f..76bda57b6 100644
--- a/src/world/generator/GeneratorManager.php
+++ b/src/world/generator/GeneratorManager.php
@@ -34,35 +34,50 @@ final class GeneratorManager{
use SingletonTrait;
/**
- * @var string[] name => classname mapping
- * @phpstan-var array>
+ * @var GeneratorManagerEntry[] name => classname mapping
+ * @phpstan-var array
*/
private $list = [];
public function __construct(){
- $this->addGenerator(Flat::class, "flat");
- $this->addGenerator(Normal::class, "normal");
- $this->addGenerator(Normal::class, "default");
- $this->addGenerator(Nether::class, "hell");
- $this->addGenerator(Nether::class, "nether");
+ $this->addGenerator(Flat::class, "flat", \Closure::fromCallable(function(string $preset) : ?InvalidGeneratorOptionsException{
+ if($preset === ""){
+ return null;
+ }
+ try{
+ FlatGeneratorOptions::parsePreset($preset);
+ return null;
+ }catch(InvalidGeneratorOptionsException $e){
+ return $e;
+ }
+ }));
+ $this->addGenerator(Normal::class, "normal", fn() => null);
+ $this->addGenerator(Normal::class, "default", fn() => null);
+ $this->addGenerator(Nether::class, "hell", fn() => null);
+ $this->addGenerator(Nether::class, "nether", fn() => null);
}
/**
- * @param string $class Fully qualified name of class that extends \pocketmine\world\generator\Generator
- * @param string $name Alias for this generator type that can be written in configs
- * @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
+ * @param string $class Fully qualified name of class that extends \pocketmine\world\generator\Generator
+ * @param string $name Alias for this generator type that can be written in configs
+ * @param \Closure $presetValidator Callback to validate generator options for new worlds
+ * @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
+ *
+ * @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
+ *
* @phpstan-param class-string $class
*
* @throws \InvalidArgumentException
*/
- public function addGenerator(string $class, string $name, bool $overwrite = false) : void{
+ public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
Utils::testValidInstance($class, Generator::class);
- if(!$overwrite and isset($this->list[$name = strtolower($name)])){
+ $name = strtolower($name);
+ if(!$overwrite && isset($this->list[$name])){
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}
- $this->list[$name] = $class;
+ $this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
}
/**
@@ -75,24 +90,10 @@ final class GeneratorManager{
}
/**
- * Returns a class name of a registered Generator matching the given name.
- *
- * @param bool $throwOnMissing @deprecated this is for backwards compatibility only
- *
- * @return string Name of class that extends Generator
- * @phpstan-return class-string
- *
- * @throws \InvalidArgumentException if the generator type isn't registered
+ * Returns the generator entry of a registered Generator matching the given name, or null if not found.
*/
- public function getGenerator(string $name, bool $throwOnMissing = false){
- if(isset($this->list[$name = strtolower($name)])){
- return $this->list[$name];
- }
-
- if($throwOnMissing){
- throw new \InvalidArgumentException("Alias \"$name\" does not map to any known generator");
- }
- return Normal::class;
+ public function getGenerator(string $name) : ?GeneratorManagerEntry{
+ return $this->list[strtolower($name)] ?? null;
}
/**
@@ -105,8 +106,8 @@ final class GeneratorManager{
*/
public function getGeneratorName(string $class) : string{
Utils::testValidInstance($class, Generator::class);
- foreach($this->list as $name => $c){
- if($c === $class){
+ foreach(Utils::stringifyKeys($this->list) as $name => $c){
+ if($c->getGeneratorClass() === $class){
return $name;
}
}
diff --git a/src/world/generator/GeneratorManagerEntry.php b/src/world/generator/GeneratorManagerEntry.php
new file mode 100644
index 000000000..41611f384
--- /dev/null
+++ b/src/world/generator/GeneratorManagerEntry.php
@@ -0,0 +1,48 @@
+ $generatorClass
+ * @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
+ */
+ public function __construct(
+ private string $generatorClass,
+ private \Closure $presetValidator
+ ){}
+
+ /** @phpstan-return class-string */
+ public function getGeneratorClass() : string{ return $this->generatorClass; }
+
+ /**
+ * @throws InvalidGeneratorOptionsException
+ */
+ public function validateGeneratorOptions(string $generatorOptions) : void{
+ if(($exception = ($this->presetValidator)($generatorOptions)) !== null){
+ throw $exception;
+ }
+ }
+}
diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php
index 87222664e..a50f884fc 100644
--- a/src/world/generator/PopulationTask.php
+++ b/src/world/generator/PopulationTask.php
@@ -23,58 +23,49 @@ declare(strict_types=1);
namespace pocketmine\world\generator;
+use pocketmine\data\bedrock\BiomeIds;
use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\AssumptionFailedError;
+use pocketmine\world\format\BiomeArray;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer;
use pocketmine\world\SimpleChunkManager;
use pocketmine\world\World;
-use function intdiv;
+use function array_map;
+use function igbinary_serialize;
+use function igbinary_unserialize;
+/**
+ * @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array $adjacentChunks) : void
+ */
class PopulationTask extends AsyncTask{
- private const TLS_KEY_WORLD = "world";
+ private const TLS_KEY_ON_COMPLETION = "onCompletion";
- /** @var int */
- public $worldId;
- /** @var int */
- private $chunkX;
- /** @var int */
- private $chunkZ;
+ private int $worldId;
+ private int $chunkX;
+ private int $chunkZ;
- /** @var string|null */
- public $chunk;
+ private ?string $chunk;
- /** @var string|null */
- public $chunk0;
- /** @var string|null */
- public $chunk1;
- /** @var string|null */
- public $chunk2;
- /** @var string|null */
- public $chunk3;
+ private string $adjacentChunks;
- //center chunk
-
- /** @var string|null */
- public $chunk5;
- /** @var string|null */
- public $chunk6;
- /** @var string|null */
- public $chunk7;
- /** @var string|null */
- public $chunk8;
-
- public function __construct(World $world, int $chunkX, int $chunkZ, ?Chunk $chunk){
- $this->worldId = $world->getId();
+ /**
+ * @param Chunk[]|null[] $adjacentChunks
+ * @phpstan-param array $adjacentChunks
+ * @phpstan-param OnCompletion $onCompletion
+ */
+ public function __construct(int $worldId, int $chunkX, int $chunkZ, ?Chunk $chunk, array $adjacentChunks, \Closure $onCompletion){
+ $this->worldId = $worldId;
$this->chunkX = $chunkX;
$this->chunkZ = $chunkZ;
- $this->chunk = $chunk !== null ? FastChunkSerializer::serializeWithoutLight($chunk) : null;
+ $this->chunk = $chunk !== null ? FastChunkSerializer::serializeTerrain($chunk) : null;
- foreach($world->getAdjacentChunks($chunkX, $chunkZ) as $i => $c){
- $this->{"chunk$i"} = $c !== null ? FastChunkSerializer::serializeWithoutLight($c) : null;
- }
+ $this->adjacentChunks = igbinary_serialize(array_map(
+ fn(?Chunk $c) => $c !== null ? FastChunkSerializer::serializeTerrain($c) : null,
+ $adjacentChunks
+ )) ?? throw new AssumptionFailedError("igbinary_serialize() returned null");
- $this->storeLocal(self::TLS_KEY_WORLD, $world);
+ $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
@@ -85,84 +76,78 @@ class PopulationTask extends AsyncTask{
$generator = $context->getGenerator();
$manager = new SimpleChunkManager($context->getWorldMinY(), $context->getWorldMaxY());
- /** @var Chunk[] $chunks */
- $chunks = [];
+ $chunk = $this->chunk !== null ? FastChunkSerializer::deserializeTerrain($this->chunk) : null;
- $chunk = $this->chunk !== null ? FastChunkSerializer::deserialize($this->chunk) : null;
+ /** @var string[] $serialChunks */
+ $serialChunks = igbinary_unserialize($this->adjacentChunks);
+ $chunks = array_map(
+ fn(?string $serialized) => $serialized !== null ? FastChunkSerializer::deserializeTerrain($serialized) : null,
+ $serialChunks
+ );
- for($i = 0; $i < 9; ++$i){
- if($i === 4){
- continue;
- }
- $ck = $this->{"chunk$i"};
- if($ck === null){
- $chunks[$i] = null;
- }else{
- $chunks[$i] = FastChunkSerializer::deserialize($ck);
- }
- }
-
- $manager->setChunk($this->chunkX, $this->chunkZ, $chunk ?? new Chunk());
- if($chunk === null){
- $generator->generateChunk($manager, $this->chunkX, $this->chunkZ);
- $chunk = $manager->getChunk($this->chunkX, $this->chunkZ);
- if($chunk === null){
- throw new AssumptionFailedError("We just set this chunk, so it must exist");
- }
- $chunk->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_TERRAIN, true);
- $chunk->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BIOMES, true);
- }
+ self::setOrGenerateChunk($manager, $generator, $this->chunkX, $this->chunkZ, $chunk);
+ /** @var Chunk[] $resultChunks */
$resultChunks = []; //this is just to keep phpstan's type inference happy
- foreach($chunks as $i => $c){
- $cX = (-1 + $i % 3) + $this->chunkX;
- $cZ = (-1 + intdiv($i, 3)) + $this->chunkZ;
- $manager->setChunk($cX, $cZ, $c ?? new Chunk());
- if($c === null){
- $generator->generateChunk($manager, $cX, $cZ);
- $c = $manager->getChunk($cX, $cZ);
- if($c === null){
- throw new AssumptionFailedError("We just set this chunk, so it must exist");
- }
- $c->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_TERRAIN, true);
- $c->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BIOMES, true);
- }
- $resultChunks[$i] = $c;
+ foreach($chunks as $relativeChunkHash => $c){
+ World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
+ $resultChunks[$relativeChunkHash] = self::setOrGenerateChunk($manager, $generator, $this->chunkX + $relativeX, $this->chunkZ + $relativeZ, $c);
}
$chunks = $resultChunks;
$generator->populateChunk($manager, $this->chunkX, $this->chunkZ);
$chunk = $manager->getChunk($this->chunkX, $this->chunkZ);
+ if($chunk === null){
+ throw new AssumptionFailedError("We just generated this chunk, so it must exist");
+ }
$chunk->setPopulated();
- $this->chunk = FastChunkSerializer::serializeWithoutLight($chunk);
+ $this->chunk = FastChunkSerializer::serializeTerrain($chunk);
- foreach($chunks as $i => $c){
- $this->{"chunk$i"} = $c->isTerrainDirty() ? FastChunkSerializer::serializeWithoutLight($c) : null;
+ $serialChunks = [];
+ foreach($chunks as $relativeChunkHash => $c){
+ $serialChunks[$relativeChunkHash] = $c->isTerrainDirty() ? FastChunkSerializer::serializeTerrain($c) : null;
}
+ $this->adjacentChunks = igbinary_serialize($serialChunks) ?? throw new AssumptionFailedError("igbinary_serialize() returned null");
+ }
+
+ private static function setOrGenerateChunk(SimpleChunkManager $manager, Generator $generator, int $chunkX, int $chunkZ, ?Chunk $chunk) : Chunk{
+ $manager->setChunk($chunkX, $chunkZ, $chunk ?? new Chunk([], BiomeArray::fill(BiomeIds::OCEAN), false));
+ if($chunk === null){
+ $generator->generateChunk($manager, $chunkX, $chunkZ);
+ $chunk = $manager->getChunk($chunkX, $chunkZ);
+ if($chunk === null){
+ throw new AssumptionFailedError("We just set this chunk, so it must exist");
+ }
+ $chunk->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS, true);
+ $chunk->setTerrainDirtyFlag(Chunk::DIRTY_FLAG_BIOMES, true);
+ }
+ return $chunk;
}
public function onCompletion() : void{
- /** @var World $world */
- $world = $this->fetchLocal(self::TLS_KEY_WORLD);
- if($world->isLoaded()){
- $chunk = $this->chunk !== null ? FastChunkSerializer::deserialize($this->chunk) : null;
+ /**
+ * @var \Closure $onCompletion
+ * @phpstan-var OnCompletion $onCompletion
+ */
+ $onCompletion = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
- for($i = 0; $i < 9; ++$i){
- if($i === 4){
- continue;
- }
- $c = $this->{"chunk$i"};
- if($c !== null){
- $xx = -1 + $i % 3;
- $zz = -1 + intdiv($i, 3);
+ $chunk = $this->chunk !== null ?
+ FastChunkSerializer::deserializeTerrain($this->chunk) :
+ throw new AssumptionFailedError("Center chunk should never be null");
- $c = FastChunkSerializer::deserialize($c);
- $world->generateChunkCallback($this->chunkX + $xx, $this->chunkZ + $zz, $c);
- }
+ /**
+ * @var string[]|null[] $serialAdjacentChunks
+ * @phpstan-var array $serialAdjacentChunks
+ */
+ $serialAdjacentChunks = igbinary_unserialize($this->adjacentChunks);
+ $adjacentChunks = [];
+ foreach($serialAdjacentChunks as $relativeChunkHash => $c){
+ if($c !== null){
+ $adjacentChunks[$relativeChunkHash] = FastChunkSerializer::deserializeTerrain($c);
}
-
- $world->generateChunkCallback($this->chunkX, $this->chunkZ, $chunk);
}
+
+ $onCompletion($chunk, $adjacentChunks);
}
}
diff --git a/src/world/generator/hell/Nether.php b/src/world/generator/hell/Nether.php
index 8657e2580..5440c6d04 100644
--- a/src/world/generator/hell/Nether.php
+++ b/src/world/generator/hell/Nether.php
@@ -86,7 +86,7 @@ class Nether extends Generator{
$chunk->setBiomeId($x, $z, BiomeIds::HELL);
for($y = 0; $y < 128; ++$y){
- if($y === 0 or $y === 127){
+ if($y === 0 || $y === 127){
$chunk->setFullBlock($x, $y, $z, $bedrock);
continue;
}
diff --git a/src/world/generator/noise/Noise.php b/src/world/generator/noise/Noise.php
index fde81d10b..1d0b7b3b1 100644
--- a/src/world/generator/noise/Noise.php
+++ b/src/world/generator/noise/Noise.php
@@ -256,7 +256,7 @@ abstract class Noise{
}
for($zz = 0; $zz < $zSize; ++$zz){
- if($xx % $samplingRate !== 0 or $zz % $samplingRate !== 0){
+ if($xx % $samplingRate !== 0 || $zz % $samplingRate !== 0){
$nx = (int) ($xx / $samplingRate) * $samplingRate;
$nz = (int) ($zz / $samplingRate) * $samplingRate;
$noiseArray[$xx][$zz] = Noise::bilinearLerp(
@@ -320,7 +320,7 @@ abstract class Noise{
$dz2 = ($zz - $nz) / ($nnz - $nz);
for($yy = 0; $yy < $ySize; ++$yy){
- if($xx % $xSamplingRate !== 0 or $zz % $zSamplingRate !== 0 or $yy % $ySamplingRate !== 0){
+ if($xx % $xSamplingRate !== 0 || $zz % $zSamplingRate !== 0 || $yy % $ySamplingRate !== 0){
$ny = (int) ($yy / $ySamplingRate) * $ySamplingRate;
$nny = $ny + $ySamplingRate;
diff --git a/src/world/generator/normal/Normal.php b/src/world/generator/normal/Normal.php
index f8a006663..a1aebfc1b 100644
--- a/src/world/generator/normal/Normal.php
+++ b/src/world/generator/normal/Normal.php
@@ -173,7 +173,7 @@ class Normal extends Generator{
$weight = $this->gaussian->kernel[$sx + $this->gaussian->smoothSize][$sz + $this->gaussian->smoothSize];
- if($sx === 0 and $sz === 0){
+ if($sx === 0 && $sz === 0){
$adjacent = $biome;
}else{
$index = World::chunkHash($absoluteX + $sx, $absoluteZ + $sz);
diff --git a/src/world/generator/object/Ore.php b/src/world/generator/object/Ore.php
index aad9cc034..5a3066030 100644
--- a/src/world/generator/object/Ore.php
+++ b/src/world/generator/object/Ore.php
@@ -80,12 +80,12 @@ class Ore{
$sizeY = ($yy + 0.5 - $seedY) / $size;
$sizeY *= $sizeY;
- if($yy > 0 and ($sizeX + $sizeY) < 1){
+ if($yy > 0 && ($sizeX + $sizeY) < 1){
for($zz = $startZ; $zz <= $endZ; ++$zz){
$sizeZ = ($zz + 0.5 - $seedZ) / $size;
$sizeZ *= $sizeZ;
- if(($sizeX + $sizeY + $sizeZ) < 1 and $world->getBlockAt($xx, $yy, $zz)->isSameType($this->type->replaces)){
+ if(($sizeX + $sizeY + $sizeZ) < 1 && $world->getBlockAt($xx, $yy, $zz)->isSameType($this->type->replaces)){
$world->setBlockAt($xx, $yy, $zz, $this->type->material);
}
}
diff --git a/src/world/generator/object/SpruceTree.php b/src/world/generator/object/SpruceTree.php
index 3439b03d4..acac1210a 100644
--- a/src/world/generator/object/SpruceTree.php
+++ b/src/world/generator/object/SpruceTree.php
@@ -58,7 +58,7 @@ class SpruceTree extends Tree{
$xOff = abs($xx - $x);
for($zz = $z - $radius; $zz <= $z + $radius; ++$zz){
$zOff = abs($zz - $z);
- if($xOff === $radius and $zOff === $radius and $radius > 0){
+ if($xOff === $radius && $zOff === $radius && $radius > 0){
continue;
}
diff --git a/src/world/generator/object/TallGrass.php b/src/world/generator/object/TallGrass.php
index 80e75e5a1..8fe70e0c8 100644
--- a/src/world/generator/object/TallGrass.php
+++ b/src/world/generator/object/TallGrass.php
@@ -47,7 +47,7 @@ class TallGrass{
for($c = 0; $c < $count; ++$c){
$x = $random->nextRange($pos->x - $radius, $pos->x + $radius);
$z = $random->nextRange($pos->z - $radius, $pos->z + $radius);
- if($world->getBlockAt($x, $pos->y + 1, $z)->getId() === BlockLegacyIds::AIR and $world->getBlockAt($x, $pos->y, $z)->getId() === BlockLegacyIds::GRASS){
+ if($world->getBlockAt($x, $pos->y + 1, $z)->getId() === BlockLegacyIds::AIR && $world->getBlockAt($x, $pos->y, $z)->getId() === BlockLegacyIds::GRASS){
$world->setBlockAt($x, $pos->y + 1, $z, $arr[$random->nextRange(0, $arrC)]);
}
}
diff --git a/src/world/generator/object/Tree.php b/src/world/generator/object/Tree.php
index 90eef6c3a..9b032ad95 100644
--- a/src/world/generator/object/Tree.php
+++ b/src/world/generator/object/Tree.php
@@ -51,7 +51,7 @@ abstract class Tree{
public function canPlaceObject(ChunkManager $world, int $x, int $y, int $z, Random $random) : bool{
$radiusToCheck = 0;
for($yy = 0; $yy < $this->treeHeight + 3; ++$yy){
- if($yy === 1 or $yy === $this->treeHeight){
+ if($yy === 1 || $yy === $this->treeHeight){
++$radiusToCheck;
}
for($xx = -$radiusToCheck; $xx < ($radiusToCheck + 1); ++$xx){
@@ -105,7 +105,7 @@ abstract class Tree{
$xOff = abs($xx - $x);
for($zz = $z - $mid; $zz <= $z + $mid; ++$zz){
$zOff = abs($zz - $z);
- if($xOff === $mid and $zOff === $mid and ($yOff === 0 or $random->nextBoundedInt(2) === 0)){
+ if($xOff === $mid && $zOff === $mid && ($yOff === 0 || $random->nextBoundedInt(2) === 0)){
continue;
}
if(!$transaction->fetchBlockAt($xx, $yy, $zz)->isSolid()){
@@ -117,6 +117,6 @@ abstract class Tree{
}
protected function canOverride(Block $block) : bool{
- return $block->canBeReplaced() or $block instanceof Sapling or $block instanceof Leaves;
+ return $block->canBeReplaced() || $block instanceof Sapling || $block instanceof Leaves;
}
}
diff --git a/src/world/generator/populator/GroundCover.php b/src/world/generator/populator/GroundCover.php
index da5a336a3..7a72e021c 100644
--- a/src/world/generator/populator/GroundCover.php
+++ b/src/world/generator/populator/GroundCover.php
@@ -57,13 +57,13 @@ class GroundCover implements Populator{
}
$startY = min(127, $startY + $diffY);
$endY = $startY - count($cover);
- for($y = $startY; $y > $endY and $y >= 0; --$y){
+ for($y = $startY; $y > $endY && $y >= 0; --$y){
$b = $cover[$startY - $y];
$id = $factory->fromFullBlock($chunk->getFullBlock($x, $y, $z));
- if($id->getId() === BlockLegacyIds::AIR and $b->isSolid()){
+ if($id->getId() === BlockLegacyIds::AIR && $b->isSolid()){
break;
}
- if($b->canBeFlowedInto() and $id instanceof Liquid){
+ if($b->canBeFlowedInto() && $id instanceof Liquid){
continue;
}
diff --git a/src/world/generator/populator/TallGrass.php b/src/world/generator/populator/TallGrass.php
index 6783e6537..10e3d53c5 100644
--- a/src/world/generator/populator/TallGrass.php
+++ b/src/world/generator/populator/TallGrass.php
@@ -52,7 +52,7 @@ class TallGrass implements Populator{
$z = $random->nextRange($chunkZ * Chunk::EDGE_LENGTH, $chunkZ * Chunk::EDGE_LENGTH + (Chunk::EDGE_LENGTH - 1));
$y = $this->getHighestWorkableBlock($world, $x, $z);
- if($y !== -1 and $this->canTallGrassStay($world, $x, $y, $z)){
+ if($y !== -1 && $this->canTallGrassStay($world, $x, $y, $z)){
$world->setBlockAt($x, $y, $z, $block);
}
}
@@ -60,13 +60,13 @@ class TallGrass implements Populator{
private function canTallGrassStay(ChunkManager $world, int $x, int $y, int $z) : bool{
$b = $world->getBlockAt($x, $y, $z)->getId();
- return ($b === BlockLegacyIds::AIR or $b === BlockLegacyIds::SNOW_LAYER) and $world->getBlockAt($x, $y - 1, $z)->getId() === BlockLegacyIds::GRASS;
+ return ($b === BlockLegacyIds::AIR || $b === BlockLegacyIds::SNOW_LAYER) && $world->getBlockAt($x, $y - 1, $z)->getId() === BlockLegacyIds::GRASS;
}
private function getHighestWorkableBlock(ChunkManager $world, int $x, int $z) : int{
for($y = 127; $y >= 0; --$y){
$b = $world->getBlockAt($x, $y, $z)->getId();
- if($b !== BlockLegacyIds::AIR and $b !== BlockLegacyIds::LEAVES and $b !== BlockLegacyIds::LEAVES2 and $b !== BlockLegacyIds::SNOW_LAYER){
+ if($b !== BlockLegacyIds::AIR && $b !== BlockLegacyIds::LEAVES && $b !== BlockLegacyIds::LEAVES2 && $b !== BlockLegacyIds::SNOW_LAYER){
return $y + 1;
}
}
diff --git a/src/world/generator/populator/Tree.php b/src/world/generator/populator/Tree.php
index 45eb64835..ba4e7d0ae 100644
--- a/src/world/generator/populator/Tree.php
+++ b/src/world/generator/populator/Tree.php
@@ -72,9 +72,9 @@ class Tree implements Populator{
private function getHighestWorkableBlock(ChunkManager $world, int $x, int $z) : int{
for($y = 127; $y >= 0; --$y){
$b = $world->getBlockAt($x, $y, $z)->getId();
- if($b === BlockLegacyIds::DIRT or $b === BlockLegacyIds::GRASS){
+ if($b === BlockLegacyIds::DIRT || $b === BlockLegacyIds::GRASS){
return $y + 1;
- }elseif($b !== BlockLegacyIds::AIR and $b !== BlockLegacyIds::SNOW_LAYER){
+ }elseif($b !== BlockLegacyIds::AIR && $b !== BlockLegacyIds::SNOW_LAYER){
return -1;
}
}
diff --git a/src/world/light/LightPopulationTask.php b/src/world/light/LightPopulationTask.php
index d7cdce64a..97d6344ed 100644
--- a/src/world/light/LightPopulationTask.php
+++ b/src/world/light/LightPopulationTask.php
@@ -51,12 +51,12 @@ class LightPopulationTask extends AsyncTask{
* @phpstan-param \Closure(array $blockLight, array $skyLight, array $heightMap) : void $onCompletion
*/
public function __construct(Chunk $chunk, \Closure $onCompletion){
- $this->chunk = FastChunkSerializer::serialize($chunk);
+ $this->chunk = FastChunkSerializer::serializeTerrain($chunk);
$this->storeLocal(self::TLS_KEY_COMPLETION_CALLBACK, $onCompletion);
}
public function onRun() : void{
- $chunk = FastChunkSerializer::deserialize($this->chunk);
+ $chunk = FastChunkSerializer::deserializeTerrain($this->chunk);
$manager = new SimpleChunkManager(World::Y_MIN, World::Y_MAX);
$manager->setChunk(0, 0, $chunk);
diff --git a/src/world/light/LightUpdate.php b/src/world/light/LightUpdate.php
index a8b196a52..45fa19df3 100644
--- a/src/world/light/LightUpdate.php
+++ b/src/world/light/LightUpdate.php
@@ -134,7 +134,7 @@ abstract class LightUpdate{
if($this->subChunkExplorer->moveTo($cx, $cy, $cz) !== SubChunkExplorerStatus::INVALID){
$this->computeRemoveLight($cx, $cy, $cz, $oldAdjacentLight, $context);
- }elseif($this->getEffectiveLight($cx, $cy, $cz) > 0 and !isset($context->spreadVisited[$index = World::blockHash($cx, $cy, $cz)])){
+ }elseif($this->getEffectiveLight($cx, $cy, $cz) > 0 && !isset($context->spreadVisited[$index = World::blockHash($cx, $cy, $cz)])){
$context->spreadVisited[$index] = true;
$context->spreadQueue->enqueue([$cx, $cy, $cz]);
}
@@ -173,7 +173,7 @@ abstract class LightUpdate{
$lz = $z & SubChunk::COORD_MASK;
$current = $lightArray->get($lx, $ly, $lz);
- if($current !== 0 and $current < $oldAdjacentLevel){
+ if($current !== 0 && $current < $oldAdjacentLevel){
$lightArray->set($lx, $ly, $lz, 0);
if(!isset($context->removalVisited[$index = World::blockHash($x, $y, $z)])){
@@ -201,7 +201,7 @@ abstract class LightUpdate{
if($current < $potentialLight){
$lightArray->set($lx, $ly, $lz, $potentialLight);
- if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)]) and $potentialLight > 1){
+ if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)]) && $potentialLight > 1){
$context->spreadVisited[$index] = true;
$context->spreadQueue->enqueue([$x, $y, $z]);
}
diff --git a/src/world/light/SkyLightUpdate.php b/src/world/light/SkyLightUpdate.php
index e9507c153..a505fde02 100644
--- a/src/world/light/SkyLightUpdate.php
+++ b/src/world/light/SkyLightUpdate.php
@@ -114,11 +114,10 @@ class SkyLightUpdate extends LightUpdate{
//have to avoid filling full light for any subchunk that contains a heightmap Y coordinate
$highestHeightMapPlusOne = max($chunk->getHeightMapArray()) + 1;
$lowestClearSubChunk = ($highestHeightMapPlusOne >> SubChunk::COORD_BIT_SIZE) + (($highestHeightMapPlusOne & SubChunk::COORD_MASK) !== 0 ? 1 : 0);
- $chunkHeight = $chunk->getSubChunks()->count();
- for($y = 0; $y < $lowestClearSubChunk && $y < $chunkHeight; $y++){
+ for($y = Chunk::MIN_SUBCHUNK_INDEX; $y < $lowestClearSubChunk && $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
$chunk->getSubChunk($y)->setBlockSkyLightArray(LightArray::fill(0));
}
- for($y = $lowestClearSubChunk, $yMax = $chunkHeight; $y < $yMax; $y++){
+ for($y = $lowestClearSubChunk; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
$chunk->getSubChunk($y)->setBlockSkyLightArray(LightArray::fill(15));
}
@@ -129,7 +128,7 @@ class SkyLightUpdate extends LightUpdate{
for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){
$currentHeight = $chunk->getHeightMap($x, $z);
- $maxAdjacentHeight = 0;
+ $maxAdjacentHeight = World::Y_MIN;
if($x !== 0){
$maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x - 1, $z));
}
@@ -173,21 +172,21 @@ class SkyLightUpdate extends LightUpdate{
* @phpstan-param \SplFixedArray $directSkyLightBlockers
*/
private static function recalculateHeightMap(Chunk $chunk, \SplFixedArray $directSkyLightBlockers) : HeightArray{
- $maxSubChunkY = $chunk->getSubChunks()->count() - 1;
- for(; $maxSubChunkY >= 0; $maxSubChunkY--){
+ $maxSubChunkY = Chunk::MAX_SUBCHUNK_INDEX;
+ for(; $maxSubChunkY >= Chunk::MIN_SUBCHUNK_INDEX; $maxSubChunkY--){
if(!$chunk->getSubChunk($maxSubChunkY)->isEmptyFast()){
break;
}
}
$result = HeightArray::fill(World::Y_MIN);
- if($maxSubChunkY === -1){ //whole column is definitely empty
+ if($maxSubChunkY < Chunk::MIN_SUBCHUNK_INDEX){ //whole column is definitely empty
return $result;
}
for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){
for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
$y = null;
- for($subChunkY = $maxSubChunkY; $subChunkY >= 0; $subChunkY--){
+ for($subChunkY = $maxSubChunkY; $subChunkY >= Chunk::MIN_SUBCHUNK_INDEX; $subChunkY--){
$subHighestBlockY = $chunk->getSubChunk($subChunkY)->getHighestBlockAt($x, $z);
if($subHighestBlockY !== null){
$y = ($subChunkY * SubChunk::EDGE_LENGTH) + $subHighestBlockY;
diff --git a/src/world/particle/BlockBreakParticle.php b/src/world/particle/BlockBreakParticle.php
index bf3c23511..40e8d8bea 100644
--- a/src/world/particle/BlockBreakParticle.php
+++ b/src/world/particle/BlockBreakParticle.php
@@ -27,6 +27,7 @@ use pocketmine\block\Block;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class BlockBreakParticle implements Particle{
@@ -38,6 +39,6 @@ class BlockBreakParticle implements Particle{
}
public function encode(Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_DESTROY, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()), $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_DESTROY, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()), $pos)];
}
}
diff --git a/src/world/particle/BlockPunchParticle.php b/src/world/particle/BlockPunchParticle.php
index e0a7eaaf8..5ee0c3352 100644
--- a/src/world/particle/BlockPunchParticle.php
+++ b/src/world/particle/BlockPunchParticle.php
@@ -27,6 +27,7 @@ use pocketmine\block\Block;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
/**
* This particle appears when a player is attacking a block face in survival mode attempting to break it.
@@ -44,6 +45,6 @@ class BlockPunchParticle implements Particle{
}
public function encode(Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_PUNCH_BLOCK, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()) | ($this->face << 24), $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_PUNCH_BLOCK, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()) | ($this->face << 24), $pos)];
}
}
diff --git a/src/world/particle/DragonEggTeleportParticle.php b/src/world/particle/DragonEggTeleportParticle.php
index 5c17650da..924fba112 100644
--- a/src/world/particle/DragonEggTeleportParticle.php
+++ b/src/world/particle/DragonEggTeleportParticle.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\particle;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
use function abs;
class DragonEggTeleportParticle implements Particle{
@@ -43,7 +44,7 @@ class DragonEggTeleportParticle implements Particle{
}
private static function boundOrThrow(int $v) : int{
- if($v < -255 or $v > 255){
+ if($v < -255 || $v > 255){
throw new \InvalidArgumentException("Value must be between -255 and 255");
}
return $v;
@@ -57,6 +58,6 @@ class DragonEggTeleportParticle implements Particle{
(abs($this->yDiff) << 8) |
abs($this->zDiff);
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_DRAGON_EGG_TELEPORT, $data, $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_DRAGON_EGG_TELEPORT, $data, $pos)];
}
}
diff --git a/src/world/particle/EndermanTeleportParticle.php b/src/world/particle/EndermanTeleportParticle.php
index 274d1540e..f370e4487 100644
--- a/src/world/particle/EndermanTeleportParticle.php
+++ b/src/world/particle/EndermanTeleportParticle.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\particle;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class EndermanTeleportParticle implements Particle{
public function encode(Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_ENDERMAN_TELEPORT, 0, $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_ENDERMAN_TELEPORT, 0, $pos)];
}
}
diff --git a/src/world/particle/FloatingTextParticle.php b/src/world/particle/FloatingTextParticle.php
index fd7e14f0c..1fe3ee008 100644
--- a/src/world/particle/FloatingTextParticle.php
+++ b/src/world/particle/FloatingTextParticle.php
@@ -28,8 +28,10 @@ use pocketmine\entity\Skin;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
+use pocketmine\network\mcpe\protocol\AdventureSettingsPacket;
use pocketmine\network\mcpe\protocol\PlayerListPacket;
use pocketmine\network\mcpe\protocol\RemoveActorPacket;
+use pocketmine\network\mcpe\protocol\types\DeviceOS;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\network\mcpe\protocol\types\entity\FloatMetadataProperty;
@@ -96,22 +98,31 @@ class FloatingTextParticle implements Particle{
$p[] = PlayerListPacket::add([PlayerListEntry::createAdditionEntry($uuid, $this->entityId, $name, SkinAdapterSingleton::get()->toSkinData(new Skin("Standard_Custom", str_repeat("\x00", 8192))))]);
- $pk = new AddPlayerPacket();
- $pk->uuid = $uuid;
- $pk->username = $name;
- $pk->entityRuntimeId = $this->entityId;
- $pk->position = $pos; //TODO: check offset
- $pk->item = ItemStackWrapper::legacy(ItemStack::null());
-
- $flags = (
+ $actorFlags = (
1 << EntityMetadataFlags::IMMOBILE
);
- $pk->metadata = [
- EntityMetadataProperties::FLAGS => new LongMetadataProperty($flags),
+ $actorMetadata = [
+ EntityMetadataProperties::FLAGS => new LongMetadataProperty($actorFlags),
EntityMetadataProperties::SCALE => new FloatMetadataProperty(0.01) //zero causes problems on debug builds
];
-
- $p[] = $pk;
+ $p[] = AddPlayerPacket::create(
+ $uuid,
+ $name,
+ $this->entityId, //TODO: actor unique ID
+ $this->entityId,
+ "",
+ $pos, //TODO: check offset
+ null,
+ 0,
+ 0,
+ 0,
+ ItemStackWrapper::legacy(ItemStack::null()),
+ $actorMetadata,
+ AdventureSettingsPacket::create(0, 0, 0, 0, 0, $this->entityId),
+ [],
+ "",
+ DeviceOS::UNKNOWN
+ );
$p[] = PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($uuid)]);
}
diff --git a/src/world/particle/MobSpawnParticle.php b/src/world/particle/MobSpawnParticle.php
index bdc2f0144..07706888e 100644
--- a/src/world/particle/MobSpawnParticle.php
+++ b/src/world/particle/MobSpawnParticle.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\particle;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class MobSpawnParticle implements Particle{
/** @var int */
@@ -39,6 +40,6 @@ class MobSpawnParticle implements Particle{
}
public function encode(Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_SPAWN, ($this->width & 0xff) | (($this->height & 0xff) << 8), $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_SPAWN, ($this->width & 0xff) | (($this->height & 0xff) << 8), $pos)];
}
}
diff --git a/src/world/particle/PotionSplashParticle.php b/src/world/particle/PotionSplashParticle.php
index 83ad8c699..1175e9518 100644
--- a/src/world/particle/PotionSplashParticle.php
+++ b/src/world/particle/PotionSplashParticle.php
@@ -26,6 +26,7 @@ namespace pocketmine\world\particle;
use pocketmine\color\Color;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class PotionSplashParticle implements Particle{
@@ -50,6 +51,6 @@ class PotionSplashParticle implements Particle{
}
public function encode(Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_PARTICLE_SPLASH, $this->color->toARGB(), $pos)];
+ return [LevelEventPacket::create(LevelEvent::PARTICLE_SPLASH, $this->color->toARGB(), $pos)];
}
}
diff --git a/src/world/sound/AnvilBreakSound.php b/src/world/sound/AnvilBreakSound.php
index 9da378b35..e57c0e02b 100644
--- a/src/world/sound/AnvilBreakSound.php
+++ b/src/world/sound/AnvilBreakSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class AnvilBreakSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_ANVIL_BREAK, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_ANVIL_BREAK, 0, $pos)];
}
}
diff --git a/src/world/sound/AnvilFallSound.php b/src/world/sound/AnvilFallSound.php
index 7abf3f6ce..fd6efc7fe 100644
--- a/src/world/sound/AnvilFallSound.php
+++ b/src/world/sound/AnvilFallSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class AnvilFallSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_ANVIL_FALL, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_ANVIL_FALL, 0, $pos)];
}
}
diff --git a/src/world/sound/AnvilUseSound.php b/src/world/sound/AnvilUseSound.php
index 93c0b64ee..b384c8624 100644
--- a/src/world/sound/AnvilUseSound.php
+++ b/src/world/sound/AnvilUseSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class AnvilUseSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_ANVIL_USE, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_ANVIL_USE, 0, $pos)];
}
}
diff --git a/src/world/sound/ArrowHitSound.php b/src/world/sound/ArrowHitSound.php
index 1927b0c2c..abfdfc4a4 100644
--- a/src/world/sound/ArrowHitSound.php
+++ b/src/world/sound/ArrowHitSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ArrowHitSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BOW_HIT, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BOW_HIT, $pos, false)];
}
}
diff --git a/src/world/sound/BarrelCloseSound.php b/src/world/sound/BarrelCloseSound.php
index a49330511..be2bdaf79 100644
--- a/src/world/sound/BarrelCloseSound.php
+++ b/src/world/sound/BarrelCloseSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BarrelCloseSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BLOCK_BARREL_CLOSE, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLOCK_BARREL_CLOSE, $pos, false)];
}
}
diff --git a/src/world/sound/BarrelOpenSound.php b/src/world/sound/BarrelOpenSound.php
index 7e77ef2c3..ee695060c 100644
--- a/src/world/sound/BarrelOpenSound.php
+++ b/src/world/sound/BarrelOpenSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BarrelOpenSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BLOCK_BARREL_OPEN, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLOCK_BARREL_OPEN, $pos, false)];
}
}
diff --git a/src/world/sound/BellRingSound.php b/src/world/sound/BellRingSound.php
index c6ee1cffb..c2a45a561 100644
--- a/src/world/sound/BellRingSound.php
+++ b/src/world/sound/BellRingSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
final class BellRingSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BLOCK_BELL_HIT, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLOCK_BELL_HIT, $pos, false)];
}
}
diff --git a/src/world/sound/BlastFurnaceSound.php b/src/world/sound/BlastFurnaceSound.php
new file mode 100644
index 000000000..c7846e6c5
--- /dev/null
+++ b/src/world/sound/BlastFurnaceSound.php
@@ -0,0 +1,35 @@
+block = $block;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BREAK, $pos, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BREAK, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
}
}
diff --git a/src/world/sound/BlockPlaceSound.php b/src/world/sound/BlockPlaceSound.php
index 8635588b4..831c879a1 100644
--- a/src/world/sound/BlockPlaceSound.php
+++ b/src/world/sound/BlockPlaceSound.php
@@ -27,6 +27,7 @@ use pocketmine\block\Block;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BlockPlaceSound implements Sound{
@@ -37,7 +38,7 @@ class BlockPlaceSound implements Sound{
$this->block = $block;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_PLACE, $pos, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::PLACE, $pos, false, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()))];
}
}
diff --git a/src/world/sound/BlockPunchSound.php b/src/world/sound/BlockPunchSound.php
index 0637c4133..03d3ded64 100644
--- a/src/world/sound/BlockPunchSound.php
+++ b/src/world/sound/BlockPunchSound.php
@@ -27,6 +27,7 @@ use pocketmine\block\Block;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when a player attacks a block in survival, attempting to break it.
@@ -40,10 +41,11 @@ class BlockPunchSound implements Sound{
$this->block = $block;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_HIT,
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(
+ LevelSoundEvent::HIT,
$pos,
+ false,
RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId())
)];
}
diff --git a/src/world/sound/BowShootSound.php b/src/world/sound/BowShootSound.php
index 09f644bfb..2b595972f 100644
--- a/src/world/sound/BowShootSound.php
+++ b/src/world/sound/BowShootSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BowShootSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BOW, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BOW, $pos, false)];
}
}
diff --git a/src/world/sound/BucketEmptyLavaSound.php b/src/world/sound/BucketEmptyLavaSound.php
index 2764cc823..e3e991597 100644
--- a/src/world/sound/BucketEmptyLavaSound.php
+++ b/src/world/sound/BucketEmptyLavaSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BucketEmptyLavaSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BUCKET_EMPTY_LAVA, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BUCKET_EMPTY_LAVA, $pos, false)];
}
}
diff --git a/src/world/sound/BucketEmptyWaterSound.php b/src/world/sound/BucketEmptyWaterSound.php
index b3b847721..b80b90311 100644
--- a/src/world/sound/BucketEmptyWaterSound.php
+++ b/src/world/sound/BucketEmptyWaterSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BucketEmptyWaterSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BUCKET_EMPTY_WATER, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BUCKET_EMPTY_WATER, $pos, false)];
}
}
diff --git a/src/world/sound/BucketFillLavaSound.php b/src/world/sound/BucketFillLavaSound.php
index c8dbba300..0315c8a8e 100644
--- a/src/world/sound/BucketFillLavaSound.php
+++ b/src/world/sound/BucketFillLavaSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BucketFillLavaSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BUCKET_FILL_LAVA, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BUCKET_FILL_LAVA, $pos, false)];
}
}
diff --git a/src/world/sound/BucketFillWaterSound.php b/src/world/sound/BucketFillWaterSound.php
index 0661a5da0..431435788 100644
--- a/src/world/sound/BucketFillWaterSound.php
+++ b/src/world/sound/BucketFillWaterSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class BucketFillWaterSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_BUCKET_FILL_WATER, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BUCKET_FILL_WATER, $pos, false)];
}
}
diff --git a/src/world/sound/ChestCloseSound.php b/src/world/sound/ChestCloseSound.php
index 744305b1b..e1c15e523 100644
--- a/src/world/sound/ChestCloseSound.php
+++ b/src/world/sound/ChestCloseSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ChestCloseSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_CHEST_CLOSED, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::CHEST_CLOSED, $pos, false)];
}
}
diff --git a/src/world/sound/ChestOpenSound.php b/src/world/sound/ChestOpenSound.php
index 86a7ea8fc..0e835d5fa 100644
--- a/src/world/sound/ChestOpenSound.php
+++ b/src/world/sound/ChestOpenSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ChestOpenSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_CHEST_OPEN, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::CHEST_OPEN, $pos, false)];
}
}
diff --git a/src/world/sound/ClickSound.php b/src/world/sound/ClickSound.php
index dad9e325a..c02471f87 100644
--- a/src/world/sound/ClickSound.php
+++ b/src/world/sound/ClickSound.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class ClickSound implements Sound{
@@ -39,7 +40,7 @@ class ClickSound implements Sound{
return $this->pitch;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_CLICK, (int) ($this->pitch * 1000), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_CLICK, (int) ($this->pitch * 1000), $pos)];
}
}
diff --git a/src/world/sound/DoorBumpSound.php b/src/world/sound/DoorBumpSound.php
index 2d13b0ad8..86ed0f339 100644
--- a/src/world/sound/DoorBumpSound.php
+++ b/src/world/sound/DoorBumpSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class DoorBumpSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_DOOR_BUMP, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_DOOR_BUMP, 0, $pos)];
}
}
diff --git a/src/world/sound/DoorCrashSound.php b/src/world/sound/DoorCrashSound.php
index fa9524a74..3259c4889 100644
--- a/src/world/sound/DoorCrashSound.php
+++ b/src/world/sound/DoorCrashSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class DoorCrashSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_DOOR_CRASH, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_DOOR_CRASH, 0, $pos)];
}
}
diff --git a/src/world/sound/DoorSound.php b/src/world/sound/DoorSound.php
index 9eca4181d..368136c91 100644
--- a/src/world/sound/DoorSound.php
+++ b/src/world/sound/DoorSound.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class DoorSound implements Sound{
@@ -39,7 +40,7 @@ class DoorSound implements Sound{
return $this->pitch;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_DOOR, (int) ($this->pitch * 1000), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_DOOR, (int) ($this->pitch * 1000), $pos)];
}
}
diff --git a/src/world/sound/EnderChestCloseSound.php b/src/world/sound/EnderChestCloseSound.php
index 07073b523..3b7269821 100644
--- a/src/world/sound/EnderChestCloseSound.php
+++ b/src/world/sound/EnderChestCloseSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class EnderChestCloseSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_ENDERCHEST_CLOSED, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ENDERCHEST_CLOSED, $pos, false)];
}
}
diff --git a/src/world/sound/EnderChestOpenSound.php b/src/world/sound/EnderChestOpenSound.php
index 17e6fef9b..6d88ac1f5 100644
--- a/src/world/sound/EnderChestOpenSound.php
+++ b/src/world/sound/EnderChestOpenSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class EnderChestOpenSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_ENDERCHEST_OPEN, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ENDERCHEST_OPEN, $pos, false)];
}
}
diff --git a/src/world/sound/EndermanTeleportSound.php b/src/world/sound/EndermanTeleportSound.php
index 544ffcd21..a5857aab2 100644
--- a/src/world/sound/EndermanTeleportSound.php
+++ b/src/world/sound/EndermanTeleportSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class EndermanTeleportSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_ENDERMAN_TELEPORT, 0, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_ENDERMAN_TELEPORT, 0, $pos)];
}
}
diff --git a/src/world/sound/EntityAttackNoDamageSound.php b/src/world/sound/EntityAttackNoDamageSound.php
index edd39483c..d329147f6 100644
--- a/src/world/sound/EntityAttackNoDamageSound.php
+++ b/src/world/sound/EntityAttackNoDamageSound.php
@@ -25,18 +25,21 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when a player attacks a mob, but fails to deal damage (e.g. cancelled or attack cooldown).
*/
class EntityAttackNoDamageSound implements Sound{
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_ATTACK_NODAMAGE,
+ LevelSoundEvent::ATTACK_NODAMAGE,
$pos,
-1,
- "minecraft:player"
+ "minecraft:player",
+ false,
+ false
)];
}
}
diff --git a/src/world/sound/EntityAttackSound.php b/src/world/sound/EntityAttackSound.php
index 04b461f49..289f2d597 100644
--- a/src/world/sound/EntityAttackSound.php
+++ b/src/world/sound/EntityAttackSound.php
@@ -25,18 +25,21 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when a player attacks a mob, dealing damage.
*/
class EntityAttackSound implements Sound{
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_ATTACK_STRONG, //TODO: seems like ATTACK is dysfunctional
+ LevelSoundEvent::ATTACK_STRONG, //TODO: seems like ATTACK is dysfunctional
$pos,
-1,
- "minecraft:player"
+ "minecraft:player",
+ false,
+ false
)];
}
}
diff --git a/src/world/sound/EntityLandSound.php b/src/world/sound/EntityLandSound.php
index 983c0d2e6..a973e3ba5 100644
--- a/src/world/sound/EntityLandSound.php
+++ b/src/world/sound/EntityLandSound.php
@@ -28,6 +28,7 @@ use pocketmine\entity\Entity;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when an entity hits the ground after falling a distance that doesn't cause damage, e.g. due to jumping.
@@ -44,13 +45,14 @@ class EntityLandSound implements Sound{
$this->blockLandedOn = $blockLandedOn;
}
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_LAND,
+ LevelSoundEvent::LAND,
$pos,
RuntimeBlockMapping::getInstance()->toRuntimeId($this->blockLandedOn->getFullId()),
- $this->entity::getNetworkTypeId()
- //TODO: does isBaby have any relevance here?
+ $this->entity::getNetworkTypeId(),
+ false, //TODO: does isBaby have any relevance here?
+ false
)];
}
}
diff --git a/src/world/sound/EntityLongFallSound.php b/src/world/sound/EntityLongFallSound.php
index 2e95b9e8c..c6cd2a33a 100644
--- a/src/world/sound/EntityLongFallSound.php
+++ b/src/world/sound/EntityLongFallSound.php
@@ -26,6 +26,7 @@ namespace pocketmine\world\sound;
use pocketmine\entity\Entity;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when an entity hits ground after falling a long distance (damage).
@@ -40,13 +41,14 @@ class EntityLongFallSound implements Sound{
$this->entity = $entity;
}
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_FALL_BIG,
+ LevelSoundEvent::FALL_BIG,
$pos,
-1,
- $this->entity::getNetworkTypeId()
- //TODO: is isBaby relevant here?
+ $this->entity::getNetworkTypeId(),
+ false, //TODO: is isBaby relevant here?
+ false
)];
}
}
diff --git a/src/world/sound/EntityShortFallSound.php b/src/world/sound/EntityShortFallSound.php
index de16c58b0..722a3c1a3 100644
--- a/src/world/sound/EntityShortFallSound.php
+++ b/src/world/sound/EntityShortFallSound.php
@@ -26,6 +26,7 @@ namespace pocketmine\world\sound;
use pocketmine\entity\Entity;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
/**
* Played when an entity hits the ground after falling a short distance.
@@ -39,13 +40,14 @@ class EntityShortFallSound implements Sound{
$this->entity = $entity;
}
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::create(
- LevelSoundEventPacket::SOUND_FALL_SMALL,
+ LevelSoundEvent::FALL_SMALL,
$pos,
-1,
- $this->entity::getNetworkTypeId()
- //TODO: does isBaby have any relevance here?
+ $this->entity::getNetworkTypeId(),
+ false, //TODO: does isBaby have any relevance here?
+ false
)];
}
}
diff --git a/src/world/sound/ExplodeSound.php b/src/world/sound/ExplodeSound.php
index 86ed9b342..b20f5a359 100644
--- a/src/world/sound/ExplodeSound.php
+++ b/src/world/sound/ExplodeSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ExplodeSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_EXPLODE, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::EXPLODE, $pos, false)];
}
}
diff --git a/src/world/sound/FireExtinguishSound.php b/src/world/sound/FireExtinguishSound.php
index 0c0707911..a4f02fb03 100644
--- a/src/world/sound/FireExtinguishSound.php
+++ b/src/world/sound/FireExtinguishSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
final class FireExtinguishSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_EXTINGUISH_FIRE, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::EXTINGUISH_FIRE, $pos, false)];
}
}
diff --git a/src/world/sound/FizzSound.php b/src/world/sound/FizzSound.php
index 0e7b42ffb..62b54ee16 100644
--- a/src/world/sound/FizzSound.php
+++ b/src/world/sound/FizzSound.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class FizzSound implements Sound{
@@ -39,7 +40,7 @@ class FizzSound implements Sound{
return $this->pitch;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_FIZZ, (int) ($this->pitch * 1000), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_FIZZ, (int) ($this->pitch * 1000), $pos)];
}
}
diff --git a/src/world/sound/FlintSteelSound.php b/src/world/sound/FlintSteelSound.php
index 185216959..7edd564cf 100644
--- a/src/world/sound/FlintSteelSound.php
+++ b/src/world/sound/FlintSteelSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class FlintSteelSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_IGNITE, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::IGNITE, $pos, false)];
}
}
diff --git a/src/world/sound/FurnaceSound.php b/src/world/sound/FurnaceSound.php
new file mode 100644
index 000000000..0576837c1
--- /dev/null
+++ b/src/world/sound/FurnaceSound.php
@@ -0,0 +1,35 @@
+block; }
+
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(
+ LevelSoundEvent::ITEM_USE_ON,
+ $pos,
+ false,
+ RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId())
+ )];
+ }
+}
diff --git a/src/world/sound/LaunchSound.php b/src/world/sound/LaunchSound.php
index e6772f1c9..11ef5ad1d 100644
--- a/src/world/sound/LaunchSound.php
+++ b/src/world/sound/LaunchSound.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class LaunchSound implements Sound{
@@ -39,7 +40,7 @@ class LaunchSound implements Sound{
return $this->pitch;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_SHOOT, (int) ($this->pitch * 1000), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_SHOOT, (int) ($this->pitch * 1000), $pos)];
}
}
diff --git a/src/world/sound/LecternPlaceBookSound.php b/src/world/sound/LecternPlaceBookSound.php
new file mode 100644
index 000000000..f7cf5df7e
--- /dev/null
+++ b/src/world/sound/LecternPlaceBookSound.php
@@ -0,0 +1,35 @@
+ 255){
+ if($note < 0 || $note > 255){
throw new \InvalidArgumentException("Note $note is outside accepted range");
}
$this->instrument = $instrument;
$this->note = $note;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_NOTE, $pos, ($this->instrument->getMagicNumber() << 8) | $this->note)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::NOTE, $pos, false, ($this->instrument->getMagicNumber() << 8) | $this->note)];
}
}
diff --git a/src/world/sound/PaintingPlaceSound.php b/src/world/sound/PaintingPlaceSound.php
index c836e0842..2363299f3 100644
--- a/src/world/sound/PaintingPlaceSound.php
+++ b/src/world/sound/PaintingPlaceSound.php
@@ -25,11 +25,12 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class PaintingPlaceSound implements Sound{
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
//item frame and painting have the same sound
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_ITEMFRAME_PLACE, 0, $pos)];
+ return [LevelEventPacket::create(LevelEvent::SOUND_ITEMFRAME_PLACE, 0, $pos)];
}
}
diff --git a/src/world/sound/PopSound.php b/src/world/sound/PopSound.php
index 5bce1145f..a706e6d03 100644
--- a/src/world/sound/PopSound.php
+++ b/src/world/sound/PopSound.php
@@ -25,6 +25,7 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelEvent;
class PopSound implements Sound{
@@ -39,7 +40,7 @@ class PopSound implements Sound{
return $this->pitch;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelEventPacket::create(LevelEventPacket::EVENT_SOUND_POP, (int) ($this->pitch * 1000), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelEventPacket::create(LevelEvent::SOUND_POP, (int) ($this->pitch * 1000), $pos)];
}
}
diff --git a/src/world/sound/PotionFinishBrewingSound.php b/src/world/sound/PotionFinishBrewingSound.php
new file mode 100644
index 000000000..5f292687b
--- /dev/null
+++ b/src/world/sound/PotionFinishBrewingSound.php
@@ -0,0 +1,35 @@
+recordType = $recordType;
}
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create($this->recordType->getSoundId(), $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound($this->recordType->getSoundId(), $pos, false)];
}
}
diff --git a/src/world/sound/RecordStopSound.php b/src/world/sound/RecordStopSound.php
index ccb139f82..0f2b7dcc9 100644
--- a/src/world/sound/RecordStopSound.php
+++ b/src/world/sound/RecordStopSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class RecordStopSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_STOP_RECORD, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::STOP_RECORD, $pos, false)];
}
}
diff --git a/src/world/sound/RedstonePowerOffSound.php b/src/world/sound/RedstonePowerOffSound.php
index d1363ed54..d32ef391e 100644
--- a/src/world/sound/RedstonePowerOffSound.php
+++ b/src/world/sound/RedstonePowerOffSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class RedstonePowerOffSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_POWER_OFF, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::POWER_OFF, $pos, false)];
}
}
diff --git a/src/world/sound/RedstonePowerOnSound.php b/src/world/sound/RedstonePowerOnSound.php
index eaea5f617..3c98832ee 100644
--- a/src/world/sound/RedstonePowerOnSound.php
+++ b/src/world/sound/RedstonePowerOnSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class RedstonePowerOnSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_POWER_ON, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::POWER_ON, $pos, false)];
}
}
diff --git a/src/world/sound/ShulkerBoxCloseSound.php b/src/world/sound/ShulkerBoxCloseSound.php
index cb170aab6..6c8ace783 100644
--- a/src/world/sound/ShulkerBoxCloseSound.php
+++ b/src/world/sound/ShulkerBoxCloseSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ShulkerBoxCloseSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_SHULKERBOX_CLOSED, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::SHULKERBOX_CLOSED, $pos, false)];
}
}
diff --git a/src/world/sound/ShulkerBoxOpenSound.php b/src/world/sound/ShulkerBoxOpenSound.php
index fd75b2f82..9aafb95ce 100644
--- a/src/world/sound/ShulkerBoxOpenSound.php
+++ b/src/world/sound/ShulkerBoxOpenSound.php
@@ -25,10 +25,11 @@ namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
+use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class ShulkerBoxOpenSound implements Sound{
- public function encode(?Vector3 $pos) : array{
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_SHULKERBOX_OPEN, $pos)];
+ public function encode(Vector3 $pos) : array{
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::SHULKERBOX_OPEN, $pos, false)];
}
}
diff --git a/src/world/sound/SmokerSound.php b/src/world/sound/SmokerSound.php
new file mode 100644
index 000000000..19cb29dce
--- /dev/null
+++ b/src/world/sound/SmokerSound.php
@@ -0,0 +1,35 @@
+xpLevel;
}
- public function encode(?Vector3 $pos) : array{
+ public function encode(Vector3 $pos) : array{
//No idea why such odd numbers, but this works...
//TODO: check arbitrary volume
- return [LevelSoundEventPacket::create(LevelSoundEventPacket::SOUND_LEVELUP, $pos, 0x10000000 * intdiv(min(30, $this->xpLevel), 5))];
+ return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::LEVELUP, $pos, false, 0x10000000 * intdiv(min(30, $this->xpLevel), 5))];
}
}
diff --git a/src/world/utils/SubChunkExplorer.php b/src/world/utils/SubChunkExplorer.php
index 9d7faeb96..1a52dd9cb 100644
--- a/src/world/utils/SubChunkExplorer.php
+++ b/src/world/utils/SubChunkExplorer.php
@@ -53,7 +53,7 @@ class SubChunkExplorer{
public function moveTo(int $x, int $y, int $z) : int{
$newChunkX = $x >> SubChunk::COORD_BIT_SIZE;
$newChunkZ = $z >> SubChunk::COORD_BIT_SIZE;
- if($this->currentChunk === null or $this->currentX !== $newChunkX or $this->currentZ !== $newChunkZ){
+ if($this->currentChunk === null || $this->currentX !== $newChunkX || $this->currentZ !== $newChunkZ){
$this->currentX = $newChunkX;
$this->currentZ = $newChunkZ;
$this->currentSubChunk = null;
@@ -65,10 +65,10 @@ class SubChunkExplorer{
}
$newChunkY = $y >> SubChunk::COORD_BIT_SIZE;
- if($this->currentSubChunk === null or $this->currentY !== $newChunkY){
+ if($this->currentSubChunk === null || $this->currentY !== $newChunkY){
$this->currentY = $newChunkY;
- if($this->currentY < 0 or $this->currentY >= $this->currentChunk->getHeight()){
+ if($this->currentY < Chunk::MIN_SUBCHUNK_INDEX || $this->currentY > Chunk::MAX_SUBCHUNK_INDEX){
$this->currentSubChunk = null;
return SubChunkExplorerStatus::INVALID;
}
diff --git a/start.cmd b/start.cmd
index 929e5f7bb..b8ab12ef8 100644
--- a/start.cmd
+++ b/start.cmd
@@ -2,11 +2,24 @@
TITLE PocketMine-MP server software for Minecraft: Bedrock Edition
cd /d %~dp0
+set PHP_BINARY=
+
+where /q php.exe
+if %ERRORLEVEL%==0 (
+ set PHP_BINARY=php
+)
+
if exist bin\php\php.exe (
+ rem always use the local PHP binary if it exists
set PHPRC=""
set PHP_BINARY=bin\php\php.exe
-) else (
- set PHP_BINARY=php
+)
+
+if "%PHP_BINARY%"=="" (
+ echo Couldn't find a PHP binary in system PATH or "%~dp0bin\php"
+ echo Please refer to the installation instructions at https://doc.pmmp.io/en/rtfd/installation.html
+ pause
+ exit 1
)
if exist PocketMine-MP.phar (
diff --git a/start.ps1 b/start.ps1
index 2528dd691..19b2a6158 100644
--- a/start.ps1
+++ b/start.ps1
@@ -11,8 +11,13 @@ if($php -ne ""){
}elseif(Test-Path "bin\php\php.exe"){
$env:PHPRC = ""
$binary = "bin\php\php.exe"
-}else{
+}elseif((Get-Command php -ErrorAction SilentlyContinue)){
$binary = "php"
+}else{
+ echo "Couldn't find a PHP binary in system PATH or $pwd\bin\php"
+ echo "Please refer to the installation instructions at https://doc.pmmp.io/en/rtfd/installation.html"
+ pause
+ exit 1
}
if($file -eq ""){
diff --git a/start.sh b/start.sh
index 087b53db8..63a9a273b 100755
--- a/start.sh
+++ b/start.sh
@@ -23,10 +23,11 @@ if [ "$PHP_BINARY" == "" ]; then
if [ -f ./bin/php7/bin/php ]; then
export PHPRC=""
PHP_BINARY="./bin/php7/bin/php"
- elif [[ ! -z $(type php) ]]; then
+ elif [[ ! -z $(type php 2> /dev/null) ]]; then
PHP_BINARY=$(type -p php)
else
- echo "Couldn't find a working PHP binary, please use the installer."
+ echo "Couldn't find a PHP binary in system PATH or $PWD/bin/php7/bin"
+ echo "Please refer to the installation instructions at https://doc.pmmp.io/en/rtfd/installation.html"
exit 1
fi
fi
diff --git a/tests/gh-actions/build.sh b/tests/gh-actions/build.sh
deleted file mode 100755
index 23b3641bf..000000000
--- a/tests/gh-actions/build.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-VERSION="$1"
-
-sudo apt update && sudo apt install -y \
- re2c \
- libtool \
- libtool-bin \
- zlib1g-dev \
- libcurl4-openssl-dev \
- libxml2-dev \
- libyaml-dev \
- libgmp-dev \
- libzip-dev \
- libssl-dev
-
-INSTALL_DIR="$(pwd)/bin/php7"
-
-export CFLAGS="$CFLAGS -march=x86-64"
-export CXXFLAGS="$CXXFLAGS -march=x86-64"
-
-function build_leveldb {
- local LEVELDB_VERSION="$1"
- echo "Building LevelDB"
- rm -rf "./leveldb-mcpe" || true
- rm -rf "./leveldb-mcpe-build" || true
- mkdir "./leveldb-mcpe"
- curl -fsSL "https://github.com/pmmp/leveldb/archive/$LEVELDB_VERSION.tar.gz" | tar -zx
- mv "./leveldb-$LEVELDB_VERSION" leveldb-mcpe-build
- cd leveldb-mcpe-build
- CFLAGS="-fPIC" CXXFLAGS="-fPIC" cmake . \
- -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \
- -DCMAKE_PREFIX_PATH="$INSTALL_DIR" \
- -DCMAKE_INSTALL_LIBDIR=lib \
- -DLEVELDB_BUILD_TESTS=OFF \
- -DLEVELDB_BUILD_BENCHMARKS=OFF \
- -DLEVELDB_SNAPPY=OFF \
- -DLEVELDB_ZSTD=OFF \
- -DLEVELDB_TCMALLOC=OFF \
- -DCMAKE_BUILD_TYPE=Release
- make -j4 install
- cd ..
-}
-build_leveldb 84348b9b826cc280cde659185695d2170b54824c
-
-rm -rf php-build
-git clone https://github.com/php-build/php-build.git
-cd php-build
-./install-dependencies.sh
-echo '"pthreads",,"https://github.com/pmmp/pthreads.git",,,"extension",' >> share/php-build/extension/definition
-echo '"leveldb",,"https://github.com/pmmp/php-leveldb.git",,"--with-leveldb='$INSTALL_DIR'","extension",' >> share/php-build/extension/definition
-echo '"chunkutils2",,"https://github.com/pmmp/ext-chunkutils2.git",,,"extension",' >> share/php-build/extension/definition
-echo '"morton",,"https://github.com/pmmp/ext-morton.git",,,"extension",' >> share/php-build/extension/definition
-PHP_BUILD_INSTALL_EXTENSION="\
-pthreads=@a6afc0434f91c1e9541444aef6ac7a1f16c595be \
-yaml=2.2.1 \
-leveldb=@60763a09bf5c7a10376d16e25b078b99a35c5c37 \
-chunkutils2=@0.3.1 \
-morton=@0.1.2 \
-igbinary=3.2.1 \
-" PHP_BUILD_ZTS_ENABLE=on PHP_BUILD_CONFIGURE_OPTS='--with-gmp' ./bin/php-build "$VERSION" "$INSTALL_DIR" || exit 1
-
-rm -rf crypto
-git clone --recursive https://github.com/bukka/php-crypto.git crypto
-cd crypto
-git checkout -qf c8867aa944fa5227eaea9d11a6ce282e64c15af9
-git submodule update --init --recursive
-"$INSTALL_DIR/bin/phpize"
-./configure --with-php-config="$INSTALL_DIR/bin/php-config"
-make -j8 install
-echo "extension=crypto.so" >> "$INSTALL_DIR/etc/conf.d/crypto.ini"
-cd ..
-
-rm "$INSTALL_DIR/etc/conf.d/xdebug.ini" || true
diff --git a/tests/gh-actions/install-dependencies.sh b/tests/gh-actions/install-dependencies.sh
deleted file mode 100755
index 368e27e3a..000000000
--- a/tests/gh-actions/install-dependencies.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-sudo apt update && sudo apt install -y \
- libzip5
diff --git a/tests/phpstan/bootstrap.php b/tests/phpstan/bootstrap.php
index ba8ccd3fc..1fef2325c 100644
--- a/tests/phpstan/bootstrap.php
+++ b/tests/phpstan/bootstrap.php
@@ -29,5 +29,5 @@ if(!extension_loaded('libdeflate')){
function libdeflate_deflate_compress(string $data, int $level = 6) : string{}
}
-//TODO: these need to be defined properly or removed
-define('pocketmine\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 2) . '/vendor/autoload.php');
+//opcache breaks PHPStan when dynamic reflection is used - see https://github.com/phpstan/phpstan-src/pull/801#issuecomment-978431013
+ini_set('opcache.enable', 'off');
diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon
index 312971fa6..13603dbe5 100644
--- a/tests/phpstan/configs/actual-problems.neon
+++ b/tests/phpstan/configs/actual-problems.neon
@@ -1,17 +1,1412 @@
parameters:
ignoreErrors:
-
- message: "#^Instanceof between pocketmine\\\\plugin\\\\PluginManager and pocketmine\\\\plugin\\\\PluginManager will always evaluate to true\\.$#"
- count: 1
- path: ../../../src/CrashDump.php
+ message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
+ count: 2
+ path: ../../../build/make-release.php
-
- message: "#^Property pocketmine\\\\event\\\\entity\\\\EntityShootBowEvent\\:\\:\\$projectile \\(pocketmine\\\\entity\\\\projectile\\\\Projectile\\) does not accept pocketmine\\\\entity\\\\Entity\\.$#"
+ message: "#^Parameter \\#1 \\$pharPath of function pocketmine\\\\build\\\\server_phar\\\\buildPhar expects string, array\\\\|string\\|false given\\.$#"
count: 1
- path: ../../../src/event/entity/EntityShootBowEvent.php
+ path: ../../../build/server-phar.php
-
- message: "#^Variable property access on \\$this\\(pocketmine\\\\world\\\\generator\\\\PopulationTask\\)\\.$#"
+ message: "#^Parameter \\#1 \\$strings of function pocketmine\\\\build\\\\server_phar\\\\preg_quote_array expects array\\, array\\ given\\.$#"
+ count: 1
+ path: ../../../build/server-phar.php
+
+ -
+ message: "#^Binary operation \"\\.\" between array\\\\|string\\|false and '/'\\|'\\\\\\\\' results in an error\\.$#"
+ count: 2
+ path: ../../../src/PocketMine.php
+
+ -
+ message: "#^Do\\-while loop condition is always false\\.$#"
+ count: 1
+ path: ../../../src/PocketMine.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 1
+ path: ../../../src/Server.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of static method pocketmine\\\\plugin\\\\PluginGraylist\\:\\:fromArray\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: ../../../src/Server.php
+
+ -
+ message: "#^Parameter \\#1 \\$input of function yaml_parse expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../../src/Server.php
+
+ -
+ message: "#^Cannot cast array\\\\|string\\|false to string\\.$#"
+ count: 1
+ path: ../../../src/ServerConfigGroup.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 2
+ path: ../../../src/ServerConfigGroup.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 2
+ path: ../../../src/ServerConfigGroup.php
+
+ -
+ message: "#^Cannot access offset 'git' on mixed\\.$#"
+ count: 2
+ path: ../../../src/VersionInfo.php
+
+ -
+ message: "#^Method pocketmine\\\\VersionInfo\\:\\:GIT_HASH\\(\\) should return string but returns mixed\\.$#"
+ count: 1
+ path: ../../../src/VersionInfo.php
+
+ -
+ message: "#^Static property pocketmine\\\\VersionInfo\\:\\:\\$gitHash \\(string\\|null\\) does not accept mixed\\.$#"
+ count: 1
+ path: ../../../src/VersionInfo.php
+
+ -
+ message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/block/Block.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Cactus.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DaylightSensor.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DaylightSensor.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DaylightSensor.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#1 \\$xDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int\\<0, max\\> given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#2 \\$yDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#3 \\$zDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/DragonEgg.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Farmland.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Farmland.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Farmland.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Fire.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/FrostedIce.php
+
+ -
+ message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Grass.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Ice.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Ice.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Ice.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Leaves.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Leaves.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Leaves.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 11
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 11
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 11
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Liquid.php
+
+ -
+ message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/block/Mycelium.php
+
+ -
+ message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/block/Mycelium.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/SnowLayer.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/SnowLayer.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/SnowLayer.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/Sugarcane.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, int\\|null given\\.$#"
count: 4
- path: ../../../src/world/generator/PopulationTask.php
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairX \\(int\\|null\\) does not accept float\\|int\\.$#"
+ count: 2
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairZ \\(int\\|null\\) does not accept float\\|int\\.$#"
+ count: 2
+ path: ../../../src/block/tile/Chest.php
+
+ -
+ message: "#^Constant pocketmine\\\\block\\\\tile\\\\Skull\\:\\:TAG_MOUTH_MOVING is unused\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Skull.php
+
+ -
+ message: "#^Constant pocketmine\\\\block\\\\tile\\\\Skull\\:\\:TAG_MOUTH_TICK_COUNT is unused\\.$#"
+ count: 1
+ path: ../../../src/block/tile/Skull.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/block/tile/Spawnable.php
+
+ -
+ message: "#^Array \\(array\\, string\\>\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: ../../../src/block/tile/TileFactory.php
+
+ -
+ message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#"
+ count: 1
+ path: ../../../src/command/Command.php
+
+ -
+ message: "#^Cannot call method startTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#"
+ count: 1
+ path: ../../../src/command/SimpleCommandMap.php
+
+ -
+ message: "#^Cannot call method stopTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#"
+ count: 1
+ path: ../../../src/command/SimpleCommandMap.php
+
+ -
+ message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
+ count: 1
+ path: ../../../src/command/defaults/ParticleCommand.php
+
+ -
+ message: "#^Cannot call method getSeed\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
+ count: 1
+ path: ../../../src/command/defaults/SeedCommand.php
+
+ -
+ message: "#^Cannot call method setSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
+ count: 1
+ path: ../../../src/command/defaults/SetWorldSpawnCommand.php
+
+ -
+ message: "#^Cannot call method getTime\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
+ 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: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#"
+ count: 2
+ path: ../../../src/console/ConsoleReader.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/crash/CrashDump.php
+
+ -
+ message: "#^Parameter \\#1 \\$index of method pocketmine\\\\inventory\\\\BaseInventory\\:\\:setItem\\(\\) expects int, int\\|string given\\.$#"
+ count: 1
+ path: ../../../src/entity/ExperienceManager.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/Living.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/Living.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/Living.php
+
+ -
+ message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/FallingBlock.php
+
+ -
+ message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/FallingBlock.php
+
+ -
+ message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/FallingBlock.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/Painting.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/Painting.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/object/Painting.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/projectile/Projectile.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/entity/projectile/Projectile.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/projectile/Projectile.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/entity/projectile/Projectile.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: ../../../src/inventory/CreativeInventory.php
+
+ -
+ message: "#^Parameter \\#1 \\$data of static method pocketmine\\\\item\\\\Item\\:\\:jsonDeserialize\\(\\) expects array\\{id\\: int, damage\\?\\: int, count\\?\\: int, nbt\\?\\: string, nbt_hex\\?\\: string, nbt_b64\\?\\: string\\}, mixed given\\.$#"
+ count: 1
+ path: ../../../src/inventory/CreativeInventory.php
+
+ -
+ message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../../src/inventory/CreativeInventory.php
+
+ -
+ message: "#^Parameter \\#2 \\$recipe of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects pocketmine\\\\crafting\\\\CraftingRecipe, pocketmine\\\\crafting\\\\CraftingRecipe\\|null given\\.$#"
+ count: 1
+ path: ../../../src/inventory/transaction/CraftingTransaction.php
+
+ -
+ message: "#^Parameter \\#3 \\$repetitions of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/inventory/transaction/CraftingTransaction.php
+
+ -
+ message: "#^Parameter \\#1 \\$buffer of method pocketmine\\\\nbt\\\\BaseNbtSerializer\\:\\:read\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../../src/item/Item.php
+
+ -
+ message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/ChunkRequestTask.php
+
+ -
+ message: "#^Cannot call method doFirstSpawn\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method getLanguage\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method getLocation\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method getUsedChunkStatus\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method getUuid\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method sendData\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method setImmobile\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Cannot call method syncAll\\(\\) on pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$clientPub of class pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask constructor expects string, string\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:onEntityEffectAdded\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:onEntityEffectRemoved\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$for of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:syncAdventureSettings\\(\\) expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\DeathPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$target of method pocketmine\\\\command\\\\Command\\:\\:testPermissionSilent\\(\\) expects pocketmine\\\\command\\\\CommandSender, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#2 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#2 \\$playerInfo of method pocketmine\\\\Server\\:\\:createPlayer\\(\\) expects pocketmine\\\\player\\\\PlayerInfo, pocketmine\\\\player\\\\PlayerInfo\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#3 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#"
+ count: 2
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Parameter \\#4 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/NetworkSession.php
+
+ -
+ message: "#^Property pocketmine\\\\network\\\\mcpe\\\\auth\\\\ProcessLoginTask\\:\\:\\$chain \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/auth/ProcessLoginTask.php
+
+ -
+ message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/compression/CompressBatchTask.php
+
+ -
+ message: "#^Property pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask\\:\\:\\$serverPrivateKey \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/encryption/PrepareEncryptionTask.php
+
+ -
+ message: "#^Method pocketmine\\\\network\\\\mcpe\\\\raklib\\\\PthreadsChannelReader\\:\\:read\\(\\) should return string\\|null but returns mixed\\.$#"
+ count: 1
+ path: ../../../src/network/mcpe/raklib/PthreadsChannelReader.php
+
+ -
+ message: "#^Method pocketmine\\\\permission\\\\DefaultPermissions\\:\\:registerPermission\\(\\) should return pocketmine\\\\permission\\\\Permission but returns pocketmine\\\\permission\\\\Permission\\|null\\.$#"
+ count: 1
+ path: ../../../src/permission/DefaultPermissions.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of static method pocketmine\\\\permission\\\\PermissionParser\\:\\:defaultFromString\\(\\) expects bool\\|string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/permission/PermissionParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$description of class pocketmine\\\\permission\\\\Permission constructor expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: ../../../src/permission/PermissionParser.php
+
+ -
+ message: "#^Cannot call method getSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
+ count: 1
+ path: ../../../src/player/Player.php
+
+ -
+ message: "#^Method pocketmine\\\\player\\\\Player\\:\\:getSpawn\\(\\) should return pocketmine\\\\world\\\\Position but returns pocketmine\\\\world\\\\Position\\|null\\.$#"
+ count: 1
+ path: ../../../src/player/Player.php
+
+ -
+ message: "#^Method pocketmine\\\\plugin\\\\PluginBase\\:\\:getConfig\\(\\) should return pocketmine\\\\utils\\\\Config but returns pocketmine\\\\utils\\\\Config\\|null\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginBase.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Array \\(array\\\\) does not accept mixed\\.$#"
+ count: 2
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 4
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Parameter \\#1 \\$data of static method pocketmine\\\\permission\\\\PermissionParser\\:\\:loadPermissions\\(\\) expects array\\\\>, mixed given\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method pocketmine\\\\plugin\\\\PluginEnableOrder\\:\\:fromString\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$depend \\(array\\\\) does not accept array\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$loadBefore \\(array\\\\) does not accept array\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$main \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$name \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$softDepend \\(array\\\\) does not accept array\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$srcNamespacePrefix \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: ../../../src/plugin/PluginDescription.php
+
+ -
+ message: "#^Cannot call method addChild\\(\\) on pocketmine\\\\permission\\\\Permission\\|null\\.$#"
+ count: 4
+ path: ../../../src/plugin/PluginManager.php
+
+ -
+ message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackSize\\(\\) should return int but returns int\\<0, max\\>\\|false\\.$#"
+ count: 1
+ path: ../../../src/resourcepacks/ZippedResourcePack.php
+
+ -
+ message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getSha256\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: ../../../src/resourcepacks/ZippedResourcePack.php
+
+ -
+ message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int given\\.$#"
+ count: 1
+ path: ../../../src/resourcepacks/ZippedResourcePack.php
+
+ -
+ message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$fileResource \\(resource\\) does not accept resource\\|false\\.$#"
+ count: 1
+ path: ../../../src/resourcepacks/ZippedResourcePack.php
+
+ -
+ message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$sha256 \\(string\\|null\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: ../../../src/resourcepacks/ZippedResourcePack.php
+
+ -
+ message: "#^Cannot call method count\\(\\) on ArrayObject\\\\>\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^Cannot call method getNotifier\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^Cannot call method handleException\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^PHPDoc type pocketmine\\\\scheduler\\\\AsyncWorker\\|null of property pocketmine\\\\scheduler\\\\AsyncTask\\:\\:\\$worker is not the same as PHPDoc type Worker of overridden property Threaded\\:\\:\\$worker\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^Parameter \\#1 \\$str of function igbinary_unserialize expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^Property pocketmine\\\\scheduler\\\\AsyncTask\\:\\:\\$result \\(bool\\|float\\|int\\|string\\|null\\) does not accept mixed\\.$#"
+ count: 1
+ path: ../../../src/scheduler/AsyncTask.php
+
+ -
+ message: "#^Property pocketmine\\\\scheduler\\\\BulkCurlTask\\:\\:\\$operations \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/BulkCurlTask.php
+
+ -
+ message: "#^Cannot call method getAsyncWorkerId\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/DumpWorkerMemoryTask.php
+
+ -
+ message: "#^Cannot call method getLogger\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
+ count: 1
+ path: ../../../src/scheduler/DumpWorkerMemoryTask.php
+
+ -
+ message: "#^Cannot call method getNextRun\\(\\) on array\\\\|int\\|pocketmine\\\\scheduler\\\\TaskHandler\\.$#"
+ count: 1
+ path: ../../../src/scheduler/TaskScheduler.php
+
+ -
+ message: "#^Method pocketmine\\\\thread\\\\ThreadManager\\:\\:getAll\\(\\) should return array\\ but returns array\\.$#"
+ count: 1
+ path: ../../../src/thread/ThreadManager.php
+
+ -
+ message: "#^Cannot access offset string on mixed\\.$#"
+ count: 3
+ path: ../../../src/utils/Config.php
+
+ -
+ message: "#^Method pocketmine\\\\utils\\\\Config\\:\\:fixYAMLIndexes\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: ../../../src/utils/Config.php
+
+ -
+ message: "#^Parameter \\#1 \\$config of static method pocketmine\\\\utils\\\\Config\\:\\:writeProperties\\(\\) expects array\\, array\\ given\\.$#"
+ count: 1
+ path: ../../../src/utils/Config.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function substr expects int, mixed given\\.$#"
+ count: 1
+ path: ../../../src/utils/Internet.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, mixed given\\.$#"
+ count: 1
+ path: ../../../src/utils/Internet.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../../src/utils/Timezone.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 1
+ path: ../../../src/utils/Utils.php
+
+ -
+ message: "#^Method pocketmine\\\\utils\\\\Utils\\:\\:printable\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: ../../../src/utils/Utils.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: ../../../src/utils/Utils.php
+
+ -
+ message: "#^Parameter \\#1 \\$trace of static method pocketmine\\\\utils\\\\Utils\\:\\:printableTrace\\(\\) expects array\\\\>, array given\\.$#"
+ count: 1
+ path: ../../../src/utils/Utils.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_map expects array, mixed given\\.$#"
+ count: 1
+ path: ../../../src/utils/Utils.php
+
+ -
+ message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Only numeric types are allowed in /, float\\|null given on the left side\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/Explosion.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Cannot access offset 'data' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Cannot access offset 'priority' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$chunk of method pocketmine\\\\player\\\\Player\\:\\:onChunkChanged\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 3
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/World.php
+
+ -
+ message: "#^Method pocketmine\\\\world\\\\biome\\\\BiomeRegistry\\:\\:getBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/biome/BiomeRegistry.php
+
+ -
+ message: "#^Method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:getSubChunk\\(\\) should return pocketmine\\\\world\\\\format\\\\SubChunk but returns pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/format/Chunk.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/format/Chunk.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/format/Chunk.php
+
+ -
+ message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/format/Chunk.php
+
+ -
+ message: "#^Method pocketmine\\\\world\\\\format\\\\HeightArray\\:\\:get\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/format/HeightArray.php
+
+ -
+ message: "#^Only numeric types are allowed in %%, int\\<0, max\\>\\|false given on the left side\\.$#"
+ count: 1
+ path: ../../../src/world/format/io/region/RegionLoader.php
+
+ -
+ message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int given\\.$#"
+ count: 1
+ path: ../../../src/world/format/io/region/RegionLoader.php
+
+ -
+ message: "#^Parameter \\#2 \\$size of function ftruncate expects int\\<0, max\\>, int given\\.$#"
+ count: 1
+ path: ../../../src/world/format/io/region/RegionLoader.php
+
+ -
+ message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: ../../../src/world/format/io/region/RegionWorldProvider.php
+
+ -
+ message: "#^Cannot access offset 1 on mixed\\.$#"
+ count: 2
+ path: ../../../src/world/format/io/region/RegionWorldProvider.php
+
+ -
+ message: "#^Cannot access offset 2 on mixed\\.$#"
+ count: 2
+ path: ../../../src/world/format/io/region/RegionWorldProvider.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 4
+ path: ../../../src/world/format/io/region/RegionWorldProvider.php
+
+ -
+ message: "#^Method pocketmine\\\\world\\\\generator\\\\biome\\\\BiomeSelector\\:\\:pickBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/biome/BiomeSelector.php
+
+ -
+ message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/hell/Nether.php
+
+ -
+ message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/hell/Nether.php
+
+ -
+ message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 3
+ path: ../../../src/world/generator/hell/Nether.php
+
+ -
+ message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/normal/Normal.php
+
+ -
+ message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/normal/Normal.php
+
+ -
+ message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 3
+ path: ../../../src/world/generator/normal/Normal.php
+
+ -
+ message: "#^Parameter \\#1 \\$start of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/generator/object/TallGrass.php
+
+ -
+ message: "#^Parameter \\#2 \\$end of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/generator/object/TallGrass.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 2
+ path: ../../../src/world/generator/object/TallGrass.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: ../../../src/world/generator/object/TallGrass.php
+
+ -
+ message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/populator/GroundCover.php
+
+ -
+ message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 2
+ path: ../../../src/world/generator/populator/GroundCover.php
+
+ -
+ message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/generator/populator/GroundCover.php
+
+ -
+ message: "#^Cannot call method getBlockLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/BlockLightUpdate.php
+
+ -
+ message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/BlockLightUpdate.php
+
+ -
+ message: "#^Cannot call method getSubChunks\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/BlockLightUpdate.php
+
+ -
+ message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
+ count: 1
+ path: ../../../src/world/light/BlockLightUpdate.php
+
+ -
+ message: "#^Parameter \\#4 \\$newLevel of method pocketmine\\\\world\\\\light\\\\LightUpdate\\:\\:setAndUpdateLight\\(\\) expects int, int\\|null given\\.$#"
+ count: 1
+ path: ../../../src/world/light/BlockLightUpdate.php
+
+ -
+ message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultBlockLightArrays \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/LightPopulationTask.php
+
+ -
+ message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultHeightMap \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/LightPopulationTask.php
+
+ -
+ message: "#^Property pocketmine\\\\world\\\\light\\\\LightPopulationTask\\:\\:\\$resultSkyLightArrays \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/LightPopulationTask.php
+
+ -
+ message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/LightUpdate.php
+
+ -
+ message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
+ count: 1
+ path: ../../../src/world/light/LightUpdate.php
+
+ -
+ message: "#^Cannot call method getBlockSkyLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method getHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 6
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method getHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method getSubChunk\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 2
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method setHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 2
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Cannot call method setHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Only booleans are allowed in an if condition, bool\\|null given\\.$#"
+ count: 3
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Only numeric types are allowed in \\+, int\\|false given on the left side\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMap\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMapColumn\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
+ count: 1
+ path: ../../../src/world/light/SkyLightUpdate.php
+
+ -
+ message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../phpunit/block/BlockTest.php
+
+ -
+ message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../phpunit/block/regenerate_consistency_check.php
+
+ -
+ message: "#^Property pocketmine\\\\event\\\\HandlerListManagerTest\\:\\:\\$isValidFunc \\(Closure\\(ReflectionClass\\\\)\\: bool\\) does not accept Closure\\|null\\.$#"
+ count: 1
+ path: ../../phpunit/event/HandlerListManagerTest.php
+
+ -
+ message: "#^Property pocketmine\\\\event\\\\HandlerListManagerTest\\:\\:\\$resolveParentFunc \\(Closure\\(ReflectionClass\\\\)\\: ReflectionClass\\\\|null\\) does not accept Closure\\|null\\.$#"
+ count: 1
+ path: ../../phpunit/event/HandlerListManagerTest.php
+
+ -
+ message: "#^Parameter \\#1 \\$logFile of class pocketmine\\\\utils\\\\MainLogger constructor expects string, string\\|false given\\.$#"
+ count: 1
+ path: ../../phpunit/scheduler/AsyncPoolTest.php
diff --git a/tests/phpstan/configs/check-explicit-mixed-baseline.neon b/tests/phpstan/configs/check-explicit-mixed-baseline.neon
deleted file mode 100644
index 56654cf26..000000000
--- a/tests/phpstan/configs/check-explicit-mixed-baseline.neon
+++ /dev/null
@@ -1,227 +0,0 @@
-parameters:
- ignoreErrors:
- -
- message: "#^Cannot access offset \\(float\\|int\\) on mixed\\.$#"
- count: 1
- path: ../../../src/CrashDump.php
-
- -
- message: "#^Cannot access offset string on mixed\\.$#"
- count: 1
- path: ../../../src/CrashDump.php
-
- -
- message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/CrashDump.php
-
- -
- message: "#^Cannot access offset 'type' on mixed\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Cannot cast mixed to string\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Parameter \\#1 \\$array of static method pocketmine\\\\plugin\\\\PluginGraylist\\:\\:fromArray\\(\\) expects array, mixed given\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Cannot cast mixed to int\\.$#"
- count: 2
- path: ../../../src/ServerConfigGroup.php
-
- -
- message: "#^Cannot cast mixed to string\\.$#"
- count: 2
- path: ../../../src/ServerConfigGroup.php
-
- -
- message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/ServerConfigGroup.php
-
- -
- message: "#^Cannot access offset 'git' on mixed\\.$#"
- count: 2
- path: ../../../src/VersionInfo.php
-
- -
- message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
- count: 1
- path: ../../../src/inventory/CreativeInventory.php
-
- -
- message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/ChunkRequestTask.php
-
- -
- message: "#^Parameter \\#1 \\$result of method pocketmine\\\\network\\\\mcpe\\\\compression\\\\CompressBatchPromise\\:\\:resolve\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/compression/CompressBatchTask.php
-
- -
- message: "#^Method pocketmine\\\\network\\\\mcpe\\\\raklib\\\\PthreadsChannelReader\\:\\:read\\(\\) should return string\\|null but returns mixed\\.$#"
- count: 1
- path: ../../../src/network/mcpe/raklib/PthreadsChannelReader.php
-
- -
- message: "#^Parameter \\#1 \\$value of static method pocketmine\\\\permission\\\\PermissionParser\\:\\:defaultFromString\\(\\) expects bool\\|string, mixed given\\.$#"
- count: 1
- path: ../../../src/permission/PermissionParser.php
-
- -
- message: "#^Parameter \\#2 \\$description of class pocketmine\\\\permission\\\\Permission constructor expects string\\|null, mixed given\\.$#"
- count: 1
- path: ../../../src/permission/PermissionParser.php
-
- -
- message: "#^Parameter \\#1 \\$description of method pocketmine\\\\command\\\\Command\\:\\:setDescription\\(\\) expects pocketmine\\\\lang\\\\Translatable\\|string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginBase.php
-
- -
- message: "#^Parameter \\#1 \\$permissionMessage of method pocketmine\\\\command\\\\Command\\:\\:setPermissionMessage\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginBase.php
-
- -
- message: "#^Parameter \\#1 \\$usage of method pocketmine\\\\command\\\\Command\\:\\:setUsage\\(\\) expects pocketmine\\\\lang\\\\Translatable\\|string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginBase.php
-
- -
- message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Array \\(array\\\\) does not accept mixed\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Cannot cast mixed to string\\.$#"
- count: 4
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#1 \\$data of static method pocketmine\\\\permission\\\\PermissionParser\\:\\:loadPermissions\\(\\) expects array\\\\>, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#1 \\$name of static method pocketmine\\\\plugin\\\\PluginEnableOrder\\:\\:fromString\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#1 \\$plugin of method pocketmine\\\\plugin\\\\PluginDescription\\:\\:loadMap\\(\\) expects array, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$main \\(string\\) does not accept mixed\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$name \\(string\\) does not accept mixed\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$srcNamespacePrefix \\(string\\) does not accept mixed\\.$#"
- count: 1
- path: ../../../src/plugin/PluginDescription.php
-
- -
- message: "#^Parameter \\#1 \\$plugins of class pocketmine\\\\plugin\\\\PluginGraylist constructor expects array\\, mixed given\\.$#"
- count: 1
- path: ../../../src/plugin/PluginGraylist.php
-
- -
- message: "#^Parameter \\#2 \\$code of class pocketmine\\\\resourcepacks\\\\ResourcePackException constructor expects int, mixed given\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Parameter \\#1 \\$str of function igbinary_unserialize expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/scheduler/AsyncTask.php
-
- -
- message: "#^Property pocketmine\\\\scheduler\\\\AsyncTask\\:\\:\\$result \\(bool\\|float\\|int\\|string\\|null\\) does not accept mixed\\.$#"
- count: 1
- path: ../../../src/scheduler/AsyncTask.php
-
- -
- message: "#^Method pocketmine\\\\thread\\\\ThreadManager\\:\\:getAll\\(\\) should return array\\ but returns array\\.$#"
- count: 1
- path: ../../../src/thread/ThreadManager.php
-
- -
- message: "#^Method pocketmine\\\\timings\\\\TimingsHandler\\:\\:time\\(\\) should return TClosureReturn but returns TClosureReturn\\.$#"
- count: 1
- path: ../../../src/timings/TimingsHandler.php
-
- -
- message: "#^Parameter \\#2 \\$offset of function substr expects int, mixed given\\.$#"
- count: 1
- path: ../../../src/utils/Internet.php
-
- -
- message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, mixed given\\.$#"
- count: 1
- path: ../../../src/utils/Internet.php
-
- -
- message: "#^Part \\$errno \\(mixed\\) of encapsed string cannot be cast to string\\.$#"
- count: 1
- path: ../../../src/utils/MainLogger.php
-
- -
- message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#"
- count: 1
- path: ../../../src/utils/Utils.php
-
- -
- message: "#^Parameter \\#2 \\$array of function array_map expects array, mixed given\\.$#"
- count: 1
- path: ../../../src/utils/Utils.php
-
- -
- message: "#^Parameter \\#1 \\$keys of function array_fill_keys expects array, mixed given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Cannot access offset 1 on mixed\\.$#"
- count: 2
- path: ../../../src/world/format/io/region/RegionWorldProvider.php
-
- -
- message: "#^Cannot access offset 2 on mixed\\.$#"
- count: 2
- path: ../../../src/world/format/io/region/RegionWorldProvider.php
-
diff --git a/tests/phpstan/configs/l7-baseline.neon b/tests/phpstan/configs/l7-baseline.neon
deleted file mode 100644
index 4f82f4aff..000000000
--- a/tests/phpstan/configs/l7-baseline.neon
+++ /dev/null
@@ -1,892 +0,0 @@
-parameters:
- ignoreErrors:
- -
- message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|false given\\.$#"
- count: 1
- path: ../../../build/make-release.php
-
- -
- message: "#^Parameter \\#1 \\$pharPath of function pocketmine\\\\build\\\\server_phar\\\\buildPhar expects string, array\\\\|string\\|false given\\.$#"
- count: 1
- path: ../../../build/server-phar.php
-
- -
- message: "#^Parameter \\#1 \\$strings of function pocketmine\\\\build\\\\server_phar\\\\preg_quote_array expects array\\, array\\ given\\.$#"
- count: 1
- path: ../../../build/server-phar.php
-
- -
- message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#"
- count: 1
- path: ../../../src/MemoryManager.php
-
- -
- message: "#^Parameter \\#1 \\$stream of function fwrite expects resource, resource\\|false given\\.$#"
- count: 1
- path: ../../../src/MemoryManager.php
-
- -
- message: "#^Binary operation \"\\.\" between array\\\\|string\\|false and '/'\\|'\\\\\\\\' results in an error\\.$#"
- count: 2
- path: ../../../src/PocketMine.php
-
- -
- message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/PocketMine.php
-
- -
- message: "#^Parameter \\#1 \\$path of function realpath expects string, array\\\\|string\\|false given\\.$#"
- count: 1
- path: ../../../src/PocketMine.php
-
- -
- message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#"
- count: 2
- path: ../../../src/PocketMine.php
-
- -
- message: "#^Parameter \\#1 \\$version1 of function version_compare expects string, string\\|false given\\.$#"
- count: 2
- path: ../../../src/PocketMine.php
-
- -
- message: "#^Only numeric types are allowed in \\+, int\\|false given on the left side\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Parameter \\#1 \\$input of function yaml_parse expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/Server.php
-
- -
- message: "#^Cannot cast array\\\\|string\\|false to string\\.$#"
- count: 1
- path: ../../../src/ServerConfigGroup.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Cactus.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DaylightSensor.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DaylightSensor.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DaylightSensor.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#1 \\$xDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int\\<0, max\\> given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#2 \\$yDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#3 \\$zDiff of class pocketmine\\\\world\\\\particle\\\\DragonEggTeleportParticle constructor expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/DragonEgg.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Farmland.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Farmland.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Farmland.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/FrostedIce.php
-
- -
- message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Grass.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Ice.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Ice.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Ice.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Leaves.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Leaves.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Leaves.php
-
- -
- message: "#^Parameter \\#1 \\$blockX of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 23
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#2 \\$blockY of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 23
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#3 \\$blockZ of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 23
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Liquid.php
-
- -
- message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Mycelium.php
-
- -
- message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/Mycelium.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/SnowLayer.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/SnowLayer.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/SnowLayer.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/Sugarcane.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairX \\(int\\|null\\) does not accept float\\|int\\.$#"
- count: 2
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairZ \\(int\\|null\\) does not accept float\\|int\\.$#"
- count: 2
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/block/tile/Spawnable.php
-
- -
- message: "#^Array \\(array\\, string\\>\\) does not accept string\\|false\\.$#"
- count: 1
- path: ../../../src/block/tile/TileFactory.php
-
- -
- message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#"
- count: 1
- path: ../../../src/command/defaults/BanIpCommand.php
-
- -
- message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#"
- count: 1
- path: ../../../src/command/defaults/PardonIpCommand.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: "#^Property pocketmine\\\\console\\\\ConsoleReader\\:\\:\\$stdin \\(resource\\) does not accept resource\\|false\\.$#"
- count: 1
- path: ../../../src/console/ConsoleReader.php
-
- -
- message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/crafting/CraftingManagerFromDataHelper.php
-
- -
- message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/data/bedrock/LegacyToStringBidirectionalIdMap.php
-
- -
- message: "#^Parameter \\#1 \\$index of method pocketmine\\\\inventory\\\\BaseInventory\\:\\:setItem\\(\\) expects int, int\\|string given\\.$#"
- count: 1
- path: ../../../src/entity/ExperienceManager.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/Living.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/Living.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/Living.php
-
- -
- message: "#^Property pocketmine\\\\entity\\\\Skin\\:\\:\\$geometryData \\(string\\) does not accept string\\|false\\.$#"
- count: 1
- path: ../../../src/entity/Skin.php
-
- -
- message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/FallingBlock.php
-
- -
- message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/FallingBlock.php
-
- -
- message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/FallingBlock.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/Painting.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/Painting.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/object/Painting.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/projectile/Projectile.php
-
- -
- message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/entity/projectile/Projectile.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/projectile/Projectile.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/entity/projectile/Projectile.php
-
- -
- message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/inventory/CreativeInventory.php
-
- -
- message: "#^Parameter \\#1 \\$buffer of method pocketmine\\\\nbt\\\\BaseNbtSerializer\\:\\:read\\(\\) expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/item/Item.php
-
- -
- message: "#^Parameter \\#2 \\$array of function array_map expects array, array\\|false given\\.$#"
- count: 1
- path: ../../../src/lang/Language.php
-
- -
- message: "#^Parameter \\#1 \\$buffer of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\serializer\\\\PacketSerializer\\:\\:decoder\\(\\) expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/convert/RuntimeBlockMapping.php
-
- -
- message: "#^Method pocketmine\\\\network\\\\mcpe\\\\encryption\\\\EncryptionUtils\\:\\:generateKey\\(\\) should return string but returns string\\|false\\.$#"
- count: 1
- path: ../../../src/network/mcpe/encryption/EncryptionUtils.php
-
- -
- message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackChunk\\(\\) should return string but returns string\\|false\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackSize\\(\\) should return int but returns int\\|false\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getSha256\\(\\) should return string but returns string\\|false\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#"
- count: 2
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$fileResource \\(resource\\) does not accept resource\\|false\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$sha256 \\(string\\|null\\) does not accept string\\|false\\.$#"
- count: 1
- path: ../../../src/resourcepacks/ZippedResourcePack.php
-
- -
- message: "#^Cannot call method getNextRun\\(\\) on array\\\\|int\\|pocketmine\\\\scheduler\\\\TaskHandler\\.$#"
- count: 1
- path: ../../../src/scheduler/TaskScheduler.php
-
- -
- message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/utils/Filesystem.php
-
- -
- message: "#^Parameter \\#1 \\$abbr of function timezone_name_from_abbr expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/utils/Timezone.php
-
- -
- message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/utils/Timezone.php
-
- -
- message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/utils/Timezone.php
-
- -
- message: "#^Parameter \\#1 \\$timezoneId of function date_default_timezone_set expects string, string\\|false given\\.$#"
- count: 1
- path: ../../../src/utils/Timezone.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:updateAllLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:updateAllLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:updateAllLight\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Cannot access offset 'data' on array\\('priority' \\=\\> int, 'data' \\=\\> pocketmine\\\\math\\\\Vector3\\)\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Cannot access offset 'priority' on array\\('priority' \\=\\> int, 'data' \\=\\> pocketmine\\\\math\\\\Vector3\\)\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$x of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$y of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:create\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 3
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#4 \\$z of method pocketmine\\\\block\\\\Block\\:\\:position\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/World.php
-
- -
- message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/format/Chunk.php
-
- -
- message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/format/Chunk.php
-
- -
- message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/format/Chunk.php
-
- -
- message: "#^Only numeric types are allowed in %%, int\\|false given on the left side\\.$#"
- count: 1
- path: ../../../src/world/format/io/region/RegionLoader.php
-
- -
- message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#"
- count: 1
- path: ../../../src/world/format/io/region/RegionWorldProvider.php
-
- -
- message: "#^Parameter \\#1 \\$start of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/generator/object/TallGrass.php
-
- -
- message: "#^Parameter \\#2 \\$end of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/generator/object/TallGrass.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 2
- path: ../../../src/world/generator/object/TallGrass.php
-
- -
- message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\ChunkManager\\:\\:setBlockAt\\(\\) expects int, float\\|int given\\.$#"
- count: 1
- path: ../../../src/world/generator/object/TallGrass.php
-
- -
- message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
- count: 1
- path: ../../phpunit/block/BlockTest.php
-
- -
- message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
- count: 1
- path: ../../phpunit/block/regenerate_consistency_check.php
-
- -
- message: "#^Parameter \\#1 \\$logFile of class pocketmine\\\\utils\\\\MainLogger constructor expects string, string\\|false given\\.$#"
- count: 1
- path: ../../phpunit/scheduler/AsyncPoolTest.php
-
diff --git a/tests/phpstan/configs/l8-baseline.neon b/tests/phpstan/configs/l8-baseline.neon
deleted file mode 100644
index cb21fad4e..000000000
--- a/tests/phpstan/configs/l8-baseline.neon
+++ /dev/null
@@ -1,447 +0,0 @@
-parameters:
- ignoreErrors:
- -
- message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
- count: 2
- path: ../../../build/make-release.php
-
- -
- message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/block/Block.php
-
- -
- message: "#^Parameter \\#1 \\$x of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, int\\|null given\\.$#"
- count: 4
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#3 \\$z of class pocketmine\\\\math\\\\Vector3 constructor expects float\\|int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/block/tile/Chest.php
-
- -
- message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#"
- count: 1
- path: ../../../src/command/Command.php
-
- -
- message: "#^Cannot call method startTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#"
- count: 1
- path: ../../../src/command/SimpleCommandMap.php
-
- -
- message: "#^Cannot call method stopTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#"
- count: 1
- path: ../../../src/command/SimpleCommandMap.php
-
- -
- message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
- count: 1
- path: ../../../src/command/defaults/ParticleCommand.php
-
- -
- message: "#^Cannot call method getSeed\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
- count: 1
- path: ../../../src/command/defaults/SeedCommand.php
-
- -
- message: "#^Cannot call method setSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
- count: 1
- path: ../../../src/command/defaults/SetWorldSpawnCommand.php
-
- -
- message: "#^Cannot call method getTime\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
- count: 1
- path: ../../../src/command/defaults/TimeCommand.php
-
- -
- message: "#^Parameter \\#2 \\$recipe of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects pocketmine\\\\crafting\\\\CraftingRecipe, pocketmine\\\\crafting\\\\CraftingRecipe\\|null given\\.$#"
- count: 1
- path: ../../../src/inventory/transaction/CraftingTransaction.php
-
- -
- message: "#^Parameter \\#3 \\$repetitions of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/inventory/transaction/CraftingTransaction.php
-
- -
- message: "#^Cannot call method doFirstSpawn\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method getLanguage\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method getLocation\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 2
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method getUsedChunkStatus\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method getUsername\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#"
- count: 2
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method getUuid\\(\\) on pocketmine\\\\player\\\\PlayerInfo\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method sendData\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method setImmobile\\(\\) on pocketmine\\\\player\\\\Player\\|null\\.$#"
- count: 2
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method syncAll\\(\\) on pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Cannot call method syncCreative\\(\\) on pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$clientPub of class pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask constructor expects string, string\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:onEntityEffectAdded\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$entity of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:onEntityEffectRemoved\\(\\) expects pocketmine\\\\entity\\\\Living, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$for of method pocketmine\\\\network\\\\mcpe\\\\NetworkSession\\:\\:syncAdventureSettings\\(\\) expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 3
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\DeathPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 2
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#1 \\$target of method pocketmine\\\\command\\\\Command\\:\\:testPermissionSilent\\(\\) expects pocketmine\\\\command\\\\CommandSender, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#2 \\$player of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\player\\\\Player, pocketmine\\\\player\\\\Player\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#2 \\$playerInfo of method pocketmine\\\\Server\\:\\:createPlayer\\(\\) expects pocketmine\\\\player\\\\PlayerInfo, pocketmine\\\\player\\\\PlayerInfo\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#3 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\InGamePacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#"
- count: 2
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Parameter \\#4 \\$inventoryManager of class pocketmine\\\\network\\\\mcpe\\\\handler\\\\PreSpawnPacketHandler constructor expects pocketmine\\\\network\\\\mcpe\\\\InventoryManager, pocketmine\\\\network\\\\mcpe\\\\InventoryManager\\|null given\\.$#"
- count: 1
- path: ../../../src/network/mcpe/NetworkSession.php
-
- -
- message: "#^Method pocketmine\\\\permission\\\\DefaultPermissions\\:\\:registerPermission\\(\\) should return pocketmine\\\\permission\\\\Permission but returns pocketmine\\\\permission\\\\Permission\\|null\\.$#"
- count: 1
- path: ../../../src/permission/DefaultPermissions.php
-
- -
- message: "#^Cannot call method getSpawnLocation\\(\\) on pocketmine\\\\world\\\\World\\|null\\.$#"
- count: 1
- path: ../../../src/player/Player.php
-
- -
- message: "#^Method pocketmine\\\\player\\\\Player\\:\\:getSpawn\\(\\) should return pocketmine\\\\world\\\\Position but returns pocketmine\\\\world\\\\Position\\|null\\.$#"
- count: 1
- path: ../../../src/player/Player.php
-
- -
- message: "#^Method pocketmine\\\\plugin\\\\PluginBase\\:\\:getConfig\\(\\) should return pocketmine\\\\utils\\\\Config but returns pocketmine\\\\utils\\\\Config\\|null\\.$#"
- count: 1
- path: ../../../src/plugin/PluginBase.php
-
- -
- message: "#^Cannot call method addChild\\(\\) on pocketmine\\\\permission\\\\Permission\\|null\\.$#"
- count: 4
- path: ../../../src/plugin/PluginManager.php
-
- -
- message: "#^Cannot call method count\\(\\) on ArrayObject\\\\>\\|null\\.$#"
- count: 1
- path: ../../../src/scheduler/AsyncTask.php
-
- -
- message: "#^Cannot call method getNotifier\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
- count: 1
- path: ../../../src/scheduler/AsyncTask.php
-
- -
- message: "#^Cannot call method handleException\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
- count: 1
- path: ../../../src/scheduler/AsyncTask.php
-
- -
- message: "#^Cannot call method getAsyncWorkerId\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
- count: 1
- path: ../../../src/scheduler/DumpWorkerMemoryTask.php
-
- -
- message: "#^Cannot call method getLogger\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#"
- count: 1
- path: ../../../src/scheduler/DumpWorkerMemoryTask.php
-
- -
- message: "#^Method pocketmine\\\\utils\\\\Config\\:\\:fixYAMLIndexes\\(\\) should return string but returns string\\|null\\.$#"
- count: 1
- path: ../../../src/utils/Config.php
-
- -
- message: "#^Method pocketmine\\\\utils\\\\Utils\\:\\:printable\\(\\) should return string but returns string\\|null\\.$#"
- count: 1
- path: ../../../src/utils/Utils.php
-
- -
- message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Only numeric types are allowed in /, float\\|null given on the left side\\.$#"
- count: 1
- path: ../../../src/world/Explosion.php
-
- -
- message: "#^Parameter \\#3 \\$chunk of method pocketmine\\\\player\\\\Player\\:\\:onChunkChanged\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
- count: 1
- path: ../../../src/world/World.php
-
- -
- message: "#^Method pocketmine\\\\world\\\\biome\\\\BiomeRegistry\\:\\:getBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#"
- count: 1
- path: ../../../src/world/biome/BiomeRegistry.php
-
- -
- message: "#^Method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:getSubChunk\\(\\) should return pocketmine\\\\world\\\\format\\\\SubChunk but returns pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/format/Chunk.php
-
- -
- message: "#^Method pocketmine\\\\world\\\\format\\\\HeightArray\\:\\:get\\(\\) should return int but returns int\\|null\\.$#"
- count: 1
- path: ../../../src/world/format/HeightArray.php
-
- -
- message: "#^Cannot call method setPopulated\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/PopulationTask.php
-
- -
- message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\format\\\\io\\\\FastChunkSerializer\\:\\:serializeWithoutLight\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
- count: 1
- path: ../../../src/world/generator/PopulationTask.php
-
- -
- message: "#^Method pocketmine\\\\world\\\\generator\\\\biome\\\\BiomeSelector\\:\\:pickBiome\\(\\) should return pocketmine\\\\world\\\\biome\\\\Biome but returns pocketmine\\\\world\\\\biome\\\\Biome\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/biome/BiomeSelector.php
-
- -
- message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/hell/Nether.php
-
- -
- message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/hell/Nether.php
-
- -
- message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 3
- path: ../../../src/world/generator/hell/Nether.php
-
- -
- message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/normal/Normal.php
-
- -
- message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/normal/Normal.php
-
- -
- message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 3
- path: ../../../src/world/generator/normal/Normal.php
-
- -
- message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/populator/GroundCover.php
-
- -
- message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 2
- path: ../../../src/world/generator/populator/GroundCover.php
-
- -
- message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/generator/populator/GroundCover.php
-
- -
- message: "#^Cannot call method getBlockLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/BlockLightUpdate.php
-
- -
- message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/BlockLightUpdate.php
-
- -
- message: "#^Cannot call method getSubChunks\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/BlockLightUpdate.php
-
- -
- message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
- count: 1
- path: ../../../src/world/light/BlockLightUpdate.php
-
- -
- message: "#^Parameter \\#4 \\$newLevel of method pocketmine\\\\world\\\\light\\\\LightUpdate\\:\\:setAndUpdateLight\\(\\) expects int, int\\|null given\\.$#"
- count: 1
- path: ../../../src/world/light/BlockLightUpdate.php
-
- -
- message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/LightUpdate.php
-
- -
- message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
- count: 1
- path: ../../../src/world/light/LightUpdate.php
-
- -
- message: "#^Cannot call method getBlockSkyLightArray\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method getHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 6
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method getHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method getSubChunk\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 2
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method getSubChunks\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method setHeightMap\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 2
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Cannot call method setHeightMapArray\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Only booleans are allowed in an if condition, bool\\|null given\\.$#"
- count: 3
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Only numeric types are allowed in \\+, int\\|false given on the left side\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMap\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Parameter \\#1 \\$chunk of static method pocketmine\\\\world\\\\light\\\\SkyLightUpdate\\:\\:recalculateHeightMapColumn\\(\\) expects pocketmine\\\\world\\\\format\\\\Chunk, pocketmine\\\\world\\\\format\\\\Chunk\\|null given\\.$#"
- count: 1
- path: ../../../src/world/light/SkyLightUpdate.php
-
- -
- message: "#^Property pocketmine\\\\event\\\\HandlerListManagerTest\\:\\:\\$isValidFunc \\(Closure\\(ReflectionClass\\\\)\\: bool\\) does not accept Closure\\|null\\.$#"
- count: 1
- path: ../../phpunit/event/HandlerListManagerTest.php
-
- -
- message: "#^Property pocketmine\\\\event\\\\HandlerListManagerTest\\:\\:\\$resolveParentFunc \\(Closure\\(ReflectionClass\\\\)\\: ReflectionClass\\\\|null\\) does not accept Closure\\|null\\.$#"
- count: 1
- path: ../../phpunit/event/HandlerListManagerTest.php
-
diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon
index a46b05a10..99fcbe5e6 100644
--- a/tests/phpstan/configs/phpstan-bugs.neon
+++ b/tests/phpstan/configs/phpstan-bugs.neon
@@ -5,11 +5,6 @@ parameters:
count: 1
path: ../../../src/block/BaseBanner.php
- -
- message: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#"
- count: 2
- path: ../../../src/console/ConsoleReader.php
-
-
message: "#^Method pocketmine\\\\crafting\\\\CraftingManager\\:\\:getDestructorCallbacks\\(\\) should return pocketmine\\\\utils\\\\ObjectSet\\ but returns pocketmine\\\\utils\\\\ObjectSet\\\\|pocketmine\\\\utils\\\\ObjectSet\\