diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 1c8f3eb579..71363897c9 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -43,30 +43,7 @@ jobs: echo ::set-output name=BUILD_NUMBER::$BUILD_NUMBER - name: Minify BedrockData JSON files - run: php src/pocketmine/resources/vanilla/.minify_json.php - - - name: Run preprocessor - run: | - PM_PREPROCESSOR_PATH="$GITHUB_WORKSPACE/build/preprocessor" - php "$PM_PREPROCESSOR_PATH/PreProcessor.php" --path=src --multisize || (echo "Preprocessor exited with code $?" && exit 1) - #dump the diff of preprocessor replacements to a patch in case it has bugs - git diff > preprocessor_diff.patch - VENDOR_PM="$GITHUB_WORKSPACE/vendor" - VENDOR_PM_BACKUP="$GITHUB_WORKSPACE/vendor-before-preprocess" - cp -r "$VENDOR_PM" "$VENDOR_PM_BACKUP" - for f in $(ls $VENDOR_PM/pocketmine); do - echo "Processing directory \"$f\"..." - php "$PM_PREPROCESSOR_PATH/PreProcessor.php" --path="$VENDOR_PM/pocketmine/$f/src" --multisize || (echo "Preprocessor exited with code $?" && exit 1) - echo "Checking for changes in \"$f\"..." - DIFF=$(git diff --no-index "$VENDOR_PM_BACKUP/pocketmine/$f" "$VENDOR_PM/pocketmine/$f" || true) - if [ "$DIFF" != "" ]; then - PATCH="$GITHUB_WORKSPACE/preprocessor_diff_$f.patch" - echo "$DIFF" > "$PATCH" - echo "Generated patch file \"$PATCH\"" - else - echo "No diff generated for \"$f\" (preprocessor made no changes)" - fi - done + 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 }} --build ${{ steps.build-number.outputs.BUILD_NUMBER }} @@ -74,10 +51,10 @@ jobs: - name: Get PocketMine-MP release version id: get-pm-version run: | - echo ::set-output name=PM_VERSION::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\BASE_VERSION;') + echo ::set-output name=PM_VERSION::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BASE_VERSION;') echo ::set-output name=MCPE_VERSION::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\network\mcpe\protocol\ProtocolInfo::MINECRAFT_VERSION_NETWORK;') - echo ::set-output name=PM_VERSION_SHORT::$(php -r 'require "vendor/autoload.php"; $v = explode(".", \pocketmine\BASE_VERSION); array_pop($v); echo implode(".", $v);') - echo ::set-output name=PM_VERSION_MD::$(php -r 'require "vendor/autoload.php"; echo str_replace(".", "", \pocketmine\BASE_VERSION);') + echo ::set-output name=PM_VERSION_SHORT::$(php -r 'require "vendor/autoload.php"; $v = explode(".", \pocketmine\VersionInfo::BASE_VERSION); array_pop($v); echo implode(".", $v);') + 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 }} ${{ steps.build-number.outputs.BUILD_NUMBER }} > build_info.json @@ -104,11 +81,3 @@ jobs: **For Minecraft: Bedrock Edition ${{ steps.get-pm-version.outputs.MCPE_VERSION }}** Please see the [changelogs](/changelogs/${{ steps.get-pm-version.outputs.PM_VERSION_SHORT }}.md#${{ steps.get-pm-version.outputs.PM_VERSION_MD }}) for details. - - - name: Upload preprocessor diffs - uses: actions/upload-artifact@v2 - if: always() - with: - name: preprocessor_diffs - path: ${{ github.workspace }}/preprocessor_diff*.patch - diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 133254c85a..fb2b8214e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -139,8 +139,8 @@ jobs: - name: Run integration tests run: ./tests/travis.sh -t4 - preprocessor: - name: Preprocessor tests + codegen: + name: Generated Code consistency checks needs: build-php runs-on: ${{ matrix.image }} strategy: @@ -151,8 +151,6 @@ jobs: steps: - uses: actions/checkout@v2 - with: - submodules: true - name: Setup PHP uses: pmmp/setup-php-action@e232f72a4330a07aae8418e8aa56b64efcdda636 @@ -176,37 +174,16 @@ jobs: - name: Install Composer dependencies run: php composer.phar install --no-dev --prefer-dist --no-interaction - - name: Run preprocessor + - name: Regenerate registry annotations + run: php build/generate-registry-annotations.php src + + - name: Regenerate KnownTranslation APIs + run: php build/generate-known-translation-apis.php + + - name: Verify code is unchanged run: | - PM_PREPROCESSOR_PATH="$GITHUB_WORKSPACE/build/preprocessor" - php "$PM_PREPROCESSOR_PATH/PreProcessor.php" --path=src --multisize || (echo "Preprocessor exited with code $?" && exit 1) - - #dump the diff of preprocessor replacements to a patch in case it has bugs - git diff > preprocessor_diff.patch - - VENDOR_PM="$GITHUB_WORKSPACE/vendor" - VENDOR_PM_BACKUP="$GITHUB_WORKSPACE/vendor-before-preprocess" - cp -r "$VENDOR_PM" "$VENDOR_PM_BACKUP" - for f in $(ls $VENDOR_PM/pocketmine); do - echo "Processing directory \"$f\"..." - php "$PM_PREPROCESSOR_PATH/PreProcessor.php" --path="$VENDOR_PM/pocketmine/$f/src" --multisize || (echo "Preprocessor exited with code $?" && exit 1) - echo "Checking for changes in \"$f\"..." - DIFF=$(git diff --no-index "$VENDOR_PM_BACKUP/pocketmine/$f" "$VENDOR_PM/pocketmine/$f" || true) - if [ "$DIFF" != "" ]; then - PATCH="$GITHUB_WORKSPACE/preprocessor_diff_$f.patch" - echo "$DIFF" > "$PATCH" - echo "Generated patch file \"$PATCH\"" - else - echo "No diff generated for \"$f\" (preprocessor made no changes)" - fi - done - - - name: Upload preprocessor diffs - uses: actions/upload-artifact@v2 - if: always() - with: - name: preprocessor_diffs_${{ matrix.php }}_${{ matrix.image }} - path: ${{ github.workspace }}/preprocessor_diff*.patch + git diff + git diff --quiet codestyle: name: Code Style checks diff --git a/.gitignore b/.gitignore index 001c585afc..13c3473764 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ players/* worlds/* +world_conversion_backups/* +backups/* plugin_data/* plugins/* bin*/* @@ -10,6 +12,7 @@ crashdumps/* *.phar server.properties /pocketmine.yml +/plugin_list.yml memory_dumps/* resource_packs/ server.lock diff --git a/.gitmodules b/.gitmodules index 28cc4692d3..0b2349472d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,6 @@ -[submodule "src/pocketmine/lang/locale"] - path = src/pocketmine/lang/locale - url = https://github.com/pmmp/Language.git -[submodule "tests/preprocessor"] - path = build/preprocessor - url = https://github.com/pmmp/preprocessor.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 "src/pocketmine/resources/vanilla"] - path = src/pocketmine/resources/vanilla - url = https://github.com/pmmp/BedrockData.git diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 590f6ebd89..8d6d98c4b0 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -4,9 +4,8 @@ $finder = PhpCsFixer\Finder::create() ->in(__DIR__ . '/src') ->in(__DIR__ . '/build') ->in(__DIR__ . '/tests') + ->in(__DIR__ . '/tools') ->notPath('plugins/DevTools') - ->notPath('preprocessor') - ->notContains('#ifndef COMPILE') //preprocessor will break if these are changed ->notName('PocketMine.php'); return (new PhpCsFixer\Config) diff --git a/BUILDING.md b/BUILDING.md index 37b7a4a8f1..d6e97e05c7 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -14,20 +14,15 @@ Because PocketMine-MP requires several non-standard PHP extensions and configura 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. -2. Preprocess the source code by running `build/preprocessor/PreProcessor.php`. Usage instructions are provided in `build/preprocessor/README.md`. - -### Note -Preprocessor requires that the `cpp` (c preprocessor) is available in your PATH. ## Building `PocketMine-MP.phar` Run `composer make-server` using your preferred PHP binary. It'll drop a `PocketMine-MP.phar` into the current working directory. @@ -41,4 +36,4 @@ Fatal error: Uncaught BadMethodCallException: unable to create temporary file in 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/PocketMine.php` using your preferred PHP binary. +Run `src/PocketMine.php` using your preferred PHP binary. diff --git a/build/generate-build-info-json.php b/build/generate-build-info-json.php index b1ca16fcaa..00ca2adc80 100644 --- a/build/generate-build-info-json.php +++ b/build/generate-build-info-json.php @@ -30,10 +30,10 @@ if(count($argv) !== 5){ echo json_encode([ "php_version" => sprintf("%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION), - "base_version" => \pocketmine\BASE_VERSION, + "base_version" => \pocketmine\VersionInfo::BASE_VERSION, "build" => (int) $argv[4], - "is_dev" => \pocketmine\IS_DEVELOPMENT_BUILD, - "channel" => \pocketmine\BUILD_CHANNEL, + "is_dev" => \pocketmine\VersionInfo::IS_DEVELOPMENT_BUILD, + "channel" => \pocketmine\VersionInfo::BUILD_CHANNEL, "git_commit" => $argv[1], "mcpe_version" => \pocketmine\network\mcpe\protocol\ProtocolInfo::MINECRAFT_VERSION_NETWORK, "date" => time(), //TODO: maybe we should embed this in VersionInfo? diff --git a/build/generate-known-translation-apis.php b/build/generate-known-translation-apis.php new file mode 100644 index 0000000000..feee9b1830 --- /dev/null +++ b/build/generate-known-translation-apis.php @@ -0,0 +1,183 @@ + $languageDefinitions + */ +function generate_known_translation_keys(array $languageDefinitions) : void{ + ob_start(); + + echo SHARED_HEADER; + echo <<<'HEADER' +/** + * 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. + */ +final class KnownTranslationKeys{ + +HEADER; + + ksort($languageDefinitions, SORT_STRING); + foreach(Utils::stringifyKeys($languageDefinitions) as $k => $_){ + echo "\tpublic const "; + echo constantify($k); + echo " = \"" . $k . "\";\n"; + } + + echo "}\n"; + + file_put_contents(dirname(__DIR__) . '/src/lang/KnownTranslationKeys.php', ob_get_clean()); + + echo "Done generating KnownTranslationKeys.\n"; +} + +/** + * @param string[] $languageDefinitions + * @phpstan-param array $languageDefinitions + */ +function generate_known_translation_factory(array $languageDefinitions) : void{ + ob_start(); + + echo SHARED_HEADER; + echo <<<'HEADER' +/** + * 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. + */ +final class KnownTranslationFactory{ + +HEADER; + ksort($languageDefinitions, SORT_STRING); + + $parameterRegex = '/{%(.+?)}/'; + + $translationContainerClass = (new \ReflectionClass(Translatable::class))->getShortName(); + foreach(Utils::stringifyKeys($languageDefinitions) as $key => $value){ + $parameters = []; + 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; + } + } + } + echo "\tpublic static function " . + functionify($key) . + "(" . implode(", ", array_map(fn(string $paramName) => "$translationContainerClass|string \$$paramName", $parameters)) . ") : $translationContainerClass{\n"; + echo "\t\treturn new $translationContainerClass(KnownTranslationKeys::" . constantify($key) . ", ["; + foreach($parameters as $parameterKey => $parameterName){ + echo "\n\t\t\t"; + if(!is_numeric($parameterKey)){ + echo "\"$parameterKey\""; + }else{ + echo $parameterKey; + } + echo " => \$$parameterName,"; + } + if(count($parameters) !== 0){ + echo "\n\t\t"; + } + echo "]);\n\t}\n\n"; + } + + echo "}\n"; + + file_put_contents(dirname(__DIR__) . '/src/lang/KnownTranslationFactory.php', ob_get_clean()); + + echo "Done generating KnownTranslationFactory.\n"; +} + +$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); +} + +generate_known_translation_keys($lang); +generate_known_translation_factory($lang); diff --git a/build/generate-registry-annotations.php b/build/generate-registry-annotations.php new file mode 100644 index 0000000000..11e15a3c60 --- /dev/null +++ b/build/generate-registry-annotations.php @@ -0,0 +1,119 @@ + $member){ + $reflect = new \ReflectionClass($member); + while($reflect !== false and $reflect->isAnonymous()){ + $reflect = $reflect->getParentClass(); + } + if($reflect === false){ + $typehint = "object"; + }elseif($reflect->getNamespaceName() === $namespaceName){ + $typehint = $reflect->getShortName(); + }else{ + $typehint = '\\' . $reflect->getName(); + } + $accessor = mb_strtoupper($name); + $memberLines[$accessor] = sprintf($lineTmpl, $accessor, $typehint); + } + ksort($memberLines, SORT_STRING); + + foreach($memberLines as $line){ + $lines[] = $line; + } + $lines[] = " */"; + return implode("\n", $lines); +} + +require dirname(__DIR__) . '/vendor/autoload.php'; + +foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($argv[1], \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME)) as $file){ + if(substr($file, -4) !== ".php"){ + continue; + } + $contents = file_get_contents($file); + if($contents === false){ + throw new \RuntimeException("Failed to get contents of $file"); + } + + if(preg_match("/(*ANYCRLF)^namespace (.+);$/m", $contents, $matches) !== 1 || preg_match('/(*ANYCRLF)^((final|abstract)\s+)?class /m', $contents) !== 1){ + continue; + } + $shortClassName = basename($file, ".php"); + $className = $matches[1] . "\\" . $shortClassName; + if(!class_exists($className)){ + continue; + } + $reflect = new \ReflectionClass($className); + $docComment = $reflect->getDocComment(); + if($docComment === false || preg_match("/(*ANYCRLF)^\s*\*\s*@generate-registry-docblock$/m", $docComment) !== 1){ + continue; + } + echo "Found registry in $file\n"; + + $replacement = generateMethodAnnotations($matches[1], $className::getAll()); + + $newContents = str_replace($docComment, $replacement, $contents); + if($newContents !== $contents){ + echo "Writing changed file $file\n"; + file_put_contents($file, $newContents); + }else{ + echo "No changes made to file $file\n"; + } +} + diff --git a/build/make-release.php b/build/make-release.php index 441e74ce24..e3a3dad816 100644 --- a/build/make-release.php +++ b/build/make-release.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\build\make_release; +use pocketmine\utils\Utils; use pocketmine\utils\VersionString; +use pocketmine\VersionInfo; use function array_keys; use function array_map; use function dirname; @@ -40,8 +42,6 @@ use function sprintf; use function str_pad; use function strlen; use function system; -use const pocketmine\BASE_VERSION; -use const pocketmine\BUILD_CHANNEL; use const STDERR; use const STDIN; use const STDOUT; @@ -77,7 +77,7 @@ const ACCEPTED_OPTS = [ function main() : void{ $filteredOpts = []; - foreach(getopt("", ["current:", "next:", "channel:", "help"]) as $optName => $optValue){ + foreach(Utils::stringifyKeys(getopt("", ["current:", "next:", "channel:", "help"])) as $optName => $optValue){ if($optName === "help"){ fwrite(STDOUT, "Options:\n"); @@ -97,7 +97,7 @@ function main() : void{ if(isset($filteredOpts["current"])){ $currentVer = new VersionString($filteredOpts["current"]); }else{ - $currentVer = new VersionString(BASE_VERSION); + $currentVer = new VersionString(VersionInfo::BASE_VERSION); } if(isset($filteredOpts["next"])){ $nextVer = new VersionString($filteredOpts["next"]); @@ -109,7 +109,7 @@ function main() : void{ $currentVer->getPatch() + 1 )); } - $channel = $filteredOpts["channel"] ?? BUILD_CHANNEL; + $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"; @@ -121,7 +121,7 @@ function main() : void{ echo "error: no changelog changes detected; aborting\n"; exit(1); } - $versionInfoPath = dirname(__DIR__) . '/src/pocketmine/VersionInfo.php'; + $versionInfoPath = dirname(__DIR__) . '/src/VersionInfo.php'; replaceVersion($versionInfoPath, $currentVer->getBaseVersion(), false, $channel); system('git commit -m "Release ' . $currentVer->getBaseVersion() . '" --include "' . $versionInfoPath . '"'); system('git tag ' . $currentVer->getBaseVersion()); diff --git a/build/preprocessor b/build/preprocessor deleted file mode 160000 index 1b9304de61..0000000000 --- a/build/preprocessor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1b9304de614090296a0906e60587789065a4cd6a diff --git a/build/server-phar.php b/build/server-phar.php index 462ff879cb..2f5aa1050e 100644 --- a/build/server-phar.php +++ b/build/server-phar.php @@ -150,6 +150,7 @@ function main() : void{ $opts["out"] ?? getcwd() . DIRECTORY_SEPARATOR . "PocketMine-MP.phar", dirname(__DIR__) . DIRECTORY_SEPARATOR, [ + 'resources', 'src', 'vendor' ], @@ -168,7 +169,7 @@ if(!is_readable($tmpDir) or !is_writable($tmpDir)){ exit(1); } -require("phar://" . __FILE__ . "/src/pocketmine/PocketMine.php"); +require("phar://" . __FILE__ . "/src/PocketMine.php"); __HALT_COMPILER(); STUB , diff --git a/changelogs/4.0-beta.md b/changelogs/4.0-beta.md new file mode 100644 index 0000000000..a08f22428d --- /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 new file mode 100644 index 0000000000..2f0673699b --- /dev/null +++ b/changelogs/4.0.md @@ -0,0 +1,1522 @@ +# 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 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. + +* [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 +- 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. +- Player movement anti-cheat has been removed. +- GS4 Query no longer breaks when disabling RakLib. +- 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 `/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 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 +- 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` +- 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. +- 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. +- 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`. + +### 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. + +#### 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. +- 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. + +### 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. +- `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. + +### 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. +- 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->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) + - `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 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` -> `FloorSign` + - `StandingBanner` -> `FloorBanner` +- 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). + - `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 + - `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[]`. + - `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` + - `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. +- Fixed `Living->lookAt()` not taking eye height into account. + +#### 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()` -> `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. + - `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`. + - `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`. +- 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`). + - `@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. +- 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). +- 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: + - `BlockTeleportEvent`: block teleporting, for example dragon egg when attacked. + - `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. + - `InventoryPickupArrowEvent` - use `EntityItemPickupEvent` instead + - `InventoryPickupItemEvent` - 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 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` + +### 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. + - `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: + - `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 + - `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()` + - `Inventory->getTitle()` + - `Inventory->onSlotChange()` + - `Inventory->open()` + - `Inventory->sendContents()` + - `Inventory->sendSlot()` + - `InventoryAction->onExecuteFail()` + - `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`. +- `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` 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` 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. +- 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()`: 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()` + - `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 + - `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: + - `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` 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. +- The following API methods have been moved: + - `Enchantment::registerEnchantment()` -> `EnchantmentIdMap->register()` + - `Enchantment::getEnchantment()` -> `EnchantmentIdMap->fromId()` + - `Enchantment->getId()` -> `EnchantmentIdMap->toId()` + - `Enchantment::getEnchantmentByName()` -> `StringToEnchantmentParser->parse()` + +### 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 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` +- 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 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()` + - `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->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->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 +- 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->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 + - `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`. +- `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 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. +- 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 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[]`. +- `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()` + +#### 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: + - `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: + - `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: + - `Server->getOfflinePlayerData()` no longer creates data when it doesn't exist. +- The following API methods have been renamed: + - `Server->getPlayer()` -> `Server->getPlayerByPrefix()` (consider using `Server->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()` +- 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) + - `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 from the public API: + - `Chunk->addEntity()` + - `Chunk->fastSerialize()` (use `FastChunkSerializer::serializeTerrain()` instead) + - `Chunk->getBlockData()` + - `Chunk->getBlockDataColumn()` + - `Chunk->getBlockId()` + - `Chunk->getBlockIdColumn()` + - `Chunk->getBlockLight()` + - `Chunk->getBlockLightColumn()` + - `Chunk->getBlockSkyLight()` + - `Chunk->getBlockSkyLightColumn()` + - `Chunk->getEntities()` + - `Chunk->getMaxY()` + - `Chunk->getSavableEntities()` + - `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->removeEntity()` + - `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::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->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->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->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->setChunks()` no longer accepts a `$deleteEntitiesAndTiles` parameter. + - `World->updateAllLight()` now accepts `int, int, int` instead of `Vector3`. + - `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: + - `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()` 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: + - `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`. +- 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 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: + - `InternetException` + - `Internet` + - `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 +- 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`. + - `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 + - 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. +- 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. + +### Misc +- Added support for emotes. diff --git a/composer.json b/composer.json index ab792210b7..415a36e955 100644 --- a/composer.json +++ b/composer.json @@ -8,12 +8,17 @@ "php": "^8.0", "php-64bit": "*", "ext-chunkutils2": "^0.3.1", + "ext-crypto": "^0.3.1", "ext-ctype": "*", "ext-curl": "*", "ext-date": "*", + "ext-gmp": "*", "ext-hash": "*", + "ext-igbinary": "^3.0.1", "ext-json": "*", + "ext-leveldb": "^0.2.1 || ^0.3.0", "ext-mbstring": "*", + "ext-morton": "^0.1.0", "ext-openssl": "*", "ext-pcre": "*", "ext-phar": "*", @@ -27,16 +32,25 @@ "ext-zlib": ">=1.2.11", "composer-runtime-api": "^2.0", "adhocore/json-comment": "^1.1", - "pocketmine/binaryutils": "^0.1.9", + "fgrosse/phpasn1": "^2.3", + "netresearch/jsonmapper": "^4.0", + "pocketmine/bedrock-data": "^1.5.0+bedrock-1.18.0", + "pocketmine/bedrock-protocol": "^7.0.0+bedrock-1.18.0", + "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", - "pocketmine/classloader": "^0.1.0", - "pocketmine/log": "^0.2.0", - "pocketmine/log-pthreads": "^0.1.0", - "pocketmine/math": "^0.2.0", - "pocketmine/nbt": "^0.2.18", - "pocketmine/raklib": "^0.12.7", - "pocketmine/snooze": "^0.1.0", - "pocketmine/spl": "^0.4.0" + "pocketmine/classloader": "^0.2.0", + "pocketmine/color": "^0.2.0", + "pocketmine/errorhandler": "^0.3.0", + "pocketmine/locale-data": "^2.0.16", + "pocketmine/log": "^0.4.0", + "pocketmine/log-pthreads": "^0.4.0", + "pocketmine/math": "^0.4.0", + "pocketmine/nbt": "^0.3.0", + "pocketmine/raklib": "^0.14.2", + "pocketmine/raklib-ipc": "^0.1.0", + "pocketmine/snooze": "^0.3.0", + "ramsey/uuid": "^4.1", + "webmozart/path-util": "^2.3" }, "require-dev": { "phpstan/phpstan": "1.2.0", @@ -46,17 +60,16 @@ }, "autoload": { "psr-4": { - "": ["src"] + "pocketmine\\": "src/" }, "files": [ - "src/pocketmine/CoreConstants.php", - "src/pocketmine/GlobalConstants.php", - "src/pocketmine/VersionInfo.php" + "src/CoreConstants.php" ] }, "autoload-dev": { "psr-4": { - "pocketmine\\": "tests/phpunit/" + "pocketmine\\": "tests/phpunit/", + "pocketmine\\phpstan\\rules\\": "tests/phpstan/rules" } }, "config": { @@ -66,10 +79,16 @@ "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" + ], + "update-registry-annotations": [ + "@php build/generate-registry-annotations.php src" + ], + "update-translation-apis": [ + "@php build/generate-known-translation-apis.php" ] } } diff --git a/composer.lock b/composer.lock index e948d9abf0..284b6914dd 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": "2d144524b177c9e7f699ba7366c16354", + "content-hash": "b515e7eebbf12a6251d2df817ce56dbc", "packages": [ { "name": "adhocore/json-comment", @@ -62,26 +62,285 @@ "time": "2021-04-09T03:06:06+00:00" }, { - "name": "pocketmine/binaryutils", - "version": "0.1.13", + "name": "brick/math", + "version": "0.9.3", "source": { "type": "git", - "url": "https://github.com/pmmp/BinaryUtils.git", - "reference": "0abee38d4e2861621f262c79a2a3d699d8a697f4" + "url": "https://github.com/brick/math.git", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/0abee38d4e2861621f262c79a2a3d699d8a697f4", - "reference": "0abee38d4e2861621f262c79a2a3d699d8a697f4", + "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.9.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "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-08-15T20:50:18+00:00" + }, + { + "name": "fgrosse/phpasn1", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/20299033c35f4300eb656e7e8e88cf52d1d6694e", + "reference": "20299033c35f4300eb656e7e8e88cf52d1d6694e", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.3", + "satooshi/php-coveralls": "~2.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.3.0" + }, + "time": "2021-04-24T19:01:55+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0" + }, + "time": "2020-12-01T19:48:11+00:00" + }, + { + "name": "pocketmine/bedrock-data", + "version": "1.5.0+bedrock-1.18.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockData.git", + "reference": "482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c" + }, + "dist": { + "type": "zip", + "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.0.0+bedrock-1.18.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockProtocol.git", + "reference": "040a883a4abb8c9b93114d5278cb5fecc635da82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/040a883a4abb8c9b93114d5278cb5fecc635da82", + "reference": "040a883a4abb8c9b93114d5278cb5fecc635da82", + "shasum": "" + }, + "require": { + "ext-json": "*", + "netresearch/jsonmapper": "^4.0", + "php": "^8.0", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/color": "^0.2.0", + "pocketmine/math": "^0.3.0 || ^0.4.0", + "pocketmine/nbt": "^0.3.0", + "ramsey/uuid": "^4.1" + }, + "require-dev": { + "phpstan/phpstan": "1.2.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-strict-rules": "^1.0.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\network\\mcpe\\protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "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/7.0.0+bedrock-1.18.0" + }, + "time": "2021-11-30T19:02:41+00:00" + }, + { + "name": "pocketmine/binaryutils", + "version": "0.2.2", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BinaryUtils.git", + "reference": "f883e1cf9099ed6a757a10a2f75b3333eeb2cdf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/f883e1cf9099ed6a757a10a2f75b3333eeb2cdf9", + "reference": "f883e1cf9099ed6a757a10a2f75b3333eeb2cdf9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", "php-64bit": "*" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.67", + "phpstan/phpstan": "0.12.99", "phpstan/phpstan-strict-rules": "^0.12.4" }, "type": "library", @@ -97,9 +356,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.1.13" + "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.2" }, - "time": "2021-01-15T14:19:13+00:00" + "time": "2021-10-22T19:54:16+00:00" }, { "name": "pocketmine/callback-validator", @@ -153,22 +412,22 @@ }, { "name": "pocketmine/classloader", - "version": "0.1.3", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/pmmp/ClassLoader.git", - "reference": "3c484a27787f7732ce842ed694928a29ba340961" + "reference": "49ea303993efdfb39cd302e2156d50aa78209e78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/3c484a27787f7732ce842ed694928a29ba340961", - "reference": "3c484a27787f7732ce842ed694928a29ba340961", + "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/49ea303993efdfb39cd302e2156d50aa78209e78", + "reference": "49ea303993efdfb39cd302e2156d50aa78209e78", "shasum": "" }, "require": { "ext-pthreads": "~3.2.0 || ^4.0", "ext-reflection": "*", - "php": "^7.2 || ^8.0" + "php": "^8.0" }, "conflict": { "pocketmine/spl": "<0.4" @@ -176,7 +435,8 @@ "require-dev": { "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "0.12.99", - "phpstan/phpstan-strict-rules": "^0.12.4" + "phpstan/phpstan-strict-rules": "^0.12.4", + "phpunit/phpunit": "^9.5" }, "type": "library", "autoload": { @@ -191,32 +451,131 @@ "description": "Ad-hoc autoloading components used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/ClassLoader/issues", - "source": "https://github.com/pmmp/ClassLoader/tree/0.1.3" + "source": "https://github.com/pmmp/ClassLoader/tree/0.2.0" }, - "time": "2021-11-01T20:13:55+00:00" + "time": "2021-11-01T20:17:27+00:00" }, { - "name": "pocketmine/log", - "version": "0.2.1", + "name": "pocketmine/color", + "version": "0.2.0", "source": { "type": "git", - "url": "https://github.com/pmmp/Log.git", - "reference": "830b44a2cf96ef703c550abe64302f230231ca49" + "url": "https://github.com/pmmp/Color.git", + "reference": "09be6ea6d76f2e33d6813c39d29c22c46c17e1d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Log/zipball/830b44a2cf96ef703c550abe64302f230231ca49", - "reference": "830b44a2cf96ef703c550abe64302f230231ca49", + "url": "https://api.github.com/repos/pmmp/Color/zipball/09be6ea6d76f2e33d6813c39d29c22c46c17e1d2", + "reference": "09be6ea6d76f2e33d6813c39d29c22c46c17e1d2", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, + "require-dev": { + "phpstan/phpstan": "0.12.59", + "phpstan/phpstan-strict-rules": "^0.12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\color\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Color handling library used by PocketMine-MP and related projects", + "support": { + "issues": "https://github.com/pmmp/Color/issues", + "source": "https://github.com/pmmp/Color/tree/0.2.0" + }, + "time": "2020-12-11T01:24:32+00:00" + }, + { + "name": "pocketmine/errorhandler", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/ErrorHandler.git", + "reference": "ec742b209e8056bbe855069c4eff94c9734ea19b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/ec742b209e8056bbe855069c4eff94c9734ea19b", + "reference": "ec742b209e8056bbe855069c4eff94c9734ea19b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "0.12.75", + "phpstan/phpstan-strict-rules": "^0.12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\errorhandler\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "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" + }, + "time": "2021-02-12T18:56:22+00:00" + }, + { + "name": "pocketmine/locale-data", + "version": "2.0.20", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Language.git", + "reference": "a6e4eb22587e0014f6d658732cf633262723f8d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Language/zipball/a6e4eb22587e0014f6d658732cf633262723f8d3", + "reference": "a6e4eb22587e0014f6d658732cf633262723f8d3", + "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.0.20" + }, + "time": "2021-11-25T20:56:12+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": { + "php": "^7.4 || ^8.0" + }, "conflict": { "pocketmine/spl": "<0.4" }, "require-dev": { - "phpstan/phpstan": "0.12.67", + "phpstan/phpstan": "0.12.88", "phpstan/phpstan-strict-rules": "^0.12.2" }, "type": "library", @@ -232,35 +591,35 @@ "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.2.1" + "source": "https://github.com/pmmp/Log/tree/0.4.0" }, - "time": "2021-01-15T14:32:41+00:00" + "time": "2021-06-18T19:08:09+00:00" }, { "name": "pocketmine/log-pthreads", - "version": "0.1.4", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/pmmp/LogPthreads.git", - "reference": "01620c3628cdaa6b4a21122cff4c5d2f70b5c1d3" + "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/01620c3628cdaa6b4a21122cff4c5d2f70b5c1d3", - "reference": "01620c3628cdaa6b4a21122cff4c5d2f70b5c1d3", + "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/61f709e8cf36bcc24e4efe02acded680a1ce23cd", + "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd", "shasum": "" }, "require": { "ext-pthreads": "~3.2.0 || ^4.0", - "php": "^7.2 || ^8.0", - "pocketmine/log": "^0.2.0" + "php": "^7.4 || ^8.0", + "pocketmine/log": "^0.4.0" }, "conflict": { "pocketmine/spl": "<0.4" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.80", + "phpstan/phpstan": "0.12.88", "phpstan/phpstan-strict-rules": "^0.12.4" }, "type": "library", @@ -276,31 +635,32 @@ "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.1.4" + "source": "https://github.com/pmmp/LogPthreads/tree/0.4.0" }, - "time": "2021-11-01T20:36:53+00:00" + "time": "2021-11-01T21:42:09+00:00" }, { "name": "pocketmine/math", - "version": "0.2.6", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/pmmp/Math.git", - "reference": "43057cb8c179a9859677b496a788db922fd5cfc3" + "reference": "6d64e2555bd2e95ed024574f75d1cefc135c89fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Math/zipball/43057cb8c179a9859677b496a788db922fd5cfc3", - "reference": "43057cb8c179a9859677b496a788db922fd5cfc3", + "url": "https://api.github.com/repos/pmmp/Math/zipball/6d64e2555bd2e95ed024574f75d1cefc135c89fc", + "reference": "6d64e2555bd2e95ed024574f75d1cefc135c89fc", "shasum": "" }, "require": { - "php": "^7.2 || ^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.67", + "phpstan/phpstan": "0.12.99", "phpstan/phpstan-strict-rules": "^0.12.4" }, "type": "library", @@ -316,34 +676,33 @@ "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.2.6" + "source": "https://github.com/pmmp/Math/tree/0.4.0" }, - "time": "2021-01-15T14:25:11+00:00" + "time": "2021-10-29T20:33:10+00:00" }, { "name": "pocketmine/nbt", - "version": "0.2.18", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/pmmp/NBT.git", - "reference": "9f82ca4d7f97fcd9a566e44b63c4f18a7657ae82" + "reference": "98c4a04b55a915e18f83d3b0c9beb24a71abcd31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/NBT/zipball/9f82ca4d7f97fcd9a566e44b63c4f18a7657ae82", - "reference": "9f82ca4d7f97fcd9a566e44b63c4f18a7657ae82", + "url": "https://api.github.com/repos/pmmp/NBT/zipball/98c4a04b55a915e18f83d3b0c9beb24a71abcd31", + "reference": "98c4a04b55a915e18f83d3b0c9beb24a71abcd31", "shasum": "" }, "require": { - "ext-zlib": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "php-64bit": "*", - "pocketmine/binaryutils": "^0.1.9" + "pocketmine/binaryutils": "^0.2.0" }, "require-dev": { - "irstea/phpunit-shim": "^7.5 || ^8.0", + "irstea/phpunit-shim": "^9.5", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.80", + "phpstan/phpstan": "0.12.85", "phpstan/phpstan-strict-rules": "^0.12.4" }, "type": "library", @@ -359,37 +718,34 @@ "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.2.18" + "source": "https://github.com/pmmp/NBT/tree/0.3.0" }, - "time": "2021-03-11T00:09:04+00:00" + "time": "2021-05-18T15:46:33+00:00" }, { "name": "pocketmine/raklib", - "version": "0.12.12", + "version": "0.14.2", "source": { "type": "git", "url": "https://github.com/pmmp/RakLib.git", - "reference": "5abe22043352e94099e4edfcef5fb3644578ddc1" + "reference": "e3a861187470e1facc6625040128f447ebbcbaec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/RakLib/zipball/5abe22043352e94099e4edfcef5fb3644578ddc1", - "reference": "5abe22043352e94099e4edfcef5fb3644578ddc1", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/e3a861187470e1facc6625040128f447ebbcbaec", + "reference": "e3a861187470e1facc6625040128f447ebbcbaec", "shasum": "" }, "require": { - "ext-pthreads": "~3.2.0 || ^4.0", "ext-sockets": "*", - "php": "^7.2 || ^8.0", + "php": "^8.0", "php-64bit": "*", "php-ipv6": "*", - "pocketmine/binaryutils": "^0.1.9", - "pocketmine/log": "^0.2.0", - "pocketmine/log-pthreads": "^0.1.0", - "pocketmine/snooze": "^0.1.0" + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/log": "^0.3.0 || ^0.4.0" }, "require-dev": { - "phpstan/phpstan": "0.12.87", + "phpstan/phpstan": "0.12.99", "phpstan/phpstan-strict-rules": "^0.12.2" }, "type": "library", @@ -405,31 +761,72 @@ "description": "A RakNet server implementation written in PHP", "support": { "issues": "https://github.com/pmmp/RakLib/issues", - "source": "https://github.com/pmmp/RakLib/tree/0.12.12" + "source": "https://github.com/pmmp/RakLib/tree/0.14.2" }, - "time": "2021-11-01T20:52:51+00:00" + "time": "2021-10-04T20:39:11+00:00" }, { - "name": "pocketmine/snooze", - "version": "0.1.6", + "name": "pocketmine/raklib-ipc", + "version": "0.1.1", "source": { "type": "git", - "url": "https://github.com/pmmp/Snooze.git", - "reference": "92abf1e988c71635d466abb777f61f89e5a9c990" + "url": "https://github.com/pmmp/RakLibIpc.git", + "reference": "922a6444b0c6c7daaa5aa5a832107e1ec4738aed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Snooze/zipball/92abf1e988c71635d466abb777f61f89e5a9c990", - "reference": "92abf1e988c71635d466abb777f61f89e5a9c990", + "url": "https://api.github.com/repos/pmmp/RakLibIpc/zipball/922a6444b0c6c7daaa5aa5a832107e1ec4738aed", + "reference": "922a6444b0c6c7daaa5aa5a832107e1ec4738aed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-64bit": "*", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/raklib": "^0.13.1 || ^0.14.0" + }, + "require-dev": { + "phpstan/phpstan": "0.12.81", + "phpstan/phpstan-strict-rules": "^0.12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "raklib\\server\\ipc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "Channel-based protocols for inter-thread/inter-process communication with RakLib", + "support": { + "issues": "https://github.com/pmmp/RakLibIpc/issues", + "source": "https://github.com/pmmp/RakLibIpc/tree/0.1.1" + }, + "time": "2021-09-22T17:01:12+00:00" + }, + { + "name": "pocketmine/snooze", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Snooze.git", + "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Snooze/zipball/0ac8fc2a781c419a1f64ebca4d5835028f59e29b", + "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b", "shasum": "" }, "require": { "ext-pthreads": "~3.2.0 || ^4.0", - "php-64bit": "^7.2 || ^8.0" + "php-64bit": "^7.3 || ^8.0" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.76", + "phpstan/phpstan": "0.12.99", "phpstan/phpstan-strict-rules": "^0.12.4" }, "type": "library", @@ -445,47 +842,536 @@ "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.1.6" + "source": "https://github.com/pmmp/Snooze/tree/0.3.1" }, - "time": "2021-11-01T20:48:46+00:00" + "time": "2021-11-01T20:50:08+00:00" }, { - "name": "pocketmine/spl", - "version": "0.4.2", + "name": "ramsey/collection", + "version": "1.2.2", "source": { "type": "git", - "url": "https://github.com/pmmp/SPL.git", - "reference": "6b08b7cf8c4afa17139c9a1b3bf1b408531de161" + "url": "https://github.com/ramsey/collection.git", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/SPL/zipball/6b08b7cf8c4afa17139c9a1b3bf1b408531de161", - "reference": "6b08b7cf8c4afa17139c9a1b3bf1b408531de161", + "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.3 || ^8", + "symfony/polyfill-php81": "^1.23" }, "require-dev": { - "phpstan/phpstan": "^0.12.8" + "captainhook/captainhook": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.6", + "fakerphp/faker": "^1.5", + "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", + "phpstan/phpstan-phpunit": "^0.12.11", + "phpunit/phpunit": "^8.5 || ^9", + "psy/psysh": "^0.10.4", + "slevomat/coding-standard": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" }, "type": "library", "autoload": { - "classmap": [ - "./src" + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/1.2.2" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2021-10-10T03:01:02+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + }, + "dist": { + "type": "zip", + "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.0", + "ramsey/collection": "^1.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php80": "^1.14" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5 || ^9", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + }, + "captainhook": { + "force-install": true + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + }, + "files": [ + "src/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" ], - "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/0.4.2" + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.2.3" }, - "abandoned": true, - "time": "2021-01-15T15:15:23+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2021-09-25T23:10:38+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "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\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.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-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.23.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "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\\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.23.1" + }, + "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-07-28T13:41:28+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "e66119f3de95efc359483f810c4c3e6436279436" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436", + "reference": "e66119f3de95efc359483f810c4c3e6436279436", + "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/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.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-05-21T13:25:03+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + }, + { + "name": "webmozart/path-util", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/path-util.git", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "webmozart/assert": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\PathUtil\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "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" } ], "packages-dev": [ @@ -2565,85 +3451,6 @@ ], "time": "2020-09-28T06:39:44+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.23.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "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\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.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-02-19T12:13:01+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.1", @@ -2693,64 +3500,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -2762,12 +3511,17 @@ "php": "^8.0", "php-64bit": "*", "ext-chunkutils2": "^0.3.1", + "ext-crypto": "^0.3.1", "ext-ctype": "*", "ext-curl": "*", "ext-date": "*", + "ext-gmp": "*", "ext-hash": "*", + "ext-igbinary": "^3.0.1", "ext-json": "*", + "ext-leveldb": "^0.2.1 || ^0.3.0", "ext-mbstring": "*", + "ext-morton": "^0.1.0", "ext-openssl": "*", "ext-pcre": "*", "ext-phar": "*", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8ad20ab63d..9ebc985025 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,15 +1,20 @@ includes: - tests/phpstan/configs/actual-problems.neon - tests/phpstan/configs/gc-hacks.neon + - tests/phpstan/configs/impossible-generics.neon - tests/phpstan/configs/php-bugs.neon - tests/phpstan/configs/phpstan-bugs.neon - - tests/phpstan/configs/phpunit-wiring-tests.neon - tests/phpstan/configs/runtime-type-checks.neon - tests/phpstan/configs/spl-fixed-array-sucks.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon +rules: + - pocketmine\phpstan\rules\DisallowEnumComparisonRule + - pocketmine\phpstan\rules\UnsafeForeachArrayOfStringRule +# - pocketmine\phpstan\rules\ThreadedSupportedTypesRule + parameters: level: 9 checkMissingCallableSignature: true @@ -19,26 +24,27 @@ parameters: scanDirectories: - build - tests/plugins/TesterPlugin + - tools scanFiles: - - src/pocketmine/PocketMine.php + - src/PocketMine.php paths: - build - src + - tests/phpstan/rules - tests/phpunit - tests/plugins/TesterPlugin + - tools excludePaths: analyseAndScan: - build/php - - build/preprocessor - analyse: - - src/pocketmine/block/StoneSlab.php #overrides STONE constant - - src/pocketmine/item/Potion.php #overrides WATER constant dynamicConstantNames: + - pocketmine\VersionInfo::IS_DEVELOPMENT_BUILD - pocketmine\DEBUG - pocketmine\IS_DEVELOPMENT_BUILD stubFiles: - - tests/phpstan/stubs/chunkutils.stub + - tests/phpstan/stubs/JsonMapper.stub - tests/phpstan/stubs/leveldb.stub + - tests/phpstan/stubs/phpasn1.stub - tests/phpstan/stubs/pthreads.stub reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings staticReflectionClassNamePatterns: diff --git a/resources/item_from_string_bc_map.json b/resources/item_from_string_bc_map.json new file mode 100644 index 0000000000..c5c502e9a2 --- /dev/null +++ b/resources/item_from_string_bc_map.json @@ -0,0 +1,1575 @@ +{ + "-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, + "acacia_fence_gate": 187, + "acacia_pressure_plate": -150, + "acacia_sign": 475, + "acacia_stairs": 163, + "acacia_standing_sign": -190, + "acacia_trapdoor": -145, + "acacia_wall_sign": -191, + "acacia_wood_stairs": 163, + "acacia_wooden_stairs": 163, + "activator_rail": 126, + "active_redstone_lamp": 124, + "air": 0, + "andesite_stairs": -171, + "anvil": 145, + "apple": 260, + "apple_enchanted": 466, + "appleenchanted": 466, + "armor_stand": 425, + "arrow": 262, + "ateupd_block": 249, + "baked_potato": 393, + "baked_potatoes": 393, + "balloon": 448, + "bamboo": -163, + "bamboo_sapling": -164, + "banner": 446, + "banner_pattern": 434, + "barrel": -203, + "barrier": -161, + "beacon": 138, + "bed": 355, + "bed_block": 26, + "bedrock": 7, + "beef": 363, + "beetroot": 457, + "beetroot_block": 244, + "beetroot_seed": 458, + "beetroot_seeds": 458, + "beetroot_soup": 459, + "bell": -206, + "birch_button": -141, + "birch_door": 428, + "birch_door_block": 194, + "birch_fence_gate": 184, + "birch_pressure_plate": -151, + "birch_sign": 473, + "birch_stairs": 135, + "birch_standing_sign": -186, + "birch_trapdoor": -146, + "birch_wall_sign": -187, + "birch_wood_stairs": 135, + "birch_wooden_stairs": 135, + "black_glazed_terracotta": 235, + "blast_furnace": -196, + "blaze_powder": 377, + "blaze_rod": 369, + "bleach": 451, + "block_moved_by_piston": 250, + "blue_glazed_terracotta": 231, + "blue_ice": -11, + "boat": 333, + "bone": 352, + "bone_block": 216, + "book": 340, + "bookshelf": 47, + "bottle_o_enchanting": 384, + "bow": 261, + "bowl": 281, + "bread": 297, + "brewing_stand": 379, + "brewing_stand_block": 117, + "brick": 336, + "brick_block": 45, + "brick_stairs": 108, + "bricks": 45, + "bricks_block": 45, + "brown_glazed_terracotta": 232, + "brown_mushroom": 39, + "brown_mushroom_block": 99, + "bubble_column": -160, + "bucket": 325, + "burning_furnace": 62, + "bush": 32, + "cactus": 81, + "cake": 354, + "cake_block": 92, + "campfire": -209, + "carpet": 171, + "carrot": 391, + "carrot_block": 141, + "carrot_on_a_stick": 398, + "carrotonastick": 398, + "carrots": 141, + "cartography_table": -200, + "carved_pumpkin": -155, + "cauldron": 380, + "cauldron_block": 118, + "chain_boots": 305, + "chain_chestplate": 303, + "chain_command_block": 189, + "chain_helmet": 302, + "chain_leggings": 304, + "chainmail_boots": 305, + "chainmail_chestplate": 303, + "chainmail_helmet": 302, + "chainmail_leggings": 304, + "chemical_heat": 192, + "chemistry_table": 238, + "chest": 54, + "chest_minecart": 342, + "chicken": 365, + "chorus_flower": 200, + "chorus_fruit": 432, + "chorus_fruit_popped": 433, + "chorus_plant": 240, + "clay": 337, + "clay_ball": 337, + "clay_block": 82, + "clock": 347, + "clown_fish": 461, + "clownfish": 461, + "coal": 263, + "coal_block": 173, + "coal_ore": 16, + "cobble": 4, + "cobble_stairs": 67, + "cobble_wall": 139, + "cobblestone": 4, + "cobblestone_stairs": 67, + "cobblestone_wall": 139, + "cobweb": 30, + "cocoa": 127, + "cocoa_block": 127, + "cocoa_pods": 127, + "colored_torch_bp": 204, + "colored_torch_rg": 202, + "command_block": 137, + "command_block_minecart": 443, + "comparator": 404, + "comparator_block": 149, + "compass": 345, + "composter": -213, + "compound": 499, + "concrete": 236, + "concrete_powder": 237, + "concretepowder": 237, + "conduit": -157, + "cooked_beef": 364, + "cooked_chicken": 366, + "cooked_fish": 350, + "cooked_mutton": 424, + "cooked_porkchop": 320, + "cooked_rabbit": 412, + "cooked_salmon": 463, + "cookie": 357, + "coral": -131, + "coral_block": -132, + "coral_fan": -133, + "coral_fan_dead": -134, + "coral_fan_hang": -135, + "coral_fan_hang2": -136, + "coral_fan_hang3": -137, + "crafting_table": 58, + "crossbow": 471, + "cyan_glazed_terracotta": 229, + "dandelion": 37, + "dark_oak_button": -142, + "dark_oak_door": 431, + "dark_oak_door_block": 197, + "dark_oak_fence_gate": 186, + "dark_oak_pressure_plate": -152, + "dark_oak_stairs": 164, + "dark_oak_trapdoor": -147, + "dark_oak_wood_stairs": 164, + "dark_oak_wooden_stairs": 164, + "dark_prismarine_stairs": -3, + "darkoak_sign": 476, + "darkoak_standing_sign": -192, + "darkoak_wall_sign": -193, + "daylight_detector": 151, + "daylight_detector_inverted": 178, + "daylight_sensor": 151, + "daylight_sensor_inverted": 178, + "dead_bush": 32, + "deadbush": 32, + "detector_rail": 28, + "diamond": 264, + "diamond_axe": 279, + "diamond_block": 57, + "diamond_boots": 313, + "diamond_chestplate": 311, + "diamond_helmet": 310, + "diamond_hoe": 293, + "diamond_horse_armor": 419, + "diamond_leggings": 312, + "diamond_ore": 56, + "diamond_pickaxe": 278, + "diamond_shovel": 277, + "diamond_sword": 276, + "diorite_stairs": -170, + "dirt": 3, + "dispenser": 23, + "door_block": 64, + "double_plant": 175, + "double_red_sandstone_slab": 181, + "double_slab": 43, + "double_slabs": 43, + "double_stone_slab": 43, + "double_stone_slab2": 181, + "double_stone_slab3": -167, + "double_stone_slab4": -168, + "double_wood_slab": 157, + "double_wood_slabs": 157, + "double_wooden_slab": 157, + "double_wooden_slabs": 157, + "dragon_breath": 437, + "dragon_egg": 122, + "dried_kelp": 464, + "dried_kelp_block": -139, + "dropper": 125, + "dye": 351, + "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_11": -22, + "element_12": -23, + "element_13": -24, + "element_14": -25, + "element_15": -26, + "element_16": -27, + "element_17": -28, + "element_18": -29, + "element_19": -30, + "element_20": -31, + "element_21": -32, + "element_22": -33, + "element_23": -34, + "element_24": -35, + "element_25": -36, + "element_26": -37, + "element_27": -38, + "element_28": -39, + "element_29": -40, + "element_30": -41, + "element_31": -42, + "element_32": -43, + "element_33": -44, + "element_34": -45, + "element_35": -46, + "element_36": -47, + "element_37": -48, + "element_38": -49, + "element_39": -50, + "element_40": -51, + "element_41": -52, + "element_42": -53, + "element_43": -54, + "element_44": -55, + "element_45": -56, + "element_46": -57, + "element_47": -58, + "element_48": -59, + "element_49": -60, + "element_50": -61, + "element_51": -62, + "element_52": -63, + "element_53": -64, + "element_54": -65, + "element_55": -66, + "element_56": -67, + "element_57": -68, + "element_58": -69, + "element_59": -70, + "element_60": -71, + "element_61": -72, + "element_62": -73, + "element_63": -74, + "element_64": -75, + "element_65": -76, + "element_66": -77, + "element_67": -78, + "element_68": -79, + "element_69": -80, + "element_70": -81, + "element_71": -82, + "element_72": -83, + "element_73": -84, + "element_74": -85, + "element_75": -86, + "element_76": -87, + "element_77": -88, + "element_78": -89, + "element_79": -90, + "element_80": -91, + "element_81": -92, + "element_82": -93, + "element_83": -94, + "element_84": -95, + "element_85": -96, + "element_86": -97, + "element_87": -98, + "element_88": -99, + "element_89": -100, + "element_90": -101, + "element_91": -102, + "element_92": -103, + "element_93": -104, + "element_94": -105, + "element_95": -106, + "element_96": -107, + "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, + "emerald_ore": 129, + "empty_map": 395, + "emptymap": 395, + "enchant_table": 116, + "enchanted_book": 403, + "enchanted_golden_apple": 466, + "enchanting_bottle": 384, + "enchanting_table": 116, + "enchantment_table": 116, + "end_brick_stairs": -178, + "end_bricks": 206, + "end_crystal": 426, + "end_gateway": 209, + "end_portal": 119, + "end_portal_frame": 120, + "end_rod": 208, + "end_stone": 121, + "ender_chest": 130, + "ender_eye": 381, + "ender_pearl": 368, + "experience_bottle": 384, + "farmland": 60, + "feather": 288, + "fence": 85, + "fence_gate": 107, + "fence_gate_acacia": 187, + "fence_gate_birch": 184, + "fence_gate_dark_oak": 186, + "fence_gate_jungle": 185, + "fence_gate_spruce": 183, + "fermented_spider_eye": 376, + "filled_map": 358, + "fire": 51, + "fire_charge": 385, + "fireball": 385, + "fireworks": 401, + "fireworks_charge": 402, + "fireworkscharge": 402, + "fish": 349, + "fishing_rod": 346, + "fletching_table": -201, + "flint": 318, + "flint_and_steel": 259, + "flint_steel": 259, + "flower_pot": 390, + "flower_pot_block": 140, + "flowing_lava": 10, + "flowing_water": 8, + "frame": 389, + "frame_block": 199, + "frosted_ice": 207, + "furnace": 61, + "ghast_tear": 370, + "glass": 20, + "glass_bottle": 374, + "glass_pane": 102, + "glass_panel": 102, + "glistering_melon": 382, + "glow_stick": 166, + "glowing_obsidian": 246, + "glowing_redstone_ore": 74, + "glowingobsidian": 246, + "glowstone": 89, + "glowstone_block": 89, + "glowstone_dust": 348, + "gold_axe": 286, + "gold_block": 41, + "gold_boots": 317, + "gold_chestplate": 315, + "gold_helmet": 314, + "gold_hoe": 294, + "gold_horse_armor": 418, + "gold_ingot": 266, + "gold_leggings": 316, + "gold_nugget": 371, + "gold_ore": 14, + "gold_pickaxe": 285, + "gold_pressure_plate": 147, + "gold_shovel": 284, + "gold_sword": 283, + "golden_apple": 322, + "golden_axe": 286, + "golden_boots": 317, + "golden_carrot": 396, + "golden_chestplate": 315, + "golden_helmet": 314, + "golden_hoe": 294, + "golden_horse_armor": 418, + "golden_leggings": 316, + "golden_nugget": 371, + "golden_pickaxe": 285, + "golden_rail": 27, + "golden_shovel": 284, + "golden_sword": 283, + "granite_stairs": -169, + "grass": 2, + "grass_path": 198, + "gravel": 13, + "gray_glazed_terracotta": 227, + "green_glazed_terracotta": 233, + "grindstone": -195, + "gunpowder": 289, + "hard_glass": 253, + "hard_glass_pane": 190, + "hard_stained_glass": 254, + "hard_stained_glass_pane": 191, + "hardened_clay": 172, + "hay_bale": 170, + "hay_block": 170, + "heart_of_the_sea": 467, + "heavy_weighted_pressure_plate": 148, + "hopper": 410, + "hopper_block": 154, + "hopper_minecart": 408, + "horse_armor_diamond": 419, + "horse_armor_gold": 418, + "horse_armor_iron": 417, + "horse_armor_leather": 416, + "horsearmordiamond": 419, + "horsearmorgold": 418, + "horsearmoriron": 417, + "horsearmorleather": 416, + "ice": 79, + "ice_bomb": 453, + "inactive_redstone_lamp": 123, + "info_reserved6": 255, + "info_update": 248, + "info_update2": 249, + "inverted_daylight_sensor": 178, + "invisible_bedrock": 95, + "invisiblebedrock": 95, + "iron_axe": 258, + "iron_bar": 101, + "iron_bars": 101, + "iron_block": 42, + "iron_boots": 309, + "iron_chestplate": 307, + "iron_door": 330, + "iron_door_block": 71, + "iron_helmet": 306, + "iron_hoe": 292, + "iron_horse_armor": 417, + "iron_ingot": 265, + "iron_leggings": 308, + "iron_nugget": 452, + "iron_ore": 15, + "iron_pickaxe": 257, + "iron_pressure_plate": 148, + "iron_shovel": 256, + "iron_sword": 267, + "iron_trapdoor": 167, + "item_frame": 389, + "item_frame_block": 199, + "jack_o_lantern": 91, + "jigsaw": -211, + "jukebox": 84, + "jungle_button": -143, + "jungle_door": 429, + "jungle_door_block": 195, + "jungle_fence_gate": 185, + "jungle_pressure_plate": -153, + "jungle_sign": 474, + "jungle_stairs": 136, + "jungle_standing_sign": -188, + "jungle_trapdoor": -148, + "jungle_wall_sign": -189, + "kelp": 335, + "kelp_block": -138, + "ladder": 65, + "lantern": -208, + "lapis_block": 22, + "lapis_ore": 21, + "lava": 11, + "lava_cauldron": -210, + "lead": 420, + "leash": 420, + "leather": 334, + "leather_boots": 301, + "leather_cap": 298, + "leather_chestplate": 299, + "leather_helmet": 298, + "leather_horse_armor": 416, + "leather_leggings": 300, + "leather_pants": 300, + "leather_tunic": 299, + "leave": 18, + "leave2": 161, + "leaves": 18, + "leaves2": 161, + "lectern": -194, + "lever": 69, + "light_blue_glazed_terracotta": 223, + "light_weighted_pressure_plate": 147, + "lily_pad": 111, + "lime_glazed_terracotta": 225, + "lingering_potion": 441, + "lit_blast_furnace": -214, + "lit_furnace": 62, + "lit_pumpkin": 91, + "lit_redstone_lamp": 124, + "lit_redstone_ore": 74, + "lit_redstone_torch": 76, + "lit_smoker": -199, + "log": 17, + "log2": 162, + "loom": -204, + "magenta_glazed_terracotta": 222, + "magma": 213, + "magma_cream": 378, + "map": 395, + "medicine": 447, + "melon": 360, + "melon_block": 103, + "melon_seeds": 362, + "melon_slice": 360, + "melon_stem": 105, + "minecart": 328, + "minecart_with_chest": 342, + "minecart_with_command_block": 443, + "minecart_with_hopper": 408, + "minecart_with_tnt": 407, + "mob_head": 397, + "mob_head_block": 144, + "mob_spawner": 52, + "monster_egg": 97, + "monster_egg_block": 97, + "monster_spawner": 52, + "moss_stone": 48, + "mossy_cobblestone": 48, + "mossy_cobblestone_stairs": -179, + "mossy_stone": 48, + "mossy_stone_brick_stairs": -175, + "moving_block": 250, + "movingblock": 250, + "mushroom_stew": 282, + "mutton": 423, + "mutton_cooked": 424, + "mutton_raw": 423, + "muttoncooked": 424, + "muttonraw": 423, + "mycelium": 110, + "name_tag": 421, + "nametag": 421, + "nautilus_shell": 465, + "nether_brick": 405, + "nether_brick_block": 112, + "nether_brick_fence": 113, + "nether_brick_stairs": 114, + "nether_bricks": 112, + "nether_bricks_stairs": 114, + "nether_quartz": 406, + "nether_quartz_ore": 153, + "nether_reactor": 247, + "nether_star": 399, + "nether_wart": 372, + "nether_wart_block": 214, + "nether_wart_plant": 115, + "netherbrick": 405, + "netherrack": 87, + "netherreactor": 247, + "netherstar": 399, + "normal_stone_stairs": -180, + "note_block": 25, + "noteblock": 25, + "oak_door": 324, + "oak_door_block": 64, + "oak_fence_gate": 107, + "oak_stairs": 53, + "oak_wood_stairs": 53, + "oak_wooden_stairs": 53, + "observer": 251, + "obsidian": 49, + "orange_glazed_terracotta": 221, + "packed_ice": 174, + "painting": 321, + "paper": 339, + "phantom_membrane": 470, + "pink_glazed_terracotta": 226, + "piston": 33, + "piston_arm_collision": 34, + "piston_head": 34, + "pistonarmcollision": 34, + "plank": 5, + "planks": 5, + "podzol": 243, + "poisonous_potato": 394, + "polished_andesite_stairs": -174, + "polished_diorite_stairs": -173, + "polished_granite_stairs": -172, + "poppy": 38, + "porkchop": 319, + "portal": 90, + "portal_block": 90, + "potato": 392, + "potato_block": 142, + "potatoes": 142, + "potion": 373, + "powered_comparator": 150, + "powered_comparator_block": 150, + "powered_rail": 27, + "powered_repeater": 94, + "powered_repeater_block": 94, + "prismarine": 168, + "prismarine_bricks_stairs": -4, + "prismarine_crystals": 422, + "prismarine_shard": 409, + "prismarine_stairs": -2, + "puffer_fish": 462, + "pufferfish": 462, + "pumpkin": 86, + "pumpkin_pie": 400, + "pumpkin_seeds": 361, + "pumpkin_stem": 104, + "purple_glazed_terracotta": 219, + "purpur_block": 201, + "purpur_stairs": 203, + "quartz": 406, + "quartz_block": 155, + "quartz_ore": 153, + "quartz_stairs": 156, + "rabbit": 411, + "rabbit_foot": 414, + "rabbit_hide": 415, + "rabbit_stew": 413, + "rail": 66, + "rapid_fertilizer": 449, + "raw_beef": 363, + "raw_chicken": 365, + "raw_fish": 349, + "raw_mutton": 423, + "raw_porkchop": 319, + "raw_rabbit": 411, + "raw_salmon": 460, + "record_11": 510, + "record_13": 500, + "record_blocks": 502, + "record_cat": 501, + "record_chirp": 503, + "record_far": 504, + "record_mall": 505, + "record_mellohi": 506, + "record_stal": 507, + "record_strad": 508, + "record_wait": 511, + "record_ward": 509, + "red_flower": 38, + "red_glazed_terracotta": 234, + "red_mushroom": 40, + "red_mushroom_block": 100, + "red_nether_brick": 215, + "red_nether_brick_stairs": -184, + "red_sandstone": 179, + "red_sandstone_slab": 182, + "red_sandstone_stairs": 180, + "redstone": 331, + "redstone_block": 152, + "redstone_dust": 331, + "redstone_lamp": 123, + "redstone_ore": 73, + "redstone_torch": 76, + "redstone_wire": 55, + "reeds": 338, + "reeds_block": 83, + "repeater": 356, + "repeater_block": 93, + "repeating_command_block": 188, + "reserved6": 255, + "rose": 38, + "rotten_flesh": 367, + "saddle": 329, + "salmon": 460, + "sand": 12, + "sandstone": 24, + "sandstone_stairs": 128, + "sapling": 6, + "scaffolding": -165, + "sea_lantern": 169, + "sea_pickle": -156, + "seagrass": -130, + "sealantern": 169, + "seeds": 295, + "shears": 359, + "shield": 513, + "shulker_box": 218, + "shulker_shell": 445, + "sign": 323, + "sign_post": 63, + "silver_glazed_terracotta": 228, + "skull": 397, + "skull_block": 144, + "slab": 44, + "slabs": 44, + "slime": 165, + "slime_ball": 341, + "slime_block": 165, + "slimeball": 341, + "smithing_table": -202, + "smoker": -198, + "smooth_quartz_stairs": -185, + "smooth_red_sandstone_stairs": -176, + "smooth_sandstone_stairs": -177, + "smooth_stone": -183, + "snow": 80, + "snow_block": 80, + "snow_layer": 78, + "snowball": 332, + "soul_sand": 88, + "sparkler": 442, + "spawn_egg": 383, + "speckled_melon": 382, + "spider_eye": 375, + "splash_potion": 438, + "sponge": 19, + "spruce_button": -144, + "spruce_door": 427, + "spruce_door_block": 193, + "spruce_fence_gate": 183, + "spruce_pressure_plate": -154, + "spruce_sign": 472, + "spruce_stairs": 134, + "spruce_standing_sign": -181, + "spruce_trapdoor": -149, + "spruce_wall_sign": -182, + "spruce_wood_stairs": 134, + "spruce_wooden_stairs": 134, + "stained_clay": 159, + "stained_glass": 241, + "stained_glass_pane": 160, + "stained_hardened_clay": 159, + "standing_banner": 176, + "standing_sign": 63, + "steak": 364, + "stick": 280, + "sticks": 280, + "sticky_piston": 29, + "still_lava": 11, + "still_water": 9, + "stone": 1, + "stone_axe": 275, + "stone_brick": 98, + "stone_brick_stairs": 109, + "stone_bricks": 98, + "stone_button": 77, + "stone_hoe": 291, + "stone_pickaxe": 274, + "stone_pressure_plate": 70, + "stone_shovel": 273, + "stone_slab": 44, + "stone_slab2": 182, + "stone_slab3": -162, + "stone_slab4": -166, + "stone_stairs": 67, + "stone_sword": 272, + "stone_wall": 139, + "stonebrick": 98, + "stonecutter": 245, + "stonecutter_block": -197, + "string": 287, + "stripped_acacia_log": -8, + "stripped_birch_log": -6, + "stripped_dark_oak_log": -9, + "stripped_jungle_log": -7, + "stripped_oak_log": -10, + "stripped_spruce_log": -5, + "structure_block": 252, + "sugar": 353, + "sugar_cane": 338, + "sugar_canes": 338, + "sugarcane": 338, + "sugarcane_block": 83, + "sweet_berries": 477, + "sweet_berry_bush": -207, + "tall_grass": 31, + "tallgrass": 31, + "terracotta": 159, + "tnt": 46, + "tnt_minecart": 407, + "torch": 50, + "totem": 450, + "trapdoor": 96, + "trapped_chest": 146, + "trident": 455, + "trip_wire": 132, + "tripwire": 132, + "tripwire_hook": 131, + "trunk": 5, + "trunk2": 162, + "turtle_egg": -159, + "turtle_helmet": 469, + "turtle_shell_piece": 468, + "underwater_torch": 239, + "undyed_shulker_box": 205, + "unlit_redstone_torch": 75, + "unpowered_comparator": 149, + "unpowered_comparator_block": 149, + "unpowered_repeater": 93, + "unpowered_repeater_block": 93, + "update_block": 248, + "vine": 106, + "vines": 106, + "wall_banner": 177, + "wall_sign": 68, + "water": 9, + "water_lily": 111, + "waterlily": 111, + "web": 30, + "weighted_pressure_plate_heavy": 148, + "weighted_pressure_plate_light": 147, + "wheat": 296, + "wheat_block": 59, + "wheat_seeds": 295, + "white_glazed_terracotta": 220, + "wood": 17, + "wood2": 162, + "wood_door_block": 64, + "wood_slab": 158, + "wood_slabs": 158, + "wood_stairs": 53, + "wooden_axe": 271, + "wooden_button": 143, + "wooden_door": 324, + "wooden_door_block": 64, + "wooden_hoe": 290, + "wooden_pickaxe": 270, + "wooden_plank": 5, + "wooden_planks": 5, + "wooden_pressure_plate": 72, + "wooden_shovel": 269, + "wooden_slab": 158, + "wooden_slabs": 158, + "wooden_stairs": 53, + "wooden_sword": 268, + "wooden_trapdoor": 96, + "wool": 35, + "workbench": 58, + "writable_book": 386, + "written_book": 387, + "yellow_flower": 37, + "yellow_glazed_terracotta": 224 +} diff --git a/resources/plugin_list.yml b/resources/plugin_list.yml new file mode 100644 index 0000000000..e74f0221aa --- /dev/null +++ b/resources/plugin_list.yml @@ -0,0 +1,8 @@ +#This configuration file allows you to control which plugins are loaded on your server. + +#List behaviour +# - blacklist: Only plugins which ARE NOT listed will load. +# - whitelist: Only plugins which ARE listed will load. +mode: blacklist +#List names of plugins here. +plugins: [] diff --git a/src/pocketmine/resources/pocketmine.yml b/resources/pocketmine.yml similarity index 92% rename from src/pocketmine/resources/pocketmine.yml rename to resources/pocketmine.yml index f12769bdf7..2e8492624e 100644 --- a/src/pocketmine/resources/pocketmine.yml +++ b/resources/pocketmine.yml @@ -92,6 +92,9 @@ network: #Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be #fragmented or split into smaller parts. Clients can request MTU sizes up to but not more than this number. max-mtu-size: 1492 + #Enable encryption of Minecraft network traffic. This has an impact on performance, but prevents hackers from stealing sessions and pretending to be other players. + #DO NOT DISABLE THIS unless you understand the risks involved. + enable-encryption: true debug: #If > 1, it will show debug messages in the console @@ -103,13 +106,10 @@ player: #If true, checks that joining players' Xbox user ID (XUID) match what was previously recorded. #This also prevents non-XBL players using XBL players' usernames to steal their data on servers with xbox-auth=off. verify-xuid: true - anti-cheat: - #If false, will try to prevent speed and noclip cheats. May cause movement issues. - allow-movement-cheats: true level-settings: #The default format that worlds will use when created - default-format: pmanvil + default-format: leveldb chunk-sending: #To change server normal render distance, change view-distance in server.properties. @@ -123,8 +123,9 @@ chunk-ticking: per-tick: 40 #Radius of chunks around a player to tick tick-radius: 3 - light-updates: false - clear-tick-list: true + #Number of blocks inside ticking areas' subchunks that get ticked every tick. Higher values will accelerate events + #like tree and plant growth, but at a higher performance cost. + blocks-per-subchunk-per-tick: 3 #IDs of blocks not to perform random ticking on. disable-block-ticking: #- 2 # grass @@ -154,7 +155,6 @@ auto-updater: enabled: true on-update: warn-console: true - warn-ops: true #Can be development, alpha, beta or stable. preferred-channel: stable #If using a development version, it will suggest changing the channel @@ -180,7 +180,8 @@ worlds: #Example: #world: # seed: 404 - # generator: FLAT:2;7,59x1,3x3,2;1;decoration(treecount=80 grasscount=45) + # generator: FLAT + # preset: 2;bedrock,59xstone,3xdirt,grass;1 plugins: #Setting this to true will cause the legacy structure to be used where plugin data is placed inside the --plugins dir. diff --git a/src/pocketmine/resources/resource_packs.yml b/resources/resource_packs.yml similarity index 100% rename from src/pocketmine/resources/resource_packs.yml rename to resources/resource_packs.yml diff --git a/src/pocketmine/CoreConstants.php b/src/CoreConstants.php similarity index 71% rename from src/pocketmine/CoreConstants.php rename to src/CoreConstants.php index 091c438981..46e00a2a32 100644 --- a/src/pocketmine/CoreConstants.php +++ b/src/CoreConstants.php @@ -33,5 +33,8 @@ if(defined('pocketmine\_CORE_CONSTANTS_INCLUDED')){ } define('pocketmine\_CORE_CONSTANTS_INCLUDED', true); -define('pocketmine\PATH', dirname(__DIR__, 2) . '/'); -define('pocketmine\RESOURCE_PATH', __DIR__ . '/resources/'); +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/pocketmine/MemoryManager.php b/src/MemoryManager.php similarity index 54% rename from src/pocketmine/MemoryManager.php rename to src/MemoryManager.php index e7b56fb5f8..020532f82c 100644 --- a/src/pocketmine/MemoryManager.php +++ b/src/MemoryManager.php @@ -24,12 +24,14 @@ 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; use function arsort; use function count; use function fclose; @@ -43,9 +45,10 @@ use function gc_enable; use function gc_mem_caches; use function get_class; use function get_declared_classes; -use function implode; +use function get_defined_functions; use function ini_get; use function ini_set; +use function intdiv; use function is_array; use function is_object; use function is_resource; @@ -67,70 +70,54 @@ use const SORT_NUMERIC; class MemoryManager{ - /** @var Server */ - private $server; + private Server $server; - /** @var int */ - private $memoryLimit; - /** @var int */ - private $globalMemoryLimit; - /** @var int */ - private $checkRate; - /** @var int */ - private $checkTicker = 0; - /** @var bool */ - private $lowMemory = false; + private int $memoryLimit; + private int $globalMemoryLimit; + private int $checkRate; + private int $checkTicker = 0; + private bool $lowMemory = false; - /** @var bool */ - private $continuousTrigger = true; - /** @var int */ - private $continuousTriggerRate; - /** @var int */ - private $continuousTriggerCount = 0; - /** @var int */ - private $continuousTriggerTicker = 0; + private bool $continuousTrigger = true; + private int $continuousTriggerRate; + private int $continuousTriggerCount = 0; + private int $continuousTriggerTicker = 0; - /** @var int */ - private $garbageCollectionPeriod; - /** @var int */ - private $garbageCollectionTicker = 0; - /** @var bool */ - private $garbageCollectionTrigger; - /** @var bool */ - private $garbageCollectionAsync; + private int $garbageCollectionPeriod; + private int $garbageCollectionTicker = 0; + private bool $garbageCollectionTrigger; + private bool $garbageCollectionAsync; - /** @var int */ - private $lowMemChunkRadiusOverride; - /** @var bool */ - private $lowMemChunkGC; + private int $lowMemChunkRadiusOverride; + private bool $lowMemChunkGC; - /** @var bool */ - private $lowMemDisableChunkCache; - /** @var bool */ - private $lowMemClearWorldCache; + private bool $lowMemDisableChunkCache; + private bool $lowMemClearWorldCache; - /** @var bool */ - private $dumpWorkers = true; + private bool $dumpWorkers = true; + + private \Logger $logger; public function __construct(Server $server){ $this->server = $server; + $this->logger = new \PrefixedLogger($server->getLogger(), "Memory Manager"); - $this->init(); + $this->init($server->getConfigGroup()); } - private function init() : void{ - $this->memoryLimit = ((int) $this->server->getProperty("memory.main-limit", 0)) * 1024 * 1024; + private function init(ServerConfigGroup $config) : void{ + $this->memoryLimit = $config->getPropertyInt("memory.main-limit", 0) * 1024 * 1024; $defaultMemory = 1024; - if(preg_match("/([0-9]+)([KMGkmg])/", $this->server->getConfigString("memory-limit", ""), $matches) > 0){ + if(preg_match("/([0-9]+)([KMGkmg])/", $config->getConfigString("memory-limit", ""), $matches) > 0){ $m = (int) $matches[1]; if($m <= 0){ $defaultMemory = 0; }else{ switch(mb_strtoupper($matches[2])){ case "K": - $defaultMemory = $m / 1024; + $defaultMemory = intdiv($m, 1024); break; case "M": $defaultMemory = $m; @@ -145,7 +132,7 @@ class MemoryManager{ } } - $hardLimit = ((int) $this->server->getProperty("memory.main-hard-limit", $defaultMemory)); + $hardLimit = $config->getPropertyInt("memory.main-hard-limit", $defaultMemory); if($hardLimit <= 0){ ini_set("memory_limit", '-1'); @@ -153,22 +140,22 @@ class MemoryManager{ ini_set("memory_limit", $hardLimit . "M"); } - $this->globalMemoryLimit = ((int) $this->server->getProperty("memory.global-limit", 0)) * 1024 * 1024; - $this->checkRate = (int) $this->server->getProperty("memory.check-rate", 20); - $this->continuousTrigger = (bool) $this->server->getProperty("memory.continuous-trigger", true); - $this->continuousTriggerRate = (int) $this->server->getProperty("memory.continuous-trigger-rate", 30); + $this->globalMemoryLimit = $config->getPropertyInt("memory.global-limit", 0) * 1024 * 1024; + $this->checkRate = $config->getPropertyInt("memory.check-rate", 20); + $this->continuousTrigger = $config->getPropertyBool("memory.continuous-trigger", true); + $this->continuousTriggerRate = $config->getPropertyInt("memory.continuous-trigger-rate", 30); - $this->garbageCollectionPeriod = (int) $this->server->getProperty("memory.garbage-collection.period", 36000); - $this->garbageCollectionTrigger = (bool) $this->server->getProperty("memory.garbage-collection.low-memory-trigger", true); - $this->garbageCollectionAsync = (bool) $this->server->getProperty("memory.garbage-collection.collect-async-worker", true); + $this->garbageCollectionPeriod = $config->getPropertyInt("memory.garbage-collection.period", 36000); + $this->garbageCollectionTrigger = $config->getPropertyBool("memory.garbage-collection.low-memory-trigger", true); + $this->garbageCollectionAsync = $config->getPropertyBool("memory.garbage-collection.collect-async-worker", true); - $this->lowMemChunkRadiusOverride = (int) $this->server->getProperty("memory.max-chunks.chunk-radius", 4); - $this->lowMemChunkGC = (bool) $this->server->getProperty("memory.max-chunks.trigger-chunk-collect", true); + $this->lowMemChunkRadiusOverride = $config->getPropertyInt("memory.max-chunks.chunk-radius", 4); + $this->lowMemChunkGC = $config->getPropertyBool("memory.max-chunks.trigger-chunk-collect", true); - $this->lowMemDisableChunkCache = (bool) $this->server->getProperty("memory.world-caches.disable-chunk-cache", true); - $this->lowMemClearWorldCache = (bool) $this->server->getProperty("memory.world-caches.low-memory-trigger", true); + $this->lowMemDisableChunkCache = $config->getPropertyBool("memory.world-caches.disable-chunk-cache", true); + $this->lowMemClearWorldCache = $config->getPropertyBool("memory.world-caches.low-memory-trigger", true); - $this->dumpWorkers = (bool) $this->server->getProperty("memory.memory-dump.dump-async-worker", true); + $this->dumpWorkers = $config->getPropertyBool("memory.memory-dump.dump-async-worker", true); gc_enable(); } @@ -176,6 +163,10 @@ class MemoryManager{ return $this->lowMemory; } + public function getGlobalMemoryLimit() : int{ + return $this->globalMemoryLimit; + } + public function canUseChunkCache() : bool{ return !$this->lowMemory or !$this->lowMemDisableChunkCache; } @@ -189,21 +180,20 @@ class MemoryManager{ /** * Triggers garbage collection and cache cleanup to try and free memory. - * - * @return void */ - public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0){ - $this->server->getLogger()->debug(sprintf("[Memory Manager] %sLow memory triggered, limit %gMB, using %gMB", + public function trigger(int $memory, int $limit, bool $global = false, int $triggerCount = 0) : void{ + $this->logger->debug(sprintf("%sLow memory triggered, limit %gMB, using %gMB", $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2))); if($this->lowMemClearWorldCache){ - foreach($this->server->getLevels() as $level){ - $level->clearCache(true); + foreach($this->server->getWorldManager()->getWorlds() as $world){ + $world->clearCache(true); } + ChunkCache::pruneCaches(); } if($this->lowMemChunkGC){ - foreach($this->server->getLevels() as $level){ - $level->doChunkGarbageCollection(); + foreach($this->server->getWorldManager()->getWorlds() as $world){ + $world->doChunkGarbageCollection(); } } @@ -215,16 +205,14 @@ class MemoryManager{ $cycles = $this->triggerGarbageCollector(); } - $this->server->getLogger()->debug(sprintf("[Memory Manager] Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); + $this->logger->debug(sprintf("Freed %gMB, $cycles cycles", round(($ev->getMemoryFreed() / 1024) / 1024, 2))); } /** * Called every tick to update the memory manager state. - * - * @return void */ - public function check(){ - Timings::$memoryManagerTimer->startTiming(); + public function check() : void{ + Timings::$memoryManager->startTiming(); if(($this->memoryLimit > 0 or $this->globalMemoryLimit > 0) and ++$this->checkTicker >= $this->checkRate){ $this->checkTicker = 0; @@ -257,16 +245,16 @@ class MemoryManager{ $this->triggerGarbageCollector(); } - Timings::$memoryManagerTimer->stopTiming(); + Timings::$memoryManager->stopTiming(); } public function triggerGarbageCollector() : int{ - Timings::$garbageCollectorTimer->startTiming(); + Timings::$garbageCollector->startTiming(); if($this->garbageCollectionAsync){ $pool = $this->server->getAsyncPool(); if(($w = $pool->shutdownUnusedWorkers()) > 0){ - $this->server->getLogger()->debug("Shut down $w idle async pool workers"); + $this->logger->debug("Shut down $w idle async pool workers"); } foreach($pool->getRunningWorkers() as $i){ $pool->submitTaskToWorker(new GarbageCollectionTask(), $i); @@ -276,19 +264,18 @@ class MemoryManager{ $cycles = gc_collect_cycles(); gc_mem_caches(); - Timings::$garbageCollectorTimer->stopTiming(); + Timings::$garbageCollector->stopTiming(); return $cycles; } /** * Dumps the server memory into the specified output folder. - * - * @return void */ - public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize){ - $this->server->getLogger()->notice("[Dump] After the memory dump is done, the server might crash"); - self::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $this->server->getLogger()); + public function dumpServerMemory(string $outputFolder, int $maxNesting, int $maxStringSize) : void{ + $logger = new \PrefixedLogger($this->server->getLogger(), "Memory Dump"); + $logger->notice("After the memory dump is done, the server might crash"); + self::dumpMemory($this->server, $outputFolder, $maxNesting, $maxStringSize, $logger); if($this->dumpWorkers){ $pool = $this->server->getAsyncPool(); @@ -302,11 +289,8 @@ class MemoryManager{ * Static memory dumper accessible from any thread. * * @param mixed $startingObject - * - * @return void - * @throws \ReflectionException */ - public static function dumpMemory($startingObject, string $outputFolder, int $maxNesting, int $maxStringSize, \Logger $logger){ + 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"); ini_set('memory_limit', '-1'); @@ -316,9 +300,7 @@ class MemoryManager{ mkdir($outputFolder, 0777, true); } - $obData = fopen($outputFolder . "/objects.js", "wb+"); - - $data = []; + $obData = fopen(Path::join($outputFolder, "objects.js"), "wb+"); $objects = []; @@ -329,6 +311,9 @@ class MemoryManager{ $staticProperties = []; $staticCount = 0; + $functionStaticVars = []; + $functionStaticVarsCount = 0; + foreach(get_declared_classes() as $className){ $reflection = new \ReflectionClass($className); $staticProperties[$className] = []; @@ -348,10 +333,24 @@ class MemoryManager{ if(count($staticProperties[$className]) === 0){ unset($staticProperties[$className]); } + + foreach($reflection->getMethods() as $method){ + if($method->getDeclaringClass()->getName() !== $reflection->getName()){ + continue; + } + $methodStatics = []; + foreach($method->getStaticVariables() as $name => $variable){ + $methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + if(count($methodStatics) > 0){ + $functionStaticVars[$className . "::" . $method->getName()] = $methodStatics; + $functionStaticVarsCount += count($functionStaticVars); + } + } } - file_put_contents($outputFolder . "/staticProperties.js", json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - $logger->info("[Dump] Wrote $staticCount static properties"); + file_put_contents(Path::join($outputFolder, "staticProperties.js"), json_encode($staticProperties, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + $logger->info("Wrote $staticCount static properties"); $globalVariables = []; $globalCount = 0; @@ -368,7 +367,7 @@ class MemoryManager{ '_SESSION' => true ]; - foreach($GLOBALS as $varName => $value){ + foreach(Utils::stringifyKeys($GLOBALS) as $varName => $value){ if(isset($ignoredGlobals[$varName])){ continue; } @@ -377,8 +376,23 @@ class MemoryManager{ $globalVariables[$varName] = self::continueDump($value, $objects, $refCounts, 0, $maxNesting, $maxStringSize); } - file_put_contents($outputFolder . "/globalVariables.js", json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - $logger->info("[Dump] Wrote $globalCount global variables"); + file_put_contents(Path::join($outputFolder, "globalVariables.js"), json_encode($globalVariables, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + $logger->info("Wrote $globalCount global variables"); + + foreach(get_defined_functions()["user"] as $function){ + $reflect = new \ReflectionFunction($function); + + $vars = []; + foreach($reflect->getStaticVariables() as $varName => $variable){ + $vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + if(count($vars) > 0){ + $functionStaticVars[$function] = $vars; + $functionStaticVarsCount += count($vars); + } + } + file_put_contents(Path::join($outputFolder, 'functionStaticVars.js'), json_encode($functionStaticVars, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + $logger->info("Wrote $functionStaticVarsCount function static variables"); $data = self::continueDump($startingObject, $objects, $refCounts, 0, $maxNesting, $maxStringSize); @@ -398,60 +412,64 @@ class MemoryManager{ } $objects[$hash] = true; - - $reflection = new \ReflectionObject($object); - $info = [ "information" => "$hash@$className", - "properties" => [] ]; + if($object instanceof \Closure){ + $info["definition"] = Utils::getNiceClosureName($object); + $info["referencedVars"] = []; + $reflect = new \ReflectionFunction($object); + if(($closureThis = $reflect->getClosureThis()) !== null){ + $info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } - if(($parent = $reflection->getParentClass()) !== false){ - $info["parent"] = $parent->getName(); - } + foreach($reflect->getStaticVariables() as $name => $variable){ + $info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } + }else{ + $reflection = new \ReflectionObject($object); - if(count($reflection->getInterfaceNames()) > 0){ - $info["implements"] = implode(", ", $reflection->getInterfaceNames()); - } + $info["properties"] = []; - for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){ - foreach($reflection->getProperties() as $property){ - if($property->isStatic()){ - continue; - } - - $name = $property->getName(); - if($reflection !== $original){ - if($property->isPrivate()){ - $name = $reflection->getName() . ":" . $name; - }else{ + for($original = $reflection; $reflection !== false; $reflection = $reflection->getParentClass()){ + foreach($reflection->getProperties() as $property){ + if($property->isStatic()){ continue; } - } - if(!$property->isPublic()){ - $property->setAccessible(true); - } - $info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize); + $name = $property->getName(); + if($reflection !== $original){ + if($property->isPrivate()){ + $name = $reflection->getName() . ":" . $name; + }else{ + continue; + } + } + if(!$property->isPublic()){ + $property->setAccessible(true); + } + + $info["properties"][$name] = self::continueDump($property->getValue($object), $objects, $refCounts, 0, $maxNesting, $maxStringSize); + } } } - fwrite($obData, "$hash@$className: " . json_encode($info, JSON_UNESCAPED_SLASHES) . "\n"); + fwrite($obData, json_encode($info, JSON_UNESCAPED_SLASHES) . "\n"); } }while($continue); - $logger->info("[Dump] Wrote " . count($objects) . " objects"); + $logger->info("Wrote " . count($objects) . " objects"); fclose($obData); - file_put_contents($outputFolder . "/serverEntry.js", json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - file_put_contents($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)); + file_put_contents(Path::join($outputFolder, "referenceCounts.js"), json_encode($refCounts, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); arsort($instanceCounts, SORT_NUMERIC); - file_put_contents($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)); - $logger->info("[Dump] Finished!"); + $logger->info("Finished!"); ini_set('memory_limit', $hardLimit); gc_enable(); @@ -479,7 +497,7 @@ class MemoryManager{ ++$refCounts[$hash]; - $data = "(object) $hash@" . get_class($from); + $data = "(object) $hash"; }elseif(is_array($from)){ if($recursion >= 5){ return "(error) ARRAY RECURSION LIMIT REACHED"; diff --git a/src/pocketmine/PocketMine.php b/src/PocketMine.php similarity index 67% rename from src/pocketmine/PocketMine.php rename to src/PocketMine.php index 2c6c1fdd87..e251fd614a 100644 --- a/src/pocketmine/PocketMine.php +++ b/src/PocketMine.php @@ -24,15 +24,23 @@ declare(strict_types=1); namespace pocketmine { use Composer\InstalledVersions; - use pocketmine\utils\Git; + use pocketmine\errorhandler\ErrorToExceptionHandler; + use pocketmine\thread\ThreadManager; + use pocketmine\utils\Filesystem; use pocketmine\utils\MainLogger; use pocketmine\utils\Process; use pocketmine\utils\ServerKiller; use pocketmine\utils\Terminal; use pocketmine\utils\Timezone; - use pocketmine\utils\Utils; - use pocketmine\utils\VersionString; use pocketmine\wizard\SetupWizard; + use Webmozart\PathUtil\Path; + use function defined; + use function extension_loaded; + use function phpversion; + use function preg_match; + use function preg_quote; + use function strpos; + use function version_compare; require_once __DIR__ . '/VersionInfo.php'; @@ -76,11 +84,16 @@ namespace pocketmine { $extensions = [ "chunkutils2" => "PocketMine ChunkUtils v2", "curl" => "cURL", + "crypto" => "php-crypto", "ctype" => "ctype", "date" => "Date", + "gmp" => "GMP", "hash" => "Hash", + "igbinary" => "igbinary", "json" => "JSON", + "leveldb" => "LevelDB", "mbstring" => "Multibyte String", + "morton" => "morton", "openssl" => "OpenSSL", "pcre" => "PCRE", "phar" => "Phar", @@ -114,15 +127,18 @@ namespace pocketmine { 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"); $wantedVersionLock = "0.3"; $wantedVersionMin = "$wantedVersionLock.0"; if($chunkutils2_version !== false && ( - version_compare($chunkutils2_version, $wantedVersionMin) < 0 || - preg_match("/^" . preg_quote($wantedVersionLock, "/") . "\.\d+(?:-dev)?$/", $chunkutils2_version) === 0 //lock in at ^0.2, optionally at a patch release - )){ + version_compare($chunkutils2_version, $wantedVersionMin) < 0 || + preg_match("/^" . preg_quote($wantedVersionLock, "/") . "\.\d+(?:-dev)?$/", $chunkutils2_version) === 0 //lock in at ^0.2, optionally at a patch release + )){ $messages[] = "chunkutils2 ^$wantedVersionMin is required, while you have $chunkutils2_version."; } @@ -130,11 +146,14 @@ 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; } /** - * @param \Logger $logger * @return void */ function emit_performance_warnings(\Logger $logger){ @@ -144,9 +163,6 @@ namespace pocketmine { if(extension_loaded("xdebug")){ $logger->warning("Xdebug extension is enabled. This has a major impact on performance."); } - if(!extension_loaded("pocketmine_chunkutils")){ - $logger->warning("ChunkUtils extension is missing. Anvil-format worlds will experience degraded performance."); - } if(((int) ini_get('zend.assertions')) !== -1){ $logger->warning("Debugging assertions are enabled. This may degrade performance. To disable them, set `zend.assertions = -1` in php.ini."); } @@ -206,48 +222,18 @@ 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__, 3) . '/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); - - set_error_handler([Utils::class, 'errorExceptionHandler']); - - $gitHash = str_repeat("00", 20); - $buildNumber = 0; - - if(\Phar::running(true) === ""){ - $gitHash = Git::getRepositoryStatePretty(\pocketmine\PATH); - }else{ - $phar = new \Phar(\Phar::running(false)); - $meta = $phar->getMetadata(); - if(isset($meta["git"])){ - $gitHash = $meta["git"]; - } - if(isset($meta["build"]) && is_int($meta["build"])){ - $buildNumber = $meta["build"]; - } - } - - define('pocketmine\GIT_COMMIT', $gitHash); - define('pocketmine\BUILD_NUMBER', $buildNumber); - - $version = new VersionString(\pocketmine\BASE_VERSION, \pocketmine\IS_DEVELOPMENT_BUILD, \pocketmine\BUILD_NUMBER); - define('pocketmine\VERSION', $version->getFullVersion(true)); + require_once($bootstrap); $composerGitHash = InstalledVersions::getReference('pocketmine/pocketmine-mp'); if($composerGitHash !== null){ - $currentGitHash = explode("-", \pocketmine\GIT_COMMIT)[0]; + //we can't verify dependency versions if we were installed without using git + $currentGitHash = explode("-", VersionInfo::GIT_HASH())[0]; if($currentGitHash !== $composerGitHash){ critical_error("Composer dependencies and/or autoloader are out of sync."); critical_error("- Current revision is $currentGitHash"); @@ -257,38 +243,31 @@ JIT_WARNING exit(1); } } + if(extension_loaded('parallel')){ + \parallel\bootstrap(\pocketmine\COMPOSER_AUTOLOADER_PATH); + } + + ErrorToExceptionHandler::set(); $opts = getopt("", ["data:", "plugins:", "no-wizard", "enable-ansi", "disable-ansi"]); - define('pocketmine\DATA', isset($opts["data"]) ? $opts["data"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR); - define('pocketmine\PLUGIN_PATH', isset($opts["plugins"]) ? $opts["plugins"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR . "plugins" . DIRECTORY_SEPARATOR); + $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; + Filesystem::addCleanedPath($pluginPath, Filesystem::CLEAN_PATH_PLUGINS_PREFIX); - if(!file_exists(\pocketmine\DATA)){ - mkdir(\pocketmine\DATA, 0777, true); + if(!file_exists($dataPath)){ + mkdir($dataPath, 0777, true); } - $lockFile = fopen(\pocketmine\DATA . 'server.lock', "a+b"); - if($lockFile === false){ - critical_error("Unable to open server.lock file. Please check that the current user has read/write permissions to it."); - exit(1); - } - define('pocketmine\LOCK_FILE', $lockFile); - if(!flock(\pocketmine\LOCK_FILE, LOCK_EX | LOCK_NB)){ - //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(\pocketmine\LOCK_FILE, LOCK_SH); - $pid = stream_get_contents(\pocketmine\LOCK_FILE); - critical_error("Another " . \pocketmine\NAME . " instance (PID $pid) is already using this folder (" . realpath(\pocketmine\DATA) . ")."); + $lockFilePath = Path::join($dataPath, 'server.lock'); + if(($pid = Filesystem::createLockFile($lockFilePath)) !== null){ + critical_error("Another " . VersionInfo::NAME . " instance (PID $pid) is already using this folder (" . realpath($dataPath) . ")."); critical_error("Please stop the other server first before running a new one."); exit(1); } - ftruncate(\pocketmine\LOCK_FILE, 0); - fwrite(\pocketmine\LOCK_FILE, (string) getmypid()); - fflush(\pocketmine\LOCK_FILE); - flock(\pocketmine\LOCK_FILE, LOCK_SH); //prevent acquiring an exclusive lock from another process, but allow reading //Logger has a dependency on timezone - $tzError = Timezone::init(); + Timezone::init(); if(isset($opts["enable-ansi"])){ Terminal::init(true); @@ -298,36 +277,28 @@ JIT_WARNING Terminal::init(); } - $logger = new MainLogger(\pocketmine\DATA . "server.log"); - $logger->registerStatic(); - - foreach($tzError as $e){ - $logger->warning($e); - } - unset($tzError); + $logger = new MainLogger(Path::join($dataPath, "server.log"), Terminal::hasFormattingCodes(), "Server", new \DateTimeZone(Timezone::get())); + \GlobalLogger::set($logger); emit_performance_warnings($logger); $exitCode = 0; do{ - if(!file_exists(\pocketmine\DATA . "server.properties") and !isset($opts["no-wizard"])){ - $installer = new SetupWizard(); + if(!file_exists(Path::join($dataPath, "server.properties")) and !isset($opts["no-wizard"])){ + $installer = new SetupWizard($dataPath); if(!$installer->run()){ $exitCode = -1; break; } } - //TODO: move this to a Server field - define('pocketmine\START_TIME', microtime(true)); - /* * We now use the Composer autoloader, but this autoloader is still for loading plugins. */ $autoloader = new \BaseClassLoader(); $autoloader->register(false); - new Server($autoloader, $logger, \pocketmine\DATA, \pocketmine\PLUGIN_PATH); + new Server($autoloader, $logger, $dataPath, $pluginPath); $logger->info("Stopping other threads"); @@ -337,22 +308,15 @@ 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); - $logger->shutdown(); - $logger->join(); + $logger->shutdownLogWriterThread(); echo Terminal::$FORMAT_RESET . PHP_EOL; - if(!flock(\pocketmine\LOCK_FILE, LOCK_UN)){ - critical_error("Failed to release the server.lock file."); - } - - if(!fclose(\pocketmine\LOCK_FILE)){ - critical_error("Could not close server.lock resource."); - } + Filesystem::releaseLockFile($lockFilePath); exit($exitCode); } diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 0000000000..f10a6b6891 --- /dev/null +++ b/src/Server.php @@ -0,0 +1,1816 @@ + + */ + private array $uniquePlayers = []; + + private QueryInfo $queryInfo; + + private ServerConfigGroup $configGroup; + + /** @var Player[] */ + private array $playerList = []; + + private SignalHandler $signalHandler; + + /** + * @var CommandSender[][] + * @phpstan-var array> + */ + private array $broadcastSubscribers = []; + + public function getName() : string{ + return VersionInfo::NAME; + } + + public function isRunning() : bool{ + return $this->isRunning; + } + + public function getPocketMineVersion() : string{ + return VersionInfo::VERSION()->getFullVersion(true); + } + + public function getVersion() : string{ + return ProtocolInfo::MINECRAFT_VERSION; + } + + public function getApiVersion() : string{ + return VersionInfo::BASE_VERSION; + } + + public function getFilePath() : string{ + return \pocketmine\PATH; + } + + public function getResourcePath() : string{ + return \pocketmine\RESOURCE_PATH; + } + + public function getDataPath() : string{ + return $this->dataPath; + } + + public function getPluginPath() : string{ + return $this->pluginPath; + } + + public function getMaxPlayers() : int{ + return $this->maxPlayers; + } + + /** + * Returns whether the server requires that players be authenticated to Xbox Live. If true, connecting players who + * are not logged into Xbox Live will be disconnected. + */ + public function getOnlineMode() : bool{ + return $this->onlineMode; + } + + /** + * Alias of {@link #getOnlineMode()}. + */ + public function requiresAuthentication() : bool{ + return $this->getOnlineMode(); + } + + public function getPort() : int{ + return $this->configGroup->getConfigInt("server-port", 19132); + } + + public function getPortV6() : int{ + return $this->configGroup->getConfigInt("server-portv6", 19133); + } + + public function getViewDistance() : int{ + return max(2, $this->configGroup->getConfigInt("view-distance", 8)); + } + + /** + * Returns a view distance up to the currently-allowed limit. + */ + public function getAllowedViewDistance(int $distance) : int{ + return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance()))); + } + + public function getIp() : string{ + $str = $this->configGroup->getConfigString("server-ip"); + 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(); + } + + public function getForceGamemode() : bool{ + return $this->configGroup->getConfigBool("force-gamemode", false); + } + + /** + * Returns Server global difficulty. Note that this may be overridden in individual worlds. + */ + public function getDifficulty() : int{ + return $this->configGroup->getConfigInt("difficulty", World::DIFFICULTY_NORMAL); + } + + public function hasWhitelist() : bool{ + return $this->configGroup->getConfigBool("white-list", false); + } + + public function isHardcore() : bool{ + return $this->configGroup->getConfigBool("hardcore", false); + } + + public function getMotd() : string{ + return $this->configGroup->getConfigString("motd", VersionInfo::NAME . " Server"); + } + + public function getLoader() : \DynamicClassLoader{ + return $this->autoloader; + } + + public function getLogger() : \AttachableThreadedLogger{ + return $this->logger; + } + + public function getUpdater() : UpdateChecker{ + return $this->updater; + } + + public function getPluginManager() : PluginManager{ + return $this->pluginManager; + } + + public function getCraftingManager() : CraftingManager{ + return $this->craftingManager; + } + + public function getResourcePackManager() : ResourcePackManager{ + return $this->resourceManager; + } + + public function getWorldManager() : WorldManager{ + return $this->worldManager; + } + + public function getAsyncPool() : AsyncPool{ + return $this->asyncPool; + } + + public function getTick() : int{ + return $this->tickCounter; + } + + /** + * Returns the last server TPS measure + */ + public function getTicksPerSecond() : float{ + return round($this->currentTPS, 2); + } + + /** + * Returns the last server TPS average measure + */ + public function getTicksPerSecondAverage() : float{ + return round(array_sum($this->tickAverage) / count($this->tickAverage), 2); + } + + /** + * Returns the TPS usage/load in % + */ + public function getTickUsage() : float{ + return round($this->currentUse * 100, 2); + } + + /** + * Returns the TPS usage/load average in % + */ + public function getTickUsageAverage() : float{ + return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2); + } + + public function getStartTime() : float{ + return $this->startTime; + } + + public function getCommandMap() : SimpleCommandMap{ + return $this->commandMap; + } + + /** + * @return Player[] + */ + public function getOnlinePlayers() : array{ + return $this->playerList; + } + + public function shouldSavePlayerData() : bool{ + return $this->configGroup->getPropertyBool("player.save-player-data", true); + } + + /** + * @return OfflinePlayer|Player + */ + public function getOfflinePlayer(string $name){ + $name = strtolower($name); + $result = $this->getPlayerExact($name); + + if($result === null){ + $result = new OfflinePlayer($name, $this->getOfflinePlayerData($name)); + } + + return $result; + } + + private function getPlayerDataPath(string $username) : string{ + return Path::join($this->getDataPath(), 'players', strtolower($username) . '.dat'); + } + + /** + * Returns whether the server has stored any saved data for this player. + */ + public function hasOfflinePlayerData(string $name) : bool{ + return file_exists($this->getPlayerDataPath($name)); + } + + private function handleCorruptedPlayerData(string $name) : void{ + $path = $this->getPlayerDataPath($name); + rename($path, $path . '.bak'); + $this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name))); + } + + public function getOfflinePlayerData(string $name) : ?CompoundTag{ + return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{ + $name = strtolower($name); + $path = $this->getPlayerDataPath($name); + + if(file_exists($path)){ + $contents = @file_get_contents($path); + if($contents === false){ + throw new \RuntimeException("Failed to read player data file \"$path\" (permission denied?)"); + } + $decompressed = @zlib_decode($contents); + if($decompressed === false){ + $this->logger->debug("Failed to decompress raw player data for \"$name\""); + $this->handleCorruptedPlayerData($name); + return null; + } + + try{ + return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag(); + }catch(NbtDataException $e){ //corrupt data + $this->logger->debug("Failed to decode NBT data for \"$name\": " . $e->getMessage()); + $this->handleCorruptedPlayerData($name); + return null; + } + } + return null; + }); + } + + public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{ + $ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name)); + if(!$this->shouldSavePlayerData()){ + $ev->cancel(); + } + + $ev->call(); + + if(!$ev->isCancelled()){ + Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{ + $nbt = new BigEndianNbtSerializer(); + try{ + file_put_contents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP)); + }catch(\ErrorException $e){ + $this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage()))); + $this->logger->logException($e); + } + }); + } + } + + /** + * @phpstan-return Promise + */ + public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{ + $ev = new PlayerCreationEvent($session); + $ev->call(); + $class = $ev->getPlayerClass(); + + if($offlinePlayerData !== null and ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString("Level", ""))) !== null){ + $playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world); + $spawn = $playerPos->asVector3(); + }else{ + $world = $this->worldManager->getDefaultWorld(); + if($world === null){ + throw new AssumptionFailedError("Default world should always be loaded"); + } + $playerPos = null; + $spawn = $world->getSpawnLocation(); + } + $playerPromiseResolver = new PromiseResolver(); + $world->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion( + function() use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{ + if(!$session->isConnected()){ + $playerPromiseResolver->reject(); + return; + } + + /* Stick with the original spawn at the time of generation request, even if it changed since then. + * This is because we know for sure that that chunk will be generated, but the one at the new location + * might not be, and it would be much more complex to go back and redo the whole thing. + * + * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the + * provided position. If this assumption is broken, we'll start seeing crashes in here. + */ + + /** + * @see Player::__construct() + * @var Player $player + */ + $player = new $class($this, $session, $playerInfo, $authenticated, $playerPos ?? Location::fromObject($world->getSafeSpawn($spawn), $world), $offlinePlayerData); + 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 + } + $playerPromiseResolver->resolve($player); + }, + static function() use ($playerPromiseResolver, $session) : void{ + if($session->isConnected()){ + $session->disconnect("Spawn terrain generation failed"); + } + $playerPromiseResolver->reject(); + } + ); + return $playerPromiseResolver->getPromise(); + } + + /** + * Returns an online player whose name begins with or equals the given string (case insensitive). + * The closest match will be returned, or null if there are no online matches. + * + * @see Server::getPlayerExact() + */ + public function getPlayerByPrefix(string $name) : ?Player{ + $found = null; + $name = strtolower($name); + $delta = PHP_INT_MAX; + foreach($this->getOnlinePlayers() as $player){ + if(stripos($player->getName(), $name) === 0){ + $curDelta = strlen($player->getName()) - strlen($name); + if($curDelta < $delta){ + $found = $player; + $delta = $curDelta; + } + if($curDelta === 0){ + break; + } + } + } + + return $found; + } + + /** + * Returns an online player with the given name (case insensitive), or null if not found. + */ + public function getPlayerExact(string $name) : ?Player{ + $name = strtolower($name); + foreach($this->getOnlinePlayers() as $player){ + if(strtolower($player->getName()) === $name){ + return $player; + } + } + + return null; + } + + /** + * Returns the player online with the specified raw UUID, or null if not found + */ + public function getPlayerByRawUUID(string $rawUUID) : ?Player{ + return $this->playerList[$rawUUID] ?? null; + } + + /** + * Returns the player online with a UUID equivalent to the specified UuidInterface object, or null if not found + */ + public function getPlayerByUUID(UuidInterface $uuid) : ?Player{ + return $this->getPlayerByRawUUID($uuid->getBytes()); + } + + public function getConfigGroup() : ServerConfigGroup{ + return $this->configGroup; + } + + /** + * @return Command|PluginOwned|null + * @phpstan-return (Command&PluginOwned)|null + */ + public function getPluginCommand(string $name){ + if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){ + return $command; + }else{ + return null; + } + } + + public function getNameBans() : BanList{ + return $this->banByName; + } + + public function getIPBans() : BanList{ + return $this->banByIP; + } + + public function addOp(string $name) : void{ + $this->operators->set(strtolower($name), true); + + if(($player = $this->getPlayerExact($name)) !== null){ + $player->setBasePermission(DefaultPermissions::ROOT_OPERATOR, true); + } + $this->operators->save(); + } + + public function removeOp(string $name) : void{ + $this->operators->remove(strtolower($name)); + + if(($player = $this->getPlayerExact($name)) !== null){ + $player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR); + } + $this->operators->save(); + } + + public function addWhitelist(string $name) : void{ + $this->whitelist->set(strtolower($name), true); + $this->whitelist->save(); + } + + public function removeWhitelist(string $name) : void{ + $this->whitelist->remove(strtolower($name)); + $this->whitelist->save(); + } + + public function isWhitelisted(string $name) : bool{ + return !$this->hasWhitelist() or $this->operators->exists($name, true) or $this->whitelist->exists($name, true); + } + + public function isOp(string $name) : bool{ + return $this->operators->exists($name, true); + } + + public function getWhitelisted() : Config{ + return $this->whitelist; + } + + public function getOps() : Config{ + return $this->operators; + } + + /** + * @return string[][] + */ + public function getCommandAliases() : array{ + $section = $this->configGroup->getProperty("aliases"); + $result = []; + if(is_array($section)){ + foreach($section as $key => $value){ + $commands = []; + if(is_array($value)){ + $commands = $value; + }else{ + $commands[] = (string) $value; + } + + $result[$key] = $commands; + } + } + + return $result; + } + + public static function getInstance() : Server{ + if(self::$instance === null){ + throw new \RuntimeException("Attempt to retrieve Server instance outside server thread"); + } + return self::$instance; + } + + public function __construct(\DynamicClassLoader $autoloader, \AttachableThreadedLogger $logger, string $dataPath, string $pluginPath){ + if(self::$instance !== null){ + throw new \LogicException("Only one server instance can exist at once"); + } + self::$instance = $this; + $this->startTime = microtime(true); + + $this->tickSleeper = new SleeperHandler(); + $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, + $pluginPath, + Path::join($dataPath, "worlds"), + Path::join($dataPath, "players") + ] as $neededPath){ + if(!file_exists($neededPath)){ + mkdir($neededPath, 0777); + } + } + + $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR; + $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR; + + $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")); + if(VersionInfo::IS_DEVELOPMENT_BUILD){ + $content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content); + } + @file_put_contents($pocketmineYmlPath, $content); + } + + $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, + "server-portv6" => 19133, + "enable-ipv6" => true, + "white-list" => false, + "max-players" => 20, + "gamemode" => 0, + "force-gamemode" => false, + "hardcore" => false, + "pvp" => true, + "difficulty" => World::DIFFICULTY_NORMAL, + "generator-settings" => "", + "level-name" => "world", + "level-seed" => "", + "level-type" => "DEFAULT", + "enable-query" => true, + "auto-save" => true, + "view-distance" => 8, + "xbox-auth" => true, + "language" => "eng" + ]) + ); + + $debugLogLevel = $this->configGroup->getPropertyInt("debug.level", 1); + if($this->logger instanceof MainLogger){ + $this->logger->setLogDebug($debugLogLevel > 1); + } + + $this->forceLanguage = $this->configGroup->getPropertyBool("settings.force-language", false); + $selectedLang = $this->configGroup->getConfigString("language", $this->configGroup->getPropertyString("settings.language", Language::FALLBACK_LANGUAGE)); + try{ + $this->language = new Language($selectedLang); + }catch(LanguageNotFoundException $e){ + $this->logger->error($e->getMessage()); + try{ + $this->language = new Language(Language::FALLBACK_LANGUAGE); + }catch(LanguageNotFoundException $e){ + $this->logger->emergency("Fallback language \"" . Language::FALLBACK_LANGUAGE . "\" not found"); + return; + } + } + + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::language_selected($this->getLanguage()->getName(), $this->getLanguage()->getLang()))); + + if(VersionInfo::IS_DEVELOPMENT_BUILD){ + if(!$this->configGroup->getPropertyBool("settings.enable-dev-builds", false)){ + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error1(VersionInfo::NAME))); + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error2())); + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error3())); + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error4("settings.enable-dev-builds"))); + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5("https://github.com/pmmp/PocketMine-MP/releases"))); + $this->forceShutdown(); + + return; + } + + $this->logger->warning(str_repeat("-", 40)); + $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning1(VersionInfo::NAME))); + $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning2())); + $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning3())); + $this->logger->warning(str_repeat("-", 40)); + } + + $this->memoryManager = new MemoryManager($this); + + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_start(TextFormat::AQUA . $this->getVersion() . TextFormat::RESET))); + + if(($poolSize = $this->configGroup->getPropertyString("settings.async-workers", "auto")) === "auto"){ + $poolSize = 2; + $processors = Utils::getCoreCount() - 2; + + if($processors > 0){ + $poolSize = max(1, $processors); + } + }else{ + $poolSize = max(1, (int) $poolSize); + } + + $this->asyncPool = new AsyncPool($poolSize, max(-1, $this->configGroup->getPropertyInt("memory.async-worker-hard-limit", 256)), $this->autoloader, $this->logger, $this->tickSleeper); + + $netCompressionThreshold = -1; + if($this->configGroup->getPropertyInt("network.batch-threshold", 256) >= 0){ + $netCompressionThreshold = $this->configGroup->getPropertyInt("network.batch-threshold", 256); + } + + $netCompressionLevel = $this->configGroup->getPropertyInt("network.compression-level", 6); + if($netCompressionLevel < 1 or $netCompressionLevel > 9){ + $this->logger->warning("Invalid network compression level $netCompressionLevel set, setting to default 6"); + $netCompressionLevel = 6; + } + ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE)); + + $this->networkCompressionAsync = $this->configGroup->getPropertyBool("network.async-compression", true); + + EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool("network.enable-encryption", true); + + $this->doTitleTick = $this->configGroup->getPropertyBool("console.title-tick", true) && Terminal::hasFormattingCodes(); + + $this->operators = new Config(Path::join($this->dataPath, "ops.txt"), Config::ENUM); + $this->whitelist = new Config(Path::join($this->dataPath, "white-list.txt"), Config::ENUM); + + $bannedTxt = Path::join($this->dataPath, "banned.txt"); + $bannedPlayersTxt = Path::join($this->dataPath, "banned-players.txt"); + if(file_exists($bannedTxt) and !file_exists($bannedPlayersTxt)){ + @rename($bannedTxt, $bannedPlayersTxt); + } + @touch($bannedPlayersTxt); + $this->banByName = new BanList($bannedPlayersTxt); + $this->banByName->load(); + $bannedIpsTxt = Path::join($this->dataPath, "banned-ips.txt"); + @touch($bannedIpsTxt); + $this->banByIP = new BanList($bannedIpsTxt); + $this->banByIP->load(); + + $this->maxPlayers = $this->configGroup->getConfigInt("max-players", 20); + + $this->onlineMode = $this->configGroup->getConfigBool("xbox-auth", true); + if($this->onlineMode){ + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_auth_enabled())); + }else{ + $this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_auth_disabled())); + $this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_authWarning())); + $this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled())); + } + + if($this->configGroup->getConfigBool("hardcore", false) and $this->getDifficulty() < World::DIFFICULTY_HARD){ + $this->configGroup->setConfigInt("difficulty", World::DIFFICULTY_HARD); + } + + @cli_set_process_title($this->getName() . " " . $this->getPocketMineVersion()); + + $this->serverID = Utils::getMachineUniqueId($this->getIp() . $this->getPort()); + + $this->getLogger()->debug("Server unique id: " . $this->getServerUniqueId()); + $this->getLogger()->debug("Machine unique id: " . Utils::getMachineUniqueId()); + + $this->network = new Network($this->logger); + $this->network->setName($this->getMotd()); + + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_info( + $this->getName(), + (VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : "") . $this->getPocketMineVersion() . TextFormat::RESET + ))); + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_license($this->getName()))); + + Timings::init(); + TimingsHandler::setEnabled($this->configGroup->getPropertyBool("settings.enable-profiling", false)); + $this->profilingTickRate = $this->configGroup->getPropertyInt("settings.profile-report-trigger", 20); + + DefaultPermissions::registerCorePermissions(); + + $this->commandMap = new SimpleCommandMap($this); + + $this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes.json")); + + $this->resourceManager = new ResourcePackManager(Path::join($this->getDataPath(), "resource_packs"), $this->logger); + + $pluginGraylist = null; + $graylistFile = Path::join($this->dataPath, "plugin_list.yml"); + if(!file_exists($graylistFile)){ + copy(Path::join(\pocketmine\RESOURCE_PATH, 'plugin_list.yml'), $graylistFile); + } + try{ + $pluginGraylist = PluginGraylist::fromArray(yaml_parse(file_get_contents($graylistFile))); + }catch(\InvalidArgumentException $e){ + $this->logger->emergency("Failed to load $graylistFile: " . $e->getMessage()); + $this->forceShutdown(); + return; + } + $this->pluginManager = new PluginManager($this, $this->configGroup->getPropertyBool("plugins.legacy-data-dir", true) ? null : Path::join($this->getDataPath(), "plugin_data"), $pluginGraylist); + $this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader)); + $this->pluginManager->registerInterface(new ScriptPluginLoader()); + + $providerManager = new WorldProviderManager(); + if( + ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString("level-settings.default-format", ""))) !== null and + $format instanceof WritableWorldProviderManagerEntry + ){ + $providerManager->setDefault($format); + }elseif($formatName !== ""){ + $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_level_badDefaultFormat($formatName))); + } + + $this->worldManager = new WorldManager($this, Path::join($this->dataPath, "worlds"), $providerManager); + $this->worldManager->setAutoSave($this->configGroup->getConfigBool("auto-save", $this->worldManager->getAutoSave())); + $this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt("ticks-per.autosave", 6000)); + + $this->updater = new UpdateChecker($this, $this->configGroup->getPropertyString("auto-updater.host", "update.pmmp.io")); + + $this->queryInfo = new QueryInfo($this); + + register_shutdown_function([$this, "crashDump"]); + + $this->pluginManager->loadPlugins($this->pluginPath); + $this->enablePlugins(PluginEnableOrder::STARTUP()); + + if(!$this->startupPrepareWorlds()){ + return; + } + $this->enablePlugins(PluginEnableOrder::POSTWORLD()); + + if(!$this->startupPrepareNetworkInterfaces()){ + $this->forceShutdown(); + return; + } + + if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){ + $this->sendUsageTicker = 6000; + $this->sendUsage(SendUsageTask::TYPE_OPEN); + } + + $this->configGroup->save(); + + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName()))); + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET))); + $this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3))))); + + //TODO: move console parts to a separate component + $consoleSender = new ConsoleCommandSender($this, $this->language); + $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $consoleSender); + $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $consoleSender); + + $consoleNotifier = new SleeperNotifier(); + $commandBuffer = new \Threaded(); + $this->console = new ConsoleReaderThread($commandBuffer, $consoleNotifier); + $this->tickSleeper->addNotifier($consoleNotifier, function() use ($commandBuffer, $consoleSender) : void{ + Timings::$serverCommand->startTiming(); + while(($line = $commandBuffer->shift()) !== null){ + $this->dispatchCommand($consoleSender, (string) $line); + } + Timings::$serverCommand->stopTiming(); + }); + $this->console->start(PTHREADS_INHERIT_NONE); + + $this->tickProcessor(); + $this->forceShutdown(); + }catch(\Throwable $e){ + $this->exceptionHandler($e); + } + } + + 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; + } + $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. + */ + public function subscribeToBroadcastChannel(string $channelId, CommandSender $subscriber) : void{ + $this->broadcastSubscribers[$channelId][spl_object_id($subscriber)] = $subscriber; + } + + /** + * Unsubscribes from a particular message broadcast channel. + */ + public function unsubscribeFromBroadcastChannel(string $channelId, CommandSender $subscriber) : void{ + if(isset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)])){ + unset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)]); + if(count($this->broadcastSubscribers[$channelId]) === 0){ + unset($this->broadcastSubscribers[$channelId]); + } + } + } + + /** + * Unsubscribes from all broadcast channels. + */ + public function unsubscribeFromAllBroadcastChannels(CommandSender $subscriber) : void{ + foreach(Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){ + $this->unsubscribeFromBroadcastChannel($channelId, $subscriber); + } + } + + /** + * Returns a list of all the CommandSenders subscribed to the given broadcast channel. + * + * @return CommandSender[] + * @phpstan-return array + */ + public function getBroadcastChannelSubscribers(string $channelId) : array{ + return $this->broadcastSubscribers[$channelId] ?? []; + } + + /** + * @param CommandSender[]|null $recipients + */ + public function broadcastMessage(Translatable|string $message, ?array $recipients = null) : int{ + $recipients = $recipients ?? $this->getBroadcastChannelSubscribers(self::BROADCAST_CHANNEL_USERS); + + foreach($recipients as $recipient){ + $recipient->sendMessage($message); + } + + return count($recipients); + } + + /** + * @return Player[] + */ + private function getPlayerBroadcastSubscribers(string $channelId) : array{ + /** @var Player[] $players */ + $players = []; + foreach($this->broadcastSubscribers[$channelId] as $subscriber){ + if($subscriber instanceof Player){ + $players[spl_object_id($subscriber)] = $subscriber; + } + } + return $players; + } + + /** + * @param Player[]|null $recipients + */ + public function broadcastTip(string $tip, ?array $recipients = null) : int{ + $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS); + + foreach($recipients as $recipient){ + $recipient->sendTip($tip); + } + + return count($recipients); + } + + /** + * @param Player[]|null $recipients + */ + public function broadcastPopup(string $popup, ?array $recipients = null) : int{ + $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS); + + foreach($recipients as $recipient){ + $recipient->sendPopup($popup); + } + + return count($recipients); + } + + /** + * @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used. + * @param int $stay Duration in ticks to stay on screen for + * @param int $fadeOut Duration in ticks for fade-out. + * @param Player[]|null $recipients + */ + public function broadcastTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1, ?array $recipients = null) : int{ + $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS); + + foreach($recipients as $recipient){ + $recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut); + } + + return count($recipients); + } + + /** + * @param Player[] $players + * @param ClientboundPacket[] $packets + */ + public function broadcastPackets(array $players, array $packets) : bool{ + if(count($packets) === 0){ + throw new \InvalidArgumentException("Cannot broadcast empty list of packets"); + } + + return Timings::$broadcastPackets->time(function() use ($players, $packets) : bool{ + /** @var NetworkSession[] $recipients */ + $recipients = []; + foreach($players as $player){ + if($player->isConnected()){ + $recipients[] = $player->getNetworkSession(); + } + } + if(count($recipients) === 0){ + return false; + } + + $ev = new DataPacketSendEvent($recipients, $packets); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + $recipients = $ev->getTargets(); + + /** @var PacketBroadcaster[] $broadcasters */ + $broadcasters = []; + /** @var NetworkSession[][] $broadcasterTargets */ + $broadcasterTargets = []; + foreach($recipients as $recipient){ + $broadcaster = $recipient->getBroadcaster(); + $broadcasters[spl_object_id($broadcaster)] = $broadcaster; + $broadcasterTargets[spl_object_id($broadcaster)][] = $recipient; + } + foreach($broadcasters as $broadcaster){ + $broadcaster->broadcastPackets($broadcasterTargets[spl_object_id($broadcaster)], $packets); + } + + return true; + }); + } + + /** + * Broadcasts a list of packets in a batch to a list of players + * + * @param bool|null $sync Compression on the main thread (true) or workers (false). Default is automatic (null). + */ + public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null) : CompressBatchPromise{ + try{ + Timings::$playerNetworkSendCompress->startTiming(); + + $buffer = $stream->getBuffer(); + + if($sync === null){ + $sync = !($this->networkCompressionAsync && $compressor->willCompress($buffer)); + } + + $promise = new CompressBatchPromise(); + if(!$sync){ + $task = new CompressBatchTask($buffer, $promise, $compressor); + $this->asyncPool->submitTask($task); + }else{ + $promise->resolve($compressor->compress($buffer)); + } + + return $promise; + }finally{ + Timings::$playerNetworkSendCompress->stopTiming(); + } + } + + public function enablePlugins(PluginEnableOrder $type) : void{ + foreach($this->pluginManager->getPlugins() as $plugin){ + if(!$plugin->isEnabled() and $plugin->getDescription()->getOrder()->equals($type)){ + $this->pluginManager->enablePlugin($plugin); + } + } + + if($type->equals(PluginEnableOrder::POSTWORLD())){ + $this->commandMap->registerServerAliases(); + } + } + + /** + * Executes a command from a CommandSender + */ + public function dispatchCommand(CommandSender $sender, string $commandLine, bool $internal = false) : bool{ + if(!$internal){ + $ev = new CommandEvent($sender, $commandLine); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $commandLine = $ev->getCommand(); + } + + return $this->commandMap->dispatch($sender, $commandLine); + } + + /** + * Shuts the server down correctly + */ + public function shutdown() : void{ + if($this->isRunning){ + $this->isRunning = false; + $this->signalHandler->unregister(); + } + } + + public function forceShutdown() : void{ + if($this->hasStopped){ + return; + } + + if($this->doTitleTick){ + echo "\x1b]0;\x07"; + } + + if($this->isRunning){ + $this->logger->emergency("Forcing server shutdown"); + } + try{ + if(!$this->isRunning()){ + $this->sendUsage(SendUsageTask::TYPE_CLOSE); + } + + $this->hasStopped = true; + + $this->shutdown(); + + if(isset($this->pluginManager)){ + $this->getLogger()->debug("Disabling all plugins"); + $this->pluginManager->disablePlugins(); + } + + if(isset($this->network)){ + $this->network->getSessionManager()->close($this->configGroup->getPropertyString("settings.shutdown-message", "Server closed")); + } + + if(isset($this->worldManager)){ + $this->getLogger()->debug("Unloading all worlds"); + foreach($this->worldManager->getWorlds() as $world){ + $this->worldManager->unloadWorld($world, true); + } + } + + $this->getLogger()->debug("Removing event handlers"); + HandlerListManager::global()->unregisterAll(); + + if(isset($this->asyncPool)){ + $this->getLogger()->debug("Shutting down async task worker pool"); + $this->asyncPool->shutdown(); + } + + if(isset($this->configGroup)){ + $this->getLogger()->debug("Saving properties"); + $this->configGroup->save(); + } + + if(isset($this->console)){ + $this->getLogger()->debug("Closing console"); + $this->console->quit(); + } + + if(isset($this->network)){ + $this->getLogger()->debug("Stopping network interfaces"); + foreach($this->network->getInterfaces() as $interface){ + $this->getLogger()->debug("Stopping network interface " . get_class($interface)); + $this->network->unregisterInterface($interface); + } + } + }catch(\Throwable $e){ + $this->logger->logException($e); + $this->logger->emergency("Crashed while crashing, killing process"); + @Process::kill(Process::pid(), true); + } + + } + + public function getQueryInformation() : QueryInfo{ + return $this->queryInfo; + } + + /** + * @param mixed[][]|null $trace + * @phpstan-param list>|null $trace + */ + public function exceptionHandler(\Throwable $e, $trace = null) : void{ + while(@ob_end_flush()){} + global $lastError; + + if($trace === null){ + $trace = $e->getTrace(); + } + + $errstr = $e->getMessage(); + $errfile = $e->getFile(); + $errline = $e->getLine(); + + $errstr = preg_replace('/\s+/', ' ', trim($errstr)); + + $errfile = Filesystem::cleanPath($errfile); + + $this->logger->logException($e, $trace); + + $lastError = [ + "type" => get_class($e), + "message" => $errstr, + "fullFile" => $e->getFile(), + "file" => $errfile, + "line" => $errline, + "trace" => $trace + ]; + + global $lastExceptionError, $lastError; + $lastExceptionError = $lastError; + $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){ + return; + } + if($this->sendUsageTicker > 0){ + $this->sendUsage(SendUsageTask::TYPE_CLOSE); + } + $this->hasStopped = false; + + ini_set("error_reporting", '0'); + 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, $this->pluginManager ?? null); + + $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()))){ + $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($plugin !== ""){ + $p = $this->pluginManager->getPlugin($plugin); + if($p instanceof Plugin and !($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){ + $report = false; + } + + if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false or 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 + } + + if($report){ + $url = ($this->configGroup->getPropertyBool("auto-report.use-https", true) ? "https" : "http") . "://" . $this->configGroup->getPropertyString("auto-report.host", "crash.pmmp.io") . "/submit/api"; + $postUrlError = "Unknown error"; + $reply = Internet::postURL($url, [ + "report" => "yes", + "name" => $this->getName() . " " . $this->getPocketMineVersion(), + "email" => "crash@pocketmine.net", + "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)){ + $reportId = $data->crashId; + $reportUrl = $data->crashUrl; + $this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId))); + }elseif(isset($data->error)){ + $this->logger->emergency("Automatic crash report submission failed: $data->error"); + } + }else{ + $this->logger->emergency("Failed to communicate with crash archive: $postUrlError"); + } + } + } + }catch(\Throwable $e){ + $this->logger->logException($e); + try{ + $this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_error($e->getMessage()))); + }catch(\Throwable $e){} + } + + $this->forceShutdown(); + $this->isRunning = false; + + //Force minimum uptime to be >= 120 seconds, to reduce the impact of spammy crash loops + $spacing = ((int) $this->startTime) - time() + 120; + if($spacing > 0){ + echo "--- Waiting $spacing seconds to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL; + sleep($spacing); + } + @Process::kill(Process::pid(), true); + exit(1); + } + + /** + * @return mixed[] + */ + public function __debugInfo() : array{ + return []; + } + + public function getTickSleeper() : SleeperHandler{ + return $this->tickSleeper; + } + + private function tickProcessor() : void{ + $this->nextTick = microtime(true); + + while($this->isRunning){ + $this->tick(); + + //sleeps are self-correcting - if we undersleep 1ms on this tick, we'll sleep an extra ms on the next tick + $this->tickSleeper->sleepUntil($this->nextTick); + } + } + + public function addOnlinePlayer(Player $player) : bool{ + $ev = new PlayerLoginEvent($player, "Plugin reason"); + $ev->call(); + if($ev->isCancelled() or !$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); + } + $rawUUID = $player->getUniqueId()->getBytes(); + $this->playerList[$rawUUID] = $player; + + if($this->sendUsageTicker > 0){ + $this->uniquePlayers[$rawUUID] = $rawUUID; + } + + return true; + } + + public function removeOnlinePlayer(Player $player) : void{ + if(isset($this->playerList[$rawUUID = $player->getUniqueId()->getBytes()])){ + unset($this->playerList[$rawUUID]); + foreach($this->playerList as $p){ + $p->getNetworkSession()->onPlayerRemoved($player); + } + } + } + + public function sendUsage(int $type = SendUsageTask::TYPE_STATUS) : void{ + if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){ + $this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers)); + } + $this->uniquePlayers = []; + } + + public function getLanguage() : Language{ + return $this->language; + } + + public function isLanguageForced() : bool{ + return $this->forceLanguage; + } + + public function getNetwork() : Network{ + return $this->network; + } + + public function getMemoryManager() : MemoryManager{ + return $this->memoryManager; + } + + private function titleTick() : void{ + Timings::$titleTick->startTiming(); + + $u = Process::getAdvancedMemoryUsage(); + $usage = sprintf("%g/%g/%g MB @ %d threads", round(($u[0] / 1024) / 1024, 2), round(($u[1] / 1024) / 1024, 2), round(($u[2] / 1024) / 1024, 2), Process::getThreadCount()); + + $online = count($this->playerList); + $connecting = $this->network->getConnectionCount() - $online; + $bandwidthStats = $this->network->getBandwidthTracker(); + + echo "\x1b]0;" . $this->getName() . " " . + $this->getPocketMineVersion() . + " | Online $online/" . $this->getMaxPlayers() . + ($connecting > 0 ? " (+$connecting connecting)" : "") . + " | Memory " . $usage . + " | U " . round($bandwidthStats->getSend()->getAverageBytes() / 1024, 2) . + " D " . round($bandwidthStats->getReceive()->getAverageBytes() / 1024, 2) . + " kB/s | TPS " . $this->getTicksPerSecondAverage() . + " | Load " . $this->getTickUsageAverage() . "%\x07"; + + Timings::$titleTick->stopTiming(); + } + + /** + * Tries to execute a server tick + */ + private function tick() : void{ + $tickTime = microtime(true); + if(($tickTime - $this->nextTick) < -0.025){ //Allow half a tick of diff + return; + } + + Timings::$serverTick->startTiming(); + + ++$this->tickCounter; + + Timings::$scheduler->startTiming(); + $this->pluginManager->tickSchedulers($this->tickCounter); + Timings::$scheduler->stopTiming(); + + Timings::$schedulerAsync->startTiming(); + $this->asyncPool->collectTasks(); + Timings::$schedulerAsync->stopTiming(); + + $this->worldManager->tick($this->tickCounter); + + Timings::$connection->startTiming(); + $this->network->tick(); + Timings::$connection->stopTiming(); + + if(($this->tickCounter % 20) === 0){ + if($this->doTitleTick){ + $this->titleTick(); + } + $this->currentTPS = 20; + $this->currentUse = 0; + + $queryRegenerateEvent = new QueryRegenerateEvent(new QueryInfo($this)); + $queryRegenerateEvent->call(); + $this->queryInfo = $queryRegenerateEvent->getQueryInfo(); + + $this->network->updateName(); + $this->network->getBandwidthTracker()->rotateAverageHistory(); + } + + if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){ + $this->sendUsageTicker = 6000; + $this->sendUsage(SendUsageTask::TYPE_STATUS); + } + + if(($this->tickCounter % 100) === 0){ + foreach($this->worldManager->getWorlds() as $world){ + $world->clearCache(); + } + + if($this->getTicksPerSecondAverage() < 12){ + $this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_tickOverload())); + } + } + + $this->getMemoryManager()->check(); + + Timings::$serverTick->stopTiming(); + + $now = microtime(true); + $this->currentTPS = min(20, 1 / max(0.001, $now - $tickTime)); + $this->currentUse = min(1, ($now - $tickTime) / 0.05); + + TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate); + + $idx = $this->tickCounter % 20; + $this->tickAverage[$idx] = $this->currentTPS; + $this->useAverage[$idx] = $this->currentUse; + + if(($this->nextTick - $tickTime) < -1){ + $this->nextTick = $tickTime; + }else{ + $this->nextTick += 0.05; + } + } +} diff --git a/src/ServerConfigGroup.php b/src/ServerConfigGroup.php new file mode 100644 index 0000000000..f69990cf85 --- /dev/null +++ b/src/ServerConfigGroup.php @@ -0,0 +1,146 @@ + + */ + private $propertyCache = []; + + public function __construct(Config $pocketmineYml, Config $serverProperties){ + $this->pocketmineYml = $pocketmineYml; + $this->serverProperties = $serverProperties; + } + + /** + * @param mixed $defaultValue + * + * @return mixed + */ + public function getProperty(string $variable, $defaultValue = null){ + if(!array_key_exists($variable, $this->propertyCache)){ + $v = getopt("", ["$variable::"]); + if(isset($v[$variable])){ + $this->propertyCache[$variable] = $v[$variable]; + }else{ + $this->propertyCache[$variable] = $this->pocketmineYml->getNested($variable); + } + } + + return $this->propertyCache[$variable] ?? $defaultValue; + } + + public function getPropertyBool(string $variable, bool $defaultValue) : bool{ + return (bool) $this->getProperty($variable, $defaultValue); + } + + public function getPropertyInt(string $variable, int $defaultValue) : int{ + return (int) $this->getProperty($variable, $defaultValue); + } + + public function getPropertyString(string $variable, string $defaultValue) : string{ + return (string) $this->getProperty($variable, $defaultValue); + } + + public function getConfigString(string $variable, string $defaultValue = "") : string{ + $v = getopt("", ["$variable::"]); + if(isset($v[$variable])){ + return (string) $v[$variable]; + } + + return $this->serverProperties->exists($variable) ? (string) $this->serverProperties->get($variable) : $defaultValue; + } + + public function setConfigString(string $variable, string $value) : void{ + $this->serverProperties->set($variable, $value); + } + + public function getConfigInt(string $variable, int $defaultValue = 0) : int{ + $v = getopt("", ["$variable::"]); + if(isset($v[$variable])){ + return (int) $v[$variable]; + } + + return $this->serverProperties->exists($variable) ? (int) $this->serverProperties->get($variable) : $defaultValue; + } + + public function setConfigInt(string $variable, int $value) : void{ + $this->serverProperties->set($variable, $value); + } + + public function getConfigBool(string $variable, bool $defaultValue = false) : bool{ + $v = getopt("", ["$variable::"]); + if(isset($v[$variable])){ + $value = $v[$variable]; + }else{ + $value = $this->serverProperties->exists($variable) ? $this->serverProperties->get($variable) : $defaultValue; + } + if(is_bool($value)){ + return $value; + } + 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; + } + + public function setConfigBool(string $variable, bool $value) : void{ + $this->serverProperties->set($variable, $value ? "1" : "0"); + } + + public function save() : void{ + if($this->serverProperties->hasChanged()){ + $this->serverProperties->save(); + } + if($this->pocketmineYml->hasChanged()){ + $this->pocketmineYml->save(); + } + } +} diff --git a/src/VersionInfo.php b/src/VersionInfo.php new file mode 100644 index 0000000000..5a5b6139bb --- /dev/null +++ b/src/VersionInfo.php @@ -0,0 +1,91 @@ +getMetadata(); + if(isset($meta["git"])){ + $gitHash = $meta["git"]; + } + } + + self::$gitHash = $gitHash; + } + + 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()); + } + return self::$fullVersion; + } +} diff --git a/src/pocketmine/block/ActivatorRail.php b/src/block/ActivatorRail.php similarity index 83% rename from src/pocketmine/block/ActivatorRail.php rename to src/block/ActivatorRail.php index 88d4a9f7f5..d842be041d 100644 --- a/src/pocketmine/block/ActivatorRail.php +++ b/src/block/ActivatorRail.php @@ -23,13 +23,10 @@ declare(strict_types=1); namespace pocketmine\block; -class ActivatorRail extends RedstoneRail{ +use pocketmine\block\utils\RailPoweredByRedstoneTrait; - protected $id = self::ACTIVATOR_RAIL; - - public function getName() : string{ - return "Activator Rail"; - } +class ActivatorRail extends StraightOnlyRail{ + use RailPoweredByRedstoneTrait; //TODO } diff --git a/src/pocketmine/block/Bedrock.php b/src/block/Air.php similarity index 66% rename from src/pocketmine/block/Bedrock.php rename to src/block/Air.php index beb5ceac1e..8e1104bba0 100644 --- a/src/pocketmine/block/Bedrock.php +++ b/src/block/Air.php @@ -23,29 +23,33 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\Item; +use pocketmine\math\AxisAlignedBB; -class Bedrock extends Solid{ +/** + * Air block + */ +class Air extends Transparent{ - protected $id = self::BEDROCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function canBeFlowedInto() : bool{ + return true; } - public function getName() : string{ - return "Bedrock"; + public function canBeReplaced() : bool{ + return true; } - public function getHardness() : float{ - return -1; - } - - public function getBlastResistance() : float{ - return 18000000; - } - - public function isBreakable(Item $item) : bool{ + public function canBePlaced() : bool{ return false; } + + public function isSolid() : bool{ + return false; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } } diff --git a/src/block/Anvil.php b/src/block/Anvil.php new file mode 100644 index 0000000000..48fade268d --- /dev/null +++ b/src/block/Anvil.php @@ -0,0 +1,97 @@ +facing) | ($this->damage << 2); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x3); + $this->damage = BlockDataSerializer::readBoundedInt("damage", $stateMeta >> 2, 0, 2); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + protected function writeStateToItemMeta() : int{ + return $this->damage << 2; + } + + public function getDamage() : int{ return $this->damage; } + + /** @return $this */ + public function setDamage(int $damage) : self{ + if($damage < 0 || $damage > 2){ + throw new \InvalidArgumentException("Damage must be in range 0-2"); + } + $this->damage = $damage; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->squash(Facing::axis(Facing::rotateY($this->facing, false)), 1 / 8)]; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + $player->setCurrentWindow(new AnvilInventory($this->position)); + } + + return true; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $this->facing = Facing::rotateY($player->getHorizontalFacing(), true); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function tickFalling() : ?Block{ + return null; + } +} diff --git a/src/block/Bamboo.php b/src/block/Bamboo.php new file mode 100644 index 0000000000..08817d12db --- /dev/null +++ b/src/block/Bamboo.php @@ -0,0 +1,240 @@ +thick = ($stateMeta & BlockLegacyMetadata::BAMBOO_FLAG_THICK) !== 0; + $this->leafSize = BlockDataSerializer::readBoundedInt("leafSize", ($stateMeta >> BlockLegacyMetadata::BAMBOO_LEAF_SIZE_SHIFT) & BlockLegacyMetadata::BAMBOO_LEAF_SIZE_MASK, self::NO_LEAVES, self::LARGE_LEAVES); + $this->ready = ($stateMeta & BlockLegacyMetadata::BAMBOO_FLAG_READY) !== 0; + } + + public function writeStateToMeta() : int{ + return ($this->thick ? BlockLegacyMetadata::BAMBOO_FLAG_THICK : 0) | ($this->leafSize << BlockLegacyMetadata::BAMBOO_LEAF_SIZE_SHIFT) | ($this->ready ? BlockLegacyMetadata::BAMBOO_FLAG_READY : 0); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isThick() : bool{ return $this->thick; } + + /** @return $this */ + public function setThick(bool $thick) : self{ + $this->thick = $thick; + return $this; + } + + public function isReady() : bool{ return $this->ready; } + + /** @return $this */ + public function setReady(bool $ready) : self{ + $this->ready = $ready; + return $this; + } + + public function getLeafSize() : int{ return $this->leafSize; } + + /** @return $this */ + public function setLeafSize(int $leafSize) : self{ + $this->leafSize = $leafSize; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + //this places the BB at the northwest corner, not the center + $inset = 1 - (($this->thick ? 3 : 2) / 16); + return [AxisAlignedBB::one()->trim(Facing::SOUTH, $inset)->trim(Facing::EAST, $inset)]; + } + + private static function getOffsetSeed(int $x, int $y, int $z) : int{ + $p1 = gmp_mul($z, 0x6ebfff5); + $p2 = gmp_mul($x, 0x2fc20f); + $p3 = $y; + + $xord = gmp_xor(gmp_xor($p1, $p2), $p3); + + $fullResult = gmp_mul(gmp_add(gmp_mul($xord, 0x285b825), 0xb), $xord); + return gmp_intval(gmp_and($fullResult, 0xffffffff)); + } + + private static function getMaxHeight(int $x, int $z) : int{ + return 12 + (self::getOffsetSeed($x, 0, $z) % 5); + } + + 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; + return new Vector3($retX, 0, $retZ); + } + + private function canBeSupportedBy(Block $block) : bool{ + //TODO: tags would be better for this + return + $block instanceof Dirt || + $block instanceof Grass || + $block instanceof Gravel || + $block instanceof Sand || + $block instanceof Mycelium || + $block instanceof Podzol; + } + + private function seekToTop() : Bamboo{ + $world = $this->position->getWorld(); + $top = $this; + while(($next = $world->getBlock($top->position->up())) instanceof Bamboo && $next->isSameType($this)){ + $top = $next; + } + return $top; + } + + 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), $player)){ + $item->pop(); + return true; + } + }elseif($item instanceof ItemBamboo){ + if($this->seekToTop()->grow(PHP_INT_MAX, 1, $player)){ + $item->pop(); + return true; + } + } + return false; + } + + public function onNearbyBlockChange() : void{ + $below = $this->position->getWorld()->getBlock($this->position->down()); + if(!$this->canBeSupportedBy($below) and !$below->isSameType($this)){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + private function grow(int $maxHeight, int $growAmount, ?Player $player) : bool{ + $world = $this->position->getWorld(); + if(!$world->getBlock($this->position->up())->canBeReplaced()){ + return false; + } + + $height = 1; + while($world->getBlock($this->position->subtract(0, $height, 0))->isSameType($this)){ + if(++$height >= $maxHeight){ + return false; + } + } + + $newHeight = $height + $growAmount; + + $stemBlock = (clone $this)->setReady(false)->setLeafSize(self::NO_LEAVES); + if($newHeight >= 4 && !$stemBlock->isThick()){ //don't change it to false if height is less, because it might have been chopped + $stemBlock = $stemBlock->setThick(true); + } + $smallLeavesBlock = (clone $stemBlock)->setLeafSize(self::SMALL_LEAVES); + $bigLeavesBlock = (clone $stemBlock)->setLeafSize(self::LARGE_LEAVES); + + $newBlocks = []; + if($newHeight === 2){ + $newBlocks[] = $smallLeavesBlock; + }elseif($newHeight === 3){ + $newBlocks[] = $smallLeavesBlock; + $newBlocks[] = $smallLeavesBlock; + }elseif($newHeight === 4){ + $newBlocks[] = $bigLeavesBlock; + $newBlocks[] = $smallLeavesBlock; + $newBlocks[] = $stemBlock; + $newBlocks[] = $stemBlock; + }elseif($newHeight > 4){ + $newBlocks[] = $bigLeavesBlock; + $newBlocks[] = $bigLeavesBlock; + $newBlocks[] = $smallLeavesBlock; + for($i = 0, $max = min($growAmount, $newHeight - count($newBlocks)); $i < $max; ++$i){ + $newBlocks[] = $stemBlock; //to replace the bottom blocks that currently have leaves + } + } + + $tx = new BlockTransaction($this->position->getWorld()); + foreach($newBlocks as $idx => $newBlock){ + $tx->addBlock($this->position->subtract(0, $idx - $growAmount, 0), $newBlock); + } + + $ev = new StructureGrowEvent($this, $tx, $player); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + return $tx->apply(); + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + $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, null)){ + $world->setBlock($this->position, $this); + } + }elseif($world->getBlock($this->position->up())->canBeReplaced()){ + $this->ready = true; + $world->setBlock($this->position, $this); + } + } +} diff --git a/src/block/BambooSapling.php b/src/block/BambooSapling.php new file mode 100644 index 0000000000..3da1e593a8 --- /dev/null +++ b/src/block/BambooSapling.php @@ -0,0 +1,130 @@ +ready = ($stateMeta & BlockLegacyMetadata::SAPLING_FLAG_READY) !== 0; + } + + protected function writeStateToMeta() : int{ + return $this->ready ? BlockLegacyMetadata::SAPLING_FLAG_READY : 0; + } + + public function getStateBitmask() : int{ return 0b1000; } + + public function isReady() : bool{ return $this->ready; } + + /** @return $this */ + public function setReady(bool $ready) : self{ + $this->ready = $ready; + return $this; + } + + private function canBeSupportedBy(Block $block) : bool{ + //TODO: tags would be better for this + return + $block instanceof Dirt || + $block instanceof Grass || + $block instanceof Gravel || + $block instanceof Sand || + $block instanceof Mycelium || + $block instanceof Podzol; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->canBeSupportedBy($blockReplace->position->getWorld()->getBlock($blockReplace->position->down()))){ + return false; + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($item instanceof Fertilizer || $item instanceof ItemBamboo){ + if($this->grow($player)){ + $item->pop(); + return true; + } + } + return false; + } + + public function onNearbyBlockChange() : void{ + if(!$this->canBeSupportedBy($this->position->getWorld()->getBlock($this->position->down()))){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + private function grow(?Player $player) : bool{ + $world = $this->position->getWorld(); + if(!$world->getBlock($this->position->up())->canBeReplaced()){ + return false; + } + + $tx = new BlockTransaction($world); + $bamboo = VanillaBlocks::BAMBOO(); + $tx->addBlock($this->position, $bamboo) + ->addBlock($this->position->up(), (clone $bamboo)->setLeafSize(Bamboo::SMALL_LEAVES)); + + $ev = new StructureGrowEvent($this, $tx, $player); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + return $tx->apply(); + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + $world = $this->position->getWorld(); + if($this->ready){ + $this->ready = false; + if($world->getFullLight($this->position) < 9 || !$this->grow(null)){ + $world->setBlock($this->position, $this); + } + }elseif($world->getBlock($this->position->up())->canBeReplaced()){ + $this->ready = true; + $world->setBlock($this->position, $this); + } + } + + public function asItem() : Item{ + return VanillaBlocks::BAMBOO()->asItem(); + } +} diff --git a/src/block/Barrel.php b/src/block/Barrel.php new file mode 100644 index 0000000000..a9d992abf5 --- /dev/null +++ b/src/block/Barrel.php @@ -0,0 +1,102 @@ +facing) | ($this->open ? BlockLegacyMetadata::BARREL_FLAG_OPEN : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readFacing($stateMeta & 0x07); + $this->open = ($stateMeta & BlockLegacyMetadata::BARREL_FLAG_OPEN) === BlockLegacyMetadata::BARREL_FLAG_OPEN; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isOpen() : bool{ + return $this->open; + } + + /** @return $this */ + public function setOpen(bool $open) : Barrel{ + $this->open = $open; + return $this; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + if(abs($player->getPosition()->getX() - $this->position->getX()) < 2 && abs($player->getPosition()->getZ() - $this->position->getZ()) < 2){ + $y = $player->getEyePos()->getY(); + + if($y - $this->position->getY() > 2){ + $this->facing = Facing::UP; + }elseif($this->position->getY() - $y > 0){ + $this->facing = Facing::DOWN; + }else{ + $this->facing = Facing::opposite($player->getHorizontalFacing()); + } + }else{ + $this->facing = Facing::opposite($player->getHorizontalFacing()); + } + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + $barrel = $this->position->getWorld()->getTile($this->position); + if($barrel instanceof TileBarrel){ + if(!$barrel->canOpenWith($item->getCustomName())){ + return true; + } + + $player->setCurrentWindow($barrel->getInventory()); + } + } + + return true; + } + + public function getFuelTime() : int{ + return 300; + } +} diff --git a/src/block/BaseBanner.php b/src/block/BaseBanner.php new file mode 100644 index 0000000000..0f7d55ae59 --- /dev/null +++ b/src/block/BaseBanner.php @@ -0,0 +1,147 @@ + + */ + protected array $patterns = []; + + public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){ + $this->color = DyeColor::BLACK(); + parent::__construct($idInfo, $name, $breakInfo); + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileBanner){ + $this->color = $tile->getBaseColor(); + $this->setPatterns($tile->getPatterns()); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileBanner); + $tile->setBaseColor($this->color); + $tile->setPatterns($this->patterns); + } + + public function isSolid() : bool{ + return false; + } + + public function getMaxStackSize() : int{ + return 16; + } + + /** + * @return BannerPatternLayer[] + * @phpstan-return list + */ + public function getPatterns() : array{ + return $this->patterns; + } + + /** + * @param BannerPatternLayer[] $patterns + * + * @phpstan-param list $patterns + * @return $this + */ + public function setPatterns(array $patterns) : self{ + $checked = array_filter($patterns, fn($v) => $v instanceof BannerPatternLayer); + if(count($checked) !== count($patterns)){ + throw new \TypeError("Deque must only contain " . BannerPatternLayer::class . " objects"); + } + $this->patterns = $checked; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($item instanceof ItemBanner){ + $this->color = $item->getColor(); + $this->setPatterns($item->getPatterns()); + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + abstract protected function getSupportingFace() : int; + + public function onNearbyBlockChange() : void{ + if($this->getSide($this->getSupportingFace())->getId() === BlockLegacyIds::AIR){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + protected function writeStateToItemMeta() : int{ + return DyeColorIdMap::getInstance()->toInvertedId($this->color); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + $drop = $this->asItem(); + if($drop instanceof ItemBanner and count($this->patterns) > 0){ + $drop->setPatterns($this->patterns); + } + + return [$drop]; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + $result = $this->asItem(); + if($addUserData and $result instanceof ItemBanner and count($this->patterns) > 0){ + $result->setPatterns($this->patterns); + } + return $result; + } +} diff --git a/src/block/BaseCoral.php b/src/block/BaseCoral.php new file mode 100644 index 0000000000..f3b37988a8 --- /dev/null +++ b/src/block/BaseCoral.php @@ -0,0 +1,85 @@ +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(); + + $hasWater = false; + foreach($this->position->sides() as $vector3){ + if($world->getBlock($vector3) instanceof Water){ + $hasWater = true; + break; + } + } + + //TODO: check water inside the block itself (not supported on the API yet) + if(!$hasWater){ + $world->setBlock($this->position, $this->setDead(true)); + } + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return []; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function isSolid() : bool{ return false; } + + protected function recalculateCollisionBoxes() : array{ return []; } +} diff --git a/src/block/BaseRail.php b/src/block/BaseRail.php new file mode 100644 index 0000000000..bf117cd7cc --- /dev/null +++ b/src/block/BaseRail.php @@ -0,0 +1,234 @@ +getSide(Facing::DOWN)->isTransparent()){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onPostPlace() : void{ + $this->tryReconnect(); + } + + /** + * @param int[] $connections + * @param int[][] $lookup + * @phpstan-param array> $lookup + */ + protected static function searchState(array $connections, array $lookup) : ?int{ + $shape = array_search($connections, $lookup, true); + if($shape === false){ + $shape = array_search(array_reverse($connections), $lookup, true); + } + return $shape === false ? null : $shape; + } + + /** + * Sets the rail shape according to the given connections, if a shape matches. + * + * @param int[] $connections + * + * @throws \InvalidArgumentException if no shape matches the given connections + */ + abstract protected function setShapeFromConnections(array $connections) : void; + + /** + * Returns the connection directions of this rail (depending on the current block state) + * + * @return int[] + */ + abstract protected function getCurrentShapeConnections() : array; + + /** + * Returns all the directions this rail is already connected in. + * + * @return int[] + */ + private function getConnectedDirections() : array{ + /** @var int[] $connections */ + $connections = []; + + /** @var int $connection */ + foreach($this->getCurrentShapeConnections() as $connection){ + $other = $this->getSide($connection & ~RailConnectionInfo::FLAG_ASCEND); + $otherConnection = Facing::opposite($connection & ~RailConnectionInfo::FLAG_ASCEND); + + if(($connection & RailConnectionInfo::FLAG_ASCEND) !== 0){ + $other = $other->getSide(Facing::UP); + + }elseif(!($other instanceof BaseRail)){ //check for rail sloping up to meet this one + $other = $other->getSide(Facing::DOWN); + $otherConnection |= RailConnectionInfo::FLAG_ASCEND; + } + + if( + $other instanceof BaseRail and + in_array($otherConnection, $other->getCurrentShapeConnections(), true) + ){ + $connections[] = $connection; + } + } + + return $connections; + } + + /** + * @param int[] $constraints + * + * @return true[] + * @phpstan-return array + */ + private function getPossibleConnectionDirections(array $constraints) : array{ + switch(count($constraints)){ + case 0: + //No constraints, can connect in any direction + $possible = [ + Facing::NORTH => true, + Facing::SOUTH => true, + Facing::WEST => true, + Facing::EAST => true + ]; + foreach($possible as $p => $_){ + $possible[$p | RailConnectionInfo::FLAG_ASCEND] = true; + } + + return $possible; + case 1: + return $this->getPossibleConnectionDirectionsOneConstraint(array_shift($constraints)); + case 2: + return []; + default: + throw new \InvalidArgumentException("Expected at most 2 constraints, got " . count($constraints)); + } + } + + /** + * @return true[] + * @phpstan-return array + */ + protected function getPossibleConnectionDirectionsOneConstraint(int $constraint) : array{ + $opposite = Facing::opposite($constraint & ~RailConnectionInfo::FLAG_ASCEND); + + $possible = [$opposite => true]; + + if(($constraint & RailConnectionInfo::FLAG_ASCEND) === 0){ + //We can slope the other way if this connection isn't already a slope + $possible[$opposite | RailConnectionInfo::FLAG_ASCEND] = true; + } + + return $possible; + } + + private function tryReconnect() : void{ + $thisConnections = $this->getConnectedDirections(); + $changed = false; + + do{ + $possible = $this->getPossibleConnectionDirections($thisConnections); + $continue = false; + + foreach($possible as $thisSide => $_){ + $otherSide = Facing::opposite($thisSide & ~RailConnectionInfo::FLAG_ASCEND); + + $other = $this->getSide($thisSide & ~RailConnectionInfo::FLAG_ASCEND); + + if(($thisSide & RailConnectionInfo::FLAG_ASCEND) !== 0){ + $other = $other->getSide(Facing::UP); + + }elseif(!($other instanceof BaseRail)){ //check if other rails can slope up to meet this one + $other = $other->getSide(Facing::DOWN); + $otherSide |= RailConnectionInfo::FLAG_ASCEND; + } + + if(!($other instanceof BaseRail) or count($otherConnections = $other->getConnectedDirections()) >= 2){ + //we can only connect to a rail that has less than 2 connections + continue; + } + + $otherPossible = $other->getPossibleConnectionDirections($otherConnections); + + if(isset($otherPossible[$otherSide])){ + $otherConnections[] = $otherSide; + $other->setConnections($otherConnections); + $this->position->getWorld()->setBlock($other->position, $other); + + $changed = true; + $thisConnections[] = $thisSide; + $continue = count($thisConnections) < 2; + + break; //force recomputing possible directions, since this connection could invalidate others + } + } + }while($continue); + + if($changed){ + $this->setConnections($thisConnections); + $this->position->getWorld()->setBlock($this->position, $this); + } + } + + /** + * @param int[] $connections + */ + private function setConnections(array $connections) : void{ + if(count($connections) === 1){ + $connections[] = Facing::opposite($connections[0] & ~RailConnectionInfo::FLAG_ASCEND); + }elseif(count($connections) !== 2){ + throw new \InvalidArgumentException("Expected exactly 2 connections, got " . count($connections)); + } + + $this->setShapeFromConnections($connections); + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + $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()){ + $this->position->getWorld()->useBreakOn($this->position); + break; + } + } + } + } +} diff --git a/src/block/BaseSign.php b/src/block/BaseSign.php new file mode 100644 index 0000000000..586eee6a13 --- /dev/null +++ b/src/block/BaseSign.php @@ -0,0 +1,139 @@ +text = new SignText(); + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileSign){ + $this->text = $tile->getText(); + $this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileSign); + $tile->setText($this->text); + $tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId); + } + + public function isSolid() : bool{ + return false; + } + + public function getMaxStackSize() : int{ + return 16; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } + + abstract protected function getSupportingFace() : int; + + public function onNearbyBlockChange() : void{ + if($this->getSide($this->getSupportingFace())->getId() === BlockLegacyIds::AIR){ + $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($player !== null){ + $this->editorEntityRuntimeId = $player->getId(); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + /** + * Returns an object containing information about the sign text. + */ + public function getText() : SignText{ + return $this->text; + } + + /** @return $this */ + public function setText(SignText $text) : self{ + $this->text = $text; + return $this; + } + + /** + * Called by the player controller (network session) to update the sign text, firing events as appropriate. + * + * @return bool if the sign update was successful. + * @throws \UnexpectedValueException if the text payload is too large + */ + public function updateText(Player $author, SignText $text) : bool{ + $size = 0; + foreach($text->getLines() as $line){ + $size += strlen($line); + } + if($size > 1000){ + throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)"); + } + $ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{ + return TextFormat::clean($line, false); + }, $text->getLines()))); + if($this->editorEntityRuntimeId === null || $this->editorEntityRuntimeId !== $author->getId()){ + $ev->cancel(); + } + $ev->call(); + if(!$ev->isCancelled()){ + $this->setText($ev->getNewText()); + $this->position->getWorld()->setBlock($this->position, $this); + return true; + } + + return false; + } +} diff --git a/src/pocketmine/block/StillLava.php b/src/block/Beacon.php similarity index 86% rename from src/pocketmine/block/StillLava.php rename to src/block/Beacon.php index b4130a7394..ef09968bb4 100644 --- a/src/pocketmine/block/StillLava.php +++ b/src/block/Beacon.php @@ -23,11 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; -class StillLava extends Lava{ +final class Beacon extends Transparent{ - protected $id = self::STILL_LAVA; - - public function getName() : string{ - return "Still Lava"; + public function getLightLevel() : int{ + return 15; } + + //TODO } diff --git a/src/block/Bed.php b/src/block/Bed.php new file mode 100644 index 0000000000..fe419547af --- /dev/null +++ b/src/block/Bed.php @@ -0,0 +1,219 @@ +color = DyeColor::RED(); + parent::__construct($idInfo, $name, $breakInfo); + } + + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing) | + ($this->occupied ? BlockLegacyMetadata::BED_FLAG_OCCUPIED : 0) | + ($this->head ? BlockLegacyMetadata::BED_FLAG_HEAD : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + $this->occupied = ($stateMeta & BlockLegacyMetadata::BED_FLAG_OCCUPIED) !== 0; + $this->head = ($stateMeta & BlockLegacyMetadata::BED_FLAG_HEAD) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + //read extra state information from the tile - this is an ugly hack + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileBed){ + $this->color = $tile->getColor(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + //extra block properties storage hack + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileBed){ + $tile->setColor($this->color); + } + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 16)]; + } + + public function isHeadPart() : bool{ + return $this->head; + } + + /** @return $this */ + public function setHead(bool $head) : self{ + $this->head = $head; + return $this; + } + + public function isOccupied() : bool{ + return $this->occupied; + } + + /** @return $this */ + public function setOccupied(bool $occupied = true) : self{ + $this->occupied = $occupied; + return $this; + } + + private function getOtherHalfSide() : int{ + return $this->head ? Facing::opposite($this->facing) : $this->facing; + } + + public function getOtherHalf() : ?Bed{ + $other = $this->getSide($this->getOtherHalfSide()); + if($other instanceof Bed and $other->head !== $this->head and $other->facing === $this->facing){ + return $other; + } + + return null; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $other = $this->getOtherHalf(); + $playerPos = $player->getPosition(); + if($other === null){ + $player->sendMessage(TextFormat::GRAY . "This bed is incomplete"); + + return true; + }elseif($playerPos->distanceSquared($this->position) > 4 and $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); + + if(!$isNight){ + $player->sendMessage(KnownTranslationFactory::tile_bed_tooFar()->prefix(TextFormat::GRAY)); + + return true; + } + + $b = ($this->isHeadPart() ? $this : $other); + + if($b->isOccupied()){ + $player->sendMessage(KnownTranslationFactory::tile_bed_occupied()->prefix(TextFormat::GRAY)); + + return true; + } + + $player->sleepOn($b->position); + } + + return true; + + } + + public function onNearbyBlockChange() : void{ + if(($other = $this->getOtherHalf()) !== null and $other->occupied !== $this->occupied){ + $this->occupied = $other->occupied; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + + public function onEntityLand(Entity $entity) : ?float{ + if($entity instanceof Living && $entity->isSneaking()){ + return null; + } + $entity->fallDistance *= 0.5; + return $entity->getMotion()->y * -3 / 4; // 2/3 in Java, according to the wiki + } + + 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->isTransparent()){ + $this->facing = $player !== null ? $player->getHorizontalFacing() : Facing::NORTH; + + $next = $this->getSide($this->getOtherHalfSide()); + if($next->canBeReplaced() and !$next->getSide(Facing::DOWN)->isTransparent()){ + $nextState = clone $this; + $nextState->head = true; + $tx->addBlock($blockReplace->position, $this)->addBlock($next->position, $nextState); + return true; + } + } + + return false; + } + + public function getDrops(Item $item) : array{ + if($this->head){ + return parent::getDrops($item); + } + + return []; + } + + protected function writeStateToItemMeta() : int{ + return DyeColorIdMap::getInstance()->toId($this->color); + } + + public function getAffectedBlocks() : array{ + if(($other = $this->getOtherHalf()) !== null){ + return [$this, $other]; + } + + return parent::getAffectedBlocks(); + } +} diff --git a/src/pocketmine/block/Netherrack.php b/src/block/Bedrock.php similarity index 58% rename from src/pocketmine/block/Netherrack.php rename to src/block/Bedrock.php index e89d12f7a1..e794e39583 100644 --- a/src/pocketmine/block/Netherrack.php +++ b/src/block/Bedrock.php @@ -23,33 +23,29 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\TieredTool; +class Bedrock extends Opaque{ -class Netherrack extends Solid{ + private bool $burnsForever = false; - protected $id = self::NETHERRACK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->burnsForever = ($stateMeta & BlockLegacyMetadata::BEDROCK_FLAG_INFINIBURN) !== 0; } - public function getName() : string{ - return "Netherrack"; + protected function writeStateToMeta() : int{ + return $this->burnsForever ? BlockLegacyMetadata::BEDROCK_FLAG_INFINIBURN : 0; } - public function getHardness() : float{ - return 0.4; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; + public function getStateBitmask() : int{ + return 0b1; } public function burnsForever() : bool{ - return true; + return $this->burnsForever; + } + + /** @return $this */ + public function setBurnsForever(bool $burnsForever) : self{ + $this->burnsForever = $burnsForever; + return $this; } } diff --git a/src/block/Beetroot.php b/src/block/Beetroot.php new file mode 100644 index 0000000000..51d05688e8 --- /dev/null +++ b/src/block/Beetroot.php @@ -0,0 +1,48 @@ +age >= 7){ + return [ + VanillaItems::BEETROOT(), + VanillaItems::BEETROOT_SEEDS()->setCount(mt_rand(0, 3)) + ]; + } + + return [ + VanillaItems::BEETROOT_SEEDS() + ]; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaItems::BEETROOT_SEEDS(); + } +} diff --git a/src/block/Bell.php b/src/block/Bell.php new file mode 100644 index 0000000000..2b33610b64 --- /dev/null +++ b/src/block/Bell.php @@ -0,0 +1,160 @@ +attachmentType = BellAttachmentType::FLOOR(); + parent::__construct($idInfo, $name, $breakInfo); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->setFacing(BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03)); + + $attachmentType = [ + BlockLegacyMetadata::BELL_ATTACHMENT_FLOOR => BellAttachmentType::FLOOR(), + BlockLegacyMetadata::BELL_ATTACHMENT_CEILING => BellAttachmentType::CEILING(), + BlockLegacyMetadata::BELL_ATTACHMENT_ONE_WALL => BellAttachmentType::ONE_WALL(), + BlockLegacyMetadata::BELL_ATTACHMENT_TWO_WALLS => BellAttachmentType::TWO_WALLS() + ][($stateMeta >> 2) & 0b11] ?? null; + if($attachmentType === null){ + throw new InvalidBlockStateException("No such attachment type"); + } + $this->setAttachmentType($attachmentType); + } + + public function writeStateToMeta() : int{ + $attachmentTypeMeta = [ + BellAttachmentType::FLOOR()->id() => BlockLegacyMetadata::BELL_ATTACHMENT_FLOOR, + BellAttachmentType::CEILING()->id() => BlockLegacyMetadata::BELL_ATTACHMENT_CEILING, + BellAttachmentType::ONE_WALL()->id() => BlockLegacyMetadata::BELL_ATTACHMENT_ONE_WALL, + BellAttachmentType::TWO_WALLS()->id() => BlockLegacyMetadata::BELL_ATTACHMENT_TWO_WALLS + ][$this->getAttachmentType()->id()] ?? null; + if($attachmentTypeMeta === null){ + throw new AssumptionFailedError("Mapping should cover all cases"); + } + return BlockDataSerializer::writeLegacyHorizontalFacing($this->getFacing()) | ($attachmentTypeMeta << 2); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getAttachmentType() : BellAttachmentType{ return $this->attachmentType; } + + /** @return $this */ + public function setAttachmentType(BellAttachmentType $attachmentType) : self{ + $this->attachmentType = $attachmentType; + return $this; + } + + private function canBeSupportedBy(Block $block) : bool{ + //TODO: this isn't the actual logic, but it's the closest approximation we can support for now + return $block->isSolid(); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face === Facing::UP){ + if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->down()))){ + return false; + } + if($player !== null){ + $this->setFacing(Facing::opposite($player->getHorizontalFacing())); + } + $this->setAttachmentType(BellAttachmentType::FLOOR()); + }elseif($face === Facing::DOWN){ + if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->up()))){ + return false; + } + $this->setAttachmentType(BellAttachmentType::CEILING()); + }else{ + $this->setFacing($face); + if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide(Facing::opposite($face))))){ + $this->setAttachmentType(BellAttachmentType::ONE_WALL()); + }else{ + return false; + } + if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide($face)))){ + $this->setAttachmentType(BellAttachmentType::TWO_WALLS()); + } + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + if( + ($this->attachmentType->equals(BellAttachmentType::CEILING()) && !$this->canBeSupportedBy($this->getSide(Facing::UP))) || + ($this->attachmentType->equals(BellAttachmentType::FLOOR()) && !$this->canBeSupportedBy($this->getSide(Facing::DOWN))) || + ($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) && !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing)))) || + ($this->attachmentType->equals(BellAttachmentType::TWO_WALLS()) && (!$this->canBeSupportedBy($this->getSide($this->facing)) || !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing))))) + ){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $faceHit = Facing::opposite($player->getHorizontalFacing()); + if($this->attachmentType->equals(BellAttachmentType::CEILING())){ + $this->ring($faceHit); + } + if($this->attachmentType->equals(BellAttachmentType::FLOOR()) && Facing::axis($faceHit) === Facing::axis($this->facing)){ + $this->ring($faceHit); + } + if( + ($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) || $this->attachmentType->equals(BellAttachmentType::TWO_WALLS())) && + ($faceHit === Facing::rotateY($this->facing, false) || $faceHit === Facing::rotateY($this->facing, true)) + ){ + $this->ring($faceHit); + } + } + + return true; + } + + public function ring(int $faceHit) : void{ + $this->position->getWorld()->addSound($this->position, new BellRingSound()); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileBell){ + $this->position->getWorld()->broadcastPacketToViewers($this->position, $tile->createFakeUpdatePacket($faceHit)); + } + } +} diff --git a/src/block/Block.php b/src/block/Block.php new file mode 100644 index 0000000000..414d9b6494 --- /dev/null +++ b/src/block/Block.php @@ -0,0 +1,637 @@ +getVariant() & $this->getStateBitmask()) !== 0){ + throw new \InvalidArgumentException("Variant 0x" . dechex($idInfo->getVariant()) . " collides with state bitmask 0x" . dechex($this->getStateBitmask())); + } + $this->idInfo = $idInfo; + $this->fallbackName = $name; + $this->breakInfo = $breakInfo; + $this->position = new Position(0, 0, 0, null); + } + + public function __clone(){ + $this->position = clone $this->position; + } + + public function getIdInfo() : BlockIdentifier{ + return $this->idInfo; + } + + public function getName() : string{ + return $this->fallbackName; + } + + public function getId() : int{ + return $this->idInfo->getBlockId(); + } + + /** + * @internal + */ + public function getFullId() : int{ + return ($this->getId() << self::INTERNAL_METADATA_BITS) | $this->getMeta(); + } + + public function asItem() : Item{ + return ItemFactory::getInstance()->get( + $this->idInfo->getItemId(), + $this->idInfo->getVariant() | $this->writeStateToItemMeta() + ); + } + + public function getMeta() : int{ + $stateMeta = $this->writeStateToMeta(); + assert(($stateMeta & ~$this->getStateBitmask()) === 0); + return $this->idInfo->getVariant() | $stateMeta; + } + + protected function writeStateToItemMeta() : int{ + return 0; + } + + /** + * Returns a bitmask used to extract state bits from block metadata. + */ + public function getStateBitmask() : int{ + return 0; + } + + protected function writeStateToMeta() : int{ + return 0; + } + + /** + * @throws InvalidBlockStateException + */ + public function readStateFromData(int $id, int $stateMeta) : void{ + //NOOP + } + + /** + * Called when this block is created, set, or has a neighbouring block update, to re-detect dynamic properties which + * are not saved on the world. + * + * Clears any cached precomputed objects, such as bounding boxes. Remove any outdated precomputed things such as + * AABBs and force recalculation. + */ + public function readStateFromWorld() : void{ + $this->collisionBoxes = null; + } + + public function writeStateToWorld() : void{ + $this->position->getWorld()->getOrLoadChunkAtPosition($this->position)->setFullBlock($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getFullId()); + + $tileType = $this->idInfo->getTileClass(); + $oldTile = $this->position->getWorld()->getTile($this->position); + if($oldTile !== null){ + if($tileType === null or !($oldTile instanceof $tileType)){ + $oldTile->close(); + $oldTile = null; + }elseif($oldTile instanceof Spawnable){ + $oldTile->setDirty(); //destroy old network cache + } + } + if($oldTile === null and $tileType !== null){ + /** + * @var Tile $tile + * @see Tile::__construct() + */ + $tile = new $tileType($this->position->getWorld(), $this->position->asVector3()); + $this->position->getWorld()->addTile($tile); + } + } + + /** + * Returns whether the given block has an equivalent type to this one. This compares base legacy ID and variant. + * + * 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(); + } + + /** + * Returns whether the given block has the same type and properties as this block. + */ + public function isSameState(Block $other) : bool{ + return $this->getFullId() === $other->getFullId(); + } + + /** + * AKA: Block->isPlaceable + */ + public function canBePlaced() : bool{ + return true; + } + + public function canBeReplaced() : bool{ + return false; + } + + public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ + return $blockReplace->canBeReplaced(); + } + + /** + * Places the Block, using block space and block target, and side. Returns if the block has been placed. + */ + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $tx->addBlock($blockReplace->position, $this); + return true; + } + + public function onPostPlace() : void{ + + } + + /** + * Returns an object containing information about the destruction requirements of this block. + */ + public function getBreakInfo() : BlockBreakInfo{ + return $this->breakInfo; + } + + /** + * Do the actions needed so the block is broken with the Item + */ + public function onBreak(Item $item, ?Player $player = null) : bool{ + if(($t = $this->position->getWorld()->getTile($this->position)) !== null){ + $t->onBlockDestroyed(); + } + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + return true; + } + + /** + * Called when this block or a block immediately adjacent to it changes state. + */ + public function onNearbyBlockChange() : void{ + + } + + /** + * Returns whether random block updates will be done on this block. + */ + public function ticksRandomly() : bool{ + return false; + } + + /** + * Called when this block is randomly updated due to chunk ticking. + * WARNING: This will not be called if ticksRandomly() does not return true! + */ + public function onRandomTick() : void{ + + } + + /** + * Called when this block is updated by the delayed blockupdate scheduler in the world. + */ + public function onScheduledUpdate() : void{ + + } + + /** + * Do actions when interacted by Item. Returns if it has done anything + */ + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + return false; + } + + /** + * Called when this block is attacked (left-clicked). This is called when a player left-clicks the block to try and + * start to break it in survival mode. + * + * @return bool if an action took place, prevents starting to break the block if true. + */ + public function onAttack(Item $item, int $face, ?Player $player = null) : bool{ + return false; + } + + public function getFrictionFactor() : float{ + return 0.6; + } + + /** + * @return int 0-15 + */ + public function getLightLevel() : int{ + return 0; + } + + /** + * Returns the amount of light this block will filter out when light passes through this block. + * This value is used in light spread calculation. + * + * @return int 0-15 + */ + public function getLightFilter() : int{ + return $this->isTransparent() ? 0 : 15; + } + + /** + * Returns whether this block blocks direct sky light from passing through it. This is independent from the light + * filter value, which is used during propagation. + * + * In most cases, this is the same as isTransparent(); however, some special cases exist such as leaves and cobwebs, + * which don't have any additional effect on light propagation, but don't allow direct sky light to pass through. + */ + public function blocksDirectSkyLight() : bool{ + return $this->getLightFilter() > 0; + } + + public function isTransparent() : bool{ + return false; + } + + public function isSolid() : bool{ + return true; + } + + /** + * AKA: Block->isFlowable + */ + public function canBeFlowedInto() : bool{ + return false; + } + + public function hasEntityCollision() : bool{ + return false; + } + + /** + * Returns whether entities can climb up this block. + */ + public function canClimb() : bool{ + return false; + } + + public function addVelocityToEntity(Entity $entity) : ?Vector3{ + return null; + } + + final public function getPosition() : Position{ + return $this->position; + } + + /** + * @internal + */ + final public function position(World $world, int $x, int $y, int $z) : void{ + $this->position = new Position($x, $y, $z, $world); + } + + /** + * Returns an array of Item objects to be dropped + * + * @return Item[] + */ + public function getDrops(Item $item) : array{ + if($this->breakInfo->isToolCompatible($item)){ + if($this->isAffectedBySilkTouch() and $item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){ + return $this->getSilkTouchDrops($item); + } + + return $this->getDropsForCompatibleTool($item); + } + + return $this->getDropsForIncompatibleTool($item); + } + + /** + * Returns an array of Items to be dropped when the block is broken using the correct tool type. + * + * @return Item[] + */ + public function getDropsForCompatibleTool(Item $item) : array{ + return [$this->asItem()]; + } + + /** + * Returns the items dropped by this block when broken with an incorrect tool type (or tool with a too-low tier). + * + * @return Item[] + */ + public function getDropsForIncompatibleTool(Item $item) : array{ + return []; + } + + /** + * Returns an array of Items to be dropped when the block is broken using a compatible Silk Touch-enchanted tool. + * + * @return Item[] + */ + public function getSilkTouchDrops(Item $item) : array{ + return [$this->asItem()]; + } + + /** + * 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)){ + return 0; + } + + return $this->getXpDropAmount(); + } + + /** + * Returns how much XP this block will drop when broken with an appropriate tool. + */ + protected function getXpDropAmount() : int{ + return 0; + } + + /** + * Returns whether Silk Touch enchanted tools will cause this block to drop as itself. + */ + public function isAffectedBySilkTouch() : bool{ + return false; + } + + /** + * Returns the item that players will equip when middle-clicking on this block. + */ + public function getPickedItem(bool $addUserData = false) : Item{ + $item = $this->asItem(); + if($addUserData){ + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof Tile){ + $nbt = $tile->getCleanedNBT(); + if($nbt instanceof CompoundTag){ + $item->setCustomBlockData($nbt); + $item->setLore(["+(DATA)"]); + } + } + } + return $item; + } + + /** + * Returns the time in ticks which the block will fuel a furnace for. + */ + public function getFuelTime() : int{ + return 0; + } + + /** + * Returns the maximum number of this block that can fit into a single item stack. + */ + public function getMaxStackSize() : int{ + return 64; + } + + /** + * Returns the chance that the block will catch fire from nearby fire sources. Higher values lead to faster catching + * fire. + */ + public function getFlameEncouragement() : int{ + return 0; + } + + /** + * Returns the base flammability of this block. Higher values lead to the block burning away more quickly. + */ + public function getFlammability() : int{ + return 0; + } + + /** + * Returns whether fire lit on this block will burn indefinitely. + */ + public function burnsForever() : bool{ + return false; + } + + /** + * Returns whether this block can catch fire. + */ + public function isFlammable() : bool{ + return $this->getFlammability() > 0; + } + + /** + * Called when this block is burned away by being on fire. + */ + public function onIncinerate() : void{ + + } + + /** + * Returns the Block on the side $side, works like Vector3::getSide() + * + * @return Block + */ + public function getSide(int $side, int $step = 1){ + if($this->position->isValid()){ + return $this->position->getWorld()->getBlock($this->position->getSide($side, $step)); + } + + throw new \LogicException("Block does not have a valid world"); + } + + /** + * Returns the 4 blocks on the horizontal axes around the block (north, south, east, west) + * + * @return Block[]|\Generator + * @phpstan-return \Generator + */ + public function getHorizontalSides() : \Generator{ + $world = $this->position->getWorld(); + foreach($this->position->sidesAroundAxis(Axis::Y) as $vector3){ + yield $world->getBlock($vector3); + } + } + + /** + * Returns the six blocks around this block. + * + * @return Block[]|\Generator + * @phpstan-return \Generator + */ + public function getAllSides() : \Generator{ + $world = $this->position->getWorld(); + foreach($this->position->sides() as $vector3){ + yield $world->getBlock($vector3); + } + } + + /** + * Returns a list of blocks that this block is part of. In most cases, only contains the block itself, but in cases + * such as double plants, beds and doors, will contain both halves. + * + * @return Block[] + */ + public function getAffectedBlocks() : array{ + return [$this]; + } + + /** + * @return string + */ + public function __toString(){ + return "Block[" . $this->getName() . "] (" . $this->getId() . ":" . $this->getMeta() . ")"; + } + + /** + * Checks for collision against an AxisAlignedBB + */ + public function collidesWithBB(AxisAlignedBB $bb) : bool{ + foreach($this->getCollisionBoxes() as $bb2){ + if($bb->intersectsWith($bb2)){ + return true; + } + } + + return false; + } + + /** + * Called when an entity's bounding box clips inside this block's cell. Note that the entity may not be intersecting + * with the collision box or bounding box. + * + * @return bool Whether the block is still the same after the intersection. If it changed (e.g. due to an explosive + * being ignited), this should return false. + */ + public function onEntityInside(Entity $entity) : bool{ + return true; + } + + /** + * Called when an entity lands on this block (usually due to falling). + * @return float|null The new vertical velocity of the entity, or null if unchanged. + */ + public function onEntityLand(Entity $entity) : ?float{ + return null; + } + + /** + * @return AxisAlignedBB[] + */ + final public function getCollisionBoxes() : array{ + if($this->collisionBoxes === null){ + $this->collisionBoxes = $this->recalculateCollisionBoxes(); + $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); + } + } + + return $this->collisionBoxes; + } + + /** + * 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 getModelPositionOffset() : ?Vector3{ + return null; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()]; + } + + public function isFullCube() : bool{ + $bb = $this->getCollisionBoxes(); + + return count($bb) === 1 and $bb[0]->getAverageEdgeLength() >= 1 and $bb[0]->isCube(); + } + + public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{ + $bbs = $this->getCollisionBoxes(); + if(count($bbs) === 0){ + return null; + } + + /** @var RayTraceResult|null $currentHit */ + $currentHit = null; + /** @var int|float $currentDistance */ + $currentDistance = PHP_INT_MAX; + + foreach($bbs as $bb){ + $nextHit = $bb->calculateIntercept($pos1, $pos2); + if($nextHit === null){ + continue; + } + + $nextDistance = $nextHit->hitVector->distanceSquared($pos1); + if($nextDistance < $currentDistance){ + $currentHit = $nextHit; + $currentDistance = $nextDistance; + } + } + + return $currentHit; + } +} diff --git a/src/block/BlockBreakInfo.php b/src/block/BlockBreakInfo.php new file mode 100644 index 0000000000..0290b02ab2 --- /dev/null +++ b/src/block/BlockBreakInfo.php @@ -0,0 +1,148 @@ +hardness = $hardness; + $this->toolType = $toolType; + $this->toolHarvestLevel = $toolHarvestLevel; + $this->blastResistance = $blastResistance ?? $hardness * 5; + } + + public static function instant(int $toolType = BlockToolType::NONE, int $toolHarvestLevel = 0) : self{ + return new self(0.0, $toolType, $toolHarvestLevel, 0.0); + } + + public static function indestructible(float $blastResistance = 18000000.0) : self{ + return new self(-1.0, BlockToolType::NONE, 0, $blastResistance); + } + + /** + * Returns a base value used to compute block break times. + */ + public function getHardness() : float{ + return $this->hardness; + } + + /** + * Returns whether the block can be broken at all. + */ + public function isBreakable() : bool{ + return $this->hardness >= 0; + } + + /** + * Returns whether this block can be instantly broken. + */ + public function breaksInstantly() : bool{ + return $this->hardness == 0.0; + } + + /** + * Returns the block's resistance to explosions. Usually 5x hardness. + */ + public function getBlastResistance() : float{ + return $this->blastResistance; + } + + public function getToolType() : int{ + return $this->toolType; + } + + /** + * Returns the level of tool required to harvest the block (for normal blocks). When the tool type matches the + * block's required tool type, the tool must have a harvest level greater than or equal to this value to be able to + * successfully harvest the block. + * + * If the block requires a specific minimum tier of tiered tool, the minimum tier required should be returned. + * Otherwise, 1 should be returned if a tool is required, 0 if not. + * + * @see Item::getBlockToolHarvestLevel() + */ + public function getToolHarvestLevel() : int{ + return $this->toolHarvestLevel; + } + + /** + * Returns whether the specified item is the proper tool to use for breaking this block. This checks tool type and + * harvest level requirement. + * + * In most cases this is also used to determine whether block drops should be created or not, except in some + * special cases such as vines. + */ + public function isToolCompatible(Item $tool) : bool{ + if($this->hardness < 0){ + return false; + } + + return $this->toolType === BlockToolType::NONE or $this->toolHarvestLevel === 0 or ( + ($this->toolType & $tool->getBlockToolType()) !== 0 and $tool->getBlockToolHarvestLevel() >= $this->toolHarvestLevel); + } + + /** + * Returns the seconds that this block takes to be broken using an specific Item + * + * @throws \InvalidArgumentException if the item efficiency is not a positive number + */ + public function getBreakTime(Item $item) : float{ + $base = $this->hardness; + if($this->isToolCompatible($item)){ + $base *= self::COMPATIBLE_TOOL_MULTIPLIER; + }else{ + $base *= self::INCOMPATIBLE_TOOL_MULTIPLIER; + } + + $efficiency = $item->getMiningEfficiency(($this->toolType & $item->getBlockToolType()) !== 0); + if($efficiency <= 0){ + throw new \InvalidArgumentException(get_class($item) . " has invalid mining efficiency: expected >= 0, got $efficiency"); + } + + $base /= $efficiency; + + return $base; + } +} diff --git a/src/block/BlockFactory.php b/src/block/BlockFactory.php new file mode 100644 index 0000000000..fd98293c5f --- /dev/null +++ b/src/block/BlockFactory.php @@ -0,0 +1,1067 @@ + + */ + private $fullList; + + /** + * @var \SplFixedArray|int[] + * @phpstan-var \SplFixedArray + */ + private \SplFixedArray $mappedStateIds; + + /** + * @var \SplFixedArray|int[] + * @phpstan-var \SplFixedArray + */ + public $light; + /** + * @var \SplFixedArray|int[] + * @phpstan-var \SplFixedArray + */ + public $lightFilter; + /** + * @var \SplFixedArray|bool[] + * @phpstan-var \SplFixedArray + */ + public $blocksDirectSkyLight; + /** + * @var \SplFixedArray|float[] + * @phpstan-var \SplFixedArray + */ + public $blastResistance; + + 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)); + $this->blocksDirectSkyLight = \SplFixedArray::fromArray(array_fill(0, 1024 << Block::INTERNAL_METADATA_BITS, false)); + $this->blastResistance = \SplFixedArray::fromArray(array_fill(0, 1024 << Block::INTERNAL_METADATA_BITS, 0.0)); + + $railBreakInfo = new BlockBreakInfo(0.7); + $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 BlockBreakInfo(2.0 /* 1.0 in PC */, BlockToolType::AXE))); + $this->registerAllMeta(new BambooSapling(new BID(Ids::BAMBOO_SAPLING, 0), "Bamboo Sapling", BlockBreakInfo::instant())); + + $bannerBreakInfo = new BlockBreakInfo(1.0, BlockToolType::AXE); + $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->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->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->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->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->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->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->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->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->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->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->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->registerAllMeta(new HardenedClay(new BID(Ids::HARDENED_CLAY, 0), "Hardened Clay", $hardenedClayBreakInfo)); + + $hardenedGlassBreakInfo = new BlockBreakInfo(10.0); + $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->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->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->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 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->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))); + $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->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->registerAllMeta($pumpkin = new Opaque(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->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->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->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->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->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->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->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; + } + }, + 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); + $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->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", new BlockBreakInfo(0.6))); + + $weightedPressurePlateBreakInfo = new BlockBreakInfo(0.5, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel()); + $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); + $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(); + $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 + $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->remap($magicNumber >= 4 ? Ids::LOG2 : Ids::LOG, ($magicNumber & 0x03) | 0b1100, $wood); + + $allSidedLogs[] = $wood; + $allSidedLogs[] = new Wood(new BID(Ids::WOOD, $magicNumber | BlockLegacyMetadata::WOOD_FLAG_STRIPPED), "Stripped $name Wood", $logBreakInfo, $treeType, true); + + $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->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 => "", + Meta::SANDSTONE_CHISELED => "Chiseled ", + Meta::SANDSTONE_CUT => "Cut ", + Meta::SANDSTONE_SMOOTH => "Smooth " + ]; + $sandstoneBreakInfo = new BlockBreakInfo(0.8, BlockToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel()); + $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){ + $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->registerAllMeta(new GlazedTerracotta(BlockLegacyIdHelper::getGlazedTerracottaIdentifier($color), $coloredName("Glazed Terracotta"), $glazedTerracottaBreakInfo)); + } + $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){ + $time *= 3; //shears break compatible blocks 15x faster, but wool 5x + } + + return $time; + } + })); + + //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->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->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->registerAllMeta(new ChemicalHeat(new BID(Ids::CHEMICAL_HEAT, 0), "Heat Block", $chemistryTableBreakInfo)); + + $this->registerMushroomBlocks(); + + $this->registerAllMeta(new Coral( + new BID(Ids::CORAL, 0), + "Coral", + BlockBreakInfo::instant(), + )); + $this->registerAllMeta(new FloorCoralFan( + new BlockIdentifierFlattened(Ids::CORAL_FAN, [Ids::CORAL_FAN_DEAD], 0, ItemIds::CORAL_FAN), + "Coral Fan", + BlockBreakInfo::instant(), + )); + $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(), + )); + + //region --- auto-generated TODOs for bedrock-1.11.0 --- + //TODO: minecraft:bubble_column + //TODO: minecraft:campfire + //TODO: minecraft:cartography_table + //TODO: minecraft:cauldron + //TODO: minecraft:chain_command_block + //TODO: minecraft:chorus_flower + //TODO: minecraft:chorus_plant + //TODO: minecraft:command_block + //TODO: minecraft:composter + //TODO: minecraft:conduit + //TODO: minecraft:dispenser + //TODO: minecraft:dropper + //TODO: minecraft:end_gateway + //TODO: minecraft:end_portal + //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 + //TODO: minecraft:pistonArmCollision + //TODO: minecraft:repeating_command_block + //TODO: minecraft:scaffolding + //TODO: minecraft:seagrass + //TODO: minecraft:smithing_table + //TODO: minecraft:sticky_piston + //TODO: minecraft:stonecutter_block + //TODO: minecraft:structure_block + //TODO: minecraft:turtle_egg + //endregion + + //region --- auto-generated TODOs for bedrock-1.13.0 --- + //TODO: minecraft:camera + //TODO: minecraft:light_block + //TODO: minecraft:stickyPistonArmCollision + //TODO: minecraft:structure_void + //TODO: minecraft:wither_rose + //endregion + + //region --- auto-generated TODOs for bedrock-1.14.0 --- + //TODO: minecraft:bee_nest + //TODO: minecraft:beehive + //TODO: minecraft:honey_block + //TODO: minecraft:honeycomb_block + //endregion + + //region --- auto-generated TODOs for bedrock-1.16.0 --- + //TODO: minecraft:allow + //TODO: minecraft:ancient_debris + //TODO: minecraft:basalt + //TODO: minecraft:blackstone + //TODO: minecraft:blackstone_double_slab + //TODO: minecraft:blackstone_slab + //TODO: minecraft:blackstone_stairs + //TODO: minecraft:blackstone_wall + //TODO: minecraft:border_block + //TODO: minecraft:chain + //TODO: minecraft:chiseled_nether_bricks + //TODO: minecraft:chiseled_polished_blackstone + //TODO: minecraft:cracked_nether_bricks + //TODO: minecraft:cracked_polished_blackstone_bricks + //TODO: minecraft:crimson_button + //TODO: minecraft:crimson_door + //TODO: minecraft:crimson_double_slab + //TODO: minecraft:crimson_fence + //TODO: minecraft:crimson_fence_gate + //TODO: minecraft:crimson_fungus + //TODO: minecraft:crimson_hyphae + //TODO: minecraft:crimson_nylium + //TODO: minecraft:crimson_planks + //TODO: minecraft:crimson_pressure_plate + //TODO: minecraft:crimson_roots + //TODO: minecraft:crimson_slab + //TODO: minecraft:crimson_stairs + //TODO: minecraft:crimson_standing_sign + //TODO: minecraft:crimson_stem + //TODO: minecraft:crimson_trapdoor + //TODO: minecraft:crimson_wall_sign + //TODO: minecraft:crying_obsidian + //TODO: minecraft:deny + //TODO: minecraft:gilded_blackstone + //TODO: minecraft:lodestone + //TODO: minecraft:nether_gold_ore + //TODO: minecraft:nether_sprouts + //TODO: minecraft:netherite_block + //TODO: minecraft:polished_basalt + //TODO: minecraft:polished_blackstone + //TODO: minecraft:polished_blackstone_brick_double_slab + //TODO: minecraft:polished_blackstone_brick_slab + //TODO: minecraft:polished_blackstone_brick_stairs + //TODO: minecraft:polished_blackstone_brick_wall + //TODO: minecraft:polished_blackstone_bricks + //TODO: minecraft:polished_blackstone_button + //TODO: minecraft:polished_blackstone_double_slab + //TODO: minecraft:polished_blackstone_pressure_plate + //TODO: minecraft:polished_blackstone_slab + //TODO: minecraft:polished_blackstone_stairs + //TODO: minecraft:polished_blackstone_wall + //TODO: minecraft:quartz_bricks + //TODO: minecraft:respawn_anchor + //TODO: minecraft:shroomlight + //TODO: minecraft:soul_campfire + //TODO: minecraft:soul_fire + //TODO: minecraft:soul_lantern + //TODO: minecraft:soul_soil + //TODO: minecraft:soul_torch + //TODO: minecraft:stripped_crimson_hyphae + //TODO: minecraft:stripped_crimson_stem + //TODO: minecraft:stripped_warped_hyphae + //TODO: minecraft:stripped_warped_stem + //TODO: minecraft:target + //TODO: minecraft:twisting_vines + //TODO: minecraft:warped_button + //TODO: minecraft:warped_door + //TODO: minecraft:warped_double_slab + //TODO: minecraft:warped_fence + //TODO: minecraft:warped_fence_gate + //TODO: minecraft:warped_fungus + //TODO: minecraft:warped_hyphae + //TODO: minecraft:warped_nylium + //TODO: minecraft:warped_planks + //TODO: minecraft:warped_pressure_plate + //TODO: minecraft:warped_roots + //TODO: minecraft:warped_slab + //TODO: minecraft:warped_stairs + //TODO: minecraft:warped_standing_sign + //TODO: minecraft:warped_stem + //TODO: minecraft:warped_trapdoor + //TODO: minecraft:warped_wall_sign + //TODO: minecraft:warped_wart_block + //TODO: minecraft:weeping_vines + //endregion + } + + private function registerMushroomBlocks() : void{ + //shrooms have to be handled one by one because some metas are variants and others aren't, and they can't be + //separated by a bitmask + + $mushroomBlockBreakInfo = new BlockBreakInfo(0.2, BlockToolType::AXE); + + $mushroomBlocks = [ + new BrownMushroomBlock(new BID(Ids::BROWN_MUSHROOM_BLOCK, 0), "Brown Mushroom Block", $mushroomBlockBreakInfo), + new RedMushroomBlock(new BID(Ids::RED_MUSHROOM_BLOCK, 0), "Red Mushroom Block", $mushroomBlockBreakInfo) + ]; + + //caps + foreach([ + Meta::MUSHROOM_BLOCK_ALL_PORES, + Meta::MUSHROOM_BLOCK_CAP_NORTHWEST_CORNER, + Meta::MUSHROOM_BLOCK_CAP_NORTH_SIDE, + Meta::MUSHROOM_BLOCK_CAP_NORTHEAST_CORNER, + Meta::MUSHROOM_BLOCK_CAP_WEST_SIDE, + Meta::MUSHROOM_BLOCK_CAP_TOP_ONLY, + Meta::MUSHROOM_BLOCK_CAP_EAST_SIDE, + Meta::MUSHROOM_BLOCK_CAP_SOUTHWEST_CORNER, + Meta::MUSHROOM_BLOCK_CAP_SOUTH_SIDE, + Meta::MUSHROOM_BLOCK_CAP_SOUTHEAST_CORNER, + Meta::MUSHROOM_BLOCK_ALL_CAP, + ] as $meta){ + foreach($mushroomBlocks as $block){ + $block->readStateFromData($block->getId(), $meta); + $this->remap($block->getId(), $meta, clone $block); + } + } + + //and the invalid states + for($meta = 11; $meta <= 13; ++$meta){ + foreach($mushroomBlocks as $block){ + $this->remap($block->getId(), $meta, clone $block); + } + } + + //finally, the stems + $mushroomStem = new MushroomStem(new BID(Ids::BROWN_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_STEM), "Mushroom Stem", $mushroomBlockBreakInfo); + $this->remap(Ids::BROWN_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_STEM, $mushroomStem); + $this->remap(Ids::RED_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_STEM, $mushroomStem); + $allSidedMushroomStem = new MushroomStem(new BID(Ids::BROWN_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_ALL_STEM), "All Sided Mushroom Stem", $mushroomBlockBreakInfo); + $this->remap(Ids::BROWN_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_ALL_STEM, $allSidedMushroomStem); + $this->remap(Ids::RED_MUSHROOM_BLOCK, Meta::MUSHROOM_BLOCK_ALL_STEM, $allSidedMushroomStem); + } + + private function registerElements() : void{ + $instaBreak = BlockBreakInfo::instant(); + $this->registerAllMeta(new Opaque(new BID(Ids::ELEMENT_0, 0), "???", $instaBreak)); + + $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{ + $this->register($block); + $identifierFlattened = $block->getIdInfo(); + if($identifierFlattened instanceof BlockIdentifierFlattened){ + $this->remap($identifierFlattened->getSecondId(), $identifierFlattened->getVariant() | 0x8, $block->setSlabType(SlabType::DOUBLE())); + } + } + + /** + * Maps a block type to its corresponding ID. This is necessary to ensure that the block is correctly loaded when + * reading from disk storage. + * + * NOTE: If you are registering a new block type, you will need to add it to the creative inventory yourself - it + * will not automatically appear there. + * + * @param bool $override Whether to override existing registrations + * + * @throws \RuntimeException if something attempted to override an already-registered block without specifying the + * $override parameter. + */ + public function register(Block $block, bool $override = false) : void{ + $variant = $block->getIdInfo()->getVariant(); + + $stateMask = $block->getStateBitmask(); + if(($variant & $stateMask) !== 0){ + throw new \InvalidArgumentException("Block variant collides with state bitmask"); + } + + foreach($block->getIdInfo()->getAllBlockIds() as $id){ + if(!$override and $this->isRegistered($id, $variant)){ + throw new \InvalidArgumentException("Block registration $id:$variant conflicts with an existing block"); + } + + for($m = $variant; $m <= ($variant | $stateMask); ++$m){ + if(($m & ~$stateMask) !== $variant){ + continue; + } + + if(!$override and $this->isRegistered($id, $m)){ + throw new \InvalidArgumentException("Block registration " . get_class($block) . " has states which conflict with other blocks"); + } + + $index = ($id << Block::INTERNAL_METADATA_BITS) | $m; + + $v = clone $block; + try{ + $v->readStateFromData($id, $m); + if($v->getFullId() !== $index){ + //if the fullID comes back different, this is a broken state that we can't rely on; map it to default + throw new InvalidBlockStateException("Corrupted state"); + } + }catch(InvalidBlockStateException $e){ //invalid property combination, fill the default state + $this->fillStaticArrays($index, $block); + continue; + } + + $this->fillStaticArrays($index, $v); + } + } + } + + public function remap(int $id, int $meta, Block $block) : void{ + $index = ($id << Block::INTERNAL_METADATA_BITS) | $meta; + if($this->isRegistered($id, $meta)){ + $existing = $this->fullList[$index]; + if($existing !== null && $existing->getFullId() === $index){ + throw new \InvalidArgumentException("$id:$meta is already mapped"); + }else{ + //if it's not a match, this was already remapped for some reason; remapping overwrites are OK + } + } + $this->fillStaticArrays(($id << Block::INTERNAL_METADATA_BITS) | $meta, $block); + } + + 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(); + $this->blastResistance[$index] = $block->getBreakInfo()->getBlastResistance(); + } + + /** + * @deprecated This method should ONLY be used for deserializing data, e.g. from a config or database. For all other + * purposes, use VanillaBlocks. + * @see VanillaBlocks + * + * 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)){ + throw new \InvalidArgumentException("Block meta value $meta is out of bounds"); + } + + $index = ($id << Block::INTERNAL_METADATA_BITS) | $meta; + if($index < 0 || $index >= $this->fullList->getSize()){ + throw new \InvalidArgumentException("Block ID $id is out of bounds"); + } + if($this->fullList[$index] !== null){ + $block = clone $this->fullList[$index]; + }else{ + $block = new UnknownBlock(new BID($id, $meta), BlockBreakInfo::instant()); + } + + return $block; + } + + public function fromFullBlock(int $fullState) : Block{ + return $this->get($fullState >> Block::INTERNAL_METADATA_BITS, $fullState & Block::INTERNAL_METADATA_MASK); + } + + /** + * Returns whether a specified block state is already registered in the block factory. + */ + 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 Block[] + */ + 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 new file mode 100644 index 0000000000..5ea45c90b0 --- /dev/null +++ b/src/block/BlockIdentifier.php @@ -0,0 +1,71 @@ +|null */ + private ?string $tileClass; + + /** + * @phpstan-param class-string|null $tileClass + */ + public function __construct(int $blockId, int $variant, ?int $itemId = null, ?string $tileClass = null){ + $this->blockId = $blockId; + $this->variant = $variant; + $this->itemId = $itemId; + $this->tileClass = $tileClass; + } + + public function getBlockId() : int{ + return $this->blockId; + } + + /** + * @return int[] + */ + public function getAllBlockIds() : array{ + return [$this->blockId]; + } + + public function getVariant() : int{ + return $this->variant; + } + + public function getItemId() : int{ + return $this->itemId ?? ($this->blockId > 255 ? 255 - $this->blockId : $this->blockId); + } + + /** + * @phpstan-return class-string|null + */ + public function getTileClass() : ?string{ + return $this->tileClass; + } +} diff --git a/src/block/BlockIdentifierFlattened.php b/src/block/BlockIdentifierFlattened.php new file mode 100644 index 0000000000..1eca84d528 --- /dev/null +++ b/src/block/BlockIdentifierFlattened.php @@ -0,0 +1,59 @@ +additionalIds = $additionalIds; + } + + public function getAdditionalId(int $index) : int{ + if(!isset($this->additionalIds[$index])){ + throw new \InvalidArgumentException("No such ID at index $index"); + } + return $this->additionalIds[$index]; + } + + public function getSecondId() : int{ + return $this->getAdditionalId(0); + } + + public function getAllBlockIds() : array{ + return [$this->getBlockId(), ...$this->additionalIds]; + } +} diff --git a/src/block/BlockLegacyIdHelper.php b/src/block/BlockLegacyIdHelper.php new file mode 100644 index 0000000000..945053824e --- /dev/null +++ b/src/block/BlockLegacyIdHelper.php @@ -0,0 +1,248 @@ +id()){ + case TreeType::OAK()->id(): + return new BID(Ids::SIGN_POST, 0, ItemIds::SIGN, TileSign::class); + case TreeType::SPRUCE()->id(): + return new BID(Ids::SPRUCE_STANDING_SIGN, 0, ItemIds::SPRUCE_SIGN, TileSign::class); + case TreeType::BIRCH()->id(): + return new BID(Ids::BIRCH_STANDING_SIGN, 0, ItemIds::BIRCH_SIGN, TileSign::class); + case TreeType::JUNGLE()->id(): + return new BID(Ids::JUNGLE_STANDING_SIGN, 0, ItemIds::JUNGLE_SIGN, TileSign::class); + case TreeType::ACACIA()->id(): + return new BID(Ids::ACACIA_STANDING_SIGN,0, ItemIds::ACACIA_SIGN, TileSign::class); + case TreeType::DARK_OAK()->id(): + return new BID(Ids::DARKOAK_STANDING_SIGN, 0, ItemIds::DARKOAK_SIGN, TileSign::class); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenWallSignIdentifier(TreeType $treeType) : BID{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BID(Ids::WALL_SIGN, 0, ItemIds::SIGN, TileSign::class); + case TreeType::SPRUCE()->id(): + return new BID(Ids::SPRUCE_WALL_SIGN, 0, ItemIds::SPRUCE_SIGN, TileSign::class); + case TreeType::BIRCH()->id(): + return new BID(Ids::BIRCH_WALL_SIGN, 0, ItemIds::BIRCH_SIGN, TileSign::class); + case TreeType::JUNGLE()->id(): + return new BID(Ids::JUNGLE_WALL_SIGN, 0, ItemIds::JUNGLE_SIGN, TileSign::class); + case TreeType::ACACIA()->id(): + return new BID(Ids::ACACIA_WALL_SIGN, 0, ItemIds::ACACIA_SIGN, TileSign::class); + case TreeType::DARK_OAK()->id(): + return new BID(Ids::DARKOAK_WALL_SIGN, 0, ItemIds::DARKOAK_SIGN, TileSign::class); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenTrapdoorIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::WOODEN_TRAPDOOR, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::SPRUCE_TRAPDOOR, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::BIRCH_TRAPDOOR, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::JUNGLE_TRAPDOOR, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::ACACIA_TRAPDOOR, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::DARK_OAK_TRAPDOOR, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenButtonIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::WOODEN_BUTTON, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::SPRUCE_BUTTON, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::BIRCH_BUTTON, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::JUNGLE_BUTTON, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::ACACIA_BUTTON, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::DARK_OAK_BUTTON, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenPressurePlateIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::WOODEN_PRESSURE_PLATE, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::SPRUCE_PRESSURE_PLATE, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::BIRCH_PRESSURE_PLATE, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::JUNGLE_PRESSURE_PLATE, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::ACACIA_PRESSURE_PLATE, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::DARK_OAK_PRESSURE_PLATE, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenDoorIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BID(Ids::OAK_DOOR_BLOCK, 0, ItemIds::OAK_DOOR); + case TreeType::SPRUCE()->id(): + return new BID(Ids::SPRUCE_DOOR_BLOCK, 0, ItemIds::SPRUCE_DOOR); + case TreeType::BIRCH()->id(): + return new BID(Ids::BIRCH_DOOR_BLOCK, 0, ItemIds::BIRCH_DOOR); + case TreeType::JUNGLE()->id(): + return new BID(Ids::JUNGLE_DOOR_BLOCK, 0, ItemIds::JUNGLE_DOOR); + case TreeType::ACACIA()->id(): + return new BID(Ids::ACACIA_DOOR_BLOCK, 0, ItemIds::ACACIA_DOOR); + case TreeType::DARK_OAK()->id(): + return new BID(Ids::DARK_OAK_DOOR_BLOCK, 0, ItemIds::DARK_OAK_DOOR); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenFenceIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::OAK_FENCE_GATE, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::SPRUCE_FENCE_GATE, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::BIRCH_FENCE_GATE, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::JUNGLE_FENCE_GATE, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::ACACIA_FENCE_GATE, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::DARK_OAK_FENCE_GATE, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getWoodenStairsIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::OAK_STAIRS, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::SPRUCE_STAIRS, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::BIRCH_STAIRS, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::JUNGLE_STAIRS, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::ACACIA_STAIRS, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::DARK_OAK_STAIRS, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getStrippedLogIdentifier(TreeType $treeType) : BlockIdentifier{ + switch($treeType->id()){ + case TreeType::OAK()->id(): + return new BlockIdentifier(Ids::STRIPPED_OAK_LOG, 0); + case TreeType::SPRUCE()->id(): + return new BlockIdentifier(Ids::STRIPPED_SPRUCE_LOG, 0); + case TreeType::BIRCH()->id(): + return new BlockIdentifier(Ids::STRIPPED_BIRCH_LOG, 0); + case TreeType::JUNGLE()->id(): + return new BlockIdentifier(Ids::STRIPPED_JUNGLE_LOG, 0); + case TreeType::ACACIA()->id(): + return new BlockIdentifier(Ids::STRIPPED_ACACIA_LOG, 0); + case TreeType::DARK_OAK()->id(): + return new BlockIdentifier(Ids::STRIPPED_DARK_OAK_LOG, 0); + } + throw new AssumptionFailedError("Switch should cover all wood types"); + } + + public static function getGlazedTerracottaIdentifier(DyeColor $color) : BlockIdentifier{ + switch($color->id()){ + case DyeColor::WHITE()->id(): + return new BlockIdentifier(Ids::WHITE_GLAZED_TERRACOTTA, 0); + case DyeColor::ORANGE()->id(): + return new BlockIdentifier(Ids::ORANGE_GLAZED_TERRACOTTA, 0); + case DyeColor::MAGENTA()->id(): + return new BlockIdentifier(Ids::MAGENTA_GLAZED_TERRACOTTA, 0); + case DyeColor::LIGHT_BLUE()->id(): + return new BlockIdentifier(Ids::LIGHT_BLUE_GLAZED_TERRACOTTA, 0); + case DyeColor::YELLOW()->id(): + return new BlockIdentifier(Ids::YELLOW_GLAZED_TERRACOTTA, 0); + case DyeColor::LIME()->id(): + return new BlockIdentifier(Ids::LIME_GLAZED_TERRACOTTA, 0); + case DyeColor::PINK()->id(): + return new BlockIdentifier(Ids::PINK_GLAZED_TERRACOTTA, 0); + case DyeColor::GRAY()->id(): + return new BlockIdentifier(Ids::GRAY_GLAZED_TERRACOTTA, 0); + case DyeColor::LIGHT_GRAY()->id(): + return new BlockIdentifier(Ids::SILVER_GLAZED_TERRACOTTA, 0); + case DyeColor::CYAN()->id(): + return new BlockIdentifier(Ids::CYAN_GLAZED_TERRACOTTA, 0); + case DyeColor::PURPLE()->id(): + return new BlockIdentifier(Ids::PURPLE_GLAZED_TERRACOTTA, 0); + case DyeColor::BLUE()->id(): + return new BlockIdentifier(Ids::BLUE_GLAZED_TERRACOTTA, 0); + case DyeColor::BROWN()->id(): + return new BlockIdentifier(Ids::BROWN_GLAZED_TERRACOTTA, 0); + case DyeColor::GREEN()->id(): + return new BlockIdentifier(Ids::GREEN_GLAZED_TERRACOTTA, 0); + case DyeColor::RED()->id(): + return new BlockIdentifier(Ids::RED_GLAZED_TERRACOTTA, 0); + case DyeColor::BLACK()->id(): + return new BlockIdentifier(Ids::BLACK_GLAZED_TERRACOTTA, 0); + } + throw new AssumptionFailedError("Switch should cover all colours"); + } + + public static function getStoneSlabIdentifier(int $stoneSlabId, int $meta) : BlockIdentifierFlattened{ + $id = [ + 1 => [Ids::STONE_SLAB, Ids::DOUBLE_STONE_SLAB], + 2 => [Ids::STONE_SLAB2, Ids::DOUBLE_STONE_SLAB2], + 3 => [Ids::STONE_SLAB3, Ids::DOUBLE_STONE_SLAB3], + 4 => [Ids::STONE_SLAB4, Ids::DOUBLE_STONE_SLAB4] + ][$stoneSlabId] ?? null; + if($id === null){ + throw new \InvalidArgumentException("Stone slab type should be 1, 2, 3 or 4"); + } + return new BlockIdentifierFlattened($id[0], [$id[1]], $meta); + } +} diff --git a/src/pocketmine/block/BlockIds.php b/src/block/BlockLegacyIds.php similarity index 56% rename from src/pocketmine/block/BlockIds.php rename to src/block/BlockLegacyIds.php index 7edaca892a..c846c74fc2 100644 --- a/src/pocketmine/block/BlockIds.php +++ b/src/block/BlockLegacyIds.php @@ -23,7 +23,11 @@ declare(strict_types=1); namespace pocketmine\block; -interface BlockIds{ +final class BlockLegacyIds{ + + private function __construct(){ + //NOOP + } public const AIR = 0; public const STONE = 1; @@ -42,7 +46,7 @@ interface BlockIds{ public const GOLD_ORE = 14; public const IRON_ORE = 15; public const COAL_ORE = 16; - public const LOG = 17, WOOD = 17; + public const LOG = 17; public const LEAVES = 18; public const SPONGE = 19; public const GLASS = 20; @@ -61,7 +65,7 @@ interface BlockIds{ public const PISTON = 33; public const PISTONARMCOLLISION = 34, PISTON_ARM_COLLISION = 34; public const WOOL = 35; - + public const ELEMENT_0 = 36; public const DANDELION = 37, YELLOW_FLOWER = 37; public const POPPY = 38, RED_FLOWER = 38; public const BROWN_MUSHROOM = 39; @@ -187,11 +191,11 @@ interface BlockIds{ public const STAINED_CLAY = 159, STAINED_HARDENED_CLAY = 159, TERRACOTTA = 159; public const STAINED_GLASS_PANE = 160; public const LEAVES2 = 161; - public const LOG2 = 162, WOOD2 = 162; + public const LOG2 = 162; public const ACACIA_STAIRS = 163; public const DARK_OAK_STAIRS = 164; public const SLIME = 165, SLIME_BLOCK = 165; - + public const GLOW_STICK = 166; public const IRON_TRAPDOOR = 167; public const PRISMARINE = 168; public const SEALANTERN = 169, SEA_LANTERN = 169; @@ -215,7 +219,9 @@ interface BlockIds{ public const ACACIA_FENCE_GATE = 187; public const REPEATING_COMMAND_BLOCK = 188; public const CHAIN_COMMAND_BLOCK = 189; - + public const HARD_GLASS_PANE = 190; + public const HARD_STAINED_GLASS_PANE = 191; + public const CHEMICAL_HEAT = 192; public const SPRUCE_DOOR_BLOCK = 193; public const BIRCH_DOOR_BLOCK = 194; public const JUNGLE_DOOR_BLOCK = 195; @@ -225,9 +231,9 @@ interface BlockIds{ public const FRAME_BLOCK = 199, ITEM_FRAME_BLOCK = 199; public const CHORUS_FLOWER = 200; public const PURPUR_BLOCK = 201; - + public const COLORED_TORCH_RG = 202; public const PURPUR_STAIRS = 203; - + public const COLORED_TORCH_BP = 204; public const UNDYED_SHULKER_BOX = 205; public const END_BRICKS = 206; public const FROSTED_ICE = 207; @@ -259,7 +265,8 @@ interface BlockIds{ public const BLACK_GLAZED_TERRACOTTA = 235; public const CONCRETE = 236; public const CONCRETEPOWDER = 237, CONCRETE_POWDER = 237; - + public const CHEMISTRY_TABLE = 238; + public const UNDERWATER_TORCH = 239; public const CHORUS_PLANT = 240; public const STAINED_GLASS = 241; @@ -273,7 +280,222 @@ interface BlockIds{ public const MOVINGBLOCK = 250, MOVING_BLOCK = 250; public const OBSERVER = 251; public const STRUCTURE_BLOCK = 252; - + public const HARD_GLASS = 253; + public const HARD_STAINED_GLASS = 254; public const RESERVED6 = 255; + public const PRISMARINE_STAIRS = 257; + public const DARK_PRISMARINE_STAIRS = 258; + public const PRISMARINE_BRICKS_STAIRS = 259; + public const STRIPPED_SPRUCE_LOG = 260; + public const STRIPPED_BIRCH_LOG = 261; + public const STRIPPED_JUNGLE_LOG = 262; + public const STRIPPED_ACACIA_LOG = 263; + public const STRIPPED_DARK_OAK_LOG = 264; + public const STRIPPED_OAK_LOG = 265; + public const BLUE_ICE = 266; + public const ELEMENT_1 = 267; + public const ELEMENT_2 = 268; + public const ELEMENT_3 = 269; + public const ELEMENT_4 = 270; + public const ELEMENT_5 = 271; + public const ELEMENT_6 = 272; + public const ELEMENT_7 = 273; + public const ELEMENT_8 = 274; + public const ELEMENT_9 = 275; + public const ELEMENT_10 = 276; + public const ELEMENT_11 = 277; + public const ELEMENT_12 = 278; + public const ELEMENT_13 = 279; + public const ELEMENT_14 = 280; + public const ELEMENT_15 = 281; + public const ELEMENT_16 = 282; + public const ELEMENT_17 = 283; + public const ELEMENT_18 = 284; + public const ELEMENT_19 = 285; + public const ELEMENT_20 = 286; + public const ELEMENT_21 = 287; + public const ELEMENT_22 = 288; + public const ELEMENT_23 = 289; + public const ELEMENT_24 = 290; + public const ELEMENT_25 = 291; + public const ELEMENT_26 = 292; + public const ELEMENT_27 = 293; + public const ELEMENT_28 = 294; + public const ELEMENT_29 = 295; + public const ELEMENT_30 = 296; + public const ELEMENT_31 = 297; + public const ELEMENT_32 = 298; + public const ELEMENT_33 = 299; + public const ELEMENT_34 = 300; + public const ELEMENT_35 = 301; + public const ELEMENT_36 = 302; + public const ELEMENT_37 = 303; + public const ELEMENT_38 = 304; + public const ELEMENT_39 = 305; + public const ELEMENT_40 = 306; + public const ELEMENT_41 = 307; + public const ELEMENT_42 = 308; + public const ELEMENT_43 = 309; + public const ELEMENT_44 = 310; + public const ELEMENT_45 = 311; + public const ELEMENT_46 = 312; + public const ELEMENT_47 = 313; + public const ELEMENT_48 = 314; + public const ELEMENT_49 = 315; + public const ELEMENT_50 = 316; + public const ELEMENT_51 = 317; + public const ELEMENT_52 = 318; + public const ELEMENT_53 = 319; + public const ELEMENT_54 = 320; + public const ELEMENT_55 = 321; + public const ELEMENT_56 = 322; + public const ELEMENT_57 = 323; + public const ELEMENT_58 = 324; + public const ELEMENT_59 = 325; + public const ELEMENT_60 = 326; + public const ELEMENT_61 = 327; + public const ELEMENT_62 = 328; + public const ELEMENT_63 = 329; + public const ELEMENT_64 = 330; + public const ELEMENT_65 = 331; + public const ELEMENT_66 = 332; + public const ELEMENT_67 = 333; + public const ELEMENT_68 = 334; + public const ELEMENT_69 = 335; + public const ELEMENT_70 = 336; + public const ELEMENT_71 = 337; + public const ELEMENT_72 = 338; + public const ELEMENT_73 = 339; + public const ELEMENT_74 = 340; + public const ELEMENT_75 = 341; + public const ELEMENT_76 = 342; + public const ELEMENT_77 = 343; + public const ELEMENT_78 = 344; + public const ELEMENT_79 = 345; + public const ELEMENT_80 = 346; + public const ELEMENT_81 = 347; + public const ELEMENT_82 = 348; + public const ELEMENT_83 = 349; + public const ELEMENT_84 = 350; + public const ELEMENT_85 = 351; + public const ELEMENT_86 = 352; + public const ELEMENT_87 = 353; + public const ELEMENT_88 = 354; + public const ELEMENT_89 = 355; + public const ELEMENT_90 = 356; + public const ELEMENT_91 = 357; + public const ELEMENT_92 = 358; + public const ELEMENT_93 = 359; + public const ELEMENT_94 = 360; + public const ELEMENT_95 = 361; + public const ELEMENT_96 = 362; + public const ELEMENT_97 = 363; + public const ELEMENT_98 = 364; + public const ELEMENT_99 = 365; + public const ELEMENT_100 = 366; + public const ELEMENT_101 = 367; + public const ELEMENT_102 = 368; + public const ELEMENT_103 = 369; + public const ELEMENT_104 = 370; + public const ELEMENT_105 = 371; + public const ELEMENT_106 = 372; + public const ELEMENT_107 = 373; + public const ELEMENT_108 = 374; + public const ELEMENT_109 = 375; + public const ELEMENT_110 = 376; + public const ELEMENT_111 = 377; + public const ELEMENT_112 = 378; + public const ELEMENT_113 = 379; + public const ELEMENT_114 = 380; + public const ELEMENT_115 = 381; + public const ELEMENT_116 = 382; + public const ELEMENT_117 = 383; + public const ELEMENT_118 = 384; + public const SEAGRASS = 385; + public const CORAL = 386; + public const CORAL_BLOCK = 387; + public const CORAL_FAN = 388; + public const CORAL_FAN_DEAD = 389; + public const CORAL_FAN_HANG = 390; + public const CORAL_FAN_HANG2 = 391; + public const CORAL_FAN_HANG3 = 392; + public const KELP = 393; + public const DRIED_KELP_BLOCK = 394; + public const ACACIA_BUTTON = 395; + public const BIRCH_BUTTON = 396; + public const DARK_OAK_BUTTON = 397; + public const JUNGLE_BUTTON = 398; + public const SPRUCE_BUTTON = 399; + public const ACACIA_TRAPDOOR = 400; + public const BIRCH_TRAPDOOR = 401; + public const DARK_OAK_TRAPDOOR = 402; + public const JUNGLE_TRAPDOOR = 403; + public const SPRUCE_TRAPDOOR = 404; + public const ACACIA_PRESSURE_PLATE = 405; + public const BIRCH_PRESSURE_PLATE = 406; + public const DARK_OAK_PRESSURE_PLATE = 407; + public const JUNGLE_PRESSURE_PLATE = 408; + public const SPRUCE_PRESSURE_PLATE = 409; + public const CARVED_PUMPKIN = 410; + public const SEA_PICKLE = 411; + public const CONDUIT = 412; + + public const TURTLE_EGG = 414; + public const BUBBLE_COLUMN = 415; + public const BARRIER = 416; + public const STONE_SLAB3 = 417; + public const BAMBOO = 418; + public const BAMBOO_SAPLING = 419; + public const SCAFFOLDING = 420; + public const STONE_SLAB4 = 421; + public const DOUBLE_STONE_SLAB3 = 422; + public const DOUBLE_STONE_SLAB4 = 423; + public const GRANITE_STAIRS = 424; + public const DIORITE_STAIRS = 425; + public const ANDESITE_STAIRS = 426; + public const POLISHED_GRANITE_STAIRS = 427; + public const POLISHED_DIORITE_STAIRS = 428; + public const POLISHED_ANDESITE_STAIRS = 429; + public const MOSSY_STONE_BRICK_STAIRS = 430; + public const SMOOTH_RED_SANDSTONE_STAIRS = 431; + public const SMOOTH_SANDSTONE_STAIRS = 432; + public const END_BRICK_STAIRS = 433; + public const MOSSY_COBBLESTONE_STAIRS = 434; + public const NORMAL_STONE_STAIRS = 435; + public const SPRUCE_STANDING_SIGN = 436; + public const SPRUCE_WALL_SIGN = 437; + public const SMOOTH_STONE = 438; + public const RED_NETHER_BRICK_STAIRS = 439; + public const SMOOTH_QUARTZ_STAIRS = 440; + public const BIRCH_STANDING_SIGN = 441; + public const BIRCH_WALL_SIGN = 442; + public const JUNGLE_STANDING_SIGN = 443; + public const JUNGLE_WALL_SIGN = 444; + public const ACACIA_STANDING_SIGN = 445; + public const ACACIA_WALL_SIGN = 446; + public const DARKOAK_STANDING_SIGN = 447; + public const DARKOAK_WALL_SIGN = 448; + public const LECTERN = 449; + public const GRINDSTONE = 450; + public const BLAST_FURNACE = 451; + public const STONECUTTER_BLOCK = 452; + public const SMOKER = 453; + public const LIT_SMOKER = 454; + public const CARTOGRAPHY_TABLE = 455; + public const FLETCHING_TABLE = 456; + public const SMITHING_TABLE = 457; + public const BARREL = 458; + public const LOOM = 459; + + public const BELL = 461; + public const SWEET_BERRY_BUSH = 462; + public const LANTERN = 463; + public const CAMPFIRE = 464; + public const LAVA_CAULDRON = 465; + public const JIGSAW = 466; + public const WOOD = 467; + public const COMPOSTER = 468; + public const LIT_BLAST_FURNACE = 469; + } diff --git a/src/block/BlockLegacyMetadata.php b/src/block/BlockLegacyMetadata.php new file mode 100644 index 0000000000..c9faa5ce73 --- /dev/null +++ b/src/block/BlockLegacyMetadata.php @@ -0,0 +1,299 @@ +meta = $meta; - } - - public function getName() : string{ - return "Bookshelf"; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } +class Bookshelf extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::BOOK, 0, 3) + VanillaItems::BOOK()->setCount(3) ]; } + public function isAffectedBySilkTouch() : bool{ + return true; + } + public function getFuelTime() : int{ return 300; } diff --git a/src/block/BrewingStand.php b/src/block/BrewingStand.php new file mode 100644 index 0000000000..8d631453f5 --- /dev/null +++ b/src/block/BrewingStand.php @@ -0,0 +1,114 @@ + + */ + protected array $slots = []; + + protected function writeStateToMeta() : int{ + $flags = 0; + foreach([ + BlockLegacyMetadata::BREWING_STAND_FLAG_EAST => BrewingStandSlot::EAST(), + BlockLegacyMetadata::BREWING_STAND_FLAG_NORTHWEST => BrewingStandSlot::NORTHWEST(), + BlockLegacyMetadata::BREWING_STAND_FLAG_SOUTHWEST => BrewingStandSlot::SOUTHWEST(), + ] as $flag => $slot){ + $flags |= (array_key_exists($slot->id(), $this->slots) ? $flag : 0); + } + return $flags; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->slots = []; + foreach([ + BlockLegacyMetadata::BREWING_STAND_FLAG_EAST => BrewingStandSlot::EAST(), + BlockLegacyMetadata::BREWING_STAND_FLAG_NORTHWEST => BrewingStandSlot::NORTHWEST(), + BlockLegacyMetadata::BREWING_STAND_FLAG_SOUTHWEST => BrewingStandSlot::SOUTHWEST(), + ] as $flag => $slot){ + if(($stateMeta & $flag) !== 0){ + $this->slots[$slot->id()] = $slot; + } + } + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function hasSlot(BrewingStandSlot $slot) : bool{ + return array_key_exists($slot->id(), $this->slots); + } + + public function setSlot(BrewingStandSlot $slot, bool $occupied) : self{ + if($occupied){ + $this->slots[$slot->id()] = $slot; + }else{ + unset($this->slots[$slot->id()]); + } + return $this; + } + + /** + * @return BrewingStandSlot[] + * @phpstan-return array + */ + public function getSlots() : array{ + return $this->slots; + } + + /** @param BrewingStandSlot[] $slots */ + public function setSlots(array $slots) : self{ + $this->slots = []; + foreach($slots as $slot){ + $this->slots[$slot->id()] = $slot; + } + return $this; + } + + 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())){ + $player->setCurrentWindow($stand->getInventory()); + } + } + + return true; + } + + public function onScheduledUpdate() : void{ + //TODO + } +} diff --git a/src/pocketmine/block/BrownMushroom.php b/src/block/BrownMushroom.php similarity index 88% rename from src/pocketmine/block/BrownMushroom.php rename to src/block/BrownMushroom.php index dbb78e176a..47b3bc7e60 100644 --- a/src/pocketmine/block/BrownMushroom.php +++ b/src/block/BrownMushroom.php @@ -25,12 +25,6 @@ namespace pocketmine\block; class BrownMushroom extends RedMushroom{ - protected $id = self::BROWN_MUSHROOM; - - public function getName() : string{ - return "Brown Mushroom"; - } - public function getLightLevel() : int{ return 1; } diff --git a/src/pocketmine/block/BrownMushroomBlock.php b/src/block/BrownMushroomBlock.php similarity index 84% rename from src/pocketmine/block/BrownMushroomBlock.php rename to src/block/BrownMushroomBlock.php index 8c6b9819c3..09b183491b 100644 --- a/src/pocketmine/block/BrownMushroomBlock.php +++ b/src/block/BrownMushroomBlock.php @@ -28,15 +28,9 @@ use function mt_rand; class BrownMushroomBlock extends RedMushroomBlock{ - protected $id = Block::BROWN_MUSHROOM_BLOCK; - - public function getName() : string{ - return "Brown Mushroom Block"; - } - public function getDropsForCompatibleTool(Item $item) : array{ return [ - Item::get(Item::BROWN_MUSHROOM, 0, mt_rand(0, 2)) + VanillaBlocks::BROWN_MUSHROOM()->asItem()->setCount(mt_rand(0, 2)) ]; } } diff --git a/src/block/Button.php b/src/block/Button.php new file mode 100644 index 0000000000..9168bbd169 --- /dev/null +++ b/src/block/Button.php @@ -0,0 +1,89 @@ +facing) | ($this->pressed ? BlockLegacyMetadata::BUTTON_FLAG_POWERED : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + //TODO: in PC it's (6 - facing) for every meta except 0 (down) + $this->facing = BlockDataSerializer::readFacing($stateMeta & 0x07); + $this->pressed = ($stateMeta & BlockLegacyMetadata::BUTTON_FLAG_POWERED) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isPressed() : bool{ return $this->pressed; } + + /** @return $this */ + public function setPressed(bool $pressed) : self{ + $this->pressed = $pressed; + return $this; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + //TODO: check valid target block + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + abstract protected function getActivationTime() : int; + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->pressed){ + $this->pressed = true; + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, $this->getActivationTime()); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOnSound()); + } + + return true; + } + + public function onScheduledUpdate() : void{ + if($this->pressed){ + $this->pressed = false; + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new RedstonePowerOffSound()); + } + } +} diff --git a/src/block/Cactus.php b/src/block/Cactus.php new file mode 100644 index 0000000000..56ee7017fe --- /dev/null +++ b/src/block/Cactus.php @@ -0,0 +1,144 @@ +age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 15); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 15){ + throw new \InvalidArgumentException("Age must be in range 0-15"); + } + $this->age = $age; + return $this; + } + + public function hasEntityCollision() : bool{ + return true; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + static $shrinkSize = 1 / 16; + return [AxisAlignedBB::one()->contract($shrinkSize, 0, $shrinkSize)->trim(Facing::UP, $shrinkSize)]; + } + + public function onEntityInside(Entity $entity) : bool{ + $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_CONTACT, 1); + $entity->attack($ev); + return true; + } + + public function onNearbyBlockChange() : void{ + $down = $this->getSide(Facing::DOWN); + if($down->getId() !== BlockLegacyIds::SAND and !$down->isSameType($this)){ + $this->position->getWorld()->useBreakOn($this->position); + }else{ + foreach(Facing::HORIZONTAL as $side){ + $b = $this->getSide($side); + if($b->isSolid()){ + $this->position->getWorld()->useBreakOn($this->position); + break; + } + } + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if(!$this->getSide(Facing::DOWN)->isSameType($this)){ + if($this->age === 15){ + for($y = 1; $y < 3; ++$y){ + if(!$this->position->getWorld()->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){ + break; + } + $b = $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z); + if($b->getId() === BlockLegacyIds::AIR){ + $ev = new BlockGrowEvent($b, VanillaBlocks::CACTUS()); + $ev->call(); + if($ev->isCancelled()){ + break; + } + $this->position->getWorld()->setBlock($b->position, $ev->getNewState()); + }else{ + break; + } + } + $this->age = 0; + $this->position->getWorld()->setBlock($this->position, $this); + }else{ + ++$this->age; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + } + + 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)){ + foreach(Facing::HORIZONTAL as $side){ + if($this->getSide($side)->isSolid()){ + return false; + } + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } +} diff --git a/src/block/Cake.php b/src/block/Cake.php new file mode 100644 index 0000000000..e9f82943f4 --- /dev/null +++ b/src/block/Cake.php @@ -0,0 +1,137 @@ +bites; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->bites = BlockDataSerializer::readBoundedInt("bites", $stateMeta, 0, 6); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [ + AxisAlignedBB::one() + ->contract(1 / 16, 0, 1 / 16) + ->trim(Facing::UP, 0.5) + ->trim(Facing::WEST, $this->bites / 8) + ]; + } + + public function getBites() : int{ return $this->bites; } + + /** @return $this */ + public function setBites(int $bites) : self{ + if($bites < 0 || $bites > 6){ + throw new \InvalidArgumentException("Bites must be in range 0-6"); + } + $this->bites = $bites; + return $this; + } + + 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::AIR){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->getId() === BlockLegacyIds::AIR){ //Replace with common break method + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return []; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + return $player->consumeObject($this); + } + + return false; + } + + public function getFoodRestore() : int{ + return 2; + } + + public function getSaturationRestore() : float{ + return 0.4; + } + + public function requiresHunger() : bool{ + return true; + } + + /** + * @return Block + */ + public function getResidue(){ + $clone = clone $this; + $clone->bites++; + if($clone->bites > 6){ + $clone = VanillaBlocks::AIR(); + } + return $clone; + } + + /** + * @return EffectInstance[] + */ + public function getAdditionalEffects() : array{ + return []; + } + + public function onConsume(Living $consumer) : void{ + $this->position->getWorld()->setBlock($this->position, $this->getResidue()); + } +} diff --git a/src/pocketmine/block/Carpet.php b/src/block/Carpet.php similarity index 52% rename from src/pocketmine/block/Carpet.php rename to src/block/Carpet.php index bd700f8d83..54a1b4abe2 100644 --- a/src/pocketmine/block/Carpet.php +++ b/src/block/Carpet.php @@ -23,58 +23,46 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\block\utils\ColorBlockMetaHelper; +use pocketmine\block\utils\ColorInMetadataTrait; +use pocketmine\block\utils\DyeColor; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; class Carpet extends Flowable{ + use ColorInMetadataTrait; - protected $id = self::CARPET; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getHardness() : float{ - return 0.1; + public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){ + $this->color = DyeColor::WHITE(); + parent::__construct($idInfo, $name, $breakInfo); } public function isSolid() : bool{ return true; } - public function getName() : string{ - return ColorBlockMetaHelper::getColorFromMeta($this->getVariant()) . " Carpet"; + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 15 / 16)]; } - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 0.0625, - $this->z + 1 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() !== self::AIR){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; + 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::AIR){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } return false; } public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() === self::AIR){ - $this->getLevelNonNull()->useBreakOn($this); + if($this->getSide(Facing::DOWN)->getId() === BlockLegacyIds::AIR){ + $this->position->getWorld()->useBreakOn($this->position); } } diff --git a/src/pocketmine/block/Carrot.php b/src/block/Carrot.php similarity index 71% rename from src/pocketmine/block/Carrot.php rename to src/block/Carrot.php index fac1652b88..612b967f60 100644 --- a/src/pocketmine/block/Carrot.php +++ b/src/block/Carrot.php @@ -24,28 +24,18 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; use function mt_rand; class Carrot extends Crops{ - protected $id = self::CARROT_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Carrot Block"; - } - public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::CARROT, 0, $this->meta >= 0x07 ? mt_rand(1, 4) : 1) + VanillaItems::CARROT()->setCount($this->age >= 7 ? mt_rand(1, 4) : 1) ]; } - public function getPickedItem() : Item{ - return ItemFactory::get(Item::CARROT); + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaItems::CARROT(); } } diff --git a/src/block/CarvedPumpkin.php b/src/block/CarvedPumpkin.php new file mode 100644 index 0000000000..d4513a08de --- /dev/null +++ b/src/block/CarvedPumpkin.php @@ -0,0 +1,45 @@ +facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + } + + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing); + } + + public function getStateBitmask() : int{ + return 0b11; + } +} diff --git a/src/block/ChemicalHeat.php b/src/block/ChemicalHeat.php new file mode 100644 index 0000000000..55cc7b9e04 --- /dev/null +++ b/src/block/ChemicalHeat.php @@ -0,0 +1,32 @@ +meta = $meta; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = Facing::opposite(BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x3)); } - public function getName() : string{ - return "Hay Bale"; + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing(Facing::opposite($this->facing)); } - public function getHardness() : float{ - return 0.5; + public function getStateBitmask() : int{ + return 0b0011; } - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta = PillarRotationHelper::getMetaFromFace($this->meta, $face); - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - public function getVariantBitmask() : int{ - return 0x03; - } - - public function getFlameEncouragement() : int{ - return 60; - } - - public function getFlammability() : int{ - return 20; + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + //TODO + return false; } } diff --git a/src/block/Chest.php b/src/block/Chest.php new file mode 100644 index 0000000000..c89b3dde7c --- /dev/null +++ b/src/block/Chest.php @@ -0,0 +1,90 @@ +contract(0.025, 0, 0.025)->trim(Facing::UP, 0.05)]; + } + + 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){ + $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; + } + } + } + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + + $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 + !$chest->canOpenWith($item->getCustomName()) + ){ + return true; + } + + $player->setCurrentWindow($chest->getInventory()); + } + } + + return true; + } + + public function getFuelTime() : int{ + return 300; + } +} diff --git a/src/pocketmine/block/Wood2.php b/src/block/Clay.php similarity index 74% rename from src/pocketmine/block/Wood2.php rename to src/block/Clay.php index a3bad03a25..8e86d4925b 100644 --- a/src/pocketmine/block/Wood2.php +++ b/src/block/Clay.php @@ -23,18 +23,18 @@ declare(strict_types=1); namespace pocketmine\block; -class Wood2 extends Wood{ +use pocketmine\item\Item; +use pocketmine\item\VanillaItems; - public const ACACIA = 0; - public const DARK_OAK = 1; +class Clay extends Opaque{ - protected $id = self::WOOD2; - - public function getName() : string{ - static $names = [ - 0 => "Acacia Wood", - 1 => "Dark Oak Wood" + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaItems::CLAY()->setCount(4) ]; - return $names[$this->getVariant()] ?? "Unknown"; + } + + public function isAffectedBySilkTouch() : bool{ + return true; } } diff --git a/src/pocketmine/item/Boat.php b/src/block/Coal.php similarity index 80% rename from src/pocketmine/item/Boat.php rename to src/block/Coal.php index 35c05d997e..821c422ded 100644 --- a/src/pocketmine/item/Boat.php +++ b/src/block/Coal.php @@ -21,16 +21,19 @@ declare(strict_types=1); -namespace pocketmine\item; +namespace pocketmine\block; -class Boat extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BOAT, $meta, "Boat"); - } +class Coal extends Opaque{ public function getFuelTime() : int{ - return 1200; //400 in PC + return 16000; } - //TODO + public function getFlameEncouragement() : int{ + return 5; + } + + public function getFlammability() : int{ + return 5; + } } diff --git a/src/pocketmine/block/Tripwire.php b/src/block/CoalOre.php similarity index 76% rename from src/pocketmine/block/Tripwire.php rename to src/block/CoalOre.php index 9a1f718ee8..3c87560b15 100644 --- a/src/pocketmine/block/Tripwire.php +++ b/src/block/CoalOre.php @@ -24,27 +24,22 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; +use function mt_rand; -class Tripwire extends Flowable{ - - protected $id = self::TRIPWIRE; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Tripwire"; - } +class CoalOre extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::STRING) + VanillaItems::COAL() ]; } public function isAffectedBySilkTouch() : bool{ - return false; + return true; + } + + protected function getXpDropAmount() : int{ + return mt_rand(0, 2); } } diff --git a/src/block/Cobweb.php b/src/block/Cobweb.php new file mode 100644 index 0000000000..680af15952 --- /dev/null +++ b/src/block/Cobweb.php @@ -0,0 +1,54 @@ +resetFallDistance(); + return true; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaItems::STRING() + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function blocksDirectSkyLight() : bool{ + return true; + } +} diff --git a/src/block/CocoaBlock.php b/src/block/CocoaBlock.php new file mode 100644 index 0000000000..efcc4590fb --- /dev/null +++ b/src/block/CocoaBlock.php @@ -0,0 +1,146 @@ +facing)) | ($this->age << 2); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = Facing::opposite(BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03)); + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta >> 2, 0, 2); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 2){ + throw new \InvalidArgumentException("Age must be in range 0-2"); + } + $this->age = $age; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [ + AxisAlignedBB::one() + ->squash(Facing::axis(Facing::rotateY($this->facing, true)), (6 - $this->age) / 16) //sides + ->trim(Facing::DOWN, (7 - $this->age * 2) / 16) + ->trim(Facing::UP, 0.25) + ->trim(Facing::opposite($this->facing), 1 / 16) //gap between log and pod + ->trim($this->facing, (11 - $this->age * 2) / 16) //outward face + ]; + } + + private function canAttachTo(Block $block) : bool{ + return $block instanceof Wood && $block->getTreeType()->equals(TreeType::JUNGLE()); + } + + 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)){ + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($item instanceof Fertilizer && $this->grow()){ + $item->pop(); + + return true; + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if(!$this->canAttachTo($this->getSide(Facing::opposite($this->facing)))){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + 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) + ]; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaItems::COCOA_BEANS(); + } +} diff --git a/src/block/Concrete.php b/src/block/Concrete.php new file mode 100644 index 0000000000..9feb5e0933 --- /dev/null +++ b/src/block/Concrete.php @@ -0,0 +1,36 @@ +color = DyeColor::WHITE(); + parent::__construct($idInfo, $name, $breakInfo); + } +} diff --git a/src/pocketmine/block/ConcretePowder.php b/src/block/ConcretePowder.php similarity index 59% rename from src/pocketmine/block/ConcretePowder.php rename to src/block/ConcretePowder.php index 68b7a49d90..b3c4c51ea8 100644 --- a/src/pocketmine/block/ConcretePowder.php +++ b/src/block/ConcretePowder.php @@ -23,33 +23,28 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\block\utils\ColorBlockMetaHelper; +use pocketmine\block\utils\ColorInMetadataTrait; +use pocketmine\block\utils\DyeColor; +use pocketmine\block\utils\Fallable; +use pocketmine\block\utils\FallableTrait; +use pocketmine\math\Facing; -class ConcretePowder extends Fallable{ - - protected $id = self::CONCRETE_POWDER; - - public function __construct(int $meta = 0){ - $this->meta = $meta; +class ConcretePowder extends Opaque implements Fallable{ + use ColorInMetadataTrait; + use FallableTrait { + onNearbyBlockChange as protected startFalling; } - public function getName() : string{ - return ColorBlockMetaHelper::getColorFromMeta($this->getVariant()) . " Concrete Powder"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; + public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){ + $this->color = DyeColor::WHITE(); + parent::__construct($idInfo, $name, $breakInfo); } public function onNearbyBlockChange() : void{ if(($block = $this->checkAdjacentWater()) !== null){ - $this->level->setBlock($this, $block); + $this->position->getWorld()->setBlock($this->position, $block); }else{ - parent::onNearbyBlockChange(); + $this->startFalling(); } } @@ -58,9 +53,12 @@ class ConcretePowder extends Fallable{ } private function checkAdjacentWater() : ?Block{ - for($i = 1; $i < 6; ++$i){ //Do not check underneath + foreach(Facing::ALL as $i){ + if($i === Facing::DOWN){ + continue; + } if($this->getSide($i) instanceof Water){ - return BlockFactory::get(Block::CONCRETE, $this->meta); + return VanillaBlocks::CONCRETE()->setColor($this->color); } } diff --git a/src/block/Coral.php b/src/block/Coral.php new file mode 100644 index 0000000000..7a991492d0 --- /dev/null +++ b/src/block/Coral.php @@ -0,0 +1,82 @@ +fromId($stateMeta); + if($coralType === null){ + throw new InvalidBlockStateException("No such coral type"); + } + $this->coralType = $coralType; + } + + public function writeStateToMeta() : int{ + return CoralTypeIdMap::getInstance()->toId($this->coralType); + } + + protected function writeStateToItemMeta() : int{ + return $this->writeStateToMeta(); + } + + public function getStateBitmask() : int{ + return 0b0111; + } + + public function readStateFromWorld() : void{ + //TODO: this hack ensures correct state of coral plants, because they don't retain their dead flag in metadata + $world = $this->position->getWorld(); + $this->dead = true; + foreach($this->position->sides() as $vector3){ + if($world->getBlock($vector3) instanceof Water){ + $this->dead = false; + break; + } + } + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$tx->fetchBlock($blockReplace->getPosition()->down())->isSolid()){ + return false; + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); + if(!$world->getBlock($this->position->down())->isSolid()){ + $world->useBreakOn($this->position); + }else{ + parent::onNearbyBlockChange(); + } + } +} diff --git a/src/block/CoralBlock.php b/src/block/CoralBlock.php new file mode 100644 index 0000000000..5399e58044 --- /dev/null +++ b/src/block/CoralBlock.php @@ -0,0 +1,109 @@ +coralType = CoralType::TUBE(); + parent::__construct($idInfo, $name, $breakInfo); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $coralType = CoralTypeIdMap::getInstance()->fromId($stateMeta & 0x7); + if($coralType === null){ + throw new InvalidBlockStateException("No such coral type"); + } + $this->coralType = $coralType; + $this->dead = ($stateMeta & BlockLegacyMetadata::CORAL_BLOCK_FLAG_DEAD) !== 0; + } + + protected function writeStateToMeta() : int{ + return ($this->dead ? BlockLegacyMetadata::CORAL_BLOCK_FLAG_DEAD : 0) | CoralTypeIdMap::getInstance()->toId($this->coralType); + } + + protected function writeStateToItemMeta() : int{ + return $this->writeStateToMeta(); + } + + public function getStateBitmask() : int{ + 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)); + } + } + + public function onScheduledUpdate() : void{ + if(!$this->dead){ + $world = $this->position->getWorld(); + + $hasWater = false; + foreach($this->position->sides() as $vector3){ + if($world->getBlock($vector3) instanceof Water){ + $hasWater = true; + break; + } + } + if(!$hasWater){ + $world->setBlock($this->position, $this->setDead(true)); + } + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [$this->setDead(true)->asItem()]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } +} diff --git a/src/pocketmine/block/NoteBlock.php b/src/block/CraftingTable.php similarity index 66% rename from src/pocketmine/block/NoteBlock.php rename to src/block/CraftingTable.php index cd9c0dc797..a35e13e5bd 100644 --- a/src/pocketmine/block/NoteBlock.php +++ b/src/block/CraftingTable.php @@ -23,29 +23,22 @@ declare(strict_types=1); namespace pocketmine\block; -class NoteBlock extends Solid{ +use pocketmine\block\inventory\CraftingTableInventory; +use pocketmine\item\Item; +use pocketmine\math\Vector3; +use pocketmine\player\Player; - protected $id = self::NOTE_BLOCK; +class CraftingTable extends Opaque{ - public function __construct(int $meta = 0){ - $this->meta = $meta; - } + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + $player->setCurrentWindow(new CraftingTableInventory($this->position)); + } - public function getName() : string{ - return "Note Block"; + return true; } public function getFuelTime() : int{ return 300; } - - public function getHardness() : float{ - return 0.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - //TODO } diff --git a/src/block/Crops.php b/src/block/Crops.php new file mode 100644 index 0000000000..c7f201b41f --- /dev/null +++ b/src/block/Crops.php @@ -0,0 +1,113 @@ +age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 7); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 7){ + throw new \InvalidArgumentException("Age must be in range 0-7"); + } + $this->age = $age; + return $this; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($blockReplace->getSide(Facing::DOWN)->getId() === BlockLegacyIds::FARMLAND){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($this->age < 7 and $item instanceof Fertilizer){ + $block = clone $this; + $block->age += mt_rand(2, 5); + if($block->age > 7){ + $block->age = 7; + } + + $ev = new BlockGrowEvent($this, $block); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $item->pop(); + } + + return true; + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->getId() !== BlockLegacyIds::FARMLAND){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if($this->age < 7 and mt_rand(0, 2) === 1){ + $block = clone $this; + ++$block->age; + $ev = new BlockGrowEvent($this, $block); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + } + } + } +} diff --git a/src/block/DaylightSensor.php b/src/block/DaylightSensor.php new file mode 100644 index 0000000000..17103ed473 --- /dev/null +++ b/src/block/DaylightSensor.php @@ -0,0 +1,117 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->inverted ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + protected function writeStateToMeta() : int{ + return $this->signalStrength; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->signalStrength = BlockDataSerializer::readBoundedInt("signalStrength", $stateMeta, 0, 15); + $this->inverted = $id === $this->idInfoFlattened->getSecondId(); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isInverted() : bool{ + return $this->inverted; + } + + /** + * @return $this + */ + public function setInverted(bool $inverted = true) : self{ + $this->inverted = $inverted; + return $this; + } + + public function getFuelTime() : int{ + return 300; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 10 / 16)]; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->inverted = !$this->inverted; + $this->signalStrength = $this->recalculateSignalStrength(); + $this->position->getWorld()->setBlock($this->position, $this); + return true; + } + + public function onScheduledUpdate() : void{ + $signalStrength = $this->recalculateSignalStrength(); + if($this->signalStrength !== $signalStrength){ + $this->signalStrength = $signalStrength; + $this->position->getWorld()->setBlock($this->position, $this); + } + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 20); + } + + private function recalculateSignalStrength() : int{ + $lightLevel = $this->position->getWorld()->getRealBlockSkyLightAt($this->position->x, $this->position->y, $this->position->z); + if($this->inverted){ + return 15 - $lightLevel; + } + + $sunAngle = $this->position->getWorld()->getSunAnglePercentage(); + return max(0, (int) round($lightLevel * cos(($sunAngle + ((($sunAngle < 0.5 ? 0 : 1) - $sunAngle) / 5)) * 2 * M_PI))); + } + + //TODO +} diff --git a/src/pocketmine/block/DeadBush.php b/src/block/DeadBush.php similarity index 53% rename from src/pocketmine/block/DeadBush.php rename to src/block/DeadBush.php index 8ee1b09777..7b5262b49c 100644 --- a/src/pocketmine/block/DeadBush.php +++ b/src/block/DeadBush.php @@ -24,53 +24,37 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; use function mt_rand; class DeadBush extends Flowable{ - protected $id = self::DEAD_BUSH; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Dead Bush"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if(!$this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - return parent::place($item, $blockReplace, $blockClicked, $face, $clickVector, $player); + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->getSide(Facing::DOWN)->isTransparent()){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } return false; } public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); } } - public function getToolType() : int{ - return BlockToolType::TYPE_SHEARS; + public function getDropsForIncompatibleTool(Item $item) : array{ + return [ + VanillaItems::STICK()->setCount(mt_rand(0, 2)) + ]; } - public function getToolHarvestLevel() : int{ - return 1; - } - - public function getDrops(Item $item) : array{ - if(!$this->isCompatibleWithTool($item)){ - return [ - ItemFactory::get(Item::STICK, 0, mt_rand(0, 2)) - ]; - } - - return parent::getDrops($item); + public function isAffectedBySilkTouch() : bool{ + return true; } public function getFlameEncouragement() : int{ diff --git a/src/block/DetectorRail.php b/src/block/DetectorRail.php new file mode 100644 index 0000000000..935fd0a04d --- /dev/null +++ b/src/block/DetectorRail.php @@ -0,0 +1,51 @@ +activated; } + + /** @return $this */ + public function setActivated(bool $activated) : self{ + $this->activated = $activated; + return $this; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + parent::readStateFromData($id, $stateMeta & ~BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED); + $this->activated = ($stateMeta & BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED) !== 0; + } + + protected function writeStateToMeta() : int{ + return parent::writeStateToMeta() | ($this->activated ? BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED : 0); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + //TODO +} diff --git a/src/pocketmine/block/RedMushroomBlock.php b/src/block/DiamondOre.php similarity index 69% rename from src/pocketmine/block/RedMushroomBlock.php rename to src/block/DiamondOre.php index 37ea63ef2f..2307d30c2f 100644 --- a/src/pocketmine/block/RedMushroomBlock.php +++ b/src/block/DiamondOre.php @@ -24,31 +24,22 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; +use pocketmine\item\VanillaItems; use function mt_rand; -class RedMushroomBlock extends Solid{ - - protected $id = Block::RED_MUSHROOM_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Red Mushroom Block"; - } - - public function getHardness() : float{ - return 0.2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } +class DiamondOre extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - Item::get(Item::RED_MUSHROOM, 0, mt_rand(0, 2)) + VanillaItems::DIAMOND() ]; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + protected function getXpDropAmount() : int{ + return mt_rand(3, 7); + } } diff --git a/src/block/Dirt.php b/src/block/Dirt.php new file mode 100644 index 0000000000..29df9f5457 --- /dev/null +++ b/src/block/Dirt.php @@ -0,0 +1,70 @@ +coarse = ($stateMeta & BlockLegacyMetadata::DIRT_FLAG_COARSE) !== 0; + } + + protected function writeStateToMeta() : int{ + return $this->coarse ? BlockLegacyMetadata::DIRT_FLAG_COARSE : 0; + } + + protected function writeStateToItemMeta() : int{ + return $this->writeStateToMeta(); + } + + public function getStateBitmask() : int{ + return 0b1; + } + + public function isCoarse() : bool{ return $this->coarse; } + + /** @return $this */ + public function setCoarse(bool $coarse) : self{ + $this->coarse = $coarse; + return $this; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face === Facing::UP and $item instanceof Hoe){ + $item->applyDamage(1); + $this->position->getWorld()->setBlock($this->position, $this->coarse ? VanillaBlocks::DIRT() : VanillaBlocks::FARMLAND()); + + return true; + } + + return false; + } +} diff --git a/src/block/Door.php b/src/block/Door.php new file mode 100644 index 0000000000..74a9f1b783 --- /dev/null +++ b/src/block/Door.php @@ -0,0 +1,187 @@ +top){ + return BlockLegacyMetadata::DOOR_FLAG_TOP | + ($this->hingeRight ? BlockLegacyMetadata::DOOR_TOP_FLAG_RIGHT : 0) | + ($this->powered ? BlockLegacyMetadata::DOOR_TOP_FLAG_POWERED : 0); + } + + return BlockDataSerializer::writeLegacyHorizontalFacing(Facing::rotateY($this->facing, true)) | ($this->open ? BlockLegacyMetadata::DOOR_BOTTOM_FLAG_OPEN : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->top = ($stateMeta & BlockLegacyMetadata::DOOR_FLAG_TOP) !== 0; + if($this->top){ + $this->hingeRight = ($stateMeta & BlockLegacyMetadata::DOOR_TOP_FLAG_RIGHT) !== 0; + $this->powered = ($stateMeta & BlockLegacyMetadata::DOOR_TOP_FLAG_POWERED) !== 0; + }else{ + $this->facing = Facing::rotateY(BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03), false); + $this->open = ($stateMeta & BlockLegacyMetadata::DOOR_BOTTOM_FLAG_OPEN) !== 0; + } + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + + //copy door properties from other half + $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + if($other instanceof Door and $other->isSameType($this)){ + if($this->top){ + $this->facing = $other->facing; + $this->open = $other->open; + }else{ + $this->hingeRight = $other->hingeRight; + $this->powered = $other->powered; + } + } + } + + public function isTop() : bool{ return $this->top; } + + /** @return $this */ + public function setTop(bool $top) : self{ + $this->top = $top; + return $this; + } + + public function isHingeRight() : bool{ return $this->hingeRight; } + + /** @return $this */ + public function setHingeRight(bool $hingeRight) : self{ + $this->hingeRight = $hingeRight; + return $this; + } + + public function isOpen() : bool{ return $this->open; } + + /** @return $this */ + public function setOpen(bool $open) : self{ + $this->open = $open; + return $this; + } + + public function isSolid() : bool{ + return false; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + //TODO: doors are 0.1825 blocks thick, instead of 0.1875 like JE (https://bugs.mojang.com/browse/MCPE-19214) + return [AxisAlignedBB::one()->trim($this->open ? Facing::rotateY($this->facing, !$this->hingeRight) : $this->facing, 327 / 400)]; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->getId() === BlockLegacyIds::AIR){ //Replace with common break method + $this->position->getWorld()->useBreakOn($this->position); //this will delete both halves if they exist + } + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face === Facing::UP){ + $blockUp = $this->getSide(Facing::UP); + $blockDown = $this->getSide(Facing::DOWN); + if(!$blockUp->canBeReplaced() or $blockDown->isTransparent()){ + return false; + } + + if($player !== null){ + $this->facing = $player->getHorizontalFacing(); + } + + $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 + $this->hingeRight = true; + } + + $topHalf = clone $this; + $topHalf->top = true; + + $tx->addBlock($blockReplace->position, $this)->addBlock($blockUp->position, $topHalf); + return true; + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->open = !$this->open; + + $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + if($other instanceof Door and $other->isSameType($this)){ + $other->open = $this->open; + $this->position->getWorld()->setBlock($other->position, $other); + } + + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->addSound($this->position, new DoorSound()); + + return true; + } + + public function getDrops(Item $item) : array{ + if(!$this->top){ + return parent::getDrops($item); + } + + return []; + } + + public function getAffectedBlocks() : array{ + $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + if($other->isSameType($this)){ + return [$this, $other]; + } + return parent::getAffectedBlocks(); + } +} diff --git a/src/block/DoublePlant.php b/src/block/DoublePlant.php new file mode 100644 index 0000000000..00d3618326 --- /dev/null +++ b/src/block/DoublePlant.php @@ -0,0 +1,106 @@ +top ? BlockLegacyMetadata::DOUBLE_PLANT_FLAG_TOP : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->top = ($stateMeta & BlockLegacyMetadata::DOUBLE_PLANT_FLAG_TOP) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1000; + } + + public function isTop() : bool{ return $this->top; } + + /** @return $this */ + public function setTop(bool $top) : self{ + $this->top = $top; + return $this; + } + + 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()){ + $top = clone $this; + $top->top = true; + $tx->addBlock($blockReplace->position, $this)->addBlock($blockReplace->position->getSide(Facing::UP), $top); + return true; + } + + return false; + } + + /** + * Returns whether this double-plant has a corresponding other half. + */ + public function isValidHalfPlant() : bool{ + $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + + return ( + $other instanceof DoublePlant and + $other->isSameType($this) and + $other->top !== $this->top + ); + } + + public function onNearbyBlockChange() : void{ + if(!$this->isValidHalfPlant() or (!$this->top and $this->getSide(Facing::DOWN)->isTransparent())){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function getDrops(Item $item) : array{ + return $this->top ? parent::getDrops($item) : []; + } + + public function getAffectedBlocks() : array{ + if($this->isValidHalfPlant()){ + return [$this, $this->getSide($this->top ? Facing::DOWN : Facing::UP)]; + } + + return parent::getAffectedBlocks(); + } + + public function getFlameEncouragement() : int{ + return 60; + } + + public function getFlammability() : int{ + return 100; + } +} diff --git a/src/pocketmine/block/WallBanner.php b/src/block/DoubleTallGrass.php similarity index 69% rename from src/pocketmine/block/WallBanner.php rename to src/block/DoubleTallGrass.php index 90a9b02717..6fee40e329 100644 --- a/src/pocketmine/block/WallBanner.php +++ b/src/block/DoubleTallGrass.php @@ -23,17 +23,20 @@ declare(strict_types=1); namespace pocketmine\block; -class WallBanner extends StandingBanner{ +use pocketmine\item\Item; +use pocketmine\item\VanillaItems; +use function mt_rand; - protected $id = self::WALL_BANNER; +class DoubleTallGrass extends DoublePlant{ - public function getName() : string{ - return "Wall Banner"; + public function canBeReplaced() : bool{ + return true; } - public function onNearbyBlockChange() : void{ - if($this->getSide($this->meta ^ 0x01)->getId() === self::AIR){ - $this->getLevelNonNull()->useBreakOn($this); + public function getDropsForIncompatibleTool(Item $item) : array{ + if($this->top and mt_rand(0, 7) === 0){ + return [VanillaItems::WHEAT_SEEDS()]; } + return []; } } diff --git a/src/block/DragonEgg.php b/src/block/DragonEgg.php new file mode 100644 index 0000000000..a2ef58cd47 --- /dev/null +++ b/src/block/DragonEgg.php @@ -0,0 +1,85 @@ +teleport(); + return true; + } + + public function onAttack(Item $item, int $face, ?Player $player = null) : bool{ + if($player !== null && !$player->getGamemode()->equals(GameMode::CREATIVE())){ + $this->teleport(); + return true; + } + return false; + } + + public function teleport() : void{ + for($tries = 0; $tries < 16; ++$tries){ + $block = $this->position->getWorld()->getBlockAt( + $this->position->x + mt_rand(-16, 16), + max(World::Y_MIN, min(World::Y_MAX - 1, $this->position->y + mt_rand(-8, 8))), + $this->position->z + mt_rand(-16, 16) + ); + if($block instanceof Air){ + $ev = new BlockTeleportEvent($this, $block->position); + $ev->call(); + if($ev->isCancelled()){ + break; + } + + $blockPos = $ev->getTo(); + $this->position->getWorld()->addParticle($this->position, new DragonEggTeleportParticle($this->position->x - $blockPos->x, $this->position->y - $blockPos->y, $this->position->z - $blockPos->z)); + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + $this->position->getWorld()->setBlock($blockPos, $this); + break; + } + } + } +} diff --git a/src/pocketmine/block/LitRedstoneLamp.php b/src/block/DriedKelp.php similarity index 78% rename from src/pocketmine/block/LitRedstoneLamp.php rename to src/block/DriedKelp.php index ecc8128f0c..b5a05efa51 100644 --- a/src/pocketmine/block/LitRedstoneLamp.php +++ b/src/block/DriedKelp.php @@ -23,15 +23,17 @@ declare(strict_types=1); namespace pocketmine\block; -class LitRedstoneLamp extends RedstoneLamp{ +class DriedKelp extends Opaque{ - protected $id = self::LIT_REDSTONE_LAMP; - - public function getName() : string{ - return "Lit Redstone Lamp"; + public function getFlameEncouragement() : int{ + return 30; } - public function getLightLevel() : int{ - return 15; + public function getFlammability() : int{ + return 60; + } + + public function getFuelTime() : int{ + return 4000; } } diff --git a/src/block/DyedShulkerBox.php b/src/block/DyedShulkerBox.php new file mode 100644 index 0000000000..7c50cacd0a --- /dev/null +++ b/src/block/DyedShulkerBox.php @@ -0,0 +1,36 @@ +color = DyeColor::WHITE(); + parent::__construct($idInfo, $name, $breakInfo); + } +} diff --git a/src/pocketmine/block/Bricks.php b/src/block/Element.php similarity index 57% rename from src/pocketmine/block/Bricks.php rename to src/block/Element.php index 22f0c1fc7c..f3f66a5871 100644 --- a/src/pocketmine/block/Bricks.php +++ b/src/block/Element.php @@ -23,33 +23,28 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\TieredTool; +class Element extends Opaque{ -class Bricks extends Solid{ + private int $atomicWeight; + private int $group; + private string $symbol; - protected $id = self::BRICK_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo, string $symbol, int $atomicWeight, int $group){ + parent::__construct($idInfo, $name, $breakInfo); + $this->atomicWeight = $atomicWeight; + $this->group = $group; + $this->symbol = $symbol; } - public function getHardness() : float{ - return 2; + public function getAtomicWeight() : int{ + return $this->atomicWeight; } - public function getBlastResistance() : float{ - return 30; + public function getGroup() : int{ + return $this->group; } - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Bricks"; + public function getSymbol() : string{ + return $this->symbol; } } diff --git a/src/block/EmeraldOre.php b/src/block/EmeraldOre.php new file mode 100644 index 0000000000..09d8f3a5a9 --- /dev/null +++ b/src/block/EmeraldOre.php @@ -0,0 +1,45 @@ +meta = $meta; + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 0.25)]; } - public function getVariantBitmask() : int{ - return 0; - } + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + //TODO lock - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - //TODO: check valid target block - $this->meta = $face; + $player->setCurrentWindow(new EnchantInventory($this->position)); + } - return $this->level->setBlock($this, $this, true, true); - } - - public function onActivate(Item $item, Player $player = null) : bool{ - //TODO return true; } } diff --git a/src/block/EndPortalFrame.php b/src/block/EndPortalFrame.php new file mode 100644 index 0000000000..a80b2cf2cb --- /dev/null +++ b/src/block/EndPortalFrame.php @@ -0,0 +1,69 @@ +facing) | ($this->eye ? BlockLegacyMetadata::END_PORTAL_FRAME_FLAG_EYE : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + $this->eye = ($stateMeta & BlockLegacyMetadata::END_PORTAL_FRAME_FLAG_EYE) !== 0; + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function hasEye() : bool{ return $this->eye; } + + /** @return $this */ + public function setEye(bool $eye) : self{ + $this->eye = $eye; + return $this; + } + + public function getLightLevel() : int{ + return 1; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 3 / 16)]; + } +} diff --git a/src/block/EndRod.php b/src/block/EndRod.php new file mode 100644 index 0000000000..39c28a097f --- /dev/null +++ b/src/block/EndRod.php @@ -0,0 +1,92 @@ +facing); + if(Facing::axis($this->facing) !== Axis::Y){ + $result ^= 1; //TODO: in PC this is always the same as facing, just PE is stupid + } + + return $result; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + if($stateMeta !== 0 and $stateMeta !== 1){ + $stateMeta ^= 1; + } + + $this->facing = BlockDataSerializer::readFacing($stateMeta); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + 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){ + $this->facing = Facing::opposite($face); + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function isSolid() : bool{ + return true; + } + + public function getLightLevel() : int{ + return 14; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + $myAxis = Facing::axis($this->facing); + + $bb = AxisAlignedBB::one(); + foreach([Axis::Y, Axis::Z, Axis::X] as $axis){ + if($axis === $myAxis){ + continue; + } + $bb->squash($axis, 6 / 16); + } + return [$bb]; + } +} diff --git a/src/block/EnderChest.php b/src/block/EnderChest.php new file mode 100644 index 0000000000..e376320086 --- /dev/null +++ b/src/block/EnderChest.php @@ -0,0 +1,69 @@ +contract(0.025, 0, 0.025)->trim(Facing::UP, 0.05)]; + } + + 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()){ + $enderChest->setViewerCount($enderChest->getViewerCount() + 1); + $player->setCurrentWindow(new EnderChestInventory($this->position, $player->getEnderInventory())); + } + } + + return true; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaBlocks::OBSIDIAN()->asItem()->setCount(8) + ]; + } +} diff --git a/src/block/Farmland.php b/src/block/Farmland.php new file mode 100644 index 0000000000..81f6a43e0c --- /dev/null +++ b/src/block/Farmland.php @@ -0,0 +1,130 @@ +wetness; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->wetness = BlockDataSerializer::readBoundedInt("wetness", $stateMeta, 0, 7); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getWetness() : int{ return $this->wetness; } + + /** @return $this */ + public function setWetness(int $wetness) : self{ + if($wetness < 0 || $wetness > 7){ + throw new \InvalidArgumentException("Wetness must be in range 0-7"); + } + $this->wetness = $wetness; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109) + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::UP)->isSolid()){ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::DIRT()); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if(!$this->canHydrate()){ + if($this->wetness > 0){ + $this->wetness--; + $this->position->getWorld()->setBlock($this->position, $this, false); + }else{ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::DIRT()); + } + }elseif($this->wetness < 7){ + $this->wetness = 7; + $this->position->getWorld()->setBlock($this->position, $this, false); + } + } + + public function onEntityLand(Entity $entity) : ?float{ + if($entity instanceof Living && lcg_value() < $entity->getFallDistance() - 0.5){ + $ev = new EntityTrampleFarmlandEvent($entity, $this); + $ev->call(); + if(!$ev->isCancelled()){ + $this->getPosition()->getWorld()->setBlock($this->getPosition(), VanillaBlocks::DIRT()); + } + } + return null; + } + + protected function canHydrate() : bool{ + //TODO: check rain + $start = $this->position->add(-4, 0, -4); + $end = $this->position->add(4, 1, 4); + for($y = $start->y; $y <= $end->y; ++$y){ + for($z = $start->z; $z <= $end->z; ++$z){ + for($x = $start->x; $x <= $end->x; ++$x){ + if($this->position->getWorld()->getBlockAt($x, $y, $z) instanceof Water){ + return true; + } + } + } + } + + return false; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaBlocks::DIRT()->asItem() + ]; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaBlocks::DIRT()->asItem(); + } +} diff --git a/src/block/Fence.php b/src/block/Fence.php new file mode 100644 index 0000000000..0ee3cdaf1b --- /dev/null +++ b/src/block/Fence.php @@ -0,0 +1,96 @@ + dummy */ + protected array $connections = []; + + public function getThickness() : float{ + return 0.25; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + + foreach(Facing::HORIZONTAL as $facing){ + $block = $this->getSide($facing); + if($block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent())){ + $this->connections[$facing] = true; + }else{ + unset($this->connections[$facing]); + } + } + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + $inset = 0.5 - $this->getThickness() / 2; + + /** @var AxisAlignedBB[] $bbs */ + $bbs = []; + + $connectWest = isset($this->connections[Facing::WEST]); + $connectEast = isset($this->connections[Facing::EAST]); + + if($connectWest or $connectEast){ + //X axis (west/east) + $bbs[] = AxisAlignedBB::one() + ->squash(Axis::Z, $inset) + ->extend(Facing::UP, 0.5) + ->trim(Facing::WEST, $connectWest ? 0 : $inset) + ->trim(Facing::EAST, $connectEast ? 0 : $inset); + } + + $connectNorth = isset($this->connections[Facing::NORTH]); + $connectSouth = isset($this->connections[Facing::SOUTH]); + + if($connectNorth or $connectSouth){ + //Z axis (north/south) + $bbs[] = AxisAlignedBB::one() + ->squash(Axis::X, $inset) + ->extend(Facing::UP, 0.5) + ->trim(Facing::NORTH, $connectNorth ? 0 : $inset) + ->trim(Facing::SOUTH, $connectSouth ? 0 : $inset); + } + + if(count($bbs) === 0){ + //centre post AABB (only needed if not connected on any axis - other BBs overlapping will do this if any connections are made) + return [ + AxisAlignedBB::one() + ->extend(Facing::UP, 0.5) + ->contract($inset, 0, $inset) + ]; + } + + return $bbs; + } +} diff --git a/src/block/FenceGate.php b/src/block/FenceGate.php new file mode 100644 index 0000000000..32d79eba32 --- /dev/null +++ b/src/block/FenceGate.php @@ -0,0 +1,131 @@ +facing) | + ($this->open ? BlockLegacyMetadata::FENCE_GATE_FLAG_OPEN : 0) | + ($this->inWall ? BlockLegacyMetadata::FENCE_GATE_FLAG_IN_WALL : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + $this->open = ($stateMeta & BlockLegacyMetadata::FENCE_GATE_FLAG_OPEN) !== 0; + $this->inWall = ($stateMeta & BlockLegacyMetadata::FENCE_GATE_FLAG_IN_WALL) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isOpen() : bool{ return $this->open; } + + /** @return $this */ + public function setOpen(bool $open) : self{ + $this->open = $open; + return $this; + } + + public function isInWall() : bool{ return $this->inWall; } + + /** @return $this */ + public function setInWall(bool $inWall) : self{ + $this->inWall = $inWall; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return $this->open ? [] : [AxisAlignedBB::one()->extend(Facing::UP, 0.5)->squash(Facing::axis($this->facing), 6 / 16)]; + } + + private function checkInWall() : bool{ + return ( + $this->getSide(Facing::rotateY($this->facing, false)) instanceof Wall or + $this->getSide(Facing::rotateY($this->facing, true)) instanceof Wall + ); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $this->facing = $player->getHorizontalFacing(); + } + + $this->inWall = $this->checkInWall(); + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $inWall = $this->checkInWall(); + if($inWall !== $this->inWall){ + $this->inWall = $inWall; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->open = !$this->open; + if($this->open and $player !== null){ + $playerFacing = $player->getHorizontalFacing(); + if($playerFacing === Facing::opposite($this->facing)){ + $this->facing = $playerFacing; + } + } + + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->addSound($this->position, new DoorSound()); + return true; + } + + public function getFuelTime() : int{ + return 300; + } + + public function getFlameEncouragement() : int{ + return 5; + } + + public function getFlammability() : int{ + return 20; + } +} diff --git a/src/pocketmine/block/Fire.php b/src/block/Fire.php similarity index 61% rename from src/pocketmine/block/Fire.php rename to src/block/Fire.php index 832ab61947..82c08bfe03 100644 --- a/src/pocketmine/block/Fire.php +++ b/src/block/Fire.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\BlockDataSerializer; use pocketmine\entity\Entity; use pocketmine\entity\projectile\Arrow; use pocketmine\event\block\BlockBurnEvent; @@ -30,50 +31,62 @@ use pocketmine\event\entity\EntityCombustByBlockEvent; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\item\Item; -use pocketmine\math\Vector3; +use pocketmine\math\Facing; use function min; use function mt_rand; class Fire extends Flowable{ - protected $id = self::FIRE; + protected int $age = 0; - public function __construct(int $meta = 0){ - $this->meta = $meta; + protected function writeStateToMeta() : int{ + return $this->age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 15); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 15){ + throw new \InvalidArgumentException("Age must be in range 0-15"); + } + $this->age = $age; + return $this; } public function hasEntityCollision() : bool{ return true; } - public function getName() : string{ - return "Fire Block"; - } - public function getLightLevel() : int{ return 15; } - public function isBreakable(Item $item) : bool{ - return false; - } - public function canBeReplaced() : bool{ return true; } - public function onEntityCollide(Entity $entity) : void{ + public function onEntityInside(Entity $entity) : bool{ $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, 1); $entity->attack($ev); $ev = new EntityCombustByBlockEvent($this, $entity, 8); if($entity instanceof Arrow){ - $ev->setCancelled(); + $ev->cancel(); } $ev->call(); if(!$ev->isCancelled()){ $entity->setOnFire($ev->getDuration()); } + return true; } public function getDropsForCompatibleTool(Item $item) : array{ @@ -81,10 +94,10 @@ class Fire extends Flowable{ } public function onNearbyBlockChange() : void{ - if(!$this->getSide(Vector3::SIDE_DOWN)->isSolid() and !$this->hasAdjacentFlammableBlocks()){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), true); + if(!$this->getSide(Facing::DOWN)->isSolid() and !$this->hasAdjacentFlammableBlocks()){ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); }else{ - $this->level->scheduleDelayedBlockUpdate($this, mt_rand(30, 40)); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); } } @@ -93,35 +106,35 @@ class Fire extends Flowable{ } public function onRandomTick() : void{ - $down = $this->getSide(Vector3::SIDE_DOWN); + $down = $this->getSide(Facing::DOWN); $result = null; - if($this->meta < 15 and mt_rand(0, 2) === 0){ - $this->meta++; + if($this->age < 15 and mt_rand(0, 2) === 0){ + $this->age++; $result = $this; } $canSpread = true; if(!$down->burnsForever()){ //TODO: check rain - if($this->meta === 15){ + if($this->age === 15){ if(!$down->isFlammable() and mt_rand(0, 3) === 3){ //1/4 chance to extinguish $canSpread = false; - $result = BlockFactory::get(Block::AIR); + $result = VanillaBlocks::AIR(); } }elseif(!$this->hasAdjacentFlammableBlocks()){ $canSpread = false; - if(!$down->isSolid() or $this->meta > 3){ //fire older than 3, or without a solid block below - $result = BlockFactory::get(Block::AIR); + if(!$down->isSolid() or $this->age > 3){ + $result = VanillaBlocks::AIR(); } } } if($result !== null){ - $this->level->setBlock($this, $result); + $this->position->getWorld()->setBlock($this->position, $result); } - $this->level->scheduleDelayedBlockUpdate($this, mt_rand(30, 40)); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(30, 40)); if($canSpread){ //TODO: raise upper bound for chance in humid biomes @@ -131,8 +144,8 @@ class Fire extends Flowable{ } //vanilla uses a 250 upper bound here, but I don't think they intended to increase the chance of incineration - $this->burnBlock($this->getSide(Vector3::SIDE_UP), 350); - $this->burnBlock($this->getSide(Vector3::SIDE_DOWN), 350); + $this->burnBlock($this->getSide(Facing::UP), 350); + $this->burnBlock($this->getSide(Facing::DOWN), 350); //TODO: fire spread } @@ -143,8 +156,8 @@ class Fire extends Flowable{ } private function hasAdjacentFlammableBlocks() : bool{ - for($i = 0; $i <= 5; ++$i){ - if($this->getSide($i)->isFlammable()){ + foreach(Facing::ALL as $face){ + if($this->getSide($face)->isFlammable()){ return true; } } @@ -159,10 +172,12 @@ class Fire extends Flowable{ if(!$ev->isCancelled()){ $block->onIncinerate(); - if(mt_rand(0, $this->meta + 9) < 5){ //TODO: check rain - $this->level->setBlock($block, BlockFactory::get(Block::FIRE, min(15, $this->meta + (mt_rand(0, 4) >> 2)))); + 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{ - $this->level->setBlock($block, BlockFactory::get(Block::AIR)); + $this->position->getWorld()->setBlock($block->position, VanillaBlocks::AIR()); } } } diff --git a/src/block/FletchingTable.php b/src/block/FletchingTable.php new file mode 100644 index 0000000000..64a71ad975 --- /dev/null +++ b/src/block/FletchingTable.php @@ -0,0 +1,30 @@ +rotation = $stateMeta; + } + + protected function writeStateToMeta() : int{ + return $this->rotation; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + protected function getSupportingFace() : int{ + return Facing::DOWN; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face !== Facing::UP){ + return false; + } + + if($player !== null){ + $this->rotation = self::getRotationFromYaw($player->getLocation()->getYaw()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/block/FloorCoralFan.php b/src/block/FloorCoralFan.php new file mode 100644 index 0000000000..262925c063 --- /dev/null +++ b/src/block/FloorCoralFan.php @@ -0,0 +1,122 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->dead = $id === $this->idInfoFlattened->getSecondId(); + $this->axis = ($stateMeta >> 3) === BlockLegacyMetadata::CORAL_FAN_EAST_WEST ? Axis::X : Axis::Z; + $coralType = CoralTypeIdMap::getInstance()->fromId($stateMeta & BlockLegacyMetadata::CORAL_FAN_TYPE_MASK); + if($coralType === null){ + throw new InvalidBlockStateException("No such coral type"); + } + $this->coralType = $coralType; + } + + public function getId() : int{ + return $this->dead ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + public function asItem() : Item{ + //TODO: HACK! workaround dead flag being lost when broken / blockpicked (original impl only uses first ID) + return ItemFactory::getInstance()->get( + $this->dead ? ItemIds::CORAL_FAN_DEAD : ItemIds::CORAL_FAN, + $this->writeStateToItemMeta() + ); + } + + protected function writeStateToMeta() : int{ + return (($this->axis === Axis::X ? BlockLegacyMetadata::CORAL_FAN_EAST_WEST : BlockLegacyMetadata::CORAL_FAN_NORTH_SOUTH) << 3) | + CoralTypeIdMap::getInstance()->toId($this->coralType); + } + + protected function writeStateToItemMeta() : int{ + return CoralTypeIdMap::getInstance()->toId($this->coralType); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getAxis() : int{ return $this->axis; } + + /** @return $this */ + public function setAxis(int $axis) : self{ + if($axis !== Axis::X && $axis !== Axis::Z){ + throw new \InvalidArgumentException("Axis must be X or Z only"); + } + $this->axis = $axis; + return $this; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$tx->fetchBlock($blockReplace->getPosition()->down())->isSolid()){ + return false; + } + if($player !== null){ + $playerBlockPos = $player->getPosition()->floor(); + $directionVector = $blockReplace->getPosition()->subtractVector($playerBlockPos)->normalize(); + $angle = rad2deg(atan2($directionVector->getZ(), $directionVector->getX())); + + if($angle <= 45 || 315 <= $angle || (135 <= $angle && $angle <= 225)){ + //TODO: This produces Z axis 75% of the time, because any negative angle will produce Z axis. + //This is a bug in vanilla. https://bugs.mojang.com/browse/MCPE-125311 + $this->axis = Axis::Z; + } + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); + if(!$world->getBlock($this->position->down())->isSolid()){ + $world->useBreakOn($this->position); + }else{ + parent::onNearbyBlockChange(); + } + } + +} diff --git a/src/block/FloorSign.php b/src/block/FloorSign.php new file mode 100644 index 0000000000..7ee4880dd3 --- /dev/null +++ b/src/block/FloorSign.php @@ -0,0 +1,62 @@ +rotation = $stateMeta; + } + + protected function writeStateToMeta() : int{ + return $this->rotation; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + protected function getSupportingFace() : int{ + return Facing::DOWN; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face !== Facing::UP){ + return false; + } + + if($player !== null){ + $this->rotation = self::getRotationFromYaw($player->getLocation()->getYaw()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/pocketmine/block/Flowable.php b/src/block/Flowable.php similarity index 87% rename from src/pocketmine/block/Flowable.php rename to src/block/Flowable.php index 5df4e9690d..60f737e1d7 100644 --- a/src/pocketmine/block/Flowable.php +++ b/src/block/Flowable.php @@ -31,15 +31,14 @@ abstract class Flowable extends Transparent{ return true; } - public function getHardness() : float{ - return 0; - } - public function isSolid() : bool{ return false; } - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return null; + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; } } diff --git a/src/pocketmine/block/Dandelion.php b/src/block/Flower.php similarity index 58% rename from src/pocketmine/block/Dandelion.php rename to src/block/Flower.php index 6ffb28a7fa..a7e10dfab7 100644 --- a/src/pocketmine/block/Dandelion.php +++ b/src/block/Flower.php @@ -24,35 +24,25 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; -class Dandelion extends Flowable{ +class Flower extends Flowable{ - protected $id = self::DANDELION; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Dandelion"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === Block::GRASS or $down->getId() === Block::DIRT or $down->getId() === Block::FARMLAND){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; + 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){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } return false; } public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); } } diff --git a/src/block/FlowerPot.php b/src/block/FlowerPot.php new file mode 100644 index 0000000000..79e8e80b1b --- /dev/null +++ b/src/block/FlowerPot.php @@ -0,0 +1,141 @@ +plant !== null ? BlockLegacyMetadata::FLOWER_POT_FLAG_OCCUPIED : 0; + } + + public function getStateBitmask() : int{ + return 0b1; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileFlowerPot){ + $this->setPlant($tile->getPlant()); + }else{ + $this->setPlant(null); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileFlowerPot); + $tile->setPlant($this->plant); + } + + public function getPlant() : ?Block{ + return $this->plant; + } + + /** @return $this */ + public function setPlant(?Block $plant) : self{ + if($plant === null or $plant instanceof Air){ + $this->plant = null; + }else{ + $this->plant = clone $plant; + } + return $this; + } + + public function canAddPlant(Block $block) : bool{ + if($this->plant !== null){ + return false; + } + + 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 + //TODO: bamboo + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->contract(3 / 16, 0, 3 / 16)->trim(Facing::UP, 5 / 8)]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + return false; + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $plant = $item->getBlock(); + if(!$this->canAddPlant($plant)){ + return false; + } + + $this->setPlant($plant); + $item->pop(); + $this->position->getWorld()->setBlock($this->position, $this); + + return true; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + $items = parent::getDropsForCompatibleTool($item); + if($this->plant !== null){ + $items[] = $this->plant->asItem(); + } + + return $items; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return $this->plant !== null ? $this->plant->asItem() : parent::getPickedItem($addUserData); + } +} diff --git a/src/block/FrostedIce.php b/src/block/FrostedIce.php new file mode 100644 index 0000000000..269832d493 --- /dev/null +++ b/src/block/FrostedIce.php @@ -0,0 +1,117 @@ +age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 3); + } + + protected function writeStateToMeta() : int{ + return $this->age; + } + + public function getStateBitmask() : int{ + return 0b11; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 3){ + throw new \InvalidArgumentException("Age must be in range 0-3"); + } + $this->age = $age; + return $this; + } + + public function onNearbyBlockChange() : void{ + if(!$this->checkAdjacentBlocks(2)){ + $this->position->getWorld()->useBreakOn($this->position); + }else{ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + } + } + + public function onRandomTick() : void{ + if((!$this->checkAdjacentBlocks(4) or mt_rand(0, 2) === 0) and + $this->position->getWorld()->getHighestAdjacentFullLightAt($this->position->x, $this->position->y, $this->position->z) >= 12 - $this->age){ + if($this->tryMelt()){ + foreach($this->getAllSides() as $block){ + if($block instanceof FrostedIce){ + $block->tryMelt(); + } + } + } + }else{ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + } + } + + public function onScheduledUpdate() : void{ + $this->onRandomTick(); + } + + private function checkAdjacentBlocks(int $requirement) : bool{ + $found = 0; + for($x = -1; $x <= 1; ++$x){ + for($z = -1; $z <= 1; ++$z){ + if($x === 0 and $z === 0){ + continue; + } + if( + $this->position->getWorld()->getBlockAt($this->position->x + $x, $this->position->y, $this->position->z + $z) instanceof FrostedIce and + ++$found >= $requirement + ){ + return true; + } + } + } + return false; + } + + /** + * Updates the age of the ice, destroying it if appropriate. + * + * @return bool Whether the ice was destroyed. + */ + private function tryMelt() : bool{ + if($this->age >= 3){ + $this->position->getWorld()->useBreakOn($this->position); + return true; + } + + $this->age++; + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, mt_rand(20, 40)); + return false; + } +} diff --git a/src/block/Furnace.php b/src/block/Furnace.php new file mode 100644 index 0000000000..c49ffa35b6 --- /dev/null +++ b/src/block/Furnace.php @@ -0,0 +1,90 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->lit ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->readFacingStateFromData($id, $stateMeta); + $this->lit = $id === $this->idInfoFlattened->getSecondId(); + } + + public function getLightLevel() : int{ + return $this->lit ? 13 : 0; + } + + public function isLit() : bool{ + return $this->lit; + } + + /** + * @return $this + */ + public function setLit(bool $lit = true) : self{ + $this->lit = $lit; + return $this; + } + + 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())){ + $player->setCurrentWindow($furnace->getInventory()); + } + } + + return true; + } + + public function onScheduledUpdate() : void{ + $furnace = $this->position->getWorld()->getTile($this->position); + if($furnace instanceof TileFurnace and $furnace->onUpdate()){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); //TODO: check this + } + } +} diff --git a/src/pocketmine/block/Glass.php b/src/block/Glass.php similarity index 80% rename from src/pocketmine/block/Glass.php rename to src/block/Glass.php index 42bd5cdd9b..dc1e2f9b8a 100644 --- a/src/pocketmine/block/Glass.php +++ b/src/block/Glass.php @@ -27,21 +27,11 @@ use pocketmine\item\Item; class Glass extends Transparent{ - protected $id = self::GLASS; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Glass"; - } - - public function getHardness() : float{ - return 0.3; - } - public function getDropsForCompatibleTool(Item $item) : array{ return []; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } } diff --git a/src/pocketmine/block/GlassPane.php b/src/block/GlassPane.php similarity index 80% rename from src/pocketmine/block/GlassPane.php rename to src/block/GlassPane.php index c009460af8..06342541d4 100644 --- a/src/pocketmine/block/GlassPane.php +++ b/src/block/GlassPane.php @@ -27,21 +27,11 @@ use pocketmine\item\Item; class GlassPane extends Thin{ - protected $id = self::GLASS_PANE; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Glass Pane"; - } - - public function getHardness() : float{ - return 0.3; - } - public function getDropsForCompatibleTool(Item $item) : array{ return []; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } } diff --git a/src/block/GlazedTerracotta.php b/src/block/GlazedTerracotta.php new file mode 100644 index 0000000000..6cd41c8f8d --- /dev/null +++ b/src/block/GlazedTerracotta.php @@ -0,0 +1,32 @@ +meta = $meta; - } - - public function getName() : string{ - return "Glowstone"; - } - - public function getHardness() : float{ - return 0.3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - public function getLightLevel() : int{ return 15; } public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::GLOWSTONE_DUST, 0, mt_rand(2, 4)) + VanillaItems::GLOWSTONE_DUST()->setCount(mt_rand(2, 4)) ]; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } } diff --git a/src/block/Grass.php b/src/block/Grass.php new file mode 100644 index 0000000000..e6a1288508 --- /dev/null +++ b/src/block/Grass.php @@ -0,0 +1,112 @@ +asItem() + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function ticksRandomly() : bool{ + return true; + } + + 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){ + //grass dies + $ev = new BlockSpreadEvent($this, $this, VanillaBlocks::DIRT()); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState(), false); + } + }elseif($lightAbove >= 9){ + //try grass spread + for($i = 0; $i < 4; ++$i){ + $x = mt_rand($this->position->x - 1, $this->position->x + 1); + $y = mt_rand($this->position->y - 3, $this->position->y + 1); + $z = mt_rand($this->position->z - 1, $this->position->z + 1); + + $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 + $this->position->getWorld()->getBlockAt($x, $y + 1, $z)->getLightFilter() >= 2 + ){ + continue; + } + + $ev = new BlockSpreadEvent($b, $this, VanillaBlocks::GRASS()); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($b->position, $ev->getNewState(), false); + } + } + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($face !== Facing::UP){ + return false; + } + if($item instanceof Fertilizer){ + $item->pop(); + TallGrassObject::growGrass($this->position->getWorld(), $this->position, new Random(mt_rand()), 8, 2); + + return true; + }elseif($item instanceof Hoe){ + $item->applyDamage(1); + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::FARMLAND()); + + return true; + }elseif($item instanceof Shovel and $this->getSide(Facing::UP)->getId() === BlockLegacyIds::AIR){ + $item->applyDamage(1); + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::GRASS_PATH()); + + return true; + } + + return false; + } +} diff --git a/src/pocketmine/block/CocoaBlock.php b/src/block/GrassPath.php similarity index 60% rename from src/pocketmine/block/CocoaBlock.php rename to src/block/GrassPath.php index e345bb45b9..4a46fe6899 100644 --- a/src/pocketmine/block/CocoaBlock.php +++ b/src/block/GrassPath.php @@ -24,43 +24,31 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -use pocketmine\item\ItemIds; -use function mt_rand; +use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Facing; -class CocoaBlock extends Transparent{ +class GrassPath extends Transparent{ - protected $id = self::COCOA_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109) } - public function getName() : string{ - return "Cocoa Block"; - } - - public function getHardness() : float{ - return 0.2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - //TODO - - public function isAffectedBySilkTouch() : bool{ - return false; + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::UP)->isSolid()){ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::DIRT()); + } } public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(ItemIds::DYE, 3, ($this->meta >> 2) === 2 ? mt_rand(2, 3) : 1) + VanillaBlocks::DIRT()->asItem() ]; } - public function getPickedItem() : Item{ - return ItemFactory::get(ItemIds::DYE, 3); + public function isAffectedBySilkTouch() : bool{ + return true; } } diff --git a/src/pocketmine/block/Gravel.php b/src/block/Gravel.php similarity index 71% rename from src/pocketmine/block/Gravel.php rename to src/block/Gravel.php index 211aa26eac..9cc7f3fea4 100644 --- a/src/pocketmine/block/Gravel.php +++ b/src/block/Gravel.php @@ -23,37 +23,30 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\Fallable; +use pocketmine\block\utils\FallableTrait; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; use function mt_rand; -class Gravel extends Fallable{ - - protected $id = self::GRAVEL; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Gravel"; - } - - public function getHardness() : float{ - return 0.6; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } +class Gravel extends Opaque implements Fallable{ + use FallableTrait; public function getDropsForCompatibleTool(Item $item) : array{ if(mt_rand(1, 10) === 1){ return [ - ItemFactory::get(Item::FLINT) + VanillaItems::FLINT() ]; } return parent::getDropsForCompatibleTool($item); } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function tickFalling() : ?Block{ + return null; + } } diff --git a/src/pocketmine/entity/Creature.php b/src/block/HardenedClay.php similarity index 91% rename from src/pocketmine/entity/Creature.php rename to src/block/HardenedClay.php index 5e9b4aa7a0..d1bcbd4725 100644 --- a/src/pocketmine/entity/Creature.php +++ b/src/block/HardenedClay.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\block; -abstract class Creature extends Living{ +class HardenedClay extends Opaque{ } diff --git a/src/block/HardenedGlass.php b/src/block/HardenedGlass.php new file mode 100644 index 0000000000..f9bfdd0861 --- /dev/null +++ b/src/block/HardenedGlass.php @@ -0,0 +1,28 @@ +facing = $facing; + $this->powered = ($stateMeta & BlockLegacyMetadata::HOPPER_FLAG_POWERED) !== 0; + } + + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeFacing($this->facing) | ($this->powered ? BlockLegacyMetadata::HOPPER_FLAG_POWERED : 0); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getFacing() : int{ return $this->facing; } + + /** @return $this */ + public function setFacing(int $facing) : self{ + if($facing === Facing::UP){ + throw new \InvalidArgumentException("Hopper may not face upward"); + } + $this->facing = $facing; + return $this; + } + + protected function recalculateCollisionBoxes() : array{ + $result = [ + AxisAlignedBB::one()->trim(Facing::UP, 6 / 16) //the empty area around the bottom is currently considered solid + ]; + + foreach(Facing::HORIZONTAL as $f){ //add the frame parts around the bowl + $result[] = AxisAlignedBB::one()->trim($f, 14 / 16); + } + return $result; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->facing = $face === Facing::DOWN ? Facing::DOWN : Facing::opposite($face); + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileHopper){ //TODO: find a way to have inventories open on click without this boilerplate in every block + $player->setCurrentWindow($tile->getInventory()); + } + return true; + } + return false; + } + + public function onScheduledUpdate() : void{ + //TODO + } + + //TODO: redstone logic, sucking logic +} diff --git a/src/pocketmine/block/Ice.php b/src/block/Ice.php similarity index 64% rename from src/pocketmine/block/Ice.php rename to src/block/Ice.php index 3e9953b50b..c1156fffae 100644 --- a/src/pocketmine/block/Ice.php +++ b/src/block/Ice.php @@ -23,26 +23,12 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\enchantment\Enchantment; +use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; class Ice extends Transparent{ - protected $id = self::ICE; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Ice"; - } - - public function getHardness() : float{ - return 0.5; - } - public function getLightFilter() : int{ return 2; } @@ -51,13 +37,10 @@ class Ice extends Transparent{ return 0.98; } - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function onBreak(Item $item, Player $player = null) : bool{ - if(($player === null or $player->isSurvival()) and !$item->hasEnchantment(Enchantment::SILK_TOUCH)){ - return $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::WATER), true); + public function onBreak(Item $item, ?Player $player = null) : bool{ + if(($player === null or $player->isSurvival()) and !$item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::WATER()); + return true; } return parent::onBreak($item, $player); } @@ -67,12 +50,16 @@ class Ice extends Transparent{ } public function onRandomTick() : void{ - if($this->level->getHighestAdjacentBlockLight($this->x, $this->y, $this->z) >= 12){ - $this->level->useBreakOn($this); + if($this->position->getWorld()->getHighestAdjacentBlockLight($this->position->x, $this->position->y, $this->position->z) >= 12){ + $this->position->getWorld()->useBreakOn($this->position); } } public function getDropsForCompatibleTool(Item $item) : array{ return []; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } } diff --git a/src/pocketmine/block/DoubleSlab.php b/src/block/InfestedStone.php similarity index 63% rename from src/pocketmine/block/DoubleSlab.php rename to src/block/InfestedStone.php index 8dfcceeda3..944d0e4565 100644 --- a/src/pocketmine/block/DoubleSlab.php +++ b/src/block/InfestedStone.php @@ -24,31 +24,31 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -abstract class DoubleSlab extends Solid{ +final class InfestedStone extends Opaque{ - public function __construct(int $meta = 0){ - $this->meta = $meta; + private int $imitated; + + public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo, Block $imitated){ + parent::__construct($idInfo, $name, $breakInfo); + $this->imitated = $imitated->getFullId(); } - abstract public function getSlabId() : int; - - public function getName() : string{ - return "Double " . BlockFactory::get($this->getSlabId(), $this->getVariant())->getName(); + public function getImitatedBlock() : Block{ + return BlockFactory::getInstance()->fromFullBlock($this->imitated); } public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get($this->getSlabId(), $this->getVariant(), 2) - ]; + return []; + } + + public function getSilkTouchDrops(Item $item) : array{ + return [$this->getImitatedBlock()->asItem()]; } public function isAffectedBySilkTouch() : bool{ - return false; + return true; } - public function getPickedItem() : Item{ - return ItemFactory::get($this->getSlabId(), $this->getVariant()); - } + //TODO } diff --git a/src/block/ItemFrame.php b/src/block/ItemFrame.php new file mode 100644 index 0000000000..aff9f65669 --- /dev/null +++ b/src/block/ItemFrame.php @@ -0,0 +1,185 @@ +facing) | ($this->hasMap ? BlockLegacyMetadata::ITEM_FRAME_FLAG_HAS_MAP : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::read5MinusHorizontalFacing($stateMeta); + $this->hasMap = ($stateMeta & BlockLegacyMetadata::ITEM_FRAME_FLAG_HAS_MAP) !== 0; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileItemFrame){ + $this->framedItem = $tile->getItem(); + if($this->framedItem->isNull()){ + $this->framedItem = null; + } + $this->itemRotation = $tile->getItemRotation() % self::ROTATIONS; + $this->itemDropChance = $tile->getItemDropChance(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileItemFrame){ + $tile->setItem($this->framedItem); + $tile->setItemRotation($this->itemRotation); + $tile->setItemDropChance($this->itemDropChance); + } + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getFramedItem() : ?Item{ + return $this->framedItem !== null ? clone $this->framedItem : null; + } + + /** @return $this */ + public function setFramedItem(?Item $item) : self{ + if($item === null or $item->isNull()){ + $this->framedItem = null; + $this->itemRotation = 0; + }else{ + $this->framedItem = clone $item; + } + return $this; + } + + public function getItemRotation() : int{ + return $this->itemRotation; + } + + /** @return $this */ + public function setItemRotation(int $itemRotation) : self{ + $this->itemRotation = $itemRotation; + return $this; + } + + public function getItemDropChance() : float{ + return $this->itemDropChance; + } + + /** @return $this */ + public function setItemDropChance(float $itemDropChance) : self{ + $this->itemDropChance = $itemDropChance; + return $this; + } + + public function hasMap() : bool{ return $this->hasMap; } + + /** + * This can be set irrespective of whether the frame actually contains a map or not. When set, the frame stretches + * to the edges of the block without leaving space around the edges. + * + * @return $this + */ + public function setHasMap(bool $hasMap) : self{ + $this->hasMap = $hasMap; + return $this; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($this->framedItem !== null){ + $this->itemRotation = ($this->itemRotation + 1) % self::ROTATIONS; + }elseif(!$item->isNull()){ + $this->framedItem = $item->pop(); + }else{ + return true; + } + + $this->position->getWorld()->setBlock($this->position, $this); + + return true; + } + + public function onAttack(Item $item, int $face, ?Player $player = null) : bool{ + if($this->framedItem === null){ + return false; + } + if(lcg_value() <= $this->itemDropChance){ + $this->position->getWorld()->dropItem($this->position->add(0.5, 0.5, 0.5), clone $this->framedItem); + } + $this->setFramedItem(null); + $this->position->getWorld()->setBlock($this->position, $this); + return true; + } + + public function onNearbyBlockChange() : void{ + if(!$this->getSide(Facing::opposite($this->facing))->isSolid()){ + $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($face === Facing::DOWN or $face === Facing::UP or !$blockClicked->isSolid()){ + return false; + } + + $this->facing = $face; + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + $drops = parent::getDropsForCompatibleTool($item); + if($this->framedItem !== null and lcg_value() <= $this->itemDropChance){ + $drops[] = clone $this->framedItem; + } + + return $drops; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return $this->framedItem !== null ? clone $this->framedItem : parent::getPickedItem($addUserData); + } +} diff --git a/src/block/Jukebox.php b/src/block/Jukebox.php new file mode 100644 index 0000000000..be8fa7f4c4 --- /dev/null +++ b/src/block/Jukebox.php @@ -0,0 +1,116 @@ +record !== null){ + $this->ejectRecord(); + }elseif($item instanceof Record){ + $player->sendJukeboxPopup("record.nowPlaying", [$player->getLanguage()->translate($item->getRecordType()->getTranslatableName())]); + $this->insertRecord($item->pop()); + } + } + + $this->position->getWorld()->setBlock($this->position, $this); + + return true; + } + + public function getRecord() : ?Record{ + return $this->record; + } + + public function ejectRecord() : void{ + if($this->record !== null){ + $this->getPosition()->getWorld()->dropItem($this->getPosition()->add(0.5, 1, 0.5), $this->record); + $this->record = null; + $this->stopSound(); + } + } + + public function insertRecord(Record $record) : void{ + if($this->record === null){ + $this->record = $record; + $this->startSound(); + } + } + + public function startSound() : void{ + if($this->record !== null){ + $this->getPosition()->getWorld()->addSound($this->getPosition(), new RecordSound($this->record->getRecordType())); + } + } + + public function stopSound() : void{ + $this->getPosition()->getWorld()->addSound($this->getPosition(), new RecordStopSound()); + } + + public function onBreak(Item $item, ?Player $player = null) : bool{ + $this->stopSound(); + return parent::onBreak($item, $player); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + $drops = parent::getDropsForCompatibleTool($item); + if($this->record !== null){ + $drops[] = $this->record; + } + return $drops; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $jukebox = $this->position->getWorld()->getTile($this->position); + if($jukebox instanceof JukeboxTile){ + $this->record = $jukebox->getRecord(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $jukebox = $this->position->getWorld()->getTile($this->position); + if($jukebox instanceof JukeboxTile){ + $jukebox->setRecord($this->record); + } + } + + //TODO: Jukebox has redstone effects, they are not implemented. +} diff --git a/src/block/Ladder.php b/src/block/Ladder.php new file mode 100644 index 0000000000..f76a28b6fb --- /dev/null +++ b/src/block/Ladder.php @@ -0,0 +1,81 @@ +getPosition()->floor()->distanceSquared($this->position) < 1){ //entity coordinates must be inside block + $entity->resetFallDistance(); + $entity->onGround = true; + } + return true; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim($this->facing, 13 / 16)]; + } + + 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){ + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if(!$this->getSide(Facing::opposite($this->facing))->isSolid()){ //Replace with common break method + $this->position->getWorld()->useBreakOn($this->position); + } + } +} diff --git a/src/block/Lantern.php b/src/block/Lantern.php new file mode 100644 index 0000000000..4bd2f59188 --- /dev/null +++ b/src/block/Lantern.php @@ -0,0 +1,93 @@ +hanging = ($stateMeta & BlockLegacyMetadata::LANTERN_FLAG_HANGING) !== 0; + } + + protected function writeStateToMeta() : int{ + return $this->hanging ? BlockLegacyMetadata::LANTERN_FLAG_HANGING : 0; + } + + public function getStateBitmask() : int{ + return 0b1; + } + + public function isHanging() : bool{ return $this->hanging; } + + /** @return $this */ + public function setHanging(bool $hanging) : self{ + $this->hanging = $hanging; + return $this; + } + + public function getLightLevel() : int{ + return 15; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [ + AxisAlignedBB::one() + ->trim(Facing::UP, $this->hanging ? 6 / 16 : 8 / 16) + ->trim(Facing::DOWN, $this->hanging ? 2 / 16 : 0) + ->squash(Axis::X, 5 / 16) + ->squash(Axis::Z, 5 / 16) + ]; + } + + protected function canAttachTo(Block $b) : bool{ + return !$b->isTransparent(); + } + + 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()))){ + return false; + } + + $this->hanging = ($face === Facing::DOWN or !$this->canAttachTo($this->position->getWorld()->getBlock($blockReplace->getPosition()->down()))); + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + if(!$this->canAttachTo($this->position->getWorld()->getBlock($this->hanging ? $this->position->up() : $this->position->down()))){ + $this->position->getWorld()->useBreakOn($this->position); + } + } +} diff --git a/src/pocketmine/block/Clay.php b/src/block/LapisOre.php similarity index 68% rename from src/pocketmine/block/Clay.php rename to src/block/LapisOre.php index 12758b8020..7dd434c797 100644 --- a/src/pocketmine/block/Clay.php +++ b/src/block/LapisOre.php @@ -24,31 +24,22 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; +use function mt_rand; -class Clay extends Solid{ - - protected $id = self::CLAY_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getHardness() : float{ - return 0.6; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getName() : string{ - return "Clay Block"; - } +class LapisOre extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::CLAY_BALL, 0, 4) + VanillaItems::LAPIS_LAZULI()->setCount(mt_rand(4, 8)) ]; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + protected function getXpDropAmount() : int{ + return mt_rand(2, 5); + } } diff --git a/src/pocketmine/block/Lava.php b/src/block/Lava.php similarity index 54% rename from src/pocketmine/block/Lava.php rename to src/block/Lava.php index 086fd66664..06ee5fd81b 100644 --- a/src/pocketmine/block/Lava.php +++ b/src/block/Lava.php @@ -27,41 +27,23 @@ use pocketmine\entity\Entity; use pocketmine\event\entity\EntityCombustByBlockEvent; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageEvent; -use pocketmine\item\Item; -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\Player; +use pocketmine\math\Facing; +use pocketmine\world\sound\BucketEmptyLavaSound; +use pocketmine\world\sound\BucketFillLavaSound; +use pocketmine\world\sound\Sound; class Lava extends Liquid{ - protected $id = self::FLOWING_LAVA; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - public function getLightLevel() : int{ return 15; } - public function getName() : string{ - return "Lava"; + public function getBucketFillSound() : Sound{ + return new BucketFillLavaSound(); } - public function getStillForm() : Block{ - return BlockFactory::get(Block::STILL_LAVA, $this->meta); - } - - public function getFlowingForm() : Block{ - return BlockFactory::get(Block::FLOWING_LAVA, $this->meta); - } - - public function getBucketFillSound() : int{ - return LevelSoundEventPacket::SOUND_BUCKET_FILL_LAVA; - } - - public function getBucketEmptySound() : int{ - return LevelSoundEventPacket::SOUND_BUCKET_EMPTY_LAVA; + public function getBucketEmptySound() : Sound{ + return new BucketEmptyLavaSound(); } public function tickRate() : int{ @@ -72,9 +54,15 @@ class Lava extends Liquid{ return 2; //TODO: this is 1 in the nether } - protected function checkForHarden(){ + protected function checkForHarden() : bool{ + if($this->falling){ + return false; + } $colliding = null; - for($side = 1; $side <= 5; ++$side){ //don't check downwards side + foreach(Facing::ALL as $side){ + if($side === Facing::DOWN){ + continue; + } $blockSide = $this->getSide($side); if($blockSide instanceof Water){ $colliding = $blockSide; @@ -83,23 +71,26 @@ class Lava extends Liquid{ } if($colliding !== null){ - if($this->getDamage() === 0){ - $this->liquidCollide($colliding, BlockFactory::get(Block::OBSIDIAN)); - }elseif($this->getDamage() <= 4){ - $this->liquidCollide($colliding, BlockFactory::get(Block::COBBLESTONE)); + if($this->decay === 0){ + $this->liquidCollide($colliding, VanillaBlocks::OBSIDIAN()); + return true; + }elseif($this->decay <= 4){ + $this->liquidCollide($colliding, VanillaBlocks::COBBLESTONE()); + return true; } } + return false; } - protected function flowIntoBlock(Block $block, int $newFlowDecay) : void{ + protected function flowIntoBlock(Block $block, int $newFlowDecay, bool $falling) : void{ if($block instanceof Water){ - $block->liquidCollide($this, BlockFactory::get(Block::STONE)); + $block->liquidCollide($this, VanillaBlocks::STONE()); }else{ - parent::flowIntoBlock($block, $newFlowDecay); + parent::flowIntoBlock($block, $newFlowDecay, $falling); } } - public function onEntityCollide(Entity $entity) : void{ + public function onEntityInside(Entity $entity) : bool{ $entity->fallDistance *= 0.5; $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_LAVA, 4); @@ -112,12 +103,6 @@ class Lava extends Liquid{ } $entity->resetFallDistance(); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $ret = $this->getLevelNonNull()->setBlock($this, $this, true, false); - $this->getLevelNonNull()->scheduleDelayedBlockUpdate($this, $this->tickRate()); - - return $ret; + return true; } } diff --git a/src/block/Leaves.php b/src/block/Leaves.php new file mode 100644 index 0000000000..03c99936e8 --- /dev/null +++ b/src/block/Leaves.php @@ -0,0 +1,166 @@ +treeType = $treeType; + } + + protected function writeStateToMeta() : int{ + return ($this->noDecay ? BlockLegacyMetadata::LEAVES_FLAG_NO_DECAY : 0) | ($this->checkDecay ? BlockLegacyMetadata::LEAVES_FLAG_CHECK_DECAY : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->noDecay = ($stateMeta & BlockLegacyMetadata::LEAVES_FLAG_NO_DECAY) !== 0; + $this->checkDecay = ($stateMeta & BlockLegacyMetadata::LEAVES_FLAG_CHECK_DECAY) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1100; + } + + public function isNoDecay() : bool{ return $this->noDecay; } + + /** @return $this */ + public function setNoDecay(bool $noDecay) : self{ + $this->noDecay = $noDecay; + return $this; + } + + public function isCheckDecay() : bool{ return $this->checkDecay; } + + /** @return $this */ + public function setCheckDecay(bool $checkDecay) : self{ + $this->checkDecay = $checkDecay; + return $this; + } + + public function blocksDirectSkyLight() : bool{ + return true; + } + + /** + * @param true[] $visited reference parameter + * @phpstan-param array $visited + */ + protected function findLog(Vector3 $pos, array &$visited = [], int $distance = 0) : bool{ + $index = World::blockHash($pos->x, $pos->y, $pos->z); + if(isset($visited[$index])){ + return false; + } + $visited[$index] = true; + + $block = $this->position->getWorld()->getBlock($pos); + if($block instanceof Wood){ //type doesn't matter + return true; + } + + if($block->getId() === $this->getId() and $distance <= 4){ + foreach(Facing::ALL as $side){ + if($this->findLog($pos->getSide($side), $visited, $distance + 1)){ + return true; + } + } + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if(!$this->noDecay and !$this->checkDecay){ + $this->checkDecay = true; + $this->position->getWorld()->setBlock($this->position, $this, false); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if(!$this->noDecay and $this->checkDecay){ + $ev = new LeavesDecayEvent($this); + $ev->call(); + if($ev->isCancelled() or $this->findLog($this->position)){ + $this->checkDecay = false; + $this->position->getWorld()->setBlock($this->position, $this, false); + }else{ + $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{ + $this->noDecay = true; //artificial leaves don't decay + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){ + return parent::getDropsForCompatibleTool($item); + } + + $drops = []; + 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 + $drops[] = VanillaItems::APPLE(); + } + + return $drops; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function getFlameEncouragement() : int{ + return 30; + } + + public function getFlammability() : int{ + return 60; + } +} diff --git a/src/block/Lever.php b/src/block/Lever.php new file mode 100644 index 0000000000..905ee87561 --- /dev/null +++ b/src/block/Lever.php @@ -0,0 +1,139 @@ +facing = LeverFacing::UP_AXIS_X(); + parent::__construct($idInfo, $name, $breakInfo); + } + + protected function writeStateToMeta() : int{ + $rotationMeta = match($this->facing->id()){ + LeverFacing::DOWN_AXIS_X()->id() => 0, + LeverFacing::EAST()->id() => 1, + LeverFacing::WEST()->id() => 2, + LeverFacing::SOUTH()->id() => 3, + LeverFacing::NORTH()->id() => 4, + LeverFacing::UP_AXIS_Z()->id() => 5, + LeverFacing::UP_AXIS_X()->id() => 6, + LeverFacing::DOWN_AXIS_Z()->id() => 7, + default => throw new AssumptionFailedError(), + }; + return $rotationMeta | ($this->activated ? BlockLegacyMetadata::LEVER_FLAG_POWERED : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $rotationMeta = $stateMeta & 0x07; + $this->facing = match($rotationMeta){ + 0 => LeverFacing::DOWN_AXIS_X(), + 1 => LeverFacing::EAST(), + 2 => LeverFacing::WEST(), + 3 => LeverFacing::SOUTH(), + 4 => LeverFacing::NORTH(), + 5 => LeverFacing::UP_AXIS_Z(), + 6 => LeverFacing::UP_AXIS_X(), + 7 => LeverFacing::DOWN_AXIS_Z(), + default => throw new AssumptionFailedError("0x07 mask should make this impossible"), //phpstan doesn't understand :( + }; + + $this->activated = ($stateMeta & BlockLegacyMetadata::LEVER_FLAG_POWERED) !== 0; + } + + public function getFacing() : LeverFacing{ return $this->facing; } + + /** @return $this */ + public function setFacing(LeverFacing $facing) : self{ + $this->facing = $facing; + return $this; + } + + public function isActivated() : bool{ return $this->activated; } + + /** @return $this */ + public function setActivated(bool $activated) : self{ + $this->activated = $activated; + return $this; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$blockClicked->isSolid()){ + return false; + } + + $selectUpDownPos = function(LeverFacing $x, LeverFacing $z) use ($player) : LeverFacing{ + if($player !== null){ + return Facing::axis($player->getHorizontalFacing()) === Axis::X ? $x : $z; + } + return $x; + }; + $this->facing = match($face){ + Facing::DOWN => $selectUpDownPos(LeverFacing::DOWN_AXIS_X(), LeverFacing::DOWN_AXIS_Z()), + Facing::UP => $selectUpDownPos(LeverFacing::UP_AXIS_X(), LeverFacing::UP_AXIS_Z()), + Facing::NORTH => LeverFacing::NORTH(), + Facing::SOUTH => LeverFacing::SOUTH(), + Facing::WEST => LeverFacing::WEST(), + Facing::EAST => LeverFacing::EAST(), + default => throw new AssumptionFailedError("Bad facing value"), + }; + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + if(!$this->getSide(Facing::opposite($this->facing->getFacing()))->isSolid()){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->activated = !$this->activated; + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->addSound( + $this->position->add(0.5, 0.5, 0.5), + $this->activated ? new RedstonePowerOnSound() : new RedstonePowerOffSound() + ); + return true; + } + + //TODO +} diff --git a/src/block/Liquid.php b/src/block/Liquid.php new file mode 100644 index 0000000000..6e3c8df359 --- /dev/null +++ b/src/block/Liquid.php @@ -0,0 +1,388 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->still ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + protected function writeStateToMeta() : int{ + return $this->decay | ($this->falling ? BlockLegacyMetadata::LIQUID_FLAG_FALLING : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->decay = BlockDataSerializer::readBoundedInt("decay", $stateMeta & 0x07, 0, 7); + $this->falling = ($stateMeta & BlockLegacyMetadata::LIQUID_FLAG_FALLING) !== 0; + $this->still = $id === $this->idInfoFlattened->getSecondId(); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isFalling() : bool{ return $this->falling; } + + /** @return $this */ + public function setFalling(bool $falling) : self{ + $this->falling = $falling; + return $this; + } + + public function getDecay() : int{ return $this->decay; } + + /** @return $this */ + public function setDecay(int $decay) : self{ + $this->decay = $decay; + return $this; + } + + public function hasEntityCollision() : bool{ + return true; + } + + public function canBeReplaced() : bool{ + return true; + } + + public function canBeFlowedInto() : bool{ + return true; + } + + public function isSolid() : bool{ + return false; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return []; + } + + public function getStillForm() : Block{ + $b = clone $this; + $b->still = true; + return $b; + } + + public function getFlowingForm() : Block{ + $b = clone $this; + $b->still = false; + return $b; + } + + abstract public function getBucketFillSound() : Sound; + + abstract public function getBucketEmptySound() : Sound; + + public function isSource() : bool{ + return !$this->falling and $this->decay === 0; + } + + /** + * @return float + */ + public function getFluidHeightPercent(){ + return (($this->falling ? 0 : $this->decay) + 1) / 9; + } + + public function isStill() : bool{ + return $this->still; + } + + /** + * @return $this + */ + public function setStill(bool $still = true) : self{ + $this->still = $still; + return $this; + } + + protected function getEffectiveFlowDecay(Block $block) : int{ + if(!($block instanceof Liquid) or !$block->isSameType($this)){ + return -1; + } + + return $block->falling ? 0 : $block->decay; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $this->flowVector = null; + } + + public function getFlowVector() : Vector3{ + if($this->flowVector !== null){ + return $this->flowVector; + } + + $vX = $vY = $vZ = 0; + + $decay = $this->getEffectiveFlowDecay($this); + + $world = $this->position->getWorld(); + + foreach(Facing::HORIZONTAL as $j){ + $x = $this->position->x; + $y = $this->position->y; + $z = $this->position->z; + + match($j){ + Facing::WEST => --$x, + Facing::EAST => ++$x, + Facing::NORTH => --$z, + Facing::SOUTH => ++$z + }; + + $sideBlock = $world->getBlockAt($x, $y, $z); + $blockDecay = $this->getEffectiveFlowDecay($sideBlock); + + if($blockDecay < 0){ + if(!$sideBlock->canBeFlowedInto()){ + continue; + } + + $blockDecay = $this->getEffectiveFlowDecay($world->getBlockAt($x, $y - 1, $z)); + + if($blockDecay >= 0){ + $realDecay = $blockDecay - ($decay - 8); + $vX += ($x - $this->position->x) * $realDecay; + $vY += ($y - $this->position->y) * $realDecay; + $vZ += ($z - $this->position->z) * $realDecay; + } + + continue; + }else{ + $realDecay = $blockDecay - $decay; + $vX += ($x - $this->position->x) * $realDecay; + $vY += ($y - $this->position->y) * $realDecay; + $vZ += ($z - $this->position->z) * $realDecay; + } + } + + $vector = new Vector3($vX, $vY, $vZ); + + if($this->falling){ + foreach(Facing::HORIZONTAL as $facing){ + $pos = $this->position->getSide($facing); + if( + !$this->canFlowInto($world->getBlockAt($pos->x, $pos->y, $pos->z)) || + !$this->canFlowInto($world->getBlockAt($pos->x, $pos->y + 1, $pos->z)) + ){ + $vector = $vector->normalize()->add(0, -6, 0); + break; + } + } + } + + return $this->flowVector = $vector->normalize(); + } + + public function addVelocityToEntity(Entity $entity) : ?Vector3{ + if($entity->canBeMovedByCurrents()){ + return $this->getFlowVector(); + } + return null; + } + + abstract public function tickRate() : int; + + /** + * Returns how many liquid levels are lost per block flowed horizontally. Affects how far the liquid can flow. + */ + public function getFlowDecayPerBlock() : int{ + return 1; + } + + /** + * Returns the number of source blocks of this liquid that must be horizontally adjacent to this block in order for + * this block to become a source block itself, or null if the liquid does not exhibit source-forming behaviour. + */ + public function getMinAdjacentSourcesToFormSource() : ?int{ + return null; + } + + public function onNearbyBlockChange() : void{ + if(!$this->checkForHarden()){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, $this->tickRate()); + } + } + + public function onScheduledUpdate() : void{ + $multiplier = $this->getFlowDecayPerBlock(); + + $world = $this->position->getWorld(); + + if(!$this->isSource()){ + $smallestFlowDecay = -100; + $this->adjacentSources = 0; + $smallestFlowDecay = $this->getSmallestFlowDecay($world->getBlockAt($this->position->x, $this->position->y, $this->position->z - 1), $smallestFlowDecay); + $smallestFlowDecay = $this->getSmallestFlowDecay($world->getBlockAt($this->position->x, $this->position->y, $this->position->z + 1), $smallestFlowDecay); + $smallestFlowDecay = $this->getSmallestFlowDecay($world->getBlockAt($this->position->x - 1, $this->position->y, $this->position->z), $smallestFlowDecay); + $smallestFlowDecay = $this->getSmallestFlowDecay($world->getBlockAt($this->position->x + 1, $this->position->y, $this->position->z), $smallestFlowDecay); + + $newDecay = $smallestFlowDecay + $multiplier; + $falling = false; + + if($newDecay >= 8 or $smallestFlowDecay < 0){ + $newDecay = -1; + } + + if($this->getEffectiveFlowDecay($world->getBlockAt($this->position->x, $this->position->y + 1, $this->position->z)) >= 0){ + $falling = true; + } + + $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())){ + $newDecay = 0; + $falling = false; + } + } + + if($falling !== $this->falling or (!$falling and $newDecay !== $this->decay)){ + if(!$falling and $newDecay < 0){ + $world->setBlock($this->position, VanillaBlocks::AIR()); + return; + } + + $this->falling = $falling; + $this->decay = $falling ? 0 : $newDecay; + $world->setBlock($this->position, $this); //local block update will cause an update to be scheduled + } + } + + $bottomBlock = $world->getBlockAt($this->position->x, $this->position->y - 1, $this->position->z); + + $this->flowIntoBlock($bottomBlock, 0, true); + + if($this->isSource() or !$bottomBlock->canBeFlowedInto()){ + if($this->falling){ + $adjacentDecay = 1; //falling liquid behaves like source block + }else{ + $adjacentDecay = $this->decay + $multiplier; + } + + if($adjacentDecay < 8){ + $calculator = new MinimumCostFlowCalculator($this->position->getWorld(), $this->getFlowDecayPerBlock(), \Closure::fromCallable([$this, 'canFlowInto'])); + foreach($calculator->getOptimalFlowDirections($this->position->getFloorX(), $this->position->getFloorY(), $this->position->getFloorZ()) as $facing){ + $this->flowIntoBlock($world->getBlock($this->position->getSide($facing)), $adjacentDecay, false); + } + } + } + + $this->checkForHarden(); + } + + protected function flowIntoBlock(Block $block, int $newFlowDecay, bool $falling) : void{ + if($this->canFlowInto($block) and !($block instanceof Liquid)){ + $new = clone $this; + $new->falling = $falling; + $new->decay = $falling ? 0 : $newFlowDecay; + + $ev = new BlockSpreadEvent($block, $this, $new); + $ev->call(); + if(!$ev->isCancelled()){ + if($block->getId() > 0){ + $this->position->getWorld()->useBreakOn($block->position); + } + + $this->position->getWorld()->setBlock($block->position, $ev->getNewState()); + } + } + } + + /** @phpstan-impure */ + private function getSmallestFlowDecay(Block $block, int $decay) : int{ + if(!($block instanceof Liquid) or !$block->isSameType($this)){ + return $decay; + } + + $blockDecay = $block->decay; + + if($block->isSource()){ + ++$this->adjacentSources; + }elseif($block->falling){ + $blockDecay = 0; + } + + return ($decay >= 0 && $blockDecay >= $decay) ? $decay : $blockDecay; + } + + protected function checkForHarden() : bool{ + return false; + } + + protected function liquidCollide(Block $cause, Block $result) : bool{ + $ev = new BlockFormEvent($this, $result); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new FizzSound(2.6 + (lcg_value() - lcg_value()) * 0.8)); + } + return true; + } + + 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 + } +} diff --git a/src/pocketmine/block/LitPumpkin.php b/src/block/LitPumpkin.php similarity index 85% rename from src/pocketmine/block/LitPumpkin.php rename to src/block/LitPumpkin.php index 5f9abf17e3..5f555402a6 100644 --- a/src/pocketmine/block/LitPumpkin.php +++ b/src/block/LitPumpkin.php @@ -23,15 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; -class LitPumpkin extends Pumpkin{ - - protected $id = self::LIT_PUMPKIN; +class LitPumpkin extends CarvedPumpkin{ public function getLightLevel() : int{ return 15; } - - public function getName() : string{ - return "Jack o'Lantern"; - } } diff --git a/src/block/Log.php b/src/block/Log.php new file mode 100644 index 0000000000..eacb817b51 --- /dev/null +++ b/src/block/Log.php @@ -0,0 +1,34 @@ +isStripped() ? 0 : 2; + } +} diff --git a/src/block/Loom.php b/src/block/Loom.php new file mode 100644 index 0000000000..976eea46c5 --- /dev/null +++ b/src/block/Loom.php @@ -0,0 +1,57 @@ +facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x3); + } + + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing); + } + + public function getStateBitmask() : int{ + return 0b11; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $player->setCurrentWindow(new LoomInventory($this->position)); + return true; + } + return false; + } +} diff --git a/src/pocketmine/block/Magma.php b/src/block/Magma.php similarity index 69% rename from src/pocketmine/block/Magma.php rename to src/block/Magma.php index 6fae86ae80..e698a6431a 100644 --- a/src/pocketmine/block/Magma.php +++ b/src/block/Magma.php @@ -24,33 +24,11 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\entity\Entity; +use pocketmine\entity\Living; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageEvent; -use pocketmine\item\TieredTool; -class Magma extends Solid{ - - protected $id = Block::MAGMA; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Magma Block"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } +class Magma extends Opaque{ public function getLightLevel() : int{ return 3; @@ -60,11 +38,12 @@ class Magma extends Solid{ return true; } - public function onEntityCollide(Entity $entity) : void{ - if(!$entity->isSneaking()){ + public function onEntityInside(Entity $entity) : bool{ + if($entity instanceof Living and !$entity->isSneaking()){ $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, 1); $entity->attack($ev); } + return true; } public function burnsForever() : bool{ diff --git a/src/block/Melon.php b/src/block/Melon.php new file mode 100644 index 0000000000..9536e5c853 --- /dev/null +++ b/src/block/Melon.php @@ -0,0 +1,41 @@ +setCount(mt_rand(3, 7)) + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } +} diff --git a/src/pocketmine/block/StillWater.php b/src/block/MelonStem.php similarity index 85% rename from src/pocketmine/block/StillWater.php rename to src/block/MelonStem.php index 3dc4e6a157..0684786c0f 100644 --- a/src/pocketmine/block/StillWater.php +++ b/src/block/MelonStem.php @@ -23,11 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; -class StillWater extends Water{ +class MelonStem extends Stem{ - protected $id = self::STILL_WATER; - - public function getName() : string{ - return "Still Water"; + protected function getPlant() : Block{ + return VanillaBlocks::MELON(); } } diff --git a/src/pocketmine/block/NetherWartBlock.php b/src/block/MonsterSpawner.php similarity index 72% rename from src/pocketmine/block/NetherWartBlock.php rename to src/block/MonsterSpawner.php index 2758f4ff49..5ecc202b84 100644 --- a/src/pocketmine/block/NetherWartBlock.php +++ b/src/block/MonsterSpawner.php @@ -23,19 +23,20 @@ declare(strict_types=1); namespace pocketmine\block; -class NetherWartBlock extends Solid{ +use pocketmine\item\Item; +use function mt_rand; - protected $id = Block::NETHER_WART_BLOCK; +class MonsterSpawner extends Transparent{ - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function getDropsForCompatibleTool(Item $item) : array{ + return []; } - public function getName() : string{ - return "Nether Wart Block"; + protected function getXpDropAmount() : int{ + return mt_rand(15, 43); } - public function getHardness() : float{ - return 1; + public function onScheduledUpdate() : void{ + //TODO } } diff --git a/src/block/MushroomStem.php b/src/block/MushroomStem.php new file mode 100644 index 0000000000..5e61a5b1c8 --- /dev/null +++ b/src/block/MushroomStem.php @@ -0,0 +1,33 @@ +meta = $meta; - } - - public function getName() : string{ - return "Mycelium"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getHardness() : float{ - return 0.6; - } +class Mycelium extends Opaque{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::DIRT) + VanillaBlocks::DIRT()->asItem() ]; } + public function isAffectedBySilkTouch() : bool{ + return true; + } + public function ticksRandomly() : bool{ return true; } public function onRandomTick() : void{ //TODO: light levels - $x = mt_rand($this->x - 1, $this->x + 1); - $y = mt_rand($this->y - 2, $this->y + 2); - $z = mt_rand($this->z - 1, $this->z + 1); - $block = $this->getLevelNonNull()->getBlockAt($x, $y, $z); - if($block->getId() === Block::DIRT){ - if($block->getSide(Vector3::SIDE_UP) instanceof Transparent){ - $ev = new BlockSpreadEvent($block, $this, BlockFactory::get(Block::MYCELIUM)); + $x = mt_rand($this->position->x - 1, $this->position->x + 1); + $y = mt_rand($this->position->y - 2, $this->position->y + 2); + $z = mt_rand($this->position->z - 1, $this->position->z + 1); + $block = $this->position->getWorld()->getBlockAt($x, $y, $z); + if($block->getId() === BlockLegacyIds::DIRT){ + if($block->getSide(Facing::UP) instanceof Transparent){ + $ev = new BlockSpreadEvent($block, $this, VanillaBlocks::MYCELIUM()); $ev->call(); if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($block, $ev->getNewState()); + $this->position->getWorld()->setBlock($block->position, $ev->getNewState()); } } } diff --git a/src/block/NetherPortal.php b/src/block/NetherPortal.php new file mode 100644 index 0000000000..b8c73d0a74 --- /dev/null +++ b/src/block/NetherPortal.php @@ -0,0 +1,86 @@ +axis = $stateMeta === BlockLegacyMetadata::NETHER_PORTAL_AXIS_Z ? Axis::Z : Axis::X; //mojang u dumb + } + + protected function writeStateToMeta() : int{ + return $this->axis === Axis::Z ? BlockLegacyMetadata::NETHER_PORTAL_AXIS_Z : BlockLegacyMetadata::NETHER_PORTAL_AXIS_X; + } + + public function getStateBitmask() : int{ + return 0b11; + } + + public function getAxis() : int{ + return $this->axis; + } + + /** + * @throws \InvalidArgumentException + * @return $this + */ + public function setAxis(int $axis) : self{ + if($axis !== Axis::X and $axis !== Axis::Z){ + throw new \InvalidArgumentException("Invalid axis"); + } + $this->axis = $axis; + return $this; + } + + public function getLightLevel() : int{ + return 11; + } + + public function isSolid() : bool{ + return false; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } + + public function getDrops(Item $item) : array{ + return []; + } + + public function onEntityInside(Entity $entity) : bool{ + //TODO + return true; + } +} diff --git a/src/block/NetherQuartzOre.php b/src/block/NetherQuartzOre.php new file mode 100644 index 0000000000..0eb9365ad6 --- /dev/null +++ b/src/block/NetherQuartzOre.php @@ -0,0 +1,45 @@ +meta = $meta; + protected function writeStateToMeta() : int{ + return $this->state; } - public function getName() : string{ - return "Beetroot Block"; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->state = BlockDataSerializer::readBoundedInt("state", $stateMeta, 0, 2); + } + + public function getStateBitmask() : int{ + return 0b11; } public function getDropsForCompatibleTool(Item $item) : array{ - if($this->meta >= 0x07){ - return [ - ItemFactory::get(Item::BEETROOT), - ItemFactory::get(Item::BEETROOT_SEEDS, 0, mt_rand(0, 3)) - ]; - } - return [ - ItemFactory::get(Item::BEETROOT_SEEDS) + VanillaItems::IRON_INGOT()->setCount(6), + VanillaItems::DIAMOND()->setCount(3) ]; } - - public function getPickedItem() : Item{ - return ItemFactory::get(Item::BEETROOT_SEEDS); - } } diff --git a/src/block/NetherWartPlant.php b/src/block/NetherWartPlant.php new file mode 100644 index 0000000000..5809c8a4ee --- /dev/null +++ b/src/block/NetherWartPlant.php @@ -0,0 +1,98 @@ +age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 3); + } + + public function getStateBitmask() : int{ + return 0b11; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 3){ + throw new \InvalidArgumentException("Age must be in range 0-3"); + } + $this->age = $age; + return $this; + } + + 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::SOUL_SAND){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->getId() !== BlockLegacyIds::SOUL_SAND){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if($this->age < 3 and mt_rand(0, 10) === 0){ //Still growing + $block = clone $this; + $block->age++; + $ev = new BlockGrowEvent($this, $block); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + } + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + $this->asItem()->setCount($this->age === 3 ? mt_rand(2, 4) : 1) + ]; + } +} diff --git a/src/block/Netherrack.php b/src/block/Netherrack.php new file mode 100644 index 0000000000..e9fb517282 --- /dev/null +++ b/src/block/Netherrack.php @@ -0,0 +1,31 @@ +position->getWorld()->getTile($this->position); + if($tile instanceof TileNote){ + $this->pitch = $tile->getPitch(); + }else{ + $this->pitch = self::MIN_PITCH; + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileNote); + $tile->setPitch($this->pitch); + } + + public function getFuelTime() : int{ + return 300; + } + + public function getPitch() : int{ + return $this->pitch; + } + + /** @return $this */ + public function setPitch(int $pitch) : self{ + if($pitch < self::MIN_PITCH or $pitch > self::MAX_PITCH){ + throw new \InvalidArgumentException("Pitch must be in range " . self::MIN_PITCH . " - " . self::MAX_PITCH); + } + $this->pitch = $pitch; + return $this; + } + + //TODO +} diff --git a/src/pocketmine/block/Solid.php b/src/block/Opaque.php similarity index 95% rename from src/pocketmine/block/Solid.php rename to src/block/Opaque.php index 5dff140836..19e16292e6 100644 --- a/src/pocketmine/block/Solid.php +++ b/src/block/Opaque.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\block; -abstract class Solid extends Block{ +class Opaque extends Block{ public function isSolid() : bool{ return true; diff --git a/src/pocketmine/block/RedstoneLamp.php b/src/block/PackedIce.php similarity index 74% rename from src/pocketmine/block/RedstoneLamp.php rename to src/block/PackedIce.php index a90e944f9a..e6d104f39f 100644 --- a/src/pocketmine/block/RedstoneLamp.php +++ b/src/block/PackedIce.php @@ -23,19 +23,19 @@ declare(strict_types=1); namespace pocketmine\block; -class RedstoneLamp extends Solid{ +use pocketmine\item\Item; - protected $id = self::REDSTONE_LAMP; +class PackedIce extends Opaque{ - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function getFrictionFactor() : float{ + return 0.98; } - public function getName() : string{ - return "Redstone Lamp"; + public function getDropsForCompatibleTool(Item $item) : array{ + return []; } - public function getHardness() : float{ - return 0.3; + public function isAffectedBySilkTouch() : bool{ + return true; } } diff --git a/src/pocketmine/block/RedstoneTorch.php b/src/block/Planks.php similarity index 79% rename from src/pocketmine/block/RedstoneTorch.php rename to src/block/Planks.php index 4d0770d1dd..b34046cdb7 100644 --- a/src/pocketmine/block/RedstoneTorch.php +++ b/src/block/Planks.php @@ -23,15 +23,17 @@ declare(strict_types=1); namespace pocketmine\block; -class RedstoneTorch extends Torch{ +class Planks extends Opaque{ - protected $id = self::LIT_REDSTONE_TORCH; - - public function getName() : string{ - return "Redstone Torch"; + public function getFuelTime() : int{ + return 300; } - public function getLightLevel() : int{ - return 7; + public function getFlameEncouragement() : int{ + return 5; + } + + public function getFlammability() : int{ + return 20; } } diff --git a/src/pocketmine/block/MossyCobblestone.php b/src/block/Podzol.php similarity index 81% rename from src/pocketmine/block/MossyCobblestone.php rename to src/block/Podzol.php index c9427c9f9f..0eeba12c80 100644 --- a/src/pocketmine/block/MossyCobblestone.php +++ b/src/block/Podzol.php @@ -23,11 +23,13 @@ declare(strict_types=1); namespace pocketmine\block; -class MossyCobblestone extends Cobblestone{ +use pocketmine\item\Item; - protected $id = self::MOSSY_COBBLESTONE; +class Podzol extends Opaque{ - public function getName() : string{ - return "Moss Stone"; + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaBlocks::DIRT()->asItem() + ]; } } diff --git a/src/pocketmine/block/PackedIce.php b/src/block/Potato.php similarity index 67% rename from src/pocketmine/block/PackedIce.php rename to src/block/Potato.php index 839c114d91..1bfda0d5c6 100644 --- a/src/pocketmine/block/PackedIce.php +++ b/src/block/Potato.php @@ -24,32 +24,22 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; +use pocketmine\item\VanillaItems; +use function mt_rand; -class PackedIce extends Solid{ - - protected $id = self::PACKED_ICE; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Packed Ice"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getFrictionFactor() : float{ - return 0.98; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } +class Potato extends Crops{ public function getDropsForCompatibleTool(Item $item) : array{ - return []; + $result = [ + VanillaItems::POTATO()->setCount($this->age >= 7 ? mt_rand(1, 5) : 1) + ]; + if($this->age >= 7 && mt_rand(0, 49) === 0){ + $result[] = VanillaItems::POISONOUS_POTATO(); + } + return $result; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaItems::POTATO(); } } diff --git a/src/pocketmine/block/PoweredRail.php b/src/block/PoweredRail.php similarity index 84% rename from src/pocketmine/block/PoweredRail.php rename to src/block/PoweredRail.php index ed7b1a00b7..0c754d57f9 100644 --- a/src/pocketmine/block/PoweredRail.php +++ b/src/block/PoweredRail.php @@ -23,10 +23,8 @@ declare(strict_types=1); namespace pocketmine\block; -class PoweredRail extends RedstoneRail{ - protected $id = self::POWERED_RAIL; +use pocketmine\block\utils\RailPoweredByRedstoneTrait; - public function getName() : string{ - return "Powered Rail"; - } +class PoweredRail extends StraightOnlyRail{ + use RailPoweredByRedstoneTrait; } diff --git a/src/pocketmine/block/DetectorRail.php b/src/block/PressurePlate.php similarity index 81% rename from src/pocketmine/block/DetectorRail.php rename to src/block/PressurePlate.php index a4eddc9107..b133e3225e 100644 --- a/src/pocketmine/block/DetectorRail.php +++ b/src/block/PressurePlate.php @@ -23,12 +23,14 @@ declare(strict_types=1); namespace pocketmine\block; -class DetectorRail extends RedstoneRail{ +abstract class PressurePlate extends Transparent{ - protected $id = self::DETECTOR_RAIL; + public function isSolid() : bool{ + return false; + } - public function getName() : string{ - return "Detector Rail"; + protected function recalculateCollisionBoxes() : array{ + return []; } //TODO diff --git a/src/block/PumpkinStem.php b/src/block/PumpkinStem.php new file mode 100644 index 0000000000..fd909a7db0 --- /dev/null +++ b/src/block/PumpkinStem.php @@ -0,0 +1,31 @@ +railShape = $stateMeta; + } + + protected function writeStateToMeta() : int{ + //TODO: railShape won't be plain metadata in future + return $this->railShape; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + protected function setShapeFromConnections(array $connections) : void{ + $railShape = self::searchState($connections, RailConnectionInfo::CONNECTIONS) ?? self::searchState($connections, RailConnectionInfo::CURVE_CONNECTIONS); + if($railShape === null){ + throw new \InvalidArgumentException("No rail shape matches these connections"); + } + $this->railShape = $railShape; + } + + protected function getCurrentShapeConnections() : array{ + return RailConnectionInfo::CURVE_CONNECTIONS[$this->railShape] ?? RailConnectionInfo::CONNECTIONS[$this->railShape]; + } + + protected function getPossibleConnectionDirectionsOneConstraint(int $constraint) : array{ + $possible = parent::getPossibleConnectionDirectionsOneConstraint($constraint); + + if(($constraint & RailConnectionInfo::FLAG_ASCEND) === 0){ + foreach([ + Facing::NORTH, + Facing::SOUTH, + Facing::WEST, + Facing::EAST + ] as $d){ + if($constraint !== $d){ + $possible[$d] = true; + } + } + } + + return $possible; + } + + public function getShape() : int{ return $this->railShape; } + + /** @return $this */ + public function setShape(int $shape) : self{ + if(!isset(RailConnectionInfo::CONNECTIONS[$shape]) && !isset(RailConnectionInfo::CURVE_CONNECTIONS[$shape])){ + throw new \InvalidArgumentException("Invalid shape, must be one of " . implode(", ", [...array_keys(RailConnectionInfo::CONNECTIONS), ...array_keys(RailConnectionInfo::CURVE_CONNECTIONS)])); + } + $this->railShape = $shape; + return $this; + } +} diff --git a/src/pocketmine/block/RedMushroom.php b/src/block/RedMushroom.php similarity index 63% rename from src/pocketmine/block/RedMushroom.php rename to src/block/RedMushroom.php index d9d48b62a6..567c36589e 100644 --- a/src/pocketmine/block/RedMushroom.php +++ b/src/block/RedMushroom.php @@ -24,37 +24,27 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; class RedMushroom extends Flowable{ - protected $id = self::RED_MUSHROOM; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Red Mushroom"; - } - public function ticksRandomly() : bool{ return true; } public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); } } - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); + 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->isTransparent()){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } return false; diff --git a/src/block/RedMushroomBlock.php b/src/block/RedMushroomBlock.php new file mode 100644 index 0000000000..a41b031f5e --- /dev/null +++ b/src/block/RedMushroomBlock.php @@ -0,0 +1,74 @@ +mushroomBlockType = MushroomBlockType::PORES(); + parent::__construct($idInfo, $name, $breakInfo); + } + + protected function writeStateToMeta() : int{ + return MushroomBlockTypeIdMap::getInstance()->toId($this->mushroomBlockType); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $type = MushroomBlockTypeIdMap::getInstance()->fromId($stateMeta); + if($type === null){ + throw new InvalidBlockStateException("No such mushroom variant $stateMeta"); + } + $this->mushroomBlockType = $type; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getMushroomBlockType() : MushroomBlockType{ return $this->mushroomBlockType; } + + /** @return $this */ + public function setMushroomBlockType(MushroomBlockType $mushroomBlockType) : self{ + $this->mushroomBlockType = $mushroomBlockType; + return $this; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaBlocks::RED_MUSHROOM()->asItem()->setCount(mt_rand(0, 2)) + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } +} diff --git a/src/pocketmine/entity/Rideable.php b/src/block/Redstone.php similarity index 92% rename from src/pocketmine/entity/Rideable.php rename to src/block/Redstone.php index 27d62effca..c149650d77 100644 --- a/src/pocketmine/entity/Rideable.php +++ b/src/block/Redstone.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\block; -interface Rideable{ +class Redstone extends Opaque{ } diff --git a/src/block/RedstoneComparator.php b/src/block/RedstoneComparator.php new file mode 100644 index 0000000000..7646769533 --- /dev/null +++ b/src/block/RedstoneComparator.php @@ -0,0 +1,129 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->powered ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + 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); + } + + public function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing) | + ($this->isSubtractMode ? BlockLegacyMetadata::REDSTONE_COMPARATOR_FLAG_SUBTRACT : 0) | + ($this->powered ? BlockLegacyMetadata::REDSTONE_COMPARATOR_FLAG_POWERED : 0); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof Comparator){ + $this->signalStrength = $tile->getSignalStrength(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof Comparator); + $tile->setSignalStrength($this->signalStrength); + } + + public function isSubtractMode() : bool{ + return $this->isSubtractMode; + } + + /** @return $this */ + public function setSubtractMode(bool $isSubtractMode) : self{ + $this->isSubtractMode = $isSubtractMode; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 8)]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$blockReplace->getSide(Facing::DOWN)->isTransparent()){ + if($player !== null){ + $this->facing = Facing::opposite($player->getHorizontalFacing()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->isSubtractMode = !$this->isSubtractMode; + $this->position->getWorld()->setBlock($this->position, $this); + return true; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + //TODO: redstone functionality +} diff --git a/src/pocketmine/block/GlowingObsidian.php b/src/block/RedstoneLamp.php similarity index 56% rename from src/pocketmine/block/GlowingObsidian.php rename to src/block/RedstoneLamp.php index 765874b1a4..b0f7bc9bc3 100644 --- a/src/pocketmine/block/GlowingObsidian.php +++ b/src/block/RedstoneLamp.php @@ -23,37 +23,27 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\TieredTool; +use pocketmine\block\utils\PoweredByRedstoneTrait; -class GlowingObsidian extends Solid{ +class RedstoneLamp extends Opaque{ + use PoweredByRedstoneTrait; - protected $id = self::GLOWING_OBSIDIAN; + protected BlockIdentifierFlattened $idInfoFlattened; - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function __construct(BlockIdentifierFlattened $idInfo, string $name, BlockBreakInfo $breakInfo){ + $this->idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); } - public function getName() : string{ - return "Glowing Obsidian"; + public function getId() : int{ + return $this->powered ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->powered = $id === $this->idInfoFlattened->getSecondId(); } public function getLightLevel() : int{ - return 12; - } - - public function getHardness() : float{ - return 10; - } - - public function getBlastResistance() : float{ - return 50; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_DIAMOND; + return $this->powered ? 15 : 0; } } diff --git a/src/block/RedstoneOre.php b/src/block/RedstoneOre.php new file mode 100644 index 0000000000..f402eacf88 --- /dev/null +++ b/src/block/RedstoneOre.php @@ -0,0 +1,106 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->lit ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->lit = $id === $this->idInfoFlattened->getSecondId(); + } + + public function isLit() : bool{ + return $this->lit; + } + + /** + * @return $this + */ + public function setLit(bool $lit = true) : self{ + $this->lit = $lit; + return $this; + } + + public function getLightLevel() : int{ + return $this->lit ? 9 : 0; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->lit){ + $this->lit = true; + $this->position->getWorld()->setBlock($this->position, $this); //no return here - this shouldn't prevent block placement + } + return false; + } + + public function onNearbyBlockChange() : void{ + if(!$this->lit){ + $this->lit = true; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if($this->lit){ + $this->lit = false; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaItems::REDSTONE_DUST()->setCount(mt_rand(4, 5)) + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + protected function getXpDropAmount() : int{ + return mt_rand(1, 5); + } +} diff --git a/src/block/RedstoneRepeater.php b/src/block/RedstoneRepeater.php new file mode 100644 index 0000000000..b0fb793fdf --- /dev/null +++ b/src/block/RedstoneRepeater.php @@ -0,0 +1,112 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function getId() : int{ + return $this->powered ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + $this->delay = BlockDataSerializer::readBoundedInt("delay", ($stateMeta >> 2) + 1, 1, 4); + $this->powered = $id === $this->idInfoFlattened->getSecondId(); + } + + public function writeStateToMeta() : int{ + return BlockDataSerializer::writeLegacyHorizontalFacing($this->facing) | (($this->delay - 1) << 2); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function getDelay() : int{ return $this->delay; } + + /** @return $this */ + public function setDelay(int $delay) : self{ + if($delay < 1 || $delay > 4){ + throw new \InvalidArgumentException("Delay must be in range 1-4"); + } + $this->delay = $delay; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 7 / 8)]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$blockReplace->getSide(Facing::DOWN)->isTransparent()){ + if($player !== null){ + $this->facing = Facing::opposite($player->getHorizontalFacing()); + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(++$this->delay > 4){ + $this->delay = 1; + } + $this->position->getWorld()->setBlock($this->position, $this); + return true; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + //TODO: redstone functionality +} diff --git a/src/pocketmine/block/GlowingRedstoneOre.php b/src/block/RedstoneTorch.php similarity index 51% rename from src/pocketmine/block/GlowingRedstoneOre.php rename to src/block/RedstoneTorch.php index 94a3ce8adb..43f6fbe642 100644 --- a/src/pocketmine/block/GlowingRedstoneOre.php +++ b/src/block/RedstoneTorch.php @@ -23,36 +23,39 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\Item; -use pocketmine\Player; +class RedstoneTorch extends Torch{ -class GlowingRedstoneOre extends RedstoneOre{ + protected BlockIdentifierFlattened $idInfoFlattened; - protected $id = self::GLOWING_REDSTONE_ORE; + protected bool $lit = true; - protected $itemId = self::REDSTONE_ORE; + public function __construct(BlockIdentifierFlattened $idInfo, string $name, BlockBreakInfo $breakInfo){ + $this->idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } - public function getName() : string{ - return "Glowing Redstone Ore"; + public function getId() : int{ + return $this->lit ? parent::getId() : $this->idInfoFlattened->getSecondId(); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + parent::readStateFromData($id, $stateMeta); + $this->lit = $id !== $this->idInfoFlattened->getSecondId(); + } + + public function isLit() : bool{ + return $this->lit; + } + + /** + * @return $this + */ + public function setLit(bool $lit = true) : self{ + $this->lit = $lit; + return $this; } public function getLightLevel() : int{ - return 9; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - return false; - } - - public function onNearbyBlockChange() : void{ - - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::REDSTONE_ORE, $this->meta), false, false); + return $this->lit ? 7 : 0; } } diff --git a/src/block/RedstoneWire.php b/src/block/RedstoneWire.php new file mode 100644 index 0000000000..6c21404068 --- /dev/null +++ b/src/block/RedstoneWire.php @@ -0,0 +1,48 @@ +signalStrength = BlockDataSerializer::readBoundedInt("signalStrength", $stateMeta, 0, 15); + } + + protected function writeStateToMeta() : int{ + return $this->signalStrength; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + //TODO: check connections to nearby redstone components + } +} diff --git a/src/pocketmine/block/Reserved6.php b/src/block/Reserved6.php similarity index 89% rename from src/pocketmine/block/Reserved6.php rename to src/block/Reserved6.php index 074717cc6d..e2c6922e28 100644 --- a/src/pocketmine/block/Reserved6.php +++ b/src/block/Reserved6.php @@ -23,9 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -class Reserved6 extends Solid{ +class Reserved6 extends Opaque{ - public function getHardness() : float{ - return 0; - } } diff --git a/src/block/Sand.php b/src/block/Sand.php new file mode 100644 index 0000000000..e64b73d22a --- /dev/null +++ b/src/block/Sand.php @@ -0,0 +1,35 @@ +treeType = $treeType; + } + + protected function writeStateToMeta() : int{ + return ($this->ready ? BlockLegacyMetadata::SAPLING_FLAG_READY : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->ready = ($stateMeta & BlockLegacyMetadata::SAPLING_FLAG_READY) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1000; + } + + public function isReady() : bool{ return $this->ready; } + + /** @return $this */ + public function setReady(bool $ready) : self{ + $this->ready = $ready; + return $this; + } + + 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){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($item instanceof Fertilizer && $this->grow($player)){ + $item->pop(); + + return true; + } + + return false; + } + + public function onNearbyBlockChange() : void{ + if($this->getSide(Facing::DOWN)->isTransparent()){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + 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->ready){ + $this->grow(null); + }else{ + $this->ready = true; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + } + + 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 false; + } + + $ev = new StructureGrowEvent($this, $transaction, $player); + $ev->call(); + if(!$ev->isCancelled()){ + return $transaction->apply(); + } + return false; + } + + public function getFuelTime() : int{ + return 100; + } +} diff --git a/src/pocketmine/block/SeaLantern.php b/src/block/SeaLantern.php similarity index 75% rename from src/pocketmine/block/SeaLantern.php rename to src/block/SeaLantern.php index 86af6679d6..ffcf8e7aa4 100644 --- a/src/pocketmine/block/SeaLantern.php +++ b/src/block/SeaLantern.php @@ -24,31 +24,21 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; class SeaLantern extends Transparent{ - protected $id = self::SEA_LANTERN; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Sea Lantern"; - } - - public function getHardness() : float{ - return 0.3; - } - public function getLightLevel() : int{ return 15; } public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::PRISMARINE_CRYSTALS, 0, 3) + VanillaItems::PRISMARINE_CRYSTALS()->setCount(3) ]; } + + public function isAffectedBySilkTouch() : bool{ + return true; + } } diff --git a/src/block/SeaPickle.php b/src/block/SeaPickle.php new file mode 100644 index 0000000000..9110dc22c4 --- /dev/null +++ b/src/block/SeaPickle.php @@ -0,0 +1,106 @@ +count = ($stateMeta & 0x03) + 1; + $this->underwater = ($stateMeta & BlockLegacyMetadata::SEA_PICKLE_FLAG_NOT_UNDERWATER) === 0; + } + + protected function writeStateToMeta() : int{ + return ($this->count - 1) | ($this->underwater ? 0 : BlockLegacyMetadata::SEA_PICKLE_FLAG_NOT_UNDERWATER); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getCount() : int{ return $this->count; } + + /** @return $this */ + public function setCount(int $count) : self{ + if($count < 1 || $count > 4){ + throw new \InvalidArgumentException("Count must be in range 1-4"); + } + $this->count = $count; + return $this; + } + + public function isUnderwater() : bool{ return $this->underwater; } + + /** @return $this */ + public function setUnderwater(bool $underwater) : self{ + $this->underwater = $underwater; + return $this; + } + + public function isSolid() : bool{ + return false; + } + + public function getLightLevel() : int{ + return $this->underwater ? ($this->count + 1) * 3 : 0; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return []; + } + + 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); + } + + 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){ + $this->count = $blockReplace->count + 1; + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + //TODO: bonemeal logic (requires coral) + return parent::onInteract($item, $face, $clickVector, $player); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [$this->asItem()->setCount($this->count)]; + } +} diff --git a/src/block/ShulkerBox.php b/src/block/ShulkerBox.php new file mode 100644 index 0000000000..a81e5009d9 --- /dev/null +++ b/src/block/ShulkerBox.php @@ -0,0 +1,106 @@ +position->getWorld()->getTile($this->position); + if($shulker instanceof TileShulkerBox){ + $shulker->setFacing($this->facing); + } + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $shulker = $this->position->getWorld()->getTile($this->position); + if($shulker instanceof TileShulkerBox){ + $this->facing = $shulker->getFacing(); + } + } + + public function getMaxStackSize() : int{ + return 1; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->facing = $face; + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + private function addDataFromTile(TileShulkerBox $tile, Item $item) : void{ + $shulkerNBT = $tile->getCleanedNBT(); + if($shulkerNBT !== null){ + $item->setNamedTag($shulkerNBT); + } + if($tile->hasName()){ + $item->setCustomName($tile->getName()); + } + } + + public function getDropsForCompatibleTool(Item $item) : array{ + $drop = $this->asItem(); + if(($tile = $this->position->getWorld()->getTile($this->position)) instanceof TileShulkerBox){ + $this->addDataFromTile($tile, $drop); + } + return [$drop]; + } + + public function getPickedItem(bool $addUserData = false) : Item{ + $result = parent::getPickedItem($addUserData); + if($addUserData && ($tile = $this->position->getWorld()->getTile($this->position)) instanceof TileShulkerBox){ + $this->addDataFromTile($tile, $result); + } + return $result; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player instanceof Player){ + + $shulker = $this->position->getWorld()->getTile($this->position); + if($shulker instanceof TileShulkerBox){ + if( + $this->getSide($this->facing)->getId() !== BlockLegacyIds::AIR or + !$shulker->canOpenWith($item->getCustomName()) + ){ + return true; + } + + $player->setCurrentWindow($shulker->getInventory()); + } + } + + return true; + } +} diff --git a/src/block/SimplePillar.php b/src/block/SimplePillar.php new file mode 100644 index 0000000000..50be03be50 --- /dev/null +++ b/src/block/SimplePillar.php @@ -0,0 +1,34 @@ +pressed ? BlockLegacyMetadata::PRESSURE_PLATE_FLAG_POWERED : 0; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->pressed = ($stateMeta & BlockLegacyMetadata::PRESSURE_PLATE_FLAG_POWERED) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1; + } + + public function isPressed() : bool{ return $this->pressed; } + + /** @return $this */ + public function setPressed(bool $pressed) : self{ + $this->pressed = $pressed; + return $this; + } +} diff --git a/src/block/Skull.php b/src/block/Skull.php new file mode 100644 index 0000000000..47476abcea --- /dev/null +++ b/src/block/Skull.php @@ -0,0 +1,152 @@ +skullType = SkullType::SKELETON(); //TODO: this should be a parameter + parent::__construct($idInfo, $name, $breakInfo); + } + + protected function writeStateToMeta() : int{ + return ($this->facing === Facing::UP ? 1 : BlockDataSerializer::writeHorizontalFacing($this->facing)) | + ($this->noDrops ? BlockLegacyMetadata::SKULL_FLAG_NO_DROPS : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = $stateMeta === 1 ? Facing::UP : BlockDataSerializer::readHorizontalFacing($stateMeta); + $this->noDrops = ($stateMeta & BlockLegacyMetadata::SKULL_FLAG_NO_DROPS) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileSkull){ + $this->skullType = $tile->getSkullType(); + $this->rotation = $tile->getRotation(); + } + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + //extra block properties storage hack + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileSkull); + $tile->setRotation($this->rotation); + $tile->setSkullType($this->skullType); + } + + public function getSkullType() : SkullType{ + return $this->skullType; + } + + /** @return $this */ + public function setSkullType(SkullType $skullType) : self{ + $this->skullType = $skullType; + return $this; + } + + public function getFacing() : int{ return $this->facing; } + + /** @return $this */ + public function setFacing(int $facing) : self{ + if($facing === Facing::DOWN){ + throw new \InvalidArgumentException("Skull may not face DOWN"); + } + $this->facing = $facing; + return $this; + } + + public function getRotation() : int{ return $this->rotation; } + + /** @return $this */ + public function setRotation(int $rotation) : self{ + if($rotation < 0 || $rotation > 15){ + throw new \InvalidArgumentException("Rotation must be a value between 0 and 15"); + } + $this->rotation = $rotation; + return $this; + } + + public function isNoDrops() : bool{ return $this->noDrops; } + + /** @return $this */ + public function setNoDrops(bool $noDrops) : self{ + $this->noDrops = $noDrops; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + $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{ + if($face === Facing::DOWN){ + return false; + } + + $this->facing = $face; + if($player !== null and $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); + } + + protected function writeStateToItemMeta() : int{ + return $this->skullType->getMagicNumber(); + } +} diff --git a/src/block/Slab.php b/src/block/Slab.php new file mode 100644 index 0000000000..610c6ee8c0 --- /dev/null +++ b/src/block/Slab.php @@ -0,0 +1,131 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name . " Slab", $breakInfo); + $this->slabType = SlabType::BOTTOM(); + } + + public function getId() : int{ + return $this->slabType->equals(SlabType::DOUBLE()) ? $this->idInfoFlattened->getSecondId() : parent::getId(); + } + + protected function writeStateToMeta() : int{ + if(!$this->slabType->equals(SlabType::DOUBLE())){ + return ($this->slabType->equals(SlabType::TOP()) ? BlockLegacyMetadata::SLAB_FLAG_UPPER : 0); + } + return 0; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + if($id === $this->idInfoFlattened->getSecondId()){ + $this->slabType = SlabType::DOUBLE(); + }else{ + $this->slabType = ($stateMeta & BlockLegacyMetadata::SLAB_FLAG_UPPER) !== 0 ? SlabType::TOP() : SlabType::BOTTOM(); + } + } + + public function getStateBitmask() : int{ + return 0b1000; + } + + public function isTransparent() : bool{ + return !$this->slabType->equals(SlabType::DOUBLE()); + } + + /** + * Returns the type of slab block. + */ + public function getSlabType() : SlabType{ + return $this->slabType; + } + + /** + * @return $this + */ + public function setSlabType(SlabType $slabType) : self{ + $this->slabType = $slabType; + return $this; + } + + public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ + if(parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock)){ + return true; + } + + if($blockReplace instanceof Slab and !$blockReplace->slabType->equals(SlabType::DOUBLE()) and $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); + }else{ + return $clickVector->y >= 0.5 or (!$isClickedBlock and $face === Facing::DOWN); + } + } + + return false; + } + + 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)) + )){ + //Clicked in empty half of existing slab + $this->slabType = SlabType::DOUBLE(); + }else{ + $this->slabType = (($face !== Facing::UP && $clickVector->y > 0.5) || $face === Facing::DOWN) ? SlabType::TOP() : SlabType::BOTTOM(); + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + if($this->slabType->equals(SlabType::DOUBLE())){ + return [AxisAlignedBB::one()]; + } + return [AxisAlignedBB::one()->trim($this->slabType->equals(SlabType::TOP()) ? Facing::DOWN : Facing::UP, 0.5)]; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [$this->asItem()->setCount($this->slabType->equals(SlabType::DOUBLE()) ? 2 : 1)]; + } +} diff --git a/src/pocketmine/block/Sand.php b/src/block/Slime.php similarity index 63% rename from src/pocketmine/block/Sand.php rename to src/block/Slime.php index b77f409d58..2212219322 100644 --- a/src/pocketmine/block/Sand.php +++ b/src/block/Slime.php @@ -23,27 +23,22 @@ declare(strict_types=1); namespace pocketmine\block; -class Sand extends Fallable{ +use pocketmine\entity\Entity; +use pocketmine\entity\Living; - protected $id = self::SAND; +final class Slime extends Transparent{ - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function getFrictionFactor() : float{ + return 0.8; //??? } - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getName() : string{ - if($this->getVariant() === 0x01){ - return "Red Sand"; + public function onEntityLand(Entity $entity) : ?float{ + if($entity instanceof Living && $entity->isSneaking()){ + return null; } - - return "Sand"; + $entity->resetFallDistance(); + return -$entity->getMotion()->y; } + + //TODO: slime blocks should slow entities walking on them to about 0.4x original speed } diff --git a/src/block/Snow.php b/src/block/Snow.php new file mode 100644 index 0000000000..eabe616ce9 --- /dev/null +++ b/src/block/Snow.php @@ -0,0 +1,40 @@ +setCount(4) + ]; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } +} diff --git a/src/block/SnowLayer.php b/src/block/SnowLayer.php new file mode 100644 index 0000000000..c901e9d0c5 --- /dev/null +++ b/src/block/SnowLayer.php @@ -0,0 +1,116 @@ +layers - 1; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->layers = BlockDataSerializer::readBoundedInt("layers", $stateMeta + 1, 1, 8); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getLayers() : int{ return $this->layers; } + + /** @return $this */ + public function setLayers(int $layers) : self{ + if($layers < 1 || $layers > 8){ + throw new \InvalidArgumentException("Layers must be in range 1-8"); + } + $this->layers = $layers; + return $this; + } + + public function canBeReplaced() : bool{ + return $this->layers < 8; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + //TODO: this zero-height BB is intended to stay in lockstep with a MCPE bug + return [AxisAlignedBB::one()->trim(Facing::UP, $this->layers >= 4 ? 0.5 : 1)]; + } + + private function canBeSupportedBy(Block $b) : bool{ + return $b->isSolid() or ($b instanceof SnowLayer and $b->isSameType($this) and $b->layers === 8); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($blockReplace instanceof SnowLayer){ + if($blockReplace->layers >= 8){ + return false; + } + $this->layers = $blockReplace->layers + 1; + } + if($this->canBeSupportedBy($blockReplace->getSide(Facing::DOWN))){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + return false; + } + + public function ticksRandomly() : bool{ + return true; + } + + 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); + } + } + + public function tickFalling() : ?Block{ + return null; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaItems::SNOWBALL()->setCount(max(1, (int) floor($this->layers / 2))) + ]; + } +} diff --git a/src/block/SoulSand.php b/src/block/SoulSand.php new file mode 100644 index 0000000000..46ec341c7f --- /dev/null +++ b/src/block/SoulSand.php @@ -0,0 +1,37 @@ +trim(Facing::UP, 1 / 8)]; + } +} diff --git a/src/pocketmine/block/BrewingStand.php b/src/block/Sponge.php similarity index 56% rename from src/pocketmine/block/BrewingStand.php rename to src/block/Sponge.php index e49cf8cb84..5ccf766abc 100644 --- a/src/pocketmine/block/BrewingStand.php +++ b/src/block/Sponge.php @@ -23,38 +23,31 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\Item; -use pocketmine\item\TieredTool; +class Sponge extends Opaque{ -class BrewingStand extends Transparent{ + protected bool $wet = false; - protected $id = self::BREWING_STAND_BLOCK; - - protected $itemId = Item::BREWING_STAND; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + protected function writeStateToMeta() : int{ + return $this->wet ? BlockLegacyMetadata::SPONGE_FLAG_WET : 0; } - public function getName() : string{ - return "Brewing Stand"; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->wet = ($stateMeta & BlockLegacyMetadata::SPONGE_FLAG_WET) !== 0; } - public function getHardness() : float{ - return 0.5; + protected function writeStateToItemMeta() : int{ + return $this->writeStateToMeta(); } - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; + public function getStateBitmask() : int{ + return 0b1; } - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } + public function isWet() : bool{ return $this->wet; } - public function getVariantBitmask() : int{ - return 0; + /** @return $this */ + public function setWet(bool $wet) : self{ + $this->wet = $wet; + return $this; } - - //TODO } diff --git a/src/pocketmine/block/StainedGlass.php b/src/block/StainedGlass.php similarity index 75% rename from src/pocketmine/block/StainedGlass.php rename to src/block/StainedGlass.php index 5011ad4449..ea4ff2d944 100644 --- a/src/pocketmine/block/StainedGlass.php +++ b/src/block/StainedGlass.php @@ -23,13 +23,8 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\block\utils\ColorBlockMetaHelper; +use pocketmine\block\utils\ColorInMetadataTrait; -class StainedGlass extends Glass{ - - protected $id = self::STAINED_GLASS; - - public function getName() : string{ - return ColorBlockMetaHelper::getColorFromMeta($this->getVariant()) . " Stained Glass"; - } +final class StainedGlass extends Glass{ + use ColorInMetadataTrait; } diff --git a/src/block/StainedGlassPane.php b/src/block/StainedGlassPane.php new file mode 100644 index 0000000000..e6bb7e4df9 --- /dev/null +++ b/src/block/StainedGlassPane.php @@ -0,0 +1,30 @@ +shape = StairShape::STRAIGHT(); + parent::__construct($idInfo, $name, $breakInfo); + } + + protected function writeStateToMeta() : int{ + return BlockDataSerializer::write5MinusHorizontalFacing($this->facing) | ($this->upsideDown ? BlockLegacyMetadata::STAIR_FLAG_UPSIDE_DOWN : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::read5MinusHorizontalFacing($stateMeta); + $this->upsideDown = ($stateMeta & BlockLegacyMetadata::STAIR_FLAG_UPSIDE_DOWN) !== 0; + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + + $clockwise = Facing::rotateY($this->facing, true); + if(($backFacing = $this->getPossibleCornerFacing(false)) !== null){ + $this->shape = $backFacing === $clockwise ? StairShape::OUTER_RIGHT() : StairShape::OUTER_LEFT(); + }elseif(($frontFacing = $this->getPossibleCornerFacing(true)) !== null){ + $this->shape = $frontFacing === $clockwise ? StairShape::INNER_RIGHT() : StairShape::INNER_LEFT(); + }else{ + $this->shape = StairShape::STRAIGHT(); + } + } + + public function isUpsideDown() : bool{ return $this->upsideDown; } + + /** @return $this */ + public function setUpsideDown(bool $upsideDown) : self{ + $this->upsideDown = $upsideDown; + return $this; + } + + public function getShape() : StairShape{ return $this->shape; } + + /** @return $this */ + public function setShape(StairShape $shape) : self{ + $this->shape = $shape; + return $this; + } + + protected function recalculateCollisionBoxes() : array{ + $topStepFace = $this->upsideDown ? Facing::DOWN : Facing::UP; + $bbs = [ + AxisAlignedBB::one()->trim($topStepFace, 0.5) + ]; + + $topStep = AxisAlignedBB::one() + ->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())){ + $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())){ + //add an extra cube + $bbs[] = AxisAlignedBB::one() + ->trim(Facing::opposite($topStepFace), 0.5) + ->trim($this->facing, 0.5) //avoid overlapping with main step + ->trim(Facing::rotateY($this->facing, $this->shape->equals(StairShape::INNER_LEFT())), 0.5); + } + + $bbs[] = $topStep; + + return $bbs; + } + + 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 + Facing::axis($side->facing) !== Facing::axis($this->facing) //perpendicular + ) ? $side->facing : null; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $this->facing = $player->getHorizontalFacing(); + } + $this->upsideDown = (($clickVector->y > 0.5 and $face !== Facing::UP) or $face === Facing::DOWN); + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/pocketmine/block/MelonStem.php b/src/block/Stem.php similarity index 54% rename from src/pocketmine/block/MelonStem.php rename to src/block/Stem.php index 4200595299..5904736f71 100644 --- a/src/pocketmine/block/MelonStem.php +++ b/src/block/Stem.php @@ -25,46 +25,39 @@ namespace pocketmine\block; use pocketmine\event\block\BlockGrowEvent; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -use pocketmine\math\Vector3; +use pocketmine\math\Facing; +use function array_rand; use function mt_rand; -class MelonStem extends Crops{ +abstract class Stem extends Crops{ - protected $id = self::MELON_STEM; - - public function getName() : string{ - return "Melon Stem"; - } - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } + abstract protected function getPlant() : Block; public function onRandomTick() : void{ if(mt_rand(0, 2) === 1){ - if($this->meta < 0x07){ + if($this->age < 7){ $block = clone $this; - ++$block->meta; + ++$block->age; $ev = new BlockGrowEvent($this, $block); $ev->call(); if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($this, $ev->getNewState(), true); + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); } }else{ - for($side = 2; $side <= 5; ++$side){ - $b = $this->getSide($side); - if($b->getId() === self::MELON_BLOCK){ + $grow = $this->getPlant(); + foreach(Facing::HORIZONTAL as $side){ + if($this->getSide($side)->isSameType($grow)){ return; } } - $side = $this->getSide(mt_rand(2, 5)); - $d = $side->getSide(Vector3::SIDE_DOWN); - if($side->getId() === self::AIR and ($d->getId() === self::FARMLAND or $d->getId() === self::GRASS or $d->getId() === self::DIRT)){ - $ev = new BlockGrowEvent($side, BlockFactory::get(Block::MELON_BLOCK)); + + $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)){ + $ev = new BlockGrowEvent($side, $grow); $ev->call(); if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($side, $ev->getNewState(), true); + $this->position->getWorld()->setBlock($side->position, $ev->getNewState()); } } } @@ -73,11 +66,7 @@ class MelonStem extends Crops{ public function getDropsForCompatibleTool(Item $item) : array{ return [ - ItemFactory::get(Item::MELON_SEEDS, 0, mt_rand(0, 2)) + $this->asItem()->setCount(mt_rand(0, 2)) ]; } - - public function getPickedItem() : Item{ - return ItemFactory::get(Item::MELON_SEEDS); - } } diff --git a/src/pocketmine/block/StoneButton.php b/src/block/StoneButton.php similarity index 77% rename from src/pocketmine/block/StoneButton.php rename to src/block/StoneButton.php index a677e0f305..110438b58b 100644 --- a/src/pocketmine/block/StoneButton.php +++ b/src/block/StoneButton.php @@ -25,17 +25,7 @@ namespace pocketmine\block; class StoneButton extends Button{ - protected $id = self::STONE_BUTTON; - - public function getName() : string{ - return "Stone Button"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; + protected function getActivationTime() : int{ + return 20; } } diff --git a/src/block/StonePressurePlate.php b/src/block/StonePressurePlate.php new file mode 100644 index 0000000000..cd8bed86b3 --- /dev/null +++ b/src/block/StonePressurePlate.php @@ -0,0 +1,28 @@ +railShape = $railShape; + } + + protected function writeStateToMeta() : int{ + //TODO: railShape won't be plain metadata in the future + return $this->railShape; + } + + public function getStateBitmask() : int{ + return 0b111; + } + + protected function setShapeFromConnections(array $connections) : void{ + $railShape = self::searchState($connections, RailConnectionInfo::CONNECTIONS); + if($railShape === null){ + throw new \InvalidArgumentException("No rail shape matches these connections"); + } + $this->railShape = $railShape; + } + + protected function getCurrentShapeConnections() : array{ + return RailConnectionInfo::CONNECTIONS[$this->railShape]; + } + + public function getShape() : int{ return $this->railShape; } + + /** @return $this */ + public function setShape(int $shape) : self{ + if(!isset(RailConnectionInfo::CONNECTIONS[$shape])){ + throw new \InvalidArgumentException("Invalid rail shape, must be one of " . implode(", ", array_keys(RailConnectionInfo::CONNECTIONS))); + } + $this->railShape = $shape; + return $this; + + } +} diff --git a/src/block/Sugarcane.php b/src/block/Sugarcane.php new file mode 100644 index 0000000000..90eef3d4be --- /dev/null +++ b/src/block/Sugarcane.php @@ -0,0 +1,134 @@ +age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("age", $stateMeta, 0, 15); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + 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; + } + $b = $this->position->getWorld()->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z); + if($b->getId() === BlockLegacyIds::AIR){ + $ev = new BlockGrowEvent($b, VanillaBlocks::SUGARCANE()); + $ev->call(); + if($ev->isCancelled()){ + 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; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < 0 || $age > 15){ + throw new \InvalidArgumentException("Age must be in range 0-15"); + } + $this->age = $age; + return $this; + } + + 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()){ + $item->pop(); + } + + return true; + } + + return false; + } + + public function onNearbyBlockChange() : void{ + $down = $this->getSide(Facing::DOWN); + if($down->isTransparent() and !$down->isSameType($this)){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if(!$this->getSide(Facing::DOWN)->isSameType($this)){ + if($this->age === 15){ + $this->grow(); + }else{ + ++$this->age; + $this->position->getWorld()->setBlock($this->position, $this); + } + } + } + + 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->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){ + foreach(Facing::HORIZONTAL as $side){ + if($down->getSide($side) instanceof Water){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + } + } + + return false; + } +} diff --git a/src/block/SweetBerryBush.php b/src/block/SweetBerryBush.php new file mode 100644 index 0000000000..4412d0cff8 --- /dev/null +++ b/src/block/SweetBerryBush.php @@ -0,0 +1,161 @@ +age; + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->age = BlockDataSerializer::readBoundedInt("stage", $stateMeta, self::STAGE_SAPLING, self::STAGE_MATURE); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getAge() : int{ return $this->age; } + + /** @return $this */ + public function setAge(int $age) : self{ + if($age < self::STAGE_SAPLING || $age > self::STAGE_MATURE){ + throw new \InvalidArgumentException("Age must be in range 0-3"); + } + $this->age = $age; + return $this; + } + + public function getBerryDropAmount() : int{ + if($this->age === self::STAGE_MATURE){ + return mt_rand(2, 3); + }elseif($this->age >= self::STAGE_BUSH_SOME_BERRIES){ + return mt_rand(1, 2); + } + return 0; + } + + protected function canBeSupportedBy(Block $block) : bool{ + $id = $block->getId(); + return $id === BlockLegacyIds::GRASS || $id === BlockLegacyIds::DIRT || $id === BlockLegacyIds::PODZOL; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->canBeSupportedBy($blockReplace->getSide(Facing::DOWN))){ + return false; + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($this->age < self::STAGE_MATURE && $item instanceof Fertilizer){ + $block = clone $this; + $block->age++; + + $ev = new BlockGrowEvent($this, $block); + $ev->call(); + + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + $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)); + } + + return true; + } + + public function asItem() : Item{ + return VanillaItems::SWEET_BERRIES(); + } + + public function getDropsForCompatibleTool(Item $item) : array{ + if(($dropAmount = $this->getBerryDropAmount()) > 0){ + return [ + $this->asItem()->setCount($dropAmount) + ]; + } + + return []; + } + + public function onNearbyBlockChange() : void{ + if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){ + $this->position->getWorld()->useBreakOn($this->position); + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + if($this->age < self::STAGE_MATURE and mt_rand(0, 2) === 1){ + $block = clone $this; + ++$block->age; + $ev = new BlockGrowEvent($this, $block); + $ev->call(); + if(!$ev->isCancelled()){ + $this->position->getWorld()->setBlock($this->position, $ev->getNewState()); + } + } + } + + public function hasEntityCollision() : bool{ + return true; + } + + public function onEntityInside(Entity $entity) : bool{ + //TODO: in MCPE, this only triggers if moving while inside the bush block - we don't have the system to deal + //with that reliably right now + if($this->age >= self::STAGE_BUSH_NO_BERRIES && $entity instanceof Living){ + $entity->attack(new EntityDamageByBlockEvent($this, $entity, EntityDamageByBlockEvent::CAUSE_CONTACT, 1)); + } + return true; + } +} diff --git a/src/block/TNT.php b/src/block/TNT.php new file mode 100644 index 0000000000..27383e9962 --- /dev/null +++ b/src/block/TNT.php @@ -0,0 +1,137 @@ +unstable = ($stateMeta & BlockLegacyMetadata::TNT_FLAG_UNSTABLE) !== 0; + $this->worksUnderwater = ($stateMeta & BlockLegacyMetadata::TNT_FLAG_UNDERWATER) !== 0; + } + + protected function writeStateToMeta() : int{ + return ($this->unstable ? BlockLegacyMetadata::TNT_FLAG_UNSTABLE : 0) | ($this->worksUnderwater ? BlockLegacyMetadata::TNT_FLAG_UNDERWATER : 0); + } + + protected function writeStateToItemMeta() : int{ + return $this->worksUnderwater ? BlockLegacyMetadata::TNT_FLAG_UNDERWATER : 0; + } + + public function getStateBitmask() : int{ + return 0b11; + } + + public function isUnstable() : bool{ return $this->unstable; } + + /** @return $this */ + public function setUnstable(bool $unstable) : self{ + $this->unstable = $unstable; + return $this; + } + + public function worksUnderwater() : bool{ return $this->worksUnderwater; } + + /** @return $this */ + public function setWorksUnderwater(bool $worksUnderwater) : self{ + $this->worksUnderwater = $worksUnderwater; + return $this; + } + + public function onBreak(Item $item, ?Player $player = null) : bool{ + if($this->unstable){ + $this->ignite(); + return true; + } + return parent::onBreak($item, $player); + } + + 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 Durable){ + $item->applyDamage(1); + } + $this->ignite(); + return true; + } + + return false; + } + + public function hasEntityCollision() : bool{ + return true; + } + + public function onEntityInside(Entity $entity) : bool{ + if($entity instanceof Arrow and $entity->isOnFire()){ + $this->ignite(); + return false; + } + return true; + } + + public function ignite(int $fuse = 80) : void{ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR()); + + $mot = (new Random())->nextSignedFloat() * M_PI * 2; + + $tnt = new PrimedTNT(Location::fromObject($this->position->add(0.5, 0, 0.5), $this->position->getWorld())); + $tnt->setFuse($fuse); + $tnt->setWorksUnderwater($this->worksUnderwater); + $tnt->setMotion(new Vector3(-sin($mot) * 0.02, 0.2, -cos($mot) * 0.02)); + + $tnt->spawnToAll(); + $tnt->broadcastSound(new IgniteSound()); + } + + public function getFlameEncouragement() : int{ + return 15; + } + + public function getFlammability() : int{ + return 100; + } + + public function onIncinerate() : void{ + $this->ignite(); + } +} diff --git a/src/pocketmine/block/TallGrass.php b/src/block/TallGrass.php similarity index 50% rename from src/pocketmine/block/TallGrass.php rename to src/block/TallGrass.php index a072cfcbae..7f5daf731f 100644 --- a/src/pocketmine/block/TallGrass.php +++ b/src/block/TallGrass.php @@ -24,65 +24,38 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; use function mt_rand; class TallGrass extends Flowable{ - protected $id = self::TALL_GRASS; - - public function __construct(int $meta = 1){ - $this->meta = $meta; - } - public function canBeReplaced() : bool{ return true; } - public function getName() : string{ - static $names = [ - 0 => "Dead Shrub", - 1 => "Tall Grass", - 2 => "Fern" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN)->getId(); - if($down === self::GRASS or $down === self::DIRT){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - - return true; + 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){ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } return false; } public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ //Replace with common break method - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), true, true); + if($this->getSide(Facing::DOWN)->isTransparent()){ //Replace with common break method + $this->position->getWorld()->useBreakOn($this->position); } } - public function getToolType() : int{ - return BlockToolType::TYPE_SHEARS; - } - - public function getToolHarvestLevel() : int{ - return 1; - } - - public function getDrops(Item $item) : array{ - if($this->isCompatibleWithTool($item)){ - return parent::getDrops($item); - } - + public function getDropsForIncompatibleTool(Item $item) : array{ if(mt_rand(0, 15) === 0){ return [ - ItemFactory::get(Item::WHEAT_SEEDS) + VanillaItems::WHEAT_SEEDS() ]; } diff --git a/src/block/Thin.php b/src/block/Thin.php new file mode 100644 index 0000000000..3f3dc86e73 --- /dev/null +++ b/src/block/Thin.php @@ -0,0 +1,85 @@ + dummy */ + protected array $connections = []; + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + + foreach(Facing::HORIZONTAL as $facing){ + $side = $this->getSide($facing); + if($side instanceof Thin or $side->isFullCube()){ + $this->connections[$facing] = true; + }else{ + unset($this->connections[$facing]); + } + } + } + + protected function recalculateCollisionBoxes() : array{ + $inset = 7 / 16; + + /** @var AxisAlignedBB[] $bbs */ + $bbs = []; + + if(isset($this->connections[Facing::WEST]) or isset($this->connections[Facing::EAST])){ + $bb = AxisAlignedBB::one()->squash(Axis::Z, $inset); + + if(!isset($this->connections[Facing::WEST])){ + $bb->trim(Facing::WEST, $inset); + }elseif(!isset($this->connections[Facing::EAST])){ + $bb->trim(Facing::EAST, $inset); + } + $bbs[] = $bb; + } + + if(isset($this->connections[Facing::NORTH]) or isset($this->connections[Facing::SOUTH])){ + $bb = AxisAlignedBB::one()->squash(Axis::X, $inset); + + if(!isset($this->connections[Facing::NORTH])){ + $bb->trim(Facing::NORTH, $inset); + }elseif(!isset($this->connections[Facing::SOUTH])){ + $bb->trim(Facing::SOUTH, $inset); + } + $bbs[] = $bb; + } + + if(count($bbs) === 0){ + //centre post AABB (only needed if not connected on any axis - other BBs overlapping will do this if any connections are made) + return [ + AxisAlignedBB::one()->contract($inset, 0, $inset) + ]; + } + + return $bbs; + } +} diff --git a/src/block/Torch.php b/src/block/Torch.php new file mode 100644 index 0000000000..292bfbb180 --- /dev/null +++ b/src/block/Torch.php @@ -0,0 +1,98 @@ +facing === Facing::UP ? 5 : 6 - BlockDataSerializer::writeHorizontalFacing($this->facing); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $facingMeta = $stateMeta & 0x7; + $this->facing = $facingMeta === 5 ? Facing::UP : BlockDataSerializer::readHorizontalFacing(6 - $facingMeta); + } + + public function getStateBitmask() : int{ + return 0b111; + } + + public function getFacing() : int{ return $this->facing; } + + /** @return $this */ + public function setFacing(int $facing) : self{ + if($facing === Facing::DOWN){ + throw new \InvalidArgumentException("Torch may not face DOWN"); + } + $this->facing = $facing; + return $this; + } + + public function getLightLevel() : int{ + return 14; + } + + public function onNearbyBlockChange() : void{ + $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))){ + $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()){ + $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)))){ + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + }else{ + foreach([ + Facing::SOUTH, + Facing::WEST, + Facing::NORTH, + Facing::EAST, + Facing::DOWN + ] as $side){ + $block = $this->getSide($side); + if(!$block->isTransparent()){ + $this->facing = Facing::opposite($side); + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + } + } + return false; + } +} diff --git a/src/pocketmine/block/Transparent.php b/src/block/Transparent.php similarity index 89% rename from src/pocketmine/block/Transparent.php rename to src/block/Transparent.php index 9c3f28ed04..46fe7217af 100644 --- a/src/pocketmine/block/Transparent.php +++ b/src/block/Transparent.php @@ -23,13 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; -abstract class Transparent extends Block{ +class Transparent extends Block{ public function isTransparent() : bool{ return true; } - - public function getLightFilter() : int{ - return 0; - } } diff --git a/src/block/Trapdoor.php b/src/block/Trapdoor.php new file mode 100644 index 0000000000..6f9d7b34c1 --- /dev/null +++ b/src/block/Trapdoor.php @@ -0,0 +1,98 @@ +facing) | ($this->top ? BlockLegacyMetadata::TRAPDOOR_FLAG_UPPER : 0) | ($this->open ? BlockLegacyMetadata::TRAPDOOR_FLAG_OPEN : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + //TODO: in PC the values are reversed (facing - 2) + + $this->facing = BlockDataSerializer::read5MinusHorizontalFacing($stateMeta); + $this->top = ($stateMeta & BlockLegacyMetadata::TRAPDOOR_FLAG_UPPER) !== 0; + $this->open = ($stateMeta & BlockLegacyMetadata::TRAPDOOR_FLAG_OPEN) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isOpen() : bool{ return $this->open; } + + /** @return $this */ + public function setOpen(bool $open) : self{ + $this->open = $open; + return $this; + } + + public function isTop() : bool{ return $this->top; } + + /** @return $this */ + public function setTop(bool $top) : self{ + $this->top = $top; + return $this; + } + + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim($this->open ? $this->facing : ($this->top ? Facing::DOWN : Facing::UP), 13 / 16)]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($player !== null){ + $this->facing = Facing::opposite($player->getHorizontalFacing()); + } + if(($clickVector->y > 0.5 and $face !== Facing::UP) or $face === Facing::DOWN){ + $this->top = true; + } + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->open = !$this->open; + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->addSound($this->position, new DoorSound()); + return true; + } +} diff --git a/src/pocketmine/block/TrappedChest.php b/src/block/TrappedChest.php similarity index 88% rename from src/pocketmine/block/TrappedChest.php rename to src/block/TrappedChest.php index a559c69426..72f8c7a979 100644 --- a/src/pocketmine/block/TrappedChest.php +++ b/src/block/TrappedChest.php @@ -27,9 +27,4 @@ class TrappedChest extends Chest{ //TODO: Redstone! - protected $id = self::TRAPPED_CHEST; - - public function getName() : string{ - return "Trapped Chest"; - } } diff --git a/src/block/Tripwire.php b/src/block/Tripwire.php new file mode 100644 index 0000000000..ef42dbc673 --- /dev/null +++ b/src/block/Tripwire.php @@ -0,0 +1,82 @@ +triggered ? BlockLegacyMetadata::TRIPWIRE_FLAG_TRIGGERED : 0) | + ($this->suspended ? BlockLegacyMetadata::TRIPWIRE_FLAG_SUSPENDED : 0) | + ($this->connected ? BlockLegacyMetadata::TRIPWIRE_FLAG_CONNECTED : 0) | + ($this->disarmed ? BlockLegacyMetadata::TRIPWIRE_FLAG_DISARMED : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->triggered = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_FLAG_TRIGGERED) !== 0; + $this->suspended = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_FLAG_SUSPENDED) !== 0; + $this->connected = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_FLAG_CONNECTED) !== 0; + $this->disarmed = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_FLAG_DISARMED) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isTriggered() : bool{ return $this->triggered; } + + /** @return $this */ + public function setTriggered(bool $triggered) : self{ + $this->triggered = $triggered; + return $this; + } + + public function isSuspended() : bool{ return $this->suspended; } + + /** @return $this */ + public function setSuspended(bool $suspended) : self{ + $this->suspended = $suspended; + return $this; + } + + public function isConnected() : bool{ return $this->connected; } + + /** @return $this */ + public function setConnected(bool $connected) : self{ + $this->connected = $connected; + return $this; + } + + public function isDisarmed() : bool{ return $this->disarmed; } + + /** @return $this */ + public function setDisarmed(bool $disarmed) : self{ + $this->disarmed = $disarmed; + return $this; + } +} diff --git a/src/block/TripwireHook.php b/src/block/TripwireHook.php new file mode 100644 index 0000000000..a6f01fc9c6 --- /dev/null +++ b/src/block/TripwireHook.php @@ -0,0 +1,83 @@ +facing) | + ($this->connected ? BlockLegacyMetadata::TRIPWIRE_HOOK_FLAG_CONNECTED : 0) | + ($this->powered ? BlockLegacyMetadata::TRIPWIRE_HOOK_FLAG_POWERED : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readLegacyHorizontalFacing($stateMeta & 0x03); + $this->connected = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_HOOK_FLAG_CONNECTED) !== 0; + $this->powered = ($stateMeta & BlockLegacyMetadata::TRIPWIRE_HOOK_FLAG_POWERED) !== 0; + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function isConnected() : bool{ return $this->connected; } + + /** @return $this */ + public function setConnected(bool $connected) : self{ + $this->connected = $connected; + return $this; + } + + public function isPowered() : bool{ return $this->powered; } + + /** @return $this */ + public function setPowered(bool $powered) : self{ + $this->powered = $powered; + return $this; + } + + 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){ + //TODO: check face is valid + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + return false; + } + + //TODO +} diff --git a/src/block/UnderwaterTorch.php b/src/block/UnderwaterTorch.php new file mode 100644 index 0000000000..a83a7c44af --- /dev/null +++ b/src/block/UnderwaterTorch.php @@ -0,0 +1,31 @@ +get(395, 0)); + self::register("acacia_door", $factory->get(196, 0)); + self::register("acacia_fence", $factory->get(85, 4)); + self::register("acacia_fence_gate", $factory->get(187, 0)); + self::register("acacia_leaves", $factory->get(161, 0)); + self::register("acacia_log", $factory->get(162, 0)); + self::register("acacia_planks", $factory->get(5, 4)); + self::register("acacia_pressure_plate", $factory->get(405, 0)); + self::register("acacia_sapling", $factory->get(6, 4)); + self::register("acacia_sign", $factory->get(445, 0)); + self::register("acacia_slab", $factory->get(158, 4)); + self::register("acacia_stairs", $factory->get(163, 0)); + self::register("acacia_trapdoor", $factory->get(400, 0)); + self::register("acacia_wall_sign", $factory->get(446, 2)); + self::register("acacia_wood", $factory->get(467, 4)); + self::register("activator_rail", $factory->get(126, 0)); + self::register("air", $factory->get(0, 0)); + self::register("all_sided_mushroom_stem", $factory->get(99, 15)); + self::register("allium", $factory->get(38, 2)); + self::register("andesite", $factory->get(1, 5)); + self::register("andesite_slab", $factory->get(417, 3)); + self::register("andesite_stairs", $factory->get(426, 0)); + self::register("andesite_wall", $factory->get(139, 4)); + self::register("anvil", $factory->get(145, 0)); + self::register("azure_bluet", $factory->get(38, 3)); + self::register("bamboo", $factory->get(418, 0)); + self::register("bamboo_sapling", $factory->get(419, 0)); + self::register("banner", $factory->get(176, 0)); + self::register("barrel", $factory->get(458, 0)); + self::register("barrier", $factory->get(416, 0)); + self::register("beacon", $factory->get(138, 0)); + self::register("bed", $factory->get(26, 0)); + self::register("bedrock", $factory->get(7, 0)); + self::register("beetroots", $factory->get(244, 0)); + self::register("bell", $factory->get(461, 0)); + self::register("birch_button", $factory->get(396, 0)); + self::register("birch_door", $factory->get(194, 0)); + self::register("birch_fence", $factory->get(85, 2)); + self::register("birch_fence_gate", $factory->get(184, 0)); + self::register("birch_leaves", $factory->get(18, 2)); + self::register("birch_log", $factory->get(17, 2)); + self::register("birch_planks", $factory->get(5, 2)); + self::register("birch_pressure_plate", $factory->get(406, 0)); + self::register("birch_sapling", $factory->get(6, 2)); + self::register("birch_sign", $factory->get(441, 0)); + self::register("birch_slab", $factory->get(158, 2)); + self::register("birch_stairs", $factory->get(135, 0)); + self::register("birch_trapdoor", $factory->get(401, 0)); + self::register("birch_wall_sign", $factory->get(442, 2)); + self::register("birch_wood", $factory->get(467, 2)); + self::register("black_glazed_terracotta", $factory->get(235, 2)); + self::register("blast_furnace", $factory->get(451, 2)); + self::register("blue_glazed_terracotta", $factory->get(231, 2)); + self::register("blue_ice", $factory->get(266, 0)); + self::register("blue_orchid", $factory->get(38, 1)); + self::register("blue_torch", $factory->get(204, 5)); + self::register("bone_block", $factory->get(216, 0)); + self::register("bookshelf", $factory->get(47, 0)); + self::register("brewing_stand", $factory->get(117, 0)); + self::register("brick_slab", $factory->get(44, 4)); + self::register("brick_stairs", $factory->get(108, 0)); + self::register("brick_wall", $factory->get(139, 6)); + self::register("bricks", $factory->get(45, 0)); + self::register("brown_glazed_terracotta", $factory->get(232, 2)); + self::register("brown_mushroom", $factory->get(39, 0)); + self::register("brown_mushroom_block", $factory->get(99, 0)); + self::register("cactus", $factory->get(81, 0)); + self::register("cake", $factory->get(92, 0)); + self::register("carpet", $factory->get(171, 0)); + self::register("carrots", $factory->get(141, 0)); + self::register("carved_pumpkin", $factory->get(410, 0)); + self::register("chemical_heat", $factory->get(192, 0)); + self::register("chest", $factory->get(54, 2)); + self::register("chiseled_quartz", $factory->get(155, 1)); + self::register("chiseled_red_sandstone", $factory->get(179, 1)); + self::register("chiseled_sandstone", $factory->get(24, 1)); + self::register("chiseled_stone_bricks", $factory->get(98, 3)); + self::register("clay", $factory->get(82, 0)); + self::register("coal", $factory->get(173, 0)); + self::register("coal_ore", $factory->get(16, 0)); + self::register("cobblestone", $factory->get(4, 0)); + self::register("cobblestone_slab", $factory->get(44, 3)); + self::register("cobblestone_stairs", $factory->get(67, 0)); + self::register("cobblestone_wall", $factory->get(139, 0)); + self::register("cobweb", $factory->get(30, 0)); + self::register("cocoa_pod", $factory->get(127, 0)); + self::register("compound_creator", $factory->get(238, 0)); + self::register("concrete", $factory->get(236, 0)); + self::register("concrete_powder", $factory->get(237, 0)); + self::register("coral", $factory->get(386, 0)); + self::register("coral_block", $factory->get(387, 0)); + self::register("coral_fan", $factory->get(388, 0)); + self::register("cornflower", $factory->get(38, 9)); + self::register("cracked_stone_bricks", $factory->get(98, 2)); + self::register("crafting_table", $factory->get(58, 0)); + self::register("cut_red_sandstone", $factory->get(179, 2)); + self::register("cut_red_sandstone_slab", $factory->get(421, 4)); + self::register("cut_sandstone", $factory->get(24, 2)); + self::register("cut_sandstone_slab", $factory->get(421, 3)); + self::register("cyan_glazed_terracotta", $factory->get(229, 2)); + self::register("dandelion", $factory->get(37, 0)); + self::register("dark_oak_button", $factory->get(397, 0)); + self::register("dark_oak_door", $factory->get(197, 0)); + self::register("dark_oak_fence", $factory->get(85, 5)); + self::register("dark_oak_fence_gate", $factory->get(186, 0)); + self::register("dark_oak_leaves", $factory->get(161, 1)); + self::register("dark_oak_log", $factory->get(162, 1)); + self::register("dark_oak_planks", $factory->get(5, 5)); + self::register("dark_oak_pressure_plate", $factory->get(407, 0)); + self::register("dark_oak_sapling", $factory->get(6, 5)); + self::register("dark_oak_sign", $factory->get(447, 0)); + self::register("dark_oak_slab", $factory->get(158, 5)); + self::register("dark_oak_stairs", $factory->get(164, 0)); + self::register("dark_oak_trapdoor", $factory->get(402, 0)); + self::register("dark_oak_wall_sign", $factory->get(448, 2)); + self::register("dark_oak_wood", $factory->get(467, 5)); + self::register("dark_prismarine", $factory->get(168, 1)); + self::register("dark_prismarine_slab", $factory->get(182, 3)); + self::register("dark_prismarine_stairs", $factory->get(258, 0)); + self::register("daylight_sensor", $factory->get(151, 0)); + self::register("dead_bush", $factory->get(32, 0)); + self::register("detector_rail", $factory->get(28, 0)); + self::register("diamond", $factory->get(57, 0)); + self::register("diamond_ore", $factory->get(56, 0)); + self::register("diorite", $factory->get(1, 3)); + self::register("diorite_slab", $factory->get(417, 4)); + self::register("diorite_stairs", $factory->get(425, 0)); + self::register("diorite_wall", $factory->get(139, 3)); + self::register("dirt", $factory->get(3, 0)); + self::register("double_tallgrass", $factory->get(175, 2)); + self::register("dragon_egg", $factory->get(122, 0)); + self::register("dried_kelp", $factory->get(394, 0)); + self::register("dyed_shulker_box", $factory->get(218, 0)); + self::register("element_actinium", $factory->get(355, 0)); + self::register("element_aluminum", $factory->get(279, 0)); + self::register("element_americium", $factory->get(361, 0)); + self::register("element_antimony", $factory->get(317, 0)); + self::register("element_argon", $factory->get(284, 0)); + self::register("element_arsenic", $factory->get(299, 0)); + self::register("element_astatine", $factory->get(351, 0)); + self::register("element_barium", $factory->get(322, 0)); + self::register("element_berkelium", $factory->get(363, 0)); + self::register("element_beryllium", $factory->get(270, 0)); + self::register("element_bismuth", $factory->get(349, 0)); + self::register("element_bohrium", $factory->get(373, 0)); + self::register("element_boron", $factory->get(271, 0)); + self::register("element_bromine", $factory->get(301, 0)); + self::register("element_cadmium", $factory->get(314, 0)); + self::register("element_calcium", $factory->get(286, 0)); + self::register("element_californium", $factory->get(364, 0)); + self::register("element_carbon", $factory->get(272, 0)); + self::register("element_cerium", $factory->get(324, 0)); + self::register("element_cesium", $factory->get(321, 0)); + self::register("element_chlorine", $factory->get(283, 0)); + self::register("element_chromium", $factory->get(290, 0)); + self::register("element_cobalt", $factory->get(293, 0)); + self::register("element_constructor", $factory->get(238, 8)); + self::register("element_copernicium", $factory->get(378, 0)); + self::register("element_copper", $factory->get(295, 0)); + self::register("element_curium", $factory->get(362, 0)); + self::register("element_darmstadtium", $factory->get(376, 0)); + self::register("element_dubnium", $factory->get(371, 0)); + self::register("element_dysprosium", $factory->get(332, 0)); + self::register("element_einsteinium", $factory->get(365, 0)); + self::register("element_erbium", $factory->get(334, 0)); + self::register("element_europium", $factory->get(329, 0)); + self::register("element_fermium", $factory->get(366, 0)); + self::register("element_flerovium", $factory->get(380, 0)); + self::register("element_fluorine", $factory->get(275, 0)); + self::register("element_francium", $factory->get(353, 0)); + self::register("element_gadolinium", $factory->get(330, 0)); + self::register("element_gallium", $factory->get(297, 0)); + self::register("element_germanium", $factory->get(298, 0)); + self::register("element_gold", $factory->get(345, 0)); + self::register("element_hafnium", $factory->get(338, 0)); + self::register("element_hassium", $factory->get(374, 0)); + self::register("element_helium", $factory->get(268, 0)); + self::register("element_holmium", $factory->get(333, 0)); + self::register("element_hydrogen", $factory->get(267, 0)); + self::register("element_indium", $factory->get(315, 0)); + self::register("element_iodine", $factory->get(319, 0)); + self::register("element_iridium", $factory->get(343, 0)); + self::register("element_iron", $factory->get(292, 0)); + self::register("element_krypton", $factory->get(302, 0)); + self::register("element_lanthanum", $factory->get(323, 0)); + self::register("element_lawrencium", $factory->get(369, 0)); + self::register("element_lead", $factory->get(348, 0)); + self::register("element_lithium", $factory->get(269, 0)); + self::register("element_livermorium", $factory->get(382, 0)); + self::register("element_lutetium", $factory->get(337, 0)); + self::register("element_magnesium", $factory->get(278, 0)); + self::register("element_manganese", $factory->get(291, 0)); + self::register("element_meitnerium", $factory->get(375, 0)); + self::register("element_mendelevium", $factory->get(367, 0)); + self::register("element_mercury", $factory->get(346, 0)); + self::register("element_molybdenum", $factory->get(308, 0)); + self::register("element_moscovium", $factory->get(381, 0)); + self::register("element_neodymium", $factory->get(326, 0)); + self::register("element_neon", $factory->get(276, 0)); + self::register("element_neptunium", $factory->get(359, 0)); + self::register("element_nickel", $factory->get(294, 0)); + self::register("element_nihonium", $factory->get(379, 0)); + self::register("element_niobium", $factory->get(307, 0)); + self::register("element_nitrogen", $factory->get(273, 0)); + self::register("element_nobelium", $factory->get(368, 0)); + self::register("element_oganesson", $factory->get(384, 0)); + self::register("element_osmium", $factory->get(342, 0)); + self::register("element_oxygen", $factory->get(274, 0)); + self::register("element_palladium", $factory->get(312, 0)); + self::register("element_phosphorus", $factory->get(281, 0)); + self::register("element_platinum", $factory->get(344, 0)); + self::register("element_plutonium", $factory->get(360, 0)); + self::register("element_polonium", $factory->get(350, 0)); + self::register("element_potassium", $factory->get(285, 0)); + self::register("element_praseodymium", $factory->get(325, 0)); + self::register("element_promethium", $factory->get(327, 0)); + self::register("element_protactinium", $factory->get(357, 0)); + self::register("element_radium", $factory->get(354, 0)); + self::register("element_radon", $factory->get(352, 0)); + self::register("element_rhenium", $factory->get(341, 0)); + self::register("element_rhodium", $factory->get(311, 0)); + self::register("element_roentgenium", $factory->get(377, 0)); + self::register("element_rubidium", $factory->get(303, 0)); + self::register("element_ruthenium", $factory->get(310, 0)); + self::register("element_rutherfordium", $factory->get(370, 0)); + self::register("element_samarium", $factory->get(328, 0)); + self::register("element_scandium", $factory->get(287, 0)); + self::register("element_seaborgium", $factory->get(372, 0)); + self::register("element_selenium", $factory->get(300, 0)); + self::register("element_silicon", $factory->get(280, 0)); + self::register("element_silver", $factory->get(313, 0)); + self::register("element_sodium", $factory->get(277, 0)); + self::register("element_strontium", $factory->get(304, 0)); + self::register("element_sulfur", $factory->get(282, 0)); + self::register("element_tantalum", $factory->get(339, 0)); + self::register("element_technetium", $factory->get(309, 0)); + self::register("element_tellurium", $factory->get(318, 0)); + self::register("element_tennessine", $factory->get(383, 0)); + self::register("element_terbium", $factory->get(331, 0)); + self::register("element_thallium", $factory->get(347, 0)); + self::register("element_thorium", $factory->get(356, 0)); + self::register("element_thulium", $factory->get(335, 0)); + self::register("element_tin", $factory->get(316, 0)); + self::register("element_titanium", $factory->get(288, 0)); + self::register("element_tungsten", $factory->get(340, 0)); + self::register("element_uranium", $factory->get(358, 0)); + self::register("element_vanadium", $factory->get(289, 0)); + self::register("element_xenon", $factory->get(320, 0)); + self::register("element_ytterbium", $factory->get(336, 0)); + self::register("element_yttrium", $factory->get(305, 0)); + self::register("element_zero", $factory->get(36, 0)); + self::register("element_zinc", $factory->get(296, 0)); + self::register("element_zirconium", $factory->get(306, 0)); + self::register("emerald", $factory->get(133, 0)); + self::register("emerald_ore", $factory->get(129, 0)); + self::register("enchanting_table", $factory->get(116, 0)); + self::register("end_portal_frame", $factory->get(120, 0)); + self::register("end_rod", $factory->get(208, 0)); + self::register("end_stone", $factory->get(121, 0)); + self::register("end_stone_brick_slab", $factory->get(417, 0)); + self::register("end_stone_brick_stairs", $factory->get(433, 0)); + self::register("end_stone_brick_wall", $factory->get(139, 10)); + self::register("end_stone_bricks", $factory->get(206, 0)); + self::register("ender_chest", $factory->get(130, 2)); + self::register("fake_wooden_slab", $factory->get(44, 2)); + 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)); + self::register("glass", $factory->get(20, 0)); + self::register("glass_pane", $factory->get(102, 0)); + self::register("glowing_obsidian", $factory->get(246, 0)); + self::register("glowstone", $factory->get(89, 0)); + self::register("gold", $factory->get(41, 0)); + self::register("gold_ore", $factory->get(14, 0)); + self::register("granite", $factory->get(1, 1)); + self::register("granite_slab", $factory->get(417, 6)); + self::register("granite_stairs", $factory->get(424, 0)); + self::register("granite_wall", $factory->get(139, 2)); + self::register("grass", $factory->get(2, 0)); + self::register("grass_path", $factory->get(198, 0)); + self::register("gravel", $factory->get(13, 0)); + self::register("gray_glazed_terracotta", $factory->get(227, 2)); + self::register("green_glazed_terracotta", $factory->get(233, 2)); + self::register("green_torch", $factory->get(202, 13)); + self::register("hardened_clay", $factory->get(172, 0)); + self::register("hardened_glass", $factory->get(253, 0)); + self::register("hardened_glass_pane", $factory->get(190, 0)); + self::register("hay_bale", $factory->get(170, 0)); + self::register("hopper", $factory->get(154, 0)); + self::register("ice", $factory->get(79, 0)); + self::register("infested_chiseled_stone_brick", $factory->get(97, 5)); + self::register("infested_cobblestone", $factory->get(97, 1)); + self::register("infested_cracked_stone_brick", $factory->get(97, 4)); + self::register("infested_mossy_stone_brick", $factory->get(97, 3)); + self::register("infested_stone", $factory->get(97, 0)); + self::register("infested_stone_brick", $factory->get(97, 2)); + self::register("info_update", $factory->get(248, 0)); + self::register("info_update2", $factory->get(249, 0)); + self::register("invisible_bedrock", $factory->get(95, 0)); + self::register("iron", $factory->get(42, 0)); + self::register("iron_bars", $factory->get(101, 0)); + self::register("iron_door", $factory->get(71, 0)); + self::register("iron_ore", $factory->get(15, 0)); + self::register("iron_trapdoor", $factory->get(167, 0)); + self::register("item_frame", $factory->get(199, 0)); + self::register("jukebox", $factory->get(84, 0)); + self::register("jungle_button", $factory->get(398, 0)); + self::register("jungle_door", $factory->get(195, 0)); + self::register("jungle_fence", $factory->get(85, 3)); + self::register("jungle_fence_gate", $factory->get(185, 0)); + self::register("jungle_leaves", $factory->get(18, 3)); + self::register("jungle_log", $factory->get(17, 3)); + self::register("jungle_planks", $factory->get(5, 3)); + self::register("jungle_pressure_plate", $factory->get(408, 0)); + self::register("jungle_sapling", $factory->get(6, 3)); + self::register("jungle_sign", $factory->get(443, 0)); + self::register("jungle_slab", $factory->get(158, 3)); + self::register("jungle_stairs", $factory->get(136, 0)); + self::register("jungle_trapdoor", $factory->get(403, 0)); + self::register("jungle_wall_sign", $factory->get(444, 2)); + self::register("jungle_wood", $factory->get(467, 3)); + self::register("lab_table", $factory->get(238, 12)); + self::register("ladder", $factory->get(65, 2)); + self::register("lantern", $factory->get(463, 0)); + self::register("lapis_lazuli", $factory->get(22, 0)); + 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("legacy_stonecutter", $factory->get(245, 0)); + self::register("lever", $factory->get(69, 0)); + self::register("light_blue_glazed_terracotta", $factory->get(223, 2)); + self::register("light_gray_glazed_terracotta", $factory->get(228, 2)); + self::register("lilac", $factory->get(175, 1)); + self::register("lily_of_the_valley", $factory->get(38, 10)); + self::register("lily_pad", $factory->get(111, 0)); + self::register("lime_glazed_terracotta", $factory->get(225, 2)); + self::register("lit_pumpkin", $factory->get(91, 0)); + self::register("loom", $factory->get(459, 0)); + self::register("magenta_glazed_terracotta", $factory->get(222, 2)); + self::register("magma", $factory->get(213, 0)); + self::register("material_reducer", $factory->get(238, 4)); + self::register("melon", $factory->get(103, 0)); + self::register("melon_stem", $factory->get(105, 0)); + self::register("mob_head", $factory->get(144, 2)); + self::register("monster_spawner", $factory->get(52, 0)); + self::register("mossy_cobblestone", $factory->get(48, 0)); + self::register("mossy_cobblestone_slab", $factory->get(182, 5)); + self::register("mossy_cobblestone_stairs", $factory->get(434, 0)); + self::register("mossy_cobblestone_wall", $factory->get(139, 1)); + self::register("mossy_stone_brick_slab", $factory->get(421, 0)); + self::register("mossy_stone_brick_stairs", $factory->get(430, 0)); + self::register("mossy_stone_brick_wall", $factory->get(139, 8)); + self::register("mossy_stone_bricks", $factory->get(98, 1)); + self::register("mushroom_stem", $factory->get(99, 10)); + self::register("mycelium", $factory->get(110, 0)); + self::register("nether_brick_fence", $factory->get(113, 0)); + self::register("nether_brick_slab", $factory->get(44, 7)); + self::register("nether_brick_stairs", $factory->get(114, 0)); + self::register("nether_brick_wall", $factory->get(139, 9)); + self::register("nether_bricks", $factory->get(112, 0)); + self::register("nether_portal", $factory->get(90, 1)); + self::register("nether_quartz_ore", $factory->get(153, 0)); + self::register("nether_reactor_core", $factory->get(247, 0)); + self::register("nether_wart", $factory->get(115, 0)); + self::register("nether_wart_block", $factory->get(214, 0)); + self::register("netherrack", $factory->get(87, 0)); + self::register("note_block", $factory->get(25, 0)); + self::register("oak_button", $factory->get(143, 0)); + self::register("oak_door", $factory->get(64, 0)); + self::register("oak_fence", $factory->get(85, 0)); + self::register("oak_fence_gate", $factory->get(107, 0)); + self::register("oak_leaves", $factory->get(18, 0)); + self::register("oak_log", $factory->get(17, 0)); + self::register("oak_planks", $factory->get(5, 0)); + self::register("oak_pressure_plate", $factory->get(72, 0)); + self::register("oak_sapling", $factory->get(6, 0)); + self::register("oak_sign", $factory->get(63, 0)); + self::register("oak_slab", $factory->get(158, 0)); + self::register("oak_stairs", $factory->get(53, 0)); + self::register("oak_trapdoor", $factory->get(96, 0)); + self::register("oak_wall_sign", $factory->get(68, 2)); + self::register("oak_wood", $factory->get(467, 0)); + self::register("obsidian", $factory->get(49, 0)); + self::register("orange_glazed_terracotta", $factory->get(221, 2)); + self::register("orange_tulip", $factory->get(38, 5)); + self::register("oxeye_daisy", $factory->get(38, 8)); + self::register("packed_ice", $factory->get(174, 0)); + self::register("peony", $factory->get(175, 5)); + self::register("pink_glazed_terracotta", $factory->get(226, 2)); + self::register("pink_tulip", $factory->get(38, 7)); + self::register("podzol", $factory->get(243, 0)); + self::register("polished_andesite", $factory->get(1, 6)); + self::register("polished_andesite_slab", $factory->get(417, 2)); + self::register("polished_andesite_stairs", $factory->get(429, 0)); + self::register("polished_diorite", $factory->get(1, 4)); + self::register("polished_diorite_slab", $factory->get(417, 5)); + self::register("polished_diorite_stairs", $factory->get(428, 0)); + self::register("polished_granite", $factory->get(1, 2)); + self::register("polished_granite_slab", $factory->get(417, 7)); + self::register("polished_granite_stairs", $factory->get(427, 0)); + self::register("poppy", $factory->get(38, 0)); + self::register("potatoes", $factory->get(142, 0)); + self::register("powered_rail", $factory->get(27, 0)); + self::register("prismarine", $factory->get(168, 0)); + self::register("prismarine_bricks", $factory->get(168, 2)); + self::register("prismarine_bricks_slab", $factory->get(182, 4)); + self::register("prismarine_bricks_stairs", $factory->get(259, 0)); + self::register("prismarine_slab", $factory->get(182, 2)); + self::register("prismarine_stairs", $factory->get(257, 0)); + self::register("prismarine_wall", $factory->get(139, 11)); + self::register("pumpkin", $factory->get(86, 0)); + self::register("pumpkin_stem", $factory->get(104, 0)); + self::register("purple_glazed_terracotta", $factory->get(219, 2)); + self::register("purple_torch", $factory->get(204, 13)); + self::register("purpur", $factory->get(201, 0)); + self::register("purpur_pillar", $factory->get(201, 2)); + self::register("purpur_slab", $factory->get(182, 1)); + self::register("purpur_stairs", $factory->get(203, 0)); + self::register("quartz", $factory->get(155, 0)); + self::register("quartz_pillar", $factory->get(155, 2)); + self::register("quartz_slab", $factory->get(44, 6)); + self::register("quartz_stairs", $factory->get(156, 0)); + self::register("rail", $factory->get(66, 0)); + self::register("red_glazed_terracotta", $factory->get(234, 2)); + self::register("red_mushroom", $factory->get(40, 0)); + self::register("red_mushroom_block", $factory->get(100, 0)); + self::register("red_nether_brick_slab", $factory->get(182, 7)); + self::register("red_nether_brick_stairs", $factory->get(439, 0)); + self::register("red_nether_brick_wall", $factory->get(139, 13)); + self::register("red_nether_bricks", $factory->get(215, 0)); + self::register("red_sand", $factory->get(12, 1)); + self::register("red_sandstone", $factory->get(179, 0)); + self::register("red_sandstone_slab", $factory->get(182, 0)); + self::register("red_sandstone_stairs", $factory->get(180, 0)); + self::register("red_sandstone_wall", $factory->get(139, 12)); + self::register("red_torch", $factory->get(202, 5)); + self::register("red_tulip", $factory->get(38, 4)); + self::register("redstone", $factory->get(152, 0)); + self::register("redstone_comparator", $factory->get(149, 0)); + self::register("redstone_lamp", $factory->get(123, 0)); + self::register("redstone_ore", $factory->get(73, 0)); + self::register("redstone_repeater", $factory->get(93, 0)); + self::register("redstone_torch", $factory->get(76, 5)); + self::register("redstone_wire", $factory->get(55, 0)); + self::register("reserved6", $factory->get(255, 0)); + self::register("rose_bush", $factory->get(175, 4)); + self::register("sand", $factory->get(12, 0)); + self::register("sandstone", $factory->get(24, 0)); + self::register("sandstone_slab", $factory->get(44, 1)); + self::register("sandstone_stairs", $factory->get(128, 0)); + self::register("sandstone_wall", $factory->get(139, 5)); + self::register("sea_lantern", $factory->get(169, 0)); + self::register("sea_pickle", $factory->get(411, 0)); + self::register("shulker_box", $factory->get(205, 0)); + self::register("slime", $factory->get(165, 0)); + self::register("smoker", $factory->get(453, 2)); + self::register("smooth_quartz", $factory->get(155, 3)); + self::register("smooth_quartz_slab", $factory->get(421, 1)); + self::register("smooth_quartz_stairs", $factory->get(440, 0)); + self::register("smooth_red_sandstone", $factory->get(179, 3)); + self::register("smooth_red_sandstone_slab", $factory->get(417, 1)); + self::register("smooth_red_sandstone_stairs", $factory->get(431, 0)); + self::register("smooth_sandstone", $factory->get(24, 3)); + self::register("smooth_sandstone_slab", $factory->get(182, 6)); + self::register("smooth_sandstone_stairs", $factory->get(432, 0)); + self::register("smooth_stone", $factory->get(438, 0)); + self::register("smooth_stone_slab", $factory->get(44, 0)); + self::register("snow", $factory->get(80, 0)); + self::register("snow_layer", $factory->get(78, 0)); + self::register("soul_sand", $factory->get(88, 0)); + self::register("sponge", $factory->get(19, 0)); + self::register("spruce_button", $factory->get(399, 0)); + self::register("spruce_door", $factory->get(193, 0)); + self::register("spruce_fence", $factory->get(85, 1)); + self::register("spruce_fence_gate", $factory->get(183, 0)); + self::register("spruce_leaves", $factory->get(18, 1)); + self::register("spruce_log", $factory->get(17, 1)); + self::register("spruce_planks", $factory->get(5, 1)); + self::register("spruce_pressure_plate", $factory->get(409, 0)); + self::register("spruce_sapling", $factory->get(6, 1)); + self::register("spruce_sign", $factory->get(436, 0)); + self::register("spruce_slab", $factory->get(158, 1)); + self::register("spruce_stairs", $factory->get(134, 0)); + self::register("spruce_trapdoor", $factory->get(404, 0)); + self::register("spruce_wall_sign", $factory->get(437, 2)); + self::register("spruce_wood", $factory->get(467, 1)); + self::register("stained_clay", $factory->get(159, 0)); + self::register("stained_glass", $factory->get(241, 0)); + self::register("stained_glass_pane", $factory->get(160, 0)); + self::register("stained_hardened_glass", $factory->get(254, 0)); + self::register("stained_hardened_glass_pane", $factory->get(191, 0)); + self::register("stone", $factory->get(1, 0)); + self::register("stone_brick_slab", $factory->get(44, 5)); + self::register("stone_brick_stairs", $factory->get(109, 0)); + self::register("stone_brick_wall", $factory->get(139, 7)); + self::register("stone_bricks", $factory->get(98, 0)); + self::register("stone_button", $factory->get(77, 0)); + self::register("stone_pressure_plate", $factory->get(70, 0)); + self::register("stone_slab", $factory->get(421, 2)); + self::register("stone_stairs", $factory->get(435, 0)); + self::register("stripped_acacia_log", $factory->get(263, 0)); + self::register("stripped_acacia_wood", $factory->get(467, 12)); + self::register("stripped_birch_log", $factory->get(261, 0)); + self::register("stripped_birch_wood", $factory->get(467, 10)); + self::register("stripped_dark_oak_log", $factory->get(264, 0)); + self::register("stripped_dark_oak_wood", $factory->get(467, 13)); + self::register("stripped_jungle_log", $factory->get(262, 0)); + self::register("stripped_jungle_wood", $factory->get(467, 11)); + self::register("stripped_oak_log", $factory->get(265, 0)); + self::register("stripped_oak_wood", $factory->get(467, 8)); + self::register("stripped_spruce_log", $factory->get(260, 0)); + self::register("stripped_spruce_wood", $factory->get(467, 9)); + self::register("sugarcane", $factory->get(83, 0)); + self::register("sunflower", $factory->get(175, 0)); + self::register("sweet_berry_bush", $factory->get(462, 0)); + self::register("tall_grass", $factory->get(31, 1)); + self::register("tnt", $factory->get(46, 0)); + self::register("torch", $factory->get(50, 5)); + self::register("trapped_chest", $factory->get(146, 2)); + self::register("tripwire", $factory->get(132, 0)); + self::register("tripwire_hook", $factory->get(131, 0)); + self::register("underwater_torch", $factory->get(239, 5)); + self::register("vines", $factory->get(106, 0)); + self::register("wall_banner", $factory->get(177, 2)); + self::register("wall_coral_fan", $factory->get(390, 0)); + self::register("water", $factory->get(8, 0)); + self::register("weighted_pressure_plate_heavy", $factory->get(148, 0)); + self::register("weighted_pressure_plate_light", $factory->get(147, 0)); + self::register("wheat", $factory->get(59, 0)); + self::register("white_glazed_terracotta", $factory->get(220, 2)); + self::register("white_tulip", $factory->get(38, 6)); + self::register("wool", $factory->get(35, 0)); + self::register("yellow_glazed_terracotta", $factory->get(224, 2)); + } +} diff --git a/src/block/Vine.php b/src/block/Vine.php new file mode 100644 index 0000000000..b9331bc40d --- /dev/null +++ b/src/block/Vine.php @@ -0,0 +1,175 @@ +faces[Facing::SOUTH]) ? BlockLegacyMetadata::VINE_FLAG_SOUTH : 0) | + (isset($this->faces[Facing::WEST]) ? BlockLegacyMetadata::VINE_FLAG_WEST : 0) | + (isset($this->faces[Facing::NORTH]) ? BlockLegacyMetadata::VINE_FLAG_NORTH : 0) | + (isset($this->faces[Facing::EAST]) ? BlockLegacyMetadata::VINE_FLAG_EAST : 0); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->setFaceFromMeta($stateMeta, BlockLegacyMetadata::VINE_FLAG_SOUTH, Facing::SOUTH); + $this->setFaceFromMeta($stateMeta, BlockLegacyMetadata::VINE_FLAG_WEST, Facing::WEST); + $this->setFaceFromMeta($stateMeta, BlockLegacyMetadata::VINE_FLAG_NORTH, Facing::NORTH); + $this->setFaceFromMeta($stateMeta, BlockLegacyMetadata::VINE_FLAG_EAST, Facing::EAST); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + private function setFaceFromMeta(int $meta, int $flag, int $face) : void{ + $this->setFace($face, ($meta & $flag) !== 0); + } + + /** @return int[] */ + public function getFaces() : array{ return $this->faces; } + + /** + * @param int[] $faces + * @phpstan-param list $faces + * @return $this + */ + public function setFaces(array $faces) : self{ + $uniqueFaces = []; + foreach($faces as $face){ + if($face !== Facing::NORTH && $face !== Facing::SOUTH && $face !== Facing::WEST && $face !== Facing::EAST){ + throw new \InvalidArgumentException("Facing can only be north, east, south or west"); + } + $uniqueFaces[$face] = $face; + } + $this->faces = $uniqueFaces; + return $this; + } + + /** @return $this */ + public function setFace(int $face, bool $value) : self{ + if($face !== Facing::NORTH && $face !== Facing::SOUTH && $face !== Facing::WEST && $face !== Facing::EAST){ + throw new \InvalidArgumentException("Facing can only be north, east, south or west"); + } + if($value){ + $this->faces[$face] = $face; + }else{ + unset($this->faces[$face]); + } + return $this; + } + + public function hasEntityCollision() : bool{ + return true; + } + + public function canClimb() : bool{ + return true; + } + + public function canBeReplaced() : bool{ + return true; + } + + public function onEntityInside(Entity $entity) : bool{ + $entity->resetFallDistance(); + return true; + } + + protected function recalculateCollisionBoxes() : array{ + return []; + } + + 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){ + return false; + } + + $this->faces = $blockReplace instanceof Vine ? $blockReplace->faces : []; + $this->faces[Facing::opposite($face)] = Facing::opposite($face); + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $changed = false; + + $up = $this->getSide(Facing::UP); + //check which faces have corresponding vines in the block above + $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()){ + unset($this->faces[$face]); + $changed = true; + } + } + + if($changed){ + if(count($this->faces) === 0){ + $this->position->getWorld()->useBreakOn($this->position); + }else{ + $this->position->getWorld()->setBlock($this->position, $this); + } + } + } + + public function ticksRandomly() : bool{ + return true; + } + + public function onRandomTick() : void{ + //TODO: vine growth + } + + public function getDrops(Item $item) : array{ + if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){ + return $this->getDropsForCompatibleTool($item); + } + + return []; + } + + public function getFlameEncouragement() : int{ + return 15; + } + + public function getFlammability() : int{ + return 100; + } +} diff --git a/src/block/Wall.php b/src/block/Wall.php new file mode 100644 index 0000000000..5a1323d14c --- /dev/null +++ b/src/block/Wall.php @@ -0,0 +1,79 @@ + facing */ + protected array $connections = []; + protected bool $up = false; + + public function readStateFromWorld() : void{ + parent::readStateFromWorld(); + + foreach(Facing::HORIZONTAL as $facing){ + $block = $this->getSide($facing); + if($block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent())){ + $this->connections[$facing] = $facing; + }else{ + unset($this->connections[$facing]); + } + } + + $this->up = $this->getSide(Facing::UP)->getId() !== BlockLegacyIds::AIR; + } + + protected function recalculateCollisionBoxes() : array{ + //walls don't have any special collision boxes like fences do + + $north = isset($this->connections[Facing::NORTH]); + $south = isset($this->connections[Facing::SOUTH]); + $west = isset($this->connections[Facing::WEST]); + $east = isset($this->connections[Facing::EAST]); + + $inset = 0.25; + if( + !$this->up and //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) + ) + ){ + //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 + $inset = 0.3125; + } + + return [ + AxisAlignedBB::one() + ->extend(Facing::UP, 0.5) + ->trim(Facing::NORTH, $north ? 0 : $inset) + ->trim(Facing::SOUTH, $south ? 0 : $inset) + ->trim(Facing::WEST, $west ? 0 : $inset) + ->trim(Facing::EAST, $east ? 0 : $inset) + ]; + } +} diff --git a/src/pocketmine/block/Pumpkin.php b/src/block/WallBanner.php similarity index 53% rename from src/pocketmine/block/Pumpkin.php rename to src/block/WallBanner.php index e5ce7e4b2f..1c807461bc 100644 --- a/src/pocketmine/block/Pumpkin.php +++ b/src/block/WallBanner.php @@ -23,40 +23,26 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\NormalHorizontalFacingInMetadataTrait; use pocketmine\item\Item; +use pocketmine\math\Axis; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; -class Pumpkin extends Solid{ +final class WallBanner extends BaseBanner{ + use NormalHorizontalFacingInMetadataTrait; - protected $id = self::PUMPKIN; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + protected function getSupportingFace() : int{ + return Facing::opposite($this->facing); } - public function getHardness() : float{ - return 1; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getName() : string{ - return "Pumpkin"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($player instanceof Player){ - $this->meta = ((int) $player->getDirection() + 1) % 4; + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(Facing::axis($face) === Axis::Y){ + return false; } - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - public function getVariantBitmask() : int{ - return 0; + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } } diff --git a/src/block/WallCoralFan.php b/src/block/WallCoralFan.php new file mode 100644 index 0000000000..f9462f5823 --- /dev/null +++ b/src/block/WallCoralFan.php @@ -0,0 +1,131 @@ +idInfoFlattened = $idInfo; + parent::__construct($idInfo, $name, $breakInfo); + } + + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readCoralFacing($stateMeta >> 2); + $this->dead = ($stateMeta & BlockLegacyMetadata::CORAL_FAN_HANG_FLAG_DEAD) !== 0; + + $coralTypeFlag = $stateMeta & BlockLegacyMetadata::CORAL_FAN_HANG_TYPE_MASK; + switch($id){ + case $this->idInfoFlattened->getBlockId(): + $this->coralType = $coralTypeFlag === BlockLegacyMetadata::CORAL_FAN_HANG_TUBE ? CoralType::TUBE() : CoralType::BRAIN(); + break; + case $this->idInfoFlattened->getAdditionalId(0): + $this->coralType = $coralTypeFlag === BlockLegacyMetadata::CORAL_FAN_HANG2_BUBBLE ? CoralType::BUBBLE() : CoralType::FIRE(); + break; + case $this->idInfoFlattened->getAdditionalId(1): + if($coralTypeFlag !== BlockLegacyMetadata::CORAL_FAN_HANG3_HORN){ + throw new InvalidBlockStateException("Invalid CORAL_FAN_HANG3 type"); + } + $this->coralType = CoralType::HORN(); + break; + default: + throw new \LogicException("ID/meta doesn't match any CORAL_FAN_HANG type"); + } + } + + public function getId() : int{ + if($this->coralType->equals(CoralType::TUBE()) || $this->coralType->equals(CoralType::BRAIN())){ + return $this->idInfoFlattened->getBlockId(); + }elseif($this->coralType->equals(CoralType::BUBBLE()) || $this->coralType->equals(CoralType::FIRE())){ + return $this->idInfoFlattened->getAdditionalId(0); + }elseif($this->coralType->equals(CoralType::HORN())){ + return $this->idInfoFlattened->getAdditionalId(1); + } + throw new AssumptionFailedError("All types of coral should be covered"); + } + + public function writeStateToMeta() : int{ + $coralTypeFlag = (function() : int{ + switch($this->coralType->id()){ + case CoralType::TUBE()->id(): return BlockLegacyMetadata::CORAL_FAN_HANG_TUBE; + case CoralType::BRAIN()->id(): return BlockLegacyMetadata::CORAL_FAN_HANG_BRAIN; + case CoralType::BUBBLE()->id(): return BlockLegacyMetadata::CORAL_FAN_HANG2_BUBBLE; + case CoralType::FIRE()->id(): return BlockLegacyMetadata::CORAL_FAN_HANG2_FIRE; + case CoralType::HORN()->id(): return BlockLegacyMetadata::CORAL_FAN_HANG3_HORN; + default: throw new AssumptionFailedError("All types of coral should be covered"); + } + })(); + return (BlockDataSerializer::writeCoralFacing($this->facing) << 2) | ($this->dead ? BlockLegacyMetadata::CORAL_FAN_HANG_FLAG_DEAD : 0) | $coralTypeFlag; + } + + protected function writeStateToItemMeta() : int{ + return CoralTypeIdMap::getInstance()->toId($this->coralType); + } + + public function getStateBitmask() : int{ + return 0b1111; + } + + public function asItem() : Item{ + return ItemFactory::getInstance()->get( + $this->dead ? ItemIds::CORAL_FAN_DEAD : ItemIds::CORAL_FAN, + $this->writeStateToItemMeta() + ); + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $axis = Facing::axis($face); + if(($axis !== Axis::X && $axis !== Axis::Z) || !$blockClicked->isSolid()){ + return false; + } + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); + if(!$world->getBlock($this->position->getSide(Facing::opposite($this->facing)))->isSolid()){ + $world->useBreakOn($this->position); + }else{ + parent::onNearbyBlockChange(); + } + } +} diff --git a/src/pocketmine/block/GlazedTerracotta.php b/src/block/WallSign.php similarity index 52% rename from src/pocketmine/block/GlazedTerracotta.php rename to src/block/WallSign.php index e2ae343bfc..38a6264338 100644 --- a/src/pocketmine/block/GlazedTerracotta.php +++ b/src/block/WallSign.php @@ -23,40 +23,26 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\NormalHorizontalFacingInMetadataTrait; use pocketmine\item\Item; -use pocketmine\item\TieredTool; +use pocketmine\math\Axis; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; -class GlazedTerracotta extends Solid{ +final class WallSign extends BaseSign{ + use NormalHorizontalFacingInMetadataTrait; - public function getHardness() : float{ - return 1.4; + protected function getSupportingFace() : int{ + return Facing::opposite($this->facing); } - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($player !== null){ - $faces = [ - 0 => 4, - 1 => 3, - 2 => 5, - 3 => 2 - ]; - $this->meta = $faces[(~($player->getDirection() - 1)) & 0x03]; + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(Facing::axis($face) === Axis::Y){ + return false; } - - return $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - } - - public function getVariantBitmask() : int{ - return 0; + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } } diff --git a/src/pocketmine/block/Fallable.php b/src/block/Water.php similarity index 54% rename from src/pocketmine/block/Fallable.php rename to src/block/Water.php index 15d87b97d0..ff638a8f9f 100644 --- a/src/pocketmine/block/Fallable.php +++ b/src/block/Water.php @@ -24,28 +24,37 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\entity\Entity; -use pocketmine\math\Vector3; +use pocketmine\world\sound\BucketEmptyWaterSound; +use pocketmine\world\sound\BucketFillWaterSound; +use pocketmine\world\sound\Sound; -abstract class Fallable extends Solid{ +class Water extends Liquid{ - public function onNearbyBlockChange() : void{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === self::AIR or $down instanceof Liquid or $down instanceof Fire){ - $this->level->setBlock($this, BlockFactory::get(Block::AIR), true); - - $nbt = Entity::createBaseNBT($this->add(0.5, 0, 0.5)); - $nbt->setInt("TileID", $this->getId()); - $nbt->setByte("Data", $this->getDamage()); - - $fall = Entity::createEntity("FallingSand", $this->getLevelNonNull(), $nbt); - - if($fall !== null){ - $fall->spawnToAll(); - } - } + public function getLightFilter() : int{ + return 2; } - public function tickFalling() : ?Block{ - return null; + public function getBucketFillSound() : Sound{ + return new BucketFillWaterSound(); + } + + public function getBucketEmptySound() : Sound{ + return new BucketEmptyWaterSound(); + } + + public function tickRate() : int{ + return 5; + } + + public function getMinAdjacentSourcesToFormSource() : ?int{ + return 2; + } + + public function onEntityInside(Entity $entity) : bool{ + $entity->resetFallDistance(); + if($entity->isOnFire()){ + $entity->extinguish(); + } + return true; } } diff --git a/src/pocketmine/block/WaterLily.php b/src/block/WaterLily.php similarity index 51% rename from src/pocketmine/block/WaterLily.php rename to src/block/WaterLily.php index ea9010fc28..9cb5396b81 100644 --- a/src/pocketmine/block/WaterLily.php +++ b/src/block/WaterLily.php @@ -25,42 +25,25 @@ namespace pocketmine\block; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\BlockTransaction; class WaterLily extends Flowable{ - protected $id = self::WATER_LILY; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + /** + * @return AxisAlignedBB[] + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->contract(1 / 16, 0, 1 / 16)->trim(Facing::UP, 63 / 64)]; } - public function getName() : string{ - return "Lily Pad"; - } - - public function getHardness() : float{ - return 0.6; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x + 0.0625, - $this->y, - $this->z + 0.0625, - $this->x + 0.9375, - $this->y + 0.015625, - $this->z + 0.9375 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($blockClicked instanceof Water){ - $up = $blockClicked->getSide(Vector3::SIDE_UP); - if($up->getId() === Block::AIR){ - $this->getLevelNonNull()->setBlock($up, $this, true, true); - return true; + $up = $blockClicked->getSide(Facing::UP); + if($up->canBeReplaced()){ + return parent::place($tx, $item, $up, $blockClicked, $face, $clickVector, $player); } } @@ -68,12 +51,8 @@ class WaterLily extends Flowable{ } public function onNearbyBlockChange() : void{ - if(!($this->getSide(Vector3::SIDE_DOWN) instanceof Water)){ - $this->getLevelNonNull()->useBreakOn($this); + if(!($this->getSide(Facing::DOWN) instanceof Water)){ + $this->position->getWorld()->useBreakOn($this->position); } } - - public function getVariantBitmask() : int{ - return 0; - } } diff --git a/src/pocketmine/block/BrickStairs.php b/src/block/WeightedPressurePlate.php similarity index 60% rename from src/pocketmine/block/BrickStairs.php rename to src/block/WeightedPressurePlate.php index 4e305e9552..fa613dac7f 100644 --- a/src/pocketmine/block/BrickStairs.php +++ b/src/block/WeightedPressurePlate.php @@ -23,33 +23,21 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\item\TieredTool; +use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; +use pocketmine\block\utils\BlockDataSerializer; -class BrickStairs extends Stair{ +abstract class WeightedPressurePlate extends PressurePlate{ + use AnalogRedstoneSignalEmitterTrait; - protected $id = self::BRICK_STAIRS; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + protected function writeStateToMeta() : int{ + return $this->signalStrength; } - public function getHardness() : float{ - return 2; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->signalStrength = BlockDataSerializer::readBoundedInt("signalStrength", $stateMeta, 0, 15); } - public function getBlastResistance() : float{ - return 30; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Brick Stairs"; + public function getStateBitmask() : int{ + return 0b1111; } } diff --git a/src/pocketmine/block/WeightedPressurePlateHeavy.php b/src/block/WeightedPressurePlateHeavy.php similarity index 78% rename from src/pocketmine/block/WeightedPressurePlateHeavy.php rename to src/block/WeightedPressurePlateHeavy.php index bcfe010d49..acc8068d97 100644 --- a/src/pocketmine/block/WeightedPressurePlateHeavy.php +++ b/src/block/WeightedPressurePlateHeavy.php @@ -23,11 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -class WeightedPressurePlateHeavy extends WeightedPressurePlateLight{ +class WeightedPressurePlateHeavy extends WeightedPressurePlate{ - protected $id = self::HEAVY_WEIGHTED_PRESSURE_PLATE; - - public function getName() : string{ - return "Weighted Pressure Plate Heavy"; - } } diff --git a/src/pocketmine/block/InfoUpdate.php b/src/block/WeightedPressurePlateLight.php similarity index 89% rename from src/pocketmine/block/InfoUpdate.php rename to src/block/WeightedPressurePlateLight.php index f640e49d4f..877211f625 100644 --- a/src/pocketmine/block/InfoUpdate.php +++ b/src/block/WeightedPressurePlateLight.php @@ -23,9 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -class InfoUpdate extends Solid{ +class WeightedPressurePlateLight extends WeightedPressurePlate{ - public function getHardness() : float{ - return 1; - } } diff --git a/src/pocketmine/block/Wheat.php b/src/block/Wheat.php similarity index 67% rename from src/pocketmine/block/Wheat.php rename to src/block/Wheat.php index 962bc5c7e4..5261e38ecd 100644 --- a/src/pocketmine/block/Wheat.php +++ b/src/block/Wheat.php @@ -24,35 +24,25 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; use function mt_rand; class Wheat extends Crops{ - protected $id = self::WHEAT_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getName() : string{ - return "Wheat Block"; - } - public function getDropsForCompatibleTool(Item $item) : array{ - if($this->meta >= 0x07){ + if($this->age >= 7){ return [ - ItemFactory::get(Item::WHEAT), - ItemFactory::get(Item::WHEAT_SEEDS, 0, mt_rand(0, 3)) + VanillaItems::WHEAT(), + VanillaItems::WHEAT_SEEDS()->setCount(mt_rand(0, 3)) ]; }else{ return [ - ItemFactory::get(Item::WHEAT_SEEDS) + VanillaItems::WHEAT_SEEDS() ]; } } - public function getPickedItem() : Item{ - return ItemFactory::get(Item::WHEAT_SEEDS); + public function getPickedItem(bool $addUserData = false) : Item{ + return VanillaItems::WHEAT_SEEDS(); } } diff --git a/src/block/Wood.php b/src/block/Wood.php new file mode 100644 index 0000000000..f6d1c30fd9 --- /dev/null +++ b/src/block/Wood.php @@ -0,0 +1,71 @@ +stripped = $stripped; //TODO: this should be dynamic, but right now legacy shit gets in the way + parent::__construct($idInfo, $name, $breakInfo); + $this->treeType = $treeType; + } + + /** + * TODO: this is ad hoc, but add an interface for this to all tree-related blocks + */ + public function getTreeType() : TreeType{ + return $this->treeType; + } + + public function isStripped() : bool{ return $this->stripped; } + + public function getFuelTime() : int{ + return 300; + } + + public function getFlameEncouragement() : int{ + return 5; + } + + public function getFlammability() : int{ + return 5; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if(!$this->stripped && ($item->getBlockToolType() & BlockToolType::AXE) !== 0){ + //TODO: strip logs; can't implement this yet because of legacy limitations :( + return true; + } + return false; + } +} diff --git a/src/pocketmine/block/WoodenButton.php b/src/block/WoodenButton.php similarity index 77% rename from src/pocketmine/block/WoodenButton.php rename to src/block/WoodenButton.php index a62225cc2e..44f4f867be 100644 --- a/src/pocketmine/block/WoodenButton.php +++ b/src/block/WoodenButton.php @@ -25,17 +25,11 @@ namespace pocketmine\block; class WoodenButton extends Button{ - protected $id = self::WOODEN_BUTTON; - - public function getName() : string{ - return "Wooden Button"; + protected function getActivationTime() : int{ + return 30; } - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; + public function hasEntityCollision() : bool{ + return false; //TODO: arrows activate wooden buttons } } diff --git a/src/pocketmine/block/WoodenDoor.php b/src/block/WoodenDoor.php similarity index 85% rename from src/pocketmine/block/WoodenDoor.php rename to src/block/WoodenDoor.php index 2ef5155c31..af6c7b25f2 100644 --- a/src/pocketmine/block/WoodenDoor.php +++ b/src/block/WoodenDoor.php @@ -25,11 +25,4 @@ namespace pocketmine\block; class WoodenDoor extends Door{ - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } } diff --git a/src/block/WoodenFence.php b/src/block/WoodenFence.php new file mode 100644 index 0000000000..b128eca0fe --- /dev/null +++ b/src/block/WoodenFence.php @@ -0,0 +1,39 @@ +color = DyeColor::WHITE(); + parent::__construct($idInfo, $name, $breakInfo); } public function getFlameEncouragement() : int{ - return 5; + return 30; } public function getFlammability() : int{ - return 20; + return 60; } } diff --git a/src/block/inventory/AnimatedBlockInventoryTrait.php b/src/block/inventory/AnimatedBlockInventoryTrait.php new file mode 100644 index 0000000000..e0bb9fee0f --- /dev/null +++ b/src/block/inventory/AnimatedBlockInventoryTrait.php @@ -0,0 +1,67 @@ +getViewers()); + } + + /** + * @return Player[] + * @phpstan-return array + */ + abstract public function getViewers() : array; + + abstract protected function getOpenSound() : Sound; + + abstract protected function getCloseSound() : Sound; + + public function onOpen(Player $who) : void{ + parent::onOpen($who); + + if($this->getHolder()->isValid() and $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()); + } + } + + abstract protected function animateBlock(bool $isOpen) : void; + + public function onClose(Player $who) : void{ + if($this->getHolder()->isValid() and $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()); + } + parent::onClose($who); + } +} diff --git a/src/block/inventory/AnvilInventory.php b/src/block/inventory/AnvilInventory.php new file mode 100644 index 0000000000..1def8f913b --- /dev/null +++ b/src/block/inventory/AnvilInventory.php @@ -0,0 +1,40 @@ +holder = $holder; + parent::__construct(2); + } +} diff --git a/src/block/inventory/BarrelInventory.php b/src/block/inventory/BarrelInventory.php new file mode 100644 index 0000000000..fe2662e30b --- /dev/null +++ b/src/block/inventory/BarrelInventory.php @@ -0,0 +1,56 @@ +holder = $holder; + parent::__construct(27); + } + + protected function getOpenSound() : Sound{ + return new BarrelOpenSound(); + } + + protected function getCloseSound() : Sound{ + return new BarrelCloseSound(); + } + + protected function animateBlock(bool $isOpen) : void{ + $holder = $this->getHolder(); + $block = $holder->getWorld()->getBlock($holder); + if($block instanceof Barrel){ + $holder->getWorld()->setBlock($holder, $block->setOpen($isOpen)); + } + } +} diff --git a/src/block/inventory/BlockInventory.php b/src/block/inventory/BlockInventory.php new file mode 100644 index 0000000000..7712688c15 --- /dev/null +++ b/src/block/inventory/BlockInventory.php @@ -0,0 +1,30 @@ +holder; + } +} diff --git a/src/pocketmine/block/CobblestoneStairs.php b/src/block/inventory/BrewingStandInventory.php similarity index 59% rename from src/pocketmine/block/CobblestoneStairs.php rename to src/block/inventory/BrewingStandInventory.php index 06357dea1d..0faf4b672f 100644 --- a/src/pocketmine/block/CobblestoneStairs.php +++ b/src/block/inventory/BrewingStandInventory.php @@ -21,31 +21,22 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\block\inventory; -use pocketmine\item\TieredTool; +use pocketmine\inventory\SimpleInventory; +use pocketmine\world\Position; -class CobblestoneStairs extends Stair{ +class BrewingStandInventory extends SimpleInventory implements BlockInventory{ + use BlockInventoryTrait; - protected $id = self::COBBLESTONE_STAIRS; + public const SLOT_INGREDIENT = 0; + public const SLOT_BOTTLE_LEFT = 1; + public const SLOT_BOTTLE_MIDDLE = 2; + public const SLOT_BOTTLE_RIGHT = 3; + public const SLOT_FUEL = 4; - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getHardness() : float{ - return 2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Cobblestone Stairs"; + public function __construct(Position $holder, int $size = 5){ + $this->holder = $holder; + parent::__construct($size); } } diff --git a/src/block/inventory/ChestInventory.php b/src/block/inventory/ChestInventory.php new file mode 100644 index 0000000000..b7594b2320 --- /dev/null +++ b/src/block/inventory/ChestInventory.php @@ -0,0 +1,56 @@ +holder = $holder; + parent::__construct(27); + } + + protected function getOpenSound() : Sound{ + return new ChestOpenSound(); + } + + protected function getCloseSound() : Sound{ + return new ChestCloseSound(); + } + + public function animateBlock(bool $isOpen) : void{ + $holder = $this->getHolder(); + + //event ID is always 1 for a chest + $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 0000000000..a885df4bef --- /dev/null +++ b/src/block/inventory/CraftingTableInventory.php @@ -0,0 +1,37 @@ +holder = $holder; + parent::__construct(CraftingGrid::SIZE_BIG); + } +} \ No newline at end of file diff --git a/src/block/inventory/DoubleChestInventory.php b/src/block/inventory/DoubleChestInventory.php new file mode 100644 index 0000000000..9d99f1e2b9 --- /dev/null +++ b/src/block/inventory/DoubleChestInventory.php @@ -0,0 +1,108 @@ +left = $left; + $this->right = $right; + $this->holder = $this->left->getHolder(); + parent::__construct(); + } + + public function getInventory(){ + return $this; + } + + public function getSize() : int{ + return $this->left->getSize() + $this->right->getSize(); + } + + public function getItem(int $index) : Item{ + return $index < $this->left->getSize() ? $this->left->getItem($index) : $this->right->getItem($index - $this->left->getSize()); + } + + protected function internalSetItem(int $index, Item $item) : void{ + $index < $this->left->getSize() ? $this->left->setItem($index, $item) : $this->right->setItem($index - $this->left->getSize(), $item); + } + + public function getContents(bool $includeEmpty = false) : array{ + $result = $this->left->getContents($includeEmpty); + $leftSize = $this->left->getSize(); + + foreach($this->right->getContents($includeEmpty) as $i => $item){ + $result[$i + $leftSize] = $item; + } + + return $result; + } + + protected function internalSetContents(array $items) : void{ + $leftSize = $this->left->getSize(); + + $leftContents = []; + $rightContents = []; + + foreach($items as $i => $item){ + if($i < $this->left->getSize()){ + $leftContents[$i] = $item; + }else{ + $rightContents[$i - $leftSize] = $item; + } + } + $this->left->setContents($leftContents); + $this->right->setContents($rightContents); + } + + protected function getOpenSound() : Sound{ return new ChestOpenSound(); } + + protected function getCloseSound() : Sound{ return new ChestCloseSound(); } + + protected function animateBlock(bool $isOpen) : void{ + $this->left->animateBlock($isOpen); + $this->right->animateBlock($isOpen); + } + + public function getLeftSide() : ChestInventory{ + return $this->left; + } + + public function getRightSide() : ChestInventory{ + return $this->right; + } +} diff --git a/src/pocketmine/block/DoubleStoneSlab.php b/src/block/inventory/EnchantInventory.php similarity index 62% rename from src/pocketmine/block/DoubleStoneSlab.php rename to src/block/inventory/EnchantInventory.php index 1466828ea3..91bfcab5e4 100644 --- a/src/pocketmine/block/DoubleStoneSlab.php +++ b/src/block/inventory/EnchantInventory.php @@ -21,27 +21,20 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\block\inventory; -use pocketmine\item\TieredTool; +use pocketmine\inventory\SimpleInventory; +use pocketmine\inventory\TemporaryInventory; +use pocketmine\world\Position; -class DoubleStoneSlab extends DoubleSlab{ +class EnchantInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ + use BlockInventoryTrait; - protected $id = self::DOUBLE_STONE_SLAB; + public const SLOT_INPUT = 0; + public const SLOT_LAPIS = 1; - public function getSlabId() : int{ - return self::STONE_SLAB; - } - - public function getHardness() : float{ - return 2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; + public function __construct(Position $holder){ + $this->holder = $holder; + parent::__construct(2); } } diff --git a/src/block/inventory/EnderChestInventory.php b/src/block/inventory/EnderChestInventory.php new file mode 100644 index 0000000000..b9ff1832ef --- /dev/null +++ b/src/block/inventory/EnderChestInventory.php @@ -0,0 +1,88 @@ +holder = $holder; + $this->inventory = $inventory; + } + + public function getEnderInventory() : PlayerEnderInventory{ + return $this->inventory; + } + + public function getViewerCount() : int{ + $enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder()); + if(!$enderChest instanceof EnderChest){ + return 0; + } + return $enderChest->getViewerCount(); + } + + protected function getOpenSound() : Sound{ + return new EnderChestOpenSound(); + } + + protected function getCloseSound() : Sound{ + return new EnderChestCloseSound(); + } + + protected function animateBlock(bool $isOpen) : void{ + $holder = $this->getHolder(); + + //event ID is always 1 for a chest + $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0)); + } + + public function onClose(Player $who) : void{ + $this->animatedBlockInventoryTrait_onClose($who); + $enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder()); + if($enderChest instanceof EnderChest){ + $enderChest->setViewerCount($enderChest->getViewerCount() - 1); + } + } +} diff --git a/src/block/inventory/FurnaceInventory.php b/src/block/inventory/FurnaceInventory.php new file mode 100644 index 0000000000..5f3a685eab --- /dev/null +++ b/src/block/inventory/FurnaceInventory.php @@ -0,0 +1,71 @@ +holder = $holder; + $this->furnaceType = $furnaceType; + parent::__construct(3); + } + + public function getFurnaceType() : FurnaceType{ return $this->furnaceType; } + + public function getResult() : Item{ + return $this->getItem(self::SLOT_RESULT); + } + + public function getFuel() : Item{ + return $this->getItem(self::SLOT_FUEL); + } + + public function getSmelting() : Item{ + return $this->getItem(self::SLOT_INPUT); + } + + public function setResult(Item $item) : void{ + $this->setItem(self::SLOT_RESULT, $item); + } + + public function setFuel(Item $item) : void{ + $this->setItem(self::SLOT_FUEL, $item); + } + + public function setSmelting(Item $item) : void{ + $this->setItem(self::SLOT_INPUT, $item); + } +} diff --git a/src/block/inventory/HopperInventory.php b/src/block/inventory/HopperInventory.php new file mode 100644 index 0000000000..eb75244c33 --- /dev/null +++ b/src/block/inventory/HopperInventory.php @@ -0,0 +1,36 @@ +holder = $holder; + parent::__construct($size); + } +} diff --git a/src/pocketmine/block/Cobblestone.php b/src/block/inventory/LoomInventory.php similarity index 60% rename from src/pocketmine/block/Cobblestone.php rename to src/block/inventory/LoomInventory.php index 87169fc63b..27a8f2dbc3 100644 --- a/src/pocketmine/block/Cobblestone.php +++ b/src/block/inventory/LoomInventory.php @@ -21,31 +21,21 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\block\inventory; -use pocketmine\item\TieredTool; +use pocketmine\inventory\SimpleInventory; +use pocketmine\inventory\TemporaryInventory; +use pocketmine\world\Position; -class Cobblestone extends Solid{ +final class LoomInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ + use BlockInventoryTrait; - protected $id = self::COBBLESTONE; + public const SLOT_BANNER = 0; + public const SLOT_DYE = 1; + public const SLOT_PATTERN = 2; - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Cobblestone"; - } - - public function getHardness() : float{ - return 2; + public function __construct(Position $holder, int $size = 3){ + $this->holder = $holder; + parent::__construct($size); } } diff --git a/src/block/inventory/ShulkerBoxInventory.php b/src/block/inventory/ShulkerBoxInventory.php new file mode 100644 index 0000000000..cace496522 --- /dev/null +++ b/src/block/inventory/ShulkerBoxInventory.php @@ -0,0 +1,65 @@ +holder = $holder; + parent::__construct(27); + } + + protected function getOpenSound() : Sound{ + return new ShulkerBoxOpenSound(); + } + + protected function getCloseSound() : Sound{ + return new ShulkerBoxCloseSound(); + } + + public function canAddItem(Item $item) : bool{ + if($item->getId() === BlockLegacyIds::UNDYED_SHULKER_BOX || $item->getId() === BlockLegacyIds::SHULKER_BOX){ + return false; + } + return parent::canAddItem($item); + } + + protected function animateBlock(bool $isOpen) : void{ + $holder = $this->getHolder(); + + //event ID is always 1 for a chest + $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0)); + } +} diff --git a/src/block/tile/Banner.php b/src/block/tile/Banner.php new file mode 100644 index 0000000000..4404a39710 --- /dev/null +++ b/src/block/tile/Banner.php @@ -0,0 +1,150 @@ + + */ + private $patterns = []; + + public function __construct(World $world, Vector3 $pos){ + $this->baseColor = DyeColor::BLACK(); + parent::__construct($world, $pos); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $colorIdMap = DyeColorIdMap::getInstance(); + if( + ($baseColorTag = $nbt->getTag(self::TAG_BASE)) instanceof IntTag && + ($baseColor = $colorIdMap->fromInvertedId($baseColorTag->getValue())) !== null + ){ + $this->baseColor = $baseColor; + }else{ + $this->baseColor = DyeColor::BLACK(); //TODO: this should be an error + } + + $patternTypeIdMap = BannerPatternTypeIdMap::getInstance(); + + $patterns = $nbt->getListTag(self::TAG_PATTERNS); + if($patterns !== null){ + /** @var CompoundTag $pattern */ + foreach($patterns as $pattern){ + $patternColor = $colorIdMap->fromInvertedId($pattern->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK(); //TODO: missing pattern colour should be an error + $patternType = $patternTypeIdMap->fromId($pattern->getString(self::TAG_PATTERN_NAME)); + if($patternType === null){ + continue; //TODO: this should be an error, but right now we don't have the setup to deal with it + } + $this->patterns[] = new BannerPatternLayer($patternType, $patternColor); + } + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $colorIdMap = DyeColorIdMap::getInstance(); + $patternIdMap = BannerPatternTypeIdMap::getInstance(); + $nbt->setInt(self::TAG_BASE, $colorIdMap->toInvertedId($this->baseColor)); + $patterns = new ListTag(); + foreach($this->patterns as $pattern){ + $patterns->push(CompoundTag::create() + ->setString(self::TAG_PATTERN_NAME, $patternIdMap->toId($pattern->getType())) + ->setInt(self::TAG_PATTERN_COLOR, $colorIdMap->toInvertedId($pattern->getColor())) + ); + } + $nbt->setTag(self::TAG_PATTERNS, $patterns); + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $colorIdMap = DyeColorIdMap::getInstance(); + $patternIdMap = BannerPatternTypeIdMap::getInstance(); + $nbt->setInt(self::TAG_BASE, $colorIdMap->toInvertedId($this->baseColor)); + $patterns = new ListTag(); + foreach($this->patterns as $pattern){ + $patterns->push(CompoundTag::create() + ->setString(self::TAG_PATTERN_NAME, $patternIdMap->toId($pattern->getType())) + ->setInt(self::TAG_PATTERN_COLOR, $colorIdMap->toInvertedId($pattern->getColor())) + ); + } + $nbt->setTag(self::TAG_PATTERNS, $patterns); + } + + /** + * Returns the color of the banner base. + */ + public function getBaseColor() : DyeColor{ + return $this->baseColor; + } + + /** + * Sets the color of the banner base. + */ + public function setBaseColor(DyeColor $color) : void{ + $this->baseColor = $color; + } + + /** + * @return BannerPatternLayer[] + * @phpstan-return list + */ + public function getPatterns() : array{ + return $this->patterns; + } + + /** + * @param BannerPatternLayer[] $patterns + * + * @phpstan-param list $patterns + */ + public function setPatterns(array $patterns) : void{ + $this->patterns = $patterns; + } + + public function getDefaultName() : string{ + return "Banner"; + } +} diff --git a/src/block/tile/Barrel.php b/src/block/tile/Barrel.php new file mode 100644 index 0000000000..68491cd089 --- /dev/null +++ b/src/block/tile/Barrel.php @@ -0,0 +1,77 @@ +inventory = new BarrelInventory($this->position); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->loadName($nbt); + $this->loadItems($nbt); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $this->saveName($nbt); + $this->saveItems($nbt); + } + + public function close() : void{ + if(!$this->closed){ + $this->inventory->removeAllViewers(); + parent::close(); + } + } + + /** + * @return BarrelInventory + */ + public function getInventory(){ + return $this->inventory; + } + + /** + * @return BarrelInventory + */ + public function getRealInventory(){ + return $this->inventory; + } + + public function getDefaultName() : string{ + return "Barrel"; + } +} diff --git a/src/block/tile/Beacon.php b/src/block/tile/Beacon.php new file mode 100644 index 0000000000..4d4b8cf6d6 --- /dev/null +++ b/src/block/tile/Beacon.php @@ -0,0 +1,60 @@ +setInt(self::TAG_PRIMARY, $this->primaryEffect); + $nbt->setInt(self::TAG_SECONDARY, $this->secondaryEffect); + } + + public function readSaveData(CompoundTag $nbt) : void{ + //TODO: PC uses Primary and Secondary (capitalized first letter), we don't read them here because the IDs would be different + $this->primaryEffect = $nbt->getInt(self::TAG_PRIMARY, 0); + $this->secondaryEffect = $nbt->getInt(self::TAG_SECONDARY, 0); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setInt(self::TAG_PRIMARY, $this->primaryEffect); + $nbt->setInt(self::TAG_SECONDARY, $this->secondaryEffect); + } + + public function getPrimaryEffect() : int{ return $this->primaryEffect; } + + public function setPrimaryEffect(int $primaryEffect) : void{ $this->primaryEffect = $primaryEffect; } + + public function getSecondaryEffect() : int{ return $this->secondaryEffect; } + + public function setSecondaryEffect(int $secondaryEffect) : void{ $this->secondaryEffect = $secondaryEffect; } +} diff --git a/src/block/tile/Bed.php b/src/block/tile/Bed.php new file mode 100644 index 0000000000..77cca1cebc --- /dev/null +++ b/src/block/tile/Bed.php @@ -0,0 +1,69 @@ +color = DyeColor::RED(); + parent::__construct($world, $pos); + } + + public function getColor() : DyeColor{ + return $this->color; + } + + public function setColor(DyeColor $color) : void{ + $this->color = $color; + } + + public function readSaveData(CompoundTag $nbt) : void{ + if( + ($colorTag = $nbt->getTag(self::TAG_COLOR)) instanceof ByteTag && + ($color = DyeColorIdMap::getInstance()->fromId($colorTag->getValue())) !== null + ){ + $this->color = $color; + }else{ + $this->color = DyeColor::RED(); //TODO: this should be an error, but we don't have the systems to handle it yet + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_COLOR, DyeColorIdMap::getInstance()->toId($this->color)); + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_COLOR, DyeColorIdMap::getInstance()->toId($this->color)); + } +} diff --git a/src/block/tile/Bell.php b/src/block/tile/Bell.php new file mode 100644 index 0000000000..bc2ab29fe7 --- /dev/null +++ b/src/block/tile/Bell.php @@ -0,0 +1,87 @@ +ringing; } + + public function setRinging(bool $ringing) : void{ $this->ringing = $ringing; } + + public function getFacing() : int{ return $this->facing; } + + public function setFacing(int $facing) : void{ $this->facing = $facing; } + + public function getTicks() : int{ return $this->ticks; } + + public function setTicks(int $ticks) : void{ $this->ticks = $ticks; } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_RINGING, $this->ringing ? 1 : 0); + $nbt->setInt(self::TAG_DIRECTION, $this->facing); + $nbt->setInt(self::TAG_TICKS, $this->ticks); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->ringing = $nbt->getByte(self::TAG_RINGING, 0) !== 0; + $this->facing = $nbt->getInt(self::TAG_DIRECTION, Facing::NORTH); + $this->ticks = $nbt->getInt(self::TAG_TICKS, 0); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_RINGING, $this->ringing ? 1 : 0); + $nbt->setInt(self::TAG_DIRECTION, $this->facing); + $nbt->setInt(self::TAG_TICKS, $this->ticks); + } + + /** + * TODO: HACK! + * Creates a BlockActorDataPacket that triggers the ringing animation on a bell block. + * + * Bedrock team overcomplicated making bells ring when they implemented this; this would have been better and much + * simpler as a BlockEventPacket. It's simpler to implement bells with this hack than to follow Mojang's complicated + * mess. + */ + public function createFakeUpdatePacket(int $bellHitFace) : BlockActorDataPacket{ + $nbt = $this->getSpawnCompound(); + $nbt->setByte(self::TAG_RINGING, 1); + $nbt->setInt(self::TAG_DIRECTION, BlockDataSerializer::writeLegacyHorizontalFacing($bellHitFace)); + $nbt->setInt(self::TAG_TICKS, 0); + return BlockActorDataPacket::create(BlockPosition::fromVector3($this->position), new CacheableNbt($nbt)); + } +} diff --git a/src/block/tile/BlastFurnace.php b/src/block/tile/BlastFurnace.php new file mode 100644 index 0000000000..ded48d7106 --- /dev/null +++ b/src/block/tile/BlastFurnace.php @@ -0,0 +1,32 @@ +inventory = new BrewingStandInventory($this->position); + $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(static function(Inventory $unused) use ($world, $pos) : void{ + $world->scheduleDelayedBlockUpdate($pos, 1); + })); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->loadName($nbt); + $this->loadItems($nbt); + + $this->brewTime = $nbt->getShort(self::TAG_BREW_TIME, $nbt->getShort(self::TAG_BREW_TIME_PE, 0)); + $this->maxFuelTime = $nbt->getShort(self::TAG_MAX_FUEL_TIME, 0); + $this->remainingFuelTime = $nbt->getByte(self::TAG_REMAINING_FUEL_TIME, $nbt->getShort(self::TAG_REMAINING_FUEL_TIME_PE, 0)); + if($this->maxFuelTime === 0){ + $this->maxFuelTime = $this->remainingFuelTime; + } + if($this->remainingFuelTime === 0){ + $this->maxFuelTime = $this->remainingFuelTime = $this->brewTime = 0; + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $this->saveName($nbt); + $this->saveItems($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"; + } + + public function close() : void{ + if(!$this->closed){ + $this->inventory->removeAllViewers(); + + parent::close(); + } + } + + /** + * @return BrewingStandInventory + */ + public function getInventory(){ + return $this->inventory; + } + + /** + * @return BrewingStandInventory + */ + public function getRealInventory(){ + return $this->inventory; + } +} diff --git a/src/pocketmine/tile/Chest.php b/src/block/tile/Chest.php similarity index 63% rename from src/pocketmine/tile/Chest.php rename to src/block/tile/Chest.php index 41468fb1c9..67534f123e 100644 --- a/src/pocketmine/tile/Chest.php +++ b/src/block/tile/Chest.php @@ -21,20 +21,24 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; -use pocketmine\inventory\ChestInventory; -use pocketmine\inventory\DoubleChestInventory; -use pocketmine\inventory\InventoryHolder; +use pocketmine\block\inventory\ChestInventory; +use pocketmine\block\inventory\DoubleChestInventory; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntTag; +use pocketmine\world\format\Chunk; +use pocketmine\world\World; +use function abs; -class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ +class Chest extends Spawnable implements Container, Nameable{ use NameableTrait { addAdditionalSpawnData as addNameSpawnData; } - use ContainerTrait; + use ContainerTrait { + onBlockDestroyedHook as containerTraitBlockDestroyedHook; + } public const TAG_PAIRX = "pairx"; public const TAG_PAIRZ = "pairz"; @@ -50,14 +54,26 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ /** @var int|null */ private $pairZ; - protected function readSaveData(CompoundTag $nbt) : void{ - if($nbt->hasTag(self::TAG_PAIRX, IntTag::class) and $nbt->hasTag(self::TAG_PAIRZ, IntTag::class)){ - $this->pairX = $nbt->getInt(self::TAG_PAIRX); - $this->pairZ = $nbt->getInt(self::TAG_PAIRZ); + public function __construct(World $world, Vector3 $pos){ + parent::__construct($world, $pos); + $this->inventory = new ChestInventory($this->position); + } + + public function readSaveData(CompoundTag $nbt) : void{ + if(($pairXTag = $nbt->getTag(self::TAG_PAIRX)) instanceof IntTag and ($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->pairX = $pairX; + $this->pairZ = $pairZ; + }else{ + $this->pairX = $this->pairZ = null; + } } $this->loadName($nbt); - - $this->inventory = new ChestInventory($this); $this->loadItems($nbt); } @@ -81,12 +97,11 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ public function close() : void{ if(!$this->closed){ - $this->inventory->removeAllViewers(true); + $this->inventory->removeAllViewers(); if($this->doubleInventory !== null){ - if($this->isPaired() and $this->level->isChunkLoaded($this->pairX >> 4, $this->pairZ >> 4)){ - $this->doubleInventory->removeAllViewers(true); - $this->doubleInventory->invalidate(); + if($this->isPaired() and $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; } @@ -94,12 +109,15 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ $this->doubleInventory = null; } - $this->inventory = null; - parent::close(); } } + protected function onBlockDestroyedHook() : void{ + $this->unpair(); + $this->containerTraitBlockDestroyedHook(); + } + /** * @return ChestInventory|DoubleChestInventory */ @@ -117,11 +135,8 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ return $this->inventory; } - /** - * @return void - */ - protected function checkPairing(){ - if($this->isPaired() and !$this->getLevelNonNull()->isInLoadedTerrain(new Vector3($this->pairX, $this->y, $this->pairZ))){ + protected function checkPairing() : void{ + if($this->isPaired() and !$this->position->getWorld()->isInLoadedTerrain(new Vector3($this->pairX, $this->position->y, $this->pairZ))){ //paired to a tile in an unloaded chunk $this->doubleInventory = null; @@ -134,10 +149,10 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ if($pair->doubleInventory !== null){ $this->doubleInventory = $pair->doubleInventory; }else{ - if(($pair->x + ($pair->z << 15)) > ($this->x + ($this->z << 15))){ //Order them correctly - $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($pair, $this); + if(($pair->getPosition()->x + ($pair->getPosition()->z << 15)) > ($this->position->x + ($this->position->z << 15))){ //Order them correctly + $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($pair->inventory, $this->inventory); }else{ - $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this, $pair); + $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this->inventory, $pair->inventory); } } } @@ -151,16 +166,13 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ return "Chest"; } - /** - * @return bool - */ - public function isPaired(){ + public function isPaired() : bool{ return $this->pairX !== null and $this->pairZ !== null; } public function getPair() : ?Chest{ if($this->isPaired()){ - $tile = $this->getLevelNonNull()->getTileAt($this->pairX, $this->y, $this->pairZ); + $tile = $this->position->getWorld()->getTileAt($this->pairX, $this->position->y, $this->pairZ); if($tile instanceof Chest){ return $tile; } @@ -169,35 +181,29 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ return null; } - /** - * @return bool - */ - public function pairWith(Chest $tile){ + public function pairWith(Chest $tile) : bool{ if($this->isPaired() or $tile->isPaired()){ return false; } $this->createPair($tile); - $this->onChanged(); - $tile->onChanged(); + $this->setDirty(); + $tile->setDirty(); $this->checkPairing(); return true; } private function createPair(Chest $tile) : void{ - $this->pairX = $tile->x; - $this->pairZ = $tile->z; + $this->pairX = $tile->getPosition()->x; + $this->pairZ = $tile->getPosition()->z; - $tile->pairX = $this->x; - $tile->pairZ = $this->z; + $tile->pairX = $this->getPosition()->x; + $tile->pairZ = $this->getPosition()->z; } - /** - * @return bool - */ - public function unpair(){ + public function unpair() : bool{ if(!$this->isPaired()){ return false; } @@ -205,12 +211,12 @@ class Chest extends Spawnable implements InventoryHolder, Container, Nameable{ $tile = $this->getPair(); $this->pairX = $this->pairZ = null; - $this->onChanged(); + $this->setDirty(); if($tile instanceof Chest){ $tile->pairX = $tile->pairZ = null; $tile->checkPairing(); - $tile->onChanged(); + $tile->setDirty(); } $this->checkPairing(); diff --git a/src/block/tile/Comparator.php b/src/block/tile/Comparator.php new file mode 100644 index 0000000000..317a682f48 --- /dev/null +++ b/src/block/tile/Comparator.php @@ -0,0 +1,54 @@ +signalStrength; + } + + public function setSignalStrength(int $signalStrength) : void{ + $this->signalStrength = $signalStrength; + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->signalStrength = $nbt->getInt(self::TAG_OUTPUT_SIGNAL, 0); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setInt(self::TAG_OUTPUT_SIGNAL, $this->signalStrength); + } +} diff --git a/src/pocketmine/tile/Container.php b/src/block/tile/Container.php similarity index 89% rename from src/pocketmine/tile/Container.php rename to src/block/tile/Container.php index 7fc9949b63..b549ad137f 100644 --- a/src/pocketmine/tile/Container.php +++ b/src/block/tile/Container.php @@ -21,19 +21,15 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\inventory\Inventory; +use pocketmine\inventory\InventoryHolder; -interface Container{ +interface Container extends InventoryHolder{ public const TAG_ITEMS = "Items"; public const TAG_LOCK = "Lock"; - /** - * @return Inventory - */ - public function getInventory(); - /** * @return Inventory */ diff --git a/src/pocketmine/tile/ContainerTrait.php b/src/block/tile/ContainerTrait.php similarity index 65% rename from src/pocketmine/tile/ContainerTrait.php rename to src/block/tile/ContainerTrait.php index ecfc5d0895..ae8cc8ea77 100644 --- a/src/pocketmine/tile/ContainerTrait.php +++ b/src/block/tile/ContainerTrait.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\inventory\Inventory; use pocketmine\item\Item; @@ -29,13 +29,14 @@ use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; +use pocketmine\world\Position; /** * This trait implements most methods in the {@link Container} interface. It should only be used by Tiles. */ trait ContainerTrait{ /** @var string|null */ - private $lock; + private $lock = null; /** * @return Inventory @@ -43,18 +44,20 @@ trait ContainerTrait{ abstract public function getRealInventory(); protected function loadItems(CompoundTag $tag) : void{ - if($tag->hasTag(Container::TAG_ITEMS, ListTag::class)){ - $inventoryTag = $tag->getListTag(Container::TAG_ITEMS); - + if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag){ $inventory = $this->getRealInventory(); + $listeners = $inventory->getListeners()->toArray(); + $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization + $inventory->clearAll(); /** @var CompoundTag $itemNBT */ foreach($inventoryTag as $itemNBT){ $inventory->setItem($itemNBT->getByte("Slot"), Item::nbtDeserialize($itemNBT)); } + $inventory->getListeners()->add(...$listeners); } - if($tag->hasTag(Container::TAG_LOCK, StringTag::class)){ - $this->lock = $tag->getString(Container::TAG_LOCK); + if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){ + $this->lock = $lockTag->getValue(); } } @@ -64,7 +67,7 @@ trait ContainerTrait{ $items[] = $item->nbtSerialize($slot); } - $tag->setTag(new ListTag(Container::TAG_ITEMS, $items, NBT::TAG_Compound)); + $tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound)); if($this->lock !== null){ $tag->setString(Container::TAG_LOCK, $this->lock); @@ -77,4 +80,22 @@ trait ContainerTrait{ public function canOpenWith(string $key) : bool{ return $this->lock === null or $this->lock === $key; } + + /** + * @see Position::asPosition() + */ + abstract protected function getPosition() : Position; + + /** + * @see Tile::onBlockDestroyedHook() + */ + protected function onBlockDestroyedHook() : void{ + $inv = $this->getRealInventory(); + $pos = $this->getPosition(); + + foreach($inv->getContents() as $k => $item){ + $pos->getWorld()->dropItem($pos->add(0.5, 0.5, 0.5), $item); + } + $inv->clearAll(); + } } diff --git a/src/pocketmine/network/mcpe/protocol/types/BlockPaletteEntry.php b/src/block/tile/DaylightSensor.php similarity index 56% rename from src/pocketmine/network/mcpe/protocol/types/BlockPaletteEntry.php rename to src/block/tile/DaylightSensor.php index 27c5fb29e0..8776d1ca51 100644 --- a/src/pocketmine/network/mcpe/protocol/types/BlockPaletteEntry.php +++ b/src/block/tile/DaylightSensor.php @@ -21,23 +21,24 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\block\tile; use pocketmine\nbt\tag\CompoundTag; -final class BlockPaletteEntry{ +/** + * @deprecated + * As per the wiki, this is an old hack to force daylight sensors to get updated every game tick. This is necessary to + * ensure that the daylight sensor's power output is always up to date with the current world time. + * It's theoretically possible to implement this without a blockentity, but this is here to ensure that vanilla can + * understand daylight sensors in worlds created by PM. + */ +class DaylightSensor extends Tile{ - /** @var string */ - private $name; - /** @var CompoundTag */ - private $states; + public function readSaveData(CompoundTag $nbt) : void{ - public function __construct(string $name, CompoundTag $states){ - $this->name = $name; - $this->states = $states; } - public function getName() : string{ return $this->name; } + protected function writeSaveData(CompoundTag $nbt) : void{ - public function getStates() : CompoundTag{ return $this->states; } + } } diff --git a/src/pocketmine/tile/EnchantTable.php b/src/block/tile/EnchantTable.php similarity index 93% rename from src/pocketmine/tile/EnchantTable.php rename to src/block/tile/EnchantTable.php index 45a6210798..8e56166bef 100644 --- a/src/pocketmine/tile/EnchantTable.php +++ b/src/block/tile/EnchantTable.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; class EnchantTable extends Spawnable implements Nameable{ use NameableTrait { - loadName as readSaveData; + loadName as public readSaveData; saveName as writeSaveData; } diff --git a/src/pocketmine/tile/EnderChest.php b/src/block/tile/EnderChest.php similarity index 70% rename from src/pocketmine/tile/EnderChest.php rename to src/block/tile/EnderChest.php index 5faa2b2929..30bedd9368 100644 --- a/src/pocketmine/tile/EnderChest.php +++ b/src/block/tile/EnderChest.php @@ -21,13 +21,26 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\nbt\tag\CompoundTag; class EnderChest extends Spawnable{ - protected function readSaveData(CompoundTag $nbt) : void{ + protected int $viewerCount = 0; + + public function getViewerCount() : int{ + return $this->viewerCount; + } + + public function setViewerCount(int $viewerCount) : void{ + if($viewerCount < 0){ + throw new \InvalidArgumentException('Viewer count cannot be negative'); + } + $this->viewerCount = $viewerCount; + } + + public function readSaveData(CompoundTag $nbt) : void{ } diff --git a/src/block/tile/FlowerPot.php b/src/block/tile/FlowerPot.php new file mode 100644 index 0000000000..f44e73e37c --- /dev/null +++ b/src/block/tile/FlowerPot.php @@ -0,0 +1,81 @@ +getTag(self::TAG_ITEM)) instanceof ShortTag and ($itemMetaTag = $nbt->getTag(self::TAG_ITEM_DATA)) instanceof IntTag){ + try{ + $this->setPlant(BlockFactory::getInstance()->get($itemIdTag->getValue(), $itemMetaTag->getValue())); + }catch(\InvalidArgumentException $e){ + //noop + } + }else{ + //TODO: new PlantBlock tag + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + if($this->plant !== null){ + $nbt->setShort(self::TAG_ITEM, $this->plant->getId()); + $nbt->setInt(self::TAG_ITEM_DATA, $this->plant->getMeta()); + } + } + + public function getPlant() : ?Block{ + return $this->plant !== null ? clone $this->plant : null; + } + + public function setPlant(?Block $plant) : void{ + if($plant === null or $plant instanceof Air){ + $this->plant = null; + }else{ + $this->plant = clone $plant; + } + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + if($this->plant !== null){ + $nbt->setShort(self::TAG_ITEM, $this->plant->getId()); + $nbt->setInt(self::TAG_ITEM_DATA, $this->plant->getMeta()); + } + } +} diff --git a/src/block/tile/Furnace.php b/src/block/tile/Furnace.php new file mode 100644 index 0000000000..b527317d6d --- /dev/null +++ b/src/block/tile/Furnace.php @@ -0,0 +1,235 @@ +inventory = new FurnaceInventory($this->position, $this->getFurnaceType()); + $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange( + static function(Inventory $unused) use ($world, $pos) : void{ + $world->scheduleDelayedBlockUpdate($pos, 1); + }) + ); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->remainingFuelTime = max(0, $nbt->getShort(self::TAG_BURN_TIME, $this->remainingFuelTime)); + + $this->cookTime = $nbt->getShort(self::TAG_COOK_TIME, $this->cookTime); + if($this->remainingFuelTime === 0){ + $this->cookTime = 0; + } + + $this->maxFuelTime = $nbt->getShort(self::TAG_MAX_TIME, $this->maxFuelTime); + if($this->maxFuelTime === 0){ + $this->maxFuelTime = $this->remainingFuelTime; + } + + $this->loadName($nbt); + $this->loadItems($nbt); + + if($this->remainingFuelTime > 0){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setShort(self::TAG_BURN_TIME, $this->remainingFuelTime); + $nbt->setShort(self::TAG_COOK_TIME, $this->cookTime); + $nbt->setShort(self::TAG_MAX_TIME, $this->maxFuelTime); + $this->saveName($nbt); + $this->saveItems($nbt); + } + + public function getDefaultName() : string{ + return "Furnace"; + } + + public function close() : void{ + if(!$this->closed){ + $this->inventory->removeAllViewers(); + + parent::close(); + } + } + + /** + * @return FurnaceInventory + */ + public function getInventory(){ + return $this->inventory; + } + + /** + * @return FurnaceInventory + */ + public function getRealInventory(){ + return $this->getInventory(); + } + + protected function checkFuel(Item $fuel) : void{ + $ev = new FurnaceBurnEvent($this, $fuel, $fuel->getFuelTime()); + $ev->call(); + if($ev->isCancelled()){ + return; + } + + $this->maxFuelTime = $this->remainingFuelTime = $ev->getBurnTime(); + $this->onStartSmelting(); + + if($this->remainingFuelTime > 0 and $ev->isBurning()){ + $this->inventory->setFuel($fuel->getFuelResidue()); + } + } + + protected function onStartSmelting() : void{ + $block = $this->getBlock(); + if($block instanceof BlockFurnace and !$block->isLit()){ + $block->setLit(true); + $this->position->getWorld()->setBlock($block->getPosition(), $block); + } + } + + protected function onStopSmelting() : void{ + $block = $this->getBlock(); + if($block instanceof BlockFurnace and $block->isLit()){ + $block->setLit(false); + $this->position->getWorld()->setBlock($block->getPosition(), $block); + } + } + + abstract public function getFurnaceType() : FurnaceType; + + public function onUpdate() : bool{ + //TODO: move this to Block + if($this->closed){ + return false; + } + + $this->timings->startTiming(); + + $prevCookTime = $this->cookTime; + $prevRemainingFuelTime = $this->remainingFuelTime; + $prevMaxFuelTime = $this->maxFuelTime; + + $ret = false; + + $fuel = $this->inventory->getFuel(); + $raw = $this->inventory->getSmelting(); + $product = $this->inventory->getResult(); + + $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())); + + if($this->remainingFuelTime <= 0 and $canSmelt and $fuel->getFuelTime() > 0 and $fuel->getCount() > 0){ + $this->checkFuel($fuel); + } + + if($this->remainingFuelTime > 0){ + --$this->remainingFuelTime; + + if($smelt instanceof FurnaceRecipe and $canSmelt){ + ++$this->cookTime; + + if($this->cookTime >= $furnaceType->getCookDurationTicks()){ + $product = $smelt->getResult()->setCount($product->getCount() + 1); + + $ev = new FurnaceSmeltEvent($this, $raw, $product); + $ev->call(); + + if(!$ev->isCancelled()){ + $this->inventory->setResult($ev->getResult()); + $raw->pop(); + $this->inventory->setSmelting($raw); + } + + $this->cookTime -= $furnaceType->getCookDurationTicks(); + } + }elseif($this->remainingFuelTime <= 0){ + $this->remainingFuelTime = $this->cookTime = $this->maxFuelTime = 0; + }else{ + $this->cookTime = 0; + } + $ret = true; + }else{ + $this->onStopSmelting(); + $this->remainingFuelTime = $this->cookTime = $this->maxFuelTime = 0; + } + + $viewers = array_map(fn(Player $p) => $p->getNetworkSession()->getInvManager(), $this->inventory->getViewers()); + foreach($viewers as $v){ + if($v === null){ + continue; + } + if($prevCookTime !== $this->cookTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_FURNACE_SMELT_PROGRESS, $this->cookTime); + } + if($prevRemainingFuelTime !== $this->remainingFuelTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_FURNACE_REMAINING_FUEL_TIME, $this->remainingFuelTime); + } + if($prevMaxFuelTime !== $this->maxFuelTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_FURNACE_MAX_FUEL_TIME, $this->maxFuelTime); + } + } + + $this->timings->stopTiming(); + + return $ret; + } +} diff --git a/src/block/tile/Hopper.php b/src/block/tile/Hopper.php new file mode 100644 index 0000000000..9ebb429ef2 --- /dev/null +++ b/src/block/tile/Hopper.php @@ -0,0 +1,88 @@ +inventory = new HopperInventory($this->position); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->loadItems($nbt); + $this->loadName($nbt); + + $this->transferCooldown = $nbt->getInt(self::TAG_TRANSFER_COOLDOWN, 0); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $this->saveItems($nbt); + $this->saveName($nbt); + + $nbt->setInt(self::TAG_TRANSFER_COOLDOWN, $this->transferCooldown); + } + + public function close() : void{ + if(!$this->closed){ + $this->inventory->removeAllViewers(); + + parent::close(); + } + } + + public function getDefaultName() : string{ + return "Hopper"; + } + + /** + * @return HopperInventory + */ + public function getInventory(){ + return $this->inventory; + } + + /** + * @return HopperInventory + */ + public function getRealInventory(){ + return $this->inventory; + } +} diff --git a/src/pocketmine/tile/ItemFrame.php b/src/block/tile/ItemFrame.php similarity index 71% rename from src/pocketmine/tile/ItemFrame.php rename to src/block/tile/ItemFrame.php index faafe1dd29..32d55f2010 100644 --- a/src/pocketmine/tile/ItemFrame.php +++ b/src/block/tile/ItemFrame.php @@ -21,12 +21,18 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\item\Item; use pocketmine\item\ItemFactory; +use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\world\World; +/** + * @deprecated + * @see \pocketmine\block\ItemFrame + */ class ItemFrame extends Spawnable{ public const TAG_ITEM_ROTATION = "ItemRotation"; public const TAG_ITEM_DROP_CHANCE = "ItemDropChance"; @@ -35,24 +41,27 @@ class ItemFrame extends Spawnable{ /** @var Item */ private $item; /** @var int */ - private $itemRotation; + private $itemRotation = 0; /** @var float */ - private $itemDropChance; + private $itemDropChance = 1.0; - protected function readSaveData(CompoundTag $nbt) : void{ + public function __construct(World $world, Vector3 $pos){ + $this->item = ItemFactory::air(); + parent::__construct($world, $pos); + } + + public function readSaveData(CompoundTag $nbt) : void{ if(($itemTag = $nbt->getCompoundTag(self::TAG_ITEM)) !== null){ $this->item = Item::nbtDeserialize($itemTag); - }else{ - $this->item = ItemFactory::get(Item::AIR, 0, 0); } - $this->itemRotation = $nbt->getByte(self::TAG_ITEM_ROTATION, 0, true); - $this->itemDropChance = $nbt->getFloat(self::TAG_ITEM_DROP_CHANCE, 1.0, true); + $this->itemRotation = $nbt->getByte(self::TAG_ITEM_ROTATION, $this->itemRotation); + $this->itemDropChance = $nbt->getFloat(self::TAG_ITEM_DROP_CHANCE, $this->itemDropChance); } protected function writeSaveData(CompoundTag $nbt) : void{ $nbt->setFloat(self::TAG_ITEM_DROP_CHANCE, $this->itemDropChance); $nbt->setByte(self::TAG_ITEM_ROTATION, $this->itemRotation); - $nbt->setTag($this->item->nbtSerialize(-1, self::TAG_ITEM)); + $nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize()); } public function hasItem() : bool{ @@ -63,45 +72,33 @@ class ItemFrame extends Spawnable{ return clone $this->item; } - /** - * @return void - */ - public function setItem(Item $item = null){ + public function setItem(?Item $item) : void{ if($item !== null and !$item->isNull()){ $this->item = clone $item; }else{ - $this->item = ItemFactory::get(Item::AIR, 0, 0); + $this->item = ItemFactory::air(); } - $this->onChanged(); } public function getItemRotation() : int{ return $this->itemRotation; } - /** - * @return void - */ - public function setItemRotation(int $rotation){ + public function setItemRotation(int $rotation) : void{ $this->itemRotation = $rotation; - $this->onChanged(); } public function getItemDropChance() : float{ return $this->itemDropChance; } - /** - * @return void - */ - public function setItemDropChance(float $chance){ + public function setItemDropChance(float $chance) : void{ $this->itemDropChance = $chance; - $this->onChanged(); } protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ $nbt->setFloat(self::TAG_ITEM_DROP_CHANCE, $this->itemDropChance); $nbt->setByte(self::TAG_ITEM_ROTATION, $this->itemRotation); - $nbt->setTag($this->item->nbtSerialize(-1, self::TAG_ITEM)); + $nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize()); } } diff --git a/src/pocketmine/tile/Bed.php b/src/block/tile/Jukebox.php similarity index 53% rename from src/pocketmine/tile/Bed.php rename to src/block/tile/Jukebox.php index e70784892d..35242ed8d5 100644 --- a/src/pocketmine/tile/Bed.php +++ b/src/block/tile/Jukebox.php @@ -21,45 +21,45 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\item\Item; -use pocketmine\math\Vector3; +use pocketmine\item\Record; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\Player; -class Bed extends Spawnable{ - public const TAG_COLOR = "color"; - /** @var int */ - private $color = 14; //default to old red +class Jukebox extends Spawnable{ + private const TAG_RECORD = "RecordItem"; //Item CompoundTag - public function getColor() : int{ - return $this->color; + /** @var Record|null */ + private $record = null; + + public function getRecord() : ?Record{ + return $this->record; } - /** - * @return void - */ - public function setColor(int $color){ - $this->color = $color & 0xf; - $this->onChanged(); + public function setRecord(?Record $record) : void{ + $this->record = $record; } - protected function readSaveData(CompoundTag $nbt) : void{ - $this->color = $nbt->getByte(self::TAG_COLOR, 14, true); + public function readSaveData(CompoundTag $nbt) : void{ + if(($tag = $nbt->getCompoundTag(self::TAG_RECORD)) !== null){ + $record = Item::nbtDeserialize($tag); + if($record instanceof Record){ + $this->record = $record; + } + } } protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setByte(self::TAG_COLOR, $this->color); + if($this->record !== null){ + $nbt->setTag(self::TAG_RECORD, $this->record->nbtSerialize()); + } } protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setByte(self::TAG_COLOR, $this->color); - } - - protected static function createAdditionalNBT(CompoundTag $nbt, Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : void{ - if($item !== null){ - $nbt->setByte(self::TAG_COLOR, $item->getDamage()); + //this is needed for the note particles to show on the client side + if($this->record !== null){ + $nbt->setTag(self::TAG_RECORD, $this->record->nbtSerialize()); } } } diff --git a/src/block/tile/MonsterSpawner.php b/src/block/tile/MonsterSpawner.php new file mode 100644 index 0000000000..8b6d0eeca1 --- /dev/null +++ b/src/block/tile/MonsterSpawner.php @@ -0,0 +1,152 @@ + + private const TAG_SPAWN_DATA = "SpawnData"; //TAG_Compound + private const TAG_MIN_SPAWN_DELAY = "MinSpawnDelay"; //TAG_Short + private const TAG_MAX_SPAWN_DELAY = "MaxSpawnDelay"; //TAG_Short + private const TAG_SPAWN_PER_ATTEMPT = "SpawnCount"; //TAG_Short + private const TAG_MAX_NEARBY_ENTITIES = "MaxNearbyEntities"; //TAG_Short + private const TAG_REQUIRED_PLAYER_RANGE = "RequiredPlayerRange"; //TAG_Short + private const TAG_SPAWN_RANGE = "SpawnRange"; //TAG_Short + private const TAG_ENTITY_WIDTH = "DisplayEntityWidth"; //TAG_Float + private const TAG_ENTITY_HEIGHT = "DisplayEntityHeight"; //TAG_Float + private const TAG_ENTITY_SCALE = "DisplayEntityScale"; //TAG_Float + + public const DEFAULT_MIN_SPAWN_DELAY = 200; //ticks + public const DEFAULT_MAX_SPAWN_DELAY = 800; + + public const DEFAULT_MAX_NEARBY_ENTITIES = 6; + public const DEFAULT_SPAWN_RANGE = 4; //blocks + public const DEFAULT_REQUIRED_PLAYER_RANGE = 16; + + /** + * @var string + * TODO: replace this with a cached entity or something of that nature + */ + private $entityTypeId = ":"; + /** + * @var ListTag|null + * TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) + */ + private $spawnPotentials = null; + /** + * @var CompoundTag|null + * TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) + */ + private $spawnData = null; + + /** @var float */ + private $displayEntityWidth = 1; + /** @var float */ + private $displayEntityHeight = 1; + /** @var float */ + private $displayEntityScale = 1; + + /** @var int */ + private $spawnDelay = self::DEFAULT_MIN_SPAWN_DELAY; + /** @var int */ + private $minSpawnDelay = self::DEFAULT_MIN_SPAWN_DELAY; + /** @var int */ + private $maxSpawnDelay = self::DEFAULT_MAX_SPAWN_DELAY; + /** @var int */ + private $spawnPerAttempt = 1; + /** @var int */ + private $maxNearbyEntities = self::DEFAULT_MAX_NEARBY_ENTITIES; + /** @var int */ + private $spawnRange = self::DEFAULT_SPAWN_RANGE; + /** @var int */ + private $requiredPlayerRange = self::DEFAULT_REQUIRED_PLAYER_RANGE; + + public function readSaveData(CompoundTag $nbt) : void{ + if(($legacyIdTag = $nbt->getTag(self::TAG_LEGACY_ENTITY_TYPE_ID)) instanceof IntTag){ + //TODO: this will cause unexpected results when there's no mapping for the entity + $this->entityTypeId = LegacyEntityIdToStringIdMap::getInstance()->legacyToString($legacyIdTag->getValue()) ?? ":"; + }elseif(($idTag = $nbt->getTag(self::TAG_ENTITY_TYPE_ID)) instanceof StringTag){ + $this->entityTypeId = $idTag->getValue(); + }else{ + $this->entityTypeId = ":"; //default - TODO: replace this with a constant + } + + $this->spawnData = $nbt->getCompoundTag(self::TAG_SPAWN_DATA); + $this->spawnPotentials = $nbt->getListTag(self::TAG_SPAWN_POTENTIALS); + + $this->spawnDelay = $nbt->getShort(self::TAG_SPAWN_DELAY, self::DEFAULT_MIN_SPAWN_DELAY); + $this->minSpawnDelay = $nbt->getShort(self::TAG_MIN_SPAWN_DELAY, self::DEFAULT_MIN_SPAWN_DELAY); + $this->maxSpawnDelay = $nbt->getShort(self::TAG_MAX_SPAWN_DELAY, self::DEFAULT_MAX_SPAWN_DELAY); + $this->spawnPerAttempt = $nbt->getShort(self::TAG_SPAWN_PER_ATTEMPT, 1); + $this->maxNearbyEntities = $nbt->getShort(self::TAG_MAX_NEARBY_ENTITIES, self::DEFAULT_MAX_NEARBY_ENTITIES); + $this->requiredPlayerRange = $nbt->getShort(self::TAG_REQUIRED_PLAYER_RANGE, self::DEFAULT_REQUIRED_PLAYER_RANGE); + $this->spawnRange = $nbt->getShort(self::TAG_SPAWN_RANGE, self::DEFAULT_SPAWN_RANGE); + + $this->displayEntityWidth = $nbt->getFloat(self::TAG_ENTITY_WIDTH, 1.0); + $this->displayEntityHeight = $nbt->getFloat(self::TAG_ENTITY_HEIGHT, 1.0); + $this->displayEntityScale = $nbt->getFloat(self::TAG_ENTITY_SCALE, 1.0); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setString(self::TAG_ENTITY_TYPE_ID, $this->entityTypeId); + if($this->spawnData !== null){ + $nbt->setTag(self::TAG_SPAWN_DATA, clone $this->spawnData); + } + if($this->spawnPotentials !== null){ + $nbt->setTag(self::TAG_SPAWN_POTENTIALS, clone $this->spawnPotentials); + } + + $nbt->setShort(self::TAG_SPAWN_DELAY, $this->spawnDelay); + $nbt->setShort(self::TAG_MIN_SPAWN_DELAY, $this->minSpawnDelay); + $nbt->setShort(self::TAG_MAX_SPAWN_DELAY, $this->maxSpawnDelay); + $nbt->setShort(self::TAG_SPAWN_PER_ATTEMPT, $this->spawnPerAttempt); + $nbt->setShort(self::TAG_MAX_NEARBY_ENTITIES, $this->maxNearbyEntities); + $nbt->setShort(self::TAG_REQUIRED_PLAYER_RANGE, $this->requiredPlayerRange); + $nbt->setShort(self::TAG_SPAWN_RANGE, $this->spawnRange); + + $nbt->setFloat(self::TAG_ENTITY_WIDTH, $this->displayEntityWidth); + $nbt->setFloat(self::TAG_ENTITY_HEIGHT, $this->displayEntityHeight); + $nbt->setFloat(self::TAG_ENTITY_SCALE, $this->displayEntityScale); + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setString(self::TAG_ENTITY_TYPE_ID, $this->entityTypeId); + + //TODO: we can't set SpawnData here because it might crash the client if it's from a PC world (we need to implement full deserialization) + + $nbt->setFloat(self::TAG_ENTITY_SCALE, $this->displayEntityScale); + } +} diff --git a/src/pocketmine/tile/Nameable.php b/src/block/tile/Nameable.php similarity index 90% rename from src/pocketmine/tile/Nameable.php rename to src/block/tile/Nameable.php index aedb7c699b..2d206ffc2b 100644 --- a/src/pocketmine/tile/Nameable.php +++ b/src/block/tile/Nameable.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; interface Nameable{ public const TAG_CUSTOM_NAME = "CustomName"; @@ -30,10 +30,7 @@ interface Nameable{ public function getName() : string; - /** - * @return void - */ - public function setName(string $str); + public function setName(string $str) : void; public function hasName() : bool; } diff --git a/src/pocketmine/tile/NameableTrait.php b/src/block/tile/NameableTrait.php similarity index 77% rename from src/pocketmine/tile/NameableTrait.php rename to src/block/tile/NameableTrait.php index acd2454a4f..4a43e8aef3 100644 --- a/src/pocketmine/tile/NameableTrait.php +++ b/src/block/tile/NameableTrait.php @@ -21,20 +21,18 @@ declare(strict_types=1); -namespace pocketmine\tile; +namespace pocketmine\block\tile; use pocketmine\item\Item; -use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\StringTag; -use pocketmine\Player; /** * This trait implements most methods in the {@link Nameable} interface. It should only be used by Tiles. */ trait NameableTrait{ /** @var string|null */ - private $customName; + private $customName = null; abstract public function getDefaultName() : string; @@ -54,12 +52,6 @@ trait NameableTrait{ return $this->customName !== null; } - protected static function createAdditionalNBT(CompoundTag $nbt, Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : void{ - if($item !== null and $item->hasCustomName()){ - $nbt->setString(Nameable::TAG_CUSTOM_NAME, $item->getCustomName()); - } - } - public function addAdditionalSpawnData(CompoundTag $nbt) : void{ if($this->customName !== null){ $nbt->setString(Nameable::TAG_CUSTOM_NAME, $this->customName); @@ -67,8 +59,8 @@ trait NameableTrait{ } protected function loadName(CompoundTag $tag) : void{ - if($tag->hasTag(Nameable::TAG_CUSTOM_NAME, StringTag::class)){ - $this->customName = $tag->getString(Nameable::TAG_CUSTOM_NAME); + if(($customNameTag = $tag->getTag(Nameable::TAG_CUSTOM_NAME)) instanceof StringTag){ + $this->customName = $customNameTag->getValue(); } } @@ -77,4 +69,14 @@ trait NameableTrait{ $tag->setString(Nameable::TAG_CUSTOM_NAME, $this->customName); } } + + /** + * @see Tile::copyDataFromItem() + */ + public function copyDataFromItem(Item $item) : void{ + parent::copyDataFromItem($item); + if($item->hasCustomName()){ //this should take precedence over saved NBT + $this->setName($item->getCustomName()); + } + } } diff --git a/src/pocketmine/block/DoubleStoneSlab2.php b/src/block/tile/NormalFurnace.php similarity index 79% rename from src/pocketmine/block/DoubleStoneSlab2.php rename to src/block/tile/NormalFurnace.php index 49fe47ad7c..c5c17e5e5f 100644 --- a/src/pocketmine/block/DoubleStoneSlab2.php +++ b/src/block/tile/NormalFurnace.php @@ -21,13 +21,12 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\block\tile; -class DoubleStoneSlab2 extends DoubleStoneSlab{ +use pocketmine\crafting\FurnaceType; - protected $id = self::DOUBLE_STONE_SLAB2; - - public function getSlabId() : int{ - return self::STONE_SLAB2; +class NormalFurnace extends Furnace{ + public function getFurnaceType() : FurnaceType{ + return FurnaceType::FURNACE(); } } diff --git a/src/block/tile/Note.php b/src/block/tile/Note.php new file mode 100644 index 0000000000..5157f74366 --- /dev/null +++ b/src/block/tile/Note.php @@ -0,0 +1,56 @@ +getByte("note", $this->pitch)) > BlockNote::MIN_PITCH and $pitch <= BlockNote::MAX_PITCH){ + $this->pitch = $pitch; + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setByte("note", $this->pitch); + } + + public function getPitch() : int{ + return $this->pitch; + } + + public function setPitch(int $pitch) : void{ + if($pitch < BlockNote::MIN_PITCH or $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/ShulkerBox.php b/src/block/tile/ShulkerBox.php new file mode 100644 index 0000000000..fb740c5663 --- /dev/null +++ b/src/block/tile/ShulkerBox.php @@ -0,0 +1,120 @@ +inventory = new ShulkerBoxInventory($this->position); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->loadName($nbt); + $this->loadItems($nbt); + $this->facing = $nbt->getByte(self::TAG_FACING, $this->facing); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $this->saveName($nbt); + $this->saveItems($nbt); + $nbt->setByte(self::TAG_FACING, $this->facing); + } + + public function copyDataFromItem(Item $item) : void{ + $this->readSaveData($item->getNamedTag()); + if($item->hasCustomName()){ + $this->setName($item->getCustomName()); + } + } + + public function close() : void{ + if(!$this->closed){ + $this->inventory->removeAllViewers(); + parent::close(); + } + } + + protected function onBlockDestroyedHook() : void{ + //NOOP override of ContainerTrait - shulker boxes retain their contents when destroyed + } + + public function getCleanedNBT() : ?CompoundTag{ + $nbt = parent::getCleanedNBT(); + if($nbt !== null){ + $nbt->removeTag(self::TAG_FACING); + } + return $nbt; + } + + public function getFacing() : int{ + return $this->facing; + } + + public function setFacing(int $facing) : void{ + $this->facing = $facing; + } + + /** + * @return ShulkerBoxInventory + */ + public function getInventory(){ + return $this->inventory; + } + + /** + * @return ShulkerBoxInventory + */ + public function getRealInventory(){ + return $this->inventory; + } + + public function getDefaultName() : string{ + return "Shulker Box"; + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_FACING, $this->facing); + $this->addNameSpawnData($nbt); + } +} diff --git a/src/block/tile/Sign.php b/src/block/tile/Sign.php new file mode 100644 index 0000000000..38cad085ff --- /dev/null +++ b/src/block/tile/Sign.php @@ -0,0 +1,115 @@ +text = new SignText(); + parent::__construct($world, $pos); + } + + public function readSaveData(CompoundTag $nbt) : void{ + if(($textBlobTag = $nbt->getTag(self::TAG_TEXT_BLOB)) instanceof StringTag){ //MCPE 1.2 save format + $this->text = SignText::fromBlob(mb_scrub($textBlobTag->getValue(), 'UTF-8')); + }else{ + $text = []; + for($i = 0; $i < SignText::LINE_COUNT; ++$i){ + $textKey = sprintf(self::TAG_TEXT_LINE, $i + 1); + if(($lineTag = $nbt->getTag($textKey)) instanceof StringTag){ + $text[$i] = mb_scrub($lineTag->getValue(), 'UTF-8'); + } + } + $this->text = new SignText($text); + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines())); + + for($i = 0; $i < SignText::LINE_COUNT; ++$i){ //Backwards-compatibility + $textKey = sprintf(self::TAG_TEXT_LINE, $i + 1); + $nbt->setString($textKey, $this->text->getLine($i)); + } + } + + public function getText() : SignText{ + return $this->text; + } + + public function setText(SignText $text) : void{ + $this->text = $text; + } + + /** + * Returns the entity runtime ID of the player who placed this sign. Only the player whose entity ID matches this + * one may edit the sign text. + * This is needed because as of 1.16.220, there is still no reliable way to detect when the MCPE client closed the + * sign edit GUI, so we have no way to know when the text is finalized. This limits editing of the text to only the + * player who placed it, and only while that player is online. + * We can say for sure that the sign is finalized if either of the following occurs: + * - The player quits (after rejoin, the player's entity runtimeID will be different). + * - The chunk is unloaded (on next load, the entity runtimeID will be null, because it's not saved). + */ + public function getEditorEntityRuntimeId() : ?int{ return $this->editorEntityRuntimeId; } + + public function setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : void{ + $this->editorEntityRuntimeId = $editorEntityRuntimeId; + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines())); + } +} diff --git a/src/block/tile/Skull.php b/src/block/tile/Skull.php new file mode 100644 index 0000000000..4db9ebd930 --- /dev/null +++ b/src/block/tile/Skull.php @@ -0,0 +1,92 @@ +skullType = SkullType::SKELETON(); + parent::__construct($world, $pos); + } + + public function readSaveData(CompoundTag $nbt) : void{ + if(($skullTypeTag = $nbt->getTag(self::TAG_SKULL_TYPE)) instanceof ByteTag){ + try{ + $this->skullType = SkullType::fromMagicNumber($skullTypeTag->getValue()); + }catch(\InvalidArgumentException $e){ + //bad data, drop it + } + } + $rotation = $nbt->getByte(self::TAG_ROT, 0); + if($rotation >= 0 and $rotation <= 15){ + $this->skullRotation = $rotation; + } + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_SKULL_TYPE, $this->skullType->getMagicNumber()); + $nbt->setByte(self::TAG_ROT, $this->skullRotation); + } + + public function setSkullType(SkullType $type) : void{ + $this->skullType = $type; + } + + public function getSkullType() : SkullType{ + return $this->skullType; + } + + public function getRotation() : int{ + return $this->skullRotation; + } + + public function setRotation(int $rotation) : void{ + $this->skullRotation = $rotation; + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_SKULL_TYPE, $this->skullType->getMagicNumber()); + $nbt->setByte(self::TAG_ROT, $this->skullRotation); + } +} diff --git a/src/block/tile/Smoker.php b/src/block/tile/Smoker.php new file mode 100644 index 0000000000..7edb5b49e1 --- /dev/null +++ b/src/block/tile/Smoker.php @@ -0,0 +1,32 @@ +|null + */ + private $spawnCompoundCache = null; + /** @var bool */ + private $dirty = true; //default dirty, until it's been spawned appropriately on the world + + /** + * Returns whether the tile needs to be respawned to viewers. + */ + public function isDirty() : bool{ + return $this->dirty; + } + + public function setDirty(bool $dirty = true) : void{ + if($dirty){ + $this->spawnCompoundCache = null; + } + $this->dirty = $dirty; + } + + /** + * Returns encoded NBT (varint, little-endian) used to spawn this tile to clients. Uses cache where possible, + * populates cache if it is null. + * + * @phpstan-return CacheableNbt<\pocketmine\nbt\tag\CompoundTag> + */ + final public function getSerializedSpawnCompound() : CacheableNbt{ + if($this->spawnCompoundCache === null){ + $this->spawnCompoundCache = new CacheableNbt($this->getSpawnCompound()); + } + + return $this->spawnCompoundCache; + } + + final public function getSpawnCompound() : CompoundTag{ + $nbt = CompoundTag::create() + ->setString(self::TAG_ID, TileFactory::getInstance()->getSaveId(get_class($this))) //TODO: disassociate network ID from save ID + ->setInt(self::TAG_X, $this->position->x) + ->setInt(self::TAG_Y, $this->position->y) + ->setInt(self::TAG_Z, $this->position->z); + $this->addAdditionalSpawnData($nbt); + return $nbt; + } + + /** + * An extension to getSpawnCompound() for + * further modifying the generic tile NBT. + */ + abstract protected function addAdditionalSpawnData(CompoundTag $nbt) : void; +} diff --git a/src/block/tile/Tile.php b/src/block/tile/Tile.php new file mode 100644 index 0000000000..00ff1bed64 --- /dev/null +++ b/src/block/tile/Tile.php @@ -0,0 +1,139 @@ +position = Position::fromObject($pos, $world); + $this->timings = Timings::getTileEntityTimings($this); + } + + /** + * @internal + * @throws NbtDataException + * Reads additional data from the CompoundTag on tile creation. + */ + abstract public function readSaveData(CompoundTag $nbt) : void; + + /** + * Writes additional save data to a CompoundTag, not including generic things like ID and coordinates. + */ + abstract protected function writeSaveData(CompoundTag $nbt) : void; + + public function saveNBT() : CompoundTag{ + $nbt = CompoundTag::create() + ->setString(self::TAG_ID, TileFactory::getInstance()->getSaveId(get_class($this))) + ->setInt(self::TAG_X, $this->position->getFloorX()) + ->setInt(self::TAG_Y, $this->position->getFloorY()) + ->setInt(self::TAG_Z, $this->position->getFloorZ()); + $this->writeSaveData($nbt); + + return $nbt; + } + + public function getCleanedNBT() : ?CompoundTag{ + $this->writeSaveData($tag = new CompoundTag()); + return $tag->getCount() > 0 ? $tag : null; + } + + /** + * @internal + * + * @throws \RuntimeException + */ + public function copyDataFromItem(Item $item) : void{ + if(($blockNbt = $item->getCustomBlockData()) !== null){ //TODO: check item root tag (MCPE doesn't use BlockEntityTag) + $this->readSaveData($blockNbt); + } + } + + public function getBlock() : Block{ + return $this->position->getWorld()->getBlock($this->position); + } + + public function getPosition() : Position{ + return $this->position; + } + + public function isClosed() : bool{ + return $this->closed; + } + + public function __destruct(){ + $this->close(); + } + + /** + * Called when the tile's block is destroyed. + */ + final public function onBlockDestroyed() : void{ + $this->onBlockDestroyedHook(); + $this->close(); + } + + /** + * Override this method to do actions you need to do when this tile is destroyed due to block being broken. + */ + protected function onBlockDestroyedHook() : void{ + + } + + public function close() : void{ + if(!$this->closed){ + $this->closed = true; + + if($this->position->isValid()){ + $this->position->getWorld()->removeTile($this); + } + } + } +} diff --git a/src/block/tile/TileFactory.php b/src/block/tile/TileFactory.php new file mode 100644 index 0000000000..f8a6db7468 --- /dev/null +++ b/src/block/tile/TileFactory.php @@ -0,0 +1,148 @@ +> + */ + private $knownTiles = []; + /** + * @var string[] + * @phpstan-var array, string> + */ + private $saveNames = []; + + public function __construct(){ + $this->register(Barrel::class, ["Barrel", "minecraft:barrel"]); + $this->register(Banner::class, ["Banner", "minecraft:banner"]); + $this->register(Beacon::class, ["Beacon", "minecraft:beacon"]); + $this->register(Bed::class, ["Bed", "minecraft:bed"]); + $this->register(Bell::class, ["Bell", "minecraft:bell"]); + $this->register(BlastFurnace::class, ["BlastFurnace", "minecraft:blast_furnace"]); + $this->register(BrewingStand::class, ["BrewingStand", "minecraft:brewing_stand"]); + $this->register(Chest::class, ["Chest", "minecraft:chest"]); + $this->register(Comparator::class, ["Comparator", "minecraft:comparator"]); + $this->register(DaylightSensor::class, ["DaylightDetector", "minecraft:daylight_detector"]); + $this->register(EnchantTable::class, ["EnchantTable", "minecraft:enchanting_table"]); + $this->register(EnderChest::class, ["EnderChest", "minecraft:ender_chest"]); + $this->register(FlowerPot::class, ["FlowerPot", "minecraft:flower_pot"]); + $this->register(NormalFurnace::class, ["Furnace", "minecraft:furnace"]); + $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(MonsterSpawner::class, ["MobSpawner", "minecraft:mob_spawner"]); + $this->register(Note::class, ["Music", "minecraft:noteblock"]); + $this->register(ShulkerBox::class, ["ShulkerBox", "minecraft:shulker_box"]); + $this->register(Sign::class, ["Sign", "minecraft:sign"]); + $this->register(Smoker::class, ["Smoker", "minecraft:smoker"]); + $this->register(Skull::class, ["Skull", "minecraft:skull"]); + + //TODO: Campfire + //TODO: Cauldron + //TODO: ChalkboardBlock + //TODO: ChemistryTable + //TODO: CommandBlock + //TODO: Conduit + //TODO: Dispenser + //TODO: Dropper + //TODO: EndGateway + //TODO: EndPortal + //TODO: JigsawBlock + //TODO: Lectern + //TODO: MovingBlock + //TODO: NetherReactor + //TODO: PistonArm + //TODO: StructureBlock + } + + /** + * @param string[] $saveNames + * @phpstan-param class-string $className + */ + public function register(string $className, array $saveNames = []) : void{ + Utils::testValidInstance($className, Tile::class); + + $shortName = (new \ReflectionClass($className))->getShortName(); + if(!in_array($shortName, $saveNames, true)){ + $saveNames[] = $shortName; + } + + foreach($saveNames as $name){ + $this->knownTiles[$name] = $className; + } + + $this->saveNames[$className] = reset($saveNames); + } + + /** + * @internal + * @throws SavedDataLoadingException + */ + public function createFromData(World $world, CompoundTag $nbt) : ?Tile{ + 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); + } + + return $tile; + } + + /** + * @phpstan-param class-string $class + */ + public function getSaveId(string $class) : string{ + if(isset($this->saveNames[$class])){ + return $this->saveNames[$class]; + } + throw new \InvalidArgumentException("Tile $class is not registered"); + } +} diff --git a/src/pocketmine/block/utils/ColorBlockMetaHelper.php b/src/block/utils/AnalogRedstoneSignalEmitterTrait.php similarity index 63% rename from src/pocketmine/block/utils/ColorBlockMetaHelper.php rename to src/block/utils/AnalogRedstoneSignalEmitterTrait.php index 6fe8bf15e8..2d5609a989 100644 --- a/src/pocketmine/block/utils/ColorBlockMetaHelper.php +++ b/src/block/utils/AnalogRedstoneSignalEmitterTrait.php @@ -23,28 +23,17 @@ declare(strict_types=1); namespace pocketmine\block\utils; -class ColorBlockMetaHelper{ +trait AnalogRedstoneSignalEmitterTrait{ + protected int $signalStrength = 0; - public static function getColorFromMeta(int $meta) : string{ - static $names = [ - 0 => "White", - 1 => "Orange", - 2 => "Magenta", - 3 => "Light Blue", - 4 => "Yellow", - 5 => "Lime", - 6 => "Pink", - 7 => "Gray", - 8 => "Light Gray", - 9 => "Cyan", - 10 => "Purple", - 11 => "Blue", - 12 => "Brown", - 13 => "Green", - 14 => "Red", - 15 => "Black" - ]; + public function getOutputSignalStrength() : int{ return $this->signalStrength; } - return $names[$meta] ?? "Unknown"; + /** @return $this */ + public function setOutputSignalStrength(int $signalStrength) : self{ + if($signalStrength < 0 || $signalStrength > 15){ + throw new \InvalidArgumentException("Signal strength must be in range 0-15"); + } + $this->signalStrength = $signalStrength; + return $this; } } diff --git a/src/pocketmine/network/mcpe/protocol/types/MapTrackedObject.php b/src/block/utils/AnyFacingTrait.php similarity index 69% rename from src/pocketmine/network/mcpe/protocol/types/MapTrackedObject.php rename to src/block/utils/AnyFacingTrait.php index 7c9f109541..751e673781 100644 --- a/src/pocketmine/network/mcpe/protocol/types/MapTrackedObject.php +++ b/src/block/utils/AnyFacingTrait.php @@ -21,23 +21,20 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\block\utils; -class MapTrackedObject{ - public const TYPE_ENTITY = 0; - public const TYPE_BLOCK = 1; +use pocketmine\math\Facing; +trait AnyFacingTrait{ /** @var int */ - public $type; + protected $facing = Facing::DOWN; - /** @var int Only set if is TYPE_ENTITY */ - public $entityUniqueId; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; + public function getFacing() : int{ return $this->facing; } + /** @return $this */ + public function setFacing(int $facing) : self{ + Facing::validate($this->facing); + $this->facing = $facing; + return $this; + } } diff --git a/src/pocketmine/block/Stonecutter.php b/src/block/utils/BannerPatternLayer.php similarity index 61% rename from src/pocketmine/block/Stonecutter.php rename to src/block/utils/BannerPatternLayer.php index ce92d37241..8095102fcb 100644 --- a/src/pocketmine/block/Stonecutter.php +++ b/src/block/utils/BannerPatternLayer.php @@ -21,31 +21,27 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\block\utils; -use pocketmine\item\TieredTool; +use pocketmine\block\BaseBanner; -class Stonecutter extends Solid{ +/** + * Contains information about a pattern layer on a banner. + * @see BaseBanner + */ +class BannerPatternLayer{ + private BannerPatternType $type; + /** @var DyeColor */ + private $color; - protected $id = self::STONECUTTER; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function __construct(BannerPatternType $type, DyeColor $color){ + $this->type = $type; + $this->color = $color; } - public function getName() : string{ - return "Stonecutter"; - } + public function getType() : BannerPatternType{ return $this->type; } - public function getHardness() : float{ - return 3.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; + public function getColor() : DyeColor{ + return $this->color; } } diff --git a/src/block/utils/BannerPatternType.php b/src/block/utils/BannerPatternType.php new file mode 100644 index 0000000000..5ede3d92b3 --- /dev/null +++ b/src/block/utils/BannerPatternType.php @@ -0,0 +1,118 @@ + Facing::DOWN, + 1 => Facing::UP, + 2 => Facing::NORTH, + 3 => Facing::SOUTH, + 4 => Facing::WEST, + 5 => Facing::EAST + ][$raw] ?? null; + if($result === null){ + throw new InvalidBlockStateException("Invalid facing $raw"); + } + return $result; + } + + public static function writeFacing(int $facing) : int{ + $result = [ //again, for redundancy + Facing::DOWN => 0, + Facing::UP => 1, + Facing::NORTH => 2, + Facing::SOUTH => 3, + Facing::WEST => 4, + Facing::EAST => 5 + ][$facing] ?? null; + if($result === null){ + throw new \InvalidArgumentException("Invalid facing $facing"); + } + return $result; + } + + /** + * @throws InvalidBlockStateException + */ + public static function readHorizontalFacing(int $facing) : int{ + $facing = self::readFacing($facing); + if(Facing::axis($facing) === Axis::Y){ + throw new InvalidBlockStateException("Invalid Y-axis facing $facing"); + } + return $facing; + } + + public static function writeHorizontalFacing(int $facing) : int{ + if(Facing::axis($facing) === Axis::Y){ + throw new \InvalidArgumentException("Invalid Y-axis facing"); + } + return self::writeFacing($facing); + } + + /** + * @throws InvalidBlockStateException + */ + public static function readLegacyHorizontalFacing(int $raw) : int{ + $result = [ //again, for redundancy + 0 => Facing::SOUTH, + 1 => Facing::WEST, + 2 => Facing::NORTH, + 3 => Facing::EAST + ][$raw] ?? null; + if($result === null){ + throw new InvalidBlockStateException("Invalid legacy facing $raw"); + } + return $result; + } + + public static function writeLegacyHorizontalFacing(int $facing) : int{ + $result = [ + Facing::SOUTH => 0, + Facing::WEST => 1, + Facing::NORTH => 2, + Facing::EAST => 3 + ][$facing] ?? null; + if($result === null){ + throw new \InvalidArgumentException("Invalid Y-axis facing"); + } + return $result; + } + + /** + * @throws InvalidBlockStateException + */ + public static function read5MinusHorizontalFacing(int $value) : int{ + return self::readHorizontalFacing(5 - ($value & 0x03)); + } + + public static function write5MinusHorizontalFacing(int $value) : int{ + return 5 - self::writeHorizontalFacing($value); + } + + public static function readCoralFacing(int $value) : int{ + $result = [ + 0 => Facing::WEST, + 1 => Facing::EAST, + 2 => Facing::NORTH, + 3 => Facing::SOUTH + ][$value] ?? null; + if($result === null){ + throw new InvalidBlockStateException("Invalid coral facing $value"); + } + return $result; + } + + public static function writeCoralFacing(int $value) : int{ + $result = [ + Facing::WEST => 0, + Facing::EAST => 1, + Facing::NORTH => 2, + Facing::SOUTH => 3 + ][$value] ?? null; + if($result === null){ + throw new \InvalidArgumentException("Invalid Y-axis facing $value"); + } + return $result; + } + + public static function readBoundedInt(string $name, int $v, int $min, int $max) : int{ + if($v < $min or $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 new file mode 100644 index 0000000000..c5b14af1bb --- /dev/null +++ b/src/block/utils/BrewingStandSlot.php @@ -0,0 +1,48 @@ +fromId($stateMeta); + if($color === null){ + throw new InvalidBlockStateException("No dye colour corresponds to ID $stateMeta"); + } + $this->color = $color; + } + + /** + * @see Block::writeStateToMeta() + */ + protected function writeStateToMeta() : int{ + return DyeColorIdMap::getInstance()->toId($this->color); + } + + /** + * @see Block::writeStateToItemMeta() + */ + protected function writeStateToItemMeta() : int{ + return DyeColorIdMap::getInstance()->toId($this->color); + } + + /** + * @see Block::getStateBitmask() + */ + public function getStateBitmask() : int{ + return 0b1111; + } +} diff --git a/src/block/utils/ColoredTrait.php b/src/block/utils/ColoredTrait.php new file mode 100644 index 0000000000..922245f9f7 --- /dev/null +++ b/src/block/utils/ColoredTrait.php @@ -0,0 +1,37 @@ +color; } + + /** @return $this */ + public function setColor(DyeColor $color) : self{ + $this->color = $color; + return $this; + } +} diff --git a/src/block/utils/CoralType.php b/src/block/utils/CoralType.php new file mode 100644 index 0000000000..07d94d7ea8 --- /dev/null +++ b/src/block/utils/CoralType.php @@ -0,0 +1,64 @@ +Enum___construct($name); + $this->displayName = $displayName; + } + + public function getDisplayName() : string{ return $this->displayName; } +} diff --git a/src/block/utils/DyeColor.php b/src/block/utils/DyeColor.php new file mode 100644 index 0000000000..91fe73fbfc --- /dev/null +++ b/src/block/utils/DyeColor.php @@ -0,0 +1,96 @@ +Enum___construct($enumName); + $this->displayName = $displayName; + $this->rgbValue = $rgbValue; + } + + public function getDisplayName() : string{ + return $this->displayName; + } + + public function getRgbValue() : Color{ + return $this->rgbValue; + } +} diff --git a/src/block/utils/FacesOppositePlacingPlayerTrait.php b/src/block/utils/FacesOppositePlacingPlayerTrait.php new file mode 100644 index 0000000000..1f566ca304 --- /dev/null +++ b/src/block/utils/FacesOppositePlacingPlayerTrait.php @@ -0,0 +1,45 @@ +facing = Facing::opposite($player->getHorizontalFacing()); + } + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/block/utils/Fallable.php b/src/block/utils/Fallable.php new file mode 100644 index 0000000000..70ac23e687 --- /dev/null +++ b/src/block/utils/Fallable.php @@ -0,0 +1,36 @@ +getPosition(); + $down = $pos->getWorld()->getBlock($pos->getSide(Facing::DOWN)); + if($down->getId() === BlockLegacyIds::AIR or $down instanceof Liquid or $down instanceof Fire){ + $pos->getWorld()->setBlock($pos, VanillaBlocks::AIR()); + + $block = $this; + if(!($block instanceof Block)) throw new AssumptionFailedError(__TRAIT__ . " should only be used by Blocks"); + + $fall = new FallingBlock(Location::fromObject($pos->add(0.5, 0, 0.5), $pos->getWorld()), $block); + $fall->spawnToAll(); + } + } +} diff --git a/src/block/utils/HorizontalFacingTrait.php b/src/block/utils/HorizontalFacingTrait.php new file mode 100644 index 0000000000..54cac0cda3 --- /dev/null +++ b/src/block/utils/HorizontalFacingTrait.php @@ -0,0 +1,44 @@ +facing; } + + /** @return $this */ + public function setFacing(int $facing) : self{ + $axis = Facing::axis($facing); + if($axis !== Axis::X && $axis !== Axis::Z){ + throw new \InvalidArgumentException("Facing must be horizontal"); + } + $this->facing = $facing; + return $this; + } +} diff --git a/src/block/utils/InvalidBlockStateException.php b/src/block/utils/InvalidBlockStateException.php new file mode 100644 index 0000000000..506b81dbb6 --- /dev/null +++ b/src/block/utils/InvalidBlockStateException.php @@ -0,0 +1,28 @@ +Enum___construct($enumName); + } + + public function getFacing() : int{ return $this->facing; } +} diff --git a/src/block/utils/MinimumCostFlowCalculator.php b/src/block/utils/MinimumCostFlowCalculator.php new file mode 100644 index 0000000000..95e8457917 --- /dev/null +++ b/src/block/utils/MinimumCostFlowCalculator.php @@ -0,0 +1,154 @@ + --$x, + Facing::EAST => ++$x, + Facing::NORTH => --$z, + Facing::SOUTH => ++$z + }; + + if(!isset($this->flowCostVisited[$hash = World::blockHash($x, $y, $z)])){ + if(!$this->world->isInWorld($x, $y, $z) || !$this->canFlowInto($this->world->getBlockAt($x, $y, $z))){ + $this->flowCostVisited[$hash] = self::BLOCKED; + }elseif($this->world->getBlockAt($x, $y - 1, $z)->canBeFlowedInto()){ + $this->flowCostVisited[$hash] = self::CAN_FLOW_DOWN; + }else{ + $this->flowCostVisited[$hash] = self::CAN_FLOW; + } + } + + $status = $this->flowCostVisited[$hash]; + + if($status === self::BLOCKED){ + continue; + }elseif($status === self::CAN_FLOW_DOWN){ + return $accumulatedCost; + } + + if($accumulatedCost >= $maxCost){ + continue; + } + + $realCost = $this->calculateFlowCost($x, $y, $z, $accumulatedCost + 1, $maxCost, $originOpposite, Facing::opposite($j)); + + if($realCost < $cost){ + $cost = $realCost; + } + } + + return $cost; + } + + /** + * @return int[] + */ + public function getOptimalFlowDirections(int $originX, int $originY, int $originZ) : array{ + $flowCost = array_fill_keys(Facing::HORIZONTAL, 1000); + $maxCost = intdiv(4, $this->flowDecayPerBlock); + foreach(Facing::HORIZONTAL as $j){ + $x = $originX; + $y = $originY; + $z = $originZ; + + match($j){ + Facing::WEST => --$x, + Facing::EAST => ++$x, + Facing::NORTH => --$z, + Facing::SOUTH => ++$z + }; + + if(!$this->world->isInWorld($x, $y, $z) || !$this->canFlowInto($this->world->getBlockAt($x, $y, $z))){ + $this->flowCostVisited[World::blockHash($x, $y, $z)] = self::BLOCKED; + }elseif($this->world->getBlockAt($x, $y - 1, $z)->canBeFlowedInto()){ + $this->flowCostVisited[World::blockHash($x, $y, $z)] = self::CAN_FLOW_DOWN; + $flowCost[$j] = $maxCost = 0; + }elseif($maxCost > 0){ + $this->flowCostVisited[World::blockHash($x, $y, $z)] = self::CAN_FLOW; + $opposite = Facing::opposite($j); + $flowCost[$j] = $this->calculateFlowCost($x, $y, $z, 1, $maxCost, $opposite, $opposite); + $maxCost = min($maxCost, $flowCost[$j]); + } + } + + $this->flowCostVisited = []; + + $minCost = min($flowCost); + + $isOptimalFlowDirection = []; + + foreach($flowCost as $facing => $cost){ + if($cost === $minCost){ + $isOptimalFlowDirection[] = $facing; + } + } + + return $isOptimalFlowDirection; + } + + private function canFlowInto(Block $block) : bool{ + return ($this->canFlowInto)($block); + } +} diff --git a/src/block/utils/MushroomBlockType.php b/src/block/utils/MushroomBlockType.php new file mode 100644 index 0000000000..33b04831d1 --- /dev/null +++ b/src/block/utils/MushroomBlockType.php @@ -0,0 +1,64 @@ +meta = $meta; + protected function writeStateToMeta() : int{ + return BlockDataSerializer::writeHorizontalFacing($this->facing); } - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->facing = BlockDataSerializer::readHorizontalFacing($stateMeta); } - public function getName() : string{ - return "Podzol"; - } - - public function getHardness() : float{ - return 0.5; + public function getStateBitmask() : int{ + return 0b111; } } diff --git a/src/block/utils/PillarRotationInMetadataTrait.php b/src/block/utils/PillarRotationInMetadataTrait.php new file mode 100644 index 0000000000..42adf7120c --- /dev/null +++ b/src/block/utils/PillarRotationInMetadataTrait.php @@ -0,0 +1,76 @@ +writeAxisToMeta(); + } + + /** + * @see Block::readStateFromData() + */ + public function readStateFromData(int $id, int $stateMeta) : void{ + $this->readAxisFromMeta($stateMeta); + } + + /** + * @see Block::getStateBitmask() + */ + public function getStateBitmask() : int{ + return 0b11 << $this->getAxisMetaShift(); + } + + protected function readAxisFromMeta(int $meta) : void{ + $axis = $meta >> $this->getAxisMetaShift(); + $mapped = [ + 0 => Axis::Y, + 1 => Axis::X, + 2 => Axis::Z + ][$axis] ?? null; + if($mapped === null){ + throw new InvalidBlockStateException("Invalid axis meta $axis"); + } + $this->axis = $mapped; + } + + protected function writeAxisToMeta() : int{ + return [ + Axis::Y => 0, + Axis::Z => 2, + Axis::X => 1 + ][$this->axis] << $this->getAxisMetaShift(); + } +} diff --git a/src/block/utils/PillarRotationTrait.php b/src/block/utils/PillarRotationTrait.php new file mode 100644 index 0000000000..806782db8f --- /dev/null +++ b/src/block/utils/PillarRotationTrait.php @@ -0,0 +1,56 @@ +axis; } + + /** @return $this */ + public function setAxis(int $axis) : self{ + $this->axis = $axis; + return $this; + } + + /** + * @see Block::place() + */ + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $this->axis = Facing::axis($face); + /** @see Block::place() */ + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } +} diff --git a/src/pocketmine/block/utils/PillarRotationHelper.php b/src/block/utils/PoweredByRedstoneTrait.php similarity index 73% rename from src/pocketmine/block/utils/PillarRotationHelper.php rename to src/block/utils/PoweredByRedstoneTrait.php index 8a633c11f4..683437a4f9 100644 --- a/src/pocketmine/block/utils/PillarRotationHelper.php +++ b/src/block/utils/PoweredByRedstoneTrait.php @@ -23,17 +23,14 @@ declare(strict_types=1); namespace pocketmine\block\utils; -use pocketmine\math\Vector3; +trait PoweredByRedstoneTrait{ + protected bool $powered = false; -class PillarRotationHelper{ + public function isPowered() : bool{ return $this->powered; } - public static function getMetaFromFace(int $meta, int $face) : int{ - $faces = [ - Vector3::SIDE_DOWN => 0, - Vector3::SIDE_NORTH => 0x08, - Vector3::SIDE_WEST => 0x04 - ]; - - return ($meta & 0x03) | $faces[$face & ~0x01]; + /** @return $this */ + public function setPowered(bool $powered) : self{ + $this->powered = $powered; + return $this; } } diff --git a/src/block/utils/RailConnectionInfo.php b/src/block/utils/RailConnectionInfo.php new file mode 100644 index 0000000000..324ee3cfb5 --- /dev/null +++ b/src/block/utils/RailConnectionInfo.php @@ -0,0 +1,82 @@ + [ + Facing::NORTH, + Facing::SOUTH + ], + BlockLegacyMetadata::RAIL_STRAIGHT_EAST_WEST => [ + Facing::EAST, + Facing::WEST + ], + + //ascending + BlockLegacyMetadata::RAIL_ASCENDING_EAST => [ + Facing::WEST, + Facing::EAST | self::FLAG_ASCEND + ], + BlockLegacyMetadata::RAIL_ASCENDING_WEST => [ + Facing::EAST, + Facing::WEST | self::FLAG_ASCEND + ], + BlockLegacyMetadata::RAIL_ASCENDING_NORTH => [ + Facing::SOUTH, + Facing::NORTH | self::FLAG_ASCEND + ], + BlockLegacyMetadata::RAIL_ASCENDING_SOUTH => [ + Facing::NORTH, + Facing::SOUTH | self::FLAG_ASCEND + ] + ]; + + /* extended meta values for regular rails, to allow curving */ + public const CURVE_CONNECTIONS = [ + BlockLegacyMetadata::RAIL_CURVE_SOUTHEAST => [ + Facing::SOUTH, + Facing::EAST + ], + BlockLegacyMetadata::RAIL_CURVE_SOUTHWEST => [ + Facing::SOUTH, + Facing::WEST + ], + BlockLegacyMetadata::RAIL_CURVE_NORTHWEST => [ + Facing::NORTH, + Facing::WEST + ], + BlockLegacyMetadata::RAIL_CURVE_NORTHEAST => [ + Facing::NORTH, + Facing::EAST + ] + ]; +} diff --git a/src/pocketmine/metadata/MetadataValue.php b/src/block/utils/RailPoweredByRedstoneTrait.php similarity index 53% rename from src/pocketmine/metadata/MetadataValue.php rename to src/block/utils/RailPoweredByRedstoneTrait.php index 46622002d7..f0549429fa 100644 --- a/src/pocketmine/metadata/MetadataValue.php +++ b/src/block/utils/RailPoweredByRedstoneTrait.php @@ -21,37 +21,24 @@ declare(strict_types=1); -namespace pocketmine\metadata; +namespace pocketmine\block\utils; -use pocketmine\plugin\Plugin; +use pocketmine\block\BlockLegacyMetadata; -abstract class MetadataValue{ - /** @var Plugin */ - private $owningPlugin; +trait RailPoweredByRedstoneTrait{ + use PoweredByRedstoneTrait; - protected function __construct(Plugin $owningPlugin){ - $this->owningPlugin = $owningPlugin; + public function readStateFromData(int $id, int $stateMeta) : void{ + parent::readStateFromData($id, $stateMeta & ~BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED); + $this->powered = ($stateMeta & BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED) !== 0; } - /** - * @return Plugin - */ - public function getOwningPlugin(){ - return $this->owningPlugin; + protected function writeStateToMeta() : int{ + //TODO: railShape won't be plain metadata in the future + return parent::writeStateToMeta() | ($this->powered ? BlockLegacyMetadata::REDSTONE_RAIL_FLAG_POWERED : 0); } - /** - * Fetches the value of this metadata item. - * - * @return mixed - */ - abstract public function value(); - - /** - * Invalidates this metadata item, forcing it to recompute when next - * accessed. - * - * @return void - */ - abstract public function invalidate(); + public function getStateBitmask() : int{ + return 0b1111; + } } diff --git a/src/block/utils/RecordType.php b/src/block/utils/RecordType.php new file mode 100644 index 0000000000..e497d419ce --- /dev/null +++ b/src/block/utils/RecordType.php @@ -0,0 +1,93 @@ +Enum___construct($enumName); + $this->soundName = $soundName; + $this->soundId = $soundId; + } + + public function getSoundName() : string{ + return $this->soundName; + } + + public function getSoundId() : int{ + return $this->soundId; + } + + public function getTranslatableName() : Translatable{ return $this->translatableName; } +} diff --git a/src/pocketmine/network/mcpe/protocol/ChunkRadiusUpdatedPacket.php b/src/block/utils/SignLikeRotationTrait.php similarity index 58% rename from src/pocketmine/network/mcpe/protocol/ChunkRadiusUpdatedPacket.php rename to src/block/utils/SignLikeRotationTrait.php index 42643d9e36..fd09a824f2 100644 --- a/src/pocketmine/network/mcpe/protocol/ChunkRadiusUpdatedPacket.php +++ b/src/block/utils/SignLikeRotationTrait.php @@ -21,27 +21,26 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\block\utils; -#include - -use pocketmine\network\mcpe\NetworkSession; - -class ChunkRadiusUpdatedPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CHUNK_RADIUS_UPDATED_PACKET; +use function floor; +trait SignLikeRotationTrait{ /** @var int */ - public $radius; + private $rotation = 0; - protected function decodePayload(){ - $this->radius = $this->getVarInt(); + public function getRotation() : int{ return $this->rotation; } + + /** @return $this */ + public function setRotation(int $rotation) : self{ + if($rotation < 0 || $rotation > 15){ + throw new \InvalidArgumentException("Rotation must be in range 0-15"); + } + $this->rotation = $rotation; + return $this; } - protected function encodePayload(){ - $this->putVarInt($this->radius); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleChunkRadiusUpdated($this); + private static function getRotationFromYaw(float $yaw) : int{ + return ((int) floor((($yaw + 180) * 16 / 360) + 0.5)) & 0xf; } } diff --git a/src/block/utils/SignText.php b/src/block/utils/SignText.php new file mode 100644 index 0000000000..753e25bb3a --- /dev/null +++ b/src/block/utils/SignText.php @@ -0,0 +1,106 @@ +lines = array_fill(0, self::LINE_COUNT, ""); + if($lines !== null){ + if(count($lines) > self::LINE_COUNT){ + throw new \InvalidArgumentException("Expected at most 4 lines, got " . count($lines)); + } + foreach($lines as $k => $line){ + $this->checkLineIndex($k); + Utils::checkUTF8($line); + if(strpos($line, "\n") !== false){ + throw new \InvalidArgumentException("Line must not contain newlines"); + } + //TODO: add length checks + $this->lines[$k] = $line; + } + } + } + + /** + * Parses sign lines from the given string blob. + * TODO: add a strict mode for this + * + * @throws \InvalidArgumentException if the text is not valid UTF-8 + */ + public static function fromBlob(string $blob) : SignText{ + return new self(array_slice(array_pad(explode("\n", $blob), self::LINE_COUNT, ""), 0, self::LINE_COUNT)); + } + + /** + * Returns an array of lines currently on the sign. + * + * @return string[] + */ + public function getLines() : array{ + return $this->lines; + } + + /** + * @param int|string $index + */ + private function checkLineIndex($index) : void{ + if(!is_int($index)){ + throw new \InvalidArgumentException("Index must be an integer"); + } + if($index < 0 or $index >= self::LINE_COUNT){ + throw new \InvalidArgumentException("Line index is out of bounds"); + } + } + + /** + * Returns the sign line at the given offset. + * + * @throws \InvalidArgumentException + */ + public function getLine(int $index) : string{ + $this->checkLineIndex($index); + return $this->lines[$index]; + } +} diff --git a/src/block/utils/SkullType.php b/src/block/utils/SkullType.php new file mode 100644 index 0000000000..2bbb566ddf --- /dev/null +++ b/src/block/utils/SkullType.php @@ -0,0 +1,96 @@ +getMagicNumber()] = $type; + } + + /** + * @internal + * + * @throws \InvalidArgumentException + */ + public static function fromMagicNumber(int $magicNumber) : SkullType{ + if(!isset(self::$numericIdMap[$magicNumber])){ + throw new \InvalidArgumentException("Unknown skull type magic number $magicNumber"); + } + return self::$numericIdMap[$magicNumber]; + } + + /** @var string */ + private $displayName; + /** @var int */ + private $magicNumber; + + private function __construct(string $enumName, string $displayName, int $magicNumber){ + $this->Enum___construct($enumName); + $this->displayName = $displayName; + $this->magicNumber = $magicNumber; + } + + public function getDisplayName() : string{ + return $this->displayName; + } + + public function getMagicNumber() : int{ + return $this->magicNumber; + } +} diff --git a/src/block/utils/SlabType.php b/src/block/utils/SlabType.php new file mode 100644 index 0000000000..26648481f3 --- /dev/null +++ b/src/block/utils/SlabType.php @@ -0,0 +1,48 @@ +getMagicNumber()] = $type; + } + + /** + * @internal + * + * @throws \InvalidArgumentException + */ + public static function fromMagicNumber(int $magicNumber) : TreeType{ + self::checkInit(); + if(!isset(self::$numericIdMap[$magicNumber])){ + throw new \InvalidArgumentException("Unknown tree type magic number $magicNumber"); + } + return self::$numericIdMap[$magicNumber]; + } + + /** @var string */ + private $displayName; + /** @var int */ + private $magicNumber; + + private function __construct(string $enumName, string $displayName, int $magicNumber){ + $this->Enum___construct($enumName); + $this->displayName = $displayName; + $this->magicNumber = $magicNumber; + } + + public function getDisplayName() : string{ + return $this->displayName; + } + + public function getMagicNumber() : int{ + return $this->magicNumber; + } +} diff --git a/src/pocketmine/command/Command.php b/src/command/Command.php similarity index 61% rename from src/pocketmine/command/Command.php rename to src/command/Command.php index 4bb0352ef2..ea324a9338 100644 --- a/src/pocketmine/command/Command.php +++ b/src/command/Command.php @@ -27,10 +27,12 @@ declare(strict_types=1); namespace pocketmine\command; use pocketmine\command\utils\CommandException; -use pocketmine\lang\TextContainer; -use pocketmine\lang\TranslationContainer; +use pocketmine\console\ConsoleCommandSender; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\lang\Translatable; use pocketmine\permission\PermissionManager; use pocketmine\Server; +use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; use pocketmine\utils\TextFormat; use function explode; @@ -56,10 +58,10 @@ abstract class Command{ /** @var CommandMap|null */ private $commandMap = null; - /** @var string */ + /** @var Translatable|string */ protected $description = ""; - /** @var string */ + /** @var Translatable|string */ protected $usageMessage; /** @var string|null */ @@ -74,7 +76,7 @@ abstract class Command{ /** * @param string[] $aliases */ - public function __construct(string $name, string $description = "", string $usageMessage = null, array $aliases = []){ + public function __construct(string $name, Translatable|string $description = "", Translatable|string|null $usageMessage = null, array $aliases = []){ $this->name = $name; $this->setLabel($name); $this->setDescription($description); @@ -94,41 +96,43 @@ abstract class Command{ return $this->name; } - /** - * @return string|null - */ - public function getPermission(){ + public function getPermission() : ?string{ return $this->permission; } - /** - * @return void - */ - public function setPermission(string $permission = null){ + public function setPermission(?string $permission) : void{ + if($permission !== null){ + foreach(explode(";", $permission) as $perm){ + if(PermissionManager::getInstance()->getPermission($perm) === null){ + throw new \InvalidArgumentException("Cannot use non-existing permission \"$perm\""); + } + } + } $this->permission = $permission; } - public function testPermission(CommandSender $target) : bool{ - if($this->testPermissionSilent($target)){ + public function testPermission(CommandSender $target, ?string $permission = null) : bool{ + if($this->testPermissionSilent($target, $permission)){ return true; } if($this->permissionMessage === null){ - $target->sendMessage($target->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); + $target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->prefix(TextFormat::RED)); }elseif($this->permissionMessage !== ""){ - $target->sendMessage(str_replace("", $this->permission, $this->permissionMessage)); + $target->sendMessage(str_replace("", $permission ?? $this->permission, $this->permissionMessage)); } return false; } - public function testPermissionSilent(CommandSender $target) : bool{ - if($this->permission === null or $this->permission === ""){ + public function testPermissionSilent(CommandSender $target, ?string $permission = null) : bool{ + $permission ??= $this->permission; + if($permission === null or $permission === ""){ return true; } - foreach(explode(";", $this->permission) as $permission){ - if($target->hasPermission($permission)){ + foreach(explode(";", $permission) as $p){ + if($target->hasPermission($p)){ return true; } } @@ -143,10 +147,7 @@ abstract class Command{ public function setLabel(string $name) : bool{ $this->nextLabel = $name; if(!$this->isRegistered()){ - if($this->timings instanceof TimingsHandler){ - $this->timings->remove(); - } - $this->timings = new TimingsHandler("** Command: " . $name); + $this->timings = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Command: " . $name); $this->label = $name; return true; @@ -199,81 +200,50 @@ abstract class Command{ return $this->permissionMessage; } - public function getDescription() : string{ + public function getDescription() : Translatable|string{ return $this->description; } - public function getUsage() : string{ + public function getUsage() : Translatable|string{ return $this->usageMessage; } /** * @param string[] $aliases - * - * @return void */ - public function setAliases(array $aliases){ + public function setAliases(array $aliases) : void{ $this->aliases = $aliases; if(!$this->isRegistered()){ $this->activeAliases = $aliases; } } - /** - * @return void - */ - public function setDescription(string $description){ + public function setDescription(Translatable|string $description) : void{ $this->description = $description; } - /** - * @return void - */ - public function setPermissionMessage(string $permissionMessage){ + public function setPermissionMessage(string $permissionMessage) : void{ $this->permissionMessage = $permissionMessage; } - /** - * @return void - */ - public function setUsage(string $usage){ + public function setUsage(Translatable|string $usage) : void{ $this->usageMessage = $usage; } - /** - * @param TextContainer|string $message - * - * @return void - */ - public static function broadcastCommandMessage(CommandSender $source, $message, bool $sendToSource = true){ - if($message instanceof TextContainer){ - $m = clone $message; - $result = "[" . $source->getName() . ": " . ($source->getServer()->getLanguage()->get($m->getText()) !== $m->getText() ? "%" : "") . $m->getText() . "]"; - - $users = PermissionManager::getInstance()->getPermissionSubscriptions(Server::BROADCAST_CHANNEL_ADMINISTRATIVE); - $colored = TextFormat::GRAY . TextFormat::ITALIC . $result; - - $m->setText($result); - $result = clone $m; - $m->setText($colored); - $colored = clone $m; - }else{ - $users = PermissionManager::getInstance()->getPermissionSubscriptions(Server::BROADCAST_CHANNEL_ADMINISTRATIVE); - $result = new TranslationContainer("chat.type.admin", [$source->getName(), $message]); - $colored = new TranslationContainer(TextFormat::GRAY . TextFormat::ITALIC . "%chat.type.admin", [$source->getName(), $message]); - } + public static function broadcastCommandMessage(CommandSender $source, Translatable|string $message, bool $sendToSource = true) : void{ + $users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE); + $result = KnownTranslationFactory::chat_type_admin($source->getName(), $message); + $colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC); if($sendToSource and !($source instanceof ConsoleCommandSender)){ $source->sendMessage($message); } foreach($users as $user){ - if($user instanceof CommandSender){ - if($user instanceof ConsoleCommandSender){ - $user->sendMessage($result); - }elseif($user !== $source){ - $user->sendMessage($colored); - } + if($user instanceof ConsoleCommandSender){ + $user->sendMessage($result); + }elseif($user !== $source){ + $user->sendMessage($colored); } } } diff --git a/src/pocketmine/command/CommandExecutor.php b/src/command/CommandExecutor.php similarity index 100% rename from src/pocketmine/command/CommandExecutor.php rename to src/command/CommandExecutor.php diff --git a/src/pocketmine/command/CommandMap.php b/src/command/CommandMap.php similarity index 83% rename from src/pocketmine/command/CommandMap.php rename to src/command/CommandMap.php index 149f36868e..3be7850e69 100644 --- a/src/pocketmine/command/CommandMap.php +++ b/src/command/CommandMap.php @@ -27,23 +27,15 @@ interface CommandMap{ /** * @param Command[] $commands - * - * @return void */ - public function registerAll(string $fallbackPrefix, array $commands); + public function registerAll(string $fallbackPrefix, array $commands) : void; - public function register(string $fallbackPrefix, Command $command, string $label = null) : bool; + public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool; public function dispatch(CommandSender $sender, string $cmdLine) : bool; - /** - * @return void - */ - public function clearCommands(); + public function clearCommands() : void; - /** - * @return Command|null - */ - public function getCommand(string $name); + public function getCommand(string $name) : ?Command; } diff --git a/src/pocketmine/command/CommandSender.php b/src/command/CommandSender.php similarity index 81% rename from src/pocketmine/command/CommandSender.php rename to src/command/CommandSender.php index 63406b2620..50734758c6 100644 --- a/src/pocketmine/command/CommandSender.php +++ b/src/command/CommandSender.php @@ -23,23 +23,18 @@ declare(strict_types=1); namespace pocketmine\command; -use pocketmine\lang\TextContainer; +use pocketmine\lang\Language; +use pocketmine\lang\Translatable; use pocketmine\permission\Permissible; use pocketmine\Server; interface CommandSender extends Permissible{ - /** - * @param TextContainer|string $message - * - * @return void - */ - public function sendMessage($message); + public function getLanguage() : Language; - /** - * @return Server - */ - public function getServer(); + public function sendMessage(Translatable|string $message) : void; + + public function getServer() : Server; public function getName() : string; @@ -51,8 +46,6 @@ interface CommandSender extends Permissible{ /** * Sets the line height used for command output pagination for this command sender. `null` will reset it to default. - * - * @return void */ - public function setScreenLineHeight(int $height = null); + public function setScreenLineHeight(?int $height) : void; } diff --git a/src/pocketmine/command/FormattedCommandAlias.php b/src/command/FormattedCommandAlias.php similarity index 100% rename from src/pocketmine/command/FormattedCommandAlias.php rename to src/command/FormattedCommandAlias.php diff --git a/src/pocketmine/command/PluginCommand.php b/src/command/PluginCommand.php similarity index 80% rename from src/pocketmine/command/PluginCommand.php rename to src/command/PluginCommand.php index 16e3bc1831..2453f4025a 100644 --- a/src/pocketmine/command/PluginCommand.php +++ b/src/command/PluginCommand.php @@ -25,19 +25,19 @@ namespace pocketmine\command; use pocketmine\command\utils\InvalidCommandSyntaxException; use pocketmine\plugin\Plugin; +use pocketmine\plugin\PluginOwned; +use pocketmine\plugin\PluginOwnedTrait; -class PluginCommand extends Command implements PluginIdentifiableCommand{ - - /** @var Plugin */ - private $owningPlugin; +final class PluginCommand extends Command implements PluginOwned{ + use PluginOwnedTrait; /** @var CommandExecutor */ private $executor; - public function __construct(string $name, Plugin $owner){ + public function __construct(string $name, Plugin $owner, CommandExecutor $executor){ parent::__construct($name); $this->owningPlugin = $owner; - $this->executor = $owner; + $this->executor = $executor; $this->usageMessage = ""; } @@ -64,14 +64,7 @@ class PluginCommand extends Command implements PluginIdentifiableCommand{ return $this->executor; } - /** - * @return void - */ - public function setExecutor(CommandExecutor $executor){ + public function setExecutor(CommandExecutor $executor) : void{ $this->executor = $executor; } - - public function getPlugin() : Plugin{ - return $this->owningPlugin; - } } diff --git a/src/pocketmine/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php similarity index 81% rename from src/pocketmine/command/SimpleCommandMap.php rename to src/command/SimpleCommandMap.php index 02926d0925..6b8f392e6b 100644 --- a/src/pocketmine/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -26,6 +26,7 @@ namespace pocketmine\command; use pocketmine\command\defaults\BanCommand; use pocketmine\command\defaults\BanIpCommand; use pocketmine\command\defaults\BanListCommand; +use pocketmine\command\defaults\ClearCommand; use pocketmine\command\defaults\DefaultGamemodeCommand; use pocketmine\command\defaults\DeopCommand; use pocketmine\command\defaults\DifficultyCommand; @@ -45,7 +46,6 @@ use pocketmine\command\defaults\PardonCommand; use pocketmine\command\defaults\PardonIpCommand; use pocketmine\command\defaults\ParticleCommand; use pocketmine\command\defaults\PluginsCommand; -use pocketmine\command\defaults\ReloadCommand; use pocketmine\command\defaults\SaveCommand; use pocketmine\command\defaults\SaveOffCommand; use pocketmine\command\defaults\SaveOnCommand; @@ -65,12 +65,13 @@ use pocketmine\command\defaults\VanillaCommand; use pocketmine\command\defaults\VersionCommand; 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; use function implode; -use function min; use function preg_match_all; use function strcasecmp; use function stripslashes; @@ -96,6 +97,7 @@ class SimpleCommandMap implements CommandMap{ new BanCommand("ban"), new BanIpCommand("ban-ip"), new BanListCommand("banlist"), + new ClearCommand("clear"), new DefaultGamemodeCommand("defaultgamemode"), new DeopCommand("deop"), new DifficultyCommand("difficulty"), @@ -115,7 +117,6 @@ class SimpleCommandMap implements CommandMap{ new PardonIpCommand("pardon-ip"), new ParticleCommand("particle"), new PluginsCommand("plugins"), - new ReloadCommand("reload"), new SaveCommand("save-all"), new SaveOffCommand("save-off"), new SaveOnCommand("save-on"), @@ -136,13 +137,13 @@ class SimpleCommandMap implements CommandMap{ ]); } - public function registerAll(string $fallbackPrefix, array $commands){ + public function registerAll(string $fallbackPrefix, array $commands) : void{ foreach($commands as $command){ $this->register($fallbackPrefix, $command); } } - public function register(string $fallbackPrefix, Command $command, string $label = null) : bool{ + public function register(string $fallbackPrefix, Command $command, ?string $label = null) : bool{ if($label === null){ $label = $command->getName(); } @@ -199,31 +200,6 @@ class SimpleCommandMap implements CommandMap{ return true; } - /** - * Returns a command to match the specified command line, or null if no matching command was found. - * This method is intended to provide capability for handling commands with spaces in their name. - * The referenced parameters will be modified accordingly depending on the resulting matched command. - * - * @param string $commandName reference parameter - * @param string[] $args reference parameter - * - * @return Command|null - */ - public function matchCommand(string &$commandName, array &$args){ - $count = min(count($args), 255); - - for($i = 0; $i < $count; ++$i){ - $commandName .= array_shift($args); - if(($command = $this->getCommand($commandName)) instanceof Command){ - return $command; - } - - $commandName .= " "; - } - - return null; - } - public function dispatch(CommandSender $sender, string $commandLine) : bool{ $args = []; preg_match_all('/"((?:\\\\.|[^\\\\"])*)"|(\S+)/u', $commandLine, $matches); @@ -235,27 +211,26 @@ class SimpleCommandMap implements CommandMap{ } } } - $sentCommandLabel = ""; - $target = $this->matchCommand($sentCommandLabel, $args); - if($target === null){ - return false; + $sentCommandLabel = array_shift($args); + 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($this->server->getLanguage()->translateString("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(){ + public function clearCommands() : void{ foreach($this->knownCommands as $command){ $command->unregister($this); } @@ -263,7 +238,7 @@ class SimpleCommandMap implements CommandMap{ $this->setDefaultCommands(); } - public function getCommand(string $name){ + public function getCommand(string $name) : ?Command{ return $this->knownCommands[$name] ?? null; } @@ -274,15 +249,12 @@ class SimpleCommandMap implements CommandMap{ return $this->knownCommands; } - /** - * @return void - */ - public function registerServerAliases(){ + public function registerServerAliases() : void{ $values = $this->server->getCommandAliases(); foreach($values as $alias => $commandStrings){ if(strpos($alias, ":") !== false){ - $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.illegal", [$alias])); + $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_illegal($alias))); continue; } @@ -292,8 +264,8 @@ class SimpleCommandMap implements CommandMap{ foreach($commandStrings as $commandString){ $args = explode(" ", $commandString); - $commandName = ""; - $command = $this->matchCommand($commandName, $args); + $commandName = array_shift($args); + $command = $this->getCommand($commandName); if($command === null){ $bad[] = $commandString; @@ -305,12 +277,12 @@ class SimpleCommandMap implements CommandMap{ } if(count($recursive) > 0){ - $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.recursive", [$alias, implode(", ", $recursive)])); + $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_recursive($alias, implode(", ", $recursive)))); continue; } if(count($bad) > 0){ - $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.notFound", [$alias, implode(", ", $bad)])); + $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_notFound($alias, implode(", ", $bad)))); continue; } diff --git a/src/pocketmine/command/defaults/BanCommand.php b/src/command/defaults/BanCommand.php similarity index 78% rename from src/pocketmine/command/defaults/BanCommand.php rename to src/command/defaults/BanCommand.php index 67ddff1edf..4b734303d1 100644 --- a/src/pocketmine/command/defaults/BanCommand.php +++ b/src/command/defaults/BanCommand.php @@ -26,8 +26,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use function array_shift; use function count; use function implode; @@ -37,10 +38,10 @@ class BanCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.ban.player.description", - "%commands.ban.usage" + KnownTranslationFactory::pocketmine_command_ban_player_description(), + KnownTranslationFactory::commands_ban_usage() ); - $this->setPermission("pocketmine.command.ban.player"); + $this->setPermission(DefaultPermissionNames::COMMAND_BAN_PLAYER); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -61,7 +62,7 @@ class BanCommand extends VanillaCommand{ $player->kick($reason !== "" ? "Banned by admin. Reason: " . $reason : "Banned by admin."); } - Command::broadcastCommandMessage($sender, new TranslationContainer("%commands.ban.success", [$player !== null ? $player->getName() : $name])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_ban_success($player !== null ? $player->getName() : $name)); return true; } diff --git a/src/pocketmine/command/defaults/BanIpCommand.php b/src/command/defaults/BanIpCommand.php similarity index 64% rename from src/pocketmine/command/defaults/BanIpCommand.php rename to src/command/defaults/BanIpCommand.php index e60b9b219f..d6e1584b94 100644 --- a/src/pocketmine/command/defaults/BanIpCommand.php +++ b/src/command/defaults/BanIpCommand.php @@ -26,22 +26,23 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +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{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.ban.ip.description", - "%commands.banip.usage" + KnownTranslationFactory::pocketmine_command_ban_ip_description(), + KnownTranslationFactory::commands_banip_usage() ); - $this->setPermission("pocketmine.command.ban.ip"); + $this->setPermission(DefaultPermissionNames::COMMAND_BAN_IP); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -56,17 +57,18 @@ 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, new TranslationContainer("commands.banip.success", [$value])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_banip_success($value)); }else{ - if(($player = $sender->getServer()->getPlayer($value)) instanceof Player){ - $this->processIPBan($player->getAddress(), $sender, $reason); + if(($player = $sender->getServer()->getPlayerByPrefix($value)) instanceof Player){ + $ip = $player->getNetworkSession()->getIp(); + $this->processIPBan($ip, $sender, $reason); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.banip.success.players", [$player->getAddress(), $player->getName()])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_banip_success_players($ip, $player->getName())); }else{ - $sender->sendMessage(new TranslationContainer("commands.banip.invalid")); + $sender->sendMessage(KnownTranslationFactory::commands_banip_invalid()); return false; } @@ -79,8 +81,8 @@ class BanIpCommand extends VanillaCommand{ $sender->getServer()->getIPBans()->addBan($ip, $reason, null, $sender->getName()); foreach($sender->getServer()->getOnlinePlayers() as $player){ - if($player->getAddress() === $ip){ - $player->kick($reason !== "" ? $reason : "IP banned."); + if($player->getNetworkSession()->getIp() === $ip){ + $player->kick("Banned by admin. Reason: " . ($reason !== "" ? $reason : "IP banned.")); } } diff --git a/src/pocketmine/command/defaults/BanListCommand.php b/src/command/defaults/BanListCommand.php similarity index 80% rename from src/pocketmine/command/defaults/BanListCommand.php rename to src/command/defaults/BanListCommand.php index 95c02f9a5c..f3bd20baa8 100644 --- a/src/pocketmine/command/defaults/BanListCommand.php +++ b/src/command/defaults/BanListCommand.php @@ -25,8 +25,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; use pocketmine\permission\BanEntry; +use pocketmine\permission\DefaultPermissionNames; use function array_map; use function count; use function implode; @@ -39,10 +40,10 @@ class BanListCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.banlist.description", - "%commands.banlist.usage" + KnownTranslationFactory::pocketmine_command_banlist_description(), + KnownTranslationFactory::commands_banlist_usage() ); - $this->setPermission("pocketmine.command.ban.list"); + $this->setPermission(DefaultPermissionNames::COMMAND_BAN_LIST); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -71,9 +72,9 @@ class BanListCommand extends VanillaCommand{ $message = implode(", ", $list); if($args[0] === "ips"){ - $sender->sendMessage(new TranslationContainer("commands.banlist.ips", [count($list)])); + $sender->sendMessage(KnownTranslationFactory::commands_banlist_ips((string) count($list))); }else{ - $sender->sendMessage(new TranslationContainer("commands.banlist.players", [count($list)])); + $sender->sendMessage(KnownTranslationFactory::commands_banlist_players((string) count($list))); } $sender->sendMessage($message); diff --git a/src/command/defaults/ClearCommand.php b/src/command/defaults/ClearCommand.php new file mode 100644 index 0000000000..59bee1e5b8 --- /dev/null +++ b/src/command/defaults/ClearCommand.php @@ -0,0 +1,174 @@ +setPermission(implode(";", [DefaultPermissionNames::COMMAND_CLEAR_SELF, DefaultPermissionNames::COMMAND_CLEAR_OTHER])); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + + if(count($args) > 3){ + throw new InvalidCommandSyntaxException(); + } + + $target = null; + if(isset($args[0])){ + $target = $sender->getServer()->getPlayerByPrefix($args[0]); + if($target === null){ + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); + return true; + } + if($target !== $sender && !$this->testPermission($sender, DefaultPermissionNames::COMMAND_CLEAR_OTHER)){ + return true; + } + }elseif($sender instanceof Player){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_CLEAR_SELF)){ + return true; + } + + $target = $sender; + }else{ + throw new InvalidCommandSyntaxException(); + } + + $item = null; + $maxCount = -1; + if(isset($args[1])){ + try{ + $item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]); + + if(isset($args[2])){ + $item->setCount($maxCount = $this->getInteger($sender, $args[2], 0)); + } + }catch(LegacyStringToItemParserException $e){ + //vanilla checks this at argument parsing layer, can't come up with a better alternative + $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED)); + return true; + } + } + + //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(); + } + + if($count > 0){ + $sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count)); + }else{ + $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED)); + } + + 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(); + } + + $target->getInventory()->clearAll(); + $target->getArmorInventory()->clearAll(); + //TODO: should the cursor inv be cleared? + }else{ + //clear the item from targets 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); + } + }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; + } + + if($maxCount <= 0){ + break; + } + + $cleared += $i->getCount(); + $maxCount -= $i->getCount(); + $target->getInventory()->clear($index); + } + } + } + } + + if($cleared > 0){ + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $cleared)); + }else{ + $sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED)); + } + + return true; + } +} diff --git a/src/pocketmine/command/defaults/DefaultGamemodeCommand.php b/src/command/defaults/DefaultGamemodeCommand.php similarity index 61% rename from src/pocketmine/command/defaults/DefaultGamemodeCommand.php rename to src/command/defaults/DefaultGamemodeCommand.php index 04c94d9903..d2ae848aef 100644 --- a/src/pocketmine/command/defaults/DefaultGamemodeCommand.php +++ b/src/command/defaults/DefaultGamemodeCommand.php @@ -25,8 +25,10 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Server; +use pocketmine\data\java\GameModeIdMap; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\GameMode; use function count; class DefaultGamemodeCommand extends VanillaCommand{ @@ -34,10 +36,10 @@ class DefaultGamemodeCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.defaultgamemode.description", - "%commands.defaultgamemode.usage" + KnownTranslationFactory::pocketmine_command_defaultgamemode_description(), + KnownTranslationFactory::commands_defaultgamemode_usage() ); - $this->setPermission("pocketmine.command.defaultgamemode"); + $this->setPermission(DefaultPermissionNames::COMMAND_DEFAULTGAMEMODE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -49,15 +51,14 @@ class DefaultGamemodeCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $gameMode = Server::getGamemodeFromString($args[0]); - - if($gameMode !== -1){ - $sender->getServer()->setConfigInt("gamemode", $gameMode); - $sender->sendMessage(new TranslationContainer("commands.defaultgamemode.success", [Server::getGamemodeString($gameMode)])); - }else{ - $sender->sendMessage("Unknown game mode"); + $gameMode = GameMode::fromString($args[0]); + if($gameMode === null){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_unknown($args[0])); + return true; } + $sender->getServer()->getConfigGroup()->setConfigInt("gamemode", GameModeIdMap::getInstance()->toId($gameMode)); + $sender->sendMessage(KnownTranslationFactory::commands_defaultgamemode_success($gameMode->getTranslatableName())); return true; } } diff --git a/src/pocketmine/command/defaults/DeopCommand.php b/src/command/defaults/DeopCommand.php similarity index 70% rename from src/pocketmine/command/defaults/DeopCommand.php rename to src/command/defaults/DeopCommand.php index 0dc92755f3..9dbd0757c5 100644 --- a/src/pocketmine/command/defaults/DeopCommand.php +++ b/src/command/defaults/DeopCommand.php @@ -26,8 +26,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function array_shift; use function count; @@ -37,10 +38,10 @@ class DeopCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.deop.description", - "%commands.deop.usage" + KnownTranslationFactory::pocketmine_command_deop_description(), + KnownTranslationFactory::commands_deop_usage() ); - $this->setPermission("pocketmine.command.op.take"); + $this->setPermission(DefaultPermissionNames::COMMAND_OP_TAKE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -57,12 +58,11 @@ class DeopCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getOfflinePlayer($name); - $player->setOp(false); - if($player instanceof Player){ - $player->sendMessage(TextFormat::GRAY . "You are no longer op!"); + $sender->getServer()->removeOp($name); + if(($player = $sender->getServer()->getPlayerExact($name)) !== null){ + $player->sendMessage(KnownTranslationFactory::commands_deop_message()->prefix(TextFormat::GRAY)); } - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.deop.success", [$player->getName()])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_deop_success($name)); return true; } diff --git a/src/pocketmine/command/defaults/DifficultyCommand.php b/src/command/defaults/DifficultyCommand.php similarity index 66% rename from src/pocketmine/command/defaults/DifficultyCommand.php rename to src/command/defaults/DifficultyCommand.php index a03f828ae6..7ccde43f09 100644 --- a/src/pocketmine/command/defaults/DifficultyCommand.php +++ b/src/command/defaults/DifficultyCommand.php @@ -26,8 +26,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\level\Level; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\world\World; use function count; class DifficultyCommand extends VanillaCommand{ @@ -35,10 +36,10 @@ class DifficultyCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.difficulty.description", - "%commands.difficulty.usage" + KnownTranslationFactory::pocketmine_command_difficulty_description(), + KnownTranslationFactory::commands_difficulty_usage() ); - $this->setPermission("pocketmine.command.difficulty"); + $this->setPermission(DefaultPermissionNames::COMMAND_DIFFICULTY); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -50,21 +51,21 @@ class DifficultyCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $difficulty = Level::getDifficultyFromString($args[0]); + $difficulty = World::getDifficultyFromString($args[0]); if($sender->getServer()->isHardcore()){ - $difficulty = Level::DIFFICULTY_HARD; + $difficulty = World::DIFFICULTY_HARD; } if($difficulty !== -1){ - $sender->getServer()->setConfigInt("difficulty", $difficulty); + $sender->getServer()->getConfigGroup()->setConfigInt("difficulty", $difficulty); //TODO: add per-world support - foreach($sender->getServer()->getLevels() as $level){ - $level->setDifficulty($difficulty); + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->setDifficulty($difficulty); } - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.difficulty.success", [$difficulty])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_difficulty_success((string) $difficulty)); }else{ throw new InvalidCommandSyntaxException(); } diff --git a/src/pocketmine/command/defaults/DumpMemoryCommand.php b/src/command/defaults/DumpMemoryCommand.php similarity index 82% rename from src/pocketmine/command/defaults/DumpMemoryCommand.php rename to src/command/defaults/DumpMemoryCommand.php index f7a48f4def..0f57103046 100644 --- a/src/pocketmine/command/defaults/DumpMemoryCommand.php +++ b/src/command/defaults/DumpMemoryCommand.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; +use pocketmine\permission\DefaultPermissionNames; +use Webmozart\PathUtil\Path; use function date; class DumpMemoryCommand extends VanillaCommand{ @@ -34,7 +36,7 @@ class DumpMemoryCommand extends VanillaCommand{ "Dumps the memory", "/$name [path]" ); - $this->setPermission("pocketmine.command.dumpmemory"); + $this->setPermission(DefaultPermissionNames::COMMAND_DUMPMEMORY); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -42,7 +44,7 @@ class DumpMemoryCommand extends VanillaCommand{ return true; } - $sender->getServer()->getMemoryManager()->dumpServerMemory($args[0] ?? ($sender->getServer()->getDataPath() . "/memory_dumps/" . date("D_M_j-H.i.s-T_Y")), 48, 80); + $sender->getServer()->getMemoryManager()->dumpServerMemory($args[0] ?? (Path::join($sender->getServer()->getDataPath(), "memory_dumps" . date("D_M_j-H.i.s-T_Y"))), 48, 80); return true; } } diff --git a/src/pocketmine/command/defaults/EffectCommand.php b/src/command/defaults/EffectCommand.php similarity index 53% rename from src/pocketmine/command/defaults/EffectCommand.php rename to src/command/defaults/EffectCommand.php index 3da54604ab..7b2541ea71 100644 --- a/src/pocketmine/command/defaults/EffectCommand.php +++ b/src/command/defaults/EffectCommand.php @@ -25,23 +25,24 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; -use pocketmine\lang\TranslationContainer; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\StringToEffectParser; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\utils\Limits; use pocketmine\utils\TextFormat; use function count; use function strtolower; -use const INT32_MAX; class EffectCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.effect.description", - "%commands.effect.usage" + KnownTranslationFactory::pocketmine_command_effect_description(), + KnownTranslationFactory::commands_effect_usage() ); - $this->setPermission("pocketmine.command.effect"); + $this->setPermission(DefaultPermissionNames::COMMAND_EFFECT); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -53,37 +54,31 @@ class EffectCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getPlayer($args[0]); + $player = $sender->getServer()->getPlayerByPrefix($args[0]); if($player === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } + $effectManager = $player->getEffects(); if(strtolower($args[1]) === "clear"){ - foreach($player->getEffects() as $effect){ - $player->removeEffect($effect->getId()); - } + $effectManager->clear(); - $sender->sendMessage(new TranslationContainer("commands.effect.success.removed.all", [$player->getDisplayName()])); + $sender->sendMessage(KnownTranslationFactory::commands_effect_success_removed_all($player->getDisplayName())); return true; } - $effect = Effect::getEffectByName($args[1]); - + $effect = StringToEffectParser::getInstance()->parse($args[1]); if($effect === null){ - $effect = Effect::getEffect((int) $args[1]); - } - - if($effect === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.effect.notFound", [$args[1]])); + $sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->prefix(TextFormat::RED)); return true; } $amplification = 0; if(count($args) >= 3){ - if(($d = $this->getBoundedInt($sender, $args[2], 0, (int) (INT32_MAX / 20))) === null){ + if(($d = $this->getBoundedInt($sender, $args[2], 0, (int) (Limits::INT32_MAX / 20))) === null){ return false; } $duration = $d * 20; //ticks @@ -107,21 +102,21 @@ class EffectCommand extends VanillaCommand{ } if($duration === 0){ - if(!$player->hasEffect($effect->getId())){ - if(count($player->getEffects()) === 0){ - $sender->sendMessage(new TranslationContainer("commands.effect.failure.notActive.all", [$player->getDisplayName()])); + if(!$effectManager->has($effect)){ + if(count($effectManager->all()) === 0){ + $sender->sendMessage(KnownTranslationFactory::commands_effect_failure_notActive_all($player->getDisplayName())); }else{ - $sender->sendMessage(new TranslationContainer("commands.effect.failure.notActive", [$effect->getName(), $player->getDisplayName()])); + $sender->sendMessage(KnownTranslationFactory::commands_effect_failure_notActive($effect->getName(), $player->getDisplayName())); } return true; } - $player->removeEffect($effect->getId()); - $sender->sendMessage(new TranslationContainer("commands.effect.success.removed", [$effect->getName(), $player->getDisplayName()])); + $effectManager->remove($effect); + $sender->sendMessage(KnownTranslationFactory::commands_effect_success_removed($effect->getName(), $player->getDisplayName())); }else{ $instance = new EffectInstance($effect, $duration, $amplification, $visible); - $player->addEffect($instance); - self::broadcastCommandMessage($sender, new TranslationContainer("%commands.effect.success", [$effect->getName(), $instance->getAmplifier(), $player->getDisplayName(), $instance->getDuration() / 20, $effect->getId()])); + $effectManager->add($instance); + self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_effect_success($effect->getName(), (string) $instance->getAmplifier(), $player->getDisplayName(), (string) ($instance->getDuration() / 20))); } return true; diff --git a/src/pocketmine/command/defaults/EnchantCommand.php b/src/command/defaults/EnchantCommand.php similarity index 66% rename from src/pocketmine/command/defaults/EnchantCommand.php rename to src/command/defaults/EnchantCommand.php index 74dd99d15d..3e53a55fb2 100644 --- a/src/pocketmine/command/defaults/EnchantCommand.php +++ b/src/command/defaults/EnchantCommand.php @@ -25,22 +25,22 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\item\enchantment\Enchantment; use pocketmine\item\enchantment\EnchantmentInstance; -use pocketmine\lang\TranslationContainer; +use pocketmine\item\enchantment\StringToEnchantmentParser; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\utils\TextFormat; use function count; -use function is_numeric; class EnchantCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.enchant.description", - "%commands.enchant.usage" + KnownTranslationFactory::pocketmine_command_enchant_description(), + KnownTranslationFactory::commands_enchant_usage() ); - $this->setPermission("pocketmine.command.enchant"); + $this->setPermission(DefaultPermissionNames::COMMAND_ENCHANT); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -52,28 +52,23 @@ class EnchantCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getPlayer($args[0]); + $player = $sender->getServer()->getPlayerByPrefix($args[0]); if($player === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } $item = $player->getInventory()->getItemInHand(); if($item->isNull()){ - $sender->sendMessage(new TranslationContainer("commands.enchant.noItem")); + $sender->sendMessage(KnownTranslationFactory::commands_enchant_noItem()); return true; } - if(is_numeric($args[1])){ - $enchantment = Enchantment::getEnchantment((int) $args[1]); - }else{ - $enchantment = Enchantment::getEnchantmentByName($args[1]); - } - - if(!($enchantment instanceof Enchantment)){ - $sender->sendMessage(new TranslationContainer("commands.enchant.notFound", [$args[1]])); + $enchantment = StringToEnchantmentParser::getInstance()->parse($args[1]); + if($enchantment === null){ + $sender->sendMessage(KnownTranslationFactory::commands_enchant_notFound($args[1])); return true; } @@ -88,7 +83,7 @@ class EnchantCommand extends VanillaCommand{ $item->addEnchantment(new EnchantmentInstance($enchantment, $level)); $player->getInventory()->setItemInHand($item); - self::broadcastCommandMessage($sender, new TranslationContainer("%commands.enchant.success", [$player->getName()])); + self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_enchant_success($player->getName())); return true; } } diff --git a/src/pocketmine/command/defaults/GamemodeCommand.php b/src/command/defaults/GamemodeCommand.php similarity index 56% rename from src/pocketmine/command/defaults/GamemodeCommand.php rename to src/command/defaults/GamemodeCommand.php index 6070d7866c..86658de6da 100644 --- a/src/pocketmine/command/defaults/GamemodeCommand.php +++ b/src/command/defaults/GamemodeCommand.php @@ -26,9 +26,10 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; -use pocketmine\Server; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\GameMode; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function count; @@ -37,10 +38,10 @@ class GamemodeCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.gamemode.description", - "%commands.gamemode.usage" + KnownTranslationFactory::pocketmine_command_gamemode_description(), + KnownTranslationFactory::commands_gamemode_usage() ); - $this->setPermission("pocketmine.command.gamemode"); + $this->setPermission(DefaultPermissionNames::COMMAND_GAMEMODE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -52,18 +53,16 @@ class GamemodeCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $gameMode = Server::getGamemodeFromString($args[0]); - - if($gameMode === -1){ - $sender->sendMessage("Unknown game mode"); - + $gameMode = GameMode::fromString($args[0]); + if($gameMode === null){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_unknown($args[0])); return true; } if(isset($args[1])){ - $target = $sender->getServer()->getPlayer($args[1]); + $target = $sender->getServer()->getPlayerByPrefix($args[1]); if($target === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } @@ -74,14 +73,14 @@ class GamemodeCommand extends VanillaCommand{ } $target->setGamemode($gameMode); - if($gameMode !== $target->getGamemode()){ - $sender->sendMessage("Game mode change for " . $target->getName() . " failed!"); + if(!$gameMode->equals($target->getGamemode())){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gamemode_failure($target->getName())); }else{ if($target === $sender){ - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.gamemode.success.self", [Server::getGamemodeString($gameMode)])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_gamemode_success_self($gameMode->getTranslatableName())); }else{ - $target->sendMessage(new TranslationContainer("gameMode.changed", [Server::getGamemodeString($gameMode)])); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.gamemode.success.other", [Server::getGamemodeString($gameMode), $target->getName()])); + $target->sendMessage(KnownTranslationFactory::gameMode_changed($gameMode->getTranslatableName())); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_gamemode_success_other($gameMode->getTranslatableName(), $target->getName())); } } diff --git a/src/command/defaults/GarbageCollectorCommand.php b/src/command/defaults/GarbageCollectorCommand.php new file mode 100644 index 0000000000..2e85ed0934 --- /dev/null +++ b/src/command/defaults/GarbageCollectorCommand.php @@ -0,0 +1,74 @@ +setPermission(DefaultPermissionNames::COMMAND_GC); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + + $chunksCollected = 0; + $entitiesCollected = 0; + + $memory = memory_get_usage(); + + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $diff = [count($world->getLoadedChunks()), count($world->getEntities())]; + $world->doChunkGarbageCollection(); + $world->unloadChunks(true); + $chunksCollected += $diff[0] - count($world->getLoadedChunks()); + $entitiesCollected += $diff[1] - count($world->getEntities()); + $world->clearCache(true); + } + + $cyclesCollected = $sender->getServer()->getMemoryManager()->triggerGarbageCollector(); + + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_header()->format(TextFormat::GREEN . "---- " . TextFormat::WHITE, TextFormat::GREEN . " ----" . TextFormat::WHITE)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_chunks(TextFormat::RED . number_format($chunksCollected))->prefix(TextFormat::GOLD)); + $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))->prefix(TextFormat::GOLD)); + return true; + } +} diff --git a/src/pocketmine/command/defaults/GiveCommand.php b/src/command/defaults/GiveCommand.php similarity index 55% rename from src/pocketmine/command/defaults/GiveCommand.php rename to src/command/defaults/GiveCommand.php index fc9a96b93a..8fd5c42983 100644 --- a/src/pocketmine/command/defaults/GiveCommand.php +++ b/src/command/defaults/GiveCommand.php @@ -26,10 +26,14 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\item\ItemFactory; -use pocketmine\lang\TranslationContainer; +use pocketmine\item\LegacyStringToItemParser; +use pocketmine\item\LegacyStringToItemParserException; +use pocketmine\item\StringToItemParser; +use pocketmine\lang\KnownTranslationFactory; use pocketmine\nbt\JsonNbtParser; -use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\NbtDataException; +use pocketmine\nbt\NbtException; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\utils\TextFormat; use function array_slice; use function count; @@ -40,10 +44,10 @@ class GiveCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.give.description", - "%pocketmine.command.give.usage" + KnownTranslationFactory::pocketmine_command_give_description(), + KnownTranslationFactory::pocketmine_command_give_usage() ); - $this->setPermission("pocketmine.command.give"); + $this->setPermission(DefaultPermissionNames::COMMAND_GIVE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -55,16 +59,16 @@ class GiveCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getPlayer($args[0]); + $player = $sender->getServer()->getPlayerByPrefix($args[0]); if($player === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } try{ - $item = ItemFactory::fromStringSingle($args[1]); - }catch(\InvalidArgumentException $e){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.give.item.notFound", [$args[1]])); + $item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]); + }catch(LegacyStringToItemParserException $e){ + $sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED)); return true; } @@ -75,30 +79,30 @@ class GiveCommand extends VanillaCommand{ } if(isset($args[3])){ - $tags = $exception = null; $data = implode(" ", array_slice($args, 3)); try{ $tags = JsonNbtParser::parseJson($data); - }catch(\Exception $ex){ - $exception = $ex; - } - - if(!($tags instanceof CompoundTag) or $exception !== null){ - $sender->sendMessage(new TranslationContainer("commands.give.tagError", [$exception !== null ? $exception->getMessage() : "Invalid tag conversion"])); + }catch(NbtDataException $e){ + $sender->sendMessage(KnownTranslationFactory::commands_give_tagError($e->getMessage())); return true; } - $item->setNamedTag($tags); + try{ + $item->setNamedTag($tags); + }catch(NbtException $e){ + $sender->sendMessage(KnownTranslationFactory::commands_give_tagError($e->getMessage())); + return true; + } } //TODO: overflow - $player->getInventory()->addItem(clone $item); + $player->getInventory()->addItem($item); - Command::broadcastCommandMessage($sender, new TranslationContainer("%commands.give.success", [ - $item->getName() . " (" . $item->getId() . ":" . $item->getDamage() . ")", + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_give_success( + $item->getName() . " (" . $item->getId() . ":" . $item->getMeta() . ")", (string) $item->getCount(), $player->getName() - ])); + )); return true; } } diff --git a/src/pocketmine/command/defaults/HelpCommand.php b/src/command/defaults/HelpCommand.php similarity index 60% rename from src/pocketmine/command/defaults/HelpCommand.php rename to src/command/defaults/HelpCommand.php index 5e761d1807..f16049a693 100644 --- a/src/pocketmine/command/defaults/HelpCommand.php +++ b/src/command/defaults/HelpCommand.php @@ -25,7 +25,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\lang\Translatable; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\utils\TextFormat; use function array_chunk; use function array_pop; @@ -44,11 +46,11 @@ class HelpCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.help.description", - "%commands.help.usage", + KnownTranslationFactory::pocketmine_command_help_description(), + KnownTranslationFactory::commands_help_usage(), ["?"] ); - $this->setPermission("pocketmine.command.help"); + $this->setPermission(DefaultPermissionNames::COMMAND_HELP); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -86,10 +88,13 @@ class HelpCommand extends VanillaCommand{ if($pageNumber < 1){ $pageNumber = 1; } - $sender->sendMessage(new TranslationContainer("commands.help.header", [$pageNumber, count($commands)])); + $sender->sendMessage(KnownTranslationFactory::commands_help_header((string) $pageNumber, (string) count($commands))); + $lang = $sender->getLanguage(); if(isset($commands[$pageNumber - 1])){ foreach($commands[$pageNumber - 1] as $command){ - $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getName() . ": " . TextFormat::WHITE . $command->getDescription()); + $description = $command->getDescription(); + $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; + $sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getName() . ": " . TextFormat::WHITE . $descriptionString); } } @@ -97,15 +102,23 @@ class HelpCommand extends VanillaCommand{ }else{ if(($cmd = $sender->getServer()->getCommandMap()->getCommand(strtolower($commandName))) instanceof Command){ if($cmd->testPermissionSilent($sender)){ - $message = TextFormat::YELLOW . "--------- " . TextFormat::WHITE . " Help: /" . $cmd->getName() . TextFormat::YELLOW . " ---------\n"; - $message .= TextFormat::GOLD . "Description: " . TextFormat::WHITE . $cmd->getDescription() . "\n"; - $message .= TextFormat::GOLD . "Usage: " . TextFormat::WHITE . implode("\n" . TextFormat::WHITE, explode("\n", $cmd->getUsage())) . "\n"; - $sender->sendMessage($message); + $lang = $sender->getLanguage(); + $description = $cmd->getDescription(); + $descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description; + $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; + $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/pocketmine/command/defaults/KickCommand.php b/src/command/defaults/KickCommand.php similarity index 64% rename from src/pocketmine/command/defaults/KickCommand.php rename to src/command/defaults/KickCommand.php index aebc688ca6..a341ca64b0 100644 --- a/src/pocketmine/command/defaults/KickCommand.php +++ b/src/command/defaults/KickCommand.php @@ -26,8 +26,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function array_shift; use function count; @@ -39,10 +40,10 @@ class KickCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.kick.description", - "%commands.kick.usage" + KnownTranslationFactory::pocketmine_command_kick_description(), + KnownTranslationFactory::commands_kick_usage() ); - $this->setPermission("pocketmine.command.kick"); + $this->setPermission(DefaultPermissionNames::COMMAND_KICK); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -57,15 +58,15 @@ class KickCommand extends VanillaCommand{ $name = array_shift($args); $reason = trim(implode(" ", $args)); - if(($player = $sender->getServer()->getPlayer($name)) instanceof Player){ - $player->kick($reason); + if(($player = $sender->getServer()->getPlayerByPrefix($name)) instanceof Player){ + $player->kick("Kicked by admin." . ($reason !== "" ? "Reason: " . $reason : "")); if($reason !== ""){ - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.kick.success.reason", [$player->getName(), $reason])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kick_success_reason($player->getName(), $reason)); }else{ - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.kick.success", [$player->getName()])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kick_success($player->getName())); } }else{ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); } return true; diff --git a/src/pocketmine/command/defaults/KillCommand.php b/src/command/defaults/KillCommand.php similarity index 64% rename from src/pocketmine/command/defaults/KillCommand.php rename to src/command/defaults/KillCommand.php index b6326aedd7..f425060e3c 100644 --- a/src/pocketmine/command/defaults/KillCommand.php +++ b/src/command/defaults/KillCommand.php @@ -27,21 +27,23 @@ use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; use pocketmine\event\entity\EntityDamageEvent; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function count; +use function implode; class KillCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.kill.description", - "%pocketmine.command.kill.usage", + KnownTranslationFactory::pocketmine_command_kill_description(), + KnownTranslationFactory::pocketmine_command_kill_usage(), ["suicide"] ); - $this->setPermission("pocketmine.command.kill.self;pocketmine.command.kill.other"); + $this->setPermission(implode(";", [DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER])); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -54,33 +56,29 @@ class KillCommand extends VanillaCommand{ } if(count($args) === 1){ - if(!$sender->hasPermission("pocketmine.command.kill.other")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_KILL_OTHER)){ return true; } - $player = $sender->getServer()->getPlayer($args[0]); + $player = $sender->getServer()->getPlayerByPrefix($args[0]); if($player instanceof Player){ $player->attack(new EntityDamageEvent($player, EntityDamageEvent::CAUSE_SUICIDE, 1000)); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.kill.successful", [$player->getName()])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kill_successful($player->getName())); }else{ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); } return true; } if($sender instanceof Player){ - if(!$sender->hasPermission("pocketmine.command.kill.self")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_KILL_SELF)){ return true; } $sender->attack(new EntityDamageEvent($sender, EntityDamageEvent::CAUSE_SUICIDE, 1000)); - $sender->sendMessage(new TranslationContainer("commands.kill.successful", [$sender->getName()])); + $sender->sendMessage(KnownTranslationFactory::commands_kill_successful($sender->getName())); }else{ throw new InvalidCommandSyntaxException(); } diff --git a/src/pocketmine/command/defaults/ListCommand.php b/src/command/defaults/ListCommand.php similarity index 76% rename from src/pocketmine/command/defaults/ListCommand.php rename to src/command/defaults/ListCommand.php index e5afd28693..a19130e3f0 100644 --- a/src/pocketmine/command/defaults/ListCommand.php +++ b/src/command/defaults/ListCommand.php @@ -24,8 +24,9 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use function array_filter; use function array_map; use function count; @@ -38,10 +39,9 @@ class ListCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.list.description", - "%command.players.usage" + KnownTranslationFactory::pocketmine_command_list_description() ); - $this->setPermission("pocketmine.command.list"); + $this->setPermission(DefaultPermissionNames::COMMAND_LIST); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -52,11 +52,11 @@ 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 $player->isOnline() and (!($sender instanceof Player) or $sender->canSee($player)); + return !($sender instanceof Player) or $sender->canSee($player); })); sort($playerNames, SORT_STRING); - $sender->sendMessage(new TranslationContainer("commands.players.list", [count($playerNames), $sender->getServer()->getMaxPlayers()])); + $sender->sendMessage(KnownTranslationFactory::commands_players_list((string) count($playerNames), (string) $sender->getServer()->getMaxPlayers())); $sender->sendMessage(implode(", ", $playerNames)); return true; diff --git a/src/pocketmine/command/defaults/MeCommand.php b/src/command/defaults/MeCommand.php similarity index 72% rename from src/pocketmine/command/defaults/MeCommand.php rename to src/command/defaults/MeCommand.php index d9aadd7f44..904b39fe40 100644 --- a/src/pocketmine/command/defaults/MeCommand.php +++ b/src/command/defaults/MeCommand.php @@ -25,8 +25,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function count; use function implode; @@ -36,10 +37,10 @@ class MeCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.me.description", - "%commands.me.usage" + KnownTranslationFactory::pocketmine_command_me_description(), + KnownTranslationFactory::commands_me_usage() ); - $this->setPermission("pocketmine.command.me"); + $this->setPermission(DefaultPermissionNames::COMMAND_ME); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -51,7 +52,7 @@ class MeCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $sender->getServer()->broadcastMessage(new TranslationContainer("chat.type.emote", [$sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::WHITE . implode(" ", $args)])); + $sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_emote($sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::WHITE . implode(" ", $args))); return true; } diff --git a/src/pocketmine/command/defaults/OpCommand.php b/src/command/defaults/OpCommand.php similarity index 70% rename from src/pocketmine/command/defaults/OpCommand.php rename to src/command/defaults/OpCommand.php index 5f677a4abe..4067eb3e74 100644 --- a/src/pocketmine/command/defaults/OpCommand.php +++ b/src/command/defaults/OpCommand.php @@ -26,8 +26,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function array_shift; use function count; @@ -37,10 +38,10 @@ class OpCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.op.description", - "%commands.op.usage" + KnownTranslationFactory::pocketmine_command_op_description(), + KnownTranslationFactory::commands_op_usage() ); - $this->setPermission("pocketmine.command.op.give"); + $this->setPermission(DefaultPermissionNames::COMMAND_OP_GIVE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -57,12 +58,11 @@ class OpCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getOfflinePlayer($name); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.op.success", [$player->getName()])); - if($player instanceof Player){ - $player->sendMessage(TextFormat::GRAY . "You are now op!"); + $sender->getServer()->addOp($name); + if(($player = $sender->getServer()->getPlayerExact($name)) !== null){ + $player->sendMessage(KnownTranslationFactory::commands_op_message()->prefix(TextFormat::GRAY)); } - $player->setOp(true); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_op_success($name)); return true; } } diff --git a/src/pocketmine/command/defaults/PardonCommand.php b/src/command/defaults/PardonCommand.php similarity index 77% rename from src/pocketmine/command/defaults/PardonCommand.php rename to src/command/defaults/PardonCommand.php index f46c0b4858..842cbe3aec 100644 --- a/src/pocketmine/command/defaults/PardonCommand.php +++ b/src/command/defaults/PardonCommand.php @@ -26,7 +26,8 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use function count; class PardonCommand extends VanillaCommand{ @@ -34,11 +35,11 @@ class PardonCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.unban.player.description", - "%commands.unban.usage", + KnownTranslationFactory::pocketmine_command_unban_player_description(), + KnownTranslationFactory::commands_unban_usage(), ["unban"] ); - $this->setPermission("pocketmine.command.unban.player"); + $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -52,7 +53,7 @@ class PardonCommand extends VanillaCommand{ $sender->getServer()->getNameBans()->remove($args[0]); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.unban.success", [$args[0]])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_unban_success($args[0])); return true; } diff --git a/src/pocketmine/command/defaults/PardonIpCommand.php b/src/command/defaults/PardonIpCommand.php similarity index 72% rename from src/pocketmine/command/defaults/PardonIpCommand.php rename to src/command/defaults/PardonIpCommand.php index 0133f046db..f0b2c9aa51 100644 --- a/src/pocketmine/command/defaults/PardonIpCommand.php +++ b/src/command/defaults/PardonIpCommand.php @@ -26,20 +26,21 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use function count; -use function preg_match; +use function inet_pton; class PardonIpCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.unban.ip.description", - "%commands.unbanip.usage", + KnownTranslationFactory::pocketmine_command_unban_ip_description(), + KnownTranslationFactory::commands_unbanip_usage(), ["unban-ip"] ); - $this->setPermission("pocketmine.command.unban.ip"); + $this->setPermission(DefaultPermissionNames::COMMAND_UNBAN_IP); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -51,12 +52,12 @@ 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, new TranslationContainer("commands.unbanip.success", [$args[0]])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_unbanip_success($args[0])); }else{ - $sender->sendMessage(new TranslationContainer("commands.unbanip.invalid")); + $sender->sendMessage(KnownTranslationFactory::commands_unbanip_invalid()); } return true; diff --git a/src/command/defaults/ParticleCommand.php b/src/command/defaults/ParticleCommand.php new file mode 100644 index 0000000000..ee7af129a0 --- /dev/null +++ b/src/command/defaults/ParticleCommand.php @@ -0,0 +1,230 @@ +setPermission(DefaultPermissionNames::COMMAND_PARTICLE); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + + if(count($args) < 7){ + throw new InvalidCommandSyntaxException(); + } + + if($sender instanceof Player){ + $senderPos = $sender->getPosition(); + $world = $senderPos->getWorld(); + $pos = new Vector3( + $this->getRelativeDouble($senderPos->getX(), $sender, $args[1]), + $this->getRelativeDouble($senderPos->getY(), $sender, $args[2], World::Y_MIN, World::Y_MAX), + $this->getRelativeDouble($senderPos->getZ(), $sender, $args[3]) + ); + }else{ + $world = $sender->getServer()->getWorldManager()->getDefaultWorld(); + $pos = new Vector3((float) $args[1], (float) $args[2], (float) $args[3]); + } + + $name = strtolower($args[0]); + + $xd = (float) $args[4]; + $yd = (float) $args[5]; + $zd = (float) $args[6]; + + $count = isset($args[7]) ? max(1, (int) $args[7]) : 1; + + $data = isset($args[8]) ? (int) $args[8] : null; + + $particle = $this->getParticle($name, $data); + + if($particle === null){ + $sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->prefix(TextFormat::RED)); + return true; + } + + $sender->sendMessage(KnownTranslationFactory::commands_particle_success($name, (string) $count)); + + $random = new Random((int) (microtime(true) * 1000) + mt_rand()); + + for($i = 0; $i < $count; ++$i){ + $world->addParticle($pos->add( + $random->nextSignedFloat() * $xd, + $random->nextSignedFloat() * $yd, + $random->nextSignedFloat() * $zd + ), $particle); + } + + return true; + } + + private function getParticle(string $name, ?int $data = null) : ?Particle{ + switch($name){ + case "explode": + return new ExplodeParticle(); + case "hugeexplosion": + return new HugeExplodeParticle(); + case "hugeexplosionseed": + return new HugeExplodeSeedParticle(); + case "bubble": + return new BubbleParticle(); + case "splash": + return new SplashParticle(); + case "wake": + case "water": + return new WaterParticle(); + case "crit": + return new CriticalParticle(); + case "smoke": + return new SmokeParticle($data ?? 0); + case "spell": + return new EnchantParticle(new Color(0, 0, 0, 255)); //TODO: colour support + case "instantspell": + return new InstantEnchantParticle(new Color(0, 0, 0, 255)); //TODO: colour support + case "dripwater": + return new WaterDripParticle(); + case "driplava": + return new LavaDripParticle(); + case "townaura": + case "spore": + return new SporeParticle(); + case "portal": + return new PortalParticle(); + case "flame": + return new FlameParticle(); + case "lava": + return new LavaParticle(); + case "reddust": + return new RedstoneParticle($data ?? 1); + case "snowballpoof": + return new ItemBreakParticle(VanillaItems::SNOWBALL()); + case "slime": + return new ItemBreakParticle(VanillaItems::SLIMEBALL()); + case "itembreak": + if($data !== null and $data !== 0){ + return new ItemBreakParticle(ItemFactory::getInstance()->get($data)); + } + break; + case "terrain": + if($data !== null and $data !== 0){ + return new TerrainParticle(BlockFactory::getInstance()->get($data, 0)); + } + break; + case "heart": + return new HeartParticle($data ?? 0); + case "ink": + return new InkParticle($data ?? 0); + case "droplet": + return new RainSplashParticle(); + case "enchantmenttable": + return new EnchantmentTableParticle(); + case "happyvillager": + return new HappyVillagerParticle(); + case "angryvillager": + return new AngryVillagerParticle(); + case "forcefield": + return new BlockForceFieldParticle($data ?? 0); + case "mobflame": + return new EntityFlameParticle(); + } + + if(strpos($name, "iconcrack_") === 0){ + $d = explode("_", $name); + if(count($d) === 3){ + return new ItemBreakParticle(ItemFactory::getInstance()->get((int) $d[1], (int) $d[2])); + } + }elseif(strpos($name, "blockcrack_") === 0){ + $d = explode("_", $name); + if(count($d) === 2){ + return new TerrainParticle(BlockFactory::getInstance()->get(((int) $d[1]) & 0xff, ((int) $d[1]) >> 12)); + } + }elseif(strpos($name, "blockdust_") === 0){ + $d = explode("_", $name); + if(count($d) >= 4){ + return new DustParticle(new Color(((int) $d[1]) & 0xff, ((int) $d[2]) & 0xff, ((int) $d[3]) & 0xff, isset($d[4]) ? ((int) $d[4]) & 0xff : 255)); + } + } + + return null; + } +} diff --git a/src/pocketmine/command/defaults/PluginsCommand.php b/src/command/defaults/PluginsCommand.php similarity index 79% rename from src/pocketmine/command/defaults/PluginsCommand.php rename to src/command/defaults/PluginsCommand.php index 993ea0bbe4..2b4ef9ad63 100644 --- a/src/pocketmine/command/defaults/PluginsCommand.php +++ b/src/command/defaults/PluginsCommand.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\plugin\Plugin; use pocketmine\utils\TextFormat; use function array_map; @@ -38,11 +39,11 @@ class PluginsCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.plugins.description", - "%pocketmine.command.plugins.usage", + KnownTranslationFactory::pocketmine_command_plugins_description(), + null, ["pl"] ); - $this->setPermission("pocketmine.command.plugins"); + $this->setPermission(DefaultPermissionNames::COMMAND_PLUGINS); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -55,7 +56,7 @@ class PluginsCommand extends VanillaCommand{ }, $sender->getServer()->getPluginManager()->getPlugins()); sort($list, SORT_STRING); - $sender->sendMessage(new TranslationContainer("pocketmine.command.plugins.success", [count($list), implode(TextFormat::WHITE . ", ", $list)])); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_plugins_success((string) count($list), implode(TextFormat::WHITE . ", ", $list))); return true; } } diff --git a/src/pocketmine/command/defaults/SaveCommand.php b/src/command/defaults/SaveCommand.php similarity index 69% rename from src/pocketmine/command/defaults/SaveCommand.php rename to src/command/defaults/SaveCommand.php index 7347d41cd0..ae31cb18a5 100644 --- a/src/pocketmine/command/defaults/SaveCommand.php +++ b/src/command/defaults/SaveCommand.php @@ -25,7 +25,8 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use function microtime; use function round; @@ -34,10 +35,9 @@ class SaveCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.save.description", - "%commands.save.usage" + KnownTranslationFactory::pocketmine_command_save_description() ); - $this->setPermission("pocketmine.command.save.perform"); + $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_PERFORM); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -45,18 +45,18 @@ class SaveCommand extends VanillaCommand{ return true; } - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.save.start")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_start()); $start = microtime(true); foreach($sender->getServer()->getOnlinePlayers() as $player){ $player->save(); } - foreach($sender->getServer()->getLevels() as $level){ - $level->save(true); + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->save(true); } - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.save.success", [round(microtime(true) - $start, 3)])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_save_success((string) round(microtime(true) - $start, 3))); return true; } diff --git a/src/pocketmine/command/defaults/SaveOffCommand.php b/src/command/defaults/SaveOffCommand.php similarity index 74% rename from src/pocketmine/command/defaults/SaveOffCommand.php rename to src/command/defaults/SaveOffCommand.php index c81d169056..223d81e64c 100644 --- a/src/pocketmine/command/defaults/SaveOffCommand.php +++ b/src/command/defaults/SaveOffCommand.php @@ -25,17 +25,17 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; class SaveOffCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.saveoff.description", - "%commands.save-off.usage" + KnownTranslationFactory::pocketmine_command_saveoff_description() ); - $this->setPermission("pocketmine.command.save.disable"); + $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_DISABLE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -43,9 +43,9 @@ class SaveOffCommand extends VanillaCommand{ return true; } - $sender->getServer()->setAutoSave(false); + $sender->getServer()->getWorldManager()->setAutoSave(false); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.disabled")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_save_disabled()); return true; } diff --git a/src/pocketmine/command/defaults/SaveOnCommand.php b/src/command/defaults/SaveOnCommand.php similarity index 74% rename from src/pocketmine/command/defaults/SaveOnCommand.php rename to src/command/defaults/SaveOnCommand.php index 8962baa2bb..0999181a98 100644 --- a/src/pocketmine/command/defaults/SaveOnCommand.php +++ b/src/command/defaults/SaveOnCommand.php @@ -25,17 +25,17 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; class SaveOnCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.saveon.description", - "%commands.save-on.usage" + KnownTranslationFactory::pocketmine_command_saveon_description() ); - $this->setPermission("pocketmine.command.save.enable"); + $this->setPermission(DefaultPermissionNames::COMMAND_SAVE_ENABLE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -43,9 +43,9 @@ class SaveOnCommand extends VanillaCommand{ return true; } - $sender->getServer()->setAutoSave(true); + $sender->getServer()->getWorldManager()->setAutoSave(true); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.enabled")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_save_enabled()); return true; } diff --git a/src/pocketmine/command/defaults/SayCommand.php b/src/command/defaults/SayCommand.php similarity index 67% rename from src/pocketmine/command/defaults/SayCommand.php rename to src/command/defaults/SayCommand.php index 841b1bb041..29513280eb 100644 --- a/src/pocketmine/command/defaults/SayCommand.php +++ b/src/command/defaults/SayCommand.php @@ -24,10 +24,11 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; -use pocketmine\command\ConsoleCommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\console\ConsoleCommandSender; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function count; use function implode; @@ -37,10 +38,10 @@ class SayCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.say.description", - "%commands.say.usage" + KnownTranslationFactory::pocketmine_command_say_description(), + KnownTranslationFactory::commands_say_usage() ); - $this->setPermission("pocketmine.command.say"); + $this->setPermission(DefaultPermissionNames::COMMAND_SAY); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -52,7 +53,10 @@ class SayCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $sender->getServer()->broadcastMessage(new TranslationContainer(TextFormat::LIGHT_PURPLE . "%chat.type.announcement", [$sender instanceof Player ? $sender->getDisplayName() : ($sender instanceof ConsoleCommandSender ? "Server" : $sender->getName()), TextFormat::LIGHT_PURPLE . implode(" ", $args)])); + $sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_announcement( + $sender instanceof Player ? $sender->getDisplayName() : ($sender instanceof ConsoleCommandSender ? "Server" : $sender->getName()), + implode(" ", $args) + )->prefix(TextFormat::LIGHT_PURPLE)); return true; } } diff --git a/src/pocketmine/command/defaults/SeedCommand.php b/src/command/defaults/SeedCommand.php similarity index 70% rename from src/pocketmine/command/defaults/SeedCommand.php rename to src/command/defaults/SeedCommand.php index 74cf328faa..50e1061457 100644 --- a/src/pocketmine/command/defaults/SeedCommand.php +++ b/src/command/defaults/SeedCommand.php @@ -24,18 +24,18 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; class SeedCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.seed.description", - "%commands.seed.usage" + KnownTranslationFactory::pocketmine_command_seed_description() ); - $this->setPermission("pocketmine.command.seed"); + $this->setPermission(DefaultPermissionNames::COMMAND_SEED); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -44,11 +44,11 @@ class SeedCommand extends VanillaCommand{ } if($sender instanceof Player){ - $seed = $sender->getLevelNonNull()->getSeed(); + $seed = $sender->getPosition()->getWorld()->getSeed(); }else{ - $seed = $sender->getServer()->getDefaultLevel()->getSeed(); + $seed = $sender->getServer()->getWorldManager()->getDefaultWorld()->getSeed(); } - $sender->sendMessage(new TranslationContainer("commands.seed.success", [$seed])); + $sender->sendMessage(KnownTranslationFactory::commands_seed_success((string) $seed)); return true; } diff --git a/src/pocketmine/command/defaults/SetWorldSpawnCommand.php b/src/command/defaults/SetWorldSpawnCommand.php similarity index 57% rename from src/pocketmine/command/defaults/SetWorldSpawnCommand.php rename to src/command/defaults/SetWorldSpawnCommand.php index 6493ec2967..96582a3490 100644 --- a/src/pocketmine/command/defaults/SetWorldSpawnCommand.php +++ b/src/command/defaults/SetWorldSpawnCommand.php @@ -26,22 +26,23 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; use pocketmine\math\Vector3; -use pocketmine\Player; +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{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.setworldspawn.description", - "%commands.setworldspawn.usage" + KnownTranslationFactory::pocketmine_command_setworldspawn_description(), + KnownTranslationFactory::commands_setworldspawn_usage() ); - $this->setPermission("pocketmine.command.setworldspawn"); + $this->setPermission(DefaultPermissionNames::COMMAND_SETWORLDSPAWN); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -51,23 +52,34 @@ class SetWorldSpawnCommand extends VanillaCommand{ if(count($args) === 0){ if($sender instanceof Player){ - $level = $sender->getLevelNonNull(); - $pos = (new Vector3($sender->x, $sender->y, $sender->z))->round(); + $location = $sender->getPosition(); + $world = $location->getWorld(); + $pos = $location->asVector3()->floor(); }else{ $sender->sendMessage(TextFormat::RED . "You can only perform this command as a player"); return true; } }elseif(count($args) === 3){ - $level = $sender->getServer()->getDefaultLevel(); - $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(); } - $level->setSpawnLocation($pos); + $world->setSpawnLocation($pos); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.setworldspawn.success", [round($pos->x, 2), round($pos->y, 2), 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/pocketmine/command/defaults/SpawnpointCommand.php b/src/command/defaults/SpawnpointCommand.php similarity index 55% rename from src/pocketmine/command/defaults/SpawnpointCommand.php rename to src/command/defaults/SpawnpointCommand.php index 13c7e6d337..a99d4a49ef 100644 --- a/src/pocketmine/command/defaults/SpawnpointCommand.php +++ b/src/command/defaults/SpawnpointCommand.php @@ -26,11 +26,12 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\level\Level; -use pocketmine\level\Position; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; +use pocketmine\world\Position; +use pocketmine\world\World; use function count; use function round; @@ -39,10 +40,10 @@ class SpawnpointCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.spawnpoint.description", - "%commands.spawnpoint.usage" + KnownTranslationFactory::pocketmine_command_spawnpoint_description(), + KnownTranslationFactory::commands_spawnpoint_usage() ); - $this->setPermission("pocketmine.command.spawnpoint"); + $this->setPermission(DefaultPermissionNames::COMMAND_SPAWNPOINT); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -61,33 +62,32 @@ class SpawnpointCommand extends VanillaCommand{ return true; } }else{ - $target = $sender->getServer()->getPlayer($args[0]); + $target = $sender->getServer()->getPlayerByPrefix($args[0]); if($target === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } } if(count($args) === 4){ - if($target->isValid()){ - $level = $target->getLevelNonNull(); - $pos = $sender instanceof Player ? $sender->getPosition() : $level->getSpawnLocation(); - $x = $this->getRelativeDouble($pos->x, $sender, $args[1]); - $y = $this->getRelativeDouble($pos->y, $sender, $args[2], 0, Level::Y_MAX); - $z = $this->getRelativeDouble($pos->z, $sender, $args[3]); - $target->setSpawn(new Position($x, $y, $z, $level)); + $world = $target->getWorld(); + $pos = $sender instanceof Player ? $sender->getPosition() : $world->getSpawnLocation(); + $x = $this->getRelativeDouble($pos->x, $sender, $args[1]); + $y = $this->getRelativeDouble($pos->y, $sender, $args[2], World::Y_MIN, World::Y_MAX); + $z = $this->getRelativeDouble($pos->z, $sender, $args[3]); + $target->setSpawn(new Position($x, $y, $z, $world)); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.spawnpoint.success", [$target->getName(), round($x, 2), round($y, 2), round($z, 2)])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_spawnpoint_success($target->getName(), (string) round($x, 2), (string) round($y, 2), (string) round($z, 2))); - return true; - } + return true; }elseif(count($args) <= 1){ if($sender instanceof Player){ - $pos = new Position($sender->getFloorX(), $sender->getFloorY(), $sender->getFloorZ(), $sender->getLevelNonNull()); + $cpos = $sender->getPosition(); + $pos = Position::fromObject($cpos->floor(), $cpos->getWorld()); $target->setSpawn($pos); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.spawnpoint.success", [$target->getName(), round($pos->x, 2), round($pos->y, 2), round($pos->z, 2)])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_spawnpoint_success($target->getName(), (string) round($pos->x, 2), (string) round($pos->y, 2), (string) round($pos->z, 2))); return true; }else{ $sender->sendMessage(TextFormat::RED . "Please provide a player!"); diff --git a/src/pocketmine/command/defaults/StatusCommand.php b/src/command/defaults/StatusCommand.php similarity index 72% rename from src/pocketmine/command/defaults/StatusCommand.php rename to src/command/defaults/StatusCommand.php index 8dab49b7a3..05dfc7abf3 100644 --- a/src/pocketmine/command/defaults/StatusCommand.php +++ b/src/command/defaults/StatusCommand.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\utils\Process; use pocketmine\utils\TextFormat; use function count; @@ -37,10 +39,9 @@ class StatusCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.status.description", - "%pocketmine.command.status.usage" + KnownTranslationFactory::pocketmine_command_status_description() ); - $this->setPermission("pocketmine.command.status"); + $this->setPermission(DefaultPermissionNames::COMMAND_STATUS); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -48,13 +49,12 @@ class StatusCommand extends VanillaCommand{ return true; } - $rUsage = Process::getRealMemoryUsage(); $mUsage = Process::getAdvancedMemoryUsage(); $server = $sender->getServer(); $sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::WHITE . "Server status" . TextFormat::GREEN . " ----"); - $time = (int) (microtime(true) - \pocketmine\START_TIME); + $time = (int) (microtime(true) - $server->getStartTime()); $seconds = $time % 60; $minutes = null; @@ -91,27 +91,28 @@ class StatusCommand extends VanillaCommand{ $sender->sendMessage(TextFormat::GOLD . "Current TPS: {$tpsColor}{$server->getTicksPerSecond()} ({$server->getTickUsage()}%)"); $sender->sendMessage(TextFormat::GOLD . "Average TPS: {$tpsColor}{$server->getTicksPerSecondAverage()} ({$server->getTickUsageAverage()}%)"); - $sender->sendMessage(TextFormat::GOLD . "Network upload: " . TextFormat::RED . round($server->getNetwork()->getUpload() / 1024, 2) . " kB/s"); - $sender->sendMessage(TextFormat::GOLD . "Network download: " . TextFormat::RED . round($server->getNetwork()->getDownload() / 1024, 2) . " kB/s"); + $bandwidth = $server->getNetwork()->getBandwidthTracker(); + $sender->sendMessage(TextFormat::GOLD . "Network upload: " . TextFormat::RED . round($bandwidth->getSend()->getAverageBytes() / 1024, 2) . " kB/s"); + $sender->sendMessage(TextFormat::GOLD . "Network download: " . TextFormat::RED . round($bandwidth->getReceive()->getAverageBytes() / 1024, 2) . " kB/s"); $sender->sendMessage(TextFormat::GOLD . "Thread count: " . TextFormat::RED . Process::getThreadCount()); $sender->sendMessage(TextFormat::GOLD . "Main thread memory: " . TextFormat::RED . number_format(round(($mUsage[0] / 1024) / 1024, 2), 2) . " MB."); $sender->sendMessage(TextFormat::GOLD . "Total memory: " . TextFormat::RED . number_format(round(($mUsage[1] / 1024) / 1024, 2), 2) . " MB."); $sender->sendMessage(TextFormat::GOLD . "Total virtual memory: " . TextFormat::RED . number_format(round(($mUsage[2] / 1024) / 1024, 2), 2) . " MB."); - $sender->sendMessage(TextFormat::GOLD . "Heap memory: " . TextFormat::RED . number_format(round(($rUsage[0] / 1024) / 1024, 2), 2) . " MB."); - if($server->getProperty("memory.global-limit") > 0){ - $sender->sendMessage(TextFormat::GOLD . "Maximum memory (manager): " . TextFormat::RED . number_format(round($server->getProperty("memory.global-limit"), 2), 2) . " MB."); + $globalLimit = $server->getMemoryManager()->getGlobalMemoryLimit(); + if($globalLimit > 0){ + $sender->sendMessage(TextFormat::GOLD . "Maximum memory (manager): " . TextFormat::RED . number_format(round($globalLimit, 2), 2) . " MB."); } - foreach($server->getLevels() as $level){ - $levelName = $level->getFolderName() !== $level->getName() ? " (" . $level->getName() . ")" : ""; - $timeColor = $level->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW; - $sender->sendMessage(TextFormat::GOLD . "World \"{$level->getFolderName()}\"$levelName: " . - TextFormat::RED . number_format(count($level->getChunks())) . TextFormat::GREEN . " chunks, " . - TextFormat::RED . number_format(count($level->getEntities())) . TextFormat::GREEN . " entities. " . - "Time $timeColor" . round($level->getTickRateTime(), 2) . "ms" + foreach($server->getWorldManager()->getWorlds() as $world){ + $worldName = $world->getFolderName() !== $world->getDisplayName() ? " (" . $world->getDisplayName() . ")" : ""; + $timeColor = $world->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW; + $sender->sendMessage(TextFormat::GOLD . "World \"{$world->getFolderName()}\"$worldName: " . + TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " chunks, " . + TextFormat::RED . number_format(count($world->getEntities())) . TextFormat::GREEN . " entities. " . + "Time $timeColor" . round($world->getTickRateTime(), 2) . "ms" ); } diff --git a/src/pocketmine/command/defaults/StopCommand.php b/src/command/defaults/StopCommand.php similarity index 78% rename from src/pocketmine/command/defaults/StopCommand.php rename to src/command/defaults/StopCommand.php index cc5e2f1bf9..2562087141 100644 --- a/src/pocketmine/command/defaults/StopCommand.php +++ b/src/command/defaults/StopCommand.php @@ -25,17 +25,17 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; class StopCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.stop.description", - "%commands.stop.usage" + KnownTranslationFactory::pocketmine_command_stop_description() ); - $this->setPermission("pocketmine.command.stop"); + $this->setPermission(DefaultPermissionNames::COMMAND_STOP); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -43,7 +43,7 @@ class StopCommand extends VanillaCommand{ return true; } - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.stop.start")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_stop_start()); $sender->getServer()->shutdown(); diff --git a/src/pocketmine/command/defaults/TeleportCommand.php b/src/command/defaults/TeleportCommand.php similarity index 77% rename from src/pocketmine/command/defaults/TeleportCommand.php rename to src/command/defaults/TeleportCommand.php index 0d65d8f5b0..4748dc2d3d 100644 --- a/src/pocketmine/command/defaults/TeleportCommand.php +++ b/src/command/defaults/TeleportCommand.php @@ -26,11 +26,13 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\level\Location; -use pocketmine\Player; +use pocketmine\entity\Location; +use pocketmine\lang\KnownTranslationFactory; +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; @@ -40,15 +42,15 @@ class TeleportCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.tp.description", - "%commands.tp.usage", + KnownTranslationFactory::pocketmine_command_tp_description(), + KnownTranslationFactory::commands_tp_usage(), ["teleport"] ); - $this->setPermission("pocketmine.command.teleport"); + $this->setPermission(DefaultPermissionNames::COMMAND_TELEPORT); } private function findPlayer(CommandSender $sender, string $playerName) : ?Player{ - $subject = $sender->getServer()->getPlayer($playerName); + $subject = $sender->getServer()->getPlayerByPrefix($playerName); if($subject === null){ $sender->sendMessage(TextFormat::RED . "Can't find player " . $playerName); return null; @@ -95,7 +97,7 @@ class TeleportCommand extends VanillaCommand{ } $subject->teleport($targetPlayer->getLocation()); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success", [$subject->getName(), $targetPlayer->getName()])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_tp_success($subject->getName(), $targetPlayer->getName())); return true; case 3: @@ -110,17 +112,17 @@ 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->getLevelNonNull()); + $targetLocation = new Location($x, $y, $z, $base->getWorld(), $yaw, $pitch); $subject->teleport($targetLocation); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.tp.success.coordinates", [ + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_tp_success_coordinates( $subject->getName(), - round($targetLocation->x, 2), - round($targetLocation->y, 2), - round($targetLocation->z, 2) - ])); + (string) round($targetLocation->x, 2), + (string) round($targetLocation->y, 2), + (string) round($targetLocation->z, 2) + )); return true; default: throw new AssumptionFailedError("This branch should be unreachable (for now)"); diff --git a/src/pocketmine/command/defaults/TellCommand.php b/src/command/defaults/TellCommand.php similarity index 58% rename from src/pocketmine/command/defaults/TellCommand.php rename to src/command/defaults/TellCommand.php index b81df78587..4e294af5bf 100644 --- a/src/pocketmine/command/defaults/TellCommand.php +++ b/src/command/defaults/TellCommand.php @@ -23,10 +23,12 @@ declare(strict_types=1); namespace pocketmine\command\defaults; +use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\utils\TextFormat; use function array_shift; use function count; @@ -37,11 +39,11 @@ class TellCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.tell.description", - "%commands.message.usage", + KnownTranslationFactory::pocketmine_command_tell_description(), + KnownTranslationFactory::commands_message_usage(), ["w", "msg"] ); - $this->setPermission("pocketmine.command.tell"); + $this->setPermission(DefaultPermissionNames::COMMAND_TELL); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -53,19 +55,21 @@ class TellCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getPlayer(array_shift($args)); + $player = $sender->getServer()->getPlayerByPrefix(array_shift($args)); if($player === $sender){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.message.sameTarget")); + $sender->sendMessage(KnownTranslationFactory::commands_message_sameTarget()->prefix(TextFormat::RED)); return true; } if($player instanceof Player){ - $sender->sendMessage("[{$sender->getName()} -> {$player->getDisplayName()}] " . implode(" ", $args)); + $message = implode(" ", $args); + $sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC)); $name = $sender instanceof Player ? $sender->getDisplayName() : $sender->getName(); - $player->sendMessage("[$name -> {$player->getName()}] " . implode(" ", $args)); + $player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC)); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message), false); }else{ - $sender->sendMessage(new TranslationContainer("commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()); } return true; diff --git a/src/command/defaults/TimeCommand.php b/src/command/defaults/TimeCommand.php new file mode 100644 index 0000000000..fcdd89dee4 --- /dev/null +++ b/src/command/defaults/TimeCommand.php @@ -0,0 +1,145 @@ +setPermission(implode(";", [ + DefaultPermissionNames::COMMAND_TIME_ADD, + DefaultPermissionNames::COMMAND_TIME_SET, + DefaultPermissionNames::COMMAND_TIME_START, + DefaultPermissionNames::COMMAND_TIME_STOP, + DefaultPermissionNames::COMMAND_TIME_QUERY + ])); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + if(count($args) < 1){ + throw new InvalidCommandSyntaxException(); + } + + if($args[0] === "start"){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_START)){ + return true; + } + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->startTime(); + } + Command::broadcastCommandMessage($sender, "Restarted the time"); + return true; + }elseif($args[0] === "stop"){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_STOP)){ + return true; + } + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->stopTime(); + } + Command::broadcastCommandMessage($sender, "Stopped the time"); + return true; + }elseif($args[0] === "query"){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_QUERY)){ + return true; + } + if($sender instanceof Player){ + $world = $sender->getWorld(); + }else{ + $world = $sender->getServer()->getWorldManager()->getDefaultWorld(); + } + $sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_time_query((string) $world->getTime()))); + return true; + } + + if(count($args) < 2){ + throw new InvalidCommandSyntaxException(); + } + + if($args[0] === "set"){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_SET)){ + return true; + } + + switch($args[1]){ + case "day": + $value = World::TIME_DAY; + break; + case "noon": + $value = World::TIME_NOON; + break; + case "sunset": + $value = World::TIME_SUNSET; + break; + case "night": + $value = World::TIME_NIGHT; + break; + case "midnight": + $value = World::TIME_MIDNIGHT; + break; + case "sunrise": + $value = World::TIME_SUNRISE; + break; + default: + $value = $this->getInteger($sender, $args[1], 0); + break; + } + + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->setTime($value); + } + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_time_set((string) $value)); + }elseif($args[0] === "add"){ + if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_TIME_ADD)){ + return true; + } + + $value = $this->getInteger($sender, $args[1], 0); + foreach($sender->getServer()->getWorldManager()->getWorlds() as $world){ + $world->setTime($world->getTime() + $value); + } + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_time_added((string) $value)); + }else{ + throw new InvalidCommandSyntaxException(); + } + + return true; + } +} diff --git a/src/pocketmine/command/defaults/TimingsCommand.php b/src/command/defaults/TimingsCommand.php similarity index 54% rename from src/pocketmine/command/defaults/TimingsCommand.php rename to src/command/defaults/TimingsCommand.php index 28e5cddad7..26839b3123 100644 --- a/src/pocketmine/command/defaults/TimingsCommand.php +++ b/src/command/defaults/TimingsCommand.php @@ -26,17 +26,21 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use pocketmine\scheduler\BulkCurlTask; -use pocketmine\Server; +use pocketmine\scheduler\BulkCurlTaskOperation; use pocketmine\timings\TimingsHandler; use pocketmine\utils\InternetException; +use pocketmine\utils\InternetRequestResult; +use Webmozart\PathUtil\Path; use function count; use function fclose; use function file_exists; use function fopen; use function fseek; +use function fwrite; use function http_build_query; use function is_array; use function json_decode; @@ -48,16 +52,17 @@ use const CURLOPT_FOLLOWLOCATION; use const CURLOPT_HTTPHEADER; use const CURLOPT_POST; use const CURLOPT_POSTFIELDS; +use const PHP_EOL; class TimingsCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.timings.description", - "%pocketmine.command.timings.usage" + KnownTranslationFactory::pocketmine_command_timings_description(), + KnownTranslationFactory::pocketmine_command_timings_usage() ); - $this->setPermission("pocketmine.command.timings"); + $this->setPermission(DefaultPermissionNames::COMMAND_TIMINGS); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -72,18 +77,22 @@ class TimingsCommand extends VanillaCommand{ $mode = strtolower($args[0]); if($mode === "on"){ + if(TimingsHandler::isEnabled()){ + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_timings_alreadyEnabled()); + return true; + } TimingsHandler::setEnabled(); - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.enable")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_enable()); return true; }elseif($mode === "off"){ TimingsHandler::setEnabled(false); - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.disable")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_disable()); return true; } if(!TimingsHandler::isEnabled()){ - $sender->sendMessage(new TranslationContainer("pocketmine.command.timings.timingsDisabled")); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_timings_timingsDisabled()); return true; } @@ -92,26 +101,29 @@ class TimingsCommand extends VanillaCommand{ if($mode === "reset"){ TimingsHandler::reload(); - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.reset")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_reset()); }elseif($mode === "merged" or $mode === "report" or $paste){ $timings = ""; if($paste){ $fileTimings = fopen("php://temp", "r+b"); }else{ $index = 0; - $timingFolder = $sender->getServer()->getDataPath() . "timings/"; + $timingFolder = Path::join($sender->getServer()->getDataPath(), "timings"); if(!file_exists($timingFolder)){ mkdir($timingFolder, 0777); } - $timings = $timingFolder . "timings.txt"; + $timings = Path::join($timingFolder, "timings.txt"); while(file_exists($timings)){ - $timings = $timingFolder . "timings" . (++$index) . ".txt"; + $timings = Path::join($timingFolder, "timings" . (++$index) . ".txt"); } $fileTimings = fopen($timings, "a+b"); } - TimingsHandler::printTimings($fileTimings); + $lines = TimingsHandler::printTimings(); + foreach($lines as $line){ + fwrite($fileTimings, $line . PHP_EOL); + } if($paste){ fseek($fileTimings, 0); @@ -121,58 +133,46 @@ class TimingsCommand extends VanillaCommand{ ]; fclose($fileTimings); - $host = $sender->getServer()->getProperty("timings.host", "timings.pmmp.io"); + $host = $sender->getServer()->getConfigGroup()->getPropertyString("timings.host", "timings.pmmp.io"); - $sender->getServer()->getAsyncPool()->submitTask(new class($sender, $host, $agent, $data) extends BulkCurlTask{ - /** @var string */ - private $host; - - /** - * @param string[] $data - * @phpstan-param array $data - */ - public function __construct(CommandSender $sender, string $host, string $agent, array $data){ - parent::__construct([ - [ - "page" => "https://$host?upload=true", - "extraOpts" => [ - CURLOPT_HTTPHEADER => [ - "User-Agent: $agent", - "Content-Type: application/x-www-form-urlencoded" - ], - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query($data), - CURLOPT_AUTOREFERER => false, - CURLOPT_FOLLOWLOCATION => false - ] - ] - ], $sender); - $this->host = $host; - } - - public function onCompletion(Server $server){ - /** @var CommandSender $sender */ - $sender = $this->fetchLocal(); + $sender->getServer()->getAsyncPool()->submitTask(new BulkCurlTask( + [new BulkCurlTaskOperation( + "https://$host?upload=true", + 10, + [], + [ + CURLOPT_HTTPHEADER => [ + "User-Agent: $agent", + "Content-Type: application/x-www-form-urlencoded" + ], + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($data), + CURLOPT_AUTOREFERER => false, + CURLOPT_FOLLOWLOCATION => false + ] + )], + 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 return; } - $result = $this->getResult()[0]; + $result = $results[0]; if($result instanceof InternetException){ - $server->getLogger()->logException($result); + $sender->getServer()->getLogger()->logException($result); return; } - $response = json_decode($result[0], true); + $response = json_decode($result->getBody(), true); if(is_array($response) && isset($response["id"])){ - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.timingsRead", - ["https://" . $this->host . "/?id=" . $response["id"]])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead( + "https://" . $host . "/?id=" . $response["id"])); }else{ - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.pasteError")); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError()); } } - }); + )); }else{ fclose($fileTimings); - Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.command.timings.timingsWrite", [$timings])); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsWrite($timings)); } }else{ throw new InvalidCommandSyntaxException(); diff --git a/src/pocketmine/command/defaults/TitleCommand.php b/src/command/defaults/TitleCommand.php similarity index 80% rename from src/pocketmine/command/defaults/TitleCommand.php rename to src/command/defaults/TitleCommand.php index 030888912b..76e6f3b6e2 100644 --- a/src/pocketmine/command/defaults/TitleCommand.php +++ b/src/command/defaults/TitleCommand.php @@ -25,7 +25,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\utils\TextFormat; use function array_slice; use function count; use function implode; @@ -35,10 +37,10 @@ class TitleCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.title.description", - "%commands.title.usage" + KnownTranslationFactory::pocketmine_command_title_description(), + KnownTranslationFactory::commands_title_usage() ); - $this->setPermission("pocketmine.command.title"); + $this->setPermission(DefaultPermissionNames::COMMAND_TITLE); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -50,9 +52,9 @@ class TitleCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $player = $sender->getServer()->getPlayer($args[0]); + $player = $sender->getServer()->getPlayerByPrefix($args[0]); if($player === null){ - $sender->sendMessage(new TranslationContainer("commands.generic.player.notFound")); + $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED)); return true; } @@ -95,7 +97,7 @@ class TitleCommand extends VanillaCommand{ throw new InvalidCommandSyntaxException(); } - $sender->sendMessage(new TranslationContainer("commands.title.success")); + $sender->sendMessage(KnownTranslationFactory::commands_title_success()); return true; } diff --git a/src/pocketmine/command/defaults/TransferServerCommand.php b/src/command/defaults/TransferServerCommand.php similarity index 80% rename from src/pocketmine/command/defaults/TransferServerCommand.php rename to src/command/defaults/TransferServerCommand.php index f0e0027771..4159a5b256 100644 --- a/src/pocketmine/command/defaults/TransferServerCommand.php +++ b/src/command/defaults/TransferServerCommand.php @@ -25,7 +25,9 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\permission\DefaultPermissionNames; +use pocketmine\player\Player; use function count; class TransferServerCommand extends VanillaCommand{ @@ -33,10 +35,10 @@ class TransferServerCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.transferserver.description", - "%pocketmine.command.transferserver.usage" + KnownTranslationFactory::pocketmine_command_transferserver_description(), + KnownTranslationFactory::pocketmine_command_transferserver_usage() ); - $this->setPermission("pocketmine.command.transferserver"); + $this->setPermission(DefaultPermissionNames::COMMAND_TRANSFERSERVER); } public function execute(CommandSender $sender, string $commandLabel, array $args){ diff --git a/src/pocketmine/command/defaults/VanillaCommand.php b/src/command/defaults/VanillaCommand.php similarity index 75% rename from src/pocketmine/command/defaults/VanillaCommand.php rename to src/command/defaults/VanillaCommand.php index c4b821e6da..7133597d8f 100644 --- a/src/pocketmine/command/defaults/VanillaCommand.php +++ b/src/command/defaults/VanillaCommand.php @@ -26,7 +26,7 @@ namespace pocketmine\command\defaults; use pocketmine\command\Command; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; use pocketmine\utils\TextFormat; use function is_numeric; use function substr; @@ -35,10 +35,7 @@ abstract class VanillaCommand extends Command{ public const MAX_COORD = 30000000; public const MIN_COORD = -30000000; - /** - * @param mixed $value - */ - protected function getInteger(CommandSender $sender, $value, int $min = self::MIN_COORD, int $max = self::MAX_COORD) : int{ + protected function getInteger(CommandSender $sender, string $value, int $min = self::MIN_COORD, int $max = self::MAX_COORD) : int{ $i = (int) $value; if($i < $min){ @@ -60,10 +57,7 @@ abstract class VanillaCommand extends Command{ return $this->getDouble($sender, $input, $min, $max); } - /** - * @param mixed $value - */ - protected function getDouble(CommandSender $sender, $value, float $min = self::MIN_COORD, float $max = self::MAX_COORD) : float{ + protected function getDouble(CommandSender $sender, string $value, float $min = self::MIN_COORD, float $max = self::MAX_COORD) : float{ $i = (double) $value; if($i < $min){ @@ -82,11 +76,11 @@ abstract class VanillaCommand extends Command{ $v = (int) $input; if($v > $max){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.num.tooBig", [$input, (string) $max])); + $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->prefix(TextFormat::RED)); return null; } if($v < $min){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.generic.num.tooSmall", [$input, (string) $min])); + $sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->prefix(TextFormat::RED)); return null; } diff --git a/src/pocketmine/command/defaults/VersionCommand.php b/src/command/defaults/VersionCommand.php similarity index 56% rename from src/pocketmine/command/defaults/VersionCommand.php rename to src/command/defaults/VersionCommand.php index e132671bd1..c5a09b98f4 100644 --- a/src/pocketmine/command/defaults/VersionCommand.php +++ b/src/command/defaults/VersionCommand.php @@ -24,25 +24,32 @@ declare(strict_types=1); namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; -use pocketmine\lang\TranslationContainer; +use pocketmine\lang\KnownTranslationFactory; use pocketmine\network\mcpe\protocol\ProtocolInfo; +use pocketmine\permission\DefaultPermissionNames; use pocketmine\plugin\Plugin; use pocketmine\utils\TextFormat; +use pocketmine\utils\Utils; +use pocketmine\VersionInfo; use function count; +use function function_exists; use function implode; +use function opcache_get_status; +use function sprintf; use function stripos; use function strtolower; +use const PHP_VERSION; class VersionCommand extends VanillaCommand{ public function __construct(string $name){ parent::__construct( $name, - "%pocketmine.command.version.description", - "%pocketmine.command.version.usage", + KnownTranslationFactory::pocketmine_command_version_description(), + KnownTranslationFactory::pocketmine_command_version_usage(), ["ver", "about"] ); - $this->setPermission("pocketmine.command.version"); + $this->setPermission(DefaultPermissionNames::COMMAND_VERSION); } public function execute(CommandSender $sender, string $commandLabel, array $args){ @@ -51,12 +58,37 @@ class VersionCommand extends VanillaCommand{ } if(count($args) === 0){ - $sender->sendMessage(new TranslationContainer("pocketmine.server.info.extended", [ - $sender->getServer()->getName(), - $sender->getServer()->getPocketMineVersion(), - $sender->getServer()->getVersion(), - ProtocolInfo::CURRENT_PROTOCOL - ])); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareName( + VersionInfo::NAME + )); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareVersion( + VersionInfo::VERSION()->getFullVersion(), + VersionInfo::GIT_HASH() + )); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_minecraftVersion( + ProtocolInfo::MINECRAFT_VERSION_NETWORK, + (string) ProtocolInfo::CURRENT_PROTOCOL + )); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpVersion(PHP_VERSION)); + + if( + function_exists('opcache_get_status') && + ($opcacheStatus = opcache_get_status(false)) !== false && + isset($opcacheStatus["jit"]["on"]) + ){ + $jit = $opcacheStatus["jit"]; + if($jit["on"] === true){ + $jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitEnabled( + sprintf("CRTO: %s%s%s%s", $jit["opt_flags"] >> 2, $jit["opt_flags"] & 0x03, $jit["kind"], $jit["opt_level"]) + ); + }else{ + $jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitDisabled(); + } + }else{ + $jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitNotSupported(); + } + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpJitStatus($jitStatus)); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_operatingSystem(Utils::getOS())); }else{ $pluginName = implode(" ", $args); $exactPlugin = $sender->getServer()->getPluginManager()->getPlugin($pluginName); @@ -77,7 +109,7 @@ class VersionCommand extends VanillaCommand{ } if(!$found){ - $sender->sendMessage(new TranslationContainer("pocketmine.command.version.noSuchPlugin")); + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_noSuchPlugin()); } } diff --git a/src/command/defaults/WhitelistCommand.php b/src/command/defaults/WhitelistCommand.php new file mode 100644 index 0000000000..2fdf5a1fa0 --- /dev/null +++ b/src/command/defaults/WhitelistCommand.php @@ -0,0 +1,129 @@ +setPermission(implode(";", [ + DefaultPermissionNames::COMMAND_WHITELIST_RELOAD, + DefaultPermissionNames::COMMAND_WHITELIST_ENABLE, + DefaultPermissionNames::COMMAND_WHITELIST_DISABLE, + DefaultPermissionNames::COMMAND_WHITELIST_LIST, + DefaultPermissionNames::COMMAND_WHITELIST_ADD, + DefaultPermissionNames::COMMAND_WHITELIST_REMOVE + ])); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(!$this->testPermission($sender)){ + return true; + } + + if(count($args) === 1){ + switch(strtolower($args[0])){ + case "reload": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_RELOAD)){ + $sender->getServer()->getWhitelisted()->reload(); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_reloaded()); + } + + return true; + case "on": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ENABLE)){ + $sender->getServer()->getConfigGroup()->setConfigBool("white-list", true); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_enabled()); + } + + return true; + case "off": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_DISABLE)){ + $sender->getServer()->getConfigGroup()->setConfigBool("white-list", false); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_disabled()); + } + + return true; + case "list": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_LIST)){ + $entries = $sender->getServer()->getWhitelisted()->getAll(true); + sort($entries, SORT_STRING); + $result = implode(", ", $entries); + $count = (string) count($entries); + + $sender->sendMessage(KnownTranslationFactory::commands_whitelist_list($count, $count)); + $sender->sendMessage($result); + } + + return true; + + case "add": + $sender->sendMessage(KnownTranslationFactory::commands_generic_usage(KnownTranslationFactory::commands_whitelist_add_usage())); + return true; + + case "remove": + $sender->sendMessage(KnownTranslationFactory::commands_generic_usage(KnownTranslationFactory::commands_whitelist_remove_usage())); + return true; + } + }elseif(count($args) === 2){ + if(!Player::isValidUserName($args[1])){ + throw new InvalidCommandSyntaxException(); + } + switch(strtolower($args[0])){ + case "add": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_ADD)){ + $sender->getServer()->addWhitelist($args[1]); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_add_success($args[1])); + } + + return true; + case "remove": + if($this->testPermission($sender, DefaultPermissionNames::COMMAND_WHITELIST_REMOVE)){ + $sender->getServer()->removeWhitelist($args[1]); + Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_whitelist_remove_success($args[1])); + } + + return true; + } + } + + throw new InvalidCommandSyntaxException(); + } +} diff --git a/src/pocketmine/command/utils/CommandException.php b/src/command/utils/CommandException.php similarity index 100% rename from src/pocketmine/command/utils/CommandException.php rename to src/command/utils/CommandException.php diff --git a/src/pocketmine/command/utils/InvalidCommandSyntaxException.php b/src/command/utils/InvalidCommandSyntaxException.php similarity index 100% rename from src/pocketmine/command/utils/InvalidCommandSyntaxException.php rename to src/command/utils/InvalidCommandSyntaxException.php diff --git a/src/console/ConsoleCommandSender.php b/src/console/ConsoleCommandSender.php new file mode 100644 index 0000000000..45ae2f5ab3 --- /dev/null +++ b/src/console/ConsoleCommandSender.php @@ -0,0 +1,86 @@ +server = $server; + $this->perm = new PermissibleBase([DefaultPermissions::ROOT_CONSOLE => true]); + $this->language = $language; + } + + public function getServer() : Server{ + return $this->server; + } + + public function getLanguage() : Language{ + return $this->language; + } + + public function sendMessage(Translatable|string $message) : void{ + $server = $this->getServer(); + if($message instanceof Translatable){ + $message = $this->getLanguage()->translate($message); + } + + foreach(explode("\n", trim($message)) as $line){ + $server->getLogger()->info($line); + } + } + + public function getName() : string{ + return "CONSOLE"; + } + + public function getScreenLineHeight() : int{ + return $this->lineHeight ?? PHP_INT_MAX; + } + + public function setScreenLineHeight(?int $height) : void{ + if($height !== null and $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 new file mode 100644 index 0000000000..77a3f4b6f5 --- /dev/null +++ b/src/console/ConsoleReader.php @@ -0,0 +1,82 @@ +initStdin(); + } + + private function initStdin() : void{ + if(is_resource($this->stdin)){ + fclose($this->stdin); + } + + $stdin = fopen("php://stdin", "r"); + if($stdin === false) throw new AssumptionFailedError("Opening stdin should never fail"); + $this->stdin = $stdin; + } + + /** + * Reads a line from the console and adds it to the buffer. This method may block the thread. + */ + public function readLine() : ?string{ + if(!is_resource($this->stdin)){ + $this->initStdin(); + } + + $r = [$this->stdin]; + $w = $e = null; + if(($count = stream_select($r, $w, $e, 0, 200000)) === 0){ //nothing changed in 200000 microseconds + return null; + }elseif($count === false){ //stream error + return null; + } + + if(($raw = fgets($this->stdin)) === false){ //broken pipe or EOF + usleep(200000); //prevent CPU waste if it's end of pipe + return null; //loop back round + } + + $line = trim($raw); + + return $line !== "" ? $line : null; + } + + public function __destruct(){ + fclose($this->stdin); + } +} diff --git a/src/console/ConsoleReaderChildProcess.php b/src/console/ConsoleReaderChildProcess.php new file mode 100644 index 0000000000..509d0daca3 --- /dev/null +++ b/src/console/ConsoleReaderChildProcess.php @@ -0,0 +1,55 @@ +readLine(); + 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 new file mode 100644 index 0000000000..bdfccd80fd --- /dev/null +++ b/src/console/ConsoleReaderThread.php @@ -0,0 +1,141 @@ +buffer = $buffer; + $this->notifier = $notifier; + } + + protected function onRun() : void{ + $buffer = $this->buffer; + $notifier = $this->notifier; + + while(!$this->isKilled){ + $this->runSubprocess($buffer, $notifier); + } + } + + /** + * This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes + * properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets() + * waiting for a line that might never come (and Windows doesn't support character-based reading either), and + * pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless. + * + * This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill + * the entire server process to make it go away. + * + * To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can + * then brutally murder when the server shuts down, without killing the entire server process. + * Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process + * communication. + */ + private function runSubprocess(\Threaded $buffer, ?SleeperNotifier $notifier) : void{ + $server = stream_socket_server("tcp://127.0.0.1:0"); + 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"); + + //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], + [ + 2 => fopen("php://stderr", "w"), + ], + $pipes + ); + if($sub === false){ + throw new AssumptionFailedError("Something has gone horribly wrong"); + } + $client = stream_socket_accept($server, 15); + if($client === false){ + throw new AssumptionFailedError("stream_socket_accept() returned false"); + } + stream_socket_shutdown($server, STREAM_SHUT_RDWR); + while(!$this->isKilled){ + $r = [$client]; + $w = null; + $e = null; + if(stream_select($r, $w, $e, 0, 200000) === 1){ + $command = fgets($client); + if($command === false){ + //subprocess died for some reason; this could be someone killed it manually from outside (e.g. + //mistyped PID) or it might be a ctrl+c signal to this process that the child is handling + //differently (different signal handlers). + //since we have no way to know the difference, we just kill the sub and start a new one. + break; + } + + $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(); + } + } + } + + //we have no way to signal to the subprocess to shut down gracefully; besides, Windows sucks, and the subprocess + //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); + } + + public function getThreadName() : string{ + return "Console"; + } +} diff --git a/src/pocketmine/inventory/CraftingGrid.php b/src/crafting/CraftingGrid.php similarity index 70% rename from src/pocketmine/inventory/CraftingGrid.php rename to src/crafting/CraftingGrid.php index 5b9f7e52fd..eb3ea5f25e 100644 --- a/src/pocketmine/inventory/CraftingGrid.php +++ b/src/crafting/CraftingGrid.php @@ -21,20 +21,18 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\crafting; +use pocketmine\inventory\SimpleInventory; use pocketmine\item\Item; -use pocketmine\Player; use function max; use function min; use const PHP_INT_MAX; -class CraftingGrid extends BaseInventory{ +abstract class CraftingGrid extends SimpleInventory{ public const SIZE_SMALL = 2; public const SIZE_BIG = 3; - /** @var Player */ - protected $holder; /** @var int */ private $gridWidth; @@ -47,51 +45,18 @@ class CraftingGrid extends BaseInventory{ /** @var int|null */ private $yLen; - public function __construct(Player $holder, int $gridWidth){ - $this->holder = $holder; + public function __construct(int $gridWidth){ $this->gridWidth = $gridWidth; - parent::__construct(); + parent::__construct($this->getGridWidth() ** 2); } public function getGridWidth() : int{ return $this->gridWidth; } - public function getDefaultSize() : int{ - return $this->getGridWidth() ** 2; - } - - public function setSize(int $size){ - throw new \BadMethodCallException("Cannot change the size of a crafting grid"); - } - - public function getName() : string{ - return "Crafting"; - } - - public function setItem(int $index, Item $item, bool $send = true) : bool{ - if(parent::setItem($index, $item, $send)){ - $this->seekRecipeBounds(); - - return true; - } - - return false; - } - - public function sendSlot(int $index, $target) : void{ - //we can't send a slot of a client-sided inventory window - } - - public function sendContents($target) : void{ - //no way to do this - } - - /** - * @return Player - */ - public function getHolder(){ - return $this->holder; + public function setItem(int $index, Item $item) : void{ + parent::setItem($index, $item); + $this->seekRecipeBounds(); } private function seekRecipeBounds() : void{ @@ -135,7 +100,7 @@ class CraftingGrid extends BaseInventory{ 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 new file mode 100644 index 0000000000..32b6b77179 --- /dev/null +++ b/src/crafting/CraftingManager.php @@ -0,0 +1,209 @@ + + */ + protected $furnaceRecipeManagers; + + /** + * @var ObjectSet + * @phpstan-var ObjectSet<\Closure() : void> + */ + private $recipeRegisteredCallbacks; + + public function __construct(){ + $this->recipeRegisteredCallbacks = new ObjectSet(); + foreach(FurnaceType::getAll() as $furnaceType){ + $this->furnaceRecipeManagers[$furnaceType->id()] = new FurnaceRecipeManager(); + } + + $recipeRegisteredCallbacks = $this->recipeRegisteredCallbacks; + foreach($this->furnaceRecipeManagers as $furnaceRecipeManager){ + $furnaceRecipeManager->getRecipeRegisteredCallbacks()->add(static function(FurnaceRecipe $recipe) use ($recipeRegisteredCallbacks) : void{ + foreach($recipeRegisteredCallbacks as $callback){ + $callback(); + } + }); + } + } + + /** @phpstan-return ObjectSet<\Closure() : void> */ + public function getRecipeRegisteredCallbacks() : ObjectSet{ return $this->recipeRegisteredCallbacks; } + + /** + * Function used to arrange Shapeless Recipe ingredient lists into a consistent order. + */ + public static function sort(Item $i1, Item $i2) : int{ + //Use spaceship operator to compare each property, then try the next one if they are equivalent. + ($retval = $i1->getId() <=> $i2->getId()) === 0 && ($retval = $i1->getMeta() <=> $i2->getMeta()) === 0 && ($retval = $i1->getCount() <=> $i2->getCount()) === 0; + + return $retval; + } + + /** + * @param Item[] $items + * + * @return Item[] + */ + private static function pack(array $items) : array{ + /** @var Item[] $result */ + $result = []; + + foreach($items as $i => $item){ + foreach($result as $otherItem){ + if($item->canStackWith($otherItem)){ + $otherItem->setCount($otherItem->getCount() + $item->getCount()); + continue 2; + } + } + + //No matching item found + $result[] = clone $item; + } + + return $result; + } + + /** + * @param Item[] $outputs + */ + private static function hashOutputs(array $outputs) : string{ + $outputs = self::pack($outputs); + usort($outputs, [self::class, "sort"]); + $result = new BinaryStream(); + foreach($outputs as $o){ + //count is not written because the outputs might be from multiple repetitions of a single recipe + //this reduces the accuracy of the hash, but it won't matter in most cases. + $result->putVarInt($o->getId()); + $result->putVarInt($o->getMeta()); + $result->put((new LittleEndianNbtSerializer())->write(new TreeRoot($o->getNamedTag()))); + } + + return $result->getBuffer(); + } + + /** + * @return ShapelessRecipe[][] + */ + public function getShapelessRecipes() : array{ + return $this->shapelessRecipes; + } + + /** + * @return ShapedRecipe[][] + */ + public function getShapedRecipes() : array{ + return $this->shapedRecipes; + } + + public function getFurnaceRecipeManager(FurnaceType $furnaceType) : FurnaceRecipeManager{ + return $this->furnaceRecipeManagers[$furnaceType->id()]; + } + + public function registerShapedRecipe(ShapedRecipe $recipe) : void{ + $this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe; + + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback(); + } + } + + public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{ + $this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe; + + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback(); + } + } + + /** + * @param Item[] $outputs + */ + public function matchRecipe(CraftingGrid $grid, array $outputs) : ?CraftingRecipe{ + //TODO: try to match special recipes before anything else (first they need to be implemented!) + + $outputHash = self::hashOutputs($outputs); + + if(isset($this->shapedRecipes[$outputHash])){ + foreach($this->shapedRecipes[$outputHash] as $recipe){ + if($recipe->matchesCraftingGrid($grid)){ + return $recipe; + } + } + } + + if(isset($this->shapelessRecipes[$outputHash])){ + foreach($this->shapelessRecipes[$outputHash] as $recipe){ + if($recipe->matchesCraftingGrid($grid)){ + return $recipe; + } + } + } + + return null; + } + + /** + * @param Item[] $outputs + * + * @return CraftingRecipe[]|\Generator + * @phpstan-return \Generator + */ + public function matchRecipeByOutputs(array $outputs) : \Generator{ + //TODO: try to match special recipes before anything else (first they need to be implemented!) + + $outputHash = self::hashOutputs($outputs); + + if(isset($this->shapedRecipes[$outputHash])){ + foreach($this->shapedRecipes[$outputHash] as $recipe){ + yield $recipe; + } + } + + if(isset($this->shapelessRecipes[$outputHash])){ + foreach($this->shapelessRecipes[$outputHash] as $recipe){ + yield $recipe; + } + } + } +} diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php new file mode 100644 index 0000000000..6660bb6024 --- /dev/null +++ b/src/crafting/CraftingManagerFromDataHelper.php @@ -0,0 +1,82 @@ +registerShapelessRecipe(new ShapelessRecipe( + array_map($itemDeserializerFunc, $recipe["input"]), + array_map($itemDeserializerFunc, $recipe["output"]) + )); + } + foreach($recipes["shaped"] as $recipe){ + if($recipe["block"] !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics + continue; + } + $result->registerShapedRecipe(new ShapedRecipe( + $recipe["shape"], + array_map($itemDeserializerFunc, $recipe["input"]), + array_map($itemDeserializerFunc, $recipe["output"]) + )); + } + foreach($recipes["smelting"] as $recipe){ + $furnaceType = match ($recipe["block"]){ + "furnace" => FurnaceType::FURNACE(), + "blast_furnace" => FurnaceType::BLAST_FURNACE(), + "smoker" => FurnaceType::SMOKER(), + //TODO: campfire + default => null + }; + if($furnaceType === null){ + continue; + } + $result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe( + Item::jsonDeserialize($recipe["output"]), + Item::jsonDeserialize($recipe["input"])) + ); + } + + return $result; + } +} diff --git a/src/pocketmine/inventory/CraftingRecipe.php b/src/crafting/CraftingRecipe.php similarity index 94% rename from src/pocketmine/inventory/CraftingRecipe.php rename to src/crafting/CraftingRecipe.php index 9e8cba8f31..44c387eff3 100644 --- a/src/pocketmine/inventory/CraftingRecipe.php +++ b/src/crafting/CraftingRecipe.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\crafting; use pocketmine\item\Item; -interface CraftingRecipe extends Recipe{ +interface CraftingRecipe{ /** * Returns a list of items needed to craft this recipe. This MUST NOT include Air items or items with a zero count. * diff --git a/src/pocketmine/inventory/FurnaceRecipe.php b/src/crafting/FurnaceRecipe.php similarity index 77% rename from src/pocketmine/inventory/FurnaceRecipe.php rename to src/crafting/FurnaceRecipe.php index 185f05ee79..55a6104fec 100644 --- a/src/pocketmine/inventory/FurnaceRecipe.php +++ b/src/crafting/FurnaceRecipe.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\crafting; use pocketmine\item\Item; -class FurnaceRecipe implements Recipe{ +class FurnaceRecipe{ /** @var Item */ private $output; @@ -38,13 +38,6 @@ class FurnaceRecipe implements Recipe{ $this->ingredient = clone $ingredient; } - /** - * @return void - */ - public function setInput(Item $item){ - $this->ingredient = clone $item; - } - public function getInput() : Item{ return clone $this->ingredient; } @@ -52,11 +45,4 @@ class FurnaceRecipe implements Recipe{ public function getResult() : Item{ return clone $this->output; } - - /** - * @deprecated - */ - public function registerToCraftingManager(CraftingManager $manager) : void{ - $manager->registerFurnaceRecipe($this); - } } diff --git a/src/crafting/FurnaceRecipeManager.php b/src/crafting/FurnaceRecipeManager.php new file mode 100644 index 0000000000..9cb9c6eef1 --- /dev/null +++ b/src/crafting/FurnaceRecipeManager.php @@ -0,0 +1,68 @@ + + */ + private $recipeRegisteredCallbacks; + + public function __construct(){ + $this->recipeRegisteredCallbacks = new ObjectSet(); + } + + /** + * @phpstan-return ObjectSet<\Closure(FurnaceRecipe) : void> + */ + public function getRecipeRegisteredCallbacks() : ObjectSet{ + return $this->recipeRegisteredCallbacks; + } + + /** + * @return FurnaceRecipe[] + */ + public function getAll() : array{ + return $this->furnaceRecipes; + } + + public function register(FurnaceRecipe $recipe) : void{ + $input = $recipe->getInput(); + $this->furnaceRecipes[$input->getId() . ":" . ($input->hasAnyDamageValue() ? "?" : $input->getMeta())] = $recipe; + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback($recipe); + } + } + + 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 new file mode 100644 index 0000000000..889150c453 --- /dev/null +++ b/src/crafting/FurnaceType.php @@ -0,0 +1,56 @@ +Enum___construct($enumName); + } + + public function getCookDurationTicks() : int{ return $this->cookDurationTicks; } +} diff --git a/src/pocketmine/inventory/ShapedRecipe.php b/src/crafting/ShapedRecipe.php similarity index 84% rename from src/pocketmine/inventory/ShapedRecipe.php rename to src/crafting/ShapedRecipe.php index 31b1797375..e346f22adb 100644 --- a/src/pocketmine/inventory/ShapedRecipe.php +++ b/src/crafting/ShapedRecipe.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\crafting; use pocketmine\item\Item; use pocketmine\item\ItemFactory; -use function array_map; +use pocketmine\utils\Utils; use function array_values; use function count; use function implode; @@ -88,10 +88,14 @@ class ShapedRecipe implements CraftingRecipe{ $this->shape = $shape; foreach($ingredients as $char => $i){ - $this->setIngredient($char, $i); + if(strpos(implode($this->shape), $char) === false){ + throw new \InvalidArgumentException("Symbol '$char' does not appear in the recipe shape"); + } + + $this->ingredientList[$char] = clone $i; } - $this->results = array_map(function(Item $item) : Item{ return clone $item; }, $results); + $this->results = Utils::cloneObjectArray($results); } public function getWidth() : int{ @@ -106,7 +110,7 @@ class ShapedRecipe implements CraftingRecipe{ * @return Item[] */ public function getResults() : array{ - return array_map(function(Item $item) : Item{ return clone $item; }, $this->results); + return Utils::cloneObjectArray($this->results); } /** @@ -116,20 +120,6 @@ class ShapedRecipe implements CraftingRecipe{ return $this->getResults(); } - /** - * @return $this - * @throws \InvalidArgumentException - */ - public function setIngredient(string $key, Item $item){ - if(strpos(implode($this->shape), $key) === false){ - throw new \InvalidArgumentException("Symbol '$key' does not appear in the recipe shape"); - } - - $this->ingredientList[$key] = clone $item; - - return $this; - } - /** * @return Item[][] */ @@ -165,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::get(Item::AIR, 0, 0); + return $exists !== null ? clone $exists : ItemFactory::air(); } /** @@ -176,20 +166,13 @@ class ShapedRecipe implements CraftingRecipe{ return $this->shape; } - /** - * @deprecated - */ - public function registerToCraftingManager(CraftingManager $manager) : void{ - $manager->registerShapedRecipe($this); - } - private function matchInputMap(CraftingGrid $grid, bool $reverse) : bool{ for($y = 0; $y < $this->height; ++$y){ for($x = 0; $x < $this->width; ++$x){ $given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $x, $y); $required = $this->getIngredient($x, $y); - if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasCompoundTag()) or $required->getCount() > $given->getCount()){ + if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasNamedTag()) or $required->getCount() > $given->getCount()){ return false; } } diff --git a/src/pocketmine/inventory/ShapelessRecipe.php b/src/crafting/ShapelessRecipe.php similarity index 63% rename from src/pocketmine/inventory/ShapelessRecipe.php rename to src/crafting/ShapelessRecipe.php index f277bf2267..195b917fad 100644 --- a/src/pocketmine/inventory/ShapelessRecipe.php +++ b/src/crafting/ShapelessRecipe.php @@ -21,10 +21,10 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\crafting; use pocketmine\item\Item; -use function array_map; +use pocketmine\utils\Utils; use function count; class ShapelessRecipe implements CraftingRecipe{ @@ -40,60 +40,34 @@ class ShapelessRecipe implements CraftingRecipe{ public function __construct(array $ingredients, array $results){ foreach($ingredients as $item){ //Ensure they get split up properly - $this->addIngredient($item); + if(count($this->ingredients) + $item->getCount() > 9){ + throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients"); + } + + while($item->getCount() > 0){ + $this->ingredients[] = $item->pop(); + } } - $this->results = array_map(function(Item $item) : Item{ return clone $item; }, $results); + $this->results = Utils::cloneObjectArray($results); } /** * @return Item[] */ public function getResults() : array{ - return array_map(function(Item $item) : Item{ return clone $item; }, $this->results); + return Utils::cloneObjectArray($this->results); } public function getResultsFor(CraftingGrid $grid) : array{ return $this->getResults(); } - /** - * @throws \InvalidArgumentException - */ - public function addIngredient(Item $item) : ShapelessRecipe{ - if(count($this->ingredients) + $item->getCount() > 9){ - throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients"); - } - - while($item->getCount() > 0){ - $this->ingredients[] = $item->pop(); - } - - return $this; - } - - /** - * @return $this - */ - public function removeIngredient(Item $item){ - foreach($this->ingredients as $index => $ingredient){ - if($item->getCount() <= 0){ - break; - } - if($ingredient->equals($item, !$item->hasAnyDamageValue(), $item->hasCompoundTag())){ - unset($this->ingredients[$index]); - $item->pop(); - } - } - - return $this; - } - /** * @return Item[] */ public function getIngredientList() : array{ - return array_map(function(Item $item) : Item{ return clone $item; }, $this->ingredients); + return Utils::cloneObjectArray($this->ingredients); } public function getIngredientCount() : int{ @@ -105,20 +79,13 @@ class ShapelessRecipe implements CraftingRecipe{ return $count; } - /** - * @deprecated - */ - public function registerToCraftingManager(CraftingManager $manager) : void{ - $manager->registerShapelessRecipe($this); - } - public function matchesCraftingGrid(CraftingGrid $grid) : bool{ //don't pack the ingredients - shapeless recipes require that each ingredient be in a separate slot $input = $grid->getContents(); foreach($this->ingredients as $needItem){ foreach($input as $j => $haveItem){ - if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasCompoundTag()) and $haveItem->getCount() >= $needItem->getCount()){ + if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasNamedTag()) and $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 0000000000..f5a3213f18 --- /dev/null +++ b/src/crash/CrashDump.php @@ -0,0 +1,294 @@ +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); + 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; + } + + 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); + if($version === false) throw new AssumptionFailedError(); + $extensions[$ext] = $version; + } + $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) 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->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 0000000000..a3f13f2cac --- /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; + } +} \ No newline at end of file diff --git a/src/crash/CrashDumpDataGeneral.php b/src/crash/CrashDumpDataGeneral.php new file mode 100644 index 0000000000..33e41ce014 --- /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, + ){} +} \ No newline at end of file diff --git a/src/crash/CrashDumpDataPluginEntry.php b/src/crash/CrashDumpDataPluginEntry.php new file mode 100644 index 0000000000..21139f4316 --- /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); + } +} \ No newline at end of file diff --git a/src/data/SavedDataLoadingException.php b/src/data/SavedDataLoadingException.php new file mode 100644 index 0000000000..fcc72c7abc --- /dev/null +++ b/src/data/SavedDataLoadingException.php @@ -0,0 +1,28 @@ + + */ + private array $idToEnum = []; + /** + * @var string[] + * @phpstan-var array + */ + private array $enumToId = []; + + public function __construct(){ + $this->register("bo", BannerPatternType::BORDER()); + $this->register("bri", BannerPatternType::BRICKS()); + $this->register("mc", BannerPatternType::CIRCLE()); + $this->register("cre", BannerPatternType::CREEPER()); + $this->register("cr", BannerPatternType::CROSS()); + $this->register("cbo", BannerPatternType::CURLY_BORDER()); + $this->register("lud", BannerPatternType::DIAGONAL_LEFT()); + $this->register("rd", BannerPatternType::DIAGONAL_RIGHT()); + $this->register("ld", BannerPatternType::DIAGONAL_UP_LEFT()); + $this->register("rud", BannerPatternType::DIAGONAL_UP_RIGHT()); + $this->register("flo", BannerPatternType::FLOWER()); + $this->register("gra", BannerPatternType::GRADIENT()); + $this->register("gru", BannerPatternType::GRADIENT_UP()); + $this->register("hh", BannerPatternType::HALF_HORIZONTAL()); + $this->register("hhb", BannerPatternType::HALF_HORIZONTAL_BOTTOM()); + $this->register("vh", BannerPatternType::HALF_VERTICAL()); + $this->register("vhr", BannerPatternType::HALF_VERTICAL_RIGHT()); + $this->register("moj", BannerPatternType::MOJANG()); + $this->register("mr", BannerPatternType::RHOMBUS()); + $this->register("sku", BannerPatternType::SKULL()); + $this->register("ss", BannerPatternType::SMALL_STRIPES()); + $this->register("bl", BannerPatternType::SQUARE_BOTTOM_LEFT()); + $this->register("br", BannerPatternType::SQUARE_BOTTOM_RIGHT()); + $this->register("tl", BannerPatternType::SQUARE_TOP_LEFT()); + $this->register("tr", BannerPatternType::SQUARE_TOP_RIGHT()); + $this->register("sc", BannerPatternType::STRAIGHT_CROSS()); + $this->register("bs", BannerPatternType::STRIPE_BOTTOM()); + $this->register("cs", BannerPatternType::STRIPE_CENTER()); + $this->register("dls", BannerPatternType::STRIPE_DOWNLEFT()); + $this->register("drs", BannerPatternType::STRIPE_DOWNRIGHT()); + $this->register("ls", BannerPatternType::STRIPE_LEFT()); + $this->register("ms", BannerPatternType::STRIPE_MIDDLE()); + $this->register("rs", BannerPatternType::STRIPE_RIGHT()); + $this->register("ts", BannerPatternType::STRIPE_TOP()); + $this->register("bt", BannerPatternType::TRIANGLE_BOTTOM()); + $this->register("tt", BannerPatternType::TRIANGLE_TOP()); + $this->register("bts", BannerPatternType::TRIANGLES_BOTTOM()); + $this->register("tts", BannerPatternType::TRIANGLES_TOP()); + } + + public function register(string $stringId, BannerPatternType $type) : void{ + $this->idToEnum[$stringId] = $type; + $this->enumToId[$type->id()] = $stringId; + } + + public function fromId(string $id) : ?BannerPatternType{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(BannerPatternType $type) : string{ + if(!array_key_exists($type->id(), $this->enumToId)){ + throw new \InvalidArgumentException("Missing mapping for banner pattern type " . $type->name()); + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/data/bedrock/BiomeIds.php b/src/data/bedrock/BiomeIds.php new file mode 100644 index 0000000000..ffb0af8d74 --- /dev/null +++ b/src/data/bedrock/BiomeIds.php @@ -0,0 +1,114 @@ + + */ + private $idToEnum = []; + /** + * @var int[] + * @phpstan-var array + */ + private $enumToId = []; + + public function __construct(){ + $this->register(BlockLegacyMetadata::CORAL_VARIANT_TUBE, CoralType::TUBE()); + $this->register(BlockLegacyMetadata::CORAL_VARIANT_BRAIN, CoralType::BRAIN()); + $this->register(BlockLegacyMetadata::CORAL_VARIANT_BUBBLE, CoralType::BUBBLE()); + $this->register(BlockLegacyMetadata::CORAL_VARIANT_FIRE, CoralType::FIRE()); + $this->register(BlockLegacyMetadata::CORAL_VARIANT_HORN, CoralType::HORN()); + } + + public function register(int $id, CoralType $type) : void{ + $this->idToEnum[$id] = $type; + $this->enumToId[$type->id()] = $id; + } + + public function fromId(int $id) : ?CoralType{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(CoralType $type) : int{ + if(!array_key_exists($type->id(), $this->enumToId)){ + throw new \InvalidArgumentException("Coral type does not have a mapped ID"); //this should never happen + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/data/bedrock/DyeColorIdMap.php b/src/data/bedrock/DyeColorIdMap.php new file mode 100644 index 0000000000..cb7455b81a --- /dev/null +++ b/src/data/bedrock/DyeColorIdMap.php @@ -0,0 +1,83 @@ + + */ + private $idToEnum = []; + + /** + * @var int[] + * @phpstan-var array + */ + private $enumToId = []; + + private function __construct(){ + $this->register(0, DyeColor::WHITE()); + $this->register(1, DyeColor::ORANGE()); + $this->register(2, DyeColor::MAGENTA()); + $this->register(3, DyeColor::LIGHT_BLUE()); + $this->register(4, DyeColor::YELLOW()); + $this->register(5, DyeColor::LIME()); + $this->register(6, DyeColor::PINK()); + $this->register(7, DyeColor::GRAY()); + $this->register(8, DyeColor::LIGHT_GRAY()); + $this->register(9, DyeColor::CYAN()); + $this->register(10, DyeColor::PURPLE()); + $this->register(11, DyeColor::BLUE()); + $this->register(12, DyeColor::BROWN()); + $this->register(13, DyeColor::GREEN()); + $this->register(14, DyeColor::RED()); + $this->register(15, DyeColor::BLACK()); + } + + private function register(int $id, DyeColor $color) : void{ + $this->idToEnum[$id] = $color; + $this->enumToId[$color->id()] = $id; + } + + public function toId(DyeColor $color) : int{ + return $this->enumToId[$color->id()]; //TODO: is it possible for this to be missing? + } + + public function toInvertedId(DyeColor $color) : int{ + return ~$this->toId($color) & 0xf; + } + + public function fromId(int $id) : ?DyeColor{ + return $this->idToEnum[$id]; + } + + public function fromInvertedId(int $id) : ?DyeColor{ + return $this->fromId(~$id & 0xf); + } +} diff --git a/src/data/bedrock/EffectIdMap.php b/src/data/bedrock/EffectIdMap.php new file mode 100644 index 0000000000..f5257fe0de --- /dev/null +++ b/src/data/bedrock/EffectIdMap.php @@ -0,0 +1,98 @@ + + */ + private $idToEffect = []; + + /** + * @var int[] + * @phpstan-var array + */ + private $effectToId = []; + + private function __construct(){ + $this->register(EffectIds::SPEED, VanillaEffects::SPEED()); + $this->register(EffectIds::SLOWNESS, VanillaEffects::SLOWNESS()); + $this->register(EffectIds::HASTE, VanillaEffects::HASTE()); + $this->register(EffectIds::MINING_FATIGUE, VanillaEffects::MINING_FATIGUE()); + $this->register(EffectIds::STRENGTH, VanillaEffects::STRENGTH()); + $this->register(EffectIds::INSTANT_HEALTH, VanillaEffects::INSTANT_HEALTH()); + $this->register(EffectIds::INSTANT_DAMAGE, VanillaEffects::INSTANT_DAMAGE()); + $this->register(EffectIds::JUMP_BOOST, VanillaEffects::JUMP_BOOST()); + $this->register(EffectIds::NAUSEA, VanillaEffects::NAUSEA()); + $this->register(EffectIds::REGENERATION, VanillaEffects::REGENERATION()); + $this->register(EffectIds::RESISTANCE, VanillaEffects::RESISTANCE()); + $this->register(EffectIds::FIRE_RESISTANCE, VanillaEffects::FIRE_RESISTANCE()); + $this->register(EffectIds::WATER_BREATHING, VanillaEffects::WATER_BREATHING()); + $this->register(EffectIds::INVISIBILITY, VanillaEffects::INVISIBILITY()); + $this->register(EffectIds::BLINDNESS, VanillaEffects::BLINDNESS()); + $this->register(EffectIds::NIGHT_VISION, VanillaEffects::NIGHT_VISION()); + $this->register(EffectIds::HUNGER, VanillaEffects::HUNGER()); + $this->register(EffectIds::WEAKNESS, VanillaEffects::WEAKNESS()); + $this->register(EffectIds::POISON, VanillaEffects::POISON()); + $this->register(EffectIds::WITHER, VanillaEffects::WITHER()); + $this->register(EffectIds::HEALTH_BOOST, VanillaEffects::HEALTH_BOOST()); + $this->register(EffectIds::ABSORPTION, VanillaEffects::ABSORPTION()); + $this->register(EffectIds::SATURATION, VanillaEffects::SATURATION()); + $this->register(EffectIds::LEVITATION, VanillaEffects::LEVITATION()); + $this->register(EffectIds::FATAL_POISON, VanillaEffects::FATAL_POISON()); + $this->register(EffectIds::CONDUIT_POWER, VanillaEffects::CONDUIT_POWER()); + //TODO: SLOW_FALLING + //TODO: BAD_OMEN + //TODO: VILLAGE_HERO + } + + //TODO: not a big fan of the code duplication here :( + + public function register(int $mcpeId, Effect $effect) : void{ + $this->idToEffect[$mcpeId] = $effect; + $this->effectToId[spl_object_id($effect)] = $mcpeId; + } + + public function fromId(int $id) : ?Effect{ + //we might not have all the effect IDs registered + return $this->idToEffect[$id] ?? null; + } + + public function toId(Effect $effect) : int{ + if(!array_key_exists(spl_object_id($effect), $this->effectToId)){ + //this should never happen, so we treat it as an exceptional condition + throw new \InvalidArgumentException("Effect does not have a mapped ID"); + } + return $this->effectToId[spl_object_id($effect)]; + } +} diff --git a/src/data/bedrock/EffectIds.php b/src/data/bedrock/EffectIds.php new file mode 100644 index 0000000000..a54000f7f9 --- /dev/null +++ b/src/data/bedrock/EffectIds.php @@ -0,0 +1,61 @@ + + */ + private $idToEnch = []; + /** + * @var int[] + * @phpstan-var array + */ + private $enchToId = []; + + private function __construct(){ + $this->register(EnchantmentIds::PROTECTION, VanillaEnchantments::PROTECTION()); + $this->register(EnchantmentIds::FIRE_PROTECTION, VanillaEnchantments::FIRE_PROTECTION()); + $this->register(EnchantmentIds::FEATHER_FALLING, VanillaEnchantments::FEATHER_FALLING()); + $this->register(EnchantmentIds::BLAST_PROTECTION, VanillaEnchantments::BLAST_PROTECTION()); + $this->register(EnchantmentIds::PROJECTILE_PROTECTION, VanillaEnchantments::PROJECTILE_PROTECTION()); + $this->register(EnchantmentIds::THORNS, VanillaEnchantments::THORNS()); + $this->register(EnchantmentIds::RESPIRATION, VanillaEnchantments::RESPIRATION()); + + $this->register(EnchantmentIds::SHARPNESS, VanillaEnchantments::SHARPNESS()); + //TODO: smite, bane of arthropods (these don't make sense now because their applicable mobs don't exist yet) + + $this->register(EnchantmentIds::KNOCKBACK, VanillaEnchantments::KNOCKBACK()); + $this->register(EnchantmentIds::FIRE_ASPECT, VanillaEnchantments::FIRE_ASPECT()); + + $this->register(EnchantmentIds::EFFICIENCY, VanillaEnchantments::EFFICIENCY()); + $this->register(EnchantmentIds::SILK_TOUCH, VanillaEnchantments::SILK_TOUCH()); + $this->register(EnchantmentIds::UNBREAKING, VanillaEnchantments::UNBREAKING()); + + $this->register(EnchantmentIds::POWER, VanillaEnchantments::POWER()); + $this->register(EnchantmentIds::PUNCH, VanillaEnchantments::PUNCH()); + $this->register(EnchantmentIds::FLAME, VanillaEnchantments::FLAME()); + $this->register(EnchantmentIds::INFINITY, VanillaEnchantments::INFINITY()); + + $this->register(EnchantmentIds::MENDING, VanillaEnchantments::MENDING()); + + $this->register(EnchantmentIds::VANISHING, VanillaEnchantments::VANISHING()); + } + + public function register(int $mcpeId, Enchantment $enchantment) : void{ + $this->idToEnch[$mcpeId] = $enchantment; + $this->enchToId[spl_object_id($enchantment)] = $mcpeId; + } + + public function fromId(int $id) : ?Enchantment{ + //we might not have all the enchantment IDs registered + return $this->idToEnch[$id] ?? null; + } + + public function toId(Enchantment $enchantment) : int{ + if(!array_key_exists(spl_object_id($enchantment), $this->enchToId)){ + //this should never happen, so we treat it as an exceptional condition + throw new \InvalidArgumentException("Enchantment does not have a mapped ID"); + } + return $this->enchToId[spl_object_id($enchantment)]; + } +} diff --git a/src/data/bedrock/EnchantmentIds.php b/src/data/bedrock/EnchantmentIds.php new file mode 100644 index 0000000000..51b076c11d --- /dev/null +++ b/src/data/bedrock/EnchantmentIds.php @@ -0,0 +1,69 @@ + + */ + private $legacyToString = []; + /** + * @var int[] + * @phpstan-var array + */ + private $stringToLegacy = []; + + public function __construct(string $file){ + $stringToLegacyId = json_decode(file_get_contents($file), true); + if(!is_array($stringToLegacyId)){ + throw new AssumptionFailedError("Invalid format of ID map"); + } + foreach($stringToLegacyId as $stringId => $legacyId){ + if(!is_string($stringId) or !is_int($legacyId)){ + throw new AssumptionFailedError("ID map should have string keys and int values"); + } + $this->legacyToString[$legacyId] = $stringId; + $this->stringToLegacy[$stringId] = $legacyId; + } + } + + public function legacyToString(int $legacy) : ?string{ + return $this->legacyToString[$legacy] ?? null; + } + + public function stringToLegacy(string $string) : ?int{ + return $this->stringToLegacy[$string] ?? null; + } + + /** + * @return string[] + * @phpstan-return array + */ + public function getLegacyToStringMap() : array{ + return $this->legacyToString; + } + + /** + * @return int[] + * @phpstan-return array + */ + public function getStringToLegacyMap() : array{ + return $this->stringToLegacy; + } +} diff --git a/src/data/bedrock/MushroomBlockTypeIdMap.php b/src/data/bedrock/MushroomBlockTypeIdMap.php new file mode 100644 index 0000000000..f3bf8711bf --- /dev/null +++ b/src/data/bedrock/MushroomBlockTypeIdMap.php @@ -0,0 +1,74 @@ + + */ + private array $idToEnum = []; + /** + * @var int[] + * @phpstan-var array + */ + private array $enumToId = []; + + public function __construct(){ + $this->register(LegacyMeta::MUSHROOM_BLOCK_ALL_PORES, MushroomBlockType::PORES()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHWEST_CORNER, MushroomBlockType::CAP_NORTHWEST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_NORTH_SIDE, MushroomBlockType::CAP_NORTH()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_NORTHEAST_CORNER, MushroomBlockType::CAP_NORTHEAST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_WEST_SIDE, MushroomBlockType::CAP_WEST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_TOP_ONLY, MushroomBlockType::CAP_MIDDLE()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_EAST_SIDE, MushroomBlockType::CAP_EAST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHWEST_CORNER, MushroomBlockType::CAP_SOUTHWEST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTH_SIDE, MushroomBlockType::CAP_SOUTH()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_CAP_SOUTHEAST_CORNER, MushroomBlockType::CAP_SOUTHEAST()); + $this->register(LegacyMeta::MUSHROOM_BLOCK_ALL_CAP, MushroomBlockType::ALL_CAP()); + } + + public function register(int $id, MushroomBlockType $type) : void{ + $this->idToEnum[$id] = $type; + $this->enumToId[$type->id()] = $id; + } + + public function fromId(int $id) : ?MushroomBlockType{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(MushroomBlockType $type) : int{ + if(!array_key_exists($type->id(), $this->enumToId)){ + throw new \InvalidArgumentException("Mushroom block type does not have a mapped ID"); //this should never happen + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/data/bedrock/PotionTypeIdMap.php b/src/data/bedrock/PotionTypeIdMap.php new file mode 100644 index 0000000000..4fcb9b693e --- /dev/null +++ b/src/data/bedrock/PotionTypeIdMap.php @@ -0,0 +1,104 @@ + + */ + private array $idToEnum; + + /** + * @var int[] + * @phpstan-var array + */ + private array $enumToId; + + private function __construct(){ + $this->register(PotionTypeIds::WATER, PotionType::WATER()); + $this->register(PotionTypeIds::MUNDANE, PotionType::MUNDANE()); + $this->register(PotionTypeIds::LONG_MUNDANE, PotionType::LONG_MUNDANE()); + $this->register(PotionTypeIds::THICK, PotionType::THICK()); + $this->register(PotionTypeIds::AWKWARD, PotionType::AWKWARD()); + $this->register(PotionTypeIds::NIGHT_VISION, PotionType::NIGHT_VISION()); + $this->register(PotionTypeIds::LONG_NIGHT_VISION, PotionType::LONG_NIGHT_VISION()); + $this->register(PotionTypeIds::INVISIBILITY, PotionType::INVISIBILITY()); + $this->register(PotionTypeIds::LONG_INVISIBILITY, PotionType::LONG_INVISIBILITY()); + $this->register(PotionTypeIds::LEAPING, PotionType::LEAPING()); + $this->register(PotionTypeIds::LONG_LEAPING, PotionType::LONG_LEAPING()); + $this->register(PotionTypeIds::STRONG_LEAPING, PotionType::STRONG_LEAPING()); + $this->register(PotionTypeIds::FIRE_RESISTANCE, PotionType::FIRE_RESISTANCE()); + $this->register(PotionTypeIds::LONG_FIRE_RESISTANCE, PotionType::LONG_FIRE_RESISTANCE()); + $this->register(PotionTypeIds::SWIFTNESS, PotionType::SWIFTNESS()); + $this->register(PotionTypeIds::LONG_SWIFTNESS, PotionType::LONG_SWIFTNESS()); + $this->register(PotionTypeIds::STRONG_SWIFTNESS, PotionType::STRONG_SWIFTNESS()); + $this->register(PotionTypeIds::SLOWNESS, PotionType::SLOWNESS()); + $this->register(PotionTypeIds::LONG_SLOWNESS, PotionType::LONG_SLOWNESS()); + $this->register(PotionTypeIds::WATER_BREATHING, PotionType::WATER_BREATHING()); + $this->register(PotionTypeIds::LONG_WATER_BREATHING, PotionType::LONG_WATER_BREATHING()); + $this->register(PotionTypeIds::HEALING, PotionType::HEALING()); + $this->register(PotionTypeIds::STRONG_HEALING, PotionType::STRONG_HEALING()); + $this->register(PotionTypeIds::HARMING, PotionType::HARMING()); + $this->register(PotionTypeIds::STRONG_HARMING, PotionType::STRONG_HARMING()); + $this->register(PotionTypeIds::POISON, PotionType::POISON()); + $this->register(PotionTypeIds::LONG_POISON, PotionType::LONG_POISON()); + $this->register(PotionTypeIds::STRONG_POISON, PotionType::STRONG_POISON()); + $this->register(PotionTypeIds::REGENERATION, PotionType::REGENERATION()); + $this->register(PotionTypeIds::LONG_REGENERATION, PotionType::LONG_REGENERATION()); + $this->register(PotionTypeIds::STRONG_REGENERATION, PotionType::STRONG_REGENERATION()); + $this->register(PotionTypeIds::STRENGTH, PotionType::STRENGTH()); + $this->register(PotionTypeIds::LONG_STRENGTH, PotionType::LONG_STRENGTH()); + $this->register(PotionTypeIds::STRONG_STRENGTH, PotionType::STRONG_STRENGTH()); + $this->register(PotionTypeIds::WEAKNESS, PotionType::WEAKNESS()); + $this->register(PotionTypeIds::LONG_WEAKNESS, PotionType::LONG_WEAKNESS()); + $this->register(PotionTypeIds::WITHER, PotionType::WITHER()); + $this->register(PotionTypeIds::TURTLE_MASTER, PotionType::TURTLE_MASTER()); + $this->register(PotionTypeIds::LONG_TURTLE_MASTER, PotionType::LONG_TURTLE_MASTER()); + $this->register(PotionTypeIds::STRONG_TURTLE_MASTER, PotionType::STRONG_TURTLE_MASTER()); + $this->register(PotionTypeIds::SLOW_FALLING, PotionType::SLOW_FALLING()); + $this->register(PotionTypeIds::LONG_SLOW_FALLING, PotionType::LONG_SLOW_FALLING()); + } + + private function register(int $id, PotionType $type) : void{ + $this->idToEnum[$id] = $type; + $this->enumToId[$type->id()] = $id; + } + + public function fromId(int $id) : ?PotionType{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(PotionType $type) : int{ + if(!isset($this->enumToId[$type->id()])){ + throw new \InvalidArgumentException("Type does not have a mapped ID"); + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/data/bedrock/PotionTypeIds.php b/src/data/bedrock/PotionTypeIds.php new file mode 100644 index 0000000000..ebe51de540 --- /dev/null +++ b/src/data/bedrock/PotionTypeIds.php @@ -0,0 +1,69 @@ + + */ + private array $idToEnum = []; + + /** + * @var int[] + * @phpstan-var array + */ + private array $enumToId = []; + + public function __construct(){ + $this->register(0, GameMode::SURVIVAL()); + $this->register(1, GameMode::CREATIVE()); + $this->register(2, GameMode::ADVENTURE()); + $this->register(3, GameMode::SPECTATOR()); + } + + private function register(int $id, GameMode $type) : void{ + $this->idToEnum[$id] = $type; + $this->enumToId[$type->id()] = $id; + } + + public function fromId(int $id) : ?GameMode{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(GameMode $type) : int{ + if(!array_key_exists($type->id(), $this->enumToId)){ + throw new \InvalidArgumentException("Game mode does not have a mapped ID"); //this should never happen + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/pocketmine/entity/Ageable.php b/src/entity/Ageable.php similarity index 100% rename from src/pocketmine/entity/Ageable.php rename to src/entity/Ageable.php diff --git a/src/pocketmine/entity/Attribute.php b/src/entity/Attribute.php similarity index 53% rename from src/pocketmine/entity/Attribute.php rename to src/entity/Attribute.php index 1c53e8f831..589a5da47f 100644 --- a/src/pocketmine/entity/Attribute.php +++ b/src/entity/Attribute.php @@ -27,28 +27,29 @@ use function max; use function min; class Attribute{ + public const MC_PREFIX = "minecraft:"; - public const ABSORPTION = 0; - public const SATURATION = 1; - public const EXHAUSTION = 2; - public const KNOCKBACK_RESISTANCE = 3; - public const HEALTH = 4; - public const MOVEMENT_SPEED = 5; - public const FOLLOW_RANGE = 6; - public const HUNGER = 7; - public const FOOD = 7; - public const ATTACK_DAMAGE = 8; - public const EXPERIENCE_LEVEL = 9; - public const EXPERIENCE = 10; - public const UNDERWATER_MOVEMENT = 11; - public const LUCK = 12; - public const FALL_DAMAGE = 13; - public const HORSE_JUMP_STRENGTH = 14; - public const ZOMBIE_SPAWN_REINFORCEMENTS = 15; - public const LAVA_MOVEMENT = 16; + public const ABSORPTION = self::MC_PREFIX . "absorption"; + public const SATURATION = self::MC_PREFIX . "player.saturation"; + public const EXHAUSTION = self::MC_PREFIX . "player.exhaustion"; + public const KNOCKBACK_RESISTANCE = self::MC_PREFIX . "knockback_resistance"; + public const HEALTH = self::MC_PREFIX . "health"; + public const MOVEMENT_SPEED = self::MC_PREFIX . "movement"; + public const FOLLOW_RANGE = self::MC_PREFIX . "follow_range"; + public const HUNGER = self::MC_PREFIX . "player.hunger"; + public const FOOD = self::HUNGER; + public const ATTACK_DAMAGE = self::MC_PREFIX . "attack_damage"; + public const EXPERIENCE_LEVEL = self::MC_PREFIX . "player.level"; + public const EXPERIENCE = self::MC_PREFIX . "player.experience"; + public const UNDERWATER_MOVEMENT = self::MC_PREFIX . "underwater_movement"; + public const LUCK = self::MC_PREFIX . "luck"; + public const FALL_DAMAGE = self::MC_PREFIX . "fall_damage"; + public const HORSE_JUMP_STRENGTH = self::MC_PREFIX . "horse.jump_strength"; + public const ZOMBIE_SPAWN_REINFORCEMENTS = self::MC_PREFIX . "zombie.spawn_reinforcements"; + public const LAVA_MOVEMENT = self::MC_PREFIX . "lava_movement"; - /** @var int */ - private $id; + /** @var string */ + protected $id; /** @var float */ protected $minValue; /** @var float */ @@ -57,65 +58,17 @@ class Attribute{ protected $defaultValue; /** @var float */ protected $currentValue; - /** @var string */ - protected $name; /** @var bool */ protected $shouldSend; /** @var bool */ protected $desynchronized = true; - /** @var Attribute[] */ - protected static $attributes = []; - - public static function init() : void{ - self::addAttribute(self::ABSORPTION, "minecraft:absorption", 0.00, 340282346638528859811704183484516925440.00, 0.00); - self::addAttribute(self::SATURATION, "minecraft:player.saturation", 0.00, 20.00, 20.00); - self::addAttribute(self::EXHAUSTION, "minecraft:player.exhaustion", 0.00, 5.00, 0.0, false); - self::addAttribute(self::KNOCKBACK_RESISTANCE, "minecraft:knockback_resistance", 0.00, 1.00, 0.00); - self::addAttribute(self::HEALTH, "minecraft:health", 0.00, 20.00, 20.00); - self::addAttribute(self::MOVEMENT_SPEED, "minecraft:movement", 0.00, 340282346638528859811704183484516925440.00, 0.10); - self::addAttribute(self::FOLLOW_RANGE, "minecraft:follow_range", 0.00, 2048.00, 16.00, false); - self::addAttribute(self::HUNGER, "minecraft:player.hunger", 0.00, 20.00, 20.00); - self::addAttribute(self::ATTACK_DAMAGE, "minecraft:attack_damage", 0.00, 340282346638528859811704183484516925440.00, 1.00, false); - self::addAttribute(self::EXPERIENCE_LEVEL, "minecraft:player.level", 0.00, 24791.00, 0.00); - self::addAttribute(self::EXPERIENCE, "minecraft:player.experience", 0.00, 1.00, 0.00); - self::addAttribute(self::UNDERWATER_MOVEMENT, "minecraft:underwater_movement", 0.0, 340282346638528859811704183484516925440.0, 0.02); - self::addAttribute(self::LUCK, "minecraft:luck", -1024.0, 1024.0, 0.0); - self::addAttribute(self::FALL_DAMAGE, "minecraft:fall_damage", 0.0, 340282346638528859811704183484516925440.0, 1.0); - self::addAttribute(self::HORSE_JUMP_STRENGTH, "minecraft:horse.jump_strength", 0.0, 2.0, 0.7); - self::addAttribute(self::ZOMBIE_SPAWN_REINFORCEMENTS, "minecraft:zombie.spawn_reinforcements", 0.0, 1.0, 0.0); - self::addAttribute(self::LAVA_MOVEMENT, "minecraft:lava_movement", 0.0, 340282346638528859811704183484516925440.0, 0.02); - } - - /** - * @throws \InvalidArgumentException - */ - public static function addAttribute(int $id, string $name, float $minValue, float $maxValue, float $defaultValue, bool $shouldSend = true) : Attribute{ + public function __construct(string $id, float $minValue, float $maxValue, float $defaultValue, bool $shouldSend = true){ if($minValue > $maxValue or $defaultValue > $maxValue or $defaultValue < $minValue){ throw new \InvalidArgumentException("Invalid ranges: min value: $minValue, max value: $maxValue, $defaultValue: $defaultValue"); } - - return self::$attributes[$id] = new Attribute($id, $name, $minValue, $maxValue, $defaultValue, $shouldSend); - } - - public static function getAttribute(int $id) : ?Attribute{ - return isset(self::$attributes[$id]) ? clone self::$attributes[$id] : null; - } - - public static function getAttributeByName(string $name) : ?Attribute{ - foreach(self::$attributes as $a){ - if($a->getName() === $name){ - return clone $a; - } - } - - return null; - } - - private function __construct(int $id, string $name, float $minValue, float $maxValue, float $defaultValue, bool $shouldSend = true){ $this->id = $id; - $this->name = $name; $this->minValue = $minValue; $this->maxValue = $maxValue; $this->defaultValue = $defaultValue; @@ -210,11 +163,7 @@ class Attribute{ return $this; } - public function getName() : string{ - return $this->name; - } - - public function getId() : int{ + public function getId() : string{ return $this->id; } diff --git a/src/entity/AttributeFactory.php b/src/entity/AttributeFactory.php new file mode 100644 index 0000000000..1dcff9b60b --- /dev/null +++ b/src/entity/AttributeFactory.php @@ -0,0 +1,72 @@ +register(Attribute::ABSORPTION, 0.00, 340282346638528859811704183484516925440.00, 0.00); + $this->register(Attribute::SATURATION, 0.00, 20.00, 20.00); + $this->register(Attribute::EXHAUSTION, 0.00, 5.00, 0.0, false); + $this->register(Attribute::KNOCKBACK_RESISTANCE, 0.00, 1.00, 0.00); + $this->register(Attribute::HEALTH, 0.00, 20.00, 20.00); + $this->register(Attribute::MOVEMENT_SPEED, 0.00, 340282346638528859811704183484516925440.00, 0.10); + $this->register(Attribute::FOLLOW_RANGE, 0.00, 2048.00, 16.00, false); + $this->register(Attribute::HUNGER, 0.00, 20.00, 20.00); + $this->register(Attribute::ATTACK_DAMAGE, 0.00, 340282346638528859811704183484516925440.00, 1.00, false); + $this->register(Attribute::EXPERIENCE_LEVEL, 0.00, 24791.00, 0.00); + $this->register(Attribute::EXPERIENCE, 0.00, 1.00, 0.00); + $this->register(Attribute::UNDERWATER_MOVEMENT, 0.0, 340282346638528859811704183484516925440.0, 0.02); + $this->register(Attribute::LUCK, -1024.0, 1024.0, 0.0); + $this->register(Attribute::FALL_DAMAGE, 0.0, 340282346638528859811704183484516925440.0, 1.0); + $this->register(Attribute::HORSE_JUMP_STRENGTH, 0.0, 2.0, 0.7); + $this->register(Attribute::ZOMBIE_SPAWN_REINFORCEMENTS, 0.0, 1.0, 0.0); + $this->register(Attribute::LAVA_MOVEMENT, 0.0, 340282346638528859811704183484516925440.0, 0.02); + } + + public function get(string $id) : ?Attribute{ + return isset($this->attributes[$id]) ? clone $this->attributes[$id] : null; + } + + public function mustGet(string $id) : Attribute{ + $result = $this->get($id); + if($result === null){ + throw new \InvalidArgumentException("Attribute $id is not registered"); + } + return $result; + } + + /** + * @throws \InvalidArgumentException + */ + public function register(string $id, float $minValue, float $maxValue, float $defaultValue, bool $shouldSend = true) : Attribute{ + return $this->attributes[$id] = new Attribute($id, $minValue, $maxValue, $defaultValue, $shouldSend); + } +} diff --git a/src/pocketmine/entity/AttributeMap.php b/src/entity/AttributeMap.php similarity index 58% rename from src/pocketmine/entity/AttributeMap.php rename to src/entity/AttributeMap.php index 73de5cbc99..0a4a11d3de 100644 --- a/src/pocketmine/entity/AttributeMap.php +++ b/src/entity/AttributeMap.php @@ -25,18 +25,15 @@ namespace pocketmine\entity; use function array_filter; -/** - * @phpstan-implements \ArrayAccess - */ -class AttributeMap implements \ArrayAccess{ +class AttributeMap{ /** @var Attribute[] */ private $attributes = []; - public function addAttribute(Attribute $attribute) : void{ + public function add(Attribute $attribute) : void{ $this->attributes[$attribute->getId()] = $attribute; } - public function getAttribute(int $id) : ?Attribute{ + public function get(string $id) : ?Attribute{ return $this->attributes[$id] ?? null; } @@ -55,36 +52,4 @@ class AttributeMap implements \ArrayAccess{ return $attribute->isSyncable() and $attribute->isDesynchronized(); }); } - - /** - * @param int $offset - */ - public function offsetExists($offset) : bool{ - return isset($this->attributes[$offset]); - } - - /** - * @param int $offset - */ - public function offsetGet($offset) : float{ - return $this->attributes[$offset]->getValue(); - } - - /** - * @param int|null $offset - * @param float $value - */ - public function offsetSet($offset, $value) : void{ - if($offset === null){ - throw new \InvalidArgumentException("Array push syntax is not supported"); - } - $this->attributes[$offset]->setValue($value); - } - - /** - * @param int $offset - */ - public function offsetUnset($offset) : void{ - throw new \RuntimeException("Could not unset an attribute from an attribute map"); - } } diff --git a/src/entity/Consumable.php b/src/entity/Consumable.php new file mode 100644 index 0000000000..650c1dd08a --- /dev/null +++ b/src/entity/Consumable.php @@ -0,0 +1,41 @@ +timings = Timings::getEntityTimings($this); + + $this->size = $this->getInitialSizeInfo(); + + $this->id = self::nextRuntimeId(); + $this->server = $location->getWorld()->getServer(); + + $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) + ); + + $this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0); + $this->recalculateBoundingBox(); + + if($nbt !== null){ + $this->motion = EntityDataHelper::parseVec3($nbt, "Motion", true); + }else{ + $this->motion = new Vector3(0, 0, 0); + } + + $this->resetLastMovements(); + + $this->networkProperties = new EntityMetadataCollection(); + + $this->attributeMap = new AttributeMap(); + $this->addAttributes(); + + $this->initEntity($nbt ?? new CompoundTag()); + + $this->getWorld()->addEntity($this); + + $this->lastUpdate = $this->server->getTick(); + (new EntitySpawnEvent($this))->call(); + + $this->scheduleUpdate(); + + } + + abstract protected function getInitialSizeInfo() : EntitySizeInfo; + + public function getNameTag() : string{ + return $this->nameTag; + } + + public function isNameTagVisible() : bool{ + return $this->nameTagVisible; + } + + public function isNameTagAlwaysVisible() : bool{ + return $this->alwaysShowNameTag; + } + + public function setNameTag(string $name) : void{ + $this->nameTag = $name; + $this->networkPropertiesDirty = true; + } + + public function setNameTagVisible(bool $value = true) : void{ + $this->nameTagVisible = $value; + } + + public function setNameTagAlwaysVisible(bool $value = true) : void{ + $this->alwaysShowNameTag = $value; + $this->networkPropertiesDirty = true; + } + + public function getScoreTag() : ?string{ + return $this->scoreTag; //TODO: maybe this shouldn't be nullable? + } + + public function setScoreTag(string $score) : void{ + $this->scoreTag = $score; + $this->networkPropertiesDirty = true; + } + + public function getScale() : float{ + return $this->scale; + } + + public function setScale(float $value) : void{ + 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; + } + + public function getBoundingBox() : AxisAlignedBB{ + return $this->boundingBox; + } + + protected function recalculateBoundingBox() : void{ + $halfWidth = $this->size->getWidth() / 2; + + $this->boundingBox = new AxisAlignedBB( + $this->location->x - $halfWidth, + $this->location->y + $this->ySize, + $this->location->z - $halfWidth, + $this->location->x + $halfWidth, + $this->location->y + $this->size->getHeight() + $this->ySize, + $this->location->z + $halfWidth + ); + } + + public function isImmobile() : bool{ + return $this->immobile; + } + + public function setImmobile(bool $value = true) : void{ + $this->immobile = $value; + $this->networkPropertiesDirty = true; + } + + public function isInvisible() : bool{ + return $this->invisible; + } + + public function setInvisible(bool $value = true) : void{ + $this->invisible = $value; + $this->networkPropertiesDirty = true; + } + + public function isSilent() : bool{ + return $this->silent; + } + + public function setSilent(bool $value = true) : void{ + $this->silent = $value; + $this->networkPropertiesDirty = true; + } + + /** + * Returns whether the entity is able to climb blocks such as ladders or vines. + */ + public function canClimb() : bool{ + return $this->canClimb; + } + + /** + * Sets whether the entity is able to climb climbable blocks. + */ + public function setCanClimb(bool $value = true) : void{ + $this->canClimb = $value; + $this->networkPropertiesDirty = true; + } + + /** + * Returns whether this entity is climbing a block. By default this is only true if the entity is climbing a ladder or vine or similar block. + */ + public function canClimbWalls() : bool{ + return $this->canClimbWalls; + } + + /** + * Sets whether the entity is climbing a block. If true, the entity can climb anything. + */ + public function setCanClimbWalls(bool $value = true) : void{ + $this->canClimbWalls = $value; + $this->networkPropertiesDirty = true; + } + + /** + * Returns the entity ID of the owning entity, or null if the entity doesn't have an owner. + */ + public function getOwningEntityId() : ?int{ + return $this->ownerId; + } + + /** + * Returns the owning entity, or null if the entity was not found. + */ + public function getOwningEntity() : ?Entity{ + return $this->ownerId !== null ? $this->server->getWorldManager()->findEntity($this->ownerId) : null; + } + + /** + * Sets the owner of the entity. Passing null will remove the current owner. + * + * @throws \InvalidArgumentException if the supplied entity is not valid + */ + public function setOwningEntity(?Entity $owner) : void{ + if($owner === null){ + $this->ownerId = null; + }elseif($owner->closed){ + throw new \InvalidArgumentException("Supplied owning entity is garbage and cannot be used"); + }else{ + $this->ownerId = $owner->getId(); + } + $this->networkPropertiesDirty = true; + } + + /** + * Returns the entity ID of the entity's target, or null if it doesn't have a target. + */ + public function getTargetEntityId() : ?int{ + return $this->targetId; + } + + /** + * Returns the entity's target entity, or null if not found. + * This is used for things like hostile mobs attacking entities, and for fishing rods reeling hit entities in. + */ + public function getTargetEntity() : ?Entity{ + return $this->targetId !== null ? $this->server->getWorldManager()->findEntity($this->targetId) : null; + } + + /** + * Sets the entity's target entity. Passing null will remove the current target. + * + * @throws \InvalidArgumentException if the target entity is not valid + */ + public function setTargetEntity(?Entity $target) : void{ + if($target === null){ + $this->targetId = null; + }elseif($target->closed){ + throw new \InvalidArgumentException("Supplied target entity is garbage and cannot be used"); + }else{ + $this->targetId = $target->getId(); + } + $this->networkPropertiesDirty = true; + } + + /** + * Returns whether this entity will be saved when its chunk is unloaded. + */ + public function canSaveWithChunk() : bool{ + return $this->savedWithChunk; + } + + /** + * Sets whether this entity will be saved when its chunk is unloaded. This can be used to prevent the entity being + * saved to disk. + */ + public function setCanSaveWithChunk(bool $value) : void{ + $this->savedWithChunk = $value; + } + + public function saveNBT() : CompoundTag{ + $nbt = CompoundTag::create() + ->setTag("Pos", new ListTag([ + new DoubleTag($this->location->x), + new DoubleTag($this->location->y), + new DoubleTag($this->location->z) + ])) + ->setTag("Motion", new ListTag([ + new DoubleTag($this->motion->x), + new DoubleTag($this->motion->y), + new DoubleTag($this->motion->z) + ])) + ->setTag("Rotation", new ListTag([ + new FloatTag($this->location->yaw), + new FloatTag($this->location->pitch) + ])); + + if(!($this instanceof Player)){ + EntityFactory::getInstance()->injectSaveId(get_class($this), $nbt); + + if($this->getNameTag() !== ""){ + $nbt->setString("CustomName", $this->getNameTag()); + $nbt->setByte("CustomNameVisible", $this->isNameTagVisible() ? 1 : 0); + } + } + + $nbt->setFloat("FallDistance", $this->fallDistance); + $nbt->setShort("Fire", $this->fireTicks); + $nbt->setByte("OnGround", $this->onGround ? 1 : 0); + + return $nbt; + } + + protected function initEntity(CompoundTag $nbt) : void{ + $this->fireTicks = $nbt->getShort("Fire", 0); + + $this->onGround = $nbt->getByte("OnGround", 0) !== 0; + + $this->fallDistance = $nbt->getFloat("FallDistance", 0.0); + + if(($customNameTag = $nbt->getTag("CustomName")) instanceof StringTag){ + $this->setNameTag($customNameTag->getValue()); + + if(($customNameVisibleTag = $nbt->getTag("CustomNameVisible")) instanceof StringTag){ + //Older versions incorrectly saved this as a string (see 890f72dbf23a77f294169b79590770470041adc4) + $this->setNameTagVisible($customNameVisibleTag->getValue() !== ""); + }else{ + $this->setNameTagVisible($nbt->getByte("CustomNameVisible", 1) !== 0); + } + } + } + + protected function addAttributes() : void{ + + } + + public function attack(EntityDamageEvent $source) : void{ + $source->call(); + if($source->isCancelled()){ + return; + } + + $this->setLastDamageCause($source); + + $this->setHealth($this->getHealth() - $source->getFinalDamage()); + } + + public function heal(EntityRegainHealthEvent $source) : void{ + $source->call(); + if($source->isCancelled()){ + return; + } + + $this->setHealth($this->getHealth() + $source->getAmount()); + } + + public function kill() : void{ + if($this->isAlive()){ + $this->health = 0; + $this->onDeath(); + $this->scheduleUpdate(); + } + } + + /** + * Override this to do actions on death. + */ + protected function onDeath() : void{ + + } + + /** + * Called to tick entities while dead. Returns whether the entity should be flagged for despawn yet. + */ + protected function onDeathUpdate(int $tickDiff) : bool{ + return true; + } + + public function isAlive() : bool{ + return $this->health > 0; + } + + public function getHealth() : float{ + return $this->health; + } + + /** + * Sets the health of the Entity. This won't send any update to the players + */ + public function setHealth(float $amount) : void{ + if($amount == $this->health){ + return; + } + + if($amount <= 0){ + if($this->isAlive()){ + if(!$this->justCreated){ + $this->kill(); + }else{ + $this->health = 0; + } + } + }elseif($amount <= $this->getMaxHealth() or $amount < $this->health){ + $this->health = $amount; + }else{ + $this->health = $this->getMaxHealth(); + } + } + + public function getMaxHealth() : int{ + return $this->maxHealth; + } + + public function setMaxHealth(int $amount) : void{ + $this->maxHealth = $amount; + } + + public function setLastDamageCause(EntityDamageEvent $type) : void{ + $this->lastDamageCause = $type; + } + + public function getLastDamageCause() : ?EntityDamageEvent{ + return $this->lastDamageCause; + } + + public function getAttributeMap() : AttributeMap{ + return $this->attributeMap; + } + + public function getNetworkProperties() : EntityMetadataCollection{ + return $this->networkProperties; + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + //TODO: check vehicles + + if($this->justCreated){ + $this->justCreated = false; + if(!$this->isAlive()){ + $this->kill(); + } + } + + $changedProperties = $this->getDirtyNetworkData(); + if(count($changedProperties) > 0){ + $this->sendData(null, $changedProperties); + $this->networkProperties->clearDirtyProperties(); + } + + $hasUpdate = false; + + $this->checkBlockIntersections(); + + if($this->location->y <= World::Y_MIN - 16 and $this->isAlive()){ + $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10); + $this->attack($ev); + $hasUpdate = true; + } + + if($this->isOnFire() and $this->doOnFireTick($tickDiff)){ + $hasUpdate = true; + } + + if($this->noDamageTicks > 0){ + $this->noDamageTicks -= $tickDiff; + if($this->noDamageTicks < 0){ + $this->noDamageTicks = 0; + } + } + + $this->ticksLived += $tickDiff; + + return $hasUpdate; + } + + public function isOnFire() : bool{ + return $this->fireTicks > 0; + } + + public function setOnFire(int $seconds) : void{ + $ticks = $seconds * 20; + if($ticks > $this->getFireTicks()){ + $this->setFireTicks($ticks); + } + $this->networkPropertiesDirty = true; + } + + public function getFireTicks() : int{ + return $this->fireTicks; + } + + /** + * @throws \InvalidArgumentException + */ + public function setFireTicks(int $fireTicks) : void{ + if($fireTicks < 0 or $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{ + return false; + } + + protected function doOnFireTick(int $tickDiff = 1) : bool{ + if($this->isFireProof() and $this->fireTicks > 1){ + $this->fireTicks = 1; + }else{ + $this->fireTicks -= $tickDiff; + } + + if(($this->fireTicks % 20 === 0) or $tickDiff > 20){ + $this->dealFireDamage(); + } + + if(!$this->isOnFire()){ + $this->extinguish(); + }else{ + return true; + } + + return false; + } + + /** + * Called to deal damage to entities when they are on fire. + */ + protected function dealFireDamage() : void{ + $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_FIRE_TICK, 1); + $this->attack($ev); + } + + public function canCollideWith(Entity $entity) : bool{ + return !$this->justCreated and $entity !== $this; + } + + public function canBeCollidedWith() : bool{ + return $this->isAlive(); + } + + protected function updateMovement(bool $teleport = false) : void{ + $diffPosition = $this->location->distanceSquared($this->lastLocation); + $diffRotation = ($this->location->yaw - $this->lastLocation->yaw) ** 2 + ($this->location->pitch - $this->lastLocation->pitch) ** 2; + + $diffMotion = $this->motion->subtractVector($this->lastMotion)->lengthSquared(); + + $still = $this->motion->lengthSquared() == 0.0; + $wasStill = $this->lastMotion->lengthSquared() == 0.0; + if($wasStill !== $still){ + //TODO: hack for client-side AI interference: prevent client sided movement when motion is 0 + $this->setImmobile($still); + } + + if($teleport or $diffPosition > 0.0001 or $diffRotation > 1.0 or (!$wasStill and $still)){ + $this->lastLocation = $this->location->asLocation(); + + $this->broadcastMovement($teleport); + } + + if($diffMotion > 0.0025 or $wasStill !== $still){ //0.05 ** 2 + $this->lastMotion = clone $this->motion; + + $this->broadcastMotion(); + } + } + + public function getOffsetPosition(Vector3 $vector3) : Vector3{ + return $vector3; + } + + protected function broadcastMovement(bool $teleport = false) : void{ + $this->server->broadcastPackets($this->hasSpawned, [MoveActorAbsolutePacket::create( + $this->id, + $this->getOffsetPosition($this->location), + + //this looks very odd but is correct as of 1.5.0.7 + //for arrows this is actually x/y/z rotation + //for mobs x and z are used for pitch and yaw, and y is used for headyaw + $this->location->pitch, + $this->location->yaw, + $this->location->yaw, + ( + ($teleport ? MoveActorAbsolutePacket::FLAG_TELEPORT : 0) | + ($this->onGround ? MoveActorAbsolutePacket::FLAG_GROUND : 0) + ) + )]); + } + + protected function broadcastMotion() : void{ + $this->server->broadcastPackets($this->hasSpawned, [SetActorMotionPacket::create($this->id, $this->getMotion())]); + } + + public function hasGravity() : bool{ + return $this->gravityEnabled; + } + + public function setHasGravity(bool $v = true) : void{ + $this->gravityEnabled = $v; + } + + protected function applyDragBeforeGravity() : bool{ + return false; + } + + protected function tryChangeMovement() : void{ + $friction = 1 - $this->drag; + + $mY = $this->motion->y; + + if($this->applyDragBeforeGravity()){ + $mY *= $friction; + } + + if($this->gravityEnabled){ + $mY -= $this->gravity; + } + + if(!$this->applyDragBeforeGravity()){ + $mY *= $friction; + } + + if($this->onGround){ + $friction *= $this->getWorld()->getBlockAt((int) floor($this->location->x), (int) floor($this->location->y - 1), (int) floor($this->location->z))->getFrictionFactor(); + } + + $this->motion = new Vector3($this->motion->x * $friction, $mY, $this->motion->z * $friction); + } + + protected function checkObstruction(float $x, float $y, float $z) : bool{ + $world = $this->getWorld(); + if(count($world->getCollisionBoxes($this, $this->getBoundingBox(), false)) === 0){ + return false; + } + + $floorX = (int) floor($x); + $floorY = (int) floor($y); + $floorZ = (int) floor($z); + + $diffX = $x - $floorX; + $diffY = $y - $floorY; + $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(); + $northNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ - 1)->isSolid(); + $southNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ + 1)->isSolid(); + + $direction = -1; + $limit = 9999; + + if($westNonSolid){ + $limit = $diffX; + $direction = Facing::WEST; + } + + if($eastNonSolid and 1 - $diffX < $limit){ + $limit = 1 - $diffX; + $direction = Facing::EAST; + } + + if($downNonSolid and $diffY < $limit){ + $limit = $diffY; + $direction = Facing::DOWN; + } + + if($upNonSolid and 1 - $diffY < $limit){ + $limit = 1 - $diffY; + $direction = Facing::UP; + } + + if($northNonSolid and $diffZ < $limit){ + $limit = $diffZ; + $direction = Facing::NORTH; + } + + if($southNonSolid and 1 - $diffZ < $limit){ + $direction = Facing::SOUTH; + } + + if($direction === -1){ + return false; + } + + $force = lcg_value() * 0.2 + 0.1; + + $this->motion = match($direction){ + Facing::WEST => $this->motion->withComponents(-$force, null, null), + Facing::EAST => $this->motion->withComponents($force, null, null), + Facing::DOWN => $this->motion->withComponents(null, -$force, null), + Facing::UP => $this->motion->withComponents(null, $force, null), + Facing::NORTH => $this->motion->withComponents(null, null, -$force), + Facing::SOUTH => $this->motion->withComponents(null, null, $force), + }; + return true; + } + + return false; + } + + public function getHorizontalFacing() : int{ + $angle = fmod($this->location->yaw, 360); + if($angle < 0){ + $angle += 360.0; + } + + if((0 <= $angle and $angle < 45) or (315 <= $angle and $angle < 360)){ + return Facing::SOUTH; + } + if(45 <= $angle and $angle < 135){ + return Facing::WEST; + } + if(135 <= $angle and $angle < 225){ + return Facing::NORTH; + } + + return Facing::EAST; + } + + public function getDirectionVector() : Vector3{ + $y = -sin(deg2rad($this->location->pitch)); + $xz = cos(deg2rad($this->location->pitch)); + $x = -$xz * sin(deg2rad($this->location->yaw)); + $z = $xz * cos(deg2rad($this->location->yaw)); + + return (new Vector3($x, $y, $z))->normalize(); + } + + public function getDirectionPlane() : Vector2{ + return (new Vector2(-cos(deg2rad($this->location->yaw) - M_PI_2), -sin(deg2rad($this->location->yaw) - M_PI_2)))->normalize(); + } + + public function onUpdate(int $currentTick) : bool{ + if($this->closed){ + return false; + } + + $tickDiff = $currentTick - $this->lastUpdate; + if($tickDiff <= 0){ + if(!$this->justCreated){ + $this->server->getLogger()->debug("Expected tick difference of at least 1, got $tickDiff for " . get_class($this)); + } + + return true; + } + + $this->lastUpdate = $currentTick; + + if(!$this->isAlive()){ + if($this->onDeathUpdate($tickDiff)){ + $this->flagForDespawn(); + } + + return true; + } + + $this->timings->startTiming(); + + if($this->hasMovementUpdate()){ + $this->tryChangeMovement(); + + $this->motion = $this->motion->withComponents( + abs($this->motion->x) <= self::MOTION_THRESHOLD ? 0 : null, + abs($this->motion->y) <= self::MOTION_THRESHOLD ? 0 : null, + abs($this->motion->z) <= self::MOTION_THRESHOLD ? 0 : null + ); + + if($this->motion->x != 0 or $this->motion->y != 0 or $this->motion->z != 0){ + $this->move($this->motion->x, $this->motion->y, $this->motion->z); + } + + $this->forceMovementUpdate = false; + } + + $this->updateMovement(); + + Timings::$entityBaseTick->startTiming(); + $hasUpdate = $this->entityBaseTick($tickDiff); + Timings::$entityBaseTick->stopTiming(); + + $this->timings->stopTiming(); + + //if($this->isStatic()) + return ($hasUpdate or $this->hasMovementUpdate()); + //return !($this instanceof Player); + } + + final public function scheduleUpdate() : void{ + if($this->closed){ + throw new \LogicException("Cannot schedule update on garbage entity " . get_class($this)); + } + $this->getWorld()->updateEntities[$this->id] = $this; + } + + public function onNearbyBlockChange() : void{ + $this->setForceMovementUpdate(); + $this->scheduleUpdate(); + } + + /** + * Called when a random update is performed on the chunk the entity is in. This happens when the chunk is within the + * ticking chunk range of a player (or chunk loader). + */ + public function onRandomUpdate() : void{ + $this->scheduleUpdate(); + } + + /** + * Flags the entity as needing a movement update on the next tick. Setting this forces a movement update even if the + * entity's motion is zero. Used to trigger movement updates when blocks change near entities. + */ + final public function setForceMovementUpdate(bool $value = true) : void{ + $this->forceMovementUpdate = $value; + + $this->blocksAround = null; + } + + /** + * Returns whether the entity needs a movement update on the next tick. + */ + public function hasMovementUpdate() : bool{ + return ( + $this->forceMovementUpdate or + $this->motion->x != 0 or + $this->motion->y != 0 or + $this->motion->z != 0 or + !$this->onGround + ); + } + + public function getFallDistance() : float{ return $this->fallDistance; } + + public function setFallDistance(float $fallDistance) : void{ + $this->fallDistance = $fallDistance; + } + + public function resetFallDistance() : void{ + $this->fallDistance = 0.0; + } + + protected function updateFallState(float $distanceThisTick, bool $onGround) : ?float{ + if($distanceThisTick < $this->fallDistance){ + //we've fallen some distance (distanceThisTick is negative) + //or we ascended back towards where fall distance was measured from initially (distanceThisTick is positive but less than existing fallDistance) + $this->fallDistance -= $distanceThisTick; + }else{ + //we ascended past the apex where fall distance was originally being measured from + //reset it so it will be measured starting from the new, higher position + $this->fallDistance = 0; + } + if($onGround && $this->fallDistance > 0){ + $newVerticalVelocity = $this->onHitGround(); + $this->resetFallDistance(); + return $newVerticalVelocity; + } + return null; + } + + /** + * Called when a falling entity hits the ground. + */ + protected function onHitGround() : ?float{ + return null; + } + + public function getEyeHeight() : float{ + return $this->size->getEyeHeight(); + } + + public function getEyePos() : Vector3{ + return new Vector3($this->location->x, $this->location->y + $this->getEyeHeight(), $this->location->z); + } + + public function onCollideWithPlayer(Player $player) : void{ + + } + + /** + * Called when interacted or tapped by a Player. Returns whether something happened as a result of the interaction. + */ + public function onInteract(Player $player, Vector3 $clickPos) : bool{ + return false; + } + + public function isUnderwater() : bool{ + $block = $this->getWorld()->getBlockAt((int) floor($this->location->x), $blockY = (int) floor($y = ($this->location->y + $this->getEyeHeight())), (int) floor($this->location->z)); + + if($block instanceof Water){ + $f = ($blockY + 1) - ($block->getFluidHeightPercent() - 0.1111111); + return $y < $f; + } + + return false; + } + + 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()); + } + + protected function move(float $dx, float $dy, float $dz) : void{ + $this->blocksAround = null; + + Timings::$entityMove->startTiming(); + + $wantedX = $dx; + $wantedY = $dy; + $wantedZ = $dz; + + if($this->keepMovement){ + $this->boundingBox->offset($dx, $dy, $dz); + }else{ + $this->ySize *= self::STEP_CLIP_MULTIPLIER; + + $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"); + + $list = $this->getWorld()->getCollisionBoxes($this, $moveBB->addCoord($dx, $dy, $dz), false); + + foreach($list as $bb){ + $dy = $bb->calculateYOffset($moveBB, $dy); + } + + $moveBB->offset(0, $dy, 0); + + $fallingFlag = ($this->onGround or ($dy != $wantedY and $wantedY < 0)); + + foreach($list as $bb){ + $dx = $bb->calculateXOffset($moveBB, $dx); + } + + $moveBB->offset($dx, 0, 0); + + foreach($list as $bb){ + $dz = $bb->calculateZOffset($moveBB, $dz); + } + + $moveBB->offset(0, 0, $dz); + + if($this->stepHeight > 0 and $fallingFlag and ($wantedX != $dx or $wantedZ != $dz)){ + $cx = $dx; + $cy = $dy; + $cz = $dz; + $dx = $wantedX; + $dy = $this->stepHeight; + $dz = $wantedZ; + + $stepBB = clone $this->boundingBox; + + $list = $this->getWorld()->getCollisionBoxes($this, $stepBB->addCoord($dx, $dy, $dz), false); + foreach($list as $bb){ + $dy = $bb->calculateYOffset($stepBB, $dy); + } + + $stepBB->offset(0, $dy, 0); + + foreach($list as $bb){ + $dx = $bb->calculateXOffset($stepBB, $dx); + } + + $stepBB->offset($dx, 0, 0); + + foreach($list as $bb){ + $dz = $bb->calculateZOffset($stepBB, $dz); + } + + $stepBB->offset(0, 0, $dz); + + $reverseDY = -$dy; + foreach($list as $bb){ + $reverseDY = $bb->calculateYOffset($stepBB, $reverseDY); + } + $dy += $reverseDY; + $stepBB->offset(0, $reverseDY, 0); + + if(($cx ** 2 + $cz ** 2) >= ($dx ** 2 + $dz ** 2)){ + $dx = $cx; + $dy = $cy; + $dz = $cz; + }else{ + $moveBB = $stepBB; + $this->ySize += $dy; + } + } + + $this->boundingBox = $moveBB; + } + + $this->location = new Location( + ($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->getWorld()->onEntityMoved($this); + $this->checkBlockIntersections(); + $this->checkGroundState($wantedX, $wantedY, $wantedZ, $dx, $dy, $dz); + $postFallVerticalVelocity = $this->updateFallState($dy, $this->onGround); + + $this->motion = $this->motion->withComponents( + $wantedX != $dx ? 0 : null, + $postFallVerticalVelocity ?? ($wantedY != $dy ? 0 : null), + $wantedZ != $dz ? 0 : null + ); + + //TODO: vehicle collision events (first we need to spawn them!) + + Timings::$entityMove->stopTiming(); + } + + 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); + } + + /** + * @return Block[] + */ + 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; + } + } + } + } + } + + return $this->blocksAround; + } + + /** + * Returns whether this entity can be moved by currents in liquids. + */ + public function canBeMovedByCurrents() : bool{ + return true; + } + + protected function checkBlockIntersections() : void{ + $vectors = []; + + foreach($this->getBlocksAroundWithEntityInsideActions() as $block){ + if(!$block->onEntityInside($this)){ + $this->blocksAround = null; + } + if(($v = $block->addVelocityToEntity($this)) !== null){ + $vectors[] = $v; + } + } + + $vector = Vector3::sum(...$vectors); + if($vector->lengthSquared() > 0){ + $d = 0.014; + $this->motion = $this->motion->addVector($vector->normalize()->multiply($d)); + } + } + + public function getPosition() : Position{ + return $this->location->asPosition(); + } + + public function getLocation() : Location{ + return $this->location->asLocation(); + } + + public function getWorld() : World{ + return $this->location->getWorld(); + } + + protected function setPosition(Vector3 $pos) : bool{ + if($this->closed){ + return false; + } + + $oldWorld = $this->getWorld(); + $newWorld = $pos instanceof Position ? $pos->getWorld() : $oldWorld; + if($oldWorld !== $newWorld){ + $this->despawnFromAll(); + $oldWorld->removeEntity($this); + } + + $this->location = Location::fromObject( + $pos, + $newWorld, + $this->location->yaw, + $this->location->pitch + ); + + $this->recalculateBoundingBox(); + + $this->blocksAround = null; + + if($oldWorld !== $newWorld){ + $newWorld->addEntity($this); + }else{ + $newWorld->onEntityMoved($this); + } + + return true; + } + + public function setRotation(float $yaw, float $pitch) : void{ + $this->location->yaw = $yaw; + $this->location->pitch = $pitch; + $this->scheduleUpdate(); + } + + protected function setPositionAndRotation(Vector3 $pos, float $yaw, float $pitch) : bool{ + if($this->setPosition($pos)){ + $this->setRotation($yaw, $pitch); + + return true; + } + + return false; + } + + protected function resetLastMovements() : void{ + $this->lastLocation = $this->location->asLocation(); + $this->lastMotion = clone $this->motion; + } + + public function getMotion() : Vector3{ + return clone $this->motion; + } + + public function setMotion(Vector3 $motion) : bool{ + if(!$this->justCreated){ + $ev = new EntityMotionEvent($this, $motion); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + } + + $this->motion = clone $motion; + + if(!$this->justCreated){ + $this->updateMovement(); + } + + return true; + } + + /** + * Adds the given values to the entity's motion vector. + */ + public function addMotion(float $x, float $y, float $z) : void{ + $this->motion = $this->motion->add($x, $y, $z); + } + + public function isOnGround() : bool{ + return $this->onGround; + } + + /** + * @param Vector3|Position|Location $pos + */ + public function teleport(Vector3 $pos, ?float $yaw = null, ?float $pitch = null) : bool{ + if($pos instanceof Location){ + $yaw = $yaw ?? $pos->yaw; + $pitch = $pitch ?? $pos->pitch; + } + $from = $this->location->asPosition(); + $to = Position::fromObject($pos, $pos instanceof Position ? $pos->getWorld() : $this->getWorld()); + $ev = new EntityTeleportEvent($this, $from, $to); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + $this->ySize = 0; + $pos = $ev->getTo(); + + $this->setMotion(new Vector3(0, 0, 0)); + if($this->setPositionAndRotation($pos, $yaw ?? $this->location->yaw, $pitch ?? $this->location->pitch)){ + $this->resetFallDistance(); + $this->setForceMovementUpdate(); + + $this->updateMovement(true); + + return true; + } + + return false; + } + + public function getId() : int{ + return $this->id; + } + + /** + * @return Player[] + */ + public function getViewers() : array{ + return $this->hasSpawned; + } + + abstract public static function getNetworkTypeId() : string; + + /** + * Called by spawnTo() to send whatever packets needed to spawn the entity to the client. + */ + protected function sendSpawnPacket(Player $player) : void{ + $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{ + $id = spl_object_id($player); + //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->getWorld() === $this->getWorld() and $player->hasReceivedChunk($this->location->getFloorX() >> Chunk::COORD_BIT_SIZE, $this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE)){ + $this->hasSpawned[$id] = $player; + + $this->sendSpawnPacket($player); + } + } + + public function spawnToAll() : void{ + if($this->closed){ + return; + } + foreach($this->getWorld()->getViewersForPosition($this->location) as $player){ + $this->spawnTo($player); + } + } + + public function respawnToAll() : void{ + foreach($this->hasSpawned as $key => $player){ + unset($this->hasSpawned[$key]); + $this->spawnTo($player); + } + } + + /** + * @deprecated WARNING: This function DOES NOT permanently hide the entity from the player. As soon as the entity or + * player moves, the player will once again be able to see the entity. + */ + public function despawnFrom(Player $player, bool $send = true) : void{ + $id = spl_object_id($player); + if(isset($this->hasSpawned[$id])){ + if($send){ + $player->getNetworkSession()->onEntityRemoved($this); + } + unset($this->hasSpawned[$id]); + } + } + + /** + * @deprecated WARNING: This function DOES NOT permanently hide the entity from viewers. As soon as the entity or + * player moves, viewers will once again be able to see the entity. + */ + public function despawnFromAll() : void{ + foreach($this->hasSpawned as $player){ + $this->despawnFrom($player); + } + } + + /** + * Flags the entity to be removed from the world on the next tick. + */ + public function flagForDespawn() : void{ + $this->needsDespawn = true; + $this->scheduleUpdate(); + } + + public function isFlaggedForDespawn() : bool{ + return $this->needsDespawn; + } + + /** + * Returns whether the entity has been "closed". + */ + public function isClosed() : bool{ + return $this->closed; + } + + /** + * Closes the entity and frees attached references. + * + * WARNING: Entities are unusable after this has been executed! + */ + final public function close() : void{ + if($this->closeInFlight){ + return; + } + + if(!$this->closed){ + $this->closeInFlight = true; + (new EntityDespawnEvent($this))->call(); + + $this->onDispose(); + $this->closed = true; + $this->destroyCycles(); + $this->closeInFlight = false; + } + } + + /** + * Called when the entity is disposed to clean up things like viewers. This SHOULD NOT destroy internal state, + * because it may be needed by descendent classes. + */ + protected function onDispose() : void{ + $this->despawnFromAll(); + if($this->location->isValid()){ + $this->getWorld()->removeEntity($this); + } + } + + /** + * Called when the entity is disposed, after all events have been fired. This should be used to perform destructive + * circular object references and things which could impact memory usage. + * + * It is expected that the object is unusable after this is called. + */ + protected function destroyCycles() : void{ + $this->lastDamageCause = null; + } + + /** + * @param Player[]|null $targets + * @param MetadataProperty[] $data Properly formatted entity data, defaults to everything + * + * @phpstan-param array $data + */ + public function sendData(?array $targets, ?array $data = null) : void{ + $targets = $targets ?? $this->hasSpawned; + $data = $data ?? $this->getAllNetworkData(); + + foreach($targets as $p){ + $p->getNetworkSession()->syncActorData($this, $data); + } + } + + /** + * @return MetadataProperty[] + * @phpstan-return array + */ + final protected function getDirtyNetworkData() : array{ + if($this->networkPropertiesDirty){ + $this->syncNetworkData($this->networkProperties); + $this->networkPropertiesDirty = false; + } + return $this->networkProperties->getDirty(); + } + + /** + * @return MetadataProperty[] + * @phpstan-return array + */ + final protected function getAllNetworkData() : array{ + if($this->networkPropertiesDirty){ + $this->syncNetworkData($this->networkProperties); + $this->networkPropertiesDirty = false; + } + return $this->networkProperties->getAll(); + } + + 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::SCALE, $this->scale); + $properties->setLong(EntityMetadataProperties::LEAD_HOLDER_EID, -1); + $properties->setLong(EntityMetadataProperties::OWNER_EID, $this->ownerId ?? -1); + $properties->setLong(EntityMetadataProperties::TARGET_EID, $this->targetId ?? 0); + $properties->setString(EntityMetadataProperties::NAMETAG, $this->nameTag); + $properties->setString(EntityMetadataProperties::SCORE_TAG, $this->scoreTag); + $properties->setByte(EntityMetadataProperties::COLOR, 0); + + $properties->setGenericFlag(EntityMetadataFlags::AFFECTED_BY_GRAVITY, true); + $properties->setGenericFlag(EntityMetadataFlags::CAN_CLIMB, $this->canClimb); + $properties->setGenericFlag(EntityMetadataFlags::CAN_SHOW_NAMETAG, $this->nameTagVisible); + $properties->setGenericFlag(EntityMetadataFlags::HAS_COLLISION, true); + $properties->setGenericFlag(EntityMetadataFlags::IMMOBILE, $this->immobile); + $properties->setGenericFlag(EntityMetadataFlags::INVISIBLE, $this->invisible); + $properties->setGenericFlag(EntityMetadataFlags::SILENT, $this->silent); + $properties->setGenericFlag(EntityMetadataFlags::ONFIRE, $this->isOnFire()); + $properties->setGenericFlag(EntityMetadataFlags::WALLCLIMBING, $this->canClimbWalls); + } + + /** + * @param Player[]|null $targets + */ + public function broadcastAnimation(Animation $animation, ?array $targets = null) : void{ + $this->server->broadcastPackets($targets ?? $this->getViewers(), $animation->encode()); + } + + /** + * Broadcasts a sound caused by the entity. If the entity is considered "silent", the sound will be dropped. + * @param Player[]|null $targets + */ + public function broadcastSound(Sound $sound, ?array $targets = null) : void{ + if(!$this->silent){ + $this->server->broadcastPackets($targets ?? $this->getViewers(), $sound->encode($this->location)); + } + } + + public function __destruct(){ + $this->close(); + } + + public function __toString(){ + return (new \ReflectionClass($this))->getShortName() . "(" . $this->getId() . ")"; + } +} diff --git a/src/entity/EntityDataHelper.php b/src/entity/EntityDataHelper.php new file mode 100644 index 0000000000..928ebcdc43 --- /dev/null +++ b/src/entity/EntityDataHelper.php @@ -0,0 +1,79 @@ +getTag("Rotation"); + if(!($yawPitch instanceof ListTag) or $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 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){ + return new Vector3(0, 0, 0); + } + if(!($pos instanceof ListTag) or ($pos->getTagType() !== NBT::TAG_Double && $pos->getTagType() !== NBT::TAG_Float)){ + throw new SavedDataLoadingException("'$tagName' should be a List or List"); + } + /** @var DoubleTag[]|FloatTag[] $values */ + $values = $pos->getValue(); + if(count($values) !== 3){ + throw new SavedDataLoadingException("Expected exactly 3 entries in '$tagName' tag"); + } + return new Vector3($values[0]->getValue(), $values[1]->getValue(), $values[2]->getValue()); + } +} diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php new file mode 100644 index 0000000000..0010f0ace9 --- /dev/null +++ b/src/entity/EntityFactory.php @@ -0,0 +1,267 @@ + creator function + * @phpstan-var array + */ + private $creationFuncs = []; + /** + * @var string[] + * @phpstan-var array, string> + */ + private $saveNames = []; + + public function __construct(){ + //define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC + //TODO: index them by version to allow proper multi-save compatibility + + $this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{ + return new Arrow(EntityDataHelper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt); + }, ['Arrow', 'minecraft:arrow'], EntityLegacyIds::ARROW); + + $this->register(Egg::class, function(World $world, CompoundTag $nbt) : Egg{ + return new Egg(EntityDataHelper::parseLocation($nbt, $world), null, $nbt); + }, ['Egg', 'minecraft:egg'], EntityLegacyIds::EGG); + + $this->register(EnderPearl::class, function(World $world, CompoundTag $nbt) : EnderPearl{ + return new EnderPearl(EntityDataHelper::parseLocation($nbt, $world), null, $nbt); + }, ['ThrownEnderpearl', 'minecraft:ender_pearl'], EntityLegacyIds::ENDER_PEARL); + + $this->register(ExperienceBottle::class, function(World $world, CompoundTag $nbt) : ExperienceBottle{ + return new ExperienceBottle(EntityDataHelper::parseLocation($nbt, $world), null, $nbt); + }, ['ThrownExpBottle', 'minecraft:xp_bottle'], EntityLegacyIds::XP_BOTTLE); + + $this->register(ExperienceOrb::class, function(World $world, CompoundTag $nbt) : ExperienceOrb{ + $value = 1; + if(($valuePcTag = $nbt->getTag(ExperienceOrb::TAG_VALUE_PC)) instanceof ShortTag){ //PC + $value = $valuePcTag->getValue(); + }elseif(($valuePeTag = $nbt->getTag(ExperienceOrb::TAG_VALUE_PE)) instanceof IntTag){ //PE save format + $value = $valuePeTag->getValue(); + } + + return new ExperienceOrb(EntityDataHelper::parseLocation($nbt, $world), $value, $nbt); + }, ['XPOrb', 'minecraft:xp_orb'], EntityLegacyIds::XP_ORB); + + $this->register(FallingBlock::class, function(World $world, CompoundTag $nbt) : FallingBlock{ + return new FallingBlock(EntityDataHelper::parseLocation($nbt, $world), FallingBlock::parseBlockNBT(BlockFactory::getInstance(), $nbt), $nbt); + }, ['FallingSand', 'minecraft:falling_block'], EntityLegacyIds::FALLING_BLOCK); + + $this->register(ItemEntity::class, function(World $world, CompoundTag $nbt) : ItemEntity{ + $itemTag = $nbt->getCompoundTag("Item"); + if($itemTag === null){ + throw new SavedDataLoadingException("Expected \"Item\" NBT tag not found"); + } + + $item = Item::nbtDeserialize($itemTag); + if($item->isNull()){ + throw new SavedDataLoadingException("Item is invalid"); + } + return new ItemEntity(EntityDataHelper::parseLocation($nbt, $world), $item, $nbt); + }, ['Item', 'minecraft:item'], EntityLegacyIds::ITEM); + + $this->register(Painting::class, function(World $world, CompoundTag $nbt) : Painting{ + $motive = PaintingMotive::getMotiveByName($nbt->getString("Motive")); + if($motive === null){ + 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){ + $facing = Painting::DATA_TO_FACING[$directionTag->getValue()] ?? Facing::NORTH; + }elseif(($facingTag = $nbt->getTag("Facing")) instanceof ByteTag){ + $facing = Painting::DATA_TO_FACING[$facingTag->getValue()] ?? Facing::NORTH; + }else{ + throw new SavedDataLoadingException("Missing facing info"); + } + + return new Painting(EntityDataHelper::parseLocation($nbt, $world), $blockIn, $facing, $motive, $nbt); + }, ['Painting', 'minecraft:painting'], EntityLegacyIds::PAINTING); + + $this->register(PrimedTNT::class, function(World $world, CompoundTag $nbt) : PrimedTNT{ + return new PrimedTNT(EntityDataHelper::parseLocation($nbt, $world), $nbt); + }, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt'], EntityLegacyIds::TNT); + + $this->register(Snowball::class, function(World $world, CompoundTag $nbt) : Snowball{ + return new Snowball(EntityDataHelper::parseLocation($nbt, $world), null, $nbt); + }, ['Snowball', 'minecraft:snowball'], EntityLegacyIds::SNOWBALL); + + $this->register(SplashPotion::class, function(World $world, CompoundTag $nbt) : SplashPotion{ + $potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort("PotionId", PotionTypeIds::WATER)); + if($potionType === null){ + throw new SavedDataLoadingException("No such potion type"); + } + return new SplashPotion(EntityDataHelper::parseLocation($nbt, $world), null, $potionType, $nbt); + }, ['ThrownPotion', 'minecraft:potion', 'thrownpotion'], EntityLegacyIds::SPLASH_POTION); + + $this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{ + return new Squid(EntityDataHelper::parseLocation($nbt, $world), $nbt); + }, ['Squid', 'minecraft:squid'], EntityLegacyIds::SQUID); + + $this->register(Villager::class, function(World $world, CompoundTag $nbt) : Villager{ + return new Villager(EntityDataHelper::parseLocation($nbt, $world), $nbt); + }, ['Villager', 'minecraft:villager'], EntityLegacyIds::VILLAGER); + + $this->register(Zombie::class, function(World $world, CompoundTag $nbt) : Zombie{ + return new Zombie(EntityDataHelper::parseLocation($nbt, $world), $nbt); + }, ['Zombie', 'minecraft:zombie'], EntityLegacyIds::ZOMBIE); + + $this->register(Human::class, function(World $world, CompoundTag $nbt) : Human{ + return new Human(EntityDataHelper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); + }, ['Human']); + + PaintingMotive::init(); + } + + /** + * @phpstan-param \Closure(World, CompoundTag) : Entity $creationFunc + */ + private static function validateCreationFunc(\Closure $creationFunc) : void{ + $sig = new CallbackType( + new ReturnType(Entity::class), + new ParameterType("world", World::class), + new ParameterType("nbt", CompoundTag::class) + ); + if(!$sig->isSatisfiedBy($creationFunc)){ + throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($creationFunc) . "` must be compatible with `" . $sig . "`"); + } + } + + /** + * Registers an entity type into the index. + * + * @param string $className Class that extends Entity + * @param string[] $saveNames An array of save names which this entity might be saved under. + * @phpstan-param class-string $className + * @phpstan-param list $saveNames + * @phpstan-param \Closure(World $world, CompoundTag $nbt) : Entity $creationFunc + * + * NOTE: The first save name in the $saveNames array will be used when saving the entity to disk. + * + * @throws \InvalidArgumentException + */ + public function register(string $className, \Closure $creationFunc, array $saveNames, ?int $legacyMcpeSaveId = null) : void{ + if(count($saveNames) === 0){ + throw new \InvalidArgumentException("At least one save name must be provided"); + } + Utils::testValidInstance($className, Entity::class); + self::validateCreationFunc($creationFunc); + + foreach($saveNames as $name){ + $this->creationFuncs[$name] = $creationFunc; + } + if($legacyMcpeSaveId !== null){ + $this->creationFuncs[$legacyMcpeSaveId] = $creationFunc; + } + + $this->saveNames[$className] = reset($saveNames); + } + + /** + * Creates an entity from data stored on a chunk. + * + * @throws SavedDataLoadingException + * @internal + */ + public function createFromData(World $world, CompoundTag $nbt) : ?Entity{ + 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; + }catch(NbtException $e){ + throw new SavedDataLoadingException($e->getMessage(), 0, $e); + } + } + + public function injectSaveId(string $class, CompoundTag $saveData) : void{ + if(isset($this->saveNames[$class])){ + $saveData->setTag("id", new StringTag($this->saveNames[$class])); + }else{ + throw new \InvalidArgumentException("Entity $class is not registered"); + } + } + + /** + * @phpstan-param class-string $class + */ + public function getSaveId(string $class) : string{ + if(isset($this->saveNames[$class])){ + return $this->saveNames[$class]; + } + throw new \InvalidArgumentException("Entity $class is not registered"); + } +} diff --git a/src/pocketmine/entity/Animal.php b/src/entity/EntitySizeInfo.php similarity index 51% rename from src/pocketmine/entity/Animal.php rename to src/entity/EntitySizeInfo.php index bc8fdbe99d..91cf7c5f7b 100644 --- a/src/pocketmine/entity/Animal.php +++ b/src/entity/EntitySizeInfo.php @@ -23,9 +23,33 @@ declare(strict_types=1); namespace pocketmine\entity; -abstract class Animal extends Creature implements Ageable{ +use function min; - public function isBaby() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_BABY); +final class EntitySizeInfo{ + /** @var float */ + private $height; + /** @var float */ + private $width; + /** @var float */ + private $eyeHeight; + + public function __construct(float $height, float $width, ?float $eyeHeight = null){ + $this->height = $height; + $this->width = $width; + $this->eyeHeight = $eyeHeight ?? min($this->height / 2 + 0.1, $this->height); + } + + public function getHeight() : float{ return $this->height; } + + public function getWidth() : float{ return $this->width; } + + public function getEyeHeight() : float{ return $this->eyeHeight; } + + public function scale(float $newScale) : self{ + return new self( + $this->height * $newScale, + $this->width * $newScale, + $this->eyeHeight * $newScale + ); } } diff --git a/src/entity/ExperienceManager.php b/src/entity/ExperienceManager.php new file mode 100644 index 0000000000..f06a29e59b --- /dev/null +++ b/src/entity/ExperienceManager.php @@ -0,0 +1,302 @@ +entity = $entity; + + $this->levelAttr = self::fetchAttribute($entity, Attribute::EXPERIENCE_LEVEL); + $this->progressAttr = self::fetchAttribute($entity, Attribute::EXPERIENCE); + } + + private static function fetchAttribute(Entity $entity, string $attributeId) : Attribute{ + $attribute = AttributeFactory::getInstance()->mustGet($attributeId); + $entity->getAttributeMap()->add($attribute); + return $attribute; + } + + /** + * Returns the player's experience level. + */ + public function getXpLevel() : int{ + return (int) $this->levelAttr->getValue(); + } + + /** + * Sets the player's experience level. This does not affect their total XP or their XP progress. + */ + public function setXpLevel(int $level) : bool{ + return $this->setXpAndProgress($level, null); + } + + /** + * Adds a number of XP levels to the player. + */ + public function addXpLevels(int $amount, bool $playSound = true) : bool{ + $oldLevel = $this->getXpLevel(); + if($this->setXpLevel($oldLevel + $amount)){ + if($playSound){ + $newLevel = $this->getXpLevel(); + if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ + $this->entity->broadcastSound(new XpLevelUpSound($newLevel)); + } + } + + return true; + } + + return false; + } + + /** + * Subtracts a number of XP levels from the player. + */ + public function subtractXpLevels(int $amount) : bool{ + return $this->addXpLevels(-$amount); + } + + /** + * Returns a value between 0.0 and 1.0 to indicate how far through the current level the player is. + */ + public function getXpProgress() : float{ + return $this->progressAttr->getValue(); + } + + /** + * Sets the player's progress through the current level to a value between 0.0 and 1.0. + */ + public function setXpProgress(float $progress) : bool{ + return $this->setXpAndProgress(null, $progress); + } + + /** + * Returns the number of XP points the player has progressed into their current level. + */ + public function getRemainderXp() : int{ + return (int) (ExperienceUtils::getXpToCompleteLevel($this->getXpLevel()) * $this->getXpProgress()); + } + + /** + * Returns the amount of XP points the player currently has, calculated from their current level and progress + * through their current level. This will be reduced by enchanting deducting levels and is used to calculate the + * amount of XP the player drops on death. + */ + public function getCurrentTotalXp() : int{ + return ExperienceUtils::getXpToReachLevel($this->getXpLevel()) + $this->getRemainderXp(); + } + + /** + * Sets the current total of XP the player has, recalculating their XP level and progress. + * Note that this DOES NOT update the player's lifetime total XP. + */ + public function setCurrentTotalXp(int $amount) : bool{ + $newLevel = ExperienceUtils::getLevelFromXp($amount); + + $xpLevel = (int) $newLevel; + $xpProgress = $newLevel - (int) $newLevel; + if($xpProgress > 1.0){ + throw new AssumptionFailedError(sprintf("newLevel - (int) newLevel should never be bigger than 1, but have %.53f (newLevel=%.53f)", $xpProgress, $newLevel)); + } + return $this->setXpAndProgress($xpLevel, $xpProgress); + } + + /** + * Adds an amount of XP to the player, recalculating their XP level and progress. XP amount will be added to the + * player's lifetime XP. + * + * @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(); + + if($this->setCurrentTotalXp($oldTotal + $amount)){ + if($amount > 0){ + $this->totalXp += $amount; + } + if($playSound){ + $newLevel = $this->getXpLevel(); + if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ + $this->entity->broadcastSound(new XpLevelUpSound($newLevel)); + }elseif($this->getCurrentTotalXp() > $oldTotal){ + $this->entity->broadcastSound(new XpCollectSound()); + } + } + + return true; + } + + return false; + } + + /** + * Takes an amount of XP from the player, recalculating their XP level and progress. + */ + public function subtractXp(int $amount) : bool{ + return $this->addXp(-$amount); + } + + public function setXpAndProgress(?int $level, ?float $progress) : bool{ + $ev = new PlayerExperienceChangeEvent($this->entity, $this->getXpLevel(), $this->getXpProgress(), $level, $progress); + $ev->call(); + + if($ev->isCancelled()){ + return false; + } + + $level = $ev->getNewLevel(); + $progress = $ev->getNewProgress(); + + if($level !== null){ + $this->levelAttr->setValue($level); + } + + if($progress !== null){ + $this->progressAttr->setValue($progress); + } + + return true; + } + + /** + * @internal + */ + public function setXpAndProgressNoEvent(int $level, float $progress) : void{ + $this->levelAttr->setValue($level); + $this->progressAttr->setValue($progress); + } + + /** + * Returns the total XP the player has collected in their lifetime. Resets when the player dies. + * XP levels being removed in enchanting do not reduce this number. + */ + public function getLifetimeTotalXp() : int{ + return $this->totalXp; + } + + /** + * Sets the lifetime total XP of the player. This does not recalculate their level or progress. Used for player + * score when they die. (TODO: add this when MCPE supports it) + */ + public function setLifetimeTotalXp(int $amount) : void{ + 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; + } + + /** + * Returns whether the human can pickup XP orbs (checks cooldown time) + */ + public function canPickupXp() : bool{ + return $this->xpCooldown === 0; + } + + public function onPickupXp(int $xpValue) : void{ + static $mainHandIndex = -1; + static $offHandIndex = -2; + + //TODO: replace this with a more generic equipment getting/setting interface + /** @var Durable[] $equipment */ + $equipment = []; + + if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){ + $equipment[$mainHandIndex] = $item; + } + if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){ + $equipment[$offHandIndex] = $item; + } + foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){ + if($armorItem instanceof Durable and $armorItem->hasEnchantment(VanillaEnchantments::MENDING())){ + $equipment[$k] = $armorItem; + } + } + + if(count($equipment) > 0){ + $repairItem = $equipment[$k = array_rand($equipment)]; + if($repairItem->getDamage() > 0){ + $repairAmount = min($repairItem->getDamage(), $xpValue * 2); + $repairItem->setDamage($repairItem->getDamage() - $repairAmount); + $xpValue -= (int) ceil($repairAmount / 2); + + if($k === $mainHandIndex){ + $this->entity->getInventory()->setItemInHand($repairItem); + }elseif($k === $offHandIndex){ + $this->entity->getOffHandInventory()->setItem(0, $repairItem); + }else{ + $this->entity->getArmorInventory()->setItem($k, $repairItem); + } + } + } + + $this->addXp($xpValue); //this will still get fired even if the value is 0 due to mending, to play sounds + $this->resetXpCooldown(); + } + + /** + * Sets the duration in ticks until the human can pick up another XP orb. + */ + public function resetXpCooldown(int $value = 2) : void{ + $this->xpCooldown = $value; + } + + public function tick(int $tickDiff = 1) : void{ + if($this->xpCooldown > 0){ + $this->xpCooldown = max(0, $this->xpCooldown - $tickDiff); + } + } +} diff --git a/src/pocketmine/entity/Explosive.php b/src/entity/Explosive.php similarity index 93% rename from src/pocketmine/entity/Explosive.php rename to src/entity/Explosive.php index fad701190e..94922cb19f 100644 --- a/src/pocketmine/entity/Explosive.php +++ b/src/entity/Explosive.php @@ -25,8 +25,5 @@ namespace pocketmine\entity; interface Explosive{ - /** - * @return void - */ - public function explode(); + public function explode() : void; } diff --git a/src/pocketmine/item/FoodSource.php b/src/entity/FoodSource.php similarity index 97% rename from src/pocketmine/item/FoodSource.php rename to src/entity/FoodSource.php index ddddb4a943..e30ece5d3e 100644 --- a/src/pocketmine/item/FoodSource.php +++ b/src/entity/FoodSource.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\item; +namespace pocketmine\entity; /** * Interface implemented by objects that can be consumed by players, giving them food and saturation. diff --git a/src/entity/Human.php b/src/entity/Human.php new file mode 100644 index 0000000000..c82cee7f82 --- /dev/null +++ b/src/entity/Human.php @@ -0,0 +1,500 @@ +skin = $skin; + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(1.8, 0.6, 1.62); } + + /** + * @throws InvalidSkinException + * @throws SavedDataLoadingException + */ + public static function parseSkinNBT(CompoundTag $nbt) : Skin{ + $skinTag = $nbt->getCompoundTag("Skin"); + if($skinTag === null){ + throw new SavedDataLoadingException("Missing skin data"); + } + return new Skin( //this throws if the skin is invalid + $skinTag->getString("Name"), + ($skinDataTag = $skinTag->getTag("Data")) instanceof StringTag ? $skinDataTag->getValue() : $skinTag->getByteArray("Data"), //old data (this used to be saved as a StringTag in older versions of PM) + $skinTag->getByteArray("CapeData", ""), + $skinTag->getString("GeometryName", ""), + $skinTag->getByteArray("GeometryData", "") + ); + } + + public function getUniqueId() : UuidInterface{ + return $this->uuid; + } + + /** + * Returns a Skin object containing information about this human's skin. + */ + public function getSkin() : Skin{ + return $this->skin; + } + + /** + * Sets the human's skin. This will not send any update to viewers, you need to do that manually using + * {@link sendSkin}. + */ + public function setSkin(Skin $skin) : void{ + $this->skin = $skin; + } + + /** + * Sends the human's skin to the specified list of players. If null is given for targets, the skin will be sent to + * all viewers. + * + * @param Player[]|null $targets + */ + public function sendSkin(?array $targets = null) : void{ + $this->server->broadcastPackets($targets ?? $this->hasSpawned, [ + PlayerSkinPacket::create($this->getUniqueId(), "", "", SkinAdapterSingleton::get()->toSkinData($this->skin)) + ]); + } + + public function jump() : void{ + parent::jump(); + if($this->isSprinting()){ + $this->hungerManager->exhaust(0.8, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING); + }else{ + $this->hungerManager->exhaust(0.2, PlayerExhaustEvent::CAUSE_JUMPING); + } + } + + public function getHungerManager() : HungerManager{ + return $this->hungerManager; + } + + public function consumeObject(Consumable $consumable) : bool{ + if($consumable instanceof FoodSource && $consumable->requiresHunger() and !$this->hungerManager->isHungry()){ + return false; + } + + return parent::consumeObject($consumable); + } + + protected function applyConsumptionResults(Consumable $consumable) : void{ + if($consumable instanceof FoodSource){ + $this->hungerManager->addFood($consumable->getFoodRestore()); + $this->hungerManager->addSaturation($consumable->getSaturationRestore()); + } + + parent::applyConsumptionResults($consumable); + } + + public function getXpManager() : ExperienceManager{ + return $this->xpManager; + } + + public function getXpDropAmount() : int{ + //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for + //about 7.5 levels of XP. + return min(100, 7 * $this->xpManager->getXpLevel()); + } + + /** + * @return PlayerInventory + */ + public function getInventory(){ + return $this->inventory; + } + + public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; } + + public function getEnderInventory() : PlayerEnderInventory{ + return $this->enderInventory; + } + + /** + * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. + */ + protected function initHumanData(CompoundTag $nbt) : void{ + if(($nameTagTag = $nbt->getTag("NameTag")) instanceof StringTag){ + $this->setNameTag($nameTagTag->getValue()); + } + + //TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace + $this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag()); + } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->hungerManager = new HungerManager($this); + $this->xpManager = new ExperienceManager($this); + + $this->inventory = new PlayerInventory($this); + $syncHeldItem = function() : void{ + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onMobMainHandItemChange($this); + } + }; + $this->inventory->getListeners()->add(new CallbackInventoryListener( + function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{ + if($slot === $this->inventory->getHeldItemIndex()){ + $syncHeldItem(); + } + }, + function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{ + if(array_key_exists($this->inventory->getHeldItemIndex(), $oldItems)){ + $syncHeldItem(); + } + } + )); + $this->offHandInventory = new PlayerOffHandInventory($this); + $this->enderInventory = new PlayerEnderInventory($this); + $this->initHumanData($nbt); + + $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(); + + /** @var CompoundTag $item */ + foreach($inventoryTag as $i => $item){ + $slot = $item->getByte("Slot"); + if($slot >= 0 and $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)); + } + } + + $this->armorInventory->getListeners()->add(...$armorListeners); + $this->inventory->getListeners()->add(...$inventoryListeners); + } + $offHand = $nbt->getCompoundTag("OffHandItem"); + if($offHand !== null){ + $this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand)); + } + $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(function() : void{ + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onMobOffHandItemChange($this); + } + })); + + $enderChestInventoryTag = $nbt->getListTag("EnderChestInventory"); + if($enderChestInventoryTag !== null){ + /** @var CompoundTag $item */ + foreach($enderChestInventoryTag as $i => $item){ + $this->enderInventory->setItem($item->getByte("Slot"), Item::nbtDeserialize($item)); + } + } + + $this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0)); + $this->inventory->getHeldItemIndexChangeListeners()->add(function(int $oldIndex) : void{ + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onMobMainHandItemChange($this); + } + }); + + $this->hungerManager->setFood((float) $nbt->getInt("foodLevel", (int) $this->hungerManager->getFood())); + $this->hungerManager->setExhaustion($nbt->getFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion())); + $this->hungerManager->setSaturation($nbt->getFloat("foodSaturationLevel", $this->hungerManager->getSaturation())); + $this->hungerManager->setFoodTickTimer($nbt->getInt("foodTickTimer", $this->hungerManager->getFoodTickTimer())); + + $this->xpManager->setXpAndProgressNoEvent( + $nbt->getInt("XpLevel", 0), + $nbt->getFloat("XpP", 0.0)); + $this->xpManager->setLifetimeTotalXp($nbt->getInt("XpTotal", 0)); + + if(($xpSeedTag = $nbt->getTag("XpSeed")) instanceof IntTag){ + $this->xpSeed = $xpSeedTag->getValue(); + }else{ + $this->xpSeed = random_int(Limits::INT32_MIN, Limits::INT32_MAX); + } + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + $this->hungerManager->tick($tickDiff); + $this->xpManager->tick($tickDiff); + + return $hasUpdate; + } + + public function getName() : string{ + return $this->getNameTag(); + } + + public function applyDamageModifiers(EntityDamageEvent $source) : void{ + 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)){ + + $compensation = $this->getHealth() - $source->getFinalDamage() - 1; + if($compensation < 0){ + $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM); + } + } + } + + protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ + parent::applyPostDamageEffects($source); + $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM); + if($totemModifier < 0){ //Totem prevented death + $this->effectManager->clear(); + + $this->effectManager->add(new EffectInstance(VanillaEffects::REGENERATION(), 40 * 20, 1)); + $this->effectManager->add(new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 40 * 20, 1)); + $this->effectManager->add(new EffectInstance(VanillaEffects::ABSORPTION(), 5 * 20, 1)); + + $this->broadcastAnimation(new TotemUseAnimation($this)); + $this->broadcastSound(new TotemUseSound()); + + $hand = $this->inventory->getItemInHand(); + if($hand instanceof Totem){ + $hand->pop(); //Plugins could alter max stack size + $this->inventory->setItemInHand($hand); + }elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){ + $offHand->pop(); + $this->offHandInventory->setItem(0, $offHand); + } + } + } + + public function getDrops() : array{ + return array_filter(array_merge( + $this->inventory !== null ? array_values($this->inventory->getContents()) : [], + $this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [], + $this->offHandInventory !== null ? array_values($this->offHandInventory->getContents()) : [], + ), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()); }); + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + + $nbt->setInt("foodLevel", (int) $this->hungerManager->getFood()); + $nbt->setFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion()); + $nbt->setFloat("foodSaturationLevel", $this->hungerManager->getSaturation()); + $nbt->setInt("foodTickTimer", $this->hungerManager->getFoodTickTimer()); + + $nbt->setInt("XpLevel", $this->xpManager->getXpLevel()); + $nbt->setFloat("XpP", $this->xpManager->getXpProgress()); + $nbt->setInt("XpTotal", $this->xpManager->getLifetimeTotalXp()); + $nbt->setInt("XpSeed", $this->xpSeed); + + $inventoryTag = new ListTag([], NBT::TAG_Compound); + $nbt->setTag("Inventory", $inventoryTag); + if($this->inventory !== null){ + //Normal inventory + $slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize(); + for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){ + $item = $this->inventory->getItem($slot - 9); + if(!$item->isNull()){ + $inventoryTag->push($item->nbtSerialize($slot)); + } + } + + //Armor + for($slot = 100; $slot < 104; ++$slot){ + $item = $this->armorInventory->getItem($slot - 100); + if(!$item->isNull()){ + $inventoryTag->push($item->nbtSerialize($slot)); + } + } + + $nbt->setInt("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); + } + $offHandItem = $this->offHandInventory->getItem(0); + if(!$offHandItem->isNull()){ + $nbt->setTag("OffHandItem", $offHandItem->nbtSerialize()); + } + + if($this->enderInventory !== null){ + /** @var CompoundTag[] $items */ + $items = []; + + $slotCount = $this->enderInventory->getSize(); + for($slot = 0; $slot < $slotCount; ++$slot){ + $item = $this->enderInventory->getItem($slot); + if(!$item->isNull()){ + $items[] = $item->nbtSerialize($slot); + } + } + + $nbt->setTag("EnderChestInventory", new ListTag($items, NBT::TAG_Compound)); + } + + if($this->skin !== null){ + $nbt->setTag("Skin", CompoundTag::create() + ->setString("Name", $this->skin->getSkinId()) + ->setByteArray("Data", $this->skin->getSkinData()) + ->setByteArray("CapeData", $this->skin->getCapeData()) + ->setString("GeometryName", $this->skin->getGeometryName()) + ->setByteArray("GeometryData", $this->skin->getGeometryData()) + ); + } + + return $nbt; + } + + public function spawnTo(Player $player) : void{ + if($player !== $this){ + parent::spawnTo($player); + } + } + + protected function sendSpawnPacket(Player $player) : void{ + if(!($this instanceof Player)){ + $player->getNetworkSession()->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), SkinAdapterSingleton::get()->toSkinData($this->skin))])); + } + + $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())]); + + $player->getNetworkSession()->onMobArmorChange($this); + $player->getNetworkSession()->onMobOffHandItemChange($this); + + if(!($this instanceof Player)){ + $player->getNetworkSession()->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)])); + } + } + + public function getOffsetPosition(Vector3 $vector3) : Vector3{ + return $vector3->add(0, 1.621, 0); //TODO: +0.001 hack for MCPE falling underground + } + + protected function onDispose() : void{ + $this->inventory->removeAllViewers(); + $this->inventory->getHeldItemIndexChangeListeners()->clear(); + $this->offHandInventory->removeAllViewers(); + $this->enderInventory->removeAllViewers(); + parent::onDispose(); + } + + protected function destroyCycles() : void{ + $this->inventory = null; + $this->offHandInventory = null; + $this->enderInventory = null; + $this->hungerManager = null; + $this->xpManager = null; + parent::destroyCycles(); + } +} diff --git a/src/entity/HungerManager.php b/src/entity/HungerManager.php new file mode 100644 index 0000000000..f0e0e9d8e0 --- /dev/null +++ b/src/entity/HungerManager.php @@ -0,0 +1,232 @@ +entity = $entity; + + $this->hungerAttr = self::fetchAttribute($entity, Attribute::HUNGER); + $this->saturationAttr = self::fetchAttribute($entity, Attribute::SATURATION); + $this->exhaustionAttr = self::fetchAttribute($entity, Attribute::EXHAUSTION); + } + + private static function fetchAttribute(Entity $entity, string $attributeId) : Attribute{ + $attribute = AttributeFactory::getInstance()->mustGet($attributeId); + $entity->getAttributeMap()->add($attribute); + return $attribute; + } + + public function getFood() : float{ + return $this->hungerAttr->getValue(); + } + + /** + * WARNING: This method does not check if full and may throw an exception if out of bounds. + * @see HungerManager::addFood() + * + * @throws \InvalidArgumentException + */ + public function setFood(float $new) : void{ + $old = $this->hungerAttr->getValue(); + $this->hungerAttr->setValue($new); + + // ranges: 18-20 (regen), 7-17 (none), 1-6 (no sprint), 0 (health depletion) + foreach([17, 6, 0] as $bound){ + if(($old > $bound) !== ($new > $bound)){ + $this->foodTickTimer = 0; + break; + } + } + } + + public function getMaxFood() : float{ + return $this->hungerAttr->getMaxValue(); + } + + public function addFood(float $amount) : void{ + $amount += $this->hungerAttr->getValue(); + $amount = max(min($amount, $this->hungerAttr->getMaxValue()), $this->hungerAttr->getMinValue()); + $this->setFood($amount); + } + + /** + * Returns whether this Human may consume objects requiring hunger. + */ + public function isHungry() : bool{ + return $this->getFood() < $this->getMaxFood(); + } + + public function getSaturation() : float{ + return $this->saturationAttr->getValue(); + } + + /** + * WARNING: This method does not check if saturated and may throw an exception if out of bounds. + * @see HungerManager::addSaturation() + * + * @throws \InvalidArgumentException + */ + public function setSaturation(float $saturation) : void{ + $this->saturationAttr->setValue($saturation); + } + + public function addSaturation(float $amount) : void{ + $this->saturationAttr->setValue($this->saturationAttr->getValue() + $amount, true); + } + + public function getExhaustion() : float{ + return $this->exhaustionAttr->getValue(); + } + + /** + * WARNING: This method does not check if exhausted and does not consume saturation/food. + * @see HungerManager::exhaust() + */ + public function setExhaustion(float $exhaustion) : void{ + $this->exhaustionAttr->setValue($exhaustion); + } + + /** + * Increases exhaustion level. + * + * @return float the amount of exhaustion level increased + */ + public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{ + if(!$this->enabled){ + return 0; + } + $ev = new PlayerExhaustEvent($this->entity, $amount, $cause); + $ev->call(); + if($ev->isCancelled()){ + return 0.0; + } + + $exhaustion = $this->getExhaustion(); + $exhaustion += $ev->getAmount(); + + while($exhaustion >= 4.0){ + $exhaustion -= 4.0; + + $saturation = $this->getSaturation(); + if($saturation > 0){ + $saturation = max(0, $saturation - 1.0); + $this->setSaturation($saturation); + }else{ + $food = $this->getFood(); + if($food > 0){ + $food--; + $this->setFood(max($food, 0)); + } + } + } + $this->setExhaustion($exhaustion); + + return $ev->getAmount(); + } + + public function getFoodTickTimer() : int{ + return $this->foodTickTimer; + } + + public function setFoodTickTimer(int $foodTickTimer) : void{ + if($foodTickTimer < 0){ + throw new \InvalidArgumentException("Expected a non-negative value"); + } + $this->foodTickTimer = $foodTickTimer; + } + + public function tick(int $tickDiff = 1) : void{ + if(!$this->entity->isAlive() or !$this->enabled){ + return; + } + $food = $this->getFood(); + $health = $this->entity->getHealth(); + $difficulty = $this->entity->getWorld()->getDifficulty(); + + $this->foodTickTimer += $tickDiff; + if($this->foodTickTimer >= 80){ + $this->foodTickTimer = 0; + } + + if($difficulty === World::DIFFICULTY_PEACEFUL and $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()){ + $this->entity->heal(new EntityRegainHealthEvent($this->entity, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); + } + } + + if($this->foodTickTimer === 0){ + if($food >= 18){ + if($health < $this->entity->getMaxHealth()){ + $this->entity->heal(new EntityRegainHealthEvent($this->entity, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); + $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){ + $this->entity->attack(new EntityDamageEvent($this->entity, EntityDamageEvent::CAUSE_STARVATION, 1)); + } + } + } + + if($food <= 6){ + $this->entity->setSprinting(false); + } + } + + public function isEnabled() : bool{ + return $this->enabled; + } + + public function setEnabled(bool $enabled) : void{ + $this->enabled = $enabled; + } +} diff --git a/src/pocketmine/entity/InvalidSkinException.php b/src/entity/InvalidSkinException.php similarity index 100% rename from src/pocketmine/entity/InvalidSkinException.php rename to src/entity/InvalidSkinException.php diff --git a/src/pocketmine/entity/Living.php b/src/entity/Living.php similarity index 55% rename from src/pocketmine/entity/Living.php rename to src/entity/Living.php index 0d952cca33..401bd051d3 100644 --- a/src/pocketmine/entity/Living.php +++ b/src/entity/Living.php @@ -24,36 +24,42 @@ declare(strict_types=1); namespace pocketmine\entity; use pocketmine\block\Block; +use pocketmine\block\BlockLegacyIds; +use pocketmine\data\bedrock\EffectIdMap; +use pocketmine\entity\animation\DeathAnimation; +use pocketmine\entity\animation\HurtAnimation; +use pocketmine\entity\animation\RespawnAnimation; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\EffectManager; +use pocketmine\entity\effect\VanillaEffects; use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDeathEvent; -use pocketmine\event\entity\EntityEffectAddEvent; -use pocketmine\event\entity\EntityEffectRemoveEvent; use pocketmine\inventory\ArmorInventory; -use pocketmine\inventory\ArmorInventoryEventProcessor; +use pocketmine\inventory\CallbackInventoryListener; +use pocketmine\inventory\Inventory; use pocketmine\item\Armor; -use pocketmine\item\Consumable; use pocketmine\item\Durable; use pocketmine\item\enchantment\Enchantment; +use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; -use pocketmine\item\MaybeConsumable; use pocketmine\math\Vector3; use pocketmine\math\VoxelRayTrace; -use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\FloatTag; -use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\ShortTag; -use pocketmine\network\mcpe\protocol\ActorEventPacket; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\network\mcpe\protocol\MobEffectPacket; -use pocketmine\Player; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties; +use pocketmine\player\Player; use pocketmine\timings\Timings; use pocketmine\utils\Binary; -use pocketmine\utils\Color; -use function abs; +use pocketmine\world\sound\EntityLandSound; +use pocketmine\world\sound\EntityLongFallSound; +use pocketmine\world\sound\EntityShortFallSound; +use pocketmine\world\sound\ItemBreakSound; use function array_shift; use function atan2; use function ceil; @@ -67,7 +73,8 @@ use function mt_rand; use function sqrt; use const M_PI; -abstract class Living extends Entity implements Damageable{ +abstract class Living extends Entity{ + protected const DEFAULT_BREATH_TICKS = 300; protected $gravity = 0.08; protected $drag = 0.02; @@ -83,44 +90,76 @@ abstract class Living extends Entity implements Damageable{ /** @var float */ protected $jumpVelocity = 0.42; - /** @var EffectInstance[] */ - protected $effects = []; + /** @var EffectManager */ + protected $effectManager; /** @var ArmorInventory */ protected $armorInventory; + /** @var bool */ + protected $breathing = true; + /** @var int */ + protected $breathTicks = self::DEFAULT_BREATH_TICKS; + /** @var int */ + protected $maxBreathTicks = self::DEFAULT_BREATH_TICKS; + + /** @var Attribute */ + protected $healthAttr; + /** @var Attribute */ + protected $absorptionAttr; + /** @var Attribute */ + protected $knockbackResistanceAttr; + /** @var Attribute */ + protected $moveSpeedAttr; + + /** @var bool */ + protected $sprinting = false; + /** @var bool */ + protected $sneaking = false; + abstract public function getName() : string; - protected function initEntity() : void{ - parent::initEntity(); + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->effectManager = new EffectManager($this); + $this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); + $this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); $this->armorInventory = new ArmorInventory($this); //TODO: load/save armor inventory contents - $this->armorInventory->setEventProcessor(new ArmorInventoryEventProcessor($this)); + $this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange( + function(Inventory $unused) : void{ + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onMobArmorChange($this); + } + } + )); - if($this->namedtag->hasTag("HealF", FloatTag::class)){ - $health = $this->namedtag->getFloat("HealF"); - $this->namedtag->removeTag("HealF"); - }elseif($this->namedtag->hasTag("Health", ShortTag::class)){ - //Older versions of PocketMine-MP incorrectly saved this as a short instead of a float - $health = $this->namedtag->getShort("Health"); - $this->namedtag->removeTag("Health"); - }else{ - $health = $this->namedtag->getFloat("Health", $this->getMaxHealth()); + $health = $this->getMaxHealth(); + + if(($healFTag = $nbt->getTag("HealF")) instanceof FloatTag){ + $health = $healFTag->getValue(); + }elseif(($healthTag = $nbt->getTag("Health")) instanceof ShortTag){ + $health = $healthTag->getValue(); //Older versions of PocketMine-MP incorrectly saved this as a short instead of a float + }elseif($healthTag instanceof FloatTag){ + $health = $healthTag->getValue(); } $this->setHealth($health); + $this->setAirSupplyTicks($nbt->getShort("Air", self::DEFAULT_BREATH_TICKS)); + /** @var CompoundTag[]|ListTag|null $activeEffectsTag */ - $activeEffectsTag = $this->namedtag->getListTag("ActiveEffects"); + $activeEffectsTag = $nbt->getListTag("ActiveEffects"); if($activeEffectsTag !== null){ foreach($activeEffectsTag as $e){ - $effect = Effect::getEffect($e->getByte("Id")); + $effect = EffectIdMap::getInstance()->fromId($e->getByte("Id")); if($effect === null){ continue; } - $this->addEffect(new EffectInstance( + $this->effectManager->add(new EffectInstance( $effect, $e->getInt("Duration"), Binary::unsignByte($e->getByte("Amplifier")), @@ -132,59 +171,91 @@ abstract class Living extends Entity implements Damageable{ } protected function addAttributes() : void{ - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::HEALTH)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::FOLLOW_RANGE)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::KNOCKBACK_RESISTANCE)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::MOVEMENT_SPEED)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::ATTACK_DAMAGE)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::ABSORPTION)); + $this->attributeMap->add($this->healthAttr = AttributeFactory::getInstance()->mustGet(Attribute::HEALTH)); + $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::FOLLOW_RANGE)); + $this->attributeMap->add($this->knockbackResistanceAttr = AttributeFactory::getInstance()->mustGet(Attribute::KNOCKBACK_RESISTANCE)); + $this->attributeMap->add($this->moveSpeedAttr = AttributeFactory::getInstance()->mustGet(Attribute::MOVEMENT_SPEED)); + $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::ATTACK_DAMAGE)); + $this->attributeMap->add($this->absorptionAttr = AttributeFactory::getInstance()->mustGet(Attribute::ABSORPTION)); } public function setHealth(float $amount) : void{ $wasAlive = $this->isAlive(); parent::setHealth($amount); - $this->attributeMap->getAttribute(Attribute::HEALTH)->setValue(ceil($this->getHealth()), true); + $this->healthAttr->setValue(ceil($this->getHealth()), true); if($this->isAlive() and !$wasAlive){ - $this->broadcastEntityEvent(ActorEventPacket::RESPAWN); + $this->broadcastAnimation(new RespawnAnimation($this)); } } public function getMaxHealth() : int{ - return (int) $this->attributeMap->getAttribute(Attribute::HEALTH)->getMaxValue(); + return (int) $this->healthAttr->getMaxValue(); } public function setMaxHealth(int $amount) : void{ - $this->attributeMap->getAttribute(Attribute::HEALTH)->setMaxValue($amount)->setDefaultValue($amount); + $this->healthAttr->setMaxValue($amount)->setDefaultValue($amount); } public function getAbsorption() : float{ - return $this->attributeMap->getAttribute(Attribute::ABSORPTION)->getValue(); + return $this->absorptionAttr->getValue(); } public function setAbsorption(float $absorption) : void{ - $this->attributeMap->getAttribute(Attribute::ABSORPTION)->setValue($absorption); + $this->absorptionAttr->setValue($absorption); } - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setFloat("Health", $this->getHealth(), true); + public function isSneaking() : bool{ + return $this->sneaking; + } - if(count($this->effects) > 0){ + public function setSneaking(bool $value = true) : void{ + $this->sneaking = $value; + $this->networkPropertiesDirty = true; + } + + public function isSprinting() : bool{ + return $this->sprinting; + } + + public function setSprinting(bool $value = true) : void{ + if($value !== $this->isSprinting()){ + $this->sprinting = $value; + $this->networkPropertiesDirty = true; + $moveSpeed = $this->getMovementSpeed(); + $this->setMovementSpeed($value ? ($moveSpeed * 1.3) : ($moveSpeed / 1.3)); + $this->moveSpeedAttr->markSynchronized(false); //TODO: reevaluate this hack + } + } + + public function getMovementSpeed() : float{ + return $this->moveSpeedAttr->getValue(); + } + + public function setMovementSpeed(float $v, bool $fit = false) : void{ + $this->moveSpeedAttr->setValue($v, $fit); + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setFloat("Health", $this->getHealth()); + + $nbt->setShort("Air", $this->getAirSupplyTicks()); + + if(count($this->effectManager->all()) > 0){ $effects = []; - foreach($this->effects as $effect){ - $effects[] = new CompoundTag("", [ - new ByteTag("Id", $effect->getId()), - new ByteTag("Amplifier", Binary::signByte($effect->getAmplifier())), - new IntTag("Duration", $effect->getDuration()), - new ByteTag("Ambient", $effect->isAmbient() ? 1 : 0), - new ByteTag("ShowParticles", $effect->isVisible() ? 1 : 0) - ]); + foreach($this->effectManager->all() as $effect){ + $effects[] = CompoundTag::create() + ->setByte("Id", EffectIdMap::getInstance()->toId($effect->getType())) + ->setByte("Amplifier", Binary::signByte($effect->getAmplifier())) + ->setInt("Duration", $effect->getDuration()) + ->setByte("Ambient", $effect->isAmbient() ? 1 : 0) + ->setByte("ShowParticles", $effect->isVisible() ? 1 : 0); } - $this->namedtag->setTag(new ListTag("ActiveEffects", $effects)); - }else{ - $this->namedtag->removeTag("ActiveEffects"); + $nbt->setTag("ActiveEffects", new ListTag($effects)); } + + return $nbt; } public function hasLineOfSight(Entity $entity) : bool{ @@ -193,165 +264,8 @@ abstract class Living extends Entity implements Damageable{ //return $this->getLevelNonNull()->rayTraceBlocks(Vector3::createVector($this->x, $this->y + $this->height, $this->z), Vector3::createVector($entity->x, $entity->y + $entity->height, $entity->z)) === null; } - /** - * Returns an array of Effects currently active on the mob. - * @return EffectInstance[] - */ - public function getEffects() : array{ - return $this->effects; - } - - /** - * Removes all effects from the mob. - */ - public function removeAllEffects() : void{ - foreach($this->effects as $effect){ - $this->removeEffect($effect->getId()); - } - } - - /** - * Removes the effect with the specified ID from the mob. - */ - public function removeEffect(int $effectId) : void{ - if(isset($this->effects[$effectId])){ - $effect = $this->effects[$effectId]; - $hasExpired = $effect->hasExpired(); - $ev = new EntityEffectRemoveEvent($this, $effect); - $ev->call(); - if($ev->isCancelled()){ - if($hasExpired and !$ev->getEffect()->hasExpired()){ //altered duration of an expired effect to make it not get removed - $this->sendEffectAdd($ev->getEffect(), true); - } - return; - } - - unset($this->effects[$effectId]); - $effect->getType()->remove($this, $effect); - $this->sendEffectRemove($effect); - - $this->recalculateEffectColor(); - } - } - - /** - * Returns the effect instance active on this entity with the specified ID, or null if the mob does not have the - * effect. - */ - public function getEffect(int $effectId) : ?EffectInstance{ - return $this->effects[$effectId] ?? null; - } - - /** - * Returns whether the specified effect is active on the mob. - */ - public function hasEffect(int $effectId) : bool{ - return isset($this->effects[$effectId]); - } - - /** - * Returns whether the mob has any active effects. - */ - public function hasEffects() : bool{ - return count($this->effects) > 0; - } - - /** - * Adds an effect to the mob. - * If a weaker effect of the same type is already applied, it will be replaced. - * If a weaker or equal-strength effect is already applied but has a shorter duration, it will be replaced. - * - * @return bool whether the effect has been successfully applied. - */ - public function addEffect(EffectInstance $effect) : bool{ - $oldEffect = null; - $cancelled = false; - - if(isset($this->effects[$effect->getId()])){ - $oldEffect = $this->effects[$effect->getId()]; - if( - abs($effect->getAmplifier()) < $oldEffect->getAmplifier() - or (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) and $effect->getDuration() < $oldEffect->getDuration()) - ){ - $cancelled = true; - } - } - - $ev = new EntityEffectAddEvent($this, $effect, $oldEffect); - $ev->setCancelled($cancelled); - - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - if($oldEffect !== null){ - $oldEffect->getType()->remove($this, $oldEffect); - } - - $effect->getType()->add($this, $effect); - $this->sendEffectAdd($effect, $oldEffect !== null); - - $this->effects[$effect->getId()] = $effect; - - $this->recalculateEffectColor(); - - return true; - } - - /** - * Recalculates the mob's potion bubbles colour based on the active effects. - */ - protected function recalculateEffectColor() : void{ - /** @var Color[] $colors */ - $colors = []; - $ambient = true; - foreach($this->effects as $effect){ - if($effect->isVisible() and $effect->getType()->hasBubbles()){ - $level = $effect->getEffectLevel(); - $color = $effect->getColor(); - for($i = 0; $i < $level; ++$i){ - $colors[] = $color; - } - - if(!$effect->isAmbient()){ - $ambient = false; - } - } - } - - if(count($colors) > 0){ - $this->propertyManager->setInt(Entity::DATA_POTION_COLOR, Color::mix(...$colors)->toARGB()); - $this->propertyManager->setByte(Entity::DATA_POTION_AMBIENT, $ambient ? 1 : 0); - }else{ - $this->propertyManager->setInt(Entity::DATA_POTION_COLOR, 0); - $this->propertyManager->setByte(Entity::DATA_POTION_AMBIENT, 0); - } - } - - /** - * Sends the mob's potion effects to the specified player. - */ - public function sendPotionEffects(Player $player) : void{ - foreach($this->effects as $effect){ - $pk = new MobEffectPacket(); - $pk->entityRuntimeId = $this->id; - $pk->effectId = $effect->getId(); - $pk->amplifier = $effect->getAmplifier(); - $pk->particles = $effect->isVisible(); - $pk->duration = $effect->getDuration(); - $pk->eventId = MobEffectPacket::EVENT_ADD; - - $player->dataPacket($pk); - } - } - - protected function sendEffectAdd(EffectInstance $effect, bool $replacesOldEffect) : void{ - - } - - protected function sendEffectRemove(EffectInstance $effect) : void{ - + public function getEffects() : EffectManager{ + return $this->effectManager; } /** @@ -359,10 +273,6 @@ abstract class Living extends Entity implements Damageable{ * etc. */ public function consumeObject(Consumable $consumable) : bool{ - if($consumable instanceof MaybeConsumable and !$consumable->canBeConsumed()){ - return false; - } - $this->applyConsumptionResults($consumable); return true; } @@ -373,7 +283,7 @@ abstract class Living extends Entity implements Damageable{ */ protected function applyConsumptionResults(Consumable $consumable) : void{ foreach($consumable->getAdditionalEffects() as $effect){ - $this->addEffect($effect); + $this->effectManager->add($effect); } $consumable->onConsume($this); @@ -383,7 +293,7 @@ abstract class Living extends Entity implements Damageable{ * Returns the initial upwards velocity of a jumping entity in blocks/tick, including additional velocity due to effects. */ public function getJumpVelocity() : float{ - return $this->jumpVelocity + ($this->hasEffect(Effect::JUMP) ? ($this->getEffect(Effect::JUMP)->getEffectLevel() / 10) : 0); + return $this->jumpVelocity + ((($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0) / 10); } /** @@ -391,16 +301,36 @@ abstract class Living extends Entity implements Damageable{ */ public function jump() : void{ if($this->onGround){ - $this->motion->y = $this->getJumpVelocity(); //Y motion should already be 0 if we're jumping from the ground. + $this->motion = $this->motion->withComponents(null, $this->getJumpVelocity(), null); //Y motion should already be 0 if we're jumping from the ground. } } - public function fall(float $fallDistance) : void{ - $damage = ceil($fallDistance - 3 - ($this->hasEffect(Effect::JUMP) ? $this->getEffect(Effect::JUMP)->getEffectLevel() : 0)); + protected function calculateFallDamage(float $fallDistance) : float{ + return ceil($fallDistance - 3 - (($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0)); + } + + protected function onHitGround() : ?float{ + $fallBlockPos = $this->location->floor(); + $fallBlock = $this->getWorld()->getBlock($fallBlockPos); + if(count($fallBlock->getCollisionBoxes()) === 0){ + $fallBlockPos = $fallBlockPos->down(); + $fallBlock = $this->getWorld()->getBlock($fallBlockPos); + } + $newVerticalVelocity = $fallBlock->onEntityLand($this); + + $damage = $this->calculateFallDamage($this->fallDistance); if($damage > 0){ $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_FALL, $damage); $this->attack($ev); + + $this->broadcastSound($damage > 4 ? + new EntityLongFallSound($this) : + new EntityShortFallSound($this) + ); + }elseif($fallBlock->getId() !== BlockLegacyIds::AIR){ + $this->broadcastSound(new EntityLandSound($this, $fallBlock)); } + return $newVerticalVelocity; } /** @@ -420,10 +350,10 @@ abstract class Living extends Entity implements Damageable{ /** * Returns the highest level of the specified enchantment on any armour piece that the entity is currently wearing. */ - public function getHighestArmorEnchantmentLevel(int $enchantmentId) : int{ + public function getHighestArmorEnchantmentLevel(Enchantment $enchantment) : int{ $result = 0; foreach($this->armorInventory->getContents() as $item){ - $result = max($result, $item->getEnchantmentLevel($enchantmentId)); + $result = max($result, $item->getEnchantmentLevel($enchantment)); } return $result; @@ -434,7 +364,7 @@ abstract class Living extends Entity implements Damageable{ } public function setOnFire(int $seconds) : void{ - parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(Enchantment::FIRE_PROTECTION) * 0.15)); + parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::FIRE_PROTECTION()) * 0.15)); } /** @@ -444,7 +374,7 @@ abstract class Living extends Entity implements Damageable{ public function applyDamageModifiers(EntityDamageEvent $source) : void{ if($this->lastDamageCause !== null and $this->attackTime > 0){ if($this->lastDamageCause->getBaseDamage() >= $source->getBaseDamage()){ - $source->setCancelled(); + $source->cancel(); } $source->setModifier(-$this->lastDamageCause->getBaseDamage(), EntityDamageEvent::MODIFIER_PREVIOUS_DAMAGE_COOLDOWN); } @@ -454,8 +384,8 @@ abstract class Living extends Entity implements Damageable{ } $cause = $source->getCause(); - if($this->hasEffect(Effect::DAMAGE_RESISTANCE) and $cause !== EntityDamageEvent::CAUSE_VOID and $cause !== EntityDamageEvent::CAUSE_SUICIDE){ - $source->setModifier(-$source->getFinalDamage() * min(1, 0.2 * $this->getEffect(Effect::DAMAGE_RESISTANCE)->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE); + if(($resistance = $this->effectManager->get(VanillaEffects::RESISTANCE())) !== null and $cause !== EntityDamageEvent::CAUSE_VOID and $cause !== EntityDamageEvent::CAUSE_SUICIDE){ + $source->setModifier(-$source->getFinalDamage() * min(1, 0.2 * $resistance->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE); } $totalEpf = 0; @@ -478,10 +408,10 @@ abstract class Living extends Entity implements Damageable{ $this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(EntityDamageEvent::MODIFIER_ABSORPTION))); $this->damageArmor($source->getBaseDamage()); - if($source instanceof EntityDamageByEntityEvent){ + if($source instanceof EntityDamageByEntityEvent and ($attacker = $source->getDamager()) !== null){ $damage = 0; foreach($this->armorInventory->getContents() as $k => $item){ - if($item instanceof Armor and ($thornsLevel = $item->getEnchantmentLevel(Enchantment::THORNS)) > 0){ + if($item instanceof Armor and ($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)); @@ -494,7 +424,7 @@ abstract class Living extends Entity implements Damageable{ } if($damage > 0){ - $source->getDamager()->attack(new EntityDamageByEntityEvent($this, $source->getDamager(), EntityDamageEvent::CAUSE_MAGIC, $damage)); + $attacker->attack(new EntityDamageByEntityEvent($this, $attacker, EntityDamageEvent::CAUSE_MAGIC, $damage)); } } } @@ -519,22 +449,22 @@ abstract class Living extends Entity implements Damageable{ private function damageItem(Durable $item, int $durabilityRemoved) : void{ $item->applyDamage($durabilityRemoved); if($item->isBroken()){ - $this->level->broadcastLevelSoundEvent($this, LevelSoundEventPacket::SOUND_BREAK); + $this->broadcastSound(new ItemBreakSound()); } } public function attack(EntityDamageEvent $source) : void{ if($this->noDamageTicks > 0){ - $source->setCancelled(); + $source->cancel(); } - if($this->hasEffect(Effect::FIRE_RESISTANCE) and ( + if($this->effectManager->has(VanillaEffects::FIRE_RESISTANCE()) and ( $source->getCause() === EntityDamageEvent::CAUSE_FIRE or $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK or $source->getCause() === EntityDamageEvent::CAUSE_LAVA ) ){ - $source->setCancelled(); + $source->cancel(); } $this->applyDamageModifiers($source); @@ -546,7 +476,7 @@ abstract class Living extends Entity implements Damageable{ //TODO: knockback should not just apply for entity damage sources //this doesn't matter for TNT right now because the PrimedTNT entity is considered the source, not the block. $base = $source->getKnockBack(); - $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(Enchantment::BLAST_PROTECTION) * 0.15)); + $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::BLAST_PROTECTION()) * 0.15)); } parent::attack($source); @@ -561,14 +491,14 @@ abstract class Living extends Entity implements Damageable{ $e = $source->getChild(); if($e !== null){ $motion = $e->getMotion(); - $this->knockBack($e, $source->getBaseDamage(), $motion->x, $motion->z, $source->getKnockBack()); + $this->knockBack($motion->x, $motion->z, $source->getKnockBack()); } }elseif($source instanceof EntityDamageByEntityEvent){ $e = $source->getDamager(); if($e !== null){ - $deltaX = $this->x - $e->x; - $deltaZ = $this->z - $e->z; - $this->knockBack($e, $source->getBaseDamage(), $deltaX, $deltaZ, $source->getKnockBack()); + $deltaX = $this->location->x - $e->location->x; + $deltaZ = $this->location->z - $e->location->z; + $this->knockBack($deltaX, $deltaZ, $source->getKnockBack()); } } @@ -579,49 +509,44 @@ abstract class Living extends Entity implements Damageable{ } protected function doHitAnimation() : void{ - $this->broadcastEntityEvent(ActorEventPacket::HURT_ANIMATION); + $this->broadcastAnimation(new HurtAnimation($this)); } - public function knockBack(Entity $attacker, float $damage, float $x, float $z, float $base = 0.4) : void{ + public function knockBack(float $x, float $z, float $force = 0.4, ?float $verticalLimit = 0.4) : void{ $f = sqrt($x * $x + $z * $z); if($f <= 0){ return; } - if(mt_rand() / mt_getrandmax() > $this->getAttributeMap()->getAttribute(Attribute::KNOCKBACK_RESISTANCE)->getValue()){ + if(mt_rand() / mt_getrandmax() > $this->knockbackResistanceAttr->getValue()){ $f = 1 / $f; - $motion = clone $this->motion; + $motionX = $this->motion->x / 2; + $motionY = $this->motion->y / 2; + $motionZ = $this->motion->z / 2; + $motionX += $x * $f * $force; + $motionY += $force; + $motionZ += $z * $f * $force; - $motion->x /= 2; - $motion->y /= 2; - $motion->z /= 2; - $motion->x += $x * $f * $base; - $motion->y += $base; - $motion->z += $z * $f * $base; - - if($motion->y > $base){ - $motion->y = $base; + $verticalLimit ??= $force; + if($motionY > $verticalLimit){ + $motionY = $verticalLimit; } - $this->setMotion($motion); + $this->setMotion(new Vector3($motionX, $motionY, $motionZ)); } } - public function kill() : void{ - parent::kill(); - $this->onDeath(); - $this->startDeathAnimation(); - } - protected function onDeath() : void{ $ev = new EntityDeathEvent($this, $this->getDrops(), $this->getXpDropAmount()); $ev->call(); foreach($ev->getDrops() as $item){ - $this->getLevelNonNull()->dropItem($this, $item); + $this->getWorld()->dropItem($this->location, $item); } //TODO: check death conditions (must have been damaged by player < 5 seconds from death) - $this->level->dropExperience($this, $ev->getXpDropAmount()); + $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount()); + + $this->startDeathAnimation(); } protected function onDeathUpdate(int $tickDiff) : bool{ @@ -636,20 +561,20 @@ abstract class Living extends Entity implements Damageable{ } protected function startDeathAnimation() : void{ - $this->broadcastEntityEvent(ActorEventPacket::DEATH_ANIMATION); + $this->broadcastAnimation(new DeathAnimation($this)); } protected function endDeathAnimation() : void{ $this->despawnFromAll(); } - public function entityBaseTick(int $tickDiff = 1) : bool{ - Timings::$timerLivingEntityBaseTick->startTiming(); + protected function entityBaseTick(int $tickDiff = 1) : bool{ + Timings::$livingEntityBaseTick->startTiming(); $hasUpdate = parent::entityBaseTick($tickDiff); if($this->isAlive()){ - if($this->doEffectsTick($tickDiff)){ + if($this->effectManager->tick($tickDiff)){ $hasUpdate = true; } @@ -668,26 +593,11 @@ abstract class Living extends Entity implements Damageable{ $this->attackTime -= $tickDiff; } - Timings::$timerLivingEntityBaseTick->stopTiming(); + Timings::$livingEntityBaseTick->stopTiming(); return $hasUpdate; } - protected function doEffectsTick(int $tickDiff = 1) : bool{ - foreach($this->effects as $instance){ - $type = $instance->getType(); - if($type->canTick($instance)){ - $type->applyEffect($this, $instance); - } - $instance->decreaseDuration($tickDiff); - if($instance->hasExpired()){ - $this->removeEffect($instance->getId()); - } - } - - return count($this->effects) > 0; - } - /** * Ticks the entity's air supply, consuming it when underwater and regenerating it when out of water. */ @@ -697,7 +607,7 @@ abstract class Living extends Entity implements Damageable{ if(!$this->canBreathe()){ $this->setBreathing(false); - if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(Enchantment::RESPIRATION)) <= 0 or + if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 or lcg_value() <= (1 / ($respirationLevel + 1)) ){ $ticks -= $tickDiff; @@ -727,14 +637,14 @@ abstract class Living extends Entity implements Damageable{ * Returns whether the entity can currently breathe. */ public function canBreathe() : bool{ - return $this->hasEffect(Effect::WATER_BREATHING) or $this->hasEffect(Effect::CONDUIT_POWER) or !$this->isUnderwater(); + return $this->effectManager->has(VanillaEffects::WATER_BREATHING()) or $this->effectManager->has(VanillaEffects::CONDUIT_POWER()) or !$this->isUnderwater(); } /** * Returns whether the entity is currently breathing or not. If this is false, the entity's air supply will be used. */ public function isBreathing() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_BREATHING); + return $this->breathing; } /** @@ -742,7 +652,8 @@ abstract class Living extends Entity implements Damageable{ * For players, this also shows the oxygen bar. */ public function setBreathing(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_BREATHING, $value); + $this->breathing = $value; + $this->networkPropertiesDirty = true; } /** @@ -750,28 +661,30 @@ abstract class Living extends Entity implements Damageable{ * this amount of time without damage due to enchantments such as Respiration. */ public function getAirSupplyTicks() : int{ - return $this->propertyManager->getShort(self::DATA_AIR); + return $this->breathTicks; } /** * Sets the number of air ticks left in the entity's air supply. */ public function setAirSupplyTicks(int $ticks) : void{ - $this->propertyManager->setShort(self::DATA_AIR, $ticks); + $this->breathTicks = $ticks; + $this->networkPropertiesDirty = true; } /** * Returns the maximum amount of air ticks the entity's air supply can contain. */ public function getMaxAirSupplyTicks() : int{ - return $this->propertyManager->getShort(self::DATA_MAX_AIR); + return $this->maxBreathTicks; } /** * Sets the maximum amount of air ticks the air supply can hold. */ public function setMaxAirSupplyTicks(int $ticks) : void{ - $this->propertyManager->setShort(self::DATA_MAX_AIR, $ticks); + $this->maxBreathTicks = $ticks; + $this->networkPropertiesDirty = true; } /** @@ -815,8 +728,8 @@ abstract class Living extends Entity implements Damageable{ $blocks = []; $nextIndex = 0; - foreach(VoxelRayTrace::inDirection($this->add(0, $this->eyeHeight, 0), $this->getDirectionVector(), $maxDistance) as $vector3){ - $block = $this->level->getBlockAt($vector3->x, $vector3->y, $vector3->z); + foreach(VoxelRayTrace::inDirection($this->location->add(0, $this->size->getEyeHeight(), 0), $this->getDirectionVector(), $maxDistance) as $vector3){ + $block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z); $blocks[$nextIndex++] = $block; if($maxLength !== 0 and count($blocks) > $maxLength){ @@ -858,31 +771,47 @@ abstract class Living extends Entity implements Damageable{ * their heads to turn. */ public function lookAt(Vector3 $target) : void{ - $horizontal = sqrt(($target->x - $this->x) ** 2 + ($target->z - $this->z) ** 2); - $vertical = $target->y - $this->y; - $this->pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down + $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 - $xDist = $target->x - $this->x; - $zDist = $target->z - $this->z; - $this->yaw = atan2($zDist, $xDist) / M_PI * 180 - 90; - if($this->yaw < 0){ - $this->yaw += 360.0; + $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; } } protected function sendSpawnPacket(Player $player) : void{ parent::sendSpawnPacket($player); - $this->armorInventory->sendContents($player); + $player->getNetworkSession()->onMobArmorChange($this); } - public function close() : void{ - if(!$this->closed){ - if($this->armorInventory !== null){ - $this->armorInventory->removeAllViewers(true); - $this->armorInventory = null; - } - parent::close(); - } + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setByte(EntityMetadataProperties::POTION_AMBIENT, $this->effectManager->hasOnlyAmbientEffects() ? 1 : 0); + $properties->setInt(EntityMetadataProperties::POTION_COLOR, Binary::signInt($this->effectManager->getBubbleColor()->toARGB())); + $properties->setShort(EntityMetadataProperties::AIR, $this->breathTicks); + $properties->setShort(EntityMetadataProperties::MAX_AIR, $this->maxBreathTicks); + + $properties->setGenericFlag(EntityMetadataFlags::BREATHING, $this->breathing); + $properties->setGenericFlag(EntityMetadataFlags::SNEAKING, $this->sneaking); + $properties->setGenericFlag(EntityMetadataFlags::SPRINTING, $this->sprinting); + } + + protected function onDispose() : void{ + $this->armorInventory->removeAllViewers(); + $this->effectManager->getEffectAddHooks()->clear(); + $this->effectManager->getEffectRemoveHooks()->clear(); + parent::onDispose(); + } + + protected function destroyCycles() : void{ + $this->armorInventory = null; + $this->effectManager = null; + parent::destroyCycles(); } } diff --git a/src/pocketmine/level/Location.php b/src/entity/Location.php similarity index 58% rename from src/pocketmine/level/Location.php rename to src/entity/Location.php index 15362ac3e5..c04ac9acbd 100644 --- a/src/pocketmine/level/Location.php +++ b/src/entity/Location.php @@ -21,9 +21,11 @@ declare(strict_types=1); -namespace pocketmine\level; +namespace pocketmine\entity; use pocketmine\math\Vector3; +use pocketmine\world\Position; +use pocketmine\world\World; class Location extends Position{ @@ -32,50 +34,36 @@ class Location extends Position{ /** @var float */ public $pitch; - /** - * @param float|int $x - * @param float|int $y - * @param float|int $z - * @param float $yaw - * @param float $pitch - */ - public function __construct($x = 0, $y = 0, $z = 0, $yaw = 0.0, $pitch = 0.0, Level $level = 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, $level); + parent::__construct($x, $y, $z, $world); } /** - * @param float $yaw default 0.0 - * @param float $pitch default 0.0 + * @return Location */ - public static function fromObject(Vector3 $pos, Level $level = null, $yaw = 0.0, $pitch = 0.0) : Location{ - return new Location($pos->x, $pos->y, $pos->z, $yaw, $pitch, $level ?? (($pos instanceof Position) ? $pos->level : null)); + 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, $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->level); + return new Location($this->x, $this->y, $this->z, $this->world, $this->yaw, $this->pitch); } - /** - * @return float - */ - public function getYaw(){ + public function getYaw() : float{ return $this->yaw; } - /** - * @return float - */ - public function getPitch(){ + public function getPitch() : float{ return $this->pitch; } public function __toString(){ - return "Location (level=" . ($this->isValid() ? $this->getLevelNonNull()->getName() : "null") . ", x=$this->x, y=$this->y, z=$this->z, yaw=$this->yaw, pitch=$this->pitch)"; + return "Location (world=" . ($this->isValid() ? $this->getWorld()->getDisplayName() : "null") . ", x=$this->x, y=$this->y, z=$this->z, yaw=$this->yaw, pitch=$this->pitch)"; } public function equals(Vector3 $v) : bool{ diff --git a/src/pocketmine/entity/Skin.php b/src/entity/Skin.php similarity index 68% rename from src/pocketmine/entity/Skin.php rename to src/entity/Skin.php index 98d1fdc4e2..0542bd75a0 100644 --- a/src/pocketmine/entity/Skin.php +++ b/src/entity/Skin.php @@ -27,9 +27,10 @@ use Ahc\Json\Comment as CommentedJsonDecoder; use function implode; use function in_array; use function json_encode; +use function json_last_error_msg; use function strlen; -class Skin{ +final class Skin{ public const ACCEPTED_SKIN_SIZES = [ 64 * 32 * 4, 64 * 64 * 4, @@ -48,6 +49,33 @@ class Skin{ private $geometryData; public function __construct(string $skinId, string $skinData, string $capeData = "", string $geometryName = "", string $geometryData = ""){ + if($skinId === ""){ + throw new InvalidSkinException("Skin ID must not be empty"); + } + $len = strlen($skinData); + 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){ + throw new InvalidSkinException("Invalid cape data size " . strlen($capeData) . " bytes (must be exactly 8192 bytes)"); + } + + if($geometryData !== ""){ + $decodedGeometry = (new CommentedJsonDecoder())->decode($geometryData); + if($decodedGeometry === false){ + throw new InvalidSkinException("Invalid geometry data (" . json_last_error_msg() . ")"); + } + + /* + * Hack to cut down on network overhead due to skins, by un-pretty-printing geometry JSON. + * + * Mojang, some stupid reason, send every single model for every single skin in the selected skin-pack. + * 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); + } + $this->skinId = $skinId; $this->skinData = $skinData; $this->capeData = $capeData; @@ -55,35 +83,6 @@ class Skin{ $this->geometryData = $geometryData; } - /** - * @deprecated - */ - public function isValid() : bool{ - try{ - $this->validate(); - return true; - }catch(InvalidSkinException $e){ - return false; - } - } - - /** - * @throws InvalidSkinException - */ - public function validate() : void{ - if($this->skinId === ""){ - throw new InvalidSkinException("Skin ID must not be empty"); - } - $len = strlen($this->skinData); - 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($this->capeData !== "" and strlen($this->capeData) !== 8192){ - throw new InvalidSkinException("Invalid cape data size " . strlen($this->capeData) . " bytes (must be exactly 8192 bytes)"); - } - //TODO: validate geometry - } - public function getSkinId() : string{ return $this->skinId; } @@ -103,17 +102,4 @@ class Skin{ public function getGeometryData() : string{ return $this->geometryData; } - - /** - * Hack to cut down on network overhead due to skins, by un-pretty-printing geometry JSON. - * - * Mojang, some stupid reason, send every single model for every single skin in the selected skin-pack. - * 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) - */ - public function debloatGeometryData() : void{ - if($this->geometryData !== ""){ - $this->geometryData = (string) json_encode((new CommentedJsonDecoder())->decode($this->geometryData)); - } - } } diff --git a/src/pocketmine/entity/Squid.php b/src/entity/Squid.php similarity index 70% rename from src/pocketmine/entity/Squid.php rename to src/entity/Squid.php index 0dd2d64e02..1010c5d78d 100644 --- a/src/pocketmine/entity/Squid.php +++ b/src/entity/Squid.php @@ -23,22 +23,21 @@ declare(strict_types=1); namespace pocketmine\entity; +use pocketmine\entity\animation\SquidInkCloudAnimation; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; -use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; use pocketmine\math\Vector3; -use pocketmine\network\mcpe\protocol\ActorEventPacket; +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; use function atan2; use function mt_rand; use function sqrt; use const M_PI; class Squid extends WaterAnimal{ - public const NETWORK_ID = self::SQUID; - public $width = 0.95; - public $height = 0.95; + public static function getNetworkTypeId() : string{ return EntityIds::SQUID; } /** @var Vector3|null */ public $swimDirection = null; @@ -48,9 +47,11 @@ class Squid extends WaterAnimal{ /** @var int */ private $switchDirectionTicker = 0; - public function initEntity() : void{ + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.95, 0.95); } + + public function initEntity(CompoundTag $nbt) : void{ $this->setMaxHealth(10); - parent::initEntity(); + parent::initEntity($nbt); } public function getName() : string{ @@ -67,10 +68,10 @@ class Squid extends WaterAnimal{ $this->swimSpeed = mt_rand(150, 350) / 2000; $e = $source->getDamager(); if($e !== null){ - $this->swimDirection = (new Vector3($this->x - $e->x, $this->y - $e->y, $this->z - $e->z))->normalize(); + $this->swimDirection = $this->location->subtractVector($e->location)->normalize(); } - $this->broadcastEntityEvent(ActorEventPacket::SQUID_INK_CLOUD); + $this->broadcastAnimation(new SquidInkCloudAnimation($this)); } } @@ -78,7 +79,7 @@ class Squid extends WaterAnimal{ return new Vector3(mt_rand(-1000, 1000) / 1000, mt_rand(-500, 500) / 1000, mt_rand(-1000, 1000) / 1000); } - public function entityBaseTick(int $tickDiff = 1) : bool{ + protected function entityBaseTick(int $tickDiff = 1) : bool{ if($this->closed){ return false; } @@ -94,11 +95,12 @@ class Squid extends WaterAnimal{ if($this->isAlive()){ - if($this->y > 62 and $this->swimDirection !== null){ - $this->swimDirection->y = -0.5; + if($this->location->y > 62 and $this->swimDirection !== null){ + $this->swimDirection = $this->swimDirection->withComponents(null, -0.5, null); } $inWater = $this->isUnderwater(); + $this->setHasGravity(!$inWater); if(!$inWater){ $this->swimDirection = null; }elseif($this->swimDirection !== null){ @@ -111,22 +113,16 @@ class Squid extends WaterAnimal{ } $f = sqrt(($this->motion->x ** 2) + ($this->motion->z ** 2)); - $this->yaw = (-atan2($this->motion->x, $this->motion->z) * 180 / M_PI); - $this->pitch = (-atan2($f, $this->motion->y) * 180 / M_PI); + $this->location->yaw = (-atan2($this->motion->x, $this->motion->z) * 180 / M_PI); + $this->location->pitch = (-atan2($f, $this->motion->y) * 180 / M_PI); } return $hasUpdate; } - protected function applyGravity() : void{ - if(!$this->isUnderwater()){ - parent::applyGravity(); - } - } - public function getDrops() : array{ return [ - ItemFactory::get(Item::DYE, 0, mt_rand(1, 3)) + VanillaItems::INK_SAC()->setCount(mt_rand(1, 3)) ]; } } diff --git a/src/entity/Villager.php b/src/entity/Villager.php new file mode 100644 index 0000000000..d43e4faf9b --- /dev/null +++ b/src/entity/Villager.php @@ -0,0 +1,96 @@ +getInt("Profession", self::PROFESSION_FARMER); + + if($profession > 4 or $profession < 0){ + $profession = self::PROFESSION_FARMER; + } + + $this->setProfession($profession); + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setInt("Profession", $this->getProfession()); + + return $nbt; + } + + /** + * Sets the villager profession + */ + public function setProfession(int $profession) : void{ + $this->profession = $profession; //TODO: validation + $this->networkPropertiesDirty = true; + } + + public function getProfession() : int{ + return $this->profession; + } + + public function isBaby() : bool{ + return $this->baby; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + $properties->setGenericFlag(EntityMetadataFlags::BABY, $this->baby); + + $properties->setInt(EntityMetadataProperties::VARIANT, $this->profession); + } +} diff --git a/src/pocketmine/entity/WaterAnimal.php b/src/entity/WaterAnimal.php similarity index 69% rename from src/pocketmine/entity/WaterAnimal.php rename to src/entity/WaterAnimal.php index 4bcc7d3b98..5edae8ffd4 100644 --- a/src/pocketmine/entity/WaterAnimal.php +++ b/src/entity/WaterAnimal.php @@ -24,11 +24,15 @@ declare(strict_types=1); namespace pocketmine\entity; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; -abstract class WaterAnimal extends Creature implements Ageable{ +abstract class WaterAnimal extends Living implements Ageable{ + /** @var bool */ + protected $baby = false; public function isBaby() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_BABY); + return $this->baby; } public function canBreathe() : bool{ @@ -39,4 +43,9 @@ abstract class WaterAnimal extends Creature implements Ageable{ $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_SUFFOCATION, 2); $this->attack($ev); } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + $properties->setGenericFlag(EntityMetadataFlags::BABY, $this->baby); + } } diff --git a/src/pocketmine/entity/Zombie.php b/src/entity/Zombie.php similarity index 69% rename from src/pocketmine/entity/Zombie.php rename to src/entity/Zombie.php index 464892a343..8cd3adb844 100644 --- a/src/pocketmine/entity/Zombie.php +++ b/src/entity/Zombie.php @@ -23,15 +23,17 @@ declare(strict_types=1); namespace pocketmine\entity; -use pocketmine\item\Item; -use pocketmine\item\ItemFactory; +use pocketmine\item\VanillaItems; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; use function mt_rand; -class Zombie extends Monster{ - public const NETWORK_ID = self::ZOMBIE; +class Zombie extends Living{ - public $width = 0.6; - public $height = 1.8; + public static function getNetworkTypeId() : string{ return EntityIds::ZOMBIE; } + + protected function getInitialSizeInfo() : EntitySizeInfo{ + return new EntitySizeInfo(1.8, 0.6); //TODO: eye height ?? + } public function getName() : string{ return "Zombie"; @@ -39,19 +41,19 @@ class Zombie extends Monster{ public function getDrops() : array{ $drops = [ - ItemFactory::get(Item::ROTTEN_FLESH, 0, mt_rand(0, 2)) + VanillaItems::ROTTEN_FLESH()->setCount(mt_rand(0, 2)) ]; if(mt_rand(0, 199) < 5){ switch(mt_rand(0, 2)){ case 0: - $drops[] = ItemFactory::get(Item::IRON_INGOT, 0, 1); + $drops[] = VanillaItems::IRON_INGOT(); break; case 1: - $drops[] = ItemFactory::get(Item::CARROT, 0, 1); + $drops[] = VanillaItems::CARROT(); break; case 2: - $drops[] = ItemFactory::get(Item::POTATO, 0, 1); + $drops[] = VanillaItems::POTATO(); break; } } diff --git a/src/entity/animation/Animation.php b/src/entity/animation/Animation.php new file mode 100644 index 0000000000..44dfa827bb --- /dev/null +++ b/src/entity/animation/Animation.php @@ -0,0 +1,36 @@ +entity = $entity; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->entity->getId(), ActorEvent::ARM_SWING, 0) + ]; + } +} diff --git a/src/pocketmine/event/inventory/InventoryPickupArrowEvent.php b/src/entity/animation/ArrowShakeAnimation.php similarity index 62% rename from src/pocketmine/event/inventory/InventoryPickupArrowEvent.php rename to src/entity/animation/ArrowShakeAnimation.php index eb26b30c1f..256c6142bf 100644 --- a/src/pocketmine/event/inventory/InventoryPickupArrowEvent.php +++ b/src/entity/animation/ArrowShakeAnimation.php @@ -21,22 +21,27 @@ declare(strict_types=1); -namespace pocketmine\event\inventory; +namespace pocketmine\entity\animation; use pocketmine\entity\projectile\Arrow; -use pocketmine\event\Cancellable; -use pocketmine\inventory\Inventory; +use pocketmine\network\mcpe\protocol\ActorEventPacket; +use pocketmine\network\mcpe\protocol\types\ActorEvent; + +class ArrowShakeAnimation implements Animation{ -class InventoryPickupArrowEvent extends InventoryEvent implements Cancellable{ /** @var Arrow */ private $arrow; + /** @var int */ + private $durationInTicks; - public function __construct(Inventory $inventory, Arrow $arrow){ + public function __construct(Arrow $arrow, int $durationInTicks){ $this->arrow = $arrow; - parent::__construct($inventory); + $this->durationInTicks = $durationInTicks; } - public function getArrow() : Arrow{ - return $this->arrow; + public function encode() : array{ + return [ + ActorEventPacket::create($this->arrow->getId(), ActorEvent::ARROW_SHAKE, $this->durationInTicks) + ]; } } diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/CreativeContentEntry.php b/src/entity/animation/ConsumingItemAnimation.php similarity index 51% rename from src/pocketmine/network/mcpe/protocol/types/inventory/CreativeContentEntry.php rename to src/entity/animation/ConsumingItemAnimation.php index 185c9c4896..b32f118bbd 100644 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/CreativeContentEntry.php +++ b/src/entity/animation/ConsumingItemAnimation.php @@ -21,35 +21,32 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types\inventory; +namespace pocketmine\entity\animation; +use pocketmine\entity\Human; use pocketmine\item\Item; -use pocketmine\network\mcpe\NetworkBinaryStream; +use pocketmine\network\mcpe\convert\ItemTranslator; +use pocketmine\network\mcpe\protocol\ActorEventPacket; +use pocketmine\network\mcpe\protocol\types\ActorEvent; -final class CreativeContentEntry{ +final class ConsumingItemAnimation implements Animation{ - /** @var int */ - private $entryId; + /** @var Human */ + private $human; /** @var Item */ private $item; - public function __construct(int $entryId, Item $item){ - $this->entryId = $entryId; + public function __construct(Human $human, Item $item){ + //TODO: maybe this can be expanded to more than just player entities? + $this->human = $human; $this->item = $item; } - public function getEntryId() : int{ return $this->entryId; } - - public function getItem() : Item{ return $this->item; } - - public static function read(NetworkBinaryStream $in) : self{ - $entryId = $in->readGenericTypeNetworkId(); - $item = $in->getItemStackWithoutStackId(); - return new self($entryId, $item); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->entryId); - $out->putItemStackWithoutStackId($this->item); + public function encode() : array{ + [$netId, $netData] = ItemTranslator::getInstance()->toNetworkId($this->item->getId(), $this->item->getMeta()); + return [ + //TODO: need to check the data values + ActorEventPacket::create($this->human->getId(), ActorEvent::EATING_ITEM, ($netId << 16) | $netData) + ]; } } diff --git a/src/entity/animation/CriticalHitAnimation.php b/src/entity/animation/CriticalHitAnimation.php new file mode 100644 index 0000000000..f3a588b796 --- /dev/null +++ b/src/entity/animation/CriticalHitAnimation.php @@ -0,0 +1,43 @@ +entity = $entity; + } + + public function encode() : array{ + return [ + AnimatePacket::create($this->entity->getId(), AnimatePacket::ACTION_CRITICAL_HIT) + ]; + } +} diff --git a/src/entity/animation/DeathAnimation.php b/src/entity/animation/DeathAnimation.php new file mode 100644 index 0000000000..a3a9244134 --- /dev/null +++ b/src/entity/animation/DeathAnimation.php @@ -0,0 +1,44 @@ +entity = $entity; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->entity->getId(), ActorEvent::DEATH_ANIMATION, 0) + ]; + } +} diff --git a/src/entity/animation/HurtAnimation.php b/src/entity/animation/HurtAnimation.php new file mode 100644 index 0000000000..355b49d470 --- /dev/null +++ b/src/entity/animation/HurtAnimation.php @@ -0,0 +1,44 @@ +entity = $entity; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->entity->getId(), ActorEvent::HURT_ANIMATION, 0) + ]; + } +} diff --git a/src/pocketmine/block/Purpur.php b/src/entity/animation/RespawnAnimation.php similarity index 61% rename from src/pocketmine/block/Purpur.php rename to src/entity/animation/RespawnAnimation.php index 30854b62bb..e8466e26e2 100644 --- a/src/pocketmine/block/Purpur.php +++ b/src/entity/animation/RespawnAnimation.php @@ -21,27 +21,24 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\entity\animation; -class Purpur extends Quartz{ +use pocketmine\entity\Living; +use pocketmine\network\mcpe\protocol\ActorEventPacket; +use pocketmine\network\mcpe\protocol\types\ActorEvent; - protected $id = self::PURPUR_BLOCK; +final class RespawnAnimation implements Animation{ - public function getName() : string{ - static $names = [ - self::NORMAL => "Purpur Block", - self::CHISELED => "Chiseled Purpur", //wtf? - self::PILLAR => "Purpur Pillar" + /** @var Living */ + private $entity; + + public function __construct(Living $entity){ + $this->entity = $entity; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->entity->getId(), ActorEvent::RESPAWN, 0) ]; - - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getBlastResistance() : float{ - return 30; } } diff --git a/src/entity/animation/SquidInkCloudAnimation.php b/src/entity/animation/SquidInkCloudAnimation.php new file mode 100644 index 0000000000..6ae728a797 --- /dev/null +++ b/src/entity/animation/SquidInkCloudAnimation.php @@ -0,0 +1,44 @@ +squid = $squid; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->squid->getId(), ActorEvent::SQUID_INK_CLOUD, 0) + ]; + } +} diff --git a/src/entity/animation/TotemUseAnimation.php b/src/entity/animation/TotemUseAnimation.php new file mode 100644 index 0000000000..ec0d3b12cc --- /dev/null +++ b/src/entity/animation/TotemUseAnimation.php @@ -0,0 +1,45 @@ +human = $human; + } + + public function encode() : array{ + return [ + ActorEventPacket::create($this->human->getId(), ActorEvent::CONSUME_TOTEM, 0) + ]; + } +} diff --git a/src/entity/effect/AbsorptionEffect.php b/src/entity/effect/AbsorptionEffect.php new file mode 100644 index 0000000000..0d7f60513c --- /dev/null +++ b/src/entity/effect/AbsorptionEffect.php @@ -0,0 +1,40 @@ +getEffectLevel()); + if($new > $entity->getAbsorption()){ + $entity->setAbsorption($new); + } + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setAbsorption(0); + } +} diff --git a/src/entity/effect/Effect.php b/src/entity/effect/Effect.php new file mode 100644 index 0000000000..3d1bd4c7cf --- /dev/null +++ b/src/entity/effect/Effect.php @@ -0,0 +1,115 @@ +name; + } + + /** + * Returns a Color object representing this effect's particle colour. + */ + public function getColor() : Color{ + return $this->color; + } + + /** + * Returns whether this effect is harmful. + * TODO: implement inverse effect results for undead mobs + */ + public function isBad() : bool{ + return $this->bad; + } + + /** + * Returns the default duration (in ticks) this effect will apply for if a duration is not specified. + */ + public function getDefaultDuration() : int{ + return $this->defaultDuration; + } + + /** + * Returns whether this effect will give the subject potion bubbles. + */ + public function hasBubbles() : bool{ + return $this->hasBubbles; + } + + /** + * Returns whether the effect will do something on the current tick. + */ + public function canTick(EffectInstance $instance) : bool{ + return false; + } + + /** + * Applies effect results to an entity. This will not be called unless canTick() returns true. + */ + public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ + + } + + /** + * Applies effects to the entity when the effect is first added. + */ + public function add(Living $entity, EffectInstance $instance) : void{ + + } + + /** + * Removes the effect from the entity, resetting any changed values back to their original defaults. + */ + public function remove(Living $entity, EffectInstance $instance) : void{ + + } +} diff --git a/src/pocketmine/entity/EffectInstance.php b/src/entity/effect/EffectInstance.php similarity index 90% rename from src/pocketmine/entity/EffectInstance.php rename to src/entity/effect/EffectInstance.php index 9423d53c77..c908c7ec7a 100644 --- a/src/pocketmine/entity/EffectInstance.php +++ b/src/entity/effect/EffectInstance.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\entity\effect; -use pocketmine\utils\Color; +use pocketmine\color\Color; +use pocketmine\utils\Limits; use function max; -use const INT32_MAX; class EffectInstance{ /** @var Effect */ @@ -52,16 +52,12 @@ class EffectInstance{ public function __construct(Effect $effectType, ?int $duration = null, int $amplifier = 0, bool $visible = true, bool $ambient = false, ?Color $overrideColor = null){ $this->effectType = $effectType; $this->setDuration($duration ?? $effectType->getDefaultDuration()); - $this->amplifier = $amplifier; + $this->setAmplifier($amplifier); $this->visible = $visible; $this->ambient = $ambient; $this->color = $overrideColor ?? $effectType->getColor(); } - public function getId() : int{ - return $this->effectType->getId(); - } - public function getType() : Effect{ return $this->effectType; } @@ -81,8 +77,8 @@ class EffectInstance{ * @return $this */ public function setDuration(int $duration) : EffectInstance{ - if($duration < 0 or $duration > INT32_MAX){ - throw new \InvalidArgumentException("Effect duration must be in range 0 - " . INT32_MAX . ", got $duration"); + if($duration < 0 or $duration > Limits::INT32_MAX){ + throw new \InvalidArgumentException("Effect duration must be in range 0 - " . Limits::INT32_MAX . ", got $duration"); } $this->duration = $duration; @@ -122,6 +118,9 @@ class EffectInstance{ * @return $this */ public function setAmplifier(int $amplifier) : EffectInstance{ + if($amplifier < 0 or $amplifier > 255){ + throw new \InvalidArgumentException("Amplifier must be in range 0 - 255, got $amplifier"); + } $this->amplifier = $amplifier; return $this; @@ -166,14 +165,14 @@ class EffectInstance{ * is not reflective of the default colour of the effect. */ public function getColor() : Color{ - return clone $this->color; + return $this->color; } /** * Sets the colour of this EffectInstance. */ public function setColor(Color $color) : EffectInstance{ - $this->color = clone $color; + $this->color = $color; return $this; } diff --git a/src/entity/effect/EffectManager.php b/src/entity/effect/EffectManager.php new file mode 100644 index 0000000000..17bdb89471 --- /dev/null +++ b/src/entity/effect/EffectManager.php @@ -0,0 +1,243 @@ + + */ + protected $effectAddHooks; + /** + * @var \Closure[]|ObjectSet + * @phpstan-var ObjectSet<\Closure(EffectInstance) : void> + */ + protected $effectRemoveHooks; + + public function __construct(Living $entity){ + $this->entity = $entity; + $this->bubbleColor = new Color(0, 0, 0, 0); + $this->effectAddHooks = new ObjectSet(); + $this->effectRemoveHooks = new ObjectSet(); + } + + /** + * Returns an array of Effects currently active on the mob. + * @return EffectInstance[] + */ + public function all() : array{ + return $this->effects; + } + + /** + * Removes all effects from the mob. + */ + public function clear() : void{ + foreach($this->effects as $effect){ + $this->remove($effect->getType()); + } + } + + /** + * Removes the effect with the specified ID from the mob. + */ + public function remove(Effect $effectType) : void{ + $index = spl_object_id($effectType); + if(isset($this->effects[$index])){ + $effect = $this->effects[$index]; + $hasExpired = $effect->hasExpired(); + $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 + foreach($this->effectAddHooks as $hook){ + $hook($ev->getEffect(), true); + } + } + return; + } + + unset($this->effects[$index]); + $effect->getType()->remove($this->entity, $effect); + foreach($this->effectRemoveHooks as $hook){ + $hook($effect); + } + + $this->recalculateEffectColor(); + } + } + + /** + * Returns the effect instance active on this entity with the specified ID, or null if the mob does not have the + * effect. + */ + public function get(Effect $effect) : ?EffectInstance{ + return $this->effects[spl_object_id($effect)] ?? null; + } + + /** + * Returns whether the specified effect is active on the mob. + */ + public function has(Effect $effect) : bool{ + return isset($this->effects[spl_object_id($effect)]); + } + + /** + * Adds an effect to the mob. + * If a weaker effect of the same type is already applied, it will be replaced. + * If a weaker or equal-strength effect is already applied but has a shorter duration, it will be replaced. + * + * @return bool whether the effect has been successfully applied. + */ + public function add(EffectInstance $effect) : bool{ + $oldEffect = null; + $cancelled = false; + + $index = spl_object_id($effect->getType()); + if(isset($this->effects[$index])){ + $oldEffect = $this->effects[$index]; + if( + abs($effect->getAmplifier()) < $oldEffect->getAmplifier() + or (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) and $effect->getDuration() < $oldEffect->getDuration()) + ){ + $cancelled = true; + } + } + + $ev = new EntityEffectAddEvent($this->entity, $effect, $oldEffect); + if($cancelled){ + $ev->cancel(); + } + + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + if($oldEffect !== null){ + $oldEffect->getType()->remove($this->entity, $oldEffect); + } + + $effect->getType()->add($this->entity, $effect); + foreach($this->effectAddHooks as $hook){ + $hook($effect, $oldEffect !== null); + } + + $this->effects[$index] = $effect; + + $this->recalculateEffectColor(); + + return true; + } + + /** + * Recalculates the mob's potion bubbles colour based on the active effects. + */ + protected function recalculateEffectColor() : void{ + /** @var Color[] $colors */ + $colors = []; + $ambient = true; + foreach($this->effects as $effect){ + if($effect->isVisible() and $effect->getType()->hasBubbles()){ + $level = $effect->getEffectLevel(); + $color = $effect->getColor(); + for($i = 0; $i < $level; ++$i){ + $colors[] = $color; + } + + if(!$effect->isAmbient()){ + $ambient = false; + } + } + } + + if(count($colors) > 0){ + $this->bubbleColor = Color::mix(...$colors); + $this->onlyAmbientEffects = $ambient; + }else{ + $this->bubbleColor = new Color(0, 0, 0, 0); + $this->onlyAmbientEffects = false; + } + } + + public function getBubbleColor() : Color{ + return $this->bubbleColor; + } + + public function hasOnlyAmbientEffects() : bool{ + return $this->onlyAmbientEffects; + } + + public function tick(int $tickDiff = 1) : bool{ + foreach($this->effects as $instance){ + $type = $instance->getType(); + if($type->canTick($instance)){ + $type->applyEffect($this->entity, $instance); + } + $instance->decreaseDuration($tickDiff); + if($instance->hasExpired()){ + $this->remove($instance->getType()); + } + } + + return count($this->effects) > 0; + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void> + */ + public function getEffectAddHooks() : ObjectSet{ + return $this->effectAddHooks; + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(EffectInstance) : void> + */ + public function getEffectRemoveHooks() : ObjectSet{ + return $this->effectRemoveHooks; + } +} diff --git a/src/entity/effect/HealthBoostEffect.php b/src/entity/effect/HealthBoostEffect.php new file mode 100644 index 0000000000..c1d2439da0 --- /dev/null +++ b/src/entity/effect/HealthBoostEffect.php @@ -0,0 +1,37 @@ +setMaxHealth($entity->getMaxHealth() + 4 * $instance->getEffectLevel()); + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setMaxHealth($entity->getMaxHealth() - 4 * $instance->getEffectLevel()); + } +} diff --git a/src/pocketmine/network/mcpe/protocol/ClientToServerHandshakePacket.php b/src/entity/effect/HungerEffect.php similarity index 58% rename from src/pocketmine/network/mcpe/protocol/ClientToServerHandshakePacket.php rename to src/entity/effect/HungerEffect.php index 9a13a7d8e8..022241be1b 100644 --- a/src/pocketmine/network/mcpe/protocol/ClientToServerHandshakePacket.php +++ b/src/entity/effect/HungerEffect.php @@ -21,28 +21,22 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\entity\effect; -#include +use pocketmine\entity\Entity; +use pocketmine\entity\Human; +use pocketmine\entity\Living; +use pocketmine\event\player\PlayerExhaustEvent; -use pocketmine\network\mcpe\NetworkSession; +class HungerEffect extends Effect{ -class ClientToServerHandshakePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CLIENT_TO_SERVER_HANDSHAKE_PACKET; - - public function canBeSentBeforeLogin() : bool{ + public function canTick(EffectInstance $instance) : bool{ return true; } - protected function decodePayload(){ - //No payload - } - - protected function encodePayload(){ - //No payload - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleClientToServerHandshake($this); + public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ + if($entity instanceof Human){ + $entity->getHungerManager()->exhaust(0.025 * $instance->getEffectLevel(), PlayerExhaustEvent::CAUSE_POTION); + } } } diff --git a/src/entity/effect/InstantDamageEffect.php b/src/entity/effect/InstantDamageEffect.php new file mode 100644 index 0000000000..88a22b8040 --- /dev/null +++ b/src/entity/effect/InstantDamageEffect.php @@ -0,0 +1,49 @@ +getAmplifier()) * $potency; + if($source !== null){ + $sourceOwner = $source->getOwningEntity(); + if($sourceOwner !== null){ + $ev = new EntityDamageByChildEntityEvent($sourceOwner, $source, $entity, EntityDamageEvent::CAUSE_MAGIC, $damage); + }else{ + $ev = new EntityDamageByEntityEvent($source, $entity, EntityDamageEvent::CAUSE_MAGIC, $damage); + } + }else{ + $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, $damage); + } + $entity->attack($ev); + } +} diff --git a/src/entity/effect/InstantEffect.php b/src/entity/effect/InstantEffect.php new file mode 100644 index 0000000000..ac5852e2f4 --- /dev/null +++ b/src/entity/effect/InstantEffect.php @@ -0,0 +1,38 @@ +getHealth() < $entity->getMaxHealth()){ + $entity->heal(new EntityRegainHealthEvent($entity, (4 << $instance->getAmplifier()) * $potency, EntityRegainHealthEvent::CAUSE_MAGIC)); + } + } + +} diff --git a/src/entity/effect/InvisibilityEffect.php b/src/entity/effect/InvisibilityEffect.php new file mode 100644 index 0000000000..e170630679 --- /dev/null +++ b/src/entity/effect/InvisibilityEffect.php @@ -0,0 +1,39 @@ +setInvisible(); + $entity->setNameTagVisible(false); + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setInvisible(false); + $entity->setNameTagVisible(); + } +} diff --git a/src/entity/effect/LevitationEffect.php b/src/entity/effect/LevitationEffect.php new file mode 100644 index 0000000000..44e9caccdf --- /dev/null +++ b/src/entity/effect/LevitationEffect.php @@ -0,0 +1,49 @@ +addMotion(0, ($instance->getEffectLevel() / 20 - $entity->getMotion()->y) / 5, 0); + } + } + + public function add(Living $entity, EffectInstance $instance) : void{ + $entity->setHasGravity(false); + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setHasGravity(); + } +} diff --git a/src/entity/effect/PoisonEffect.php b/src/entity/effect/PoisonEffect.php new file mode 100644 index 0000000000..e3b1618b5f --- /dev/null +++ b/src/entity/effect/PoisonEffect.php @@ -0,0 +1,55 @@ +fatal = $fatal; + } + + public function canTick(EffectInstance $instance) : bool{ + if(($interval = (25 >> $instance->getAmplifier())) > 0){ + return ($instance->getDuration() % $interval) === 0; + } + return true; + } + + public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ + if($entity->getHealth() > 1 or $this->fatal){ + $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, 1); + $entity->attack($ev); + } + } +} diff --git a/src/entity/effect/RegenerationEffect.php b/src/entity/effect/RegenerationEffect.php new file mode 100644 index 0000000000..ed5ca0f5f8 --- /dev/null +++ b/src/entity/effect/RegenerationEffect.php @@ -0,0 +1,45 @@ +> $instance->getAmplifier())) > 0){ + return ($instance->getDuration() % $interval) === 0; + } + return true; + } + + public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ + if($entity->getHealth() < $entity->getMaxHealth()){ + $ev = new EntityRegainHealthEvent($entity, 1, EntityRegainHealthEvent::CAUSE_MAGIC); + $entity->heal($ev); + } + } +} diff --git a/src/entity/effect/SaturationEffect.php b/src/entity/effect/SaturationEffect.php new file mode 100644 index 0000000000..638c781306 --- /dev/null +++ b/src/entity/effect/SaturationEffect.php @@ -0,0 +1,39 @@ +getHungerManager(); + $manager->addFood($instance->getEffectLevel()); + $manager->addSaturation($instance->getEffectLevel() * 2); + } + } +} diff --git a/src/entity/effect/SlownessEffect.php b/src/entity/effect/SlownessEffect.php new file mode 100644 index 0000000000..6ab11bdbec --- /dev/null +++ b/src/entity/effect/SlownessEffect.php @@ -0,0 +1,37 @@ +setMovementSpeed($entity->getMovementSpeed() * (1 - 0.15 * $instance->getEffectLevel()), true); + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setMovementSpeed($entity->getMovementSpeed() / (1 - 0.15 * $instance->getEffectLevel())); + } +} diff --git a/src/entity/effect/SpeedEffect.php b/src/entity/effect/SpeedEffect.php new file mode 100644 index 0000000000..d84d3a6d62 --- /dev/null +++ b/src/entity/effect/SpeedEffect.php @@ -0,0 +1,37 @@ +setMovementSpeed($entity->getMovementSpeed() * (1 + 0.2 * $instance->getEffectLevel())); + } + + public function remove(Living $entity, EffectInstance $instance) : void{ + $entity->setMovementSpeed($entity->getMovementSpeed() / (1 + 0.2 * $instance->getEffectLevel())); + } +} diff --git a/src/entity/effect/StringToEffectParser.php b/src/entity/effect/StringToEffectParser.php new file mode 100644 index 0000000000..b35ff68fcb --- /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); + } +} \ No newline at end of file diff --git a/src/entity/effect/VanillaEffects.php b/src/entity/effect/VanillaEffects.php new file mode 100644 index 0000000000..d382a9f60b --- /dev/null +++ b/src/entity/effect/VanillaEffects.php @@ -0,0 +1,111 @@ +> $instance->getAmplifier())) > 0){ + return ($instance->getDuration() % $interval) === 0; + } + return true; + } + + public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{ + $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, 1); + $entity->attack($ev); + } +} diff --git a/src/pocketmine/entity/object/ExperienceOrb.php b/src/entity/object/ExperienceOrb.php similarity index 63% rename from src/pocketmine/entity/object/ExperienceOrb.php rename to src/entity/object/ExperienceOrb.php index aaea3653d5..8011a6a47a 100644 --- a/src/pocketmine/entity/object/ExperienceOrb.php +++ b/src/entity/object/ExperienceOrb.php @@ -24,14 +24,19 @@ declare(strict_types=1); namespace pocketmine\entity\object; use pocketmine\entity\Entity; +use pocketmine\entity\EntitySizeInfo; use pocketmine\entity\Human; -use pocketmine\nbt\tag\IntTag; -use pocketmine\nbt\tag\ShortTag; -use pocketmine\Player; +use pocketmine\entity\Location; +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties; +use pocketmine\player\Player; use function sqrt; class ExperienceOrb extends Entity{ - public const NETWORK_ID = self::XP_ORB; + + public static function getNetworkTypeId() : string{ return EntityIds::XP_ORB; } public const TAG_VALUE_PC = "Value"; //short public const TAG_VALUE_PE = "experience value"; //int (WTF?) @@ -73,9 +78,6 @@ class ExperienceOrb extends Entity{ return $result; } - public $height = 0.25; - public $width = 0.25; - public $gravity = 0.04; public $drag = 0.02; @@ -94,39 +96,43 @@ class ExperienceOrb extends Entity{ */ protected $targetPlayerRuntimeId = null; - protected function initEntity() : void{ - parent::initEntity(); + /** @var int */ + protected $xpValue; - $this->age = $this->namedtag->getShort("Age", 0); - - $value = 0; - if($this->namedtag->hasTag(self::TAG_VALUE_PC, ShortTag::class)){ //PC - $value = $this->namedtag->getShort(self::TAG_VALUE_PC); - }elseif($this->namedtag->hasTag(self::TAG_VALUE_PE, IntTag::class)){ //PE save format - $value = $this->namedtag->getInt(self::TAG_VALUE_PE); - } - - $this->setXpValue($value); + public function __construct(Location $location, int $xpValue, ?CompoundTag $nbt = null){ + $this->xpValue = $xpValue; + parent::__construct($location, $nbt); } - public function saveNBT() : void{ - parent::saveNBT(); + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } - $this->namedtag->setShort("Age", $this->age); + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); - $this->namedtag->setShort(self::TAG_VALUE_PC, $this->getXpValue()); - $this->namedtag->setInt(self::TAG_VALUE_PE, $this->getXpValue()); + $this->age = $nbt->getShort("Age", 0); + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + + $nbt->setShort("Age", $this->age); + + $nbt->setShort(self::TAG_VALUE_PC, $this->getXpValue()); + $nbt->setInt(self::TAG_VALUE_PE, $this->getXpValue()); + + return $nbt; } public function getXpValue() : int{ - return $this->propertyManager->getInt(self::DATA_EXPERIENCE_VALUE) ?? 0; + return $this->xpValue; } public function setXpValue(int $amount) : void{ if($amount <= 0){ throw new \InvalidArgumentException("XP amount must be greater than 0, got $amount"); } - $this->propertyManager->setInt(self::DATA_EXPERIENCE_VALUE, $amount); + $this->xpValue = $amount; + $this->networkPropertiesDirty = true; } public function hasTargetPlayer() : bool{ @@ -138,7 +144,7 @@ class ExperienceOrb extends Entity{ return null; } - $entity = $this->level->getEntity($this->targetPlayerRuntimeId); + $entity = $this->getWorld()->getEntity($this->targetPlayerRuntimeId); if($entity instanceof Human){ return $entity; } @@ -150,7 +156,7 @@ class ExperienceOrb extends Entity{ $this->targetPlayerRuntimeId = $player !== null ? $player->getId() : null; } - public function entityBaseTick(int $tickDiff = 1) : bool{ + protected function entityBaseTick(int $tickDiff = 1) : bool{ $hasUpdate = parent::entityBaseTick($tickDiff); $this->age += $tickDiff; @@ -160,13 +166,13 @@ class ExperienceOrb extends Entity{ } $currentTarget = $this->getTargetPlayer(); - if($currentTarget !== null and (!$currentTarget->isAlive() or $currentTarget->distanceSquared($this) > self::MAX_TARGET_DISTANCE ** 2)){ + if($currentTarget !== null and (!$currentTarget->isAlive() or $currentTarget->location->distanceSquared($this->location) > self::MAX_TARGET_DISTANCE ** 2)){ $currentTarget = null; } if($this->lookForTargetTime >= 20){ if($currentTarget === null){ - $newTarget = $this->level->getNearestEntity($this, self::MAX_TARGET_DISTANCE, Human::class); + $newTarget = $this->getWorld()->getNearestEntity($this->location, self::MAX_TARGET_DISTANCE, Human::class); if($newTarget instanceof Human and !($newTarget instanceof Player and $newTarget->isSpectator())){ $currentTarget = $newTarget; @@ -181,21 +187,17 @@ class ExperienceOrb extends Entity{ $this->setTargetPlayer($currentTarget); if($currentTarget !== null){ - $vector = $currentTarget->add(0, $currentTarget->getEyeHeight() / 2, 0)->subtract($this)->divide(self::MAX_TARGET_DISTANCE); + $vector = $currentTarget->getPosition()->add(0, $currentTarget->getEyeHeight() / 2, 0)->subtractVector($this->location)->divide(self::MAX_TARGET_DISTANCE); $distance = $vector->lengthSquared(); if($distance < 1){ - $diff = $vector->normalize()->multiply(0.2 * (1 - sqrt($distance)) ** 2); - - $this->motion->x += $diff->x; - $this->motion->y += $diff->y; - $this->motion->z += $diff->z; + $this->motion = $this->motion->addVector($vector->normalize()->multiply(0.2 * (1 - sqrt($distance)) ** 2)); } - if($currentTarget->canPickupXp() and $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){ + if($currentTarget->getXpManager()->canPickupXp() and $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){ $this->flagForDespawn(); - $currentTarget->onPickupXp($this->getXpValue()); + $currentTarget->getXpManager()->onPickupXp($this->getXpValue()); } } @@ -203,11 +205,17 @@ class ExperienceOrb extends Entity{ } protected function tryChangeMovement() : void{ - $this->checkObstruction($this->x, $this->y, $this->z); + $this->checkObstruction($this->location->x, $this->location->y, $this->location->z); parent::tryChangeMovement(); } public function canBeCollidedWith() : bool{ return false; } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setInt(EntityMetadataProperties::EXPERIENCE_VALUE, $this->xpValue); + } } diff --git a/src/entity/object/FallingBlock.php b/src/entity/object/FallingBlock.php new file mode 100644 index 0000000000..5376b866ed --- /dev/null +++ b/src/entity/object/FallingBlock.php @@ -0,0 +1,156 @@ +block = $block; + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.98, 0.98); } + + public static function parseBlockNBT(BlockFactory $factory, CompoundTag $nbt) : Block{ + $blockId = 0; + + //TODO: 1.8+ save format + if(($tileIdTag = $nbt->getTag("TileID")) instanceof IntTag){ + $blockId = $tileIdTag->getValue(); + }elseif(($tileTag = $nbt->getTag("Tile")) instanceof ByteTag){ + $blockId = $tileTag->getValue(); + } + + if($blockId === 0){ + throw new \UnexpectedValueException("Missing block info from NBT"); + } + + $damage = $nbt->getByte("Data", 0); + + return $factory->get($blockId, $damage); + } + + public function canCollideWith(Entity $entity) : bool{ + return false; + } + + public function canBeMovedByCurrents() : bool{ + return false; + } + + public function attack(EntityDamageEvent $source) : void{ + if($source->getCause() === EntityDamageEvent::CAUSE_VOID){ + parent::attack($source); + } + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + if($this->closed){ + return false; + } + + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn()){ + $world = $this->getWorld(); + $pos = $this->location->add(-$this->size->getWidth() / 2, $this->size->getHeight(), -$this->size->getWidth() / 2)->floor(); + + $this->block->position($world, $pos->x, $pos->y, $pos->z); + + $blockTarget = null; + if($this->block instanceof Fallable){ + $blockTarget = $this->block->tickFalling(); + } + + if($this->onGround or $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)){ + //FIXME: anvils are supposed to destroy torches + $world->dropItem($this->location, $this->block->asItem()); + }else{ + $ev = new EntityBlockChangeEvent($this, $block, $blockTarget ?? $this->block); + $ev->call(); + if(!$ev->isCancelled()){ + $world->setBlock($pos, $ev->getTo()); + } + } + $hasUpdate = true; + } + } + + return $hasUpdate; + } + + public function getBlock() : Block{ + return $this->block; + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setInt("TileID", $this->block->getId()); + $nbt->setByte("Data", $this->block->getMeta()); + + return $nbt; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setInt(EntityMetadataProperties::VARIANT, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId())); + } + + public function getOffsetPosition(Vector3 $vector3) : Vector3{ + return $vector3->add(0, 0.49, 0); //TODO: check if height affects this + } +} diff --git a/src/entity/object/ItemEntity.php b/src/entity/object/ItemEntity.php new file mode 100644 index 0000000000..899cbc279c --- /dev/null +++ b/src/entity/object/ItemEntity.php @@ -0,0 +1,258 @@ +isNull()){ + throw new \InvalidArgumentException("Item entity must have a non-air item with a count of at least 1"); + } + $this->item = $item; + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->setMaxHealth(5); + $this->setHealth($nbt->getShort("Health", (int) $this->getHealth())); + + $age = $nbt->getShort("Age", 0); + if($age === -32768){ + $this->despawnDelay = self::NEVER_DESPAWN; + }else{ + $this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $age); + } + $this->pickupDelay = $nbt->getShort("PickupDelay", $this->pickupDelay); + $this->owner = $nbt->getString("Owner", $this->owner); + $this->thrower = $nbt->getString("Thrower", $this->thrower); + + (new ItemSpawnEvent($this))->call(); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + if($this->closed){ + return false; + } + + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn() and $this->pickupDelay !== self::NEVER_DESPAWN){ //Infinite delay + $this->pickupDelay -= $tickDiff; + if($this->pickupDelay < 0){ + $this->pickupDelay = 0; + } + + $this->despawnDelay -= $tickDiff; + if($this->despawnDelay <= 0){ + $ev = new ItemDespawnEvent($this); + $ev->call(); + if($ev->isCancelled()){ + $this->despawnDelay = self::DEFAULT_DESPAWN_DELAY; + }else{ + $this->flagForDespawn(); + $hasUpdate = true; + } + } + } + + return $hasUpdate; + } + + protected function tryChangeMovement() : void{ + $this->checkObstruction($this->location->x, $this->location->y, $this->location->z); + parent::tryChangeMovement(); + } + + protected function applyDragBeforeGravity() : bool{ + return true; + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setTag("Item", $this->item->nbtSerialize()); + $nbt->setShort("Health", (int) $this->getHealth()); + if($this->despawnDelay === self::NEVER_DESPAWN){ + $age = -32768; + }else{ + $age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay; + } + $nbt->setShort("Age", $age); + $nbt->setShort("PickupDelay", $this->pickupDelay); + if($this->owner !== null){ + $nbt->setString("Owner", $this->owner); + } + if($this->thrower !== null){ + $nbt->setString("Thrower", $this->thrower); + } + + return $nbt; + } + + public function getItem() : Item{ + return $this->item; + } + + public function canCollideWith(Entity $entity) : bool{ + return false; + } + + public function canBeCollidedWith() : bool{ + return false; + } + + public function getPickupDelay() : int{ + return $this->pickupDelay; + } + + public function setPickupDelay(int $delay) : void{ + $this->pickupDelay = $delay; + } + + /** + * Returns the number of ticks left before this item will despawn. If -1, the item will never despawn. + */ + public function getDespawnDelay() : int{ + return $this->despawnDelay; + } + + /** + * @throws \InvalidArgumentException + */ + public function setDespawnDelay(int $despawnDelay) : void{ + if(($despawnDelay < 0 or $despawnDelay > self::MAX_DESPAWN_DELAY) and $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; + } + + public function getOwner() : string{ + return $this->owner; + } + + public function setOwner(string $owner) : void{ + $this->owner = $owner; + } + + public function getThrower() : string{ + return $this->thrower; + } + + public function setThrower(string $thrower) : void{ + $this->thrower = $thrower; + } + + protected function sendSpawnPacket(Player $player) : void{ + $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 + )); + } + + public function getOffsetPosition(Vector3 $vector3) : Vector3{ + return $vector3->add(0, 0.125, 0); + } + + public function onCollideWithPlayer(Player $player) : void{ + if($this->getPickupDelay() !== 0){ + return; + } + + $item = $this->getItem(); + $playerInventory = match(true){ + $player->getOffHandInventory()->getItem(0)->canStackWith($item) and $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){ + $ev->cancel(); + } + + $ev->call(); + if($ev->isCancelled()){ + return; + } + + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onPlayerPickUpItem($player, $this); + } + + $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 new file mode 100644 index 0000000000..a95560e243 --- /dev/null +++ b/src/entity/object/Painting.php @@ -0,0 +1,233 @@ + Facing::SOUTH, + 1 => Facing::WEST, + 2 => Facing::NORTH, + 3 => Facing::EAST + ]; + private const FACING_TO_DATA = [ + Facing::SOUTH => 0, + Facing::WEST => 1, + Facing::NORTH => 2, + Facing::EAST => 3 + ]; + + /** @var float */ + protected $gravity = 0.0; + /** @var float */ + protected $drag = 1.0; + + /** @var Vector3 */ + protected $blockIn; + /** @var int */ + protected $facing = Facing::NORTH; + /** @var PaintingMotive */ + protected $motive; + + public function __construct(Location $location, Vector3 $blockIn, int $facing, PaintingMotive $motive, ?CompoundTag $nbt = null){ + $this->motive = $motive; + $this->blockIn = $blockIn->asVector3(); + $this->facing = $facing; + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ + //these aren't accurate, but it doesn't matter since they aren't used (vanilla PC does something similar) + return new EntitySizeInfo(0.5, 0.5); + } + + protected function initEntity(CompoundTag $nbt) : void{ + $this->setMaxHealth(1); + $this->setHealth(1); + parent::initEntity($nbt); + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setInt("TileX", (int) $this->blockIn->x); + $nbt->setInt("TileY", (int) $this->blockIn->y); + $nbt->setInt("TileZ", (int) $this->blockIn->z); + + $nbt->setByte("Facing", self::FACING_TO_DATA[$this->facing]); + $nbt->setByte("Direction", self::FACING_TO_DATA[$this->facing]); //Save both for full compatibility + + $nbt->setString("Motive", $this->motive->getName()); + + return $nbt; + } + + protected function onDeath() : void{ + parent::onDeath(); + + $drops = true; + + if($this->lastDamageCause instanceof EntityDamageByEntityEvent){ + $killer = $this->lastDamageCause->getDamager(); + if($killer instanceof Player and !$killer->hasFiniteResources()){ + $drops = false; + } + } + + if($drops){ + //non-living entities don't have a way to create drops generically yet + $this->getWorld()->dropItem($this->location, VanillaItems::PAINTING()); + } + $this->getWorld()->addParticle($this->location->add(0.5, 0.5, 0.5), new BlockBreakParticle(VanillaBlocks::OAK_PLANKS())); + } + + protected function recalculateBoundingBox() : void{ + $side = $this->blockIn->getSide($this->facing); + $this->boundingBox = self::getPaintingBB($this->facing, $this->getMotive())->offset($side->x, $side->y, $side->z); + } + + public function onNearbyBlockChange() : void{ + parent::onNearbyBlockChange(); + + if(!self::canFit($this->getWorld(), $this->blockIn->getSide($this->facing), $this->facing, false, $this->getMotive())){ + $this->kill(); + } + } + + public function onRandomUpdate() : void{ + //NOOP + } + + public function hasMovementUpdate() : bool{ + return false; + } + + protected function updateMovement(bool $teleport = false) : void{ + + } + + public function canBeCollidedWith() : bool{ + return false; + } + + protected function sendSpawnPacket(Player $player) : void{ + $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() + )); + } + + /** + * Returns the painting motive (which image is displayed on the painting) + */ + public function getMotive() : PaintingMotive{ + return $this->motive; + } + + public function getFacing() : int{ + return $this->facing; + } + + /** + * Returns the bounding-box a painting with the specified motive would have at the given position and direction. + */ + private static function getPaintingBB(int $facing, PaintingMotive $motive) : AxisAlignedBB{ + $width = $motive->getWidth(); + $height = $motive->getHeight(); + + $horizontalStart = (int) (ceil($width / 2) - 1); + $verticalStart = (int) (ceil($height / 2) - 1); + + return AxisAlignedBB::one() + ->trim($facing, 15 / 16) + ->extend(Facing::rotateY($facing, true), $horizontalStart) + ->extend(Facing::rotateY($facing, false), -$horizontalStart + $width - 1) + ->extend(Facing::DOWN, $verticalStart) + ->extend(Facing::UP, -$verticalStart + $height - 1); + } + + /** + * Returns whether a painting with the specified motive can be placed at the given position. + */ + public static function canFit(World $world, Vector3 $blockIn, int $facing, bool $checkOverlap, PaintingMotive $motive) : bool{ + $width = $motive->getWidth(); + $height = $motive->getHeight(); + + $horizontalStart = (int) (ceil($width / 2) - 1); + $verticalStart = (int) (ceil($height / 2) - 1); + + $rotatedFace = Facing::rotateY($facing, false); + + $oppositeSide = Facing::opposite($facing); + + $startPos = $blockIn->asVector3()->getSide(Facing::opposite($rotatedFace), $horizontalStart)->getSide(Facing::DOWN, $verticalStart); + + for($w = 0; $w < $width; ++$w){ + for($h = 0; $h < $height; ++$h){ + $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()){ + return false; + } + } + } + + if($checkOverlap){ + $bb = self::getPaintingBB($facing, $motive)->offset($blockIn->x, $blockIn->y, $blockIn->z); + + foreach($world->getNearbyEntities($bb) as $entity){ + if($entity instanceof self){ + return false; + } + } + } + + return true; + } +} diff --git a/src/pocketmine/entity/object/PaintingMotive.php b/src/entity/object/PaintingMotive.php similarity index 100% rename from src/pocketmine/entity/object/PaintingMotive.php rename to src/entity/object/PaintingMotive.php diff --git a/src/entity/object/PrimedTNT.php b/src/entity/object/PrimedTNT.php new file mode 100644 index 0000000000..f3ed921b89 --- /dev/null +++ b/src/entity/object/PrimedTNT.php @@ -0,0 +1,142 @@ +fuse; + } + + public function setFuse(int $fuse) : void{ + if($fuse < 0 or $fuse > 32767){ + throw new \InvalidArgumentException("Fuse must be in the range 0-32767"); + } + $this->fuse = $fuse; + $this->networkPropertiesDirty = true; + } + + public function worksUnderwater() : bool{ return $this->worksUnderwater; } + + public function setWorksUnderwater(bool $worksUnderwater) : void{ + $this->worksUnderwater = $worksUnderwater; + $this->networkPropertiesDirty = true; + } + + public function attack(EntityDamageEvent $source) : void{ + if($source->getCause() === EntityDamageEvent::CAUSE_VOID){ + parent::attack($source); + } + } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->fuse = $nbt->getShort("Fuse", 80); + } + + public function canCollideWith(Entity $entity) : bool{ + return false; + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setShort("Fuse", $this->fuse); + + return $nbt; + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + if($this->closed){ + return false; + } + + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn()){ + $this->fuse -= $tickDiff; + $this->networkPropertiesDirty = true; + + if($this->fuse <= 0){ + $this->flagForDespawn(); + $this->explode(); + } + } + + return $hasUpdate or $this->fuse >= 0; + } + + public function explode() : void{ + $ev = new ExplosionPrimeEvent($this, 4); + $ev->call(); + if(!$ev->isCancelled()){ + //TODO: deal with underwater TNT (underwater TNT treats water as if it has a blast resistance of 0) + $explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getForce(), $this); + if($ev->isBlockBreaking()){ + $explosion->explodeA(); + } + $explosion->explodeB(); + } + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setGenericFlag(EntityMetadataFlags::IGNITED, true); + $properties->setInt(EntityMetadataProperties::VARIANT, $this->worksUnderwater ? 1 : 0); + $properties->setInt(EntityMetadataProperties::FUSE_LENGTH, $this->fuse); + } + + public function getOffsetPosition(Vector3 $vector3) : Vector3{ + return $vector3->add(0, 0.49, 0); + } +} diff --git a/src/pocketmine/entity/projectile/Arrow.php b/src/entity/projectile/Arrow.php similarity index 56% rename from src/pocketmine/entity/projectile/Arrow.php rename to src/entity/projectile/Arrow.php index 60de955b65..ca194d2c56 100644 --- a/src/pocketmine/entity/projectile/Arrow.php +++ b/src/entity/projectile/Arrow.php @@ -24,32 +24,33 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\block\Block; +use pocketmine\entity\animation\ArrowShakeAnimation; use pocketmine\entity\Entity; +use pocketmine\entity\EntitySizeInfo; +use pocketmine\entity\Location; +use pocketmine\event\entity\EntityItemPickupEvent; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\event\inventory\InventoryPickupArrowEvent; -use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -use pocketmine\level\Level; +use pocketmine\item\VanillaItems; use pocketmine\math\RayTraceResult; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\protocol\ActorEventPacket; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\network\mcpe\protocol\TakeItemActorPacket; -use pocketmine\Player; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; +use pocketmine\player\Player; +use pocketmine\world\sound\ArrowHitSound; use function mt_rand; use function sqrt; class Arrow extends Projectile{ - public const NETWORK_ID = self::ARROW; + + public static function getNetworkTypeId() : string{ return EntityIds::ARROW; } public const PICKUP_NONE = 0; public const PICKUP_ANY = 1; public const PICKUP_CREATIVE = 2; private const TAG_PICKUP = "pickup"; //TAG_Byte - - public $width = 0.25; - public $height = 0.25; + public const TAG_CRIT = "crit"; //TAG_Byte protected $gravity = 0.05; protected $drag = 0.01; @@ -66,31 +67,39 @@ class Arrow extends Projectile{ /** @var int */ protected $collideTicks = 0; - public function __construct(Level $level, CompoundTag $nbt, ?Entity $shootingEntity = null, bool $critical = false){ - parent::__construct($level, $nbt, $shootingEntity); + /** @var bool */ + protected $critical = false; + + public function __construct(Location $location, ?Entity $shootingEntity, bool $critical, ?CompoundTag $nbt = null){ + parent::__construct($location, $shootingEntity, $nbt); $this->setCritical($critical); } - protected function initEntity() : void{ - parent::initEntity(); + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } - $this->pickupMode = $this->namedtag->getByte(self::TAG_PICKUP, self::PICKUP_ANY, true); - $this->collideTicks = $this->namedtag->getShort("life", $this->collideTicks); + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->pickupMode = $nbt->getByte(self::TAG_PICKUP, self::PICKUP_ANY); + $this->critical = $nbt->getByte(self::TAG_CRIT, 0) === 1; + $this->collideTicks = $nbt->getShort("life", $this->collideTicks); } - public function saveNBT() : void{ - parent::saveNBT(); - - $this->namedtag->setByte(self::TAG_PICKUP, $this->pickupMode, true); - $this->namedtag->setShort("life", $this->collideTicks); + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setByte(self::TAG_PICKUP, $this->pickupMode); + $nbt->setByte(self::TAG_CRIT, $this->critical ? 1 : 0); + $nbt->setShort("life", $this->collideTicks); + return $nbt; } public function isCritical() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_CRITICAL); + return $this->critical; } public function setCritical(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_CRITICAL, $value); + $this->critical = $value; + $this->networkPropertiesDirty = true; } public function getResultDamage() : int{ @@ -110,7 +119,7 @@ class Arrow extends Projectile{ $this->punchKnockback = $punchKnockback; } - public function entityBaseTick(int $tickDiff = 1) : bool{ + protected function entityBaseTick(int $tickDiff = 1) : bool{ if($this->closed){ return false; } @@ -132,12 +141,12 @@ class Arrow extends Projectile{ protected function onHit(ProjectileHitEvent $event) : void{ $this->setCritical(false); - $this->level->broadcastLevelSoundEvent($this, LevelSoundEventPacket::SOUND_BOW_HIT); + $this->broadcastSound(new ArrowHitSound()); } protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ parent::onHitBlock($blockHit, $hitResult); - $this->broadcastEntityEvent(ActorEventPacket::ARROW_SHAKE, 7); //7 ticks + $this->broadcastAnimation(new ArrowShakeAnimation($this, 7)); } protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{ @@ -164,16 +173,20 @@ class Arrow extends Projectile{ return; } - $item = ItemFactory::get(Item::ARROW, 0, 1); + $item = VanillaItems::ARROW(); + $playerInventory = match(true){ + !$player->hasFiniteResources() => null, //arrows are not picked up in creative + $player->getOffHandInventory()->getItem(0)->canStackWith($item) and $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(), + $player->getInventory()->canAddItem($item) => $player->getInventory(), + default => null + }; - $playerInventory = $player->getInventory(); - if($player->isSurvival() and !$playerInventory->canAddItem($item)){ - return; + $ev = new EntityItemPickupEvent($player, $this, $item, $playerInventory); + if($player->hasFiniteResources() and $playerInventory === null){ + $ev->cancel(); } - - $ev = new InventoryPickupArrowEvent($playerInventory, $this); if($this->pickupMode === self::PICKUP_NONE or ($this->pickupMode === self::PICKUP_CREATIVE and !$player->isCreative())){ - $ev->setCancelled(); + $ev->cancel(); } $ev->call(); @@ -181,12 +194,17 @@ class Arrow extends Projectile{ return; } - $pk = new TakeItemActorPacket(); - $pk->eid = $player->getId(); - $pk->target = $this->getId(); - $this->server->broadcastPacket($this->getViewers(), $pk); + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onPlayerPickUpItem($player, $this); + } - $playerInventory->addItem(clone $item); + $ev->getInventory()?->addItem($ev->getItem()); $this->flagForDespawn(); } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setGenericFlag(EntityMetadataFlags::CRITICAL, $this->critical); + } } diff --git a/src/pocketmine/entity/projectile/Egg.php b/src/entity/projectile/Egg.php similarity index 75% rename from src/pocketmine/entity/projectile/Egg.php rename to src/entity/projectile/Egg.php index 21cfb5a732..c12af4731e 100644 --- a/src/pocketmine/entity/projectile/Egg.php +++ b/src/entity/projectile/Egg.php @@ -24,18 +24,18 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -use pocketmine\level\particle\ItemBreakParticle; +use pocketmine\item\VanillaItems; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\world\particle\ItemBreakParticle; class Egg extends Throwable{ - public const NETWORK_ID = self::EGG; + public static function getNetworkTypeId() : string{ return EntityIds::EGG; } //TODO: spawn chickens on collision protected function onHit(ProjectileHitEvent $event) : void{ for($i = 0; $i < 6; ++$i){ - $this->level->addParticle(new ItemBreakParticle($this, ItemFactory::get(Item::EGG))); + $this->getWorld()->addParticle($this->location, new ItemBreakParticle(VanillaItems::EGG())); } } } diff --git a/src/pocketmine/entity/projectile/EnderPearl.php b/src/entity/projectile/EnderPearl.php similarity index 68% rename from src/pocketmine/entity/projectile/EnderPearl.php rename to src/entity/projectile/EnderPearl.php index 627f84fde2..2714ab81d9 100644 --- a/src/pocketmine/entity/projectile/EnderPearl.php +++ b/src/entity/projectile/EnderPearl.php @@ -25,11 +25,12 @@ namespace pocketmine\entity\projectile; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\level\sound\EndermanTeleportSound; -use pocketmine\network\mcpe\protocol\LevelEventPacket; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\world\particle\EndermanTeleportParticle; +use pocketmine\world\sound\EndermanTeleportSound; class EnderPearl extends Throwable{ - public const NETWORK_ID = self::ENDER_PEARL; + public static function getNetworkTypeId() : string{ return EntityIds::ENDER_PEARL; } protected function onHit(ProjectileHitEvent $event) : void{ $owner = $this->getOwningEntity(); @@ -37,10 +38,10 @@ class EnderPearl extends Throwable{ //TODO: check end gateways (when they are added) //TODO: spawn endermites at origin - $this->level->broadcastLevelEvent($owner, LevelEventPacket::EVENT_PARTICLE_ENDERMAN_TELEPORT); - $this->level->addSound(new EndermanTeleportSound($owner)); - $owner->teleport($event->getRayTraceResult()->getHitVector()); - $this->level->addSound(new EndermanTeleportSound($owner)); + $this->getWorld()->addParticle($origin = $owner->getPosition(), new EndermanTeleportParticle()); + $this->getWorld()->addSound($origin, new EndermanTeleportSound()); + $owner->teleport($target = $event->getRayTraceResult()->getHitVector()); + $this->getWorld()->addSound($target, new EndermanTeleportSound()); $owner->attack(new EntityDamageEvent($owner, EntityDamageEvent::CAUSE_FALL, 5)); } diff --git a/src/pocketmine/entity/projectile/ExperienceBottle.php b/src/entity/projectile/ExperienceBottle.php similarity index 68% rename from src/pocketmine/entity/projectile/ExperienceBottle.php rename to src/entity/projectile/ExperienceBottle.php index 213f52b0b0..eb69b0edfa 100644 --- a/src/pocketmine/entity/projectile/ExperienceBottle.php +++ b/src/entity/projectile/ExperienceBottle.php @@ -24,13 +24,13 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\network\mcpe\protocol\LevelEventPacket; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\utils\Color; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\world\particle\PotionSplashParticle; +use pocketmine\world\sound\PotionSplashSound; use function mt_rand; class ExperienceBottle extends Throwable{ - public const NETWORK_ID = self::XP_BOTTLE; + public static function getNetworkTypeId() : string{ return EntityIds::XP_BOTTLE; } protected $gravity = 0.07; @@ -39,9 +39,9 @@ class ExperienceBottle extends Throwable{ } public function onHit(ProjectileHitEvent $event) : void{ - $this->level->broadcastLevelEvent($this, LevelEventPacket::EVENT_PARTICLE_SPLASH, (new Color(0x38, 0x5d, 0xc6))->toARGB()); - $this->level->broadcastLevelSoundEvent($this, LevelSoundEventPacket::SOUND_GLASS); + $this->getWorld()->addParticle($this->location, new PotionSplashParticle(PotionSplashParticle::DEFAULT_COLOR())); + $this->broadcastSound(new PotionSplashSound()); - $this->level->dropExperience($this, mt_rand(3, 11)); + $this->getWorld()->dropExperience($this->location, mt_rand(3, 11)); } } diff --git a/src/pocketmine/entity/projectile/Projectile.php b/src/entity/projectile/Projectile.php similarity index 70% rename from src/pocketmine/entity/projectile/Projectile.php rename to src/entity/projectile/Projectile.php index f05b75c3cf..cf700b052a 100644 --- a/src/pocketmine/entity/projectile/Projectile.php +++ b/src/entity/projectile/Projectile.php @@ -24,8 +24,10 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\block\Block; +use pocketmine\block\BlockFactory; use pocketmine\entity\Entity; use pocketmine\entity\Living; +use pocketmine\entity\Location; use pocketmine\event\entity\EntityCombustByEntityEvent; use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; @@ -33,7 +35,6 @@ use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\ProjectileHitBlockEvent; use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\level\Level; use pocketmine\math\RayTraceResult; use pocketmine\math\Vector3; use pocketmine\math\VoxelRayTrace; @@ -53,15 +54,11 @@ abstract class Projectile extends Entity{ /** @var float */ protected $damage = 0.0; - /** @var Vector3|null */ + /** @var Block|null */ protected $blockHit; - /** @var int|null */ - protected $blockHitId; - /** @var int|null */ - protected $blockHitData; - public function __construct(Level $level, CompoundTag $nbt, ?Entity $shootingEntity = null){ - parent::__construct($level, $nbt); + public function __construct(Location $location, ?Entity $shootingEntity, ?CompoundTag $nbt = null){ + parent::__construct($location, $nbt); if($shootingEntity !== null){ $this->setOwningEntity($shootingEntity); } @@ -73,35 +70,34 @@ abstract class Projectile extends Entity{ } } - protected function initEntity() : void{ - parent::initEntity(); + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); $this->setMaxHealth(1); $this->setHealth(1); - $this->damage = $this->namedtag->getDouble("damage", $this->damage); + $this->damage = $nbt->getDouble("damage", $this->damage); - (function() : void{ - if($this->namedtag->hasTag("tileX", IntTag::class) and $this->namedtag->hasTag("tileY", IntTag::class) and $this->namedtag->hasTag("tileZ", IntTag::class)){ - $blockHit = new Vector3($this->namedtag->getInt("tileX"), $this->namedtag->getInt("tileY"), $this->namedtag->getInt("tileZ")); + (function() use ($nbt) : void{ + if(($tileXTag = $nbt->getTag("tileX")) instanceof IntTag and ($tileYTag = $nbt->getTag("tileY")) instanceof IntTag and ($tileZTag = $nbt->getTag("tileZ")) instanceof IntTag){ + $blockPos = new Vector3($tileXTag->getValue(), $tileYTag->getValue(), $tileZTag->getValue()); }else{ return; } - if($this->namedtag->hasTag("blockId", IntTag::class)){ - $blockId = $this->namedtag->getInt("blockId"); + if(($blockIdTag = $nbt->getTag("blockId")) instanceof IntTag){ + $blockId = $blockIdTag->getValue(); }else{ return; } - if($this->namedtag->hasTag("blockData", ByteTag::class)){ - $blockData = $this->namedtag->getByte("blockData"); + if(($blockDataTag = $nbt->getTag("blockData")) instanceof ByteTag){ + $blockData = $blockDataTag->getValue(); }else{ return; } - $this->blockHit = $blockHit; - $this->blockHitId = $blockId; - $this->blockHitData = $blockData; + $this->blockHit = BlockFactory::getInstance()->get($blockId, $blockData); + $this->blockHit->position($this->getWorld(), $blockPos->getFloorX(), $blockPos->getFloorY(), $blockPos->getFloorZ()); })(); } @@ -135,20 +131,23 @@ abstract class Projectile extends Entity{ return (int) ceil($this->motion->length() * $this->damage); } - public function saveNBT() : void{ - parent::saveNBT(); + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); - $this->namedtag->setDouble("damage", $this->damage); + $nbt->setDouble("damage", $this->damage); if($this->blockHit !== null){ - $this->namedtag->setInt("tileX", $this->blockHit->x); - $this->namedtag->setInt("tileY", $this->blockHit->y); - $this->namedtag->setInt("tileZ", $this->blockHit->z); + $pos = $this->blockHit->getPosition(); + $nbt->setInt("tileX", $pos->x); + $nbt->setInt("tileY", $pos->y); + $nbt->setInt("tileZ", $pos->z); //we intentionally use different ones to PC because we don't have stringy IDs - $this->namedtag->setInt("blockId", $this->blockHitId); - $this->namedtag->setByte("blockData", $this->blockHitData); + $nbt->setInt("blockId", $this->blockHit->getId()); + $nbt->setByte("blockData", $this->blockHit->getMeta()); } + + return $nbt; } protected function applyDragBeforeGravity() : bool{ @@ -156,11 +155,8 @@ abstract class Projectile extends Entity{ } public function onNearbyBlockChange() : void{ - if($this->blockHit !== null){ - $blockIn = $this->level->getBlockAt($this->blockHit->x, $this->blockHit->y, $this->blockHit->z); - if($blockIn->getId() !== $this->blockHitId or $blockIn->getDamage() !== $this->blockHitData){ - $this->blockHit = $this->blockHitId = $this->blockHitData = null; - } + if($this->blockHit !== null and $this->getWorld()->isInLoadedTerrain($this->blockHit->getPosition()) and !$this->blockHit->isSameState($this->getWorld()->getBlock($this->blockHit->getPosition()))){ + $this->blockHit = null; } parent::onNearbyBlockChange(); @@ -170,12 +166,12 @@ abstract class Projectile extends Entity{ return $this->blockHit === null and 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::$entityMoveTimer->startTiming(); + Timings::$entityMove->startTiming(); - $start = $this->asVector3(); + $start = $this->location->asVector3(); $end = $start->add($dx, $dy, $dz); $blockHit = null; @@ -183,7 +179,7 @@ abstract class Projectile extends Entity{ $hitResult = null; foreach(VoxelRayTrace::betweenPoints($start, $end) as $vector3){ - $block = $this->level->getBlockAt($vector3->x, $vector3->y, $vector3->z); + $block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z); $blockHitResult = $this->calculateInterceptWithBlock($block, $start, $end); if($blockHitResult !== null){ @@ -196,8 +192,8 @@ abstract class Projectile extends Entity{ $entityDistance = PHP_INT_MAX; - $newDiff = $end->subtract($start); - foreach($this->level->getCollidingEntities($this->boundingBox->addCoord($newDiff->x, $newDiff->y, $newDiff->z)->expand(1, 1, 1), $this) as $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){ continue; } @@ -209,7 +205,7 @@ abstract class Projectile extends Entity{ continue; } - $distance = $this->distanceSquared($entityHitResult->hitVector); + $distance = $this->location->distanceSquared($entityHitResult->hitVector); if($distance < $entityDistance){ $entityDistance = $distance; @@ -219,9 +215,12 @@ abstract class Projectile extends Entity{ } } - $this->x = $end->x; - $this->y = $end->y; - $this->z = $end->z; + $this->location = Location::fromObject( + $end, + $this->location->world, + $this->location->yaw, + $this->location->pitch + ); $this->recalculateBoundingBox(); if($hitResult !== null){ @@ -247,21 +246,21 @@ abstract class Projectile extends Entity{ } $this->isCollided = $this->onGround = true; - $this->motion->x = $this->motion->y = $this->motion->z = 0; + $this->motion = new Vector3(0, 0, 0); }else{ $this->isCollided = $this->onGround = false; - $this->blockHit = $this->blockHitId = $this->blockHitData = null; + $this->blockHit = null; //recompute angles... $f = sqrt(($this->motion->x ** 2) + ($this->motion->z ** 2)); - $this->yaw = (atan2($this->motion->x, $this->motion->z) * 180 / M_PI); - $this->pitch = (atan2($this->motion->y, $f) * 180 / M_PI); + $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->checkChunks(); - $this->checkBlockCollision(); + $this->getWorld()->onEntityMoved($this); + $this->checkBlockIntersections(); - Timings::$entityMoveTimer->stopTiming(); + Timings::$entityMove->stopTiming(); } /** @@ -314,8 +313,6 @@ abstract class Projectile extends Entity{ * Called when the projectile collides with a Block. */ protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ - $this->blockHit = $blockHit->asVector3(); - $this->blockHitId = $blockHit->getId(); - $this->blockHitData = $blockHit->getDamage(); + $this->blockHit = clone $blockHit; } } diff --git a/src/pocketmine/entity/projectile/ProjectileSource.php b/src/entity/projectile/ProjectileSource.php similarity index 100% rename from src/pocketmine/entity/projectile/ProjectileSource.php rename to src/entity/projectile/ProjectileSource.php diff --git a/src/pocketmine/entity/projectile/Snowball.php b/src/entity/projectile/Snowball.php similarity index 77% rename from src/pocketmine/entity/projectile/Snowball.php rename to src/entity/projectile/Snowball.php index 93304822bd..54c18b6e35 100644 --- a/src/pocketmine/entity/projectile/Snowball.php +++ b/src/entity/projectile/Snowball.php @@ -24,14 +24,15 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\level\particle\SnowballPoofParticle; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\world\particle\SnowballPoofParticle; class Snowball extends Throwable{ - public const NETWORK_ID = self::SNOWBALL; + public static function getNetworkTypeId() : string{ return EntityIds::SNOWBALL; } protected function onHit(ProjectileHitEvent $event) : void{ for($i = 0; $i < 6; ++$i){ - $this->level->addParticle(new SnowballPoofParticle($this)); + $this->getWorld()->addParticle($this->location, new SnowballPoofParticle()); } } } diff --git a/src/pocketmine/entity/projectile/SplashPotion.php b/src/entity/projectile/SplashPotion.php similarity index 53% rename from src/pocketmine/entity/projectile/SplashPotion.php rename to src/entity/projectile/SplashPotion.php index 1d841f37ef..a7ecfa47a3 100644 --- a/src/pocketmine/entity/projectile/SplashPotion.php +++ b/src/entity/projectile/SplashPotion.php @@ -23,37 +23,52 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; -use pocketmine\entity\EffectInstance; +use pocketmine\block\BlockLegacyIds; +use pocketmine\block\VanillaBlocks; +use pocketmine\color\Color; +use pocketmine\data\bedrock\PotionTypeIdMap; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\InstantEffect; +use pocketmine\entity\Entity; use pocketmine\entity\Living; +use pocketmine\entity\Location; use pocketmine\event\entity\ProjectileHitBlockEvent; use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEvent; use pocketmine\item\Potion; -use pocketmine\network\mcpe\protocol\LevelEventPacket; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\utils\Color; +use pocketmine\item\PotionType; +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\protocol\types\entity\EntityIds; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; +use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties; +use pocketmine\world\particle\PotionSplashParticle; +use pocketmine\world\sound\PotionSplashSound; use function count; use function round; use function sqrt; class SplashPotion extends Throwable{ - public const NETWORK_ID = self::SPLASH_POTION; + public static function getNetworkTypeId() : string{ return EntityIds::SPLASH_POTION; } protected $gravity = 0.05; protected $drag = 0.01; - protected function initEntity() : void{ - parent::initEntity(); + /** @var bool */ + protected $linger = false; + protected PotionType $potionType; - $this->setPotionId($this->namedtag->getShort("PotionId", 0)); + public function __construct(Location $location, ?Entity $shootingEntity, PotionType $potionType, ?CompoundTag $nbt = null){ + $this->potionType = $potionType; + parent::__construct($location, $shootingEntity, $nbt); } - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setShort("PotionId", $this->getPotionId()); + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setShort("PotionId", PotionTypeIdMap::getInstance()->toId($this->getPotionType())); + + return $nbt; } public function getResultDamage() : int{ @@ -65,9 +80,7 @@ class SplashPotion extends Throwable{ $hasEffects = true; if(count($effects) === 0){ - $colors = [ - new Color(0x38, 0x5d, 0xc6) //Default colour for splash water bottle and similar with no effects. - ]; + $particle = new PotionSplashParticle(PotionSplashParticle::DEFAULT_COLOR()); $hasEffects = false; }else{ $colors = []; @@ -77,16 +90,17 @@ class SplashPotion extends Throwable{ $colors[] = $effect->getColor(); } } + $particle = new PotionSplashParticle(Color::mix(...$colors)); } - $this->level->broadcastLevelEvent($this, LevelEventPacket::EVENT_PARTICLE_SPLASH, Color::mix(...$colors)->toARGB()); - $this->level->broadcastLevelSoundEvent($this, LevelSoundEventPacket::SOUND_GLASS); + $this->getWorld()->addParticle($this->location, $particle); + $this->broadcastSound(new PotionSplashSound()); if($hasEffects){ if(!$this->willLinger()){ - foreach($this->level->getNearbyEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){ + foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){ if($entity instanceof Living and $entity->isAlive()){ - $distanceSquared = $entity->add(0, $entity->getEyeHeight(), 0)->distanceSquared($this); + $distanceSquared = $entity->getEyePos()->distanceSquared($this->location); if($distanceSquared > 16){ //4 blocks continue; } @@ -99,15 +113,15 @@ class SplashPotion extends Throwable{ foreach($this->getPotionEffects() as $effect){ //getPotionEffects() is used to get COPIES to avoid accidentally modifying the same effect instance already applied to another entity - if(!$effect->getType()->isInstantEffect()){ + if(!($effect->getType() instanceof InstantEffect)){ $newDuration = (int) round($effect->getDuration() * 0.75 * $distanceMultiplier); if($newDuration < 20){ continue; } $effect->setDuration($newDuration); - $entity->addEffect($effect); + $entity->getEffects()->add($effect); }else{ - $effect->getType()->applyEffect($entity, $effect, $distanceMultiplier, $this, $this->getOwningEntity()); + $effect->getType()->applyEffect($entity, $effect, $distanceMultiplier, $this); } } } @@ -115,15 +129,15 @@ class SplashPotion extends Throwable{ }else{ //TODO: lingering potions } - }elseif($event instanceof ProjectileHitBlockEvent and $this->getPotionId() === Potion::WATER){ + }elseif($event instanceof ProjectileHitBlockEvent and $this->getPotionType()->equals(PotionType::WATER())){ $blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace()); - if($blockIn->getId() === Block::FIRE){ - $this->level->setBlock($blockIn, BlockFactory::get(Block::AIR)); + if($blockIn->getId() === BlockLegacyIds::FIRE){ + $this->getWorld()->setBlock($blockIn->getPosition(), VanillaBlocks::AIR()); } foreach($blockIn->getHorizontalSides() as $horizontalSide){ - if($horizontalSide->getId() === Block::FIRE){ - $this->level->setBlock($horizontalSide, BlockFactory::get(Block::AIR)); + if($horizontalSide->getId() === BlockLegacyIds::FIRE){ + $this->getWorld()->setBlock($horizontalSide->getPosition(), VanillaBlocks::AIR()); } } } @@ -132,32 +146,41 @@ class SplashPotion extends Throwable{ /** * Returns the meta value of the potion item that this splash potion corresponds to. This decides what effects will be applied to the entity when it collides with its target. */ - public function getPotionId() : int{ - return $this->propertyManager->getShort(self::DATA_POTION_AUX_VALUE) ?? 0; + public function getPotionType() : PotionType{ + return $this->potionType; } - public function setPotionId(int $id) : void{ - $this->propertyManager->setShort(self::DATA_POTION_AUX_VALUE, $id); + public function setPotionType(PotionType $type) : void{ + $this->potionType = $type; + $this->networkPropertiesDirty = true; } /** * Returns whether this splash potion will create an area-effect cloud when it lands. */ public function willLinger() : bool{ - return $this->getDataFlag(self::DATA_FLAGS, self::DATA_FLAG_LINGER); + return $this->linger; } /** * Sets whether this splash potion will create an area-effect-cloud when it lands. */ public function setLinger(bool $value = true) : void{ - $this->setDataFlag(self::DATA_FLAGS, self::DATA_FLAG_LINGER, $value); + $this->linger = $value; + $this->networkPropertiesDirty = true; } /** * @return EffectInstance[] */ public function getPotionEffects() : array{ - return Potion::getPotionEffectsById($this->getPotionId()); + return $this->potionType->getEffects(); + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setShort(EntityMetadataProperties::POTION_AUX_VALUE, PotionTypeIdMap::getInstance()->toId($this->potionType)); + $properties->setGenericFlag(EntityMetadataFlags::LINGER, $this->linger); } } diff --git a/src/pocketmine/entity/projectile/Throwable.php b/src/entity/projectile/Throwable.php similarity index 88% rename from src/pocketmine/entity/projectile/Throwable.php rename to src/entity/projectile/Throwable.php index 42086aae47..20bf86912a 100644 --- a/src/pocketmine/entity/projectile/Throwable.php +++ b/src/entity/projectile/Throwable.php @@ -24,16 +24,16 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\block\Block; +use pocketmine\entity\EntitySizeInfo; use pocketmine\math\RayTraceResult; abstract class Throwable extends Projectile{ - public $width = 0.25; - public $height = 0.25; - protected $gravity = 0.03; protected $drag = 0.01; + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } + protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ parent::onHitBlock($blockHit, $hitResult); $this->flagForDespawn(); diff --git a/src/pocketmine/entity/utils/ExperienceUtils.php b/src/entity/utils/ExperienceUtils.php similarity index 100% rename from src/pocketmine/entity/utils/ExperienceUtils.php rename to src/entity/utils/ExperienceUtils.php diff --git a/src/pocketmine/inventory/InventoryEventProcessor.php b/src/event/Cancellable.php similarity index 51% rename from src/pocketmine/inventory/InventoryEventProcessor.php rename to src/event/Cancellable.php index da7a650d3c..e01d0b52d6 100644 --- a/src/pocketmine/inventory/InventoryEventProcessor.php +++ b/src/event/Cancellable.php @@ -21,23 +21,21 @@ declare(strict_types=1); -namespace pocketmine\inventory; - -use pocketmine\item\Item; +namespace pocketmine\event; /** - * This interface can be used to listen for events on a specific Inventory. + * This interface is implemented by an Event subclass if and only if it can be cancelled. * - * If you want to listen to changes on an inventory, create a class implementing this interface and implement its - * methods, then register it onto the inventory or inventories that you want to receive events for. + * The cancellation of an event directly affects whether downstream event handlers + * without `@handleCancelled` will be called with this event. + * Implementations may provide a direct setter for cancellation (typically by using `CancellableTrait`) + * or implement an alternative logic (such as a function on another data field) for `isCancelled()`. */ -interface InventoryEventProcessor{ - +interface Cancellable{ /** - * Called prior to a slot in the given inventory changing. This is called by inventories that this listener is - * attached to. + * Returns whether this instance of the event is currently cancelled. * - * @return Item|null that should be used in place of $newItem, or null if the slot change should not proceed. + * If it is cancelled, only downstream handlers that declare `@handleCancelled` will be called with this event. */ - public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem, Item $newItem) : ?Item; + public function isCancelled() : bool; } diff --git a/src/pocketmine/network/mcpe/protocol/SetCommandsEnabledPacket.php b/src/event/CancellableTrait.php similarity index 56% rename from src/pocketmine/network/mcpe/protocol/SetCommandsEnabledPacket.php rename to src/event/CancellableTrait.php index f1fa52c95f..313c499288 100644 --- a/src/pocketmine/network/mcpe/protocol/SetCommandsEnabledPacket.php +++ b/src/event/CancellableTrait.php @@ -21,27 +21,29 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; - -#include - -use pocketmine\network\mcpe\NetworkSession; - -class SetCommandsEnabledPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_COMMANDS_ENABLED_PACKET; +namespace pocketmine\event; +/** + * This trait provides a basic boolean-setter-style implementation for `Cancellable` to reduce boilerplate. + * The precise meaning of `setCancelled` is subject to definition by the class using this trait. + * + * Implementors of `Cancellable` are not required to use this trait. + * + * @see Cancellable + */ +trait CancellableTrait{ /** @var bool */ - public $enabled; + private $isCancelled = false; - protected function decodePayload(){ - $this->enabled = $this->getBool(); + public function isCancelled() : bool{ + return $this->isCancelled; } - protected function encodePayload(){ - $this->putBool($this->enabled); + public function cancel() : void{ + $this->isCancelled = true; } - public function handle(NetworkSession $session) : bool{ - return $session->handleSetCommandsEnabled($this); + public function uncancel() : void{ + $this->isCancelled = false; } } diff --git a/src/pocketmine/event/Event.php b/src/event/Event.php similarity index 72% rename from src/pocketmine/event/Event.php rename to src/event/Event.php index 1ed0f4f253..b9a8f9828a 100644 --- a/src/pocketmine/event/Event.php +++ b/src/event/Event.php @@ -26,7 +26,6 @@ declare(strict_types=1); */ namespace pocketmine\event; -use function assert; use function get_class; abstract class Event{ @@ -36,35 +35,11 @@ abstract class Event{ /** @var string|null */ protected $eventName = null; - /** @var bool */ - private $isCancelled = false; final public function getEventName() : string{ return $this->eventName ?? get_class($this); } - /** - * @throws \BadMethodCallException - */ - public function isCancelled() : bool{ - if(!($this instanceof Cancellable)){ - throw new \BadMethodCallException(get_class($this) . " is not Cancellable"); - } - - return $this->isCancelled; - } - - /** - * @throws \BadMethodCallException - */ - public function setCancelled(bool $value = true) : void{ - if(!($this instanceof Cancellable)){ - throw new \BadMethodCallException(get_class($this) . " is not Cancellable"); - } - - $this->isCancelled = $value; - } - /** * Calls event handlers registered for this event. * @@ -76,8 +51,7 @@ abstract class Event{ throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)"); } - $handlerList = HandlerList::getHandlerListFor(get_class($this)); - assert($handlerList !== null, "Called event should have a valid HandlerList"); + $handlerList = HandlerListManager::global()->getListFor(get_class($this)); ++self::$eventCallDepth; try{ diff --git a/src/pocketmine/event/EventPriority.php b/src/event/EventPriority.php similarity index 86% rename from src/pocketmine/event/EventPriority.php rename to src/event/EventPriority.php index cdc63e8b33..1ef6e9da37 100644 --- a/src/pocketmine/event/EventPriority.php +++ b/src/event/EventPriority.php @@ -23,8 +23,6 @@ declare(strict_types=1); namespace pocketmine\event; -use function constant; -use function defined; use function mb_strtoupper; /** @@ -35,7 +33,12 @@ use function mb_strtoupper; * * MONITOR events should not change the event outcome or contents */ -abstract class EventPriority{ +final class EventPriority{ + + private function __construct(){ + //NOOP + } + public const ALL = [ self::LOWEST, self::LOW, @@ -79,10 +82,16 @@ abstract class EventPriority{ * @throws \InvalidArgumentException */ public static function fromString(string $name) : int{ - $name = mb_strtoupper($name); - $const = self::class . "::" . $name; - if($name !== "ALL" and defined($const)){ - return constant($const); + $value = [ + "LOWEST" => self::LOWEST, + "LOW" => self::LOW, + "NORMAL" => self::NORMAL, + "HIGH" => self::HIGH, + "HIGHEST" => self::HIGHEST, + "MONITOR" => self::MONITOR + ][mb_strtoupper($name)] ?? null; + if($value !== null){ + return $value; } throw new \InvalidArgumentException("Unable to resolve priority \"$name\""); diff --git a/src/pocketmine/event/HandlerList.php b/src/event/HandlerList.php similarity index 51% rename from src/pocketmine/event/HandlerList.php rename to src/event/HandlerList.php index abfd41b1cc..b135a53cdb 100644 --- a/src/pocketmine/event/HandlerList.php +++ b/src/event/HandlerList.php @@ -24,72 +24,10 @@ declare(strict_types=1); namespace pocketmine\event; use pocketmine\plugin\Plugin; -use pocketmine\plugin\RegisteredListener; -use pocketmine\utils\Utils; use function array_fill_keys; -use function in_array; -use function spl_object_hash; +use function spl_object_id; class HandlerList{ - /** @var HandlerList[] classname => HandlerList */ - private static $allLists = []; - - /** - * Unregisters all the listeners - * If a Plugin or Listener is passed, all the listeners with that object will be removed - * - * @param Plugin|Listener|null $object - */ - public static function unregisterAll($object = null) : void{ - if($object instanceof Listener or $object instanceof Plugin){ - foreach(self::$allLists as $h){ - $h->unregister($object); - } - }else{ - foreach(self::$allLists as $h){ - foreach($h->handlerSlots as $key => $list){ - $h->handlerSlots[$key] = []; - } - } - } - } - - /** - * Returns the HandlerList for listeners that explicitly handle this event. - * - * Calling this method also lazily initializes the $classMap inheritance tree of handler lists. - * - * @throws \ReflectionException - */ - public static function getHandlerListFor(string $event) : ?HandlerList{ - if(isset(self::$allLists[$event])){ - return self::$allLists[$event]; - } - - $class = new \ReflectionClass($event); - $tags = Utils::parseDocComment((string) $class->getDocComment()); - - if($class->isAbstract() && !isset($tags["allowHandle"])){ - return null; - } - - $super = $class; - $parentList = null; - while($parentList === null && ($super = $super->getParentClass()) !== false){ - // skip $noHandle events in the inheritance tree to go to the nearest ancestor - // while loop to allow skipping $noHandle events in the inheritance tree - $parentList = self::getHandlerListFor($super->getName()); - } - - return new HandlerList($event, $parentList); - } - - /** - * @return HandlerList[] - */ - public static function getHandlerLists() : array{ - return self::$allLists; - } /** @var string */ private $class; @@ -102,20 +40,16 @@ class HandlerList{ $this->class = $class; $this->handlerSlots = array_fill_keys(EventPriority::ALL, []); $this->parentList = $parentList; - self::$allLists[$this->class] = $this; } /** * @throws \Exception */ public function register(RegisteredListener $listener) : void{ - if(!in_array($listener->getPriority(), EventPriority::ALL, true)){ - return; + if(isset($this->handlerSlots[$listener->getPriority()][spl_object_id($listener)])){ + throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}"); } - if(isset($this->handlerSlots[$listener->getPriority()][spl_object_hash($listener)])){ - throw new \InvalidStateException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}"); - } - $this->handlerSlots[$listener->getPriority()][spl_object_hash($listener)] = $listener; + $this->handlerSlots[$listener->getPriority()][spl_object_id($listener)] = $listener; } /** @@ -135,19 +69,23 @@ class HandlerList{ foreach($this->handlerSlots as $priority => $list){ foreach($list as $hash => $listener){ if(($object instanceof Plugin and $listener->getPlugin() === $object) - or ($object instanceof Listener and $listener->getListener() === $object) + or ($object instanceof Listener and (new \ReflectionFunction($listener->getHandler()))->getClosureThis() === $object) //this doesn't even need to be a listener :D ){ unset($this->handlerSlots[$priority][$hash]); } } } }elseif($object instanceof RegisteredListener){ - if(isset($this->handlerSlots[$object->getPriority()][spl_object_hash($object)])){ - unset($this->handlerSlots[$object->getPriority()][spl_object_hash($object)]); + if(isset($this->handlerSlots[$object->getPriority()][spl_object_id($object)])){ + unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]); } } } + public function clear() : void{ + $this->handlerSlots = array_fill_keys(EventPriority::ALL, []); + } + /** * @return RegisteredListener[] */ diff --git a/src/event/HandlerListManager.php b/src/event/HandlerListManager.php new file mode 100644 index 0000000000..0859b1e5ec --- /dev/null +++ b/src/event/HandlerListManager.php @@ -0,0 +1,113 @@ + HandlerList */ + private $allLists = []; + + /** + * Unregisters all the listeners + * If a Plugin or Listener is passed, all the listeners with that object will be removed + * + * @param Plugin|Listener|RegisteredListener|null $object + */ + public function unregisterAll($object = null) : void{ + if($object instanceof Listener or $object instanceof Plugin or $object instanceof RegisteredListener){ + foreach($this->allLists as $h){ + $h->unregister($object); + } + }else{ + foreach($this->allLists as $h){ + $h->clear(); + } + } + } + + /** + * @phpstan-param \ReflectionClass $class + */ + private static function isValidClass(\ReflectionClass $class) : bool{ + $tags = Utils::parseDocComment((string) $class->getDocComment()); + return !$class->isAbstract() || isset($tags["allowHandle"]); + } + + /** + * @phpstan-param \ReflectionClass $class + * + * @phpstan-return \ReflectionClass|null + */ + private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{ + for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){ + if(self::isValidClass($parent)){ + return $parent; + } + //NOOP + } + return null; + } + + /** + * Returns the HandlerList for listeners that explicitly handle this event. + * + * Calling this method also lazily initializes the $classMap inheritance tree of handler lists. + * + * @phpstan-template TEvent of Event + * @phpstan-param class-string $event + * + * @throws \ReflectionException + * @throws \InvalidArgumentException + */ + public function getListFor(string $event) : HandlerList{ + if(isset($this->allLists[$event])){ + return $this->allLists[$event]; + } + + $class = new \ReflectionClass($event); + if(!self::isValidClass($class)){ + throw new \InvalidArgumentException("Event must be non-abstract or have the @allowHandle annotation"); + } + + $parent = self::resolveNearestHandleableParent($class); + return $this->allLists[$event] = new HandlerList($event, $parent !== null ? $this->getListFor($parent->getName()) : null); + } + + /** + * @return HandlerList[] + */ + public function getAll() : array{ + return $this->allLists; + } +} diff --git a/src/pocketmine/event/Listener.php b/src/event/Listener.php similarity index 87% rename from src/pocketmine/event/Listener.php rename to src/event/Listener.php index 0a3f84f508..e39d0dcd99 100644 --- a/src/pocketmine/event/Listener.php +++ b/src/event/Listener.php @@ -43,10 +43,7 @@ use pocketmine\plugin\PluginManager; * Functions which meet the criteria can have the following annotations in their doc comments: * * - `@notHandler`: Marks a function as NOT being an event handler. Only needed if the function meets the above criteria. - * - `@softDepend [PluginName]`: Handler WILL NOT be registered if its event doesn't exist. Useful for soft-depending - * on plugin events. Plugin name is optional. - * Example: `@softDepend SimpleAuth` - * - `@ignoreCancelled`: Cancelled events WILL NOT be passed to this handler. + * - `@handleCancelled`: Cancelled events will STILL invoke this handler. * - `@priority `: Sets the priority at which this event handler will receive events. * Example: `@priority HIGHEST` * @see EventPriority for a list of possible options. diff --git a/src/pocketmine/event/Cancellable.php b/src/event/ListenerMethodTags.php similarity index 74% rename from src/pocketmine/event/Cancellable.php rename to src/event/ListenerMethodTags.php index e284e739c8..ba72fc4cf5 100644 --- a/src/pocketmine/event/Cancellable.php +++ b/src/event/ListenerMethodTags.php @@ -24,13 +24,11 @@ declare(strict_types=1); namespace pocketmine\event; /** - * Events that can be cancelled must use the interface Cancellable + * Provides constants for all the PhpDoc tags supported for Listener methods. + * @see Listener */ -interface Cancellable{ - public function isCancelled() : bool; - - /** - * @return void - */ - public function setCancelled(bool $value = true); +final class ListenerMethodTags{ + public const HANDLE_CANCELLED = "handleCancelled"; + public const NOT_HANDLER = "notHandler"; + public const PRIORITY = "priority"; } diff --git a/src/pocketmine/plugin/RegisteredListener.php b/src/event/RegisteredListener.php similarity index 60% rename from src/pocketmine/plugin/RegisteredListener.php rename to src/event/RegisteredListener.php index 25c286bc1e..cc22a0511a 100644 --- a/src/pocketmine/plugin/RegisteredListener.php +++ b/src/event/RegisteredListener.php @@ -21,17 +21,16 @@ declare(strict_types=1); -namespace pocketmine\plugin; +namespace pocketmine\event; -use pocketmine\event\Cancellable; -use pocketmine\event\Event; -use pocketmine\event\Listener; +use pocketmine\plugin\Plugin; use pocketmine\timings\TimingsHandler; +use function in_array; class RegisteredListener{ - /** @var Listener */ - private $listener; + /** @var \Closure */ + private $handler; /** @var int */ private $priority; @@ -39,26 +38,25 @@ class RegisteredListener{ /** @var Plugin */ private $plugin; - /** @var EventExecutor */ - private $executor; - /** @var bool */ - private $ignoreCancelled; + private $handleCancelled; /** @var TimingsHandler */ private $timings; - public function __construct(Listener $listener, EventExecutor $executor, int $priority, Plugin $plugin, bool $ignoreCancelled, TimingsHandler $timings){ - $this->listener = $listener; + public function __construct(\Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled, TimingsHandler $timings){ + if(!in_array($priority, EventPriority::ALL, true)){ + throw new \InvalidArgumentException("Invalid priority number $priority"); + } + $this->handler = $handler; $this->priority = $priority; $this->plugin = $plugin; - $this->executor = $executor; - $this->ignoreCancelled = $ignoreCancelled; + $this->handleCancelled = $handleCancelled; $this->timings = $timings; } - public function getListener() : Listener{ - return $this->listener; + public function getHandler() : \Closure{ + return $this->handler; } public function getPlugin() : Plugin{ @@ -69,23 +67,16 @@ class RegisteredListener{ return $this->priority; } - /** - * @return void - */ - public function callEvent(Event $event){ - if($event instanceof Cancellable and $event->isCancelled() and $this->isIgnoringCancelled()){ + public function callEvent(Event $event) : void{ + if($event instanceof Cancellable and $event->isCancelled() and !$this->isHandlingCancelled()){ return; } $this->timings->startTiming(); - $this->executor->execute($this->listener, $event); + ($this->handler)($event); $this->timings->stopTiming(); } - public function __destruct(){ - $this->timings->remove(); - } - - public function isIgnoringCancelled() : bool{ - return $this->ignoreCancelled; + public function isHandlingCancelled() : bool{ + return $this->handleCancelled; } } diff --git a/src/pocketmine/event/block/BlockGrowEvent.php b/src/event/block/BaseBlockChangeEvent.php similarity index 85% rename from src/pocketmine/event/block/BlockGrowEvent.php rename to src/event/block/BaseBlockChangeEvent.php index d6ff655d8b..1c01e629cf 100644 --- a/src/pocketmine/event/block/BlockGrowEvent.php +++ b/src/event/block/BaseBlockChangeEvent.php @@ -25,13 +25,15 @@ namespace pocketmine\event\block; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** - * Called when plants or crops grow. + * @internal */ -class BlockGrowEvent extends BlockEvent implements Cancellable{ - /** @var Block */ - private $newState; +abstract class BaseBlockChangeEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + + private Block $newState; public function __construct(Block $block, Block $newState){ parent::__construct($block); diff --git a/src/pocketmine/event/block/BlockBreakEvent.php b/src/event/block/BlockBreakEvent.php similarity index 96% rename from src/pocketmine/event/block/BlockBreakEvent.php rename to src/event/block/BlockBreakEvent.php index df821ee85a..2540ecec49 100644 --- a/src/pocketmine/event/block/BlockBreakEvent.php +++ b/src/event/block/BlockBreakEvent.php @@ -25,13 +25,16 @@ namespace pocketmine\event\block; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player destroys a block somewhere in the world. */ class BlockBreakEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + /** @var Player */ protected $player; diff --git a/src/pocketmine/event/block/BlockBurnEvent.php b/src/event/block/BlockBurnEvent.php similarity index 95% rename from src/pocketmine/event/block/BlockBurnEvent.php rename to src/event/block/BlockBurnEvent.php index 328e676691..96ddb62349 100644 --- a/src/pocketmine/event/block/BlockBurnEvent.php +++ b/src/event/block/BlockBurnEvent.php @@ -25,11 +25,14 @@ namespace pocketmine\event\block; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when a block is burned away by fire. */ class BlockBurnEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + /** @var Block */ private $causingBlock; diff --git a/src/pocketmine/event/block/BlockEvent.php b/src/event/block/BlockEvent.php similarity index 100% rename from src/pocketmine/event/block/BlockEvent.php rename to src/event/block/BlockEvent.php diff --git a/src/pocketmine/event/block/BlockFormEvent.php b/src/event/block/BlockFormEvent.php similarity index 78% rename from src/pocketmine/event/block/BlockFormEvent.php rename to src/event/block/BlockFormEvent.php index 0f1602d91f..f955319c0a 100644 --- a/src/pocketmine/event/block/BlockFormEvent.php +++ b/src/event/block/BlockFormEvent.php @@ -23,6 +23,10 @@ declare(strict_types=1); namespace pocketmine\event\block; -class BlockFormEvent extends BlockGrowEvent{ +/** + * Called when a new block forms, usually as the result of some action. + * This could be things like obsidian forming due to collision of lava and water. + */ +class BlockFormEvent extends BaseBlockChangeEvent{ } diff --git a/src/pocketmine/event/level/LevelUnloadEvent.php b/src/event/block/BlockGrowEvent.php similarity index 84% rename from src/pocketmine/event/level/LevelUnloadEvent.php rename to src/event/block/BlockGrowEvent.php index e5cc36e8af..09389acbc6 100644 --- a/src/pocketmine/event/level/LevelUnloadEvent.php +++ b/src/event/block/BlockGrowEvent.php @@ -21,13 +21,13 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\block; use pocketmine\event\Cancellable; /** - * Called when a Level is unloaded + * Called when plants or crops grow. */ -class LevelUnloadEvent extends LevelEvent implements Cancellable{ +class BlockGrowEvent extends BaseBlockChangeEvent implements Cancellable{ } diff --git a/src/event/block/BlockItemPickupEvent.php b/src/event/block/BlockItemPickupEvent.php new file mode 100644 index 0000000000..2428405386 --- /dev/null +++ b/src/event/block/BlockItemPickupEvent.php @@ -0,0 +1,60 @@ +origin; + } + + /** + * Items to be received + */ + public function getItem() : Item{ + return clone $this->item; + } + + /** + * Change the items to receive. + */ + public function setItem(Item $item) : void{ + $this->item = clone $item; + } + + /** + * Inventory to which received items will be added. + */ + public function getInventory() : ?Inventory{ + return $this->inventory; + } + + /** + * Change the inventory to which received items are added. + */ + public function setInventory(?Inventory $inventory) : void{ + $this->inventory = $inventory; + } +} \ No newline at end of file diff --git a/src/pocketmine/event/block/BlockPlaceEvent.php b/src/event/block/BlockPlaceEvent.php similarity index 95% rename from src/pocketmine/event/block/BlockPlaceEvent.php rename to src/event/block/BlockPlaceEvent.php index d619164e2d..955b901346 100644 --- a/src/pocketmine/event/block/BlockPlaceEvent.php +++ b/src/event/block/BlockPlaceEvent.php @@ -25,13 +25,16 @@ namespace pocketmine\event\block; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player places a block */ class BlockPlaceEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + /** @var Player */ protected $player; diff --git a/src/pocketmine/event/block/BlockSpreadEvent.php b/src/event/block/BlockSpreadEvent.php similarity index 95% rename from src/pocketmine/event/block/BlockSpreadEvent.php rename to src/event/block/BlockSpreadEvent.php index 098e2d6d91..bd2517d198 100644 --- a/src/pocketmine/event/block/BlockSpreadEvent.php +++ b/src/event/block/BlockSpreadEvent.php @@ -28,7 +28,7 @@ use pocketmine\block\Block; /** * Called when a block spreads to another block, such as grass spreading to nearby dirt blocks. */ -class BlockSpreadEvent extends BlockFormEvent{ +class BlockSpreadEvent extends BaseBlockChangeEvent{ /** @var Block */ private $source; diff --git a/src/pocketmine/level/generator/object/Pond.php b/src/event/block/BlockTeleportEvent.php similarity index 62% rename from src/pocketmine/level/generator/object/Pond.php rename to src/event/block/BlockTeleportEvent.php index f547239be4..3dcd041ae4 100644 --- a/src/pocketmine/level/generator/object/Pond.php +++ b/src/event/block/BlockTeleportEvent.php @@ -21,32 +21,29 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\event\block; use pocketmine\block\Block; -use pocketmine\level\ChunkManager; +use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\math\Vector3; -use pocketmine\utils\Random; -class Pond{ - /** @var Random */ - private $random; - /** @var Block */ - public $type; +class BlockTeleportEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; - public function __construct(Random $random, Block $type){ - $this->type = $type; - $this->random = $random; + /** @var Vector3 */ + private $to; + + public function __construct(Block $block, Vector3 $to){ + parent::__construct($block); + $this->to = $to; } - public function canPlaceObject(ChunkManager $level, Vector3 $pos) : bool{ - return false; + public function getTo() : Vector3{ + return $this->to; } - /** - * @return void - */ - public function placeObject(ChunkManager $level, Vector3 $pos){ - + public function setTo(Vector3 $to) : void{ + $this->to = $to; } } diff --git a/src/pocketmine/event/block/BlockUpdateEvent.php b/src/event/block/BlockUpdateEvent.php similarity index 93% rename from src/pocketmine/event/block/BlockUpdateEvent.php rename to src/event/block/BlockUpdateEvent.php index 8e31251537..0944afd9e4 100644 --- a/src/pocketmine/event/block/BlockUpdateEvent.php +++ b/src/event/block/BlockUpdateEvent.php @@ -24,10 +24,11 @@ declare(strict_types=1); namespace pocketmine\event\block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when a block tries to be updated due to a neighbor change */ class BlockUpdateEvent extends BlockEvent implements Cancellable{ - + use CancellableTrait; } diff --git a/src/pocketmine/event/block/LeavesDecayEvent.php b/src/event/block/LeavesDecayEvent.php similarity index 93% rename from src/pocketmine/event/block/LeavesDecayEvent.php rename to src/event/block/LeavesDecayEvent.php index 9fe15234a6..c68461b6f4 100644 --- a/src/pocketmine/event/block/LeavesDecayEvent.php +++ b/src/event/block/LeavesDecayEvent.php @@ -24,10 +24,11 @@ declare(strict_types=1); namespace pocketmine\event\block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when leaves decay due to not being attached to wood. */ class LeavesDecayEvent extends BlockEvent implements Cancellable{ - + use CancellableTrait; } diff --git a/src/event/block/SignChangeEvent.php b/src/event/block/SignChangeEvent.php new file mode 100644 index 0000000000..4864eb7522 --- /dev/null +++ b/src/event/block/SignChangeEvent.php @@ -0,0 +1,82 @@ +sign = $sign; + $this->player = $player; + $this->text = $text; + } + + public function getSign() : BaseSign{ + return $this->sign; + } + + public function getPlayer() : Player{ + return $this->player; + } + + /** + * Returns the text currently on the sign. + */ + public function getOldText() : SignText{ + return $this->sign->getText(); + } + + /** + * Returns the text which will be on the sign after the event. + */ + public function getNewText() : SignText{ + return $this->text; + } + + /** + * Sets the text to be written on the sign after the event. + */ + public function setNewText(SignText $text) : void{ + $this->text = $text; + } +} diff --git a/src/event/block/StructureGrowEvent.php b/src/event/block/StructureGrowEvent.php new file mode 100644 index 0000000000..30d7c7ceb1 --- /dev/null +++ b/src/event/block/StructureGrowEvent.php @@ -0,0 +1,40 @@ +transaction = $transaction; + $this->player = $player; + } + + public function getTransaction() : BlockTransaction{ + return $this->transaction; + } + + /** + * It returns the player which grows the structure. + * It returns null when the structure grows by itself. + */ + public function getPlayer() : ?Player{ + return $this->player; + } +} \ No newline at end of file diff --git a/src/pocketmine/event/entity/EntityBlockChangeEvent.php b/src/event/entity/EntityBlockChangeEvent.php similarity index 95% rename from src/pocketmine/event/entity/EntityBlockChangeEvent.php rename to src/event/entity/EntityBlockChangeEvent.php index 5cd485d05b..f73da94ecc 100644 --- a/src/pocketmine/event/entity/EntityBlockChangeEvent.php +++ b/src/event/entity/EntityBlockChangeEvent.php @@ -26,12 +26,15 @@ namespace pocketmine\event\entity; use pocketmine\block\Block; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when an Entity, excluding players, changes a block directly * @phpstan-extends EntityEvent */ class EntityBlockChangeEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Block */ private $from; /** @var Block */ diff --git a/src/pocketmine/event/entity/EntityCombustByBlockEvent.php b/src/event/entity/EntityCombustByBlockEvent.php similarity index 100% rename from src/pocketmine/event/entity/EntityCombustByBlockEvent.php rename to src/event/entity/EntityCombustByBlockEvent.php diff --git a/src/pocketmine/event/entity/EntityCombustByEntityEvent.php b/src/event/entity/EntityCombustByEntityEvent.php similarity index 100% rename from src/pocketmine/event/entity/EntityCombustByEntityEvent.php rename to src/event/entity/EntityCombustByEntityEvent.php diff --git a/src/pocketmine/event/entity/EntityCombustEvent.php b/src/event/entity/EntityCombustEvent.php similarity index 95% rename from src/pocketmine/event/entity/EntityCombustEvent.php rename to src/event/entity/EntityCombustEvent.php index e0a5902584..442c25f9b9 100644 --- a/src/pocketmine/event/entity/EntityCombustEvent.php +++ b/src/event/entity/EntityCombustEvent.php @@ -25,11 +25,14 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * @phpstan-extends EntityEvent */ class EntityCombustEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var int */ protected $duration; diff --git a/src/pocketmine/event/entity/EntityDamageByBlockEvent.php b/src/event/entity/EntityDamageByBlockEvent.php similarity index 100% rename from src/pocketmine/event/entity/EntityDamageByBlockEvent.php rename to src/event/entity/EntityDamageByBlockEvent.php diff --git a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php b/src/event/entity/EntityDamageByChildEntityEvent.php similarity index 93% rename from src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php rename to src/event/entity/EntityDamageByChildEntityEvent.php index 1ad1e5b0e9..e3e28ab0db 100644 --- a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php +++ b/src/event/entity/EntityDamageByChildEntityEvent.php @@ -44,6 +44,6 @@ class EntityDamageByChildEntityEvent extends EntityDamageByEntityEvent{ * Returns the entity which caused the damage, or null if the entity has been killed or closed. */ public function getChild() : ?Entity{ - return $this->getEntity()->getLevelNonNull()->getServer()->findEntity($this->childEntityEid); + return $this->getEntity()->getWorld()->getServer()->getWorldManager()->findEntity($this->childEntityEid); } } diff --git a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php b/src/event/entity/EntityDamageByEntityEvent.php similarity index 76% rename from src/pocketmine/event/entity/EntityDamageByEntityEvent.php rename to src/event/entity/EntityDamageByEntityEvent.php index 28241e6d2b..90d487c704 100644 --- a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php +++ b/src/event/entity/EntityDamageByEntityEvent.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\event\entity; -use pocketmine\entity\Effect; +use pocketmine\entity\effect\VanillaEffects; use pocketmine\entity\Entity; use pocketmine\entity\Living; @@ -48,12 +48,13 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{ protected function addAttackerModifiers(Entity $damager) : void{ if($damager instanceof Living){ //TODO: move this to entity classes - if($damager->hasEffect(Effect::STRENGTH)){ - $this->setModifier($this->getBaseDamage() * 0.3 * $damager->getEffect(Effect::STRENGTH)->getEffectLevel(), self::MODIFIER_STRENGTH); + $effects = $damager->getEffects(); + if(($strength = $effects->get(VanillaEffects::STRENGTH())) !== null){ + $this->setModifier($this->getBaseDamage() * 0.3 * $strength->getEffectLevel(), self::MODIFIER_STRENGTH); } - if($damager->hasEffect(Effect::WEAKNESS)){ - $this->setModifier(-($this->getBaseDamage() * 0.2 * $damager->getEffect(Effect::WEAKNESS)->getEffectLevel()), self::MODIFIER_WEAKNESS); + if(($weakness = $effects->get(VanillaEffects::WEAKNESS())) !== null){ + $this->setModifier(-($this->getBaseDamage() * 0.2 * $weakness->getEffectLevel()), self::MODIFIER_WEAKNESS); } } } @@ -62,7 +63,7 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{ * Returns the attacking entity, or null if the attacker has been killed or closed. */ public function getDamager() : ?Entity{ - return $this->getEntity()->getLevelNonNull()->getServer()->findEntity($this->damagerEntityId); + return $this->getEntity()->getWorld()->getServer()->getWorldManager()->findEntity($this->damagerEntityId); } public function getKnockBack() : float{ diff --git a/src/pocketmine/event/entity/EntityDamageEvent.php b/src/event/entity/EntityDamageEvent.php similarity index 98% rename from src/pocketmine/event/entity/EntityDamageEvent.php rename to src/event/entity/EntityDamageEvent.php index bfd94dc89f..79775e4891 100644 --- a/src/pocketmine/event/entity/EntityDamageEvent.php +++ b/src/event/entity/EntityDamageEvent.php @@ -25,6 +25,7 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use function array_sum; use function max; @@ -33,6 +34,8 @@ use function max; * @phpstan-extends EntityEvent */ class EntityDamageEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + public const MODIFIER_ARMOR = 1; public const MODIFIER_STRENGTH = 2; public const MODIFIER_WEAKNESS = 3; diff --git a/src/pocketmine/event/entity/EntityDeathEvent.php b/src/event/entity/EntityDeathEvent.php similarity index 100% rename from src/pocketmine/event/entity/EntityDeathEvent.php rename to src/event/entity/EntityDeathEvent.php diff --git a/src/event/entity/EntityDespawnEvent.php b/src/event/entity/EntityDespawnEvent.php new file mode 100644 index 0000000000..0fd0da8a0a --- /dev/null +++ b/src/event/entity/EntityDespawnEvent.php @@ -0,0 +1,37 @@ + + */ +class EntityDespawnEvent extends EntityEvent{ + + public function __construct(Entity $entity){ + $this->entity = $entity; + } +} diff --git a/src/pocketmine/event/entity/EntityEffectAddEvent.php b/src/event/entity/EntityEffectAddEvent.php similarity index 94% rename from src/pocketmine/event/entity/EntityEffectAddEvent.php rename to src/event/entity/EntityEffectAddEvent.php index ddc382a778..3061a399c4 100644 --- a/src/pocketmine/event/entity/EntityEffectAddEvent.php +++ b/src/event/entity/EntityEffectAddEvent.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\event\entity; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; use pocketmine\entity\Entity; /** @@ -33,7 +33,7 @@ class EntityEffectAddEvent extends EntityEffectEvent{ /** @var EffectInstance|null */ private $oldEffect; - public function __construct(Entity $entity, EffectInstance $effect, EffectInstance $oldEffect = null){ + public function __construct(Entity $entity, EffectInstance $effect, ?EffectInstance $oldEffect = null){ parent::__construct($entity, $effect); $this->oldEffect = $oldEffect; } diff --git a/src/pocketmine/event/entity/EntityEffectEvent.php b/src/event/entity/EntityEffectEvent.php similarity index 91% rename from src/pocketmine/event/entity/EntityEffectEvent.php rename to src/event/entity/EntityEffectEvent.php index 12671e646f..f93e06b72f 100644 --- a/src/pocketmine/event/entity/EntityEffectEvent.php +++ b/src/event/entity/EntityEffectEvent.php @@ -23,14 +23,17 @@ declare(strict_types=1); namespace pocketmine\event\entity; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * @phpstan-extends EntityEvent */ class EntityEffectEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var EffectInstance */ private $effect; diff --git a/src/pocketmine/event/entity/EntityEffectRemoveEvent.php b/src/event/entity/EntityEffectRemoveEvent.php similarity index 84% rename from src/pocketmine/event/entity/EntityEffectRemoveEvent.php rename to src/event/entity/EntityEffectRemoveEvent.php index 56099e958c..35e59023c4 100644 --- a/src/pocketmine/event/entity/EntityEffectRemoveEvent.php +++ b/src/event/entity/EntityEffectRemoveEvent.php @@ -27,10 +27,10 @@ namespace pocketmine\event\entity; * Called when an effect is removed from an entity. */ class EntityEffectRemoveEvent extends EntityEffectEvent{ - public function setCancelled(bool $value = true) : void{ + 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::setCancelled($value); + parent::cancel(); } } diff --git a/src/pocketmine/event/entity/EntityEvent.php b/src/event/entity/EntityEvent.php similarity index 100% rename from src/pocketmine/event/entity/EntityEvent.php rename to src/event/entity/EntityEvent.php diff --git a/src/pocketmine/event/entity/EntityExplodeEvent.php b/src/event/entity/EntityExplodeEvent.php similarity index 95% rename from src/pocketmine/event/entity/EntityExplodeEvent.php rename to src/event/entity/EntityExplodeEvent.php index 757a08118f..6daf923cc7 100644 --- a/src/pocketmine/event/entity/EntityExplodeEvent.php +++ b/src/event/entity/EntityExplodeEvent.php @@ -26,14 +26,17 @@ namespace pocketmine\event\entity; use pocketmine\block\Block; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; -use pocketmine\level\Position; +use pocketmine\event\CancellableTrait; use pocketmine\utils\Utils; +use pocketmine\world\Position; /** * Called when a entity explodes * @phpstan-extends EntityEvent */ class EntityExplodeEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Position */ protected $position; diff --git a/src/pocketmine/event/entity/EntityInventoryChangeEvent.php b/src/event/entity/EntityItemPickupEvent.php similarity index 50% rename from src/pocketmine/event/entity/EntityInventoryChangeEvent.php rename to src/event/entity/EntityItemPickupEvent.php index 1ef3b5ffd3..2e00f8570e 100644 --- a/src/pocketmine/event/entity/EntityInventoryChangeEvent.php +++ b/src/event/entity/EntityItemPickupEvent.php @@ -25,49 +25,56 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; +use pocketmine\inventory\Inventory; use pocketmine\item\Item; /** - * Called before a slot in an entity's inventory changes. + * Called when an entity picks up an item, arrow, etc. * @phpstan-extends EntityEvent */ -class EntityInventoryChangeEvent extends EntityEvent implements Cancellable{ - /** @var Item */ - private $oldItem; - /** @var Item */ - private $newItem; - /** @var int */ - private $slot; +class EntityItemPickupEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; - public function __construct(Entity $entity, Item $oldItem, Item $newItem, int $slot){ - $this->entity = $entity; - $this->oldItem = $oldItem; - $this->newItem = $newItem; - $this->slot = $slot; + public function __construct( + Entity $collector, + private Entity $origin, + private Item $item, + private ?Inventory $inventory + ){ + $this->entity = $collector; + } + + public function getOrigin() : Entity{ + return $this->origin; } /** - * Returns the inventory slot number affected by the event. + * Items to be received */ - public function getSlot() : int{ - return $this->slot; + public function getItem() : Item{ + return clone $this->item; } /** - * Returns the item which will be in the slot after the event. + * Change the items to receive. */ - public function getNewItem() : Item{ - return $this->newItem; - } - - public function setNewItem(Item $item) : void{ - $this->newItem = $item; + public function setItem(Item $item) : void{ + $this->item = clone $item; } /** - * Returns the item currently in the slot. + * Inventory to which received items will be added. */ - public function getOldItem() : Item{ - return $this->oldItem; + public function getInventory() : ?Inventory{ + return $this->inventory; } + + /** + * Change the inventory to which received items are added. + */ + public function setInventory(?Inventory $inventory) : void{ + $this->inventory = $inventory; + } + } diff --git a/src/pocketmine/event/entity/EntityMotionEvent.php b/src/event/entity/EntityMotionEvent.php similarity index 94% rename from src/pocketmine/event/entity/EntityMotionEvent.php rename to src/event/entity/EntityMotionEvent.php index fce46493cd..dc90e2b96d 100644 --- a/src/pocketmine/event/entity/EntityMotionEvent.php +++ b/src/event/entity/EntityMotionEvent.php @@ -25,12 +25,15 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\math\Vector3; /** * @phpstan-extends EntityEvent */ class EntityMotionEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Vector3 */ private $mot; diff --git a/src/pocketmine/event/entity/EntityRegainHealthEvent.php b/src/event/entity/EntityRegainHealthEvent.php similarity index 96% rename from src/pocketmine/event/entity/EntityRegainHealthEvent.php rename to src/event/entity/EntityRegainHealthEvent.php index 213ae5de81..cb37ee6124 100644 --- a/src/pocketmine/event/entity/EntityRegainHealthEvent.php +++ b/src/event/entity/EntityRegainHealthEvent.php @@ -25,11 +25,14 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * @phpstan-extends EntityEvent */ class EntityRegainHealthEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + public const CAUSE_REGEN = 0; public const CAUSE_EATING = 1; public const CAUSE_MAGIC = 2; diff --git a/src/pocketmine/event/entity/EntityShootBowEvent.php b/src/event/entity/EntityShootBowEvent.php similarity index 97% rename from src/pocketmine/event/entity/EntityShootBowEvent.php rename to src/event/entity/EntityShootBowEvent.php index cf2775bb0d..32f4f4d70d 100644 --- a/src/pocketmine/event/entity/EntityShootBowEvent.php +++ b/src/event/entity/EntityShootBowEvent.php @@ -27,6 +27,7 @@ use pocketmine\entity\Entity; use pocketmine\entity\Living; use pocketmine\entity\projectile\Projectile; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; use function count; @@ -34,6 +35,8 @@ use function count; * @phpstan-extends EntityEvent */ class EntityShootBowEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Item */ private $bow; /** @var Projectile */ diff --git a/src/pocketmine/event/player/cheat/PlayerCheatEvent.php b/src/event/entity/EntitySpawnEvent.php similarity index 74% rename from src/pocketmine/event/player/cheat/PlayerCheatEvent.php rename to src/event/entity/EntitySpawnEvent.php index 508c24e124..89e660a42b 100644 --- a/src/pocketmine/event/player/cheat/PlayerCheatEvent.php +++ b/src/event/entity/EntitySpawnEvent.php @@ -21,16 +21,17 @@ declare(strict_types=1); -/** - * Events called when the server detected that a player is cheating - */ -namespace pocketmine\event\player\cheat; +namespace pocketmine\event\entity; -use pocketmine\event\player\PlayerEvent; +use pocketmine\entity\Entity; /** - * @allowHandle + * Called when a entity is spawned + * @phpstan-extends EntityEvent */ -abstract class PlayerCheatEvent extends PlayerEvent{ +class EntitySpawnEvent extends EntityEvent{ + public function __construct(Entity $entity){ + $this->entity = $entity; + } } diff --git a/src/pocketmine/event/entity/EntityTeleportEvent.php b/src/event/entity/EntityTeleportEvent.php similarity index 93% rename from src/pocketmine/event/entity/EntityTeleportEvent.php rename to src/event/entity/EntityTeleportEvent.php index 6ed4c517b0..33f1d8de6a 100644 --- a/src/pocketmine/event/entity/EntityTeleportEvent.php +++ b/src/event/entity/EntityTeleportEvent.php @@ -25,12 +25,15 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; -use pocketmine\level\Position; +use pocketmine\event\CancellableTrait; +use pocketmine\world\Position; /** * @phpstan-extends EntityEvent */ class EntityTeleportEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Position */ private $from; /** @var Position */ diff --git a/src/event/entity/EntityTrampleFarmlandEvent.php b/src/event/entity/EntityTrampleFarmlandEvent.php new file mode 100644 index 0000000000..feea85bfe6 --- /dev/null +++ b/src/event/entity/EntityTrampleFarmlandEvent.php @@ -0,0 +1,48 @@ + + */ +class EntityTrampleFarmlandEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + + /** @var Block */ + private $block; + + public function __construct(Living $entity, Block $block){ + $this->entity = $entity; + $this->block = $block; + } + + public function getBlock() : Block{ + return $this->block; + } +} diff --git a/src/pocketmine/event/entity/ExplosionPrimeEvent.php b/src/event/entity/ExplosionPrimeEvent.php similarity index 96% rename from src/pocketmine/event/entity/ExplosionPrimeEvent.php rename to src/event/entity/ExplosionPrimeEvent.php index 145afb9cb7..68bec6fa4b 100644 --- a/src/pocketmine/event/entity/ExplosionPrimeEvent.php +++ b/src/event/entity/ExplosionPrimeEvent.php @@ -25,12 +25,15 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when a entity decides to explode * @phpstan-extends EntityEvent */ class ExplosionPrimeEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var float */ protected $force; /** @var bool */ diff --git a/src/pocketmine/event/entity/ItemDespawnEvent.php b/src/event/entity/ItemDespawnEvent.php similarity index 94% rename from src/pocketmine/event/entity/ItemDespawnEvent.php rename to src/event/entity/ItemDespawnEvent.php index 8bcf851f76..6942fcf1bd 100644 --- a/src/pocketmine/event/entity/ItemDespawnEvent.php +++ b/src/event/entity/ItemDespawnEvent.php @@ -25,15 +25,16 @@ namespace pocketmine\event\entity; use pocketmine\entity\object\ItemEntity; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * @phpstan-extends EntityEvent */ class ItemDespawnEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; public function __construct(ItemEntity $item){ $this->entity = $item; - } /** diff --git a/src/pocketmine/event/entity/ItemSpawnEvent.php b/src/event/entity/ItemSpawnEvent.php similarity index 100% rename from src/pocketmine/event/entity/ItemSpawnEvent.php rename to src/event/entity/ItemSpawnEvent.php diff --git a/src/pocketmine/event/entity/ProjectileHitBlockEvent.php b/src/event/entity/ProjectileHitBlockEvent.php similarity index 100% rename from src/pocketmine/event/entity/ProjectileHitBlockEvent.php rename to src/event/entity/ProjectileHitBlockEvent.php diff --git a/src/pocketmine/event/entity/ProjectileHitEntityEvent.php b/src/event/entity/ProjectileHitEntityEvent.php similarity index 100% rename from src/pocketmine/event/entity/ProjectileHitEntityEvent.php rename to src/event/entity/ProjectileHitEntityEvent.php diff --git a/src/pocketmine/event/entity/ProjectileHitEvent.php b/src/event/entity/ProjectileHitEvent.php similarity index 100% rename from src/pocketmine/event/entity/ProjectileHitEvent.php rename to src/event/entity/ProjectileHitEvent.php diff --git a/src/pocketmine/event/entity/ProjectileLaunchEvent.php b/src/event/entity/ProjectileLaunchEvent.php similarity index 94% rename from src/pocketmine/event/entity/ProjectileLaunchEvent.php rename to src/event/entity/ProjectileLaunchEvent.php index 6c20832eda..9ccbaa0091 100644 --- a/src/pocketmine/event/entity/ProjectileLaunchEvent.php +++ b/src/event/entity/ProjectileLaunchEvent.php @@ -25,14 +25,16 @@ namespace pocketmine\event\entity; use pocketmine\entity\projectile\Projectile; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * @phpstan-extends EntityEvent */ class ProjectileLaunchEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + public function __construct(Projectile $entity){ $this->entity = $entity; - } /** diff --git a/src/pocketmine/event/inventory/CraftItemEvent.php b/src/event/inventory/CraftItemEvent.php similarity index 95% rename from src/pocketmine/event/inventory/CraftItemEvent.php rename to src/event/inventory/CraftItemEvent.php index 263cb5920e..63773158a4 100644 --- a/src/pocketmine/event/inventory/CraftItemEvent.php +++ b/src/event/inventory/CraftItemEvent.php @@ -23,14 +23,17 @@ declare(strict_types=1); namespace pocketmine\event\inventory; +use pocketmine\crafting\CraftingRecipe; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\event\Event; -use pocketmine\inventory\CraftingRecipe; use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; class CraftItemEvent extends Event implements Cancellable{ + use CancellableTrait; + /** @var CraftingTransaction */ private $transaction; /** @var CraftingRecipe */ diff --git a/src/pocketmine/event/inventory/FurnaceBurnEvent.php b/src/event/inventory/FurnaceBurnEvent.php similarity index 95% rename from src/pocketmine/event/inventory/FurnaceBurnEvent.php rename to src/event/inventory/FurnaceBurnEvent.php index 8820466318..d613c5d9ed 100644 --- a/src/pocketmine/event/inventory/FurnaceBurnEvent.php +++ b/src/event/inventory/FurnaceBurnEvent.php @@ -23,15 +23,18 @@ declare(strict_types=1); namespace pocketmine\event\inventory; +use pocketmine\block\tile\Furnace; use pocketmine\event\block\BlockEvent; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\tile\Furnace; /** * Called when a furnace is about to consume a new fuel item. */ class FurnaceBurnEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + /** @var Furnace */ private $furnace; /** @var Item */ diff --git a/src/pocketmine/event/inventory/FurnaceSmeltEvent.php b/src/event/inventory/FurnaceSmeltEvent.php similarity index 94% rename from src/pocketmine/event/inventory/FurnaceSmeltEvent.php rename to src/event/inventory/FurnaceSmeltEvent.php index 3b44455752..cb2b5d8284 100644 --- a/src/pocketmine/event/inventory/FurnaceSmeltEvent.php +++ b/src/event/inventory/FurnaceSmeltEvent.php @@ -23,12 +23,15 @@ declare(strict_types=1); namespace pocketmine\event\inventory; +use pocketmine\block\tile\Furnace; use pocketmine\event\block\BlockEvent; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\tile\Furnace; class FurnaceSmeltEvent extends BlockEvent implements Cancellable{ + use CancellableTrait; + /** @var Furnace */ private $furnace; /** @var Item */ diff --git a/src/pocketmine/event/inventory/InventoryCloseEvent.php b/src/event/inventory/InventoryCloseEvent.php similarity index 97% rename from src/pocketmine/event/inventory/InventoryCloseEvent.php rename to src/event/inventory/InventoryCloseEvent.php index 1a862c7184..1ea65234d6 100644 --- a/src/pocketmine/event/inventory/InventoryCloseEvent.php +++ b/src/event/inventory/InventoryCloseEvent.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\event\inventory; use pocketmine\inventory\Inventory; -use pocketmine\Player; +use pocketmine\player\Player; class InventoryCloseEvent extends InventoryEvent{ /** @var Player */ diff --git a/src/pocketmine/event/inventory/InventoryEvent.php b/src/event/inventory/InventoryEvent.php similarity index 96% rename from src/pocketmine/event/inventory/InventoryEvent.php rename to src/event/inventory/InventoryEvent.php index 7c00c35fe1..ac19d37055 100644 --- a/src/pocketmine/event/inventory/InventoryEvent.php +++ b/src/event/inventory/InventoryEvent.php @@ -26,9 +26,9 @@ declare(strict_types=1); */ namespace pocketmine\event\inventory; -use pocketmine\entity\Human; use pocketmine\event\Event; use pocketmine\inventory\Inventory; +use pocketmine\player\Player; abstract class InventoryEvent extends Event{ /** @var Inventory */ @@ -43,7 +43,7 @@ abstract class InventoryEvent extends Event{ } /** - * @return Human[] + * @return Player[] */ public function getViewers() : array{ return $this->inventory->getViewers(); diff --git a/src/pocketmine/event/inventory/InventoryOpenEvent.php b/src/event/inventory/InventoryOpenEvent.php similarity index 92% rename from src/pocketmine/event/inventory/InventoryOpenEvent.php rename to src/event/inventory/InventoryOpenEvent.php index c29ab45ff7..ee5c13eb53 100644 --- a/src/pocketmine/event/inventory/InventoryOpenEvent.php +++ b/src/event/inventory/InventoryOpenEvent.php @@ -24,10 +24,13 @@ declare(strict_types=1); namespace pocketmine\event\inventory; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\inventory\Inventory; -use pocketmine\Player; +use pocketmine\player\Player; class InventoryOpenEvent extends InventoryEvent implements Cancellable{ + use CancellableTrait; + /** @var Player */ private $who; diff --git a/src/pocketmine/event/inventory/InventoryTransactionEvent.php b/src/event/inventory/InventoryTransactionEvent.php similarity index 95% rename from src/pocketmine/event/inventory/InventoryTransactionEvent.php rename to src/event/inventory/InventoryTransactionEvent.php index bd3089fff2..7ec8c90234 100644 --- a/src/pocketmine/event/inventory/InventoryTransactionEvent.php +++ b/src/event/inventory/InventoryTransactionEvent.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\event\inventory; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\event\Event; use pocketmine\inventory\transaction\InventoryTransaction; @@ -32,6 +33,8 @@ use pocketmine\inventory\transaction\InventoryTransaction; * The source of this can be a Player, entities, mobs, or even hoppers in the future! */ class InventoryTransactionEvent extends Event implements Cancellable{ + use CancellableTrait; + /** @var InventoryTransaction */ private $transaction; diff --git a/src/pocketmine/event/player/PlayerBedEnterEvent.php b/src/event/player/PlayerBedEnterEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerBedEnterEvent.php rename to src/event/player/PlayerBedEnterEvent.php index f05c604cb7..a9e571c6fc 100644 --- a/src/pocketmine/event/player/PlayerBedEnterEvent.php +++ b/src/event/player/PlayerBedEnterEvent.php @@ -25,9 +25,12 @@ namespace pocketmine\event\player; use pocketmine\block\Block; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerBedEnterEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Block */ private $bed; diff --git a/src/pocketmine/event/player/PlayerBedLeaveEvent.php b/src/event/player/PlayerBedLeaveEvent.php similarity index 97% rename from src/pocketmine/event/player/PlayerBedLeaveEvent.php rename to src/event/player/PlayerBedLeaveEvent.php index e1038c970a..7efe026733 100644 --- a/src/pocketmine/event/player/PlayerBedLeaveEvent.php +++ b/src/event/player/PlayerBedLeaveEvent.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\block\Block; -use pocketmine\Player; +use pocketmine\player\Player; class PlayerBedLeaveEvent extends PlayerEvent{ /** @var Block */ diff --git a/src/pocketmine/event/player/PlayerBlockPickEvent.php b/src/event/player/PlayerBlockPickEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerBlockPickEvent.php rename to src/event/player/PlayerBlockPickEvent.php index f4b1b5d821..c365cda167 100644 --- a/src/pocketmine/event/player/PlayerBlockPickEvent.php +++ b/src/event/player/PlayerBlockPickEvent.php @@ -25,13 +25,16 @@ namespace pocketmine\event\player; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player middle-clicks on a block to get an item in creative mode. */ class PlayerBlockPickEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Block */ private $blockClicked; /** @var Item */ @@ -50,8 +53,4 @@ class PlayerBlockPickEvent extends PlayerEvent implements Cancellable{ public function getResultItem() : Item{ return $this->resultItem; } - - public function setResultItem(Item $item) : void{ - $this->resultItem = clone $item; - } } diff --git a/src/pocketmine/event/player/PlayerBucketEmptyEvent.php b/src/event/player/PlayerBucketEmptyEvent.php similarity index 100% rename from src/pocketmine/event/player/PlayerBucketEmptyEvent.php rename to src/event/player/PlayerBucketEmptyEvent.php diff --git a/src/pocketmine/event/player/PlayerBucketEvent.php b/src/event/player/PlayerBucketEvent.php similarity index 95% rename from src/pocketmine/event/player/PlayerBucketEvent.php rename to src/event/player/PlayerBucketEvent.php index 81329b348e..3cea822428 100644 --- a/src/pocketmine/event/player/PlayerBucketEvent.php +++ b/src/event/player/PlayerBucketEvent.php @@ -25,13 +25,16 @@ namespace pocketmine\event\player; use pocketmine\block\Block; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * @allowHandle */ abstract class PlayerBucketEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Block */ private $blockClicked; /** @var int */ diff --git a/src/pocketmine/event/player/PlayerBucketFillEvent.php b/src/event/player/PlayerBucketFillEvent.php similarity index 100% rename from src/pocketmine/event/player/PlayerBucketFillEvent.php rename to src/event/player/PlayerBucketFillEvent.php diff --git a/src/pocketmine/event/player/PlayerChangeSkinEvent.php b/src/event/player/PlayerChangeSkinEvent.php similarity index 94% rename from src/pocketmine/event/player/PlayerChangeSkinEvent.php rename to src/event/player/PlayerChangeSkinEvent.php index 0983c7a0f0..51fde74a3f 100644 --- a/src/pocketmine/event/player/PlayerChangeSkinEvent.php +++ b/src/event/player/PlayerChangeSkinEvent.php @@ -25,12 +25,15 @@ namespace pocketmine\event\player; use pocketmine\entity\Skin; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; /** * Called when a player changes their skin in-game. */ class PlayerChangeSkinEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Skin */ private $oldSkin; /** @var Skin */ @@ -54,7 +57,6 @@ class PlayerChangeSkinEvent extends PlayerEvent implements Cancellable{ * @throws \InvalidArgumentException if the specified skin is not valid */ public function setNewSkin(Skin $skin) : void{ - $skin->validate(); $this->newSkin = $skin; } } diff --git a/src/pocketmine/event/player/PlayerChatEvent.php b/src/event/player/PlayerChatEvent.php similarity index 78% rename from src/pocketmine/event/player/PlayerChatEvent.php rename to src/event/player/PlayerChatEvent.php index c425248df9..138410357e 100644 --- a/src/pocketmine/event/player/PlayerChatEvent.php +++ b/src/event/player/PlayerChatEvent.php @@ -25,16 +25,16 @@ namespace pocketmine\event\player; use pocketmine\command\CommandSender; use pocketmine\event\Cancellable; -use pocketmine\permission\PermissionManager; -use pocketmine\Player; -use pocketmine\Server; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; use pocketmine\utils\Utils; -use function spl_object_id; /** * Called when a player chats something */ class PlayerChatEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var string */ protected $message; @@ -47,21 +47,13 @@ class PlayerChatEvent extends PlayerEvent implements Cancellable{ /** * @param CommandSender[] $recipients */ - public function __construct(Player $player, string $message, string $format = "chat.type.text", array $recipients = null){ + public function __construct(Player $player, string $message, array $recipients, string $format = "chat.type.text"){ $this->player = $player; $this->message = $message; $this->format = $format; - if($recipients === null){ - foreach(PermissionManager::getInstance()->getPermissionSubscriptions(Server::BROADCAST_CHANNEL_USERS) as $permissible){ - if($permissible instanceof CommandSender){ - $this->recipients[spl_object_id($permissible)] = $permissible; - } - } - }else{ - $this->recipients = $recipients; - } + $this->recipients = $recipients; } public function getMessage() : string{ diff --git a/src/pocketmine/event/player/PlayerCommandPreprocessEvent.php b/src/event/player/PlayerCommandPreprocessEvent.php similarity index 94% rename from src/pocketmine/event/player/PlayerCommandPreprocessEvent.php rename to src/event/player/PlayerCommandPreprocessEvent.php index 8ad542ecb0..f1fae9665a 100644 --- a/src/pocketmine/event/player/PlayerCommandPreprocessEvent.php +++ b/src/event/player/PlayerCommandPreprocessEvent.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; /** * Called when a player runs a command or chats, early in the process @@ -35,6 +36,8 @@ use pocketmine\Player; * The message contains a slash at the start */ class PlayerCommandPreprocessEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var string */ protected $message; diff --git a/src/pocketmine/event/player/PlayerCreationEvent.php b/src/event/player/PlayerCreationEvent.php similarity index 57% rename from src/pocketmine/event/player/PlayerCreationEvent.php rename to src/event/player/PlayerCreationEvent.php index 55c284695a..013fcdb2aa 100644 --- a/src/pocketmine/event/player/PlayerCreationEvent.php +++ b/src/event/player/PlayerCreationEvent.php @@ -24,66 +24,44 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Event; -use pocketmine\network\SourceInterface; -use pocketmine\Player; +use pocketmine\network\mcpe\NetworkSession; +use pocketmine\player\Player; +use pocketmine\utils\Utils; use function is_a; /** * Allows the creation of players overriding the base Player class */ class PlayerCreationEvent extends Event{ - /** @var SourceInterface */ - private $interface; - /** @var string */ - private $address; - /** @var int */ - private $port; + + /** @var NetworkSession */ + private $session; /** * @var string * @phpstan-var class-string */ - private $baseClass; + private $baseClass = Player::class; /** * @var string * @phpstan-var class-string */ - private $playerClass; + private $playerClass = Player::class; - /** - * @param string $baseClass - * @param string $playerClass - * @phpstan-param class-string $baseClass - * @phpstan-param class-string $playerClass - */ - public function __construct(SourceInterface $interface, $baseClass, $playerClass, string $address, int $port){ - $this->interface = $interface; - $this->address = $address; - $this->port = $port; - - if(!is_a($baseClass, Player::class, true)){ - throw new \RuntimeException("Base class $baseClass must extend " . Player::class); - } - - $this->baseClass = $baseClass; - - if(!is_a($playerClass, Player::class, true)){ - throw new \RuntimeException("Class $playerClass must extend " . Player::class); - } - - $this->playerClass = $playerClass; + public function __construct(NetworkSession $session){ + $this->session = $session; } - public function getInterface() : SourceInterface{ - return $this->interface; + public function getNetworkSession() : NetworkSession{ + return $this->session; } public function getAddress() : string{ - return $this->address; + return $this->session->getIp(); } public function getPort() : int{ - return $this->port; + return $this->session->getPort(); } /** @@ -97,10 +75,8 @@ class PlayerCreationEvent extends Event{ /** * @param string $class * @phpstan-param class-string $class - * - * @return void */ - public function setBaseClass($class){ + public function setBaseClass($class) : void{ if(!is_a($class, $this->baseClass, true)){ throw new \RuntimeException("Base class $class must extend " . $this->baseClass); } @@ -119,14 +95,9 @@ class PlayerCreationEvent extends Event{ /** * @param string $class * @phpstan-param class-string $class - * - * @return void */ - public function setPlayerClass($class){ - if(!is_a($class, $this->baseClass, true)){ - throw new \RuntimeException("Class $class must extend " . $this->baseClass); - } - + public function setPlayerClass($class) : void{ + Utils::testValidInstance($class, $this->baseClass); $this->playerClass = $class; } } diff --git a/src/pocketmine/event/player/PlayerDataSaveEvent.php b/src/event/player/PlayerDataSaveEvent.php similarity index 80% rename from src/pocketmine/event/player/PlayerDataSaveEvent.php rename to src/event/player/PlayerDataSaveEvent.php index b142fe6d8b..70e7443044 100644 --- a/src/pocketmine/event/player/PlayerDataSaveEvent.php +++ b/src/event/player/PlayerDataSaveEvent.php @@ -24,23 +24,28 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\event\Event; -use pocketmine\IPlayer; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\Server; +use pocketmine\player\Player; /** * Called when a player's data is about to be saved to disk. */ class PlayerDataSaveEvent extends Event implements Cancellable{ + use CancellableTrait; + /** @var CompoundTag */ protected $data; /** @var string */ protected $playerName; + /** @var Player|null */ + private $player; - public function __construct(CompoundTag $nbt, string $playerName){ + public function __construct(CompoundTag $nbt, string $playerName, ?Player $player){ $this->data = $nbt; $this->playerName = $playerName; + $this->player = $player; } /** @@ -62,10 +67,10 @@ class PlayerDataSaveEvent extends Event implements Cancellable{ } /** - * Returns the player whose data is being saved. This may be a Player or an OfflinePlayer. - * @return IPlayer (Player or OfflinePlayer) + * Returns the player whose data is being saved, if online. + * If null, this data is for an offline player (possibly just disconnected). */ - public function getPlayer() : IPlayer{ - return Server::getInstance()->getOfflinePlayer($this->playerName); + public function getPlayer() : ?Player{ + return $this->player; } } diff --git a/src/pocketmine/event/player/PlayerDeathEvent.php b/src/event/player/PlayerDeathEvent.php similarity index 59% rename from src/pocketmine/event/player/PlayerDeathEvent.php rename to src/event/player/PlayerDeathEvent.php index ba48894aa4..6c44d38197 100644 --- a/src/pocketmine/event/player/PlayerDeathEvent.php +++ b/src/event/player/PlayerDeathEvent.php @@ -23,31 +23,31 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\block\Block; +use pocketmine\block\BlockLegacyIds; use pocketmine\entity\Living; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDeathEvent; use pocketmine\item\Item; -use pocketmine\lang\TextContainer; -use pocketmine\lang\TranslationContainer; -use pocketmine\Player; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\lang\Translatable; +use pocketmine\player\Player; class PlayerDeathEvent extends EntityDeathEvent{ /** @var Player */ protected $player; - /** @var TextContainer|string */ + /** @var Translatable|string */ private $deathMessage; /** @var bool */ private $keepInventory = false; /** - * @param Item[] $drops - * @param string|TextContainer|null $deathMessage Null will cause the default vanilla message to be used + * @param Item[] $drops + * @param string|Translatable|null $deathMessage Null will cause the default vanilla message to be used */ - public function __construct(Player $entity, array $drops, $deathMessage = null, int $xp = 0){ + public function __construct(Player $entity, array $drops, int $xp, Translatable|string|null $deathMessage){ parent::__construct($entity, $drops, $xp); $this->player = $entity; $this->deathMessage = $deathMessage ?? self::deriveMessage($entity->getDisplayName(), $entity->getLastDamageCause()); @@ -64,17 +64,11 @@ class PlayerDeathEvent extends EntityDeathEvent{ return $this->player; } - /** - * @return TextContainer|string - */ - public function getDeathMessage(){ + public function getDeathMessage() : Translatable|string{ return $this->deathMessage; } - /** - * @param TextContainer|string $deathMessage - */ - public function setDeathMessage($deathMessage) : void{ + public function setDeathMessage(Translatable|string $deathMessage) : void{ $this->deathMessage = $deathMessage; } @@ -89,24 +83,15 @@ class PlayerDeathEvent extends EntityDeathEvent{ /** * Returns the vanilla death message for the given death cause. */ - public static function deriveMessage(string $name, ?EntityDamageEvent $deathCause) : TranslationContainer{ - $message = "death.attack.generic"; - $params = [$name]; - + public static function deriveMessage(string $name, ?EntityDamageEvent $deathCause) : Translatable{ switch($deathCause === null ? EntityDamageEvent::CAUSE_CUSTOM : $deathCause->getCause()){ case EntityDamageEvent::CAUSE_ENTITY_ATTACK: if($deathCause instanceof EntityDamageByEntityEvent){ $e = $deathCause->getDamager(); if($e instanceof Player){ - $message = "death.attack.player"; - $params[] = $e->getDisplayName(); - break; + return KnownTranslationFactory::death_attack_player($name, $e->getDisplayName()); }elseif($e instanceof Living){ - $message = "death.attack.mob"; - $params[] = $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName(); - break; - }else{ - $params[] = "Unknown"; + return KnownTranslationFactory::death_attack_mob($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName()); } } break; @@ -114,57 +99,41 @@ class PlayerDeathEvent extends EntityDeathEvent{ if($deathCause instanceof EntityDamageByEntityEvent){ $e = $deathCause->getDamager(); if($e instanceof Player){ - $message = "death.attack.arrow"; - $params[] = $e->getDisplayName(); + return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName()); }elseif($e instanceof Living){ - $message = "death.attack.arrow"; - $params[] = $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName(); - break; - }else{ - $params[] = "Unknown"; + return KnownTranslationFactory::death_attack_arrow($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName()); } } break; case EntityDamageEvent::CAUSE_SUICIDE: - $message = "death.attack.generic"; - break; + return KnownTranslationFactory::death_attack_generic($name); case EntityDamageEvent::CAUSE_VOID: - $message = "death.attack.outOfWorld"; - break; + return KnownTranslationFactory::death_attack_outOfWorld($name); case EntityDamageEvent::CAUSE_FALL: - if($deathCause instanceof EntityDamageEvent){ - if($deathCause->getFinalDamage() > 2){ - $message = "death.fell.accident.generic"; - break; - } + if($deathCause instanceof EntityDamageEvent && $deathCause->getFinalDamage() > 2){ + return KnownTranslationFactory::death_fell_accident_generic($name); } - $message = "death.attack.fall"; - break; + return KnownTranslationFactory::death_attack_fall($name); case EntityDamageEvent::CAUSE_SUFFOCATION: - $message = "death.attack.inWall"; - break; + return KnownTranslationFactory::death_attack_inWall($name); case EntityDamageEvent::CAUSE_LAVA: - $message = "death.attack.lava"; - break; + return KnownTranslationFactory::death_attack_lava($name); case EntityDamageEvent::CAUSE_FIRE: - $message = "death.attack.onFire"; - break; + return KnownTranslationFactory::death_attack_onFire($name); case EntityDamageEvent::CAUSE_FIRE_TICK: - $message = "death.attack.inFire"; - break; + return KnownTranslationFactory::death_attack_inFire($name); case EntityDamageEvent::CAUSE_DROWNING: - $message = "death.attack.drown"; - break; + return KnownTranslationFactory::death_attack_drown($name); case EntityDamageEvent::CAUSE_CONTACT: if($deathCause instanceof EntityDamageByBlockEvent){ - if($deathCause->getDamager()->getId() === Block::CACTUS){ - $message = "death.attack.cactus"; + if($deathCause->getDamager()->getId() === BlockLegacyIds::CACTUS){ + return KnownTranslationFactory::death_attack_cactus($name); } } break; @@ -174,21 +143,15 @@ class PlayerDeathEvent extends EntityDeathEvent{ if($deathCause instanceof EntityDamageByEntityEvent){ $e = $deathCause->getDamager(); if($e instanceof Player){ - $message = "death.attack.explosion.player"; - $params[] = $e->getDisplayName(); + return KnownTranslationFactory::death_attack_explosion_player($name, $e->getDisplayName()); }elseif($e instanceof Living){ - $message = "death.attack.explosion.player"; - $params[] = $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName(); - break; + return KnownTranslationFactory::death_attack_explosion_player($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName()); } - }else{ - $message = "death.attack.explosion"; } - break; + return KnownTranslationFactory::death_attack_explosion($name); case EntityDamageEvent::CAUSE_MAGIC: - $message = "death.attack.magic"; - break; + return KnownTranslationFactory::death_attack_magic($name); case EntityDamageEvent::CAUSE_CUSTOM: break; @@ -197,6 +160,6 @@ class PlayerDeathEvent extends EntityDeathEvent{ break; } - return new TranslationContainer($message, $params); + return KnownTranslationFactory::death_attack_generic($name); } } diff --git a/src/pocketmine/event/player/PlayerAnimationEvent.php b/src/event/player/PlayerDisplayNameChangeEvent.php similarity index 64% rename from src/pocketmine/event/player/PlayerAnimationEvent.php rename to src/event/player/PlayerDisplayNameChangeEvent.php index 0c34b648d8..27de577c8e 100644 --- a/src/pocketmine/event/player/PlayerAnimationEvent.php +++ b/src/event/player/PlayerDisplayNameChangeEvent.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\player\Player; -/** - * Called when a player does an animation - */ -class PlayerAnimationEvent extends PlayerEvent implements Cancellable{ - /** @var int */ - private $animationType; +class PlayerDisplayNameChangeEvent extends PlayerEvent{ - public function __construct(Player $player, int $animation){ + /** @var string */ + private $oldName; + /** @var string */ + private $newName; + + public function __construct(Player $player, string $oldName, string $newName){ $this->player = $player; - $this->animationType = $animation; + $this->oldName = $oldName; + $this->newName = $newName; } - public function getAnimationType() : int{ - return $this->animationType; + public function getOldName() : string{ + return $this->oldName; } -} + + public function getNewName() : string{ + return $this->newName; + } +} \ No newline at end of file diff --git a/src/pocketmine/event/player/PlayerDropItemEvent.php b/src/event/player/PlayerDropItemEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerDropItemEvent.php rename to src/event/player/PlayerDropItemEvent.php index 787528146a..a97a6f5fad 100644 --- a/src/pocketmine/event/player/PlayerDropItemEvent.php +++ b/src/event/player/PlayerDropItemEvent.php @@ -24,13 +24,16 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player tries to drop an item from its hotbar */ class PlayerDropItemEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Item */ private $drop; diff --git a/src/event/player/PlayerDuplicateLoginEvent.php b/src/event/player/PlayerDuplicateLoginEvent.php new file mode 100644 index 0000000000..92fc34547a --- /dev/null +++ b/src/event/player/PlayerDuplicateLoginEvent.php @@ -0,0 +1,68 @@ +connectingSession = $connectingSession; + $this->existingSession = $existingSession; + } + + public function getConnectingSession() : NetworkSession{ + return $this->connectingSession; + } + + public function getExistingSession() : NetworkSession{ + return $this->existingSession; + } + + /** + * Returns the message shown to the session which gets disconnected. + */ + public function getDisconnectMessage() : string{ + return $this->disconnectMessage; + } + + public function setDisconnectMessage(string $message) : void{ + $this->disconnectMessage = $message; + } +} diff --git a/src/pocketmine/event/player/PlayerEditBookEvent.php b/src/event/player/PlayerEditBookEvent.php similarity index 80% rename from src/pocketmine/event/player/PlayerEditBookEvent.php rename to src/event/player/PlayerEditBookEvent.php index c6c29bf391..9a60de1377 100644 --- a/src/pocketmine/event/player/PlayerEditBookEvent.php +++ b/src/event/player/PlayerEditBookEvent.php @@ -24,21 +24,24 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\item\WritableBook; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\item\WritableBookBase; +use pocketmine\player\Player; class PlayerEditBookEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + public const ACTION_REPLACE_PAGE = 0; public const ACTION_ADD_PAGE = 1; public const ACTION_DELETE_PAGE = 2; public const ACTION_SWAP_PAGES = 3; public const ACTION_SIGN_BOOK = 4; - /** @var WritableBook */ + /** @var WritableBookBase */ private $oldBook; /** @var int */ private $action; - /** @var WritableBook */ + /** @var WritableBookBase */ private $newBook; /** @var int[] */ private $modifiedPages; @@ -46,7 +49,7 @@ class PlayerEditBookEvent extends PlayerEvent implements Cancellable{ /** * @param int[] $modifiedPages */ - public function __construct(Player $player, WritableBook $oldBook, WritableBook $newBook, int $action, array $modifiedPages){ + public function __construct(Player $player, WritableBookBase $oldBook, WritableBookBase $newBook, int $action, array $modifiedPages){ $this->player = $player; $this->oldBook = $oldBook; $this->newBook = $newBook; @@ -64,7 +67,7 @@ class PlayerEditBookEvent extends PlayerEvent implements Cancellable{ /** * Returns the book before it was modified. */ - public function getOldBook() : WritableBook{ + public function getOldBook() : WritableBookBase{ return $this->oldBook; } @@ -72,14 +75,14 @@ class PlayerEditBookEvent extends PlayerEvent implements Cancellable{ * Returns the book after it was modified. * The new book may be a written book, if the book was signed. */ - public function getNewBook() : WritableBook{ + public function getNewBook() : WritableBookBase{ return $this->newBook; } /** * Sets the new book as the given instance. */ - public function setNewBook(WritableBook $book) : void{ + public function setNewBook(WritableBookBase $book) : void{ $this->newBook = $book; } diff --git a/src/pocketmine/event/player/PlayerAchievementAwardedEvent.php b/src/event/player/PlayerEmoteEvent.php similarity index 66% rename from src/pocketmine/event/player/PlayerAchievementAwardedEvent.php rename to src/event/player/PlayerEmoteEvent.php index 25b83bae72..b10a3171e1 100644 --- a/src/pocketmine/event/player/PlayerAchievementAwardedEvent.php +++ b/src/event/player/PlayerEmoteEvent.php @@ -24,21 +24,28 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; /** - * Called when a player is awarded an achievement + * Called when a player uses an emote. */ -class PlayerAchievementAwardedEvent extends PlayerEvent implements Cancellable{ - /** @var string */ - protected $achievement; +class PlayerEmoteEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; - public function __construct(Player $player, string $achievementId){ + public function __construct( + Player $player, + private string $emoteId + ){ $this->player = $player; - $this->achievement = $achievementId; } - public function getAchievement() : string{ - return $this->achievement; + public function getEmoteId() : string{ + return $this->emoteId; } + + public function setEmoteId(string $emoteId) : void{ + $this->emoteId = $emoteId; + } + } diff --git a/src/event/player/PlayerEntityInteractEvent.php b/src/event/player/PlayerEntityInteractEvent.php new file mode 100644 index 0000000000..262d5dbc16 --- /dev/null +++ b/src/event/player/PlayerEntityInteractEvent.php @@ -0,0 +1,56 @@ +player = $player; + } + + public function getEntity() : Entity{ + return $this->entity; + } + + /** + * Returns the absolute coordinates of the click. This is usually on the surface of the entity's hitbox. + */ + public function getClickPosition() : Vector3{ + return $this->clickPos; + } +} diff --git a/src/pocketmine/event/player/PlayerEvent.php b/src/event/player/PlayerEvent.php similarity index 97% rename from src/pocketmine/event/player/PlayerEvent.php rename to src/event/player/PlayerEvent.php index 77cce34441..8be23d3509 100644 --- a/src/pocketmine/event/player/PlayerEvent.php +++ b/src/event/player/PlayerEvent.php @@ -27,7 +27,7 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Event; -use pocketmine\Player; +use pocketmine\player\Player; abstract class PlayerEvent extends Event{ /** @var Player */ diff --git a/src/pocketmine/event/player/PlayerExhaustEvent.php b/src/event/player/PlayerExhaustEvent.php similarity index 97% rename from src/pocketmine/event/player/PlayerExhaustEvent.php rename to src/event/player/PlayerExhaustEvent.php index 0607661cfa..42408a7b29 100644 --- a/src/pocketmine/event/player/PlayerExhaustEvent.php +++ b/src/event/player/PlayerExhaustEvent.php @@ -25,12 +25,15 @@ namespace pocketmine\event\player; use pocketmine\entity\Human; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\event\entity\EntityEvent; /** * @phpstan-extends EntityEvent */ class PlayerExhaustEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + public const CAUSE_ATTACK = 1; public const CAUSE_DAMAGE = 2; public const CAUSE_MINING = 3; diff --git a/src/pocketmine/event/player/PlayerExperienceChangeEvent.php b/src/event/player/PlayerExperienceChangeEvent.php similarity index 97% rename from src/pocketmine/event/player/PlayerExperienceChangeEvent.php rename to src/event/player/PlayerExperienceChangeEvent.php index 3322a91b80..f1569d79d4 100644 --- a/src/pocketmine/event/player/PlayerExperienceChangeEvent.php +++ b/src/event/player/PlayerExperienceChangeEvent.php @@ -25,6 +25,7 @@ namespace pocketmine\event\player; use pocketmine\entity\Human; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\event\entity\EntityEvent; /** @@ -32,6 +33,8 @@ use pocketmine\event\entity\EntityEvent; * @phpstan-extends EntityEvent */ class PlayerExperienceChangeEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + /** @var Human */ protected $entity; /** @var int */ diff --git a/src/pocketmine/event/player/PlayerGameModeChangeEvent.php b/src/event/player/PlayerGameModeChangeEvent.php similarity index 80% rename from src/pocketmine/event/player/PlayerGameModeChangeEvent.php rename to src/event/player/PlayerGameModeChangeEvent.php index 9697c9f72c..926d17d2bc 100644 --- a/src/pocketmine/event/player/PlayerGameModeChangeEvent.php +++ b/src/event/player/PlayerGameModeChangeEvent.php @@ -24,21 +24,25 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\GameMode; +use pocketmine\player\Player; /** * Called when a player has its gamemode changed */ class PlayerGameModeChangeEvent extends PlayerEvent implements Cancellable{ - /** @var int */ + use CancellableTrait; + + /** @var GameMode */ protected $gamemode; - public function __construct(Player $player, int $newGamemode){ + public function __construct(Player $player, GameMode $newGamemode){ $this->player = $player; $this->gamemode = $newGamemode; } - public function getNewGamemode() : int{ + public function getNewGamemode() : GameMode{ return $this->gamemode; } } diff --git a/src/pocketmine/event/player/PlayerInteractEvent.php b/src/event/player/PlayerInteractEvent.php similarity index 77% rename from src/pocketmine/event/player/PlayerInteractEvent.php rename to src/event/player/PlayerInteractEvent.php index d8696bbff8..f67b72d9cf 100644 --- a/src/pocketmine/event/player/PlayerInteractEvent.php +++ b/src/event/player/PlayerInteractEvent.php @@ -24,23 +24,20 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\level\Position; use pocketmine\math\Vector3; -use pocketmine\Player; -use function assert; +use pocketmine\player\Player; /** * Called when a player interacts or touches a block (including air?) */ class PlayerInteractEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + public const LEFT_CLICK_BLOCK = 0; public const RIGHT_CLICK_BLOCK = 1; - public const LEFT_CLICK_AIR = 2; - public const RIGHT_CLICK_AIR = 3; - public const PHYSICAL = 4; /** @var Block */ protected $blockTouched; @@ -57,11 +54,10 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{ /** @var int */ protected $action; - public function __construct(Player $player, Item $item, ?Block $block, ?Vector3 $touchVector, int $face, int $action = PlayerInteractEvent::RIGHT_CLICK_BLOCK){ - assert($block !== null or $touchVector !== null); + public function __construct(Player $player, Item $item, Block $block, ?Vector3 $touchVector, int $face, int $action = PlayerInteractEvent::RIGHT_CLICK_BLOCK){ $this->player = $player; $this->item = $item; - $this->blockTouched = $block ?? BlockFactory::get(0, 0, new Position(0, 0, 0, $player->level)); + $this->blockTouched = $block; $this->touchVector = $touchVector ?? new Vector3(0, 0, 0); $this->blockFace = $face; $this->action = $action; diff --git a/src/pocketmine/event/player/PlayerItemConsumeEvent.php b/src/event/player/PlayerItemConsumeEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerItemConsumeEvent.php rename to src/event/player/PlayerItemConsumeEvent.php index 278ec4c33c..52bb5e2433 100644 --- a/src/pocketmine/event/player/PlayerItemConsumeEvent.php +++ b/src/event/player/PlayerItemConsumeEvent.php @@ -24,13 +24,16 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player eats something */ class PlayerItemConsumeEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Item */ private $item; diff --git a/src/pocketmine/event/player/PlayerItemHeldEvent.php b/src/event/player/PlayerItemHeldEvent.php similarity index 94% rename from src/pocketmine/event/player/PlayerItemHeldEvent.php rename to src/event/player/PlayerItemHeldEvent.php index 1e1f36535e..71f55fc534 100644 --- a/src/pocketmine/event/player/PlayerItemHeldEvent.php +++ b/src/event/player/PlayerItemHeldEvent.php @@ -24,10 +24,13 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; class PlayerItemHeldEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Item */ private $item; /** @var int */ diff --git a/src/pocketmine/event/inventory/InventoryPickupItemEvent.php b/src/event/player/PlayerItemUseEvent.php similarity index 50% rename from src/pocketmine/event/inventory/InventoryPickupItemEvent.php rename to src/event/player/PlayerItemUseEvent.php index d255b0238c..b9216d5281 100644 --- a/src/pocketmine/event/inventory/InventoryPickupItemEvent.php +++ b/src/event/player/PlayerItemUseEvent.php @@ -21,22 +21,42 @@ declare(strict_types=1); -namespace pocketmine\event\inventory; +namespace pocketmine\event\player; -use pocketmine\entity\object\ItemEntity; use pocketmine\event\Cancellable; -use pocketmine\inventory\Inventory; +use pocketmine\event\CancellableTrait; +use pocketmine\item\Item; +use pocketmine\math\Vector3; +use pocketmine\player\Player; -class InventoryPickupItemEvent extends InventoryEvent implements Cancellable{ - /** @var ItemEntity */ +/** + * Called when a player uses its held item, for example when throwing a projectile. + */ +class PlayerItemUseEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + + /** @var Item */ private $item; + /** @var Vector3 */ + private $directionVector; - public function __construct(Inventory $inventory, ItemEntity $item){ + public function __construct(Player $player, Item $item, Vector3 $directionVector){ + $this->player = $player; $this->item = $item; - parent::__construct($inventory); + $this->directionVector = $directionVector; } - public function getItem() : ItemEntity{ + /** + * Returns the item used. + */ + public function getItem() : Item{ return $this->item; } + + /** + * Returns the direction the player is aiming when activating this item. Used for projectile direction. + */ + public function getDirectionVector() : Vector3{ + return $this->directionVector; + } } diff --git a/src/pocketmine/event/player/PlayerJoinEvent.php b/src/event/player/PlayerJoinEvent.php similarity index 75% rename from src/pocketmine/event/player/PlayerJoinEvent.php rename to src/event/player/PlayerJoinEvent.php index b05a2858a1..e46e3fd7d5 100644 --- a/src/pocketmine/event/player/PlayerJoinEvent.php +++ b/src/event/player/PlayerJoinEvent.php @@ -23,8 +23,8 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\lang\TextContainer; -use pocketmine\Player; +use pocketmine\lang\Translatable; +use pocketmine\player\Player; /** * Called when the player spawns in the world after logging in, when they first see the terrain. @@ -34,30 +34,19 @@ use pocketmine\Player; * @see PlayerLoginEvent */ class PlayerJoinEvent extends PlayerEvent{ - /** @var string|TextContainer */ + /** @var string|Translatable */ protected $joinMessage; - /** - * PlayerJoinEvent constructor. - * - * @param TextContainer|string $joinMessage - */ - public function __construct(Player $player, $joinMessage){ + public function __construct(Player $player, Translatable|string $joinMessage){ $this->player = $player; $this->joinMessage = $joinMessage; } - /** - * @param string|TextContainer $joinMessage - */ - public function setJoinMessage($joinMessage) : void{ + public function setJoinMessage(Translatable|string $joinMessage) : void{ $this->joinMessage = $joinMessage; } - /** - * @return string|TextContainer - */ - public function getJoinMessage(){ + public function getJoinMessage() : Translatable|string{ return $this->joinMessage; } } diff --git a/src/pocketmine/event/player/PlayerJumpEvent.php b/src/event/player/PlayerJumpEvent.php similarity index 97% rename from src/pocketmine/event/player/PlayerJumpEvent.php rename to src/event/player/PlayerJumpEvent.php index 30c82e5255..3d9b6d1674 100644 --- a/src/pocketmine/event/player/PlayerJumpEvent.php +++ b/src/event/player/PlayerJumpEvent.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\Player; +use pocketmine\player\Player; /** * Called when a player jumps diff --git a/src/pocketmine/event/player/PlayerKickEvent.php b/src/event/player/PlayerKickEvent.php similarity index 74% rename from src/pocketmine/event/player/PlayerKickEvent.php rename to src/event/player/PlayerKickEvent.php index 4fc5e736fd..7c91399d01 100644 --- a/src/pocketmine/event/player/PlayerKickEvent.php +++ b/src/event/player/PlayerKickEvent.php @@ -24,25 +24,23 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\lang\TextContainer; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\lang\Translatable; +use pocketmine\player\Player; /** * Called when a player leaves the server */ class PlayerKickEvent extends PlayerEvent implements Cancellable{ - /** @var TextContainer|string */ + use CancellableTrait; + + /** @var Translatable|string */ protected $quitMessage; /** @var string */ protected $reason; - /** - * PlayerKickEvent constructor. - * - * @param TextContainer|string $quitMessage - */ - public function __construct(Player $player, string $reason, $quitMessage){ + public function __construct(Player $player, string $reason, Translatable|string $quitMessage){ $this->player = $player; $this->quitMessage = $quitMessage; $this->reason = $reason; @@ -56,17 +54,11 @@ class PlayerKickEvent extends PlayerEvent implements Cancellable{ return $this->reason; } - /** - * @param TextContainer|string $quitMessage - */ - public function setQuitMessage($quitMessage) : void{ + public function setQuitMessage(Translatable|string $quitMessage) : void{ $this->quitMessage = $quitMessage; } - /** - * @return TextContainer|string - */ - public function getQuitMessage(){ + public function getQuitMessage() : Translatable|string{ return $this->quitMessage; } } diff --git a/src/pocketmine/event/player/PlayerLoginEvent.php b/src/event/player/PlayerLoginEvent.php similarity index 94% rename from src/pocketmine/event/player/PlayerLoginEvent.php rename to src/event/player/PlayerLoginEvent.php index 0eb24f62a5..8c418f7037 100644 --- a/src/pocketmine/event/player/PlayerLoginEvent.php +++ b/src/event/player/PlayerLoginEvent.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; /** * Called after the player has successfully authenticated, before it spawns. The player is on the loading screen when @@ -32,6 +33,8 @@ use pocketmine\Player; * Cancelling this event will cause the player to be disconnected with the kick message set. */ class PlayerLoginEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var string */ protected $kickMessage; diff --git a/src/pocketmine/event/player/PlayerMoveEvent.php b/src/event/player/PlayerMoveEvent.php similarity index 91% rename from src/pocketmine/event/player/PlayerMoveEvent.php rename to src/event/player/PlayerMoveEvent.php index 961cb7086a..d9f258a3c7 100644 --- a/src/pocketmine/event/player/PlayerMoveEvent.php +++ b/src/event/player/PlayerMoveEvent.php @@ -23,11 +23,14 @@ declare(strict_types=1); namespace pocketmine\event\player; +use pocketmine\entity\Location; use pocketmine\event\Cancellable; -use pocketmine\level\Location; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerMoveEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var Location */ private $from; /** @var Location */ diff --git a/src/event/player/PlayerPreLoginEvent.php b/src/event/player/PlayerPreLoginEvent.php new file mode 100644 index 0000000000..66cf550d2b --- /dev/null +++ b/src/event/player/PlayerPreLoginEvent.php @@ -0,0 +1,175 @@ + associated message */ + protected $kickReasons = []; + + public function __construct(PlayerInfo $playerInfo, string $ip, int $port, bool $authRequired){ + $this->playerInfo = $playerInfo; + $this->ip = $ip; + $this->port = $port; + $this->authRequired = $authRequired; + } + + /** + * Returns an object containing self-proclaimed information about the connecting player. + * WARNING: THE PLAYER IS NOT VERIFIED DURING THIS EVENT. At this point, it's unknown if the player is real or a + * hacker. + */ + public function getPlayerInfo() : PlayerInfo{ + return $this->playerInfo; + } + + public function getIp() : string{ + return $this->ip; + } + + public function getPort() : int{ + return $this->port; + } + + public function isAuthRequired() : bool{ + return $this->authRequired; + } + + public function setAuthRequired(bool $v) : void{ + $this->authRequired = $v; + } + + /** + * Returns an array of kick reasons currently assigned. + * + * @return int[] + */ + public function getKickReasons() : array{ + return array_keys($this->kickReasons); + } + + /** + * Returns whether the given kick reason is set for this event. + */ + public function isKickReasonSet(int $flag) : bool{ + return isset($this->kickReasons[$flag]); + } + + /** + * Sets a reason to disallow the player to continue continue authenticating, with a message. + * This can also be used to change kick messages for already-set flags. + */ + public function setKickReason(int $flag, string $message) : void{ + $this->kickReasons[$flag] = $message; + } + + /** + * Clears a specific kick flag if it was set. This allows fine-tuned kick reason removal without impacting other + * reasons (for example, a ban can be bypassed without accidentally allowing a player to join a full server). + * + * @param int $flag Specific flag to clear. + */ + public function clearKickReason(int $flag) : void{ + unset($this->kickReasons[$flag]); + } + + /** + * Clears all pre-assigned kick reasons, allowing the player to continue logging in. + */ + public function clearAllKickReasons() : void{ + $this->kickReasons = []; + } + + /** + * Returns whether the player is allowed to continue logging in. + */ + public function isAllowed() : bool{ + return count($this->kickReasons) === 0; + } + + /** + * Returns the kick message provided for the given kick flag, or null if not set. + */ + public function getKickMessage(int $flag) : ?string{ + return $this->kickReasons[$flag] ?? null; + } + + /** + * Returns the final kick message which will be shown on the disconnect screen. + * + * Note: Only one message (the highest priority one) will be shown. See priority order to decide how to set your + * messages. + * + * @see PlayerPreLoginEvent::KICK_REASON_PRIORITY + */ + public function getFinalKickMessage() : string{ + foreach(self::KICK_REASON_PRIORITY as $p){ + if(isset($this->kickReasons[$p])){ + return $this->kickReasons[$p]; + } + } + + return ""; + } + + public function isCancelled() : bool{ + return !$this->isAllowed(); + } +} diff --git a/src/pocketmine/event/player/PlayerQuitEvent.php b/src/event/player/PlayerQuitEvent.php similarity index 74% rename from src/pocketmine/event/player/PlayerQuitEvent.php rename to src/event/player/PlayerQuitEvent.php index 4b1585384c..9791cd8938 100644 --- a/src/pocketmine/event/player/PlayerQuitEvent.php +++ b/src/event/player/PlayerQuitEvent.php @@ -23,39 +23,30 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\lang\TextContainer; -use pocketmine\Player; +use pocketmine\lang\Translatable; +use pocketmine\player\Player; /** * Called when a player leaves the server */ class PlayerQuitEvent extends PlayerEvent{ - /** @var TextContainer|string */ + /** @var Translatable|string */ protected $quitMessage; /** @var string */ protected $quitReason; - /** - * @param TextContainer|string $quitMessage - */ - public function __construct(Player $player, $quitMessage, string $quitReason){ + public function __construct(Player $player, Translatable|string $quitMessage, string $quitReason){ $this->player = $player; $this->quitMessage = $quitMessage; $this->quitReason = $quitReason; } - /** - * @param TextContainer|string $quitMessage - */ - public function setQuitMessage($quitMessage) : void{ + public function setQuitMessage(Translatable|string $quitMessage) : void{ $this->quitMessage = $quitMessage; } - /** - * @return TextContainer|string - */ - public function getQuitMessage(){ + public function getQuitMessage() : Translatable|string{ return $this->quitMessage; } diff --git a/src/pocketmine/event/player/PlayerRespawnEvent.php b/src/event/player/PlayerRespawnEvent.php similarity index 95% rename from src/pocketmine/event/player/PlayerRespawnEvent.php rename to src/event/player/PlayerRespawnEvent.php index 1dad225c65..5f1f291ac4 100644 --- a/src/pocketmine/event/player/PlayerRespawnEvent.php +++ b/src/event/player/PlayerRespawnEvent.php @@ -23,8 +23,8 @@ declare(strict_types=1); namespace pocketmine\event\player; -use pocketmine\level\Position; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\Position; /** * Called when a player is respawned diff --git a/src/pocketmine/event/player/PlayerToggleFlightEvent.php b/src/event/player/PlayerToggleFlightEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerToggleFlightEvent.php rename to src/event/player/PlayerToggleFlightEvent.php index 8c3b292c4f..b8c0f93d9e 100644 --- a/src/pocketmine/event/player/PlayerToggleFlightEvent.php +++ b/src/event/player/PlayerToggleFlightEvent.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerToggleFlightEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var bool */ protected $isFlying; diff --git a/src/pocketmine/event/player/PlayerToggleSneakEvent.php b/src/event/player/PlayerToggleSneakEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerToggleSneakEvent.php rename to src/event/player/PlayerToggleSneakEvent.php index d86d775738..0b7538975a 100644 --- a/src/pocketmine/event/player/PlayerToggleSneakEvent.php +++ b/src/event/player/PlayerToggleSneakEvent.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerToggleSneakEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var bool */ protected $isSneaking; diff --git a/src/pocketmine/event/player/PlayerToggleSprintEvent.php b/src/event/player/PlayerToggleSprintEvent.php similarity index 92% rename from src/pocketmine/event/player/PlayerToggleSprintEvent.php rename to src/event/player/PlayerToggleSprintEvent.php index 5eeacadd9c..2da14d7b7a 100644 --- a/src/pocketmine/event/player/PlayerToggleSprintEvent.php +++ b/src/event/player/PlayerToggleSprintEvent.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerToggleSprintEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var bool */ protected $isSprinting; diff --git a/src/pocketmine/event/player/PlayerTransferEvent.php b/src/event/player/PlayerTransferEvent.php similarity index 94% rename from src/pocketmine/event/player/PlayerTransferEvent.php rename to src/event/player/PlayerTransferEvent.php index d868aa50de..9e6669893e 100644 --- a/src/pocketmine/event/player/PlayerTransferEvent.php +++ b/src/event/player/PlayerTransferEvent.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace pocketmine\event\player; use pocketmine\event\Cancellable; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\player\Player; class PlayerTransferEvent extends PlayerEvent implements Cancellable{ + use CancellableTrait; + /** @var string */ protected $address; /** @var int */ diff --git a/src/pocketmine/event/plugin/PluginDisableEvent.php b/src/event/plugin/PluginDisableEvent.php similarity index 100% rename from src/pocketmine/event/plugin/PluginDisableEvent.php rename to src/event/plugin/PluginDisableEvent.php diff --git a/src/pocketmine/event/plugin/PluginEnableEvent.php b/src/event/plugin/PluginEnableEvent.php similarity index 100% rename from src/pocketmine/event/plugin/PluginEnableEvent.php rename to src/event/plugin/PluginEnableEvent.php diff --git a/src/pocketmine/event/plugin/PluginEvent.php b/src/event/plugin/PluginEvent.php similarity index 100% rename from src/pocketmine/event/plugin/PluginEvent.php rename to src/event/plugin/PluginEvent.php diff --git a/src/pocketmine/event/server/CommandEvent.php b/src/event/server/CommandEvent.php similarity index 96% rename from src/pocketmine/event/server/CommandEvent.php rename to src/event/server/CommandEvent.php index aaecf75f38..818a465cb2 100644 --- a/src/pocketmine/event/server/CommandEvent.php +++ b/src/event/server/CommandEvent.php @@ -25,6 +25,7 @@ namespace pocketmine\event\server; use pocketmine\command\CommandSender; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when any CommandSender runs a command, early in the process @@ -35,6 +36,8 @@ use pocketmine\event\Cancellable; * The message DOES NOT contain a slash at the start */ class CommandEvent extends ServerEvent implements Cancellable{ + use CancellableTrait; + /** @var string */ protected $command; diff --git a/src/pocketmine/event/server/DataPacketReceiveEvent.php b/src/event/server/DataPacketReceiveEvent.php similarity index 67% rename from src/pocketmine/event/server/DataPacketReceiveEvent.php rename to src/event/server/DataPacketReceiveEvent.php index 7ae6e2a84e..c3443d2061 100644 --- a/src/pocketmine/event/server/DataPacketReceiveEvent.php +++ b/src/event/server/DataPacketReceiveEvent.php @@ -24,25 +24,28 @@ declare(strict_types=1); namespace pocketmine\event\server; use pocketmine\event\Cancellable; -use pocketmine\network\mcpe\protocol\DataPacket; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\ServerboundPacket; class DataPacketReceiveEvent extends ServerEvent implements Cancellable{ - /** @var DataPacket */ - private $packet; - /** @var Player */ - private $player; + use CancellableTrait; - public function __construct(Player $player, DataPacket $packet){ + /** @var ServerboundPacket */ + private $packet; + /** @var NetworkSession */ + private $origin; + + public function __construct(NetworkSession $origin, ServerboundPacket $packet){ $this->packet = $packet; - $this->player = $player; + $this->origin = $origin; } - public function getPacket() : DataPacket{ + public function getPacket() : ServerboundPacket{ return $this->packet; } - public function getPlayer() : Player{ - return $this->player; + public function getOrigin() : NetworkSession{ + return $this->origin; } } diff --git a/src/pocketmine/event/server/DataPacketSendEvent.php b/src/event/server/DataPacketSendEvent.php similarity index 54% rename from src/pocketmine/event/server/DataPacketSendEvent.php rename to src/event/server/DataPacketSendEvent.php index 9ec1a4822b..dac9ac8195 100644 --- a/src/pocketmine/event/server/DataPacketSendEvent.php +++ b/src/event/server/DataPacketSendEvent.php @@ -24,25 +24,41 @@ declare(strict_types=1); namespace pocketmine\event\server; use pocketmine\event\Cancellable; -use pocketmine\network\mcpe\protocol\DataPacket; -use pocketmine\Player; +use pocketmine\event\CancellableTrait; +use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\ClientboundPacket; +/** + * Called when packets are sent to network sessions. + */ class DataPacketSendEvent extends ServerEvent implements Cancellable{ - /** @var DataPacket */ - private $packet; - /** @var Player */ - private $player; + use CancellableTrait; - public function __construct(Player $player, DataPacket $packet){ - $this->packet = $packet; - $this->player = $player; + /** @var NetworkSession[] */ + private $targets; + /** @var ClientboundPacket[] */ + private $packets; + + /** + * @param NetworkSession[] $targets + * @param ClientboundPacket[] $packets + */ + public function __construct(array $targets, array $packets){ + $this->targets = $targets; + $this->packets = $packets; } - public function getPacket() : DataPacket{ - return $this->packet; + /** + * @return NetworkSession[] + */ + public function getTargets() : array{ + return $this->targets; } - public function getPlayer() : Player{ - return $this->player; + /** + * @return ClientboundPacket[] + */ + public function getPackets() : array{ + return $this->packets; } } diff --git a/src/pocketmine/event/server/LowMemoryEvent.php b/src/event/server/LowMemoryEvent.php similarity index 100% rename from src/pocketmine/event/server/LowMemoryEvent.php rename to src/event/server/LowMemoryEvent.php diff --git a/src/pocketmine/event/server/NetworkInterfaceEvent.php b/src/event/server/NetworkInterfaceEvent.php similarity index 83% rename from src/pocketmine/event/server/NetworkInterfaceEvent.php rename to src/event/server/NetworkInterfaceEvent.php index 720f1404ef..98f48f07db 100644 --- a/src/pocketmine/event/server/NetworkInterfaceEvent.php +++ b/src/event/server/NetworkInterfaceEvent.php @@ -23,17 +23,17 @@ declare(strict_types=1); namespace pocketmine\event\server; -use pocketmine\network\SourceInterface; +use pocketmine\network\NetworkInterface; class NetworkInterfaceEvent extends ServerEvent{ - /** @var SourceInterface */ + /** @var NetworkInterface */ protected $interface; - public function __construct(SourceInterface $interface){ + public function __construct(NetworkInterface $interface){ $this->interface = $interface; } - public function getInterface() : SourceInterface{ + public function getInterface() : NetworkInterface{ return $this->interface; } } diff --git a/src/pocketmine/event/server/NetworkInterfaceRegisterEvent.php b/src/event/server/NetworkInterfaceRegisterEvent.php similarity index 94% rename from src/pocketmine/event/server/NetworkInterfaceRegisterEvent.php rename to src/event/server/NetworkInterfaceRegisterEvent.php index b2dafb82ea..a2ee78af65 100644 --- a/src/pocketmine/event/server/NetworkInterfaceRegisterEvent.php +++ b/src/event/server/NetworkInterfaceRegisterEvent.php @@ -24,10 +24,11 @@ declare(strict_types=1); namespace pocketmine\event\server; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when a network interface is registered into the network, for example the RakLib interface. */ class NetworkInterfaceRegisterEvent extends NetworkInterfaceEvent implements Cancellable{ - + use CancellableTrait; } diff --git a/src/pocketmine/event/server/NetworkInterfaceUnregisterEvent.php b/src/event/server/NetworkInterfaceUnregisterEvent.php similarity index 100% rename from src/pocketmine/event/server/NetworkInterfaceUnregisterEvent.php rename to src/event/server/NetworkInterfaceUnregisterEvent.php diff --git a/src/pocketmine/event/server/RemoteServerCommandEvent.php b/src/event/server/QueryRegenerateEvent.php similarity index 70% rename from src/pocketmine/event/server/RemoteServerCommandEvent.php rename to src/event/server/QueryRegenerateEvent.php index f73c1d46ef..5bf879f942 100644 --- a/src/pocketmine/event/server/RemoteServerCommandEvent.php +++ b/src/event/server/QueryRegenerateEvent.php @@ -23,16 +23,17 @@ declare(strict_types=1); namespace pocketmine\event\server; -use pocketmine\command\CommandSender; +use pocketmine\network\query\QueryInfo; -/** - * This event is called when a command is received over RCON. - * - * @deprecated Use CommandEvent instead. - */ -class RemoteServerCommandEvent extends ServerCommandEvent{ +class QueryRegenerateEvent extends ServerEvent{ + /** @var QueryInfo */ + private $queryInfo; - public function __construct(CommandSender $sender, string $command){ - parent::__construct($sender, $command); + public function __construct(QueryInfo $queryInfo){ + $this->queryInfo = $queryInfo; + } + + public function getQueryInfo() : QueryInfo{ + return $this->queryInfo; } } diff --git a/src/pocketmine/event/server/ServerEvent.php b/src/event/server/ServerEvent.php similarity index 100% rename from src/pocketmine/event/server/ServerEvent.php rename to src/event/server/ServerEvent.php diff --git a/src/pocketmine/event/server/UpdateNotifyEvent.php b/src/event/server/UpdateNotifyEvent.php similarity index 86% rename from src/pocketmine/event/server/UpdateNotifyEvent.php rename to src/event/server/UpdateNotifyEvent.php index c52b8a637d..843b2c60e8 100644 --- a/src/pocketmine/event/server/UpdateNotifyEvent.php +++ b/src/event/server/UpdateNotifyEvent.php @@ -23,21 +23,21 @@ declare(strict_types=1); namespace pocketmine\event\server; -use pocketmine\updater\AutoUpdater; +use pocketmine\updater\UpdateChecker; /** * Called when the AutoUpdater receives notification of an available PocketMine-MP update. * Plugins may use this event to perform actions when an update notification is received. */ class UpdateNotifyEvent extends ServerEvent{ - /** @var AutoUpdater */ + /** @var UpdateChecker */ private $updater; - public function __construct(AutoUpdater $updater){ + public function __construct(UpdateChecker $updater){ $this->updater = $updater; } - public function getUpdater() : AutoUpdater{ + public function getUpdater() : UpdateChecker{ return $this->updater; } } diff --git a/src/pocketmine/network/mcpe/protocol/ItemFrameDropItemPacket.php b/src/event/world/ChunkEvent.php similarity index 55% rename from src/pocketmine/network/mcpe/protocol/ItemFrameDropItemPacket.php rename to src/event/world/ChunkEvent.php index 200bb5824d..ebdc2be913 100644 --- a/src/pocketmine/network/mcpe/protocol/ItemFrameDropItemPacket.php +++ b/src/event/world/ChunkEvent.php @@ -21,32 +21,34 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\event\world; -#include - -use pocketmine\network\mcpe\NetworkSession; - -class ItemFrameDropItemPacket extends DataPacket{ - - public const NETWORK_ID = ProtocolInfo::ITEM_FRAME_DROP_ITEM_PACKET; +use pocketmine\world\format\Chunk; +use pocketmine\world\World; +/** + * Chunk-related events + */ +abstract class ChunkEvent extends WorldEvent{ + /** @var Chunk */ + private $chunk; /** @var int */ - public $x; + private $chunkX; /** @var int */ - public $y; - /** @var int */ - public $z; + private $chunkZ; - protected function decodePayload(){ - $this->getBlockPosition($this->x, $this->y, $this->z); + public function __construct(World $world, int $chunkX, int $chunkZ, Chunk $chunk){ + parent::__construct($world); + $this->chunk = $chunk; + $this->chunkX = $chunkX; + $this->chunkZ = $chunkZ; } - protected function encodePayload(){ - $this->putBlockPosition($this->x, $this->y, $this->z); + public function getChunk() : Chunk{ + return $this->chunk; } - public function handle(NetworkSession $session) : bool{ - return $session->handleItemFrameDropItem($this); - } + public function getChunkX() : int{ return $this->chunkX; } + + public function getChunkZ() : int{ return $this->chunkZ; } } diff --git a/src/pocketmine/event/level/ChunkLoadEvent.php b/src/event/world/ChunkLoadEvent.php similarity index 79% rename from src/pocketmine/event/level/ChunkLoadEvent.php rename to src/event/world/ChunkLoadEvent.php index c8c08511ce..f8f1ef7765 100644 --- a/src/pocketmine/event/level/ChunkLoadEvent.php +++ b/src/event/world/ChunkLoadEvent.php @@ -21,10 +21,10 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; -use pocketmine\level\format\Chunk; -use pocketmine\level\Level; +use pocketmine\world\format\Chunk; +use pocketmine\world\World; /** * Called when a Chunk is loaded @@ -33,8 +33,8 @@ class ChunkLoadEvent extends ChunkEvent{ /** @var bool */ private $newChunk; - public function __construct(Level $level, Chunk $chunk, bool $newChunk){ - parent::__construct($level, $chunk); + public function __construct(World $world, int $chunkX, int $chunkZ, Chunk $chunk, bool $newChunk){ + parent::__construct($world, $chunkX, $chunkZ, $chunk); $this->newChunk = $newChunk; } diff --git a/src/pocketmine/event/level/ChunkPopulateEvent.php b/src/event/world/ChunkPopulateEvent.php similarity index 96% rename from src/pocketmine/event/level/ChunkPopulateEvent.php rename to src/event/world/ChunkPopulateEvent.php index 76c6abb8da..3797bb58b4 100644 --- a/src/pocketmine/event/level/ChunkPopulateEvent.php +++ b/src/event/world/ChunkPopulateEvent.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; /** * Called when a Chunk is populated (after receiving it on the main thread) diff --git a/src/pocketmine/event/level/ChunkUnloadEvent.php b/src/event/world/ChunkUnloadEvent.php similarity index 90% rename from src/pocketmine/event/level/ChunkUnloadEvent.php rename to src/event/world/ChunkUnloadEvent.php index 288325864d..89b58b6d47 100644 --- a/src/pocketmine/event/level/ChunkUnloadEvent.php +++ b/src/event/world/ChunkUnloadEvent.php @@ -21,13 +21,14 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; use pocketmine\event\Cancellable; +use pocketmine\event\CancellableTrait; /** * Called when a Chunk is unloaded */ class ChunkUnloadEvent extends ChunkEvent implements Cancellable{ - + use CancellableTrait; } diff --git a/src/pocketmine/event/level/SpawnChangeEvent.php b/src/event/world/SpawnChangeEvent.php similarity index 77% rename from src/pocketmine/event/level/SpawnChangeEvent.php rename to src/event/world/SpawnChangeEvent.php index 4e73669dff..dba959a649 100644 --- a/src/pocketmine/event/level/SpawnChangeEvent.php +++ b/src/event/world/SpawnChangeEvent.php @@ -21,21 +21,21 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; -use pocketmine\level\Level; -use pocketmine\level\Position; +use pocketmine\world\Position; +use pocketmine\world\World; /** - * An event that is called when a level spawn changes. + * An event that is called when a world spawn changes. * The previous spawn is included */ -class SpawnChangeEvent extends LevelEvent{ +class SpawnChangeEvent extends WorldEvent{ /** @var Position */ private $previousSpawn; - public function __construct(Level $level, Position $previousSpawn){ - parent::__construct($level); + public function __construct(World $world, Position $previousSpawn){ + parent::__construct($world); $this->previousSpawn = $previousSpawn; } diff --git a/src/pocketmine/event/level/LevelEvent.php b/src/event/world/WorldEvent.php similarity index 72% rename from src/pocketmine/event/level/LevelEvent.php rename to src/event/world/WorldEvent.php index 16d4b01f8d..c842825ae4 100644 --- a/src/pocketmine/event/level/LevelEvent.php +++ b/src/event/world/WorldEvent.php @@ -22,22 +22,22 @@ declare(strict_types=1); /** - * Level related events + * World related events */ -namespace pocketmine\event\level; +namespace pocketmine\event\world; use pocketmine\event\Event; -use pocketmine\level\Level; +use pocketmine\world\World; -abstract class LevelEvent extends Event{ - /** @var Level */ - private $level; +abstract class WorldEvent extends Event{ + /** @var World */ + private $world; - public function __construct(Level $level){ - $this->level = $level; + public function __construct(World $world){ + $this->world = $world; } - public function getLevel() : Level{ - return $this->level; + public function getWorld() : World{ + return $this->world; } } diff --git a/src/pocketmine/event/level/LevelInitEvent.php b/src/event/world/WorldInitEvent.php similarity index 86% rename from src/pocketmine/event/level/LevelInitEvent.php rename to src/event/world/WorldInitEvent.php index 7bf1b4c256..4b85571ed9 100644 --- a/src/pocketmine/event/level/LevelInitEvent.php +++ b/src/event/world/WorldInitEvent.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; /** - * Called when a Level is initializing + * Called when a World is initializing */ -class LevelInitEvent extends LevelEvent{ +class WorldInitEvent extends WorldEvent{ } diff --git a/src/pocketmine/event/level/LevelLoadEvent.php b/src/event/world/WorldLoadEvent.php similarity index 87% rename from src/pocketmine/event/level/LevelLoadEvent.php rename to src/event/world/WorldLoadEvent.php index 32e58bbc37..18235c29b6 100644 --- a/src/pocketmine/event/level/LevelLoadEvent.php +++ b/src/event/world/WorldLoadEvent.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; /** - * Called when a Level is loaded + * Called when a World is loaded */ -class LevelLoadEvent extends LevelEvent{ +class WorldLoadEvent extends WorldEvent{ } diff --git a/src/pocketmine/event/level/LevelSaveEvent.php b/src/event/world/WorldSaveEvent.php similarity index 87% rename from src/pocketmine/event/level/LevelSaveEvent.php rename to src/event/world/WorldSaveEvent.php index 5e077f343a..e438947911 100644 --- a/src/pocketmine/event/level/LevelSaveEvent.php +++ b/src/event/world/WorldSaveEvent.php @@ -21,11 +21,11 @@ declare(strict_types=1); -namespace pocketmine\event\level; +namespace pocketmine\event\world; /** - * Called when a Level is saved + * Called when a World is saved */ -class LevelSaveEvent extends LevelEvent{ +class WorldSaveEvent extends WorldEvent{ } diff --git a/src/event/world/WorldUnloadEvent.php b/src/event/world/WorldUnloadEvent.php new file mode 100644 index 0000000000..7f5b3ff8b4 --- /dev/null +++ b/src/event/world/WorldUnloadEvent.php @@ -0,0 +1,34 @@ +holder = $holder; + parent::__construct(4); + } + + public function getHolder() : Living{ + return $this->holder; + } + + public function getHelmet() : Item{ + return $this->getItem(self::SLOT_HEAD); + } + + public function getChestplate() : Item{ + return $this->getItem(self::SLOT_CHEST); + } + + public function getLeggings() : Item{ + return $this->getItem(self::SLOT_LEGS); + } + + public function getBoots() : Item{ + return $this->getItem(self::SLOT_FEET); + } + + public function setHelmet(Item $helmet) : void{ + $this->setItem(self::SLOT_HEAD, $helmet); + } + + public function setChestplate(Item $chestplate) : void{ + $this->setItem(self::SLOT_CHEST, $chestplate); + } + + public function setLeggings(Item $leggings) : void{ + $this->setItem(self::SLOT_LEGS, $leggings); + } + + public function setBoots(Item $boots) : void{ + $this->setItem(self::SLOT_FEET, $boots); + } +} diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php new file mode 100644 index 0000000000..c5b854a6df --- /dev/null +++ b/src/inventory/BaseInventory.php @@ -0,0 +1,376 @@ + + */ + protected $listeners; + + public function __construct(){ + $this->listeners = new ObjectSet(); + } + + public function getMaxStackSize() : int{ + return $this->maxStackSize; + } + + /** + * @param Item[] $items + */ + abstract protected function internalSetContents(array $items) : void; + + /** + * @param Item[] $items + */ + public function setContents(array $items) : void{ + if(count($items) > $this->getSize()){ + $items = array_slice($items, 0, $this->getSize(), true); + } + + $oldContents = $this->getContents(true); + + $listeners = $this->listeners->toArray(); + $this->listeners->clear(); + $viewers = $this->viewers; + $this->viewers = []; + + $this->internalSetContents($items); + + $this->listeners->add(...$listeners); //don't directly write, in case listeners were added while operation was in progress + foreach($viewers as $id => $viewer){ + $this->viewers[$id] = $viewer; + } + + $this->onContentChange($oldContents); + } + + abstract protected function internalSetItem(int $index, Item $item) : void; + + public function setItem(int $index, Item $item) : void{ + if($item->isNull()){ + $item = ItemFactory::air(); + }else{ + $item = clone $item; + } + + $oldItem = $this->getItem($index); + + $this->internalSetItem($index, $item); + $this->onSlotChange($index, $oldItem); + } + + public function contains(Item $item) : bool{ + $count = max(1, $item->getCount()); + $checkDamage = !$item->hasAnyDamageValue(); + $checkTags = $item->hasNamedTag(); + foreach($this->getContents() as $i){ + if($item->equals($i, $checkDamage, $checkTags)){ + $count -= $i->getCount(); + if($count <= 0){ + return true; + } + } + } + + return false; + } + + public function all(Item $item) : array{ + $slots = []; + $checkDamage = !$item->hasAnyDamageValue(); + $checkTags = $item->hasNamedTag(); + foreach($this->getContents() as $index => $i){ + if($item->equals($i, $checkDamage, $checkTags)){ + $slots[$index] = $i; + } + } + + return $slots; + } + + public function remove(Item $item) : void{ + $checkDamage = !$item->hasAnyDamageValue(); + $checkTags = $item->hasNamedTag(); + + foreach($this->getContents() as $index => $i){ + if($item->equals($i, $checkDamage, $checkTags)){ + $this->clear($index); + } + } + } + + public function first(Item $item, bool $exact = false) : int{ + $count = $exact ? $item->getCount() : max(1, $item->getCount()); + $checkDamage = $exact || !$item->hasAnyDamageValue(); + $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))){ + return $index; + } + } + + return -1; + } + + public function firstEmpty() : int{ + foreach($this->getContents(true) as $i => $slot){ + if($slot->isNull()){ + return $i; + } + } + + return -1; + } + + public function isSlotEmpty(int $index) : bool{ + return $this->getItem($index)->isNull(); + } + + 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); + if($item->canStackWith($slot)){ + if(($diff = min($slot->getMaxStackSize(), $item->getMaxStackSize()) - $slot->getCount()) > 0){ + $count -= $diff; + } + }elseif($slot->isNull()){ + $count -= min($this->getMaxStackSize(), $item->getMaxStackSize()); + } + + if($count <= 0){ + return $item->getCount(); + } + } + + return $item->getCount() - $count; + } + + public function addItem(Item ...$slots) : array{ + /** @var Item[] $itemSlots */ + /** @var Item[] $slots */ + $itemSlots = []; + foreach($slots as $slot){ + if(!$slot->isNull()){ + $itemSlots[] = clone $slot; + } + } + + /** @var Item[] $returnSlots */ + $returnSlots = []; + + foreach($itemSlots as $item){ + $leftover = $this->internalAddItem($item); + if(!$leftover->isNull()){ + $returnSlots[] = $leftover; + } + } + + return $returnSlots; + } + + private function internalAddItem(Item $slot) : Item{ + $emptySlots = []; + + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ + $item = $this->getItem($i); + if($item->isNull()){ + $emptySlots[] = $i; + } + + if($slot->canStackWith($item) and $item->getCount() < $item->getMaxStackSize()){ + $amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize()); + if($amount > 0){ + $slot->setCount($slot->getCount() - $amount); + $item->setCount($item->getCount() + $amount); + $this->setItem($i, $item); + if($slot->getCount() <= 0){ + break; + } + } + } + } + + if(count($emptySlots) > 0){ + foreach($emptySlots as $slotIndex){ + $amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize()); + $slot->setCount($slot->getCount() - $amount); + $item = clone $slot; + $item->setCount($amount); + $this->setItem($slotIndex, $item); + if($slot->getCount() <= 0){ + break; + } + } + } + + return $slot; + } + + public function removeItem(Item ...$slots) : array{ + /** @var Item[] $itemSlots */ + /** @var Item[] $slots */ + $itemSlots = []; + foreach($slots as $slot){ + if(!$slot->isNull()){ + $itemSlots[] = clone $slot; + } + } + + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ + $item = $this->getItem($i); + if($item->isNull()){ + continue; + } + + foreach($itemSlots as $index => $slot){ + if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasNamedTag())){ + $amount = min($item->getCount(), $slot->getCount()); + $slot->setCount($slot->getCount() - $amount); + $item->setCount($item->getCount() - $amount); + $this->setItem($i, $item); + if($slot->getCount() <= 0){ + unset($itemSlots[$index]); + } + } + } + + if(count($itemSlots) === 0){ + break; + } + } + + return $itemSlots; + } + + public function clear(int $index) : void{ + $this->setItem($index, ItemFactory::air()); + } + + public function clearAll() : void{ + $this->setContents([]); + } + + public function swap(int $slot1, int $slot2) : void{ + $i1 = $this->getItem($slot1); + $i2 = $this->getItem($slot2); + $this->setItem($slot1, $i2); + $this->setItem($slot2, $i1); + } + + /** + * @return Player[] + */ + public function getViewers() : array{ + return $this->viewers; + } + + /** + * Removes the inventory window from all players currently viewing it. + */ + public function removeAllViewers() : void{ + foreach($this->viewers as $hash => $viewer){ + if($viewer->getCurrentWindow() === $this){ //this might not be the case for the player's own inventory + $viewer->removeCurrentWindow(); + } + unset($this->viewers[$hash]); + } + } + + public function setMaxStackSize(int $size) : void{ + $this->maxStackSize = $size; + } + + public function onOpen(Player $who) : void{ + $this->viewers[spl_object_id($who)] = $who; + } + + public function onClose(Player $who) : void{ + unset($this->viewers[spl_object_id($who)]); + } + + protected function onSlotChange(int $index, Item $before) : void{ + foreach($this->listeners as $listener){ + $listener->onSlotChange($this, $index, $before); + } + foreach($this->viewers as $viewer){ + $invManager = $viewer->getNetworkSession()->getInvManager(); + if($invManager === null){ + continue; + } + $invManager->syncSlot($this, $index); + } + } + + /** + * @param Item[] $itemsBefore + * @phpstan-param array $itemsBefore + */ + protected function onContentChange(array $itemsBefore) : void{ + foreach($this->listeners as $listener){ + $listener->onContentChange($this, $itemsBefore); + } + + foreach($this->getViewers() as $viewer){ + $invManager = $viewer->getNetworkSession()->getInvManager(); + if($invManager === null){ + continue; + } + $invManager->syncContents($this); + } + } + + public function slotExists(int $slot) : bool{ + return $slot >= 0 and $slot < $this->getSize(); + } + + public function getListeners() : ObjectSet{ + return $this->listeners; + } +} diff --git a/src/inventory/CallbackInventoryListener.php b/src/inventory/CallbackInventoryListener.php new file mode 100644 index 0000000000..8d08c4c78f --- /dev/null +++ b/src/inventory/CallbackInventoryListener.php @@ -0,0 +1,88 @@ +onSlotChangeCallback = $onSlotChange; + $this->onContentChangeCallback = $onContentChange; + } + + /** + * @phpstan-param \Closure(Inventory) : void $onChange + */ + public static function onAnyChange(\Closure $onChange) : self{ + return new self( + static function(Inventory $inventory, int $unused, Item $unusedB) use ($onChange) : void{ + $onChange($inventory); + }, + static function(Inventory $inventory, array $unused) use ($onChange) : void{ + $onChange($inventory); + } + ); + } + + public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{ + if($this->onSlotChangeCallback !== null){ + ($this->onSlotChangeCallback)($inventory, $slot, $oldItem); + } + } + + /** + * @param Item[] $oldContents + */ + public function onContentChange(Inventory $inventory, array $oldContents) : void{ + if($this->onContentChangeCallback !== null){ + ($this->onContentChangeCallback)($inventory, $oldContents); + } + } +} diff --git a/src/inventory/CreativeInventory.php b/src/inventory/CreativeInventory.php new file mode 100644 index 0000000000..e71966f446 --- /dev/null +++ b/src/inventory/CreativeInventory.php @@ -0,0 +1,102 @@ +getName() === "Unknown"){ + continue; + } + $this->add($item); + } + } + + /** + * Removes all previously added items from the creative menu. + * Note: Players who are already online when this is called will not see this change. + */ + public function clear() : void{ + $this->creative = []; + } + + /** + * @return Item[] + */ + public function getAll() : array{ + return $this->creative; + } + + public function getItem(int $index) : ?Item{ + return $this->creative[$index] ?? null; + } + + public function getItemIndex(Item $item) : int{ + foreach($this->creative as $i => $d){ + if($item->equals($d, !($item instanceof Durable))){ + return $i; + } + } + + return -1; + } + + /** + * Adds an item to the creative menu. + * Note: Players who are already online when this is called will not see this change. + */ + public function add(Item $item) : void{ + $this->creative[] = clone $item; + } + + /** + * Removes an item from the creative menu. + * Note: Players who are already online when this is called will not see this change. + */ + public function remove(Item $item) : void{ + $index = $this->getItemIndex($item); + if($index !== -1){ + unset($this->creative[$index]); + } + } + + public function contains(Item $item) : bool{ + return $this->getItemIndex($item) !== -1; + } +} diff --git a/src/inventory/DelegateInventory.php b/src/inventory/DelegateInventory.php new file mode 100644 index 0000000000..484df6d24e --- /dev/null +++ b/src/inventory/DelegateInventory.php @@ -0,0 +1,78 @@ +backingInventory = $backingInventory; + $this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener( + function(Inventory $unused, int $slot, Item $oldItem) : void{ + $this->onSlotChange($slot, $oldItem); + }, + function(Inventory $unused, array $oldContents) : void{ + $this->onContentChange($oldContents); + } + )); + } + + public function getSize() : int{ + return $this->backingInventory->getSize(); + } + + public function getItem(int $index) : Item{ + return $this->backingInventory->getItem($index); + } + + protected function internalSetItem(int $index, Item $item) : void{ + $this->backingInventory->setItem($index, $item); + } + + public function getContents(bool $includeEmpty = false) : array{ + return $this->backingInventory->getContents($includeEmpty); + } + + protected function internalSetContents(array $items) : void{ + $this->backingInventory->setContents($items); + } + + public function onClose(Player $who) : void{ + parent::onClose($who); + if(count($this->getViewers()) === 0 && count($this->getListeners()->toArray()) === 1){ + $this->backingInventory->getListeners()->remove($this->inventoryListener); + $this->inventoryListener = CallbackInventoryListener::onAnyChange(static function() : void{}); //break cyclic reference + } + } +} diff --git a/src/pocketmine/inventory/Inventory.php b/src/inventory/Inventory.php similarity index 73% rename from src/pocketmine/inventory/Inventory.php rename to src/inventory/Inventory.php index f950b73af6..b1fad3904d 100644 --- a/src/pocketmine/inventory/Inventory.php +++ b/src/inventory/Inventory.php @@ -27,9 +27,8 @@ declare(strict_types=1); namespace pocketmine\inventory; use pocketmine\item\Item; -use pocketmine\level\Level; -use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\utils\ObjectSet; interface Inventory{ public const MAX_STACK = 64; @@ -40,17 +39,12 @@ interface Inventory{ public function setMaxStackSize(int $size) : void; - public function getName() : string; - - public function getTitle() : string; - public function getItem(int $index) : Item; /** * Puts an Item in a slot. - * If a plugin refuses the update or $index is invalid, it'll return false */ - public function setItem(int $index, Item $item, bool $send = true) : bool; + public function setItem(int $index, Item $item) : void; /** * Stores the given Items in the inventory. This will try to fill @@ -69,6 +63,11 @@ 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. @@ -87,23 +86,7 @@ interface Inventory{ /** * @param Item[] $items */ - public function setContents(array $items, bool $send = true) : void; - - /** - * Drops the contents of the inventory into the specified Level at the specified position and clears the inventory - * contents. - */ - public function dropContents(Level $level, Vector3 $position) : void; - - /** - * @param Player|Player[] $target - */ - public function sendContents($target) : void; - - /** - * @param Player|Player[] $target - */ - public function sendSlot(int $index, $target) : void; + public function setContents(array $items) : void; /** * Checks if the inventory contains any Item with the same material data. @@ -145,12 +128,17 @@ interface Inventory{ /** * Will clear a specific slot */ - public function clear(int $index, bool $send = true) : bool; + public function clear(int $index) : void; /** * Clears all the slots */ - public function clearAll(bool $send = true) : void; + public function clearAll() : void; + + /** + * Swaps the specified slots. + */ + public function swap(int $slot1, int $slot2) : void; /** * Gets all the Players viewing the inventory @@ -160,25 +148,21 @@ interface Inventory{ */ public function getViewers() : array; + /** + * Called when a player opens this inventory. + */ public function onOpen(Player $who) : void; - /** - * Tries to open the inventory to a player - */ - public function open(Player $who) : bool; - - public function close(Player $who) : void; - public function onClose(Player $who) : void; - public function onSlotChange(int $index, Item $before, bool $send) : void; - /** * Returns whether the specified slot exists in the inventory. */ public function slotExists(int $slot) : bool; - public function getEventProcessor() : ?InventoryEventProcessor; - - public function setEventProcessor(?InventoryEventProcessor $eventProcessor) : void; + /** + * @return InventoryListener[]|ObjectSet + * @phpstan-return ObjectSet + */ + public function getListeners() : ObjectSet; } diff --git a/src/pocketmine/inventory/InventoryHolder.php b/src/inventory/InventoryHolder.php similarity index 100% rename from src/pocketmine/inventory/InventoryHolder.php rename to src/inventory/InventoryHolder.php diff --git a/src/pocketmine/inventory/ArmorInventoryEventProcessor.php b/src/inventory/InventoryListener.php similarity index 64% rename from src/pocketmine/inventory/ArmorInventoryEventProcessor.php rename to src/inventory/InventoryListener.php index f0866571e7..c5040993ff 100644 --- a/src/pocketmine/inventory/ArmorInventoryEventProcessor.php +++ b/src/inventory/InventoryListener.php @@ -23,25 +23,20 @@ declare(strict_types=1); namespace pocketmine\inventory; -use pocketmine\entity\Entity; -use pocketmine\event\entity\EntityArmorChangeEvent; use pocketmine\item\Item; -class ArmorInventoryEventProcessor implements InventoryEventProcessor{ - /** @var Entity */ - private $entity; +/** + * Classes implementing this interface can be injected into inventories to receive notifications when content changes + * occur. + * @see CallbackInventoryListener for a closure-based listener + * @see Inventory::getListeners() + */ +interface InventoryListener{ - public function __construct(Entity $entity){ - $this->entity = $entity; - } + public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void; - public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem, Item $newItem) : ?Item{ - $ev = new EntityArmorChangeEvent($this->entity, $oldItem, $newItem, $slot); - $ev->call(); - if($ev->isCancelled()){ - return null; - } - - return $ev->getNewItem(); - } + /** + * @param Item[] $oldContents + */ + public function onContentChange(Inventory $inventory, array $oldContents) : void; } diff --git a/src/pocketmine/network/mcpe/protocol/types/GeneratorType.php b/src/inventory/PlayerCraftingInventory.php similarity index 70% rename from src/pocketmine/network/mcpe/protocol/types/GeneratorType.php rename to src/inventory/PlayerCraftingInventory.php index 778978fb51..d3a8174342 100644 --- a/src/pocketmine/network/mcpe/protocol/types/GeneratorType.php +++ b/src/inventory/PlayerCraftingInventory.php @@ -21,17 +21,16 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\inventory; -final class GeneratorType{ +use pocketmine\crafting\CraftingGrid; +use pocketmine\player\Player; - private function __construct(){ - //NOOP +final class PlayerCraftingInventory extends CraftingGrid implements TemporaryInventory{ + + public function __construct(private Player $holder){ + parent::__construct(CraftingGrid::SIZE_SMALL); } - public const FINITE_OVERWORLD = 0; - public const OVERWORLD = 1; - public const FLAT = 2; - public const NETHER = 3; - public const THE_END = 4; -} + public function getHolder() : Player{ return $this->holder; } +} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/protocol/types/EducationEditionOffer.php b/src/inventory/PlayerCursorInventory.php similarity index 69% rename from src/pocketmine/network/mcpe/protocol/types/EducationEditionOffer.php rename to src/inventory/PlayerCursorInventory.php index eedf874aac..7c219fd007 100644 --- a/src/pocketmine/network/mcpe/protocol/types/EducationEditionOffer.php +++ b/src/inventory/PlayerCursorInventory.php @@ -21,15 +21,23 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\inventory; -final class EducationEditionOffer{ +use pocketmine\player\Player; - private function __construct(){ - //NOOP +class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{ + /** @var Player */ + protected $holder; + + public function __construct(Player $holder){ + $this->holder = $holder; + parent::__construct(1); } - public const NONE = 0; - public const EVERYWHERE_EXCEPT_CHINA = 1; - public const CHINA = 2; + /** + * @return Player + */ + public function getHolder(){ + return $this->holder; + } } diff --git a/src/pocketmine/block/TripwireHook.php b/src/inventory/PlayerEnderInventory.php similarity index 69% rename from src/pocketmine/block/TripwireHook.php rename to src/inventory/PlayerEnderInventory.php index 8bf328573a..c1282b341d 100644 --- a/src/pocketmine/block/TripwireHook.php +++ b/src/inventory/PlayerEnderInventory.php @@ -21,23 +21,18 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\inventory; -class TripwireHook extends Flowable{ +use pocketmine\entity\Human; - protected $id = self::TRIPWIRE_HOOK; +final class PlayerEnderInventory extends SimpleInventory{ - public function __construct(int $meta = 0){ - $this->meta = $meta; + private Human $holder; + + public function __construct(Human $holder, int $size = 27){ + $this->holder = $holder; + parent::__construct($size); } - public function getName() : string{ - return "Tripwire Hook"; - } - - public function getVariantBitmask() : int{ - return 0; - } - - //TODO + public function getHolder() : Human{ return $this->holder; } } diff --git a/src/inventory/PlayerInventory.php b/src/inventory/PlayerInventory.php new file mode 100644 index 0000000000..253ddfdeaf --- /dev/null +++ b/src/inventory/PlayerInventory.php @@ -0,0 +1,132 @@ + + */ + protected $heldItemIndexChangeListeners; + + public function __construct(Human $player){ + $this->holder = $player; + $this->heldItemIndexChangeListeners = new ObjectSet(); + parent::__construct(36); + } + + public function isHotbarSlot(int $slot) : bool{ + return $slot >= 0 and $slot <= $this->getHotbarSize(); + } + + /** + * @throws \InvalidArgumentException + */ + private function throwIfNotHotbarSlot(int $slot) : void{ + if(!$this->isHotbarSlot($slot)){ + throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getHotbarSize() - 1) . ")"); + } + } + + /** + * Returns the item in the specified hotbar slot. + * + * @throws \InvalidArgumentException if the hotbar slot index is out of range + */ + public function getHotbarSlotItem(int $hotbarSlot) : Item{ + $this->throwIfNotHotbarSlot($hotbarSlot); + return $this->getItem($hotbarSlot); + } + + /** + * Returns the hotbar slot number the holder is currently holding. + */ + public function getHeldItemIndex() : int{ + return $this->itemInHandIndex; + } + + /** + * Sets which hotbar slot the player is currently loading. + * + * @param int $hotbarSlot 0-8 index of the hotbar slot to hold + * + * @throws \InvalidArgumentException if the hotbar slot is out of range + */ + public function setHeldItemIndex(int $hotbarSlot) : void{ + $this->throwIfNotHotbarSlot($hotbarSlot); + + $oldIndex = $this->itemInHandIndex; + $this->itemInHandIndex = $hotbarSlot; + + foreach($this->heldItemIndexChangeListeners as $callback){ + $callback($oldIndex); + } + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(int $oldIndex) : void> + */ + public function getHeldItemIndexChangeListeners() : ObjectSet{ return $this->heldItemIndexChangeListeners; } + + /** + * Returns the currently-held item. + */ + public function getItemInHand() : Item{ + return $this->getHotbarSlotItem($this->itemInHandIndex); + } + + /** + * Sets the item in the currently-held slot to the specified item. + */ + public function setItemInHand(Item $item) : void{ + $this->setItem($this->getHeldItemIndex(), $item); + } + + /** + * Returns the number of slots in the hotbar. + */ + public function getHotbarSize() : int{ + return 9; + } + + /** + * @return Human|Player + */ + public function getHolder(){ + return $this->holder; + } +} diff --git a/src/pocketmine/inventory/CustomInventory.php b/src/inventory/PlayerOffHandInventory.php similarity index 72% rename from src/pocketmine/inventory/CustomInventory.php rename to src/inventory/PlayerOffHandInventory.php index 4fb2eff480..1db8d63e0c 100644 --- a/src/pocketmine/inventory/CustomInventory.php +++ b/src/inventory/PlayerOffHandInventory.php @@ -23,9 +23,16 @@ declare(strict_types=1); namespace pocketmine\inventory; -/** - * All plugins that want to create their custom inventory should use this class as a base - */ -abstract class CustomInventory extends ContainerInventory{ +use pocketmine\entity\Human; +final class PlayerOffHandInventory extends SimpleInventory{ + /** @var Human */ + private $holder; + + public function __construct(Human $player){ + $this->holder = $player; + parent::__construct(1); + } + + public function getHolder() : Human{ return $this->holder; } } diff --git a/src/inventory/SimpleInventory.php b/src/inventory/SimpleInventory.php new file mode 100644 index 0000000000..f05b527a23 --- /dev/null +++ b/src/inventory/SimpleInventory.php @@ -0,0 +1,85 @@ + + */ + protected \SplFixedArray $slots; + + public function __construct(int $size){ + $this->slots = new \SplFixedArray($size); + parent::__construct(); + } + + /** + * Returns the size of the inventory. + */ + public function getSize() : int{ + return $this->slots->getSize(); + } + + public function getItem(int $index) : Item{ + return $this->slots[$index] !== null ? clone $this->slots[$index] : ItemFactory::air(); + } + + /** + * @return Item[] + */ + public function getContents(bool $includeEmpty = false) : array{ + $contents = []; + + foreach($this->slots as $i => $slot){ + if($slot !== null){ + $contents[$i] = clone $slot; + }elseif($includeEmpty){ + $contents[$i] = ItemFactory::air(); + } + } + + return $contents; + } + + protected function internalSetContents(array $items) : void{ + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ + if(!isset($items[$i])){ + $this->clear($i); + }else{ + $this->setItem($i, $items[$i]); + } + } + } + + protected function internalSetItem(int $index, Item $item) : void{ + $this->slots[$index] = $item->isNull() ? null : $item; + } +} diff --git a/src/inventory/TemporaryInventory.php b/src/inventory/TemporaryInventory.php new file mode 100644 index 0000000000..9e239dcdf9 --- /dev/null +++ b/src/inventory/TemporaryInventory.php @@ -0,0 +1,28 @@ +craftingManager = $craftingManager; + } /** * @param Item[] $txItems @@ -77,7 +84,7 @@ class CraftingTransaction extends InventoryTransaction{ $recipeItem = array_pop($recipeItems); $needCount = $recipeItem->getCount(); foreach($recipeItems as $i => $otherRecipeItem){ - if($otherRecipeItem->equals($recipeItem)){ //make sure they have the same wildcards set + if($otherRecipeItem->canStackWith($recipeItem)){ //make sure they have the same wildcards set $needCount += $otherRecipeItem->getCount(); unset($recipeItems[$i]); } @@ -85,7 +92,7 @@ class CraftingTransaction extends InventoryTransaction{ $haveCount = 0; foreach($txItems as $j => $txItem){ - if($txItem->equals($recipeItem, !$wildcards or !$recipeItem->hasAnyDamageValue(), !$wildcards or $recipeItem->hasCompoundTag())){ + if($txItem->equals($recipeItem, !$wildcards or !$recipeItem->hasAnyDamageValue(), !$wildcards or $recipeItem->hasNamedTag())){ $haveCount += $txItem->getCount(); unset($txItems[$j]); } @@ -125,7 +132,7 @@ class CraftingTransaction extends InventoryTransaction{ $this->matchItems($this->outputs, $this->inputs); $failed = 0; - foreach($this->source->getServer()->getCraftingManager()->matchRecipeByOutputs($this->outputs) as $recipe){ + foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){ try{ //compute number of times recipe was crafted $this->repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false); @@ -151,62 +158,4 @@ class CraftingTransaction extends InventoryTransaction{ $ev->call(); return !$ev->isCancelled(); } - - protected function sendInventories() : void{ - parent::sendInventories(); - - /* - * 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. - */ - $pk = new ContainerClosePacket(); - $pk->windowId = Player::HARDCODED_CRAFTING_GRID_WINDOW_ID; - $pk->server = true; - $this->source->dataPacket($pk); - } - - public function execute() : bool{ - if(parent::execute()){ - foreach($this->outputs as $item){ - switch($item->getId()){ - case Item::CRAFTING_TABLE: - $this->source->awardAchievement("buildWorkBench"); - break; - case Item::WOODEN_PICKAXE: - $this->source->awardAchievement("buildPickaxe"); - break; - case Item::FURNACE: - $this->source->awardAchievement("buildFurnace"); - break; - case Item::WOODEN_HOE: - $this->source->awardAchievement("buildHoe"); - break; - case Item::BREAD: - $this->source->awardAchievement("makeBread"); - break; - case Item::CAKE: - $this->source->awardAchievement("bakeCake"); - break; - case Item::STONE_PICKAXE: - case Item::GOLDEN_PICKAXE: - case Item::IRON_PICKAXE: - case Item::DIAMOND_PICKAXE: - $this->source->awardAchievement("buildBetterPickaxe"); - break; - case Item::WOODEN_SWORD: - $this->source->awardAchievement("buildSword"); - break; - case Item::DIAMOND: - $this->source->awardAchievement("diamond"); - break; - } - } - - return true; - } - - return false; - } } diff --git a/src/pocketmine/inventory/transaction/InventoryTransaction.php b/src/inventory/transaction/InventoryTransaction.php similarity index 90% rename from src/pocketmine/inventory/transaction/InventoryTransaction.php rename to src/inventory/transaction/InventoryTransaction.php index 6d005651a3..d79b07cd1c 100644 --- a/src/pocketmine/inventory/transaction/InventoryTransaction.php +++ b/src/inventory/transaction/InventoryTransaction.php @@ -28,7 +28,7 @@ use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\InventoryAction; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; use function array_keys; use function array_values; use function assert; @@ -37,6 +37,7 @@ use function get_class; use function min; use function shuffle; use function spl_object_hash; +use function spl_object_id; /** * This is the basic type for an inventory transaction. This is used for moving items between inventories, dropping @@ -99,7 +100,7 @@ class InventoryTransaction{ } public function addAction(InventoryAction $action) : void{ - if(!isset($this->actions[$hash = spl_object_hash($action)])){ + if(!isset($this->actions[$hash = spl_object_id($action)])){ $this->actions[$hash] = $action; $action->onAddToTransaction($this); }else{ @@ -125,7 +126,7 @@ class InventoryTransaction{ * involving inventories. */ public function addInventory(Inventory $inventory) : void{ - if(!isset($this->inventories[$hash = spl_object_hash($inventory)])){ + if(!isset($this->inventories[$hash = spl_object_id($inventory)])){ $this->inventories[$hash] = $inventory; } } @@ -144,8 +145,10 @@ class InventoryTransaction{ $needItems[] = $action->getTargetItem(); } - if(!$action->isValid($this->source)){ - throw new TransactionValidationException("Action " . get_class($action) . " is not valid in the current transaction"); + try{ + $action->validate($this->source); + }catch(TransactionValidationException $e){ + throw new TransactionValidationException(get_class($action) . ": " . $e->getMessage(), 0, $e); } if(!$action->getSourceItem()->isNull()){ @@ -155,7 +158,7 @@ class InventoryTransaction{ foreach($needItems as $i => $needItem){ foreach($haveItems as $j => $haveItem){ - if($needItem->equals($haveItem)){ + if($needItem->canStackWith($haveItem)){ $amount = min($needItem->getCount(), $haveItem->getCount()); $needItem->setCount($needItem->getCount() - $amount); $haveItem->setCount($haveItem->getCount() - $amount); @@ -217,7 +220,7 @@ class InventoryTransaction{ } foreach($list as $action){ - unset($this->actions[spl_object_hash($action)]); + unset($this->actions[spl_object_id($action)]); } if(!$targetItem->equalsExact($sourceItem)){ @@ -286,12 +289,6 @@ class InventoryTransaction{ } } - protected function sendInventories() : void{ - foreach($this->inventories as $inventory){ - $inventory->sendContents($this->source); - } - } - protected function callExecuteEvent() : bool{ $ev = new InventoryTransactionEvent($this); $ev->call(); @@ -301,46 +298,32 @@ class InventoryTransaction{ /** * Executes the group of actions, returning whether the transaction executed successfully or not. * - * @throws TransactionValidationException + * @throws TransactionException */ - public function execute() : bool{ + public function execute() : void{ if($this->hasExecuted()){ - $this->sendInventories(); - return false; + throw new TransactionValidationException("Transaction has already been executed"); } $this->shuffleActions(); - try{ - $this->validate(); - }catch(TransactionValidationException $e){ - $this->sendInventories(); - throw $e; - } + $this->validate(); if(!$this->callExecuteEvent()){ - $this->sendInventories(); - return false; + throw new TransactionCancelledException("Transaction event cancelled"); } foreach($this->actions as $action){ if(!$action->onPreExecute($this->source)){ - $this->sendInventories(); - return false; + throw new TransactionCancelledException("One of the actions in this transaction was cancelled"); } } foreach($this->actions as $action){ - if($action->execute($this->source)){ - $action->onExecuteSuccess($this->source); - }else{ - $action->onExecuteFail($this->source); - } + $action->execute($this->source); } $this->hasExecuted = true; - - return true; } public function hasExecuted() : bool{ diff --git a/src/inventory/transaction/TransactionBuilderInventory.php b/src/inventory/transaction/TransactionBuilderInventory.php new file mode 100644 index 0000000000..8888b4a452 --- /dev/null +++ b/src/inventory/transaction/TransactionBuilderInventory.php @@ -0,0 +1,106 @@ + + */ + private \SplFixedArray $changedSlots; + + public function __construct( + private Inventory $actualInventory + ){ + parent::__construct(); + $this->changedSlots = new \SplFixedArray($this->actualInventory->getSize()); + } + + protected function internalSetContents(array $items) : void{ + for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ + if(!isset($items[$i])){ + $this->clear($i); + }else{ + $this->setItem($i, $items[$i]); + } + } + } + + protected function internalSetItem(int $index, Item $item) : void{ + if(!$item->equalsExact($this->actualInventory->getItem($index))){ + $this->changedSlots[$index] = $item->isNull() ? ItemFactory::air() : clone $item; + } + } + + public function getSize() : int{ + return $this->actualInventory->getSize(); + } + + public function getItem(int $index) : Item{ + return $this->changedSlots[$index] !== null ? clone $this->changedSlots[$index] : $this->actualInventory->getItem($index); + } + + public function getContents(bool $includeEmpty = false) : array{ + $contents = $this->actualInventory->getContents($includeEmpty); + foreach($this->changedSlots as $index => $item){ + if($item !== null){ + if($includeEmpty || !$item->isNull()){ + $contents[$index] = clone $item; + }else{ + unset($contents[$index]); + } + } + } + return $contents; + } + + /** + * @return SlotChangeAction[] + */ + public function generateActions() : array{ + $result = []; + foreach($this->changedSlots as $index => $newItem){ + if($newItem !== null){ + $oldItem = $this->actualInventory->getItem($index); + if(!$newItem->equalsExact($oldItem)){ + $result[] = new SlotChangeAction($this->actualInventory, $index, $oldItem, $newItem); + } + } + } + return $result; + } +} diff --git a/src/inventory/transaction/TransactionCancelledException.php b/src/inventory/transaction/TransactionCancelledException.php new file mode 100644 index 0000000000..7db33ed3a4 --- /dev/null +++ b/src/inventory/transaction/TransactionCancelledException.php @@ -0,0 +1,31 @@ +hasFiniteResources()){ + throw new TransactionValidationException("Player has finite resources, cannot create items"); + } + if(!CreativeInventory::getInstance()->contains($this->sourceItem)){ + throw new TransactionValidationException("Creative inventory does not contain requested item"); + } + } + + public function execute(Player $source) : void{ + //NOOP + } +} diff --git a/src/pocketmine/block/Potato.php b/src/inventory/transaction/action/DestroyItemAction.php similarity index 51% rename from src/pocketmine/block/Potato.php rename to src/inventory/transaction/action/DestroyItemAction.php index 14a8aff602..6edd30af2a 100644 --- a/src/pocketmine/block/Potato.php +++ b/src/inventory/transaction/action/DestroyItemAction.php @@ -21,35 +21,30 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\inventory\transaction\action; +use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; use pocketmine\item\ItemFactory; -use function mt_rand; +use pocketmine\player\Player; -class Potato extends Crops{ +/** + * This action type shows up when a creative player puts an item into the creative inventory menu to destroy it. + * The output is the item destroyed. You can think of this action type like setting an item into /dev/null + */ +class DestroyItemAction extends InventoryAction{ - protected $id = self::POTATO_BLOCK; - - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function __construct(Item $targetItem){ + parent::__construct(ItemFactory::air(), $targetItem); } - public function getName() : string{ - return "Potato Block"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - $result = [ - ItemFactory::get(Item::POTATO, 0, $this->getDamage() >= 0x07 ? mt_rand(1, 5) : 1) - ]; - if($this->getDamage() >= 7 && mt_rand(0, 49) === 0){ - $result[] = ItemFactory::get(Item::POISONOUS_POTATO); + public function validate(Player $source) : void{ + if($source->hasFiniteResources()){ + throw new TransactionValidationException("Player has finite resources, cannot destroy items"); } - return $result; } - public function getPickedItem() : Item{ - return ItemFactory::get(Item::POTATO); + public function execute(Player $source) : void{ + //NOOP } } diff --git a/src/pocketmine/inventory/transaction/action/DropItemAction.php b/src/inventory/transaction/action/DropItemAction.php similarity index 76% rename from src/pocketmine/inventory/transaction/action/DropItemAction.php rename to src/inventory/transaction/action/DropItemAction.php index 5dbe496072..2e20d5f93d 100644 --- a/src/pocketmine/inventory/transaction/action/DropItemAction.php +++ b/src/inventory/transaction/action/DropItemAction.php @@ -24,9 +24,10 @@ declare(strict_types=1); 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\Player; +use pocketmine\player\Player; /** * Represents an action involving dropping an item into the world. @@ -34,11 +35,13 @@ use pocketmine\Player; class DropItemAction extends InventoryAction{ public function __construct(Item $targetItem){ - parent::__construct(ItemFactory::get(Item::AIR, 0, 0), $targetItem); + parent::__construct(ItemFactory::air(), $targetItem); } - public function isValid(Player $source) : bool{ - return !$this->targetItem->isNull(); + public function validate(Player $source) : void{ + if($this->targetItem->isNull()){ + throw new TransactionValidationException("Cannot drop an empty itemstack"); + } } public function onPreExecute(Player $source) : bool{ @@ -54,15 +57,7 @@ class DropItemAction extends InventoryAction{ /** * Drops the target item in front of the player. */ - public function execute(Player $source) : bool{ - return $source->dropItem($this->targetItem); - } - - public function onExecuteSuccess(Player $source) : void{ - - } - - public function onExecuteFail(Player $source) : void{ - + public function execute(Player $source) : void{ + $source->dropItem($this->targetItem); } } diff --git a/src/pocketmine/inventory/transaction/action/InventoryAction.php b/src/inventory/transaction/action/InventoryAction.php similarity index 76% rename from src/pocketmine/inventory/transaction/action/InventoryAction.php rename to src/inventory/transaction/action/InventoryAction.php index 5a60521c47..4fa3ac15e5 100644 --- a/src/pocketmine/inventory/transaction/action/InventoryAction.php +++ b/src/inventory/transaction/action/InventoryAction.php @@ -24,8 +24,9 @@ declare(strict_types=1); namespace pocketmine\inventory\transaction\action; use pocketmine\inventory\transaction\InventoryTransaction; +use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; -use pocketmine\Player; +use pocketmine\player\Player; /** * Represents an action involving a change that applies in some way to an inventory or other item-source. @@ -57,8 +58,10 @@ abstract class InventoryAction{ /** * Returns whether this action is currently valid. This should perform any necessary sanity checks. + * + * @throws TransactionValidationException */ - abstract public function isValid(Player $source) : bool; + abstract public function validate(Player $source) : void; /** * Called when the action is added to the specified InventoryTransaction. @@ -76,20 +79,8 @@ abstract class InventoryAction{ } /** - * Performs actions needed to complete the inventory-action server-side. Returns if it was successful. Will return - * false if plugins cancelled events. This will only be called if the transaction which it is part of is considered - * valid. + * Performs actions needed to complete the inventory-action server-side. This will only be called if the transaction + * which it is part of is considered valid. */ - abstract public function execute(Player $source) : bool; - - /** - * Performs additional actions when this inventory-action completed successfully. - */ - abstract public function onExecuteSuccess(Player $source) : void; - - /** - * Performs additional actions when this inventory-action did not complete successfully. - */ - abstract public function onExecuteFail(Player $source) : void; - + abstract public function execute(Player $source) : void; } diff --git a/src/pocketmine/inventory/transaction/action/SlotChangeAction.php b/src/inventory/transaction/action/SlotChangeAction.php similarity index 68% rename from src/pocketmine/inventory/transaction/action/SlotChangeAction.php rename to src/inventory/transaction/action/SlotChangeAction.php index 4f8de5d328..1686c261bb 100644 --- a/src/pocketmine/inventory/transaction/action/SlotChangeAction.php +++ b/src/inventory/transaction/action/SlotChangeAction.php @@ -25,9 +25,9 @@ namespace pocketmine\inventory\transaction\action; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\InventoryTransaction; +use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; -use pocketmine\Player; -use function spl_object_hash; +use pocketmine\player\Player; /** * Represents an action causing a change in an inventory slot. @@ -61,12 +61,16 @@ class SlotChangeAction extends InventoryAction{ /** * Checks if the item in the inventory at the specified slot is the same as this action's source item. + * + * @throws TransactionValidationException */ - public function isValid(Player $source) : bool{ - return ( - $this->inventory->slotExists($this->inventorySlot) and - $this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem) - ); + public function validate(Player $source) : void{ + if(!$this->inventory->slotExists($this->inventorySlot)){ + throw new TransactionValidationException("Slot does not exist"); + } + if(!$this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){ + throw new TransactionValidationException("Slot does not contain expected original item"); + } } /** @@ -79,23 +83,7 @@ class SlotChangeAction extends InventoryAction{ /** * Sets the item into the target inventory. */ - public function execute(Player $source) : bool{ - return $this->inventory->setItem($this->inventorySlot, $this->targetItem, false); - } - - /** - * Sends slot changes to other viewers of the inventory. This will not send any change back to the source Player. - */ - public function onExecuteSuccess(Player $source) : void{ - $viewers = $this->inventory->getViewers(); - unset($viewers[spl_object_hash($source)]); - $this->inventory->sendSlot($this->inventorySlot, $viewers); - } - - /** - * Sends the original slot contents to the source player to revert the action. - */ - public function onExecuteFail(Player $source) : void{ - $this->inventory->sendSlot($this->inventorySlot, $source); + public function execute(Player $source) : void{ + $this->inventory->setItem($this->inventorySlot, $this->targetItem); } } diff --git a/src/pocketmine/item/Apple.php b/src/item/Apple.php similarity index 90% rename from src/pocketmine/item/Apple.php rename to src/item/Apple.php index ea212087a8..5f2a711c01 100644 --- a/src/pocketmine/item/Apple.php +++ b/src/item/Apple.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Apple extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::APPLE, $meta, "Apple"); - } public function getFoodRestore() : int{ return 4; diff --git a/src/item/Armor.php b/src/item/Armor.php new file mode 100644 index 0000000000..39ced43e33 --- /dev/null +++ b/src/item/Armor.php @@ -0,0 +1,153 @@ +armorInfo = $info; + } + + public function getMaxDurability() : int{ + return $this->armorInfo->getMaxDurability(); + } + + public function getDefensePoints() : int{ + return $this->armorInfo->getDefensePoints(); + } + + /** + * @see ArmorInventory + */ + public function getArmorSlot() : int{ + return $this->armorInfo->getArmorSlot(); + } + + public function getMaxStackSize() : int{ + return 1; + } + + /** + * Returns the dyed colour of this armour piece. This generally only applies to leather armour. + */ + public function getCustomColor() : ?Color{ + return $this->customColor; + } + + /** + * Sets the dyed colour of this armour piece. This generally only applies to leather armour. + * + * @return $this + */ + public function setCustomColor(Color $color) : self{ + $this->customColor = $color; + return $this; + } + + /** + * Returns the total enchantment protection factor this armour piece offers from all applicable protection + * enchantments on the item. + */ + public function getEnchantmentProtectionFactor(EntityDamageEvent $event) : int{ + $epf = 0; + + foreach($this->getEnchantments() as $enchantment){ + $type = $enchantment->getType(); + if($type instanceof ProtectionEnchantment and $type->isApplicable($event)){ + $epf += $type->getProtectionFactor($enchantment->getLevel()); + } + } + + return $epf; + } + + protected function getUnbreakingDamageReduction(int $amount) : int{ + if(($unbreakingLevel = $this->getEnchantmentLevel(VanillaEnchantments::UNBREAKING())) > 0){ + $negated = 0; + + $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 + $negated++; + } + } + + return $negated; + } + + return 0; + } + + public function onClickAir(Player $player, Vector3 $directionVector) : ItemUseResult{ + $existing = $player->getArmorInventory()->getItem($this->getArmorSlot()); + $thisCopy = clone $this; + $new = $thisCopy->pop(); + $player->getArmorInventory()->setItem($this->getArmorSlot(), $new); + if($thisCopy->getCount() === 0){ + $player->getInventory()->setItemInHand($existing); + }else{ //if the stack size was bigger than 1 (usually won't happen, but might be caused by plugins + $player->getInventory()->setItemInHand($thisCopy); + $player->getInventory()->addItem($existing); + } + return ItemUseResult::SUCCESS(); + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + if(($colorTag = $tag->getTag(self::TAG_CUSTOM_COLOR)) instanceof IntTag){ + $this->customColor = Color::fromARGB(Binary::unsignInt($colorTag->getValue())); + }else{ + $this->customColor = null; + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + $this->customColor !== null ? + $tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->customColor->toARGB())) : + $tag->removeTag(self::TAG_CUSTOM_COLOR); + } +} diff --git a/src/pocketmine/item/ChainLeggings.php b/src/item/ArmorTypeInfo.php similarity index 64% rename from src/pocketmine/item/ChainLeggings.php rename to src/item/ArmorTypeInfo.php index bbbfd85536..3542a554e8 100644 --- a/src/pocketmine/item/ChainLeggings.php +++ b/src/item/ArmorTypeInfo.php @@ -23,16 +23,30 @@ declare(strict_types=1); namespace pocketmine\item; -class ChainLeggings extends Armor{ - public function __construct(int $meta = 0){ - parent::__construct(self::CHAIN_LEGGINGS, $meta, "Chain Leggings"); +class ArmorTypeInfo{ + + /** @var int */ + private $defensePoints; + /** @var int */ + private $maxDurability; + /** @var int */ + private $armorSlot; + + public function __construct(int $defensePoints, int $maxDurability, int $armorSlot){ + $this->defensePoints = $defensePoints; + $this->maxDurability = $maxDurability; + $this->armorSlot = $armorSlot; } public function getDefensePoints() : int{ - return 4; + return $this->defensePoints; } public function getMaxDurability() : int{ - return 226; + return $this->maxDurability; + } + + public function getArmorSlot() : int{ + return $this->armorSlot; } } diff --git a/src/pocketmine/entity/Damageable.php b/src/item/Arrow.php similarity index 93% rename from src/pocketmine/entity/Damageable.php rename to src/item/Arrow.php index fbc33ff152..592d76141a 100644 --- a/src/pocketmine/entity/Damageable.php +++ b/src/item/Arrow.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\item; -interface Damageable{ +class Arrow extends Item{ } diff --git a/src/pocketmine/item/Axe.php b/src/item/Axe.php similarity index 88% rename from src/pocketmine/item/Axe.php rename to src/item/Axe.php index 1369110e37..15305d05ff 100644 --- a/src/pocketmine/item/Axe.php +++ b/src/item/Axe.php @@ -30,19 +30,19 @@ use pocketmine\entity\Entity; class Axe extends TieredTool{ public function getBlockToolType() : int{ - return BlockToolType::TYPE_AXE; + return BlockToolType::AXE; } public function getBlockToolHarvestLevel() : int{ - return $this->tier; + return $this->tier->getHarvestLevel(); } public function getAttackPoints() : int{ - return self::getBaseDamageFromTier($this->tier) - 1; + return $this->tier->getBaseAttackPoints() - 1; } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() > 0){ + if(!$block->getBreakInfo()->breaksInstantly()){ return $this->applyDamage(1); } return false; diff --git a/src/pocketmine/item/BakedPotato.php b/src/item/BakedPotato.php similarity index 88% rename from src/pocketmine/item/BakedPotato.php rename to src/item/BakedPotato.php index 5c3147137e..c4527e7697 100644 --- a/src/pocketmine/item/BakedPotato.php +++ b/src/item/BakedPotato.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class BakedPotato extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::BAKED_POTATO, $meta, "Baked Potato"); - } public function getFoodRestore() : int{ return 5; diff --git a/src/pocketmine/item/Coal.php b/src/item/Bamboo.php similarity index 79% rename from src/pocketmine/item/Coal.php rename to src/item/Bamboo.php index 9f12c963d9..daf911dc4c 100644 --- a/src/pocketmine/item/Coal.php +++ b/src/item/Bamboo.php @@ -23,15 +23,16 @@ declare(strict_types=1); namespace pocketmine\item; -class Coal extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::COAL, $meta, "Coal"); - if($this->meta === 1){ - $this->name = "Charcoal"; - } - } +use pocketmine\block\Block; +use pocketmine\block\VanillaBlocks; + +final class Bamboo extends Item{ public function getFuelTime() : int{ - return 1600; + return 50; + } + + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::BAMBOO_SAPLING(); } } diff --git a/src/item/Banner.php b/src/item/Banner.php new file mode 100644 index 0000000000..18d0f1081c --- /dev/null +++ b/src/item/Banner.php @@ -0,0 +1,133 @@ + + */ + private $patterns = []; + + public function __construct(ItemIdentifier $identifier, Block $floorVariant, Block $wallVariant){ + parent::__construct($identifier, $floorVariant, $wallVariant); + $this->color = DyeColor::BLACK(); + } + + public function getColor() : DyeColor{ + return $this->color; + } + + /** @return $this */ + public function setColor(DyeColor $color) : self{ + $this->color = $color; + return $this; + } + + public function getMeta() : int{ + return DyeColorIdMap::getInstance()->toInvertedId($this->color); + } + + /** + * @return BannerPatternLayer[] + * @phpstan-return list + */ + public function getPatterns() : array{ + return $this->patterns; + } + + /** + * @param BannerPatternLayer[] $patterns + * + * @phpstan-param list $patterns + * + * @return $this + */ + public function setPatterns(array $patterns) : self{ + $this->patterns = $patterns; + return $this; + } + + public function getFuelTime() : int{ + return 300; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $this->patterns = []; + + $colorIdMap = DyeColorIdMap::getInstance(); + $patternIdMap = BannerPatternTypeIdMap::getInstance(); + $patterns = $tag->getListTag(self::TAG_PATTERNS); + if($patterns !== null){ + /** @var CompoundTag $t */ + foreach($patterns as $t){ + $patternColor = $colorIdMap->fromInvertedId($t->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK(); //TODO: missing pattern colour should be an error + $patternType = $patternIdMap->fromId($t->getString(self::TAG_PATTERN_NAME)); + if($patternType === null){ + continue; //TODO: this should be an error + } + $this->patterns[] = new BannerPatternLayer($patternType, $patternColor); + } + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + if(count($this->patterns) > 0){ + $patterns = new ListTag(); + $colorIdMap = DyeColorIdMap::getInstance(); + $patternIdMap = BannerPatternTypeIdMap::getInstance(); + foreach($this->patterns as $pattern){ + $patterns->push(CompoundTag::create() + ->setString(self::TAG_PATTERN_NAME, $patternIdMap->toId($pattern->getType())) + ->setInt(self::TAG_PATTERN_COLOR, $colorIdMap->toInvertedId($pattern->getColor())) + ); + } + + $tag->setTag(self::TAG_PATTERNS, $patterns); + }else{ + $tag->removeTag(self::TAG_PATTERNS); + } + } +} diff --git a/src/pocketmine/item/Sign.php b/src/item/Bed.php similarity index 63% rename from src/pocketmine/item/Sign.php rename to src/item/Bed.php index 56a06749d7..c1251ac634 100644 --- a/src/pocketmine/item/Sign.php +++ b/src/item/Bed.php @@ -24,18 +24,28 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\utils\DyeColor; +use pocketmine\block\VanillaBlocks; -class Sign extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::SIGN, $meta, "Sign"); +class Bed extends Item{ + + /** @var DyeColor */ + private $color; + + public function __construct(ItemIdentifier $identifier, string $name, DyeColor $color){ + parent::__construct($identifier, $name); + $this->color = $color; } - public function getBlock() : Block{ - return BlockFactory::get(Block::SIGN_POST); + public function getColor() : DyeColor{ + return $this->color; + } + + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::BED()->setColor($this->color); } public function getMaxStackSize() : int{ - return 16; + return 1; } } diff --git a/src/pocketmine/item/Beetroot.php b/src/item/Beetroot.php similarity index 89% rename from src/pocketmine/item/Beetroot.php rename to src/item/Beetroot.php index 21f95a3d36..63b990a159 100644 --- a/src/pocketmine/item/Beetroot.php +++ b/src/item/Beetroot.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Beetroot extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::BEETROOT, $meta, "Beetroot"); - } public function getFoodRestore() : int{ return 1; diff --git a/src/pocketmine/item/BeetrootSeeds.php b/src/item/BeetrootSeeds.php similarity index 77% rename from src/pocketmine/item/BeetrootSeeds.php rename to src/item/BeetrootSeeds.php index 3e3c091a7d..e85d2cace0 100644 --- a/src/pocketmine/item/BeetrootSeeds.php +++ b/src/item/BeetrootSeeds.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class BeetrootSeeds extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BEETROOT_SEEDS, $meta, "Beetroot Seeds"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::BEETROOT_BLOCK); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::BEETROOTS(); } } diff --git a/src/pocketmine/item/BeetrootSoup.php b/src/item/BeetrootSoup.php similarity index 84% rename from src/pocketmine/item/BeetrootSoup.php rename to src/item/BeetrootSoup.php index 75c5747c8c..4ddab67f22 100644 --- a/src/pocketmine/item/BeetrootSoup.php +++ b/src/item/BeetrootSoup.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class BeetrootSoup extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::BEETROOT_SOUP, $meta, "Beetroot Soup"); - } public function getMaxStackSize() : int{ return 1; @@ -40,7 +37,7 @@ class BeetrootSoup extends Food{ return 7.2; } - public function getResidue(){ - return ItemFactory::get(Item::BOWL); + public function getResidue() : Item{ + return VanillaItems::BOWL(); } } diff --git a/src/pocketmine/item/BlazeRod.php b/src/item/BlazeRod.php similarity index 88% rename from src/pocketmine/item/BlazeRod.php rename to src/item/BlazeRod.php index c9276ea682..34a6e1e0d6 100644 --- a/src/pocketmine/item/BlazeRod.php +++ b/src/item/BlazeRod.php @@ -25,10 +25,6 @@ namespace pocketmine\item; class BlazeRod extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BLAZE_ROD, $meta, "Blaze Rod"); - } - public function getFuelTime() : int{ return 2400; } diff --git a/src/pocketmine/block/DaylightSensor.php b/src/item/Boat.php similarity index 66% rename from src/pocketmine/block/DaylightSensor.php rename to src/item/Boat.php index b5381b41a8..c23f691460 100644 --- a/src/pocketmine/block/DaylightSensor.php +++ b/src/item/Boat.php @@ -21,30 +21,25 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\item; -class DaylightSensor extends Transparent{ +use pocketmine\block\utils\TreeType; - protected $id = self::DAYLIGHT_SENSOR; +class Boat extends Item{ + /** @var TreeType */ + private $woodType; - public function __construct(int $meta = 0){ - $this->meta = $meta; + public function __construct(ItemIdentifier $identifier, string $name, TreeType $woodType){ + parent::__construct($identifier, $name); + $this->woodType = $woodType; } - public function getName() : string{ - return "Daylight Sensor"; - } - - public function getHardness() : float{ - return 0.2; + public function getWoodType() : TreeType{ + return $this->woodType; } public function getFuelTime() : int{ - return 300; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; + return 1200; //400 in PC } //TODO diff --git a/src/pocketmine/item/Book.php b/src/item/Book.php similarity index 88% rename from src/pocketmine/item/Book.php rename to src/item/Book.php index 8aec5186e5..0020594dff 100644 --- a/src/pocketmine/item/Book.php +++ b/src/item/Book.php @@ -24,7 +24,5 @@ declare(strict_types=1); namespace pocketmine\item; class Book extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BOOK, $meta, "Book"); - } + } diff --git a/src/item/Bow.php b/src/item/Bow.php new file mode 100644 index 0000000000..41777e0a6c --- /dev/null +++ b/src/item/Bow.php @@ -0,0 +1,130 @@ +getOffHandInventory()->contains($arrow) => $player->getOffHandInventory(), + $player->getInventory()->contains($arrow) => $player->getInventory(), + default => null + }; + + if($player->hasFiniteResources() and $inventory === null){ + return ItemUseResult::FAIL(); + } + + $location = $player->getLocation(); + + $diff = $player->getItemUseDuration(); + $p = $diff / 20; + $baseForce = min((($p ** 2) + $p * 2) / 3, 1); + + $entity = new ArrowEntity(Location::fromObject( + $player->getEyePos(), + $player->getWorld(), + ($location->yaw > 180 ? 360 : 0) - $location->yaw, + -$location->pitch + ), $player, $baseForce >= 1); + $entity->setMotion($player->getDirectionVector()); + + $infinity = $this->hasEnchantment(VanillaEnchantments::INFINITY()); + if($infinity){ + $entity->setPickupMode(ArrowEntity::PICKUP_CREATIVE); + } + if(($punchLevel = $this->getEnchantmentLevel(VanillaEnchantments::PUNCH())) > 0){ + $entity->setPunchKnockback($punchLevel); + } + if(($powerLevel = $this->getEnchantmentLevel(VanillaEnchantments::POWER())) > 0){ + $entity->setBaseDamage($entity->getBaseDamage() + (($powerLevel + 1) / 2)); + } + if($this->hasEnchantment(VanillaEnchantments::FLAME())){ + $entity->setOnFire(intdiv($entity->getFireTicks(), 20) + 100); + } + $ev = new EntityShootBowEvent($player, $this, $entity, $baseForce * 3); + + if($baseForce < 0.1 or $diff < 5 or $player->isSpectator()){ + $ev->cancel(); + } + + $ev->call(); + + $entity = $ev->getProjectile(); //This might have been changed by plugins + + if($ev->isCancelled()){ + $entity->flagForDespawn(); + return ItemUseResult::FAIL(); + } + + $entity->setMotion($entity->getMotion()->multiply($ev->getForce())); + + if($entity instanceof Projectile){ + $projectileEv = new ProjectileLaunchEvent($entity); + $projectileEv->call(); + if($projectileEv->isCancelled()){ + $ev->getProjectile()->flagForDespawn(); + return ItemUseResult::FAIL(); + } + + $ev->getProjectile()->spawnToAll(); + $location->getWorld()->addSound($location, new BowShootSound()); + }else{ + $entity->spawnToAll(); + } + + if($player->hasFiniteResources()){ + if(!$infinity){ //TODO: tipped arrows are still consumed when Infinity is applied + $inventory?->removeItem($arrow); + } + $this->applyDamage(1); + } + + 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/pocketmine/item/Bowl.php b/src/item/Bowl.php similarity index 89% rename from src/pocketmine/item/Bowl.php rename to src/item/Bowl.php index 3d54d6ac16..66ef530b01 100644 --- a/src/pocketmine/item/Bowl.php +++ b/src/item/Bowl.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Bowl extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BOWL, $meta, "Bowl"); - } //TODO: check fuel } diff --git a/src/pocketmine/item/Bread.php b/src/item/Bread.php similarity index 90% rename from src/pocketmine/item/Bread.php rename to src/item/Bread.php index 5e70816d33..2687fccffd 100644 --- a/src/pocketmine/item/Bread.php +++ b/src/item/Bread.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Bread extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::BREAD, $meta, "Bread"); - } public function getFoodRestore() : int{ return 5; diff --git a/src/item/Bucket.php b/src/item/Bucket.php new file mode 100644 index 0000000000..b7fd2a19f3 --- /dev/null +++ b/src/item/Bucket.php @@ -0,0 +1,69 @@ +isSource()){ + $stack = clone $this; + $stack->pop(); + + $resultItem = ItemFactory::getInstance()->get(ItemIds::BUCKET, $blockClicked->getFlowingForm()->getId()); + $ev = new PlayerBucketFillEvent($player, $blockReplace, $face, $this, $resultItem); + $ev->call(); + if(!$ev->isCancelled()){ + $player->getWorld()->setBlock($blockClicked->getPosition(), VanillaBlocks::AIR()); + $player->getWorld()->addSound($blockClicked->getPosition()->add(0.5, 0.5, 0.5), $blockClicked->getBucketFillSound()); + if($player->hasFiniteResources()){ + if($stack->getCount() === 0){ + $player->getInventory()->setItemInHand($ev->getItem()); + }else{ + $player->getInventory()->setItemInHand($stack); + $player->getInventory()->addItem($ev->getItem()); + } + }else{ + $player->getInventory()->addItem($ev->getItem()); + } + return ItemUseResult::SUCCESS(); + } + + return ItemUseResult::FAIL(); + } + + return ItemUseResult::NONE(); + } +} diff --git a/src/pocketmine/item/Carrot.php b/src/item/Carrot.php similarity index 80% rename from src/pocketmine/item/Carrot.php rename to src/item/Carrot.php index 24b525fdba..75782ae490 100644 --- a/src/pocketmine/item/Carrot.php +++ b/src/item/Carrot.php @@ -24,15 +24,12 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class Carrot extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::CARROT, $meta, "Carrot"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::CARROT_BLOCK); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::CARROTS(); } public function getFoodRestore() : int{ diff --git a/src/pocketmine/item/ChorusFruit.php b/src/item/ChorusFruit.php similarity index 67% rename from src/pocketmine/item/ChorusFruit.php rename to src/item/ChorusFruit.php index 269bdcf309..7cf621e671 100644 --- a/src/pocketmine/item/ChorusFruit.php +++ b/src/item/ChorusFruit.php @@ -25,17 +25,13 @@ namespace pocketmine\item; use pocketmine\block\Liquid; use pocketmine\entity\Living; -use pocketmine\level\sound\EndermanTeleportSound; use pocketmine\math\Vector3; +use pocketmine\world\sound\EndermanTeleportSound; use function min; use function mt_rand; class ChorusFruit extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::CHORUS_FRUIT, $meta, "Chorus Fruit"); - } - public function getFoodRestore() : int{ return 4; } @@ -48,12 +44,13 @@ class ChorusFruit extends Food{ return false; } - public function onConsume(Living $consumer){ - $level = $consumer->getLevelNonNull(); + public function onConsume(Living $consumer) : void{ + $world = $consumer->getWorld(); - $minX = $consumer->getFloorX() - 8; - $minY = min($consumer->getFloorY(), $consumer->getLevelNonNull()->getWorldHeight()) - 8; - $minZ = $consumer->getFloorZ() - 8; + $origin = $consumer->getPosition(); + $minX = $origin->getFloorX() - 8; + $minY = min($origin->getFloorY(), $consumer->getWorld()->getMaxY()) - 8; + $minZ = $origin->getFloorZ() - 8; $maxX = $minX + 16; $maxY = $minY + 16; @@ -64,23 +61,23 @@ class ChorusFruit extends Food{ $y = mt_rand($minY, $maxY); $z = mt_rand($minZ, $maxZ); - while($y >= 0 and !$level->getBlockAt($x, $y, $z)->isSolid()){ + while($y >= 0 and !$world->getBlockAt($x, $y, $z)->isSolid()){ $y--; } if($y < 0){ continue; } - $blockUp = $level->getBlockAt($x, $y + 1, $z); - $blockUp2 = $level->getBlockAt($x, $y + 2, $z); + $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){ continue; } //Sounds are broadcasted at both source and destination - $level->addSound(new EndermanTeleportSound($consumer->asVector3())); - $consumer->teleport(new Vector3($x + 0.5, $y + 1, $z + 0.5)); - $level->addSound(new EndermanTeleportSound($consumer->asVector3())); + $world->addSound($origin, new EndermanTeleportSound()); + $consumer->teleport($target = new Vector3($x + 0.5, $y + 1, $z + 0.5)); + $world->addSound($target, new EndermanTeleportSound()); break; } diff --git a/src/pocketmine/item/Clock.php b/src/item/Clock.php similarity index 88% rename from src/pocketmine/item/Clock.php rename to src/item/Clock.php index d2769167e7..78f9f3ccb4 100644 --- a/src/pocketmine/item/Clock.php +++ b/src/item/Clock.php @@ -24,7 +24,5 @@ declare(strict_types=1); namespace pocketmine\item; class Clock extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::CLOCK, $meta, "Clock"); - } + } diff --git a/src/pocketmine/item/Clownfish.php b/src/item/Clownfish.php similarity index 89% rename from src/pocketmine/item/Clownfish.php rename to src/item/Clownfish.php index 4cd83e1001..1dba40e748 100644 --- a/src/pocketmine/item/Clownfish.php +++ b/src/item/Clownfish.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Clownfish extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::CLOWNFISH, $meta, "Clownfish"); - } public function getFoodRestore() : int{ return 1; diff --git a/src/item/Coal.php b/src/item/Coal.php new file mode 100644 index 0000000000..55aa444400 --- /dev/null +++ b/src/item/Coal.php @@ -0,0 +1,31 @@ +damage; + } + /** * Returns whether this item will take damage when used. */ public function isUnbreakable() : bool{ - return $this->getNamedTag()->getByte("Unbreakable", 0) !== 0; + return $this->unbreakable; } /** * Sets whether the item will take damage when used. * - * @return void + * @return $this */ - public function setUnbreakable(bool $value = true){ - $this->setNamedTagEntry(new ByteTag("Unbreakable", $value ? 1 : 0)); + public function setUnbreakable(bool $value = true) : self{ + $this->unbreakable = $value; + return $this; } /** @@ -58,7 +68,7 @@ abstract class Durable extends Item{ $amount -= $this->getUnbreakingDamageReduction($amount); - $this->meta = min($this->meta + $amount, $this->getMaxDurability()); + $this->damage = min($this->damage + $amount, $this->getMaxDurability()); if($this->isBroken()){ $this->onBroken(); } @@ -66,8 +76,20 @@ abstract class Durable extends Item{ return true; } + public function getDamage() : int{ + return $this->damage; + } + + public function setDamage(int $damage) : Item{ + if($damage < 0 or $damage > $this->getMaxDurability()){ + throw new \InvalidArgumentException("Damage must be in range 0 - " . $this->getMaxDurability()); + } + $this->damage = $damage; + return $this; + } + protected function getUnbreakingDamageReduction(int $amount) : int{ - if(($unbreakingLevel = $this->getEnchantmentLevel(Enchantment::UNBREAKING)) > 0){ + if(($unbreakingLevel = $this->getEnchantmentLevel(VanillaEnchantments::UNBREAKING())) > 0){ $negated = 0; $chance = 1 / ($unbreakingLevel + 1); @@ -99,6 +121,16 @@ abstract class Durable extends Item{ * Returns whether the item is broken. */ public function isBroken() : bool{ - return $this->meta >= $this->getMaxDurability(); + return $this->damage >= $this->getMaxDurability(); + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + $this->unbreakable = $tag->getByte("Unbreakable", 0) !== 0; + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + $this->unbreakable ? $tag->setByte("Unbreakable", 1) : $tag->removeTag("Unbreakable"); } } diff --git a/src/pocketmine/item/Bed.php b/src/item/Dye.php similarity index 70% rename from src/pocketmine/item/Bed.php rename to src/item/Dye.php index 6433b8f587..dcd6c9955d 100644 --- a/src/pocketmine/item/Bed.php +++ b/src/item/Dye.php @@ -23,19 +23,19 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\utils\DyeColor; -class Bed extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::BED, $meta, "Bed"); +class Dye extends Item{ + + /** @var DyeColor */ + private $color; + + public function __construct(ItemIdentifier $identifier, string $name, DyeColor $color){ + parent::__construct($identifier, $name); + $this->color = $color; } - public function getBlock() : Block{ - return BlockFactory::get(Block::BED_BLOCK); - } - - public function getMaxStackSize() : int{ - return 1; + public function getColor() : DyeColor{ + return $this->color; } } diff --git a/src/pocketmine/item/Egg.php b/src/item/Egg.php similarity index 76% rename from src/pocketmine/item/Egg.php rename to src/item/Egg.php index fa2716b9d8..afacbf5679 100644 --- a/src/pocketmine/item/Egg.php +++ b/src/item/Egg.php @@ -23,17 +23,19 @@ declare(strict_types=1); namespace pocketmine\item; +use pocketmine\entity\Location; +use pocketmine\entity\projectile\Egg as EggEntity; +use pocketmine\entity\projectile\Throwable; +use pocketmine\player\Player; + class Egg extends ProjectileItem{ - public function __construct(int $meta = 0){ - parent::__construct(self::EGG, $meta, "Egg"); - } public function getMaxStackSize() : int{ return 16; } - public function getProjectileEntityType() : string{ - return "Egg"; + protected function createEntity(Location $location, Player $thrower) : Throwable{ + return new EggEntity($location, $thrower); } public function getThrowForce() : float{ diff --git a/src/pocketmine/item/EnderPearl.php b/src/item/EnderPearl.php similarity index 76% rename from src/pocketmine/item/EnderPearl.php rename to src/item/EnderPearl.php index ec0e9d788b..1cd9d9d0f4 100644 --- a/src/pocketmine/item/EnderPearl.php +++ b/src/item/EnderPearl.php @@ -23,17 +23,19 @@ declare(strict_types=1); namespace pocketmine\item; +use pocketmine\entity\Location; +use pocketmine\entity\projectile\EnderPearl as EnderPearlEntity; +use pocketmine\entity\projectile\Throwable; +use pocketmine\player\Player; + class EnderPearl extends ProjectileItem{ - public function __construct(int $meta = 0){ - parent::__construct(self::ENDER_PEARL, $meta, "Ender Pearl"); - } public function getMaxStackSize() : int{ return 16; } - public function getProjectileEntityType() : string{ - return "ThrownEnderpearl"; + protected function createEntity(Location $location, Player $thrower) : Throwable{ + return new EnderPearlEntity($location, $thrower); } public function getThrowForce() : float{ diff --git a/src/pocketmine/item/ExperienceBottle.php b/src/item/ExperienceBottle.php similarity index 72% rename from src/pocketmine/item/ExperienceBottle.php rename to src/item/ExperienceBottle.php index 5158cd06b4..f764971516 100644 --- a/src/pocketmine/item/ExperienceBottle.php +++ b/src/item/ExperienceBottle.php @@ -23,13 +23,15 @@ declare(strict_types=1); namespace pocketmine\item; -class ExperienceBottle extends ProjectileItem{ - public function __construct(int $meta = 0){ - parent::__construct(self::EXPERIENCE_BOTTLE, $meta, "Bottle o' Enchanting"); - } +use pocketmine\entity\Location; +use pocketmine\entity\projectile\ExperienceBottle as ExperienceBottleEntity; +use pocketmine\entity\projectile\Throwable; +use pocketmine\player\Player; - public function getProjectileEntityType() : string{ - return "ThrownExpBottle"; +class ExperienceBottle extends ProjectileItem{ + + protected function createEntity(Location $location, Player $thrower) : Throwable{ + return new ExperienceBottleEntity($location, $thrower); } public function getThrowForce() : float{ diff --git a/src/pocketmine/entity/NPC.php b/src/item/Fertilizer.php similarity index 92% rename from src/pocketmine/entity/NPC.php rename to src/item/Fertilizer.php index 70094a3cec..3399147c6b 100644 --- a/src/pocketmine/entity/NPC.php +++ b/src/item/Fertilizer.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\item; -interface NPC{ +class Fertilizer extends Item{ } diff --git a/src/pocketmine/item/FishingRod.php b/src/item/FishingRod.php similarity index 87% rename from src/pocketmine/item/FishingRod.php rename to src/item/FishingRod.php index 85991e5e98..1e9c71e210 100644 --- a/src/pocketmine/item/FishingRod.php +++ b/src/item/FishingRod.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class FishingRod extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::FISHING_ROD, $meta, "Fishing Rod"); - } //TODO } diff --git a/src/pocketmine/item/FlintSteel.php b/src/item/FlintSteel.php similarity index 59% rename from src/pocketmine/item/FlintSteel.php rename to src/item/FlintSteel.php index 49aa84e545..ee387f4e68 100644 --- a/src/pocketmine/item/FlintSteel.php +++ b/src/item/FlintSteel.php @@ -24,28 +24,26 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\BlockLegacyIds; +use pocketmine\block\VanillaBlocks; use pocketmine\math\Vector3; -use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\sound\FlintSteelSound; class FlintSteel extends Tool{ - public function __construct(int $meta = 0){ - parent::__construct(self::FLINT_STEEL, $meta, "Flint and Steel"); - } - public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{ - if($blockReplace->getId() === self::AIR){ - $level = $player->getLevelNonNull(); - $level->setBlock($blockReplace, BlockFactory::get(Block::FIRE), true); - $level->broadcastLevelSoundEvent($blockReplace->add(0.5, 0.5, 0.5), LevelSoundEventPacket::SOUND_IGNITE); + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ + if($blockReplace->getId() === BlockLegacyIds::AIR){ + $world = $player->getWorld(); + $world->setBlock($blockReplace->getPosition(), VanillaBlocks::FIRE()); + $world->addSound($blockReplace->getPosition()->add(0.5, 0.5, 0.5), new FlintSteelSound()); $this->applyDamage(1); - return true; + return ItemUseResult::SUCCESS(); } - return false; + return ItemUseResult::NONE(); } public function getMaxDurability() : int{ diff --git a/src/pocketmine/item/Food.php b/src/item/Food.php similarity index 72% rename from src/pocketmine/item/Food.php rename to src/item/Food.php index 36f4cee4fb..ddaa77da6e 100644 --- a/src/pocketmine/item/Food.php +++ b/src/item/Food.php @@ -24,24 +24,26 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\entity\Living; +use pocketmine\player\Player; -abstract class Food extends Item implements FoodSource{ +abstract class Food extends Item implements FoodSourceItem{ public function requiresHunger() : bool{ return true; } - /** - * @return Item - */ - public function getResidue(){ - return ItemFactory::get(Item::AIR, 0, 0); + public function getResidue() : Item{ + return ItemFactory::air(); } public function getAdditionalEffects() : array{ return []; } - public function onConsume(Living $consumer){ + public function onConsume(Living $consumer) : void{ } + + public function canStartUsingItem(Player $player) : bool{ + return !$this->requiresHunger() || $player->getHungerManager()->isHungry(); + } } diff --git a/src/pocketmine/item/Arrow.php b/src/item/FoodSourceItem.php similarity index 85% rename from src/pocketmine/item/Arrow.php rename to src/item/FoodSourceItem.php index 31b73b7254..f47ffde8be 100644 --- a/src/pocketmine/item/Arrow.php +++ b/src/item/FoodSourceItem.php @@ -23,8 +23,8 @@ declare(strict_types=1); namespace pocketmine\item; -class Arrow extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::ARROW, $meta, "Arrow"); - } +use pocketmine\entity\FoodSource; + +interface FoodSourceItem extends ConsumableItem, FoodSource{ + } diff --git a/src/pocketmine/item/GlassBottle.php b/src/item/GlassBottle.php similarity index 87% rename from src/pocketmine/item/GlassBottle.php rename to src/item/GlassBottle.php index 682eecacf7..d3ce86a581 100644 --- a/src/pocketmine/item/GlassBottle.php +++ b/src/item/GlassBottle.php @@ -24,7 +24,5 @@ declare(strict_types=1); namespace pocketmine\item; class GlassBottle extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::GLASS_BOTTLE, $meta, "Glass Bottle"); - } + } diff --git a/src/pocketmine/item/GoldenApple.php b/src/item/GoldenApple.php similarity index 76% rename from src/pocketmine/item/GoldenApple.php rename to src/item/GoldenApple.php index dbd3550d23..76487fdd67 100644 --- a/src/pocketmine/item/GoldenApple.php +++ b/src/item/GoldenApple.php @@ -23,15 +23,11 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; class GoldenApple extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::GOLDEN_APPLE, $meta, "Golden Apple"); - } - public function requiresHunger() : bool{ return false; } @@ -46,8 +42,8 @@ class GoldenApple extends Food{ public function getAdditionalEffects() : array{ return [ - new EffectInstance(Effect::getEffect(Effect::REGENERATION), 100, 1), - new EffectInstance(Effect::getEffect(Effect::ABSORPTION), 2400) + new EffectInstance(VanillaEffects::REGENERATION(), 100, 1), + new EffectInstance(VanillaEffects::ABSORPTION(), 2400) ]; } } diff --git a/src/pocketmine/item/GoldenAppleEnchanted.php b/src/item/GoldenAppleEnchanted.php similarity index 63% rename from src/pocketmine/item/GoldenAppleEnchanted.php rename to src/item/GoldenAppleEnchanted.php index 4961d8498e..c807ac897d 100644 --- a/src/pocketmine/item/GoldenAppleEnchanted.php +++ b/src/item/GoldenAppleEnchanted.php @@ -23,21 +23,17 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; class GoldenAppleEnchanted extends GoldenApple{ - public function __construct(int $meta = 0){ - Food::__construct(self::ENCHANTED_GOLDEN_APPLE, $meta, "Enchanted Golden Apple"); //skip parent constructor - } - public function getAdditionalEffects() : array{ return [ - new EffectInstance(Effect::getEffect(Effect::REGENERATION), 600, 4), - new EffectInstance(Effect::getEffect(Effect::ABSORPTION), 2400, 3), - new EffectInstance(Effect::getEffect(Effect::RESISTANCE), 6000), - new EffectInstance(Effect::getEffect(Effect::FIRE_RESISTANCE), 6000) + new EffectInstance(VanillaEffects::REGENERATION(), 600, 4), + new EffectInstance(VanillaEffects::ABSORPTION(), 2400, 3), + new EffectInstance(VanillaEffects::RESISTANCE(), 6000), + new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 6000) ]; } } diff --git a/src/pocketmine/item/GoldenCarrot.php b/src/item/GoldenCarrot.php similarity index 88% rename from src/pocketmine/item/GoldenCarrot.php rename to src/item/GoldenCarrot.php index 78b99ab74b..9c2acdc891 100644 --- a/src/pocketmine/item/GoldenCarrot.php +++ b/src/item/GoldenCarrot.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class GoldenCarrot extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::GOLDEN_CARROT, $meta, "Golden Carrot"); - } public function getFoodRestore() : int{ return 6; diff --git a/src/pocketmine/item/Hoe.php b/src/item/Hoe.php similarity index 93% rename from src/pocketmine/item/Hoe.php rename to src/item/Hoe.php index 4baa9b9ad5..87dd77c352 100644 --- a/src/pocketmine/item/Hoe.php +++ b/src/item/Hoe.php @@ -30,7 +30,7 @@ use pocketmine\entity\Entity; class Hoe extends TieredTool{ public function getBlockToolType() : int{ - return BlockToolType::TYPE_HOE; + return BlockToolType::HOE; } public function onAttackEntity(Entity $victim) : bool{ @@ -38,7 +38,7 @@ class Hoe extends TieredTool{ } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() > 0){ + if(!$block->getBreakInfo()->breaksInstantly()){ return $this->applyDamage(1); } return false; diff --git a/src/item/Item.php b/src/item/Item.php new file mode 100644 index 0000000000..bceb921425 --- /dev/null +++ b/src/item/Item.php @@ -0,0 +1,715 @@ + + */ + protected $canPlaceOn = []; + /** + * @var string[] + * @phpstan-var array + */ + protected $canDestroy; + + /** + * Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register + * into the index. + * + * NOTE: This should NOT BE USED for creating items to set into an inventory. Use {@link ItemFactory#get} for that + * purpose. + */ + public function __construct(ItemIdentifier $identifier, string $name = "Unknown"){ + $this->identifier = $identifier; + $this->name = $name; + + $this->canPlaceOn = []; + $this->canDestroy = []; + $this->nbt = new CompoundTag(); + } + + public function hasCustomBlockData() : bool{ + return $this->blockEntityTag !== null; + } + + /** + * @return $this + */ + public function clearCustomBlockData(){ + $this->blockEntityTag = null; + return $this; + } + + /** + * @return $this + */ + public function setCustomBlockData(CompoundTag $compound) : Item{ + $this->blockEntityTag = clone $compound; + + return $this; + } + + public function getCustomBlockData() : ?CompoundTag{ + return $this->blockEntityTag; + } + + public function hasCustomName() : bool{ + return $this->customName !== ""; + } + + public function getCustomName() : string{ + return $this->customName; + } + + /** + * @return $this + */ + public function setCustomName(string $name) : Item{ + Utils::checkUTF8($name); + $this->customName = $name; + return $this; + } + + /** + * @return $this + */ + public function clearCustomName() : Item{ + $this->setCustomName(""); + return $this; + } + + /** + * @return string[] + */ + public function getLore() : array{ + return $this->lore; + } + + /** + * @param string[] $lines + * + * @return $this + */ + public function setLore(array $lines) : Item{ + foreach($lines as $line){ + if(!is_string($line)){ + throw new \TypeError("Expected string[], but found " . gettype($line) . " in given array"); + } + Utils::checkUTF8($line); + } + $this->lore = $lines; + return $this; + } + + /** + * @return string[] + * @phpstan-return array + */ + public function getCanPlaceOn() : array{ + return $this->canPlaceOn; + } + + /** + * @param string[] $canPlaceOn + */ + public function setCanPlaceOn(array $canPlaceOn) : void{ + $this->canPlaceOn = []; + foreach($canPlaceOn as $value){ + $this->canPlaceOn[$value] = $value; + } + } + + /** + * @return string[] + * @phpstan-return array + */ + public function getCanDestroy() : array{ + return $this->canDestroy; + } + + /** + * @param string[] $canDestroy + */ + public function setCanDestroy(array $canDestroy) : void{ + $this->canDestroy = []; + foreach($canDestroy as $value){ + $this->canDestroy[$value] = $value; + } + } + + /** + * Returns whether this Item has a non-empty NBT. + */ + public function hasNamedTag() : bool{ + return $this->getNamedTag()->count() > 0; + } + + /** + * Returns a tree of Tag objects representing the Item's NBT. If the item does not have any NBT, an empty CompoundTag + * object is returned to allow the caller to manipulate and apply back to the item. + */ + public function getNamedTag() : CompoundTag{ + $this->serializeCompoundTag($this->nbt); + return $this->nbt; + } + + /** + * Sets the Item's NBT from the supplied CompoundTag object. + * + * @return $this + * @throws NbtException + */ + public function setNamedTag(CompoundTag $tag) : Item{ + if($tag->getCount() === 0){ + return $this->clearNamedTag(); + } + + $this->nbt = clone $tag; + $this->deserializeCompoundTag($this->nbt); + + return $this; + } + + /** + * Removes the Item's NBT. + * @return $this + * @throws NbtException + */ + public function clearNamedTag() : Item{ + $this->nbt = new CompoundTag(); + $this->deserializeCompoundTag($this->nbt); + return $this; + } + + /** + * @throws NbtException + */ + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + $this->customName = ""; + $this->lore = []; + + $display = $tag->getCompoundTag(self::TAG_DISPLAY); + if($display !== null){ + $this->customName = $display->getString(self::TAG_DISPLAY_NAME, $this->customName); + $lore = $display->getListTag(self::TAG_DISPLAY_LORE); + if($lore !== null and $lore->getTagType() === NBT::TAG_String){ + /** @var StringTag $t */ + foreach($lore as $t){ + $this->lore[] = $t->getValue(); + } + } + } + + $this->removeEnchantments(); + $enchantments = $tag->getListTag(self::TAG_ENCH); + if($enchantments !== null and $enchantments->getTagType() === NBT::TAG_Compound){ + /** @var CompoundTag $enchantment */ + foreach($enchantments as $enchantment){ + $magicNumber = $enchantment->getShort("id", -1); + $level = $enchantment->getShort("lvl", 0); + if($level <= 0){ + continue; + } + $type = EnchantmentIdMap::getInstance()->fromId($magicNumber); + if($type !== null){ + $this->addEnchantment(new EnchantmentInstance($type, $level)); + } + } + } + + $this->blockEntityTag = $tag->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG); + + $this->canPlaceOn = []; + $canPlaceOn = $tag->getListTag("CanPlaceOn"); + if($canPlaceOn !== null){ + /** @var StringTag $entry */ + foreach($canPlaceOn as $entry){ + $this->canPlaceOn[$entry->getValue()] = $entry->getValue(); + } + } + $this->canDestroy = []; + $canDestroy = $tag->getListTag("CanDestroy"); + if($canDestroy !== null){ + /** @var StringTag $entry */ + foreach($canDestroy as $entry){ + $this->canDestroy[$entry->getValue()] = $entry->getValue(); + } + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + $display = $tag->getCompoundTag(self::TAG_DISPLAY) ?? new CompoundTag(); + + $this->hasCustomName() ? + $display->setString(self::TAG_DISPLAY_NAME, $this->getCustomName()) : + $display->removeTag(self::TAG_DISPLAY_NAME); + + if(count($this->lore) > 0){ + $loreTag = new ListTag(); + foreach($this->lore as $line){ + $loreTag->push(new StringTag($line)); + } + $display->setTag(self::TAG_DISPLAY_LORE, $loreTag); + }else{ + $display->removeTag(self::TAG_DISPLAY_LORE); + } + $display->count() > 0 ? + $tag->setTag(self::TAG_DISPLAY, $display) : + $tag->removeTag(self::TAG_DISPLAY); + + if($this->hasEnchantments()){ + $ench = new ListTag(); + foreach($this->getEnchantments() as $enchantmentInstance){ + $ench->push(CompoundTag::create() + ->setShort("id", EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType())) + ->setShort("lvl", $enchantmentInstance->getLevel()) + ); + } + $tag->setTag(self::TAG_ENCH, $ench); + }else{ + $tag->removeTag(self::TAG_ENCH); + } + + ($blockData = $this->getCustomBlockData()) !== null ? + $tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $blockData) : + $tag->removeTag(self::TAG_BLOCK_ENTITY_TAG); + + if(count($this->canPlaceOn) > 0){ + $canPlaceOn = new ListTag(); + foreach($this->canPlaceOn as $item){ + $canPlaceOn->push(new StringTag($item)); + } + $tag->setTag("CanPlaceOn", $canPlaceOn); + }else{ + $tag->removeTag("CanPlaceOn"); + } + if(count($this->canDestroy) > 0){ + $canDestroy = new ListTag(); + foreach($this->canDestroy as $item){ + $canDestroy->push(new StringTag($item)); + } + $tag->setTag("CanDestroy", $canDestroy); + }else{ + $tag->removeTag("CanDestroy"); + } + } + + public function getCount() : int{ + return $this->count; + } + + /** + * @return $this + */ + public function setCount(int $count) : Item{ + $this->count = $count; + + return $this; + } + + /** + * Pops an item from the stack and returns it, decreasing the stack count of this item stack by one. + * + * @return static A clone of this itemstack containing the amount of items that were removed from this stack. + * @throws \InvalidArgumentException if trying to pop more items than are on the stack + */ + public function pop(int $count = 1) : Item{ + if($count > $this->count){ + throw new \InvalidArgumentException("Cannot pop $count items from a stack of $this->count"); + } + + $item = clone $this; + $item->count = $count; + + $this->count -= $count; + + return $item; + } + + public function isNull() : bool{ + return $this->count <= 0 or $this->getId() === ItemIds::AIR; + } + + /** + * Returns the name of the item, or the custom name if it is set. + */ + final public function getName() : string{ + return $this->hasCustomName() ? $this->getCustomName() : $this->getVanillaName(); + } + + /** + * Returns the vanilla name of the item, disregarding custom names. + */ + public function getVanillaName() : string{ + return $this->name; + } + + final public function canBePlaced() : bool{ + return $this->getBlock()->canBePlaced(); + } + + /** + * Returns the block corresponding to this Item. + */ + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::AIR(); + } + + final public function getId() : int{ + return $this->identifier->getId(); + } + + public function getMeta() : int{ + return $this->identifier->getMeta(); + } + + /** + * Returns whether this item can match any item with an equivalent ID with any meta value. + * Used in crafting recipes which accept multiple variants of the same item, for example crafting tables recipes. + */ + public function hasAnyDamageValue() : bool{ + return $this->identifier->getMeta() === -1; + } + + /** + * Returns the highest amount of this item which will fit into one inventory slot. + */ + public function getMaxStackSize() : int{ + return 64; + } + + /** + * Returns the time in ticks which the item will fuel a furnace for. + */ + public function getFuelTime() : int{ + return 0; + } + + /** + * Returns an item after burning fuel + */ + public function getFuelResidue() : Item{ + $item = clone $this; + $item->pop(); + + return $item; + } + + /** + * Returns how many points of damage this item will deal to an entity when used as a weapon. + */ + public function getAttackPoints() : int{ + return 1; + } + + /** + * Returns how many armor points can be gained by wearing this item. + */ + public function getDefensePoints() : int{ + return 0; + } + + /** + * Returns what type of block-breaking tool this is. Blocks requiring the same tool type as the item will break + * faster (except for blocks requiring no tool, which break at the same speed regardless of the tool used) + */ + public function getBlockToolType() : int{ + return BlockToolType::NONE; + } + + /** + * Returns the harvesting power that this tool has. This affects what blocks it can mine when the tool type matches + * the mined block. + * This should return 1 for non-tiered tools, and the tool tier for tiered tools. + * + * @see BlockBreakInfo::getToolHarvestLevel() + */ + public function getBlockToolHarvestLevel() : int{ + return 0; + } + + public function getMiningEfficiency(bool $isCorrectTool) : float{ + return 1; + } + + /** + * Called when a player uses this item on a block. + */ + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ + return ItemUseResult::NONE(); + } + + /** + * Called when a player uses the item on air, for example throwing a projectile. + * Returns whether the item was changed, for example count decrease or durability change. + */ + public function onClickAir(Player $player, Vector3 $directionVector) : ItemUseResult{ + return ItemUseResult::NONE(); + } + + /** + * Called when a player is using this item and releases it. Used to handle bow shoot actions. + * Returns whether the item was changed, for example count decrease or durability change. + */ + public function onReleaseUsing(Player $player) : ItemUseResult{ + return ItemUseResult::NONE(); + } + + /** + * Called when this item is used to destroy a block. Usually used to update durability. + */ + public function onDestroyBlock(Block $block) : bool{ + return false; + } + + /** + * Called when this item is used to attack an entity. Usually used to update durability. + */ + public function onAttackEntity(Entity $victim) : bool{ + return false; + } + + /** + * Returns the number of ticks a player must wait before activating this item again. + */ + public function getCooldownTicks() : int{ + return 0; + } + + /** + * Compares an Item to this Item and check if they match. + * + * @param bool $checkDamage Whether to verify that the damage values match. + * @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())); + } + + /** + * Returns whether this item could stack with the given item (ignoring stack size and count). + */ + final public function canStackWith(Item $other) : bool{ + return $this->equals($other, true, true); + } + + /** + * 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; + } + + final public function __toString() : string{ + return "Item " . $this->name . " (" . $this->getId() . ":" . ($this->hasAnyDamageValue() ? "?" : $this->getMeta()) . ")x" . $this->count . ($this->hasNamedTag() ? " tags:0x" . base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))) : ""); + } + + /** + * Returns an array of item stack properties that can be serialized to json. + * + * @return mixed[] + * @phpstan-return array{id: int, damage?: int, count?: int, nbt_b64?: string} + */ + final public function jsonSerialize() : array{ + $data = [ + "id" => $this->getId() + ]; + + if($this->getMeta() !== 0){ + $data["damage"] = $this->getMeta(); + } + + if($this->getCount() !== 1){ + $data["count"] = $this->getCount(); + } + + if($this->hasNamedTag()){ + $data["nbt_b64"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))); + } + + return $data; + } + + /** + * Returns an Item from properties created in an array by {@link Item#jsonSerialize} + * @param mixed[] $data + * @phpstan-param array{ + * id: int, + * damage?: int, + * count?: int, + * nbt?: string, + * nbt_hex?: string, + * nbt_b64?: string + * } $data + * + * @throws NbtDataException + * @throws \InvalidArgumentException + */ + final public static function jsonDeserialize(array $data) : Item{ + $nbt = ""; + + //Backwards compatibility + if(isset($data["nbt"])){ + $nbt = $data["nbt"]; + }elseif(isset($data["nbt_hex"])){ + $nbt = hex2bin($data["nbt_hex"]); + }elseif(isset($data["nbt_b64"])){ + $nbt = base64_decode($data["nbt_b64"], true); + } + return ItemFactory::getInstance()->get( + (int) $data["id"], (int) ($data["damage"] ?? 0), (int) ($data["count"] ?? 1), $nbt !== "" ? (new LittleEndianNbtSerializer())->read($nbt)->mustGetCompoundTag() : null + ); + } + + /** + * Serializes the item to an NBT CompoundTag + * + * @param int $slot optional, the inventory slot of the item + */ + public function nbtSerialize(int $slot = -1) : CompoundTag{ + $result = CompoundTag::create() + ->setShort("id", $this->getId()) + ->setByte("Count", Binary::signByte($this->count)) + ->setShort("Damage", $this->getMeta()); + + if($this->hasNamedTag()){ + $result->setTag("tag", $this->getNamedTag()); + } + + if($slot !== -1){ + $result->setByte("Slot", $slot); + } + + return $result; + } + + /** + * 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){ + return ItemFactory::getInstance()->get(0); + } + + $count = Binary::unsignByte($tag->getByte("Count")); + $meta = $tag->getShort("Damage", 0); + + $idTag = $tag->getTag("id"); + if($idTag instanceof ShortTag){ + $item = ItemFactory::getInstance()->get($idTag->getValue(), $meta, $count); + }elseif($idTag instanceof StringTag){ //PC item save format + try{ + $item = LegacyStringToItemParser::getInstance()->parse($idTag->getValue() . ":$meta"); + }catch(LegacyStringToItemParserException $e){ + //TODO: improve error handling + return ItemFactory::air(); + } + $item->setCount($count); + }else{ + throw new SavedDataLoadingException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given"); + } + + $itemNBT = $tag->getCompoundTag("tag"); + if($itemNBT !== null){ + $item->setNamedTag(clone $itemNBT); + } + + return $item; + } + + public function __clone(){ + $this->nbt = clone $this->nbt; + if($this->blockEntityTag !== null){ + $this->blockEntityTag = clone $this->blockEntityTag; + } + } +} diff --git a/src/pocketmine/item/ItemBlock.php b/src/item/ItemBlock.php similarity index 66% rename from src/pocketmine/item/ItemBlock.php rename to src/item/ItemBlock.php index 4e93ee8dd2..0836d86e1c 100644 --- a/src/pocketmine/item/ItemBlock.php +++ b/src/item/ItemBlock.php @@ -31,25 +31,22 @@ use pocketmine\block\BlockFactory; */ class ItemBlock extends Item{ /** @var int */ - protected $blockId; + private $blockFullId; - /** - * @param int $meta usually 0-15 (placed blocks may only have meta values 0-15) - */ - public function __construct(int $blockId, int $meta = 0, int $itemId = null){ - $this->blockId = $blockId; - parent::__construct($itemId ?? $blockId, $meta, $this->getBlock()->getName()); + public function __construct(ItemIdentifier $identifier, Block $block){ + parent::__construct($identifier, $block->getName()); + $this->blockFullId = $block->getFullId(); } - public function getBlock() : Block{ - return BlockFactory::get($this->blockId, $this->meta === -1 ? 0 : $this->meta & 0xf); - } - - public function getVanillaName() : string{ - return $this->getBlock()->getName(); + public function getBlock(?int $clickedFace = null) : Block{ + return BlockFactory::getInstance()->fromFullBlock($this->blockFullId); } public function getFuelTime() : int{ return $this->getBlock()->getFuelTime(); } + + public function getMaxStackSize() : int{ + return $this->getBlock()->getMaxStackSize(); + } } diff --git a/src/item/ItemBlockWallOrFloor.php b/src/item/ItemBlockWallOrFloor.php new file mode 100644 index 0000000000..92a6afcf53 --- /dev/null +++ b/src/item/ItemBlockWallOrFloor.php @@ -0,0 +1,58 @@ +getName()); + $this->floorVariant = $floorVariant->getFullId(); + $this->wallVariant = $wallVariant->getFullId(); + } + + public function getBlock(?int $clickedFace = null) : Block{ + if($clickedFace !== null && Facing::axis($clickedFace) !== Axis::Y){ + return BlockFactory::getInstance()->fromFullBlock($this->wallVariant); + } + return BlockFactory::getInstance()->fromFullBlock($this->floorVariant); + } + + public function getFuelTime() : int{ + return $this->getBlock()->getFuelTime(); + } + + public function getMaxStackSize() : int{ + return $this->getBlock()->getMaxStackSize(); + } +} diff --git a/src/item/ItemEnchantmentHandlingTrait.php b/src/item/ItemEnchantmentHandlingTrait.php new file mode 100644 index 0000000000..927509a415 --- /dev/null +++ b/src/item/ItemEnchantmentHandlingTrait.php @@ -0,0 +1,94 @@ +enchantments) > 0; + } + + 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); + } + + public function getEnchantment(Enchantment $enchantment) : ?EnchantmentInstance{ + return $this->enchantments[spl_object_id($enchantment)] ?? null; + } + + /** + * @return $this + */ + public function removeEnchantment(Enchantment $enchantment, int $level = -1) : self{ + $instance = $this->getEnchantment($enchantment); + if($instance !== null and ($level === -1 or $instance->getLevel() === $level)){ + unset($this->enchantments[spl_object_id($enchantment)]); + } + + return $this; + } + + /** + * @return $this + */ + public function removeEnchantments() : self{ + $this->enchantments = []; + return $this; + } + + /** + * @return $this + */ + public function addEnchantment(EnchantmentInstance $enchantment) : self{ + $this->enchantments[spl_object_id($enchantment->getType())] = $enchantment; + return $this; + } + + /** + * @return EnchantmentInstance[] + */ + public function getEnchantments() : array{ + return $this->enchantments; + } + + /** + * Returns the level of the enchantment on this item with the specified ID, or 0 if the item does not have the + * enchantment. + */ + public function getEnchantmentLevel(Enchantment $enchantment) : int{ + return ($instance = $this->getEnchantment($enchantment)) !== null ? $instance->getLevel() : 0; + } +} diff --git a/src/item/ItemFactory.php b/src/item/ItemFactory.php new file mode 100644 index 0000000000..95622b4e91 --- /dev/null +++ b/src/item/ItemFactory.php @@ -0,0 +1,509 @@ +registerArmorItems(); + $this->registerSpawnEggs(); + $this->registerTierToolItems(); + + $this->register(new Apple(new ItemIdentifier(ItemIds::APPLE, 0), "Apple")); + $this->register(new Arrow(new ItemIdentifier(ItemIds::ARROW, 0), "Arrow")); + + $this->register(new BakedPotato(new ItemIdentifier(ItemIds::BAKED_POTATO, 0), "Baked Potato")); + $this->register(new Bamboo(new ItemIdentifier(ItemIds::BAMBOO, 0), "Bamboo"), true); + $this->register(new Beetroot(new ItemIdentifier(ItemIds::BEETROOT, 0), "Beetroot")); + $this->register(new BeetrootSeeds(new ItemIdentifier(ItemIds::BEETROOT_SEEDS, 0), "Beetroot Seeds")); + $this->register(new BeetrootSoup(new ItemIdentifier(ItemIds::BEETROOT_SOUP, 0), "Beetroot Soup")); + $this->register(new BlazeRod(new ItemIdentifier(ItemIds::BLAZE_ROD, 0), "Blaze Rod")); + $this->register(new Book(new ItemIdentifier(ItemIds::BOOK, 0), "Book")); + $this->register(new Bow(new ItemIdentifier(ItemIds::BOW, 0), "Bow")); + $this->register(new Bowl(new ItemIdentifier(ItemIds::BOWL, 0), "Bowl")); + $this->register(new Bread(new ItemIdentifier(ItemIds::BREAD, 0), "Bread")); + $this->register(new Bucket(new ItemIdentifier(ItemIds::BUCKET, 0), "Bucket")); + $this->register(new Carrot(new ItemIdentifier(ItemIds::CARROT, 0), "Carrot")); + $this->register(new ChorusFruit(new ItemIdentifier(ItemIds::CHORUS_FRUIT, 0), "Chorus Fruit")); + $this->register(new Clock(new ItemIdentifier(ItemIds::CLOCK, 0), "Clock")); + $this->register(new Clownfish(new ItemIdentifier(ItemIds::CLOWNFISH, 0), "Clownfish")); + $this->register(new Coal(new ItemIdentifier(ItemIds::COAL, 0), "Coal")); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::CORAL_FAN, 0), VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::TUBE()), VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::TUBE())), true); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::CORAL_FAN, 1), VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::BRAIN()), VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::BRAIN())), true); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::CORAL_FAN, 2), VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::BUBBLE()), VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::BUBBLE())), true); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::CORAL_FAN, 3), VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::FIRE()), VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::FIRE())), true); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::CORAL_FAN, 4), VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::HORN()), VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::HORN())), true); + $this->register(new Coal(new ItemIdentifier(ItemIds::COAL, 1), "Charcoal")); + $this->register(new CocoaBeans(new ItemIdentifier(ItemIds::DYE, 3), "Cocoa Beans")); + $this->register(new Compass(new ItemIdentifier(ItemIds::COMPASS, 0), "Compass")); + $this->register(new CookedChicken(new ItemIdentifier(ItemIds::COOKED_CHICKEN, 0), "Cooked Chicken")); + $this->register(new CookedFish(new ItemIdentifier(ItemIds::COOKED_FISH, 0), "Cooked Fish")); + $this->register(new CookedMutton(new ItemIdentifier(ItemIds::COOKED_MUTTON, 0), "Cooked Mutton")); + $this->register(new CookedPorkchop(new ItemIdentifier(ItemIds::COOKED_PORKCHOP, 0), "Cooked Porkchop")); + $this->register(new CookedRabbit(new ItemIdentifier(ItemIds::COOKED_RABBIT, 0), "Cooked Rabbit")); + $this->register(new CookedSalmon(new ItemIdentifier(ItemIds::COOKED_SALMON, 0), "Cooked Salmon")); + $this->register(new Cookie(new ItemIdentifier(ItemIds::COOKIE, 0), "Cookie")); + $this->register(new DriedKelp(new ItemIdentifier(ItemIds::DRIED_KELP, 0), "Dried Kelp")); + $this->register(new Egg(new ItemIdentifier(ItemIds::EGG, 0), "Egg")); + $this->register(new EnderPearl(new ItemIdentifier(ItemIds::ENDER_PEARL, 0), "Ender Pearl")); + $this->register(new ExperienceBottle(new ItemIdentifier(ItemIds::EXPERIENCE_BOTTLE, 0), "Bottle o' Enchanting")); + $this->register(new Fertilizer(new ItemIdentifier(ItemIds::DYE, 15), "Bone Meal")); + $this->register(new FishingRod(new ItemIdentifier(ItemIds::FISHING_ROD, 0), "Fishing Rod")); + $this->register(new FlintSteel(new ItemIdentifier(ItemIds::FLINT_STEEL, 0), "Flint and Steel")); + $this->register(new GlassBottle(new ItemIdentifier(ItemIds::GLASS_BOTTLE, 0), "Glass Bottle")); + $this->register(new GoldenApple(new ItemIdentifier(ItemIds::GOLDEN_APPLE, 0), "Golden Apple")); + $this->register(new GoldenAppleEnchanted(new ItemIdentifier(ItemIds::ENCHANTED_GOLDEN_APPLE, 0), "Enchanted Golden Apple")); + $this->register(new GoldenCarrot(new ItemIdentifier(ItemIds::GOLDEN_CARROT, 0), "Golden Carrot")); + $this->register(new Item(new ItemIdentifier(ItemIds::BLAZE_POWDER, 0), "Blaze Powder")); + $this->register(new Item(new ItemIdentifier(ItemIds::BLEACH, 0), "Bleach")); //EDU + $this->register(new Item(new ItemIdentifier(ItemIds::BONE, 0), "Bone")); + $this->register(new Item(new ItemIdentifier(ItemIds::BRICK, 0), "Brick")); + $this->register(new Item(new ItemIdentifier(ItemIds::CHORUS_FRUIT_POPPED, 0), "Popped Chorus Fruit")); + $this->register(new Item(new ItemIdentifier(ItemIds::CLAY_BALL, 0), "Clay")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 0), "Salt")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 1), "Sodium Oxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 2), "Sodium Hydroxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 3), "Magnesium Nitrate")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 4), "Iron Sulphide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 5), "Lithium Hydride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 6), "Sodium Hydride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 7), "Calcium Bromide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 8), "Magnesium Oxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 9), "Sodium Acetate")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 10), "Luminol")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 11), "Charcoal")); //??? maybe bug + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 12), "Sugar")); //??? maybe bug + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 13), "Aluminium Oxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 14), "Boron Trioxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 15), "Soap")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 16), "Polyethylene")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 17), "Rubbish")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 18), "Magnesium Salts")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 19), "Sulphate")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 20), "Barium Sulphate")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 21), "Potassium Chloride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 22), "Mercuric Chloride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 23), "Cerium Chloride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 24), "Tungsten Chloride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 25), "Calcium Chloride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 26), "Water")); //??? + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 27), "Glue")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 28), "Hypochlorite")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 29), "Crude Oil")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 30), "Latex")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 31), "Potassium Iodide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 32), "Sodium Fluoride")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 33), "Benzene")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 34), "Ink")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 35), "Hydrogen Peroxide")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 36), "Ammonia")); + $this->register(new Item(new ItemIdentifier(ItemIds::COMPOUND, 37), "Sodium Hypochlorite")); + $this->register(new Item(new ItemIdentifier(ItemIds::DIAMOND, 0), "Diamond")); + $this->register(new Item(new ItemIdentifier(ItemIds::DRAGON_BREATH, 0), "Dragon's Breath")); + $this->register(new Item(new ItemIdentifier(ItemIds::DYE, 0), "Ink Sac")); + $this->register(new Item(new ItemIdentifier(ItemIds::DYE, 4), "Lapis Lazuli")); + $this->register(new Item(new ItemIdentifier(ItemIds::EMERALD, 0), "Emerald")); + $this->register(new Item(new ItemIdentifier(ItemIds::FEATHER, 0), "Feather")); + $this->register(new Item(new ItemIdentifier(ItemIds::FERMENTED_SPIDER_EYE, 0), "Fermented Spider Eye")); + $this->register(new Item(new ItemIdentifier(ItemIds::FLINT, 0), "Flint")); + $this->register(new Item(new ItemIdentifier(ItemIds::GHAST_TEAR, 0), "Ghast Tear")); + $this->register(new Item(new ItemIdentifier(ItemIds::GLISTERING_MELON, 0), "Glistering Melon")); + $this->register(new Item(new ItemIdentifier(ItemIds::GLOWSTONE_DUST, 0), "Glowstone Dust")); + $this->register(new Item(new ItemIdentifier(ItemIds::GOLD_INGOT, 0), "Gold Ingot")); + $this->register(new Item(new ItemIdentifier(ItemIds::GOLD_NUGGET, 0), "Gold Nugget")); + $this->register(new Item(new ItemIdentifier(ItemIds::GUNPOWDER, 0), "Gunpowder")); + $this->register(new Item(new ItemIdentifier(ItemIds::HEART_OF_THE_SEA, 0), "Heart of the Sea")); + $this->register(new Item(new ItemIdentifier(ItemIds::IRON_INGOT, 0), "Iron Ingot")); + $this->register(new Item(new ItemIdentifier(ItemIds::IRON_NUGGET, 0), "Iron Nugget")); + $this->register(new Item(new ItemIdentifier(ItemIds::LEATHER, 0), "Leather")); + $this->register(new Item(new ItemIdentifier(ItemIds::MAGMA_CREAM, 0), "Magma Cream")); + $this->register(new Item(new ItemIdentifier(ItemIds::NAUTILUS_SHELL, 0), "Nautilus Shell")); + $this->register(new Item(new ItemIdentifier(ItemIds::NETHER_BRICK, 0), "Nether Brick")); + $this->register(new Item(new ItemIdentifier(ItemIds::NETHER_QUARTZ, 0), "Nether Quartz")); + $this->register(new Item(new ItemIdentifier(ItemIds::NETHER_STAR, 0), "Nether Star")); + $this->register(new Item(new ItemIdentifier(ItemIds::PAPER, 0), "Paper")); + $this->register(new Item(new ItemIdentifier(ItemIds::PRISMARINE_CRYSTALS, 0), "Prismarine Crystals")); + $this->register(new Item(new ItemIdentifier(ItemIds::PRISMARINE_SHARD, 0), "Prismarine Shard")); + $this->register(new Item(new ItemIdentifier(ItemIds::RABBIT_FOOT, 0), "Rabbit's Foot")); + $this->register(new Item(new ItemIdentifier(ItemIds::RABBIT_HIDE, 0), "Rabbit Hide")); + $this->register(new Item(new ItemIdentifier(ItemIds::SHULKER_SHELL, 0), "Shulker Shell")); + $this->register(new Item(new ItemIdentifier(ItemIds::SLIME_BALL, 0), "Slimeball")); + $this->register(new Item(new ItemIdentifier(ItemIds::SUGAR, 0), "Sugar")); + $this->register(new Item(new ItemIdentifier(ItemIds::TURTLE_SHELL_PIECE, 0), "Scute")); + $this->register(new Item(new ItemIdentifier(ItemIds::WHEAT, 0), "Wheat")); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::ACACIA_DOOR, 0), VanillaBlocks::ACACIA_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::BIRCH_DOOR, 0), VanillaBlocks::BIRCH_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::BREWING_STAND, 0), VanillaBlocks::BREWING_STAND())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::CAKE, 0), VanillaBlocks::CAKE())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::COMPARATOR, 0), VanillaBlocks::REDSTONE_COMPARATOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::DARK_OAK_DOOR, 0), VanillaBlocks::DARK_OAK_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::FLOWER_POT, 0), VanillaBlocks::FLOWER_POT())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::HOPPER, 0), VanillaBlocks::HOPPER())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::IRON_DOOR, 0), VanillaBlocks::IRON_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::ITEM_FRAME, 0), VanillaBlocks::ITEM_FRAME())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::JUNGLE_DOOR, 0), VanillaBlocks::JUNGLE_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::NETHER_WART, 0), VanillaBlocks::NETHER_WART())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::OAK_DOOR, 0), VanillaBlocks::OAK_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::REPEATER, 0), VanillaBlocks::REDSTONE_REPEATER())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::SPRUCE_DOOR, 0), VanillaBlocks::SPRUCE_DOOR())); + $this->register(new ItemBlock(new ItemIdentifier(ItemIds::SUGARCANE, 0), VanillaBlocks::SUGARCANE())); + + //the meta values for buckets are intentionally hardcoded because block IDs will change in the future + $waterBucket = new LiquidBucket(new ItemIdentifier(ItemIds::BUCKET, 8), "Water Bucket", VanillaBlocks::WATER()); + $this->register($waterBucket); + $this->remap(new ItemIdentifier(ItemIds::BUCKET, 9), $waterBucket); + $lavaBucket = new LiquidBucket(new ItemIdentifier(ItemIds::BUCKET, 10), "Lava Bucket", VanillaBlocks::LAVA()); + $this->register($lavaBucket); + $this->remap(new ItemIdentifier(ItemIds::BUCKET, 11), $lavaBucket); + $this->register(new Melon(new ItemIdentifier(ItemIds::MELON, 0), "Melon")); + $this->register(new MelonSeeds(new ItemIdentifier(ItemIds::MELON_SEEDS, 0), "Melon Seeds")); + $this->register(new MilkBucket(new ItemIdentifier(ItemIds::BUCKET, 1), "Milk Bucket")); + $this->register(new Minecart(new ItemIdentifier(ItemIds::MINECART, 0), "Minecart")); + $this->register(new MushroomStew(new ItemIdentifier(ItemIds::MUSHROOM_STEW, 0), "Mushroom Stew")); + $this->register(new PaintingItem(new ItemIdentifier(ItemIds::PAINTING, 0), "Painting")); + $this->register(new PoisonousPotato(new ItemIdentifier(ItemIds::POISONOUS_POTATO, 0), "Poisonous Potato")); + $this->register(new Potato(new ItemIdentifier(ItemIds::POTATO, 0), "Potato")); + $this->register(new Pufferfish(new ItemIdentifier(ItemIds::PUFFERFISH, 0), "Pufferfish")); + $this->register(new PumpkinPie(new ItemIdentifier(ItemIds::PUMPKIN_PIE, 0), "Pumpkin Pie")); + $this->register(new PumpkinSeeds(new ItemIdentifier(ItemIds::PUMPKIN_SEEDS, 0), "Pumpkin Seeds")); + $this->register(new RabbitStew(new ItemIdentifier(ItemIds::RABBIT_STEW, 0), "Rabbit Stew")); + $this->register(new RawBeef(new ItemIdentifier(ItemIds::RAW_BEEF, 0), "Raw Beef")); + $this->register(new RawChicken(new ItemIdentifier(ItemIds::RAW_CHICKEN, 0), "Raw Chicken")); + $this->register(new RawFish(new ItemIdentifier(ItemIds::RAW_FISH, 0), "Raw Fish")); + $this->register(new RawMutton(new ItemIdentifier(ItemIds::RAW_MUTTON, 0), "Raw Mutton")); + $this->register(new RawPorkchop(new ItemIdentifier(ItemIds::RAW_PORKCHOP, 0), "Raw Porkchop")); + $this->register(new RawRabbit(new ItemIdentifier(ItemIds::RAW_RABBIT, 0), "Raw Rabbit")); + $this->register(new RawSalmon(new ItemIdentifier(ItemIds::RAW_SALMON, 0), "Raw Salmon")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_13, 0), RecordType::DISK_13(), "Record 13")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_CAT, 0), RecordType::DISK_CAT(), "Record Cat")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_BLOCKS, 0), RecordType::DISK_BLOCKS(), "Record Blocks")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_CHIRP, 0), RecordType::DISK_CHIRP(), "Record Chirp")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_FAR, 0), RecordType::DISK_FAR(), "Record Far")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_MALL, 0), RecordType::DISK_MALL(), "Record Mall")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_MELLOHI, 0), RecordType::DISK_MELLOHI(), "Record Mellohi")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_STAL, 0), RecordType::DISK_STAL(), "Record Stal")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_STRAD, 0), RecordType::DISK_STRAD(), "Record Strad")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_WARD, 0), RecordType::DISK_WARD(), "Record Ward")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_11, 0), RecordType::DISK_11(), "Record 11")); + $this->register(new Record(new ItemIdentifier(ItemIds::RECORD_WAIT, 0), RecordType::DISK_WAIT(), "Record Wait")); + $this->register(new Redstone(new ItemIdentifier(ItemIds::REDSTONE, 0), "Redstone")); + $this->register(new RottenFlesh(new ItemIdentifier(ItemIds::ROTTEN_FLESH, 0), "Rotten Flesh")); + $this->register(new Shears(new ItemIdentifier(ItemIds::SHEARS, 0), "Shears")); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::SIGN, 0), VanillaBlocks::OAK_SIGN(), VanillaBlocks::OAK_WALL_SIGN())); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::SPRUCE_SIGN, 0), VanillaBlocks::SPRUCE_SIGN(), VanillaBlocks::SPRUCE_WALL_SIGN())); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::BIRCH_SIGN, 0), VanillaBlocks::BIRCH_SIGN(), VanillaBlocks::BIRCH_WALL_SIGN())); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::JUNGLE_SIGN, 0), VanillaBlocks::JUNGLE_SIGN(), VanillaBlocks::JUNGLE_WALL_SIGN())); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::ACACIA_SIGN, 0), VanillaBlocks::ACACIA_SIGN(), VanillaBlocks::ACACIA_WALL_SIGN())); + $this->register(new ItemBlockWallOrFloor(new ItemIdentifier(ItemIds::DARKOAK_SIGN, 0), VanillaBlocks::DARK_OAK_SIGN(), VanillaBlocks::DARK_OAK_WALL_SIGN())); + $this->register(new Snowball(new ItemIdentifier(ItemIds::SNOWBALL, 0), "Snowball")); + $this->register(new SpiderEye(new ItemIdentifier(ItemIds::SPIDER_EYE, 0), "Spider Eye")); + $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")); + $this->register(new WrittenBook(new ItemIdentifier(ItemIds::WRITTEN_BOOK, 0), "Written Book")); + + foreach(SkullType::getAll() as $skullType){ + $this->register(new Skull(new ItemIdentifier(ItemIds::SKULL, $skullType->getMagicNumber()), $skullType->getDisplayName(), $skullType)); + } + + $dyeMap = [ + DyeColor::BLACK()->id() => 16, + DyeColor::BROWN()->id() => 17, + DyeColor::BLUE()->id() => 18, + DyeColor::WHITE()->id() => 19 + ]; + $colorIdMap = DyeColorIdMap::getInstance(); + foreach(DyeColor::getAll() as $color){ + //TODO: use colour object directly + //TODO: add interface to dye-colour objects + $this->register(new Dye(new ItemIdentifier(ItemIds::DYE, $dyeMap[$color->id()] ?? $colorIdMap->toInvertedId($color)), $color->getDisplayName() . " Dye", $color)); + $this->register(new Bed(new ItemIdentifier(ItemIds::BED, $colorIdMap->toId($color)), $color->getDisplayName() . " Bed", $color)); + $this->register((new Banner( + new ItemIdentifier(ItemIds::BANNER, 0), + VanillaBlocks::BANNER(), + VanillaBlocks::WALL_BANNER() + ))->setColor($color)); + } + + foreach(PotionType::getAll() as $type){ + $typeId = PotionTypeIdMap::getInstance()->toId($type); + $this->register(new Potion(new ItemIdentifier(ItemIds::POTION, $typeId), $type->getDisplayName() . " Potion", $type)); + $this->register(new SplashPotion(new ItemIdentifier(ItemIds::SPLASH_POTION, $typeId), $type->getDisplayName() . " Splash Potion", $type)); + } + + foreach(TreeType::getAll() as $type){ + $this->register(new Boat(new ItemIdentifier(ItemIds::BOAT, $type->getMagicNumber()), $type->getDisplayName() . " Boat", $type)); + } + + //region --- auto-generated TODOs --- + //TODO: minecraft:armor_stand + //TODO: minecraft:balloon + //TODO: minecraft:banner_pattern + //TODO: minecraft:campfire + //TODO: minecraft:carrotOnAStick + //TODO: minecraft:chest_minecart + //TODO: minecraft:command_block_minecart + //TODO: minecraft:crossbow + //TODO: minecraft:elytra + //TODO: minecraft:emptyMap + //TODO: minecraft:enchanted_book + //TODO: minecraft:end_crystal + //TODO: minecraft:ender_eye + //TODO: minecraft:fireball + //TODO: minecraft:fireworks + //TODO: minecraft:fireworksCharge + //TODO: minecraft:glow_stick + //TODO: minecraft:hopper_minecart + //TODO: minecraft:horsearmordiamond + //TODO: minecraft:horsearmorgold + //TODO: minecraft:horsearmoriron + //TODO: minecraft:horsearmorleather + //TODO: minecraft:ice_bomb + //TODO: minecraft:kelp + //TODO: minecraft:lead + //TODO: minecraft:lingering_potion + //TODO: minecraft:map + //TODO: minecraft:medicine + //TODO: minecraft:name_tag + //TODO: minecraft:phantom_membrane + //TODO: minecraft:rapid_fertilizer + //TODO: minecraft:record_pigstep + //TODO: minecraft:saddle + //TODO: minecraft:shield + //TODO: minecraft:sparkler + //TODO: minecraft:spawn_egg + //TODO: minecraft:tnt_minecart + //TODO: minecraft:trident + //TODO: minecraft:turtle_helmet + //endregion + } + + private function registerSpawnEggs() : void{ + //TODO: the meta values should probably be hardcoded; they won't change, but the EntityLegacyIds might + $this->register(new class(new ItemIdentifier(ItemIds::SPAWN_EGG, EntityLegacyIds::ZOMBIE), "Zombie Spawn Egg") extends SpawnEgg{ + protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + return new Zombie(Location::fromObject($pos, $world, $yaw, $pitch)); + } + }); + $this->register(new class(new ItemIdentifier(ItemIds::SPAWN_EGG, EntityLegacyIds::SQUID), "Squid Spawn Egg") extends SpawnEgg{ + public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + return new Squid(Location::fromObject($pos, $world, $yaw, $pitch)); + } + }); + $this->register(new class(new ItemIdentifier(ItemIds::SPAWN_EGG, EntityLegacyIds::VILLAGER), "Villager Spawn Egg") extends SpawnEgg{ + public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + return new Villager(Location::fromObject($pos, $world, $yaw, $pitch)); + } + }); + } + + private function registerTierToolItems() : void{ + $this->register(new Axe(new ItemIdentifier(ItemIds::DIAMOND_AXE, 0), "Diamond Axe", ToolTier::DIAMOND())); + $this->register(new Axe(new ItemIdentifier(ItemIds::GOLDEN_AXE, 0), "Golden Axe", ToolTier::GOLD())); + $this->register(new Axe(new ItemIdentifier(ItemIds::IRON_AXE, 0), "Iron Axe", ToolTier::IRON())); + $this->register(new Axe(new ItemIdentifier(ItemIds::STONE_AXE, 0), "Stone Axe", ToolTier::STONE())); + $this->register(new Axe(new ItemIdentifier(ItemIds::WOODEN_AXE, 0), "Wooden Axe", ToolTier::WOOD())); + $this->register(new Hoe(new ItemIdentifier(ItemIds::DIAMOND_HOE, 0), "Diamond Hoe", ToolTier::DIAMOND())); + $this->register(new Hoe(new ItemIdentifier(ItemIds::GOLDEN_HOE, 0), "Golden Hoe", ToolTier::GOLD())); + $this->register(new Hoe(new ItemIdentifier(ItemIds::IRON_HOE, 0), "Iron Hoe", ToolTier::IRON())); + $this->register(new Hoe(new ItemIdentifier(ItemIds::STONE_HOE, 0), "Stone Hoe", ToolTier::STONE())); + $this->register(new Hoe(new ItemIdentifier(ItemIds::WOODEN_HOE, 0), "Wooden Hoe", ToolTier::WOOD())); + $this->register(new Pickaxe(new ItemIdentifier(ItemIds::DIAMOND_PICKAXE, 0), "Diamond Pickaxe", ToolTier::DIAMOND())); + $this->register(new Pickaxe(new ItemIdentifier(ItemIds::GOLDEN_PICKAXE, 0), "Golden Pickaxe", ToolTier::GOLD())); + $this->register(new Pickaxe(new ItemIdentifier(ItemIds::IRON_PICKAXE, 0), "Iron Pickaxe", ToolTier::IRON())); + $this->register(new Pickaxe(new ItemIdentifier(ItemIds::STONE_PICKAXE, 0), "Stone Pickaxe", ToolTier::STONE())); + $this->register(new Pickaxe(new ItemIdentifier(ItemIds::WOODEN_PICKAXE, 0), "Wooden Pickaxe", ToolTier::WOOD())); + $this->register(new Shovel(new ItemIdentifier(ItemIds::DIAMOND_SHOVEL, 0), "Diamond Shovel", ToolTier::DIAMOND())); + $this->register(new Shovel(new ItemIdentifier(ItemIds::GOLDEN_SHOVEL, 0), "Golden Shovel", ToolTier::GOLD())); + $this->register(new Shovel(new ItemIdentifier(ItemIds::IRON_SHOVEL, 0), "Iron Shovel", ToolTier::IRON())); + $this->register(new Shovel(new ItemIdentifier(ItemIds::STONE_SHOVEL, 0), "Stone Shovel", ToolTier::STONE())); + $this->register(new Shovel(new ItemIdentifier(ItemIds::WOODEN_SHOVEL, 0), "Wooden Shovel", ToolTier::WOOD())); + $this->register(new Sword(new ItemIdentifier(ItemIds::DIAMOND_SWORD, 0), "Diamond Sword", ToolTier::DIAMOND())); + $this->register(new Sword(new ItemIdentifier(ItemIds::GOLDEN_SWORD, 0), "Golden Sword", ToolTier::GOLD())); + $this->register(new Sword(new ItemIdentifier(ItemIds::IRON_SWORD, 0), "Iron Sword", ToolTier::IRON())); + $this->register(new Sword(new ItemIdentifier(ItemIds::STONE_SWORD, 0), "Stone Sword", ToolTier::STONE())); + $this->register(new Sword(new ItemIdentifier(ItemIds::WOODEN_SWORD, 0), "Wooden Sword", ToolTier::WOOD())); + } + + private function registerArmorItems() : void{ + $this->register(new Armor(new ItemIdentifier(ItemIds::CHAIN_BOOTS, 0), "Chainmail Boots", new ArmorTypeInfo(1, 196, ArmorInventory::SLOT_FEET))); + $this->register(new Armor(new ItemIdentifier(ItemIds::DIAMOND_BOOTS, 0), "Diamond Boots", new ArmorTypeInfo(3, 430, ArmorInventory::SLOT_FEET))); + $this->register(new Armor(new ItemIdentifier(ItemIds::GOLDEN_BOOTS, 0), "Golden Boots", new ArmorTypeInfo(1, 92, ArmorInventory::SLOT_FEET))); + $this->register(new Armor(new ItemIdentifier(ItemIds::IRON_BOOTS, 0), "Iron Boots", new ArmorTypeInfo(2, 196, ArmorInventory::SLOT_FEET))); + $this->register(new Armor(new ItemIdentifier(ItemIds::LEATHER_BOOTS, 0), "Leather Boots", new ArmorTypeInfo(1, 66, ArmorInventory::SLOT_FEET))); + $this->register(new Armor(new ItemIdentifier(ItemIds::CHAIN_CHESTPLATE, 0), "Chainmail Chestplate", new ArmorTypeInfo(5, 241, ArmorInventory::SLOT_CHEST))); + $this->register(new Armor(new ItemIdentifier(ItemIds::DIAMOND_CHESTPLATE, 0), "Diamond Chestplate", new ArmorTypeInfo(8, 529, ArmorInventory::SLOT_CHEST))); + $this->register(new Armor(new ItemIdentifier(ItemIds::GOLDEN_CHESTPLATE, 0), "Golden Chestplate", new ArmorTypeInfo(5, 113, ArmorInventory::SLOT_CHEST))); + $this->register(new Armor(new ItemIdentifier(ItemIds::IRON_CHESTPLATE, 0), "Iron Chestplate", new ArmorTypeInfo(6, 241, ArmorInventory::SLOT_CHEST))); + $this->register(new Armor(new ItemIdentifier(ItemIds::LEATHER_CHESTPLATE, 0), "Leather Tunic", new ArmorTypeInfo(3, 81, ArmorInventory::SLOT_CHEST))); + $this->register(new Armor(new ItemIdentifier(ItemIds::CHAIN_HELMET, 0), "Chainmail Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD))); + $this->register(new Armor(new ItemIdentifier(ItemIds::DIAMOND_HELMET, 0), "Diamond Helmet", new ArmorTypeInfo(3, 364, ArmorInventory::SLOT_HEAD))); + $this->register(new Armor(new ItemIdentifier(ItemIds::GOLDEN_HELMET, 0), "Golden Helmet", new ArmorTypeInfo(2, 78, ArmorInventory::SLOT_HEAD))); + $this->register(new Armor(new ItemIdentifier(ItemIds::IRON_HELMET, 0), "Iron Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD))); + $this->register(new Armor(new ItemIdentifier(ItemIds::LEATHER_HELMET, 0), "Leather Cap", new ArmorTypeInfo(1, 56, ArmorInventory::SLOT_HEAD))); + $this->register(new Armor(new ItemIdentifier(ItemIds::CHAIN_LEGGINGS, 0), "Chainmail Leggings", new ArmorTypeInfo(4, 226, ArmorInventory::SLOT_LEGS))); + $this->register(new Armor(new ItemIdentifier(ItemIds::DIAMOND_LEGGINGS, 0), "Diamond Leggings", new ArmorTypeInfo(6, 496, ArmorInventory::SLOT_LEGS))); + $this->register(new Armor(new ItemIdentifier(ItemIds::GOLDEN_LEGGINGS, 0), "Golden Leggings", new ArmorTypeInfo(3, 106, ArmorInventory::SLOT_LEGS))); + $this->register(new Armor(new ItemIdentifier(ItemIds::IRON_LEGGINGS, 0), "Iron Leggings", new ArmorTypeInfo(5, 226, ArmorInventory::SLOT_LEGS))); + $this->register(new Armor(new ItemIdentifier(ItemIds::LEATHER_LEGGINGS, 0), "Leather Pants", new ArmorTypeInfo(2, 76, ArmorInventory::SLOT_LEGS))); + } + + /** + * Maps an item type to its corresponding ID. This is necessary to ensure that the item is correctly loaded when + * reading data from disk storage. + * + * NOTE: If you are registering a new item type, you will need to add it to the creative inventory yourself - it + * will not automatically appear there. + * + * @throws \RuntimeException if something attempted to override an already-registered item without specifying the + * $override parameter. + */ + public function register(Item $item, bool $override = false) : void{ + $id = $item->getId(); + $variant = $item->getMeta(); + + if(!$override and $this->isRegistered($id, $variant)){ + throw new \RuntimeException("Trying to overwrite an already registered item"); + } + + $this->list[self::getListOffset($id, $variant)] = clone $item; + } + + public function remap(ItemIdentifier $identifier, Item $item, bool $override = false) : void{ + if(!$override && $this->isRegistered($identifier->getId(), $identifier->getMeta())){ + throw new \RuntimeException("Trying to overwrite an already registered item"); + } + + $this->list[self::getListOffset($identifier->getId(), $identifier->getMeta())] = clone $item; + } + + private static function itemToBlockId(int $id) : int{ + return $id < 0 ? 255 - $id : $id; + } + + /** + * @deprecated This method should ONLY be used for deserializing data, e.g. from a config or database. For all other + * purposes, use VanillaItems. + * @see VanillaItems + * + * 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 */ + $item = null; + 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){ + if($meta <= $this->list[$zero]->getMaxDurability()){ + $item = clone $this->list[$zero]; + $item->setDamage($meta); + }else{ + $item = new Item(new ItemIdentifier($id, $meta)); + } + }elseif($id < 256){ //intentionally includes negatives, for extended block IDs + //TODO: do not assume that item IDs and block IDs are the same or related + $item = new ItemBlock(new ItemIdentifier($id, $meta), BlockFactory::getInstance()->get(self::itemToBlockId($id), $meta & 0xf)); + } + } + + if($item === null){ + //negative damage values will fallthru to here, to avoid crazy shit with crafting wildcard hacks + $item = new Item(new ItemIdentifier($id, $meta)); + } + + $item->setCount($count); + if($tags !== null){ + $item->setNamedTag($tags); + } + return $item; + } + + public static function air() : Item{ + return self::getInstance()->get(ItemIds::AIR, 0, 0); + } + + /** + * Returns whether the specified item ID is already registered in the item factory. + */ + public function isRegistered(int $id, int $variant = 0) : bool{ + if($id < 256){ + return BlockFactory::getInstance()->isRegistered(self::itemToBlockId($id)); + } + + return isset($this->list[self::getListOffset($id, $variant)]); + } + + private static function getListOffset(int $id, int $variant) : int{ + if($id < -0x8000 or $id > 0x7fff){ + throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); + } + return (($id & 0xffff) << 16) | ($variant & 0xffff); + } + + /** + * @return Item[] + */ + public function getAllRegistered() : array{ + return $this->list; + } +} diff --git a/src/item/ItemIdentifier.php b/src/item/ItemIdentifier.php new file mode 100644 index 0000000000..0242706f42 --- /dev/null +++ b/src/item/ItemIdentifier.php @@ -0,0 +1,48 @@ + 0x7fff){ //signed short range + throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); + } + $this->id = $id; + $this->meta = $meta !== -1 ? $meta & 0x7FFF : -1; + } + + public function getId() : int{ + return $this->id; + } + + public function getMeta() : int{ + return $this->meta; + } +} diff --git a/src/item/ItemIds.php b/src/item/ItemIds.php new file mode 100644 index 0000000000..a38e53171a --- /dev/null +++ b/src/item/ItemIds.php @@ -0,0 +1,736 @@ + $id){ + if(!is_int($id)) throw new AssumptionFailedError("Invalid mappings format, expected int values"); + $result->addMapping((string) $name, $id); + } + + return $result; + } + + /** + * @var int[] + * @phpstan-var array + */ + private $map = []; + + public function __construct(ItemFactory $itemFactory){ + $this->itemFactory = $itemFactory; + } + + public function addMapping(string $alias, int $id) : void{ + $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. + * + * Example accepted formats: + * - `diamond_pickaxe:5` + * - `minecraft:string` + * - `351:4 (lapis lazuli ID:meta)` + * + * @throws LegacyStringToItemParserException if the given string cannot be parsed as an item identifier + */ + public function parse(string $input) : Item{ + $key = $this->reprocess($input); + $b = explode(":", $key); + + if(!isset($b[1])){ + $meta = 0; + }elseif(is_numeric($b[1])){ + $meta = (int) $b[1]; + }else{ + throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value"); + } + + 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"); + } + + return $item; + } + + protected function reprocess(string $input) : string{ + return str_replace([" ", "minecraft:"], ["_", ""], trim($input)); + } +} diff --git a/src/item/LegacyStringToItemParserException.php b/src/item/LegacyStringToItemParserException.php new file mode 100644 index 0000000000..8b6e7212da --- /dev/null +++ b/src/item/LegacyStringToItemParserException.php @@ -0,0 +1,28 @@ +liquid = $liquid; + } + + public function getMaxStackSize() : int{ + return 1; + } + + public function getFuelTime() : int{ + if($this->liquid instanceof Lava){ + return 20000; + } + + return 0; + } + + public function getFuelResidue() : Item{ + return VanillaItems::BUCKET(); + } + + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ + if(!$blockReplace->canBeReplaced()){ + return ItemUseResult::NONE(); + } + + //TODO: move this to generic placement logic + $resultBlock = clone $this->liquid; + + $ev = new PlayerBucketEmptyEvent($player, $blockReplace, $face, $this, VanillaItems::BUCKET()); + $ev->call(); + if(!$ev->isCancelled()){ + $player->getWorld()->setBlock($blockReplace->getPosition(), $resultBlock->getFlowingForm()); + $player->getWorld()->addSound($blockReplace->getPosition()->add(0.5, 0.5, 0.5), $resultBlock->getBucketEmptySound()); + + if($player->hasFiniteResources()){ + $player->getInventory()->setItemInHand($ev->getItem()); + } + return ItemUseResult::SUCCESS(); + } + + return ItemUseResult::FAIL(); + } + + public function getLiquid() : Liquid{ + return $this->liquid; + } +} diff --git a/src/pocketmine/item/Melon.php b/src/item/Melon.php similarity index 90% rename from src/pocketmine/item/Melon.php rename to src/item/Melon.php index 104fa10d62..f1807296e7 100644 --- a/src/pocketmine/item/Melon.php +++ b/src/item/Melon.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Melon extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::MELON, $meta, "Melon"); - } public function getFoodRestore() : int{ return 2; diff --git a/src/pocketmine/item/MelonSeeds.php b/src/item/MelonSeeds.php similarity index 78% rename from src/pocketmine/item/MelonSeeds.php rename to src/item/MelonSeeds.php index a536f93969..c475673a6e 100644 --- a/src/pocketmine/item/MelonSeeds.php +++ b/src/item/MelonSeeds.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class MelonSeeds extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::MELON_SEEDS, $meta, "Melon Seeds"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::MELON_STEM); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::MELON_STEM(); } } diff --git a/src/pocketmine/item/SplashPotion.php b/src/item/MilkBucket.php similarity index 66% rename from src/pocketmine/item/SplashPotion.php rename to src/item/MilkBucket.php index fe1db43115..f8c8b3ddf1 100644 --- a/src/pocketmine/item/SplashPotion.php +++ b/src/item/MilkBucket.php @@ -23,27 +23,28 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\nbt\tag\CompoundTag; +use pocketmine\entity\Living; +use pocketmine\player\Player; -class SplashPotion extends ProjectileItem{ - - public function __construct(int $meta = 0){ - parent::__construct(self::SPLASH_POTION, $meta, "Splash Potion"); - } +class MilkBucket extends Item implements ConsumableItem{ public function getMaxStackSize() : int{ return 1; } - public function getProjectileEntityType() : string{ - return "ThrownPotion"; + public function getResidue() : Item{ + return VanillaItems::BUCKET(); } - public function getThrowForce() : float{ - return 0.5; + public function getAdditionalEffects() : array{ + return []; } - protected function addExtraTags(CompoundTag $tag) : void{ - $tag->setShort("PotionId", $this->meta); + public function onConsume(Living $consumer) : void{ + $consumer->getEffects()->clear(); + } + + public function canStartUsingItem(Player $player) : bool{ + return true; } } diff --git a/src/pocketmine/item/Minecart.php b/src/item/Minecart.php similarity index 88% rename from src/pocketmine/item/Minecart.php rename to src/item/Minecart.php index bba2b997d6..59e47b98a3 100644 --- a/src/pocketmine/item/Minecart.php +++ b/src/item/Minecart.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Minecart extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::MINECART, $meta, "Minecart"); - } //TODO } diff --git a/src/pocketmine/item/MushroomStew.php b/src/item/MushroomStew.php similarity index 84% rename from src/pocketmine/item/MushroomStew.php rename to src/item/MushroomStew.php index a3776393d2..0120c7eb04 100644 --- a/src/pocketmine/item/MushroomStew.php +++ b/src/item/MushroomStew.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class MushroomStew extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::MUSHROOM_STEW, $meta, "Mushroom Stew"); - } public function getMaxStackSize() : int{ return 1; @@ -40,7 +37,7 @@ class MushroomStew extends Food{ return 7.2; } - public function getResidue(){ - return ItemFactory::get(Item::BOWL); + public function getResidue() : Item{ + return VanillaItems::BOWL(); } } diff --git a/src/pocketmine/item/PaintingItem.php b/src/item/PaintingItem.php similarity index 54% rename from src/pocketmine/item/PaintingItem.php rename to src/item/PaintingItem.php index 90709223a3..061f837ca8 100644 --- a/src/pocketmine/item/PaintingItem.php +++ b/src/item/PaintingItem.php @@ -24,23 +24,22 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\entity\Entity; +use pocketmine\entity\Location; use pocketmine\entity\object\Painting; use pocketmine\entity\object\PaintingMotive; +use pocketmine\math\Axis; +use pocketmine\math\Facing; use pocketmine\math\Vector3; -use pocketmine\network\mcpe\protocol\LevelEventPacket; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\sound\PaintingPlaceSound; use function array_rand; use function count; class PaintingItem extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::PAINTING, $meta, "Painting"); - } - public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{ - if($face === Vector3::SIDE_DOWN or $face === Vector3::SIDE_UP){ - return false; + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ + if(Facing::axis($face) === Axis::Y){ + return ItemUseResult::NONE(); } $motives = []; @@ -52,7 +51,7 @@ class PaintingItem extends Item{ continue; } - if(Painting::canFit($player->level, $blockReplace, $face, true, $motive)){ + if(Painting::canFit($player->getWorld(), $blockReplace->getPosition(), $face, true, $motive)){ if($currentTotalDimension > $totalDimension){ $totalDimension = $currentTotalDimension; /* @@ -68,41 +67,20 @@ class PaintingItem extends Item{ } if(count($motives) === 0){ //No space available - return false; + return ItemUseResult::NONE(); } /** @var PaintingMotive $motive */ $motive = $motives[array_rand($motives)]; - static $directions = [ - Vector3::SIDE_SOUTH => 0, - Vector3::SIDE_WEST => 1, - Vector3::SIDE_NORTH => 2, - Vector3::SIDE_EAST => 3 - ]; + $replacePos = $blockReplace->getPosition(); + $clickedPos = $blockClicked->getPosition(); - $direction = $directions[$face] ?? -1; - if($direction === -1){ - return false; - } + $entity = new Painting(Location::fromObject($replacePos, $replacePos->getWorld()), $clickedPos, $face, $motive); + $this->pop(); + $entity->spawnToAll(); - $nbt = Entity::createBaseNBT($blockReplace, null, $direction * 90, 0); - $nbt->setByte("Direction", $direction); - $nbt->setString("Motive", $motive->getName()); - $nbt->setInt("TileX", $blockClicked->getFloorX()); - $nbt->setInt("TileY", $blockClicked->getFloorY()); - $nbt->setInt("TileZ", $blockClicked->getFloorZ()); - - $entity = Entity::createEntity("Painting", $blockReplace->getLevelNonNull(), $nbt); - - if($entity instanceof Entity){ - $this->pop(); - $entity->spawnToAll(); - - $player->getLevelNonNull()->broadcastLevelEvent($blockReplace->add(0.5, 0.5, 0.5), LevelEventPacket::EVENT_SOUND_ITEMFRAME_PLACE); //item frame and painting have the same sound - return true; - } - - return false; + $player->getWorld()->addSound($replacePos->add(0.5, 0.5, 0.5), new PaintingPlaceSound()); + return ItemUseResult::SUCCESS(); } } diff --git a/src/pocketmine/item/Pickaxe.php b/src/item/Pickaxe.php similarity index 87% rename from src/pocketmine/item/Pickaxe.php rename to src/item/Pickaxe.php index 64bef6a3fe..4e1a7693e8 100644 --- a/src/pocketmine/item/Pickaxe.php +++ b/src/item/Pickaxe.php @@ -30,19 +30,19 @@ use pocketmine\entity\Entity; class Pickaxe extends TieredTool{ public function getBlockToolType() : int{ - return BlockToolType::TYPE_PICKAXE; + return BlockToolType::PICKAXE; } public function getBlockToolHarvestLevel() : int{ - return $this->tier; + return $this->tier->getHarvestLevel(); } public function getAttackPoints() : int{ - return self::getBaseDamageFromTier($this->tier) - 2; + return $this->tier->getBaseAttackPoints() - 2; } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() > 0){ + if(!$block->getBreakInfo()->breaksInstantly()){ return $this->applyDamage(1); } return false; diff --git a/src/pocketmine/item/PoisonousPotato.php b/src/item/PoisonousPotato.php similarity index 80% rename from src/pocketmine/item/PoisonousPotato.php rename to src/item/PoisonousPotato.php index 9b93ef0100..fb997d48ef 100644 --- a/src/pocketmine/item/PoisonousPotato.php +++ b/src/item/PoisonousPotato.php @@ -23,14 +23,11 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; use function mt_rand; class PoisonousPotato extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::POISONOUS_POTATO, $meta, "Poisonous Potato"); - } public function getFoodRestore() : int{ return 2; @@ -43,7 +40,7 @@ class PoisonousPotato extends Food{ public function getAdditionalEffects() : array{ if(mt_rand(0, 100) > 40){ return [ - new EffectInstance(Effect::getEffect(Effect::POISON), 100) + new EffectInstance(VanillaEffects::POISON(), 100) ]; } return []; diff --git a/src/pocketmine/item/Potato.php b/src/item/Potato.php similarity index 80% rename from src/pocketmine/item/Potato.php rename to src/item/Potato.php index b2531bb7d5..d4aff32fe8 100644 --- a/src/pocketmine/item/Potato.php +++ b/src/item/Potato.php @@ -24,15 +24,12 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class Potato extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::POTATO, $meta, "Potato"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::POTATO_BLOCK); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::POTATOES(); } public function getFoodRestore() : int{ diff --git a/src/item/Potion.php b/src/item/Potion.php new file mode 100644 index 0000000000..5be216e96d --- /dev/null +++ b/src/item/Potion.php @@ -0,0 +1,60 @@ +potionType = $potionType; + } + + public function getType() : PotionType{ return $this->potionType; } + + public function getMaxStackSize() : int{ + return 1; + } + + public function onConsume(Living $consumer) : void{ + + } + + public function getAdditionalEffects() : array{ + //TODO: check CustomPotionEffects NBT + return $this->potionType->getEffects(); + } + + public function getResidue() : Item{ + return VanillaItems::GLASS_BOTTLE(); + } + + public function canStartUsingItem(Player $player) : bool{ + return true; + } +} diff --git a/src/item/PotionType.php b/src/item/PotionType.php new file mode 100644 index 0000000000..952aa7e713 --- /dev/null +++ b/src/item/PotionType.php @@ -0,0 +1,228 @@ + []), + new self("mundane", "Mundane", fn() => []), + new self("long_mundane", "Long Mundane", fn() => []), + new self("thick", "Thick", fn() => []), + new self("awkward", "Awkward", fn() => []), + new self("night_vision", "Night Vision", fn() => [ + new EffectInstance(VanillaEffects::NIGHT_VISION(), 3600) + ]), + new self("long_night_vision", "Long Night Vision", fn() => [ + new EffectInstance(VanillaEffects::NIGHT_VISION(), 9600) + ]), + new self("invisibility", "Invisibility", fn() => [ + new EffectInstance(VanillaEffects::INVISIBILITY(), 3600) + ]), + new self("long_invisibility", "Long Invisibility", fn() => [ + new EffectInstance(VanillaEffects::INVISIBILITY(), 9600) + ]), + new self("leaping", "Leaping", fn() => [ + new EffectInstance(VanillaEffects::JUMP_BOOST(), 3600) + ]), + new self("long_leaping", "Long Leaping", fn() => [ + new EffectInstance(VanillaEffects::JUMP_BOOST(), 9600) + ]), + new self("strong_leaping", "Strong Leaping", fn() => [ + new EffectInstance(VanillaEffects::JUMP_BOOST(), 1800, 1) + ]), + new self("fire_resistance", "Fire Resistance", fn() => [ + new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 3600) + ]), + new self("long_fire_resistance", "Long Fire Resistance", fn() => [ + new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 9600) + ]), + new self("swiftness", "Swiftness", fn() => [ + new EffectInstance(VanillaEffects::SPEED(), 3600) + ]), + new self("long_swiftness", "Long Swiftness", fn() => [ + new EffectInstance(VanillaEffects::SPEED(), 9600) + ]), + new self("strong_swiftness", "Strong Swiftness", fn() => [ + new EffectInstance(VanillaEffects::SPEED(), 1800, 1) + ]), + new self("slowness", "Slowness", fn() => [ + new EffectInstance(VanillaEffects::SLOWNESS(), 1800) + ]), + new self("long_slowness", "Long Slowness", fn() => [ + new EffectInstance(VanillaEffects::SLOWNESS(), 4800) + ]), + new self("water_breathing", "Water Breathing", fn() => [ + new EffectInstance(VanillaEffects::WATER_BREATHING(), 3600) + ]), + new self("long_water_breathing", "Long Water Breathing", fn() => [ + new EffectInstance(VanillaEffects::WATER_BREATHING(), 9600) + ]), + new self("healing", "Healing", fn() => [ + new EffectInstance(VanillaEffects::INSTANT_HEALTH()) + ]), + new self("strong_healing", "Strong Healing", fn() => [ + new EffectInstance(VanillaEffects::INSTANT_HEALTH(), null, 1) + ]), + new self("harming", "Harming", fn() => [ + new EffectInstance(VanillaEffects::INSTANT_DAMAGE()) + ]), + new self("strong_harming", "Strong Harming", fn() => [ + new EffectInstance(VanillaEffects::INSTANT_DAMAGE(), null, 1) + ]), + new self("poison", "Poison", fn() => [ + new EffectInstance(VanillaEffects::POISON(), 900) + ]), + new self("long_poison", "Long Poison", fn() => [ + new EffectInstance(VanillaEffects::POISON(), 2400) + ]), + new self("strong_poison", "Strong Poison", fn() => [ + new EffectInstance(VanillaEffects::POISON(), 440, 1) + ]), + new self("regeneration", "Regeneration", fn() => [ + new EffectInstance(VanillaEffects::REGENERATION(), 900) + ]), + new self("long_regeneration", "Long Regeneration", fn() => [ + new EffectInstance(VanillaEffects::REGENERATION(), 2400) + ]), + new self("strong_regeneration", "Strong Regeneration", fn() => [ + new EffectInstance(VanillaEffects::REGENERATION(), 440, 1) + ]), + new self("strength", "Strength", fn() => [ + new EffectInstance(VanillaEffects::STRENGTH(), 3600) + ]), + new self("long_strength", "Long Strength", fn() => [ + new EffectInstance(VanillaEffects::STRENGTH(), 9600) + ]), + new self("strong_strength", "Strong Strength", fn() => [ + new EffectInstance(VanillaEffects::STRENGTH(), 1800, 1) + ]), + new self("weakness", "Weakness", fn() => [ + new EffectInstance(VanillaEffects::WEAKNESS(), 1800) + ]), + new self("long_weakness", "Long Weakness", fn() => [ + new EffectInstance(VanillaEffects::WEAKNESS(), 4800) + ]), + new self("wither", "Wither", fn() => [ + new EffectInstance(VanillaEffects::WITHER(), 800, 1) + ]), + new self("turtle_master", "Turtle Master", fn() => [ + //TODO + ]), + new self("long_turtle_master", "Long Turtle Master", fn() => [ + //TODO + ]), + new self("strong_turtle_master", "Strong Turtle Master", fn() => [ + //TODO + ]), + new self("slow_falling", "Slow Falling", fn() => [ + //TODO + ]), + new self("long_slow_falling", "Long Slow Falling", fn() => [ + //TODO + ]) + ); + } + + /** @phpstan-var \Closure() : list */ + private \Closure $effectsGetter; + + /** + * @phpstan-param \Closure() : list $effectsGetter + */ + private function __construct(string $enumName, string $displayName, \Closure $effectsGetter){ + $this->Enum___construct($enumName); + $this->displayName = $displayName; + $this->effectsGetter = $effectsGetter; + } + + public function getDisplayName() : string{ return $this->displayName; } + + /** + * @return EffectInstance[] + * @phpstan-return list + */ + public function getEffects() : array{ + return ($this->effectsGetter)(); + } +} diff --git a/src/item/ProjectileItem.php b/src/item/ProjectileItem.php new file mode 100644 index 0000000000..f8fb55673c --- /dev/null +++ b/src/item/ProjectileItem.php @@ -0,0 +1,60 @@ +getLocation(); + + $projectile = $this->createEntity(Location::fromObject($player->getEyePos(), $player->getWorld(), $location->yaw, $location->pitch), $player); + $projectile->setMotion($directionVector->multiply($this->getThrowForce())); + + $projectileEv = new ProjectileLaunchEvent($projectile); + $projectileEv->call(); + if($projectileEv->isCancelled()){ + $projectile->flagForDespawn(); + return ItemUseResult::FAIL(); + } + + $projectile->spawnToAll(); + + $location->getWorld()->addSound($location, new ThrowSound()); + + $this->pop(); + + return ItemUseResult::SUCCESS(); + } +} diff --git a/src/pocketmine/item/Pufferfish.php b/src/item/Pufferfish.php similarity index 72% rename from src/pocketmine/item/Pufferfish.php rename to src/item/Pufferfish.php index cef122f938..d657ae8ad7 100644 --- a/src/pocketmine/item/Pufferfish.php +++ b/src/item/Pufferfish.php @@ -23,13 +23,10 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; class Pufferfish extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::PUFFERFISH, $meta, "Pufferfish"); - } public function getFoodRestore() : int{ return 1; @@ -41,9 +38,9 @@ class Pufferfish extends Food{ public function getAdditionalEffects() : array{ return [ - new EffectInstance(Effect::getEffect(Effect::HUNGER), 300, 2), - new EffectInstance(Effect::getEffect(Effect::POISON), 1200, 3), - new EffectInstance(Effect::getEffect(Effect::NAUSEA), 300, 1) + new EffectInstance(VanillaEffects::HUNGER(), 300, 2), + new EffectInstance(VanillaEffects::POISON(), 1200, 3), + new EffectInstance(VanillaEffects::NAUSEA(), 300, 1) ]; } } diff --git a/src/pocketmine/item/PumpkinPie.php b/src/item/PumpkinPie.php similarity index 89% rename from src/pocketmine/item/PumpkinPie.php rename to src/item/PumpkinPie.php index 69a1f74862..ba4567358e 100644 --- a/src/pocketmine/item/PumpkinPie.php +++ b/src/item/PumpkinPie.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class PumpkinPie extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::PUMPKIN_PIE, $meta, "Pumpkin Pie"); - } public function getFoodRestore() : int{ return 8; diff --git a/src/pocketmine/item/PumpkinSeeds.php b/src/item/PumpkinSeeds.php similarity index 77% rename from src/pocketmine/item/PumpkinSeeds.php rename to src/item/PumpkinSeeds.php index ec8e840bd8..561355419c 100644 --- a/src/pocketmine/item/PumpkinSeeds.php +++ b/src/item/PumpkinSeeds.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class PumpkinSeeds extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::PUMPKIN_SEEDS, $meta, "Pumpkin Seeds"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::PUMPKIN_STEM); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::PUMPKIN_STEM(); } } diff --git a/src/pocketmine/item/RabbitStew.php b/src/item/RabbitStew.php similarity index 84% rename from src/pocketmine/item/RabbitStew.php rename to src/item/RabbitStew.php index acdf635ff4..294cc772bc 100644 --- a/src/pocketmine/item/RabbitStew.php +++ b/src/item/RabbitStew.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RabbitStew extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RABBIT_STEW, $meta, "Rabbit Stew"); - } public function getMaxStackSize() : int{ return 1; @@ -40,7 +37,7 @@ class RabbitStew extends Food{ return 12; } - public function getResidue(){ - return ItemFactory::get(Item::BOWL); + public function getResidue() : Item{ + return VanillaItems::BOWL(); } } diff --git a/src/pocketmine/item/RawBeef.php b/src/item/RawBeef.php similarity index 89% rename from src/pocketmine/item/RawBeef.php rename to src/item/RawBeef.php index 0e1a3a8b7e..64f09701f9 100644 --- a/src/pocketmine/item/RawBeef.php +++ b/src/item/RawBeef.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawBeef extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_BEEF, $meta, "Raw Beef"); - } public function getFoodRestore() : int{ return 3; diff --git a/src/pocketmine/item/RawChicken.php b/src/item/RawChicken.php similarity index 78% rename from src/pocketmine/item/RawChicken.php rename to src/item/RawChicken.php index 748d303354..16a795203f 100644 --- a/src/pocketmine/item/RawChicken.php +++ b/src/item/RawChicken.php @@ -23,14 +23,11 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; use function mt_rand; class RawChicken extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_CHICKEN, $meta, "Raw Chicken"); - } public function getFoodRestore() : int{ return 2; @@ -41,6 +38,6 @@ class RawChicken extends Food{ } public function getAdditionalEffects() : array{ - return mt_rand(0, 9) < 3 ? [new EffectInstance(Effect::getEffect(Effect::HUNGER), 600)] : []; + return mt_rand(0, 9) < 3 ? [new EffectInstance(VanillaEffects::HUNGER(), 600)] : []; } } diff --git a/src/pocketmine/item/RawFish.php b/src/item/RawFish.php similarity index 89% rename from src/pocketmine/item/RawFish.php rename to src/item/RawFish.php index 0ecfe970e1..cbd0aef135 100644 --- a/src/pocketmine/item/RawFish.php +++ b/src/item/RawFish.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawFish extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_FISH, $meta, "Raw Fish"); - } public function getFoodRestore() : int{ return 2; diff --git a/src/pocketmine/item/RawMutton.php b/src/item/RawMutton.php similarity index 89% rename from src/pocketmine/item/RawMutton.php rename to src/item/RawMutton.php index 7812bfe5f3..1fd389fbc3 100644 --- a/src/pocketmine/item/RawMutton.php +++ b/src/item/RawMutton.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawMutton extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_MUTTON, $meta, "Raw Mutton"); - } public function getFoodRestore() : int{ return 2; diff --git a/src/pocketmine/item/RawPorkchop.php b/src/item/RawPorkchop.php similarity index 88% rename from src/pocketmine/item/RawPorkchop.php rename to src/item/RawPorkchop.php index 85ccd69d6b..ce2e12623f 100644 --- a/src/pocketmine/item/RawPorkchop.php +++ b/src/item/RawPorkchop.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawPorkchop extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_PORKCHOP, $meta, "Raw Porkchop"); - } public function getFoodRestore() : int{ return 3; diff --git a/src/pocketmine/item/RawRabbit.php b/src/item/RawRabbit.php similarity index 89% rename from src/pocketmine/item/RawRabbit.php rename to src/item/RawRabbit.php index f3b9380fb1..bd1111fe8e 100644 --- a/src/pocketmine/item/RawRabbit.php +++ b/src/item/RawRabbit.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawRabbit extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_RABBIT, $meta, "Raw Rabbit"); - } public function getFoodRestore() : int{ return 3; diff --git a/src/pocketmine/item/RawSalmon.php b/src/item/RawSalmon.php similarity index 89% rename from src/pocketmine/item/RawSalmon.php rename to src/item/RawSalmon.php index c702d3869b..5e0de0e81a 100644 --- a/src/pocketmine/item/RawSalmon.php +++ b/src/item/RawSalmon.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class RawSalmon extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::RAW_SALMON, $meta, "Raw Salmon"); - } public function getFoodRestore() : int{ return 2; diff --git a/src/pocketmine/item/LeatherBoots.php b/src/item/Record.php similarity index 69% rename from src/pocketmine/item/LeatherBoots.php rename to src/item/Record.php index d188fa2793..35c56bb675 100644 --- a/src/pocketmine/item/LeatherBoots.php +++ b/src/item/Record.php @@ -23,16 +23,22 @@ declare(strict_types=1); namespace pocketmine\item; -class LeatherBoots extends Armor{ - public function __construct(int $meta = 0){ - parent::__construct(self::LEATHER_BOOTS, $meta, "Leather Boots"); +use pocketmine\block\utils\RecordType; + +class Record extends Item{ + /** @var RecordType */ + private $recordType; + + public function __construct(ItemIdentifier $identifier, RecordType $recordType, string $name){ + $this->recordType = $recordType; + parent::__construct($identifier, $name); } - public function getDefensePoints() : int{ + public function getRecordType() : RecordType{ + return $this->recordType; + } + + public function getMaxStackSize() : int{ return 1; } - - public function getMaxDurability() : int{ - return 66; - } } diff --git a/src/pocketmine/item/Redstone.php b/src/item/Redstone.php similarity index 78% rename from src/pocketmine/item/Redstone.php rename to src/item/Redstone.php index 9826b4e670..1a4107d9a0 100644 --- a/src/pocketmine/item/Redstone.php +++ b/src/item/Redstone.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class Redstone extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::REDSTONE, $meta, "Redstone"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::REDSTONE_WIRE); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::REDSTONE_WIRE(); } } diff --git a/src/pocketmine/network/mcpe/protocol/types/UIProfile.php b/src/item/Releasable.php similarity index 81% rename from src/pocketmine/network/mcpe/protocol/types/UIProfile.php rename to src/item/Releasable.php index 5103880ced..acb9d65c8a 100644 --- a/src/pocketmine/network/mcpe/protocol/types/UIProfile.php +++ b/src/item/Releasable.php @@ -21,14 +21,15 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\item; -final class UIProfile{ +use pocketmine\player\Player; - private function __construct(){ - //NOOP - } +/** + * Interface implemented by objects that can be used. + */ +interface Releasable{ + + public function canStartUsingItem(Player $player) : bool; - public const CLASSIC = 0; - public const POCKET = 1; } diff --git a/src/pocketmine/item/RottenFlesh.php b/src/item/RottenFlesh.php similarity index 81% rename from src/pocketmine/item/RottenFlesh.php rename to src/item/RottenFlesh.php index 8aba9b0286..401149a4a8 100644 --- a/src/pocketmine/item/RottenFlesh.php +++ b/src/item/RottenFlesh.php @@ -23,16 +23,12 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; use function lcg_value; class RottenFlesh extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::ROTTEN_FLESH, $meta, "Rotten Flesh"); - } - public function getFoodRestore() : int{ return 4; } @@ -44,7 +40,7 @@ class RottenFlesh extends Food{ public function getAdditionalEffects() : array{ if(lcg_value() <= 0.8){ return [ - new EffectInstance(Effect::getEffect(Effect::HUNGER), 600) + new EffectInstance(VanillaEffects::HUNGER(), 600) ]; } diff --git a/src/pocketmine/item/Shears.php b/src/item/Shears.php similarity index 81% rename from src/pocketmine/item/Shears.php rename to src/item/Shears.php index f2dfb345f0..888b6ce759 100644 --- a/src/pocketmine/item/Shears.php +++ b/src/item/Shears.php @@ -27,16 +27,13 @@ use pocketmine\block\Block; use pocketmine\block\BlockToolType; class Shears extends Tool{ - public function __construct(int $meta = 0){ - parent::__construct(self::SHEARS, $meta, "Shears"); - } public function getMaxDurability() : int{ return 239; } public function getBlockToolType() : int{ - return BlockToolType::TYPE_SHEARS; + return BlockToolType::SHEARS; } public function getBlockToolHarvestLevel() : int{ @@ -48,9 +45,6 @@ class Shears extends Tool{ } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() === 0.0 or $block->isCompatibleWithTool($this)){ - return $this->applyDamage(1); - } - return false; + return $this->applyDamage(1); } } diff --git a/src/pocketmine/item/Shovel.php b/src/item/Shovel.php similarity index 87% rename from src/pocketmine/item/Shovel.php rename to src/item/Shovel.php index fe4ebe9e80..c167bd2ba7 100644 --- a/src/pocketmine/item/Shovel.php +++ b/src/item/Shovel.php @@ -30,19 +30,19 @@ use pocketmine\entity\Entity; class Shovel extends TieredTool{ public function getBlockToolType() : int{ - return BlockToolType::TYPE_SHOVEL; + return BlockToolType::SHOVEL; } public function getBlockToolHarvestLevel() : int{ - return $this->tier; + return $this->tier->getHarvestLevel(); } public function getAttackPoints() : int{ - return self::getBaseDamageFromTier($this->tier) - 3; + return $this->tier->getBaseAttackPoints() - 3; } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() > 0){ + if(!$block->getBreakInfo()->breaksInstantly()){ return $this->applyDamage(1); } return false; diff --git a/src/item/Skull.php b/src/item/Skull.php new file mode 100644 index 0000000000..6e4a982038 --- /dev/null +++ b/src/item/Skull.php @@ -0,0 +1,49 @@ +skullType = $skullType; + } + + public function getBlock(?int $clickedFace = null) : Block{ + //TODO: we ought to be able to represent this as a regular ItemBlock + //but that's not currently possible because the skulltype isn't part of the internal block runtimeID + return VanillaBlocks::MOB_HEAD()->setSkullType($this->skullType); + } + + public function getSkullType() : SkullType{ + return $this->skullType; + } +} diff --git a/src/pocketmine/item/Snowball.php b/src/item/Snowball.php similarity index 75% rename from src/pocketmine/item/Snowball.php rename to src/item/Snowball.php index 1e2fc00d14..a1cb0ced69 100644 --- a/src/pocketmine/item/Snowball.php +++ b/src/item/Snowball.php @@ -23,17 +23,19 @@ declare(strict_types=1); namespace pocketmine\item; +use pocketmine\entity\Location; +use pocketmine\entity\projectile\Snowball as SnowballEntity; +use pocketmine\entity\projectile\Throwable; +use pocketmine\player\Player; + class Snowball extends ProjectileItem{ - public function __construct(int $meta = 0){ - parent::__construct(self::SNOWBALL, $meta, "Snowball"); - } public function getMaxStackSize() : int{ return 16; } - public function getProjectileEntityType() : string{ - return "Snowball"; + protected function createEntity(Location $location, Player $thrower) : Throwable{ + return new SnowballEntity($location, $thrower); } public function getThrowForce() : float{ diff --git a/src/pocketmine/item/SpawnEgg.php b/src/item/SpawnEgg.php similarity index 58% rename from src/pocketmine/item/SpawnEgg.php rename to src/item/SpawnEgg.php index 42400b0cd0..e905c5f2b8 100644 --- a/src/pocketmine/item/SpawnEgg.php +++ b/src/item/SpawnEgg.php @@ -26,29 +26,23 @@ namespace pocketmine\item; use pocketmine\block\Block; use pocketmine\entity\Entity; use pocketmine\math\Vector3; -use pocketmine\Player; +use pocketmine\player\Player; +use pocketmine\world\World; use function lcg_value; -class SpawnEgg extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::SPAWN_EGG, $meta, "Spawn Egg"); - } +abstract class SpawnEgg extends Item{ - public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{ - $nbt = Entity::createBaseNBT($blockReplace->add(0.5, 0, 0.5), null, lcg_value() * 360, 0); + abstract protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity; + + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ + $entity = $this->createEntity($player->getWorld(), $blockReplace->getPosition()->add(0.5, 0, 0.5), lcg_value() * 360, 0); if($this->hasCustomName()){ - $nbt->setString("CustomName", $this->getCustomName()); + $entity->setNameTag($this->getCustomName()); } - - $entity = Entity::createEntity($this->meta, $player->getLevelNonNull(), $nbt); - - if($entity instanceof Entity){ - $this->pop(); - $entity->spawnToAll(); - return true; - } - - return false; + $this->pop(); + $entity->spawnToAll(); + //TODO: what if the entity was marked for deletion? + return ItemUseResult::SUCCESS(); } } diff --git a/src/pocketmine/item/SpiderEye.php b/src/item/SpiderEye.php similarity index 79% rename from src/pocketmine/item/SpiderEye.php rename to src/item/SpiderEye.php index 4feea931b2..22e5ecab1c 100644 --- a/src/pocketmine/item/SpiderEye.php +++ b/src/item/SpiderEye.php @@ -23,13 +23,10 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\entity\Effect; -use pocketmine\entity\EffectInstance; +use pocketmine\entity\effect\EffectInstance; +use pocketmine\entity\effect\VanillaEffects; class SpiderEye extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::SPIDER_EYE, $meta, "Spider Eye"); - } public function getFoodRestore() : int{ return 2; @@ -40,6 +37,6 @@ class SpiderEye extends Food{ } public function getAdditionalEffects() : array{ - return [new EffectInstance(Effect::getEffect(Effect::POISON), 80)]; + return [new EffectInstance(VanillaEffects::POISON(), 80)]; } } diff --git a/src/item/SplashPotion.php b/src/item/SplashPotion.php new file mode 100644 index 0000000000..1b338a96b1 --- /dev/null +++ b/src/item/SplashPotion.php @@ -0,0 +1,51 @@ +potionType = $potionType; + } + + public function getMaxStackSize() : int{ + return 1; + } + + protected function createEntity(Location $location, Player $thrower) : Throwable{ + return new SplashPotionEntity($location, $thrower, $this->potionType); + } + + public function getThrowForce() : float{ + return 0.5; + } +} diff --git a/src/pocketmine/item/Steak.php b/src/item/Steak.php similarity index 90% rename from src/pocketmine/item/Steak.php rename to src/item/Steak.php index 57ec814a1c..baeee5e153 100644 --- a/src/pocketmine/item/Steak.php +++ b/src/item/Steak.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Steak extends Food{ - public function __construct(int $meta = 0){ - parent::__construct(self::STEAK, $meta, "Steak"); - } public function getFoodRestore() : int{ return 8; diff --git a/src/pocketmine/item/Stick.php b/src/item/Stick.php similarity index 89% rename from src/pocketmine/item/Stick.php rename to src/item/Stick.php index c6972114db..59c5ed91be 100644 --- a/src/pocketmine/item/Stick.php +++ b/src/item/Stick.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Stick extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::STICK, $meta, "Stick"); - } public function getFuelTime() : int{ return 100; diff --git a/src/pocketmine/item/StringItem.php b/src/item/StringItem.php similarity index 79% rename from src/pocketmine/item/StringItem.php rename to src/item/StringItem.php index eb5868a21c..2ebcae63bb 100644 --- a/src/pocketmine/item/StringItem.php +++ b/src/item/StringItem.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class StringItem extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::STRING, $meta, "String"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::TRIPWIRE); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::TRIPWIRE(); } } diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php new file mode 100644 index 0000000000..df0b8944f9 --- /dev/null +++ b/src/item/StringToItemParser.php @@ -0,0 +1,1334 @@ + + */ +final class StringToItemParser extends StringToTParser{ + use SingletonTrait; + + private static function make() : self{ + $result = new self; + + foreach(DyeColor::getAll() as $color){ + $prefix = fn(string $name) => $color->name() . "_" . $name; + //wall and floor banner are the same item + $result->registerBlock($prefix("banner"), fn() => VanillaBlocks::BANNER()->setColor($color)); + $result->registerBlock($prefix("bed"), fn() => VanillaBlocks::BED()->setColor($color)); + $result->registerBlock($prefix("carpet"), fn() => VanillaBlocks::CARPET()->setColor($color)); + $result->registerBlock($prefix("concrete"), fn() => VanillaBlocks::CONCRETE()->setColor($color)); + $result->registerBlock($prefix("concrete_powder"), fn() => VanillaBlocks::CONCRETE()->setColor($color)); + $result->registerBlock($prefix("stained_clay"), fn() => VanillaBlocks::STAINED_CLAY()->setColor($color)); + $result->registerBlock($prefix("stained_glass"), fn() => VanillaBlocks::STAINED_GLASS()->setColor($color)); + $result->registerBlock($prefix("stained_glass_pane"), fn() => VanillaBlocks::STAINED_GLASS_PANE()->setColor($color)); + $result->registerBlock($prefix("stained_hardened_glass"), fn() => VanillaBlocks::STAINED_HARDENED_GLASS()->setColor($color)); + $result->registerBlock($prefix("stained_hardened_glass_pane"), fn() => VanillaBlocks::STAINED_HARDENED_GLASS_PANE()->setColor($color)); + $result->registerBlock($prefix("wool"), fn() => VanillaBlocks::WOOL()->setColor($color)); + $result->registerBlock($prefix("shulker_box"), fn() => VanillaBlocks::DYED_SHULKER_BOX()->setColor($color)); + } + foreach(CoralType::getAll() as $coralType){ + $prefix = fn(string $name) => $coralType->name() . "_" . $name; + $result->registerBlock($prefix("coral"), fn() => VanillaBlocks::CORAL()->setCoralType($coralType)); + $result->registerBlock($prefix("coral_block"), fn() => VanillaBlocks::CORAL_BLOCK()->setCoralType($coralType)); + //wall and floor coral fans are the same item + $result->registerBlock($prefix("coral_fan"), fn() => VanillaBlocks::CORAL_FAN()->setCoralType($coralType)); + } + + $result->registerBlock("acacia_button", fn() => VanillaBlocks::ACACIA_BUTTON()); + $result->registerBlock("acacia_door", fn() => VanillaBlocks::ACACIA_DOOR()); + $result->registerBlock("acacia_door_block", fn() => VanillaBlocks::ACACIA_DOOR()); + $result->registerBlock("acacia_fence", fn() => VanillaBlocks::ACACIA_FENCE()); + $result->registerBlock("acacia_fence_gate", fn() => VanillaBlocks::ACACIA_FENCE_GATE()); + $result->registerBlock("acacia_leaves", fn() => VanillaBlocks::ACACIA_LEAVES()); + $result->registerBlock("acacia_log", fn() => VanillaBlocks::ACACIA_LOG()); + $result->registerBlock("acacia_planks", fn() => VanillaBlocks::ACACIA_PLANKS()); + $result->registerBlock("acacia_pressure_plate", fn() => VanillaBlocks::ACACIA_PRESSURE_PLATE()); + $result->registerBlock("acacia_sapling", fn() => VanillaBlocks::ACACIA_SAPLING()); + $result->registerBlock("acacia_sign", fn() => VanillaBlocks::ACACIA_SIGN()); + $result->registerBlock("acacia_slab", fn() => VanillaBlocks::ACACIA_SLAB()); + $result->registerBlock("acacia_stairs", fn() => VanillaBlocks::ACACIA_STAIRS()); + $result->registerBlock("acacia_standing_sign", fn() => VanillaBlocks::ACACIA_SIGN()); + $result->registerBlock("acacia_trapdoor", fn() => VanillaBlocks::ACACIA_TRAPDOOR()); + $result->registerBlock("acacia_wall_sign", fn() => VanillaBlocks::ACACIA_WALL_SIGN()); + $result->registerBlock("acacia_wood", fn() => VanillaBlocks::ACACIA_WOOD()); + $result->registerBlock("acacia_wood_stairs", fn() => VanillaBlocks::ACACIA_STAIRS()); + $result->registerBlock("acacia_wooden_stairs", fn() => VanillaBlocks::ACACIA_STAIRS()); + $result->registerBlock("activator_rail", fn() => VanillaBlocks::ACTIVATOR_RAIL()); + $result->registerBlock("active_redstone_lamp", fn() => VanillaBlocks::REDSTONE_LAMP()->setPowered(true)); + $result->registerBlock("air", fn() => VanillaBlocks::AIR()); + $result->registerBlock("all_sided_mushroom_stem", fn() => VanillaBlocks::ALL_SIDED_MUSHROOM_STEM()); + $result->registerBlock("allium", fn() => VanillaBlocks::ALLIUM()); + $result->registerBlock("andesite", fn() => VanillaBlocks::ANDESITE()); + $result->registerBlock("andesite_slab", fn() => VanillaBlocks::ANDESITE_SLAB()); + $result->registerBlock("andesite_stairs", fn() => VanillaBlocks::ANDESITE_STAIRS()); + $result->registerBlock("andesite_wall", fn() => VanillaBlocks::ANDESITE_WALL()); + $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_sapling", fn() => VanillaBlocks::BAMBOO_SAPLING()); + $result->registerBlock("banner", fn() => VanillaBlocks::BANNER()); + $result->registerBlock("barrel", fn() => VanillaBlocks::BARREL()); + $result->registerBlock("barrier", fn() => VanillaBlocks::BARRIER()); + $result->registerBlock("beacon", fn() => VanillaBlocks::BEACON()); + $result->registerBlock("bed_block", fn() => VanillaBlocks::BED()); + $result->registerBlock("bedrock", fn() => VanillaBlocks::BEDROCK()); + $result->registerBlock("beetroot_block", fn() => VanillaBlocks::BEETROOTS()); + $result->registerBlock("beetroots", fn() => VanillaBlocks::BEETROOTS()); + $result->registerBlock("bell", fn() => VanillaBlocks::BELL()); + $result->registerBlock("birch_button", fn() => VanillaBlocks::BIRCH_BUTTON()); + $result->registerBlock("birch_door", fn() => VanillaBlocks::BIRCH_DOOR()); + $result->registerBlock("birch_door_block", fn() => VanillaBlocks::BIRCH_DOOR()); + $result->registerBlock("birch_fence", fn() => VanillaBlocks::BIRCH_FENCE()); + $result->registerBlock("birch_fence_gate", fn() => VanillaBlocks::BIRCH_FENCE_GATE()); + $result->registerBlock("birch_leaves", fn() => VanillaBlocks::BIRCH_LEAVES()); + $result->registerBlock("birch_log", fn() => VanillaBlocks::BIRCH_LOG()); + $result->registerBlock("birch_planks", fn() => VanillaBlocks::BIRCH_PLANKS()); + $result->registerBlock("birch_pressure_plate", fn() => VanillaBlocks::BIRCH_PRESSURE_PLATE()); + $result->registerBlock("birch_sapling", fn() => VanillaBlocks::BIRCH_SAPLING()); + $result->registerBlock("birch_sign", fn() => VanillaBlocks::BIRCH_SIGN()); + $result->registerBlock("birch_slab", fn() => VanillaBlocks::BIRCH_SLAB()); + $result->registerBlock("birch_stairs", fn() => VanillaBlocks::BIRCH_STAIRS()); + $result->registerBlock("birch_standing_sign", fn() => VanillaBlocks::BIRCH_SIGN()); + $result->registerBlock("birch_trapdoor", fn() => VanillaBlocks::BIRCH_TRAPDOOR()); + $result->registerBlock("birch_wall_sign", fn() => VanillaBlocks::BIRCH_WALL_SIGN()); + $result->registerBlock("birch_wood", fn() => VanillaBlocks::BIRCH_WOOD()); + $result->registerBlock("birch_wood_stairs", fn() => VanillaBlocks::BIRCH_STAIRS()); + $result->registerBlock("birch_wooden_stairs", fn() => VanillaBlocks::BIRCH_STAIRS()); + $result->registerBlock("black_glazed_terracotta", fn() => VanillaBlocks::BLACK_GLAZED_TERRACOTTA()); + $result->registerBlock("blast_furnace", fn() => VanillaBlocks::BLAST_FURNACE()); + $result->registerBlock("blue_glazed_terracotta", fn() => VanillaBlocks::BLUE_GLAZED_TERRACOTTA()); + $result->registerBlock("blue_ice", fn() => VanillaBlocks::BLUE_ICE()); + $result->registerBlock("blue_orchid", fn() => VanillaBlocks::BLUE_ORCHID()); + $result->registerBlock("blue_torch", fn() => VanillaBlocks::BLUE_TORCH()); + $result->registerBlock("bone_block", fn() => VanillaBlocks::BONE_BLOCK()); + $result->registerBlock("bookshelf", fn() => VanillaBlocks::BOOKSHELF()); + $result->registerBlock("brewing_stand", fn() => VanillaBlocks::BREWING_STAND()); + $result->registerBlock("brewing_stand_block", fn() => VanillaBlocks::BREWING_STAND()); + $result->registerBlock("brick_block", fn() => VanillaBlocks::BRICKS()); + $result->registerBlock("brick_slab", fn() => VanillaBlocks::BRICK_SLAB()); + $result->registerBlock("brick_stairs", fn() => VanillaBlocks::BRICK_STAIRS()); + $result->registerBlock("brick_wall", fn() => VanillaBlocks::BRICK_WALL()); + $result->registerBlock("bricks", fn() => VanillaBlocks::BRICKS()); + $result->registerBlock("bricks_block", fn() => VanillaBlocks::BRICKS()); + $result->registerBlock("brown_glazed_terracotta", fn() => VanillaBlocks::BROWN_GLAZED_TERRACOTTA()); + $result->registerBlock("brown_mushroom", fn() => VanillaBlocks::BROWN_MUSHROOM()); + $result->registerBlock("brown_mushroom_block", fn() => VanillaBlocks::BROWN_MUSHROOM_BLOCK()); + $result->registerBlock("burning_furnace", fn() => VanillaBlocks::FURNACE()); + $result->registerBlock("bush", fn() => VanillaBlocks::DEAD_BUSH()); + $result->registerBlock("cactus", fn() => VanillaBlocks::CACTUS()); + $result->registerBlock("cake", fn() => VanillaBlocks::CAKE()); + $result->registerBlock("cake_block", fn() => VanillaBlocks::CAKE()); + $result->registerBlock("carpet", fn() => VanillaBlocks::CARPET()); + $result->registerBlock("carrot_block", fn() => VanillaBlocks::CARROTS()); + $result->registerBlock("carrots", fn() => VanillaBlocks::CARROTS()); + $result->registerBlock("carved_pumpkin", fn() => VanillaBlocks::CARVED_PUMPKIN()); + $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()); + $result->registerBlock("chiseled_stone_bricks", fn() => VanillaBlocks::CHISELED_STONE_BRICKS()); + $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()); + $result->registerBlock("cobblestone", fn() => VanillaBlocks::COBBLESTONE()); + $result->registerBlock("cobblestone_slab", fn() => VanillaBlocks::COBBLESTONE_SLAB()); + $result->registerBlock("cobblestone_stairs", fn() => VanillaBlocks::COBBLESTONE_STAIRS()); + $result->registerBlock("cobblestone_wall", fn() => VanillaBlocks::COBBLESTONE_WALL()); + $result->registerBlock("cobweb", fn() => VanillaBlocks::COBWEB()); + $result->registerBlock("cocoa", fn() => VanillaBlocks::COCOA_POD()); + $result->registerBlock("cocoa_block", fn() => VanillaBlocks::COCOA_POD()); + $result->registerBlock("cocoa_pod", fn() => VanillaBlocks::COCOA_POD()); + $result->registerBlock("cocoa_pods", fn() => VanillaBlocks::COCOA_POD()); + $result->registerBlock("colored_torch_bp", fn() => VanillaBlocks::BLUE_TORCH()); + $result->registerBlock("colored_torch_rg", fn() => VanillaBlocks::RED_TORCH()); + $result->registerBlock("comparator", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("comparator_block", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("compound_creator", fn() => VanillaBlocks::COMPOUND_CREATOR()); + $result->registerBlock("concrete", fn() => VanillaBlocks::CONCRETE()); + $result->registerBlock("concrete_powder", fn() => VanillaBlocks::CONCRETE_POWDER()); + $result->registerBlock("concretepowder", fn() => VanillaBlocks::CONCRETE_POWDER()); + $result->registerBlock("coral", fn() => VanillaBlocks::CORAL()); + $result->registerBlock("coral_block", fn() => VanillaBlocks::CORAL_BLOCK()); + $result->registerBlock("coral_fan", fn() => VanillaBlocks::CORAL_FAN()); + $result->registerBlock("coral_fan_dead", fn() => VanillaBlocks::CORAL_FAN()->setCoralType(CoralType::TUBE())->setDead(true)); + $result->registerBlock("coral_fan_hang", fn() => VanillaBlocks::WALL_CORAL_FAN()); + $result->registerBlock("coral_fan_hang2", fn() => VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::BUBBLE())); + $result->registerBlock("coral_fan_hang3", fn() => VanillaBlocks::WALL_CORAL_FAN()->setCoralType(CoralType::HORN())); + $result->registerBlock("cornflower", fn() => VanillaBlocks::CORNFLOWER()); + $result->registerBlock("cracked_stone_bricks", fn() => VanillaBlocks::CRACKED_STONE_BRICKS()); + $result->registerBlock("crafting_table", fn() => VanillaBlocks::CRAFTING_TABLE()); + $result->registerBlock("cut_red_sandstone", fn() => VanillaBlocks::CUT_RED_SANDSTONE()); + $result->registerBlock("cut_red_sandstone_slab", fn() => VanillaBlocks::CUT_RED_SANDSTONE_SLAB()); + $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()); + $result->registerBlock("dark_oak_door_block", fn() => VanillaBlocks::DARK_OAK_DOOR()); + $result->registerBlock("dark_oak_fence", fn() => VanillaBlocks::DARK_OAK_FENCE()); + $result->registerBlock("dark_oak_fence_gate", fn() => VanillaBlocks::DARK_OAK_FENCE_GATE()); + $result->registerBlock("dark_oak_leaves", fn() => VanillaBlocks::DARK_OAK_LEAVES()); + $result->registerBlock("dark_oak_log", fn() => VanillaBlocks::DARK_OAK_LOG()); + $result->registerBlock("dark_oak_planks", fn() => VanillaBlocks::DARK_OAK_PLANKS()); + $result->registerBlock("dark_oak_pressure_plate", fn() => VanillaBlocks::DARK_OAK_PRESSURE_PLATE()); + $result->registerBlock("dark_oak_sapling", fn() => VanillaBlocks::DARK_OAK_SAPLING()); + $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()); + $result->registerBlock("dark_oak_wood_stairs", fn() => VanillaBlocks::DARK_OAK_STAIRS()); + $result->registerBlock("dark_oak_wooden_stairs", fn() => VanillaBlocks::DARK_OAK_STAIRS()); + $result->registerBlock("dark_prismarine", fn() => VanillaBlocks::DARK_PRISMARINE()); + $result->registerBlock("dark_prismarine_slab", fn() => VanillaBlocks::DARK_PRISMARINE_SLAB()); + $result->registerBlock("dark_prismarine_stairs", fn() => VanillaBlocks::DARK_PRISMARINE_STAIRS()); + $result->registerBlock("darkoak_sign", fn() => VanillaBlocks::DARK_OAK_SIGN()); + $result->registerBlock("darkoak_standing_sign", fn() => VanillaBlocks::DARK_OAK_SIGN()); + $result->registerBlock("darkoak_wall_sign", fn() => VanillaBlocks::DARK_OAK_WALL_SIGN()); + $result->registerBlock("daylight_detector", fn() => VanillaBlocks::DAYLIGHT_SENSOR()); + $result->registerBlock("daylight_detector_inverted", fn() => VanillaBlocks::DAYLIGHT_SENSOR()->setInverted(true)); + $result->registerBlock("daylight_sensor", fn() => VanillaBlocks::DAYLIGHT_SENSOR()); + $result->registerBlock("daylight_sensor_inverted", fn() => VanillaBlocks::DAYLIGHT_SENSOR()->setInverted(true)); + $result->registerBlock("dead_bush", fn() => VanillaBlocks::DEAD_BUSH()); + $result->registerBlock("deadbush", fn() => VanillaBlocks::DEAD_BUSH()); + $result->registerBlock("detector_rail", fn() => VanillaBlocks::DETECTOR_RAIL()); + $result->registerBlock("diamond_block", fn() => VanillaBlocks::DIAMOND()); + $result->registerBlock("diamond_ore", fn() => VanillaBlocks::DIAMOND_ORE()); + $result->registerBlock("diorite", fn() => VanillaBlocks::DIORITE()); + $result->registerBlock("diorite_slab", fn() => VanillaBlocks::DIORITE_SLAB()); + $result->registerBlock("diorite_stairs", fn() => VanillaBlocks::DIORITE_STAIRS()); + $result->registerBlock("diorite_wall", fn() => VanillaBlocks::DIORITE_WALL()); + $result->registerBlock("dirt", fn() => VanillaBlocks::DIRT()); + $result->registerBlock("door_block", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("double_plant", fn() => VanillaBlocks::SUNFLOWER()); + $result->registerBlock("double_red_sandstone_slab", fn() => VanillaBlocks::RED_SANDSTONE_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_slab", fn() => VanillaBlocks::STONE_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_slabs", fn() => VanillaBlocks::STONE_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_stone_slab", fn() => VanillaBlocks::STONE_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_stone_slab2", fn() => VanillaBlocks::RED_SANDSTONE_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_stone_slab3", fn() => VanillaBlocks::END_STONE_BRICK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_stone_slab4", fn() => VanillaBlocks::MOSSY_STONE_BRICK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_tallgrass", fn() => VanillaBlocks::DOUBLE_TALLGRASS()); + $result->registerBlock("double_wood_slab", fn() => VanillaBlocks::OAK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_wood_slabs", fn() => VanillaBlocks::OAK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_wooden_slab", fn() => VanillaBlocks::OAK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("double_wooden_slabs", fn() => VanillaBlocks::OAK_SLAB()->setSlabType(SlabType::DOUBLE())); + $result->registerBlock("dragon_egg", fn() => VanillaBlocks::DRAGON_EGG()); + $result->registerBlock("dried_kelp_block", fn() => VanillaBlocks::DRIED_KELP()); + $result->registerBlock("dyed_shulker_box", fn() => VanillaBlocks::DYED_SHULKER_BOX()); + $result->registerBlock("element_0", fn() => VanillaBlocks::ELEMENT_ZERO()); + $result->registerBlock("element_1", fn() => VanillaBlocks::ELEMENT_HYDROGEN()); + $result->registerBlock("element_10", fn() => VanillaBlocks::ELEMENT_NEON()); + $result->registerBlock("element_100", fn() => VanillaBlocks::ELEMENT_FERMIUM()); + $result->registerBlock("element_101", fn() => VanillaBlocks::ELEMENT_MENDELEVIUM()); + $result->registerBlock("element_102", fn() => VanillaBlocks::ELEMENT_NOBELIUM()); + $result->registerBlock("element_103", fn() => VanillaBlocks::ELEMENT_LAWRENCIUM()); + $result->registerBlock("element_104", fn() => VanillaBlocks::ELEMENT_RUTHERFORDIUM()); + $result->registerBlock("element_105", fn() => VanillaBlocks::ELEMENT_DUBNIUM()); + $result->registerBlock("element_106", fn() => VanillaBlocks::ELEMENT_SEABORGIUM()); + $result->registerBlock("element_107", fn() => VanillaBlocks::ELEMENT_BOHRIUM()); + $result->registerBlock("element_108", fn() => VanillaBlocks::ELEMENT_HASSIUM()); + $result->registerBlock("element_109", fn() => VanillaBlocks::ELEMENT_MEITNERIUM()); + $result->registerBlock("element_11", fn() => VanillaBlocks::ELEMENT_SODIUM()); + $result->registerBlock("element_110", fn() => VanillaBlocks::ELEMENT_DARMSTADTIUM()); + $result->registerBlock("element_111", fn() => VanillaBlocks::ELEMENT_ROENTGENIUM()); + $result->registerBlock("element_112", fn() => VanillaBlocks::ELEMENT_COPERNICIUM()); + $result->registerBlock("element_113", fn() => VanillaBlocks::ELEMENT_NIHONIUM()); + $result->registerBlock("element_114", fn() => VanillaBlocks::ELEMENT_FLEROVIUM()); + $result->registerBlock("element_115", fn() => VanillaBlocks::ELEMENT_MOSCOVIUM()); + $result->registerBlock("element_116", fn() => VanillaBlocks::ELEMENT_LIVERMORIUM()); + $result->registerBlock("element_117", fn() => VanillaBlocks::ELEMENT_TENNESSINE()); + $result->registerBlock("element_118", fn() => VanillaBlocks::ELEMENT_OGANESSON()); + $result->registerBlock("element_12", fn() => VanillaBlocks::ELEMENT_MAGNESIUM()); + $result->registerBlock("element_13", fn() => VanillaBlocks::ELEMENT_ALUMINUM()); + $result->registerBlock("element_14", fn() => VanillaBlocks::ELEMENT_SILICON()); + $result->registerBlock("element_15", fn() => VanillaBlocks::ELEMENT_PHOSPHORUS()); + $result->registerBlock("element_16", fn() => VanillaBlocks::ELEMENT_SULFUR()); + $result->registerBlock("element_17", fn() => VanillaBlocks::ELEMENT_CHLORINE()); + $result->registerBlock("element_18", fn() => VanillaBlocks::ELEMENT_ARGON()); + $result->registerBlock("element_19", fn() => VanillaBlocks::ELEMENT_POTASSIUM()); + $result->registerBlock("element_2", fn() => VanillaBlocks::ELEMENT_HELIUM()); + $result->registerBlock("element_20", fn() => VanillaBlocks::ELEMENT_CALCIUM()); + $result->registerBlock("element_21", fn() => VanillaBlocks::ELEMENT_SCANDIUM()); + $result->registerBlock("element_22", fn() => VanillaBlocks::ELEMENT_TITANIUM()); + $result->registerBlock("element_23", fn() => VanillaBlocks::ELEMENT_VANADIUM()); + $result->registerBlock("element_24", fn() => VanillaBlocks::ELEMENT_CHROMIUM()); + $result->registerBlock("element_25", fn() => VanillaBlocks::ELEMENT_MANGANESE()); + $result->registerBlock("element_26", fn() => VanillaBlocks::ELEMENT_IRON()); + $result->registerBlock("element_27", fn() => VanillaBlocks::ELEMENT_COBALT()); + $result->registerBlock("element_28", fn() => VanillaBlocks::ELEMENT_NICKEL()); + $result->registerBlock("element_29", fn() => VanillaBlocks::ELEMENT_COPPER()); + $result->registerBlock("element_3", fn() => VanillaBlocks::ELEMENT_LITHIUM()); + $result->registerBlock("element_30", fn() => VanillaBlocks::ELEMENT_ZINC()); + $result->registerBlock("element_31", fn() => VanillaBlocks::ELEMENT_GALLIUM()); + $result->registerBlock("element_32", fn() => VanillaBlocks::ELEMENT_GERMANIUM()); + $result->registerBlock("element_33", fn() => VanillaBlocks::ELEMENT_ARSENIC()); + $result->registerBlock("element_34", fn() => VanillaBlocks::ELEMENT_SELENIUM()); + $result->registerBlock("element_35", fn() => VanillaBlocks::ELEMENT_BROMINE()); + $result->registerBlock("element_36", fn() => VanillaBlocks::ELEMENT_KRYPTON()); + $result->registerBlock("element_37", fn() => VanillaBlocks::ELEMENT_RUBIDIUM()); + $result->registerBlock("element_38", fn() => VanillaBlocks::ELEMENT_STRONTIUM()); + $result->registerBlock("element_39", fn() => VanillaBlocks::ELEMENT_YTTRIUM()); + $result->registerBlock("element_4", fn() => VanillaBlocks::ELEMENT_BERYLLIUM()); + $result->registerBlock("element_40", fn() => VanillaBlocks::ELEMENT_ZIRCONIUM()); + $result->registerBlock("element_41", fn() => VanillaBlocks::ELEMENT_NIOBIUM()); + $result->registerBlock("element_42", fn() => VanillaBlocks::ELEMENT_MOLYBDENUM()); + $result->registerBlock("element_43", fn() => VanillaBlocks::ELEMENT_TECHNETIUM()); + $result->registerBlock("element_44", fn() => VanillaBlocks::ELEMENT_RUTHENIUM()); + $result->registerBlock("element_45", fn() => VanillaBlocks::ELEMENT_RHODIUM()); + $result->registerBlock("element_46", fn() => VanillaBlocks::ELEMENT_PALLADIUM()); + $result->registerBlock("element_47", fn() => VanillaBlocks::ELEMENT_SILVER()); + $result->registerBlock("element_48", fn() => VanillaBlocks::ELEMENT_CADMIUM()); + $result->registerBlock("element_49", fn() => VanillaBlocks::ELEMENT_INDIUM()); + $result->registerBlock("element_5", fn() => VanillaBlocks::ELEMENT_BORON()); + $result->registerBlock("element_50", fn() => VanillaBlocks::ELEMENT_TIN()); + $result->registerBlock("element_51", fn() => VanillaBlocks::ELEMENT_ANTIMONY()); + $result->registerBlock("element_52", fn() => VanillaBlocks::ELEMENT_TELLURIUM()); + $result->registerBlock("element_53", fn() => VanillaBlocks::ELEMENT_IODINE()); + $result->registerBlock("element_54", fn() => VanillaBlocks::ELEMENT_XENON()); + $result->registerBlock("element_55", fn() => VanillaBlocks::ELEMENT_CESIUM()); + $result->registerBlock("element_56", fn() => VanillaBlocks::ELEMENT_BARIUM()); + $result->registerBlock("element_57", fn() => VanillaBlocks::ELEMENT_LANTHANUM()); + $result->registerBlock("element_58", fn() => VanillaBlocks::ELEMENT_CERIUM()); + $result->registerBlock("element_59", fn() => VanillaBlocks::ELEMENT_PRASEODYMIUM()); + $result->registerBlock("element_6", fn() => VanillaBlocks::ELEMENT_CARBON()); + $result->registerBlock("element_60", fn() => VanillaBlocks::ELEMENT_NEODYMIUM()); + $result->registerBlock("element_61", fn() => VanillaBlocks::ELEMENT_PROMETHIUM()); + $result->registerBlock("element_62", fn() => VanillaBlocks::ELEMENT_SAMARIUM()); + $result->registerBlock("element_63", fn() => VanillaBlocks::ELEMENT_EUROPIUM()); + $result->registerBlock("element_64", fn() => VanillaBlocks::ELEMENT_GADOLINIUM()); + $result->registerBlock("element_65", fn() => VanillaBlocks::ELEMENT_TERBIUM()); + $result->registerBlock("element_66", fn() => VanillaBlocks::ELEMENT_DYSPROSIUM()); + $result->registerBlock("element_67", fn() => VanillaBlocks::ELEMENT_HOLMIUM()); + $result->registerBlock("element_68", fn() => VanillaBlocks::ELEMENT_ERBIUM()); + $result->registerBlock("element_69", fn() => VanillaBlocks::ELEMENT_THULIUM()); + $result->registerBlock("element_7", fn() => VanillaBlocks::ELEMENT_NITROGEN()); + $result->registerBlock("element_70", fn() => VanillaBlocks::ELEMENT_YTTERBIUM()); + $result->registerBlock("element_71", fn() => VanillaBlocks::ELEMENT_LUTETIUM()); + $result->registerBlock("element_72", fn() => VanillaBlocks::ELEMENT_HAFNIUM()); + $result->registerBlock("element_73", fn() => VanillaBlocks::ELEMENT_TANTALUM()); + $result->registerBlock("element_74", fn() => VanillaBlocks::ELEMENT_TUNGSTEN()); + $result->registerBlock("element_75", fn() => VanillaBlocks::ELEMENT_RHENIUM()); + $result->registerBlock("element_76", fn() => VanillaBlocks::ELEMENT_OSMIUM()); + $result->registerBlock("element_77", fn() => VanillaBlocks::ELEMENT_IRIDIUM()); + $result->registerBlock("element_78", fn() => VanillaBlocks::ELEMENT_PLATINUM()); + $result->registerBlock("element_79", fn() => VanillaBlocks::ELEMENT_GOLD()); + $result->registerBlock("element_8", fn() => VanillaBlocks::ELEMENT_OXYGEN()); + $result->registerBlock("element_80", fn() => VanillaBlocks::ELEMENT_MERCURY()); + $result->registerBlock("element_81", fn() => VanillaBlocks::ELEMENT_THALLIUM()); + $result->registerBlock("element_82", fn() => VanillaBlocks::ELEMENT_LEAD()); + $result->registerBlock("element_83", fn() => VanillaBlocks::ELEMENT_BISMUTH()); + $result->registerBlock("element_84", fn() => VanillaBlocks::ELEMENT_POLONIUM()); + $result->registerBlock("element_85", fn() => VanillaBlocks::ELEMENT_ASTATINE()); + $result->registerBlock("element_86", fn() => VanillaBlocks::ELEMENT_RADON()); + $result->registerBlock("element_87", fn() => VanillaBlocks::ELEMENT_FRANCIUM()); + $result->registerBlock("element_88", fn() => VanillaBlocks::ELEMENT_RADIUM()); + $result->registerBlock("element_89", fn() => VanillaBlocks::ELEMENT_ACTINIUM()); + $result->registerBlock("element_9", fn() => VanillaBlocks::ELEMENT_FLUORINE()); + $result->registerBlock("element_90", fn() => VanillaBlocks::ELEMENT_THORIUM()); + $result->registerBlock("element_91", fn() => VanillaBlocks::ELEMENT_PROTACTINIUM()); + $result->registerBlock("element_92", fn() => VanillaBlocks::ELEMENT_URANIUM()); + $result->registerBlock("element_93", fn() => VanillaBlocks::ELEMENT_NEPTUNIUM()); + $result->registerBlock("element_94", fn() => VanillaBlocks::ELEMENT_PLUTONIUM()); + $result->registerBlock("element_95", fn() => VanillaBlocks::ELEMENT_AMERICIUM()); + $result->registerBlock("element_96", fn() => VanillaBlocks::ELEMENT_CURIUM()); + $result->registerBlock("element_97", fn() => VanillaBlocks::ELEMENT_BERKELIUM()); + $result->registerBlock("element_98", fn() => VanillaBlocks::ELEMENT_CALIFORNIUM()); + $result->registerBlock("element_99", fn() => VanillaBlocks::ELEMENT_EINSTEINIUM()); + $result->registerBlock("element_actinium", fn() => VanillaBlocks::ELEMENT_ACTINIUM()); + $result->registerBlock("element_aluminum", fn() => VanillaBlocks::ELEMENT_ALUMINUM()); + $result->registerBlock("element_americium", fn() => VanillaBlocks::ELEMENT_AMERICIUM()); + $result->registerBlock("element_antimony", fn() => VanillaBlocks::ELEMENT_ANTIMONY()); + $result->registerBlock("element_argon", fn() => VanillaBlocks::ELEMENT_ARGON()); + $result->registerBlock("element_arsenic", fn() => VanillaBlocks::ELEMENT_ARSENIC()); + $result->registerBlock("element_astatine", fn() => VanillaBlocks::ELEMENT_ASTATINE()); + $result->registerBlock("element_barium", fn() => VanillaBlocks::ELEMENT_BARIUM()); + $result->registerBlock("element_berkelium", fn() => VanillaBlocks::ELEMENT_BERKELIUM()); + $result->registerBlock("element_beryllium", fn() => VanillaBlocks::ELEMENT_BERYLLIUM()); + $result->registerBlock("element_bismuth", fn() => VanillaBlocks::ELEMENT_BISMUTH()); + $result->registerBlock("element_bohrium", fn() => VanillaBlocks::ELEMENT_BOHRIUM()); + $result->registerBlock("element_boron", fn() => VanillaBlocks::ELEMENT_BORON()); + $result->registerBlock("element_bromine", fn() => VanillaBlocks::ELEMENT_BROMINE()); + $result->registerBlock("element_cadmium", fn() => VanillaBlocks::ELEMENT_CADMIUM()); + $result->registerBlock("element_calcium", fn() => VanillaBlocks::ELEMENT_CALCIUM()); + $result->registerBlock("element_californium", fn() => VanillaBlocks::ELEMENT_CALIFORNIUM()); + $result->registerBlock("element_carbon", fn() => VanillaBlocks::ELEMENT_CARBON()); + $result->registerBlock("element_cerium", fn() => VanillaBlocks::ELEMENT_CERIUM()); + $result->registerBlock("element_cesium", fn() => VanillaBlocks::ELEMENT_CESIUM()); + $result->registerBlock("element_chlorine", fn() => VanillaBlocks::ELEMENT_CHLORINE()); + $result->registerBlock("element_chromium", fn() => VanillaBlocks::ELEMENT_CHROMIUM()); + $result->registerBlock("element_cobalt", fn() => VanillaBlocks::ELEMENT_COBALT()); + $result->registerBlock("element_constructor", fn() => VanillaBlocks::ELEMENT_CONSTRUCTOR()); + $result->registerBlock("element_copernicium", fn() => VanillaBlocks::ELEMENT_COPERNICIUM()); + $result->registerBlock("element_copper", fn() => VanillaBlocks::ELEMENT_COPPER()); + $result->registerBlock("element_curium", fn() => VanillaBlocks::ELEMENT_CURIUM()); + $result->registerBlock("element_darmstadtium", fn() => VanillaBlocks::ELEMENT_DARMSTADTIUM()); + $result->registerBlock("element_dubnium", fn() => VanillaBlocks::ELEMENT_DUBNIUM()); + $result->registerBlock("element_dysprosium", fn() => VanillaBlocks::ELEMENT_DYSPROSIUM()); + $result->registerBlock("element_einsteinium", fn() => VanillaBlocks::ELEMENT_EINSTEINIUM()); + $result->registerBlock("element_erbium", fn() => VanillaBlocks::ELEMENT_ERBIUM()); + $result->registerBlock("element_europium", fn() => VanillaBlocks::ELEMENT_EUROPIUM()); + $result->registerBlock("element_fermium", fn() => VanillaBlocks::ELEMENT_FERMIUM()); + $result->registerBlock("element_flerovium", fn() => VanillaBlocks::ELEMENT_FLEROVIUM()); + $result->registerBlock("element_fluorine", fn() => VanillaBlocks::ELEMENT_FLUORINE()); + $result->registerBlock("element_francium", fn() => VanillaBlocks::ELEMENT_FRANCIUM()); + $result->registerBlock("element_gadolinium", fn() => VanillaBlocks::ELEMENT_GADOLINIUM()); + $result->registerBlock("element_gallium", fn() => VanillaBlocks::ELEMENT_GALLIUM()); + $result->registerBlock("element_germanium", fn() => VanillaBlocks::ELEMENT_GERMANIUM()); + $result->registerBlock("element_gold", fn() => VanillaBlocks::ELEMENT_GOLD()); + $result->registerBlock("element_hafnium", fn() => VanillaBlocks::ELEMENT_HAFNIUM()); + $result->registerBlock("element_hassium", fn() => VanillaBlocks::ELEMENT_HASSIUM()); + $result->registerBlock("element_helium", fn() => VanillaBlocks::ELEMENT_HELIUM()); + $result->registerBlock("element_holmium", fn() => VanillaBlocks::ELEMENT_HOLMIUM()); + $result->registerBlock("element_hydrogen", fn() => VanillaBlocks::ELEMENT_HYDROGEN()); + $result->registerBlock("element_indium", fn() => VanillaBlocks::ELEMENT_INDIUM()); + $result->registerBlock("element_iodine", fn() => VanillaBlocks::ELEMENT_IODINE()); + $result->registerBlock("element_iridium", fn() => VanillaBlocks::ELEMENT_IRIDIUM()); + $result->registerBlock("element_iron", fn() => VanillaBlocks::ELEMENT_IRON()); + $result->registerBlock("element_krypton", fn() => VanillaBlocks::ELEMENT_KRYPTON()); + $result->registerBlock("element_lanthanum", fn() => VanillaBlocks::ELEMENT_LANTHANUM()); + $result->registerBlock("element_lawrencium", fn() => VanillaBlocks::ELEMENT_LAWRENCIUM()); + $result->registerBlock("element_lead", fn() => VanillaBlocks::ELEMENT_LEAD()); + $result->registerBlock("element_lithium", fn() => VanillaBlocks::ELEMENT_LITHIUM()); + $result->registerBlock("element_livermorium", fn() => VanillaBlocks::ELEMENT_LIVERMORIUM()); + $result->registerBlock("element_lutetium", fn() => VanillaBlocks::ELEMENT_LUTETIUM()); + $result->registerBlock("element_magnesium", fn() => VanillaBlocks::ELEMENT_MAGNESIUM()); + $result->registerBlock("element_manganese", fn() => VanillaBlocks::ELEMENT_MANGANESE()); + $result->registerBlock("element_meitnerium", fn() => VanillaBlocks::ELEMENT_MEITNERIUM()); + $result->registerBlock("element_mendelevium", fn() => VanillaBlocks::ELEMENT_MENDELEVIUM()); + $result->registerBlock("element_mercury", fn() => VanillaBlocks::ELEMENT_MERCURY()); + $result->registerBlock("element_molybdenum", fn() => VanillaBlocks::ELEMENT_MOLYBDENUM()); + $result->registerBlock("element_moscovium", fn() => VanillaBlocks::ELEMENT_MOSCOVIUM()); + $result->registerBlock("element_neodymium", fn() => VanillaBlocks::ELEMENT_NEODYMIUM()); + $result->registerBlock("element_neon", fn() => VanillaBlocks::ELEMENT_NEON()); + $result->registerBlock("element_neptunium", fn() => VanillaBlocks::ELEMENT_NEPTUNIUM()); + $result->registerBlock("element_nickel", fn() => VanillaBlocks::ELEMENT_NICKEL()); + $result->registerBlock("element_nihonium", fn() => VanillaBlocks::ELEMENT_NIHONIUM()); + $result->registerBlock("element_niobium", fn() => VanillaBlocks::ELEMENT_NIOBIUM()); + $result->registerBlock("element_nitrogen", fn() => VanillaBlocks::ELEMENT_NITROGEN()); + $result->registerBlock("element_nobelium", fn() => VanillaBlocks::ELEMENT_NOBELIUM()); + $result->registerBlock("element_oganesson", fn() => VanillaBlocks::ELEMENT_OGANESSON()); + $result->registerBlock("element_osmium", fn() => VanillaBlocks::ELEMENT_OSMIUM()); + $result->registerBlock("element_oxygen", fn() => VanillaBlocks::ELEMENT_OXYGEN()); + $result->registerBlock("element_palladium", fn() => VanillaBlocks::ELEMENT_PALLADIUM()); + $result->registerBlock("element_phosphorus", fn() => VanillaBlocks::ELEMENT_PHOSPHORUS()); + $result->registerBlock("element_platinum", fn() => VanillaBlocks::ELEMENT_PLATINUM()); + $result->registerBlock("element_plutonium", fn() => VanillaBlocks::ELEMENT_PLUTONIUM()); + $result->registerBlock("element_polonium", fn() => VanillaBlocks::ELEMENT_POLONIUM()); + $result->registerBlock("element_potassium", fn() => VanillaBlocks::ELEMENT_POTASSIUM()); + $result->registerBlock("element_praseodymium", fn() => VanillaBlocks::ELEMENT_PRASEODYMIUM()); + $result->registerBlock("element_promethium", fn() => VanillaBlocks::ELEMENT_PROMETHIUM()); + $result->registerBlock("element_protactinium", fn() => VanillaBlocks::ELEMENT_PROTACTINIUM()); + $result->registerBlock("element_radium", fn() => VanillaBlocks::ELEMENT_RADIUM()); + $result->registerBlock("element_radon", fn() => VanillaBlocks::ELEMENT_RADON()); + $result->registerBlock("element_rhenium", fn() => VanillaBlocks::ELEMENT_RHENIUM()); + $result->registerBlock("element_rhodium", fn() => VanillaBlocks::ELEMENT_RHODIUM()); + $result->registerBlock("element_roentgenium", fn() => VanillaBlocks::ELEMENT_ROENTGENIUM()); + $result->registerBlock("element_rubidium", fn() => VanillaBlocks::ELEMENT_RUBIDIUM()); + $result->registerBlock("element_ruthenium", fn() => VanillaBlocks::ELEMENT_RUTHENIUM()); + $result->registerBlock("element_rutherfordium", fn() => VanillaBlocks::ELEMENT_RUTHERFORDIUM()); + $result->registerBlock("element_samarium", fn() => VanillaBlocks::ELEMENT_SAMARIUM()); + $result->registerBlock("element_scandium", fn() => VanillaBlocks::ELEMENT_SCANDIUM()); + $result->registerBlock("element_seaborgium", fn() => VanillaBlocks::ELEMENT_SEABORGIUM()); + $result->registerBlock("element_selenium", fn() => VanillaBlocks::ELEMENT_SELENIUM()); + $result->registerBlock("element_silicon", fn() => VanillaBlocks::ELEMENT_SILICON()); + $result->registerBlock("element_silver", fn() => VanillaBlocks::ELEMENT_SILVER()); + $result->registerBlock("element_sodium", fn() => VanillaBlocks::ELEMENT_SODIUM()); + $result->registerBlock("element_strontium", fn() => VanillaBlocks::ELEMENT_STRONTIUM()); + $result->registerBlock("element_sulfur", fn() => VanillaBlocks::ELEMENT_SULFUR()); + $result->registerBlock("element_tantalum", fn() => VanillaBlocks::ELEMENT_TANTALUM()); + $result->registerBlock("element_technetium", fn() => VanillaBlocks::ELEMENT_TECHNETIUM()); + $result->registerBlock("element_tellurium", fn() => VanillaBlocks::ELEMENT_TELLURIUM()); + $result->registerBlock("element_tennessine", fn() => VanillaBlocks::ELEMENT_TENNESSINE()); + $result->registerBlock("element_terbium", fn() => VanillaBlocks::ELEMENT_TERBIUM()); + $result->registerBlock("element_thallium", fn() => VanillaBlocks::ELEMENT_THALLIUM()); + $result->registerBlock("element_thorium", fn() => VanillaBlocks::ELEMENT_THORIUM()); + $result->registerBlock("element_thulium", fn() => VanillaBlocks::ELEMENT_THULIUM()); + $result->registerBlock("element_tin", fn() => VanillaBlocks::ELEMENT_TIN()); + $result->registerBlock("element_titanium", fn() => VanillaBlocks::ELEMENT_TITANIUM()); + $result->registerBlock("element_tungsten", fn() => VanillaBlocks::ELEMENT_TUNGSTEN()); + $result->registerBlock("element_uranium", fn() => VanillaBlocks::ELEMENT_URANIUM()); + $result->registerBlock("element_vanadium", fn() => VanillaBlocks::ELEMENT_VANADIUM()); + $result->registerBlock("element_xenon", fn() => VanillaBlocks::ELEMENT_XENON()); + $result->registerBlock("element_ytterbium", fn() => VanillaBlocks::ELEMENT_YTTERBIUM()); + $result->registerBlock("element_yttrium", fn() => VanillaBlocks::ELEMENT_YTTRIUM()); + $result->registerBlock("element_zero", fn() => VanillaBlocks::ELEMENT_ZERO()); + $result->registerBlock("element_zinc", fn() => VanillaBlocks::ELEMENT_ZINC()); + $result->registerBlock("element_zirconium", fn() => VanillaBlocks::ELEMENT_ZIRCONIUM()); + $result->registerBlock("emerald_block", fn() => VanillaBlocks::EMERALD()); + $result->registerBlock("emerald_ore", fn() => VanillaBlocks::EMERALD_ORE()); + $result->registerBlock("enchant_table", fn() => VanillaBlocks::ENCHANTING_TABLE()); + $result->registerBlock("enchanting_table", fn() => VanillaBlocks::ENCHANTING_TABLE()); + $result->registerBlock("enchantment_table", fn() => VanillaBlocks::ENCHANTING_TABLE()); + $result->registerBlock("end_brick_stairs", fn() => VanillaBlocks::END_STONE_BRICK_STAIRS()); + $result->registerBlock("end_bricks", fn() => VanillaBlocks::END_STONE_BRICKS()); + $result->registerBlock("end_portal_frame", fn() => VanillaBlocks::END_PORTAL_FRAME()); + $result->registerBlock("end_rod", fn() => VanillaBlocks::END_ROD()); + $result->registerBlock("end_stone", fn() => VanillaBlocks::END_STONE()); + $result->registerBlock("end_stone_brick_slab", fn() => VanillaBlocks::END_STONE_BRICK_SLAB()); + $result->registerBlock("end_stone_brick_stairs", fn() => VanillaBlocks::END_STONE_BRICK_STAIRS()); + $result->registerBlock("end_stone_brick_wall", fn() => VanillaBlocks::END_STONE_BRICK_WALL()); + $result->registerBlock("end_stone_bricks", fn() => VanillaBlocks::END_STONE_BRICKS()); + $result->registerBlock("ender_chest", fn() => VanillaBlocks::ENDER_CHEST()); + $result->registerBlock("fake_wooden_slab", fn() => VanillaBlocks::FAKE_WOODEN_SLAB()); + $result->registerBlock("farmland", fn() => VanillaBlocks::FARMLAND()); + $result->registerBlock("fence", fn() => VanillaBlocks::OAK_FENCE()); + $result->registerBlock("fence_gate", fn() => VanillaBlocks::OAK_FENCE_GATE()); + $result->registerBlock("fence_gate_acacia", fn() => VanillaBlocks::ACACIA_FENCE_GATE()); + $result->registerBlock("fence_gate_birch", fn() => VanillaBlocks::BIRCH_FENCE_GATE()); + $result->registerBlock("fence_gate_dark_oak", fn() => VanillaBlocks::DARK_OAK_FENCE_GATE()); + $result->registerBlock("fence_gate_jungle", fn() => VanillaBlocks::JUNGLE_FENCE_GATE()); + $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()); + $result->registerBlock("flowing_water", fn() => VanillaBlocks::WATER()); + $result->registerBlock("frame", fn() => VanillaBlocks::ITEM_FRAME()); + $result->registerBlock("frame_block", fn() => VanillaBlocks::ITEM_FRAME()); + $result->registerBlock("frosted_ice", fn() => VanillaBlocks::FROSTED_ICE()); + $result->registerBlock("furnace", fn() => VanillaBlocks::FURNACE()); + $result->registerBlock("glass", fn() => VanillaBlocks::GLASS()); + $result->registerBlock("glass_pane", fn() => VanillaBlocks::GLASS_PANE()); + $result->registerBlock("glass_panel", fn() => VanillaBlocks::GLASS_PANE()); + $result->registerBlock("glowing_obsidian", fn() => VanillaBlocks::GLOWING_OBSIDIAN()); + $result->registerBlock("glowing_redstone_ore", fn() => VanillaBlocks::REDSTONE_ORE()->setLit(true)); + $result->registerBlock("glowingobsidian", fn() => VanillaBlocks::GLOWING_OBSIDIAN()); + $result->registerBlock("glowstone", fn() => VanillaBlocks::GLOWSTONE()); + $result->registerBlock("glowstone_block", fn() => VanillaBlocks::GLOWSTONE()); + $result->registerBlock("gold", fn() => VanillaBlocks::GOLD()); + $result->registerBlock("gold_block", fn() => VanillaBlocks::GOLD()); + $result->registerBlock("gold_ore", fn() => VanillaBlocks::GOLD_ORE()); + $result->registerBlock("gold_pressure_plate", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT()); + $result->registerBlock("golden_rail", fn() => VanillaBlocks::POWERED_RAIL()); + $result->registerBlock("granite", fn() => VanillaBlocks::GRANITE()); + $result->registerBlock("granite_slab", fn() => VanillaBlocks::GRANITE_SLAB()); + $result->registerBlock("granite_stairs", fn() => VanillaBlocks::GRANITE_STAIRS()); + $result->registerBlock("granite_wall", fn() => VanillaBlocks::GRANITE_WALL()); + $result->registerBlock("grass", fn() => VanillaBlocks::GRASS()); + $result->registerBlock("grass_path", fn() => VanillaBlocks::GRASS_PATH()); + $result->registerBlock("gravel", fn() => VanillaBlocks::GRAVEL()); + $result->registerBlock("gray_glazed_terracotta", fn() => VanillaBlocks::GRAY_GLAZED_TERRACOTTA()); + $result->registerBlock("green_glazed_terracotta", fn() => VanillaBlocks::GREEN_GLAZED_TERRACOTTA()); + $result->registerBlock("green_torch", fn() => VanillaBlocks::GREEN_TORCH()); + $result->registerBlock("hard_glass", fn() => VanillaBlocks::HARDENED_GLASS()); + $result->registerBlock("hard_glass_pane", fn() => VanillaBlocks::HARDENED_GLASS_PANE()); + $result->registerBlock("hard_stained_glass", fn() => VanillaBlocks::STAINED_HARDENED_GLASS()); + $result->registerBlock("hard_stained_glass_pane", fn() => VanillaBlocks::STAINED_HARDENED_GLASS_PANE()); + $result->registerBlock("hardened_clay", fn() => VanillaBlocks::HARDENED_CLAY()); + $result->registerBlock("hardened_glass", fn() => VanillaBlocks::HARDENED_GLASS()); + $result->registerBlock("hardened_glass_pane", fn() => VanillaBlocks::HARDENED_GLASS_PANE()); + $result->registerBlock("hay_bale", fn() => VanillaBlocks::HAY_BALE()); + $result->registerBlock("hay_block", fn() => VanillaBlocks::HAY_BALE()); + $result->registerBlock("heavy_weighted_pressure_plate", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY()); + $result->registerBlock("hopper", fn() => VanillaBlocks::HOPPER()); + $result->registerBlock("hopper_block", fn() => VanillaBlocks::HOPPER()); + $result->registerBlock("ice", fn() => VanillaBlocks::ICE()); + $result->registerBlock("inactive_redstone_lamp", fn() => VanillaBlocks::REDSTONE_LAMP()); + $result->registerBlock("infested_chiseled_stone_brick", fn() => VanillaBlocks::INFESTED_CHISELED_STONE_BRICK()); + $result->registerBlock("infested_cobblestone", fn() => VanillaBlocks::INFESTED_COBBLESTONE()); + $result->registerBlock("infested_cracked_stone_brick", fn() => VanillaBlocks::INFESTED_CRACKED_STONE_BRICK()); + $result->registerBlock("infested_mossy_stone_brick", fn() => VanillaBlocks::INFESTED_MOSSY_STONE_BRICK()); + $result->registerBlock("infested_stone", fn() => VanillaBlocks::INFESTED_STONE()); + $result->registerBlock("infested_stone_brick", fn() => VanillaBlocks::INFESTED_STONE_BRICK()); + $result->registerBlock("info_reserved6", fn() => VanillaBlocks::RESERVED6()); + $result->registerBlock("info_update", fn() => VanillaBlocks::INFO_UPDATE()); + $result->registerBlock("info_update2", fn() => VanillaBlocks::INFO_UPDATE2()); + $result->registerBlock("inverted_daylight_sensor", fn() => VanillaBlocks::DAYLIGHT_SENSOR()->setInverted(true)); + $result->registerBlock("invisible_bedrock", fn() => VanillaBlocks::INVISIBLE_BEDROCK()); + $result->registerBlock("invisiblebedrock", fn() => VanillaBlocks::INVISIBLE_BEDROCK()); + $result->registerBlock("iron", fn() => VanillaBlocks::IRON()); + $result->registerBlock("iron_bar", fn() => VanillaBlocks::IRON_BARS()); + $result->registerBlock("iron_bars", fn() => VanillaBlocks::IRON_BARS()); + $result->registerBlock("iron_block", fn() => VanillaBlocks::IRON()); + $result->registerBlock("iron_door", fn() => VanillaBlocks::IRON_DOOR()); + $result->registerBlock("iron_door_block", fn() => VanillaBlocks::IRON_DOOR()); + $result->registerBlock("iron_ore", fn() => VanillaBlocks::IRON_ORE()); + $result->registerBlock("iron_pressure_plate", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY()); + $result->registerBlock("iron_trapdoor", fn() => VanillaBlocks::IRON_TRAPDOOR()); + $result->registerBlock("item_frame", fn() => VanillaBlocks::ITEM_FRAME()); + $result->registerBlock("item_frame_block", fn() => VanillaBlocks::ITEM_FRAME()); + $result->registerBlock("jack_o_lantern", fn() => VanillaBlocks::LIT_PUMPKIN()); + $result->registerBlock("jukebox", fn() => VanillaBlocks::JUKEBOX()); + $result->registerBlock("jungle_button", fn() => VanillaBlocks::JUNGLE_BUTTON()); + $result->registerBlock("jungle_door", fn() => VanillaBlocks::JUNGLE_DOOR()); + $result->registerBlock("jungle_door_block", fn() => VanillaBlocks::JUNGLE_DOOR()); + $result->registerBlock("jungle_fence", fn() => VanillaBlocks::JUNGLE_FENCE()); + $result->registerBlock("jungle_fence_gate", fn() => VanillaBlocks::JUNGLE_FENCE_GATE()); + $result->registerBlock("jungle_leaves", fn() => VanillaBlocks::JUNGLE_LEAVES()); + $result->registerBlock("jungle_log", fn() => VanillaBlocks::JUNGLE_LOG()); + $result->registerBlock("jungle_planks", fn() => VanillaBlocks::JUNGLE_PLANKS()); + $result->registerBlock("jungle_pressure_plate", fn() => VanillaBlocks::JUNGLE_PRESSURE_PLATE()); + $result->registerBlock("jungle_sapling", fn() => VanillaBlocks::JUNGLE_SAPLING()); + $result->registerBlock("jungle_sign", fn() => VanillaBlocks::JUNGLE_SIGN()); + $result->registerBlock("jungle_slab", fn() => VanillaBlocks::JUNGLE_SLAB()); + $result->registerBlock("jungle_stairs", fn() => VanillaBlocks::JUNGLE_STAIRS()); + $result->registerBlock("jungle_standing_sign", fn() => VanillaBlocks::JUNGLE_SIGN()); + $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()); + $result->registerBlock("lapis_block", fn() => VanillaBlocks::LAPIS_LAZULI()); + $result->registerBlock("lapis_lazuli_block", fn() => VanillaBlocks::LAPIS_LAZULI()); + $result->registerBlock("lapis_lazuli_ore", fn() => VanillaBlocks::LAPIS_LAZULI_ORE()); + $result->registerBlock("lapis_ore", fn() => VanillaBlocks::LAPIS_LAZULI_ORE()); + $result->registerBlock("large_fern", fn() => VanillaBlocks::LARGE_FERN()); + $result->registerBlock("lava", fn() => VanillaBlocks::LAVA()); + $result->registerBlock("leave", fn() => VanillaBlocks::OAK_LEAVES()); + $result->registerBlock("leave2", fn() => VanillaBlocks::ACACIA_LEAVES()); + $result->registerBlock("leaves", fn() => VanillaBlocks::OAK_LEAVES()); + $result->registerBlock("leaves2", fn() => VanillaBlocks::ACACIA_LEAVES()); + $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()); + $result->registerBlock("light_gray_glazed_terracotta", fn() => VanillaBlocks::LIGHT_GRAY_GLAZED_TERRACOTTA()); + $result->registerBlock("light_weighted_pressure_plate", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT()); + $result->registerBlock("lilac", fn() => VanillaBlocks::LILAC()); + $result->registerBlock("lily_of_the_valley", fn() => VanillaBlocks::LILY_OF_THE_VALLEY()); + $result->registerBlock("lily_pad", fn() => VanillaBlocks::LILY_PAD()); + $result->registerBlock("lime_glazed_terracotta", fn() => VanillaBlocks::LIME_GLAZED_TERRACOTTA()); + $result->registerBlock("lit_blast_furnace", fn() => VanillaBlocks::BLAST_FURNACE()); + $result->registerBlock("lit_furnace", fn() => VanillaBlocks::FURNACE()); + $result->registerBlock("lit_pumpkin", fn() => VanillaBlocks::LIT_PUMPKIN()); + $result->registerBlock("lit_redstone_lamp", fn() => VanillaBlocks::REDSTONE_LAMP()->setPowered(true)); + $result->registerBlock("lit_redstone_ore", fn() => VanillaBlocks::REDSTONE_ORE()->setLit(true)); + $result->registerBlock("lit_redstone_torch", fn() => VanillaBlocks::REDSTONE_TORCH()); + $result->registerBlock("lit_smoker", fn() => VanillaBlocks::SMOKER()); + $result->registerBlock("log", fn() => VanillaBlocks::OAK_LOG()); + $result->registerBlock("log2", fn() => VanillaBlocks::ACACIA_LOG()); + $result->registerBlock("loom", fn() => VanillaBlocks::LOOM()); + $result->registerBlock("magenta_glazed_terracotta", fn() => VanillaBlocks::MAGENTA_GLAZED_TERRACOTTA()); + $result->registerBlock("magma", fn() => VanillaBlocks::MAGMA()); + $result->registerBlock("material_reducer", fn() => VanillaBlocks::MATERIAL_REDUCER()); + $result->registerBlock("melon_block", fn() => VanillaBlocks::MELON()); + $result->registerBlock("melon_stem", fn() => VanillaBlocks::MELON_STEM()); + $result->registerBlock("mob_head_block", fn() => VanillaBlocks::MOB_HEAD()); + $result->registerBlock("mob_spawner", fn() => VanillaBlocks::MONSTER_SPAWNER()); + $result->registerBlock("monster_egg", fn() => VanillaBlocks::INFESTED_STONE()); + $result->registerBlock("monster_egg_block", fn() => VanillaBlocks::INFESTED_STONE()); + $result->registerBlock("monster_spawner", fn() => VanillaBlocks::MONSTER_SPAWNER()); + $result->registerBlock("moss_stone", fn() => VanillaBlocks::MOSSY_COBBLESTONE()); + $result->registerBlock("mossy_cobblestone", fn() => VanillaBlocks::MOSSY_COBBLESTONE()); + $result->registerBlock("mossy_cobblestone_slab", fn() => VanillaBlocks::MOSSY_COBBLESTONE_SLAB()); + $result->registerBlock("mossy_cobblestone_stairs", fn() => VanillaBlocks::MOSSY_COBBLESTONE_STAIRS()); + $result->registerBlock("mossy_cobblestone_wall", fn() => VanillaBlocks::MOSSY_COBBLESTONE_WALL()); + $result->registerBlock("mossy_stone", fn() => VanillaBlocks::MOSSY_COBBLESTONE()); + $result->registerBlock("mossy_stone_brick_slab", fn() => VanillaBlocks::MOSSY_STONE_BRICK_SLAB()); + $result->registerBlock("mossy_stone_brick_stairs", fn() => VanillaBlocks::MOSSY_STONE_BRICK_STAIRS()); + $result->registerBlock("mossy_stone_brick_wall", fn() => VanillaBlocks::MOSSY_STONE_BRICK_WALL()); + $result->registerBlock("mossy_stone_bricks", fn() => VanillaBlocks::MOSSY_STONE_BRICKS()); + $result->registerBlock("mushroom_stem", fn() => VanillaBlocks::MUSHROOM_STEM()); + $result->registerBlock("mycelium", fn() => VanillaBlocks::MYCELIUM()); + $result->registerBlock("nether_brick_block", fn() => VanillaBlocks::NETHER_BRICKS()); + $result->registerBlock("nether_brick_fence", fn() => VanillaBlocks::NETHER_BRICK_FENCE()); + $result->registerBlock("nether_brick_slab", fn() => VanillaBlocks::NETHER_BRICK_SLAB()); + $result->registerBlock("nether_brick_stairs", fn() => VanillaBlocks::NETHER_BRICK_STAIRS()); + $result->registerBlock("nether_brick_wall", fn() => VanillaBlocks::NETHER_BRICK_WALL()); + $result->registerBlock("nether_bricks", fn() => VanillaBlocks::NETHER_BRICKS()); + $result->registerBlock("nether_bricks_stairs", fn() => VanillaBlocks::NETHER_BRICK_STAIRS()); + $result->registerBlock("nether_portal", fn() => VanillaBlocks::NETHER_PORTAL()); + $result->registerBlock("nether_quartz_ore", fn() => VanillaBlocks::NETHER_QUARTZ_ORE()); + $result->registerBlock("nether_reactor", fn() => VanillaBlocks::NETHER_REACTOR_CORE()); + $result->registerBlock("nether_reactor_core", fn() => VanillaBlocks::NETHER_REACTOR_CORE()); + $result->registerBlock("nether_wart", fn() => VanillaBlocks::NETHER_WART()); + $result->registerBlock("nether_wart_block", fn() => VanillaBlocks::NETHER_WART_BLOCK()); + $result->registerBlock("nether_wart_plant", fn() => VanillaBlocks::NETHER_WART()); + $result->registerBlock("netherrack", fn() => VanillaBlocks::NETHERRACK()); + $result->registerBlock("netherreactor", fn() => VanillaBlocks::NETHER_REACTOR_CORE()); + $result->registerBlock("normal_stone_stairs", fn() => VanillaBlocks::STONE_STAIRS()); + $result->registerBlock("note_block", fn() => VanillaBlocks::NOTE_BLOCK()); + $result->registerBlock("noteblock", fn() => VanillaBlocks::NOTE_BLOCK()); + $result->registerBlock("oak_button", fn() => VanillaBlocks::OAK_BUTTON()); + $result->registerBlock("oak_door", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("oak_door_block", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("oak_fence", fn() => VanillaBlocks::OAK_FENCE()); + $result->registerBlock("oak_fence_gate", fn() => VanillaBlocks::OAK_FENCE_GATE()); + $result->registerBlock("oak_leaves", fn() => VanillaBlocks::OAK_LEAVES()); + $result->registerBlock("oak_log", fn() => VanillaBlocks::OAK_LOG()); + $result->registerBlock("oak_planks", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("oak_pressure_plate", fn() => VanillaBlocks::OAK_PRESSURE_PLATE()); + $result->registerBlock("oak_sapling", fn() => VanillaBlocks::OAK_SAPLING()); + $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()); + $result->registerBlock("oak_wood_stairs", fn() => VanillaBlocks::OAK_STAIRS()); + $result->registerBlock("oak_wooden_stairs", fn() => VanillaBlocks::OAK_STAIRS()); + $result->registerBlock("obsidian", fn() => VanillaBlocks::OBSIDIAN()); + $result->registerBlock("orange_glazed_terracotta", fn() => VanillaBlocks::ORANGE_GLAZED_TERRACOTTA()); + $result->registerBlock("orange_tulip", fn() => VanillaBlocks::ORANGE_TULIP()); + $result->registerBlock("oxeye_daisy", fn() => VanillaBlocks::OXEYE_DAISY()); + $result->registerBlock("packed_ice", fn() => VanillaBlocks::PACKED_ICE()); + $result->registerBlock("peony", fn() => VanillaBlocks::PEONY()); + $result->registerBlock("pink_glazed_terracotta", fn() => VanillaBlocks::PINK_GLAZED_TERRACOTTA()); + $result->registerBlock("pink_tulip", fn() => VanillaBlocks::PINK_TULIP()); + $result->registerBlock("plank", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("planks", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("podzol", fn() => VanillaBlocks::PODZOL()); + $result->registerBlock("polished_andesite", fn() => VanillaBlocks::POLISHED_ANDESITE()); + $result->registerBlock("polished_andesite_slab", fn() => VanillaBlocks::POLISHED_ANDESITE_SLAB()); + $result->registerBlock("polished_andesite_stairs", fn() => VanillaBlocks::POLISHED_ANDESITE_STAIRS()); + $result->registerBlock("polished_diorite", fn() => VanillaBlocks::POLISHED_DIORITE()); + $result->registerBlock("polished_diorite_slab", fn() => VanillaBlocks::POLISHED_DIORITE_SLAB()); + $result->registerBlock("polished_diorite_stairs", fn() => VanillaBlocks::POLISHED_DIORITE_STAIRS()); + $result->registerBlock("polished_granite", fn() => VanillaBlocks::POLISHED_GRANITE()); + $result->registerBlock("polished_granite_slab", fn() => VanillaBlocks::POLISHED_GRANITE_SLAB()); + $result->registerBlock("polished_granite_stairs", fn() => VanillaBlocks::POLISHED_GRANITE_STAIRS()); + $result->registerBlock("poppy", fn() => VanillaBlocks::POPPY()); + $result->registerBlock("portal", fn() => VanillaBlocks::NETHER_PORTAL()); + $result->registerBlock("portal_block", fn() => VanillaBlocks::NETHER_PORTAL()); + $result->registerBlock("potato_block", fn() => VanillaBlocks::POTATOES()); + $result->registerBlock("potatoes", fn() => VanillaBlocks::POTATOES()); + $result->registerBlock("powered_comparator", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("powered_comparator_block", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("powered_rail", fn() => VanillaBlocks::POWERED_RAIL()); + $result->registerBlock("powered_repeater", fn() => VanillaBlocks::REDSTONE_REPEATER()->setPowered(true)); + $result->registerBlock("powered_repeater_block", fn() => VanillaBlocks::REDSTONE_REPEATER()->setPowered(true)); + $result->registerBlock("prismarine", fn() => VanillaBlocks::PRISMARINE()); + $result->registerBlock("prismarine_bricks", fn() => VanillaBlocks::PRISMARINE_BRICKS()); + $result->registerBlock("prismarine_bricks_slab", fn() => VanillaBlocks::PRISMARINE_BRICKS_SLAB()); + $result->registerBlock("prismarine_bricks_stairs", fn() => VanillaBlocks::PRISMARINE_BRICKS_STAIRS()); + $result->registerBlock("prismarine_slab", fn() => VanillaBlocks::PRISMARINE_SLAB()); + $result->registerBlock("prismarine_stairs", fn() => VanillaBlocks::PRISMARINE_STAIRS()); + $result->registerBlock("prismarine_wall", fn() => VanillaBlocks::PRISMARINE_WALL()); + $result->registerBlock("pumpkin", fn() => VanillaBlocks::PUMPKIN()); + $result->registerBlock("pumpkin_stem", fn() => VanillaBlocks::PUMPKIN_STEM()); + $result->registerBlock("purple_glazed_terracotta", fn() => VanillaBlocks::PURPLE_GLAZED_TERRACOTTA()); + $result->registerBlock("purple_torch", fn() => VanillaBlocks::PURPLE_TORCH()); + $result->registerBlock("purpur", fn() => VanillaBlocks::PURPUR()); + $result->registerBlock("purpur_block", fn() => VanillaBlocks::PURPUR()); + $result->registerBlock("purpur_pillar", fn() => VanillaBlocks::PURPUR_PILLAR()); + $result->registerBlock("purpur_slab", fn() => VanillaBlocks::PURPUR_SLAB()); + $result->registerBlock("purpur_stairs", fn() => VanillaBlocks::PURPUR_STAIRS()); + $result->registerBlock("quartz_block", fn() => VanillaBlocks::QUARTZ()); + $result->registerBlock("quartz_ore", fn() => VanillaBlocks::NETHER_QUARTZ_ORE()); + $result->registerBlock("quartz_pillar", fn() => VanillaBlocks::QUARTZ_PILLAR()); + $result->registerBlock("quartz_slab", fn() => VanillaBlocks::QUARTZ_SLAB()); + $result->registerBlock("quartz_stairs", fn() => VanillaBlocks::QUARTZ_STAIRS()); + $result->registerBlock("rail", fn() => VanillaBlocks::RAIL()); + $result->registerBlock("red_flower", fn() => VanillaBlocks::POPPY()); + $result->registerBlock("red_glazed_terracotta", fn() => VanillaBlocks::RED_GLAZED_TERRACOTTA()); + $result->registerBlock("red_mushroom", fn() => VanillaBlocks::RED_MUSHROOM()); + $result->registerBlock("red_mushroom_block", fn() => VanillaBlocks::RED_MUSHROOM_BLOCK()); + $result->registerBlock("red_nether_brick", fn() => VanillaBlocks::RED_NETHER_BRICKS()); + $result->registerBlock("red_nether_brick_slab", fn() => VanillaBlocks::RED_NETHER_BRICK_SLAB()); + $result->registerBlock("red_nether_brick_stairs", fn() => VanillaBlocks::RED_NETHER_BRICK_STAIRS()); + $result->registerBlock("red_nether_brick_wall", fn() => VanillaBlocks::RED_NETHER_BRICK_WALL()); + $result->registerBlock("red_nether_bricks", fn() => VanillaBlocks::RED_NETHER_BRICKS()); + $result->registerBlock("red_sand", fn() => VanillaBlocks::RED_SAND()); + $result->registerBlock("red_sandstone", fn() => VanillaBlocks::RED_SANDSTONE()); + $result->registerBlock("red_sandstone_slab", fn() => VanillaBlocks::RED_SANDSTONE_SLAB()); + $result->registerBlock("red_sandstone_stairs", fn() => VanillaBlocks::RED_SANDSTONE_STAIRS()); + $result->registerBlock("red_sandstone_wall", fn() => VanillaBlocks::RED_SANDSTONE_WALL()); + $result->registerBlock("red_torch", fn() => VanillaBlocks::RED_TORCH()); + $result->registerBlock("red_tulip", fn() => VanillaBlocks::RED_TULIP()); + $result->registerBlock("redstone_block", fn() => VanillaBlocks::REDSTONE()); + $result->registerBlock("redstone_comparator", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("redstone_lamp", fn() => VanillaBlocks::REDSTONE_LAMP()); + $result->registerBlock("redstone_ore", fn() => VanillaBlocks::REDSTONE_ORE()); + $result->registerBlock("redstone_repeater", fn() => VanillaBlocks::REDSTONE_REPEATER()); + $result->registerBlock("redstone_torch", fn() => VanillaBlocks::REDSTONE_TORCH()); + $result->registerBlock("redstone_wire", fn() => VanillaBlocks::REDSTONE_WIRE()); + $result->registerBlock("reeds", fn() => VanillaBlocks::SUGARCANE()); + $result->registerBlock("reeds_block", fn() => VanillaBlocks::SUGARCANE()); + $result->registerBlock("repeater", fn() => VanillaBlocks::REDSTONE_REPEATER()); + $result->registerBlock("repeater_block", fn() => VanillaBlocks::REDSTONE_REPEATER()); + $result->registerBlock("reserved6", fn() => VanillaBlocks::RESERVED6()); + $result->registerBlock("rose", fn() => VanillaBlocks::POPPY()); + $result->registerBlock("rose_bush", fn() => VanillaBlocks::ROSE_BUSH()); + $result->registerBlock("sand", fn() => VanillaBlocks::SAND()); + $result->registerBlock("sandstone", fn() => VanillaBlocks::SANDSTONE()); + $result->registerBlock("sandstone_slab", fn() => VanillaBlocks::SANDSTONE_SLAB()); + $result->registerBlock("sandstone_stairs", fn() => VanillaBlocks::SANDSTONE_STAIRS()); + $result->registerBlock("sandstone_wall", fn() => VanillaBlocks::SANDSTONE_WALL()); + $result->registerBlock("sapling", fn() => VanillaBlocks::OAK_SAPLING()); + $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("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("smoker", fn() => VanillaBlocks::SMOKER()); + $result->registerBlock("smooth_quartz", fn() => VanillaBlocks::SMOOTH_QUARTZ()); + $result->registerBlock("smooth_quartz_slab", fn() => VanillaBlocks::SMOOTH_QUARTZ_SLAB()); + $result->registerBlock("smooth_quartz_stairs", fn() => VanillaBlocks::SMOOTH_QUARTZ_STAIRS()); + $result->registerBlock("smooth_red_sandstone", fn() => VanillaBlocks::SMOOTH_RED_SANDSTONE()); + $result->registerBlock("smooth_red_sandstone_slab", fn() => VanillaBlocks::SMOOTH_RED_SANDSTONE_SLAB()); + $result->registerBlock("smooth_red_sandstone_stairs", fn() => VanillaBlocks::SMOOTH_RED_SANDSTONE_STAIRS()); + $result->registerBlock("smooth_sandstone", fn() => VanillaBlocks::SMOOTH_SANDSTONE()); + $result->registerBlock("smooth_sandstone_slab", fn() => VanillaBlocks::SMOOTH_SANDSTONE_SLAB()); + $result->registerBlock("smooth_sandstone_stairs", fn() => VanillaBlocks::SMOOTH_SANDSTONE_STAIRS()); + $result->registerBlock("smooth_stone", fn() => VanillaBlocks::SMOOTH_STONE()); + $result->registerBlock("smooth_stone_slab", fn() => VanillaBlocks::SMOOTH_STONE_SLAB()); + $result->registerBlock("snow", fn() => VanillaBlocks::SNOW()); + $result->registerBlock("snow_block", fn() => VanillaBlocks::SNOW()); + $result->registerBlock("snow_layer", fn() => VanillaBlocks::SNOW_LAYER()); + $result->registerBlock("soul_sand", fn() => VanillaBlocks::SOUL_SAND()); + $result->registerBlock("sponge", fn() => VanillaBlocks::SPONGE()); + $result->registerBlock("spruce_button", fn() => VanillaBlocks::SPRUCE_BUTTON()); + $result->registerBlock("spruce_door", fn() => VanillaBlocks::SPRUCE_DOOR()); + $result->registerBlock("spruce_door_block", fn() => VanillaBlocks::SPRUCE_DOOR()); + $result->registerBlock("spruce_fence", fn() => VanillaBlocks::SPRUCE_FENCE()); + $result->registerBlock("spruce_fence_gate", fn() => VanillaBlocks::SPRUCE_FENCE_GATE()); + $result->registerBlock("spruce_leaves", fn() => VanillaBlocks::SPRUCE_LEAVES()); + $result->registerBlock("spruce_log", fn() => VanillaBlocks::SPRUCE_LOG()); + $result->registerBlock("spruce_planks", fn() => VanillaBlocks::SPRUCE_PLANKS()); + $result->registerBlock("spruce_pressure_plate", fn() => VanillaBlocks::SPRUCE_PRESSURE_PLATE()); + $result->registerBlock("spruce_sapling", fn() => VanillaBlocks::SPRUCE_SAPLING()); + $result->registerBlock("spruce_sign", fn() => VanillaBlocks::SPRUCE_SIGN()); + $result->registerBlock("spruce_slab", fn() => VanillaBlocks::SPRUCE_SLAB()); + $result->registerBlock("spruce_stairs", fn() => VanillaBlocks::SPRUCE_STAIRS()); + $result->registerBlock("spruce_standing_sign", fn() => VanillaBlocks::SPRUCE_SIGN()); + $result->registerBlock("spruce_trapdoor", fn() => VanillaBlocks::SPRUCE_TRAPDOOR()); + $result->registerBlock("spruce_wall_sign", fn() => VanillaBlocks::SPRUCE_WALL_SIGN()); + $result->registerBlock("spruce_wood", fn() => VanillaBlocks::SPRUCE_WOOD()); + $result->registerBlock("spruce_wood_stairs", fn() => VanillaBlocks::SPRUCE_STAIRS()); + $result->registerBlock("spruce_wooden_stairs", fn() => VanillaBlocks::SPRUCE_STAIRS()); + $result->registerBlock("stained_clay", fn() => VanillaBlocks::STAINED_CLAY()); + $result->registerBlock("stained_glass", fn() => VanillaBlocks::STAINED_GLASS()); + $result->registerBlock("stained_glass_pane", fn() => VanillaBlocks::STAINED_GLASS_PANE()); + $result->registerBlock("stained_hardened_clay", fn() => VanillaBlocks::STAINED_CLAY()); + $result->registerBlock("stained_hardened_glass", fn() => VanillaBlocks::STAINED_HARDENED_GLASS()); + $result->registerBlock("stained_hardened_glass_pane", fn() => VanillaBlocks::STAINED_HARDENED_GLASS_PANE()); + $result->registerBlock("standing_banner", fn() => VanillaBlocks::BANNER()); + $result->registerBlock("standing_sign", fn() => VanillaBlocks::OAK_SIGN()); + $result->registerBlock("still_lava", fn() => VanillaBlocks::LAVA()->setStill(true)); + $result->registerBlock("still_water", fn() => VanillaBlocks::WATER()->setStill(true)); + $result->registerBlock("stone", fn() => VanillaBlocks::STONE()); + $result->registerBlock("stone_brick", fn() => VanillaBlocks::STONE_BRICKS()); + $result->registerBlock("stone_brick_slab", fn() => VanillaBlocks::STONE_BRICK_SLAB()); + $result->registerBlock("stone_brick_stairs", fn() => VanillaBlocks::STONE_BRICK_STAIRS()); + $result->registerBlock("stone_brick_wall", fn() => VanillaBlocks::STONE_BRICK_WALL()); + $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_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_wall", fn() => VanillaBlocks::COBBLESTONE_WALL()); + $result->registerBlock("stonebrick", fn() => VanillaBlocks::STONE_BRICKS()); + $result->registerBlock("stonecutter", fn() => VanillaBlocks::LEGACY_STONECUTTER()); + $result->registerBlock("stripped_acacia_log", fn() => VanillaBlocks::STRIPPED_ACACIA_LOG()); + $result->registerBlock("stripped_acacia_wood", fn() => VanillaBlocks::STRIPPED_ACACIA_WOOD()); + $result->registerBlock("stripped_birch_log", fn() => VanillaBlocks::STRIPPED_BIRCH_LOG()); + $result->registerBlock("stripped_birch_wood", fn() => VanillaBlocks::STRIPPED_BIRCH_WOOD()); + $result->registerBlock("stripped_dark_oak_log", fn() => VanillaBlocks::STRIPPED_DARK_OAK_LOG()); + $result->registerBlock("stripped_dark_oak_wood", fn() => VanillaBlocks::STRIPPED_DARK_OAK_WOOD()); + $result->registerBlock("stripped_jungle_log", fn() => VanillaBlocks::STRIPPED_JUNGLE_LOG()); + $result->registerBlock("stripped_jungle_wood", fn() => VanillaBlocks::STRIPPED_JUNGLE_WOOD()); + $result->registerBlock("stripped_oak_log", fn() => VanillaBlocks::STRIPPED_OAK_LOG()); + $result->registerBlock("stripped_oak_wood", fn() => VanillaBlocks::STRIPPED_OAK_WOOD()); + $result->registerBlock("stripped_spruce_log", fn() => VanillaBlocks::STRIPPED_SPRUCE_LOG()); + $result->registerBlock("stripped_spruce_wood", fn() => VanillaBlocks::STRIPPED_SPRUCE_WOOD()); + $result->registerBlock("sugar_cane", fn() => VanillaBlocks::SUGARCANE()); + $result->registerBlock("sugar_canes", fn() => VanillaBlocks::SUGARCANE()); + $result->registerBlock("sugarcane", fn() => VanillaBlocks::SUGARCANE()); + $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("tallgrass", fn() => VanillaBlocks::FERN()); + $result->registerBlock("terracotta", fn() => VanillaBlocks::STAINED_CLAY()); + $result->registerBlock("tnt", fn() => VanillaBlocks::TNT()); + $result->registerBlock("torch", fn() => VanillaBlocks::TORCH()); + $result->registerBlock("trapdoor", fn() => VanillaBlocks::OAK_TRAPDOOR()); + $result->registerBlock("trapped_chest", fn() => VanillaBlocks::TRAPPED_CHEST()); + $result->registerBlock("trip_wire", fn() => VanillaBlocks::TRIPWIRE()); + $result->registerBlock("tripwire", fn() => VanillaBlocks::TRIPWIRE()); + $result->registerBlock("tripwire_hook", fn() => VanillaBlocks::TRIPWIRE_HOOK()); + $result->registerBlock("trunk", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("trunk2", fn() => VanillaBlocks::ACACIA_LOG()); + $result->registerBlock("underwater_torch", fn() => VanillaBlocks::UNDERWATER_TORCH()); + $result->registerBlock("undyed_shulker_box", fn() => VanillaBlocks::SHULKER_BOX()); + $result->registerBlock("unlit_redstone_torch", fn() => VanillaBlocks::REDSTONE_TORCH()); + $result->registerBlock("unpowered_comparator", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("unpowered_comparator_block", fn() => VanillaBlocks::REDSTONE_COMPARATOR()); + $result->registerBlock("unpowered_repeater", fn() => VanillaBlocks::REDSTONE_REPEATER()); + $result->registerBlock("unpowered_repeater_block", fn() => VanillaBlocks::REDSTONE_REPEATER()); + $result->registerBlock("update_block", fn() => VanillaBlocks::INFO_UPDATE()); + $result->registerBlock("vine", fn() => VanillaBlocks::VINES()); + $result->registerBlock("vines", fn() => VanillaBlocks::VINES()); + $result->registerBlock("wall_banner", fn() => VanillaBlocks::WALL_BANNER()); + $result->registerBlock("wall_coral_fan", fn() => VanillaBlocks::WALL_CORAL_FAN()); + $result->registerBlock("wall_sign", fn() => VanillaBlocks::OAK_WALL_SIGN()); + $result->registerBlock("water", fn() => VanillaBlocks::WATER()); + $result->registerBlock("water_lily", fn() => VanillaBlocks::LILY_PAD()); + $result->registerBlock("waterlily", fn() => VanillaBlocks::LILY_PAD()); + $result->registerBlock("web", fn() => VanillaBlocks::COBWEB()); + $result->registerBlock("weighted_pressure_plate_heavy", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY()); + $result->registerBlock("weighted_pressure_plate_light", fn() => VanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT()); + $result->registerBlock("wheat_block", fn() => VanillaBlocks::WHEAT()); + $result->registerBlock("white_glazed_terracotta", fn() => VanillaBlocks::WHITE_GLAZED_TERRACOTTA()); + $result->registerBlock("white_tulip", fn() => VanillaBlocks::WHITE_TULIP()); + $result->registerBlock("wood", fn() => VanillaBlocks::OAK_LOG()); + $result->registerBlock("wood2", fn() => VanillaBlocks::ACACIA_LOG()); + $result->registerBlock("wood_door_block", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("wood_slab", fn() => VanillaBlocks::OAK_SLAB()); + $result->registerBlock("wood_slabs", fn() => VanillaBlocks::OAK_SLAB()); + $result->registerBlock("wood_stairs", fn() => VanillaBlocks::OAK_STAIRS()); + $result->registerBlock("wooden_button", fn() => VanillaBlocks::OAK_BUTTON()); + $result->registerBlock("wooden_door", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("wooden_door_block", fn() => VanillaBlocks::OAK_DOOR()); + $result->registerBlock("wooden_plank", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("wooden_planks", fn() => VanillaBlocks::OAK_PLANKS()); + $result->registerBlock("wooden_pressure_plate", fn() => VanillaBlocks::OAK_PRESSURE_PLATE()); + $result->registerBlock("wooden_slab", fn() => VanillaBlocks::OAK_SLAB()); + $result->registerBlock("wooden_slabs", fn() => VanillaBlocks::OAK_SLAB()); + $result->registerBlock("wooden_stairs", fn() => VanillaBlocks::OAK_STAIRS()); + $result->registerBlock("wooden_trapdoor", fn() => VanillaBlocks::OAK_TRAPDOOR()); + $result->registerBlock("wool", fn() => VanillaBlocks::WOOL()); + $result->registerBlock("workbench", fn() => VanillaBlocks::CRAFTING_TABLE()); + $result->registerBlock("yellow_flower", fn() => VanillaBlocks::DANDELION()); + $result->registerBlock("yellow_glazed_terracotta", fn() => VanillaBlocks::YELLOW_GLAZED_TERRACOTTA()); + + $result->register("acacia_boat", fn() => VanillaItems::ACACIA_BOAT()); + $result->register("apple", fn() => VanillaItems::APPLE()); + $result->register("apple_enchanted", fn() => VanillaItems::ENCHANTED_GOLDEN_APPLE()); + $result->register("appleenchanted", fn() => VanillaItems::ENCHANTED_GOLDEN_APPLE()); + $result->register("arrow", fn() => VanillaItems::ARROW()); + $result->register("awkward_potion", fn() => VanillaItems::AWKWARD_POTION()); + $result->register("awkward_splash_potion", fn() => VanillaItems::AWKWARD_SPLASH_POTION()); + $result->register("baked_potato", fn() => VanillaItems::BAKED_POTATO()); + $result->register("baked_potatoes", fn() => VanillaItems::BAKED_POTATO()); + $result->register("bed", fn() => VanillaItems::WHITE_BED()); + $result->register("beef", fn() => VanillaItems::RAW_BEEF()); + $result->register("beetroot", fn() => VanillaItems::BEETROOT()); + $result->register("beetroot_seed", fn() => VanillaItems::BEETROOT_SEEDS()); + $result->register("beetroot_seeds", fn() => VanillaItems::BEETROOT_SEEDS()); + $result->register("beetroot_soup", fn() => VanillaItems::BEETROOT_SOUP()); + $result->register("birch_boat", fn() => VanillaItems::BIRCH_BOAT()); + $result->register("black_dye", fn() => VanillaItems::BLACK_DYE()); + $result->register("blaze_powder", fn() => VanillaItems::BLAZE_POWDER()); + $result->register("blaze_rod", fn() => VanillaItems::BLAZE_ROD()); + $result->register("bleach", fn() => VanillaItems::BLEACH()); + $result->register("blue_dye", fn() => VanillaItems::BLUE_DYE()); + $result->register("boat", fn() => VanillaItems::OAK_BOAT()); + $result->register("bone", fn() => VanillaItems::BONE()); + $result->register("bone_meal", fn() => VanillaItems::BONE_MEAL()); + $result->register("book", fn() => VanillaItems::BOOK()); + $result->register("bottle_o_enchanting", fn() => VanillaItems::EXPERIENCE_BOTTLE()); + $result->register("bow", fn() => VanillaItems::BOW()); + $result->register("bowl", fn() => VanillaItems::BOWL()); + $result->register("bread", fn() => VanillaItems::BREAD()); + $result->register("brick", fn() => VanillaItems::BRICK()); + $result->register("brown_dye", fn() => VanillaItems::BROWN_DYE()); + $result->register("bucket", fn() => VanillaItems::BUCKET()); + $result->register("carrot", fn() => VanillaItems::CARROT()); + $result->register("chain_boots", fn() => VanillaItems::CHAINMAIL_BOOTS()); + $result->register("chain_chestplate", fn() => VanillaItems::CHAINMAIL_CHESTPLATE()); + $result->register("chain_helmet", fn() => VanillaItems::CHAINMAIL_HELMET()); + $result->register("chain_leggings", fn() => VanillaItems::CHAINMAIL_LEGGINGS()); + $result->register("chainmail_boots", fn() => VanillaItems::CHAINMAIL_BOOTS()); + $result->register("chainmail_chestplate", fn() => VanillaItems::CHAINMAIL_CHESTPLATE()); + $result->register("chainmail_helmet", fn() => VanillaItems::CHAINMAIL_HELMET()); + $result->register("chainmail_leggings", fn() => VanillaItems::CHAINMAIL_LEGGINGS()); + $result->register("charcoal", fn() => VanillaItems::CHARCOAL()); + $result->register("chemical_aluminium_oxide", fn() => VanillaItems::CHEMICAL_ALUMINIUM_OXIDE()); + $result->register("chemical_ammonia", fn() => VanillaItems::CHEMICAL_AMMONIA()); + $result->register("chemical_barium_sulphate", fn() => VanillaItems::CHEMICAL_BARIUM_SULPHATE()); + $result->register("chemical_benzene", fn() => VanillaItems::CHEMICAL_BENZENE()); + $result->register("chemical_boron_trioxide", fn() => VanillaItems::CHEMICAL_BORON_TRIOXIDE()); + $result->register("chemical_calcium_bromide", fn() => VanillaItems::CHEMICAL_CALCIUM_BROMIDE()); + $result->register("chemical_calcium_chloride", fn() => VanillaItems::CHEMICAL_CALCIUM_CHLORIDE()); + $result->register("chemical_cerium_chloride", fn() => VanillaItems::CHEMICAL_CERIUM_CHLORIDE()); + $result->register("chemical_charcoal", fn() => VanillaItems::CHEMICAL_CHARCOAL()); + $result->register("chemical_crude_oil", fn() => VanillaItems::CHEMICAL_CRUDE_OIL()); + $result->register("chemical_glue", fn() => VanillaItems::CHEMICAL_GLUE()); + $result->register("chemical_hydrogen_peroxide", fn() => VanillaItems::CHEMICAL_HYDROGEN_PEROXIDE()); + $result->register("chemical_hypochlorite", fn() => VanillaItems::CHEMICAL_HYPOCHLORITE()); + $result->register("chemical_ink", fn() => VanillaItems::CHEMICAL_INK()); + $result->register("chemical_iron_sulphide", fn() => VanillaItems::CHEMICAL_IRON_SULPHIDE()); + $result->register("chemical_latex", fn() => VanillaItems::CHEMICAL_LATEX()); + $result->register("chemical_lithium_hydride", fn() => VanillaItems::CHEMICAL_LITHIUM_HYDRIDE()); + $result->register("chemical_luminol", fn() => VanillaItems::CHEMICAL_LUMINOL()); + $result->register("chemical_magnesium_nitrate", fn() => VanillaItems::CHEMICAL_MAGNESIUM_NITRATE()); + $result->register("chemical_magnesium_oxide", fn() => VanillaItems::CHEMICAL_MAGNESIUM_OXIDE()); + $result->register("chemical_magnesium_salts", fn() => VanillaItems::CHEMICAL_MAGNESIUM_SALTS()); + $result->register("chemical_mercuric_chloride", fn() => VanillaItems::CHEMICAL_MERCURIC_CHLORIDE()); + $result->register("chemical_polyethylene", fn() => VanillaItems::CHEMICAL_POLYETHYLENE()); + $result->register("chemical_potassium_chloride", fn() => VanillaItems::CHEMICAL_POTASSIUM_CHLORIDE()); + $result->register("chemical_potassium_iodide", fn() => VanillaItems::CHEMICAL_POTASSIUM_IODIDE()); + $result->register("chemical_rubbish", fn() => VanillaItems::CHEMICAL_RUBBISH()); + $result->register("chemical_salt", fn() => VanillaItems::CHEMICAL_SALT()); + $result->register("chemical_soap", fn() => VanillaItems::CHEMICAL_SOAP()); + $result->register("chemical_sodium_acetate", fn() => VanillaItems::CHEMICAL_SODIUM_ACETATE()); + $result->register("chemical_sodium_fluoride", fn() => VanillaItems::CHEMICAL_SODIUM_FLUORIDE()); + $result->register("chemical_sodium_hydride", fn() => VanillaItems::CHEMICAL_SODIUM_HYDRIDE()); + $result->register("chemical_sodium_hydroxide", fn() => VanillaItems::CHEMICAL_SODIUM_HYDROXIDE()); + $result->register("chemical_sodium_hypochlorite", fn() => VanillaItems::CHEMICAL_SODIUM_HYPOCHLORITE()); + $result->register("chemical_sodium_oxide", fn() => VanillaItems::CHEMICAL_SODIUM_OXIDE()); + $result->register("chemical_sugar", fn() => VanillaItems::CHEMICAL_SUGAR()); + $result->register("chemical_sulphate", fn() => VanillaItems::CHEMICAL_SULPHATE()); + $result->register("chemical_tungsten_chloride", fn() => VanillaItems::CHEMICAL_TUNGSTEN_CHLORIDE()); + $result->register("chemical_water", fn() => VanillaItems::CHEMICAL_WATER()); + $result->register("chicken", fn() => VanillaItems::RAW_CHICKEN()); + $result->register("chorus_fruit", fn() => VanillaItems::CHORUS_FRUIT()); + $result->register("chorus_fruit_popped", fn() => VanillaItems::POPPED_CHORUS_FRUIT()); + $result->register("clay", fn() => VanillaItems::CLAY()); + $result->register("clay_ball", fn() => VanillaItems::CLAY()); + $result->register("clock", fn() => VanillaItems::CLOCK()); + $result->register("clown_fish", fn() => VanillaItems::CLOWNFISH()); + $result->register("clownfish", fn() => VanillaItems::CLOWNFISH()); + $result->register("coal", fn() => VanillaItems::COAL()); + $result->register("cocoa_beans", fn() => VanillaItems::COCOA_BEANS()); + $result->register("compass", fn() => VanillaItems::COMPASS()); + $result->register("compound", fn() => VanillaItems::CHEMICAL_SALT()); + $result->register("cooked_beef", fn() => VanillaItems::STEAK()); + $result->register("cooked_chicken", fn() => VanillaItems::COOKED_CHICKEN()); + $result->register("cooked_fish", fn() => VanillaItems::COOKED_FISH()); + $result->register("cooked_mutton", fn() => VanillaItems::COOKED_MUTTON()); + $result->register("cooked_porkchop", fn() => VanillaItems::COOKED_PORKCHOP()); + $result->register("cooked_rabbit", fn() => VanillaItems::COOKED_RABBIT()); + $result->register("cooked_salmon", fn() => VanillaItems::COOKED_SALMON()); + $result->register("cookie", fn() => VanillaItems::COOKIE()); + $result->register("creeper_head", fn() => VanillaItems::CREEPER_HEAD()); + $result->register("cyan_dye", fn() => VanillaItems::CYAN_DYE()); + $result->register("dark_oak_boat", fn() => VanillaItems::DARK_OAK_BOAT()); + $result->register("diamond", fn() => VanillaItems::DIAMOND()); + $result->register("diamond_axe", fn() => VanillaItems::DIAMOND_AXE()); + $result->register("diamond_boots", fn() => VanillaItems::DIAMOND_BOOTS()); + $result->register("diamond_chestplate", fn() => VanillaItems::DIAMOND_CHESTPLATE()); + $result->register("diamond_helmet", fn() => VanillaItems::DIAMOND_HELMET()); + $result->register("diamond_hoe", fn() => VanillaItems::DIAMOND_HOE()); + $result->register("diamond_leggings", fn() => VanillaItems::DIAMOND_LEGGINGS()); + $result->register("diamond_pickaxe", fn() => VanillaItems::DIAMOND_PICKAXE()); + $result->register("diamond_shovel", fn() => VanillaItems::DIAMOND_SHOVEL()); + $result->register("diamond_sword", fn() => VanillaItems::DIAMOND_SWORD()); + $result->register("dragon_breath", fn() => VanillaItems::DRAGON_BREATH()); + $result->register("dragon_head", fn() => VanillaItems::DRAGON_HEAD()); + $result->register("dried_kelp", fn() => VanillaItems::DRIED_KELP()); + $result->register("dye", fn() => VanillaItems::INK_SAC()); + $result->register("egg", fn() => VanillaItems::EGG()); + $result->register("emerald", fn() => VanillaItems::EMERALD()); + $result->register("enchanted_golden_apple", fn() => VanillaItems::ENCHANTED_GOLDEN_APPLE()); + $result->register("enchanting_bottle", fn() => VanillaItems::EXPERIENCE_BOTTLE()); + $result->register("ender_pearl", fn() => VanillaItems::ENDER_PEARL()); + $result->register("experience_bottle", fn() => VanillaItems::EXPERIENCE_BOTTLE()); + $result->register("feather", fn() => VanillaItems::FEATHER()); + $result->register("fermented_spider_eye", fn() => VanillaItems::FERMENTED_SPIDER_EYE()); + $result->register("fire_resistance_potion", fn() => VanillaItems::FIRE_RESISTANCE_POTION()); + $result->register("fire_resistance_splash_potion", fn() => VanillaItems::FIRE_RESISTANCE_SPLASH_POTION()); + $result->register("fish", fn() => VanillaItems::RAW_FISH()); + $result->register("fishing_rod", fn() => VanillaItems::FISHING_ROD()); + $result->register("flint", fn() => VanillaItems::FLINT()); + $result->register("flint_and_steel", fn() => VanillaItems::FLINT_AND_STEEL()); + $result->register("flint_steel", fn() => VanillaItems::FLINT_AND_STEEL()); + $result->register("ghast_tear", fn() => VanillaItems::GHAST_TEAR()); + $result->register("glass_bottle", fn() => VanillaItems::GLASS_BOTTLE()); + $result->register("glistering_melon", fn() => VanillaItems::GLISTERING_MELON()); + $result->register("glowstone_dust", fn() => VanillaItems::GLOWSTONE_DUST()); + $result->register("gold_axe", fn() => VanillaItems::GOLDEN_AXE()); + $result->register("gold_boots", fn() => VanillaItems::GOLDEN_BOOTS()); + $result->register("gold_chestplate", fn() => VanillaItems::GOLDEN_CHESTPLATE()); + $result->register("gold_helmet", fn() => VanillaItems::GOLDEN_HELMET()); + $result->register("gold_hoe", fn() => VanillaItems::GOLDEN_HOE()); + $result->register("gold_ingot", fn() => VanillaItems::GOLD_INGOT()); + $result->register("gold_leggings", fn() => VanillaItems::GOLDEN_LEGGINGS()); + $result->register("gold_nugget", fn() => VanillaItems::GOLD_NUGGET()); + $result->register("gold_pickaxe", fn() => VanillaItems::GOLDEN_PICKAXE()); + $result->register("gold_shovel", fn() => VanillaItems::GOLDEN_SHOVEL()); + $result->register("gold_sword", fn() => VanillaItems::GOLDEN_SWORD()); + $result->register("golden_apple", fn() => VanillaItems::GOLDEN_APPLE()); + $result->register("golden_axe", fn() => VanillaItems::GOLDEN_AXE()); + $result->register("golden_boots", fn() => VanillaItems::GOLDEN_BOOTS()); + $result->register("golden_carrot", fn() => VanillaItems::GOLDEN_CARROT()); + $result->register("golden_chestplate", fn() => VanillaItems::GOLDEN_CHESTPLATE()); + $result->register("golden_helmet", fn() => VanillaItems::GOLDEN_HELMET()); + $result->register("golden_hoe", fn() => VanillaItems::GOLDEN_HOE()); + $result->register("golden_leggings", fn() => VanillaItems::GOLDEN_LEGGINGS()); + $result->register("golden_nugget", fn() => VanillaItems::GOLD_NUGGET()); + $result->register("golden_pickaxe", fn() => VanillaItems::GOLDEN_PICKAXE()); + $result->register("golden_shovel", fn() => VanillaItems::GOLDEN_SHOVEL()); + $result->register("golden_sword", fn() => VanillaItems::GOLDEN_SWORD()); + $result->register("gray_dye", fn() => VanillaItems::GRAY_DYE()); + $result->register("green_dye", fn() => VanillaItems::GREEN_DYE()); + $result->register("gunpowder", fn() => VanillaItems::GUNPOWDER()); + $result->register("harming_potion", fn() => VanillaItems::HARMING_POTION()); + $result->register("harming_splash_potion", fn() => VanillaItems::HARMING_SPLASH_POTION()); + $result->register("healing_potion", fn() => VanillaItems::HEALING_POTION()); + $result->register("healing_splash_potion", fn() => VanillaItems::HEALING_SPLASH_POTION()); + $result->register("heart_of_the_sea", fn() => VanillaItems::HEART_OF_THE_SEA()); + $result->register("ink_sac", fn() => VanillaItems::INK_SAC()); + $result->register("invisibility_potion", fn() => VanillaItems::INVISIBILITY_POTION()); + $result->register("invisibility_splash_potion", fn() => VanillaItems::INVISIBILITY_SPLASH_POTION()); + $result->register("iron_axe", fn() => VanillaItems::IRON_AXE()); + $result->register("iron_boots", fn() => VanillaItems::IRON_BOOTS()); + $result->register("iron_chestplate", fn() => VanillaItems::IRON_CHESTPLATE()); + $result->register("iron_helmet", fn() => VanillaItems::IRON_HELMET()); + $result->register("iron_hoe", fn() => VanillaItems::IRON_HOE()); + $result->register("iron_ingot", fn() => VanillaItems::IRON_INGOT()); + $result->register("iron_leggings", fn() => VanillaItems::IRON_LEGGINGS()); + $result->register("iron_nugget", fn() => VanillaItems::IRON_NUGGET()); + $result->register("iron_pickaxe", fn() => VanillaItems::IRON_PICKAXE()); + $result->register("iron_shovel", fn() => VanillaItems::IRON_SHOVEL()); + $result->register("iron_sword", fn() => VanillaItems::IRON_SWORD()); + $result->register("jungle_boat", fn() => VanillaItems::JUNGLE_BOAT()); + $result->register("lapis_lazuli", fn() => VanillaItems::LAPIS_LAZULI()); + $result->register("lava_bucket", fn() => VanillaItems::LAVA_BUCKET()); + $result->register("leaping_potion", fn() => VanillaItems::LEAPING_POTION()); + $result->register("leaping_splash_potion", fn() => VanillaItems::LEAPING_SPLASH_POTION()); + $result->register("leather", fn() => VanillaItems::LEATHER()); + $result->register("leather_boots", fn() => VanillaItems::LEATHER_BOOTS()); + $result->register("leather_cap", fn() => VanillaItems::LEATHER_CAP()); + $result->register("leather_chestplate", fn() => VanillaItems::LEATHER_TUNIC()); + $result->register("leather_helmet", fn() => VanillaItems::LEATHER_CAP()); + $result->register("leather_leggings", fn() => VanillaItems::LEATHER_PANTS()); + $result->register("leather_pants", fn() => VanillaItems::LEATHER_PANTS()); + $result->register("leather_tunic", fn() => VanillaItems::LEATHER_TUNIC()); + $result->register("light_blue_dye", fn() => VanillaItems::LIGHT_BLUE_DYE()); + $result->register("light_gray_dye", fn() => VanillaItems::LIGHT_GRAY_DYE()); + $result->register("lime_dye", fn() => VanillaItems::LIME_DYE()); + $result->register("long_fire_resistance_potion", fn() => VanillaItems::LONG_FIRE_RESISTANCE_POTION()); + $result->register("long_fire_resistance_splash_potion", fn() => VanillaItems::LONG_FIRE_RESISTANCE_SPLASH_POTION()); + $result->register("long_invisibility_potion", fn() => VanillaItems::LONG_INVISIBILITY_POTION()); + $result->register("long_invisibility_splash_potion", fn() => VanillaItems::LONG_INVISIBILITY_SPLASH_POTION()); + $result->register("long_leaping_potion", fn() => VanillaItems::LONG_LEAPING_POTION()); + $result->register("long_leaping_splash_potion", fn() => VanillaItems::LONG_LEAPING_SPLASH_POTION()); + $result->register("long_mundane_potion", fn() => VanillaItems::LONG_MUNDANE_POTION()); + $result->register("long_mundane_splash_potion", fn() => VanillaItems::LONG_MUNDANE_SPLASH_POTION()); + $result->register("long_night_vision_potion", fn() => VanillaItems::LONG_NIGHT_VISION_POTION()); + $result->register("long_night_vision_splash_potion", fn() => VanillaItems::LONG_NIGHT_VISION_SPLASH_POTION()); + $result->register("long_poison_potion", fn() => VanillaItems::LONG_POISON_POTION()); + $result->register("long_poison_splash_potion", fn() => VanillaItems::LONG_POISON_SPLASH_POTION()); + $result->register("long_regeneration_potion", fn() => VanillaItems::LONG_REGENERATION_POTION()); + $result->register("long_regeneration_splash_potion", fn() => VanillaItems::LONG_REGENERATION_SPLASH_POTION()); + $result->register("long_slow_falling_potion", fn() => VanillaItems::LONG_SLOW_FALLING_POTION()); + $result->register("long_slow_falling_splash_potion", fn() => VanillaItems::LONG_SLOW_FALLING_SPLASH_POTION()); + $result->register("long_slowness_potion", fn() => VanillaItems::LONG_SLOWNESS_POTION()); + $result->register("long_slowness_splash_potion", fn() => VanillaItems::LONG_SLOWNESS_SPLASH_POTION()); + $result->register("long_strength_potion", fn() => VanillaItems::LONG_STRENGTH_POTION()); + $result->register("long_strength_splash_potion", fn() => VanillaItems::LONG_STRENGTH_SPLASH_POTION()); + $result->register("long_swiftness_potion", fn() => VanillaItems::LONG_SWIFTNESS_POTION()); + $result->register("long_swiftness_splash_potion", fn() => VanillaItems::LONG_SWIFTNESS_SPLASH_POTION()); + $result->register("long_turtle_master_potion", fn() => VanillaItems::LONG_TURTLE_MASTER_POTION()); + $result->register("long_turtle_master_splash_potion", fn() => VanillaItems::LONG_TURTLE_MASTER_SPLASH_POTION()); + $result->register("long_water_breathing_potion", fn() => VanillaItems::LONG_WATER_BREATHING_POTION()); + $result->register("long_water_breathing_splash_potion", fn() => VanillaItems::LONG_WATER_BREATHING_SPLASH_POTION()); + $result->register("long_weakness_potion", fn() => VanillaItems::LONG_WEAKNESS_POTION()); + $result->register("long_weakness_splash_potion", fn() => VanillaItems::LONG_WEAKNESS_SPLASH_POTION()); + $result->register("magenta_dye", fn() => VanillaItems::MAGENTA_DYE()); + $result->register("magma_cream", fn() => VanillaItems::MAGMA_CREAM()); + $result->register("melon", fn() => VanillaItems::MELON()); + $result->register("melon_seeds", fn() => VanillaItems::MELON_SEEDS()); + $result->register("melon_slice", fn() => VanillaItems::MELON()); + $result->register("milk_bucket", fn() => VanillaItems::MILK_BUCKET()); + $result->register("minecart", fn() => VanillaItems::MINECART()); + $result->register("mob_head", fn() => VanillaItems::SKELETON_SKULL()); + $result->register("mundane_potion", fn() => VanillaItems::MUNDANE_POTION()); + $result->register("mundane_splash_potion", fn() => VanillaItems::MUNDANE_SPLASH_POTION()); + $result->register("mushroom_stew", fn() => VanillaItems::MUSHROOM_STEW()); + $result->register("mutton", fn() => VanillaItems::RAW_MUTTON()); + $result->register("mutton_cooked", fn() => VanillaItems::COOKED_MUTTON()); + $result->register("mutton_raw", fn() => VanillaItems::RAW_MUTTON()); + $result->register("muttoncooked", fn() => VanillaItems::COOKED_MUTTON()); + $result->register("muttonraw", fn() => VanillaItems::RAW_MUTTON()); + $result->register("nautilus_shell", fn() => VanillaItems::NAUTILUS_SHELL()); + $result->register("nether_brick", fn() => VanillaItems::NETHER_BRICK()); + $result->register("nether_quartz", fn() => VanillaItems::NETHER_QUARTZ()); + $result->register("nether_star", fn() => VanillaItems::NETHER_STAR()); + $result->register("netherbrick", fn() => VanillaItems::NETHER_BRICK()); + $result->register("netherstar", fn() => VanillaItems::NETHER_STAR()); + $result->register("night_vision_potion", fn() => VanillaItems::NIGHT_VISION_POTION()); + $result->register("night_vision_splash_potion", fn() => VanillaItems::NIGHT_VISION_SPLASH_POTION()); + $result->register("oak_boat", fn() => VanillaItems::OAK_BOAT()); + $result->register("orange_dye", fn() => VanillaItems::ORANGE_DYE()); + $result->register("painting", fn() => VanillaItems::PAINTING()); + $result->register("paper", fn() => VanillaItems::PAPER()); + $result->register("pink_dye", fn() => VanillaItems::PINK_DYE()); + $result->register("player_head", fn() => VanillaItems::PLAYER_HEAD()); + $result->register("poison_potion", fn() => VanillaItems::POISON_POTION()); + $result->register("poison_splash_potion", fn() => VanillaItems::POISON_SPLASH_POTION()); + $result->register("poisonous_potato", fn() => VanillaItems::POISONOUS_POTATO()); + $result->register("popped_chorus_fruit", fn() => VanillaItems::POPPED_CHORUS_FRUIT()); + $result->register("porkchop", fn() => VanillaItems::RAW_PORKCHOP()); + $result->register("potato", fn() => VanillaItems::POTATO()); + $result->register("potion", fn() => VanillaItems::WATER_POTION()); + $result->register("prismarine_crystals", fn() => VanillaItems::PRISMARINE_CRYSTALS()); + $result->register("prismarine_shard", fn() => VanillaItems::PRISMARINE_SHARD()); + $result->register("puffer_fish", fn() => VanillaItems::PUFFERFISH()); + $result->register("pufferfish", fn() => VanillaItems::PUFFERFISH()); + $result->register("pumpkin_pie", fn() => VanillaItems::PUMPKIN_PIE()); + $result->register("pumpkin_seeds", fn() => VanillaItems::PUMPKIN_SEEDS()); + $result->register("purple_dye", fn() => VanillaItems::PURPLE_DYE()); + $result->register("quartz", fn() => VanillaItems::NETHER_QUARTZ()); + $result->register("rabbit", fn() => VanillaItems::RAW_RABBIT()); + $result->register("rabbit_foot", fn() => VanillaItems::RABBIT_FOOT()); + $result->register("rabbit_hide", fn() => VanillaItems::RABBIT_HIDE()); + $result->register("rabbit_stew", fn() => VanillaItems::RABBIT_STEW()); + $result->register("raw_beef", fn() => VanillaItems::RAW_BEEF()); + $result->register("raw_chicken", fn() => VanillaItems::RAW_CHICKEN()); + $result->register("raw_fish", fn() => VanillaItems::RAW_FISH()); + $result->register("raw_mutton", fn() => VanillaItems::RAW_MUTTON()); + $result->register("raw_porkchop", fn() => VanillaItems::RAW_PORKCHOP()); + $result->register("raw_rabbit", fn() => VanillaItems::RAW_RABBIT()); + $result->register("raw_salmon", fn() => VanillaItems::RAW_SALMON()); + $result->register("record_11", fn() => VanillaItems::RECORD_11()); + $result->register("record_13", fn() => VanillaItems::RECORD_13()); + $result->register("record_blocks", fn() => VanillaItems::RECORD_BLOCKS()); + $result->register("record_cat", fn() => VanillaItems::RECORD_CAT()); + $result->register("record_chirp", fn() => VanillaItems::RECORD_CHIRP()); + $result->register("record_far", fn() => VanillaItems::RECORD_FAR()); + $result->register("record_mall", fn() => VanillaItems::RECORD_MALL()); + $result->register("record_mellohi", fn() => VanillaItems::RECORD_MELLOHI()); + $result->register("record_stal", fn() => VanillaItems::RECORD_STAL()); + $result->register("record_strad", fn() => VanillaItems::RECORD_STRAD()); + $result->register("record_wait", fn() => VanillaItems::RECORD_WAIT()); + $result->register("record_ward", fn() => VanillaItems::RECORD_WARD()); + $result->register("red_dye", fn() => VanillaItems::RED_DYE()); + $result->register("redstone", fn() => VanillaItems::REDSTONE_DUST()); + $result->register("redstone_dust", fn() => VanillaItems::REDSTONE_DUST()); + $result->register("regeneration_potion", fn() => VanillaItems::REGENERATION_POTION()); + $result->register("regeneration_splash_potion", fn() => VanillaItems::REGENERATION_SPLASH_POTION()); + $result->register("rotten_flesh", fn() => VanillaItems::ROTTEN_FLESH()); + $result->register("salmon", fn() => VanillaItems::RAW_SALMON()); + $result->register("scute", fn() => VanillaItems::SCUTE()); + $result->register("seeds", fn() => VanillaItems::WHEAT_SEEDS()); + $result->register("shears", fn() => VanillaItems::SHEARS()); + $result->register("shulker_shell", fn() => VanillaItems::SHULKER_SHELL()); + $result->register("skeleton_skull", fn() => VanillaItems::SKELETON_SKULL()); + $result->register("skull", fn() => VanillaItems::SKELETON_SKULL()); + $result->register("slime_ball", fn() => VanillaItems::SLIMEBALL()); + $result->register("slimeball", fn() => VanillaItems::SLIMEBALL()); + $result->register("slow_falling_potion", fn() => VanillaItems::SLOW_FALLING_POTION()); + $result->register("slow_falling_splash_potion", fn() => VanillaItems::SLOW_FALLING_SPLASH_POTION()); + $result->register("slowness_potion", fn() => VanillaItems::SLOWNESS_POTION()); + $result->register("slowness_splash_potion", fn() => VanillaItems::SLOWNESS_SPLASH_POTION()); + $result->register("snowball", fn() => VanillaItems::SNOWBALL()); + $result->register("speckled_melon", fn() => VanillaItems::GLISTERING_MELON()); + $result->register("spider_eye", fn() => VanillaItems::SPIDER_EYE()); + $result->register("splash_potion", fn() => VanillaItems::WATER_SPLASH_POTION()); + $result->register("spruce_boat", fn() => VanillaItems::SPRUCE_BOAT()); + $result->register("squid_spawn_egg", fn() => VanillaItems::SQUID_SPAWN_EGG()); + $result->register("steak", fn() => VanillaItems::STEAK()); + $result->register("stick", fn() => VanillaItems::STICK()); + $result->register("sticks", fn() => VanillaItems::STICK()); + $result->register("stone_axe", fn() => VanillaItems::STONE_AXE()); + $result->register("stone_hoe", fn() => VanillaItems::STONE_HOE()); + $result->register("stone_pickaxe", fn() => VanillaItems::STONE_PICKAXE()); + $result->register("stone_shovel", fn() => VanillaItems::STONE_SHOVEL()); + $result->register("stone_sword", fn() => VanillaItems::STONE_SWORD()); + $result->register("strength_potion", fn() => VanillaItems::STRENGTH_POTION()); + $result->register("strength_splash_potion", fn() => VanillaItems::STRENGTH_SPLASH_POTION()); + $result->register("string", fn() => VanillaItems::STRING()); + $result->register("strong_harming_potion", fn() => VanillaItems::STRONG_HARMING_POTION()); + $result->register("strong_harming_splash_potion", fn() => VanillaItems::STRONG_HARMING_SPLASH_POTION()); + $result->register("strong_healing_potion", fn() => VanillaItems::STRONG_HEALING_POTION()); + $result->register("strong_healing_splash_potion", fn() => VanillaItems::STRONG_HEALING_SPLASH_POTION()); + $result->register("strong_leaping_potion", fn() => VanillaItems::STRONG_LEAPING_POTION()); + $result->register("strong_leaping_splash_potion", fn() => VanillaItems::STRONG_LEAPING_SPLASH_POTION()); + $result->register("strong_poison_potion", fn() => VanillaItems::STRONG_POISON_POTION()); + $result->register("strong_poison_splash_potion", fn() => VanillaItems::STRONG_POISON_SPLASH_POTION()); + $result->register("strong_regeneration_potion", fn() => VanillaItems::STRONG_REGENERATION_POTION()); + $result->register("strong_regeneration_splash_potion", fn() => VanillaItems::STRONG_REGENERATION_SPLASH_POTION()); + $result->register("strong_strength_potion", fn() => VanillaItems::STRONG_STRENGTH_POTION()); + $result->register("strong_strength_splash_potion", fn() => VanillaItems::STRONG_STRENGTH_SPLASH_POTION()); + $result->register("strong_swiftness_potion", fn() => VanillaItems::STRONG_SWIFTNESS_POTION()); + $result->register("strong_swiftness_splash_potion", fn() => VanillaItems::STRONG_SWIFTNESS_SPLASH_POTION()); + $result->register("strong_turtle_master_potion", fn() => VanillaItems::STRONG_TURTLE_MASTER_POTION()); + $result->register("strong_turtle_master_splash_potion", fn() => VanillaItems::STRONG_TURTLE_MASTER_SPLASH_POTION()); + $result->register("sugar", fn() => VanillaItems::SUGAR()); + $result->register("sweet_berries", fn() => VanillaItems::SWEET_BERRIES()); + $result->register("swiftness_potion", fn() => VanillaItems::SWIFTNESS_POTION()); + $result->register("swiftness_splash_potion", fn() => VanillaItems::SWIFTNESS_SPLASH_POTION()); + $result->register("thick_potion", fn() => VanillaItems::THICK_POTION()); + $result->register("thick_splash_potion", fn() => VanillaItems::THICK_SPLASH_POTION()); + $result->register("totem", fn() => VanillaItems::TOTEM()); + $result->register("turtle_master_potion", fn() => VanillaItems::TURTLE_MASTER_POTION()); + $result->register("turtle_master_splash_potion", fn() => VanillaItems::TURTLE_MASTER_SPLASH_POTION()); + $result->register("turtle_shell_piece", fn() => VanillaItems::SCUTE()); + $result->register("villager_spawn_egg", fn() => VanillaItems::VILLAGER_SPAWN_EGG()); + $result->register("water_breathing_potion", fn() => VanillaItems::WATER_BREATHING_POTION()); + $result->register("water_breathing_splash_potion", fn() => VanillaItems::WATER_BREATHING_SPLASH_POTION()); + $result->register("water_bucket", fn() => VanillaItems::WATER_BUCKET()); + $result->register("water_potion", fn() => VanillaItems::WATER_POTION()); + $result->register("water_splash_potion", fn() => VanillaItems::WATER_SPLASH_POTION()); + $result->register("weakness_potion", fn() => VanillaItems::WEAKNESS_POTION()); + $result->register("weakness_splash_potion", fn() => VanillaItems::WEAKNESS_SPLASH_POTION()); + $result->register("wheat", fn() => VanillaItems::WHEAT()); + $result->register("wheat_seeds", fn() => VanillaItems::WHEAT_SEEDS()); + $result->register("white_dye", fn() => VanillaItems::WHITE_DYE()); + $result->register("wither_potion", fn() => VanillaItems::WITHER_POTION()); + $result->register("wither_skeleton_skull", fn() => VanillaItems::WITHER_SKELETON_SKULL()); + $result->register("wither_splash_potion", fn() => VanillaItems::WITHER_SPLASH_POTION()); + $result->register("wooden_axe", fn() => VanillaItems::WOODEN_AXE()); + $result->register("wooden_hoe", fn() => VanillaItems::WOODEN_HOE()); + $result->register("wooden_pickaxe", fn() => VanillaItems::WOODEN_PICKAXE()); + $result->register("wooden_shovel", fn() => VanillaItems::WOODEN_SHOVEL()); + $result->register("wooden_sword", fn() => VanillaItems::WOODEN_SWORD()); + $result->register("writable_book", fn() => VanillaItems::WRITABLE_BOOK()); + $result->register("written_book", fn() => VanillaItems::WRITTEN_BOOK()); + $result->register("yellow_dye", fn() => VanillaItems::YELLOW_DYE()); + $result->register("zombie_head", fn() => VanillaItems::ZOMBIE_HEAD()); + $result->register("zombie_spawn_egg", fn() => VanillaItems::ZOMBIE_SPAWN_EGG()); + + return $result; + } + + /** @phpstan-param \Closure(string $input) : Block $callback */ + public function registerBlock(string $alias, \Closure $callback) : void{ + $this->register($alias, fn(string $input) => $callback($input)->asItem()); + } + + public function parse(string $input) : ?Item{ + return parent::parse($input); + } +} diff --git a/src/item/SweetBerries.php b/src/item/SweetBerries.php new file mode 100644 index 0000000000..6e2d1ee4ef --- /dev/null +++ b/src/item/SweetBerries.php @@ -0,0 +1,42 @@ +tier); + return $this->tier->getBaseAttackPoints(); } public function getBlockToolHarvestLevel() : int{ return 1; } - public function getMiningEfficiency(Block $block) : float{ - return parent::getMiningEfficiency($block) * 1.5; //swords break any block 1.5x faster than hand + public function getMiningEfficiency(bool $isCorrectTool) : float{ + return parent::getMiningEfficiency($isCorrectTool) * 1.5; //swords break any block 1.5x faster than hand } protected function getBaseMiningEfficiency() : float{ @@ -50,7 +50,7 @@ class Sword extends TieredTool{ } public function onDestroyBlock(Block $block) : bool{ - if($block->getHardness() > 0){ + if(!$block->getBreakInfo()->breaksInstantly()){ return $this->applyDamage(2); } return false; diff --git a/src/pocketmine/item/ChainBoots.php b/src/item/TieredTool.php similarity index 59% rename from src/pocketmine/item/ChainBoots.php rename to src/item/TieredTool.php index 8b39921050..cf4647481f 100644 --- a/src/pocketmine/item/ChainBoots.php +++ b/src/item/TieredTool.php @@ -23,16 +23,33 @@ declare(strict_types=1); namespace pocketmine\item; -class ChainBoots extends Armor{ - public function __construct(int $meta = 0){ - parent::__construct(self::CHAIN_BOOTS, $meta, "Chainmail Boots"); - } +abstract class TieredTool extends Tool{ - public function getDefensePoints() : int{ - return 1; + /** @var ToolTier */ + protected $tier; + + public function __construct(ItemIdentifier $identifier, string $name, ToolTier $tier){ + parent::__construct($identifier, $name); + $this->tier = $tier; } public function getMaxDurability() : int{ - return 196; + return $this->tier->getMaxDurability(); + } + + public function getTier() : ToolTier{ + return $this->tier; + } + + protected function getBaseMiningEfficiency() : float{ + return $this->tier->getBaseEfficiency(); + } + + public function getFuelTime() : int{ + if($this->tier->equals(ToolTier::WOOD())){ + return 200; + } + + return 0; } } diff --git a/src/pocketmine/item/Tool.php b/src/item/Tool.php similarity index 79% rename from src/pocketmine/item/Tool.php rename to src/item/Tool.php index b0f3b1be0c..d6cc1a061d 100644 --- a/src/pocketmine/item/Tool.php +++ b/src/item/Tool.php @@ -23,8 +23,7 @@ declare(strict_types=1); namespace pocketmine\item; -use pocketmine\block\Block; -use pocketmine\item\enchantment\Enchantment; +use pocketmine\item\enchantment\VanillaEnchantments; abstract class Tool extends Durable{ @@ -32,11 +31,11 @@ abstract class Tool extends Durable{ return 1; } - public function getMiningEfficiency(Block $block) : float{ + public function getMiningEfficiency(bool $isCorrectTool) : float{ $efficiency = 1; - if(($block->getToolType() & $this->getBlockToolType()) !== 0){ + if($isCorrectTool){ $efficiency = $this->getBaseMiningEfficiency(); - if(($enchantmentLevel = $this->getEnchantmentLevel(Enchantment::EFFICIENCY)) > 0){ + if(($enchantmentLevel = $this->getEnchantmentLevel(VanillaEnchantments::EFFICIENCY())) > 0){ $efficiency += ($enchantmentLevel ** 2 + 1); } } diff --git a/src/item/ToolTier.php b/src/item/ToolTier.php new file mode 100644 index 0000000000..a0984c6add --- /dev/null +++ b/src/item/ToolTier.php @@ -0,0 +1,87 @@ +Enum___construct($name); + $this->harvestLevel = $harvestLevel; + $this->maxDurability = $maxDurability; + $this->baseAttackPoints = $baseAttackPoints; + $this->baseEfficiency = $baseEfficiency; + } + + public function getHarvestLevel() : int{ + return $this->harvestLevel; + } + + public function getMaxDurability() : int{ + return $this->maxDurability; + } + + public function getBaseAttackPoints() : int{ + return $this->baseAttackPoints; + } + + public function getBaseEfficiency() : int{ + return $this->baseEfficiency; + } +} diff --git a/src/pocketmine/item/Totem.php b/src/item/Totem.php similarity index 88% rename from src/pocketmine/item/Totem.php rename to src/item/Totem.php index f64ced723b..8928dd139e 100644 --- a/src/pocketmine/item/Totem.php +++ b/src/item/Totem.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\item; class Totem extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::TOTEM, $meta, "Totem of Undying"); - } public function getMaxStackSize() : int{ return 1; diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php new file mode 100644 index 0000000000..c817ae6862 --- /dev/null +++ b/src/item/VanillaItems.php @@ -0,0 +1,733 @@ +get(333, 4)); + self::register("apple", $factory->get(260)); + self::register("arrow", $factory->get(262)); + self::register("awkward_potion", $factory->get(373, 4)); + self::register("awkward_splash_potion", $factory->get(438, 4)); + self::register("baked_potato", $factory->get(393)); + self::register("beetroot", $factory->get(457)); + self::register("beetroot_seeds", $factory->get(458)); + self::register("beetroot_soup", $factory->get(459)); + self::register("birch_boat", $factory->get(333, 2)); + self::register("black_bed", $factory->get(355, 15)); + self::register("black_dye", $factory->get(351, 16)); + self::register("blaze_powder", $factory->get(377)); + self::register("blaze_rod", $factory->get(369)); + self::register("bleach", $factory->get(451)); + self::register("blue_bed", $factory->get(355, 11)); + self::register("blue_dye", $factory->get(351, 18)); + self::register("bone", $factory->get(352)); + self::register("bone_meal", $factory->get(351, 15)); + self::register("book", $factory->get(340)); + self::register("bow", $factory->get(261)); + self::register("bowl", $factory->get(281)); + self::register("bread", $factory->get(297)); + self::register("brick", $factory->get(336)); + self::register("brown_bed", $factory->get(355, 12)); + self::register("brown_dye", $factory->get(351, 17)); + self::register("bucket", $factory->get(325)); + self::register("carrot", $factory->get(391)); + self::register("chainmail_boots", $factory->get(305)); + self::register("chainmail_chestplate", $factory->get(303)); + self::register("chainmail_helmet", $factory->get(302)); + self::register("chainmail_leggings", $factory->get(304)); + self::register("charcoal", $factory->get(263, 1)); + self::register("chemical_aluminium_oxide", $factory->get(499, 13)); + self::register("chemical_ammonia", $factory->get(499, 36)); + self::register("chemical_barium_sulphate", $factory->get(499, 20)); + self::register("chemical_benzene", $factory->get(499, 33)); + self::register("chemical_boron_trioxide", $factory->get(499, 14)); + self::register("chemical_calcium_bromide", $factory->get(499, 7)); + self::register("chemical_calcium_chloride", $factory->get(499, 25)); + self::register("chemical_cerium_chloride", $factory->get(499, 23)); + self::register("chemical_charcoal", $factory->get(499, 11)); + self::register("chemical_crude_oil", $factory->get(499, 29)); + self::register("chemical_glue", $factory->get(499, 27)); + self::register("chemical_hydrogen_peroxide", $factory->get(499, 35)); + self::register("chemical_hypochlorite", $factory->get(499, 28)); + self::register("chemical_ink", $factory->get(499, 34)); + self::register("chemical_iron_sulphide", $factory->get(499, 4)); + self::register("chemical_latex", $factory->get(499, 30)); + self::register("chemical_lithium_hydride", $factory->get(499, 5)); + self::register("chemical_luminol", $factory->get(499, 10)); + self::register("chemical_magnesium_nitrate", $factory->get(499, 3)); + self::register("chemical_magnesium_oxide", $factory->get(499, 8)); + self::register("chemical_magnesium_salts", $factory->get(499, 18)); + self::register("chemical_mercuric_chloride", $factory->get(499, 22)); + self::register("chemical_polyethylene", $factory->get(499, 16)); + self::register("chemical_potassium_chloride", $factory->get(499, 21)); + self::register("chemical_potassium_iodide", $factory->get(499, 31)); + self::register("chemical_rubbish", $factory->get(499, 17)); + self::register("chemical_salt", $factory->get(499)); + self::register("chemical_soap", $factory->get(499, 15)); + self::register("chemical_sodium_acetate", $factory->get(499, 9)); + self::register("chemical_sodium_fluoride", $factory->get(499, 32)); + self::register("chemical_sodium_hydride", $factory->get(499, 6)); + self::register("chemical_sodium_hydroxide", $factory->get(499, 2)); + self::register("chemical_sodium_hypochlorite", $factory->get(499, 37)); + self::register("chemical_sodium_oxide", $factory->get(499, 1)); + self::register("chemical_sugar", $factory->get(499, 12)); + self::register("chemical_sulphate", $factory->get(499, 19)); + self::register("chemical_tungsten_chloride", $factory->get(499, 24)); + self::register("chemical_water", $factory->get(499, 26)); + self::register("chorus_fruit", $factory->get(432)); + self::register("clay", $factory->get(337)); + self::register("clock", $factory->get(347)); + self::register("clownfish", $factory->get(461)); + self::register("coal", $factory->get(263)); + self::register("cocoa_beans", $factory->get(351, 3)); + self::register("compass", $factory->get(345)); + self::register("cooked_chicken", $factory->get(366)); + self::register("cooked_fish", $factory->get(350)); + self::register("cooked_mutton", $factory->get(424)); + self::register("cooked_porkchop", $factory->get(320)); + self::register("cooked_rabbit", $factory->get(412)); + self::register("cooked_salmon", $factory->get(463)); + self::register("cookie", $factory->get(357)); + self::register("creeper_head", $factory->get(397, 4)); + self::register("cyan_bed", $factory->get(355, 9)); + self::register("cyan_dye", $factory->get(351, 6)); + self::register("dark_oak_boat", $factory->get(333, 5)); + self::register("diamond", $factory->get(264)); + self::register("diamond_axe", $factory->get(279)); + self::register("diamond_boots", $factory->get(313)); + self::register("diamond_chestplate", $factory->get(311)); + self::register("diamond_helmet", $factory->get(310)); + self::register("diamond_hoe", $factory->get(293)); + self::register("diamond_leggings", $factory->get(312)); + self::register("diamond_pickaxe", $factory->get(278)); + self::register("diamond_shovel", $factory->get(277)); + self::register("diamond_sword", $factory->get(276)); + self::register("dragon_breath", $factory->get(437)); + self::register("dragon_head", $factory->get(397, 5)); + self::register("dried_kelp", $factory->get(464)); + self::register("egg", $factory->get(344)); + self::register("emerald", $factory->get(388)); + self::register("enchanted_golden_apple", $factory->get(466)); + self::register("ender_pearl", $factory->get(368)); + self::register("experience_bottle", $factory->get(384)); + self::register("feather", $factory->get(288)); + self::register("fermented_spider_eye", $factory->get(376)); + self::register("fire_resistance_potion", $factory->get(373, 12)); + self::register("fire_resistance_splash_potion", $factory->get(438, 12)); + self::register("fishing_rod", $factory->get(346)); + self::register("flint", $factory->get(318)); + self::register("flint_and_steel", $factory->get(259)); + self::register("ghast_tear", $factory->get(370)); + self::register("glass_bottle", $factory->get(374)); + self::register("glistering_melon", $factory->get(382)); + self::register("glowstone_dust", $factory->get(348)); + self::register("gold_ingot", $factory->get(266)); + self::register("gold_nugget", $factory->get(371)); + self::register("golden_apple", $factory->get(322)); + self::register("golden_axe", $factory->get(286)); + self::register("golden_boots", $factory->get(317)); + self::register("golden_carrot", $factory->get(396)); + self::register("golden_chestplate", $factory->get(315)); + self::register("golden_helmet", $factory->get(314)); + self::register("golden_hoe", $factory->get(294)); + self::register("golden_leggings", $factory->get(316)); + self::register("golden_pickaxe", $factory->get(285)); + self::register("golden_shovel", $factory->get(284)); + self::register("golden_sword", $factory->get(283)); + self::register("gray_bed", $factory->get(355, 7)); + self::register("gray_dye", $factory->get(351, 8)); + self::register("green_bed", $factory->get(355, 13)); + self::register("green_dye", $factory->get(351, 2)); + self::register("gunpowder", $factory->get(289)); + self::register("harming_potion", $factory->get(373, 23)); + self::register("harming_splash_potion", $factory->get(438, 23)); + self::register("healing_potion", $factory->get(373, 21)); + self::register("healing_splash_potion", $factory->get(438, 21)); + self::register("heart_of_the_sea", $factory->get(467)); + self::register("ink_sac", $factory->get(351)); + self::register("invisibility_potion", $factory->get(373, 7)); + self::register("invisibility_splash_potion", $factory->get(438, 7)); + self::register("iron_axe", $factory->get(258)); + self::register("iron_boots", $factory->get(309)); + self::register("iron_chestplate", $factory->get(307)); + self::register("iron_helmet", $factory->get(306)); + self::register("iron_hoe", $factory->get(292)); + self::register("iron_ingot", $factory->get(265)); + self::register("iron_leggings", $factory->get(308)); + self::register("iron_nugget", $factory->get(452)); + self::register("iron_pickaxe", $factory->get(257)); + self::register("iron_shovel", $factory->get(256)); + self::register("iron_sword", $factory->get(267)); + self::register("jungle_boat", $factory->get(333, 3)); + self::register("lapis_lazuli", $factory->get(351, 4)); + self::register("lava_bucket", $factory->get(325, 10)); + self::register("leaping_potion", $factory->get(373, 9)); + self::register("leaping_splash_potion", $factory->get(438, 9)); + self::register("leather", $factory->get(334)); + self::register("leather_boots", $factory->get(301)); + self::register("leather_cap", $factory->get(298)); + self::register("leather_pants", $factory->get(300)); + self::register("leather_tunic", $factory->get(299)); + self::register("light_blue_bed", $factory->get(355, 3)); + self::register("light_blue_dye", $factory->get(351, 12)); + self::register("light_gray_bed", $factory->get(355, 8)); + self::register("light_gray_dye", $factory->get(351, 7)); + self::register("lime_bed", $factory->get(355, 5)); + self::register("lime_dye", $factory->get(351, 10)); + self::register("long_fire_resistance_potion", $factory->get(373, 13)); + self::register("long_fire_resistance_splash_potion", $factory->get(438, 13)); + self::register("long_invisibility_potion", $factory->get(373, 8)); + self::register("long_invisibility_splash_potion", $factory->get(438, 8)); + self::register("long_leaping_potion", $factory->get(373, 10)); + self::register("long_leaping_splash_potion", $factory->get(438, 10)); + self::register("long_mundane_potion", $factory->get(373, 2)); + self::register("long_mundane_splash_potion", $factory->get(438, 2)); + self::register("long_night_vision_potion", $factory->get(373, 6)); + self::register("long_night_vision_splash_potion", $factory->get(438, 6)); + self::register("long_poison_potion", $factory->get(373, 26)); + self::register("long_poison_splash_potion", $factory->get(438, 26)); + self::register("long_regeneration_potion", $factory->get(373, 29)); + self::register("long_regeneration_splash_potion", $factory->get(438, 29)); + self::register("long_slow_falling_potion", $factory->get(373, 41)); + self::register("long_slow_falling_splash_potion", $factory->get(438, 41)); + self::register("long_slowness_potion", $factory->get(373, 18)); + self::register("long_slowness_splash_potion", $factory->get(438, 18)); + self::register("long_strength_potion", $factory->get(373, 32)); + self::register("long_strength_splash_potion", $factory->get(438, 32)); + self::register("long_swiftness_potion", $factory->get(373, 15)); + self::register("long_swiftness_splash_potion", $factory->get(438, 15)); + self::register("long_turtle_master_potion", $factory->get(373, 38)); + self::register("long_turtle_master_splash_potion", $factory->get(438, 38)); + self::register("long_water_breathing_potion", $factory->get(373, 20)); + self::register("long_water_breathing_splash_potion", $factory->get(438, 20)); + self::register("long_weakness_potion", $factory->get(373, 35)); + self::register("long_weakness_splash_potion", $factory->get(438, 35)); + self::register("magenta_bed", $factory->get(355, 2)); + self::register("magenta_dye", $factory->get(351, 13)); + self::register("magma_cream", $factory->get(378)); + self::register("melon", $factory->get(360)); + self::register("melon_seeds", $factory->get(362)); + self::register("milk_bucket", $factory->get(325, 1)); + self::register("minecart", $factory->get(328)); + self::register("mundane_potion", $factory->get(373, 1)); + self::register("mundane_splash_potion", $factory->get(438, 1)); + self::register("mushroom_stew", $factory->get(282)); + self::register("nautilus_shell", $factory->get(465)); + self::register("nether_brick", $factory->get(405)); + self::register("nether_quartz", $factory->get(406)); + self::register("nether_star", $factory->get(399)); + self::register("night_vision_potion", $factory->get(373, 5)); + self::register("night_vision_splash_potion", $factory->get(438, 5)); + self::register("oak_boat", $factory->get(333)); + self::register("orange_bed", $factory->get(355, 1)); + self::register("orange_dye", $factory->get(351, 14)); + self::register("painting", $factory->get(321)); + self::register("paper", $factory->get(339)); + self::register("pink_bed", $factory->get(355, 6)); + self::register("pink_dye", $factory->get(351, 9)); + self::register("player_head", $factory->get(397, 3)); + self::register("poison_potion", $factory->get(373, 25)); + self::register("poison_splash_potion", $factory->get(438, 25)); + self::register("poisonous_potato", $factory->get(394)); + self::register("popped_chorus_fruit", $factory->get(433)); + self::register("potato", $factory->get(392)); + self::register("prismarine_crystals", $factory->get(422)); + self::register("prismarine_shard", $factory->get(409)); + self::register("pufferfish", $factory->get(462)); + self::register("pumpkin_pie", $factory->get(400)); + self::register("pumpkin_seeds", $factory->get(361)); + self::register("purple_bed", $factory->get(355, 10)); + self::register("purple_dye", $factory->get(351, 5)); + self::register("rabbit_foot", $factory->get(414)); + self::register("rabbit_hide", $factory->get(415)); + self::register("rabbit_stew", $factory->get(413)); + self::register("raw_beef", $factory->get(363)); + self::register("raw_chicken", $factory->get(365)); + self::register("raw_fish", $factory->get(349)); + self::register("raw_mutton", $factory->get(423)); + self::register("raw_porkchop", $factory->get(319)); + self::register("raw_rabbit", $factory->get(411)); + self::register("raw_salmon", $factory->get(460)); + self::register("record_11", $factory->get(510)); + self::register("record_13", $factory->get(500)); + self::register("record_blocks", $factory->get(502)); + self::register("record_cat", $factory->get(501)); + self::register("record_chirp", $factory->get(503)); + self::register("record_far", $factory->get(504)); + self::register("record_mall", $factory->get(505)); + self::register("record_mellohi", $factory->get(506)); + self::register("record_stal", $factory->get(507)); + self::register("record_strad", $factory->get(508)); + self::register("record_wait", $factory->get(511)); + self::register("record_ward", $factory->get(509)); + self::register("red_bed", $factory->get(355, 14)); + self::register("red_dye", $factory->get(351, 1)); + self::register("redstone_dust", $factory->get(331)); + self::register("regeneration_potion", $factory->get(373, 28)); + self::register("regeneration_splash_potion", $factory->get(438, 28)); + self::register("rotten_flesh", $factory->get(367)); + self::register("scute", $factory->get(468)); + self::register("shears", $factory->get(359)); + self::register("shulker_shell", $factory->get(445)); + self::register("skeleton_skull", $factory->get(397)); + self::register("slimeball", $factory->get(341)); + self::register("slow_falling_potion", $factory->get(373, 40)); + self::register("slow_falling_splash_potion", $factory->get(438, 40)); + self::register("slowness_potion", $factory->get(373, 17)); + self::register("slowness_splash_potion", $factory->get(438, 17)); + self::register("snowball", $factory->get(332)); + self::register("spider_eye", $factory->get(375)); + self::register("spruce_boat", $factory->get(333, 1)); + self::register("squid_spawn_egg", $factory->get(383, 17)); + self::register("steak", $factory->get(364)); + self::register("stick", $factory->get(280)); + self::register("stone_axe", $factory->get(275)); + self::register("stone_hoe", $factory->get(291)); + self::register("stone_pickaxe", $factory->get(274)); + self::register("stone_shovel", $factory->get(273)); + self::register("stone_sword", $factory->get(272)); + self::register("strength_potion", $factory->get(373, 31)); + self::register("strength_splash_potion", $factory->get(438, 31)); + self::register("string", $factory->get(287)); + self::register("strong_harming_potion", $factory->get(373, 24)); + self::register("strong_harming_splash_potion", $factory->get(438, 24)); + self::register("strong_healing_potion", $factory->get(373, 22)); + self::register("strong_healing_splash_potion", $factory->get(438, 22)); + self::register("strong_leaping_potion", $factory->get(373, 11)); + self::register("strong_leaping_splash_potion", $factory->get(438, 11)); + self::register("strong_poison_potion", $factory->get(373, 27)); + self::register("strong_poison_splash_potion", $factory->get(438, 27)); + self::register("strong_regeneration_potion", $factory->get(373, 30)); + self::register("strong_regeneration_splash_potion", $factory->get(438, 30)); + self::register("strong_strength_potion", $factory->get(373, 33)); + self::register("strong_strength_splash_potion", $factory->get(438, 33)); + self::register("strong_swiftness_potion", $factory->get(373, 16)); + self::register("strong_swiftness_splash_potion", $factory->get(438, 16)); + self::register("strong_turtle_master_potion", $factory->get(373, 39)); + self::register("strong_turtle_master_splash_potion", $factory->get(438, 39)); + self::register("sugar", $factory->get(353)); + self::register("sweet_berries", $factory->get(477)); + self::register("swiftness_potion", $factory->get(373, 14)); + self::register("swiftness_splash_potion", $factory->get(438, 14)); + self::register("thick_potion", $factory->get(373, 3)); + self::register("thick_splash_potion", $factory->get(438, 3)); + self::register("totem", $factory->get(450)); + self::register("turtle_master_potion", $factory->get(373, 37)); + self::register("turtle_master_splash_potion", $factory->get(438, 37)); + self::register("villager_spawn_egg", $factory->get(383, 15)); + self::register("water_breathing_potion", $factory->get(373, 19)); + self::register("water_breathing_splash_potion", $factory->get(438, 19)); + self::register("water_bucket", $factory->get(325, 8)); + self::register("water_potion", $factory->get(373)); + self::register("water_splash_potion", $factory->get(438)); + self::register("weakness_potion", $factory->get(373, 34)); + self::register("weakness_splash_potion", $factory->get(438, 34)); + self::register("wheat", $factory->get(296)); + self::register("wheat_seeds", $factory->get(295)); + self::register("white_bed", $factory->get(355)); + self::register("white_dye", $factory->get(351, 19)); + self::register("wither_potion", $factory->get(373, 36)); + self::register("wither_skeleton_skull", $factory->get(397, 1)); + self::register("wither_splash_potion", $factory->get(438, 36)); + self::register("wooden_axe", $factory->get(271)); + self::register("wooden_hoe", $factory->get(290)); + self::register("wooden_pickaxe", $factory->get(270)); + self::register("wooden_shovel", $factory->get(269)); + self::register("wooden_sword", $factory->get(268)); + self::register("writable_book", $factory->get(386)); + self::register("written_book", $factory->get(387)); + self::register("yellow_bed", $factory->get(355, 4)); + self::register("yellow_dye", $factory->get(351, 11)); + self::register("zombie_head", $factory->get(397, 2)); + self::register("zombie_spawn_egg", $factory->get(383, 32)); + } +} diff --git a/src/pocketmine/item/WheatSeeds.php b/src/item/WheatSeeds.php similarity index 78% rename from src/pocketmine/item/WheatSeeds.php rename to src/item/WheatSeeds.php index 5bf2dccdd8..72196e2345 100644 --- a/src/pocketmine/item/WheatSeeds.php +++ b/src/item/WheatSeeds.php @@ -24,14 +24,11 @@ declare(strict_types=1); namespace pocketmine\item; use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; class WheatSeeds extends Item{ - public function __construct(int $meta = 0){ - parent::__construct(self::WHEAT_SEEDS, $meta, "Wheat Seeds"); - } - public function getBlock() : Block{ - return BlockFactory::get(Block::WHEAT_BLOCK); + public function getBlock(?int $clickedFace = null) : Block{ + return VanillaBlocks::WHEAT(); } } diff --git a/src/pocketmine/entity/Monster.php b/src/item/WritableBook.php similarity index 91% rename from src/pocketmine/entity/Monster.php rename to src/item/WritableBook.php index 92cf3bd29a..fca4e1222a 100644 --- a/src/pocketmine/entity/Monster.php +++ b/src/item/WritableBook.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\entity; +namespace pocketmine\item; -abstract class Monster extends Creature{ +class WritableBook extends WritableBookBase{ } diff --git a/src/item/WritableBookBase.php b/src/item/WritableBookBase.php new file mode 100644 index 0000000000..b19de34baf --- /dev/null +++ b/src/item/WritableBookBase.php @@ -0,0 +1,197 @@ + + public const TAG_PAGE_TEXT = "text"; //TAG_String + public const TAG_PAGE_PHOTONAME = "photoname"; //TAG_String - TODO + + /** + * @var WritableBookPage[] + * @phpstan-var list + */ + private $pages = []; + + public function __construct(ItemIdentifier $identifier, string $name){ + parent::__construct($identifier, $name); + } + + /** + * Returns whether the given page exists in this book. + */ + public function pageExists(int $pageId) : bool{ + return isset($this->pages[$pageId]); + } + + /** + * Returns a string containing the content of a page (which could be empty), or null if the page doesn't exist. + * + * @throws \OutOfRangeException if requesting a nonexisting page + */ + public function getPageText(int $pageId) : string{ + return $this->pages[$pageId]->getText(); + } + + /** + * Sets the text of a page in the book. Adds the page if the page does not yet exist. + * + * @return $this + */ + public function setPageText(int $pageId, string $pageText) : self{ + if(!$this->pageExists($pageId)){ + $this->addPage($pageId); + } + + $this->pages[$pageId] = new WritableBookPage($pageText); + return $this; + } + + /** + * Adds a new page with the given page ID. + * Creates a new page for every page between the given ID and existing pages that doesn't yet exist. + * + * @return $this + */ + public function addPage(int $pageId) : self{ + if($pageId < 0){ + throw new \InvalidArgumentException("Page number \"$pageId\" is out of range"); + } + + for($current = count($this->pages); $current <= $pageId; $current++){ + $this->pages[] = new WritableBookPage(""); + } + return $this; + } + + /** + * Deletes an existing page with the given page ID. + * + * @return $this + */ + public function deletePage(int $pageId) : self{ + unset($this->pages[$pageId]); + $this->pages = array_values($this->pages); + return $this; + } + + /** + * Inserts a new page with the given text and moves other pages upwards. + * + * @return $this + */ + public function insertPage(int $pageId, string $pageText = "") : self{ + if($pageId < 0 || $pageId > count($this->pages)){ + throw new \InvalidArgumentException("Page ID must not be negative"); + } + $newPages = array_slice($this->pages, 0, $pageId); + $newPages[] = new WritableBookPage($pageText); + array_push($newPages, ...array_slice($this->pages, $pageId)); + $this->pages = $newPages; + return $this; + } + + /** + * Switches the text of two pages with each other. + * + * @return bool indicating success + * @throws \OutOfRangeException if either of the pages does not exist + */ + public function swapPages(int $pageId1, int $pageId2) : bool{ + $pageContents1 = $this->getPageText($pageId1); + $pageContents2 = $this->getPageText($pageId2); + $this->setPageText($pageId1, $pageContents2); + $this->setPageText($pageId2, $pageContents1); + + return true; + } + + public function getMaxStackSize() : int{ + return 1; + } + + /** + * Returns an array containing all pages of this book. + * + * @return WritableBookPage[] + */ + public function getPages() : array{ + return $this->pages; + } + + /** + * @param WritableBookPage[] $pages + * + * @return $this + */ + public function setPages(array $pages) : self{ + $this->pages = array_values($pages); + return $this; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + $this->pages = []; + + $pages = $tag->getListTag(self::TAG_PAGES); + if($pages !== null){ + 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, "")); + } + }elseif($pages->getTagType() === NBT::TAG_String){ //PC format + /** @var StringTag $page */ + foreach($pages as $page){ + $this->pages[] = new WritableBookPage($page->getValue()); + } + } + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + if(count($this->pages) > 0){ + $pages = new ListTag(); + foreach($this->pages as $page){ + $pages->push(CompoundTag::create() + ->setString(self::TAG_PAGE_TEXT, $page->getText()) + ->setString(self::TAG_PAGE_PHOTONAME, $page->getPhotoName()) + ); + } + $tag->setTag(self::TAG_PAGES, $pages); + }else{ + $tag->removeTag(self::TAG_PAGES); + } + } +} diff --git a/src/pocketmine/lang/TextContainer.php b/src/item/WritableBookPage.php similarity index 68% rename from src/pocketmine/lang/TextContainer.php rename to src/item/WritableBookPage.php index e0a32acc92..62ebbcf50c 100644 --- a/src/pocketmine/lang/TextContainer.php +++ b/src/item/WritableBookPage.php @@ -21,29 +21,29 @@ declare(strict_types=1); -namespace pocketmine\lang; +namespace pocketmine\item; -class TextContainer{ +use pocketmine\utils\Utils; - /** @var string $text */ - protected $text; +class WritableBookPage{ - public function __construct(string $text){ - $this->text = $text; - } - - /** - * @return void - */ - public function setText(string $text){ + /** @var string */ + private $text; + /** @var string */ + private $photoName; + + public function __construct(string $text, string $photoName = ""){ + //TODO: data validation + Utils::checkUTF8($text); $this->text = $text; + $this->photoName = $photoName; } public function getText() : string{ return $this->text; } - public function __toString() : string{ - return $this->getText(); + public function getPhotoName() : string{ + return $this->photoName; } } diff --git a/src/pocketmine/item/WrittenBook.php b/src/item/WrittenBook.php similarity index 58% rename from src/pocketmine/item/WrittenBook.php rename to src/item/WrittenBook.php index 13137ab052..f356b1cd64 100644 --- a/src/pocketmine/item/WrittenBook.php +++ b/src/item/WrittenBook.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace pocketmine\item; -class WrittenBook extends WritableBook{ +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\utils\Utils; + +class WrittenBook extends WritableBookBase{ public const GENERATION_ORIGINAL = 0; public const GENERATION_COPY = 1; @@ -34,9 +37,12 @@ class WrittenBook extends WritableBook{ public const TAG_AUTHOR = "author"; //TAG_String public const TAG_TITLE = "title"; //TAG_String - public function __construct(int $meta = 0){ - Item::__construct(self::WRITTEN_BOOK, $meta, "Written Book"); - } + /** @var int */ + private $generation = self::GENERATION_ORIGINAL; + /** @var string */ + private $author = ""; + /** @var string */ + private $title = ""; public function getMaxStackSize() : int{ return 16; @@ -47,19 +53,21 @@ class WrittenBook extends WritableBook{ * Generations higher than 1 can not be copied. */ public function getGeneration() : int{ - return $this->getNamedTag()->getInt(self::TAG_GENERATION, -1); + return $this->generation; } /** * Sets the generation of a book. + * + * @return $this */ - public function setGeneration(int $generation) : void{ + public function setGeneration(int $generation) : self{ if($generation < 0 or $generation > 3){ throw new \InvalidArgumentException("Generation \"$generation\" is out of range"); } - $namedTag = $this->getNamedTag(); - $namedTag->setInt(self::TAG_GENERATION, $generation); - $this->setNamedTag($namedTag); + + $this->generation = $generation; + return $this; } /** @@ -68,31 +76,49 @@ class WrittenBook extends WritableBook{ * The author can be set to anything when signing a book. */ public function getAuthor() : string{ - return $this->getNamedTag()->getString(self::TAG_AUTHOR, ""); + return $this->author; } /** * Sets the author of this book. + * + * @return $this */ - public function setAuthor(string $authorName) : void{ - $namedTag = $this->getNamedTag(); - $namedTag->setString(self::TAG_AUTHOR, $authorName); - $this->setNamedTag($namedTag); + public function setAuthor(string $authorName) : self{ + Utils::checkUTF8($authorName); + $this->author = $authorName; + return $this; } /** * Returns the title of this book. */ public function getTitle() : string{ - return $this->getNamedTag()->getString(self::TAG_TITLE, ""); + return $this->title; } /** * Sets the author of this book. + * + * @return $this */ - public function setTitle(string $title) : void{ - $namedTag = $this->getNamedTag(); - $namedTag->setString(self::TAG_TITLE, $title); - $this->setNamedTag($namedTag); + public function setTitle(string $title) : self{ + Utils::checkUTF8($title); + $this->title = $title; + return $this; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + $this->generation = $tag->getInt(self::TAG_GENERATION, $this->generation); + $this->author = $tag->getString(self::TAG_AUTHOR, $this->author); + $this->title = $tag->getString(self::TAG_TITLE, $this->title); + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + $tag->setInt(self::TAG_GENERATION, $this->generation); + $tag->setString(self::TAG_AUTHOR, $this->author); + $tag->setString(self::TAG_TITLE, $this->title); } } diff --git a/src/item/enchantment/Enchantment.php b/src/item/enchantment/Enchantment.php new file mode 100644 index 0000000000..7185a7af27 --- /dev/null +++ b/src/item/enchantment/Enchantment.php @@ -0,0 +1,96 @@ +name; + } + + /** + * Returns an int constant indicating how rare this enchantment type is. + */ + public function getRarity() : int{ + return $this->rarity; + } + + /** + * Returns a bitset indicating what item types can have this item applied from an enchanting table. + */ + public function getPrimaryItemFlags() : int{ + return $this->primaryItemFlags; + } + + /** + * Returns a bitset indicating what item types cannot have this item applied from an enchanting table, but can from + * an anvil. + */ + public function getSecondaryItemFlags() : int{ + return $this->secondaryItemFlags; + } + + /** + * Returns whether this enchantment can apply to the item type from an enchanting table. + */ + public function hasPrimaryItemType(int $flag) : bool{ + return ($this->primaryItemFlags & $flag) !== 0; + } + + /** + * Returns whether this enchantment can apply to the item type from an anvil, if it is not a primary item. + */ + public function hasSecondaryItemType(int $flag) : bool{ + return ($this->secondaryItemFlags & $flag) !== 0; + } + + /** + * Returns the maximum level of this enchantment that can be found on an enchantment table. + */ + public function getMaxLevel() : int{ + return $this->maxLevel; + } + + //TODO: methods for min/max XP cost bounds based on enchantment level (not needed yet - enchanting is client-side) +} diff --git a/src/pocketmine/item/enchantment/EnchantmentInstance.php b/src/item/enchantment/EnchantmentInstance.php similarity index 81% rename from src/pocketmine/item/enchantment/EnchantmentInstance.php rename to src/item/enchantment/EnchantmentInstance.php index 33a44f4ac3..f46d2c75b8 100644 --- a/src/pocketmine/item/enchantment/EnchantmentInstance.php +++ b/src/item/enchantment/EnchantmentInstance.php @@ -25,8 +25,10 @@ namespace pocketmine\item\enchantment; /** * Container for enchantment data applied to items. + * + * Note: This class is assumed to be immutable. Consider this before making alterations. */ -class EnchantmentInstance{ +final class EnchantmentInstance{ /** @var Enchantment */ private $enchantment; /** @var int */ @@ -50,28 +52,10 @@ class EnchantmentInstance{ return $this->enchantment; } - /** - * Returns the type identifier of this enchantment instance. - */ - public function getId() : int{ - return $this->enchantment->getId(); - } - /** * Returns the level of the enchantment. */ public function getLevel() : int{ return $this->level; } - - /** - * Sets the level of the enchantment. - * - * @return $this - */ - public function setLevel(int $level){ - $this->level = $level; - - return $this; - } } diff --git a/src/pocketmine/item/enchantment/FireAspectEnchantment.php b/src/item/enchantment/FireAspectEnchantment.php similarity index 100% rename from src/pocketmine/item/enchantment/FireAspectEnchantment.php rename to src/item/enchantment/FireAspectEnchantment.php diff --git a/src/item/enchantment/ItemFlags.php b/src/item/enchantment/ItemFlags.php new file mode 100644 index 0000000000..e0638b97f9 --- /dev/null +++ b/src/item/enchantment/ItemFlags.php @@ -0,0 +1,55 @@ +knockBack($attacker, 0, $victim->x - $attacker->x, $victim->z - $attacker->z, $enchantmentLevel * 0.5); + $diff = $victim->getPosition()->subtractVector($attacker->getPosition()); + $victim->knockBack($diff->x, $diff->z, $enchantmentLevel * 0.5); } } } diff --git a/src/pocketmine/item/enchantment/MeleeWeaponEnchantment.php b/src/item/enchantment/MeleeWeaponEnchantment.php similarity index 100% rename from src/pocketmine/item/enchantment/MeleeWeaponEnchantment.php rename to src/item/enchantment/MeleeWeaponEnchantment.php diff --git a/src/pocketmine/item/enchantment/ProtectionEnchantment.php b/src/item/enchantment/ProtectionEnchantment.php similarity index 87% rename from src/pocketmine/item/enchantment/ProtectionEnchantment.php rename to src/item/enchantment/ProtectionEnchantment.php index 8fc603aa97..30312ce341 100644 --- a/src/pocketmine/item/enchantment/ProtectionEnchantment.php +++ b/src/item/enchantment/ProtectionEnchantment.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\item\enchantment; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\lang\Translatable; use function array_flip; use function floor; @@ -38,8 +39,8 @@ class ProtectionEnchantment extends Enchantment{ * * @param int[]|null $applicableDamageTypes EntityDamageEvent::CAUSE_* constants which this enchantment type applies to, or null if it applies to all types of damage. */ - public function __construct(int $id, string $name, int $rarity, int $primaryItemFlags, int $secondaryItemFlags, int $maxLevel, float $typeModifier, ?array $applicableDamageTypes){ - parent::__construct($id, $name, $rarity, $primaryItemFlags, $secondaryItemFlags, $maxLevel); + public function __construct(Translatable|string $name, int $rarity, int $primaryItemFlags, int $secondaryItemFlags, int $maxLevel, float $typeModifier, ?array $applicableDamageTypes){ + parent::__construct($name, $rarity, $primaryItemFlags, $secondaryItemFlags, $maxLevel); $this->typeModifier = $typeModifier; if($applicableDamageTypes !== null){ diff --git a/src/pocketmine/network/mcpe/protocol/types/GameRuleType.php b/src/item/enchantment/Rarity.php similarity index 82% rename from src/pocketmine/network/mcpe/protocol/types/GameRuleType.php rename to src/item/enchantment/Rarity.php index 66a6b47619..29a7ea2d3e 100644 --- a/src/pocketmine/network/mcpe/protocol/types/GameRuleType.php +++ b/src/item/enchantment/Rarity.php @@ -21,15 +21,15 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; - -final class GameRuleType{ +namespace pocketmine\item\enchantment; +final class Rarity{ private function __construct(){ //NOOP } - public const BOOL = 1; - public const INT = 2; - public const FLOAT = 3; + public const COMMON = 10; + public const UNCOMMON = 5; + public const RARE = 2; + public const MYTHIC = 1; } diff --git a/src/pocketmine/item/enchantment/SharpnessEnchantment.php b/src/item/enchantment/SharpnessEnchantment.php similarity index 100% rename from src/pocketmine/item/enchantment/SharpnessEnchantment.php rename to src/item/enchantment/SharpnessEnchantment.php diff --git a/src/item/enchantment/StringToEnchantmentParser.php b/src/item/enchantment/StringToEnchantmentParser.php new file mode 100644 index 0000000000..2205f12ff8 --- /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); + } +} \ No newline at end of file diff --git a/src/item/enchantment/VanillaEnchantments.php b/src/item/enchantment/VanillaEnchantments.php new file mode 100644 index 0000000000..f42039160d --- /dev/null +++ b/src/item/enchantment/VanillaEnchantments.php @@ -0,0 +1,116 @@ + + */ + public static function getAll() : array{ + /** + * @var Enchantment[] $result + * @phpstan-var array $result + */ + $result = self::_registryGetAll(); + return $result; + } +} diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php new file mode 100644 index 0000000000..35418d18cd --- /dev/null +++ b/src/lang/KnownTranslationFactory.php @@ -0,0 +1,2178 @@ + $param0, + 1 => $param1, + ]); + } + + public static function chat_type_admin(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::CHAT_TYPE_ADMIN, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function chat_type_announcement(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::CHAT_TYPE_ANNOUNCEMENT, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function chat_type_emote(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::CHAT_TYPE_EMOTE, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function chat_type_text(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::CHAT_TYPE_TEXT, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_ban_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BAN_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_ban_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BAN_USAGE, []); + } + + public static function commands_banip_invalid() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANIP_INVALID, []); + } + + public static function commands_banip_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANIP_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_banip_success_players(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANIP_SUCCESS_PLAYERS, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_banip_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANIP_USAGE, []); + } + + public static function commands_banlist_ips(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANLIST_IPS, [ + 0 => $param0, + ]); + } + + public static function commands_banlist_players(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANLIST_PLAYERS, [ + 0 => $param0, + ]); + } + + public static function commands_banlist_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_BANLIST_USAGE, []); + } + + public static function commands_clear_failure_no_items(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_CLEAR_FAILURE_NO_ITEMS, [ + 0 => $param0, + ]); + } + + public static function commands_clear_success(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_CLEAR_SUCCESS, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_clear_testing(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_CLEAR_TESTING, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_defaultgamemode_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DEFAULTGAMEMODE_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_defaultgamemode_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DEFAULTGAMEMODE_USAGE, []); + } + + public static function commands_deop_message() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DEOP_MESSAGE, []); + } + + public static function commands_deop_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DEOP_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_deop_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DEOP_USAGE, []); + } + + public static function commands_difficulty_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DIFFICULTY_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_difficulty_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_DIFFICULTY_USAGE, []); + } + + public static function commands_effect_failure_notActive(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_FAILURE_NOTACTIVE, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_effect_failure_notActive_all(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_FAILURE_NOTACTIVE_ALL, [ + 0 => $param0, + ]); + } + + public static function commands_effect_notFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_NOTFOUND, [ + 0 => $param0, + ]); + } + + public static function commands_effect_success(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_SUCCESS, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + ]); + } + + public static function commands_effect_success_removed(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_SUCCESS_REMOVED, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_effect_success_removed_all(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_SUCCESS_REMOVED_ALL, [ + 0 => $param0, + ]); + } + + public static function commands_effect_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_EFFECT_USAGE, []); + } + + public static function commands_enchant_noItem() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_ENCHANT_NOITEM, []); + } + + public static function commands_enchant_notFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_ENCHANT_NOTFOUND, [ + 0 => $param0, + ]); + } + + public static function commands_enchant_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_ENCHANT_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_enchant_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_ENCHANT_USAGE, []); + } + + public static function commands_gamemode_success_other(Translatable|string $param1, Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GAMEMODE_SUCCESS_OTHER, [ + 1 => $param1, + 0 => $param0, + ]); + } + + public static function commands_gamemode_success_self(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GAMEMODE_SUCCESS_SELF, [ + 0 => $param0, + ]); + } + + public static function commands_gamemode_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GAMEMODE_USAGE, []); + } + + public static function commands_generic_notFound() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_NOTFOUND, []); + } + + public static function commands_generic_num_tooBig(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_NUM_TOOBIG, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_generic_num_tooSmall(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_NUM_TOOSMALL, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_generic_permission() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_PERMISSION, []); + } + + public static function commands_generic_player_notFound() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_PLAYER_NOTFOUND, []); + } + + public static function commands_generic_usage(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GENERIC_USAGE, [ + 0 => $param0, + ]); + } + + public static function commands_give_item_notFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GIVE_ITEM_NOTFOUND, [ + 0 => $param0, + ]); + } + + public static function commands_give_success(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GIVE_SUCCESS, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function commands_give_tagError(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_GIVE_TAGERROR, [ + 0 => $param0, + ]); + } + + public static function commands_help_header(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_HELP_HEADER, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_help_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_HELP_USAGE, []); + } + + public static function commands_kick_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_KICK_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_kick_success_reason(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_KICK_SUCCESS_REASON, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_kick_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_KICK_USAGE, []); + } + + public static function commands_kill_successful(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_KILL_SUCCESSFUL, [ + 0 => $param0, + ]); + } + + public static function commands_me_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_ME_USAGE, []); + } + + public static function commands_message_display_incoming(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_MESSAGE_DISPLAY_INCOMING, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_message_display_outgoing(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_MESSAGE_DISPLAY_OUTGOING, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_message_sameTarget() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_MESSAGE_SAMETARGET, []); + } + + public static function commands_message_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_MESSAGE_USAGE, []); + } + + public static function commands_op_message() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_OP_MESSAGE, []); + } + + public static function commands_op_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_OP_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_op_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_OP_USAGE, []); + } + + public static function commands_particle_notFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_PARTICLE_NOTFOUND, [ + 0 => $param0, + ]); + } + + public static function commands_particle_success(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_PARTICLE_SUCCESS, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_players_list(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_PLAYERS_LIST, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_save_disabled() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SAVE_DISABLED, []); + } + + public static function commands_save_enabled() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SAVE_ENABLED, []); + } + + public static function commands_save_start() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SAVE_START, []); + } + + public static function commands_save_success() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SAVE_SUCCESS, []); + } + + public static function commands_say_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SAY_USAGE, []); + } + + public static function commands_seed_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SEED_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_setworldspawn_success(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SETWORLDSPAWN_SUCCESS, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function commands_setworldspawn_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SETWORLDSPAWN_USAGE, []); + } + + public static function commands_spawnpoint_success(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SPAWNPOINT_SUCCESS, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + ]); + } + + public static function commands_spawnpoint_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_SPAWNPOINT_USAGE, []); + } + + public static function commands_stop_start() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_STOP_START, []); + } + + public static function commands_time_added(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TIME_ADDED, [ + 0 => $param0, + ]); + } + + public static function commands_time_query(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TIME_QUERY, [ + 0 => $param0, + ]); + } + + public static function commands_time_set(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TIME_SET, [ + 0 => $param0, + ]); + } + + public static function commands_title_success() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TITLE_SUCCESS, []); + } + + public static function commands_title_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TITLE_USAGE, []); + } + + public static function commands_tp_success(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TP_SUCCESS, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_tp_success_coordinates(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TP_SUCCESS_COORDINATES, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + ]); + } + + public static function commands_tp_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_TP_USAGE, []); + } + + public static function commands_unban_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_UNBAN_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_unban_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_UNBAN_USAGE, []); + } + + public static function commands_unbanip_invalid() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_UNBANIP_INVALID, []); + } + + public static function commands_unbanip_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_UNBANIP_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_unbanip_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_UNBANIP_USAGE, []); + } + + public static function commands_whitelist_add_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_ADD_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_whitelist_add_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_ADD_USAGE, []); + } + + public static function commands_whitelist_disabled() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_DISABLED, []); + } + + public static function commands_whitelist_enabled() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_ENABLED, []); + } + + public static function commands_whitelist_list(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_LIST, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function commands_whitelist_reloaded() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_RELOADED, []); + } + + public static function commands_whitelist_remove_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_REMOVE_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function commands_whitelist_remove_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::COMMANDS_WHITELIST_REMOVE_USAGE, []); + } + + public static function commands_whitelist_usage() : Translatable{ + 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, + 1 => $param1, + ]); + } + + public static function death_attack_arrow_item(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_ARROW_ITEM, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function death_attack_cactus(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_CACTUS, [ + 0 => $param0, + ]); + } + + public static function death_attack_drown(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_DROWN, [ + 0 => $param0, + ]); + } + + public static function death_attack_explosion(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_EXPLOSION, [ + 0 => $param0, + ]); + } + + public static function death_attack_explosion_player(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_EXPLOSION_PLAYER, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function death_attack_fall(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_FALL, [ + 0 => $param0, + ]); + } + + public static function death_attack_generic(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_GENERIC, [ + 0 => $param0, + ]); + } + + public static function death_attack_inFire(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_INFIRE, [ + 0 => $param0, + ]); + } + + public static function death_attack_inWall(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_INWALL, [ + 0 => $param0, + ]); + } + + public static function death_attack_lava(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_LAVA, [ + 0 => $param0, + ]); + } + + public static function death_attack_magic(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_MAGIC, [ + 0 => $param0, + ]); + } + + public static function death_attack_mob(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_MOB, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function death_attack_onFire(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_ONFIRE, [ + 0 => $param0, + ]); + } + + public static function death_attack_outOfWorld(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_OUTOFWORLD, [ + 0 => $param0, + ]); + } + + public static function death_attack_player(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_PLAYER, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function death_attack_player_item(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_PLAYER_ITEM, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function death_attack_wither(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_ATTACK_WITHER, [ + 0 => $param0, + ]); + } + + public static function death_fell_accident_generic(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::DEATH_FELL_ACCIDENT_GENERIC, [ + 0 => $param0, + ]); + } + + public static function default_gamemode() : Translatable{ + return new Translatable(KnownTranslationKeys::DEFAULT_GAMEMODE, []); + } + + public static function default_values_info() : Translatable{ + return new Translatable(KnownTranslationKeys::DEFAULT_VALUES_INFO, []); + } + + public static function disconnectionScreen_invalidName() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDNAME, []); + } + + public static function disconnectionScreen_invalidSkin() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDSKIN, []); + } + + public static function disconnectionScreen_noReason() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_NOREASON, []); + } + + public static function disconnectionScreen_notAuthenticated() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_NOTAUTHENTICATED, []); + } + + public static function disconnectionScreen_outdatedClient() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_OUTDATEDCLIENT, []); + } + + public static function disconnectionScreen_outdatedServer() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_OUTDATEDSERVER, []); + } + + public static function disconnectionScreen_resourcePack() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_RESOURCEPACK, []); + } + + public static function disconnectionScreen_serverFull() : Translatable{ + return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_SERVERFULL, []); + } + + public static function enchantment_arrowDamage() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_ARROWDAMAGE, []); + } + + public static function enchantment_arrowFire() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_ARROWFIRE, []); + } + + public static function enchantment_arrowInfinite() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_ARROWINFINITE, []); + } + + public static function enchantment_arrowKnockback() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_ARROWKNOCKBACK, []); + } + + public static function enchantment_crossbowMultishot() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_CROSSBOWMULTISHOT, []); + } + + public static function enchantment_crossbowPiercing() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_CROSSBOWPIERCING, []); + } + + public static function enchantment_crossbowQuickCharge() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_CROSSBOWQUICKCHARGE, []); + } + + public static function enchantment_curse_binding() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_CURSE_BINDING, []); + } + + public static function enchantment_curse_vanishing() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_CURSE_VANISHING, []); + } + + public static function enchantment_damage_all() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_DAMAGE_ALL, []); + } + + public static function enchantment_damage_arthropods() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_DAMAGE_ARTHROPODS, []); + } + + public static function enchantment_damage_undead() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_DAMAGE_UNDEAD, []); + } + + public static function enchantment_digging() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_DIGGING, []); + } + + public static function enchantment_durability() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_DURABILITY, []); + } + + public static function enchantment_fire() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_FIRE, []); + } + + public static function enchantment_fishingSpeed() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_FISHINGSPEED, []); + } + + public static function enchantment_frostwalker() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_FROSTWALKER, []); + } + + public static function enchantment_knockback() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_KNOCKBACK, []); + } + + public static function enchantment_lootBonus() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_LOOTBONUS, []); + } + + public static function enchantment_lootBonusDigger() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_LOOTBONUSDIGGER, []); + } + + public static function enchantment_lootBonusFishing() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_LOOTBONUSFISHING, []); + } + + public static function enchantment_mending() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_MENDING, []); + } + + public static function enchantment_oxygen() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_OXYGEN, []); + } + + public static function enchantment_protect_all() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_PROTECT_ALL, []); + } + + public static function enchantment_protect_explosion() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_PROTECT_EXPLOSION, []); + } + + public static function enchantment_protect_fall() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_PROTECT_FALL, []); + } + + public static function enchantment_protect_fire() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_PROTECT_FIRE, []); + } + + public static function enchantment_protect_projectile() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_PROTECT_PROJECTILE, []); + } + + public static function enchantment_soul_speed() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_SOUL_SPEED, []); + } + + public static function enchantment_thorns() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_THORNS, []); + } + + public static function enchantment_tridentChanneling() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_TRIDENTCHANNELING, []); + } + + public static function enchantment_tridentImpaling() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_TRIDENTIMPALING, []); + } + + public static function enchantment_tridentLoyalty() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_TRIDENTLOYALTY, []); + } + + public static function enchantment_tridentRiptide() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_TRIDENTRIPTIDE, []); + } + + public static function enchantment_untouching() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_UNTOUCHING, []); + } + + public static function enchantment_waterWalker() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_WATERWALKER, []); + } + + public static function enchantment_waterWorker() : Translatable{ + return new Translatable(KnownTranslationKeys::ENCHANTMENT_WATERWORKER, []); + } + + public static function gameMode_adventure() : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_ADVENTURE, []); + } + + public static function gameMode_changed(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_CHANGED, [ + 0 => $param0, + ]); + } + + public static function gameMode_creative() : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_CREATIVE, []); + } + + public static function gameMode_spectator() : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_SPECTATOR, []); + } + + public static function gameMode_survival() : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_SURVIVAL, []); + } + + public static function gamemode_info() : Translatable{ + return new Translatable(KnownTranslationKeys::GAMEMODE_INFO, []); + } + + public static function invalid_port() : Translatable{ + return new Translatable(KnownTranslationKeys::INVALID_PORT, []); + } + + public static function ip_confirm() : Translatable{ + return new Translatable(KnownTranslationKeys::IP_CONFIRM, []); + } + + public static function ip_get() : Translatable{ + return new Translatable(KnownTranslationKeys::IP_GET, []); + } + + public static function ip_warning(Translatable|string $EXTERNAL_IP, Translatable|string $INTERNAL_IP) : Translatable{ + return new Translatable(KnownTranslationKeys::IP_WARNING, [ + "EXTERNAL_IP" => $EXTERNAL_IP, + "INTERNAL_IP" => $INTERNAL_IP, + ]); + } + + public static function item_record_11_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_11_DESC, []); + } + + public static function item_record_13_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_13_DESC, []); + } + + public static function item_record_blocks_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_BLOCKS_DESC, []); + } + + public static function item_record_cat_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_CAT_DESC, []); + } + + public static function item_record_chirp_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_CHIRP_DESC, []); + } + + public static function item_record_far_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_FAR_DESC, []); + } + + public static function item_record_mall_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_MALL_DESC, []); + } + + public static function item_record_mellohi_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_MELLOHI_DESC, []); + } + + public static function item_record_pigstep_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_PIGSTEP_DESC, []); + } + + public static function item_record_stal_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_STAL_DESC, []); + } + + public static function item_record_strad_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_STRAD_DESC, []); + } + + public static function item_record_wait_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_WAIT_DESC, []); + } + + public static function item_record_ward_desc() : Translatable{ + return new Translatable(KnownTranslationKeys::ITEM_RECORD_WARD_DESC, []); + } + + public static function kick_admin() : Translatable{ + return new Translatable(KnownTranslationKeys::KICK_ADMIN, []); + } + + public static function kick_admin_reason(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::KICK_ADMIN_REASON, [ + 0 => $param0, + ]); + } + + public static function kick_reason_cheat(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::KICK_REASON_CHEAT, [ + 0 => $param0, + ]); + } + + public static function language_name() : Translatable{ + return new Translatable(KnownTranslationKeys::LANGUAGE_NAME, []); + } + + public static function language_selected(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::LANGUAGE_SELECTED, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function language_has_been_selected() : Translatable{ + return new Translatable(KnownTranslationKeys::LANGUAGE_HAS_BEEN_SELECTED, []); + } + + public static function max_players() : Translatable{ + return new Translatable(KnownTranslationKeys::MAX_PLAYERS, []); + } + + public static function multiplayer_player_joined(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::MULTIPLAYER_PLAYER_JOINED, [ + 0 => $param0, + ]); + } + + public static function multiplayer_player_left(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::MULTIPLAYER_PLAYER_LEFT, [ + 0 => $param0, + ]); + } + + public static function name_your_server() : Translatable{ + return new Translatable(KnownTranslationKeys::NAME_YOUR_SERVER, []); + } + + public static function op_info() : Translatable{ + return new Translatable(KnownTranslationKeys::OP_INFO, []); + } + + public static function op_warning() : Translatable{ + return new Translatable(KnownTranslationKeys::OP_WARNING, []); + } + + public static function op_who() : Translatable{ + return new Translatable(KnownTranslationKeys::OP_WHO, []); + } + + public static function pocketmine_command_alias_illegal(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ALIAS_ILLEGAL, [ + 0 => $param0, + ]); + } + + public static function pocketmine_command_alias_notFound(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ALIAS_NOTFOUND, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_command_alias_recursive(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_ALIAS_RECURSIVE, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_command_ban_ip_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_BAN_IP_DESCRIPTION, []); + } + + public static function pocketmine_command_ban_player_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_BAN_PLAYER_DESCRIPTION, []); + } + + public static function pocketmine_command_banlist_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_BANLIST_DESCRIPTION, []); + } + + public static function pocketmine_command_clear_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CLEAR_DESCRIPTION, []); + } + + public static function pocketmine_command_clear_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_CLEAR_USAGE, []); + } + + public static function pocketmine_command_defaultgamemode_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DEFAULTGAMEMODE_DESCRIPTION, []); + } + + public static function pocketmine_command_deop_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DEOP_DESCRIPTION, []); + } + + public static function pocketmine_command_difficulty_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DIFFICULTY_DESCRIPTION, []); + } + + public static function pocketmine_command_effect_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_EFFECT_DESCRIPTION, []); + } + + public static function pocketmine_command_enchant_description() : Translatable{ + 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, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function pocketmine_command_gamemode_description() : Translatable{ + 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, + ]); + } + + public static function pocketmine_command_gc_cycles(Translatable|string $cyclesCollected) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_CYCLES, [ + "cyclesCollected" => $cyclesCollected, + ]); + } + + public static function pocketmine_command_gc_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_DESCRIPTION, []); + } + + public static function pocketmine_command_gc_entities(Translatable|string $entitiesCollected) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_ENTITIES, [ + "entitiesCollected" => $entitiesCollected, + ]); + } + + public static function pocketmine_command_gc_header() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_HEADER, []); + } + + public static function pocketmine_command_gc_memoryFreed(Translatable|string $memoryFreed) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GC_MEMORYFREED, [ + "memoryFreed" => $memoryFreed, + ]); + } + + public static function pocketmine_command_give_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GIVE_DESCRIPTION, []); + } + + public static function pocketmine_command_give_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_GIVE_USAGE, []); + } + + public static function pocketmine_command_help_description() : Translatable{ + 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, []); + } + + public static function pocketmine_command_kill_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_KILL_DESCRIPTION, []); + } + + public static function pocketmine_command_kill_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_KILL_USAGE, []); + } + + public static function pocketmine_command_list_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_LIST_DESCRIPTION, []); + } + + public static function pocketmine_command_me_description() : Translatable{ + 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, []); + } + + public static function pocketmine_command_particle_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_PARTICLE_DESCRIPTION, []); + } + + public static function pocketmine_command_particle_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_PARTICLE_USAGE, []); + } + + public static function pocketmine_command_plugins_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_PLUGINS_DESCRIPTION, []); + } + + public static function pocketmine_command_plugins_success(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_PLUGINS_SUCCESS, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_command_save_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SAVE_DESCRIPTION, []); + } + + public static function pocketmine_command_saveoff_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SAVEOFF_DESCRIPTION, []); + } + + public static function pocketmine_command_saveon_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SAVEON_DESCRIPTION, []); + } + + public static function pocketmine_command_say_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SAY_DESCRIPTION, []); + } + + public static function pocketmine_command_seed_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SEED_DESCRIPTION, []); + } + + public static function pocketmine_command_setworldspawn_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SETWORLDSPAWN_DESCRIPTION, []); + } + + public static function pocketmine_command_spawnpoint_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_SPAWNPOINT_DESCRIPTION, []); + } + + public static function pocketmine_command_status_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_STATUS_DESCRIPTION, []); + } + + public static function pocketmine_command_stop_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_STOP_DESCRIPTION, []); + } + + public static function pocketmine_command_tell_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TELL_DESCRIPTION, []); + } + + public static function pocketmine_command_time_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIME_DESCRIPTION, []); + } + + public static function pocketmine_command_time_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIME_USAGE, []); + } + + public static function pocketmine_command_timings_alreadyEnabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_ALREADYENABLED, []); + } + + public static function pocketmine_command_timings_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_DESCRIPTION, []); + } + + public static function pocketmine_command_timings_disable() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_DISABLE, []); + } + + public static function pocketmine_command_timings_enable() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_ENABLE, []); + } + + public static function pocketmine_command_timings_pasteError() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_PASTEERROR, []); + } + + public static function pocketmine_command_timings_reset() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_RESET, []); + } + + public static function pocketmine_command_timings_timingsDisabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_TIMINGSDISABLED, []); + } + + public static function pocketmine_command_timings_timingsRead(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_TIMINGSREAD, [ + 0 => $param0, + ]); + } + + public static function pocketmine_command_timings_timingsUpload(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_TIMINGSUPLOAD, [ + 0 => $param0, + ]); + } + + public static function pocketmine_command_timings_timingsWrite(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_TIMINGSWRITE, [ + 0 => $param0, + ]); + } + + public static function pocketmine_command_timings_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TIMINGS_USAGE, []); + } + + public static function pocketmine_command_title_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TITLE_DESCRIPTION, []); + } + + public static function pocketmine_command_tp_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TP_DESCRIPTION, []); + } + + public static function pocketmine_command_transferserver_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TRANSFERSERVER_DESCRIPTION, []); + } + + public static function pocketmine_command_transferserver_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_TRANSFERSERVER_USAGE, []); + } + + public static function pocketmine_command_unban_ip_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_UNBAN_IP_DESCRIPTION, []); + } + + public static function pocketmine_command_unban_player_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_UNBAN_PLAYER_DESCRIPTION, []); + } + + public static function pocketmine_command_version_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_DESCRIPTION, []); + } + + public static function pocketmine_command_version_minecraftVersion(Translatable|string $minecraftVersion, Translatable|string $minecraftProtocolVersion) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_MINECRAFTVERSION, [ + "minecraftVersion" => $minecraftVersion, + "minecraftProtocolVersion" => $minecraftProtocolVersion, + ]); + } + + public static function pocketmine_command_version_noSuchPlugin() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_NOSUCHPLUGIN, []); + } + + public static function pocketmine_command_version_operatingSystem(Translatable|string $operatingSystemName) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_OPERATINGSYSTEM, [ + "operatingSystemName" => $operatingSystemName, + ]); + } + + public static function pocketmine_command_version_phpJitDisabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_PHPJITDISABLED, []); + } + + public static function pocketmine_command_version_phpJitEnabled(Translatable|string $extraJitInfo) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_PHPJITENABLED, [ + "extraJitInfo" => $extraJitInfo, + ]); + } + + public static function pocketmine_command_version_phpJitNotSupported() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_PHPJITNOTSUPPORTED, []); + } + + public static function pocketmine_command_version_phpJitStatus(Translatable|string $jitStatus) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_PHPJITSTATUS, [ + "jitStatus" => $jitStatus, + ]); + } + + public static function pocketmine_command_version_phpVersion(Translatable|string $phpVersion) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_PHPVERSION, [ + "phpVersion" => $phpVersion, + ]); + } + + public static function pocketmine_command_version_serverSoftwareName(Translatable|string $serverSoftwareName) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_SERVERSOFTWARENAME, [ + "serverSoftwareName" => $serverSoftwareName, + ]); + } + + public static function pocketmine_command_version_serverSoftwareVersion(Translatable|string $serverSoftwareVersion, Translatable|string $serverGitHash) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_SERVERSOFTWAREVERSION, [ + "serverSoftwareVersion" => $serverSoftwareVersion, + "serverGitHash" => $serverGitHash, + ]); + } + + public static function pocketmine_command_version_usage() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_VERSION_USAGE, []); + } + + public static function pocketmine_command_whitelist_description() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_WHITELIST_DESCRIPTION, []); + } + + public static function pocketmine_crash_archive(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_CRASH_ARCHIVE, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_crash_create() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_CRASH_CREATE, []); + } + + public static function pocketmine_crash_error(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_CRASH_ERROR, [ + 0 => $param0, + ]); + } + + public static function pocketmine_crash_submit(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_CRASH_SUBMIT, [ + 0 => $param0, + ]); + } + + public static function pocketmine_data_playerCorrupted(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DATA_PLAYERCORRUPTED, [ + 0 => $param0, + ]); + } + + public static function pocketmine_data_playerNotFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DATA_PLAYERNOTFOUND, [ + 0 => $param0, + ]); + } + + public static function pocketmine_data_playerOld(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DATA_PLAYEROLD, [ + 0 => $param0, + ]); + } + + public static function pocketmine_data_saveError(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DATA_SAVEERROR, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_debug_enable() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DEBUG_ENABLE, []); + } + + public static function pocketmine_disconnect_incompatibleProtocol(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INCOMPATIBLEPROTOCOL, [ + 0 => $param0, + ]); + } + + public static function pocketmine_disconnect_invalidSession(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION, [ + 0 => $param0, + ]); + } + + public static function pocketmine_disconnect_invalidSession_badSignature() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE, []); + } + + public static function pocketmine_disconnect_invalidSession_missingKey() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY, []); + } + + public static function pocketmine_disconnect_invalidSession_tooEarly() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY, []); + } + + public static function pocketmine_disconnect_invalidSession_tooLate() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE, []); + } + + public static function pocketmine_level_ambiguousFormat(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_AMBIGUOUSFORMAT, [ + 0 => $param0, + ]); + } + + public static function pocketmine_level_backgroundGeneration(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_BACKGROUNDGENERATION, [ + 0 => $param0, + ]); + } + + public static function pocketmine_level_badDefaultFormat(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_BADDEFAULTFORMAT, [ + 0 => $param0, + ]); + } + + 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, []); + } + + public static function pocketmine_level_generationError(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_GENERATIONERROR, [ + 0 => $param0, + 1 => $param1, + ]); + } + + 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, + 1 => $param1, + ]); + } + + public static function pocketmine_level_notFound(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_NOTFOUND, [ + 0 => $param0, + ]); + } + + public static function pocketmine_level_preparing(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_PREPARING, [ + 0 => $param0, + ]); + } + + 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, + ]); + } + + public static function pocketmine_player_invalidMove(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLAYER_INVALIDMOVE, [ + 0 => $param0, + ]); + } + + public static function pocketmine_player_logIn(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3, Translatable|string $param4, Translatable|string $param5, Translatable|string $param6, Translatable|string $param7) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLAYER_LOGIN, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + 4 => $param4, + 5 => $param5, + 6 => $param6, + 7 => $param7, + ]); + } + + public static function pocketmine_player_logOut(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLAYER_LOGOUT, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + ]); + } + + 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, + ]); + } + + public static function pocketmine_plugin_ambiguousMinAPI(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_AMBIGUOUSMINAPI, [ + 0 => $param0, + ]); + } + + 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|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_COMMANDERROR, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function pocketmine_plugin_deprecatedEvent(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DEPRECATEDEVENT, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + ]); + } + + public static function pocketmine_plugin_disable(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_DISABLE, [ + 0 => $param0, + ]); + } + + 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_extensionNotLoaded(Translatable|string $extensionName) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_EXTENSIONNOTLOADED, [ + "extensionName" => $extensionName, + ]); + } + + public static function pocketmine_plugin_genericLoadError(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_GENERICLOADERROR, [ + 0 => $param0, + ]); + } + + public static function pocketmine_plugin_incompatibleAPI(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INCOMPATIBLEAPI, [ + 0 => $param0, + ]); + } + + 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, + ]); + } + + public static function pocketmine_plugin_incompatiblePhpVersion(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INCOMPATIBLEPHPVERSION, [ + 0 => $param0, + ]); + } + + public static function pocketmine_plugin_incompatibleProtocol(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_INCOMPATIBLEPROTOCOL, [ + 0 => $param0, + ]); + } + + 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, + ]); + } + + public static function pocketmine_plugin_loadError(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_LOADERROR, [ + 0 => $param0, + 1 => $param1, + ]); + } + + 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, []); + } + + public static function pocketmine_plugin_spacesDiscouraged(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_SPACESDISCOURAGED, [ + 0 => $param0, + ]); + } + + public static function pocketmine_plugin_unknownDependency(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_UNKNOWNDEPENDENCY, [ + 0 => $param0, + ]); + } + + public static function pocketmine_save_start() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SAVE_START, []); + } + + public static function pocketmine_save_success(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SAVE_SUCCESS, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_auth_disabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_AUTH_DISABLED, []); + } + + public static function pocketmine_server_auth_enabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_AUTH_ENABLED, []); + } + + public static function pocketmine_server_authProperty_disabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_AUTHPROPERTY_DISABLED, []); + } + + public static function pocketmine_server_authProperty_enabled() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_AUTHPROPERTY_ENABLED, []); + } + + public static function pocketmine_server_authWarning() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_AUTHWARNING, []); + } + + public static function pocketmine_server_defaultGameMode(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEFAULTGAMEMODE, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_devBuild_error1(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_ERROR1, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_devBuild_error2() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_ERROR2, []); + } + + public static function pocketmine_server_devBuild_error3() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_ERROR3, []); + } + + public static function pocketmine_server_devBuild_error4(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_ERROR4, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_devBuild_error5(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_ERROR5, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_devBuild_warning1(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_WARNING1, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_devBuild_warning2() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_WARNING2, []); + } + + public static function pocketmine_server_devBuild_warning3() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DEVBUILD_WARNING3, []); + } + + public static function pocketmine_server_donate(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_DONATE, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_info(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_INFO, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_server_info_extended(Translatable|string $param0, Translatable|string $param1, Translatable|string $param2, Translatable|string $param3) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_INFO_EXTENDED, [ + 0 => $param0, + 1 => $param1, + 2 => $param2, + 3 => $param3, + ]); + } + + public static function pocketmine_server_license(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_LICENSE, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_networkStart(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_NETWORKSTART, [ + 0 => $param0, + 1 => $param1, + ]); + } + + 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_query_running(Translatable|string $param0, Translatable|string $param1) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_QUERY_RUNNING, [ + 0 => $param0, + 1 => $param1, + ]); + } + + public static function pocketmine_server_start(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_START, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_startFinished(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_STARTFINISHED, [ + 0 => $param0, + ]); + } + + public static function pocketmine_server_tickOverload() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_TICKOVERLOAD, []); + } + + public static function pocketmine_plugins() : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGINS, []); + } + + public static function pocketmine_will_start(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_WILL_START, [ + 0 => $param0, + ]); + } + + public static function port_warning() : Translatable{ + return new Translatable(KnownTranslationKeys::PORT_WARNING, []); + } + + public static function potion_absorption() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_ABSORPTION, []); + } + + public static function potion_blindness() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_BLINDNESS, []); + } + + public static function potion_conduitPower() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_CONDUITPOWER, []); + } + + public static function potion_confusion() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_CONFUSION, []); + } + + public static function potion_damageBoost() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_DAMAGEBOOST, []); + } + + public static function potion_digSlowDown() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_DIGSLOWDOWN, []); + } + + public static function potion_digSpeed() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_DIGSPEED, []); + } + + public static function potion_fireResistance() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_FIRERESISTANCE, []); + } + + public static function potion_harm() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_HARM, []); + } + + public static function potion_heal() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_HEAL, []); + } + + public static function potion_healthBoost() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_HEALTHBOOST, []); + } + + public static function potion_hunger() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_HUNGER, []); + } + + public static function potion_invisibility() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_INVISIBILITY, []); + } + + public static function potion_jump() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_JUMP, []); + } + + public static function potion_levitation() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_LEVITATION, []); + } + + public static function potion_moveSlowdown() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_MOVESLOWDOWN, []); + } + + public static function potion_moveSpeed() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_MOVESPEED, []); + } + + public static function potion_nightVision() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_NIGHTVISION, []); + } + + public static function potion_poison() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_POISON, []); + } + + public static function potion_regeneration() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_REGENERATION, []); + } + + public static function potion_resistance() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_RESISTANCE, []); + } + + public static function potion_saturation() : Translatable{ + 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, []); + } + + public static function potion_weakness() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_WEAKNESS, []); + } + + public static function potion_wither() : Translatable{ + return new Translatable(KnownTranslationKeys::POTION_WITHER, []); + } + + public static function query_disable() : Translatable{ + return new Translatable(KnownTranslationKeys::QUERY_DISABLE, []); + } + + public static function query_warning1() : Translatable{ + return new Translatable(KnownTranslationKeys::QUERY_WARNING1, []); + } + + public static function query_warning2() : Translatable{ + return new Translatable(KnownTranslationKeys::QUERY_WARNING2, []); + } + + public static function server_port() : Translatable{ + return new Translatable(KnownTranslationKeys::SERVER_PORT, []); + } + + public static function server_properties() : Translatable{ + return new Translatable(KnownTranslationKeys::SERVER_PROPERTIES, []); + } + + public static function setting_up_server_now() : Translatable{ + return new Translatable(KnownTranslationKeys::SETTING_UP_SERVER_NOW, []); + } + + public static function skip_installer() : Translatable{ + return new Translatable(KnownTranslationKeys::SKIP_INSTALLER, []); + } + + public static function tile_bed_noSleep() : Translatable{ + return new Translatable(KnownTranslationKeys::TILE_BED_NOSLEEP, []); + } + + public static function tile_bed_occupied() : Translatable{ + return new Translatable(KnownTranslationKeys::TILE_BED_OCCUPIED, []); + } + + public static function tile_bed_tooFar() : Translatable{ + return new Translatable(KnownTranslationKeys::TILE_BED_TOOFAR, []); + } + + public static function welcome_to_pocketmine(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::WELCOME_TO_POCKETMINE, [ + 0 => $param0, + ]); + } + + public static function whitelist_enable() : Translatable{ + return new Translatable(KnownTranslationKeys::WHITELIST_ENABLE, []); + } + + public static function whitelist_info() : Translatable{ + return new Translatable(KnownTranslationKeys::WHITELIST_INFO, []); + } + + public static function whitelist_warning() : Translatable{ + return new Translatable(KnownTranslationKeys::WHITELIST_WARNING, []); + } + + public static function you_have_finished() : Translatable{ + return new Translatable(KnownTranslationKeys::YOU_HAVE_FINISHED, []); + } + + public static function you_have_to_accept_the_license(Translatable|string $param0) : Translatable{ + return new Translatable(KnownTranslationKeys::YOU_HAVE_TO_ACCEPT_THE_LICENSE, [ + 0 => $param0, + ]); + } + +} diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php new file mode 100644 index 0000000000..7b77513dc2 --- /dev/null +++ b/src/lang/KnownTranslationKeys.php @@ -0,0 +1,455 @@ + + * + * @throws LanguageNotFoundException */ public static function getLanguageList(string $path = "") : array{ if($path === ""){ - $path = \pocketmine\PATH . "src/pocketmine/lang/locale/"; + $path = \pocketmine\LOCALE_DATA_PATH; } if(is_dir($path)){ @@ -63,10 +66,10 @@ class BaseLang{ $result = []; foreach($files as $file){ - $strings = []; - self::loadLang($path . $file, $strings); + $code = explode(".", $file)[0]; + $strings = self::loadLang($path, $code); if(isset($strings["language.name"])){ - $result[substr($file, 0, -4)] = $strings["language.name"]; + $result[$code] = $strings["language.name"]; } } @@ -74,35 +77,39 @@ class BaseLang{ } } - return []; + throw new LanguageNotFoundException("Language directory $path does not exist or is not a directory"); } /** @var string */ protected $langName; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var array + */ protected $lang = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var array + */ protected $fallbackLang = []; - public function __construct(string $lang, string $path = null, string $fallback = self::FALLBACK_LANGUAGE){ - + /** + * @throws LanguageNotFoundException + */ + public function __construct(string $lang, ?string $path = null, string $fallback = self::FALLBACK_LANGUAGE){ $this->langName = strtolower($lang); if($path === null){ - $path = \pocketmine\PATH . "src/pocketmine/lang/locale/"; + $path = \pocketmine\LOCALE_DATA_PATH; } - if(!self::loadLang($file = $path . $this->langName . ".ini", $this->lang)){ - MainLogger::getLogger()->error("Missing required language file $file"); - } - if(!self::loadLang($file = $path . $fallback . ".ini", $this->fallbackLang)){ - MainLogger::getLogger()->error("Missing required language file $file"); - } + $this->lang = self::loadLang($path, $this->langName); + $this->fallbackLang = self::loadLang($path, $fallback); } public function getName() : string{ - return $this->get("language.name"); + return $this->get(KnownTranslationKeys::LANGUAGE_NAME); } public function getLang() : string{ @@ -110,75 +117,54 @@ class BaseLang{ } /** - * @param string[] $d reference parameter - * - * @return bool + * @return string[] + * @phpstan-return array */ - protected static function loadLang(string $path, array &$d){ - if(file_exists($path)){ - $d = array_map('\stripcslashes', parse_ini_file($path, false, INI_SCANNER_RAW)); - return true; - }else{ - return false; + 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)); } + + throw new LanguageNotFoundException("Language \"$languageCode\" not found"); } /** - * @param (float|int|string)[] $params + * @param (float|int|string|Translatable)[] $params */ - public function translateString(string $str, array $params = [], string $onlyPrefix = null) : string{ + 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); foreach($params as $i => $p){ - $baseText = str_replace("{%$i}", $this->parseTranslation((string) $p), $baseText); + $replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p; + $baseText = str_replace("{%$i}", $replacement, $baseText); } return $baseText; } - /** - * @return string - */ - public function translate(TextContainer $c){ - if($c instanceof TranslationContainer){ - $baseText = $this->internalGet($c->getText()); - $baseText = $this->parseTranslation($baseText ?? $c->getText()); + public function translate(Translatable $c) : string{ + $baseText = $this->internalGet($c->getText()); + $baseText = $this->parseTranslation($baseText ?? $c->getText()); - foreach($c->getParameters() as $i => $p){ - $baseText = str_replace("{%$i}", $this->parseTranslation($p), $baseText); - } - }else{ - $baseText = $this->parseTranslation($c->getText()); + foreach($c->getParameters() as $i => $p){ + $replacement = $p instanceof Translatable ? $this->translate($p) : $p; + $baseText = str_replace("{%$i}", $replacement, $baseText); } return $baseText; } - /** - * @return string|null - */ - public function internalGet(string $id){ - if(isset($this->lang[$id])){ - return $this->lang[$id]; - }elseif(isset($this->fallbackLang[$id])){ - return $this->fallbackLang[$id]; - } - - return null; + protected function internalGet(string $id) : ?string{ + return $this->lang[$id] ?? $this->fallbackLang[$id] ?? null; } public function get(string $id) : string{ - if(isset($this->lang[$id])){ - return $this->lang[$id]; - }elseif(isset($this->fallbackLang[$id])){ - return $this->fallbackLang[$id]; - } - - return $id; + return $this->internalGet($id) ?? $id; } - protected function parseTranslation(string $text, string $onlyPrefix = null) : string{ + protected function parseTranslation(string $text, ?string $onlyPrefix = null) : string{ $newString = ""; $replaceString = null; diff --git a/src/lang/LanguageNotFoundException.php b/src/lang/LanguageNotFoundException.php new file mode 100644 index 0000000000..8d8b39b5b2 --- /dev/null +++ b/src/lang/LanguageNotFoundException.php @@ -0,0 +1,28 @@ +text = $text; - $this->setParameters($params); + foreach($params as $k => $param){ + if(!($param instanceof Translatable)){ + $this->params[$k] = (string) $param; + }else{ + $this->params[$k] = $param; + } + } + } + + public function getText() : string{ + return $this->text; } /** - * @return string[] + * @return string[]|Translatable[] */ public function getParameters() : array{ return $this->params; } - /** - * @return string|null - */ - public function getParameter(int $i){ + public function getParameter(int|string $i) : Translatable|string|null{ return $this->params[$i] ?? null; } - /** - * @return void - */ - public function setParameter(int $i, string $str){ - if($i < 0 or $i > count($this->params)){ //Intended, allow to set the last - throw new \InvalidArgumentException("Invalid index $i, have " . count($this->params)); - } - - $this->params[$i] = $str; + public function format(string $before, string $after) : self{ + return new self("$before%$this->text$after", $this->params); } - /** - * @param (float|int|string)[] $params - * - * @return void - */ - public function setParameters(array $params){ - $i = 0; - foreach($params as $str){ - $this->params[$i] = (string) $str; + public function prefix(string $prefix) : self{ + return new self("$prefix%$this->text", $this->params); + } - ++$i; - } + public function postfix(string $postfix) : self{ + return new self("%$this->text" . $postfix); } } diff --git a/src/pocketmine/network/AdvancedSourceInterface.php b/src/network/AdvancedNetworkInterface.php similarity index 75% rename from src/pocketmine/network/AdvancedSourceInterface.php rename to src/network/AdvancedNetworkInterface.php index f3e0b899ff..db18361b31 100644 --- a/src/pocketmine/network/AdvancedSourceInterface.php +++ b/src/network/AdvancedNetworkInterface.php @@ -30,34 +30,30 @@ namespace pocketmine\network; * Advanced network interfaces have some additional capabilities, such as being able to ban addresses and process raw * packets. */ -interface AdvancedSourceInterface extends SourceInterface{ +interface AdvancedNetworkInterface extends NetworkInterface{ /** * Prevents packets received from the IP address getting processed for the given timeout. * * @param int $timeout Seconds - * - * @return void */ - public function blockAddress(string $address, int $timeout = 300); + public function blockAddress(string $address, int $timeout = 300) : void; /** * Unblocks a previously-blocked address. - * - * @return void */ - public function unblockAddress(string $address); + public function unblockAddress(string $address) : void; - /** - * @return void - */ - public function setNetwork(Network $network); + public function setNetwork(Network $network) : void; /** * Sends a raw payload to the network interface, bypassing any sessions. - * - * @return void */ - public function sendRawPacket(string $address, int $port, string $payload); + public function sendRawPacket(string $address, int $port, string $payload) : void; + /** + * Adds a regex filter for raw packets to this network interface. This filter should be used to check validity of + * raw packets before relaying them to the main thread. + */ + public function addRawPacketFilter(string $regex) : void; } diff --git a/src/network/BandwidthStatsTracker.php b/src/network/BandwidthStatsTracker.php new file mode 100644 index 0000000000..5d0b6e6c1c --- /dev/null +++ b/src/network/BandwidthStatsTracker.php @@ -0,0 +1,75 @@ +history = array_fill(0, $historySize, 0); + } + + public function add(int $bytes) : void{ + $this->totalBytes += $bytes; + $this->bytesSinceLastRotation += $bytes; + } + + public function getTotalBytes() : int{ return $this->totalBytes; } + + /** + * Adds the bytes tracked since the last rotation to the history array, overwriting an old entry. + * This should be called on a regular interval that you want to collect average measurements over + * (e.g. if you want bytes per second, call this every second). + */ + public function rotateHistory() : void{ + $this->history[$this->nextHistoryIndex] = $this->bytesSinceLastRotation; + $this->bytesSinceLastRotation = 0; + $this->nextHistoryIndex = ($this->nextHistoryIndex + 1) % count($this->history); + } + + /** + * Returns the average of all the tracked history values. + */ + public function getAverageBytes() : float{ + return array_sum($this->history) / count($this->history); + } + + public function resetHistory() : void{ + $this->history = array_fill(0, count($this->history), 0); + } +} diff --git a/src/network/BidirectionalBandwidthStatsTracker.php b/src/network/BidirectionalBandwidthStatsTracker.php new file mode 100644 index 0000000000..f9ed1a5645 --- /dev/null +++ b/src/network/BidirectionalBandwidthStatsTracker.php @@ -0,0 +1,60 @@ +send = new BandwidthStatsTracker($historySize); + $this->receive = new BandwidthStatsTracker($historySize); + } + + public function getSend() : BandwidthStatsTracker{ return $this->send; } + + public function getReceive() : BandwidthStatsTracker{ return $this->receive; } + + public function add(int $sendBytes, int $recvBytes) : void{ + $this->send->add($sendBytes); + $this->receive->add($recvBytes); + } + + /** @see BandwidthStatsTracker::rotateHistory() */ + public function rotateAverageHistory() : void{ + $this->send->rotateHistory(); + $this->receive->rotateHistory(); + } + + /** @see BandwidthStatsTracker::resetHistory() */ + public function resetHistory() : void{ + $this->send->resetHistory(); + $this->receive->resetHistory(); + } +} diff --git a/src/network/Network.php b/src/network/Network.php new file mode 100644 index 0000000000..fc65e3acac --- /dev/null +++ b/src/network/Network.php @@ -0,0 +1,215 @@ +sessionManager = new NetworkSessionManager(); + $this->logger = $logger; + $this->bandwidthTracker = new BidirectionalBandwidthStatsTracker(5); + } + + public function getBandwidthTracker() : BidirectionalBandwidthStatsTracker{ return $this->bandwidthTracker; } + + /** + * @return NetworkInterface[] + */ + public function getInterfaces() : array{ + return $this->interfaces; + } + + public function getSessionManager() : NetworkSessionManager{ + return $this->sessionManager; + } + + public function getConnectionCount() : int{ + return $this->sessionManager->getSessionCount(); + } + + public function tick() : void{ + foreach($this->interfaces as $interface){ + $interface->tick(); + } + + $this->sessionManager->tick(); + } + + /** + * @throws NetworkInterfaceStartException + */ + public function registerInterface(NetworkInterface $interface) : bool{ + $ev = new NetworkInterfaceRegisterEvent($interface); + $ev->call(); + if(!$ev->isCancelled()){ + $interface->start(); + $this->interfaces[$hash = spl_object_id($interface)] = $interface; + if($interface instanceof AdvancedNetworkInterface){ + $this->advancedInterfaces[$hash] = $interface; + $interface->setNetwork($this); + foreach($this->bannedIps as $ip => $until){ + $interface->blockAddress($ip); + } + foreach($this->rawPacketHandlers as $handler){ + $interface->addRawPacketFilter($handler->getPattern()); + } + } + $interface->setName($this->name); + return true; + } + return false; + } + + /** + * @throws \InvalidArgumentException + */ + public function unregisterInterface(NetworkInterface $interface) : void{ + if(!isset($this->interfaces[$hash = spl_object_id($interface)])){ + throw new \InvalidArgumentException("Interface " . get_class($interface) . " is not registered on this network"); + } + (new NetworkInterfaceUnregisterEvent($interface))->call(); + unset($this->interfaces[$hash], $this->advancedInterfaces[$hash]); + $interface->shutdown(); + } + + /** + * Sets the server name shown on each interface Query + */ + public function setName(string $name) : void{ + $this->name = $name; + foreach($this->interfaces as $interface){ + $interface->setName($this->name); + } + } + + public function getName() : string{ + return $this->name; + } + + public function updateName() : void{ + foreach($this->interfaces as $interface){ + $interface->setName($this->name); + } + } + + public function sendPacket(string $address, int $port, string $payload) : void{ + foreach($this->advancedInterfaces as $interface){ + $interface->sendRawPacket($address, $port, $payload); + } + } + + /** + * Blocks an IP address from the main interface. Setting timeout to -1 will block it forever + */ + public function blockAddress(string $address, int $timeout = 300) : void{ + $this->bannedIps[$address] = $timeout > 0 ? time() + $timeout : PHP_INT_MAX; + foreach($this->advancedInterfaces as $interface){ + $interface->blockAddress($address, $timeout); + } + } + + public function unblockAddress(string $address) : void{ + unset($this->bannedIps[$address]); + foreach($this->advancedInterfaces as $interface){ + $interface->unblockAddress($address); + } + } + + /** + * Registers a raw packet handler on the network. + */ + public function registerRawPacketHandler(RawPacketHandler $handler) : void{ + $this->rawPacketHandlers[spl_object_id($handler)] = $handler; + + $regex = $handler->getPattern(); + foreach($this->advancedInterfaces as $interface){ + $interface->addRawPacketFilter($regex); + } + } + + /** + * Unregisters a previously-registered raw packet handler. + */ + public function unregisterRawPacketHandler(RawPacketHandler $handler) : void{ + unset($this->rawPacketHandlers[spl_object_id($handler)]); + } + + public function processRawPacket(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : void{ + if(isset($this->bannedIps[$address]) and time() < $this->bannedIps[$address]){ + $this->logger->debug("Dropped raw packet from banned address $address $port"); + return; + } + $handled = false; + foreach($this->rawPacketHandlers as $handler){ + if(preg_match($handler->getPattern(), $packet) === 1){ + try{ + $handled = $handler->handle($interface, $address, $port, $packet); + }catch(PacketHandlingException $e){ + $handled = true; + $this->logger->error("Bad raw packet from /$address:$port: " . $e->getMessage()); + $this->blockAddress($address, 600); + break; + } + } + } + if(!$handled){ + $this->logger->debug("Unhandled raw packet from /$address:$port: " . base64_encode($packet)); + } + } +} diff --git a/src/pocketmine/network/SourceInterface.php b/src/network/NetworkInterface.php similarity index 56% rename from src/pocketmine/network/SourceInterface.php rename to src/network/NetworkInterface.php index 168f68b221..a6beddbaeb 100644 --- a/src/pocketmine/network/SourceInterface.php +++ b/src/network/NetworkInterface.php @@ -26,58 +26,26 @@ declare(strict_types=1); */ namespace pocketmine\network; -use pocketmine\network\mcpe\protocol\DataPacket; -use pocketmine\Player; - /** * Network interfaces are transport layers which can be used to transmit packets between the server and clients. */ -interface SourceInterface{ +interface NetworkInterface{ /** * Performs actions needed to start the interface after it is registered. - * - * @return void + * @throws NetworkInterfaceStartException */ - public function start(); + public function start() : void; - /** - * Sends a DataPacket to the interface, returns an unique identifier for the packet if $needACK is true - * - * @return int|null - */ - public function putPacket(Player $player, DataPacket $packet, bool $needACK = false, bool $immediate = true); - - /** - * Terminates the connection - * - * @return void - */ - public function close(Player $player, string $reason = "unknown reason"); - - /** - * @return void - */ - public function setName(string $name); + public function setName(string $name) : void; /** * Called every tick to process events on the interface. */ - public function process() : void; + public function tick() : void; /** * Gracefully shuts down the network interface. - * - * @return void */ - public function shutdown(); - - /** - * @deprecated - * Shuts down the network interface in an emergency situation, such as due to a crash. - * - * @return void - */ - public function emergencyShutdown(); - + public function shutdown() : void; } diff --git a/src/network/NetworkInterfaceStartException.php b/src/network/NetworkInterfaceStartException.php new file mode 100644 index 0000000000..3dfd2b0a7f --- /dev/null +++ b/src/network/NetworkInterfaceStartException.php @@ -0,0 +1,32 @@ +sessions[$idx] = $session; + } + + /** + * Removes the given network session, due to disconnect. This should only be called by a network session on + * disconnection. + */ + public function remove(NetworkSession $session) : void{ + $idx = spl_object_id($session); + unset($this->sessions[$idx]); + } + + /** + * Returns the number of known connected sessions. + */ + public function getSessionCount() : int{ + return count($this->sessions); + } + + /** @return NetworkSession[] */ + public function getSessions() : array{ return $this->sessions; } + + /** + * Updates all sessions which need it. + */ + public function tick() : void{ + foreach($this->sessions as $k => $session){ + $session->tick(); + if(!$session->isConnected()){ + unset($this->sessions[$k]); + } + } + } + + /** + * Terminates all connected sessions with the given reason. + */ + public function close(string $reason = "") : void{ + foreach($this->sessions as $session){ + $session->disconnect($reason); + } + $this->sessions = []; + } +} diff --git a/src/network/PacketHandlingException.php b/src/network/PacketHandlingException.php new file mode 100644 index 0000000000..9b305430df --- /dev/null +++ b/src/network/PacketHandlingException.php @@ -0,0 +1,35 @@ +getMessage(), 0, $previous); + } +} diff --git a/src/pocketmine/level/generator/object/BigTree.php b/src/network/RawPacketHandler.php similarity index 63% rename from src/pocketmine/level/generator/object/BigTree.php rename to src/network/RawPacketHandler.php index 0e1951f5f7..17f221827c 100644 --- a/src/pocketmine/level/generator/object/BigTree.php +++ b/src/network/RawPacketHandler.php @@ -21,24 +21,18 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\network; -use pocketmine\level\ChunkManager; -use pocketmine\utils\Random; - -/** - * @deprecated - */ -class BigTree extends Tree{ - - public function canPlaceObject(ChunkManager $level, int $x, int $y, int $z, Random $random) : bool{ - return false; - } +interface RawPacketHandler{ /** - * @return void + * Returns a preg_match() compatible regex pattern used to filter packets on this handler. Only packets matching + * this pattern will be delivered to the handler. */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z, Random $random){ + public function getPattern() : string; - } + /** + * @throws PacketHandlingException + */ + public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool; } diff --git a/src/network/mcpe/ChunkRequestTask.php b/src/network/mcpe/ChunkRequestTask.php new file mode 100644 index 0000000000..c4ae8b5da1 --- /dev/null +++ b/src/network/mcpe/ChunkRequestTask.php @@ -0,0 +1,94 @@ +compressor = $compressor; + + $this->chunk = FastChunkSerializer::serializeTerrain($chunk); + $this->chunkX = $chunkX; + $this->chunkZ = $chunkZ; + $this->tiles = ChunkSerializer::serializeTiles($chunk); + + $this->storeLocal(self::TLS_KEY_PROMISE, $promise); + $this->storeLocal(self::TLS_KEY_ERROR_HOOK, $onError); + } + + public function onRun() : void{ + $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::create($this->chunkX, $this->chunkZ, $subCount, null, $payload))->getBuffer())); + } + + public function onError() : void{ + /** + * @var \Closure|null $hook + * @phpstan-var (\Closure() : void)|null $hook + */ + $hook = $this->fetchLocal(self::TLS_KEY_ERROR_HOOK); + if($hook !== null){ + $hook(); + } + } + + public function onCompletion() : void{ + /** @var CompressBatchPromise $promise */ + $promise = $this->fetchLocal(self::TLS_KEY_PROMISE); + $promise->resolve($this->getResult()); + } +} diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php new file mode 100644 index 0000000000..2c3fff30c8 --- /dev/null +++ b/src/network/mcpe/InventoryManager.php @@ -0,0 +1,319 @@ +|null) + */ +class InventoryManager{ + + //TODO: HACK! + //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 HARDCODED_INVENTORY_WINDOW_ID = self::RESERVED_WINDOW_ID_RANGE_START + 2; + + /** @var Player */ + private $player; + /** @var NetworkSession */ + private $session; + + /** @var Inventory[] */ + private $windowMap = []; + /** @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> + */ + private $initiatedSlotChanges = []; + /** @var int */ + private $clientSelectedHotbarSlot = -1; + + /** @phpstan-var ObjectSet */ + private ObjectSet $containerOpenCallbacks; + + public function __construct(Player $player, NetworkSession $session){ + $this->player = $player; + $this->session = $session; + + $this->containerOpenCallbacks = new ObjectSet(); + $this->containerOpenCallbacks->add(\Closure::fromCallable([self::class, 'createContainerOpen'])); + + $this->add(ContainerIds::INVENTORY, $this->player->getInventory()); + $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory()); + $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); + $this->add(ContainerIds::UI, $this->player->getCursorInventory()); + + $this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{ + $this->syncSelectedHotbarSlot(); + }); + } + + private function add(int $id, Inventory $inventory) : void{ + $this->windowMap[$id] = $inventory; + } + + private function remove(int $id) : void{ + unset($this->windowMap[$id], $this->initiatedSlotChanges[$id]); + } + + public function getWindowId(Inventory $inventory) : ?int{ + return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null; + } + + public function getCurrentWindowId() : int{ + return $this->lastInventoryNetworkId; + } + + public function getWindow(int $windowId) : ?Inventory{ + return $this->windowMap[$windowId] ?? null; + } + + public function onTransactionStart(InventoryTransaction $tx) : void{ + foreach($tx->getActions() as $action){ + if($action instanceof SlotChangeAction and ($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(); + } + } + } + + public function onCurrentWindowChange(Inventory $inventory) : void{ + $this->onCurrentWindowRemove(); + $this->add($this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % self::RESERVED_WINDOW_ID_RANGE_START), $inventory); + + foreach($this->containerOpenCallbacks as $callback){ + $pks = $callback($this->lastInventoryNetworkId, $inventory); + if($pks !== null){ + foreach($pks as $pk){ + $this->session->sendDataPacket($pk); + } + $this->syncContents($inventory); + return; + } + } + throw new \LogicException("Unsupported inventory type"); + } + + /** @phpstan-return ObjectSet */ + public function getContainerOpenCallbacks() : ObjectSet{ return $this->containerOpenCallbacks; } + + /** + * @return ClientboundPacket[]|null + * @phpstan-return list|null + */ + protected static function createContainerOpen(int $id, Inventory $inv) : ?array{ + //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){ + $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") + }, + $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); + $this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, true)); + } + } + + public function onClientRemoveWindow(int $id) : void{ + 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{ + $windowId = $this->getWindowId($inventory); + if($windowId !== null){ + $currentItem = $inventory->getItem($slot); + $clientSideItem = $this->initiatedSlotChanges[$windowId][$slot] ?? null; + if($clientSideItem === null or !$clientSideItem->equalsExact($currentItem)){ + $itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem)); + if($windowId === ContainerIds::OFFHAND){ + //TODO: HACK! + //The client may sometimes ignore the InventorySlotPacket for the offhand slot. + //This can cause a lot of problems (totems, arrows, and more...). + //The workaround is to send an InventoryContentPacket instead + //BDS (Bedrock Dedicated Server) also seems to work this way. + $this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper])); + }else{ + $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $slot, $itemStackWrapper)); + } + } + unset($this->initiatedSlotChanges[$windowId][$slot]); + } + } + + public function syncContents(Inventory $inventory) : void{ + $windowId = $this->getWindowId($inventory); + if($windowId !== null){ + unset($this->initiatedSlotChanges[$windowId]); + $typeConverter = TypeConverter::getInstance(); + if($windowId === ContainerIds::UI){ + //TODO: HACK! + //Since 1.13, cursor is now part of a larger "UI inventory", and sending contents for this larger inventory does + //not work the way it's intended to. Even if it did, it would be necessary to send all 51 slots just to update + //this one, which is just not worth it. + //This workaround isn't great, but it's at least simple. + $this->session->sendDataPacket(InventorySlotPacket::create( + $windowId, + 0, + ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem(0))) + )); + }else{ + $this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{ + return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack)); + }, $inventory->getContents(true)))); + } + } + } + + public function syncAll() : void{ + foreach($this->windowMap as $inventory){ + $this->syncContents($inventory); + } + } + + public function syncData(Inventory $inventory, int $propertyId, int $value) : void{ + $windowId = $this->getWindowId($inventory); + if($windowId !== null){ + $this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value)); + } + } + + public function onClientSelectHotbarSlot(int $slot) : void{ + $this->clientSelectedHotbarSlot = $slot; + } + + public function syncSelectedHotbarSlot() : void{ + $selected = $this->player->getInventory()->getHeldItemIndex(); + if($selected !== $this->clientSelectedHotbarSlot){ + $this->session->sendDataPacket(MobEquipmentPacket::create( + $this->player->getId(), + ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())), + $selected, + $selected, + ContainerIds::INVENTORY + )); + $this->clientSelectedHotbarSlot = $selected; + } + } + + public function syncCreative() : void{ + $typeConverter = TypeConverter::getInstance(); + + $nextEntryId = 1; + $this->session->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use($typeConverter, &$nextEntryId) : CreativeContentEntry{ + return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item)); + }, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll()))); + } +} diff --git a/src/network/mcpe/JwtException.php b/src/network/mcpe/JwtException.php new file mode 100644 index 0000000000..14a3c21a23 --- /dev/null +++ b/src/network/mcpe/JwtException.php @@ -0,0 +1,28 @@ + gmp_strval(gmp_import($str, 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST), 10); + + $sequence = new Sequence( + new Integer($convert($rString)), + new Integer($convert($sString)) + ); + + $v = openssl_verify( + $header . '.' . $body, + $sequence->getBinary(), + $signingKey, + OPENSSL_ALGO_SHA384 + ); + switch($v){ + case 0: return false; + case 1: return true; + case -1: throw new JwtException("Error verifying JWT signature: " . openssl_error_string()); + default: throw new AssumptionFailedError("openssl_verify() should only return -1, 0 or 1"); + } + } + + /** + * @phpstan-param array $header + * @phpstan-param array $claims + */ + public static function create(array $header, array $claims, \OpenSSLAsymmetricKey $signingKey) : string{ + $jwtBody = JwtUtils::b64UrlEncode(json_encode($header, JSON_THROW_ON_ERROR)) . "." . JwtUtils::b64UrlEncode(json_encode($claims, JSON_THROW_ON_ERROR)); + + openssl_sign( + $jwtBody, + $rawDerSig, + $signingKey, + OPENSSL_ALGO_SHA384 + ); + + try{ + $asnObject = Sequence::fromBinary($rawDerSig); + }catch(ParserException $e){ + throw new AssumptionFailedError("Failed to parse OpenSSL signature: " . $e->getMessage(), 0, $e); + } + if(count($asnObject) !== 2){ + throw new AssumptionFailedError("OpenSSL produced invalid signature, expected exactly 2 parts"); + } + [$r, $s] = [$asnObject[0], $asnObject[1]]; + if(!($r instanceof Integer) || !($s instanceof Integer)){ + throw new AssumptionFailedError("OpenSSL produced invalid signature, expected 2 INTEGER parts"); + } + $rString = $r->getContent(); + $sString = $s->getContent(); + + $toBinary = fn($str) => str_pad( + gmp_export(gmp_init($str, 10), 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST), + 48, + "\x00", + STR_PAD_LEFT + ); + $jwtSig = JwtUtils::b64UrlEncode($toBinary($rString) . $toBinary($sString)); + + return "$jwtBody.$jwtSig"; + } + + public static function b64UrlEncode(string $str) : string{ + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); + } + + public static function b64UrlDecode(string $str) : string{ + if(($len = strlen($str) % 4) !== 0){ + $str .= str_repeat('=', 4 - $len); + } + $decoded = base64_decode(strtr($str, '-_', '+/'), true); + if($decoded === false){ + throw new JwtException("Malformed base64url encoded payload could not be decoded"); + } + return $decoded; + } + + 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"); + } + + /** @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){ + $derKey = base64_decode(str_replace("\n", "", $matches[1]), true); + if($derKey !== false){ + return $derKey; + } + } + throw new AssumptionFailedError("OpenSSL resource contains invalid public key"); + } + + public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{ + $signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey))); + if($signingKeyOpenSSL === false){ + throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string()); + } + return $signingKeyOpenSSL; + } +} diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php new file mode 100644 index 0000000000..2fe44e3acb --- /dev/null +++ b/src/network/mcpe/NetworkSession.php @@ -0,0 +1,1075 @@ + + */ + private \SplQueue $compressedQueue; + private Compressor $compressor; + private bool $forceAsyncCompression = true; + + private PacketPool $packetPool; + private PacketSerializerContext $packetSerializerContext; + + private ?InventoryManager $invManager = null; + + private PacketSender $sender; + + private PacketBroadcaster $broadcaster; + + /** + * @var \Closure[]|ObjectSet + * @phpstan-var ObjectSet<\Closure() : void> + */ + private ObjectSet $disposeHooks; + + public function __construct(Server $server, NetworkSessionManager $manager, PacketPool $packetPool, PacketSender $sender, PacketBroadcaster $broadcaster, Compressor $compressor, string $ip, int $port){ + $this->server = $server; + $this->manager = $manager; + $this->sender = $sender; + $this->broadcaster = $broadcaster; + $this->ip = $ip; + $this->port = $port; + + $this->logger = new \PrefixedLogger($this->server->getLogger(), $this->getLogPrefix()); + + $this->compressedQueue = new \SplQueue(); + $this->compressor = $compressor; + $this->packetPool = $packetPool; + + //TODO: allow this to be injected + $this->packetSerializerContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary()); + + $this->disposeHooks = new ObjectSet(); + + $this->connectTime = time(); + + $this->setHandler(new LoginPacketHandler( + $this->server, + $this, + function(PlayerInfo $info) : void{ + $this->info = $info; + $this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET); + $this->logger->setPrefix($this->getLogPrefix()); + }, + function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{ + $this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey); + } + )); + + $this->manager->add($this); + $this->logger->info("Session opened"); + } + + private function getLogPrefix() : string{ + return "NetworkSession: " . $this->getDisplayName(); + } + + public function getLogger() : \Logger{ + return $this->logger; + } + + protected function createPlayer() : void{ + $this->server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion( + \Closure::fromCallable([$this, 'onPlayerCreated']), + fn() => $this->disconnect("Player creation failed") //TODO: this should never actually occur... right? + ); + } + + private function onPlayerCreated(Player $player) : void{ + if(!$this->isConnected()){ + //the remote player might have disconnected before spawn terrain generation was finished + return; + } + $this->player = $player; + if(!$this->server->addOnlinePlayer($player)){ + return; + } + + $this->invManager = new InventoryManager($this->player, $this); + + $effectManager = $this->player->getEffects(); + $effectManager->getEffectAddHooks()->add($effectAddHook = function(EffectInstance $effect, bool $replacesOldEffect) : void{ + $this->onEntityEffectAdded($this->player, $effect, $replacesOldEffect); + }); + $effectManager->getEffectRemoveHooks()->add($effectRemoveHook = function(EffectInstance $effect) : void{ + $this->onEntityEffectRemoved($this->player, $effect); + }); + $this->disposeHooks->add(static function() use ($effectManager, $effectAddHook, $effectRemoveHook) : void{ + $effectManager->getEffectAddHooks()->remove($effectAddHook); + $effectManager->getEffectRemoveHooks()->remove($effectRemoveHook); + }); + + $permissionHooks = $this->player->getPermissionRecalculationCallbacks(); + $permissionHooks->add($permHook = function() : void{ + $this->logger->debug("Syncing available commands and adventure settings due to permission recalculation"); + $this->syncAdventureSettings($this->player); + $this->syncAvailableCommands(); + }); + $this->disposeHooks->add(static function() use ($permissionHooks, $permHook) : void{ + $permissionHooks->remove($permHook); + }); + $this->beginSpawnSequence(); + } + + public function getPlayer() : ?Player{ + return $this->player; + } + + public function getPlayerInfo() : ?PlayerInfo{ + return $this->info; + } + + public function isConnected() : bool{ + return $this->connected && !$this->disconnectGuard; + } + + public function getIp() : string{ + return $this->ip; + } + + public function getPort() : int{ + return $this->port; + } + + public function getDisplayName() : string{ + return $this->info !== null ? $this->info->getUsername() : $this->ip . " " . $this->port; + } + + /** + * Returns the last recorded ping measurement for this session, in milliseconds, or null if a ping measurement has not yet been recorded. + */ + public function getPing() : ?int{ + return $this->ping; + } + + /** + * @internal Called by the network interface to update last recorded ping measurements. + */ + public function updatePing(int $ping) : void{ + $this->ping = $ping; + } + + public function getHandler() : ?PacketHandler{ + return $this->handler; + } + + public function setHandler(?PacketHandler $handler) : void{ + if($this->connected){ //TODO: this is fine since we can't handle anything from a disconnected session, but it might produce surprises in some cases + $this->handler = $handler; + if($this->handler !== null){ + $this->handler->setUp(); + } + } + } + + /** + * @throws PacketHandlingException + */ + public function handleEncoded(string $payload) : void{ + if(!$this->connected){ + return; + } + + if($this->cipher !== null){ + Timings::$playerNetworkReceiveDecrypt->startTiming(); + try{ + $payload = $this->cipher->decrypt($payload); + }catch(DecryptionException $e){ + $this->logger->debug("Encrypted packet: " . base64_encode($payload)); + throw PacketHandlingException::wrap($e, "Packet decryption error"); + }finally{ + Timings::$playerNetworkReceiveDecrypt->stopTiming(); + } + } + + Timings::$playerNetworkReceiveDecompress->startTiming(); + try{ + $stream = new PacketBatch($this->compressor->decompress($payload)); + }catch(DecompressionException $e){ + $this->logger->debug("Failed to decompress packet: " . base64_encode($payload)); + throw PacketHandlingException::wrap($e, "Compressed packet batch decode error"); + }finally{ + Timings::$playerNetworkReceiveDecompress->stopTiming(); + } + + 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){ + $this->logger->debug($packet->getName() . ": " . base64_encode($buffer)); + throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName()); + } + } + }catch(PacketDecodeException $e){ + $this->logger->logException($e); + throw PacketHandlingException::wrap($e, "Packet batch decode error"); + } + } + + /** + * @throws PacketHandlingException + */ + public function handleDataPacket(Packet $packet, string $buffer) : void{ + if(!($packet instanceof ServerboundPacket)){ + throw new PacketHandlingException("Unexpected non-serverbound packet"); + } + + $timings = Timings::getReceiveDataPacketTimings($packet); + $timings->startTiming(); + + try{ + $stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext); + try{ + $packet->decode($stream); + }catch(PacketDecodeException $e){ + throw PacketHandlingException::wrap($e); + } + if(!$stream->feof()){ + $remains = substr($stream->getBuffer(), $stream->getOffset()); + $this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains)); + } + + $ev = new DataPacketReceiveEvent($this, $packet); + $ev->call(); + if(!$ev->isCancelled() and ($this->handler === null or !$packet->handle($this->handler))){ + $this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer())); + } + }finally{ + $timings->stopTiming(); + } + } + + public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{ + //Basic safety restriction. TODO: improve this + if(!$this->loggedIn and !$packet->canBeSentBeforeLogin()){ + throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getDisplayName() . " too early"); + } + + $timings = Timings::getSendDataPacketTimings($packet); + $timings->startTiming(); + try{ + $ev = new DataPacketSendEvent([$this], [$packet]); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $this->addToSendBuffer($packet); + if($immediate){ + $this->flushSendBuffer(true); + } + + return true; + }finally{ + $timings->stopTiming(); + } + } + + /** + * @internal + */ + public function addToSendBuffer(ClientboundPacket $packet) : void{ + $timings = Timings::getSendDataPacketTimings($packet); + $timings->startTiming(); + try{ + $this->sendBuffer[] = $packet; + }finally{ + $timings->stopTiming(); + } + } + + private function flushSendBuffer(bool $immediate = false) : void{ + if(count($this->sendBuffer) > 0){ + $syncMode = null; //automatic + if($immediate){ + $syncMode = true; + }elseif($this->forceAsyncCompression){ + $syncMode = false; + } + $promise = $this->server->prepareBatch(PacketBatch::fromPackets($this->packetSerializerContext, ...$this->sendBuffer), $this->compressor, $syncMode); + $this->sendBuffer = []; + $this->queueCompressedNoBufferFlush($promise, $immediate); + } + } + + public function getPacketSerializerContext() : PacketSerializerContext{ return $this->packetSerializerContext; } + + public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; } + + public function getCompressor() : Compressor{ + return $this->compressor; + } + + public function queueCompressed(CompressBatchPromise $payload, bool $immediate = false) : void{ + $this->flushSendBuffer($immediate); //Maintain ordering if possible + $this->queueCompressedNoBufferFlush($payload, $immediate); + } + + private function queueCompressedNoBufferFlush(CompressBatchPromise $payload, bool $immediate = false) : void{ + if($immediate){ + //Skips all queues + $this->sendEncoded($payload->getResult(), true); + }else{ + $this->compressedQueue->enqueue($payload); + $payload->onResolve(function(CompressBatchPromise $payload) : void{ + if($this->connected and $this->compressedQueue->bottom() === $payload){ + $this->compressedQueue->dequeue(); //result unused + $this->sendEncoded($payload->getResult()); + + while(!$this->compressedQueue->isEmpty()){ + /** @var CompressBatchPromise $current */ + $current = $this->compressedQueue->bottom(); + if($current->hasResult()){ + $this->compressedQueue->dequeue(); + + $this->sendEncoded($current->getResult()); + }else{ + //can't send any more queued until this one is ready + break; + } + } + } + }); + } + } + + private function sendEncoded(string $payload, bool $immediate = false) : void{ + if($this->cipher !== null){ + Timings::$playerNetworkSendEncrypt->startTiming(); + $payload = $this->cipher->encrypt($payload); + Timings::$playerNetworkSendEncrypt->stopTiming(); + } + $this->sender->send($payload, $immediate); + } + + /** + * @phpstan-param \Closure() : void $func + */ + private function tryDisconnect(\Closure $func, string $reason) : void{ + if($this->connected and !$this->disconnectGuard){ + $this->disconnectGuard = true; + $func(); + $this->disconnectGuard = false; + foreach($this->disposeHooks as $callback){ + $callback(); + } + $this->disposeHooks->clear(); + $this->setHandler(null); + $this->connected = false; + $this->manager->remove($this); + $this->logger->info("Session closed due to $reason"); + + $this->invManager = null; //break cycles - TODO: this really ought to be deferred until it's safe + } + } + + /** + * Disconnects the session, destroying the associated player (if it exists). + */ + public function disconnect(string $reason, bool $notify = true) : void{ + $this->tryDisconnect(function() use ($reason, $notify) : void{ + if($this->player !== null){ + $this->player->onPostDisconnect($reason, null); + } + $this->doServerDisconnect($reason, $notify); + }, $reason); + } + + /** + * Instructs the remote client to connect to a different server. + */ + public function transfer(string $ip, int $port, string $reason = "transfer") : void{ + $this->tryDisconnect(function() use ($ip, $port, $reason) : void{ + $this->sendDataPacket(TransferPacket::create($ip, $port), true); + if($this->player !== null){ + $this->player->onPostDisconnect($reason, null); + } + $this->doServerDisconnect($reason, false); + }, $reason); + } + + /** + * Called by the Player when it is closed (for example due to getting kicked). + */ + public function onPlayerDestroyed(string $reason) : void{ + $this->tryDisconnect(function() use ($reason) : void{ + $this->doServerDisconnect($reason, true); + }, $reason); + } + + /** + * Internal helper function used to handle server disconnections. + */ + private function doServerDisconnect(string $reason, bool $notify = true) : void{ + if($notify){ + $this->sendDataPacket(DisconnectPacket::create($reason !== "" ? $reason : null), true); + } + + $this->sender->close($notify ? $reason : ""); + } + + /** + * Called by the network interface to close the session when the client disconnects without server input, for + * example in a timeout condition or voluntary client disconnect. + */ + public function onClientDisconnect(string $reason) : void{ + $this->tryDisconnect(function() use ($reason) : void{ + if($this->player !== null){ + $this->player->onPostDisconnect($reason, null); + } + }, $reason); + } + + private function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{ + if(!$this->connected){ + return; + } + if($error === null){ + if($authenticated and !($this->info instanceof XboxLivePlayerInfo)){ + $error = "Expected XUID but none found"; + }elseif($clientPubKey === null){ + $error = "Missing client public key"; //failsafe + } + } + + if($error !== null){ + $this->disconnect($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_invalidSession($this->server->getLanguage()->translateString($error)))); + + return; + } + + $this->authenticated = $authenticated; + + if(!$this->authenticated){ + if($authRequired){ + $this->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_NOTAUTHENTICATED); + return; + } + if($this->info instanceof XboxLivePlayerInfo){ + $this->logger->warning("Discarding unexpected XUID for non-authenticated player"); + $this->info = $this->info->withoutXboxData(); + } + } + $this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO")); + + $checkXUID = $this->server->getConfigGroup()->getPropertyBool("player.verify-xuid", true); + $myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() : ""; + $kickForXUIDMismatch = function(string $xuid) use ($checkXUID, $myXUID) : bool{ + if($checkXUID && $myXUID !== $xuid){ + $this->logger->debug("XUID mismatch: expected '$xuid', but got '$myXUID'"); + //TODO: Longer term, we should be identifying playerdata using something more reliable, like XUID or UUID. + //However, that would be a very disruptive change, so this will serve as a stopgap for now. + //Side note: this will also prevent offline players hijacking XBL playerdata on online servers, since their + //XUID will always be empty. + $this->disconnect("XUID does not match (possible impersonation attempt)"); + return true; + } + return false; + }; + + foreach($this->manager->getSessions() as $existingSession){ + if($existingSession === $this){ + continue; + } + $info = $existingSession->getPlayerInfo(); + if($info !== null and ($info->getUsername() === $this->info->getUsername() or $info->getUuid()->equals($this->info->getUuid()))){ + if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() : "")){ + return; + } + $ev = new PlayerDuplicateLoginEvent($this, $existingSession); + $ev->call(); + if($ev->isCancelled()){ + $this->disconnect($ev->getDisconnectMessage()); + return; + } + + $existingSession->disconnect($ev->getDisconnectMessage()); + } + } + + //TODO: make player data loading async + //TODO: we shouldn't be loading player data here at all, but right now we don't have any choice :( + $this->cachedOfflinePlayerData = $this->server->getOfflinePlayerData($this->info->getUsername()); + if($checkXUID){ + $recordedXUID = $this->cachedOfflinePlayerData !== null ? $this->cachedOfflinePlayerData->getTag("LastKnownXUID") : null; + if(!($recordedXUID instanceof StringTag)){ + $this->logger->debug("No previous XUID recorded, no choice but to trust this player"); + }elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){ + $this->logger->debug("XUID match"); + } + } + + if(EncryptionContext::$ENABLED){ + $this->server->getAsyncPool()->submitTask(new PrepareEncryptionTask($clientPubKey, function(string $encryptionKey, string $handshakeJwt) : void{ + if(!$this->connected){ + return; + } + $this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt), true); //make sure this gets sent before encryption is enabled + + $this->cipher = EncryptionContext::fakeGCM($encryptionKey); + + $this->setHandler(new HandshakePacketHandler(function() : void{ + $this->onServerLoginSuccess(); + })); + $this->logger->debug("Enabled encryption"); + })); + }else{ + $this->onServerLoginSuccess(); + } + } + + private function onServerLoginSuccess() : void{ + $this->loggedIn = true; + + $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_SUCCESS)); + + $this->logger->debug("Initiating resource packs phase"); + $this->setHandler(new ResourcePacksPacketHandler($this, $this->server->getResourcePackManager(), function() : void{ + $this->createPlayer(); + })); + } + + private function beginSpawnSequence() : void{ + $this->setHandler(new PreSpawnPacketHandler($this->server, $this->player, $this, $this->invManager)); + $this->player->setImmobile(); //TODO: HACK: fix client-side falling pre-spawn + + $this->logger->debug("Waiting for chunk radius request"); + } + + public function notifyTerrainReady() : void{ + $this->logger->debug("Sending spawn notification, waiting for spawn response"); + $this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN)); + $this->setHandler(new SpawnResponsePacketHandler(function() : void{ + $this->onClientSpawnResponse(); + })); + } + + private function onClientSpawnResponse() : void{ + $this->logger->debug("Received spawn response, entering in-game phase"); + $this->player->setImmobile(false); //TODO: HACK: we set this during the spawn sequence to prevent the client sending junk movements + $this->player->doFirstSpawn(); + $this->forceAsyncCompression = false; + $this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager)); + } + + 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->invManager ?? throw new AssumptionFailedError())); + } + } + + public function onServerRespawn() : void{ + $this->player->sendData(null); + + $this->syncAdventureSettings($this->player); + $this->invManager->syncAll(); + $this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager)); + } + + public function syncMovement(Vector3 $pos, ?float $yaw = null, ?float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL) : void{ + if($this->player !== null){ + $location = $this->player->getLocation(); + $yaw = $yaw ?? $location->getYaw(); + $pitch = $pitch ?? $location->getPitch(); + + $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; + } + } + } + + public function syncViewAreaRadius(int $distance) : void{ + $this->sendDataPacket(ChunkRadiusUpdatedPacket::create($distance)); + } + + public function syncViewAreaCenterPoint(Vector3 $newPos, int $viewDistance) : void{ + $this->sendDataPacket(NetworkChunkPublisherUpdatePacket::create(BlockPosition::fromVector3($newPos), $viewDistance * 16)); //blocks, not chunks >.> + } + + public function syncPlayerSpawnPoint(Position $newSpawn) : void{ + $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))); + if($this->player !== null){ + $this->syncAdventureSettings($this->player); + } + if(!$isRollback && $this->invManager !== null){ + $this->invManager->syncCreative(); + } + } + + /** + * TODO: make this less specialized + */ + public function syncAdventureSettings(Player $for) : void{ + $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::FLYING, $for->isFlying()); + + //TODO: permission flags + + $this->sendDataPacket($pk); + } + + /** + * @param Attribute[] $attributes + */ + public function syncAttributes(Living $entity, array $attributes) : void{ + if(count($attributes) > 0){ + $this->sendDataPacket(UpdateAttributesPacket::create($entity->getId(), array_map(function(Attribute $attr) : NetworkAttribute{ + return new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue()); + }, $attributes), 0)); + } + } + + /** + * @param MetadataProperty[] $properties + * @phpstan-param array $properties + */ + public function syncActorData(Entity $entity, array $properties) : void{ + $this->sendDataPacket(SetActorDataPacket::create($entity->getId(), $properties, 0)); + } + + public function onEntityEffectAdded(Living $entity, EffectInstance $effect, bool $replacesOldEffect) : void{ + //TODO: we may need yet another effect <=> ID map in the future depending on protocol changes + $this->sendDataPacket(MobEffectPacket::add($entity->getId(), $replacesOldEffect, EffectIdMap::getInstance()->toId($effect->getType()), $effect->getAmplifier(), $effect->isVisible(), $effect->getDuration())); + } + + public function onEntityEffectRemoved(Living $entity, EffectInstance $effect) : void{ + $this->sendDataPacket(MobEffectPacket::remove($entity->getId(), EffectIdMap::getInstance()->toId($effect->getType()))); + } + + public function onEntityRemoved(Entity $entity) : void{ + $this->sendDataPacket(RemoveActorPacket::create($entity->getId())); + } + + public function syncAvailableCommands() : void{ + $commandData = []; + foreach($this->server->getCommandMap()->getCommands() as $name => $command){ + if(isset($commandData[$command->getName()]) or $command->getName() === "help" or !$command->testPermissionSilent($this->player)){ + continue; + } + + $lname = strtolower($command->getName()); + $aliases = $command->getAliases(); + $aliasObj = null; + if(count($aliases) > 0){ + if(!in_array($lname, $aliases, true)){ + //work around a client bug which makes the original name not show when aliases are used + $aliases[] = $lname; + } + $aliasObj = new CommandEnum(ucfirst($command->getName()) . "Aliases", array_values($aliases)); + } + + $description = $command->getDescription(); + $data = new CommandData( + $lname, //TODO: commands containing uppercase letters in the name crash 1.9.0 client + $description instanceof Translatable ? $this->player->getLanguage()->translate($description) : $description, + 0, + 0, + $aliasObj, + [ + [CommandParameter::standard("args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0, true)] + ] + ); + + $commandData[$command->getName()] = $data; + } + + $this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], [])); + } + + public function onRawChatMessage(string $message) : void{ + $this->sendDataPacket(TextPacket::raw($message)); + } + + /** + * @param string[] $parameters + */ + public function onTranslatedChatMessage(string $key, array $parameters) : void{ + $this->sendDataPacket(TextPacket::translation($key, $parameters)); + } + + /** + * @param string[] $parameters + */ + public function onJukeboxPopup(string $key, array $parameters) : void{ + $this->sendDataPacket(TextPacket::jukeboxPopup($key, $parameters)); + } + + public function onPopup(string $message) : void{ + $this->sendDataPacket(TextPacket::popup($message)); + } + + public function onTip(string $message) : void{ + $this->sendDataPacket(TextPacket::tip($message)); + } + + 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)); + } + + /** + * Instructs the networksession to start using the chunk at the given coordinates. This may occur asynchronously. + * @param \Closure $onCompletion To be called when chunk sending has completed. + * @phpstan-param \Closure() : void $onCompletion + */ + public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{ + Utils::validateCallableSignature(function() : void{}, $onCompletion); + + $world = $this->player->getLocation()->getWorld(); + ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve( + + //this callback may be called synchronously or asynchronously, depending on whether the promise is resolved yet + function(CompressBatchPromise $promise) use ($world, $onCompletion, $chunkX, $chunkZ) : void{ + if(!$this->isConnected()){ + return; + } + $currentWorld = $this->player->getLocation()->getWorld(); + if($world !== $currentWorld or ($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_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 + //to NEEDED if they want to be resent. + return; + } + $world->timings->syncChunkSend->startTiming(); + try{ + $this->queueCompressed($promise); + $onCompletion(); + }finally{ + $world->timings->syncChunkSend->stopTiming(); + } + } + ); + } + + public function stopUsingChunk(int $chunkX, int $chunkZ) : void{ + + } + + public function onEnterWorld() : void{ + if($this->player !== null){ + $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) + } + } + + public function syncWorldTime(int $worldTime) : void{ + $this->sendDataPacket(SetTimePacket::create($worldTime)); + } + + public function syncWorldDifficulty(int $worldDifficulty) : void{ + $this->sendDataPacket(SetDifficultyPacket::create($worldDifficulty)); + } + + public function getInvManager() : ?InventoryManager{ + return $this->invManager; + } + + /** + * TODO: expand this to more than just humans + */ + 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(), $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, 0, ContainerIds::OFFHAND)); + } + + public function onMobArmorChange(Living $mob) : void{ + $inv = $mob->getArmorInventory(); + $converter = TypeConverter::getInstance(); + $this->sendDataPacket(MobArmorEquipmentPacket::create( + $mob->getId(), + ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getHelmet())), + ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getChestplate())), + ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getLeggings())), + ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getBoots())) + )); + } + + public function onPlayerPickUpItem(Player $collector, Entity $pickedUp) : void{ + $this->sendDataPacket(TakeItemActorPacket::create($collector->getId(), $pickedUp->getId())); + } + + /** + * @param Player[] $players + */ + public function syncPlayerList(array $players) : void{ + $this->sendDataPacket(PlayerListPacket::add(array_map(function(Player $player) : PlayerListEntry{ + return PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), SkinAdapterSingleton::get()->toSkinData($player->getSkin()), $player->getXuid()); + }, $players))); + } + + public function onPlayerAdded(Player $p) : void{ + $this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), SkinAdapterSingleton::get()->toSkinData($p->getSkin()), $p->getXuid())])); + } + + public function onPlayerRemoved(Player $p) : void{ + if($p !== $this->player){ + $this->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($p->getUniqueId())])); + } + } + + public function onTitle(string $title) : void{ + $this->sendDataPacket(SetTitlePacket::title($title)); + } + + public function onSubTitle(string $subtitle) : void{ + $this->sendDataPacket(SetTitlePacket::subtitle($subtitle)); + } + + public function onActionBar(string $actionBar) : void{ + $this->sendDataPacket(SetTitlePacket::actionBarMessage($actionBar)); + } + + public function onClearTitle() : void{ + $this->sendDataPacket(SetTitlePacket::clearTitle()); + } + + public function onResetTitleOptions() : void{ + $this->sendDataPacket(SetTitlePacket::resetTitleOptions()); + } + + public function onTitleDuration(int $fadeIn, int $stay, int $fadeOut) : void{ + $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){ + $this->disconnect("Login timeout"); + } + + return; + } + + if($this->player !== null){ + $this->player->doChunkRequests(); + + $dirtyAttributes = $this->player->getAttributeMap()->needSend(); + $this->syncAttributes($this->player, $dirtyAttributes); + foreach($dirtyAttributes as $attribute){ + //TODO: we might need to send these to other players in the future + //if that happens, this will need to become more complex than a flag on the attribute itself + $attribute->markSynchronized(); + } + } + + $this->flushSendBuffer(); + } +} diff --git a/src/network/mcpe/PacketBroadcaster.php b/src/network/mcpe/PacketBroadcaster.php new file mode 100644 index 0000000000..8a23fac750 --- /dev/null +++ b/src/network/mcpe/PacketBroadcaster.php @@ -0,0 +1,35 @@ +internalData ?? ($this->internalData = parent::toInternalBinary()); - } + /** + * Closes the channel, terminating the connection. + */ + public function close(string $reason = "unknown reason") : void; } diff --git a/src/pocketmine/network/mcpe/README.md b/src/network/mcpe/README.md similarity index 100% rename from src/pocketmine/network/mcpe/README.md rename to src/network/mcpe/README.md diff --git a/src/network/mcpe/StandardPacketBroadcaster.php b/src/network/mcpe/StandardPacketBroadcaster.php new file mode 100644 index 0000000000..4da9b09511 --- /dev/null +++ b/src/network/mcpe/StandardPacketBroadcaster.php @@ -0,0 +1,76 @@ +server = $server; + } + + public function broadcastPackets(array $recipients, array $packets) : void{ + $buffers = []; + $compressors = []; + $targetMap = []; + foreach($recipients as $recipient){ + $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 + $compressor = $recipient->getCompressor(); + $compressors[spl_object_id($compressor)] = $compressor; + + $targetMap[$bufferId][spl_object_id($compressor)][] = $recipient; + } + + 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); + } + } + } + } + } +} diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php new file mode 100644 index 0000000000..c0e537c646 --- /dev/null +++ b/src/network/mcpe/auth/ProcessLoginTask.php @@ -0,0 +1,205 @@ +storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + $this->chain = igbinary_serialize($chainJwts); + $this->clientDataJwt = $clientDataJwt; + $this->authRequired = $authRequired; + } + + public function onRun() : void{ + try{ + $this->clientPublicKey = $this->validateChain(); + $this->error = null; + }catch(VerifyLoginException $e){ + $this->error = $e->getMessage(); + } + } + + private function validateChain() : string{ + /** @var string[] $chain */ + $chain = igbinary_unserialize($this->chain); + + $currentKey = null; + $first = true; + + foreach($chain as $jwt){ + $this->validateToken($jwt, $currentKey, $first); + if($first){ + $first = false; + } + } + + /** @var string $clientKey */ + $clientKey = $currentKey; + + $this->validateToken($this->clientDataJwt, $currentKey); + + return $clientKey; + } + + /** + * @throws VerifyLoginException if errors are encountered + */ + private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{ + try{ + [$headersArray, $claimsArray, ] = JwtUtils::parse($jwt); + }catch(JwtException $e){ + throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), 0, $e); + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnMissingData = true; + $mapper->bExceptionOnUndefinedProperty = true; + $mapper->bEnforceMapType = false; + + try{ + /** @var JwtHeader $headers */ + $headers = $mapper->map($headersArray, new JwtHeader()); + }catch(\JsonMapper_Exception $e){ + throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e); + } + + $headerDerKey = base64_decode($headers->x5u, true); + if($headerDerKey === false){ + throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u"); + } + + if($currentPublicKey === null){ + if(!$first){ + throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY); + } + }elseif($headerDerKey !== $currentPublicKey){ + //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway + throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE); + } + + try{ + $signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey); + }catch(JwtException $e){ + throw new VerifyLoginException("Invalid JWT public key: " . openssl_error_string()); + } + try{ + if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){ + throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE); + } + }catch(JwtException $e){ + 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 + } + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case + $mapper->bExceptionOnMissingData = true; + $mapper->bEnforceMapType = false; + $mapper->bRemoveUndefinedAttributes = true; + try{ + /** @var JwtChainLinkBody $claims */ + $claims = $mapper->map($claimsArray, new JwtChainLinkBody()); + }catch(\JsonMapper_Exception $e){ + throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), 0, $e); + } + + $time = time(); + if(isset($claims->nbf) and $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){ + throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE); + } + + if(isset($claims->identityPublicKey)){ + $identityPublicKey = base64_decode($claims->identityPublicKey, true); + if($identityPublicKey === false){ + throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding"); + } + $currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this + } + } + + public function onCompletion() : void{ + /** + * @var \Closure $callback + * @phpstan-var \Closure(bool, bool, ?string, ?string) : void $callback + */ + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + $callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey); + } +} diff --git a/src/pocketmine/network/mcpe/VerifyLoginException.php b/src/network/mcpe/auth/VerifyLoginException.php similarity index 95% rename from src/pocketmine/network/mcpe/VerifyLoginException.php rename to src/network/mcpe/auth/VerifyLoginException.php index ee2268a41c..fc5fbbacaa 100644 --- a/src/pocketmine/network/mcpe/VerifyLoginException.php +++ b/src/network/mcpe/auth/VerifyLoginException.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe; +namespace pocketmine\network\mcpe\auth; class VerifyLoginException extends \RuntimeException{ diff --git a/src/network/mcpe/cache/ChunkCache.php b/src/network/mcpe/cache/ChunkCache.php new file mode 100644 index 0000000000..bf0ee44191 --- /dev/null +++ b/src/network/mcpe/cache/ChunkCache.php @@ -0,0 +1,238 @@ +addOnUnloadCallback(static function() use ($worldId) : void{ + foreach(self::$instances[$worldId] as $cache){ + $cache->caches = []; + } + unset(self::$instances[$worldId]); + \GlobalLogger::get()->debug("Destroyed chunk packet caches for world#$worldId"); + }); + } + if(!isset(self::$instances[$worldId][$compressorId])){ + \GlobalLogger::get()->debug("Created new chunk packet cache (world#$worldId, compressor#$compressorId)"); + self::$instances[$worldId][$compressorId] = new self($world, $compressor); + } + 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 */ + private $compressor; + + /** @var CompressBatchPromise[] */ + private $caches = []; + + /** @var int */ + private $hits = 0; + /** @var int */ + private $misses = 0; + + private function __construct(World $world, Compressor $compressor){ + $this->world = $world; + $this->compressor = $compressor; + } + + /** + * Requests asynchronous preparation of the chunk at the given coordinates. + * + * @return CompressBatchPromise a promise of resolution which will contain a compressed chunk packet. + */ + public function request(int $chunkX, int $chunkZ) : CompressBatchPromise{ + $this->world->registerChunkListener($this, $chunkX, $chunkZ); + $chunk = $this->world->getChunk($chunkX, $chunkZ); + if($chunk === null){ + throw new \InvalidArgumentException("Cannot request an unloaded chunk"); + } + $chunkHash = World::chunkHash($chunkX, $chunkZ); + + if(isset($this->caches[$chunkHash])){ + ++$this->hits; + return $this->caches[$chunkHash]; + } + + ++$this->misses; + + $this->world->timings->syncChunkSendPrepare->startTiming(); + try{ + $this->caches[$chunkHash] = new CompressBatchPromise(); + + $this->world->getServer()->getAsyncPool()->submitTask( + new ChunkRequestTask( + $chunkX, + $chunkZ, + $chunk, + $this->caches[$chunkHash], + $this->compressor, + function() use ($chunkX, $chunkZ) : void{ + $this->world->getLogger()->error("Failed preparing chunk $chunkX $chunkZ, retrying"); + + $this->restartPendingRequest($chunkX, $chunkZ); + } + ) + ); + + return $this->caches[$chunkHash]; + }finally{ + $this->world->timings->syncChunkSendPrepare->stopTiming(); + } + } + + private function destroy(int $chunkX, int $chunkZ) : bool{ + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $existing = $this->caches[$chunkHash] ?? null; + unset($this->caches[$chunkHash]); + + return $existing !== null; + } + + /** + * Restarts an async request for an unresolved chunk. + * + * @throws \InvalidArgumentException + */ + private function restartPendingRequest(int $chunkX, int $chunkZ) : void{ + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $existing = $this->caches[$chunkHash] ?? null; + if($existing === null or $existing->hasResult()){ + throw new \InvalidArgumentException("Restart can only be applied to unresolved promises"); + } + $existing->cancel(); + unset($this->caches[$chunkHash]); + + $this->request($chunkX, $chunkZ)->onResolve(...$existing->getResolveCallbacks()); + } + + /** + * @throws \InvalidArgumentException + */ + private function destroyOrRestart(int $chunkX, int $chunkZ) : void{ + $cache = $this->caches[World::chunkHash($chunkX, $chunkZ)] ?? null; + if($cache !== null){ + if(!$cache->hasResult()){ + //some requesters are waiting for this chunk, so their request needs to be fulfilled + $this->restartPendingRequest($chunkX, $chunkZ); + }else{ + //dump the cache, it'll be regenerated the next time it's requested + $this->destroy($chunkX, $chunkZ); + } + } + } + + use ChunkListenerNoOpTrait { + //force overriding of these + onChunkChanged as private; + onBlockChanged as private; + onChunkUnloaded as private; + } + + /** + * @see ChunkListener::onChunkChanged() + */ + public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + $this->destroyOrRestart($chunkX, $chunkZ); + } + + /** + * @see ChunkListener::onBlockChanged() + */ + public function onBlockChanged(Vector3 $block) : void{ + //FIXME: requesters will still receive this chunk after it's been dropped, but we can't mark this for a simple + //sync here because it can spam the worker pool + $this->destroy($block->getFloorX() >> Chunk::COORD_BIT_SIZE, $block->getFloorZ() >> Chunk::COORD_BIT_SIZE); + } + + /** + * @see ChunkListener::onChunkUnloaded() + */ + public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + $this->destroy($chunkX, $chunkZ); + $this->world->unregisterChunkListener($this, $chunkX, $chunkZ); + } + + /** + * Returns the number of bytes occupied by the cache data in this cache. This does not include the size of any + * promises referenced by the cache. + */ + public function calculateCacheSize() : int{ + $result = 0; + foreach($this->caches as $cache){ + if($cache->hasResult()){ + $result += strlen($cache->getResult()); + } + } + return $result; + } + + /** + * Returns the percentage of requests to the cache which resulted in a cache hit. + */ + public function getHitPercentage() : float{ + $total = $this->hits + $this->misses; + return $total > 0 ? $this->hits / $total : 0.0; + } +} diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php new file mode 100644 index 0000000000..369a450397 --- /dev/null +++ b/src/network/mcpe/cache/CraftingDataCache.php @@ -0,0 +1,143 @@ + + */ + private $caches = []; + + public function getCache(CraftingManager $manager) : CraftingDataPacket{ + $id = spl_object_id($manager); + if(!isset($this->caches[$id])){ + $manager->getDestructorCallbacks()->add(function() use ($id) : void{ + unset($this->caches[$id]); + }); + $manager->getRecipeRegisteredCallbacks()->add(function() use ($id) : void{ + unset($this->caches[$id]); + }); + $this->caches[$id] = $this->buildCraftingDataCache($manager); + } + return $this->caches[$id]; + } + + /** + * Rebuilds the cached CraftingDataPacket. + */ + private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{ + Timings::$craftingDataCacheRebuild->startTiming(); + + $counter = 0; + $nullUUID = Uuid::fromString(Uuid::NIL); + $converter = TypeConverter::getInstance(); + $recipesWithTypeIds = []; + foreach($manager->getShapelessRecipes() as $list){ + foreach($list as $recipe){ + $recipesWithTypeIds[] = new ProtocolShapelessRecipe( + CraftingDataPacket::ENTRY_SHAPELESS, + Binary::writeInt(++$counter), + array_map(function(Item $item) use ($converter) : RecipeIngredient{ + return $converter->coreItemStackToRecipeIngredient($item); + }, $recipe->getIngredientList()), + array_map(function(Item $item) use ($converter) : ItemStack{ + return $converter->coreItemStackToNet($item); + }, $recipe->getResults()), + $nullUUID, + CraftingRecipeBlockName::CRAFTING_TABLE, + 50, + $counter + ); + } + } + foreach($manager->getShapedRecipes() as $list){ + foreach($list as $recipe){ + $inputs = []; + + for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){ + for($column = 0, $width = $recipe->getWidth(); $column < $width; ++$column){ + $inputs[$row][$column] = $converter->coreItemStackToRecipeIngredient($recipe->getIngredient($column, $row)); + } + } + $recipesWithTypeIds[] = $r = new ProtocolShapedRecipe( + CraftingDataPacket::ENTRY_SHAPED, + Binary::writeInt(++$counter), + $inputs, + array_map(function(Item $item) use ($converter) : ItemStack{ + return $converter->coreItemStackToNet($item); + }, $recipe->getResults()), + $nullUUID, + CraftingRecipeBlockName::CRAFTING_TABLE, + 50, + $counter + ); + } + } + + foreach(FurnaceType::getAll() as $furnaceType){ + $typeTag = match($furnaceType->id()){ + FurnaceType::FURNACE()->id() => FurnaceRecipeBlockName::FURNACE, + FurnaceType::BLAST_FURNACE()->id() => FurnaceRecipeBlockName::BLAST_FURNACE, + FurnaceType::SMOKER()->id() => FurnaceRecipeBlockName::SMOKER, + default => throw new AssumptionFailedError("Unreachable"), + }; + foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){ + $input = $converter->coreItemStackToNet($recipe->getInput()); + $recipesWithTypeIds[] = new ProtocolFurnaceRecipe( + CraftingDataPacket::ENTRY_FURNACE_DATA, + $input->getId(), + $input->getMeta(), + $converter->coreItemStackToNet($recipe->getResult()), + $typeTag + ); + } + } + + Timings::$craftingDataCacheRebuild->stopTiming(); + return CraftingDataPacket::create($recipesWithTypeIds, [], [], [], true); + } +} diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php new file mode 100644 index 0000000000..39b8ea7743 --- /dev/null +++ b/src/network/mcpe/cache/StaticPacketCache.php @@ -0,0 +1,70 @@ + + */ + private static function loadCompoundFromFile(string $filePath) : CacheableNbt{ + $rawNbt = @file_get_contents($filePath); + if($rawNbt === false) throw new \RuntimeException("Failed to read file"); + return new CacheableNbt((new NetworkNbtSerializer())->read($rawNbt)->mustGetCompoundTag()); + } + + private static function make() : self{ + return new self( + 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'))) + ); + } + + public function __construct(BiomeDefinitionListPacket $biomeDefs, AvailableActorIdentifiersPacket $availableActorIdentifiers){ + $this->biomeDefs = $biomeDefs; + $this->availableActorIdentifiers = $availableActorIdentifiers; + } + + public function getBiomeDefs() : BiomeDefinitionListPacket{ + return $this->biomeDefs; + } + + public function getAvailableActorIdentifiers() : AvailableActorIdentifiersPacket{ + return $this->availableActorIdentifiers; + } +} diff --git a/src/network/mcpe/compression/CompressBatchPromise.php b/src/network/mcpe/compression/CompressBatchPromise.php new file mode 100644 index 0000000000..c62388f6aa --- /dev/null +++ b/src/network/mcpe/compression/CompressBatchPromise.php @@ -0,0 +1,108 @@ +checkCancelled(); + foreach($callbacks as $callback){ + Utils::validateCallableSignature(function(CompressBatchPromise $promise) : void{}, $callback); + } + if($this->result !== null){ + foreach($callbacks as $callback){ + $callback($this); + } + }else{ + array_push($this->callbacks, ...$callbacks); + } + } + + public function resolve(string $result) : void{ + if(!$this->cancelled){ + if($this->result !== null){ + throw new \LogicException("Cannot resolve promise more than once"); + } + $this->result = $result; + foreach($this->callbacks as $callback){ + $callback($this); + } + $this->callbacks = []; + } + } + + /** + * @return \Closure[] + * @phpstan-return (\Closure(self) : void)[] + */ + public function getResolveCallbacks() : array{ + return $this->callbacks; + } + + public function getResult() : string{ + $this->checkCancelled(); + if($this->result === null){ + throw new \LogicException("Promise has not yet been resolved"); + } + return $this->result; + } + + public function hasResult() : bool{ + return $this->result !== null; + } + + public function isCancelled() : bool{ + return $this->cancelled; + } + + public function cancel() : void{ + if($this->hasResult()){ + throw new \LogicException("Cannot cancel a resolved promise"); + } + $this->cancelled = true; + } + + private function checkCancelled() : void{ + if($this->cancelled){ + throw new \InvalidArgumentException("Promise has been cancelled"); + } + } +} diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandOriginData.php b/src/network/mcpe/compression/CompressBatchTask.php similarity index 52% rename from src/pocketmine/network/mcpe/protocol/types/CommandOriginData.php rename to src/network/mcpe/compression/CompressBatchTask.php index 738cff75aa..d13546b3a6 100644 --- a/src/pocketmine/network/mcpe/protocol/types/CommandOriginData.php +++ b/src/network/mcpe/compression/CompressBatchTask.php @@ -21,32 +21,32 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\network\mcpe\compression; -use pocketmine\utils\UUID; +use pocketmine\scheduler\AsyncTask; -class CommandOriginData{ - public const ORIGIN_PLAYER = 0; - public const ORIGIN_BLOCK = 1; - public const ORIGIN_MINECART_BLOCK = 2; - public const ORIGIN_DEV_CONSOLE = 3; - public const ORIGIN_TEST = 4; - public const ORIGIN_AUTOMATION_PLAYER = 5; - public const ORIGIN_CLIENT_AUTOMATION = 6; - public const ORIGIN_DEDICATED_SERVER = 7; - public const ORIGIN_ENTITY = 8; - public const ORIGIN_VIRTUAL = 9; - public const ORIGIN_GAME_ARGUMENT = 10; - public const ORIGIN_ENTITY_SERVER = 11; //??? +class CompressBatchTask extends AsyncTask{ - /** @var int */ - public $type; - /** @var UUID */ - public $uuid; + private const TLS_KEY_PROMISE = "promise"; /** @var string */ - public $requestId; + private $data; + /** @var Compressor */ + private $compressor; - /** @var int */ - public $playerEntityUniqueId; + public function __construct(string $data, CompressBatchPromise $promise, Compressor $compressor){ + $this->data = $data; + $this->compressor = $compressor; + $this->storeLocal(self::TLS_KEY_PROMISE, $promise); + } + + public function onRun() : void{ + $this->setResult($this->compressor->compress($this->data)); + } + + public function onCompletion() : void{ + /** @var CompressBatchPromise $promise */ + $promise = $this->fetchLocal(self::TLS_KEY_PROMISE); + $promise->resolve($this->getResult()); + } } diff --git a/src/network/mcpe/compression/Compressor.php b/src/network/mcpe/compression/Compressor.php new file mode 100644 index 0000000000..bbc8fac4f1 --- /dev/null +++ b/src/network/mcpe/compression/Compressor.php @@ -0,0 +1,36 @@ +level = $level; + $this->threshold = $minCompressionSize; + $this->maxDecompressionSize = $maxDecompressionSize; + } + + public function willCompress(string $data) : bool{ + return $this->threshold > -1 and strlen($data) >= $this->threshold; + } + + /** + * @throws DecompressionException + */ + public function decompress(string $payload) : string{ + $result = @zlib_decode($payload, $this->maxDecompressionSize); + if($result === false){ + throw new DecompressionException("Failed to decompress data"); + } + return $result; + } + + 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; + } + + public function compress(string $payload) : string{ + if(function_exists('libdeflate_deflate_compress')){ + return $this->willCompress($payload) ? + libdeflate_deflate_compress($payload, $this->level) : + self::zlib_encode($payload, 0); + } + return self::zlib_encode($payload, $this->willCompress($payload) ? $this->level : 0); + } +} diff --git a/src/pocketmine/network/mcpe/convert/ItemTypeDictionary.php b/src/network/mcpe/convert/GlobalItemTypeDictionary.php similarity index 55% rename from src/pocketmine/network/mcpe/convert/ItemTypeDictionary.php rename to src/network/mcpe/convert/GlobalItemTypeDictionary.php index b23983dc47..d3ab00f533 100644 --- a/src/pocketmine/network/mcpe/convert/ItemTypeDictionary.php +++ b/src/network/mcpe/convert/GlobalItemTypeDictionary.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; +use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; -use function array_key_exists; +use Webmozart\PathUtil\Path; use function file_get_contents; use function is_array; use function is_bool; @@ -34,27 +35,11 @@ use function is_int; use function is_string; use function json_decode; -final class ItemTypeDictionary{ +final class GlobalItemTypeDictionary{ use SingletonTrait; - /** - * @var ItemTypeEntry[] - * @phpstan-var list - */ - private $itemTypes; - /** - * @var string[] - * @phpstan-var array - */ - private $intToStringIdMap = []; - /** - * @var int[] - * @phpstan-var array - */ - private $stringToIntMap = []; - private static function make() : self{ - $data = file_get_contents(\pocketmine\RESOURCE_PATH . '/vanilla/required_item_list.json'); + $data = file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'required_item_list.json')); if($data === false) throw new AssumptionFailedError("Missing required resource file"); $table = json_decode($data, true); if(!is_array($table)){ @@ -68,39 +53,14 @@ final class ItemTypeDictionary{ } $params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"]); } - return new self($params); + return new self(new ItemTypeDictionary($params)); } - /** - * @param ItemTypeEntry[] $itemTypes - */ - public function __construct(array $itemTypes){ - $this->itemTypes = $itemTypes; - foreach($this->itemTypes as $type){ - $this->stringToIntMap[$type->getStringId()] = $type->getNumericId(); - $this->intToStringIdMap[$type->getNumericId()] = $type->getStringId(); - } + private ItemTypeDictionary $dictionary; + + public function __construct(ItemTypeDictionary $dictionary){ + $this->dictionary = $dictionary; } - /** - * @return ItemTypeEntry[] - * @phpstan-return list - */ - public function getEntries() : array{ - return $this->itemTypes; - } - - public function fromStringId(string $stringId) : int{ - if(!array_key_exists($stringId, $this->stringToIntMap)){ - throw new \InvalidArgumentException("Unmapped string ID \"$stringId\""); - } - return $this->stringToIntMap[$stringId]; - } - - public function fromIntId(int $intId) : string{ - if(!array_key_exists($intId, $this->intToStringIdMap)){ - throw new \InvalidArgumentException("Unmapped int ID $intId"); - } - return $this->intToStringIdMap[$intId]; - } + public function getDictionary() : ItemTypeDictionary{ return $this->dictionary; } } diff --git a/src/pocketmine/network/mcpe/convert/ItemTranslator.php b/src/network/mcpe/convert/ItemTranslator.php similarity index 77% rename from src/pocketmine/network/mcpe/convert/ItemTranslator.php rename to src/network/mcpe/convert/ItemTranslator.php index 7e68a46fb8..4aa82f32b7 100644 --- a/src/pocketmine/network/mcpe/convert/ItemTranslator.php +++ b/src/network/mcpe/convert/ItemTranslator.php @@ -23,8 +23,12 @@ 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; use function is_array; @@ -63,21 +67,14 @@ final class ItemTranslator{ private $complexNetToCoreMapping = []; private static function make() : self{ - $data = file_get_contents(\pocketmine\RESOURCE_PATH . '/vanilla/r16_to_current_item_map.json'); + $data = file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'r16_to_current_item_map.json')); if($data === false) throw new AssumptionFailedError("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"])){ throw new AssumptionFailedError("Invalid item table format"); } - $legacyStringToIntMapRaw = file_get_contents(\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 = []; @@ -85,13 +82,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"); } @@ -108,11 +106,16 @@ 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]; } } - return new self(ItemTypeDictionary::getInstance(), $simpleMappings, $complexMappings); + return new self(GlobalItemTypeDictionary::getInstance()->getDictionary(), $simpleMappings, $complexMappings); } /** @@ -140,10 +143,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; } @@ -154,17 +157,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]; @@ -173,12 +186,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/pocketmine/network/mcpe/protocol/types/LegacySkinAdapter.php b/src/network/mcpe/convert/LegacySkinAdapter.php similarity index 84% rename from src/pocketmine/network/mcpe/protocol/types/LegacySkinAdapter.php rename to src/network/mcpe/convert/LegacySkinAdapter.php index 55d604be7c..31d4ae92bb 100644 --- a/src/pocketmine/network/mcpe/protocol/types/LegacySkinAdapter.php +++ b/src/network/mcpe/convert/LegacySkinAdapter.php @@ -21,15 +21,17 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\network\mcpe\convert; use pocketmine\entity\InvalidSkinException; use pocketmine\entity\Skin; - +use pocketmine\network\mcpe\protocol\types\skin\SkinData; +use pocketmine\network\mcpe\protocol\types\skin\SkinImage; 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; @@ -42,10 +44,14 @@ 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 - json_encode(["geometry" => ["default" => $geometryName]]), + $resourcePatch, SkinImage::fromLegacy($skin->getSkinData()), [], $capeImage, $skin->getGeometryData() diff --git a/src/pocketmine/network/mcpe/convert/R12ToCurrentBlockMapEntry.php b/src/network/mcpe/convert/R12ToCurrentBlockMapEntry.php similarity index 100% rename from src/pocketmine/network/mcpe/convert/R12ToCurrentBlockMapEntry.php rename to src/network/mcpe/convert/R12ToCurrentBlockMapEntry.php diff --git a/src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php b/src/network/mcpe/convert/RuntimeBlockMapping.php similarity index 50% rename from src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php rename to src/network/mcpe/convert/RuntimeBlockMapping.php index eba93c8804..dfa69168e6 100644 --- a/src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php +++ b/src/network/mcpe/convert/RuntimeBlockMapping.php @@ -23,63 +23,59 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; -use pocketmine\block\BlockIds; -use pocketmine\nbt\NBT; -use pocketmine\nbt\NetworkLittleEndianNBTStream; +use pocketmine\block\Block; +use pocketmine\block\BlockLegacyIds; +use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\NetworkBinaryStream; +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 Webmozart\PathUtil\Path; use function file_get_contents; -use function json_decode; /** * @internal */ final class RuntimeBlockMapping{ + use SingletonTrait; /** @var int[] */ - private static $legacyToRuntimeMap = []; + private $legacyToRuntimeMap = []; /** @var int[] */ - private static $runtimeToLegacyMap = []; - /** @var CompoundTag[]|null */ - private static $bedrockKnownStates = null; + private $runtimeToLegacyMap = []; + /** @var CompoundTag[] */ + private $bedrockKnownStates; private function __construct(){ - //NOOP - } - - public static function init() : void{ - $canonicalBlockStatesFile = file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/canonical_block_states.nbt"); + $canonicalBlockStatesFile = file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt")); if($canonicalBlockStatesFile === false){ throw new AssumptionFailedError("Missing required resource file"); } - $stream = new NetworkBinaryStream($canonicalBlockStatesFile); + $stream = PacketSerializer::decoder($canonicalBlockStatesFile, 0, new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())); $list = []; while(!$stream->feof()){ $list[] = $stream->getNbtCompoundRoot(); } - self::$bedrockKnownStates = $list; + $this->bedrockKnownStates = $list; - self::setupLegacyMappings(); + $this->setupLegacyMappings(); } - private static function setupLegacyMappings() : void{ - $legacyIdMap = json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/block_id_map.json"), true); - + private function setupLegacyMappings() : void{ + $legacyIdMap = LegacyBlockIdToStringIdMap::getInstance(); /** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */ $legacyStateMap = []; - $legacyStateMapReader = new NetworkBinaryStream(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla/r12_to_current_block_map.bin")); - $nbtReader = new NetworkLittleEndianNBTStream(); + $legacyStateMapReader = PacketSerializer::decoder(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "r12_to_current_block_map.bin")), 0, new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())); + $nbtReader = new NetworkNbtSerializer(); while(!$legacyStateMapReader->feof()){ $id = $legacyStateMapReader->getString(); $meta = $legacyStateMapReader->getLShort(); $offset = $legacyStateMapReader->getOffset(); - $state = $nbtReader->read($legacyStateMapReader->getBuffer(), false, $offset); + $state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag(); $legacyStateMapReader->setOffset($offset); - if(!($state instanceof CompoundTag)){ - throw new \RuntimeException("Blockstate should be a TAG_Compound"); - } $legacyStateMap[] = new R12ToCurrentBlockMapEntry($id, $meta, $state); } @@ -87,11 +83,11 @@ final class RuntimeBlockMapping{ * @var int[][] $idToStatesMap string id -> int[] list of candidate state indices */ $idToStatesMap = []; - foreach(self::$bedrockKnownStates as $k => $state){ + foreach($this->bedrockKnownStates as $k => $state){ $idToStatesMap[$state->getString("name")][] = $k; } foreach($legacyStateMap as $pair){ - $id = $legacyIdMap[$pair->getId()] ?? null; + $id = $legacyIdMap->stringToLegacy($pair->getId()); if($id === null){ throw new \RuntimeException("No legacy ID matches " . $pair->getId()); } @@ -101,17 +97,14 @@ final class RuntimeBlockMapping{ continue; } $mappedState = $pair->getBlockState(); - - //TODO HACK: idiotic NBT compare behaviour on 3.x compares keys which are stored by values - $mappedState->setName(""); $mappedName = $mappedState->getString("name"); if(!isset($idToStatesMap[$mappedName])){ throw new \RuntimeException("Mapped new state does not appear in network table"); } foreach($idToStatesMap[$mappedName] as $k){ - $networkState = self::$bedrockKnownStates[$k]; + $networkState = $this->bedrockKnownStates[$k]; if($mappedState->equals($networkState)){ - self::registerMapping($k, $id, $data); + $this->registerMapping($k, $id, $data); continue 2; } } @@ -119,41 +112,23 @@ final class RuntimeBlockMapping{ } } - private static function lazyInit() : void{ - if(self::$bedrockKnownStates === null){ - self::init(); - } + public function toRuntimeId(int $internalStateId) : int{ + return $this->legacyToRuntimeMap[$internalStateId] ?? $this->legacyToRuntimeMap[BlockLegacyIds::INFO_UPDATE << Block::INTERNAL_METADATA_BITS]; } - public static function toStaticRuntimeId(int $id, int $meta = 0) : int{ - self::lazyInit(); - /* - * try id+meta first - * if not found, try id+0 (strip meta) - * if still not found, return update! block - */ - return self::$legacyToRuntimeMap[($id << 4) | $meta] ?? self::$legacyToRuntimeMap[$id << 4] ?? self::$legacyToRuntimeMap[BlockIds::INFO_UPDATE << 4]; + public function fromRuntimeId(int $runtimeId) : int{ + return $this->runtimeToLegacyMap[$runtimeId]; } - /** - * @return int[] [id, meta] - */ - public static function fromStaticRuntimeId(int $runtimeId) : array{ - self::lazyInit(); - $v = self::$runtimeToLegacyMap[$runtimeId]; - return [$v >> 4, $v & 0xf]; - } - - private static function registerMapping(int $staticRuntimeId, int $legacyId, int $legacyMeta) : void{ - self::$legacyToRuntimeMap[($legacyId << 4) | $legacyMeta] = $staticRuntimeId; - self::$runtimeToLegacyMap[$staticRuntimeId] = ($legacyId << 4) | $legacyMeta; + private function registerMapping(int $staticRuntimeId, int $legacyId, int $legacyMeta) : void{ + $this->legacyToRuntimeMap[($legacyId << Block::INTERNAL_METADATA_BITS) | $legacyMeta] = $staticRuntimeId; + $this->runtimeToLegacyMap[$staticRuntimeId] = ($legacyId << Block::INTERNAL_METADATA_BITS) | $legacyMeta; } /** * @return CompoundTag[] */ - public static function getBedrockKnownStates() : array{ - self::lazyInit(); - return self::$bedrockKnownStates; + public function getBedrockKnownStates() : array{ + return $this->bedrockKnownStates; } } diff --git a/src/pocketmine/network/mcpe/protocol/types/SkinAdapter.php b/src/network/mcpe/convert/SkinAdapter.php similarity index 92% rename from src/pocketmine/network/mcpe/protocol/types/SkinAdapter.php rename to src/network/mcpe/convert/SkinAdapter.php index 5ab4b97615..14ffc386db 100644 --- a/src/pocketmine/network/mcpe/protocol/types/SkinAdapter.php +++ b/src/network/mcpe/convert/SkinAdapter.php @@ -21,10 +21,11 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\network\mcpe\convert; use pocketmine\entity\InvalidSkinException; use pocketmine\entity\Skin; +use pocketmine\network\mcpe\protocol\types\skin\SkinData; /** * Used to convert new skin data to the skin entity or old skin entity to skin data. diff --git a/src/pocketmine/network/mcpe/protocol/types/SkinAdapterSingleton.php b/src/network/mcpe/convert/SkinAdapterSingleton.php similarity index 95% rename from src/pocketmine/network/mcpe/protocol/types/SkinAdapterSingleton.php rename to src/network/mcpe/convert/SkinAdapterSingleton.php index b92cdf0df3..c49d3a780c 100644 --- a/src/pocketmine/network/mcpe/protocol/types/SkinAdapterSingleton.php +++ b/src/network/mcpe/convert/SkinAdapterSingleton.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\network\mcpe\convert; /** * Accessor for SkinAdapter diff --git a/src/network/mcpe/convert/TypeConversionException.php b/src/network/mcpe/convert/TypeConversionException.php new file mode 100644 index 0000000000..fb24fefd36 --- /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 new file mode 100644 index 0000000000..efa4e79c08 --- /dev/null +++ b/src/network/mcpe/convert/TypeConverter.php @@ -0,0 +1,333 @@ +shieldRuntimeId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromStringId("minecraft:shield"); + } + + /** + * Returns a client-friendly gamemode of the specified real gamemode + * This function takes care of handling gamemodes known to MCPE (as of 1.1.0.3, that includes Survival, Creative and Adventure) + * + * @internal + */ + public function coreGameModeToProtocol(GameMode $gamemode) : int{ + switch($gamemode->id()){ + case GameMode::SURVIVAL()->id(): + return ProtocolGameMode::SURVIVAL; + case GameMode::CREATIVE()->id(): + case GameMode::SPECTATOR()->id(): + return ProtocolGameMode::CREATIVE; + case GameMode::ADVENTURE()->id(): + return ProtocolGameMode::ADVENTURE; + default: + throw new AssumptionFailedError("Unknown game mode"); + } + } + + public function protocolGameModeName(GameMode $gameMode) : string{ + switch($gameMode->id()){ + case GameMode::SURVIVAL()->id(): return "Survival"; + case GameMode::ADVENTURE()->id(): return "Adventure"; + default: return "Creative"; + } + } + + public function protocolGameModeToCore(int $gameMode) : ?GameMode{ + switch($gameMode){ + case ProtocolGameMode::SURVIVAL: + return GameMode::SURVIVAL(); + case ProtocolGameMode::CREATIVE: + return GameMode::CREATIVE(); + case ProtocolGameMode::ADVENTURE: + return GameMode::ADVENTURE(); + case ProtocolGameMode::CREATIVE_VIEWER: + case ProtocolGameMode::SURVIVAL_VIEWER: + return GameMode::SPECTATOR(); + default: + return null; + } + } + + public function coreItemStackToRecipeIngredient(Item $itemStack) : RecipeIngredient{ + if($itemStack->isNull()){ + return new RecipeIngredient(0, 0, 0); + } + if($itemStack->hasAnyDamageValue()){ + [$id, ] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), 0); + $meta = 0x7fff; + }else{ + [$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), $itemStack->getMeta()); + } + return new RecipeIngredient($id, $meta, $itemStack->getCount()); + } + + public function recipeIngredientToCoreItemStack(RecipeIngredient $ingredient) : Item{ + if($ingredient->getId() === 0){ + return ItemFactory::getInstance()->get(ItemIds::AIR, 0, 0); + } + [$id, $meta] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($ingredient->getId(), $ingredient->getMeta()); + return ItemFactory::getInstance()->get($id, $meta, $ingredient->getCount()); + } + + public function coreItemStackToNet(Item $itemStack) : ItemStack{ + if($itemStack->isNull()){ + return ItemStack::null(); + } + $nbt = null; + if($itemStack->hasNamedTag()){ + $nbt = clone $itemStack->getNamedTag(); + } + + $isBlockItem = $itemStack->getId() < 256; + + $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 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. + if($nbt === null){ + $nbt = new CompoundTag(); + } + $nbt->setInt(self::PM_META_TAG, $itemStack->getMeta()); + } + } + + $blockRuntimeId = 0; + if($isBlockItem){ + $block = $itemStack->getBlock(); + if($block->getId() !== BlockLegacyIds::AIR){ + $blockRuntimeId = RuntimeBlockMapping::getInstance()->toRuntimeId($block->getFullId()); + } + } + + return new ItemStack( + $id, + $meta, + $itemStack->getCount(), + $blockRuntimeId, + $nbt, + [], + [], + $id === $this->shieldRuntimeId ? 0 : null + ); + } + + /** + * @throws TypeConversionException + */ + public function netItemStackToCore(ItemStack $itemStack) : Item{ + if($itemStack->getId() === 0){ + return ItemFactory::getInstance()->get(ItemIds::AIR, 0, 0); + } + $compound = $itemStack->getNbt(); + + [$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta()); + + 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(($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 + //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. + $meta = $metaTag->getValue(); + $compound->removeTag(self::PM_META_TAG); + } + if($compound->count() === 0){ + $compound = null; + } + } + + try{ + return ItemFactory::getInstance()->get( + $id, + $meta, + $itemStack->getCount(), + $compound + ); + }catch(NbtException $e){ + throw TypeConversionException::wrap($e, "Bad itemstack NBT data"); + } + } + + /** + * @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; + } + 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: + $window = null; + if($action->windowId === ContainerIds::UI and $action->inventorySlot > 0){ + if($action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){ + return null; //useless noise + } + $pSlot = $action->inventorySlot; + + $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($slot === null){ + throw new TypeConversionException("Unmatched UI inventory slot offset $pSlot"); + } + }else{ + $window = $inventoryManager->getWindow($action->windowId); + $slot = $action->inventorySlot; + } + if($window !== null){ + return new SlotChangeAction($window, $slot, $old, $new); + } + + 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 TypeConversionException("Only expecting drop-item world actions from the client!"); + } + + return new DropItemAction($new); + case NetworkInventoryAction::SOURCE_CREATIVE: + switch($action->inventorySlot){ + case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_DELETE_ITEM: + return new DestroyItemAction($new); + case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_CREATE_ITEM: + return new CreateItemAction($old); + default: + throw new TypeConversionException("Unexpected creative action type $action->inventorySlot"); + + } + case NetworkInventoryAction::SOURCE_TODO: + //These types need special handling. + switch($action->windowId){ + case NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT: + case NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT: + return null; + } + + //TODO: more stuff + throw new TypeConversionException("No open container with window ID $action->windowId"); + default: + throw new TypeConversionException("Unknown inventory source type $action->sourceType"); + } + } +} diff --git a/src/network/mcpe/encryption/DecryptionException.php b/src/network/mcpe/encryption/DecryptionException.php new file mode 100644 index 0000000000..1a5e7e6901 --- /dev/null +++ b/src/network/mcpe/encryption/DecryptionException.php @@ -0,0 +1,28 @@ +key = $encryptionKey; + + $this->decryptCipher = new Cipher($algorithm); + $this->decryptCipher->decryptInit($this->key, $iv); + + $this->encryptCipher = new Cipher($algorithm); + $this->encryptCipher->encryptInit($this->key, $iv); + } + + /** + * Returns an EncryptionContext suitable for decrypting Minecraft packets from 1.16.200 and up. + * + * MCPE uses GCM, but without the auth tag, which defeats the whole purpose of using GCM. + * GCM is just a wrapper around CTR which adds the auth tag, so CTR can replace GCM for this case. + * However, since GCM passes only the first 12 bytes of the IV followed by 0002, we must do the same for + * compatibility with MCPE. + * In PM, we could skip this and just use GCM directly (since we use OpenSSL), but this way is more portable, and + * better for developers who come digging in the PM code looking for answers. + */ + public static function fakeGCM(string $encryptionKey) : self{ + return new EncryptionContext( + $encryptionKey, + "AES-256-CTR", + substr($encryptionKey, 0, 12) . "\x00\x00\x00\x02" + ); + } + + public static function cfb8(string $encryptionKey) : self{ + return new EncryptionContext( + $encryptionKey, + "AES-256-CFB8", + substr($encryptionKey, 0, 16) + ); + } + + /** + * @throws DecryptionException + */ + public function decrypt(string $encrypted) : string{ + if(strlen($encrypted) < 9){ + throw new DecryptionException("Payload is too short"); + } + $decrypted = $this->decryptCipher->decryptUpdate($encrypted); + $payload = substr($decrypted, 0, -8); + + $packetCounter = $this->decryptCounter++; + + if(($expected = $this->calculateChecksum($packetCounter, $payload)) !== ($actual = substr($decrypted, -8))){ + throw new DecryptionException("Encrypted packet $packetCounter has invalid checksum (expected " . bin2hex($expected) . ", got " . bin2hex($actual) . ")"); + } + + return $payload; + } + + public function encrypt(string $payload) : string{ + return $this->encryptCipher->encryptUpdate($payload . $this->calculateChecksum($this->encryptCounter++, $payload)); + } + + private function calculateChecksum(int $counter, string $payload) : string{ + $hash = openssl_digest(Binary::writeLLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true); + if($hash === false){ + throw new \RuntimeException("openssl_digest() error: " . openssl_error_string()); + } + return substr($hash, 0, 8); + } +} diff --git a/src/network/mcpe/encryption/EncryptionUtils.php b/src/network/mcpe/encryption/EncryptionUtils.php new file mode 100644 index 0000000000..0ad2ebfee2 --- /dev/null +++ b/src/network/mcpe/encryption/EncryptionUtils.php @@ -0,0 +1,68 @@ + base64_encode($derPublicKey), + "alg" => "ES384" + ], + [ + "salt" => base64_encode($salt) + ], + $serverPriv + ); + } +} diff --git a/src/network/mcpe/encryption/PrepareEncryptionTask.php b/src/network/mcpe/encryption/PrepareEncryptionTask.php new file mode 100644 index 0000000000..75b3b1f1cf --- /dev/null +++ b/src/network/mcpe/encryption/PrepareEncryptionTask.php @@ -0,0 +1,97 @@ + ["curve_name" => "secp384r1"]]); + if($serverPrivateKey === false){ + throw new \RuntimeException("openssl_pkey_new() failed: " . openssl_error_string()); + } + self::$SERVER_PRIVATE_KEY = $serverPrivateKey; + } + + $this->serverPrivateKey = igbinary_serialize(openssl_pkey_get_details(self::$SERVER_PRIVATE_KEY)); + $this->clientPub = $clientPub; + $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); + } + + public function onRun() : void{ + /** @var mixed[] $serverPrivDetails */ + $serverPrivDetails = igbinary_unserialize($this->serverPrivateKey); + $serverPriv = openssl_pkey_new($serverPrivDetails); + if($serverPriv === false) throw new AssumptionFailedError("Failed to restore server signing key from details"); + $clientPub = JwtUtils::parseDerPublicKey($this->clientPub); + $sharedSecret = EncryptionUtils::generateSharedSecret($serverPriv, $clientPub); + + $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{ + /** + * @var \Closure $callback + * @phpstan-var \Closure(string $encryptionKey, string $handshakeJwt) : void $callback + */ + $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + if($this->aesKey === null || $this->handshakeJwt === null){ + throw new AssumptionFailedError("Something strange happened here ..."); + } + $callback($this->aesKey, $this->handshakeJwt); + } +} diff --git a/src/network/mcpe/handler/DeathPacketHandler.php b/src/network/mcpe/handler/DeathPacketHandler.php new file mode 100644 index 0000000000..bc56729c9b --- /dev/null +++ b/src/network/mcpe/handler/DeathPacketHandler.php @@ -0,0 +1,81 @@ +player = $player; + $this->session = $session; + $this->inventoryManager = $inventoryManager; + } + + public function setUp() : void{ + $this->session->sendDataPacket(RespawnPacket::create( + $this->player->getOffsetPosition($this->player->getSpawn()), + RespawnPacket::SEARCHING_FOR_SPAWN, + $this->player->getId() + )); + } + + public function handlePlayerAction(PlayerActionPacket $packet) : bool{ + if($packet->action === PlayerAction::RESPAWN){ + $this->player->respawn(); + return true; + } + + 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( + $this->player->getOffsetPosition($this->player->getSpawn()), + RespawnPacket::READY_TO_SPAWN, + $this->player->getId() + )); + return true; + } + return false; + } +} diff --git a/src/pocketmine/network/mcpe/protocol/ServerToClientHandshakePacket.php b/src/network/mcpe/handler/HandshakePacketHandler.php similarity index 54% rename from src/pocketmine/network/mcpe/protocol/ServerToClientHandshakePacket.php rename to src/network/mcpe/handler/HandshakePacketHandler.php index 25449d9427..ed64da49f8 100644 --- a/src/pocketmine/network/mcpe/protocol/ServerToClientHandshakePacket.php +++ b/src/network/mcpe/handler/HandshakePacketHandler.php @@ -21,34 +21,30 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\network\mcpe\handler; -#include +use pocketmine\network\mcpe\protocol\ClientToServerHandshakePacket; -use pocketmine\network\mcpe\NetworkSession; - -class ServerToClientHandshakePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SERVER_TO_CLIENT_HANDSHAKE_PACKET; +/** + * Handler responsible for awaiting client response from crypto handshake. + */ +class HandshakePacketHandler extends PacketHandler{ /** - * @var string - * Server pubkey and token is contained in the JWT. + * @var \Closure + * @phpstan-var \Closure() : void */ - public $jwt; + private $onHandshakeCompleted; - public function canBeSentBeforeLogin() : bool{ + /** + * @phpstan-param \Closure() : void $onHandshakeCompleted + */ + public function __construct(\Closure $onHandshakeCompleted){ + $this->onHandshakeCompleted = $onHandshakeCompleted; + } + + public function handleClientToServerHandshake(ClientToServerHandshakePacket $packet) : bool{ + ($this->onHandshakeCompleted)(); return true; } - - protected function decodePayload(){ - $this->jwt = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->jwt); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleServerToClientHandshake($this); - } } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php new file mode 100644 index 0000000000..e4553c49be --- /dev/null +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -0,0 +1,847 @@ +player = $player; + $this->session = $session; + $this->inventoryManager = $inventoryManager; + } + + public function handleText(TextPacket $packet) : bool{ + if($packet->type === TextPacket::TYPE_CHAT){ + return $this->player->chat($packet->message); + } + + return false; + } + + public function handleMovePlayer(MovePlayerPacket $packet) : bool{ + $rawPos = $packet->position; + foreach([$rawPos->x, $rawPos->y, $rawPos->z, $packet->yaw, $packet->headYaw, $packet->pitch] 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); + if($yaw < 0){ + $yaw += 360; + } + + $this->player->setRotation($yaw, $pitch); + + $curPos = $this->player->getLocation(); + $newPos = $packet->position->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 + $this->session->getLogger()->debug("Got outdated pre-teleport movement, received " . $newPos . ", expected " . $curPos); + //Still getting movements from before teleport, ignore them + return false; + } + + // Once we get a movement within a reasonable distance, treat it as a teleport ACK and remove position lock + $this->forceMoveSync = false; + + $this->player->handleMovement($newPos); + + return true; + } + + public function handleLevelSoundEventPacketV1(LevelSoundEventPacketV1 $packet) : bool{ + return true; //useless leftover from 1.8 + } + + public function handleActorEvent(ActorEventPacket $packet) : bool{ + if($packet->actorRuntimeId !== $this->player->getId()){ + //TODO HACK: EATING_ITEM is sent back to the server when the server sends it for other players (1.14 bug, maybe earlier) + return $packet->actorRuntimeId === ActorEvent::EATING_ITEM; + } + $this->player->removeCurrentWindow(); + + 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; + } + $this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $this->player->getInventory()->getItemInHand())); + break; + default: + return false; + } + + return true; + } + + public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{ + $result = true; + + if($packet->trData instanceof NormalTransactionData){ + $result = $this->handleNormalTransaction($packet->trData); + }elseif($packet->trData instanceof MismatchTransactionData){ + $this->session->getLogger()->debug("Mismatch transaction received"); + $this->inventoryManager->syncAll(); + $result = true; + }elseif($packet->trData instanceof UseItemTransactionData){ + $result = $this->handleUseItemTransaction($packet->trData); + }elseif($packet->trData instanceof UseItemOnEntityTransactionData){ + $result = $this->handleUseItemOnEntityTransaction($packet->trData); + }elseif($packet->trData instanceof ReleaseItemTransactionData){ + $result = $this->handleReleaseItemTransaction($packet->trData); + } + + if(!$result){ + $this->inventoryManager->syncAll(); + } + return $result; + } + + private function handleNormalTransaction(NormalTransactionData $data) : bool{ + /** @var InventoryAction[] $actions */ + $actions = []; + + $isCraftingPart = false; + $converter = TypeConverter::getInstance(); + foreach($data->getActions() as $networkInventoryAction){ + if( + ( + $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO and ( + $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT or + $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT + ) + ) or ( + $this->craftingTransaction !== null && + !$networkInventoryAction->oldItem->getItemStack()->equals($networkInventoryAction->newItem->getItemStack()) && + $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && + $networkInventoryAction->windowId === ContainerIds::UI && + $networkInventoryAction->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT + ) + ){ + $isCraftingPart = true; + } + + try{ + $action = $converter->createInventoryAction($networkInventoryAction, $this->player, $this->inventoryManager); + if($action !== null){ + $actions[] = $action; + } + }catch(TypeConversionException $e){ + $this->session->getLogger()->debug("Error unpacking inventory action: " . $e->getMessage()); + return false; + } + } + + if($isCraftingPart){ + if($this->craftingTransaction === null){ + //TODO: this might not be crafting if there is a special inventory open (anvil, enchanting, loom etc) + $this->craftingTransaction = new CraftingTransaction($this->player, $this->player->getServer()->getCraftingManager(), $actions); + }else{ + foreach($actions as $action){ + $this->craftingTransaction->addAction($action); + } + } + + try{ + $this->craftingTransaction->validate(); + }catch(TransactionValidationException $e){ + //transaction is incomplete - crafting transaction comes in lots of little bits, so we have to collect + //all of the parts before we can execute it + return true; + } + $this->player->setUsingItem(false); + try{ + $this->inventoryManager->onTransactionStart($this->craftingTransaction); + $this->craftingTransaction->execute(); + }catch(TransactionException $e){ + $this->session->getLogger()->debug("Failed to execute crafting transaction: " . $e->getMessage()); + + //TODO: only sync slots that the client tried to change + foreach($this->craftingTransaction->getInventories() as $inventory){ + $this->inventoryManager->syncContents($inventory); + } + return false; + }finally{ + $this->craftingTransaction = null; + } + }else{ + //normal transaction fallthru + if($this->craftingTransaction !== null){ + $this->session->getLogger()->debug("Got unexpected normal inventory action with incomplete crafting transaction, refusing to execute crafting"); + $this->craftingTransaction = null; + return false; + } + + if(count($actions) === 0){ + //TODO: 1.13+ often sends transactions with nothing but useless crap in them, no need for the debug noise + return true; + } + + $this->player->setUsingItem(false); + $transaction = new InventoryTransaction($this->player, $actions); + $this->inventoryManager->onTransactionStart($transaction); + try{ + $transaction->execute(); + }catch(TransactionException $e){ + $logger = $this->session->getLogger(); + $logger->debug("Failed to execute inventory transaction: " . $e->getMessage()); + $logger->debug("Actions: " . json_encode($data->getActions())); + + foreach($transaction->getInventories() as $inventory){ + $this->inventoryManager->syncContents($inventory); + } + + return false; + } + } + + return true; + } + + private function handleUseItemTransaction(UseItemTransactionData $data) : bool{ + $this->player->selectHotbarSlot($data->getHotbarSlot()); + + switch($data->getActionType()){ + case UseItemTransactionData::ACTION_CLICK_BLOCK: + //TODO: start hack for client spam bug + $clickPos = $data->getClickPosition(); + $spamBug = ($this->lastRightClickData !== null and + microtime(true) - $this->lastRightClickTime < 0.1 and //100ms + $this->lastRightClickData->getPlayerPosition()->distanceSquared($data->getPlayerPosition()) < 0.00001 and + $this->lastRightClickData->getBlockPosition()->equals($data->getBlockPosition()) and + $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; + $this->lastRightClickTime = microtime(true); + if($spamBug){ + return true; + } + //TODO: end hack for client spam bug + + $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->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()){ + $this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex()); + } + return true; + } + if(!$this->player->useHeldItem()){ + $this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex()); + } + return true; + } + + return false; + } + + /** + * Internal function used to execute rollbacks when an action fails on a block. + */ + private function onFailedBlockAction(Vector3 $blockPos, ?int $face) : void{ + $this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex()); + if($blockPos->distanceSquared($this->player->getLocation()) < 10000){ + $blocks = $blockPos->sidesArray(); + if($face !== null){ + $sidePos = $blockPos->getSide($face); + + /** @var Vector3[] $blocks */ + array_push($blocks, ...$sidePos->sidesArray()); //getAllSides() on each of these will include $blockPos and $sidePos because they are next to each other + }else{ + $blocks[] = $blockPos; + } + foreach($this->player->getWorld()->createBlockUpdatePackets($blocks) as $packet){ + $this->session->sendDataPacket($packet); + } + } + } + + private function handleUseItemOnEntityTransaction(UseItemOnEntityTransactionData $data) : bool{ + $target = $this->player->getWorld()->getEntity($data->getActorRuntimeId()); + if($target === null){ + return false; + } + + $this->player->selectHotbarSlot($data->getHotbarSlot()); + + //TODO: use transactiondata for rollbacks here + switch($data->getActionType()){ + case UseItemOnEntityTransactionData::ACTION_INTERACT: + if(!$this->player->interactEntity($target, $data->getClickPosition())){ + $this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex()); + } + return true; + case UseItemOnEntityTransactionData::ACTION_ATTACK: + if(!$this->player->attackEntity($target)){ + $this->inventoryManager->syncSlot($this->player->getInventory(), $this->player->getInventory()->getHeldItemIndex()); + } + return true; + } + + return false; + } + + private function handleReleaseItemTransaction(ReleaseItemTransactionData $data) : bool{ + $this->player->selectHotbarSlot($data->getHotbarSlot()); + + //TODO: use transactiondata for rollbacks here (resending entire inventory is very wasteful) + switch($data->getActionType()){ + case ReleaseItemTransactionData::ACTION_RELEASE: + if(!$this->player->releaseHeldItem()){ + $this->inventoryManager->syncContents($this->player->getInventory()); + } + return true; + } + + return false; + } + + public function handleMobEquipment(MobEquipmentPacket $packet) : bool{ + if($packet->windowId === ContainerIds::OFFHAND){ + return true; //this happens when we put an item into the offhand + } + if($packet->windowId === ContainerIds::INVENTORY){ + $this->inventoryManager->onClientSelectHotbarSlot($packet->hotbarSlot); + if(!$this->player->selectHotbarSlot($packet->hotbarSlot)){ + $this->inventoryManager->syncSelectedHotbarSlot(); + } + return true; + } + return false; + } + + public function handleMobArmorEquipment(MobArmorEquipmentPacket $packet) : bool{ + return true; //Not used + } + + public function handleInteract(InteractPacket $packet) : bool{ + if($packet->action === InteractPacket::ACTION_MOUSEOVER){ + //TODO HACK: silence useless spam (MCPE 1.8) + //due to some messy Mojang hacks, it sends this when changing the held item now, which causes us to think + //the inventory was closed when it wasn't. + //this is also sent whenever entity metadata updates, which can get really spammy. + //TODO: implement handling for this where it matters + return true; + } + $target = $this->player->getWorld()->getEntity($packet->targetActorRuntimeId); + if($target === null){ + return false; + } + 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->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()), $packet->addUserData); + } + + public function handleActorPickRequest(ActorPickRequestPacket $packet) : bool{ + return false; //TODO + } + + public function handlePlayerAction(PlayerActionPacket $packet) : bool{ + $pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()); + + switch($packet->action){ + case PlayerAction::START_BREAK: + if(!$this->player->attackBlock($pos, $packet->face)){ + $this->onFailedBlockAction($pos, $packet->face); + } + + break; + + case PlayerAction::ABORT_BREAK: + case PlayerAction::STOP_BREAK: + $this->player->stopBreakBlock($pos); + break; + case PlayerAction::START_SLEEPING: + //unused + break; + case PlayerAction::STOP_SLEEPING: + $this->player->stopSleep(); + break; + case PlayerAction::JUMP: + $this->player->jump(); + return true; + case PlayerAction::START_SPRINT: + if(!$this->player->toggleSprint(true)){ + $this->player->sendData([$this->player]); + } + return true; + case PlayerAction::STOP_SPRINT: + if(!$this->player->toggleSprint(false)){ + $this->player->sendData([$this->player]); + } + return true; + case PlayerAction::START_SNEAK: + if(!$this->player->toggleSneak(true)){ + $this->player->sendData([$this->player]); + } + return true; + case PlayerAction::STOP_SNEAK: + if(!$this->player->toggleSneak(false)){ + $this->player->sendData([$this->player]); + } + return true; + case PlayerAction::START_GLIDE: + case PlayerAction::STOP_GLIDE: + break; //TODO + case PlayerAction::CRACK_BREAK: + $this->player->continueBreakBlock($pos, $packet->face); + break; + case PlayerAction::START_SWIMMING: + break; //TODO + case PlayerAction::STOP_SWIMMING: + //TODO: handle this when it doesn't spam every damn tick (yet another spam bug!!) + break; + case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now) + break; + 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); + return false; + } + + $this->player->setUsingItem(false); + + return true; + } + + public function handleSetActorMotion(SetActorMotionPacket $packet) : bool{ + return true; //Not used: This packet is (erroneously) sent to the server when the client is riding a vehicle. + } + + public function handleAnimate(AnimatePacket $packet) : bool{ + return true; //Not used + } + + public function handleContainerClose(ContainerClosePacket $packet) : bool{ + $this->inventoryManager->onClientRemoveWindow($packet->windowId); + return true; + } + + public function handlePlayerHotbar(PlayerHotbarPacket $packet) : bool{ + return true; //this packet is useless + } + + public function handleCraftingEvent(CraftingEventPacket $packet) : bool{ + return true; //this is a broken useless packet, so we don't use it + } + + public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{ + if($packet->targetActorUniqueId !== $this->player->getId()){ + return false; //TODO: operators can change other people's permissions using this + } + + $handled = false; + + $isFlying = $packet->getFlag(AdventureSettingsPacket::FLYING); + if($isFlying !== $this->player->isFlying()){ + if(!$this->player->toggleFlight($isFlying)){ + $this->session->syncAdventureSettings($this->player); + } + $handled = true; + } + + //TODO: check for other changes + + return $handled; + } + + public function handleBlockActorData(BlockActorDataPacket $packet) : bool{ + $pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()); + if($pos->distanceSquared($this->player->getLocation()) > 10000){ + return false; + } + + $block = $this->player->getLocation()->getWorld()->getBlock($pos); + $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){ + if(($textBlobTag = $nbt->getTag("Text")) instanceof StringTag){ + try{ + $text = SignText::fromBlob($textBlobTag->getValue()); + }catch(\InvalidArgumentException $e){ + throw PacketHandlingException::wrap($e, "Invalid sign text update"); + } + + try{ + if(!$block->updateText($this->player, $text)){ + foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){ + $this->session->sendDataPacket($updatePacket); + } + } + }catch(\UnexpectedValueException $e){ + throw PacketHandlingException::wrap($e); + } + + return true; + } + + $this->session->getLogger()->debug("Invalid sign update data: " . base64_encode($packet->nbt->getEncodedNbt())); + } + + return false; + } + + public function handlePlayerInput(PlayerInputPacket $packet) : bool{ + return false; //TODO + } + + public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{ + $gameMode = TypeConverter::getInstance()->protocolGameModeToCore($packet->gamemode); + if($gameMode === null || !$gameMode->equals($this->player->getGamemode())){ + //Set this back to default. TODO: handle this properly + $this->session->syncGameMode($this->player->getGamemode(), true); + } + return true; + } + + public function handleSpawnExperienceOrb(SpawnExperienceOrbPacket $packet) : bool{ + return false; //TODO + } + + public function handleMapInfoRequest(MapInfoRequestPacket $packet) : bool{ + return false; //TODO + } + + public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{ + $this->player->setViewDistance($packet->radius); + + return true; + } + + public function handleItemFrameDropItem(ItemFrameDropItemPacket $packet) : bool{ + $blockPosition = $packet->blockPosition; + $block = $this->player->getWorld()->getBlockAt($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ()); + if($block instanceof ItemFrame and $block->getFramedItem() !== null){ + return $this->player->attackBlock(new Vector3($blockPosition->getX(), $blockPosition->getY(), $blockPosition->getZ()), $block->getFacing()); + } + return false; + } + + public function handleBossEvent(BossEventPacket $packet) : bool{ + return false; //TODO + } + + public function handleShowCredits(ShowCreditsPacket $packet) : bool{ + return false; //TODO: handle resume + } + + public function handleCommandRequest(CommandRequestPacket $packet) : bool{ + if(strpos($packet->command, '/') === 0){ + $this->player->chat($packet->command); + return true; + } + return false; + } + + public function handleCommandBlockUpdate(CommandBlockUpdatePacket $packet) : bool{ + return false; //TODO + } + + public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{ + try{ + $skin = SkinAdapterSingleton::get()->fromSkinData($packet->skin); + }catch(InvalidSkinException $e){ + throw PacketHandlingException::wrap($e, "Invalid skin in PlayerSkinPacket"); + } + return $this->player->changeSkin($skin, $packet->newSkinName, $packet->oldSkinName); + } + + public function handleSubClientLogin(SubClientLoginPacket $packet) : bool{ + return false; //TODO + } + + public function handleBookEdit(BookEditPacket $packet) : bool{ + //TODO: break this up into book API things + $oldBook = $this->player->getInventory()->getItem($packet->inventorySlot); + if(!($oldBook instanceof WritableBook)){ + return false; + } + + $newBook = clone $oldBook; + $modifiedPages = []; + + switch($packet->type){ + case BookEditPacket::TYPE_REPLACE_PAGE: + $newBook->setPageText($packet->pageNumber, $packet->text); + $modifiedPages[] = $packet->pageNumber; + break; + case BookEditPacket::TYPE_ADD_PAGE: + if(!$newBook->pageExists($packet->pageNumber)){ + //this may only come before a page which already exists + //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); + $modifiedPages[] = $packet->pageNumber; + break; + case BookEditPacket::TYPE_DELETE_PAGE: + if(!$newBook->pageExists($packet->pageNumber)){ + return false; + } + $newBook->deletePage($packet->pageNumber); + $modifiedPages[] = $packet->pageNumber; + break; + case BookEditPacket::TYPE_SWAP_PAGES: + if(!$newBook->pageExists($packet->pageNumber) or !$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)); + } + $newBook->swapPages($packet->pageNumber, $packet->secondaryPageNumber); + $modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber]; + break; + case BookEditPacket::TYPE_SIGN_BOOK: + /** @var WrittenBook $newBook */ + $newBook = VanillaItems::WRITTEN_BOOK() + ->setPages($oldBook->getPages()) + ->setAuthor($packet->author) + ->setTitle($packet->title) + ->setGeneration(WrittenBook::GENERATION_ORIGINAL); + break; + default: + return false; + } + + $event = new PlayerEditBookEvent($this->player, $oldBook, $newBook, $packet->type, $modifiedPages); + $event->call(); + if($event->isCancelled()){ + return true; + } + + $this->player->getInventory()->setItem($packet->inventorySlot, $event->getNewBook()); + + return true; + } + + public function handleModalFormResponse(ModalFormResponsePacket $packet) : bool{ + return $this->player->onFormSubmit($packet->formId, self::stupid_json_decode($packet->formData, true)); + } + + /** + * Hack to work around a stupid bug in Minecraft W10 which causes empty strings to be sent unquoted in form responses. + * + * @return mixed + * @throws PacketHandlingException + */ + private static function stupid_json_decode(string $json, bool $assoc = false){ + if(preg_match('/^\[(.+)\]$/s', $json, $matches) > 0){ + $raw = $matches[1]; + $lastComma = -1; + $newParts = []; + $inQuotes = false; + for($i = 0, $len = strlen($raw); $i <= $len; ++$i){ + if($i === $len or ($raw[$i] === "," and !$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 = '""'; + } + $newParts[] = $part; + $lastComma = $i; + }elseif($raw[$i] === '"'){ + if(!$inQuotes){ + $inQuotes = true; + }else{ + $backslashes = 0; + for(; $backslashes < $i && $raw[$i - $backslashes - 1] === "\\"; ++$backslashes){} + if(($backslashes % 2) === 0){ //unescaped quote + $inQuotes = false; + } + } + } + } + + $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)"); + } + + return $ret; + } + + return json_decode($json, $assoc); + } + + public function handleServerSettingsRequest(ServerSettingsRequestPacket $packet) : bool{ + return false; //TODO: GUI stuff + } + + public function handleLabTable(LabTablePacket $packet) : bool{ + return false; //TODO + } + + public function handleNetworkStackLatency(NetworkStackLatencyPacket $packet) : bool{ + return true; //TODO: implement this properly - this is here to silence debug spam from MCPE dev builds + } + + public function handleLevelSoundEvent(LevelSoundEventPacket $packet) : bool{ + /* + * We don't handle this - all sounds are handled by the server now. + * However, some plugins find this useful to detect events like left-click-air, which doesn't have any other + * action bound to it. + * In addition, we use this handler to silence debug noise, since this packet is frequently sent by the client. + */ + 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 new file mode 100644 index 0000000000..3ff77e2c43 --- /dev/null +++ b/src/network/mcpe/handler/LoginPacketHandler.php @@ -0,0 +1,238 @@ +session = $session; + $this->server = $server; + $this->playerInfoConsumer = $playerInfoConsumer; + $this->authCallback = $authCallback; + } + + public function handleLogin(LoginPacket $packet) : bool{ + if(!$this->isCompatibleProtocol($packet->protocol)){ + $this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true); + + //This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client) + $this->session->disconnect( + $this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $packet->protocol)), + false + ); + + return true; + } + + $extraData = $this->fetchAuthData($packet->chainDataJwt); + + if(!Player::isValidUserName($extraData->displayName)){ + $this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDNAME); + + return true; + } + + $clientData = $this->parseClientData($packet->clientDataJwt); + try{ + $skin = SkinAdapterSingleton::get()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData)); + }catch(\InvalidArgumentException | InvalidSkinException $e){ + $this->session->getLogger()->debug("Invalid skin: " . $e->getMessage()); + $this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDSKIN); + + return true; + } + + if(!Uuid::isValid($extraData->identity)){ + throw new PacketHandlingException("Invalid login UUID"); + } + $uuid = Uuid::fromString($extraData->identity); + if($extraData->XUID !== ""){ + $playerInfo = new XboxLivePlayerInfo( + $extraData->XUID, + $extraData->displayName, + $uuid, + $skin, + $clientData->LanguageCode, + (array) $clientData + ); + }else{ + $playerInfo = new PlayerInfo( + $extraData->displayName, + $uuid, + $skin, + $clientData->LanguageCode, + (array) $clientData + ); + } + ($this->playerInfoConsumer)($playerInfo); + + $ev = new PlayerPreLoginEvent( + $playerInfo, + $this->session->getIp(), + $this->session->getPort(), + $this->server->requiresAuthentication() + ); + if($this->server->getNetwork()->getConnectionCount() > $this->server->getMaxPlayers()){ + $ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_FULL, KnownTranslationKeys::DISCONNECTIONSCREEN_SERVERFULL); + } + 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())){ + $ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_BANNED, "You are banned"); + } + + $ev->call(); + if(!$ev->isAllowed()){ + $this->session->disconnect($ev->getFinalKickMessage()); + return true; + } + + $this->processLogin($packet, $ev->isAuthRequired()); + + return true; + } + + /** + * @throws PacketHandlingException + */ + protected function fetchAuthData(JwtChain $chain) : AuthenticationData{ + /** @var AuthenticationData|null $extraData */ + $extraData = null; + foreach($chain->chain as $k => $jwt){ + //validate every chain element + try{ + [, $claims, ] = JwtUtils::parse($jwt); + }catch(JwtException $e){ + throw PacketHandlingException::wrap($e); + } + if(isset($claims["extraData"])){ + if($extraData !== null){ + throw new PacketHandlingException("Found 'extraData' more than once in chainData"); + } + + if(!is_array($claims["extraData"])){ + throw new PacketHandlingException("'extraData' key should be an array"); + } + $mapper = new \JsonMapper; + $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models + $mapper->bExceptionOnMissingData = true; + $mapper->bExceptionOnUndefinedProperty = true; + try{ + /** @var AuthenticationData $extraData */ + $extraData = $mapper->map($claims["extraData"], new AuthenticationData); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e); + } + } + } + if($extraData === null){ + throw new PacketHandlingException("'extraData' not found in chain data"); + } + return $extraData; + } + + /** + * @throws PacketHandlingException + */ + protected function parseClientData(string $clientDataJwt) : ClientData{ + try{ + [, $clientDataClaims, ] = JwtUtils::parse($clientDataJwt); + }catch(JwtException $e){ + throw PacketHandlingException::wrap($e); + } + + $mapper = new \JsonMapper; + $mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models + $mapper->bExceptionOnMissingData = true; + $mapper->bExceptionOnUndefinedProperty = true; + try{ + $clientData = $mapper->map($clientDataClaims, new ClientData); + }catch(\JsonMapper_Exception $e){ + throw PacketHandlingException::wrap($e); + } + return $clientData; + } + + /** + * TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication. + * In the future this won't be necessary. + * + * @throws \InvalidArgumentException + */ + protected function processLogin(LoginPacket $packet, bool $authRequired) : void{ + $this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback)); + $this->session->setHandler(null); //drop packets received during login verification + } + + protected function isCompatibleProtocol(int $protocolVersion) : bool{ + return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL; + } +} diff --git a/src/network/mcpe/handler/PacketHandler.php b/src/network/mcpe/handler/PacketHandler.php new file mode 100644 index 0000000000..501a87df64 --- /dev/null +++ b/src/network/mcpe/handler/PacketHandler.php @@ -0,0 +1,41 @@ +player = $player; + $this->server = $server; + $this->session = $session; + $this->inventoryManager = $inventoryManager; + } + + public function setUp() : void{ + $location = $this->player->getLocation(); + + $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 + ]; + $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::LEGACY, 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()); + $this->session->syncAttributes($this->player, $this->player->getAttributeMap()->getAll()); + $this->session->syncAvailableCommands(); + $this->session->syncAdventureSettings($this->player); + foreach($this->player->getEffects()->all() as $effect){ + $this->session->onEntityEffectAdded($this->player, $effect, false); + } + $this->player->sendData([$this->player]); + + $this->inventoryManager->syncAll(); + $this->inventoryManager->syncCreative(); + $this->inventoryManager->syncSelectedHotbarSlot(); + $this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager())); + + $this->session->syncPlayerList($this->server->getOnlinePlayers()); + } + + public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{ + $this->player->setViewDistance($packet->radius); + + return true; + } +} diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php new file mode 100644 index 0000000000..f82ed1232e --- /dev/null +++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php @@ -0,0 +1,181 @@ + [chunk index => hasSent] */ + private $downloadedChunks = []; + + /** + * @phpstan-param \Closure() : void $completionCallback + */ + public function __construct(NetworkSession $session, ResourcePackManager $resourcePackManager, \Closure $completionCallback){ + $this->session = $session; + $this->resourcePackManager = $resourcePackManager; + $this->completionCallback = $completionCallback; + } + + public function setUp() : void{ + $resourcePackEntries = array_map(static function(ResourcePack $pack) : ResourcePackInfoEntry{ + //TODO: more stuff + return new ResourcePackInfoEntry($pack->getPackId(), $pack->getPackVersion(), $pack->getPackSize(), "", "", "", false); + }, $this->resourcePackManager->getResourceStack()); + //TODO: support forcing server packs + $this->session->sendDataPacket(ResourcePacksInfoPacket::create($resourcePackEntries, [], $this->resourcePackManager->resourcePacksRequired(), false, false)); + $this->session->getLogger()->debug("Waiting for client to accept resource packs"); + } + + private function disconnectWithError(string $error) : void{ + $this->session->getLogger()->error("Error downloading resource packs: " . $error); + $this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_RESOURCEPACK); + } + + public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ + switch($packet->status){ + case ResourcePackClientResponsePacket::STATUS_REFUSED: + //TODO: add lang strings for this + $this->session->disconnect("You must accept resource packs to join this server.", true); + break; + case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: + foreach($packet->packIds as $uuid){ + //dirty hack for mojang's dirty hack for versions + $splitPos = strpos($uuid, "_"); + if($splitPos !== false){ + $uuid = substr($uuid, 0, $splitPos); + } + $pack = $this->resourcePackManager->getPackById($uuid); + + if(!($pack instanceof ResourcePack)){ + //Client requested a resource pack but we don't have it available on the server + $this->disconnectWithError("Unknown pack $uuid requested, available packs: " . implode(", ", $this->resourcePackManager->getPackIdList())); + return false; + } + + $this->session->sendDataPacket(ResourcePackDataInfoPacket::create( + $pack->getPackId(), + self::PACK_CHUNK_SIZE, + (int) ceil($pack->getPackSize() / self::PACK_CHUNK_SIZE), + $pack->getPackSize(), + $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"); + + break; + case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: + $stack = array_map(static function(ResourcePack $pack) : ResourcePackStackEntry{ + return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), ""); //TODO: subpacks + }, $this->resourcePackManager->getResourceStack()); + + //we support chemistry blocks by default, the client should already have this installed + $stack[] = new ResourcePackStackEntry("0fba4063-dba1-4281-9b89-ff9390653530", "1.0.0", ""); + + //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, ProtocolInfo::MINECRAFT_VERSION_NETWORK, new Experiments([], false))); + $this->session->getLogger()->debug("Applying resource pack stack"); + break; + case ResourcePackClientResponsePacket::STATUS_COMPLETED: + $this->session->getLogger()->debug("Resource packs sequence completed"); + ($this->completionCallback)(); + break; + default: + return false; + } + + return true; + } + + public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ + $pack = $this->resourcePackManager->getPackById($packet->packId); + if(!($pack instanceof ResourcePack)){ + $this->disconnectWithError("Invalid request for chunk $packet->chunkIndex of unknown pack $packet->packId, available packs: " . implode(", ", $this->resourcePackManager->getPackIdList())); + return false; + } + + $packId = $pack->getPackId(); //use this because case may be different + + if(isset($this->downloadedChunks[$packId][$packet->chunkIndex])){ + $this->disconnectWithError("Duplicate request for chunk $packet->chunkIndex of pack $packet->packId"); + return false; + } + + $offset = $packet->chunkIndex * self::PACK_CHUNK_SIZE; + if($offset < 0 or $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; + } + + if(!isset($this->downloadedChunks[$packId])){ + $this->downloadedChunks[$packId] = [$packet->chunkIndex => true]; + }else{ + $this->downloadedChunks[$packId][$packet->chunkIndex] = true; + } + + $this->session->sendDataPacket(ResourcePackChunkDataPacket::create($packId, $packet->chunkIndex, $offset, $pack->getPackChunk($offset, self::PACK_CHUNK_SIZE))); + + return true; + } +} diff --git a/src/pocketmine/level/particle/EntityFlameParticle.php b/src/network/mcpe/handler/SpawnResponsePacketHandler.php similarity index 57% rename from src/pocketmine/level/particle/EntityFlameParticle.php rename to src/network/mcpe/handler/SpawnResponsePacketHandler.php index 41e688f38e..cc33a69086 100644 --- a/src/pocketmine/level/particle/EntityFlameParticle.php +++ b/src/network/mcpe/handler/SpawnResponsePacketHandler.php @@ -21,12 +21,27 @@ declare(strict_types=1); -namespace pocketmine\level\particle; +namespace pocketmine\network\mcpe\handler; -use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\SetLocalPlayerAsInitializedPacket; -class EntityFlameParticle extends GenericParticle{ - public function __construct(Vector3 $pos){ - parent::__construct($pos, Particle::TYPE_MOB_FLAME); +final class SpawnResponsePacketHandler extends PacketHandler{ + + /** + * @var \Closure + * @phpstan-var \Closure() : void + */ + private $responseCallback; + + /** + * @phpstan-param \Closure() : void $responseCallback + */ + public function __construct(\Closure $responseCallback){ + $this->responseCallback = $responseCallback; + } + + public function handleSetLocalPlayerAsInitialized(SetLocalPlayerAsInitializedPacket $packet) : bool{ + ($this->responseCallback)(); + return true; } } diff --git a/src/pocketmine/block/NetherBrick.php b/src/network/mcpe/raklib/PthreadsChannelReader.php similarity index 68% rename from src/pocketmine/block/NetherBrick.php rename to src/network/mcpe/raklib/PthreadsChannelReader.php index e1f074af43..4b32b0f383 100644 --- a/src/pocketmine/block/NetherBrick.php +++ b/src/network/mcpe/raklib/PthreadsChannelReader.php @@ -21,21 +21,19 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\network\mcpe\raklib; -use pocketmine\item\TieredTool; +use raklib\server\ipc\InterThreadChannelReader; -class NetherBrick extends Solid{ +final class PthreadsChannelReader implements InterThreadChannelReader{ + /** @var \Threaded */ + private $buffer; - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; + public function __construct(\Threaded $buffer){ + $this->buffer = $buffer; } - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 2; + public function read() : ?string{ + return $this->buffer->shift(); } } diff --git a/src/network/mcpe/raklib/PthreadsChannelWriter.php b/src/network/mcpe/raklib/PthreadsChannelWriter.php new file mode 100644 index 0000000000..a51c08c4de --- /dev/null +++ b/src/network/mcpe/raklib/PthreadsChannelWriter.php @@ -0,0 +1,33 @@ +buffer = $buffer; + } + + public function write(string $str) : void{ + $this->buffer[] = $str; + } +} diff --git a/src/network/mcpe/raklib/RakLibInterface.php b/src/network/mcpe/raklib/RakLibInterface.php new file mode 100644 index 0000000000..15a8177fb4 --- /dev/null +++ b/src/network/mcpe/raklib/RakLibInterface.php @@ -0,0 +1,277 @@ +server = $server; + $this->rakServerId = mt_rand(0, PHP_INT_MAX); + + $this->sleeper = new SleeperNotifier(); + + $mainToThreadBuffer = new \Threaded; + $threadToMainBuffer = new \Threaded; + + $this->rakLib = new RakLibServer( + $this->server->getLogger(), + $mainToThreadBuffer, + $threadToMainBuffer, + new InternetAddress($ip, $port, $ipV6 ? 6 : 4), + $this->rakServerId, + $this->server->getConfigGroup()->getPropertyInt("network.max-mtu-size", 1492), + self::MCPE_RAKNET_PROTOCOL_VERSION, + $this->sleeper + ); + $this->eventReceiver = new RakLibToUserThreadMessageReceiver( + new PthreadsChannelReader($threadToMainBuffer) + ); + $this->interface = new UserToRakLibThreadMessageSender( + new PthreadsChannelWriter($mainToThreadBuffer) + ); + + $this->broadcaster = new StandardPacketBroadcaster($this->server); + } + + public function start() : void{ + $this->server->getTickSleeper()->addNotifier($this->sleeper, function() : void{ + while($this->eventReceiver->handle($this)); + }); + $this->server->getLogger()->debug("Waiting for RakLib to start..."); + try{ + $this->rakLib->startAndWait(); + }catch(SocketException $e){ + throw new NetworkInterfaceStartException($e->getMessage(), 0, $e); + } + $this->server->getLogger()->debug("RakLib booted successfully"); + } + + public function setNetwork(Network $network) : void{ + $this->network = $network; + } + + public function tick() : void{ + if(!$this->rakLib->isRunning()){ + $e = $this->rakLib->getCrashInfo(); + if($e !== null){ + throw new \RuntimeException("RakLib crashed: " . $e->makePrettyMessage()); + } + throw new \Exception("RakLib Thread crashed without crash information"); + } + } + + public function onClientDisconnect(int $sessionId, string $reason) : void{ + if(isset($this->sessions[$sessionId])){ + $session = $this->sessions[$sessionId]; + unset($this->sessions[$sessionId]); + $session->onClientDisconnect($reason); + } + } + + public function close(int $sessionId) : void{ + if(isset($this->sessions[$sessionId])){ + unset($this->sessions[$sessionId]); + $this->interface->closeSession($sessionId); + } + } + + public function shutdown() : void{ + $this->server->getTickSleeper()->removeNotifier($this->sleeper); + $this->rakLib->quit(); + } + + public function onClientConnect(int $sessionId, string $address, int $port, int $clientID) : void{ + $session = new NetworkSession( + $this->server, + $this->network->getSessionManager(), + PacketPool::getInstance(), + new RakLibPacketSender($sessionId, $this), + $this->broadcaster, + ZlibCompressor::getInstance(), //TODO: this shouldn't be hardcoded, but we might need the RakNet protocol version to select it + $address, + $port + ); + $this->sessions[$sessionId] = $session; + } + + public function onPacketReceive(int $sessionId, string $packet) : void{ + if(isset($this->sessions[$sessionId])){ + if($packet === "" or $packet[0] !== self::MCPE_RAKNET_PACKET_ID){ + $this->sessions[$sessionId]->getLogger()->debug("Non-FE packet received: " . base64_encode($packet)); + return; + } + //get this now for blocking in case the player was closed before the exception was raised + $session = $this->sessions[$sessionId]; + $address = $session->getIp(); + $buf = substr($packet, 1); + try{ + $session->handleEncoded($buf); + }catch(PacketHandlingException $e){ + $errorId = bin2hex(random_bytes(6)); + + $logger = $session->getLogger(); + $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(implode("\n", Utils::printableExceptionInfo($e))); + $session->disconnect("Packet processing error (Error ID: $errorId)"); + $this->interface->blockAddress($address, 5); + } + } + } + + public function blockAddress(string $address, int $timeout = 300) : void{ + $this->interface->blockAddress($address, $timeout); + } + + public function unblockAddress(string $address) : void{ + $this->interface->unblockAddress($address); + } + + public function onRawPacketReceive(string $address, int $port, string $payload) : void{ + $this->network->processRawPacket($this, $address, $port, $payload); + } + + public function sendRawPacket(string $address, int $port, string $payload) : void{ + $this->interface->sendRaw($address, $port, $payload); + } + + public function addRawPacketFilter(string $regex) : void{ + $this->interface->addRawPacketFilter($regex); + } + + public function onPacketAck(int $sessionId, int $identifierACK) : void{ + + } + + public function setName(string $name) : void{ + $info = $this->server->getQueryInformation(); + + $this->interface->setName(implode(";", + [ + "MCPE", + rtrim(addcslashes($name, ";"), '\\'), + ProtocolInfo::CURRENT_PROTOCOL, + ProtocolInfo::MINECRAFT_VERSION_NETWORK, + $info->getPlayerCount(), + $info->getMaxPlayerCount(), + $this->rakServerId, + $this->server->getName(), + TypeConverter::getInstance()->protocolGameModeName($this->server->getGamemode()) + ]) . ";" + ); + } + + public function setPortCheck(bool $name) : void{ + $this->interface->setPortCheck($name); + } + + public function setPacketLimit(int $limit) : void{ + $this->interface->setPacketsPerTickLimit($limit); + } + + public function onBandwidthStatsUpdate(int $bytesSentDiff, int $bytesReceivedDiff) : void{ + $this->network->getBandwidthTracker()->add($bytesSentDiff, $bytesReceivedDiff); + } + + public function putPacket(int $sessionId, string $payload, bool $immediate = true) : void{ + if(isset($this->sessions[$sessionId])){ + $pk = new EncapsulatedPacket(); + $pk->buffer = self::MCPE_RAKNET_PACKET_ID . $payload; + $pk->reliability = PacketReliability::RELIABLE_ORDERED; + $pk->orderChannel = 0; + + $this->interface->sendEncapsulated($sessionId, $pk, $immediate); + } + } + + public function onPingMeasure(int $sessionId, int $pingMS) : void{ + if(isset($this->sessions[$sessionId])){ + $this->sessions[$sessionId]->updatePing($pingMS); + } + } +} diff --git a/src/pocketmine/network/mcpe/protocol/ContainerClosePacket.php b/src/network/mcpe/raklib/RakLibPacketSender.php similarity index 53% rename from src/pocketmine/network/mcpe/protocol/ContainerClosePacket.php rename to src/network/mcpe/raklib/RakLibPacketSender.php index 252f377696..11821959b1 100644 --- a/src/pocketmine/network/mcpe/protocol/ContainerClosePacket.php +++ b/src/network/mcpe/raklib/RakLibPacketSender.php @@ -21,31 +21,35 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\network\mcpe\raklib; -#include +use pocketmine\network\mcpe\PacketSender; -use pocketmine\network\mcpe\NetworkSession; - -class ContainerClosePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CONTAINER_CLOSE_PACKET; +class RakLibPacketSender implements PacketSender{ /** @var int */ - public $windowId; + private $sessionId; + /** @var RakLibInterface */ + private $handler; + /** @var bool */ - public $server = false; + private $closed = false; - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->server = $this->getBool(); + public function __construct(int $sessionId, RakLibInterface $handler){ + $this->sessionId = $sessionId; + $this->handler = $handler; } - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putBool($this->server); + public function send(string $payload, bool $immediate) : void{ + if(!$this->closed){ + $this->handler->putPacket($this->sessionId, $payload, $immediate); + } } - public function handle(NetworkSession $session) : bool{ - return $session->handleContainerClose($this); + public function close(string $reason = "unknown reason") : void{ + if(!$this->closed){ + $this->closed = true; + $this->handler->close($this->sessionId); + } } } diff --git a/src/network/mcpe/raklib/RakLibServer.php b/src/network/mcpe/raklib/RakLibServer.php new file mode 100644 index 0000000000..94ea2c9736 --- /dev/null +++ b/src/network/mcpe/raklib/RakLibServer.php @@ -0,0 +1,187 @@ +address = $address; + + $this->serverId = $serverId; + $this->maxMtuSize = $maxMtuSize; + + $this->logger = $logger; + + $this->mainToThreadBuffer = $mainToThreadBuffer; + $this->threadToMainBuffer = $threadToMainBuffer; + + $this->mainPath = \pocketmine\PATH; + + $this->protocolVersion = $protocolVersion; + + $this->mainThreadNotifier = $sleeper; + } + + /** + * @return void + */ + public function shutdownHandler(){ + 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(RakLibThreadCrashInfo::fromLastErrorInfo($error)); + }else{ + $this->logger->emergency("RakLib shutdown unexpectedly"); + } + } + } + + public function getCrashInfo() : ?RakLibThreadCrashInfo{ + return $this->crashInfo; + } + + private function setCrashInfo(RakLibThreadCrashInfo $info) : void{ + $this->synchronized(function(RakLibThreadCrashInfo $info) : void{ + $this->crashInfo = $info; + $this->notify(); + }, $info); + } + + public function startAndWait(int $options = PTHREADS_INHERIT_NONE) : void{ + $this->start($options); + $this->synchronized(function() : void{ + while(!$this->ready and $this->crashInfo === null){ + $this->wait(); + } + $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()); + } + }); + } + + protected function onRun() : void{ + try{ + gc_enable(); + ini_set("display_errors", '1'); + ini_set("display_startup_errors", '1'); + + register_shutdown_function([$this, "shutdownHandler"]); + + try{ + $socket = new Socket($this->address); + }catch(SocketException $e){ + $this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e)); + return; + } + $manager = new Server( + $this->serverId, + $this->logger, + $socket, + $this->maxMtuSize, + new SimpleProtocolAcceptor($this->protocolVersion), + new UserToRakLibThreadMessageReceiver(new PthreadsChannelReader($this->mainToThreadBuffer)), + new RakLibToUserThreadMessageSender(new SnoozeAwarePthreadsChannelWriter($this->threadToMainBuffer, $this->mainThreadNotifier)), + new ExceptionTraceCleaner($this->mainPath) + ); + $this->synchronized(function() : void{ + $this->ready = true; + $this->notify(); + }); + while(!$this->isKilled){ + $manager->tickProcessor(); + } + $manager->waitShutdown(); + $this->cleanShutdown = true; + }catch(\Throwable $e){ + $this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e)); + $this->logger->logException($e); + } + } + + public function getThreadName() : string{ + return "RakLib"; + } +} diff --git a/src/network/mcpe/raklib/RakLibThreadCrashInfo.php b/src/network/mcpe/raklib/RakLibThreadCrashInfo.php new file mode 100644 index 0000000000..806aebc85f --- /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); + } +} \ No newline at end of file diff --git a/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php b/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php new file mode 100644 index 0000000000..2600c85e37 --- /dev/null +++ b/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php @@ -0,0 +1,42 @@ +buffer = $buffer; + $this->notifier = $notifier; + } + + public function write(string $str) : void{ + $this->buffer[] = $str; + $this->notifier->wakeupSleeper(); + } +} diff --git a/src/network/mcpe/serializer/ChunkSerializer.php b/src/network/mcpe/serializer/ChunkSerializer.php new file mode 100644 index 0000000000..0fe5d891ae --- /dev/null +++ b/src/network/mcpe/serializer/ChunkSerializer.php @@ -0,0 +1,171 @@ +getSubChunks()); $y >= Chunk::MIN_SUBCHUNK_INDEX; --$y, --$count){ + if($chunk->getSubChunk($y)->isEmptyFast()){ + continue; + } + return $count; + } + + return 0; + } + + 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 = Chunk::MIN_SUBCHUNK_INDEX, $writtenCount = 0; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){ + self::serializeSubChunk($chunk->getSubChunk($y), $blockMapper, $stream, false); + } + + //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. + + if($tiles !== null){ + $stream->put($tiles); + }else{ + $stream->put(self::serializeTiles($chunk)); + } + return $stream->getBuffer(); + } + + public static function serializeSubChunk(SubChunk $subChunk, RuntimeBlockMapping $blockMapper, PacketSerializer $stream, bool $persistentBlockStates) : void{ + $layers = $subChunk->getBlockLayers(); + $stream->putByte(8); //version + + $stream->putByte(count($layers)); + + foreach($layers as $blocks){ + $bitsPerBlock = $blocks->getBitsPerBlock(); + $words = $blocks->getWordArray(); + $stream->putByte(($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1)); + $stream->put($words); + $palette = $blocks->getPalette(); + + if($bitsPerBlock !== 0){ + //these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here + //but since we know they are always unsigned, we can avoid the extra fcall overhead of + //zigzag and just shift directly. + $stream->putUnsignedVarInt(count($palette) << 1); //yes, this is intentionally zigzag + } + if($persistentBlockStates){ + $nbtSerializer = new NetworkNbtSerializer(); + foreach($palette as $p){ + $stream->put($nbtSerializer->write(new TreeRoot($blockMapper->getBedrockKnownStates()[$blockMapper->toRuntimeId($p)]))); + } + }else{ + foreach($palette as $p){ + $stream->put(Binary::writeUnsignedVarInt($blockMapper->toRuntimeId($p) << 1)); + } + } + } + } + + public static function serializeTiles(Chunk $chunk) : string{ + $stream = new BinaryStream(); + foreach($chunk->getTiles() as $tile){ + if($tile instanceof Spawnable){ + $stream->put($tile->getSerializedSpawnCompound()->getEncodedNbt()); + } + } + + 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 new file mode 100644 index 0000000000..70a021a5a9 --- /dev/null +++ b/src/network/query/DedicatedQueryNetworkInterface.php @@ -0,0 +1,171 @@ + timeout time + * @phpstan-var array + */ + private $blockedIps = []; + /** @var string[] */ + private $rawPacketPatterns = []; + + public function __construct(string $ip, int $port, bool $ipV6, \Logger $logger){ + $this->ip = $ip; + $this->port = $port; + $this->logger = $logger; + + $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; + } + + public function start() : void{ + if(!@socket_bind($this->socket, $this->ip, $this->port)){ + $error = socket_last_error($this->socket); + if($error === SOCKET_EADDRINUSE){ //platform error messages aren't consistent + throw new \RuntimeException("Failed to bind socket: Something else is already running on $this->ip $this->port", $error); + } + throw new \RuntimeException("Failed to bind to $this->ip $this->port: " . trim(socket_strerror($error)), $error); + } + socket_set_nonblock($this->socket); + $this->logger->info("Running on $this->ip $this->port"); + } + + public function setName(string $name) : void{ + //NOOP + } + + public function tick() : void{ + $r = [$this->socket]; + $w = null; + $e = null; + if(@socket_select($r, $w, $e, 0, 0) === 1){ + $address = ""; + $port = 0; + $buffer = ""; + while(true){ + $bytes = @socket_recvfrom($this->socket, $buffer, 65535, 0, $address, $port); + if($bytes !== false){ + if(isset($this->blockedIps[$address]) && $this->blockedIps[$address] > time()){ + $this->logger->debug("Dropped packet from banned address $address"); + continue; + } + foreach($this->rawPacketPatterns as $pattern){ + if(preg_match($pattern, $buffer) === 1){ + $this->network->processRawPacket($this, $address, $port, $buffer); + break; + } + } + }else{ + $errno = socket_last_error($this->socket); + if($errno === SOCKET_EWOULDBLOCK){ + break; + } + if($errno !== SOCKET_ECONNRESET){ //remote peer disappeared unexpectedly, this might spam like crazy so we don't log it + $this->logger->debug("Failed to recv (errno $errno): " . trim(socket_strerror($errno))); + } + } + } + } + } + + public function blockAddress(string $address, int $timeout = 300) : void{ + $this->blockedIps[$address] = $timeout > 0 ? time() + $timeout : PHP_INT_MAX; + } + + public function unblockAddress(string $address) : void{ + unset($this->blockedIps[$address]); + } + + public function setNetwork(Network $network) : void{ + $this->network = $network; + } + + public function sendRawPacket(string $address, int $port, string $payload) : void{ + if(@socket_sendto($this->socket, $payload, strlen($payload), 0, $address, $port) === false){ + $errno = socket_last_error($this->socket); + throw new \RuntimeException("Failed to send to $address $port (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + } + + public function addRawPacketFilter(string $regex) : void{ + $this->rawPacketPatterns[] = $regex; + } + + public function shutdown() : void{ + @socket_close($this->socket); + } +} diff --git a/src/network/query/QueryHandler.php b/src/network/query/QueryHandler.php new file mode 100644 index 0000000000..1fe2079694 --- /dev/null +++ b/src/network/query/QueryHandler.php @@ -0,0 +1,132 @@ +server = $server; + $this->logger = new \PrefixedLogger($this->server->getLogger(), "Query Handler"); + + /* + The Query protocol is built on top of the existing Minecraft PE UDP network stack. + Because the 0xFE packet does not exist in the MCPE protocol, + we can identify Query packets and remove them from the packet queue. + + Then, the Query class handles itself sending the packets in raw form, because + packets can conflict with the MCPE ones. + */ + + $this->regenerateToken(); + $this->lastToken = $this->token; + } + + public function getPattern() : string{ + return '/^\xfe\xfd.+$/s'; + } + + public function regenerateToken() : void{ + $this->lastToken = $this->token; + $this->token = random_bytes(16); + } + + public static function getTokenString(string $token, string $salt) : int{ + return Binary::readInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4)); + } + + public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{ + try{ + $stream = new BinaryStream($packet); + $header = $stream->get(2); + if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above + return false; + } + $packetType = $stream->getByte(); + $sessionID = $stream->getInt(); + + switch($packetType){ + case self::HANDSHAKE: //Handshake + $reply = chr(self::HANDSHAKE); + $reply .= Binary::writeInt($sessionID); + $reply .= self::getTokenString($this->token, $address) . "\x00"; + + $interface->sendRawPacket($address, $port, $reply); + + 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))){ + $this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2"); + + return true; + } + $reply = chr(self::STATISTICS); + $reply .= Binary::writeInt($sessionID); + + $remaining = $stream->getRemaining(); + if(strlen($remaining) === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01 + $reply .= $this->server->getQueryInformation()->getLongQuery(); + }else{ + $reply .= $this->server->getQueryInformation()->getShortQuery(); + } + $interface->sendRawPacket($address, $port, $reply); + + return true; + default: + return false; + } + }catch(BinaryDataException $e){ + $this->logger->debug("Bad packet from $address $port: " . $e->getMessage()); + return false; + } + } +} diff --git a/src/pocketmine/event/server/QueryRegenerateEvent.php b/src/network/query/QueryInfo.php similarity index 89% rename from src/pocketmine/event/server/QueryRegenerateEvent.php rename to src/network/query/QueryInfo.php index c9e40581c2..559f9a6531 100644 --- a/src/pocketmine/event/server/QueryRegenerateEvent.php +++ b/src/network/query/QueryInfo.php @@ -21,9 +21,10 @@ declare(strict_types=1); -namespace pocketmine\event\server; +namespace pocketmine\network\query; -use pocketmine\Player; +use pocketmine\player\GameMode; +use pocketmine\player\Player; use pocketmine\plugin\Plugin; use pocketmine\Server; use pocketmine\utils\Binary; @@ -33,7 +34,7 @@ use function count; use function str_replace; use function substr; -class QueryRegenerateEvent extends ServerEvent{ +final class QueryInfo{ public const GAME_ID = "MINECRAFTPE"; /** @var string */ @@ -77,19 +78,15 @@ class QueryRegenerateEvent extends ServerEvent{ public function __construct(Server $server){ $this->serverName = $server->getMotd(); - $this->listPlugins = (bool) $server->getProperty("settings.query-plugins", true); + $this->listPlugins = $server->getConfigGroup()->getPropertyBool("settings.query-plugins", true); $this->plugins = $server->getPluginManager()->getPlugins(); - $this->players = []; - foreach($server->getOnlinePlayers() as $player){ - if($player->isOnline()){ - $this->players[] = $player; - } - } + $this->players = $server->getOnlinePlayers(); - $this->gametype = ($server->getGamemode() & 0x01) === 0 ? "SMP" : "CMP"; + $this->gametype = ($server->getGamemode()->equals(GameMode::SURVIVAL()) || $server->getGamemode()->equals(GameMode::ADVENTURE())) ? "SMP" : "CMP"; $this->version = $server->getVersion(); $this->server_engine = $server->getName() . " " . $server->getPocketMineVersion(); - $this->map = $server->getDefaultLevel() === null ? "unknown" : $server->getDefaultLevel()->getName(); + $world = $server->getWorldManager()->getDefaultWorld(); + $this->map = $world === null ? "unknown" : $world->getDisplayName(); $this->numPlayers = count($this->players); $this->maxPlayers = $server->getMaxPlayers(); $this->whitelist = $server->hasWhitelist() ? "on" : "off"; @@ -98,20 +95,6 @@ class QueryRegenerateEvent extends ServerEvent{ } - /** - * @deprecated - */ - public function getTimeout() : int{ - return 0; - } - - /** - * @deprecated - */ - public function setTimeout(int $timeout) : void{ - - } - private function destroyCache() : void{ $this->longQueryCache = null; $this->shortQueryCache = null; @@ -206,6 +189,7 @@ class QueryRegenerateEvent extends ServerEvent{ /** * @param string[] $extraData + * * @phpstan-param array $extraData */ public function setExtraData(array $extraData) : void{ @@ -249,7 +233,7 @@ class QueryRegenerateEvent extends ServerEvent{ $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/pocketmine/network/upnp/UPnP.php b/src/network/upnp/UPnP.php similarity index 76% rename from src/pocketmine/network/upnp/UPnP.php rename to src/network/upnp/UPnP.php index b1e99e013c..c2ed90f6e5 100644 --- a/src/pocketmine/network/upnp/UPnP.php +++ b/src/network/upnp/UPnP.php @@ -79,12 +79,9 @@ use const SOCKET_ETIMEDOUT; use const SOL_SOCKET; use const SOL_UDP; -abstract class UPnP{ +class UPnP{ private const MAX_DISCOVERY_ATTEMPTS = 3; - /** @var string|null */ - private static $serviceURL = null; - private static function makePcreError() : \RuntimeException{ $errorCode = preg_last_error(); $message = [ @@ -98,13 +95,16 @@ abstract class UPnP{ throw new \RuntimeException("PCRE error: $message"); } + /** + * @throws UPnPException + */ public static function getServiceUrl() : string{ $socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if($socket === false){ - throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error()))); + 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 \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + throw new AssumptionFailedError("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); } $contents = "M-SEARCH * HTTP/1.1\r\n" . @@ -116,17 +116,17 @@ abstract class UPnP{ for($i = 0; $i < self::MAX_DISCOVERY_ATTEMPTS; ++$i){ $sendbyte = @socket_sendto($socket, $contents, strlen($contents), 0, "239.255.255.250", 1900); if($sendbyte === false){ - throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + throw new UPnPException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); } if($sendbyte !== strlen($contents)){ - throw new \RuntimeException("Socket error: Unable to send the entire contents."); + throw new UPnPException("Socket error: Unable to send the entire contents."); } while(true){ if(@socket_recvfrom($socket, $buffer, 1024, 0, $responseHost, $responsePort) === false){ if(socket_last_error($socket) === SOCKET_ETIMEDOUT){ continue 2; } - throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + throw new UPnPException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); } $pregResult = preg_match('/location\s*:\s*(.+)\n/i', $buffer, $matches); if($pregResult === false){ @@ -141,33 +141,33 @@ abstract class UPnP{ } socket_close($socket); if($location === null){ - throw new \RuntimeException("Unable to find the router. Ensure that network discovery is enabled in Control Panel."); + throw new UPnPException("Unable to find the router. Ensure that network discovery is enabled in Control Panel."); } $url = parse_url($location); if($url === false){ - throw new \RuntimeException("Failed to parse the router's url: {$location}"); + throw new UPnPException("Failed to parse the router's url: {$location}"); } if(!isset($url['host'])){ - throw new \RuntimeException("Failed to recognize the host name from the router's url: {$location}"); + throw new UPnPException("Failed to recognize the host name from the router's url: {$location}"); } $urlHost = $url['host']; if(!isset($url['port'])){ - throw new \RuntimeException("Failed to recognize the port number from the router's url: {$location}"); + throw new UPnPException("Failed to recognize the port number from the router's url: {$location}"); } $urlPort = $url['port']; - $response = Internet::getURL($location, 3, [], $err, $headers, $httpCode); - if($response === false){ - throw new \RuntimeException("Unable to access XML: {$err}"); + $response = Internet::getURL($location, 3, [], $err); + if($response === null){ + throw new UPnPException("Unable to access XML: {$err}"); } - if($httpCode !== 200){ - throw new \RuntimeException("Unable to access XML: {$response}"); + if($response->getCode() !== 200){ + throw new UPnPException("Unable to access XML: {$response->getBody()}"); } $defaultInternalError = libxml_use_internal_errors(true); try{ - $root = new \SimpleXMLElement($response); + $root = new \SimpleXMLElement($response->getBody()); }catch(\Exception $e){ - throw new \RuntimeException("Broken XML."); + throw new UPnPException("Broken XML."); } libxml_use_internal_errors($defaultInternalError); $root->registerXPathNamespace("upnp", "urn:schemas-upnp-org:device-1-0"); @@ -183,28 +183,24 @@ abstract class UPnP{ throw new AssumptionFailedError("xpath query should not error here"); } if(count($xpathResult) === 0){ - throw new \RuntimeException("Your router does not support portforwarding"); + throw new UPnPException("Your router does not support portforwarding"); } $controlURL = (string) $xpathResult[0]; $serviceURL = sprintf("%s:%d/%s", $urlHost, $urlPort, $controlURL); return $serviceURL; } - public static function PortForward(int $port) : void{ - if(!Internet::$online){ - throw new \RuntimeException("Server is offline"); - } - - if(self::$serviceURL === null){ - self::$serviceURL = self::getServiceUrl(); - } + /** + * @throws UPnPException + */ + public static function portForward(string $serviceURL, string $internalIP, int $internalPort, int $externalPort) : void{ $body = '' . '' . - '' . $port . '' . + '' . $externalPort . '' . 'UDP' . - '' . $port . '' . - '' . Internet::getInternalIP() . '' . + '' . $internalPort . '' . + '' . $internalIP . '' . '1' . 'PocketMine-MP' . '0' . @@ -220,23 +216,16 @@ abstract class UPnP{ 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"' ]; - if(Internet::postURL(self::$serviceURL, $contents, 3, $headers, $err) === false){ - throw new \RuntimeException("Failed to portforward using UPnP: " . $err); + if(Internet::postURL($serviceURL, $contents, 3, $headers, $err) === null){ + throw new UPnPException("Failed to portforward using UPnP: " . $err); } } - public static function RemovePortForward(int $port) : bool{ - if(!Internet::$online){ - return false; - } - if(self::$serviceURL === null){ - return false; - } - + public static function removePortForward(string $serviceURL, int $externalPort) : void{ $body = '' . '' . - '' . $port . '' . + '' . $externalPort . '' . 'UDP' . ''; @@ -250,10 +239,6 @@ abstract class UPnP{ 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"' ]; - if(Internet::postURL(self::$serviceURL, $contents, 3, $headers) === false){ - return false; - } - - return true; + Internet::postURL($serviceURL, $contents, 3, $headers); } } diff --git a/src/network/upnp/UPnPException.php b/src/network/upnp/UPnPException.php new file mode 100644 index 0000000000..9152cec74f --- /dev/null +++ b/src/network/upnp/UPnPException.php @@ -0,0 +1,28 @@ +ip = $ip; + $this->port = $port; + $this->logger = new \PrefixedLogger($logger, "UPnP Port Forwarder"); + } + + public function start() : void{ + $this->logger->info("Attempting to portforward..."); + + 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 | InternetException $e){ + $this->logger->error("UPnP portforward failed: " . $e->getMessage()); + } + } + + public function setName(string $name) : void{ + + } + + public function tick() : void{ + + } + + public function shutdown() : void{ + if($this->serviceURL === null){ + return; + } + + UPnP::removePortForward($this->serviceURL, $this->port); + } +} diff --git a/src/pocketmine/permission/BanEntry.php b/src/permission/BanEntry.php similarity index 74% rename from src/pocketmine/permission/BanEntry.php rename to src/permission/BanEntry.php index 397de1cc26..63858d39c8 100644 --- a/src/pocketmine/permission/BanEntry.php +++ b/src/permission/BanEntry.php @@ -49,6 +49,7 @@ class BanEntry{ public function __construct(string $name){ $this->name = strtolower($name); + /** @noinspection PhpUnhandledExceptionInspection */ $this->creationDate = new \DateTime(); } @@ -61,9 +62,9 @@ class BanEntry{ } /** - * @return void + * @throws \InvalidArgumentException */ - public function setCreated(\DateTime $date){ + public function setCreated(\DateTime $date) : void{ self::validateDate($date); $this->creationDate = $date; } @@ -72,24 +73,18 @@ class BanEntry{ return $this->source; } - /** - * @return void - */ - public function setSource(string $source){ + public function setSource(string $source) : void{ $this->source = $source; } - /** - * @return \DateTime|null - */ - public function getExpires(){ + public function getExpires() : ?\DateTime{ return $this->expirationDate; } /** - * @return void + * @throws \InvalidArgumentException */ - public function setExpires(\DateTime $date = null){ + public function setExpires(?\DateTime $date) : void{ if($date !== null){ self::validateDate($date); } @@ -97,6 +92,7 @@ class BanEntry{ } public function hasExpired() : bool{ + /** @noinspection PhpUnhandledExceptionInspection */ $now = new \DateTime(); return $this->expirationDate === null ? false : $this->expirationDate < $now; @@ -106,10 +102,7 @@ class BanEntry{ return $this->reason; } - /** - * @return void - */ - public function setReason(string $reason){ + public function setReason(string $reason) : void{ $this->reason = $reason; } @@ -134,10 +127,14 @@ class BanEntry{ * * @link https://bugs.php.net/bug.php?id=75992 * - * @throws \RuntimeException if the argument can't be parsed from a formatted date string + * @throws \InvalidArgumentException if the argument can't be parsed from a formatted date string */ private static function validateDate(\DateTime $dateTime) : void{ - self::parseDate($dateTime->format(self::$format)); + try{ + self::parseDate($dateTime->format(self::$format)); + }catch(\RuntimeException $e){ + throw new \InvalidArgumentException($e->getMessage(), 0, $e); + } } /** @@ -148,7 +145,7 @@ class BanEntry{ if(!($datetime instanceof \DateTime)){ $lastErrors = \DateTime::getLastErrors(); if($lastErrors === false) throw new AssumptionFailedError("DateTime::getLastErrors() should not be returning false in here"); - throw new \RuntimeException("Error parsing date for BanEntry: " . implode(", ", $lastErrors["errors"])); + throw new \RuntimeException("Corrupted date/time: " . implode(", ", $lastErrors["errors"])); } return $datetime; @@ -160,32 +157,26 @@ class BanEntry{ public static function fromString(string $str) : ?BanEntry{ if(strlen($str) < 2){ return null; - }else{ - $str = explode("|", trim($str)); - $entry = new BanEntry(trim(array_shift($str))); - if(count($str) === 0){ - return $entry; - } + } - $entry->setCreated(self::parseDate(array_shift($str))); - if(count($str) === 0){ - return $entry; - } - - $entry->setSource(trim(array_shift($str))); - if(count($str) === 0){ - return $entry; - } - - $expire = trim(array_shift($str)); + $parts = explode("|", trim($str)); + $entry = new BanEntry(trim(array_shift($parts))); + if(count($parts) > 0){ + $entry->setCreated(self::parseDate(array_shift($parts))); + } + if(count($parts) > 0){ + $entry->setSource(trim(array_shift($parts))); + } + if(count($parts) > 0){ + $expire = trim(array_shift($parts)); if($expire !== "" and strtolower($expire) !== "forever"){ $entry->setExpires(self::parseDate($expire)); } - if(count($str) === 0){ - return $entry; - } - $entry->setReason(trim(array_shift($str))); - return $entry; } + if(count($parts) > 0){ + $entry->setReason(trim(array_shift($parts))); + } + + return $entry; } } diff --git a/src/pocketmine/permission/BanList.php b/src/permission/BanList.php similarity index 73% rename from src/pocketmine/permission/BanList.php rename to src/permission/BanList.php index 71dae9c84c..714f1a4a37 100644 --- a/src/pocketmine/permission/BanList.php +++ b/src/permission/BanList.php @@ -23,16 +23,13 @@ declare(strict_types=1); namespace pocketmine\permission; -use pocketmine\Server; -use pocketmine\utils\MainLogger; use function fclose; use function fgets; use function fopen; use function fwrite; use function is_resource; -use function strftime; use function strtolower; -use function time; +use function trim; class BanList{ @@ -53,10 +50,7 @@ class BanList{ return $this->enabled; } - /** - * @return void - */ - public function setEnabled(bool $flag){ + public function setEnabled(bool $flag) : void{ $this->enabled = $flag; } @@ -86,15 +80,12 @@ class BanList{ } } - /** - * @return void - */ - public function add(BanEntry $entry){ + public function add(BanEntry $entry) : void{ $this->list[$entry->getName()] = $entry; $this->save(); } - public function addBan(string $target, string $reason = null, \DateTime $expires = null, string $source = null) : BanEntry{ + public function addBan(string $target, ?string $reason = null, ?\DateTime $expires = null, ?string $source = null) : BanEntry{ $entry = new BanEntry($target); $entry->setSource($source ?? $entry->getSource()); $entry->setExpires($expires); @@ -106,10 +97,7 @@ class BanList{ return $entry; } - /** - * @return void - */ - public function remove(string $name){ + public function remove(string $name) : void{ $name = strtolower($name); if(isset($this->list[$name])){ unset($this->list[$name]); @@ -117,10 +105,7 @@ class BanList{ } } - /** - * @return void - */ - public function removeExpired(){ + public function removeExpired() : void{ foreach($this->list as $name => $entry){ if($entry->hasExpired()){ unset($this->list[$name]); @@ -128,10 +113,7 @@ class BanList{ } } - /** - * @return void - */ - public function load(){ + public function load() : void{ $this->list = []; $fp = @fopen($this->file, "r"); if(is_resource($fp)){ @@ -139,31 +121,27 @@ class BanList{ if($line[0] !== "#"){ try{ $entry = BanEntry::fromString($line); - if($entry instanceof BanEntry){ + if($entry !== null){ $this->list[$entry->getName()] = $entry; } - }catch(\Throwable $e){ - $logger = MainLogger::getLogger(); - $logger->critical("Failed to parse ban entry from string \"$line\": " . $e->getMessage()); - $logger->logException($e); + }catch(\RuntimeException $e){ + $logger = \GlobalLogger::get(); + $logger->critical("Failed to parse ban entry from string \"" . trim($line) . "\": " . $e->getMessage()); } + } } fclose($fp); }else{ - MainLogger::getLogger()->error("Could not load ban list"); + \GlobalLogger::get()->error("Could not load ban list"); } } - /** - * @return void - */ - public function save(bool $writeHeader = true){ + public function save(bool $writeHeader = true) : void{ $this->removeExpired(); $fp = @fopen($this->file, "w"); if(is_resource($fp)){ if($writeHeader){ - fwrite($fp, "# Updated " . strftime("%x %H:%M", time()) . " by " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion() . "\n"); fwrite($fp, "# victim name | ban date | banned by | banned until | reason\n\n"); } @@ -172,7 +150,7 @@ class BanList{ } fclose($fp); }else{ - MainLogger::getLogger()->error("Could not save ban list"); + \GlobalLogger::get()->error("Could not save ban list"); } } } diff --git a/src/permission/DefaultPermissionNames.php b/src/permission/DefaultPermissionNames.php new file mode 100644 index 0000000000..d90b72f54d --- /dev/null +++ b/src/permission/DefaultPermissionNames.php @@ -0,0 +1,83 @@ +addChild($candidate->getName(), true); + } + foreach($deniedBy as $permission){ + $permission->addChild($candidate->getName(), false); + } + PermissionManager::getInstance()->addPermission($candidate); + + return PermissionManager::getInstance()->getPermission($candidate->getName()); + } + + public static function registerCorePermissions() : void{ + $consoleRoot = self::registerPermission(new Permission(self::ROOT_CONSOLE, "Grants all console permissions")); + $operatorRoot = self::registerPermission(new Permission(self::ROOT_OPERATOR, "Grants all operator permissions"), [$consoleRoot]); + $everyoneRoot = self::registerPermission(new Permission(self::ROOT_USER, "Grants all non-sensitive permissions that everyone gets by default"), [$operatorRoot]); + + 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_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_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_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_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_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/Permissible.php b/src/permission/Permissible.php new file mode 100644 index 0000000000..81fc23fea8 --- /dev/null +++ b/src/permission/Permissible.php @@ -0,0 +1,86 @@ + old value + * @phpstan-return array + */ + public function recalculatePermissions() : array; + + /** + * @return ObjectSet|\Closure[] + * @phpstan-return ObjectSet<\Closure(array $changedPermissionsOldValues) : void> + */ + public function getPermissionRecalculationCallbacks() : ObjectSet; + + /** + * @return PermissionAttachmentInfo[] + */ + public function getEffectivePermissions() : array; + +} diff --git a/src/permission/PermissibleBase.php b/src/permission/PermissibleBase.php new file mode 100644 index 0000000000..c2d92e9d50 --- /dev/null +++ b/src/permission/PermissibleBase.php @@ -0,0 +1,44 @@ + $basePermissions + */ + public function __construct(array $basePermissions){ + $this->permissibleBase = new PermissibleInternal($basePermissions); + $this->perm = $this->permissibleBase; + } + + public function __destruct(){ + //permission subscriptions need to be cleaned up explicitly + $this->permissibleBase->destroyCycles(); + } +} diff --git a/src/permission/PermissibleDelegateTrait.php b/src/permission/PermissibleDelegateTrait.php new file mode 100644 index 0000000000..d9de362f23 --- /dev/null +++ b/src/permission/PermissibleDelegateTrait.php @@ -0,0 +1,89 @@ +perm->setBasePermission($name, $value); + } + + /** + * @param Permission|string $name + */ + public function unsetBasePermission($name) : void{ + $this->perm->unsetBasePermission($name); + } + + /** + * @param Permission|string $name + */ + public function isPermissionSet($name) : bool{ + return $this->perm->isPermissionSet($name); + } + + /** + * @param Permission|string $name + */ + public function hasPermission($name) : bool{ + return $this->perm->hasPermission($name); + } + + public function addAttachment(Plugin $plugin, ?string $name = null, ?bool $value = null) : PermissionAttachment{ + return $this->perm->addAttachment($plugin, $name, $value); + } + + public function removeAttachment(PermissionAttachment $attachment) : void{ + $this->perm->removeAttachment($attachment); + } + + public function recalculatePermissions() : array{ + return $this->perm->recalculatePermissions(); + } + + /** + * @return ObjectSet|\Closure[] + * @phpstan-return ObjectSet<\Closure(array $changedPermissionsOldValues) : void> + */ + public function getPermissionRecalculationCallbacks() : ObjectSet{ + return $this->perm->getPermissionRecalculationCallbacks(); + } + + /** + * @return PermissionAttachmentInfo[] + */ + public function getEffectivePermissions() : array{ + return $this->perm->getEffectivePermissions(); + } + +} diff --git a/src/permission/PermissibleInternal.php b/src/permission/PermissibleInternal.php new file mode 100644 index 0000000000..14cb0ba1ad --- /dev/null +++ b/src/permission/PermissibleInternal.php @@ -0,0 +1,230 @@ + + */ + private $rootPermissions; + + /** @var PermissionAttachment[] */ + private $attachments = []; + + /** @var PermissionAttachmentInfo[] */ + private $permissions = []; + + /** + * @var ObjectSet|\Closure[] + * @phpstan-var ObjectSet<\Closure(array $changedPermissionsOldValues) : void> + */ + private $permissionRecalculationCallbacks; + + /** + * @param bool[] $basePermissions + * @phpstan-param array $basePermissions + */ + public function __construct(array $basePermissions){ + $this->permissionRecalculationCallbacks = new ObjectSet(); + + $this->rootPermissions = $basePermissions; + $this->recalculatePermissions(); + } + + public function setBasePermission($name, bool $grant) : void{ + if($name instanceof Permission){ + $name = $name->getName(); + } + $this->rootPermissions[$name] = $grant; + $this->recalculatePermissions(); + } + + public function unsetBasePermission($name) : void{ + unset($this->rootPermissions[$name instanceof Permission ? $name->getName() : $name]); + $this->recalculatePermissions(); + } + + /** + * @param Permission|string $name + */ + public function isPermissionSet($name) : bool{ + return isset($this->permissions[$name instanceof Permission ? $name->getName() : $name]); + } + + /** + * @param Permission|string $name + */ + public function hasPermission($name) : bool{ + if($name instanceof Permission){ + $name = $name->getName(); + } + + if($this->isPermissionSet($name)){ + return $this->permissions[$name]->getValue(); + } + + return false; + } + + /** + * //TODO: tick scheduled attachments + */ + public function addAttachment(Plugin $plugin, ?string $name = null, ?bool $value = null) : PermissionAttachment{ + if(!$plugin->isEnabled()){ + throw new PluginException("Plugin " . $plugin->getDescription()->getName() . " is disabled"); + } + + $result = new PermissionAttachment($plugin); + $this->attachments[spl_object_id($result)] = $result; + if($name !== null and $value !== null){ + $result->setPermission($name, $value); + } + + $result->subscribePermissible($this); + + $this->recalculatePermissions(); + + return $result; + } + + public function removeAttachment(PermissionAttachment $attachment) : void{ + if(isset($this->attachments[spl_object_id($attachment)])){ + unset($this->attachments[spl_object_id($attachment)]); + $attachment->unsubscribePermissible($this); + + $this->recalculatePermissions(); + + } + + } + + public function recalculatePermissions() : array{ + Timings::$permissibleCalculation->startTiming(); + + $permManager = PermissionManager::getInstance(); + $permManager->unsubscribeFromAllPermissions($this); + $oldPermissions = $this->permissions; + $this->permissions = []; + + foreach(Utils::stringifyKeys($this->rootPermissions) as $name => $isGranted){ + $perm = $permManager->getPermission($name); + if($perm === null){ + throw new \LogicException("Unregistered root permission $name"); + } + $this->permissions[$name] = new PermissionAttachmentInfo($name, null, $isGranted, null); + $permManager->subscribeToPermission($name, $this); + $this->calculateChildPermissions($perm->getChildren(), !$isGranted, null, $this->permissions[$name]); + } + + foreach($this->attachments as $attachment){ + $this->calculateChildPermissions($attachment->getPermissions(), false, $attachment, null); + } + + $diff = []; + Timings::$permissibleCalculationDiff->time(function() use ($oldPermissions, &$diff) : void{ + foreach($this->permissions as $permissionAttachmentInfo){ + $name = $permissionAttachmentInfo->getPermission(); + if(!isset($oldPermissions[$name])){ + $diff[$name] = false; + }elseif($oldPermissions[$name]->getValue() !== $permissionAttachmentInfo->getValue()){ + continue; + } + unset($oldPermissions[$name]); + } + //oldPermissions now only contains permissions that changed or are no longer set + foreach($oldPermissions as $permissionAttachmentInfo){ + $diff[$permissionAttachmentInfo->getPermission()] = $permissionAttachmentInfo->getValue(); + } + }); + + Timings::$permissibleCalculationCallback->time(function() use ($diff) : void{ + if(count($diff) > 0){ + foreach($this->permissionRecalculationCallbacks as $closure){ + $closure($diff); + } + } + }); + + Timings::$permissibleCalculation->stopTiming(); + return $diff; + } + + /** + * @param bool[] $children + * @phpstan-param array $children + */ + private function calculateChildPermissions(array $children, bool $invert, ?PermissionAttachment $attachment, ?PermissionAttachmentInfo $parent) : void{ + $permManager = PermissionManager::getInstance(); + foreach(Utils::stringifyKeys($children) as $name => $v){ + $perm = $permManager->getPermission($name); + $value = ($v xor $invert); + $this->permissions[$name] = new PermissionAttachmentInfo($name, $attachment, $value, $parent); + $permManager->subscribeToPermission($name, $this); + + if($perm instanceof Permission){ + $this->calculateChildPermissions($perm->getChildren(), !$value, $attachment, $this->permissions[$name]); + } + } + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(array $changedPermissionsOldValues) : void> + */ + public function getPermissionRecalculationCallbacks() : ObjectSet{ return $this->permissionRecalculationCallbacks; } + + /** + * @return PermissionAttachmentInfo[] + */ + public function getEffectivePermissions() : array{ + return $this->permissions; + } + + public function destroyCycles() : void{ + PermissionManager::getInstance()->unsubscribeFromAllPermissions($this); + $this->permissions = []; //PermissionAttachmentInfo doesn't reference Permissible anymore, but it references PermissionAttachment which does + foreach($this->attachments as $attachment){ + $attachment->unsubscribePermissible($this); + } + $this->attachments = []; + $this->permissionRecalculationCallbacks->clear(); + } +} diff --git a/src/permission/Permission.php b/src/permission/Permission.php new file mode 100644 index 0000000000..126f9aba87 --- /dev/null +++ b/src/permission/Permission.php @@ -0,0 +1,105 @@ + + */ + private $children; + + /** + * Creates a new Permission object to be attached to Permissible objects + * + * @param bool[] $children + * @phpstan-param array $children + */ + public function __construct(string $name, ?string $description = null, array $children = []){ + $this->name = $name; + $this->description = $description ?? ""; + $this->children = $children; + + $this->recalculatePermissibles(); + } + + public function getName() : string{ + return $this->name; + } + + /** + * @return bool[] + * @phpstan-return array + */ + public function getChildren() : array{ + return $this->children; + } + + public function getDescription() : string{ + return $this->description; + } + + public function setDescription(string $value) : void{ + $this->description = $value; + } + + /** + * @return PermissibleInternal[] + */ + public function getPermissibles() : array{ + return PermissionManager::getInstance()->getPermissionSubscriptions($this->name); + } + + public function recalculatePermissibles() : void{ + $perms = $this->getPermissibles(); + + foreach($perms as $p){ + $p->recalculatePermissions(); + } + } + + public function addChild(string $name, bool $value) : void{ + $this->children[$name] = $value; + $this->recalculatePermissibles(); + } + + public function removeChild(string $name) : void{ + unset($this->children[$name]); + $this->recalculatePermissibles(); + + } +} diff --git a/src/pocketmine/permission/PermissionAttachment.php b/src/permission/PermissionAttachment.php similarity index 69% rename from src/pocketmine/permission/PermissionAttachment.php rename to src/permission/PermissionAttachment.php index 3427dadc06..1af03baa9f 100644 --- a/src/pocketmine/permission/PermissionAttachment.php +++ b/src/permission/PermissionAttachment.php @@ -25,16 +25,17 @@ namespace pocketmine\permission; use pocketmine\plugin\Plugin; use pocketmine\plugin\PluginException; +use function spl_object_id; class PermissionAttachment{ - /** @var PermissionRemovedExecutor|null */ - private $removed = null; - /** @var bool[] */ private $permissions = []; - /** @var Permissible */ - private $permissible; + /** + * @var PermissibleInternal[] + * @phpstan-var array + */ + private $subscribers = []; /** @var Plugin */ private $plugin; @@ -42,12 +43,11 @@ class PermissionAttachment{ /** * @throws PluginException */ - public function __construct(Plugin $plugin, Permissible $permissible){ + public function __construct(Plugin $plugin){ if(!$plugin->isEnabled()){ throw new PluginException("Plugin " . $plugin->getDescription()->getName() . " is disabled"); } - $this->permissible = $permissible; $this->plugin = $plugin; } @@ -56,22 +56,10 @@ class PermissionAttachment{ } /** - * @return void + * @return PermissibleInternal[] + * @phpstan-return array */ - public function setRemovalCallback(PermissionRemovedExecutor $ex){ - $this->removed = $ex; - } - - /** - * @return PermissionRemovedExecutor|null - */ - public function getRemovalCallback(){ - return $this->removed; - } - - public function getPermissible() : Permissible{ - return $this->permissible; - } + public function getSubscribers() : array{ return $this->subscribers; } /** * @return bool[] @@ -80,44 +68,41 @@ class PermissionAttachment{ return $this->permissions; } - /** - * @return void - */ - public function clearPermissions(){ + private function recalculatePermissibles() : void{ + foreach($this->subscribers as $permissible){ + $permissible->recalculatePermissions(); + } + } + + public function clearPermissions() : void{ $this->permissions = []; - $this->permissible->recalculatePermissions(); + $this->recalculatePermissibles(); } /** * @param bool[] $permissions - * - * @return void */ - public function setPermissions(array $permissions){ + public function setPermissions(array $permissions) : void{ foreach($permissions as $key => $value){ $this->permissions[$key] = $value; } - $this->permissible->recalculatePermissions(); + $this->recalculatePermissibles(); } /** * @param string[] $permissions - * - * @return void */ - public function unsetPermissions(array $permissions){ + public function unsetPermissions(array $permissions) : void{ foreach($permissions as $node){ unset($this->permissions[$node]); } - $this->permissible->recalculatePermissions(); + $this->recalculatePermissibles(); } /** * @param string|Permission $name - * - * @return void */ - public function setPermission($name, bool $value){ + public function setPermission($name, bool $value) : void{ $name = $name instanceof Permission ? $name->getName() : $name; if(isset($this->permissions[$name])){ if($this->permissions[$name] === $value){ @@ -135,26 +120,31 @@ class PermissionAttachment{ unset($this->permissions[$name]); } $this->permissions[$name] = $value; - $this->permissible->recalculatePermissions(); + $this->recalculatePermissibles(); } /** * @param string|Permission $name - * - * @return void */ - public function unsetPermission($name){ + public function unsetPermission($name) : void{ $name = $name instanceof Permission ? $name->getName() : $name; if(isset($this->permissions[$name])){ unset($this->permissions[$name]); - $this->permissible->recalculatePermissions(); + $this->recalculatePermissibles(); } } /** - * @return void + * @internal */ - public function remove(){ - $this->permissible->removeAttachment($this); + public function subscribePermissible(PermissibleInternal $permissible) : void{ + $this->subscribers[spl_object_id($permissible)] = $permissible; + } + + /** + * @internal + */ + public function unsubscribePermissible(PermissibleInternal $permissible) : void{ + unset($this->subscribers[spl_object_id($permissible)]); } } diff --git a/src/pocketmine/permission/PermissionAttachmentInfo.php b/src/permission/PermissionAttachmentInfo.php similarity index 66% rename from src/pocketmine/permission/PermissionAttachmentInfo.php rename to src/permission/PermissionAttachmentInfo.php index 4d5587ce0e..b786d567e6 100644 --- a/src/pocketmine/permission/PermissionAttachmentInfo.php +++ b/src/permission/PermissionAttachmentInfo.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace pocketmine\permission; class PermissionAttachmentInfo{ - /** @var Permissible */ - private $permissible; - /** @var string */ private $permission; @@ -36,29 +33,31 @@ class PermissionAttachmentInfo{ /** @var bool */ private $value; - public function __construct(Permissible $permissible, string $permission, PermissionAttachment $attachment = null, bool $value){ - $this->permissible = $permissible; + /** @var PermissionAttachmentInfo|null */ + private $groupPermission; + + public function __construct(string $permission, ?PermissionAttachment $attachment, bool $value, ?PermissionAttachmentInfo $groupPermission){ $this->permission = $permission; $this->attachment = $attachment; $this->value = $value; - } - - public function getPermissible() : Permissible{ - return $this->permissible; + $this->groupPermission = $groupPermission; } public function getPermission() : string{ return $this->permission; } - /** - * @return PermissionAttachment|null - */ - public function getAttachment(){ + public function getAttachment() : ?PermissionAttachment{ return $this->attachment; } public function getValue() : bool{ return $this->value; } + + /** + * Returns the info of the permission group that caused this permission to be set, if any. + * If null, the permission was set explicitly, either by a permission attachment or base permission. + */ + public function getGroupPermissionInfo() : ?PermissionAttachmentInfo{ return $this->groupPermission; } } diff --git a/src/permission/PermissionManager.php b/src/permission/PermissionManager.php new file mode 100644 index 0000000000..b8c859af35 --- /dev/null +++ b/src/permission/PermissionManager.php @@ -0,0 +1,113 @@ +permissions[$name] ?? null; + } + + public function addPermission(Permission $permission) : bool{ + if(!isset($this->permissions[$permission->getName()])){ + $this->permissions[$permission->getName()] = $permission; + + return true; + } + + return false; + } + + /** + * @param string|Permission $permission + */ + public function removePermission($permission) : void{ + if($permission instanceof Permission){ + unset($this->permissions[$permission->getName()]); + }else{ + unset($this->permissions[$permission]); + } + } + + public function subscribeToPermission(string $permission, PermissibleInternal $permissible) : void{ + if(!isset($this->permSubs[$permission])){ + $this->permSubs[$permission] = []; + } + $this->permSubs[$permission][spl_object_id($permissible)] = $permissible; + } + + public function unsubscribeFromPermission(string $permission, PermissibleInternal $permissible) : void{ + if(isset($this->permSubs[$permission])){ + unset($this->permSubs[$permission][spl_object_id($permissible)]); + if(count($this->permSubs[$permission]) === 0){ + unset($this->permSubs[$permission]); + } + } + } + + public function unsubscribeFromAllPermissions(PermissibleInternal $permissible) : void{ + foreach($this->permSubs as $permission => &$subs){ + unset($subs[spl_object_id($permissible)]); + if(count($subs) === 0){ + unset($this->permSubs[$permission]); + } + } + } + + /** + * @return PermissibleInternal[] + */ + public function getPermissionSubscriptions(string $permission) : array{ + return $this->permSubs[$permission] ?? []; + } + + /** + * @return Permission[] + */ + public function getPermissions() : array{ + return $this->permissions; + } + + public function clearPermissions() : void{ + $this->permissions = []; + } +} diff --git a/src/permission/PermissionParser.php b/src/permission/PermissionParser.php new file mode 100644 index 0000000000..8aab762cf6 --- /dev/null +++ b/src/permission/PermissionParser.php @@ -0,0 +1,105 @@ + self::DEFAULT_OP, + "isop" => self::DEFAULT_OP, + "operator" => self::DEFAULT_OP, + "isoperator" => self::DEFAULT_OP, + "admin" => self::DEFAULT_OP, + "isadmin" => self::DEFAULT_OP, + + "!op" => self::DEFAULT_NOT_OP, + "notop" => self::DEFAULT_NOT_OP, + "!operator" => self::DEFAULT_NOT_OP, + "notoperator" => self::DEFAULT_NOT_OP, + "!admin" => self::DEFAULT_NOT_OP, + "notadmin" => self::DEFAULT_NOT_OP, + + "true" => self::DEFAULT_TRUE, + "false" => self::DEFAULT_FALSE, + ]; + + /** + * @param bool|string $value + * + * @throws PermissionParserException + */ + public static function defaultFromString($value) : string{ + if(is_bool($value)){ + if($value){ + return "true"; + }else{ + return "false"; + } + } + $lower = strtolower($value); + if(isset(self::DEFAULT_STRING_MAP[$lower])){ + return self::DEFAULT_STRING_MAP[$lower]; + } + + throw new PermissionParserException("Unknown permission default name \"$value\""); + } + + /** + * @param mixed[][] $data + * @phpstan-param array> $data + * + * @return Permission[][] + * @phpstan-return array> + * @throws PermissionParserException + */ + public static function loadPermissions(array $data, string $default = self::DEFAULT_FALSE) : array{ + $result = []; + foreach(Utils::stringifyKeys($data) as $name => $entry){ + $desc = null; + if(isset($entry["default"])){ + $default = PermissionParser::defaultFromString($entry["default"]); + } + + if(isset($entry["children"])){ + throw new PermissionParserException("Nested permission declarations are no longer supported. Declare each permission separately."); + } + + if(isset($entry["description"])){ + $desc = $entry["description"]; + } + + $result[$default][] = new Permission($name, $desc); + } + return $result; + } +} diff --git a/src/pocketmine/permission/PermissionRemovedExecutor.php b/src/permission/PermissionParserException.php similarity index 83% rename from src/pocketmine/permission/PermissionRemovedExecutor.php rename to src/permission/PermissionParserException.php index 423f883b0a..a0231f8058 100644 --- a/src/pocketmine/permission/PermissionRemovedExecutor.php +++ b/src/permission/PermissionParserException.php @@ -23,10 +23,9 @@ declare(strict_types=1); namespace pocketmine\permission; -interface PermissionRemovedExecutor{ +/** + * Thrown by PermissionParser when it encounters data that it doesn't like. + */ +final class PermissionParserException extends \RuntimeException{ - /** - * @return void - */ - public function attachmentRemoved(PermissionAttachment $attachment); } diff --git a/src/player/ChunkSelector.php b/src/player/ChunkSelector.php new file mode 100644 index 0000000000..3ba3142159 --- /dev/null +++ b/src/player/ChunkSelector.php @@ -0,0 +1,79 @@ + + */ + public function selectChunks(int $radius, int $centerX, int $centerZ) : \Generator{ + for($subRadius = 0; $subRadius < $radius; $subRadius++){ + $subRadiusSquared = $subRadius ** 2; + $nextSubRadiusSquared = ($subRadius + 1) ** 2; + $minX = (int) ($subRadius / M_SQRT2); + + $lastZ = 0; + + 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 + } + + $lastZ = $z; + //If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be. + + /* 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/GameMode.php b/src/player/GameMode.php new file mode 100644 index 0000000000..a604f720ee --- /dev/null +++ b/src/player/GameMode.php @@ -0,0 +1,100 @@ +getAliases() as $alias){ + self::$aliasMap[mb_strtolower($alias)] = $member; + } + } + + public static function fromString(string $str) : ?self{ + self::checkInit(); + return self::$aliasMap[mb_strtolower($str)] ?? null; + } + + /** @var string */ + private $englishName; + /** @var string[] */ + private $aliases; + + /** + * @param string[] $aliases + */ + private function __construct(string $enumName, string $englishName, private Translatable $translatableName, array $aliases = []){ + $this->Enum___construct($enumName); + $this->englishName = $englishName; + $this->aliases = $aliases; + } + + public function getEnglishName() : string{ + return $this->englishName; + } + + public function getTranslatableName() : Translatable{ return $this->translatableName; } + + /** + * @return string[] + */ + public function getAliases() : array{ + return $this->aliases; + } + + //TODO: ability sets per gamemode +} diff --git a/src/player/IPlayer.php b/src/player/IPlayer.php new file mode 100644 index 0000000000..4fa30141e4 --- /dev/null +++ b/src/player/IPlayer.php @@ -0,0 +1,36 @@ +name = $name; - $this->componentNbt = $componentNbt; + $this->namedtag = $namedtag; } - public function getName() : string{ return $this->name; } + public function getName() : string{ + return $this->name; + } - public function getComponentNbt() : CompoundTag{ return $this->componentNbt; } + public function getFirstPlayed() : ?int{ + return ($this->namedtag !== null and ($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; + } + + public function hasPlayedBefore() : bool{ + return $this->namedtag !== null; + } } diff --git a/src/player/Player.php b/src/player/Player.php new file mode 100644 index 0000000000..aab421014e --- /dev/null +++ b/src/player/Player.php @@ -0,0 +1,2448 @@ += 1 and $len <= 16 and preg_match("/[^A-Za-z0-9_ ]/", $name) === 0; + } + + protected ?NetworkSession $networkSession; + + public bool $spawned = false; + + protected string $username; + protected string $displayName; + protected string $xuid = ""; + protected bool $authenticated; + protected PlayerInfo $playerInfo; + + protected ?Inventory $currentWindow = null; + /** @var Inventory[] */ + protected array $permanentWindows = []; + protected PlayerCursorInventory $cursorInventory; + protected PlayerCraftingInventory $craftingGrid; + + protected int $messageCounter = 2; + + protected int $firstPlayed; + protected int $lastPlayed; + protected GameMode $gamemode; + + /** + * @var UsedChunkStatus[] chunkHash => status + * @phpstan-var array + */ + protected array $usedChunks = []; + /** + * @var true[] + * @phpstan-var array + */ + private array $activeChunkGenerationRequests = []; + /** + * @var true[] chunkHash => dummy + * @phpstan-var array + */ + protected array $loadQueue = []; + protected int $nextChunkOrderRun = 5; + + protected int $viewDistance = -1; + protected int $spawnThreshold; + protected int $spawnChunkLoadCount = 0; + protected int $chunksPerTick; + protected ChunkSelector $chunkSelector; + protected PlayerChunkLoader $chunkLoader; + + /** @var bool[] map: raw UUID (string) => bool */ + protected array $hiddenPlayers = []; + + protected float $moveRateLimit = 10 * self::MOVES_PER_TICK; + protected ?float $lastMovementProcess = null; + + protected int $inAirTicks = 0; + /** @var float */ + protected $stepHeight = 0.6; + + protected ?Vector3 $sleeping = null; + private ?Position $spawnPosition = null; + + private bool $respawnLocked = false; + + //TODO: Abilities + protected bool $autoJump = true; + protected bool $allowFlight = false; + protected bool $flying = false; + + protected ?int $lineHeight = null; + protected string $locale = "en_US"; + + protected int $startAction = -1; + /** @var int[] ID => ticks map */ + protected array $usedItemsCooldown = []; + + private int $lastEmoteTick = 0; + + protected int $formIdCounter = 0; + /** @var Form[] */ + protected array $forms = []; + + protected \Logger $logger; + + protected ?SurvivalBlockBreakHandler $blockBreakHandler = null; + + public function __construct(Server $server, NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, Location $spawnLocation, ?CompoundTag $namedtag){ + $username = TextFormat::clean($playerInfo->getUsername()); + $this->logger = new \PrefixedLogger($server->getLogger(), "Player: $username"); + + $this->server = $server; + $this->networkSession = $session; + $this->playerInfo = $playerInfo; + $this->authenticated = $authenticated; + + $this->username = $username; + $this->displayName = $this->username; + $this->locale = $this->playerInfo->getLocale(); + + $this->uuid = $this->playerInfo->getUuid(); + $this->xuid = $this->playerInfo instanceof XboxLivePlayerInfo ? $this->playerInfo->getXuid() : ""; + + $rootPermissions = [DefaultPermissions::ROOT_USER => true]; + if($this->server->isOp($this->username)){ + $rootPermissions[DefaultPermissions::ROOT_OPERATOR] = true; + } + $this->perm = new PermissibleBase($rootPermissions); + $this->chunksPerTick = $this->server->getConfigGroup()->getPropertyInt("chunk-sending.per-tick", 4); + $this->spawnThreshold = (int) (($this->server->getConfigGroup()->getPropertyInt("chunk-sending.spawn-radius", 4) ** 2) * M_PI); + $this->chunkSelector = new ChunkSelector(); + + $this->chunkLoader = new PlayerChunkLoader($spawnLocation); + + $world = $spawnLocation->getWorld(); + //load the spawn chunk so we can see the terrain + $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); + } + + protected function initHumanData(CompoundTag $nbt) : void{ + $this->setNameTag($this->username); + } + + protected function initEntity(CompoundTag $nbt) : void{ + 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){ + $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()); + } + + $this->keepMovement = true; + + $this->setNameTagVisible(); + $this->setNameTagAlwaysVisible(); + $this->setCanClimb(); + + if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString("SpawnLevel", ""))) instanceof World){ + $this->spawnPosition = new Position($nbt->getInt("SpawnX"), $nbt->getInt("SpawnY"), $nbt->getInt("SpawnZ"), $world); + } + } + + public function getLeaveMessage() : Translatable|string{ + if($this->spawned){ + return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW); + } + + return ""; + } + + public function isAuthenticated() : bool{ + return $this->authenticated; + } + + /** + * Returns an object containing information about the player, such as their username, skin, and misc extra + * client-specific data. + */ + public function getPlayerInfo() : PlayerInfo{ return $this->playerInfo; } + + /** + * If the player is logged into Xbox Live, returns their Xbox user ID (XUID) as a string. Returns an empty string if + * the player is not logged into Xbox Live. + */ + public function getXuid() : string{ + return $this->xuid; + } + + /** + * Returns the player's UUID. This should be the preferred method to identify a player. + * It does not change if the player changes their username. + * + * All players will have a UUID, regardless of whether they are logged into Xbox Live or not. However, note that + * non-XBL players can fake their UUIDs. + */ + public function getUniqueId() : UuidInterface{ + return parent::getUniqueId(); + } + + /** + * TODO: not sure this should be nullable + */ + public function getFirstPlayed() : ?int{ + return $this->firstPlayed; + } + + /** + * TODO: not sure this should be nullable + */ + public function getLastPlayed() : ?int{ + return $this->lastPlayed; + } + + public function hasPlayedBefore() : bool{ + return $this->lastPlayed - $this->firstPlayed > 1; // microtime(true) - microtime(true) may have less than one millisecond difference + } + + public function setAllowFlight(bool $value) : void{ + $this->allowFlight = $value; + $this->getNetworkSession()->syncAdventureSettings($this); + } + + public function getAllowFlight() : bool{ + return $this->allowFlight; + } + + public function setFlying(bool $value) : void{ + if($this->flying !== $value){ + $this->flying = $value; + $this->resetFallDistance(); + $this->getNetworkSession()->syncAdventureSettings($this); + } + } + + public function isFlying() : bool{ + return $this->flying; + } + + public function setAutoJump(bool $value) : void{ + $this->autoJump = $value; + $this->getNetworkSession()->syncAdventureSettings($this); + } + + public function hasAutoJump() : bool{ + return $this->autoJump; + } + + public function spawnTo(Player $player) : void{ + if($this->isAlive() and $player->isAlive() and $player->canSee($this) and !$this->isSpectator()){ + parent::spawnTo($player); + } + } + + public function getServer() : Server{ + return $this->server; + } + + public function getScreenLineHeight() : int{ + return $this->lineHeight ?? 7; + } + + public function setScreenLineHeight(?int $height) : void{ + if($height !== null and $height < 1){ + throw new \InvalidArgumentException("Line height must be at least 1"); + } + $this->lineHeight = $height; + } + + public function canSee(Player $player) : bool{ + return !isset($this->hiddenPlayers[$player->getUniqueId()->getBytes()]); + } + + public function hidePlayer(Player $player) : void{ + if($player === $this){ + return; + } + $this->hiddenPlayers[$player->getUniqueId()->getBytes()] = true; + $player->despawnFrom($this); + } + + public function showPlayer(Player $player) : void{ + if($player === $this){ + return; + } + unset($this->hiddenPlayers[$player->getUniqueId()->getBytes()]); + if($player->isOnline()){ + $player->spawnTo($this); + } + } + + public function canCollideWith(Entity $entity) : bool{ + return false; + } + + public function canBeCollidedWith() : bool{ + return !$this->isSpectator() and parent::canBeCollidedWith(); + } + + public function resetFallDistance() : void{ + parent::resetFallDistance(); + $this->inAirTicks = 0; + } + + public function getViewDistance() : int{ + return $this->viewDistance; + } + + public function setViewDistance(int $distance) : void{ + $this->viewDistance = $this->server->getAllowedViewDistance($distance); + + $this->spawnThreshold = (int) (min($this->viewDistance, $this->server->getConfigGroup()->getPropertyInt("chunk-sending.spawn-radius", 4)) ** 2 * M_PI); + + $this->nextChunkOrderRun = 0; + + $this->getNetworkSession()->syncViewAreaRadius($this->viewDistance); + + $this->logger->debug("Setting view distance to " . $this->viewDistance . " (requested " . $distance . ")"); + } + + public function isOnline() : bool{ + return $this->isConnected(); + } + + public function isConnected() : bool{ + return $this->networkSession !== null and $this->networkSession->isConnected(); + } + + public function getNetworkSession() : NetworkSession{ + if($this->networkSession === null){ + throw new \LogicException("Player is not connected"); + } + return $this->networkSession; + } + + /** + * Gets the username + */ + public function getName() : string{ + return $this->username; + } + + /** + * Returns the "friendly" display name of this player to use in the chat. + */ + public function getDisplayName() : string{ + return $this->displayName; + } + + public function setDisplayName(string $name) : void{ + $ev = new PlayerDisplayNameChangeEvent($this, $this->displayName, $name); + $ev->call(); + + $this->displayName = $ev->getNewName(); + } + + /** + * Returns the player's locale, e.g. en_US. + */ + public function getLocale() : string{ + return $this->locale; + } + + public function getLanguage() : Language{ + return $this->server->getLanguage(); + } + + /** + * Called when a player changes their skin. + * Plugin developers should not use this, use setSkin() and sendSkin() instead. + */ + public function changeSkin(Skin $skin, string $newSkinName, string $oldSkinName) : bool{ + $ev = new PlayerChangeSkinEvent($this, $this->getSkin(), $skin); + $ev->call(); + + if($ev->isCancelled()){ + $this->sendSkin([$this]); + return true; + } + + $this->setSkin($ev->getNewSkin()); + $this->sendSkin($this->server->getOnlinePlayers()); + return true; + } + + /** + * {@inheritdoc} + * + * If null is given, will additionally send the skin to the player itself as well as its viewers. + */ + public function sendSkin(?array $targets = null) : void{ + parent::sendSkin($targets ?? $this->server->getOnlinePlayers()); + } + + /** + * Returns whether the player is currently using an item (right-click and hold). + */ + public function isUsingItem() : bool{ + return $this->startAction > -1; + } + + public function setUsingItem(bool $value) : void{ + $this->startAction = $value ? $this->server->getTick() : -1; + $this->networkPropertiesDirty = true; + } + + /** + * Returns how long the player has been using their currently-held item for. Used for determining arrow shoot force + * for bows. + */ + public function getItemUseDuration() : int{ + return $this->startAction === -1 ? -1 : ($this->server->getTick() - $this->startAction); + } + + /** + * Returns the server tick on which the player's cooldown period expires for the given item. + */ + public function getItemCooldownExpiry(Item $item) : int{ + $this->checkItemCooldowns(); + return $this->usedItemsCooldown[$item->getId()] ?? 0; + } + + /** + * Returns whether the player has a cooldown period left before it can use the given item again. + */ + public function hasItemCooldown(Item $item) : bool{ + $this->checkItemCooldowns(); + return isset($this->usedItemsCooldown[$item->getId()]); + } + + /** + * Resets the player's cooldown time for the given item back to the maximum. + */ + public function resetItemCooldown(Item $item, ?int $ticks = null) : void{ + $ticks = $ticks ?? $item->getCooldownTicks(); + if($ticks > 0){ + $this->usedItemsCooldown[$item->getId()] = $this->server->getTick() + $ticks; + } + } + + protected function checkItemCooldowns() : void{ + $serverTick = $this->server->getTick(); + foreach($this->usedItemsCooldown as $itemId => $cooldownUntil){ + if($cooldownUntil <= $serverTick){ + unset($this->usedItemsCooldown[$itemId]); + } + } + } + + protected function setPosition(Vector3 $pos) : bool{ + $oldWorld = $this->location->isValid() ? $this->location->getWorld() : null; + if(parent::setPosition($pos)){ + $newWorld = $this->getWorld(); + if($oldWorld !== $newWorld){ + if($oldWorld !== null){ + foreach($this->usedChunks as $index => $status){ + World::getXZ($index, $X, $Z); + $this->unloadChunk($X, $Z, $oldWorld); + } + } + + $this->usedChunks = []; + $this->loadQueue = []; + $this->getNetworkSession()->onEnterWorld(); + } + + return true; + } + + return false; + } + + protected function unloadChunk(int $x, int $z, ?World $world = null) : void{ + $world = $world ?? $this->getWorld(); + $index = World::chunkHash($x, $z); + if(isset($this->usedChunks[$index])){ + foreach($world->getChunkEntities($x, $z) as $entity){ + if($entity !== $this){ + $entity->despawnFrom($this); + } + } + $this->getNetworkSession()->stopUsingChunk($x, $z); + unset($this->usedChunks[$index]); + unset($this->activeChunkGenerationRequests[$index]); + } + $world->unregisterChunkLoader($this->chunkLoader, $x, $z); + $world->unregisterChunkListener($this, $x, $z); + unset($this->loadQueue[$index]); + } + + protected function spawnEntitiesOnAllChunks() : void{ + foreach($this->usedChunks as $chunkHash => $status){ + if($status->equals(UsedChunkStatus::SENT())){ + World::getXZ($chunkHash, $chunkX, $chunkZ); + $this->spawnEntitiesOnChunk($chunkX, $chunkZ); + } + } + } + + protected function spawnEntitiesOnChunk(int $chunkX, int $chunkZ) : void{ + foreach($this->getWorld()->getChunkEntities($chunkX, $chunkZ) as $entity){ + if($entity !== $this and !$entity->isFlaggedForDespawn()){ + $entity->spawnTo($this); + } + } + } + + /** + * Requests chunks from the world to be sent, up to a set limit every tick. This operates on the results of the most recent chunk + * order. + */ + protected function requestChunks() : void{ + if(!$this->isConnected()){ + return; + } + + Timings::$playerChunkSend->startTiming(); + + $count = 0; + $world = $this->getWorld(); + + $limit = $this->chunksPerTick - count($this->activeChunkGenerationRequests); + foreach($this->loadQueue as $index => $distance){ + if($count >= $limit){ + break; + } + + $X = null; + $Z = null; + World::getXZ($index, $X, $Z); + assert(is_int($X) and is_int($Z)); + + ++$count; + + $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); + + $this->getWorld()->requestChunkPopulation($X, $Z, $this->chunkLoader)->onCompletion( + function() use ($X, $Z, $index, $world) : void{ + if(!$this->isConnected() || !isset($this->usedChunks[$index]) || $world !== $this->getWorld()){ + return; + } + 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->activeChunkGenerationRequests[$index]); + $this->usedChunks[$index] = UsedChunkStatus::REQUESTED_SENDING(); + + $this->getNetworkSession()->startUsingChunk($X, $Z, function() use ($X, $Z, $index) : void{ + $this->usedChunks[$index] = UsedChunkStatus::SENT(); + if($this->spawnChunkLoadCount === -1){ + $this->spawnEntitiesOnChunk($X, $Z); + }elseif($this->spawnChunkLoadCount++ === $this->spawnThreshold){ + $this->spawnChunkLoadCount = -1; + + $this->spawnEntitiesOnAllChunks(); + + $this->getNetworkSession()->notifyTerrainReady(); + } + }); + }, + static function() : void{ + //NOOP: we'll re-request this if it fails anyway + } + ); + } + + Timings::$playerChunkSend->stopTiming(); + } + + private function recheckBroadcastPermissions() : void{ + foreach([ + DefaultPermissionNames::BROADCAST_ADMIN => Server::BROADCAST_CHANNEL_ADMINISTRATIVE, + DefaultPermissionNames::BROADCAST_USER => Server::BROADCAST_CHANNEL_USERS + ] as $permission => $channel){ + if($this->hasPermission($permission)){ + $this->server->subscribeToBroadcastChannel($channel, $this); + }else{ + $this->server->unsubscribeFromBroadcastChannel($channel, $this); + } + } + } + + /** + * Called by the network system when the pre-spawn sequence is completed (e.g. after sending spawn chunks). + * This fires join events and broadcasts join messages to other online players. + */ + public function doFirstSpawn() : void{ + if($this->spawned){ + return; + } + $this->spawned = true; + $this->recheckBroadcastPermissions(); + $this->getPermissionRecalculationCallbacks()->add(function(array $changedPermissionsOldValues) : void{ + if(isset($changedPermissionsOldValues[Server::BROADCAST_CHANNEL_ADMINISTRATIVE]) || isset($changedPermissionsOldValues[Server::BROADCAST_CHANNEL_USERS])){ + $this->recheckBroadcastPermissions(); + } + }); + + $ev = new PlayerJoinEvent($this, + KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW) + ); + $ev->call(); + if($ev->getJoinMessage() !== ""){ + $this->server->broadcastMessage($ev->getJoinMessage()); + } + + $this->noDamageTicks = 60; + + $this->spawnToAll(); + + if($this->getHealth() <= 0){ + $this->logger->debug("Quit while dead, forcing respawn"); + $this->actuallyRespawn(); + } + } + + /** + * Calculates which new chunks this player needs to use, and which currently-used chunks it needs to stop using. + * 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){ + return; + } + + Timings::$playerChunkOrder->startTiming(); + + $newOrder = []; + $unloadChunks = $this->usedChunks; + + foreach($this->chunkSelector->selectChunks( + $this->server->getAllowedViewDistance($this->viewDistance), + $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())){ + $newOrder[$hash] = true; + } + unset($unloadChunks[$hash]); + } + + foreach($unloadChunks as $index => $status){ + World::getXZ($index, $X, $Z); + $this->unloadChunk($X, $Z); + } + + $this->loadQueue = $newOrder; + if(count($this->loadQueue) > 0 or count($unloadChunks) > 0){ + $this->chunkLoader->setCurrentLocation($this->location); + $this->getNetworkSession()->syncViewAreaCenterPoint($this->location, $this->viewDistance); + } + + Timings::$playerChunkOrder->stopTiming(); + } + + /** + * Returns whether the player is using the chunk with the given coordinates, irrespective of whether the chunk has + * been sent yet. + */ + public function isUsingChunk(int $chunkX, int $chunkZ) : bool{ + return isset($this->usedChunks[World::chunkHash($chunkX, $chunkZ)]); + } + + /** + * @return UsedChunkStatus[] chunkHash => status + * @phpstan-return array + */ + public function getUsedChunks() : array{ + return $this->usedChunks; + } + + /** + * Returns a usage status of the given chunk, or null if the player is not using the given chunk. + */ + public function getUsedChunkStatus(int $chunkX, int $chunkZ) : ?UsedChunkStatus{ + return $this->usedChunks[World::chunkHash($chunkX, $chunkZ)] ?? null; + } + + /** + * Returns whether the target chunk has been sent to this player. + */ + public function hasReceivedChunk(int $chunkX, int $chunkZ) : bool{ + $status = $this->usedChunks[World::chunkHash($chunkX, $chunkZ)] ?? null; + return $status !== null and $status->equals(UsedChunkStatus::SENT()); + } + + /** + * Ticks the chunk-requesting mechanism. + */ + public function doChunkRequests() : void{ + if($this->nextChunkOrderRun !== PHP_INT_MAX and $this->nextChunkOrderRun-- <= 0){ + $this->nextChunkOrderRun = PHP_INT_MAX; + $this->orderChunks(); + } + + if(count($this->loadQueue) > 0){ + $this->requestChunks(); + } + } + + /** + * @return Position + */ + public function getSpawn(){ + if($this->hasValidCustomSpawn()){ + return $this->spawnPosition; + }else{ + $world = $this->server->getWorldManager()->getDefaultWorld(); + + return $world->getSpawnLocation(); + } + } + + public function hasValidCustomSpawn() : bool{ + return $this->spawnPosition !== null and $this->spawnPosition->isValid(); + } + + /** + * Sets the spawnpoint of the player (and the compass direction) to a Vector3, or set it on another world with a + * Position object + * + * @param Vector3|Position|null $pos + */ + public function setSpawn(?Vector3 $pos) : void{ + if($pos !== null){ + if(!($pos instanceof Position)){ + $world = $this->getWorld(); + }else{ + $world = $pos->getWorld(); + } + $this->spawnPosition = new Position($pos->x, $pos->y, $pos->z, $world); + }else{ + $this->spawnPosition = null; + } + $this->getNetworkSession()->syncPlayerSpawnPoint($this->getSpawn()); + } + + public function isSleeping() : bool{ + return $this->sleeping !== null; + } + + public function sleepOn(Vector3 $pos) : bool{ + $pos = $pos->floor(); + $b = $this->getWorld()->getBlock($pos); + + $ev = new PlayerBedEnterEvent($this, $b); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + if($b instanceof Bed){ + $b->setOccupied(); + $this->getWorld()->setBlock($pos, $b); + } + + $this->sleeping = $pos; + $this->networkPropertiesDirty = true; + + $this->setSpawn($pos); + + $this->getWorld()->setSleepTicks(60); + + return true; + } + + public function stopSleep() : void{ + if($this->sleeping instanceof Vector3){ + $b = $this->getWorld()->getBlock($this->sleeping); + if($b instanceof Bed){ + $b->setOccupied(false); + $this->getWorld()->setBlock($this->sleeping, $b); + } + (new PlayerBedLeaveEvent($this, $b))->call(); + + $this->sleeping = null; + $this->networkPropertiesDirty = true; + + $this->getWorld()->setSleepTicks(0); + + $this->getNetworkSession()->sendDataPacket(AnimatePacket::create($this->getId(), AnimatePacket::ACTION_STOP_SLEEP)); + } + } + + public function getGamemode() : GameMode{ + return $this->gamemode; + } + + protected function internalSetGameMode(GameMode $gameMode) : void{ + $this->gamemode = $gameMode; + + $this->allowFlight = $this->isCreative(); + $this->hungerManager->setEnabled($this->isSurvival()); + + if($this->isSpectator()){ + $this->setFlying(true); + $this->setSilent(); + $this->onGround = false; + + //TODO: HACK! this syncs the onground flag with the client so that flying works properly + //this is a yucky hack but we don't have any other options :( + $this->sendPosition($this->location, null, null, MovePlayerPacket::MODE_TELEPORT); + }else{ + if($this->isSurvival()){ + $this->setFlying(false); + } + $this->setSilent(false); + $this->checkGroundState(0, 0, 0, 0, 0, 0); + } + } + + /** + * Sets the gamemode, and if needed, kicks the Player. + */ + public function setGamemode(GameMode $gm) : bool{ + if($this->gamemode->equals($gm)){ + return false; + } + + $ev = new PlayerGameModeChangeEvent($this, $gm); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $this->internalSetGameMode($gm); + + if($this->isSpectator()){ + $this->despawnFromAll(); + }else{ + $this->spawnToAll(); + } + + $this->getNetworkSession()->syncGameMode($this->gamemode); + return true; + } + + /** + * NOTE: Because Survival and Adventure Mode share some similar behaviour, this method will also return true if the player is + * in Adventure Mode. Supply the $literal parameter as true to force a literal Survival Mode check. + * + * @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())); + } + + /** + * NOTE: Because Creative and Spectator Mode share some similar behaviour, this method will also return true if the player is + * in Spectator Mode. Supply the $literal parameter as true to force a literal Creative Mode check. + * + * @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())); + } + + /** + * NOTE: Because Adventure and Spectator Mode share some similar behaviour, this method will also return true if the player is + * in Spectator Mode. Supply the $literal parameter as true to force a literal Adventure Mode check. + * + * @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())); + } + + public function isSpectator() : bool{ + return $this->gamemode->equals(GameMode::SPECTATOR()); + } + + /** + * 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()); + } + + public function isFireProof() : bool{ + return $this->isCreative(); + } + + public function getDrops() : array{ + if($this->hasFiniteResources()){ + return parent::getDrops(); + } + + return []; + } + + public function getXpDropAmount() : int{ + if($this->hasFiniteResources()){ + return parent::getXpDropAmount(); + } + + return 0; + } + + protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{ + if($this->isSpectator()){ + $this->onGround = false; + }else{ + $bb = clone $this->boundingBox; + $bb->minY = $this->location->y - 0.2; + $bb->maxY = $this->location->y + 0.2; + + $this->onGround = $this->isCollided = count($this->getWorld()->getCollisionBlocks($bb, true)) > 0; + } + } + + public function canBeMovedByCurrents() : bool{ + return false; //currently has no server-side movement + } + + protected function checkNearEntities() : void{ + foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(1, 0.5, 1), $this) as $entity){ + $entity->scheduleUpdate(); + + if(!$entity->isAlive() or $entity->isFlaggedForDespawn()){ + continue; + } + + $entity->onCollideWithPlayer($this); + } + } + + public function getInAirTicks() : int{ + return $this->inAirTicks; + } + + /** + * Attempts to move the player to the given coordinates. Unless you have some particularly specialized logic, you + * probably want to use teleport() instead of this. + * + * This is used for processing movements sent by the player over network. + * + * @param Vector3 $newPos Coordinates of the player's feet, centered horizontally at the base of their bounding box. + */ + public function handleMovement(Vector3 $newPos) : void{ + $this->moveRateLimit--; + if($this->moveRateLimit < 0){ + return; + } + + $oldPos = $this->getLocation(); + $distanceSquared = $newPos->distanceSquared($oldPos); + + $revert = false; + + if($distanceSquared > 100){ + //TODO: this is probably too big if we process every movement + /* !!! BEWARE YE WHO ENTER HERE !!! + * + * This is NOT an anti-cheat check. It is a safety check. + * Without it hackers can teleport with freedom on their own and cause lots of undesirable behaviour, like + * freezes, lag spikes and memory exhaustion due to sync chunk loading and collision checks across large distances. + * Not only that, but high-latency players can trigger such behaviour innocently. + * + * If you must tamper with this code, be aware that this can cause very nasty results. Do not waste our time + * asking for help if you suffer the consequences of messing with this. + */ + $this->logger->debug("Moved too fast, reverting movement"); + $this->logger->debug("Old position: " . $this->location->asVector3() . ", new position: " . $newPos); + $revert = true; + }elseif(!$this->getWorld()->isInLoadedTerrain($newPos)){ + $revert = true; + $this->nextChunkOrderRun = 0; + } + + if(!$revert and $distanceSquared != 0){ + $dx = $newPos->x - $this->location->x; + $dy = $newPos->y - $this->location->y; + $dz = $newPos->z - $this->location->z; + + $this->move($dx, $dy, $dz); + } + + if($revert){ + $this->revertMovement($oldPos); + } + } + + /** + * Fires movement events and synchronizes player movement, every tick. + */ + protected function processMostRecentMovements() : void{ + $now = microtime(true); + $multiplier = $this->lastMovementProcess !== null ? ($now - $this->lastMovementProcess) * 20 : 1; + $exceededRateLimit = $this->moveRateLimit < 0; + $this->moveRateLimit = min(self::MOVE_BACKLOG_SIZE, max(0, $this->moveRateLimit) + self::MOVES_PER_TICK * $multiplier); + $this->lastMovementProcess = $now; + + $from = clone $this->lastLocation; + $to = clone $this->location; + + $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){ + $ev = new PlayerMoveEvent($this, $from, $to); + + $ev->call(); + + if($ev->isCancelled()){ + $this->revertMovement($from); + return; + } + + if($to->distanceSquared($ev->getTo()) > 0.01){ //If plugins modify the destination + $this->teleport($ev->getTo()); + return; + } + + $this->lastLocation = $to; + $this->broadcastMovement(); + + $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($exceededRateLimit){ //client and server positions will be out of sync if this happens + $this->logger->debug("Exceeded movement rate limit, forcing to last accepted position"); + $this->sendPosition($this->location, $this->location->getYaw(), $this->location->getPitch(), MovePlayerPacket::MODE_RESET); + } + } + + protected function revertMovement(Location $from) : void{ + $this->setPosition($from); + $this->sendPosition($from, $from->yaw, $from->pitch, MovePlayerPacket::MODE_RESET); + } + + protected function calculateFallDamage(float $fallDistance) : float{ + return $this->flying ? 0 : parent::calculateFallDamage($fallDistance); + } + + public function jump() : void{ + (new PlayerJumpEvent($this))->call(); + parent::jump(); + } + + public function setMotion(Vector3 $motion) : bool{ + if(parent::setMotion($motion)){ + $this->broadcastMotion(); + $this->getNetworkSession()->sendDataPacket(SetActorMotionPacket::create($this->id, $motion)); + + return true; + } + return false; + } + + protected function updateMovement(bool $teleport = false) : void{ + + } + + protected function tryChangeMovement() : void{ + + } + + public function onUpdate(int $currentTick) : bool{ + $tickDiff = $currentTick - $this->lastUpdate; + + if($tickDiff <= 0){ + return true; + } + + $this->messageCounter = 2; + + $this->lastUpdate = $currentTick; + + if(!$this->isAlive() and $this->spawned){ + $this->onDeathUpdate($tickDiff); + return true; + } + + $this->timings->startTiming(); + + if($this->spawned){ + $this->processMostRecentMovements(); + $this->motion = new Vector3(0, 0, 0); //TODO: HACK! (Fixes player knockback being messed up) + if($this->onGround){ + $this->inAirTicks = 0; + }else{ + $this->inAirTicks += $tickDiff; + } + + Timings::$entityBaseTick->startTiming(); + $this->entityBaseTick($tickDiff); + Timings::$entityBaseTick->stopTiming(); + + if(!$this->isSpectator() and $this->isAlive()){ + Timings::$playerCheckNearEntities->startTiming(); + $this->checkNearEntities(); + Timings::$playerCheckNearEntities->stopTiming(); + } + + if($this->blockBreakHandler !== null and !$this->blockBreakHandler->update()){ + $this->blockBreakHandler = null; + } + } + + $this->timings->stopTiming(); + + return true; + } + + public function canBreathe() : bool{ + return $this->isCreative() or parent::canBreathe(); + } + + /** + * Returns whether the player can interact with the specified position. This checks distance and direction. + * + * @param float $maxDiff defaults to half of the 3D diagonal width of a block + */ + public function canInteract(Vector3 $pos, float $maxDistance, float $maxDiff = M_SQRT3 / 2) : bool{ + $eyePos = $this->getEyePos(); + if($eyePos->distanceSquared($pos) > $maxDistance ** 2){ + return false; + } + + $dV = $this->getDirectionVector(); + $eyeDot = $dV->dot($eyePos); + $targetDot = $dV->dot($pos); + return ($targetDot - $eyeDot) >= -$maxDiff; + } + + /** + * Sends a chat message as this player. If the message begins with a / (forward-slash) it will be treated + * as a command. + */ + public function chat(string $message) : bool{ + $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(strpos($messagePart, './') === 0){ + $messagePart = substr($messagePart, 1); + } + + $ev = new PlayerCommandPreprocessEvent($this, $messagePart); + $ev->call(); + + if($ev->isCancelled()){ + break; + } + + if(strpos($ev->getMessage(), "/") === 0){ + Timings::$playerCommand->startTiming(); + $this->server->dispatchCommand($ev->getPlayer(), substr($ev->getMessage(), 1)); + Timings::$playerCommand->stopTiming(); + }else{ + $ev = new PlayerChatEvent($this, $ev->getMessage(), $this->server->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_USERS)); + $ev->call(); + if(!$ev->isCancelled()){ + $this->server->broadcastMessage($this->getServer()->getLanguage()->translateString($ev->getFormat(), [$ev->getPlayer()->getDisplayName(), $ev->getMessage()]), $ev->getRecipients()); + } + } + } + } + + return true; + } + + public function selectHotbarSlot(int $hotbarSlot) : bool{ + if(!$this->inventory->isHotbarSlot($hotbarSlot)){ //TODO: exception here? + return false; + } + if($hotbarSlot === $this->inventory->getHeldItemIndex()){ + return true; + } + + $ev = new PlayerItemHeldEvent($this, $this->inventory->getItem($hotbarSlot), $hotbarSlot); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $this->inventory->setHeldItemIndex($hotbarSlot); + $this->setUsingItem(false); + + return true; + } + + /** + * Activates the item in hand, for example throwing a projectile. + * + * @return bool if it did something + */ + public function useHeldItem() : bool{ + $directionVector = $this->getDirectionVector(); + $item = $this->inventory->getItemInHand(); + $oldItem = clone $item; + + $ev = new PlayerItemUseEvent($this, $item, $directionVector); + if($this->hasItemCooldown($item) or $this->isSpectator()){ + $ev->cancel(); + } + + $ev->call(); + + if($ev->isCancelled()){ + return false; + } + + $result = $item->onClickAir($this, $directionVector); + if($result->equals(ItemUseResult::FAIL())){ + return false; + } + + $this->resetItemCooldown($item); + if($this->hasFiniteResources() and !$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){ + if($item instanceof Durable && $item->isBroken()){ + $this->broadcastSound(new ItemBreakSound()); + } + $this->inventory->setItemInHand($item); + } + + $this->setUsingItem($item instanceof Releasable && $item->canStartUsingItem($this)); + + return true; + } + + /** + * Consumes the currently-held item. + * + * @return bool if the consumption succeeded. + */ + public function consumeHeldItem() : bool{ + $slot = $this->inventory->getItemInHand(); + if($slot instanceof ConsumableItem){ + $oldItem = clone $slot; + + $ev = new PlayerItemConsumeEvent($this, $slot); + if($this->hasItemCooldown($slot)){ + $ev->cancel(); + } + $ev->call(); + + if($ev->isCancelled() or !$this->consumeObject($slot)){ + return false; + } + + $this->setUsingItem(false); + $this->resetItemCooldown($slot); + + if($this->hasFiniteResources() && $oldItem->equalsExact($this->inventory->getItemInHand())){ + $slot->pop(); + $this->inventory->setItemInHand($slot); + $this->inventory->addItem($slot->getResidue()); + } + + return true; + } + + return false; + } + + /** + * Releases the held item, for example to fire a bow. This should be preceded by a call to useHeldItem(). + * + * @return bool if it did something. + */ + public function releaseHeldItem() : bool{ + try{ + $item = $this->inventory->getItemInHand(); + if(!$this->isUsingItem() or $this->hasItemCooldown($item)){ + return false; + } + + $oldItem = clone $item; + + $result = $item->onReleaseUsing($this); + if($result->equals(ItemUseResult::SUCCESS())){ + $this->resetItemCooldown($item); + if(!$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){ + if($item instanceof Durable && $item->isBroken()){ + $this->broadcastSound(new ItemBreakSound()); + } + $this->inventory->setItemInHand($item); + } + return true; + } + + return false; + }finally{ + $this->setUsingItem(false); + } + } + + public function pickBlock(Vector3 $pos, bool $addTileNBT) : bool{ + $block = $this->getWorld()->getBlock($pos); + if($block instanceof UnknownBlock){ + return true; + } + + $item = $block->getPickedItem($addTileNBT); + + $ev = new PlayerBlockPickEvent($this, $block, $item); + $existingSlot = $this->inventory->first($item); + if($existingSlot === -1 and $this->hasFiniteResources()){ + $ev->cancel(); + } + $ev->call(); + + if(!$ev->isCancelled()){ + if($existingSlot !== -1){ + if($existingSlot < $this->inventory->getHotbarSize()){ + $this->inventory->setHeldItemIndex($existingSlot); + }else{ + $this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot); + } + }else{ + $firstEmpty = $this->inventory->firstEmpty(); + if($firstEmpty === -1){ //full inventory + $this->inventory->setItemInHand($item); + }elseif($firstEmpty < $this->inventory->getHotbarSize()){ + $this->inventory->setItem($firstEmpty, $item); + $this->inventory->setHeldItemIndex($firstEmpty); + }else{ + $this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty); + $this->inventory->setItemInHand($item); + } + } + } + + return true; + } + + /** + * Performs a left-click (attack) action on the block. + * + * @return bool if an action took place successfully + */ + public function attackBlock(Vector3 $pos, int $face) : bool{ + if($pos->distanceSquared($this->location) > 10000){ + return false; //TODO: maybe this should throw an exception instead? + } + + $target = $this->getWorld()->getBlock($pos); + + $ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK); + if($this->isSpectator()){ + $ev->cancel(); + } + $ev->call(); + if($ev->isCancelled()){ + return false; + } + $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers()); + if($target->onAttack($this->inventory->getItemInHand(), $face, $this)){ + return true; + } + + $block = $target->getSide($face); + if($block->getId() === BlockLegacyIds::FIRE){ + $this->getWorld()->setBlock($block->getPosition(), VanillaBlocks::AIR()); + $this->getWorld()->addSound($block->getPosition()->add(0.5, 0.5, 0.5), new FireExtinguishSound()); + return true; + } + + 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 + $this->blockBreakHandler->setTargetedFace($face); + } + } + + public function stopBreakBlock(Vector3 $pos) : void{ + if($this->blockBreakHandler !== null and $this->blockBreakHandler->getBlockPos()->distanceSquared($pos) < 0.0001){ + $this->blockBreakHandler = null; + } + } + + /** + * Breaks the block at the given position using the currently-held item. + * + * @return bool if the block was successfully broken, false if a rollback needs to take place. + */ + public function breakBlock(Vector3 $pos) : bool{ + $this->removeCurrentWindow(); + + if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? 13 : 7)){ + $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($item instanceof Durable && $item->isBroken()){ + $this->broadcastSound(new ItemBreakSound()); + } + $this->inventory->setItemInHand($item); + } + $this->hungerManager->exhaust(0.025, PlayerExhaustEvent::CAUSE_MINING); + return true; + } + }else{ + $this->logger->debug("Cancelled block break at $pos due to not currently being interactable"); + } + + return false; + } + + /** + * Touches the block at the given position with the currently-held item. + * + * @return bool if it did something + */ + 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)){ + $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($item instanceof Durable && $item->isBroken()){ + $this->broadcastSound(new ItemBreakSound()); + } + $this->inventory->setItemInHand($item); + } + return true; + } + }else{ + $this->logger->debug("Cancelled interaction of block at $pos due to not currently being interactable"); + } + + return false; + } + + /** + * Attacks the given entity with the currently-held item. + * TODO: move this up the class hierarchy + * + * @return bool if the entity was dealt damage + */ + public function attackEntity(Entity $entity) : bool{ + if(!$entity->isAlive()){ + return false; + } + if($entity instanceof ItemEntity or $entity instanceof Arrow){ + $this->logger->debug("Attempted to attack non-attackable entity " . get_class($entity)); + return false; + } + + $heldItem = $this->inventory->getItemInHand(); + $oldItem = clone $heldItem; + + $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints()); + if(!$this->canInteract($entity->getLocation(), 8)){ + $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"))){ + $ev->cancel(); + } + + $meleeEnchantmentDamage = 0; + /** @var EnchantmentInstance[] $meleeEnchantments */ + $meleeEnchantments = []; + foreach($heldItem->getEnchantments() as $enchantment){ + $type = $enchantment->getType(); + if($type instanceof MeleeWeaponEnchantment and $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()){ + $ev->setModifier($ev->getFinalDamage() / 2, EntityDamageEvent::MODIFIER_CRITICAL); + } + + $entity->attack($ev); + + $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){ + $entity->broadcastAnimation(new CriticalHitAnimation($entity)); + } + + foreach($meleeEnchantments as $enchantment){ + $type = $enchantment->getType(); + assert($type instanceof MeleeWeaponEnchantment); + $type->onPostAttack($this, $entity, $enchantment->getLevel()); + } + + 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 instanceof Durable && $heldItem->isBroken()){ + $this->broadcastSound(new ItemBreakSound()); + } + $this->inventory->setItemInHand($heldItem); + } + + $this->hungerManager->exhaust(0.3, PlayerExhaustEvent::CAUSE_ATTACK); + } + + return true; + } + + /** + * Interacts with the given entity using the currently-held item. + */ + public function interactEntity(Entity $entity, Vector3 $clickPos) : bool{ + $ev = new PlayerEntityInteractEvent($this, $entity, $clickPos); + + if(!$this->canInteract($entity->getLocation(), 8)){ + $this->logger->debug("Cancelled interaction with entity " . $entity->getId() . " due to not currently being interactable"); + $ev->cancel(); + } + + $ev->call(); + + if(!$ev->isCancelled()){ + return $entity->onInteract($this, $clickPos); + } + return false; + } + + public function toggleSprint(bool $sprint) : bool{ + $ev = new PlayerToggleSprintEvent($this, $sprint); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + $this->setSprinting($sprint); + return true; + } + + public function toggleSneak(bool $sneak) : bool{ + $ev = new PlayerToggleSneakEvent($this, $sneak); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + $this->setSneaking($sneak); + return true; + } + + public function toggleFlight(bool $fly) : bool{ + $ev = new PlayerToggleFlightEvent($this, $fly); + if(!$this->allowFlight){ + $ev->cancel(); + } + $ev->call(); + 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(); + 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. + */ + public function dropItem(Item $item) : void{ + $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers()); + $this->getWorld()->dropItem($this->location->add(0, 1.3, 0), $item, $this->getDirectionVector()->multiply(0.4), 40); + } + + /** + * Adds a title text to the user's screen, with an optional subtitle. + * + * @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used. + * @param int $stay Duration in ticks to stay on screen for + * @param int $fadeOut Duration in ticks for fade-out. + */ + public function sendTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1) : void{ + $this->setTitleDuration($fadeIn, $stay, $fadeOut); + if($subtitle !== ""){ + $this->sendSubTitle($subtitle); + } + $this->getNetworkSession()->onTitle($title); + } + + /** + * Sets the subtitle message, without sending a title. + */ + public function sendSubTitle(string $subtitle) : void{ + $this->getNetworkSession()->onSubTitle($subtitle); + } + + /** + * Adds small text to the user's screen. + */ + public function sendActionBarMessage(string $message) : void{ + $this->getNetworkSession()->onActionBar($message); + } + + /** + * Removes the title from the client's screen. + */ + public function removeTitles() : void{ + $this->getNetworkSession()->onClearTitle(); + } + + /** + * Resets the title duration settings to defaults and removes any existing titles. + */ + public function resetTitles() : void{ + $this->getNetworkSession()->onResetTitleOptions(); + } + + /** + * Sets the title duration. + * + * @param int $fadeIn Title fade-in time in ticks. + * @param int $stay Title stay time in ticks. + * @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){ + $this->getNetworkSession()->onTitleDuration($fadeIn, $stay, $fadeOut); + } + } + + /** + * Sends a direct chat message to a player + */ + public function sendMessage(Translatable|string $message) : void{ + if($message instanceof Translatable){ + $this->sendTranslation($message->getText(), $message->getParameters()); + return; + } + + $this->getNetworkSession()->onRawChatMessage($message); + } + + /** + * @param string[]|Translatable[] $parameters + */ + public function sendTranslation(string $message, array $parameters = []) : void{ + //we can't send nested translations to the client, so make sure they are always pre-translated by the server + $parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $this->getLanguage()->translate($p) : $p, $parameters); + if(!$this->server->isLanguageForced()){ + foreach($parameters as $i => $p){ + $parameters[$i] = $this->getLanguage()->translateString($p, [], "pocketmine."); + } + $this->getNetworkSession()->onTranslatedChatMessage($this->getLanguage()->translateString($message, $parameters, "pocketmine."), $parameters); + }else{ + $this->sendMessage($this->getLanguage()->translateString($message, $parameters)); + } + } + + /** + * @param string[] $args + */ + public function sendJukeboxPopup(string $key, array $args) : void{ + $this->getNetworkSession()->onJukeboxPopup($key, $args); + } + + /** + * Sends a popup message to the player + * + * TODO: add translation type popups + */ + public function sendPopup(string $message) : void{ + $this->getNetworkSession()->onPopup($message); + } + + public function sendTip(string $message) : void{ + $this->getNetworkSession()->onTip($message); + } + + /** + * Sends a Form to the player, or queue to send it if a form is already open. + * + * @throws \InvalidArgumentException + */ + public function sendForm(Form $form) : void{ + $id = $this->formIdCounter++; + if($this->getNetworkSession()->onFormSent($id, $form)){ + $this->forms[$id] = $form; + } + } + + /** + * @param mixed $responseData + */ + public function onFormSubmit(int $formId, $responseData) : bool{ + if(!isset($this->forms[$formId])){ + $this->logger->debug("Got unexpected response for form $formId"); + return false; + } + + try{ + $this->forms[$formId]->handleResponse($this, $responseData); + }catch(FormValidationException $e){ + $this->logger->critical("Failed to validate form " . get_class($this->forms[$formId]) . ": " . $e->getMessage()); + $this->logger->logException($e); + }finally{ + unset($this->forms[$formId]); + } + + return true; + } + + /** + * Transfers a player to another server. + * + * @param string $address The IP address or hostname of the destination server + * @param int $port The destination port, defaults to 19132 + * @param string $message Message to show in the console when closing the player + * + * @return bool if transfer was successful. + */ + public function transfer(string $address, int $port = 19132, string $message = "transfer") : bool{ + $ev = new PlayerTransferEvent($this, $address, $port, $message); + $ev->call(); + if(!$ev->isCancelled()){ + $this->getNetworkSession()->transfer($ev->getAddress(), $ev->getPort(), $ev->getMessage()); + return true; + } + + return false; + } + + /** + * Kicks a player from the server + */ + public function kick(string $reason = "", Translatable|string|null $quitMessage = null) : bool{ + $ev = new PlayerKickEvent($this, $reason, $quitMessage ?? $this->getLeaveMessage()); + $ev->call(); + if(!$ev->isCancelled()){ + $reason = $ev->getReason(); + if($reason === ""){ + $reason = KnownTranslationKeys::DISCONNECTIONSCREEN_NOREASON; + } + $this->disconnect($reason, $ev->getQuitMessage()); + + return true; + } + + return false; + } + + /** + * Removes the player from the server. This cannot be cancelled. + * This is used for remote disconnects and for uninterruptible disconnects (for example, when the server shuts down). + * + * Note for plugin developers: Prefer kick() instead of this method. + * That way other plugins can have a say in whether the player is removed or not. + * + * Note for internals developers: Do not call this from network sessions. It will cause a feedback loop. + * + * @param string $reason Shown to the player, usually this will appear on their disconnect screen. + * @param Translatable|string|null $quitMessage Message to broadcast to online players (null will use default) + */ + public function disconnect(string $reason, Translatable|string|null $quitMessage = null) : void{ + if(!$this->isConnected()){ + return; + } + + $this->getNetworkSession()->onPlayerDestroyed($reason); + $this->onPostDisconnect($reason, $quitMessage); + } + + /** + * @internal + * This method executes post-disconnect actions and cleanups. + * + * @param string $reason Shown to the player, usually this will appear on their disconnect screen. + * @param Translatable|string|null $quitMessage Message to broadcast to online players (null will use default) + */ + public function onPostDisconnect(string $reason, Translatable|string|null $quitMessage) : void{ + if($this->isConnected()){ + throw new \LogicException("Player is still connected"); + } + + //prevent the player receiving their own disconnect message + $this->server->unsubscribeFromAllBroadcastChannels($this); + + $this->removeCurrentWindow(); + + $ev = new PlayerQuitEvent($this, $quitMessage ?? $this->getLeaveMessage(), $reason); + $ev->call(); + if(($quitMessage = $ev->getQuitMessage()) != ""){ + $this->server->broadcastMessage($quitMessage); + } + $this->save(); + + $this->spawned = false; + + $this->stopSleep(); + $this->blockBreakHandler = null; + $this->despawnFromAll(); + + $this->server->removeOnlinePlayer($this); + + foreach($this->server->getOnlinePlayers() as $player){ + if(!$player->canSee($this)){ + $player->showPlayer($this); + } + } + $this->hiddenPlayers = []; + + if($this->location->isValid()){ + foreach($this->usedChunks as $index => $status){ + World::getXZ($index, $chunkX, $chunkZ); + $this->unloadChunk($chunkX, $chunkZ); + } + } + if(count($this->usedChunks) !== 0){ + throw new AssumptionFailedError("Previous loop should have cleared this array"); + } + $this->loadQueue = []; + + $this->removeCurrentWindow(); + $this->removePermanentInventories(); + + $this->perm->getPermissionRecalculationCallbacks()->clear(); + + $this->flagForDespawn(); + } + + protected function onDispose() : void{ + $this->disconnect("Player destroyed"); + $this->cursorInventory->removeAllViewers(); + $this->craftingGrid->removeAllViewers(); + parent::onDispose(); + } + + protected function destroyCycles() : void{ + $this->networkSession = null; + unset($this->cursorInventory); + unset($this->craftingGrid); + $this->spawnPosition = null; + $this->blockBreakHandler = null; + parent::destroyCycles(); + } + + /** + * @return mixed[] + */ + public function __debugInfo() : array{ + return []; + } + + public function __destruct(){ + parent::__destruct(); + $this->logger->debug("Destroyed by garbage collector"); + } + + public function canSaveWithChunk() : bool{ + return false; + } + + public function setCanSaveWithChunk(bool $value) : void{ + throw new \BadMethodCallException("Players can't be saved with chunks"); + } + + public function getSaveData() : CompoundTag{ + $nbt = $this->saveNBT(); + + $nbt->setString("LastKnownXUID", $this->xuid); + + if($this->location->isValid()){ + $nbt->setString("Level", $this->getWorld()->getFolderName()); + } + + if($this->hasValidCustomSpawn()){ + $spawn = $this->getSpawn(); + $nbt->setString("SpawnLevel", $spawn->getWorld()->getFolderName()); + $nbt->setInt("SpawnX", $spawn->getFloorX()); + $nbt->setInt("SpawnY", $spawn->getFloorY()); + $nbt->setInt("SpawnZ", $spawn->getFloorZ()); + } + + $nbt->setInt("playerGameType", GameModeIdMap::getInstance()->toId($this->gamemode)); + $nbt->setLong("firstPlayed", $this->firstPlayed); + $nbt->setLong("lastPlayed", (int) floor(microtime(true) * 1000)); + + return $nbt; + } + + /** + * Handles player data saving + */ + public function save() : void{ + $this->server->saveOfflinePlayerData($this->username, $this->getSaveData()); + } + + 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->removeCurrentWindow(); + + $ev = new PlayerDeathEvent($this, $this->getDrops(), $this->getXpDropAmount(), null); + $ev->call(); + + if(!$ev->getKeepInventory()){ + foreach($ev->getDrops() as $item){ + $this->getWorld()->dropItem($this->location, $item); + } + + if($this->inventory !== null){ + $this->inventory->setHeldItemIndex(0); + $this->inventory->clearAll(); + } + if($this->armorInventory !== null){ + $this->armorInventory->clearAll(); + } + if($this->offHandInventory !== null){ + $this->offHandInventory->clearAll(); + } + } + + $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount()); + $this->xpManager->setXpAndProgress(0, 0.0); + + if($ev->getDeathMessage() != ""){ + $this->server->broadcastMessage($ev->getDeathMessage()); + } + + $this->startDeathAnimation(); + + $this->getNetworkSession()->onServerDeath(); + } + + protected function onDeathUpdate(int $tickDiff) : bool{ + parent::onDeathUpdate($tickDiff); + return false; //never flag players for despawn + } + + public function respawn() : void{ + 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"); + } + 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( + function() use ($spawn) : void{ + if(!$this->isConnected()){ + return; + } + $this->logger->debug("Spawn terrain generation done, completing respawn"); + $spawn = $spawn->getWorld()->getSafeSpawn($spawn); + $ev = new PlayerRespawnEvent($this, $spawn); + $ev->call(); + + $realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld()); + $this->teleport($realSpawn); + + $this->setSprinting(false); + $this->setSneaking(false); + $this->setFlying(false); + + $this->extinguish(); + $this->setAirSupplyTicks($this->getMaxAirSupplyTicks()); + $this->deadTicks = 0; + $this->noDamageTicks = 60; + + $this->effectManager->clear(); + $this->setHealth($this->getMaxHealth()); + + foreach($this->attributeMap->getAll() as $attr){ + $attr->resetToDefault(); + } + + $this->spawnToAll(); + $this->scheduleUpdate(); + + $this->getNetworkSession()->onServerRespawn(); + $this->respawnLocked = false; + }, + function() : void{ + if($this->isConnected()){ + $this->disconnect("Unable to find a respawn position"); + } + } + ); + } + + protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ + parent::applyPostDamageEffects($source); + + $this->hungerManager->exhaust(0.3, PlayerExhaustEvent::CAUSE_DAMAGE); + } + + public function attack(EntityDamageEvent $source) : void{ + if(!$this->isAlive()){ + return; + } + + if($this->isCreative() + and $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE + and $source->getCause() !== EntityDamageEvent::CAUSE_VOID + ){ + $source->cancel(); + }elseif($this->allowFlight and $source->getCause() === EntityDamageEvent::CAUSE_FALL){ + $source->cancel(); + } + + parent::attack($source); + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setGenericFlag(EntityMetadataFlags::ACTION, $this->startAction > -1); + + $properties->setPlayerFlag(PlayerMetadataFlags::SLEEP, $this->sleeping !== null); + $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{ + if($targets === null){ + $targets = $this->getViewers(); + $targets[] = $this; + } + parent::sendData($targets, $data); + } + + public function broadcastAnimation(Animation $animation, ?array $targets = null) : void{ + if($this->spawned and $targets === null){ + $targets = $this->getViewers(); + $targets[] = $this; + } + parent::broadcastAnimation($animation, $targets); + } + + public function broadcastSound(Sound $sound, ?array $targets = null) : void{ + if($this->spawned && $targets === null){ + $targets = $this->getViewers(); + $targets[] = $this; + } + parent::broadcastSound($sound, $targets); + } + + /** + * TODO: remove this + */ + protected function sendPosition(Vector3 $pos, ?float $yaw = null, ?float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL) : void{ + $this->getNetworkSession()->syncMovement($pos, $yaw, $pitch, $mode); + + $this->ySize = 0; + } + + /** + * {@inheritdoc} + */ + public function teleport(Vector3 $pos, ?float $yaw = null, ?float $pitch = null) : bool{ + if(parent::teleport($pos, $yaw, $pitch)){ + + $this->removeCurrentWindow(); + + $this->sendPosition($this->location, $this->location->yaw, $this->location->pitch, MovePlayerPacket::MODE_TELEPORT); + $this->broadcastMovement(true); + + $this->spawnToAll(); + + $this->resetFallDistance(); + $this->nextChunkOrderRun = 0; + if($this->spawnChunkLoadCount !== -1){ + $this->spawnChunkLoadCount = 0; + } + $this->stopSleep(); + $this->blockBreakHandler = null; + + //TODO: workaround for player last pos not getting updated + //Entity::updateMovement() normally handles this, but it's overridden with an empty function in Player + $this->resetLastMovements(); + + return true; + } + + return false; + } + + protected function addDefaultWindows() : void{ + $this->cursorInventory = new PlayerCursorInventory($this); + $this->craftingGrid = new PlayerCraftingInventory($this); + + $this->addPermanentInventories($this->inventory, $this->armorInventory, $this->cursorInventory, $this->offHandInventory); + + //TODO: more windows + } + + public function getCursorInventory() : PlayerCursorInventory{ + return $this->cursorInventory; + } + + public function getCraftingGrid() : CraftingGrid{ + return $this->craftingGrid; + } + + /** + * @internal Called to clean up crafting grid and cursor inventory when it is detected that the player closed their + * inventory. + */ + 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); + foreach($inventories as $inventory){ + $contents = $inventory->getContents(); + + if(count($contents) > 0){ + $drops = $mainInventoryTransactionBuilder->addItem(...$contents); + foreach($drops as $drop){ + $transaction->addAction(new DropItemAction($drop)); + } + + $clearedInventoryTransactionBuilder = new TransactionBuilderInventory($inventory); + $clearedInventoryTransactionBuilder->clearAll(); + foreach($clearedInventoryTransactionBuilder->generateActions() as $action){ + $transaction->addAction($action); + } + } + } + foreach($mainInventoryTransactionBuilder->generateActions() as $action){ + $transaction->addAction($action); + } + + if(count($transaction->getActions()) !== 0){ + try{ + $transaction->execute(); + $this->logger->debug("Successfully evacuated items from temporary inventories"); + }catch(TransactionCancelledException){ + $this->logger->debug("Plugin cancelled transaction evacuating items from temporary inventories; items will be destroyed"); + foreach($inventories as $inventory){ + $inventory->clearAll(); + } + }catch(TransactionValidationException $e){ + throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e); + } + } + } + + /** + * Returns the inventory the player is currently viewing. This might be a chest, furnace, or any other container. + */ + public function getCurrentWindow() : ?Inventory{ + return $this->currentWindow; + } + + /** + * Opens an inventory window to the player. Returns if it was successful. + */ + public function setCurrentWindow(Inventory $inventory) : bool{ + if($inventory === $this->currentWindow){ + return true; + } + $ev = new InventoryOpenEvent($inventory, $this); + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + //TODO: client side race condition here makes the opening work incorrectly + $this->removeCurrentWindow(); + + if(($inventoryManager = $this->getNetworkSession()->getInvManager()) === null){ + throw new \InvalidArgumentException("Player cannot open inventories in this state"); + } + $this->logger->debug("Opening inventory " . get_class($inventory) . "#" . spl_object_id($inventory)); + $inventoryManager->onCurrentWindowChange($inventory); + $inventory->onOpen($this); + $this->currentWindow = $inventory; + return true; + } + + public function removeCurrentWindow() : void{ + $this->doCloseInventory(); + if($this->currentWindow !== null){ + (new InventoryCloseEvent($this->currentWindow, $this))->call(); + + $this->logger->debug("Closing inventory " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow)); + $this->currentWindow->onClose($this); + if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){ + $inventoryManager->onCurrentWindowRemove(); + } + $this->currentWindow = null; + } + } + + protected function addPermanentInventories(Inventory ...$inventories) : void{ + foreach($inventories as $inventory){ + $inventory->onOpen($this); + $this->permanentWindows[spl_object_id($inventory)] = $inventory; + } + } + + protected function removePermanentInventories() : void{ + foreach($this->permanentWindows as $inventory){ + $inventory->onClose($this); + } + $this->permanentWindows = []; + } + + use ChunkListenerNoOpTrait { + onChunkChanged as private; + onChunkUnloaded as private; + } + + public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + $status = $this->usedChunks[$hash = World::chunkHash($chunkX, $chunkZ)] ?? null; + if($status !== null && $status->equals(UsedChunkStatus::SENT())){ + $this->usedChunks[$hash] = UsedChunkStatus::NEEDED(); + $this->nextChunkOrderRun = 0; + } + } + + public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + if($this->isUsingChunk($chunkX, $chunkZ)){ + $this->logger->debug("Detected forced unload of chunk " . $chunkX . " " . $chunkZ); + $this->unloadChunk($chunkX, $chunkZ); + } + } +} diff --git a/src/player/PlayerChunkLoader.php b/src/player/PlayerChunkLoader.php new file mode 100644 index 0000000000..deec33780c --- /dev/null +++ b/src/player/PlayerChunkLoader.php @@ -0,0 +1,49 @@ +currentLocation = $currentLocation; + } + + public function setCurrentLocation(Vector3 $currentLocation) : void{ + $this->currentLocation = $currentLocation; + } + + public function getX() : float{ + return $this->currentLocation->getFloorX(); + } + + public function getZ() : float{ + return $this->currentLocation->getFloorZ(); + } +} diff --git a/src/player/PlayerInfo.php b/src/player/PlayerInfo.php new file mode 100644 index 0000000000..ca1df8cf6f --- /dev/null +++ b/src/player/PlayerInfo.php @@ -0,0 +1,84 @@ + + */ + private $extraData; + + /** + * @param mixed[] $extraData + * @phpstan-param array $extraData + */ + public function __construct(string $username, UuidInterface $uuid, Skin $skin, string $locale, array $extraData = []){ + $this->username = TextFormat::clean($username); + $this->uuid = $uuid; + $this->skin = $skin; + $this->locale = $locale; + $this->extraData = $extraData; + } + + public function getUsername() : string{ + return $this->username; + } + + public function getUuid() : UuidInterface{ + return $this->uuid; + } + + public function getSkin() : Skin{ + return $this->skin; + } + + public function getLocale() : string{ + return $this->locale; + } + + /** + * @return mixed[] + * @phpstan-return array + */ + public function getExtraData() : array{ + return $this->extraData; + } +} diff --git a/src/player/SurvivalBlockBreakHandler.php b/src/player/SurvivalBlockBreakHandler.php new file mode 100644 index 0000000000..5e27392cf4 --- /dev/null +++ b/src/player/SurvivalBlockBreakHandler.php @@ -0,0 +1,146 @@ +player = $player; + $this->blockPos = $blockPos; + $this->block = $block; + $this->targetedFace = $targetedFace; + $this->fxTickInterval = $fxTickInterval; + $this->maxPlayerDistance = $maxPlayerDistance; + + $this->breakSpeed = $this->calculateBreakProgressPerTick(); + if($this->breakSpeed > 0){ + $this->player->getWorld()->broadcastPacketToViewers( + $this->blockPos, + LevelEventPacket::create(LevelEvent::BLOCK_START_BREAK, (int) (65535 * $this->breakSpeed), $this->blockPos) + ); + } + } + + /** + * Returns the calculated break speed as percentage progress per game tick. + */ + private function calculateBreakProgressPerTick() : float{ + if(!$this->block->getBreakInfo()->isBreakable()){ + return 0.0; + } + //TODO: improve this to take stuff like swimming, ladders, enchanted tools into account, fix wrong tool break time calculations for bad tools (pmmp/PocketMine-MP#211) + $breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20; + + if($breakTimePerTick > 0){ + return 1 / $breakTimePerTick; + } + return 1; + } + + public function update() : bool{ + if($this->player->getPosition()->distanceSquared($this->blockPos->add(0.5, 0.5, 0.5)) > $this->maxPlayerDistance ** 2){ + return false; + } + + $newBreakSpeed = $this->calculateBreakProgressPerTick(); + if(abs($newBreakSpeed - $this->breakSpeed) > 0.0001){ + $this->breakSpeed = $newBreakSpeed; + //TODO: sync with client + } + + $this->breakProgress += $this->breakSpeed; + + if(($this->fxTicker++ % $this->fxTickInterval) === 0 and $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()); + } + + return $this->breakProgress < 1; + } + + public function getBlockPos() : Vector3{ + return $this->blockPos; + } + + public function getTargetedFace() : int{ + return $this->targetedFace; + } + + public function setTargetedFace(int $face) : void{ + Facing::validate($face); + $this->targetedFace = $face; + } + + public function getBreakSpeed() : float{ + return $this->breakSpeed; + } + + public function getBreakProgress() : float{ + return $this->breakProgress; + } + + public function __destruct(){ + if($this->player->getWorld()->isInLoadedTerrain($this->blockPos)){ + $this->player->getWorld()->broadcastPacketToViewers( + $this->blockPos, + LevelEventPacket::create(LevelEvent::BLOCK_STOP_BREAK, 0, $this->blockPos) + ); + } + } +} diff --git a/src/player/UsedChunkStatus.php b/src/player/UsedChunkStatus.php new file mode 100644 index 0000000000..fa8227628a --- /dev/null +++ b/src/player/UsedChunkStatus.php @@ -0,0 +1,50 @@ +xuid = $xuid; + } + + public function getXuid() : string{ + return $this->xuid; + } + + /** + * Returns a new PlayerInfo with XBL player info stripped. This is used to ensure that non-XBL players can't spoof + * XBL data. + */ + public function withoutXboxData() : PlayerInfo{ + return new PlayerInfo( + $this->getUsername(), + $this->getUuid(), + $this->getSkin(), + $this->getLocale(), + $this->getExtraData() + ); + } +} diff --git a/src/plugin/ApiVersion.php b/src/plugin/ApiVersion.php new file mode 100644 index 0000000000..5a2bb035b5 --- /dev/null +++ b/src/plugin/ApiVersion.php @@ -0,0 +1,100 @@ +getBaseVersion() !== $myVersion->getBaseVersion()){ + if($version->getMajor() !== $myVersion->getMajor()){ + continue; + } + + if($version->getMinor() > $myVersion->getMinor()){ //If the plugin requires new API features, being backwards compatible + continue; + } + + if($version->getMinor() === $myVersion->getMinor() and $version->getPatch() > $myVersion->getPatch()){ //If the plugin requires bug fixes in patches, being backwards compatible + continue; + } + } + + return true; + } + + return false; + } + + /** + * @param string[] $versions + * + * @return string[] + */ + public static function checkAmbiguousVersions(array $versions) : array{ + /** @var VersionString[][] $indexedVersions */ + $indexedVersions = []; + + foreach($versions as $str){ + $v = new VersionString($str); + if($v->getSuffix() !== ""){ //suffix is always unambiguous + continue; + } + if(!isset($indexedVersions[$v->getMajor()])){ + $indexedVersions[$v->getMajor()] = [$v]; + }else{ + $indexedVersions[$v->getMajor()][] = $v; + } + } + + /** @var VersionString[] $result */ + $result = []; + foreach($indexedVersions as $major => $list){ + if(count($list) > 1){ + array_push($result, ...$list); + } + } + + usort($result, static function(VersionString $string1, VersionString $string2) : int{ return $string1->compare($string2); }); + + return array_map(static function(VersionString $string) : string{ return $string->getBaseVersion(); }, $result); + } +} diff --git a/src/plugin/DiskResourceProvider.php b/src/plugin/DiskResourceProvider.php new file mode 100644 index 0000000000..b1fd026864 --- /dev/null +++ b/src/plugin/DiskResourceProvider.php @@ -0,0 +1,84 @@ +file = rtrim(str_replace(DIRECTORY_SEPARATOR, "/", $path), "/") . "/"; + } + + /** + * Gets an embedded resource on the plugin file. + * WARNING: You must close the resource given using fclose() + * + * @return null|resource Resource data, or null + */ + public function getResource(string $filename){ + $filename = rtrim(str_replace(DIRECTORY_SEPARATOR, "/", $filename), "/"); + if(file_exists($this->file . $filename)){ + $resource = fopen($this->file . $filename, "rb"); + if($resource === false) throw new AssumptionFailedError("fopen() should not fail on a file which exists"); + return $resource; + } + + return null; + } + + /** + * Returns all the resources packaged with the plugin in the form ["path/in/resources" => SplFileInfo] + * + * @return \SplFileInfo[] + */ + public function getResources() : array{ + $resources = []; + if(is_dir($this->file)){ + foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->file)) as $resource){ + if($resource->isFile()){ + $path = str_replace(DIRECTORY_SEPARATOR, "/", substr((string) $resource, strlen($this->file))); + $resources[$path] = $resource; + } + } + } + + return $resources; + } +} diff --git a/src/pocketmine/plugin/PharPluginLoader.php b/src/plugin/PharPluginLoader.php similarity index 85% rename from src/pocketmine/plugin/PharPluginLoader.php rename to src/plugin/PharPluginLoader.php index fd902dae8b..d42fa0b5ec 100644 --- a/src/pocketmine/plugin/PharPluginLoader.php +++ b/src/plugin/PharPluginLoader.php @@ -32,10 +32,10 @@ use function substr; */ class PharPluginLoader implements PluginLoader{ - /** @var \ClassLoader */ + /** @var \DynamicClassLoader */ private $loader; - public function __construct(\ClassLoader $loader){ + public function __construct(\DynamicClassLoader $loader){ $this->loader = $loader; } @@ -48,7 +48,10 @@ class PharPluginLoader implements PluginLoader{ * Loads the plugin contained in $file */ public function loadPlugin(string $file) : void{ - $this->loader->addPath("$file/src"); + $description = $this->getPluginDescription($file); + if($description !== null){ + $this->loader->addPath($description->getSrcNamespacePrefix(), "$file/src"); + } } /** diff --git a/src/pocketmine/plugin/Plugin.php b/src/plugin/Plugin.php similarity index 54% rename from src/pocketmine/plugin/Plugin.php rename to src/plugin/Plugin.php index 2a56aa38ac..da313d07c6 100644 --- a/src/pocketmine/plugin/Plugin.php +++ b/src/plugin/Plugin.php @@ -26,31 +26,15 @@ declare(strict_types=1); */ namespace pocketmine\plugin; -use pocketmine\command\CommandExecutor; use pocketmine\scheduler\TaskScheduler; use pocketmine\Server; -use pocketmine\utils\Config; /** * It is recommended to use PluginBase for the actual plugin */ -interface Plugin extends CommandExecutor{ +interface Plugin{ - public function __construct(PluginLoader $loader, Server $server, PluginDescription $description, string $dataFolder, string $file); - - /** - * Called when the plugin is loaded, before calling onEnable() - * - * @return void - */ - public function onLoad(); - - /** - * Called when the plugin is enabled - * - * @return void - */ - public function onEnable(); + public function __construct(PluginLoader $loader, Server $server, PluginDescription $description, string $dataFolder, string $file, ResourceProvider $resourceProvider); public function isEnabled() : bool; @@ -61,17 +45,7 @@ interface Plugin extends CommandExecutor{ * @see PluginManager::enablePlugin() * @see PluginManager::disablePlugin() */ - public function setEnabled(bool $enabled = true) : void; - - /** - * Called when the plugin is disabled - * Use this to free open things and finish actions - * - * @return void - */ - public function onDisable(); - - public function isDisabled() : bool; + public function onEnableStateChange(bool $enabled) : void; /** * Gets the plugin's data folder to save files and configuration. @@ -81,49 +55,11 @@ interface Plugin extends CommandExecutor{ public function getDescription() : PluginDescription; - /** - * Gets an embedded resource in the plugin file. - * - * @return null|resource Resource data, or null - */ - public function getResource(string $filename); - - /** - * Saves an embedded resource to its relative location in the data folder - */ - public function saveResource(string $filename, bool $replace = false) : bool; - - /** - * Returns all the resources packaged with the plugin - * - * @return \SplFileInfo[] - */ - public function getResources() : array; - - public function getConfig() : Config; - - /** - * @return void - */ - public function saveConfig(); - - public function saveDefaultConfig() : bool; - - /** - * @return void - */ - public function reloadConfig(); - - public function getServer() : Server; - public function getName() : string; - public function getLogger() : PluginLogger; + public function getLogger() : \AttachableLogger; - /** - * @return PluginLoader - */ - public function getPluginLoader(); + public function getPluginLoader() : PluginLoader; public function getScheduler() : TaskScheduler; diff --git a/src/pocketmine/plugin/PluginBase.php b/src/plugin/PluginBase.php similarity index 60% rename from src/pocketmine/plugin/PluginBase.php rename to src/plugin/PluginBase.php index a15a7db9bf..32b30794e3 100644 --- a/src/pocketmine/plugin/PluginBase.php +++ b/src/plugin/PluginBase.php @@ -24,29 +24,29 @@ declare(strict_types=1); namespace pocketmine\plugin; use pocketmine\command\Command; +use pocketmine\command\CommandExecutor; use pocketmine\command\CommandSender; -use pocketmine\command\PluginIdentifiableCommand; +use pocketmine\command\PluginCommand; +use pocketmine\lang\KnownTranslationFactory; 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 is_dir; use function mkdir; use function rtrim; -use function str_replace; use function stream_copy_to_stream; -use function strlen; use function strpos; use function strtolower; -use function substr; use function trim; -use const DIRECTORY_SEPARATOR; -abstract class PluginBase implements Plugin{ +abstract class PluginBase implements Plugin, CommandExecutor{ /** @var PluginLoader */ private $loader; @@ -75,26 +75,47 @@ abstract class PluginBase implements Plugin{ /** @var TaskScheduler */ private $scheduler; - public function __construct(PluginLoader $loader, Server $server, PluginDescription $description, string $dataFolder, string $file){ + /** @var ResourceProvider */ + private $resourceProvider; + + public function __construct(PluginLoader $loader, Server $server, PluginDescription $description, string $dataFolder, string $file, ResourceProvider $resourceProvider){ $this->loader = $loader; $this->server = $server; $this->description = $description; $this->dataFolder = rtrim($dataFolder, "/" . DIRECTORY_SEPARATOR) . "/"; + //TODO: this is accessed externally via reflection, not unused $this->file = rtrim($file, "/" . DIRECTORY_SEPARATOR) . "/"; - $this->configFile = $this->dataFolder . "config.yml"; - $this->logger = new PluginLogger($this); - $this->scheduler = new TaskScheduler($this->logger, $this->getFullName()); + $this->configFile = Path::join($this->dataFolder, "config.yml"); + + $prefix = $this->getDescription()->getPrefix(); + $this->logger = new PluginLogger($server->getLogger(), $prefix !== "" ? $prefix : $this->getName()); + $this->scheduler = new TaskScheduler($this->getFullName()); + $this->resourceProvider = $resourceProvider; + + $this->onLoad(); + + $this->registerYamlCommands(); } - public function onLoad(){ + /** + * Called when the plugin is loaded, before calling onEnable() + */ + protected function onLoad() : void{ } - public function onEnable(){ + /** + * Called when the plugin is enabled + */ + protected function onEnable() : void{ } - public function onDisable(){ + /** + * Called when the plugin is disabled + * Use this to free open things and finish actions + */ + protected function onDisable() : void{ } @@ -109,7 +130,7 @@ abstract class PluginBase implements Plugin{ * @see PluginManager::enablePlugin() * @see PluginManager::disablePlugin() */ - final public function setEnabled(bool $enabled = true) : void{ + final public function onEnableStateChange(bool $enabled) : void{ if($this->isEnabled !== $enabled){ $this->isEnabled = $enabled; if($this->isEnabled){ @@ -132,20 +153,67 @@ abstract class PluginBase implements Plugin{ return $this->description; } - public function getLogger() : PluginLogger{ + public function getLogger() : \AttachableLogger{ return $this->logger; } /** - * @return Command|PluginIdentifiableCommand|null + * Registers commands declared in the plugin manifest + */ + private function registerYamlCommands() : void{ + $pluginCmds = []; + + 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(), ":"))); + continue; + } + + $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){ + $this->server->getCommandMap()->registerAll($this->getDescription()->getName(), $pluginCmds); + } + } + + /** + * @return Command|PluginOwned|null + * @phpstan-return (Command&PluginOwned)|null */ public function getCommand(string $name){ $command = $this->getServer()->getPluginCommand($name); - if($command === null or $command->getPlugin() !== $this){ + if($command === null or $command->getOwningPlugin() !== $this){ $command = $this->getServer()->getPluginCommand(strtolower($this->description->getName()) . ":" . $name); } - if($command instanceof PluginIdentifiableCommand and $command->getPlugin() === $this){ + if($command instanceof PluginOwned and $command->getOwningPlugin() === $this){ return $command; }else{ return null; @@ -159,10 +227,6 @@ abstract class PluginBase implements Plugin{ return false; } - protected function isPhar() : bool{ - return strpos($this->file, "phar://") === 0; - } - /** * Gets an embedded resource on the plugin file. * WARNING: You must close the resource given using fclose() @@ -170,16 +234,12 @@ abstract class PluginBase implements Plugin{ * @return null|resource Resource data, or null */ public function getResource(string $filename){ - $filename = rtrim(str_replace(DIRECTORY_SEPARATOR, "/", $filename), "/"); - if(file_exists($this->file . "resources/" . $filename)){ - $resource = fopen($this->file . "resources/" . $filename, "rb"); - if($resource === false) throw new AssumptionFailedError("fopen() should not fail on a file which exists"); - return $resource; - } - - return null; + return $this->resourceProvider->getResource($filename); } + /** + * Saves an embedded resource to its relative location in the data folder + */ public function saveResource(string $filename, bool $replace = false) : bool{ if(trim($filename) === ""){ return false; @@ -189,7 +249,7 @@ abstract class PluginBase implements Plugin{ return false; } - $out = $this->dataFolder . $filename; + $out = Path::join($this->dataFolder, $filename); if(!file_exists(dirname($out))){ mkdir(dirname($out), 0755, true); } @@ -213,17 +273,7 @@ abstract class PluginBase implements Plugin{ * @return \SplFileInfo[] */ public function getResources() : array{ - $resources = []; - if(is_dir($this->file . "resources/")){ - foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->file . "resources/")) as $resource){ - if($resource->isFile()){ - $path = str_replace(DIRECTORY_SEPARATOR, "/", substr((string) $resource, strlen($this->file . "resources/"))); - $resources[$path] = $resource; - } - } - } - - return $resources; + return $this->resourceProvider->getResources(); } public function getConfig() : Config{ @@ -234,10 +284,8 @@ abstract class PluginBase implements Plugin{ return $this->config; } - public function saveConfig(){ - if(!$this->getConfig()->save()){ - $this->getLogger()->critical("Could not save config to " . $this->configFile); - } + public function saveConfig() : void{ + $this->getConfig()->save(); } public function saveDefaultConfig() : bool{ @@ -247,7 +295,7 @@ abstract class PluginBase implements Plugin{ return false; } - public function reloadConfig(){ + public function reloadConfig() : void{ $this->saveDefaultConfig(); $this->config = new Config($this->configFile); } @@ -268,10 +316,7 @@ abstract class PluginBase implements Plugin{ return $this->file; } - /** - * @return PluginLoader - */ - public function getPluginLoader(){ + public function getPluginLoader() : PluginLoader{ return $this->loader; } diff --git a/src/pocketmine/plugin/PluginDescription.php b/src/plugin/PluginDescription.php similarity index 64% rename from src/pocketmine/plugin/PluginDescription.php rename to src/plugin/PluginDescription.php index 76fb2a799e..c1ab144fe8 100644 --- a/src/pocketmine/plugin/PluginDescription.php +++ b/src/plugin/PluginDescription.php @@ -24,19 +24,15 @@ declare(strict_types=1); 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 constant; -use function defined; use function is_array; -use function mb_strtoupper; -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{ @@ -44,49 +40,46 @@ 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 int */ - private $order = PluginLoadOrder::POSTWORLD; + private array $authors = []; + private string $website = ""; + private string $prefix = ""; + private PluginEnableOrder $order; - /** @var Permission[] */ - private $permissions = []; + /** + * @var Permission[][] + * @phpstan-var array> + */ + private array $permissions = []; /** * @param string|mixed[] $yamlString @@ -97,28 +90,47 @@ class PluginDescription{ /** * @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"] ?? ""; + $this->api = array_map("\strval", (array) ($plugin["api"] ?? [])); $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"]; + 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"])){ @@ -147,13 +159,15 @@ class PluginDescription{ $this->prefix = (string) ($plugin["prefix"] ?? $this->prefix); if(isset($plugin["load"])){ - $order = mb_strtoupper($plugin["load"]); - if(!defined(PluginLoadOrder::class . "::" . $order)){ - throw new PluginException("Invalid Plugin load"); - }else{ - $this->order = constant(PluginLoadOrder::class . "::" . $order); + $order = PluginEnableOrder::fromString($plugin["load"]); + if($order === null){ + throw new PluginDescriptionParseException("Invalid Plugin \"load\""); } + $this->order = $order; + }else{ + $this->order = PluginEnableOrder::POSTWORLD(); } + $this->authors = []; if(isset($plugin["author"])){ if(is_array($plugin["author"])){ @@ -169,7 +183,11 @@ class PluginDescription{ } if(isset($plugin["permissions"])){ - $this->permissions = Permission::loadPermissions($plugin["permissions"]); + try{ + $this->permissions = PermissionParser::loadPermissions($plugin["permissions"]); + }catch(PermissionParserException $e){ + throw new PluginDescriptionParseException("Invalid Plugin \"permissions\": " . $e->getMessage(), 0, $e); + } } } @@ -210,8 +228,8 @@ class PluginDescription{ } /** - * @return mixed[][] - * @phpstan-return array> + * @return PluginDescriptionCommandEntry[] + * @phpstan-return array */ public function getCommands() : array{ return $this->commands; @@ -225,41 +243,6 @@ class PluginDescription{ return $this->extensions; } - /** - * Checks if the current PHP runtime has the extensions required by the plugin. - * - * @return void - * @throws PluginException if there are required extensions missing or have incompatible version, or if the version constraint cannot be parsed - */ - public function checkRequiredExtensions(){ - 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[] */ @@ -282,16 +265,19 @@ class PluginDescription{ return $this->main; } + public function getSrcNamespacePrefix() : string{ return $this->srcNamespacePrefix; } + public function getName() : string{ return $this->name; } - public function getOrder() : int{ + public function getOrder() : PluginEnableOrder{ return $this->order; } /** - * @return Permission[] + * @return Permission[][] + * @phpstan-return array> */ public function getPermissions() : array{ return $this->permissions; diff --git a/src/plugin/PluginDescriptionCommandEntry.php b/src/plugin/PluginDescriptionCommandEntry.php new file mode 100644 index 0000000000..b9b618e8a8 --- /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/pocketmine/plugin/EventExecutor.php b/src/plugin/PluginDescriptionParseException.php similarity index 81% rename from src/pocketmine/plugin/EventExecutor.php rename to src/plugin/PluginDescriptionParseException.php index edd25a99ed..ad98342f08 100644 --- a/src/pocketmine/plugin/EventExecutor.php +++ b/src/plugin/PluginDescriptionParseException.php @@ -23,13 +23,9 @@ declare(strict_types=1); namespace pocketmine\plugin; -use pocketmine\event\Event; -use pocketmine\event\Listener; +/** + * Thrown when invalid things are found in a PluginDescription during loading + */ +final class PluginDescriptionParseException extends PluginException{ -interface EventExecutor{ - - /** - * @return void - */ - public function execute(Listener $listener, Event $event); } diff --git a/src/plugin/PluginEnableOrder.php b/src/plugin/PluginEnableOrder.php new file mode 100644 index 0000000000..16851cc724 --- /dev/null +++ b/src/plugin/PluginEnableOrder.php @@ -0,0 +1,89 @@ + + */ + private static array $aliasMap = []; + + protected static function register(self $member) : void{ + self::Enum_register($member); + foreach($member->getAliases() as $alias){ + self::$aliasMap[mb_strtolower($alias)] = $member; + } + } + + public static function fromString(string $name) : ?self{ + self::checkInit(); + return self::$aliasMap[mb_strtolower($name)] ?? null; + } + + /** + * @var string[] + * @phpstan-var list + */ + private array $aliases; + + /** + * @param string[] $aliases + * @phpstan-param list $aliases + */ + private function __construct(string $enumName, array $aliases){ + $this->Enum___construct($enumName); + $this->aliases = $aliases; + } + + /** + * @return string[] + * @phpstan-return list + */ + public function getAliases() : array{ return $this->aliases; } +} diff --git a/src/pocketmine/plugin/PluginException.php b/src/plugin/PluginException.php similarity index 100% rename from src/pocketmine/plugin/PluginException.php rename to src/plugin/PluginException.php diff --git a/src/plugin/PluginGraylist.php b/src/plugin/PluginGraylist.php new file mode 100644 index 0000000000..cd6287f829 --- /dev/null +++ b/src/plugin/PluginGraylist.php @@ -0,0 +1,102 @@ +plugins = array_flip($plugins); + $this->isWhitelist = $whitelist; + } + + /** + * @return string[] + */ + public function getPlugins() : array{ + return array_flip($this->plugins); + } + + public function isWhitelist() : bool{ + return $this->isWhitelist; + } + + /** + * Returns whether the given name is permitted by this graylist. + */ + public function isAllowed(string $name) : bool{ + return $this->isWhitelist() === isset($this->plugins[$name]); + } + + /** + * @param mixed[] $array + */ + public static function fromArray(array $array) : PluginGraylist{ + if(!isset($array["mode"]) || ($array["mode"] !== "whitelist" && $array["mode"] !== "blacklist")){ + throw new \InvalidArgumentException("\"mode\" must be set"); + } + $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); + } + + /** + * @return mixed[] + * @phpstan-return array + */ + public function toArray() : array{ + return [ + "mode" => $this->isWhitelist ? 'whitelist' : 'blacklist', + "plugins" => $this->plugins + ]; + } +} diff --git a/src/plugin/PluginLoadTriage.php b/src/plugin/PluginLoadTriage.php new file mode 100644 index 0000000000..c9c4fff773 --- /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 0000000000..5a1a875de3 --- /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; } +} \ No newline at end of file diff --git a/src/plugin/PluginLoadabilityChecker.php b/src/plugin/PluginLoadabilityChecker.php new file mode 100644 index 0000000000..a293f33c00 --- /dev/null +++ b/src/plugin/PluginLoadabilityChecker.php @@ -0,0 +1,108 @@ +getName(); + if(stripos($name, "pocketmine") !== false or stripos($name, "minecraft") !== false or 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 and !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){ + $gotVersion = phpversion($extensionName); + if($gotVersion === false){ + return KnownTranslationFactory::pocketmine_plugin_extensionNotLoaded($extensionName); + } + + 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; + } +} \ No newline at end of file diff --git a/src/pocketmine/plugin/PluginLoader.php b/src/plugin/PluginLoader.php similarity index 96% rename from src/pocketmine/plugin/PluginLoader.php rename to src/plugin/PluginLoader.php index 2bf603ddeb..7e4787fa6d 100644 --- a/src/pocketmine/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 new file mode 100644 index 0000000000..1a1ab9cdb1 --- /dev/null +++ b/src/plugin/PluginLogger.php @@ -0,0 +1,67 @@ +attachments[spl_object_id($attachment)] = $attachment; + } + + /** + * @phpstan-param LoggerAttachment $attachment + */ + public function removeAttachment(\Closure $attachment){ + unset($this->attachments[spl_object_id($attachment)]); + } + + public function removeAttachments(){ + $this->attachments = []; + } + + public function getAttachments(){ + return $this->attachments; + } + + public function log($level, $message){ + parent::log($level, $message); + foreach($this->attachments as $attachment){ + $attachment($level, $message); + } + } +} diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php new file mode 100644 index 0000000000..de84130973 --- /dev/null +++ b/src/plugin/PluginManager.php @@ -0,0 +1,594 @@ +, PluginLoader> + */ + protected $fileAssociations = []; + + /** @var string|null */ + private $pluginDataDirectory; + /** @var PluginGraylist|null */ + private $graylist; + + public function __construct(Server $server, ?string $pluginDataDirectory, ?PluginGraylist $graylist = null){ + $this->server = $server; + $this->pluginDataDirectory = $pluginDataDirectory; + if($this->pluginDataDirectory !== null){ + if(!file_exists($this->pluginDataDirectory)){ + @mkdir($this->pluginDataDirectory, 0777, true); + }elseif(!is_dir($this->pluginDataDirectory)){ + throw new \RuntimeException("Plugin data path $this->pluginDataDirectory exists and is not a directory"); + } + } + + $this->graylist = $graylist; + } + + public function getPlugin(string $name) : ?Plugin{ + if(isset($this->plugins[$name])){ + return $this->plugins[$name]; + } + + return null; + } + + public function registerInterface(PluginLoader $loader) : void{ + $this->fileAssociations[get_class($loader)] = $loader; + } + + /** + * @return Plugin[] + */ + public function getPlugins() : array{ + return $this->plugins; + } + + private function getDataDirectory(string $pluginPath, string $pluginName) : string{ + if($this->pluginDataDirectory !== null){ + return Path::join($this->pluginDataDirectory, $pluginName); + } + return Path::join(dirname($pluginPath), $pluginName); + } + + 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($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); + + $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(); + 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; + } + } + } + + /** + * @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 + */ + private function triagePlugins(string $path, PluginLoadTriage $triage, ?array $newLoaders = null) : void{ + if(is_array($newLoaders)){ + $loaders = []; + foreach($newLoaders as $key){ + if(isset($this->fileAssociations[$key])){ + $loaders[$key] = $this->fileAssociations[$key]; + } + } + }else{ + $loaders = $this->fileAssociations; + } + + 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 = realpath($path); + if($realPath === false) throw new AssumptionFailedError("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"); + if(!$loader->canLoadPlugin($file)){ + continue; + } + 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_loadError($file, $e->getMessage()))); + $this->server->getLogger()->logException($e); + continue; + } + if($description === null){ + continue; + } + + $name = $description->getName(); + + if(($loadabilityError = $loadabilityChecker->check($description)) !== null){ + $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError))); + continue; + } + + if(isset($triage->plugins[$name]) or $this->getPlugin($name) instanceof Plugin){ + $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_duplicateError($name))); + continue; + } + + if(strpos($name, " ") !== false){ + $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name))); + } + + if($this->graylist !== null and !$this->graylist->isAllowed($name)){ + $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError( + $name, + $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist() + ))); + continue; + } + + $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($triage->softDependencies[$before])){ + $triage->softDependencies[$before][] = $name; + }else{ + $triage->softDependencies[$before] = [$name]; + } + } + } + } + } + + /** + * @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]) or $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(Utils::stringifyKeys($triage->plugins) as $name => $entry){ + $this->checkDepsForTriage($name, "hard", $triage->dependencies, $loadedPlugins, $triage); + $this->checkDepsForTriage($name, "soft", $triage->softDependencies, $loadedPlugins, $triage); + + if(!isset($triage->dependencies[$name]) and !isset($triage->softDependencies[$name])){ + unset($triage->plugins[$name]); + $loadedThisLoop++; + + $oldRegisteredLoaders = $this->fileAssociations; + if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof Plugin){ + $loadedPlugins[$name] = $plugin; + $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 :( + + //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()))); + } + break; + } + } + + $this->loadPluginsGuard = false; + return $loadedPlugins; + } + + public function isPluginEnabled(Plugin $plugin) : bool{ + return isset($this->plugins[$plugin->getDescription()->getName()]) and $plugin->isEnabled(); + } + + public function enablePlugin(Plugin $plugin) : void{ + if(!$plugin->isEnabled()){ + $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_enable($plugin->getDescription()->getFullName()))); + + $plugin->getScheduler()->setEnabled(true); + $plugin->onEnableStateChange(true); + + $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin; + + (new PluginEnableEvent($plugin))->call(); + } + } + + public function disablePlugins() : void{ + foreach($this->getPlugins() as $plugin){ + $this->disablePlugin($plugin); + } + } + + public function disablePlugin(Plugin $plugin) : void{ + if($plugin->isEnabled()){ + $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_disable($plugin->getDescription()->getFullName()))); + (new PluginDisableEvent($plugin))->call(); + + unset($this->enabledPlugins[$plugin->getDescription()->getName()]); + + $plugin->onEnableStateChange(false); + $plugin->getScheduler()->shutdown(); + HandlerListManager::global()->unregisterAll($plugin); + } + } + + public function tickSchedulers(int $currentTick) : void{ + foreach($this->enabledPlugins as $p){ + $p->getScheduler()->mainThreadHeartbeat($currentTick); + } + } + + public function clearPlugins() : void{ + $this->disablePlugins(); + $this->plugins = []; + $this->enabledPlugins = []; + $this->fileAssociations = []; + } + + /** + * Returns whether the given ReflectionMethod could be used as an event handler. Used to filter methods on Listeners + * when registering. + * + * Note: This DOES NOT validate the listener annotations; if this method returns false, the method will be ignored + * completely. Invalid annotations on candidate listener methods should result in an error, so those aren't checked + * here. + * + * @phpstan-return class-string|null + */ + private function getEventsHandledBy(\ReflectionMethod $method) : ?string{ + if($method->isStatic() or !$method->getDeclaringClass()->implementsInterface(Listener::class)){ + return null; + } + $tags = Utils::parseDocComment((string) $method->getDocComment()); + if(isset($tags[ListenerMethodTags::NOT_HANDLER])){ + return null; + } + + $parameters = $method->getParameters(); + if(count($parameters) !== 1){ + return null; + } + + $paramType = $parameters[0]->getType(); + //isBuiltin() returns false for builtin classes .................. + if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){ + return null; + } + + /** @phpstan-var class-string $paramClass */ + $paramClass = $paramType->getName(); + $eventClass = new \ReflectionClass($paramClass); + if(!$eventClass->isSubclassOf(Event::class)){ + return null; + } + + /** @var \ReflectionClass $eventClass */ + return $eventClass->getName(); + } + + /** + * Registers all the events in the given Listener class + * + * @throws PluginException + */ + public function registerEvents(Listener $listener, Plugin $plugin) : void{ + if(!$plugin->isEnabled()){ + throw new PluginException("Plugin attempted to register " . get_class($listener) . " while not enabled"); + } + + $reflection = new \ReflectionClass(get_class($listener)); + foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){ + $tags = Utils::parseDocComment((string) $method->getDocComment()); + if(isset($tags[ListenerMethodTags::NOT_HANDLER]) || ($eventClass = $this->getEventsHandledBy($method)) === null){ + continue; + } + $handlerClosure = $method->getClosure($listener); + if($handlerClosure === null) throw new AssumptionFailedError("This should never happen"); + + try{ + $priority = isset($tags[ListenerMethodTags::PRIORITY]) ? EventPriority::fromString($tags[ListenerMethodTags::PRIORITY]) : EventPriority::NORMAL; + }catch(\InvalidArgumentException $e){ + throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid/unknown priority \"" . $tags[ListenerMethodTags::PRIORITY] . "\""); + } + + $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 "": + $handleCancelled = true; + break; + case "false": + break; + default: + throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . " value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . "\""); + } + } + + $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled); + } + } + + /** + * @param string $event Class name that extends Event + * + * @phpstan-template TEvent of Event + * @phpstan-param class-string $event + * @phpstan-param \Closure(TEvent) : void $handler + * + * @throws \ReflectionException + */ + public function registerEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false) : void{ + if(!is_subclass_of($event, Event::class)){ + throw new PluginException($event . " is not an Event"); + } + + $handlerName = Utils::getNiceClosureName($handler); + + if(!$plugin->isEnabled()){ + throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled"); + } + + $timings = new TimingsHandler("Plugin: " . $plugin->getDescription()->getFullName() . " Event: " . $handlerName . "(" . (new \ReflectionClass($event))->getShortName() . ")"); + + HandlerListManager::global()->getListFor($event)->register(new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings)); + } +} diff --git a/src/plugin/PluginOwned.php b/src/plugin/PluginOwned.php new file mode 100644 index 0000000000..8497c78663 --- /dev/null +++ b/src/plugin/PluginOwned.php @@ -0,0 +1,32 @@ +owningPlugin = $owningPlugin; + } + + public function getOwningPlugin() : Plugin{ + return $this->owningPlugin; + } } diff --git a/src/pocketmine/plugin/MethodEventExecutor.php b/src/plugin/ResourceProvider.php similarity index 64% rename from src/pocketmine/plugin/MethodEventExecutor.php rename to src/plugin/ResourceProvider.php index f25229fdb7..fd2f29b627 100644 --- a/src/pocketmine/plugin/MethodEventExecutor.php +++ b/src/plugin/ResourceProvider.php @@ -23,29 +23,19 @@ declare(strict_types=1); namespace pocketmine\plugin; -use pocketmine\event\Event; -use pocketmine\event\Listener; - -class MethodEventExecutor implements EventExecutor{ - - /** @var string */ - private $method; +interface ResourceProvider{ + /** + * Gets an embedded resource on the plugin file. + * WARNING: You must close the resource given using fclose() + * + * @return null|resource Resource data, or null + */ + public function getResource(string $filename); /** - * @param string $method + * Returns all the resources packaged with the plugin in the form ["path/in/resources" => SplFileInfo] + * + * @return \SplFileInfo[] */ - public function __construct($method){ - $this->method = $method; - } - - public function execute(Listener $listener, Event $event){ - $listener->{$this->getMethod()}($event); - } - - /** - * @return string - */ - public function getMethod(){ - return $this->method; - } + public function getResources() : array; } diff --git a/src/pocketmine/plugin/ScriptPluginLoader.php b/src/plugin/ScriptPluginLoader.php similarity index 100% rename from src/pocketmine/plugin/ScriptPluginLoader.php rename to src/plugin/ScriptPluginLoader.php diff --git a/src/pocketmine/Achievement.php b/src/pocketmine/Achievement.php deleted file mode 100644 index ae9a62b69f..0000000000 --- a/src/pocketmine/Achievement.php +++ /dev/null @@ -1,140 +0,0 @@ -}> - */ - public static $list = [ - /*"openInventory" => array( - "name" => "Taking Inventory", - "requires" => [], - ),*/ - "mineWood" => [ - "name" => "Getting Wood", - "requires" => [ //"openInventory", - ] - ], - "buildWorkBench" => [ - "name" => "Benchmarking", - "requires" => [ - "mineWood" - ] - ], - "buildPickaxe" => [ - "name" => "Time to Mine!", - "requires" => [ - "buildWorkBench" - ] - ], - "buildFurnace" => [ - "name" => "Hot Topic", - "requires" => [ - "buildPickaxe" - ] - ], - "acquireIron" => [ - "name" => "Acquire hardware", - "requires" => [ - "buildFurnace" - ] - ], - "buildHoe" => [ - "name" => "Time to Farm!", - "requires" => [ - "buildWorkBench" - ] - ], - "makeBread" => [ - "name" => "Bake Bread", - "requires" => [ - "buildHoe" - ] - ], - "bakeCake" => [ - "name" => "The Lie", - "requires" => [ - "buildHoe" - ] - ], - "buildBetterPickaxe" => [ - "name" => "Getting an Upgrade", - "requires" => [ - "buildPickaxe" - ] - ], - "buildSword" => [ - "name" => "Time to Strike!", - "requires" => [ - "buildWorkBench" - ] - ], - "diamonds" => [ - "name" => "DIAMONDS!", - "requires" => [ - "acquireIron" - ] - ] - - ]; - - public static function broadcast(Player $player, string $achievementId) : bool{ - if(isset(Achievement::$list[$achievementId])){ - $translation = new TranslationContainer("chat.type.achievement", [$player->getDisplayName(), TextFormat::GREEN . Achievement::$list[$achievementId]["name"] . TextFormat::RESET]); - if(Server::getInstance()->getConfigBool("announce-player-achievements", true)){ - Server::getInstance()->broadcastMessage($translation); - }else{ - $player->sendMessage($translation); - } - - return true; - } - - return false; - } - - /** - * @param string[] $requires - */ - public static function add(string $achievementId, string $achievementName, array $requires = []) : bool{ - if(!isset(Achievement::$list[$achievementId])){ - Achievement::$list[$achievementId] = [ - "name" => $achievementName, - "requires" => $requires - ]; - - return true; - } - - return false; - } -} diff --git a/src/pocketmine/CrashDump.php b/src/pocketmine/CrashDump.php deleted file mode 100644 index 511f673883..0000000000 --- a/src/pocketmine/CrashDump.php +++ /dev/null @@ -1,410 +0,0 @@ - - */ - private $data = []; - /** @var string */ - private $encodedData = ""; - /** @var string */ - private $path; - - public function __construct(Server $server){ - $this->time = microtime(true); - $this->server = $server; - if(!is_dir($this->server->getDataPath() . "crashdumps")){ - mkdir($this->server->getDataPath() . "crashdumps"); - } - $this->path = $this->server->getDataPath() . "crashdumps/" . 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 - \pocketmine\START_TIME; - $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; - } - - /** - * @return string - */ - public function getEncodedData(){ - 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" => $d->getOrder() === PluginLoadOrder::POSTWORLD ? "POSTWORLD" : "STARTUP", - "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->getProperty("auto-report.send-settings", true) !== false){ - $this->data["parameters"] = (array) $argv; - if(($serverDotProperties = @file_get_contents($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($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->getProperty("auto-report.send-phpinfo", true) !== false){ - 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 - $errorConversion = [ - E_ERROR => "E_ERROR", - E_WARNING => "E_WARNING", - E_PARSE => "E_PARSE", - E_NOTICE => "E_NOTICE", - E_CORE_ERROR => "E_CORE_ERROR", - E_CORE_WARNING => "E_CORE_WARNING", - E_COMPILE_ERROR => "E_COMPILE_ERROR", - E_COMPILE_WARNING => "E_COMPILE_WARNING", - E_USER_ERROR => "E_USER_ERROR", - E_USER_WARNING => "E_USER_WARNING", - E_USER_NOTICE => "E_USER_NOTICE", - E_STRICT => "E_STRICT", - E_RECOVERABLE_ERROR => "E_RECOVERABLE_ERROR", - E_DEPRECATED => "E_DEPRECATED", - E_USER_DEPRECATED => "E_USER_DEPRECATED" - ]; - $error["fullFile"] = $error["file"]; - $error["file"] = Utils::cleanPath($error["file"]); - $error["type"] = $errorConversion[$error["type"]] ?? $error["type"]; - 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->getProperty("auto-report.send-code", true) !== false 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 = Utils::cleanPath($filePath); - if(strpos($frameCleanPath, Utils::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 = Utils::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 = new VersionString(\pocketmine\BASE_VERSION, \pocketmine\IS_DEVELOPMENT_BUILD, \pocketmine\BUILD_NUMBER); - $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"] = \pocketmine\BASE_VERSION; - $this->data["general"]["build"] = \pocketmine\BUILD_NUMBER; - $this->data["general"]["is_dev"] = \pocketmine\IS_DEVELOPMENT_BUILD; - $this->data["general"]["protocol"] = ProtocolInfo::CURRENT_PROTOCOL; - $this->data["general"]["git"] = \pocketmine\GIT_COMMIT; - $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: " . \pocketmine\GIT_COMMIT); - $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 - * - * @return void - */ - public function addLine($line = ""){ - fwrite($this->fp, $line . PHP_EOL); - } - - /** - * @param string $str - * - * @return void - */ - public function add($str){ - fwrite($this->fp, $str); - } -} diff --git a/src/pocketmine/GlobalConstants.php b/src/pocketmine/GlobalConstants.php deleted file mode 100644 index e32d07c040..0000000000 --- a/src/pocketmine/GlobalConstants.php +++ /dev/null @@ -1,32 +0,0 @@ -server = $server; - $this->name = $name; - if($this->server->hasOfflinePlayerData($this->name)){ - $this->namedtag = $this->server->getOfflinePlayerData($this->name); - } - } - - public function isOnline() : bool{ - return $this->getPlayer() !== null; - } - - public function getName() : string{ - return $this->name; - } - - /** - * @return Server - */ - public function getServer(){ - return $this->server; - } - - public function isOp() : bool{ - return $this->server->isOp($this->name); - } - - public function setOp(bool $value){ - if($value === $this->isOp()){ - return; - } - - if($value){ - $this->server->addOp($this->name); - }else{ - $this->server->removeOp($this->name); - } - } - - public function isBanned() : bool{ - return $this->server->getNameBans()->isBanned($this->name); - } - - public function setBanned(bool $value){ - if($value){ - $this->server->getNameBans()->addBan($this->name, null, null, null); - }else{ - $this->server->getNameBans()->remove($this->name); - } - } - - public function isWhitelisted() : bool{ - return $this->server->isWhitelisted($this->name); - } - - public function setWhitelisted(bool $value){ - if($value){ - $this->server->addWhitelist($this->name); - }else{ - $this->server->removeWhitelist($this->name); - } - } - - public function getPlayer(){ - return $this->server->getPlayerExact($this->name); - } - - public function getFirstPlayed(){ - return $this->namedtag instanceof CompoundTag ? $this->namedtag->getLong("firstPlayed", 0, true) : null; - } - - public function getLastPlayed(){ - return $this->namedtag instanceof CompoundTag ? $this->namedtag->getLong("lastPlayed", 0, true) : null; - } - - public function hasPlayedBefore() : bool{ - return $this->namedtag instanceof CompoundTag; - } - - public function setMetadata(string $metadataKey, MetadataValue $newMetadataValue){ - $this->server->getPlayerMetadata()->setMetadata($this, $metadataKey, $newMetadataValue); - } - - public function getMetadata(string $metadataKey){ - return $this->server->getPlayerMetadata()->getMetadata($this, $metadataKey); - } - - public function hasMetadata(string $metadataKey) : bool{ - return $this->server->getPlayerMetadata()->hasMetadata($this, $metadataKey); - } - - public function removeMetadata(string $metadataKey, Plugin $owningPlugin){ - $this->server->getPlayerMetadata()->removeMetadata($this, $metadataKey, $owningPlugin); - } -} diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php deleted file mode 100644 index 7721b40134..0000000000 --- a/src/pocketmine/Player.php +++ /dev/null @@ -1,4245 +0,0 @@ -= 1 and $len <= 16 and preg_match("/[^A-Za-z0-9_ ]/", $name) === 0; - } - - /** @var SourceInterface */ - protected $interface; - - /** - * @var PlayerNetworkSessionAdapter - * TODO: remove this once player and network are divorced properly - */ - protected $sessionAdapter; - - /** @var string */ - protected $ip; - /** @var int */ - protected $port; - - /** @var bool[] */ - private $needACK = []; - - /** @var DataPacket[] */ - private $batchedPackets = []; - - /** - * @var int - * Last measurement of player's latency in milliseconds. - */ - protected $lastPingMeasure = 1; - - /** @var float */ - public $creationTime = 0; - - /** @var bool */ - public $loggedIn = false; - - /** @var bool */ - private $seenLoginPacket = false; - /** @var bool */ - private $resourcePacksDone = false; - - /** @var bool */ - public $spawned = false; - - /** @var string */ - protected $username = ""; - /** @var string */ - protected $iusername = ""; - /** @var string */ - protected $displayName = ""; - /** @var int */ - protected $randomClientId; - /** @var string */ - protected $xuid = ""; - - /** @var int */ - protected $windowCnt = 2; - /** @var int[] */ - protected $windows = []; - /** @var Inventory[] */ - protected $windowIndex = []; - /** @var bool[] */ - protected $permanentWindows = []; - /** @var PlayerCursorInventory */ - protected $cursorInventory; - /** @var CraftingGrid */ - protected $craftingGrid; - /** @var CraftingTransaction|null */ - protected $craftingTransaction = null; - - /** - * 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 - */ - public $openHardcodedWindows = []; - - /** @var int */ - protected $messageCounter = 2; - /** @var bool */ - protected $removeFormat = true; - - /** @var bool[] name of achievement => bool */ - protected $achievements = []; - /** @var bool */ - protected $playedBefore; - /** @var int */ - protected $gamemode; - - /** @var int */ - private $loaderId = 0; - /** @var bool[] chunkHash => bool (true = sent, false = needs sending) */ - public $usedChunks = []; - /** @var bool[] chunkHash => dummy */ - protected $loadQueue = []; - /** @var int */ - protected $nextChunkOrderRun = 5; - - /** @var int */ - protected $viewDistance = -1; - /** @var int */ - protected $spawnThreshold; - /** @var int */ - protected $spawnChunkLoadCount = 0; - /** @var int */ - protected $chunksPerTick; - - /** @var bool[] map: raw UUID (string) => bool */ - protected $hiddenPlayers = []; - - /** @var float */ - protected $moveRateLimit = 10 * self::MOVES_PER_TICK; - /** @var float|null */ - protected $lastMovementProcess = null; - /** @var Vector3|null */ - protected $forceMoveSync = null; - - /** @var int */ - protected $inAirTicks = 0; - /** @var float */ - protected $stepHeight = 0.6; - /** @var bool */ - protected $allowMovementCheats = false; - - /** @var Vector3|null */ - protected $sleeping = null; - /** @var Position|null */ - private $spawnPosition = null; - - //TODO: Abilities - /** @var bool */ - protected $autoJump = true; - /** @var bool */ - protected $allowFlight = false; - /** @var bool */ - protected $flying = false; - - /** @var PermissibleBase */ - private $perm; - - /** @var int|null */ - protected $lineHeight = null; - /** @var string */ - protected $locale = "en_US"; - - /** @var int */ - protected $startAction = -1; - /** @var int[] ID => ticks map */ - protected $usedItemsCooldown = []; - - /** @var int */ - protected $formIdCounter = 0; - /** @var Form[] */ - protected $forms = []; - - /** @var float */ - protected $lastRightClickTime = 0.0; - /** @var UseItemTransactionData|null */ - protected $lastRightClickData = null; - - /** - * @return TranslationContainer|string - */ - public function getLeaveMessage(){ - if($this->spawned){ - return new TranslationContainer(TextFormat::YELLOW . "%multiplayer.player.left", [ - $this->getDisplayName() - ]); - } - - return ""; - } - - /** - * This might disappear in the future. Please use getUniqueId() instead. - * @deprecated - * - * @return int - */ - public function getClientId(){ - return $this->randomClientId; - } - - public function isBanned() : bool{ - return $this->server->getNameBans()->isBanned($this->username); - } - - public function setBanned(bool $value){ - if($value){ - $this->server->getNameBans()->addBan($this->getName(), null, null, null); - $this->kick("You have been banned"); - }else{ - $this->server->getNameBans()->remove($this->getName()); - } - } - - public function isWhitelisted() : bool{ - return $this->server->isWhitelisted($this->username); - } - - public function setWhitelisted(bool $value){ - if($value){ - $this->server->addWhitelist($this->username); - }else{ - $this->server->removeWhitelist($this->username); - } - } - - public function isAuthenticated() : bool{ - return $this->xuid !== ""; - } - - /** - * If the player is logged into Xbox Live, returns their Xbox user ID (XUID) as a string. Returns an empty string if - * the player is not logged into Xbox Live. - */ - public function getXuid() : string{ - return $this->xuid; - } - - /** - * Returns the player's UUID. This should be the preferred method to identify a player. - * It does not change if the player changes their username. - * - * All players will have a UUID, regardless of whether they are logged into Xbox Live or not. However, note that - * non-XBL players can fake their UUIDs. - * - * WARNING: DO NOT trust this before PlayerLoginEvent. Before PlayerLoginEvent, the player hasn't yet been - * authenticated, and any of their data might be faked. - */ - public function getUniqueId() : ?UUID{ - return parent::getUniqueId(); - } - - public function getPlayer(){ - return $this; - } - - public function getFirstPlayed(){ - return $this->namedtag->getLong("firstPlayed", 0, true); - } - - public function getLastPlayed(){ - return $this->namedtag->getLong("lastPlayed", 0, true); - } - - public function hasPlayedBefore() : bool{ - return $this->playedBefore; - } - - /** - * @return void - */ - public function setAllowFlight(bool $value){ - $this->allowFlight = $value; - $this->sendSettings(); - } - - public function getAllowFlight() : bool{ - return $this->allowFlight; - } - - /** - * @return void - */ - public function setFlying(bool $value){ - if($this->flying !== $value){ - $this->flying = $value; - $this->resetFallDistance(); - $this->sendSettings(); - } - } - - public function isFlying() : bool{ - return $this->flying; - } - - /** - * @return void - */ - public function setAutoJump(bool $value){ - $this->autoJump = $value; - $this->sendSettings(); - } - - public function hasAutoJump() : bool{ - return $this->autoJump; - } - - public function allowMovementCheats() : bool{ - return $this->allowMovementCheats; - } - - /** - * @return void - */ - public function setAllowMovementCheats(bool $value = true){ - $this->allowMovementCheats = $value; - } - - public function spawnTo(Player $player) : void{ - if($this->spawned and $player->spawned and $this->isAlive() and $player->isAlive() and $player->canSee($this) and !$this->isSpectator()){ - parent::spawnTo($player); - } - } - - /** - * @return Server - */ - public function getServer(){ - return $this->server; - } - - public function getRemoveFormat() : bool{ - return $this->removeFormat; - } - - /** - * @return void - */ - public function setRemoveFormat(bool $remove = true){ - $this->removeFormat = $remove; - } - - public function getScreenLineHeight() : int{ - return $this->lineHeight ?? 7; - } - - public function setScreenLineHeight(int $height = null){ - if($height !== null and $height < 1){ - throw new \InvalidArgumentException("Line height must be at least 1"); - } - $this->lineHeight = $height; - } - - public function canSee(Player $player) : bool{ - return !isset($this->hiddenPlayers[$player->getRawUniqueId()]); - } - - /** - * @return void - */ - public function hidePlayer(Player $player){ - if($player === $this){ - return; - } - $this->hiddenPlayers[$player->getRawUniqueId()] = true; - $player->despawnFrom($this); - } - - /** - * @return void - */ - public function showPlayer(Player $player){ - if($player === $this){ - return; - } - unset($this->hiddenPlayers[$player->getRawUniqueId()]); - if($player->isOnline()){ - $player->spawnTo($this); - } - } - - public function canCollideWith(Entity $entity) : bool{ - return false; - } - - public function canBeCollidedWith() : bool{ - return !$this->isSpectator() and parent::canBeCollidedWith(); - } - - public function resetFallDistance() : void{ - parent::resetFallDistance(); - $this->inAirTicks = 0; - } - - public function getViewDistance() : int{ - return $this->viewDistance; - } - - /** - * @return void - */ - public function setViewDistance(int $distance){ - $this->viewDistance = $this->server->getAllowedViewDistance($distance); - - $this->spawnThreshold = (int) (min($this->viewDistance, $this->server->getProperty("chunk-sending.spawn-radius", 4)) ** 2 * M_PI); - - $this->nextChunkOrderRun = 0; - - $pk = new ChunkRadiusUpdatedPacket(); - $pk->radius = $this->viewDistance; - $this->dataPacket($pk); - - $this->server->getLogger()->debug("Setting view distance for " . $this->getName() . " to " . $this->viewDistance . " (requested " . $distance . ")"); - } - - public function isOnline() : bool{ - return $this->isConnected() and $this->loggedIn; - } - - public function isOp() : bool{ - return $this->server->isOp($this->getName()); - } - - /** - * @return void - */ - public function setOp(bool $value){ - if($value === $this->isOp()){ - return; - } - - if($value){ - $this->server->addOp($this->getName()); - }else{ - $this->server->removeOp($this->getName()); - } - - $this->sendSettings(); - } - - /** - * @param permission\Permission|string $name - */ - public function isPermissionSet($name) : bool{ - return $this->perm->isPermissionSet($name); - } - - /** - * @param permission\Permission|string $name - * - * @throws \InvalidStateException if the player is closed - */ - public function hasPermission($name) : bool{ - if($this->closed){ - throw new \InvalidStateException("Trying to get permissions of closed player"); - } - return $this->perm->hasPermission($name); - } - - public function addAttachment(Plugin $plugin, string $name = null, bool $value = null) : PermissionAttachment{ - return $this->perm->addAttachment($plugin, $name, $value); - } - - /** - * @return void - */ - public function removeAttachment(PermissionAttachment $attachment){ - $this->perm->removeAttachment($attachment); - } - - public function recalculatePermissions(){ - $permManager = PermissionManager::getInstance(); - $permManager->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this); - $permManager->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); - - if($this->perm === null){ - return; - } - - $this->perm->recalculatePermissions(); - - if($this->spawned){ - if($this->hasPermission(Server::BROADCAST_CHANNEL_USERS)){ - $permManager->subscribeToPermission(Server::BROADCAST_CHANNEL_USERS, $this); - } - if($this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE)){ - $permManager->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); - } - - $this->sendCommandData(); - } - } - - /** - * @return PermissionAttachmentInfo[] - */ - public function getEffectivePermissions() : array{ - return $this->perm->getEffectivePermissions(); - } - - /** - * @return void - */ - public function sendCommandData(){ - $pk = new AvailableCommandsPacket(); - foreach($this->server->getCommandMap()->getCommands() as $name => $command){ - if(isset($pk->commandData[$command->getName()]) or $command->getName() === "help" or !$command->testPermissionSilent($this)){ - continue; - } - - $data = new CommandData(); - //TODO: commands containing uppercase letters in the name crash 1.9.0 client - $data->commandName = strtolower($command->getName()); - $data->commandDescription = $this->server->getLanguage()->translateString($command->getDescription()); - $data->flags = 0; - $data->permission = 0; - - $parameter = new CommandParameter(); - $parameter->paramName = "args"; - $parameter->paramType = AvailableCommandsPacket::ARG_FLAG_VALID | AvailableCommandsPacket::ARG_TYPE_RAWTEXT; - $parameter->isOptional = true; - $data->overloads[0][0] = $parameter; - - $aliases = $command->getAliases(); - if(count($aliases) > 0){ - if(!in_array($data->commandName, $aliases, true)){ - //work around a client bug which makes the original name not show when aliases are used - $aliases[] = $data->commandName; - } - $data->aliases = new CommandEnum(); - $data->aliases->enumName = ucfirst($command->getName()) . "Aliases"; - $data->aliases->enumValues = array_values($aliases); - } - - $pk->commandData[$command->getName()] = $data; - } - - $this->dataPacket($pk); - - } - - public function __construct(SourceInterface $interface, string $ip, int $port){ - $this->interface = $interface; - $this->perm = new PermissibleBase($this); - $this->namedtag = new CompoundTag(); - $this->server = Server::getInstance(); - $this->ip = $ip; - $this->port = $port; - $this->loaderId = Level::generateChunkLoaderId($this); - $this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4); - $this->spawnThreshold = (int) (($this->server->getProperty("chunk-sending.spawn-radius", 4) ** 2) * M_PI); - $this->gamemode = $this->server->getGamemode(); - $this->setLevel($this->server->getDefaultLevel()); - $this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0); - - $this->creationTime = microtime(true); - - $this->allowMovementCheats = (bool) $this->server->getProperty("player.anti-cheat.allow-movement-cheats", false); - - $this->sessionAdapter = new PlayerNetworkSessionAdapter($this->server, $this); - } - - public function isConnected() : bool{ - return $this->sessionAdapter !== null; - } - - /** - * Gets the username - */ - public function getName() : string{ - return $this->username; - } - - public function getLowerCaseName() : string{ - return $this->iusername; - } - - /** - * Returns the "friendly" display name of this player to use in the chat. - */ - public function getDisplayName() : string{ - return $this->displayName; - } - - /** - * @return void - */ - public function setDisplayName(string $name){ - $this->displayName = $name; - if($this->spawned){ - $this->server->updatePlayerListData($this->getUniqueId(), $this->getId(), $this->getDisplayName(), $this->getSkin(), $this->getXuid()); - } - } - - /** - * Returns the player's locale, e.g. en_US. - */ - public function getLocale() : string{ - return $this->locale; - } - - /** - * Called when a player changes their skin. - * Plugin developers should not use this, use setSkin() and sendSkin() instead. - */ - public function changeSkin(Skin $skin, string $newSkinName, string $oldSkinName) : bool{ - if(!$skin->isValid()){ - return false; - } - - $ev = new PlayerChangeSkinEvent($this, $this->getSkin(), $skin); - $ev->call(); - - if($ev->isCancelled()){ - $this->sendSkin([$this]); - return true; - } - - $this->setSkin($ev->getNewSkin()); - $this->sendSkin($this->server->getOnlinePlayers()); - return true; - } - - /** - * {@inheritdoc} - * - * If null is given, will additionally send the skin to the player itself as well as its viewers. - */ - public function sendSkin(?array $targets = null) : void{ - parent::sendSkin($targets ?? $this->server->getOnlinePlayers()); - } - - /** - * Gets the player IP address - */ - public function getAddress() : string{ - return $this->ip; - } - - public function getPort() : int{ - return $this->port; - } - - /** - * Returns the last measured latency for this player, in milliseconds. This is measured automatically and reported - * back by the network interface. - */ - public function getPing() : int{ - return $this->lastPingMeasure; - } - - /** - * Updates the player's last ping measurement. - * - * @internal Plugins should not use this method. - * - * @return void - */ - public function updatePing(int $pingMS){ - $this->lastPingMeasure = $pingMS; - } - - /** - * @deprecated - */ - public function getNextPosition() : Position{ - return $this->getPosition(); - } - - public function getInAirTicks() : int{ - return $this->inAirTicks; - } - - /** - * Returns whether the player is currently using an item (right-click and hold). - */ - public function isUsingItem() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_ACTION) and $this->startAction > -1; - } - - /** - * @return void - */ - public function setUsingItem(bool $value){ - $this->startAction = $value ? $this->server->getTick() : -1; - $this->setGenericFlag(self::DATA_FLAG_ACTION, $value); - } - - /** - * Returns how long the player has been using their currently-held item for. Used for determining arrow shoot force - * for bows. - */ - public function getItemUseDuration() : int{ - return $this->startAction === -1 ? -1 : ($this->server->getTick() - $this->startAction); - } - - /** - * Returns whether the player has a cooldown period left before it can use the given item again. - */ - public function hasItemCooldown(Item $item) : bool{ - $this->checkItemCooldowns(); - return isset($this->usedItemsCooldown[$item->getId()]); - } - - /** - * Resets the player's cooldown time for the given item back to the maximum. - */ - public function resetItemCooldown(Item $item, ?int $ticks = null) : void{ - $ticks = $ticks ?? $item->getCooldownTicks(); - if($ticks > 0){ - $this->usedItemsCooldown[$item->getId()] = $this->server->getTick() + $ticks; - } - } - - protected function checkItemCooldowns() : void{ - $serverTick = $this->server->getTick(); - foreach($this->usedItemsCooldown as $itemId => $cooldownUntil){ - if($cooldownUntil <= $serverTick){ - unset($this->usedItemsCooldown[$itemId]); - } - } - } - - protected function switchLevel(Level $targetLevel) : bool{ - $oldLevel = $this->level; - if(parent::switchLevel($targetLevel)){ - if($oldLevel !== null){ - foreach($this->usedChunks as $index => $d){ - Level::getXZ($index, $X, $Z); - $this->unloadChunk($X, $Z, $oldLevel); - } - } - - $this->usedChunks = []; - $this->loadQueue = []; - $this->level->sendTime($this); - $this->level->sendDifficulty($this); - - return true; - } - - return false; - } - - /** - * @return void - */ - protected function unloadChunk(int $x, int $z, Level $level = null){ - $level = $level ?? $this->level; - $index = Level::chunkHash($x, $z); - if(isset($this->usedChunks[$index])){ - foreach($level->getChunkEntities($x, $z) as $entity){ - if($entity !== $this){ - $entity->despawnFrom($this); - } - } - - unset($this->usedChunks[$index]); - } - $level->unregisterChunkLoader($this, $x, $z); - unset($this->loadQueue[$index]); - } - - /** - * @return void - */ - public function sendChunk(int $x, int $z, BatchPacket $payload){ - if(!$this->isConnected()){ - return; - } - - $this->usedChunks[Level::chunkHash($x, $z)] = true; - $this->dataPacket($payload); - - if($this->spawned){ - foreach($this->level->getChunkEntities($x, $z) as $entity){ - if($entity !== $this and !$entity->isClosed() and $entity->isAlive()){ - $entity->spawnTo($this); - } - } - } - - if($this->spawnChunkLoadCount !== -1 and ++$this->spawnChunkLoadCount >= $this->spawnThreshold){ - $this->sendPlayStatus(PlayStatusPacket::PLAYER_SPAWN); - $this->spawnChunkLoadCount = -1; - } - } - - /** - * @return void - */ - protected function sendNextChunk(){ - if(!$this->isConnected()){ - return; - } - - Timings::$playerChunkSendTimer->startTiming(); - - $count = 0; - foreach($this->loadQueue as $index => $distance){ - if($count >= $this->chunksPerTick){ - break; - } - - $X = null; - $Z = null; - Level::getXZ($index, $X, $Z); - assert(is_int($X) and is_int($Z)); - - ++$count; - - $this->usedChunks[$index] = false; - $this->level->registerChunkLoader($this, $X, $Z, false); - - if(!$this->level->populateChunk($X, $Z)){ - continue; - } - - unset($this->loadQueue[$index]); - $this->level->requestChunk($X, $Z, $this); - } - - Timings::$playerChunkSendTimer->stopTiming(); - } - - /** - * @return void - */ - public function doFirstSpawn(){ - if($this->spawned || !$this->constructed){ - return; //avoid player spawning twice (this can only happen on 3.x with a custom malicious client) - } - $this->spawned = true; - $this->setImmobile(false); - - if($this->hasPermission(Server::BROADCAST_CHANNEL_USERS)){ - PermissionManager::getInstance()->subscribeToPermission(Server::BROADCAST_CHANNEL_USERS, $this); - } - if($this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE)){ - PermissionManager::getInstance()->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); - } - - $ev = new PlayerJoinEvent($this, - new TranslationContainer(TextFormat::YELLOW . "%multiplayer.player.joined", [ - $this->getDisplayName() - ]) - ); - $ev->call(); - if(strlen(trim((string) $ev->getJoinMessage())) > 0){ - $this->server->broadcastMessage($ev->getJoinMessage()); - } - - $this->noDamageTicks = 60; - - foreach($this->usedChunks as $index => $hasSent){ - if(!$hasSent){ - continue; //this will happen when the chunk is ready to send - } - Level::getXZ($index, $chunkX, $chunkZ); - foreach($this->level->getChunkEntities($chunkX, $chunkZ) as $entity){ - if($entity !== $this and !$entity->isClosed() and $entity->isAlive() and !$entity->isFlaggedForDespawn()){ - $entity->spawnTo($this); - } - } - } - - $this->spawnToAll(); - - if($this->server->getUpdater()->hasUpdate() and $this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE) and $this->server->getProperty("auto-updater.on-update.warn-ops", true)){ - $this->server->getUpdater()->showPlayerUpdate($this); - } - - if($this->getHealth() <= 0){ - $this->actuallyRespawn(); - } - } - - /** - * @return void - */ - protected function sendRespawnPacket(Vector3 $pos, int $respawnState = RespawnPacket::SEARCHING_FOR_SPAWN){ - $pk = new RespawnPacket(); - $pk->position = $pos->add(0, $this->baseOffset, 0); - $pk->respawnState = $respawnState; - $pk->entityRuntimeId = $this->getId(); - - $this->dataPacket($pk); - } - - protected function orderChunks() : void{ - if(!$this->isConnected() or $this->viewDistance === -1){ - return; - } - - Timings::$playerChunkOrderTimer->startTiming(); - - $radius = $this->server->getAllowedViewDistance($this->viewDistance); - $radiusSquared = $radius ** 2; - - $newOrder = []; - $unloadChunks = $this->usedChunks; - - $centerX = $this->getFloorX() >> 4; - $centerZ = $this->getFloorZ() >> 4; - - for($x = 0; $x < $radius; ++$x){ - for($z = 0; $z <= $x; ++$z){ - if(($x ** 2 + $z ** 2) > $radiusSquared){ - break; //skip to next band - } - - //If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be. - - /* Top right quadrant */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $x, $centerZ + $z)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Top left quadrant */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $x - 1, $centerZ + $z)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Bottom right quadrant */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $x, $centerZ - $z - 1)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Bottom left quadrant */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $x - 1, $centerZ - $z - 1)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - if($x !== $z){ - /* Top right quadrant mirror */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $z, $centerZ + $x)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Top left quadrant mirror */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $z - 1, $centerZ + $x)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Bottom right quadrant mirror */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $z, $centerZ - $x - 1)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - - /* Bottom left quadrant mirror */ - if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $z - 1, $centerZ - $x - 1)]) or $this->usedChunks[$index] === false){ - $newOrder[$index] = true; - } - unset($unloadChunks[$index]); - } - } - } - - foreach($unloadChunks as $index => $bool){ - Level::getXZ($index, $X, $Z); - $this->unloadChunk($X, $Z); - } - - $this->loadQueue = $newOrder; - if(count($this->loadQueue) > 0 or count($unloadChunks) > 0){ - $pk = new NetworkChunkPublisherUpdatePacket(); - $pk->x = $this->getFloorX(); - $pk->y = $this->getFloorY(); - $pk->z = $this->getFloorZ(); - $pk->radius = $this->viewDistance * 16; //blocks, not chunks >.> - $this->dataPacket($pk); - } - - Timings::$playerChunkOrderTimer->stopTiming(); - } - - /** - * @return Position - */ - public function getSpawn(){ - if($this->hasValidSpawnPosition()){ - return $this->spawnPosition; - }else{ - $level = $this->server->getDefaultLevel(); - - return $level->getSafeSpawn(); - } - } - - public function hasValidSpawnPosition() : bool{ - return $this->spawnPosition !== null and $this->spawnPosition->isValid(); - } - - /** - * Sets the spawnpoint of the player (and the compass direction) to a Vector3, or set it on another world with a - * Position object - * - * @param Vector3|Position $pos - * - * @return void - */ - public function setSpawn(Vector3 $pos){ - if(!($pos instanceof Position)){ - $level = $this->level; - }else{ - $level = $pos->getLevelNonNull(); - } - $this->spawnPosition = new Position($pos->x, $pos->y, $pos->z, $level); - $pk = new SetSpawnPositionPacket(); - $pk->x = $pk->x2 = $this->spawnPosition->getFloorX(); - $pk->y = $pk->y2 = $this->spawnPosition->getFloorY(); - $pk->z = $pk->z2 = $this->spawnPosition->getFloorZ(); - $pk->dimension = DimensionIds::OVERWORLD; - $pk->spawnType = SetSpawnPositionPacket::TYPE_PLAYER_SPAWN; - - $this->dataPacket($pk); - } - - public function isSleeping() : bool{ - return $this->sleeping !== null; - } - - public function sleepOn(Vector3 $pos) : bool{ - if(!$this->isOnline()){ - return false; - } - - $pos = $pos->floor(); - $b = $this->level->getBlock($pos); - - $ev = new PlayerBedEnterEvent($this, $b); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - if($b instanceof Bed){ - $b->setOccupied(); - } - - $this->sleeping = clone $pos; - - $this->propertyManager->setBlockPos(self::DATA_PLAYER_BED_POSITION, $pos); - $this->setPlayerFlag(self::DATA_PLAYER_FLAG_SLEEP, true); - - $this->setSpawn($pos); - - $this->level->setSleepTicks(60); - - return true; - } - - /** - * @return void - */ - public function stopSleep(){ - if($this->sleeping instanceof Vector3){ - $b = $this->level->getBlock($this->sleeping); - if($b instanceof Bed){ - $b->setOccupied(false); - } - (new PlayerBedLeaveEvent($this, $b))->call(); - - $this->sleeping = null; - $this->propertyManager->setBlockPos(self::DATA_PLAYER_BED_POSITION, null); - $this->setPlayerFlag(self::DATA_PLAYER_FLAG_SLEEP, false); - - $this->level->setSleepTicks(0); - - $pk = new AnimatePacket(); - $pk->entityRuntimeId = $this->id; - $pk->action = AnimatePacket::ACTION_STOP_SLEEP; - $this->dataPacket($pk); - } - } - - public function hasAchievement(string $achievementId) : bool{ - if(!isset(Achievement::$list[$achievementId])){ - return false; - } - - return $this->achievements[$achievementId] ?? false; - } - - public function awardAchievement(string $achievementId) : bool{ - if(isset(Achievement::$list[$achievementId]) and !$this->hasAchievement($achievementId)){ - foreach(Achievement::$list[$achievementId]["requires"] as $requirementId){ - if(!$this->hasAchievement($requirementId)){ - return false; - } - } - $ev = new PlayerAchievementAwardedEvent($this, $achievementId); - $ev->call(); - if(!$ev->isCancelled()){ - $this->achievements[$achievementId] = true; - Achievement::broadcast($this, $achievementId); - - return true; - }else{ - return false; - } - } - - return false; - } - - /** - * @return void - */ - public function removeAchievement(string $achievementId){ - if($this->hasAchievement($achievementId)){ - $this->achievements[$achievementId] = false; - } - } - - public function getGamemode() : int{ - return $this->gamemode; - } - - /** - * @internal - * - * Returns a client-friendly gamemode of the specified real gamemode - * This function takes care of handling gamemodes known to MCPE (as of 1.1.0.3, that includes Survival, Creative and Adventure) - * - * TODO: remove this when Spectator Mode gets added properly to MCPE - */ - public static function getClientFriendlyGamemode(int $gamemode) : int{ - static $map = [ - self::SURVIVAL => GameMode::SURVIVAL, - self::CREATIVE => GameMode::CREATIVE, - self::ADVENTURE => GameMode::ADVENTURE, - self::SPECTATOR => GameMode::CREATIVE - ]; - return $map[$gamemode & 0x3]; - } - - /** - * Sets the gamemode, and if needed, kicks the Player. - * - * @param bool $client if the client made this change in their GUI - */ - public function setGamemode(int $gm, bool $client = false) : bool{ - if($gm < 0 or $gm > 3 or $this->gamemode === $gm){ - return false; - } - - $ev = new PlayerGameModeChangeEvent($this, $gm); - $ev->call(); - if($ev->isCancelled()){ - if($client){ //gamemode change by client in the GUI - $this->sendGamemode(); - } - return false; - } - - $this->gamemode = $gm; - - $this->allowFlight = $this->isCreative(); - if($this->isSpectator()){ - $this->setFlying(true); - $this->keepMovement = true; - $this->onGround = false; - - //TODO: HACK! this syncs the onground flag with the client so that flying works properly - //this is a yucky hack but we don't have any other options :( - $this->sendPosition($this, null, null, MovePlayerPacket::MODE_TELEPORT); - - $this->despawnFromAll(); - }else{ - $this->keepMovement = $this->allowMovementCheats; - $this->checkGroundState(0, 0, 0, 0, 0, 0); - if($this->isSurvival()){ - $this->setFlying(false); - } - $this->spawnToAll(); - } - - $this->namedtag->setInt("playerGameType", $this->gamemode); - if(!$client){ //Gamemode changed by server, do not send for client changes - $this->sendGamemode(); - }else{ - Command::broadcastCommandMessage($this, new TranslationContainer("commands.gamemode.success.self", [Server::getGamemodeString($gm)])); - } - - $this->sendSettings(); - $this->inventory->sendCreativeContents(); - - return true; - } - - /** - * @internal - * Sends the player's gamemode to the client. - * - * @return void - */ - public function sendGamemode(){ - $pk = new SetPlayerGameTypePacket(); - $pk->gamemode = Player::getClientFriendlyGamemode($this->gamemode); - $this->dataPacket($pk); - } - - /** - * Sends all the option flags - * - * @return void - */ - public function sendSettings(){ - $pk = new AdventureSettingsPacket(); - - $pk->setFlag(AdventureSettingsPacket::WORLD_IMMUTABLE, $this->isSpectator()); - $pk->setFlag(AdventureSettingsPacket::NO_PVP, $this->isSpectator()); - $pk->setFlag(AdventureSettingsPacket::AUTO_JUMP, $this->autoJump); - $pk->setFlag(AdventureSettingsPacket::ALLOW_FLIGHT, $this->allowFlight); - $pk->setFlag(AdventureSettingsPacket::NO_CLIP, $this->isSpectator()); - $pk->setFlag(AdventureSettingsPacket::FLYING, $this->flying); - - $pk->commandPermission = ($this->isOp() ? AdventureSettingsPacket::PERMISSION_OPERATOR : AdventureSettingsPacket::PERMISSION_NORMAL); - $pk->playerPermission = ($this->isOp() ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER); - $pk->entityUniqueId = $this->getId(); - - $this->dataPacket($pk); - } - - /** - * NOTE: Because Survival and Adventure Mode share some similar behaviour, this method will also return true if the player is - * in Adventure Mode. Supply the $literal parameter as true to force a literal Survival Mode check. - * - * @param bool $literal whether a literal check should be performed - */ - public function isSurvival(bool $literal = false) : bool{ - if($literal){ - return $this->gamemode === Player::SURVIVAL; - }else{ - return ($this->gamemode & 0x01) === 0; - } - } - - /** - * NOTE: Because Creative and Spectator Mode share some similar behaviour, this method will also return true if the player is - * in Spectator Mode. Supply the $literal parameter as true to force a literal Creative Mode check. - * - * @param bool $literal whether a literal check should be performed - */ - public function isCreative(bool $literal = false) : bool{ - if($literal){ - return $this->gamemode === Player::CREATIVE; - }else{ - return ($this->gamemode & 0x01) === 1; - } - } - - /** - * NOTE: Because Adventure and Spectator Mode share some similar behaviour, this method will also return true if the player is - * in Spectator Mode. Supply the $literal parameter as true to force a literal Adventure Mode check. - * - * @param bool $literal whether a literal check should be performed - */ - public function isAdventure(bool $literal = false) : bool{ - if($literal){ - return $this->gamemode === Player::ADVENTURE; - }else{ - return ($this->gamemode & 0x02) > 0; - } - } - - public function isSpectator() : bool{ - return $this->gamemode === Player::SPECTATOR; - } - - public function isFireProof() : bool{ - return $this->isCreative(); - } - - public function getDrops() : array{ - if(!$this->isCreative()){ - return parent::getDrops(); - } - - return []; - } - - public function getXpDropAmount() : int{ - if(!$this->isCreative()){ - return parent::getXpDropAmount(); - } - - return 0; - } - - protected function checkGroundState(float $movX, float $movY, float $movZ, float $dx, float $dy, float $dz) : void{ - if($this->isSpectator()){ - $this->onGround = false; - }else{ - $bb = clone $this->boundingBox; - $bb->minY = $this->y - 0.2; - $bb->maxY = $this->y + 0.2; - - $this->onGround = $this->isCollided = count($this->level->getCollisionBlocks($bb, true)) > 0; - } - } - - public function canBeMovedByCurrents() : bool{ - return false; //currently has no server-side movement - } - - /** - * @return void - */ - protected function checkNearEntities(){ - foreach($this->level->getNearbyEntities($this->boundingBox->expandedCopy(1, 0.5, 1), $this) as $entity){ - $entity->scheduleUpdate(); - - if(!$entity->isAlive() or $entity->isFlaggedForDespawn()){ - continue; - } - - $entity->onCollideWithPlayer($this); - } - } - - protected function handleMovement(Vector3 $newPos) : void{ - $this->moveRateLimit--; - if($this->moveRateLimit < 0){ - return; - } - - $oldPos = $this->asLocation(); - $distanceSquared = $newPos->distanceSquared($oldPos); - - $revert = false; - - if($distanceSquared > 100){ - //TODO: this is probably too big if we process every movement - /* !!! BEWARE YE WHO ENTER HERE !!! - * - * This is NOT an anti-cheat check. It is a safety check. - * Without it hackers can teleport with freedom on their own and cause lots of undesirable behaviour, like - * freezes, lag spikes and memory exhaustion due to sync chunk loading and collision checks across large distances. - * Not only that, but high-latency players can trigger such behaviour innocently. - * - * If you must tamper with this code, be aware that this can cause very nasty results. Do not waste our time - * asking for help if you suffer the consequences of messing with this. - */ - $this->server->getLogger()->debug($this->getName() . " moved too fast, reverting movement"); - $this->server->getLogger()->debug("Old position: " . $this->asVector3() . ", new position: " . $newPos); - $revert = true; - }elseif(!$this->level->isInLoadedTerrain($newPos) or !$this->level->isChunkGenerated($newPos->getFloorX() >> 4, $newPos->getFloorZ() >> 4)){ - $revert = true; - $this->nextChunkOrderRun = 0; - } - - if(!$revert and $distanceSquared != 0){ - $dx = $newPos->x - $this->x; - $dy = $newPos->y - $this->y; - $dz = $newPos->z - $this->z; - - //the client likes to clip into blocks like stairs, but we do full server-side prediction of that without - //help from the client's position changes, so we deduct the expected clip height from the moved distance. - $expectedClipDistance = $this->ySize * (1 - self::STEP_CLIP_MULTIPLIER); - $dy -= $expectedClipDistance; - $this->move($dx, $dy, $dz); - - $diff = $this->distanceSquared($newPos); - - //TODO: Explore lowering this threshold now that stairs work properly. - if($this->isSurvival() and $diff > 0.0625){ - $ev = new PlayerIllegalMoveEvent($this, $newPos, new Vector3($this->lastX, $this->lastY, $this->lastZ)); - $ev->setCancelled($this->allowMovementCheats); - - $ev->call(); - - if(!$ev->isCancelled()){ - $revert = true; - $this->server->getLogger()->debug($this->getServer()->getLanguage()->translateString("pocketmine.player.invalidMove", [$this->getName()])); - $this->server->getLogger()->debug("Old position: " . $this->asVector3() . ", new position: " . $newPos . ", expected clip distance: $expectedClipDistance"); - } - } - - if($diff > 0 and !$revert){ - $this->setPosition($newPos); - } - } - - if($revert){ - $this->revertMovement($oldPos); - } - } - - /** - * Fires movement events and synchronizes player movement, every tick. - */ - protected function processMostRecentMovements() : void{ - $now = microtime(true); - $multiplier = $this->lastMovementProcess !== null ? ($now - $this->lastMovementProcess) * 20 : 1; - $exceededRateLimit = $this->moveRateLimit < 0; - $this->moveRateLimit = min(self::MOVE_BACKLOG_SIZE, max(0, $this->moveRateLimit) + self::MOVES_PER_TICK * $multiplier); - $this->lastMovementProcess = $now; - - $from = new Location($this->lastX, $this->lastY, $this->lastZ, $this->lastYaw, $this->lastPitch, $this->level); - $to = $this->getLocation(); - - $delta = (($this->lastX - $to->x) ** 2) + (($this->lastY - $to->y) ** 2) + (($this->lastZ - $to->z) ** 2); - $deltaAngle = abs($this->lastYaw - $to->yaw) + abs($this->lastPitch - $to->pitch); - - if($delta > 0.0001 or $deltaAngle > 1.0){ - $ev = new PlayerMoveEvent($this, $from, $to); - - $ev->call(); - - if($ev->isCancelled()){ - $this->revertMovement($from); - return; - } - - if($to->distanceSquared($ev->getTo()) > 0.01){ //If plugins modify the destination - $this->teleport($ev->getTo()); - return; - } - - $this->lastX = $to->x; - $this->lastY = $to->y; - $this->lastZ = $to->z; - - $this->lastYaw = $to->yaw; - $this->lastPitch = $to->pitch; - $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->exhaust(0.1 * $distance, PlayerExhaustEvent::CAUSE_SPRINTING); - }else{ - $this->exhaust(0.01 * $distance, PlayerExhaustEvent::CAUSE_WALKING); - } - - if($this->nextChunkOrderRun > 20){ - $this->nextChunkOrderRun = 20; - } - } - - if($exceededRateLimit){ //client and server positions will be out of sync if this happens - $this->server->getLogger()->debug("Player " . $this->getName() . " exceeded movement rate limit, forcing to last accepted position"); - $this->sendPosition($this, $this->yaw, $this->pitch, MovePlayerPacket::MODE_RESET); - } - } - - protected function revertMovement(Location $from) : void{ - $this->setPosition($from); - $this->sendPosition($from, $from->yaw, $from->pitch, MovePlayerPacket::MODE_RESET); - } - - public function fall(float $fallDistance) : void{ - if(!$this->flying){ - parent::fall($fallDistance); - } - } - - public function jump() : void{ - (new PlayerJumpEvent($this))->call(); - parent::jump(); - } - - public function setMotion(Vector3 $motion) : bool{ - if(parent::setMotion($motion)){ - $this->broadcastMotion(); - - return true; - } - return false; - } - - protected function updateMovement(bool $teleport = false) : void{ - - } - - protected function tryChangeMovement() : void{ - - } - - /** - * @return void - */ - public function sendAttributes(bool $sendAll = false){ - $entries = $sendAll ? $this->attributeMap->getAll() : $this->attributeMap->needSend(); - if(count($entries) > 0){ - $pk = new UpdateAttributesPacket(); - $pk->entityRuntimeId = $this->id; - $pk->entries = $entries; - $this->dataPacket($pk); - foreach($entries as $entry){ - $entry->markSynchronized(); - } - } - } - - public function onUpdate(int $currentTick) : bool{ - if(!$this->loggedIn){ - return false; - } - - $tickDiff = $currentTick - $this->lastUpdate; - - if($tickDiff <= 0){ - return true; - } - - $this->messageCounter = 2; - - $this->lastUpdate = $currentTick; - - $this->sendAttributes(); - - if(!$this->isAlive() and $this->spawned){ - $this->onDeathUpdate($tickDiff); - return true; - } - - $this->timings->startTiming(); - - if($this->spawned){ - $this->processMostRecentMovements(); - $this->motion->x = $this->motion->y = $this->motion->z = 0; //TODO: HACK! (Fixes player knockback being messed up) - if($this->onGround){ - $this->inAirTicks = 0; - }else{ - $this->inAirTicks += $tickDiff; - } - - Timings::$timerEntityBaseTick->startTiming(); - $this->entityBaseTick($tickDiff); - Timings::$timerEntityBaseTick->stopTiming(); - - if(!$this->isSpectator() and $this->isAlive()){ - Timings::$playerCheckNearEntitiesTimer->startTiming(); - $this->checkNearEntities(); - Timings::$playerCheckNearEntitiesTimer->stopTiming(); - } - } - - $this->timings->stopTiming(); - - return true; - } - - protected function doFoodTick(int $tickDiff = 1) : void{ - if($this->isSurvival()){ - parent::doFoodTick($tickDiff); - } - } - - public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{ - if($this->isSurvival()){ - return parent::exhaust($amount, $cause); - } - - return 0.0; - } - - public function isHungry() : bool{ - return $this->isSurvival() and parent::isHungry(); - } - - public function canBreathe() : bool{ - return $this->isCreative() or parent::canBreathe(); - } - - protected function sendEffectAdd(EffectInstance $effect, bool $replacesOldEffect) : void{ - $pk = new MobEffectPacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->eventId = $replacesOldEffect ? MobEffectPacket::EVENT_MODIFY : MobEffectPacket::EVENT_ADD; - $pk->effectId = $effect->getId(); - $pk->amplifier = $effect->getAmplifier(); - $pk->particles = $effect->isVisible(); - $pk->duration = $effect->getDuration(); - - $this->dataPacket($pk); - } - - protected function sendEffectRemove(EffectInstance $effect) : void{ - $pk = new MobEffectPacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->eventId = MobEffectPacket::EVENT_REMOVE; - $pk->effectId = $effect->getId(); - - $this->dataPacket($pk); - } - - /** - * @return void - */ - public function checkNetwork(){ - if(!$this->isOnline()){ - return; - } - - if($this->nextChunkOrderRun !== PHP_INT_MAX and $this->nextChunkOrderRun-- <= 0){ - $this->nextChunkOrderRun = PHP_INT_MAX; - $this->orderChunks(); - } - - if(count($this->loadQueue) > 0){ - $this->sendNextChunk(); - } - - if(count($this->batchedPackets) > 0){ - $this->server->batchPackets([$this], $this->batchedPackets, false); - $this->batchedPackets = []; - } - } - - /** - * Returns whether the player can interact with the specified position. This checks distance and direction. - * - * @param float $maxDiff defaults to half of the 3D diagonal width of a block - */ - public function canInteract(Vector3 $pos, float $maxDistance, float $maxDiff = M_SQRT3 / 2) : bool{ - $eyePos = $this->getPosition()->add(0, $this->getEyeHeight(), 0); - if($eyePos->distanceSquared($pos) > $maxDistance ** 2){ - return false; - } - - $dV = $this->getDirectionVector(); - $eyeDot = $dV->dot($eyePos); - $targetDot = $dV->dot($pos); - return ($targetDot - $eyeDot) >= -$maxDiff; - } - - protected function initHumanData() : void{ - $this->setNameTag($this->username); - } - - protected function initEntity() : void{ - parent::initEntity(); - $this->addDefaultWindows(); - } - - public function handleLogin(LoginPacket $packet) : bool{ - if($this->seenLoginPacket){ - return false; - } - $this->seenLoginPacket = true; - - if($packet->protocol !== ProtocolInfo::CURRENT_PROTOCOL){ - if($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL){ - $this->sendPlayStatus(PlayStatusPacket::LOGIN_FAILED_CLIENT, true); - }else{ - $this->sendPlayStatus(PlayStatusPacket::LOGIN_FAILED_SERVER, true); - } - - //This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client) - $this->close("", $this->server->getLanguage()->translateString("pocketmine.disconnect.incompatibleProtocol", [$packet->protocol]), false); - - return true; - } - - if(!self::isValidUserName($packet->username)){ - $this->close("", "disconnectionScreen.invalidName"); - - return true; - } - - $this->username = TextFormat::clean($packet->username); - $this->displayName = $this->username; - $this->iusername = strtolower($this->username); - - if($packet->locale !== null){ - $this->locale = $packet->locale; - } - - if(count($this->server->getOnlinePlayers()) >= $this->server->getMaxPlayers() and $this->kick("disconnectionScreen.serverFull", false)){ - return true; - } - - $this->randomClientId = $packet->clientId; - - $this->uuid = UUID::fromString($packet->clientUUID); - $this->rawUUID = $this->uuid->toBinary(); - - $animations = []; - foreach($packet->clientData["AnimatedImageData"] as $animation){ - $animations[] = new SkinAnimation( - new SkinImage( - $animation["ImageHeight"], - $animation["ImageWidth"], - base64_decode($animation["Image"], true)), - $animation["Type"], - $animation["Frames"], - $animation["AnimationExpression"] - ); - } - - $personaPieces = []; - foreach($packet->clientData["PersonaPieces"] as $piece){ - $personaPieces[] = new PersonaSkinPiece( - $piece["PieceId"], - $piece["PieceType"], - $piece["PackId"], - $piece["IsDefault"], - $piece["ProductId"] - ); - } - - $pieceTintColors = []; - foreach($packet->clientData["PieceTintColors"] as $tintColor){ - $pieceTintColors[] = new PersonaPieceTintColor($tintColor["PieceType"], $tintColor["Colors"]); - } - - $skinData = new SkinData( - $packet->clientData["SkinId"], - $packet->clientData["PlayFabId"], - base64_decode($packet->clientData["SkinResourcePatch"] ?? "", true), - new SkinImage( - $packet->clientData["SkinImageHeight"], - $packet->clientData["SkinImageWidth"], - base64_decode($packet->clientData["SkinData"], true) - ), - $animations, - new SkinImage( - $packet->clientData["CapeImageHeight"], - $packet->clientData["CapeImageWidth"], - base64_decode($packet->clientData["CapeData"] ?? "", true) - ), - base64_decode($packet->clientData["SkinGeometryData"] ?? "", true), - base64_decode($packet->clientData["SkinGeometryDataEngineVersion"], true), - base64_decode($packet->clientData["SkinAnimationData"] ?? "", true), - $packet->clientData["CapeId"] ?? "", - null, - $packet->clientData["ArmSize"] ?? SkinData::ARM_SIZE_WIDE, - $packet->clientData["SkinColor"] ?? "", - $personaPieces, - $pieceTintColors, - true, - $packet->clientData["PremiumSkin"] ?? false, - $packet->clientData["PersonaSkin"] ?? false, - $packet->clientData["CapeOnClassicSkin"] ?? false, - true, //assume this is true? there's no field for it ... - ); - - try{ - $skin = SkinAdapterSingleton::get()->fromSkinData($skinData); - $skin->validate(); - }catch(InvalidSkinException $e){ - $this->server->getLogger()->debug("$this->username: Invalid skin: " . $e->getMessage()); - $this->close("", "disconnectionScreen.invalidSkin"); - - return true; - } - - $this->setSkin($skin); - - $ev = new PlayerPreLoginEvent($this, "Plugin reason"); - $ev->call(); - if($ev->isCancelled()){ - $this->close("", $ev->getKickMessage()); - - return true; - } - - if(!$this->server->isWhitelisted($this->username) and $this->kick("Server is white-listed", false)){ - return true; - } - - if( - ($this->isBanned() or $this->server->getIPBans()->isBanned($this->getAddress())) and - $this->kick("You are banned", false) - ){ - return true; - } - - if(!$packet->skipVerification){ - $this->server->getAsyncPool()->submitTask(new VerifyLoginTask($this, $packet)); - }else{ - $this->onVerifyCompleted($packet, null, true); - } - - return true; - } - - /** - * @return void - */ - public function sendPlayStatus(int $status, bool $immediate = false){ - $pk = new PlayStatusPacket(); - $pk->status = $status; - $this->sendDataPacket($pk, false, $immediate); - } - - public function onVerifyCompleted(LoginPacket $packet, ?string $error, bool $signedByMojang) : void{ - if($this->closed){ - return; - } - - if($error !== null){ - $this->close("", $this->server->getLanguage()->translateString("pocketmine.disconnect.invalidSession", [$error])); - return; - } - - $xuid = $packet->xuid; - - if(!$signedByMojang and $xuid !== ""){ - $this->server->getLogger()->warning($this->getName() . " has an XUID, but their login keychain is not signed by Mojang"); - $xuid = ""; - } - - if($xuid === "" or !is_string($xuid)){ - if($signedByMojang){ - $this->server->getLogger()->error($this->getName() . " should have an XUID, but none found"); - } - - if($this->server->requiresAuthentication() and $this->kick("disconnectionScreen.notAuthenticated", false)){ //use kick to allow plugins to cancel this - return; - } - - $this->server->getLogger()->debug($this->getName() . " is NOT logged into Xbox Live"); - }else{ - $this->server->getLogger()->debug($this->getName() . " is logged into Xbox Live"); - $this->xuid = $xuid; - } - - //TODO: encryption - - $this->processLogin(); - } - - /** - * @return void - */ - protected function processLogin(){ - $checkXUID = (bool) $this->server->getProperty("player.verify-xuid", true); - $kickForXUIDMismatch = function(string $xuid) use ($checkXUID) : bool{ - if($checkXUID && $this->xuid !== $xuid){ - $this->server->getLogger()->debug($this->getName() . " XUID mismatch: expected '$xuid', but got '$this->xuid'"); - if($this->kick("XUID does not match (possible impersonation attempt)", false)){ - //TODO: Longer term, we should be identifying playerdata using something more reliable, like XUID or UUID. - //However, that would be a very disruptive change, so this will serve as a stopgap for now. - //Side note: this will also prevent offline players hijacking XBL playerdata on online servers, since their - //XUID will always be empty. - return true; - } - $this->server->getLogger()->debug("XUID mismatch for " . $this->getName() . ", but plugin cancelled event allowing them to join anyway"); - } - return false; - }; - - foreach($this->server->getLoggedInPlayers() as $p){ - if($p !== $this and ($p->iusername === $this->iusername or $this->getUniqueId()->equals($p->getUniqueId()))){ - if($kickForXUIDMismatch($p->getXuid())){ - return; - } - if(!$p->kick("logged in from another location")){ - $this->close($this->getLeaveMessage(), "Logged in from another location"); - return; - } - } - } - - $this->namedtag = $this->server->getOfflinePlayerData($this->username); - if($checkXUID){ - $recordedXUID = $this->namedtag->getTag("LastKnownXUID"); - if(!($recordedXUID instanceof StringTag)){ - $this->server->getLogger()->debug("No previous XUID recorded for " . $this->getName() . ", no choice but to trust this player"); - }elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){ - $this->server->getLogger()->debug("XUID match for " . $this->getName()); - } - } - - $this->playedBefore = ($this->getLastPlayed() - $this->getFirstPlayed()) > 1; // microtime(true) - microtime(true) may have less than one millisecond difference - $this->namedtag->setString("NameTag", $this->username); - - $this->gamemode = $this->namedtag->getInt("playerGameType", self::SURVIVAL) & 0x03; - if($this->server->getForceGamemode()){ - $this->gamemode = $this->server->getGamemode(); - $this->namedtag->setInt("playerGameType", $this->gamemode); - } - - $this->allowFlight = $this->isCreative(); - $this->keepMovement = $this->isSpectator() || $this->allowMovementCheats(); - - if(($level = $this->server->getLevelByName($this->namedtag->getString("Level", "", true))) === null){ - $this->setLevel($this->server->getDefaultLevel()); - $this->namedtag->setString("Level", $this->level->getFolderName()); - $spawnLocation = $this->level->getSafeSpawn(); - $this->namedtag->setTag(new ListTag("Pos", [ - new DoubleTag("", $spawnLocation->x), - new DoubleTag("", $spawnLocation->y), - new DoubleTag("", $spawnLocation->z) - ])); - }else{ - $this->setLevel($level); - } - - $this->achievements = []; - - $achievements = $this->namedtag->getCompoundTag("Achievements") ?? []; - /** @var ByteTag $achievement */ - foreach($achievements as $achievement){ - $this->achievements[$achievement->getName()] = $achievement->getValue() !== 0; - } - - $this->sendPlayStatus(PlayStatusPacket::LOGIN_SUCCESS); - - $this->loggedIn = true; - $this->server->onPlayerLogin($this); - - $pk = new ResourcePacksInfoPacket(); - $manager = $this->server->getResourcePackManager(); - $pk->resourcePackEntries = $manager->getResourceStack(); - $pk->mustAccept = $manager->resourcePacksRequired(); - $this->dataPacket($pk); - } - - public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ - if($this->resourcePacksDone){ - return false; - } - switch($packet->status){ - case ResourcePackClientResponsePacket::STATUS_REFUSED: - //TODO: add lang strings for this - $this->close("", "You must accept resource packs to join this server.", true); - break; - case ResourcePackClientResponsePacket::STATUS_SEND_PACKS: - $manager = $this->server->getResourcePackManager(); - foreach($packet->packIds as $uuid){ - //dirty hack for mojang's dirty hack for versions - $splitPos = strpos($uuid, "_"); - if($splitPos !== false){ - $uuid = substr($uuid, 0, $splitPos); - } - - $pack = $manager->getPackById($uuid); - if(!($pack instanceof ResourcePack)){ - //Client requested a resource pack but we don't have it available on the server - $this->close("", "disconnectionScreen.resourcePack", true); - $this->server->getLogger()->debug("Got a resource pack request for unknown pack with UUID " . $uuid . ", available packs: " . implode(", ", $manager->getPackIdList())); - - return false; - } - - $pk = new ResourcePackDataInfoPacket(); - $pk->packId = $pack->getPackId(); - $pk->maxChunkSize = self::RESOURCE_PACK_CHUNK_SIZE; - $pk->chunkCount = (int) ceil($pack->getPackSize() / $pk->maxChunkSize); - $pk->compressedPackSize = $pack->getPackSize(); - $pk->sha256 = $pack->getSha256(); - $this->dataPacket($pk); - } - - break; - case ResourcePackClientResponsePacket::STATUS_HAVE_ALL_PACKS: - $pk = new ResourcePackStackPacket(); - $manager = $this->server->getResourcePackManager(); - $pk->resourcePackStack = $manager->getResourceStack(); - //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. - $pk->mustAccept = false; - $pk->experiments = new Experiments([], false); - $this->dataPacket($pk); - break; - case ResourcePackClientResponsePacket::STATUS_COMPLETED: - $this->resourcePacksDone = true; - $this->completeLoginSequence(); - break; - default: - return false; - } - - return true; - } - - /** - * @return void - */ - protected function completeLoginSequence(){ - /** @var float[] $pos */ - $pos = $this->namedtag->getListTag("Pos")->getAllValues(); - $this->level->registerChunkLoader($this, ((int) floor($pos[0])) >> 4, ((int) floor($pos[2])) >> 4, true); - $this->usedChunks[Level::chunkHash(((int) floor($pos[0])) >> 4, ((int) floor($pos[2])) >> 4)] = false; - - parent::__construct($this->level, $this->namedtag); - $ev = new PlayerLoginEvent($this, "Plugin reason"); - $ev->call(); - if($ev->isCancelled()){ - $this->close($this->getLeaveMessage(), $ev->getKickMessage()); - - return; - } - - if(!$this->hasValidSpawnPosition()){ - if(($level = $this->server->getLevelByName($this->namedtag->getString("SpawnLevel", ""))) instanceof Level){ - $this->spawnPosition = new Position($this->namedtag->getInt("SpawnX"), $this->namedtag->getInt("SpawnY"), $this->namedtag->getInt("SpawnZ"), $level); - }else{ - $this->spawnPosition = $this->level->getSafeSpawn(); - } - } - - $spawnPosition = $this->getSpawn(); - - $pk = new StartGamePacket(); - $pk->entityUniqueId = $this->id; - $pk->entityRuntimeId = $this->id; - $pk->playerGamemode = Player::getClientFriendlyGamemode($this->gamemode); - - $pk->playerPosition = $this->getOffsetPosition($this); - - $pk->pitch = $this->pitch; - $pk->yaw = $this->yaw; - $pk->seed = -1; - $pk->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly - $pk->worldGamemode = Player::getClientFriendlyGamemode($this->server->getGamemode()); - $pk->difficulty = $this->level->getDifficulty(); - $pk->spawnX = $spawnPosition->getFloorX(); - $pk->spawnY = $spawnPosition->getFloorY(); - $pk->spawnZ = $spawnPosition->getFloorZ(); - $pk->hasAchievementsDisabled = true; - $pk->time = $this->level->getTime(); - $pk->eduEditionOffer = 0; - $pk->rainLevel = 0; //TODO: implement these properly - $pk->lightningLevel = 0; - $pk->commandsEnabled = true; - $pk->levelId = ""; - $pk->worldName = $this->server->getMotd(); - $pk->experiments = new Experiments([], false); - $pk->itemTable = ItemTypeDictionary::getInstance()->getEntries(); - $pk->playerMovementSettings = new PlayerMovementSettings(PlayerMovementType::LEGACY, 0, false); - $pk->serverSoftwareVersion = sprintf("%s %s", \pocketmine\NAME, \pocketmine\VERSION); - $pk->blockPaletteChecksum = 0; //we don't bother with this (0 skips verification) - the preimage is some dumb stringified NBT, not even actual NBT - $this->dataPacket($pk); - - $this->sendDataPacket(new AvailableActorIdentifiersPacket()); - $this->sendDataPacket(new BiomeDefinitionListPacket()); - - $this->level->sendTime($this); - - $this->sendAttributes(true); - $this->setNameTagVisible(); - $this->setNameTagAlwaysVisible(); - $this->setCanClimb(); - $this->setImmobile(); //disable pre-spawn movement - - $this->server->getLogger()->info($this->getServer()->getLanguage()->translateString("pocketmine.player.logIn", [ - TextFormat::AQUA . $this->username . TextFormat::WHITE, - $this->ip, - $this->port, - $this->id, - $this->level->getName(), - round($this->x, 4), - round($this->y, 4), - round($this->z, 4) - ])); - - if($this->isOp()){ - $this->setRemoveFormat(false); - } - - $this->sendCommandData(); - $this->sendSettings(); - $this->sendPotionEffects($this); - $this->sendData($this); - - $this->sendAllInventories(); - $this->inventory->sendCreativeContents(); - $this->inventory->sendHeldItem($this); - $this->dataPacket($this->server->getCraftingManager()->getCraftingDataPacket()); - - $this->server->addOnlinePlayer($this); - $this->server->sendFullPlayerListData($this); - } - - /** - * Sends a chat message as this player. If the message begins with a / (forward-slash) it will be treated - * as a command. - */ - public function chat(string $message) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return false; - } - - $this->doCloseInventory(); - - $message = TextFormat::clean($message, $this->removeFormat); - foreach(explode("\n", $message) as $messagePart){ - if(trim($messagePart) !== "" and strlen($messagePart) <= 255 and $this->messageCounter-- > 0){ - if(strpos($messagePart, './') === 0){ - $messagePart = substr($messagePart, 1); - } - - $ev = new PlayerCommandPreprocessEvent($this, $messagePart); - $ev->call(); - - if($ev->isCancelled()){ - break; - } - - if(strpos($ev->getMessage(), "/") === 0){ - Timings::$playerCommandTimer->startTiming(); - $this->server->dispatchCommand($ev->getPlayer(), substr($ev->getMessage(), 1)); - Timings::$playerCommandTimer->stopTiming(); - }else{ - $ev = new PlayerChatEvent($this, $ev->getMessage()); - $ev->call(); - if(!$ev->isCancelled()){ - $this->server->broadcastMessage($this->getServer()->getLanguage()->translateString($ev->getFormat(), [$ev->getPlayer()->getDisplayName(), $ev->getMessage()]), $ev->getRecipients()); - } - } - } - } - - return true; - } - - public function handleMovePlayer(MovePlayerPacket $packet) : bool{ - $rawPos = $packet->position; - foreach([$rawPos->x, $rawPos->y, $rawPos->z, $packet->yaw, $packet->headYaw, $packet->pitch] as $float){ - if(is_infinite($float) || is_nan($float)){ - $this->server->getLogger()->debug("Invalid movement from " . $this->getName() . ", contains NAN/INF components"); - return false; - } - } - - $newPos = $rawPos->round(4)->subtract(0, $this->baseOffset, 0); - if($this->forceMoveSync !== null and $newPos->distanceSquared($this->forceMoveSync) > 1){ //Tolerate up to 1 block to avoid problems with client-sided physics when spawning in blocks - $this->server->getLogger()->debug("Got outdated pre-teleport movement from " . $this->getName() . ", received " . $newPos . ", expected " . $this->asVector3()); - //Still getting movements from before teleport, ignore them - }elseif((!$this->isAlive() or !$this->spawned) and $newPos->distanceSquared($this) > 0.01){ - $this->sendPosition($this, null, null, MovePlayerPacket::MODE_RESET); - $this->server->getLogger()->debug("Reverted movement of " . $this->getName() . " due to not alive or not spawned, received " . $newPos . ", locked at " . $this->asVector3()); - }else{ - // Once we get a movement within a reasonable distance, treat it as a teleport ACK and remove position lock - $this->forceMoveSync = null; - - $packet->yaw = fmod($packet->yaw, 360); - $packet->pitch = fmod($packet->pitch, 360); - - if($packet->yaw < 0){ - $packet->yaw += 360; - } - - $this->setRotation($packet->yaw, $packet->pitch); - $this->handleMovement($newPos); - } - - return true; - } - - public function handleLevelSoundEvent(LevelSoundEventPacket $packet) : bool{ - //TODO: add events so plugins can change this - $this->getLevelNonNull()->broadcastPacketToViewers($this, $packet); - return true; - } - - public function handleEntityEvent(ActorEventPacket $packet) : bool{ - if($packet->entityRuntimeId !== $this->id){ - //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; - } - if(!$this->spawned or !$this->isAlive()){ - return true; - } - $this->doCloseInventory(); - - switch($packet->event){ - case ActorEventPacket::EATING_ITEM: - if($packet->data === 0){ - return false; - } - - $this->dataPacket($packet); - $this->server->broadcastPacket($this->getViewers(), $packet); - break; - default: - return false; - } - - return true; - } - - /** - * Don't expect much from this handler. Most of it is roughly hacked and duct-taped together. - */ - public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return false; - } - - /** @var InventoryAction[] $actions */ - $actions = []; - $isCraftingPart = false; - foreach($packet->trData->getActions() as $networkInventoryAction){ - if( - $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO and ( - $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_RESULT or - $networkInventoryAction->windowId === NetworkInventoryAction::SOURCE_TYPE_CRAFTING_USE_INGREDIENT - ) or ( - $this->craftingTransaction !== null && - !$networkInventoryAction->oldItem->getItemStack()->equalsExact($networkInventoryAction->newItem->getItemStack()) && - $networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && - $networkInventoryAction->windowId === ContainerIds::UI && - $networkInventoryAction->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT - ) - ){ - $isCraftingPart = true; - } - try{ - $action = $networkInventoryAction->createInventoryAction($this); - if($action !== null){ - $actions[] = $action; - } - }catch(\UnexpectedValueException $e){ - $this->server->getLogger()->debug("Unhandled inventory action from " . $this->getName() . ": " . $e->getMessage()); - $this->sendAllInventories(); - return false; - } - } - - if($isCraftingPart){ - if($this->craftingTransaction === null){ - $this->craftingTransaction = new CraftingTransaction($this, $actions); - }else{ - foreach($actions as $action){ - $this->craftingTransaction->addAction($action); - } - } - - try{ - $this->craftingTransaction->validate(); - }catch(TransactionValidationException $e){ - //transaction is incomplete - crafting transaction comes in lots of little bits, so we have to collect - //all of the parts before we can execute it - return true; - } - - try{ - $this->craftingTransaction->execute(); - return true; - }catch(TransactionValidationException $e){ - $this->server->getLogger()->debug("Failed to execute crafting transaction for " . $this->getName() . ": " . $e->getMessage()); - return false; - }finally{ - $this->craftingTransaction = null; - } - }elseif($this->craftingTransaction !== null){ - $this->server->getLogger()->debug("Got unexpected normal inventory action with incomplete crafting transaction from " . $this->getName() . ", refusing to execute crafting"); - $this->craftingTransaction = null; - } - - if($packet->trData instanceof NormalTransactionData){ - $this->setUsingItem(false); - $transaction = new InventoryTransaction($this, $actions); - - try{ - $transaction->execute(); - }catch(TransactionValidationException $e){ - $this->server->getLogger()->debug("Failed to execute inventory transaction from " . $this->getName() . ": " . $e->getMessage()); - $this->server->getLogger()->debug("Actions: " . json_encode($packet->trData->getActions())); - - return false; - } - - //TODO: fix achievement for getting iron from furnace - - return true; - }elseif($packet->trData instanceof MismatchTransactionData){ - if(count($packet->trData->getActions()) > 0){ - $this->server->getLogger()->debug("Expected 0 actions for mismatch, got " . count($packet->trData->getActions()) . ", " . json_encode($packet->trData->getActions())); - } - $this->setUsingItem(false); - $this->sendAllInventories(); - - return true; - }elseif($packet->trData instanceof UseItemTransactionData){ - - $blockVector = $packet->trData->getBlockPos(); - $face = $packet->trData->getFace(); - - if($this->inventory->getHeldItemIndex() !== $packet->trData->getHotbarSlot()){ - $this->inventory->equipItem($packet->trData->getHotbarSlot()); - } - - switch($packet->trData->getActionType()){ - case UseItemTransactionData::ACTION_CLICK_BLOCK: - //TODO: start hack for client spam bug - $spamBug = ($this->lastRightClickData !== null and - microtime(true) - $this->lastRightClickTime < 0.1 and //100ms - $this->lastRightClickData->getPlayerPos()->distanceSquared($packet->trData->getPlayerPos()) < 0.00001 and - $this->lastRightClickData->getBlockPos()->equals($packet->trData->getBlockPos()) and - $this->lastRightClickData->getClickPos()->distanceSquared($packet->trData->getClickPos()) < 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 = $packet->trData; - $this->lastRightClickTime = microtime(true); - if($spamBug){ - return true; - } - //TODO: end hack for client spam bug - - $this->setUsingItem(false); - - if(!$this->canInteract($blockVector->add(0.5, 0.5, 0.5), 13)){ - }elseif($this->isCreative()){ - $item = $this->inventory->getItemInHand(); - if($this->level->useItemOn($blockVector, $item, $face, $packet->trData->getClickPos(), $this, true)){ - return true; - } - }elseif(!$this->inventory->getItemInHand()->equals($packet->trData->getItemInHand()->getItemStack())){ - $this->inventory->sendHeldItem($this); - }else{ - $item = $this->inventory->getItemInHand(); - $oldItem = clone $item; - if($this->level->useItemOn($blockVector, $item, $face, $packet->trData->getClickPos(), $this, true)){ - if(!$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){ - $this->inventory->setItemInHand($item); - $this->inventory->sendHeldItem($this->hasSpawned); - } - - return true; - } - } - - $this->inventory->sendHeldItem($this); - - if($blockVector->distanceSquared($this) > 10000){ - return true; - } - - $target = $this->level->getBlock($blockVector); - $block = $target->getSide($face); - - /** @var Block[] $blocks */ - $blocks = array_merge($target->getAllSides(), $block->getAllSides()); //getAllSides() on each of these will include $target and $block because they are next to each other - - $this->level->sendBlocks([$this], $blocks, UpdateBlockPacket::FLAG_ALL_PRIORITY); - - return true; - case UseItemTransactionData::ACTION_BREAK_BLOCK: - $this->doCloseInventory(); - - $item = $this->inventory->getItemInHand(); - $oldItem = clone $item; - - if($this->canInteract($blockVector->add(0.5, 0.5, 0.5), $this->isCreative() ? 13 : 7) and $this->level->useBreakOn($blockVector, $item, $this, true)){ - if($this->isSurvival()){ - if(!$item->equalsExact($oldItem) and $oldItem->equalsExact($this->inventory->getItemInHand())){ - $this->inventory->setItemInHand($item); - $this->inventory->sendHeldItem($this->hasSpawned); - } - - $this->exhaust(0.025, PlayerExhaustEvent::CAUSE_MINING); - } - return true; - } - - $this->inventory->sendContents($this); - $this->inventory->sendHeldItem($this); - - $target = $this->level->getBlock($blockVector); - /** @var Block[] $blocks */ - $blocks = $target->getAllSides(); - $blocks[] = $target; - - $this->level->sendBlocks([$this], $blocks, UpdateBlockPacket::FLAG_ALL_PRIORITY); - - foreach($blocks as $b){ - $tile = $this->level->getTile($b); - if($tile instanceof Spawnable){ - $tile->spawnTo($this); - } - } - - return true; - case UseItemTransactionData::ACTION_CLICK_AIR: - if($this->isUsingItem()){ - $slot = $this->inventory->getItemInHand(); - if($slot instanceof Consumable and !($slot instanceof MaybeConsumable and !$slot->canBeConsumed())){ - $ev = new PlayerItemConsumeEvent($this, $slot); - if($this->hasItemCooldown($slot)){ - $ev->setCancelled(); - } - $ev->call(); - if($ev->isCancelled() or !$this->consumeObject($slot)){ - $this->inventory->sendContents($this); - return true; - } - $this->resetItemCooldown($slot); - if($this->isSurvival()){ - $slot->pop(); - $this->inventory->setItemInHand($slot); - $this->inventory->addItem($slot->getResidue()); - } - $this->setUsingItem(false); - } - } - $directionVector = $this->getDirectionVector(); - - if($this->isCreative()){ - $item = $this->inventory->getItemInHand(); - }elseif(!$this->inventory->getItemInHand()->equals($packet->trData->getItemInHand()->getItemStack())){ - $this->inventory->sendHeldItem($this); - return true; - }else{ - $item = $this->inventory->getItemInHand(); - } - - $ev = new PlayerInteractEvent($this, $item, null, $directionVector, $face, PlayerInteractEvent::RIGHT_CLICK_AIR); - if($this->hasItemCooldown($item) or $this->isSpectator()){ - $ev->setCancelled(); - } - - $ev->call(); - if($ev->isCancelled()){ - $this->inventory->sendHeldItem($this); - return true; - } - - if($item->onClickAir($this, $directionVector)){ - $this->resetItemCooldown($item); - if($this->isSurvival()){ - $this->inventory->setItemInHand($item); - } - } - - $this->setUsingItem(true); - - return true; - default: - //unknown - break; - } - - $this->inventory->sendContents($this); - return false; - }elseif($packet->trData instanceof UseItemOnEntityTransactionData){ - $target = $this->level->getEntity($packet->trData->getEntityRuntimeId()); - if($target === null){ - return false; - } - - if($this->inventory->getHeldItemIndex() !== $packet->trData->getHotbarSlot()){ - $this->inventory->equipItem($packet->trData->getHotbarSlot()); - } - - switch($packet->trData->getActionType()){ - case UseItemOnEntityTransactionData::ACTION_INTERACT: - break; //TODO - case UseItemOnEntityTransactionData::ACTION_ATTACK: - if(!$target->isAlive()){ - return true; - } - if($target instanceof ItemEntity or $target instanceof Arrow){ - $this->kick("Attempting to attack an invalid entity"); - $this->server->getLogger()->warning($this->getServer()->getLanguage()->translateString("pocketmine.player.invalidEntity", [$this->getName()])); - return false; - } - - $cancelled = false; - - $heldItem = $this->inventory->getItemInHand(); - $oldItem = clone $heldItem; - - if(!$this->canInteract($target, 8) or $this->isSpectator()){ - $cancelled = true; - }elseif($target instanceof Player){ - if(!$this->server->getConfigBool("pvp")){ - $cancelled = true; - } - } - - $ev = new EntityDamageByEntityEvent($this, $target, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints()); - - $meleeEnchantmentDamage = 0; - /** @var EnchantmentInstance[] $meleeEnchantments */ - $meleeEnchantments = []; - foreach($heldItem->getEnchantments() as $enchantment){ - $type = $enchantment->getType(); - if($type instanceof MeleeWeaponEnchantment and $type->isApplicableTo($target)){ - $meleeEnchantmentDamage += $type->getDamageBonus($enchantment->getLevel()); - $meleeEnchantments[] = $enchantment; - } - } - $ev->setModifier($meleeEnchantmentDamage, EntityDamageEvent::MODIFIER_WEAPON_ENCHANTMENTS); - - if($cancelled){ - $ev->setCancelled(); - } - - if(!$this->isSprinting() and !$this->isFlying() and $this->fallDistance > 0 and !$this->hasEffect(Effect::BLINDNESS) and !$this->isUnderwater()){ - $ev->setModifier($ev->getFinalDamage() / 2, EntityDamageEvent::MODIFIER_CRITICAL); - } - - $target->attack($ev); - - if($ev->isCancelled()){ - if($heldItem instanceof Durable and $this->isSurvival()){ - $this->inventory->sendContents($this); - } - return true; - } - - if($ev->getModifier(EntityDamageEvent::MODIFIER_CRITICAL) > 0){ - $pk = new AnimatePacket(); - $pk->action = AnimatePacket::ACTION_CRITICAL_HIT; - $pk->entityRuntimeId = $target->getId(); - $this->server->broadcastPacket($target->getViewers(), $pk); - if($target instanceof Player){ - $target->dataPacket($pk); - } - } - - foreach($meleeEnchantments as $enchantment){ - $type = $enchantment->getType(); - assert($type instanceof MeleeWeaponEnchantment); - $type->onPostAttack($this, $target, $enchantment->getLevel()); - } - - 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($target) and $this->isSurvival() and $oldItem->equalsExact($this->inventory->getItemInHand())){ //always fire the hook, even if we are survival - $this->inventory->setItemInHand($heldItem); - } - - $this->exhaust(0.3, PlayerExhaustEvent::CAUSE_ATTACK); - } - - return true; - default: - break; //unknown - } - - $this->inventory->sendContents($this); - return false; - }elseif($packet->trData instanceof ReleaseItemTransactionData){ - if($this->inventory->getHeldItemIndex() !== $packet->trData->getHotbarSlot()){ - $this->inventory->equipItem($packet->trData->getHotbarSlot()); - } - - try{ - switch($packet->trData->getActionType()){ - case ReleaseItemTransactionData::ACTION_RELEASE: - if($this->isUsingItem()){ - $item = $this->inventory->getItemInHand(); - if($this->hasItemCooldown($item)){ - $this->inventory->sendContents($this); - return false; - } - if($item->onReleaseUsing($this)){ - $this->resetItemCooldown($item); - $this->inventory->setItemInHand($item); - } - return true; - } - break; - default: - break; - } - }finally{ - $this->setUsingItem(false); - } - - $this->inventory->sendContents($this); - return false; - }else{ - $this->inventory->sendContents($this); - return false; - } - } - - public function handleMobEquipment(MobEquipmentPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return true; - } - - $item = $this->inventory->getItem($packet->hotbarSlot); - - if(!$item->equals($packet->item->getItemStack())){ - $this->server->getLogger()->debug("Tried to equip " . $packet->item->getItemStack() . " but have " . $item . " in target slot"); - $this->inventory->sendContents($this); - return false; - } - - $this->inventory->equipItem($packet->hotbarSlot); - - $this->setUsingItem(false); - - return true; - } - - public function handleInteract(InteractPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return true; - } - - if($packet->action !== InteractPacket::ACTION_MOUSEOVER){ - //mouseover fires when the player swaps their held itemstack in the inventory menu - $this->doCloseInventory(); - } - - $target = $this->level->getEntity($packet->target); - if($target === null){ - return false; - } - - switch($packet->action){ - case InteractPacket::ACTION_LEAVE_VEHICLE: - case InteractPacket::ACTION_MOUSEOVER: - break; //TODO: handle these - case InteractPacket::ACTION_OPEN_INVENTORY: - if($target === $this && !array_key_exists($windowId = self::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; - $pk = new ContainerOpenPacket(); - $pk->windowId = $windowId; - $pk->type = WindowTypes::INVENTORY; - $pk->x = $pk->y = $pk->z = 0; - $pk->entityUniqueId = $this->getId(); - $this->sendDataPacket($pk); - break; - } - return false; - default: - $this->server->getLogger()->debug("Unhandled/unknown interaction type " . $packet->action . " received from " . $this->getName()); - - return false; - } - - return true; - } - - public function handleBlockPickRequest(BlockPickRequestPacket $packet) : bool{ - $block = $this->level->getBlockAt($packet->blockX, $packet->blockY, $packet->blockZ); - if($block instanceof UnknownBlock){ - return true; - } - - $item = $block->getPickedItem(); - if($packet->addUserData){ - $tile = $this->getLevelNonNull()->getTile($block); - if($tile instanceof Tile){ - $nbt = $tile->getCleanedNBT(); - if($nbt instanceof CompoundTag){ - $item->setCustomBlockData($nbt); - $item->setLore(["+(DATA)"]); - } - } - } - - $ev = new PlayerBlockPickEvent($this, $block, $item); - if(!$this->isCreative(true)){ - $this->server->getLogger()->debug("Got block-pick request from " . $this->getName() . " when not in creative mode (gamemode " . $this->getGamemode() . ")"); - $ev->setCancelled(); - } - - $ev->call(); - if(!$ev->isCancelled()){ - $this->inventory->setItemInHand($ev->getResultItem()); - } - - return true; - - } - - public function handlePlayerAction(PlayerActionPacket $packet) : bool{ - if(!$this->spawned or (!$this->isAlive() and $packet->action !== PlayerActionPacket::ACTION_RESPAWN)){ - return true; - } - - $packet->entityRuntimeId = $this->id; - $pos = new Vector3($packet->x, $packet->y, $packet->z); - - switch($packet->action){ - case PlayerActionPacket::ACTION_START_BREAK: - if($pos->distanceSquared($this) > 10000){ - break; - } - - $target = $this->level->getBlock($pos); - - $ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $target, null, $packet->face, PlayerInteractEvent::LEFT_CLICK_BLOCK); - if($this->isSpectator() || $this->level->checkSpawnProtection($this, $target)){ - $ev->setCancelled(); - } - - $ev->call(); - if($ev->isCancelled()){ - $this->inventory->sendHeldItem($this); - break; - } - - $tile = $this->level->getTile($pos); - if($tile instanceof ItemFrame and $tile->hasItem()){ - if (lcg_value() <= $tile->getItemDropChance()){ - $this->level->dropItem($tile->getBlock(), $tile->getItem()); - } - $tile->setItem(null); - $tile->setItemRotation(0); - break; - } - - $block = $target->getSide($packet->face); - if($block->getId() === Block::FIRE){ - $this->level->setBlock($block, BlockFactory::get(Block::AIR)); - break; - } - - if(!$this->isCreative()){ - //TODO: improve this to take stuff like swimming, ladders, enchanted tools into account, fix wrong tool break time calculations for bad tools (pmmp/PocketMine-MP#211) - $breakTime = ceil($target->getBreakTime($this->inventory->getItemInHand()) * 20); - if($breakTime > 0){ - $this->level->broadcastLevelEvent($pos, LevelEventPacket::EVENT_BLOCK_START_BREAK, (int) (65535 / $breakTime)); - } - } - - break; - - case PlayerActionPacket::ACTION_ABORT_BREAK: - case PlayerActionPacket::ACTION_STOP_BREAK: - $this->level->broadcastLevelEvent($pos, LevelEventPacket::EVENT_BLOCK_STOP_BREAK); - break; - case PlayerActionPacket::ACTION_START_SLEEPING: - //unused - break; - case PlayerActionPacket::ACTION_STOP_SLEEPING: - $this->stopSleep(); - break; - case PlayerActionPacket::ACTION_RESPAWN: - if($this->isAlive()){ - break; - } - - $this->respawn(); - break; - case PlayerActionPacket::ACTION_JUMP: - $this->jump(); - return true; - case PlayerActionPacket::ACTION_START_SPRINT: - $this->toggleSprint(true); - return true; - case PlayerActionPacket::ACTION_STOP_SPRINT: - $this->toggleSprint(false); - return true; - case PlayerActionPacket::ACTION_START_SNEAK: - $this->toggleSneak(true); - return true; - case PlayerActionPacket::ACTION_STOP_SNEAK: - $this->toggleSneak(false); - return true; - case PlayerActionPacket::ACTION_START_GLIDE: - case PlayerActionPacket::ACTION_STOP_GLIDE: - break; //TODO - case PlayerActionPacket::ACTION_CRACK_BREAK: - $block = $this->level->getBlock($pos); - $this->level->broadcastLevelEvent($pos, LevelEventPacket::EVENT_PARTICLE_PUNCH_BLOCK, $block->getRuntimeId() | ($packet->face << 24)); - //TODO: destroy-progress level event - 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!!) - break; - case PlayerActionPacket::ACTION_INTERACT_BLOCK: //TODO: ignored (for now) - break; - case PlayerActionPacket::ACTION_CREATIVE_PLAYER_DESTROY_BLOCK: - //TODO: do we need to handle this? - break; - default: - $this->server->getLogger()->debug("Unhandled/unknown player action type " . $packet->action . " from " . $this->getName()); - return false; - } - - $this->setUsingItem(false); - - return true; - } - - public function toggleSprint(bool $sprint) : void{ - $ev = new PlayerToggleSprintEvent($this, $sprint); - $ev->call(); - if($ev->isCancelled()){ - $this->sendData($this); - }else{ - $this->setSprinting($sprint); - } - } - - public function toggleSneak(bool $sneak) : void{ - $ev = new PlayerToggleSneakEvent($this, $sneak); - $ev->call(); - if($ev->isCancelled()){ - $this->sendData($this); - }else{ - $this->setSneaking($sneak); - } - } - - public function handleAnimate(AnimatePacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return true; - } - - $ev = new PlayerAnimationEvent($this, $packet->action); - $ev->call(); - if($ev->isCancelled()){ - return true; - } - - $pk = new AnimatePacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->action = $ev->getAnimationType(); - $this->server->broadcastPacket($this->getViewers(), $pk); - - return true; - } - - public function handleRespawn(RespawnPacket $packet) : bool{ - if(!$this->isAlive() && $packet->respawnState === RespawnPacket::CLIENT_READY_TO_SPAWN){ - $this->sendRespawnPacket($this, RespawnPacket::READY_TO_SPAWN); - return true; - } - - return false; - } - - /** - * Drops an item on the ground in front of the player. Returns if the item drop was successful. - * - * @return bool if the item was dropped or if the item was null - */ - public function dropItem(Item $item) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return false; - } - - if($item->isNull()){ - $this->server->getLogger()->debug($this->getName() . " attempted to drop a null item (" . $item . ")"); - return true; - } - - $motion = $this->getDirectionVector()->multiply(0.4); - - $this->level->dropItem($this->add(0, 1.3, 0), $item, $motion, 40); - - return true; - } - - /** @var int|null */ - private $closingWindowId = null; - - /** @internal */ - public function getClosingWindowId() : ?int{ return $this->closingWindowId; } - - public function handleContainerClose(ContainerClosePacket $packet) : bool{ - if(!$this->spawned){ - return true; - } - - $this->doCloseInventory(); - - if(array_key_exists($packet->windowId, $this->openHardcodedWindows)){ - unset($this->openHardcodedWindows[$packet->windowId]); - $pk = new ContainerClosePacket(); - $pk->windowId = $packet->windowId; - $pk->server = false; - $this->sendDataPacket($pk); - return true; - } - if(isset($this->windowIndex[$packet->windowId])){ - $this->closingWindowId = $packet->windowId; - $this->removeWindow($this->windowIndex[$packet->windowId]); - $this->closingWindowId = null; - //removeWindow handles sending the appropriate - }else{ - /* - * TODO: HACK! - * If we told the client to remove a window on our own (e.g. a plugin called removeWindow()), our - * first ContainerClose tricks the client into behaving as if it itself asked for the window to be closed. - * This means that it will send us a ContainerClose of its own, which we must respond to the same way as if - * the client closed the window by itself. - * If we don't, the client will not be able to open any new windows. - */ - $pk = new ContainerClosePacket(); - $pk->windowId = $packet->windowId; - $pk->server = false; - $this->sendDataPacket($pk); - } - - return true; - } - - public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{ - if($packet->entityUniqueId !== $this->getId()){ - return false; //TODO - } - - $handled = false; - - $isFlying = $packet->getFlag(AdventureSettingsPacket::FLYING); - if($isFlying !== $this->isFlying()){ - $ev = new PlayerToggleFlightEvent($this, $isFlying); - if($isFlying and !$this->allowFlight){ - $ev->setCancelled(); - } - - $ev->call(); - if($ev->isCancelled()){ - $this->sendSettings(); - }else{ //don't use setFlying() here, to avoid feedback loops - $this->flying = $ev->isFlying(); - $this->resetFallDistance(); - } - - $handled = true; - } - - if($packet->getFlag(AdventureSettingsPacket::NO_CLIP) and !$this->allowMovementCheats and !$this->isSpectator()){ - $this->kick($this->server->getLanguage()->translateString("kick.reason.cheat", ["%ability.noclip"])); - return true; - } - - //TODO: check other changes - - return $handled; - } - - public function handleBlockEntityData(BlockActorDataPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return true; - } - $this->doCloseInventory(); - - $pos = new Vector3($packet->x, $packet->y, $packet->z); - if($pos->distanceSquared($this) > 10000 or $this->level->checkSpawnProtection($this, $pos)){ - return true; - } - - $t = $this->level->getTile($pos); - if($t instanceof Spawnable){ - $nbt = new NetworkLittleEndianNBTStream(); - $_ = 0; - $compound = $nbt->read($packet->namedtag, false, $_, 512); - - if(!($compound instanceof CompoundTag)){ - throw new \InvalidArgumentException("Expected " . CompoundTag::class . " in block entity NBT, got " . (is_object($compound) ? get_class($compound) : gettype($compound))); - } - if(!$t->updateCompoundTag($compound, $this)){ - $t->spawnTo($this); - } - } - - return true; - } - - public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{ - if($packet->gamemode !== $this->gamemode){ - //Set this back to default. TODO: handle this properly - $this->sendGamemode(); - $this->sendSettings(); - } - return true; - } - - public function handleItemFrameDropItem(ItemFrameDropItemPacket $packet) : bool{ - if(!$this->spawned or !$this->isAlive()){ - return true; - } - - $tile = $this->level->getTileAt($packet->x, $packet->y, $packet->z); - if($tile instanceof ItemFrame){ - $ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $tile->getBlock(), null, 5 - $tile->getBlock()->getDamage(), PlayerInteractEvent::LEFT_CLICK_BLOCK); - if($this->isSpectator() or $this->level->checkSpawnProtection($this, $tile)){ - $ev->setCancelled(); - } - - $ev->call(); - if($ev->isCancelled()){ - $tile->spawnTo($this); - return true; - } - - if(lcg_value() <= $tile->getItemDropChance()){ - $this->level->dropItem($tile->getBlock(), $tile->getItem()); - } - $tile->setItem(null); - $tile->setItemRotation(0); - } - - return true; - } - - public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ - if($this->resourcePacksDone){ - return false; - } - $manager = $this->server->getResourcePackManager(); - $pack = $manager->getPackById($packet->packId); - if(!($pack instanceof ResourcePack)){ - $this->close("", "disconnectionScreen.resourcePack", true); - $this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $packet->packId . ", available packs: " . implode(", ", $manager->getPackIdList())); - - return false; - } - - $pk = new ResourcePackChunkDataPacket(); - $pk->packId = $pack->getPackId(); - $pk->chunkIndex = $packet->chunkIndex; - $pk->data = $pack->getPackChunk(self::RESOURCE_PACK_CHUNK_SIZE * $packet->chunkIndex, self::RESOURCE_PACK_CHUNK_SIZE); - $pk->progress = (self::RESOURCE_PACK_CHUNK_SIZE * $packet->chunkIndex); - $this->dataPacket($pk); - return true; - } - - public function handleBookEdit(BookEditPacket $packet) : bool{ - /** @var WritableBook $oldBook */ - $oldBook = $this->inventory->getItem($packet->inventorySlot); - if($oldBook->getId() !== Item::WRITABLE_BOOK){ - return false; - } - - $newBook = clone $oldBook; - $modifiedPages = []; - - switch($packet->type){ - case BookEditPacket::TYPE_REPLACE_PAGE: - $newBook->setPageText($packet->pageNumber, $packet->text); - $modifiedPages[] = $packet->pageNumber; - break; - case BookEditPacket::TYPE_ADD_PAGE: - if(!$newBook->pageExists($packet->pageNumber)){ - //this may only come before a page which already exists - //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); - $modifiedPages[] = $packet->pageNumber; - break; - case BookEditPacket::TYPE_DELETE_PAGE: - if(!$newBook->pageExists($packet->pageNumber)){ - return false; - } - $newBook->deletePage($packet->pageNumber); - $modifiedPages[] = $packet->pageNumber; - break; - case BookEditPacket::TYPE_SWAP_PAGES: - if(!$newBook->pageExists($packet->pageNumber) or !$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)); - } - $newBook->swapPages($packet->pageNumber, $packet->secondaryPageNumber); - $modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber]; - break; - case BookEditPacket::TYPE_SIGN_BOOK: - /** @var WrittenBook $newBook */ - $newBook = Item::get(Item::WRITTEN_BOOK, 0, 1, $newBook->getNamedTag()); - $newBook->setAuthor($packet->author); - $newBook->setTitle($packet->title); - $newBook->setGeneration(WrittenBook::GENERATION_ORIGINAL); - break; - default: - return false; - } - - $event = new PlayerEditBookEvent($this, $oldBook, $newBook, $packet->type, $modifiedPages); - $event->call(); - if($event->isCancelled()){ - return true; - } - - $this->getInventory()->setItem($packet->inventorySlot, $event->getNewBook()); - - return true; - } - - /** - * Called when a packet is received from the client. This method will call DataPacketReceiveEvent. - * - * @return void - */ - public function handleDataPacket(DataPacket $packet){ - if($this->sessionAdapter !== null){ - $this->sessionAdapter->handleDataPacket($packet); - } - } - - /** - * Batch a Data packet into the channel list to send at the end of the tick - */ - public function batchDataPacket(DataPacket $packet) : bool{ - if(!$this->isConnected()){ - return false; - } - - $timings = Timings::getSendDataPacketTimings($packet); - $timings->startTiming(); - $ev = new DataPacketSendEvent($this, $packet); - $ev->call(); - if($ev->isCancelled()){ - $timings->stopTiming(); - return false; - } - - $this->batchedPackets[] = clone $packet; - $timings->stopTiming(); - return true; - } - - /** - * @return bool|int - */ - public function sendDataPacket(DataPacket $packet, bool $needACK = false, bool $immediate = false){ - if(!$this->isConnected()){ - return false; - } - - //Basic safety restriction. TODO: improve this - if(!$this->loggedIn and !$packet->canBeSentBeforeLogin()){ - throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getName() . " too early"); - } - - $timings = Timings::getSendDataPacketTimings($packet); - $timings->startTiming(); - try{ - $ev = new DataPacketSendEvent($this, $packet); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - $identifier = $this->interface->putPacket($this, $packet, $needACK, $immediate); - - if($needACK and $identifier !== null){ - $this->needACK[$identifier] = false; - return $identifier; - } - - return true; - }finally{ - $timings->stopTiming(); - } - } - - /** - * @return bool|int - */ - public function dataPacket(DataPacket $packet, bool $needACK = false){ - return $this->sendDataPacket($packet, $needACK, false); - } - - /** - * @return bool|int - */ - public function directDataPacket(DataPacket $packet, bool $needACK = false){ - return $this->sendDataPacket($packet, $needACK, true); - } - - /** - * Transfers a player to another server. - * - * @param string $address The IP address or hostname of the destination server - * @param int $port The destination port, defaults to 19132 - * @param string $message Message to show in the console when closing the player - * - * @return bool if transfer was successful. - */ - public function transfer(string $address, int $port = 19132, string $message = "transfer") : bool{ - $ev = new PlayerTransferEvent($this, $address, $port, $message); - $ev->call(); - if(!$ev->isCancelled()){ - $pk = new TransferPacket(); - $pk->address = $ev->getAddress(); - $pk->port = $ev->getPort(); - $this->directDataPacket($pk); - $this->close("", $ev->getMessage(), false); - - return true; - } - - return false; - } - - /** - * Kicks a player from the server - * - * @param TextContainer|string $quitMessage - */ - public function kick(string $reason = "", bool $isAdmin = true, $quitMessage = null) : bool{ - $ev = new PlayerKickEvent($this, $reason, $quitMessage ?? $this->getLeaveMessage()); - $ev->call(); - if(!$ev->isCancelled()){ - $reason = $ev->getReason(); - $message = $reason; - if($isAdmin){ - if(!$this->isBanned()){ - $message = "Kicked by admin." . ($reason !== "" ? " Reason: " . $reason : ""); - } - }else{ - if($reason === ""){ - $message = "disconnectionScreen.noReason"; - } - } - $this->close($ev->getQuitMessage(), $message); - - return true; - } - - return false; - } - - /** - * @deprecated - * @see Player::sendTitle() - * - * @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used. - * @param int $stay Duration in ticks to stay on screen for - * @param int $fadeOut Duration in ticks for fade-out. - * - * @return void - */ - public function addTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1){ - $this->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut); - } - - /** - * Adds a title text to the user's screen, with an optional subtitle. - * - * @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used. - * @param int $stay Duration in ticks to stay on screen for - * @param int $fadeOut Duration in ticks for fade-out. - */ - public function sendTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1) : void{ - $this->setTitleDuration($fadeIn, $stay, $fadeOut); - if($subtitle !== ""){ - $this->sendSubTitle($subtitle); - } - $this->sendTitleText($title, SetTitlePacket::TYPE_SET_TITLE); - } - - /** - * @deprecated - * @see Player::sendSubTitle() - * - * @return void - */ - public function addSubTitle(string $subtitle){ - $this->sendSubTitle($subtitle); - } - - /** - * Sets the subtitle message, without sending a title. - */ - public function sendSubTitle(string $subtitle) : void{ - $this->sendTitleText($subtitle, SetTitlePacket::TYPE_SET_SUBTITLE); - } - - /** - * @deprecated - * @see Player::sendActionBarMessage() - * - * @return void - */ - public function addActionBarMessage(string $message){ - $this->sendActionBarMessage($message); - } - - /** - * Adds small text to the user's screen. - */ - public function sendActionBarMessage(string $message) : void{ - $this->sendTitleText($message, SetTitlePacket::TYPE_SET_ACTIONBAR_MESSAGE); - } - - /** - * Removes the title from the client's screen. - * - * @return void - */ - public function removeTitles(){ - $pk = new SetTitlePacket(); - $pk->type = SetTitlePacket::TYPE_CLEAR_TITLE; - $this->dataPacket($pk); - } - - /** - * Resets the title duration settings to defaults and removes any existing titles. - * - * @return void - */ - public function resetTitles(){ - $pk = new SetTitlePacket(); - $pk->type = SetTitlePacket::TYPE_RESET_TITLE; - $this->dataPacket($pk); - } - - /** - * Sets the title duration. - * - * @param int $fadeIn Title fade-in time in ticks. - * @param int $stay Title stay time in ticks. - * @param int $fadeOut Title fade-out time in ticks. - * - * @return void - */ - public function setTitleDuration(int $fadeIn, int $stay, int $fadeOut){ - if($fadeIn >= 0 and $stay >= 0 and $fadeOut >= 0){ - $pk = new SetTitlePacket(); - $pk->type = SetTitlePacket::TYPE_SET_ANIMATION_TIMES; - $pk->fadeInTime = $fadeIn; - $pk->stayTime = $stay; - $pk->fadeOutTime = $fadeOut; - $this->dataPacket($pk); - } - } - - /** - * Internal function used for sending titles. - * - * @return void - */ - protected function sendTitleText(string $title, int $type){ - $pk = new SetTitlePacket(); - $pk->type = $type; - $pk->text = $title; - $this->dataPacket($pk); - } - - /** - * Sends a direct chat message to a player - * - * @param TextContainer|string $message - * - * @return void - */ - public function sendMessage($message){ - if($message instanceof TextContainer){ - if($message instanceof TranslationContainer){ - $this->sendTranslation($message->getText(), $message->getParameters()); - return; - } - $message = $message->getText(); - } - - $pk = new TextPacket(); - $pk->type = TextPacket::TYPE_RAW; - $pk->message = $this->server->getLanguage()->translateString($message); - $this->dataPacket($pk); - } - - /** - * @param string[] $parameters - * - * @return void - */ - public function sendTranslation(string $message, array $parameters = []){ - $pk = new TextPacket(); - if(!$this->server->isLanguageForced()){ - $pk->type = TextPacket::TYPE_TRANSLATION; - $pk->needsTranslation = true; - $pk->message = $this->server->getLanguage()->translateString($message, $parameters, "pocketmine."); - foreach($parameters as $i => $p){ - $parameters[$i] = $this->server->getLanguage()->translateString($p, [], "pocketmine."); - } - $pk->parameters = $parameters; - }else{ - $pk->type = TextPacket::TYPE_RAW; - $pk->message = $this->server->getLanguage()->translateString($message, $parameters); - } - $this->dataPacket($pk); - } - - /** - * Sends a popup message to the player - * - * TODO: add translation type popups - * - * @param string $subtitle @deprecated - * - * @return void - */ - public function sendPopup(string $message, string $subtitle = ""){ - $pk = new TextPacket(); - $pk->type = TextPacket::TYPE_POPUP; - $pk->message = $message; - $this->dataPacket($pk); - } - - /** - * @return void - */ - public function sendTip(string $message){ - $pk = new TextPacket(); - $pk->type = TextPacket::TYPE_TIP; - $pk->message = $message; - $this->dataPacket($pk); - } - - /** - * @return void - */ - public function sendWhisper(string $sender, string $message){ - $pk = new TextPacket(); - $pk->type = TextPacket::TYPE_WHISPER; - $pk->sourceName = $sender; - $pk->message = $message; - $this->dataPacket($pk); - } - - /** - * Sends a Form to the player, or queue to send it if a form is already open. - */ - public function sendForm(Form $form) : void{ - $formData = json_encode($form); - if($formData === false){ - throw new \InvalidArgumentException("Failed to encode form JSON: " . json_last_error_msg()); - } - $id = $this->formIdCounter++; - $pk = new ModalFormRequestPacket(); - $pk->formId = $id; - $pk->formData = $formData; - if($this->dataPacket($pk) !== false){ - $this->forms[$id] = $form; - } - } - - /** - * @param mixed $responseData - */ - public function onFormSubmit(int $formId, $responseData) : bool{ - if(!isset($this->forms[$formId])){ - $this->server->getLogger()->debug("Got unexpected response for form $formId"); - return false; - } - - try{ - $this->forms[$formId]->handleResponse($this, $responseData); - }catch(FormValidationException $e){ - $this->server->getLogger()->critical("Failed to validate form " . get_class($this->forms[$formId]) . ": " . $e->getMessage()); - $this->server->getLogger()->logException($e); - }finally{ - unset($this->forms[$formId]); - } - - return true; - } - - /** - * Note for plugin developers: use kick() with the isAdmin - * flag set to kick without the "Kicked by admin" part instead of this method. - * - * @param TextContainer|string $message Message to be broadcasted - * @param string $reason Reason showed in console - */ - final public function close($message = "", string $reason = "generic reason", bool $notify = true) : void{ - if($this->isConnected() and !$this->closed){ - if($notify and strlen($reason) > 0){ - $pk = new DisconnectPacket(); - $pk->message = $reason; - $this->directDataPacket($pk); - } - $this->interface->close($this, $notify ? $reason : ""); - $this->sessionAdapter = null; - - PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this); - PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); - - $this->stopSleep(); - - if($this->spawned){ - $this->doCloseInventory(); - - $ev = new PlayerQuitEvent($this, $message, $reason); - $ev->call(); - if($ev->getQuitMessage() != ""){ - $this->server->broadcastMessage($ev->getQuitMessage()); - } - - $this->save(); - } - - if($this->isValid()){ - foreach($this->usedChunks as $index => $d){ - Level::getXZ($index, $chunkX, $chunkZ); - $this->level->unregisterChunkLoader($this, $chunkX, $chunkZ); - foreach($this->level->getChunkEntities($chunkX, $chunkZ) as $entity){ - $entity->despawnFrom($this); - } - unset($this->usedChunks[$index]); - } - } - $this->usedChunks = []; - $this->loadQueue = []; - - if($this->loggedIn){ - $this->server->onPlayerLogout($this); - foreach($this->server->getOnlinePlayers() as $player){ - if(!$player->canSee($this)){ - $player->showPlayer($this); - } - } - $this->hiddenPlayers = []; - } - - $this->removeAllWindows(true); - $this->windows = []; - $this->windowIndex = []; - $this->cursorInventory = null; - $this->craftingGrid = null; - - if($this->constructed){ - parent::close(); - } - $this->spawned = false; - - if($this->loggedIn){ - $this->loggedIn = false; - $this->server->removeOnlinePlayer($this); - } - - $this->server->removePlayer($this); - - $this->server->getLogger()->info($this->getServer()->getLanguage()->translateString("pocketmine.player.logOut", [ - TextFormat::AQUA . $this->getName() . TextFormat::WHITE, - $this->ip, - $this->port, - $this->getServer()->getLanguage()->translateString($reason) - ])); - - $this->spawnPosition = null; - - if($this->perm !== null){ - $this->perm->clearPermissions(); - $this->perm = null; - } - } - } - - /** - * @return mixed[] - */ - public function __debugInfo(){ - return []; - } - - public function canSaveWithChunk() : bool{ - return false; - } - - public function setCanSaveWithChunk(bool $value) : void{ - throw new \BadMethodCallException("Players can't be saved with chunks"); - } - - /** - * Handles player data saving - * - * @throws \InvalidStateException if the player is closed - * - * @return void - */ - public function save(){ - if($this->closed){ - throw new \InvalidStateException("Tried to save closed player"); - } - - parent::saveNBT(); - - $this->namedtag->setString("LastKnownXUID", $this->xuid); - - if($this->isValid()){ - $this->namedtag->setString("Level", $this->level->getFolderName()); - } - - if($this->hasValidSpawnPosition()){ - $this->namedtag->setString("SpawnLevel", $this->spawnPosition->getLevelNonNull()->getFolderName()); - $this->namedtag->setInt("SpawnX", $this->spawnPosition->getFloorX()); - $this->namedtag->setInt("SpawnY", $this->spawnPosition->getFloorY()); - $this->namedtag->setInt("SpawnZ", $this->spawnPosition->getFloorZ()); - - if(!$this->isAlive()){ - //hack for respawn after quit - $this->namedtag->setTag(new ListTag("Pos", [ - new DoubleTag("", $this->spawnPosition->x), - new DoubleTag("", $this->spawnPosition->y), - new DoubleTag("", $this->spawnPosition->z) - ])); - } - } - - $achievements = new CompoundTag("Achievements"); - foreach($this->achievements as $achievement => $status){ - $achievements->setByte($achievement, $status ? 1 : 0); - } - $this->namedtag->setTag($achievements); - - $this->namedtag->setInt("playerGameType", $this->gamemode); - $this->namedtag->setLong("lastPlayed", (int) floor(microtime(true) * 1000)); - - if($this->username != ""){ - $this->server->saveOfflinePlayerData($this->username, $this->namedtag); - } - } - - public function kill() : void{ - if(!$this->spawned){ - return; - } - - parent::kill(); - - $this->sendRespawnPacket($this->getSpawn()); - } - - 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(); - - $ev = new PlayerDeathEvent($this, $this->getDrops(), null, $this->getXpDropAmount()); - $ev->call(); - - if(!$ev->getKeepInventory()){ - foreach($ev->getDrops() as $item){ - $this->level->dropItem($this, $item); - } - - if($this->inventory !== null){ - $this->inventory->setHeldItemIndex(0); - $this->inventory->clearAll(); - } - if($this->armorInventory !== null){ - $this->armorInventory->clearAll(); - } - } - - $this->level->dropExperience($this, $ev->getXpDropAmount()); - $this->setXpAndProgress(0, 0); - - if($ev->getDeathMessage() != ""){ - $this->server->broadcastMessage($ev->getDeathMessage()); - } - } - - protected function onDeathUpdate(int $tickDiff) : bool{ - parent::onDeathUpdate($tickDiff); - return false; //never flag players for despawn - } - - protected function respawn() : void{ - if($this->server->isHardcore()){ - $this->setBanned(true); - return; - } - - $this->actuallyRespawn(); - } - - protected function actuallyRespawn() : void{ - $ev = new PlayerRespawnEvent($this, $this->getSpawn()); - $ev->call(); - - $realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getLevelNonNull()); - $this->teleport($realSpawn); - - $this->setSprinting(false); - $this->setSneaking(false); - - $this->extinguish(); - $this->setAirSupplyTicks($this->getMaxAirSupplyTicks()); - $this->deadTicks = 0; - $this->noDamageTicks = 60; - - $this->removeAllEffects(); - $this->setHealth($this->getMaxHealth()); - - foreach($this->attributeMap->getAll() as $attr){ - $attr->resetToDefault(); - } - - $this->sendData($this); - $this->sendData($this->getViewers()); - - $this->sendSettings(); - $this->sendAllInventories(); - - $this->spawnToAll(); - $this->scheduleUpdate(); - } - - protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ - parent::applyPostDamageEffects($source); - - $this->exhaust(0.3, PlayerExhaustEvent::CAUSE_DAMAGE); - } - - public function attack(EntityDamageEvent $source) : void{ - if(!$this->isAlive()){ - return; - } - - if($this->isCreative() - and $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE - and $source->getCause() !== EntityDamageEvent::CAUSE_VOID - ){ - $source->setCancelled(); - }elseif($this->allowFlight and $source->getCause() === EntityDamageEvent::CAUSE_FALL){ - $source->setCancelled(); - } - - parent::attack($source); - } - - public function broadcastEntityEvent(int $eventId, ?int $eventData = null, ?array $players = null) : void{ - if($this->spawned and $players === null){ - $players = $this->getViewers(); - $players[] = $this; - } - parent::broadcastEntityEvent($eventId, $eventData, $players); - } - - public function getOffsetPosition(Vector3 $vector3) : Vector3{ - $result = parent::getOffsetPosition($vector3); - $result->y += 0.001; //Hack for MCPE falling underground for no good reason (TODO: find out why it's doing this) - return $result; - } - - /** - * @param Player[]|null $targets - * - * @return void - */ - public function sendPosition(Vector3 $pos, float $yaw = null, float $pitch = null, int $mode = MovePlayerPacket::MODE_NORMAL, array $targets = null){ - $yaw = $yaw ?? $this->yaw; - $pitch = $pitch ?? $this->pitch; - - $pk = new MovePlayerPacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->position = $this->getOffsetPosition($pos); - $pk->pitch = $pitch; - $pk->headYaw = $yaw; - $pk->yaw = $yaw; - $pk->mode = $mode; - $pk->onGround = $this->onGround; - - if($targets !== null){ - if(in_array($this, $targets, true)){ - $this->forceMoveSync = $pos->asVector3(); - $this->ySize = 0; - } - $this->server->broadcastPacket($targets, $pk); - }else{ - $this->forceMoveSync = $pos->asVector3(); - $this->ySize = 0; - $this->dataPacket($pk); - } - } - - /** - * {@inheritdoc} - */ - public function teleport(Vector3 $pos, float $yaw = null, float $pitch = null) : bool{ - if(parent::teleport($pos, $yaw, $pitch)){ - - $this->removeAllWindows(); - - $this->sendPosition($this, $this->yaw, $this->pitch, MovePlayerPacket::MODE_TELEPORT); - $this->sendPosition($this, $this->yaw, $this->pitch, MovePlayerPacket::MODE_TELEPORT, $this->getViewers()); - - $this->spawnToAll(); - - $this->resetFallDistance(); - $this->nextChunkOrderRun = 0; - if($this->spawnChunkLoadCount !== -1){ - $this->spawnChunkLoadCount = 0; - } - $this->stopSleep(); - - //TODO: workaround for player last pos not getting updated - //Entity::updateMovement() normally handles this, but it's overridden with an empty function in Player - $this->resetLastMovements(); - - return true; - } - - return false; - } - - /** - * @return void - */ - protected function addDefaultWindows(){ - $this->addWindow($this->getInventory(), ContainerIds::INVENTORY, true); - - $this->addWindow($this->getArmorInventory(), ContainerIds::ARMOR, true); - - $this->cursorInventory = new PlayerCursorInventory($this); - $this->addWindow($this->cursorInventory, ContainerIds::UI, true); - - $this->craftingGrid = new CraftingGrid($this, CraftingGrid::SIZE_SMALL); - - //TODO: more windows - } - - public function getCursorInventory() : PlayerCursorInventory{ - return $this->cursorInventory; - } - - public function getCraftingGrid() : CraftingGrid{ - return $this->craftingGrid; - } - - public function setCraftingGrid(CraftingGrid $grid) : void{ - $this->craftingGrid = $grid; - } - - public function doCloseInventory() : void{ - /** @var Inventory[] $inventories */ - $inventories = [$this->craftingGrid, $this->cursorInventory]; - foreach($inventories as $inventory){ - $contents = $inventory->getContents(); - if(count($contents) > 0){ - $drops = $this->inventory->addItem(...$contents); - foreach($drops as $drop){ - $this->dropItem($drop); - } - - $inventory->clearAll(); - } - } - - if($this->craftingGrid->getGridWidth() > CraftingGrid::SIZE_SMALL){ - $this->craftingGrid = new CraftingGrid($this, CraftingGrid::SIZE_SMALL); - } - } - - /** - * Returns the window ID which the inventory has for this player, or -1 if the window is not open to the player. - */ - public function getWindowId(Inventory $inventory) : int{ - return $this->windows[spl_object_hash($inventory)] ?? ContainerIds::NONE; - } - - /** - * Returns the inventory window open to the player with the specified window ID, or null if no window is open with - * that ID. - * - * @return Inventory|null - */ - public function getWindow(int $windowId){ - return $this->windowIndex[$windowId] ?? null; - } - - /** - * Opens an inventory window to the player. Returns the ID of the created window, or the existing window ID if the - * player is already viewing the specified inventory. - * - * @param int|null $forceId Forces a special ID for the window - * @param bool $isPermanent Prevents the window being removed if true. - * - * @throws \InvalidArgumentException if a forceID which is already in use is specified - * @throws \InvalidStateException if trying to add a window without forceID when no slots are free - */ - public function addWindow(Inventory $inventory, int $forceId = null, bool $isPermanent = false) : int{ - if(($id = $this->getWindowId($inventory)) !== ContainerIds::NONE){ - return $id; - } - - if($forceId === null){ - $cnt = $this->windowCnt; - do{ - $cnt = max(ContainerIds::FIRST, ($cnt + 1) % self::RESERVED_WINDOW_ID_RANGE_START); - if($cnt === $this->windowCnt){ //wraparound, no free slots - throw new \InvalidStateException("No free window IDs found"); - } - }while(isset($this->windowIndex[$cnt])); - $this->windowCnt = $cnt; - }else{ - $cnt = $forceId; - if(isset($this->windowIndex[$cnt]) or ($cnt >= self::RESERVED_WINDOW_ID_RANGE_START && $cnt <= self::RESERVED_WINDOW_ID_RANGE_END)){ - throw new \InvalidArgumentException("Requested force ID $forceId already in use"); - } - } - - $this->windowIndex[$cnt] = $inventory; - $this->windows[spl_object_hash($inventory)] = $cnt; - if($inventory->open($this)){ - if($isPermanent){ - $this->permanentWindows[$cnt] = true; - } - return $cnt; - }else{ - $this->removeWindow($inventory); - - return -1; - } - } - - /** - * Removes an inventory window from the player. - * - * @param bool $force Forces removal of permanent windows such as normal inventory, cursor - * - * @return void - * @throws \InvalidArgumentException if trying to remove a fixed inventory window without the `force` parameter as true - */ - public function removeWindow(Inventory $inventory, bool $force = false){ - $id = $this->windows[$hash = spl_object_hash($inventory)] ?? null; - - if($id !== null and !$force and isset($this->permanentWindows[$id])){ - throw new \InvalidArgumentException("Cannot remove fixed window $id (" . get_class($inventory) . ") from " . $this->getName()); - } - - if($id !== null){ - (new InventoryCloseEvent($inventory, $this))->call(); - $inventory->close($this); - unset($this->windows[$hash], $this->windowIndex[$id], $this->permanentWindows[$id]); - } - } - - /** - * Removes all inventory windows from the player. By default this WILL NOT remove permanent windows. - * - * @param bool $removePermanentWindows Whether to remove permanent windows. - * - * @return void - */ - public function removeAllWindows(bool $removePermanentWindows = false){ - foreach($this->windowIndex as $id => $window){ - if(!$removePermanentWindows and isset($this->permanentWindows[$id])){ - continue; - } - - $this->removeWindow($window, $removePermanentWindows); - } - } - - /** - * @return void - */ - protected function sendAllInventories(){ - foreach($this->windowIndex as $id => $inventory){ - $inventory->sendContents($this); - } - } - - public function setMetadata(string $metadataKey, MetadataValue $newMetadataValue){ - $this->server->getPlayerMetadata()->setMetadata($this, $metadataKey, $newMetadataValue); - } - - public function getMetadata(string $metadataKey){ - return $this->server->getPlayerMetadata()->getMetadata($this, $metadataKey); - } - - public function hasMetadata(string $metadataKey) : bool{ - return $this->server->getPlayerMetadata()->hasMetadata($this, $metadataKey); - } - - public function removeMetadata(string $metadataKey, Plugin $owningPlugin){ - $this->server->getPlayerMetadata()->removeMetadata($this, $metadataKey, $owningPlugin); - } - - public function onChunkChanged(Chunk $chunk){ - $hasSent = $this->usedChunks[$hash = Level::chunkHash($chunk->getX(), $chunk->getZ())] ?? false; - if($hasSent){ - $this->usedChunks[$hash] = false; - $this->nextChunkOrderRun = 0; - } - } - - public function onChunkLoaded(Chunk $chunk){ - - } - - public function onChunkPopulated(Chunk $chunk){ - - } - - public function onChunkUnloaded(Chunk $chunk){ - - } - - public function onBlockChanged(Vector3 $block){ - - } - - public function getLoaderId() : int{ - return $this->loaderId; - } - - public function isLoaderActive() : bool{ - return $this->isConnected(); - } -} diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php deleted file mode 100644 index f097ad2436..0000000000 --- a/src/pocketmine/Server.php +++ /dev/null @@ -1,2472 +0,0 @@ - - */ - private $uniquePlayers = []; - - /** @var QueryHandler|null */ - private $queryHandler = null; - - /** @var QueryRegenerateEvent */ - private $queryRegenerateTask; - - /** @var Config */ - private $properties; - /** @var mixed[] */ - private $propertyCache = []; - - /** @var Config */ - private $config; - - /** @var Player[] */ - private $players = []; - - /** @var Player[] */ - private $loggedInPlayers = []; - - /** @var Player[] */ - private $playerList = []; - - /** @var Level[] */ - private $levels = []; - - /** @var Level|null */ - private $levelDefault = null; - - public function getName() : string{ - return \pocketmine\NAME; - } - - public function isRunning() : bool{ - return $this->isRunning; - } - - public function getPocketMineVersion() : string{ - return \pocketmine\VERSION; - } - - public function getVersion() : string{ - return ProtocolInfo::MINECRAFT_VERSION; - } - - public function getApiVersion() : string{ - return \pocketmine\BASE_VERSION; - } - - public function getFilePath() : string{ - return \pocketmine\PATH; - } - - public function getResourcePath() : string{ - return \pocketmine\RESOURCE_PATH; - } - - public function getDataPath() : string{ - return $this->dataPath; - } - - public function getPluginPath() : string{ - return $this->pluginPath; - } - - public function getMaxPlayers() : int{ - return $this->maxPlayers; - } - - /** - * Returns whether the server requires that players be authenticated to Xbox Live. If true, connecting players who - * are not logged into Xbox Live will be disconnected. - */ - public function getOnlineMode() : bool{ - return $this->onlineMode; - } - - /** - * Alias of {@link #getOnlineMode()}. - */ - public function requiresAuthentication() : bool{ - return $this->getOnlineMode(); - } - - public function getPort() : int{ - return $this->getConfigInt("server-port", 19132); - } - - public function getViewDistance() : int{ - return max(2, $this->getConfigInt("view-distance", 8)); - } - - /** - * Returns a view distance up to the currently-allowed limit. - */ - public function getAllowedViewDistance(int $distance) : int{ - return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance()))); - } - - public function getIp() : string{ - $str = $this->getConfigString("server-ip"); - return $str !== "" ? $str : "0.0.0.0"; - } - - /** - * @return UUID - */ - public function getServerUniqueId(){ - return $this->serverID; - } - - public function getAutoSave() : bool{ - return $this->autoSave; - } - - /** - * @return void - */ - public function setAutoSave(bool $value){ - $this->autoSave = $value; - foreach($this->getLevels() as $level){ - $level->setAutoSave($this->autoSave); - } - } - - public function getLevelType() : string{ - return $this->getConfigString("level-type", "DEFAULT"); - } - - public function getGenerateStructures() : bool{ - return $this->getConfigBool("generate-structures", true); - } - - public function getGamemode() : int{ - return $this->getConfigInt("gamemode", 0) & 0b11; - } - - public function getForceGamemode() : bool{ - return $this->getConfigBool("force-gamemode", false); - } - - /** - * Returns the gamemode text name - */ - public static function getGamemodeString(int $mode) : string{ - switch($mode){ - case Player::SURVIVAL: - return "%gameMode.survival"; - case Player::CREATIVE: - return "%gameMode.creative"; - case Player::ADVENTURE: - return "%gameMode.adventure"; - case Player::SPECTATOR: - return "%gameMode.spectator"; - } - - return "UNKNOWN"; - } - - public static function getGamemodeName(int $mode) : string{ - switch($mode){ - case Player::SURVIVAL: - return "Survival"; - case Player::CREATIVE: - return "Creative"; - case Player::ADVENTURE: - return "Adventure"; - case Player::SPECTATOR: - return "Spectator"; - default: - throw new \InvalidArgumentException("Invalid gamemode $mode"); - } - } - - /** - * Parses a string and returns a gamemode integer, -1 if not found - */ - public static function getGamemodeFromString(string $str) : int{ - switch(strtolower(trim($str))){ - case (string) Player::SURVIVAL: - case "survival": - case "s": - return Player::SURVIVAL; - - case (string) Player::CREATIVE: - case "creative": - case "c": - return Player::CREATIVE; - - case (string) Player::ADVENTURE: - case "adventure": - case "a": - return Player::ADVENTURE; - - case (string) Player::SPECTATOR: - case "spectator": - case "view": - case "v": - return Player::SPECTATOR; - } - return -1; - } - - /** - * Returns Server global difficulty. Note that this may be overridden in individual Levels. - */ - public function getDifficulty() : int{ - return $this->getConfigInt("difficulty", Level::DIFFICULTY_NORMAL); - } - - public function hasWhitelist() : bool{ - return $this->getConfigBool("white-list", false); - } - - public function getSpawnRadius() : int{ - return $this->getConfigInt("spawn-protection", 16); - } - - /** - * @deprecated - */ - public function getAllowFlight() : bool{ - return true; - } - - public function isHardcore() : bool{ - return $this->getConfigBool("hardcore", false); - } - - public function getDefaultGamemode() : int{ - return $this->getConfigInt("gamemode", 0) & 0b11; - } - - public function getMotd() : string{ - return $this->getConfigString("motd", \pocketmine\NAME . " Server"); - } - - /** - * @return \ClassLoader - */ - public function getLoader(){ - return $this->autoloader; - } - - /** - * @return \AttachableThreadedLogger - */ - public function getLogger(){ - return $this->logger; - } - - /** - * @return EntityMetadataStore - */ - public function getEntityMetadata(){ - return $this->entityMetadata; - } - - /** - * @return PlayerMetadataStore - */ - public function getPlayerMetadata(){ - return $this->playerMetadata; - } - - /** - * @return LevelMetadataStore - */ - public function getLevelMetadata(){ - return $this->levelMetadata; - } - - /** - * @return AutoUpdater - */ - public function getUpdater(){ - return $this->updater; - } - - /** - * @return PluginManager - */ - public function getPluginManager(){ - return $this->pluginManager; - } - - /** - * @return CraftingManager - */ - public function getCraftingManager(){ - return $this->craftingManager; - } - - public function getResourcePackManager() : ResourcePackManager{ - return $this->resourceManager; - } - - public function getAsyncPool() : AsyncPool{ - return $this->asyncPool; - } - - public function getTick() : int{ - return $this->tickCounter; - } - - /** - * Returns the last server TPS measure - */ - public function getTicksPerSecond() : float{ - return round($this->currentTPS, 2); - } - - /** - * Returns the last server TPS average measure - */ - public function getTicksPerSecondAverage() : float{ - return round(array_sum($this->tickAverage) / count($this->tickAverage), 2); - } - - /** - * Returns the TPS usage/load in % - */ - public function getTickUsage() : float{ - return round($this->currentUse * 100, 2); - } - - /** - * Returns the TPS usage/load average in % - */ - public function getTickUsageAverage() : float{ - return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2); - } - - /** - * @return SimpleCommandMap - */ - public function getCommandMap(){ - return $this->commandMap; - } - - /** - * @return Player[] - */ - public function getLoggedInPlayers() : array{ - return $this->loggedInPlayers; - } - - /** - * @return Player[] - */ - public function getOnlinePlayers() : array{ - return $this->playerList; - } - - public function shouldSavePlayerData() : bool{ - return (bool) $this->getProperty("player.save-player-data", true); - } - - /** - * @return OfflinePlayer|Player - */ - public function getOfflinePlayer(string $name){ - $name = strtolower($name); - $result = $this->getPlayerExact($name); - - if($result === null){ - $result = new OfflinePlayer($this, $name); - } - - return $result; - } - - private function getPlayerDataPath(string $username) : string{ - return $this->getDataPath() . '/players/' . strtolower($username) . '.dat'; - } - - /** - * Returns whether the server has stored any saved data for this player. - */ - public function hasOfflinePlayerData(string $name) : bool{ - return file_exists($this->getPlayerDataPath($name)); - } - - public function getOfflinePlayerData(string $name) : CompoundTag{ - $name = strtolower($name); - $path = $this->getPlayerDataPath($name); - if($this->shouldSavePlayerData()){ - if(file_exists($path)){ - try{ - $nbt = new BigEndianNBTStream(); - $compound = $nbt->readCompressed(file_get_contents($path)); - if(!($compound instanceof CompoundTag)){ - throw new \RuntimeException("Invalid data found in \"$name.dat\", expected " . CompoundTag::class . ", got " . (is_object($compound) ? get_class($compound) : gettype($compound))); - } - - return $compound; - }catch(\Throwable $e){ //zlib decode error / corrupt data - rename($path, $path . '.bak'); - $this->logger->notice($this->getLanguage()->translateString("pocketmine.data.playerCorrupted", [$name])); - } - }else{ - $this->logger->notice($this->getLanguage()->translateString("pocketmine.data.playerNotFound", [$name])); - } - } - $spawn = $this->getDefaultLevel()->getSafeSpawn(); - $currentTimeMillis = (int) (microtime(true) * 1000); - - $nbt = new CompoundTag("", [ - new LongTag("firstPlayed", $currentTimeMillis), - new LongTag("lastPlayed", $currentTimeMillis), - new ListTag("Pos", [ - new DoubleTag("", $spawn->x), - new DoubleTag("", $spawn->y), - new DoubleTag("", $spawn->z) - ], NBT::TAG_Double), - new StringTag("Level", $this->getDefaultLevel()->getFolderName()), - //new StringTag("SpawnLevel", $this->getDefaultLevel()->getFolderName()), - //new IntTag("SpawnX", $spawn->getFloorX()), - //new IntTag("SpawnY", $spawn->getFloorY()), - //new IntTag("SpawnZ", $spawn->getFloorZ()), - //new ByteTag("SpawnForced", 1), //TODO - new ListTag("Inventory", [], NBT::TAG_Compound), - new ListTag("EnderChestInventory", [], NBT::TAG_Compound), - new CompoundTag("Achievements", []), - new IntTag("playerGameType", $this->getGamemode()), - new ListTag("Motion", [ - new DoubleTag("", 0.0), - new DoubleTag("", 0.0), - new DoubleTag("", 0.0) - ], NBT::TAG_Double), - new ListTag("Rotation", [ - new FloatTag("", 0.0), - new FloatTag("", 0.0) - ], NBT::TAG_Float), - new FloatTag("FallDistance", 0.0), - new ShortTag("Fire", 0), - new ShortTag("Air", 300), - new ByteTag("OnGround", 1), - new ByteTag("Invulnerable", 0), - new StringTag("NameTag", $name) - ]); - - return $nbt; - - } - - /** - * @return void - */ - public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag){ - $ev = new PlayerDataSaveEvent($nbtTag, $name); - $ev->setCancelled(!$this->shouldSavePlayerData()); - - $ev->call(); - - if(!$ev->isCancelled()){ - $nbt = new BigEndianNBTStream(); - try{ - file_put_contents($this->getPlayerDataPath($name), $nbt->writeCompressed($ev->getSaveData())); - }catch(\Throwable $e){ - $this->logger->critical($this->getLanguage()->translateString("pocketmine.data.saveError", [$name, $e->getMessage()])); - $this->logger->logException($e); - } - } - } - - /** - * Returns an online player whose name begins with or equals the given string (case insensitive). - * The closest match will be returned, or null if there are no online matches. - * - * @see Server::getPlayerExact() - * - * @return Player|null - */ - public function getPlayer(string $name){ - $found = null; - $name = strtolower($name); - $delta = PHP_INT_MAX; - foreach($this->getOnlinePlayers() as $player){ - if(stripos($player->getName(), $name) === 0){ - $curDelta = strlen($player->getName()) - strlen($name); - if($curDelta < $delta){ - $found = $player; - $delta = $curDelta; - } - if($curDelta === 0){ - break; - } - } - } - - return $found; - } - - /** - * Returns an online player with the given name (case insensitive), or null if not found. - * - * @return Player|null - */ - public function getPlayerExact(string $name){ - $name = strtolower($name); - foreach($this->getOnlinePlayers() as $player){ - if($player->getLowerCaseName() === $name){ - return $player; - } - } - - return null; - } - - /** - * Returns a list of online players whose names contain with the given string (case insensitive). - * If an exact match is found, only that match is returned. - * - * @return Player[] - */ - public function matchPlayer(string $partialName) : array{ - $partialName = strtolower($partialName); - $matchedPlayers = []; - foreach($this->getOnlinePlayers() as $player){ - if($player->getLowerCaseName() === $partialName){ - $matchedPlayers = [$player]; - break; - }elseif(stripos($player->getName(), $partialName) !== false){ - $matchedPlayers[] = $player; - } - } - - return $matchedPlayers; - } - - /** - * Returns the player online with the specified raw UUID, or null if not found - */ - public function getPlayerByRawUUID(string $rawUUID) : ?Player{ - return $this->playerList[$rawUUID] ?? null; - } - - /** - * Returns the player online with a UUID equivalent to the specified UUID object, or null if not found - */ - public function getPlayerByUUID(UUID $uuid) : ?Player{ - return $this->getPlayerByRawUUID($uuid->toBinary()); - } - - /** - * @return Level[] - */ - public function getLevels() : array{ - return $this->levels; - } - - public function getDefaultLevel() : ?Level{ - return $this->levelDefault; - } - - /** - * Sets the default level to a different level - * This won't change the level-name property, - * it only affects the server on runtime - */ - public function setDefaultLevel(?Level $level) : void{ - if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){ - $this->levelDefault = $level; - } - } - - public function isLevelLoaded(string $name) : bool{ - return $this->getLevelByName($name) instanceof Level; - } - - public function getLevel(int $levelId) : ?Level{ - return $this->levels[$levelId] ?? null; - } - - /** - * NOTE: This matches levels based on the FOLDER name, NOT the display name. - */ - public function getLevelByName(string $name) : ?Level{ - foreach($this->getLevels() as $level){ - if($level->getFolderName() === $name){ - return $level; - } - } - - return null; - } - - /** - * @throws \InvalidStateException - */ - public function unloadLevel(Level $level, bool $forceUnload = false) : bool{ - if($level === $this->getDefaultLevel() and !$forceUnload){ - throw new \InvalidStateException("The default world cannot be unloaded while running, please switch worlds."); - } - - return $level->unload($forceUnload); - } - - /** - * @internal - */ - public function removeLevel(Level $level) : void{ - unset($this->levels[$level->getId()]); - } - - /** - * Loads a level from the data directory - * - * @throws LevelException - */ - public function loadLevel(string $name) : bool{ - if(trim($name) === ""){ - throw new LevelException("Invalid empty world name"); - } - if($this->isLevelLoaded($name)){ - return true; - }elseif(!$this->isLevelGenerated($name)){ - $this->logger->notice($this->getLanguage()->translateString("pocketmine.level.notFound", [$name])); - - return false; - } - - $path = $this->getDataPath() . "worlds/" . $name . "/"; - - $providerClass = LevelProviderManager::getProvider($path); - - if($providerClass === null){ - $this->logger->error($this->getLanguage()->translateString("pocketmine.level.loadError", [$name, "Cannot identify format of world"])); - - return false; - } - - try{ - /** - * @var LevelProvider $provider - * @see LevelProvider::__construct() - */ - $provider = new $providerClass($path); - }catch(LevelException $e){ - $this->logger->error($this->getLanguage()->translateString("pocketmine.level.loadError", [$name, $e->getMessage()])); - return false; - } - try{ - GeneratorManager::getGenerator($provider->getGenerator(), true); - }catch(\InvalidArgumentException $e){ - $this->logger->error($this->getLanguage()->translateString("pocketmine.level.loadError", [$name, "Unknown generator \"" . $provider->getGenerator() . "\""])); - return false; - } - - $level = new Level($this, $name, $provider); - - $this->levels[$level->getId()] = $level; - - (new LevelLoadEvent($level))->call(); - - return true; - } - - /** - * Generates a new level if it does not exist - * - * @param string|null $generator Class name that extends pocketmine\level\generator\Generator - * @phpstan-param class-string $generator - * @phpstan-param array $options - */ - public function generateLevel(string $name, int $seed = null, $generator = null, array $options = []) : bool{ - if(trim($name) === "" or $this->isLevelGenerated($name)){ - return false; - } - - $seed = $seed ?? random_int(INT32_MIN, INT32_MAX); - - if(!isset($options["preset"])){ - $options["preset"] = $this->getConfigString("generator-settings", ""); - } - - if(!($generator !== null and class_exists($generator, true) and is_subclass_of($generator, Generator::class))){ - $generator = GeneratorManager::getGenerator($this->getLevelType()); - } - - if(($providerClass = LevelProviderManager::getProviderByName($this->getProperty("level-settings.default-format", "pmanvil"))) === null){ - $providerClass = LevelProviderManager::getProviderByName("pmanvil"); - if($providerClass === null){ - throw new \InvalidStateException("Default world provider has not been registered"); - } - } - - $path = $this->getDataPath() . "worlds/" . $name . "/"; - /** @var LevelProvider $providerClass */ - $providerClass::generate($path, $name, $seed, $generator, $options); - - /** @see LevelProvider::__construct() */ - $level = new Level($this, $name, new $providerClass($path)); - $this->levels[$level->getId()] = $level; - - (new LevelInitEvent($level))->call(); - - (new LevelLoadEvent($level))->call(); - - $this->getLogger()->notice($this->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name])); - - $spawnLocation = $level->getSpawnLocation(); - $centerX = $spawnLocation->getFloorX() >> 4; - $centerZ = $spawnLocation->getFloorZ() >> 4; - - $order = []; - - for($X = -3; $X <= 3; ++$X){ - for($Z = -3; $Z <= 3; ++$Z){ - $distance = $X ** 2 + $Z ** 2; - $chunkX = $X + $centerX; - $chunkZ = $Z + $centerZ; - $index = Level::chunkHash($chunkX, $chunkZ); - $order[$index] = $distance; - } - } - - asort($order); - - foreach($order as $index => $distance){ - Level::getXZ($index, $chunkX, $chunkZ); - $level->populateChunk($chunkX, $chunkZ, true); - } - - return true; - } - - public function isLevelGenerated(string $name) : bool{ - if(trim($name) === ""){ - return false; - } - $path = $this->getDataPath() . "worlds/" . $name . "/"; - if(!($this->getLevelByName($name) instanceof Level)){ - return is_dir($path) and count(array_filter(scandir($path, SCANDIR_SORT_NONE), function(string $v) : bool{ - return $v !== ".." and $v !== "."; - })) > 0; - } - - return true; - } - - /** - * Searches all levels for the entity with the specified ID. - * Useful for tracking entities across multiple worlds without needing strong references. - * - * @param Level|null $expectedLevel @deprecated Level to look in first for the target - * - * @return Entity|null - */ - public function findEntity(int $entityId, Level $expectedLevel = null){ - foreach($this->levels as $level){ - assert(!$level->isClosed()); - if(($entity = $level->getEntity($entityId)) instanceof Entity){ - return $entity; - } - } - - return null; - } - - /** - * @param mixed $defaultValue - * - * @return mixed - */ - public function getProperty(string $variable, $defaultValue = null){ - if(!array_key_exists($variable, $this->propertyCache)){ - $v = getopt("", ["$variable::"]); - if(isset($v[$variable])){ - $this->propertyCache[$variable] = $v[$variable]; - }else{ - $this->propertyCache[$variable] = $this->config->getNested($variable); - } - } - - return $this->propertyCache[$variable] ?? $defaultValue; - } - - public function getConfigString(string $variable, string $defaultValue = "") : string{ - $v = getopt("", ["$variable::"]); - if(isset($v[$variable])){ - return (string) $v[$variable]; - } - - return $this->properties->exists($variable) ? (string) $this->properties->get($variable) : $defaultValue; - } - - /** - * @return void - */ - public function setConfigString(string $variable, string $value){ - $this->properties->set($variable, $value); - } - - public function getConfigInt(string $variable, int $defaultValue = 0) : int{ - $v = getopt("", ["$variable::"]); - if(isset($v[$variable])){ - return (int) $v[$variable]; - } - - return $this->properties->exists($variable) ? (int) $this->properties->get($variable) : $defaultValue; - } - - /** - * @return void - */ - public function setConfigInt(string $variable, int $value){ - $this->properties->set($variable, $value); - } - - public function getConfigBool(string $variable, bool $defaultValue = false) : bool{ - $v = getopt("", ["$variable::"]); - if(isset($v[$variable])){ - $value = $v[$variable]; - }else{ - $value = $this->properties->exists($variable) ? $this->properties->get($variable) : $defaultValue; - } - - if(is_bool($value)){ - return $value; - } - switch(strtolower($value)){ - case "on": - case "true": - case "1": - case "yes": - return true; - } - - return false; - } - - /** - * @return void - */ - public function setConfigBool(string $variable, bool $value){ - $this->properties->set($variable, $value ? "1" : "0"); - } - - /** - * @return PluginIdentifiableCommand|null - */ - public function getPluginCommand(string $name){ - if(($command = $this->commandMap->getCommand($name)) instanceof PluginIdentifiableCommand){ - return $command; - }else{ - return null; - } - } - - /** - * @return BanList - */ - public function getNameBans(){ - return $this->banByName; - } - - /** - * @return BanList - */ - public function getIPBans(){ - return $this->banByIP; - } - - /** - * @return void - */ - public function addOp(string $name){ - $this->operators->set(strtolower($name), true); - - if(($player = $this->getPlayerExact($name)) !== null){ - $player->recalculatePermissions(); - } - $this->operators->save(); - } - - /** - * @return void - */ - public function removeOp(string $name){ - $this->operators->remove(strtolower($name)); - - if(($player = $this->getPlayerExact($name)) !== null){ - $player->recalculatePermissions(); - } - $this->operators->save(); - } - - /** - * @return void - */ - public function addWhitelist(string $name){ - $this->whitelist->set(strtolower($name), true); - $this->whitelist->save(); - } - - /** - * @return void - */ - public function removeWhitelist(string $name){ - $this->whitelist->remove(strtolower($name)); - $this->whitelist->save(); - } - - public function isWhitelisted(string $name) : bool{ - return !$this->hasWhitelist() or $this->operators->exists($name, true) or $this->whitelist->exists($name, true); - } - - public function isOp(string $name) : bool{ - return $this->operators->exists($name, true); - } - - /** - * @return Config - */ - public function getWhitelisted(){ - return $this->whitelist; - } - - /** - * @return Config - */ - public function getOps(){ - return $this->operators; - } - - /** - * @return void - */ - public function reloadWhitelist(){ - $this->whitelist->reload(); - } - - /** - * @return string[][] - */ - public function getCommandAliases() : array{ - $section = $this->getProperty("aliases"); - $result = []; - if(is_array($section)){ - foreach($section as $key => $value){ - $commands = []; - if(is_array($value)){ - $commands = $value; - }else{ - $commands[] = (string) $value; - } - - $result[$key] = $commands; - } - } - - return $result; - } - - public static function getInstance() : Server{ - if(self::$instance === null){ - throw new \RuntimeException("Attempt to retrieve Server instance outside server thread"); - } - return self::$instance; - } - - /** - * @return void - */ - public static function microSleep(int $microseconds){ - if(self::$sleeper === null){ - self::$sleeper = new \Threaded(); - } - self::$sleeper->synchronized(function(int $ms) : void{ - Server::$sleeper->wait($ms); - }, $microseconds); - } - - public function __construct(\ClassLoader $autoloader, \AttachableThreadedLogger $logger, string $dataPath, string $pluginPath){ - if(self::$instance !== null){ - throw new \InvalidStateException("Only one server instance can exist at once"); - } - self::$instance = $this; - $this->tickSleeper = new SleeperHandler(); - $this->autoloader = $autoloader; - $this->logger = $logger; - - try{ - if(!file_exists($dataPath . "worlds/")){ - mkdir($dataPath . "worlds/", 0777); - } - - if(!file_exists($dataPath . "players/")){ - mkdir($dataPath . "players/", 0777); - } - - if(!file_exists($pluginPath)){ - mkdir($pluginPath, 0777); - } - - $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR; - $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR; - - $this->logger->info("Loading pocketmine.yml..."); - if(!file_exists($this->dataPath . "pocketmine.yml")){ - $content = file_get_contents(\pocketmine\RESOURCE_PATH . "pocketmine.yml"); - if(\pocketmine\IS_DEVELOPMENT_BUILD){ - $content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content); - } - @file_put_contents($this->dataPath . "pocketmine.yml", $content); - } - $this->config = new Config($this->dataPath . "pocketmine.yml", Config::YAML, []); - - $this->logger->info("Loading server properties..."); - $this->properties = new Config($this->dataPath . "server.properties", Config::PROPERTIES, [ - "motd" => \pocketmine\NAME . " Server", - "server-port" => 19132, - "white-list" => false, - "announce-player-achievements" => true, - "spawn-protection" => 16, - "max-players" => 20, - "gamemode" => 0, - "force-gamemode" => false, - "hardcore" => false, - "pvp" => true, - "difficulty" => Level::DIFFICULTY_NORMAL, - "generator-settings" => "", - "level-name" => "world", - "level-seed" => "", - "level-type" => "DEFAULT", - "enable-query" => true, - "enable-rcon" => false, - "rcon.password" => substr(base64_encode(random_bytes(20)), 3, 10), - "auto-save" => true, - "view-distance" => 8, - "xbox-auth" => true, - "language" => "eng" - ]); - - define('pocketmine\DEBUG', (int) $this->getProperty("debug.level", 1)); - - $this->forceLanguage = (bool) $this->getProperty("settings.force-language", false); - $this->baseLang = new BaseLang($this->getConfigString("language", $this->getProperty("settings.language", BaseLang::FALLBACK_LANGUAGE))); - $this->logger->info($this->getLanguage()->translateString("language.selected", [$this->getLanguage()->getName(), $this->getLanguage()->getLang()])); - - if(\pocketmine\IS_DEVELOPMENT_BUILD and !((bool) $this->getProperty("settings.enable-dev-builds", false))){ - $this->logger->emergency($this->baseLang->translateString("pocketmine.server.devBuild.error1", [\pocketmine\NAME])); - $this->logger->emergency($this->baseLang->translateString("pocketmine.server.devBuild.error2")); - $this->logger->emergency($this->baseLang->translateString("pocketmine.server.devBuild.error3")); - $this->logger->emergency($this->baseLang->translateString("pocketmine.server.devBuild.error4", ["settings.enable-dev-builds"])); - $this->logger->emergency($this->baseLang->translateString("pocketmine.server.devBuild.error5", ["https://github.com/pmmp/PocketMine-MP/releases"])); - $this->forceShutdown(); - return; - } - - if($this->logger instanceof MainLogger){ - $this->logger->setLogDebug(\pocketmine\DEBUG > 1); - } - - $this->memoryManager = new MemoryManager($this); - - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.start", [TextFormat::AQUA . $this->getVersion() . TextFormat::RESET])); - - if(($poolSize = $this->getProperty("settings.async-workers", "auto")) === "auto"){ - $poolSize = 2; - $processors = Utils::getCoreCount() - 2; - - if($processors > 0){ - $poolSize = max(1, $processors); - } - }else{ - $poolSize = max(1, (int) $poolSize); - } - - $this->asyncPool = new AsyncPool($this, $poolSize, max(-1, (int) $this->getProperty("memory.async-worker-hard-limit", 256)), $this->autoloader, $this->logger); - - if($this->getProperty("network.batch-threshold", 256) >= 0){ - Network::$BATCH_THRESHOLD = (int) $this->getProperty("network.batch-threshold", 256); - }else{ - Network::$BATCH_THRESHOLD = -1; - } - - $this->networkCompressionLevel = (int) $this->getProperty("network.compression-level", 6); - if($this->networkCompressionLevel < 1 or $this->networkCompressionLevel > 9){ - $this->logger->warning("Invalid network compression level $this->networkCompressionLevel set, setting to default 6"); - $this->networkCompressionLevel = 6; - } - $this->networkCompressionAsync = (bool) $this->getProperty("network.async-compression", true); - - $this->doTitleTick = ((bool) $this->getProperty("console.title-tick", true)) && Terminal::hasFormattingCodes(); - - $consoleSender = new ConsoleCommandSender(); - PermissionManager::getInstance()->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $consoleSender); - - $consoleNotifier = new SleeperNotifier(); - $this->console = new CommandReader($consoleNotifier); - $this->tickSleeper->addNotifier($consoleNotifier, function() use ($consoleSender) : void{ - Timings::$serverCommandTimer->startTiming(); - while(($line = $this->console->getLine()) !== null){ - $ev = new ServerCommandEvent($consoleSender, $line); - $ev->call(); - if(!$ev->isCancelled()){ - $this->dispatchCommand($ev->getSender(), $ev->getCommand()); - } - } - Timings::$serverCommandTimer->stopTiming(); - }); - $this->console->start(PTHREADS_INHERIT_NONE); - - if($this->getConfigBool("enable-rcon", false)){ - try{ - $this->rcon = new RCON( - $this, - $this->getConfigString("rcon.password", ""), - $this->getConfigInt("rcon.port", $this->getPort()), - $this->getIp(), - $this->getConfigInt("rcon.max-clients", 50) - ); - }catch(\Exception $e){ - $this->getLogger()->critical("RCON can't be started: " . $e->getMessage()); - } - } - - $this->entityMetadata = new EntityMetadataStore(); - $this->playerMetadata = new PlayerMetadataStore(); - $this->levelMetadata = new LevelMetadataStore(); - - $this->operators = new Config($this->dataPath . "ops.txt", Config::ENUM); - $this->whitelist = new Config($this->dataPath . "white-list.txt", Config::ENUM); - if(file_exists($this->dataPath . "banned.txt") and !file_exists($this->dataPath . "banned-players.txt")){ - @rename($this->dataPath . "banned.txt", $this->dataPath . "banned-players.txt"); - } - @touch($this->dataPath . "banned-players.txt"); - $this->banByName = new BanList($this->dataPath . "banned-players.txt"); - $this->banByName->load(); - @touch($this->dataPath . "banned-ips.txt"); - $this->banByIP = new BanList($this->dataPath . "banned-ips.txt"); - $this->banByIP->load(); - - $this->maxPlayers = $this->getConfigInt("max-players", 20); - $this->setAutoSave($this->getConfigBool("auto-save", true)); - - $this->onlineMode = $this->getConfigBool("xbox-auth", true); - if($this->onlineMode){ - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.auth.enabled")); - }else{ - $this->logger->warning($this->getLanguage()->translateString("pocketmine.server.auth.disabled")); - $this->logger->warning($this->getLanguage()->translateString("pocketmine.server.authWarning")); - $this->logger->warning($this->getLanguage()->translateString("pocketmine.server.authProperty.disabled")); - } - - if($this->getConfigBool("hardcore", false) and $this->getDifficulty() < Level::DIFFICULTY_HARD){ - $this->setConfigInt("difficulty", Level::DIFFICULTY_HARD); - } - - if(\pocketmine\DEBUG >= 0){ - @cli_set_process_title($this->getName() . " " . $this->getPocketMineVersion()); - } - - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.networkStart", [$this->getIp(), $this->getPort()])); - define("BOOTUP_RANDOM", random_bytes(16)); - $this->serverID = Utils::getMachineUniqueId($this->getIp() . $this->getPort()); - - $this->getLogger()->debug("Server unique id: " . $this->getServerUniqueId()); - $this->getLogger()->debug("Machine unique id: " . Utils::getMachineUniqueId()); - - $this->network = new Network($this); - $this->network->setName($this->getMotd()); - - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.info", [ - $this->getName(), - (\pocketmine\IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : "") . $this->getPocketMineVersion() . TextFormat::RESET - ])); - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.license", [$this->getName()])); - - Timings::init(); - TimingsHandler::setEnabled((bool) $this->getProperty("settings.enable-profiling", false)); - - $this->commandMap = new SimpleCommandMap($this); - - Entity::init(); - Tile::init(); - BlockFactory::init(); - Enchantment::init(); - ItemFactory::init(); - Item::initCreativeItems(); - Biome::init(); - - LevelProviderManager::init(); - if(extension_loaded("leveldb")){ - $this->logger->debug($this->getLanguage()->translateString("pocketmine.debug.enable")); - } - GeneratorManager::registerDefaultGenerators(); - - $this->craftingManager = new CraftingManager(); - - $this->resourceManager = new ResourcePackManager($this->getDataPath() . "resource_packs" . DIRECTORY_SEPARATOR, $this->logger); - - $this->pluginManager = new PluginManager($this, $this->commandMap, ((bool) $this->getProperty("plugins.legacy-data-dir", true)) ? null : $this->getDataPath() . "plugin_data" . DIRECTORY_SEPARATOR); - $this->profilingTickRate = (float) $this->getProperty("settings.profile-report-trigger", 20); - $this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader)); - $this->pluginManager->registerInterface(new ScriptPluginLoader()); - - register_shutdown_function([$this, "crashDump"]); - - $this->queryRegenerateTask = new QueryRegenerateEvent($this); - - $this->updater = new AutoUpdater($this, $this->getProperty("auto-updater.host", "update.pmmp.io")); - - $this->pluginManager->loadPlugins($this->pluginPath); - $this->enablePlugins(PluginLoadOrder::STARTUP); - - $this->network->registerInterface(new RakLibInterface($this)); - - foreach((array) $this->getProperty("worlds", []) as $name => $options){ - if($options === null){ - $options = []; - }elseif(!is_array($options)){ - continue; - } - if(!$this->loadLevel($name)){ - if(isset($options["generator"])){ - $generatorOptions = explode(":", $options["generator"]); - $generator = GeneratorManager::getGenerator(array_shift($generatorOptions)); - if(count($generatorOptions) > 0){ - $options["preset"] = implode(":", $generatorOptions); - } - }else{ - $generator = GeneratorManager::getGenerator("default"); - } - - $this->generateLevel($name, Generator::convertSeed((string) ($options["seed"] ?? "")), $generator, $options); - } - } - - if($this->getDefaultLevel() === null){ - $default = $this->getConfigString("level-name", "world"); - if(trim($default) == ""){ - $this->getLogger()->warning("level-name cannot be null, using default"); - $default = "world"; - $this->setConfigString("level-name", "world"); - } - if(!$this->loadLevel($default)){ - $this->generateLevel($default, Generator::convertSeed($this->getConfigString("level-seed"))); - } - - $this->setDefaultLevel($this->getLevelByName($default)); - } - - if($this->properties->hasChanged()){ - $this->properties->save(); - } - - if(!($this->getDefaultLevel() instanceof Level)){ - $this->getLogger()->emergency($this->getLanguage()->translateString("pocketmine.level.defaultError")); - $this->forceShutdown(); - - return; - } - - if($this->getProperty("ticks-per.autosave", 6000) > 0){ - $this->autoSaveTicks = (int) $this->getProperty("ticks-per.autosave", 6000); - } - - $this->enablePlugins(PluginLoadOrder::POSTWORLD); - - $this->start(); - }catch(\Throwable $e){ - $this->exceptionHandler($e); - } - } - - /** - * @param TextContainer|string $message - * @param CommandSender[]|null $recipients - */ - public function broadcastMessage($message, array $recipients = null) : int{ - if(!is_array($recipients)){ - return $this->broadcast($message, self::BROADCAST_CHANNEL_USERS); - } - - foreach($recipients as $recipient){ - $recipient->sendMessage($message); - } - - return count($recipients); - } - - /** - * @param Player[]|null $recipients - */ - public function broadcastTip(string $tip, array $recipients = null) : int{ - if(!is_array($recipients)){ - /** @var Player[] $recipients */ - $recipients = []; - foreach(PermissionManager::getInstance()->getPermissionSubscriptions(self::BROADCAST_CHANNEL_USERS) as $permissible){ - if($permissible instanceof Player and $permissible->hasPermission(self::BROADCAST_CHANNEL_USERS)){ - $recipients[spl_object_hash($permissible)] = $permissible; // do not send messages directly, or some might be repeated - } - } - } - - foreach($recipients as $recipient){ - $recipient->sendTip($tip); - } - - return count($recipients); - } - - /** - * @param Player[]|null $recipients - */ - public function broadcastPopup(string $popup, array $recipients = null) : int{ - if(!is_array($recipients)){ - /** @var Player[] $recipients */ - $recipients = []; - - foreach(PermissionManager::getInstance()->getPermissionSubscriptions(self::BROADCAST_CHANNEL_USERS) as $permissible){ - if($permissible instanceof Player and $permissible->hasPermission(self::BROADCAST_CHANNEL_USERS)){ - $recipients[spl_object_hash($permissible)] = $permissible; // do not send messages directly, or some might be repeated - } - } - } - - foreach($recipients as $recipient){ - $recipient->sendPopup($popup); - } - - return count($recipients); - } - - /** - * @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used. - * @param int $stay Duration in ticks to stay on screen for - * @param int $fadeOut Duration in ticks for fade-out. - * @param Player[]|null $recipients - */ - public function broadcastTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1, array $recipients = null) : int{ - if(!is_array($recipients)){ - /** @var Player[] $recipients */ - $recipients = []; - - foreach(PermissionManager::getInstance()->getPermissionSubscriptions(self::BROADCAST_CHANNEL_USERS) as $permissible){ - if($permissible instanceof Player and $permissible->hasPermission(self::BROADCAST_CHANNEL_USERS)){ - $recipients[spl_object_hash($permissible)] = $permissible; // do not send messages directly, or some might be repeated - } - } - } - - foreach($recipients as $recipient){ - $recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut); - } - - return count($recipients); - } - - /** - * @param TextContainer|string $message - */ - public function broadcast($message, string $permissions) : int{ - /** @var CommandSender[] $recipients */ - $recipients = []; - foreach(explode(";", $permissions) as $permission){ - foreach(PermissionManager::getInstance()->getPermissionSubscriptions($permission) as $permissible){ - if($permissible instanceof CommandSender and $permissible->hasPermission($permission)){ - $recipients[spl_object_hash($permissible)] = $permissible; // do not send messages directly, or some might be repeated - } - } - } - - foreach($recipients as $recipient){ - $recipient->sendMessage($message); - } - - return count($recipients); - } - - /** - * Broadcasts a Minecraft packet to a list of players - * - * @param Player[] $players - * - * @return void - */ - public function broadcastPacket(array $players, DataPacket $packet){ - $packet->encode(); - $this->batchPackets($players, [$packet], false); - } - - /** - * Broadcasts a list of packets in a batch to a list of players - * - * @param Player[] $players - * @param DataPacket[] $packets - * - * @return void - */ - public function batchPackets(array $players, array $packets, bool $forceSync = false, bool $immediate = false){ - if(count($packets) === 0){ - throw new \InvalidArgumentException("Cannot send empty batch"); - } - Timings::$playerNetworkTimer->startTiming(); - - $targets = array_filter($players, function(Player $player) : bool{ return $player->isConnected(); }); - - if(count($targets) > 0){ - $pk = new BatchPacket(); - - foreach($packets as $p){ - $pk->addPacket($p); - } - - if(Network::$BATCH_THRESHOLD >= 0 and strlen($pk->payload) >= Network::$BATCH_THRESHOLD){ - $pk->setCompressionLevel($this->networkCompressionLevel); - }else{ - $pk->setCompressionLevel(0); //Do not compress packets under the threshold - $forceSync = true; - } - - if(!$forceSync and !$immediate and $this->networkCompressionAsync){ - $task = new CompressBatchedTask($pk, $targets); - $this->asyncPool->submitTask($task); - }else{ - $this->broadcastPacketsCallback($pk, $targets, $immediate); - } - } - - Timings::$playerNetworkTimer->stopTiming(); - } - - /** - * @param Player[] $players - * - * @return void - */ - public function broadcastPacketsCallback(BatchPacket $pk, array $players, bool $immediate = false){ - if(!$pk->isEncoded){ - $pk->encode(); - } - - foreach($players as $i){ - $i->sendDataPacket($pk, false, $immediate); - } - } - - /** - * @return void - */ - public function enablePlugins(int $type){ - foreach($this->pluginManager->getPlugins() as $plugin){ - if(!$plugin->isEnabled() and $plugin->getDescription()->getOrder() === $type){ - $this->enablePlugin($plugin); - } - } - - if($type === PluginLoadOrder::POSTWORLD){ - $this->commandMap->registerServerAliases(); - DefaultPermissions::registerCorePermissions(); - } - } - - /** - * @return void - */ - public function enablePlugin(Plugin $plugin){ - $this->pluginManager->enablePlugin($plugin); - } - - /** - * @return void - */ - public function disablePlugins(){ - $this->pluginManager->disablePlugins(); - } - - /** - * Executes a command from a CommandSender - */ - public function dispatchCommand(CommandSender $sender, string $commandLine, bool $internal = false) : bool{ - if(!$internal){ - $ev = new CommandEvent($sender, $commandLine); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - $commandLine = $ev->getCommand(); - } - - if($this->commandMap->dispatch($sender, $commandLine)){ - return true; - } - - $sender->sendMessage($this->getLanguage()->translateString(TextFormat::RED . "%commands.generic.notFound")); - - return false; - } - - /** - * @return void - */ - public function reload(){ - $this->logger->info("Saving worlds..."); - - foreach($this->levels as $level){ - $level->save(); - } - - $this->pluginManager->disablePlugins(); - $this->pluginManager->clearPlugins(); - PermissionManager::getInstance()->clearPermissions(); - $this->commandMap->clearCommands(); - - $this->logger->info("Reloading properties..."); - $this->properties->reload(); - $this->maxPlayers = $this->getConfigInt("max-players", 20); - - if($this->getConfigBool("hardcore", false) and $this->getDifficulty() < Level::DIFFICULTY_HARD){ - $this->setConfigInt("difficulty", Level::DIFFICULTY_HARD); - } - - $this->banByIP->load(); - $this->banByName->load(); - $this->reloadWhitelist(); - $this->operators->reload(); - - foreach($this->getIPBans()->getEntries() as $entry){ - $this->getNetwork()->blockAddress($entry->getName(), -1); - } - - $this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader)); - $this->pluginManager->registerInterface(new ScriptPluginLoader()); - $this->pluginManager->loadPlugins($this->pluginPath); - $this->enablePlugins(PluginLoadOrder::STARTUP); - $this->enablePlugins(PluginLoadOrder::POSTWORLD); - TimingsHandler::reload(); - } - - /** - * Shuts the server down correctly - * - * @return void - */ - public function shutdown(){ - $this->isRunning = false; - } - - /** - * @return void - */ - public function forceShutdown(){ - if($this->hasStopped){ - return; - } - - if($this->doTitleTick){ - echo "\x1b]0;\x07"; - } - - try{ - if(!$this->isRunning()){ - $this->sendUsage(SendUsageTask::TYPE_CLOSE); - } - - $this->hasStopped = true; - - $this->shutdown(); - if($this->rcon instanceof RCON){ - $this->rcon->stop(); - } - - if((bool) $this->getProperty("network.upnp-forwarding", false)){ - $this->logger->info("[UPnP] Removing port forward..."); - UPnP::RemovePortForward($this->getPort()); - } - - if($this->pluginManager instanceof PluginManager){ - $this->getLogger()->debug("Disabling all plugins"); - $this->pluginManager->disablePlugins(); - } - - foreach($this->players as $player){ - $player->close($player->getLeaveMessage(), $this->getProperty("settings.shutdown-message", "Server closed")); - } - - $this->getLogger()->debug("Unloading all worlds"); - foreach($this->getLevels() as $level){ - $this->unloadLevel($level, true); - } - - $this->getLogger()->debug("Removing event handlers"); - HandlerList::unregisterAll(); - - if($this->asyncPool instanceof AsyncPool){ - $this->getLogger()->debug("Shutting down async task worker pool"); - $this->asyncPool->shutdown(); - } - - if($this->properties !== null and $this->properties->hasChanged()){ - $this->getLogger()->debug("Saving properties"); - $this->properties->save(); - } - - if($this->console instanceof CommandReader){ - $this->getLogger()->debug("Closing console"); - $this->console->shutdown(); - $this->console->notify(); - } - - if($this->network instanceof Network){ - $this->getLogger()->debug("Stopping network interfaces"); - foreach($this->network->getInterfaces() as $interface){ - $this->getLogger()->debug("Stopping network interface " . get_class($interface)); - $interface->shutdown(); - $this->network->unregisterInterface($interface); - } - } - }catch(\Throwable $e){ - $this->logger->logException($e); - $this->logger->emergency("Crashed while crashing, killing process"); - @Process::kill(Process::pid()); - } - - } - - /** - * @return QueryRegenerateEvent - */ - public function getQueryInformation(){ - return $this->queryRegenerateTask; - } - - /** - * Starts the PocketMine-MP server and starts processing ticks and packets - */ - private function start() : void{ - if($this->getConfigBool("enable-query", true)){ - $this->queryHandler = new QueryHandler(); - } - - foreach($this->getIPBans()->getEntries() as $entry){ - $this->network->blockAddress($entry->getName(), -1); - } - - if((bool) $this->getProperty("anonymous-statistics.enabled", true)){ - $this->sendUsageTicker = 6000; - $this->sendUsage(SendUsageTask::TYPE_OPEN); - } - - if((bool) $this->getProperty("network.upnp-forwarding", false)){ - $this->logger->info("[UPnP] Trying to port forward..."); - try{ - UPnP::PortForward($this->getPort()); - }catch(\Exception $e){ - $this->logger->alert("UPnP portforward failed: " . $e->getMessage()); - } - } - - $this->tickCounter = 0; - - if(function_exists("pcntl_signal")){ - pcntl_signal(SIGTERM, [$this, "handleSignal"]); - pcntl_signal(SIGINT, [$this, "handleSignal"]); - pcntl_signal(SIGHUP, [$this, "handleSignal"]); - $this->dispatchSignals = true; - } - - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.defaultGameMode", [self::getGamemodeString($this->getGamemode())])); - - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.donate", [TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET])); - $this->logger->info($this->getLanguage()->translateString("pocketmine.server.startFinished", [round(microtime(true) - \pocketmine\START_TIME, 3)])); - - $this->tickProcessor(); - $this->forceShutdown(); - } - - /** - * @param int $signo - * - * @return void - */ - public function handleSignal($signo){ - if($signo === SIGTERM or $signo === SIGINT or $signo === SIGHUP){ - $this->shutdown(); - } - } - - /** - * @param mixed[][]|null $trace - * @phpstan-param list>|null $trace - * - * @return void - */ - public function exceptionHandler(\Throwable $e, $trace = null){ - while(@ob_end_flush()){} - global $lastError; - - if($trace === null){ - $trace = $e->getTrace(); - } - - $errstr = $e->getMessage(); - $errfile = $e->getFile(); - $errline = $e->getLine(); - - $errstr = preg_replace('/\s+/', ' ', trim($errstr)); - - $errfile = Utils::cleanPath($errfile); - - $this->logger->logException($e, $trace); - - $lastError = [ - "type" => get_class($e), - "message" => $errstr, - "fullFile" => $e->getFile(), - "file" => $errfile, - "line" => $errline, - "trace" => $trace - ]; - - global $lastExceptionError, $lastError; - $lastExceptionError = $lastError; - $this->crashDump(); - } - - /** - * @return void - */ - public function crashDump(){ - while(@ob_end_flush()){} - if(!$this->isRunning){ - return; - } - if($this->sendUsageTicker > 0){ - $this->sendUsage(SendUsageTask::TYPE_CLOSE); - } - $this->hasStopped = false; - - ini_set("error_reporting", '0'); - ini_set("memory_limit", '-1'); //Fix error dump not dumped on memory problems - try{ - $this->logger->emergency($this->getLanguage()->translateString("pocketmine.crash.create")); - $dump = new CrashDump($this); - - $this->logger->emergency($this->getLanguage()->translateString("pocketmine.crash.submit", [$dump->getPath()])); - - if($this->getProperty("auto-report.enabled", true) !== false){ - $report = true; - - $stamp = $this->getDataPath() . "crashdumps/.last_crash"; - $crashInterval = 120; //2 minutes - if(file_exists($stamp) and !($report = (filemtime($stamp) + $crashInterval < time()))){ - $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)){ - $p = $this->pluginManager->getPlugin($plugin); - if($p instanceof Plugin and !($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){ - $report = false; - } - - if(strrpos(\pocketmine\GIT_COMMIT, "-dirty") !== false or \pocketmine\GIT_COMMIT === str_repeat("00", 20)){ - $this->logger->debug("Not sending crashdump due to locally modified"); - $report = false; //Don't send crashdumps for locally modified builds - } - - if($report){ - $url = ((bool) $this->getProperty("auto-report.use-https", true) ? "https" : "http") . "://" . $this->getProperty("auto-report.host", "crash.pmmp.io") . "/submit/api"; - $postUrlError = "Unknown error"; - $reply = Internet::postURL($url, [ - "report" => "yes", - "name" => $this->getName() . " " . $this->getPocketMineVersion(), - "email" => "crash@pocketmine.net", - "reportPaste" => base64_encode($dump->getEncodedData()) - ], 10, [], $postUrlError); - - if($reply !== false and ($data = json_decode($reply)) !== null){ - if(isset($data->crashId) and isset($data->crashUrl)){ - $reportId = $data->crashId; - $reportUrl = $data->crashUrl; - $this->logger->emergency($this->getLanguage()->translateString("pocketmine.crash.archive", [$reportUrl, $reportId])); - }elseif(isset($data->error)){ - $this->logger->emergency("Automatic crash report submission failed: $data->error"); - } - }else{ - $this->logger->emergency("Failed to communicate with crash archive: $postUrlError"); - } - } - } - }catch(\Throwable $e){ - $this->logger->logException($e); - try{ - $this->logger->critical($this->getLanguage()->translateString("pocketmine.crash.error", [$e->getMessage()])); - }catch(\Throwable $e){} - } - - $this->forceShutdown(); - $this->isRunning = false; - - //Force minimum uptime to be >= 120 seconds, to reduce the impact of spammy crash loops - $spacing = ((int) \pocketmine\START_TIME) - time() + 120; - if($spacing > 0){ - echo "--- Waiting $spacing seconds to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL; - sleep($spacing); - } - @Process::kill(Process::pid()); - exit(1); - } - - /** - * @return mixed[] - */ - public function __debugInfo(){ - return []; - } - - public function getTickSleeper() : SleeperHandler{ - return $this->tickSleeper; - } - - private function tickProcessor() : void{ - $this->nextTick = microtime(true); - - while($this->isRunning){ - $this->tick(); - - //sleeps are self-correcting - if we undersleep 1ms on this tick, we'll sleep an extra ms on the next tick - $this->tickSleeper->sleepUntil($this->nextTick); - } - } - - /** - * @return void - */ - public function onPlayerLogin(Player $player){ - if($this->sendUsageTicker > 0){ - $this->uniquePlayers[$player->getRawUniqueId()] = $player->getRawUniqueId(); - } - - $this->loggedInPlayers[$player->getRawUniqueId()] = $player; - } - - /** - * @return void - */ - public function onPlayerLogout(Player $player){ - unset($this->loggedInPlayers[$player->getRawUniqueId()]); - } - - /** - * @return void - */ - public function addPlayer(Player $player){ - $this->players[spl_object_hash($player)] = $player; - } - - /** - * @return void - */ - public function removePlayer(Player $player){ - unset($this->players[spl_object_hash($player)]); - } - - /** - * @return void - */ - public function addOnlinePlayer(Player $player){ - $this->updatePlayerListData($player->getUniqueId(), $player->getId(), $player->getDisplayName(), $player->getSkin(), $player->getXuid()); - - $this->playerList[$player->getRawUniqueId()] = $player; - } - - /** - * @return void - */ - public function removeOnlinePlayer(Player $player){ - if(isset($this->playerList[$player->getRawUniqueId()])){ - unset($this->playerList[$player->getRawUniqueId()]); - - $this->removePlayerListData($player->getUniqueId()); - } - } - - /** - * @param Player[]|null $players - * - * @return void - */ - public function updatePlayerListData(UUID $uuid, int $entityId, string $name, Skin $skin, string $xboxUserId = "", array $players = null){ - $pk = new PlayerListPacket(); - $pk->type = PlayerListPacket::TYPE_ADD; - - $pk->entries[] = PlayerListEntry::createAdditionEntry($uuid, $entityId, $name, SkinAdapterSingleton::get()->toSkinData($skin), $xboxUserId); - - $this->broadcastPacket($players ?? $this->playerList, $pk); - } - - /** - * @param Player[]|null $players - * - * @return void - */ - public function removePlayerListData(UUID $uuid, array $players = null){ - $pk = new PlayerListPacket(); - $pk->type = PlayerListPacket::TYPE_REMOVE; - $pk->entries[] = PlayerListEntry::createRemovalEntry($uuid); - $this->broadcastPacket($players ?? $this->playerList, $pk); - } - - /** - * @return void - */ - public function sendFullPlayerListData(Player $p){ - $pk = new PlayerListPacket(); - $pk->type = PlayerListPacket::TYPE_ADD; - foreach($this->playerList as $player){ - $pk->entries[] = PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), SkinAdapterSingleton::get()->toSkinData($player->getSkin()), $player->getXuid()); - } - - $p->dataPacket($pk); - } - - private function checkTickUpdates(int $currentTick, float $tickTime) : void{ - foreach($this->players as $p){ - if(!$p->loggedIn and ($tickTime - $p->creationTime) >= 10){ - $p->close("", "Login timeout"); - } - } - - //Do level ticks - foreach($this->levels as $k => $level){ - if(!isset($this->levels[$k])){ - // Level unloaded during the tick of a level earlier in this loop, perhaps by plugin - continue; - } - - $levelTime = microtime(true); - $level->doTick($currentTick); - $tickMs = (microtime(true) - $levelTime) * 1000; - $level->tickRateTime = $tickMs; - if($tickMs >= 50){ - $this->getLogger()->debug(sprintf("World \"%s\" took too long to tick: %gms (%g ticks)", $level->getName(), $tickMs, round($tickMs / 50, 2))); - } - } - } - - /** - * @return void - */ - public function doAutoSave(){ - if($this->getAutoSave()){ - Timings::$worldSaveTimer->startTiming(); - foreach($this->players as $index => $player){ - if($player->spawned){ - $player->save(); - }elseif(!$player->isConnected()){ - $this->removePlayer($player); - } - } - - foreach($this->getLevels() as $level){ - $level->save(false); - } - Timings::$worldSaveTimer->stopTiming(); - } - } - - /** - * @param int $type - * - * @return void - */ - public function sendUsage($type = SendUsageTask::TYPE_STATUS){ - if((bool) $this->getProperty("anonymous-statistics.enabled", true)){ - $this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers)); - } - $this->uniquePlayers = []; - } - - /** - * @return BaseLang - */ - public function getLanguage(){ - return $this->baseLang; - } - - public function isLanguageForced() : bool{ - return $this->forceLanguage; - } - - /** - * @return Network - */ - public function getNetwork(){ - return $this->network; - } - - /** - * @return MemoryManager - */ - public function getMemoryManager(){ - return $this->memoryManager; - } - - private function titleTick() : void{ - Timings::$titleTickTimer->startTiming(); - $d = Process::getRealMemoryUsage(); - - $u = Process::getAdvancedMemoryUsage(); - $usage = sprintf("%g/%g/%g/%g MB @ %d threads", round(($u[0] / 1024) / 1024, 2), round(($d[0] / 1024) / 1024, 2), round(($u[1] / 1024) / 1024, 2), round(($u[2] / 1024) / 1024, 2), Process::getThreadCount()); - - echo "\x1b]0;" . $this->getName() . " " . - $this->getPocketMineVersion() . - " | Online " . count($this->players) . "/" . $this->getMaxPlayers() . - " | Memory " . $usage . - " | U " . round($this->network->getUpload() / 1024, 2) . - " D " . round($this->network->getDownload() / 1024, 2) . - " kB/s | TPS " . $this->getTicksPerSecondAverage() . - " | Load " . $this->getTickUsageAverage() . "%\x07"; - - Timings::$titleTickTimer->stopTiming(); - } - - /** - * @return void - * - * TODO: move this to Network - */ - public function handlePacket(AdvancedSourceInterface $interface, string $address, int $port, string $payload){ - try{ - if(strlen($payload) > 2 and substr($payload, 0, 2) === "\xfe\xfd" and $this->queryHandler instanceof QueryHandler){ - $this->queryHandler->handle($interface, $address, $port, $payload); - }else{ - $this->logger->debug("Unhandled raw packet from $address $port: " . base64_encode($payload)); - } - }catch(\Throwable $e){ - $this->logger->logException($e); - - $this->getNetwork()->blockAddress($address, 600); - } - //TODO: add raw packet events - } - - /** - * Tries to execute a server tick - */ - private function tick() : void{ - $tickTime = microtime(true); - if(($tickTime - $this->nextTick) < -0.025){ //Allow half a tick of diff - return; - } - - Timings::$serverTickTimer->startTiming(); - - ++$this->tickCounter; - - Timings::$connectionTimer->startTiming(); - $this->network->processInterfaces(); - Timings::$connectionTimer->stopTiming(); - - Timings::$schedulerTimer->startTiming(); - $this->pluginManager->tickSchedulers($this->tickCounter); - Timings::$schedulerTimer->stopTiming(); - - Timings::$schedulerAsyncTimer->startTiming(); - $this->asyncPool->collectTasks(); - Timings::$schedulerAsyncTimer->stopTiming(); - - $this->checkTickUpdates($this->tickCounter, $tickTime); - - foreach($this->players as $player){ - $player->checkNetwork(); - } - - if(($this->tickCounter % 20) === 0){ - if($this->doTitleTick){ - $this->titleTick(); - } - $this->currentTPS = 20; - $this->currentUse = 0; - - ($this->queryRegenerateTask = new QueryRegenerateEvent($this))->call(); - - $this->network->updateName(); - $this->network->resetStatistics(); - } - - if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){ - $this->autoSaveTicker = 0; - $this->getLogger()->debug("[Auto Save] Saving worlds..."); - $start = microtime(true); - $this->doAutoSave(); - $time = (microtime(true) - $start); - $this->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); - } - - if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){ - $this->sendUsageTicker = 6000; - $this->sendUsage(SendUsageTask::TYPE_STATUS); - } - - if(($this->tickCounter % 100) === 0){ - foreach($this->levels as $level){ - $level->clearCache(); - } - - if($this->getTicksPerSecondAverage() < 12){ - $this->logger->warning($this->getLanguage()->translateString("pocketmine.server.tickOverload")); - } - } - - if($this->dispatchSignals and $this->tickCounter % 5 === 0){ - pcntl_signal_dispatch(); - } - - $this->getMemoryManager()->check(); - - Timings::$serverTickTimer->stopTiming(); - - $now = microtime(true); - $this->currentTPS = min(20, 1 / max(0.001, $now - $tickTime)); - $this->currentUse = min(1, ($now - $tickTime) / 0.05); - - TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate); - - $idx = $this->tickCounter % 20; - $this->tickAverage[$idx] = $this->currentTPS; - $this->useAverage[$idx] = $this->currentUse; - - if(($this->nextTick - $tickTime) < -1){ - $this->nextTick = $tickTime; - }else{ - $this->nextTick += 0.05; - } - } - - /** - * Called when something attempts to serialize the server instance. - * - * @throws \BadMethodCallException because Server instances cannot be serialized - */ - public function __sleep(){ - throw new \BadMethodCallException("Cannot serialize Server instance"); - } -} diff --git a/src/pocketmine/VersionInfo.php b/src/pocketmine/VersionInfo.php deleted file mode 100644 index 8699310ef1..0000000000 --- a/src/pocketmine/VersionInfo.php +++ /dev/null @@ -1,38 +0,0 @@ -classLoader; - } - - /** - * @return void - */ - public function setClassLoader(\ClassLoader $loader = null){ - $this->composerAutoloaderPath = \pocketmine\COMPOSER_AUTOLOADER_PATH; - - if($loader === null){ - $loader = Server::getInstance()->getLoader(); - } - $this->classLoader = $loader; - } - - /** - * Registers the class loader for this thread. - * - * WARNING: This method MUST be called from any descendent threads' run() method to make autoloading usable. - * If you do not do this, you will not be able to use new classes that were not loaded when the thread was started - * (unless you are using a custom autoloader). - * - * @return void - */ - public function registerClassLoader(){ - if($this->composerAutoloaderPath !== null){ - require $this->composerAutoloaderPath; - } - if($this->classLoader !== null){ - $this->classLoader->register(false); - } - } - - /** - * @return bool - */ - public function start(int $options = PTHREADS_INHERIT_ALL){ - ThreadManager::getInstance()->add($this); - - if($this->getClassLoader() === null){ - $this->setClassLoader(); - } - return parent::start($options); - } - - /** - * Stops the thread using the best way possible. Try to stop it yourself before calling this. - * - * @return void - */ - public function quit(){ - $this->isKilled = true; - - if(!$this->isShutdown()){ - while($this->unstack() !== null); - $this->notify(); - $this->shutdown(); - } - - ThreadManager::getInstance()->remove($this); - } - - public function getThreadName() : string{ - return (new \ReflectionClass($this))->getShortName(); - } -} diff --git a/src/pocketmine/block/Air.php b/src/pocketmine/block/Air.php deleted file mode 100644 index 7772c9ba9d..0000000000 --- a/src/pocketmine/block/Air.php +++ /dev/null @@ -1,83 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Air"; - } - - public function canPassThrough() : bool{ - return true; - } - - public function isBreakable(Item $item) : bool{ - return false; - } - - public function canBeFlowedInto() : bool{ - return true; - } - - public function canBeReplaced() : bool{ - return true; - } - - public function canBePlaced() : bool{ - return false; - } - - public function isSolid() : bool{ - return false; - } - - public function getBoundingBox() : ?AxisAlignedBB{ - return null; - } - - public function getCollisionBoxes() : array{ - return []; - } - - public function getHardness() : float{ - return -1; - } - - public function getBlastResistance() : float{ - return 0; - } -} diff --git a/src/pocketmine/block/Anvil.php b/src/pocketmine/block/Anvil.php deleted file mode 100644 index 7fa7cfc1af..0000000000 --- a/src/pocketmine/block/Anvil.php +++ /dev/null @@ -1,115 +0,0 @@ -meta = $meta; - } - - public function isTransparent() : bool{ - return true; - } - - public function getHardness() : float{ - return 5; - } - - public function getBlastResistance() : float{ - return 6000; - } - - public function getVariantBitmask() : int{ - return 0x0c; - } - - public function getName() : string{ - static $names = [ - self::TYPE_NORMAL => "Anvil", - self::TYPE_SLIGHTLY_DAMAGED => "Slightly Damaged Anvil", - self::TYPE_VERY_DAMAGED => "Very Damaged Anvil" - ]; - return $names[$this->getVariant()] ?? "Anvil"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function recalculateBoundingBox() : ?AxisAlignedBB{ - $inset = 0.125; - - if(($this->meta & 0x01) !== 0){ //east/west - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z + $inset, - $this->x + 1, - $this->y + 1, - $this->z + 1 - $inset - ); - }else{ - return new AxisAlignedBB( - $this->x + $inset, - $this->y, - $this->z, - $this->x + 1 - $inset, - $this->y + 1, - $this->z + 1 - ); - } - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - $player->addWindow(new AnvilInventory($this)); - } - - return true; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $direction = ($player !== null ? $player->getDirection() : 0) & 0x03; - $this->meta = $this->getVariant() | $direction; - return $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - } -} diff --git a/src/pocketmine/block/BaseRail.php b/src/pocketmine/block/BaseRail.php deleted file mode 100644 index 1cd8cf1676..0000000000 --- a/src/pocketmine/block/BaseRail.php +++ /dev/null @@ -1,289 +0,0 @@ - Vector3::SIDE_NORTH, - self::ASCENDING_EAST => Vector3::SIDE_EAST, - self::ASCENDING_SOUTH => Vector3::SIDE_SOUTH, - self::ASCENDING_WEST => Vector3::SIDE_WEST - ]; - - protected const FLAG_ASCEND = 1 << 24; //used to indicate direction-up - - protected const CONNECTIONS = [ - //straights - self::STRAIGHT_NORTH_SOUTH => [ - Vector3::SIDE_NORTH, - Vector3::SIDE_SOUTH - ], - self::STRAIGHT_EAST_WEST => [ - Vector3::SIDE_EAST, - Vector3::SIDE_WEST - ], - - //ascending - self::ASCENDING_EAST => [ - Vector3::SIDE_WEST, - Vector3::SIDE_EAST | self::FLAG_ASCEND - ], - self::ASCENDING_WEST => [ - Vector3::SIDE_EAST, - Vector3::SIDE_WEST | self::FLAG_ASCEND - ], - self::ASCENDING_NORTH => [ - Vector3::SIDE_SOUTH, - Vector3::SIDE_NORTH | self::FLAG_ASCEND - ], - self::ASCENDING_SOUTH => [ - Vector3::SIDE_NORTH, - Vector3::SIDE_SOUTH | self::FLAG_ASCEND - ] - ]; - - public function __construct(int $meta = 0){ - $this->meta = $meta; - } - - public function getHardness() : float{ - return 0.7; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if(!$blockReplace->getSide(Vector3::SIDE_DOWN)->isTransparent() and $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true)){ - $this->tryReconnect(); - return true; - } - - return false; - } - - /** - * @param int[] $connections - * @param int[][] $lookup - * @phpstan-param array> $lookup - */ - protected static function searchState(array $connections, array $lookup) : int{ - $meta = array_search($connections, $lookup, true); - if($meta === false){ - $meta = array_search(array_reverse($connections), $lookup, true); - } - if($meta === false){ - throw new \InvalidArgumentException("No meta value matches connections " . implode(", ", array_map('\dechex', $connections))); - } - - return $meta; - } - - /** - * Returns a meta value for the rail with the given connections. - * - * @param int[] $connections - * - * @throws \InvalidArgumentException if no state matches the given connections - */ - protected function getMetaForState(array $connections) : int{ - return self::searchState($connections, self::CONNECTIONS); - } - - /** - * Returns the connection directions of this rail (depending on the current block state) - * - * @return int[] - */ - abstract protected function getConnectionsForState() : array; - - /** - * Returns all the directions this rail is already connected in. - * - * @return int[] - */ - private function getConnectedDirections() : array{ - /** @var int[] $connections */ - $connections = []; - - /** @var int $connection */ - foreach($this->getConnectionsForState() as $connection){ - $other = $this->getSide($connection & ~self::FLAG_ASCEND); - $otherConnection = Vector3::getOppositeSide($connection & ~self::FLAG_ASCEND); - - if(($connection & self::FLAG_ASCEND) !== 0){ - $other = $other->getSide(Vector3::SIDE_UP); - - }elseif(!($other instanceof BaseRail)){ //check for rail sloping up to meet this one - $other = $other->getSide(Vector3::SIDE_DOWN); - $otherConnection |= self::FLAG_ASCEND; - } - - if( - $other instanceof BaseRail and - in_array($otherConnection, $other->getConnectionsForState(), true) - ){ - $connections[] = $connection; - } - } - - return $connections; - } - - /** - * @param int[] $constraints - * - * @return true[] - * @phpstan-return array - */ - private function getPossibleConnectionDirections(array $constraints) : array{ - switch(count($constraints)){ - case 0: - //No constraints, can connect in any direction - $possible = [ - Vector3::SIDE_NORTH => true, - Vector3::SIDE_SOUTH => true, - Vector3::SIDE_WEST => true, - Vector3::SIDE_EAST => true - ]; - foreach($possible as $p => $_){ - $possible[$p | self::FLAG_ASCEND] = true; - } - - return $possible; - case 1: - return $this->getPossibleConnectionDirectionsOneConstraint(array_shift($constraints)); - case 2: - return []; - default: - throw new \InvalidArgumentException("Expected at most 2 constraints, got " . count($constraints)); - } - } - - /** - * @return true[] - * @phpstan-return array - */ - protected function getPossibleConnectionDirectionsOneConstraint(int $constraint) : array{ - $opposite = Vector3::getOppositeSide($constraint & ~self::FLAG_ASCEND); - - $possible = [$opposite => true]; - - if(($constraint & self::FLAG_ASCEND) === 0){ - //We can slope the other way if this connection isn't already a slope - $possible[$opposite | self::FLAG_ASCEND] = true; - } - - return $possible; - } - - private function tryReconnect() : void{ - $thisConnections = $this->getConnectedDirections(); - $changed = false; - - do{ - $possible = $this->getPossibleConnectionDirections($thisConnections); - $continue = false; - - foreach($possible as $thisSide => $_){ - $otherSide = Vector3::getOppositeSide($thisSide & ~self::FLAG_ASCEND); - - $other = $this->getSide($thisSide & ~self::FLAG_ASCEND); - - if(($thisSide & self::FLAG_ASCEND) !== 0){ - $other = $other->getSide(Vector3::SIDE_UP); - - }elseif(!($other instanceof BaseRail)){ //check if other rails can slope up to meet this one - $other = $other->getSide(Vector3::SIDE_DOWN); - $otherSide |= self::FLAG_ASCEND; - } - - if(!($other instanceof BaseRail) or count($otherConnections = $other->getConnectedDirections()) >= 2){ - //we can only connect to a rail that has less than 2 connections - continue; - } - - $otherPossible = $other->getPossibleConnectionDirections($otherConnections); - - if(isset($otherPossible[$otherSide])){ - $otherConnections[] = $otherSide; - $other->updateState($otherConnections); - - $changed = true; - $thisConnections[] = $thisSide; - $continue = count($thisConnections) < 2; - - break; //force recomputing possible directions, since this connection could invalidate others - } - } - }while($continue); - - if($changed){ - $this->updateState($thisConnections); - } - } - - /** - * @param int[] $connections - */ - private function updateState(array $connections) : void{ - if(count($connections) === 1){ - $connections[] = Vector3::getOppositeSide($connections[0] & ~self::FLAG_ASCEND); - }elseif(count($connections) !== 2){ - throw new \InvalidArgumentException("Expected exactly 2 connections, got " . count($connections)); - } - - $this->meta = $this->getMetaForState($connections); - $this->level->setBlock($this, $this, false, false); //avoid recursion - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent() or ( - isset(self::ASCENDING_SIDES[$this->meta & 0x07]) and - $this->getSide(self::ASCENDING_SIDES[$this->meta & 0x07])->isTransparent() - )){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Bed.php b/src/pocketmine/block/Bed.php deleted file mode 100644 index 166b1c35ba..0000000000 --- a/src/pocketmine/block/Bed.php +++ /dev/null @@ -1,216 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.2; - } - - public function getName() : string{ - return "Bed Block"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 0.5625, - $this->z + 1 - ); - } - - public function isHeadPart() : bool{ - return ($this->meta & self::BITFLAG_HEAD) !== 0; - } - - public function isOccupied() : bool{ - return ($this->meta & self::BITFLAG_OCCUPIED) !== 0; - } - - /** - * @return void - */ - public function setOccupied(bool $occupied = true){ - if($occupied){ - $this->meta |= self::BITFLAG_OCCUPIED; - }else{ - $this->meta &= ~self::BITFLAG_OCCUPIED; - } - - $this->getLevelNonNull()->setBlock($this, $this, false, false); - - if(($other = $this->getOtherHalf()) !== null and $other->isOccupied() !== $occupied){ - $other->setOccupied($occupied); - } - } - - public static function getOtherHalfSide(int $meta, bool $isHead = false) : int{ - $rotation = $meta & 0x03; - $side = -1; - - switch($rotation){ - case 0x00: //South - $side = Vector3::SIDE_SOUTH; - break; - case 0x01: //West - $side = Vector3::SIDE_WEST; - break; - case 0x02: //North - $side = Vector3::SIDE_NORTH; - break; - case 0x03: //East - $side = Vector3::SIDE_EAST; - break; - } - - if($isHead){ - $side = Vector3::getOppositeSide($side); - } - - return $side; - } - - public function getOtherHalf() : ?Bed{ - $other = $this->getSide(self::getOtherHalfSide($this->meta, $this->isHeadPart())); - if($other instanceof Bed and $other->getId() === $this->getId() and $other->isHeadPart() !== $this->isHeadPart() and (($other->getDamage() & 0x03) === ($this->getDamage() & 0x03))){ - return $other; - } - - return null; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player !== null){ - $other = $this->getOtherHalf(); - if($other === null){ - $player->sendMessage(TextFormat::GRAY . "This bed is incomplete"); - - return true; - }elseif($player->distanceSquared($this) > 4 and $player->distanceSquared($other) > 4){ - $player->sendMessage(new TranslationContainer(TextFormat::GRAY . "%tile.bed.tooFar")); - return true; - } - - $time = $this->getLevelNonNull()->getTimeOfDay(); - - $isNight = ($time >= Level::TIME_NIGHT and $time < Level::TIME_SUNRISE); - - if(!$isNight){ - $player->sendMessage(new TranslationContainer(TextFormat::GRAY . "%tile.bed.noSleep")); - - return true; - } - - $b = ($this->isHeadPart() ? $this : $other); - - if($b->isOccupied()){ - $player->sendMessage(new TranslationContainer(TextFormat::GRAY . "%tile.bed.occupied")); - - return true; - } - - $player->sleepOn($b); - } - - return true; - - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if(!$down->isTransparent()){ - $meta = (($player instanceof Player ? $player->getDirection() : 0) - 1) & 0x03; - $next = $this->getSide(self::getOtherHalfSide($meta)); - if($next->canBeReplaced() and !$next->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get($this->id, $meta), true, true); - $this->getLevelNonNull()->setBlock($next, BlockFactory::get($this->id, $meta | self::BITFLAG_HEAD), true, true); - - Tile::createTile(Tile::BED, $this->getLevelNonNull(), TileBed::createNBT($this, $face, $item, $player)); - Tile::createTile(Tile::BED, $this->getLevelNonNull(), TileBed::createNBT($next, $face, $item, $player)); - - return true; - } - } - - return false; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - if($this->isHeadPart()){ - return [$this->getItem()]; - } - - return []; - } - - public function getPickedItem() : Item{ - return $this->getItem(); - } - - private function getItem() : Item{ - $tile = $this->getLevelNonNull()->getTile($this); - if($tile instanceof TileBed){ - return ItemFactory::get($this->getItemId(), $tile->getColor()); - } - - return ItemFactory::get($this->getItemId(), 14); //Red - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function getAffectedBlocks() : array{ - if(($other = $this->getOtherHalf()) !== null){ - return [$this, $other]; - } - - return parent::getAffectedBlocks(); - } -} diff --git a/src/pocketmine/block/Block.php b/src/pocketmine/block/Block.php deleted file mode 100644 index d35abc935f..0000000000 --- a/src/pocketmine/block/Block.php +++ /dev/null @@ -1,654 +0,0 @@ -id = $id; - $this->meta = $meta; - $this->fallbackName = $name; - $this->itemId = $itemId; - } - - public function getName() : string{ - return $this->fallbackName ?? "Unknown"; - } - - final public function getId() : int{ - return $this->id; - } - - /** - * Returns the ID of the item form of the block. - * Used for drops for blocks (some blocks such as doors have a different item ID). - */ - public function getItemId() : int{ - return $this->itemId ?? $this->getId(); - } - - /** - * @internal - */ - public function getRuntimeId() : int{ - return RuntimeBlockMapping::toStaticRuntimeId($this->getId(), $this->getDamage()); - } - - final public function getDamage() : int{ - return $this->meta; - } - - final public function setDamage(int $meta) : void{ - if($meta < 0 or $meta > 0xf){ - throw new \InvalidArgumentException("Block damage values must be 0-15, not $meta"); - } - $this->meta = $meta; - } - - /** - * Bitmask to use to remove superfluous information from block meta when getting its item form or name. - * This defaults to -1 (don't remove any data). Used to remove rotation data and bitflags from block drops. - * - * If your block should not have any meta value when it's dropped as an item, override this to return 0 in - * descendent classes. - */ - public function getVariantBitmask() : int{ - return -1; - } - - /** - * Returns the block meta, stripped of non-variant flags. - */ - public function getVariant() : int{ - return $this->meta & $this->getVariantBitmask(); - } - - /** - * AKA: Block->isPlaceable - */ - public function canBePlaced() : bool{ - return true; - } - - public function canBeReplaced() : bool{ - return false; - } - - public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ - return $blockReplace->canBeReplaced(); - } - - /** - * Places the Block, using block space and block target, and side. Returns if the block has been placed. - */ - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - return $this->getLevelNonNull()->setBlock($this, $this, true, true); - } - - /** - * Returns if the block can be broken with an specific Item - */ - public function isBreakable(Item $item) : bool{ - return true; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_NONE; - } - - /** - * Returns the level of tool required to harvest this block (for normal blocks). When the tool type matches the - * block's required tool type, the tool must have a harvest level greater than or equal to this value to be able to - * successfully harvest the block. - * - * If the block requires a specific minimum tier of tiered tool, the minimum tier required should be returned. - * Otherwise, 1 should be returned if a tool is required, 0 if not. - * - * @see Item::getBlockToolHarvestLevel() - */ - public function getToolHarvestLevel() : int{ - return 0; - } - - /** - * Returns whether the specified item is the proper tool to use for breaking this block. This checks tool type and - * harvest level requirement. - * - * In most cases this is also used to determine whether block drops should be created or not, except in some - * special cases such as vines. - */ - public function isCompatibleWithTool(Item $tool) : bool{ - if($this->getHardness() < 0){ - return false; - } - - $toolType = $this->getToolType(); - $harvestLevel = $this->getToolHarvestLevel(); - return $toolType === BlockToolType::TYPE_NONE or $harvestLevel === 0 or ( - ($toolType & $tool->getBlockToolType()) !== 0 and $tool->getBlockToolHarvestLevel() >= $harvestLevel); - } - - /** - * Do the actions needed so the block is broken with the Item - */ - public function onBreak(Item $item, Player $player = null) : bool{ - return $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), true, true); - } - - /** - * Returns the seconds that this block takes to be broken using an specific Item - * - * @throws \InvalidArgumentException if the item efficiency is not a positive number - */ - public function getBreakTime(Item $item) : float{ - $base = $this->getHardness(); - if($this->isCompatibleWithTool($item)){ - $base *= 1.5; - }else{ - $base *= 5; - } - - $efficiency = $item->getMiningEfficiency($this); - if($efficiency <= 0){ - throw new \InvalidArgumentException(get_class($item) . " has invalid mining efficiency: expected >= 0, got $efficiency"); - } - - $base /= $efficiency; - - return $base; - } - - /** - * Called when this block or a block immediately adjacent to it changes state. - */ - public function onNearbyBlockChange() : void{ - - } - - /** - * Returns whether random block updates will be done on this block. - */ - public function ticksRandomly() : bool{ - return false; - } - - /** - * Called when this block is randomly updated due to chunk ticking. - * WARNING: This will not be called if ticksRandomly() does not return true! - */ - public function onRandomTick() : void{ - - } - - /** - * Called when this block is updated by the delayed blockupdate scheduler in the level. - */ - public function onScheduledUpdate() : void{ - - } - - /** - * Do actions when activated by Item. Returns if it has done anything - */ - public function onActivate(Item $item, Player $player = null) : bool{ - return false; - } - - /** - * Returns a base value used to compute block break times. - */ - public function getHardness() : float{ - return 10; - } - - /** - * Returns the block's resistance to explosions. Usually 5x hardness. - */ - public function getBlastResistance() : float{ - return $this->getHardness() * 5; - } - - public function getFrictionFactor() : float{ - return 0.6; - } - - /** - * @return int 0-15 - */ - public function getLightLevel() : int{ - return 0; - } - - /** - * Returns the amount of light this block will filter out when light passes through this block. - * This value is used in light spread calculation. - * - * @return int 0-15 - */ - public function getLightFilter() : int{ - return 15; - } - - /** - * Returns whether this block will diffuse sky light passing through it vertically. - * Diffusion means that full-strength sky light passing through this block will not be reduced, but will start being filtered below the block. - * Examples of this behaviour include leaves and cobwebs. - * - * Light-diffusing blocks are included by the heightmap. - */ - public function diffusesSkyLight() : bool{ - return false; - } - - public function isTransparent() : bool{ - return false; - } - - public function isSolid() : bool{ - return true; - } - - /** - * AKA: Block->isFlowable - */ - public function canBeFlowedInto() : bool{ - return false; - } - - public function hasEntityCollision() : bool{ - return false; - } - - public function canPassThrough() : bool{ - return false; - } - - /** - * Returns whether entities can climb up this block. - */ - public function canClimb() : bool{ - return false; - } - - public function addVelocityToEntity(Entity $entity, Vector3 $vector) : void{ - - } - - /** - * Sets the block position to a new Position object - */ - final public function position(Position $v) : void{ - $this->x = (int) $v->x; - $this->y = (int) $v->y; - $this->z = (int) $v->z; - $this->level = $v->level; - $this->boundingBox = null; - } - - /** - * Returns an array of Item objects to be dropped - * - * @return Item[] - */ - public function getDrops(Item $item) : array{ - if($this->isCompatibleWithTool($item)){ - if($this->isAffectedBySilkTouch() and $item->hasEnchantment(Enchantment::SILK_TOUCH)){ - return $this->getSilkTouchDrops($item); - } - - return $this->getDropsForCompatibleTool($item); - } - - return []; - } - - /** - * Returns an array of Items to be dropped when the block is broken using the correct tool type. - * - * @return Item[] - */ - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get($this->getItemId(), $this->getVariant()) - ]; - } - - /** - * Returns an array of Items to be dropped when the block is broken using a compatible Silk Touch-enchanted tool. - * - * @return Item[] - */ - public function getSilkTouchDrops(Item $item) : array{ - return [ - ItemFactory::get($this->getItemId(), $this->getVariant()) - ]; - } - - /** - * Returns how much XP will be dropped by breaking this block with the given item. - */ - public function getXpDropForTool(Item $item) : int{ - if($item->hasEnchantment(Enchantment::SILK_TOUCH) or !$this->isCompatibleWithTool($item)){ - return 0; - } - - return $this->getXpDropAmount(); - } - - /** - * Returns how much XP this block will drop when broken with an appropriate tool. - */ - protected function getXpDropAmount() : int{ - return 0; - } - - /** - * Returns whether Silk Touch enchanted tools will cause this block to drop as itself. Since most blocks drop - * themselves anyway, this is implicitly true. - */ - public function isAffectedBySilkTouch() : bool{ - return true; - } - - /** - * Returns the item that players will equip when middle-clicking on this block. - */ - public function getPickedItem() : Item{ - return ItemFactory::get($this->getItemId(), $this->getVariant()); - } - - /** - * Returns the time in ticks which the block will fuel a furnace for. - */ - public function getFuelTime() : int{ - return 0; - } - - /** - * Returns the chance that the block will catch fire from nearby fire sources. Higher values lead to faster catching - * fire. - */ - public function getFlameEncouragement() : int{ - return 0; - } - - /** - * Returns the base flammability of this block. Higher values lead to the block burning away more quickly. - */ - public function getFlammability() : int{ - return 0; - } - - /** - * Returns whether fire lit on this block will burn indefinitely. - */ - public function burnsForever() : bool{ - return false; - } - - /** - * Returns whether this block can catch fire. - */ - public function isFlammable() : bool{ - return $this->getFlammability() > 0; - } - - /** - * Called when this block is burned away by being on fire. - */ - public function onIncinerate() : void{ - - } - - /** - * Returns the Block on the side $side, works like Vector3::getSide() - * - * @return Block - */ - public function getSide(int $side, int $step = 1){ - if($this->isValid()){ - return $this->getLevelNonNull()->getBlock(Vector3::getSide($side, $step)); - } - - return BlockFactory::get(Block::AIR, 0, Position::fromObject(Vector3::getSide($side, $step))); - } - - /** - * Returns the 4 blocks on the horizontal axes around the block (north, south, east, west) - * - * @return Block[] - */ - public function getHorizontalSides() : array{ - return [ - $this->getSide(Vector3::SIDE_NORTH), - $this->getSide(Vector3::SIDE_SOUTH), - $this->getSide(Vector3::SIDE_WEST), - $this->getSide(Vector3::SIDE_EAST) - ]; - } - - /** - * Returns the six blocks around this block. - * - * @return Block[] - */ - public function getAllSides() : array{ - return array_merge( - [ - $this->getSide(Vector3::SIDE_DOWN), - $this->getSide(Vector3::SIDE_UP) - ], - $this->getHorizontalSides() - ); - } - - /** - * Returns a list of blocks that this block is part of. In most cases, only contains the block itself, but in cases - * such as double plants, beds and doors, will contain both halves. - * - * @return Block[] - */ - public function getAffectedBlocks() : array{ - return [$this]; - } - - /** - * @return string - */ - public function __toString(){ - return "Block[" . $this->getName() . "] (" . $this->getId() . ":" . $this->getDamage() . ")"; - } - - /** - * Checks for collision against an AxisAlignedBB - */ - public function collidesWithBB(AxisAlignedBB $bb) : bool{ - foreach($this->getCollisionBoxes() as $bb2){ - if($bb->intersectsWith($bb2)){ - return true; - } - } - - return false; - } - - public function onEntityCollide(Entity $entity) : void{ - - } - - /** - * @return AxisAlignedBB[] - */ - public function getCollisionBoxes() : array{ - if($this->collisionBoxes === null){ - $this->collisionBoxes = $this->recalculateCollisionBoxes(); - } - - return $this->collisionBoxes; - } - - /** - * @return AxisAlignedBB[] - */ - protected function recalculateCollisionBoxes() : array{ - if(($bb = $this->recalculateBoundingBox()) !== null){ - return [$bb]; - } - - return []; - } - - public function getBoundingBox() : ?AxisAlignedBB{ - if($this->boundingBox === null){ - $this->boundingBox = $this->recalculateBoundingBox(); - } - return $this->boundingBox; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - - /** - * Clears any cached precomputed objects, such as bounding boxes. This is called on block neighbour update and when - * the block is set into the world to remove any outdated precomputed things such as AABBs and force recalculation. - */ - public function clearCaches() : void{ - $this->boundingBox = null; - $this->collisionBoxes = null; - } - - public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{ - $bbs = $this->getCollisionBoxes(); - if(count($bbs) === 0){ - return null; - } - - /** @var RayTraceResult|null $currentHit */ - $currentHit = null; - /** @var int|float $currentDistance */ - $currentDistance = PHP_INT_MAX; - - foreach($bbs as $bb){ - $nextHit = $bb->calculateIntercept($pos1, $pos2); - if($nextHit === null){ - continue; - } - - $nextDistance = $nextHit->hitVector->distanceSquared($pos1); - if($nextDistance < $currentDistance){ - $currentHit = $nextHit; - $currentDistance = $nextDistance; - } - } - - return $currentHit; - } - - public function setMetadata(string $metadataKey, MetadataValue $newMetadataValue){ - if($this->isValid()){ - $this->level->getBlockMetadata()->setMetadata($this, $metadataKey, $newMetadataValue); - } - } - - public function getMetadata(string $metadataKey){ - if($this->isValid()){ - return $this->level->getBlockMetadata()->getMetadata($this, $metadataKey); - } - - return []; - } - - public function hasMetadata(string $metadataKey) : bool{ - if($this->isValid()){ - return $this->level->getBlockMetadata()->hasMetadata($this, $metadataKey); - } - - return false; - } - - public function removeMetadata(string $metadataKey, Plugin $owningPlugin){ - if($this->isValid()){ - $this->level->getBlockMetadata()->removeMetadata($this, $metadataKey, $owningPlugin); - } - } -} diff --git a/src/pocketmine/block/BlockFactory.php b/src/pocketmine/block/BlockFactory.php deleted file mode 100644 index e8c2d232b0..0000000000 --- a/src/pocketmine/block/BlockFactory.php +++ /dev/null @@ -1,449 +0,0 @@ - - */ - private static $fullList; - - /** - * @var \SplFixedArray|bool[] - * @phpstan-var \SplFixedArray - */ - public static $solid; - /** - * @var \SplFixedArray|bool[] - * @phpstan-var \SplFixedArray - */ - public static $transparent; - /** - * @var \SplFixedArray|float[] - * @phpstan-var \SplFixedArray - */ - public static $hardness; - /** - * @var \SplFixedArray|int[] - * @phpstan-var \SplFixedArray - */ - public static $light; - /** - * @var \SplFixedArray|int[] - * @phpstan-var \SplFixedArray - */ - public static $lightFilter; - /** - * @var \SplFixedArray|bool[] - * @phpstan-var \SplFixedArray - */ - public static $diffusesSkyLight; - /** - * @var \SplFixedArray|float[] - * @phpstan-var \SplFixedArray - */ - public static $blastResistance; - - /** - * Initializes the block factory. By default this is called only once on server start, however you may wish to use - * this if you need to reset the block factory back to its original defaults for whatever reason. - */ - public static function init() : void{ - self::$fullList = new \SplFixedArray(4096); - - self::$light = new \SplFixedArray(256); - self::$lightFilter = new \SplFixedArray(256); - self::$solid = new \SplFixedArray(256); - self::$hardness = new \SplFixedArray(256); - self::$transparent = new \SplFixedArray(256); - self::$diffusesSkyLight = new \SplFixedArray(256); - self::$blastResistance = new \SplFixedArray(256); - - self::registerBlock(new Air()); - self::registerBlock(new Stone()); - self::registerBlock(new Grass()); - self::registerBlock(new Dirt()); - self::registerBlock(new Cobblestone()); - self::registerBlock(new Planks()); - self::registerBlock(new Sapling()); - self::registerBlock(new Bedrock()); - self::registerBlock(new Water()); - self::registerBlock(new StillWater()); - self::registerBlock(new Lava()); - self::registerBlock(new StillLava()); - self::registerBlock(new Sand()); - self::registerBlock(new Gravel()); - self::registerBlock(new GoldOre()); - self::registerBlock(new IronOre()); - self::registerBlock(new CoalOre()); - self::registerBlock(new Wood()); - self::registerBlock(new Leaves()); - self::registerBlock(new Sponge()); - self::registerBlock(new Glass()); - self::registerBlock(new LapisOre()); - self::registerBlock(new Lapis()); - //TODO: DISPENSER - self::registerBlock(new Sandstone()); - self::registerBlock(new NoteBlock()); - self::registerBlock(new Bed()); - self::registerBlock(new PoweredRail()); - self::registerBlock(new DetectorRail()); - //TODO: STICKY_PISTON - self::registerBlock(new Cobweb()); - self::registerBlock(new TallGrass()); - self::registerBlock(new DeadBush()); - //TODO: PISTON - //TODO: PISTONARMCOLLISION - self::registerBlock(new Wool()); - - self::registerBlock(new Dandelion()); - self::registerBlock(new Flower()); - self::registerBlock(new BrownMushroom()); - self::registerBlock(new RedMushroom()); - self::registerBlock(new Gold()); - self::registerBlock(new Iron()); - self::registerBlock(new DoubleStoneSlab()); - self::registerBlock(new StoneSlab()); - self::registerBlock(new Bricks()); - self::registerBlock(new TNT()); - self::registerBlock(new Bookshelf()); - self::registerBlock(new MossyCobblestone()); - self::registerBlock(new Obsidian()); - self::registerBlock(new Torch()); - self::registerBlock(new Fire()); - self::registerBlock(new MonsterSpawner()); - self::registerBlock(new WoodenStairs(Block::OAK_STAIRS, 0, "Oak Stairs")); - self::registerBlock(new Chest()); - //TODO: REDSTONE_WIRE - self::registerBlock(new DiamondOre()); - self::registerBlock(new Diamond()); - self::registerBlock(new CraftingTable()); - self::registerBlock(new Wheat()); - self::registerBlock(new Farmland()); - self::registerBlock(new Furnace()); - self::registerBlock(new BurningFurnace()); - self::registerBlock(new SignPost()); - self::registerBlock(new WoodenDoor(Block::OAK_DOOR_BLOCK, 0, "Oak Door", Item::OAK_DOOR)); - self::registerBlock(new Ladder()); - self::registerBlock(new Rail()); - self::registerBlock(new CobblestoneStairs()); - self::registerBlock(new WallSign()); - self::registerBlock(new Lever()); - self::registerBlock(new StonePressurePlate()); - self::registerBlock(new IronDoor()); - self::registerBlock(new WoodenPressurePlate()); - self::registerBlock(new RedstoneOre()); - self::registerBlock(new GlowingRedstoneOre()); - self::registerBlock(new RedstoneTorchUnlit()); - self::registerBlock(new RedstoneTorch()); - self::registerBlock(new StoneButton()); - self::registerBlock(new SnowLayer()); - self::registerBlock(new Ice()); - self::registerBlock(new Snow()); - self::registerBlock(new Cactus()); - self::registerBlock(new Clay()); - self::registerBlock(new Sugarcane()); - //TODO: JUKEBOX - self::registerBlock(new WoodenFence()); - self::registerBlock(new Pumpkin()); - self::registerBlock(new Netherrack()); - self::registerBlock(new SoulSand()); - self::registerBlock(new Glowstone()); - //TODO: PORTAL - self::registerBlock(new LitPumpkin()); - self::registerBlock(new Cake()); - //TODO: REPEATER_BLOCK - //TODO: POWERED_REPEATER - self::registerBlock(new InvisibleBedrock()); - self::registerBlock(new Trapdoor()); - //TODO: MONSTER_EGG - self::registerBlock(new StoneBricks()); - self::registerBlock(new BrownMushroomBlock()); - self::registerBlock(new RedMushroomBlock()); - self::registerBlock(new IronBars()); - self::registerBlock(new GlassPane()); - self::registerBlock(new Melon()); - self::registerBlock(new PumpkinStem()); - self::registerBlock(new MelonStem()); - self::registerBlock(new Vine()); - self::registerBlock(new FenceGate(Block::OAK_FENCE_GATE, 0, "Oak Fence Gate")); - self::registerBlock(new BrickStairs()); - self::registerBlock(new StoneBrickStairs()); - self::registerBlock(new Mycelium()); - self::registerBlock(new WaterLily()); - self::registerBlock(new NetherBrick(Block::NETHER_BRICK_BLOCK, 0, "Nether Bricks")); - self::registerBlock(new NetherBrickFence()); - self::registerBlock(new NetherBrickStairs()); - self::registerBlock(new NetherWartPlant()); - self::registerBlock(new EnchantingTable()); - self::registerBlock(new BrewingStand()); - //TODO: CAULDRON_BLOCK - //TODO: END_PORTAL - self::registerBlock(new EndPortalFrame()); - self::registerBlock(new EndStone()); - //TODO: DRAGON_EGG - self::registerBlock(new RedstoneLamp()); - self::registerBlock(new LitRedstoneLamp()); - //TODO: DROPPER - self::registerBlock(new ActivatorRail()); - self::registerBlock(new CocoaBlock()); - self::registerBlock(new SandstoneStairs()); - self::registerBlock(new EmeraldOre()); - self::registerBlock(new EnderChest()); - self::registerBlock(new TripwireHook()); - self::registerBlock(new Tripwire()); - self::registerBlock(new Emerald()); - self::registerBlock(new WoodenStairs(Block::SPRUCE_STAIRS, 0, "Spruce Stairs")); - self::registerBlock(new WoodenStairs(Block::BIRCH_STAIRS, 0, "Birch Stairs")); - self::registerBlock(new WoodenStairs(Block::JUNGLE_STAIRS, 0, "Jungle Stairs")); - //TODO: COMMAND_BLOCK - //TODO: BEACON - self::registerBlock(new CobblestoneWall()); - self::registerBlock(new FlowerPot()); - self::registerBlock(new Carrot()); - self::registerBlock(new Potato()); - self::registerBlock(new WoodenButton()); - self::registerBlock(new Skull()); - self::registerBlock(new Anvil()); - self::registerBlock(new TrappedChest()); - self::registerBlock(new WeightedPressurePlateLight()); - self::registerBlock(new WeightedPressurePlateHeavy()); - //TODO: COMPARATOR_BLOCK - //TODO: POWERED_COMPARATOR - self::registerBlock(new DaylightSensor()); - self::registerBlock(new Redstone()); - self::registerBlock(new NetherQuartzOre()); - //TODO: HOPPER_BLOCK - self::registerBlock(new Quartz()); - self::registerBlock(new QuartzStairs()); - self::registerBlock(new DoubleWoodenSlab()); - self::registerBlock(new WoodenSlab()); - self::registerBlock(new StainedClay()); - self::registerBlock(new StainedGlassPane()); - self::registerBlock(new Leaves2()); - self::registerBlock(new Wood2()); - self::registerBlock(new WoodenStairs(Block::ACACIA_STAIRS, 0, "Acacia Stairs")); - self::registerBlock(new WoodenStairs(Block::DARK_OAK_STAIRS, 0, "Dark Oak Stairs")); - //TODO: SLIME - - self::registerBlock(new IronTrapdoor()); - self::registerBlock(new Prismarine()); - self::registerBlock(new SeaLantern()); - self::registerBlock(new HayBale()); - self::registerBlock(new Carpet()); - self::registerBlock(new HardenedClay()); - self::registerBlock(new Coal()); - self::registerBlock(new PackedIce()); - self::registerBlock(new DoublePlant()); - self::registerBlock(new StandingBanner()); - self::registerBlock(new WallBanner()); - //TODO: DAYLIGHT_DETECTOR_INVERTED - self::registerBlock(new RedSandstone()); - self::registerBlock(new RedSandstoneStairs()); - self::registerBlock(new DoubleStoneSlab2()); - self::registerBlock(new StoneSlab2()); - self::registerBlock(new FenceGate(Block::SPRUCE_FENCE_GATE, 0, "Spruce Fence Gate")); - self::registerBlock(new FenceGate(Block::BIRCH_FENCE_GATE, 0, "Birch Fence Gate")); - self::registerBlock(new FenceGate(Block::JUNGLE_FENCE_GATE, 0, "Jungle Fence Gate")); - self::registerBlock(new FenceGate(Block::DARK_OAK_FENCE_GATE, 0, "Dark Oak Fence Gate")); - self::registerBlock(new FenceGate(Block::ACACIA_FENCE_GATE, 0, "Acacia Fence Gate")); - //TODO: REPEATING_COMMAND_BLOCK - //TODO: CHAIN_COMMAND_BLOCK - - self::registerBlock(new WoodenDoor(Block::SPRUCE_DOOR_BLOCK, 0, "Spruce Door", Item::SPRUCE_DOOR)); - self::registerBlock(new WoodenDoor(Block::BIRCH_DOOR_BLOCK, 0, "Birch Door", Item::BIRCH_DOOR)); - self::registerBlock(new WoodenDoor(Block::JUNGLE_DOOR_BLOCK, 0, "Jungle Door", Item::JUNGLE_DOOR)); - self::registerBlock(new WoodenDoor(Block::ACACIA_DOOR_BLOCK, 0, "Acacia Door", Item::ACACIA_DOOR)); - self::registerBlock(new WoodenDoor(Block::DARK_OAK_DOOR_BLOCK, 0, "Dark Oak Door", Item::DARK_OAK_DOOR)); - self::registerBlock(new GrassPath()); - self::registerBlock(new ItemFrame()); - //TODO: CHORUS_FLOWER - self::registerBlock(new Purpur()); - - self::registerBlock(new PurpurStairs()); - - //TODO: UNDYED_SHULKER_BOX - self::registerBlock(new EndStoneBricks()); - //TODO: FROSTED_ICE - self::registerBlock(new EndRod()); - //TODO: END_GATEWAY - - self::registerBlock(new Magma()); - self::registerBlock(new NetherWartBlock()); - self::registerBlock(new NetherBrick(Block::RED_NETHER_BRICK, 0, "Red Nether Bricks")); - self::registerBlock(new BoneBlock()); - - //TODO: SHULKER_BOX - self::registerBlock(new GlazedTerracotta(Block::PURPLE_GLAZED_TERRACOTTA, 0, "Purple Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::WHITE_GLAZED_TERRACOTTA, 0, "White Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::ORANGE_GLAZED_TERRACOTTA, 0, "Orange Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::MAGENTA_GLAZED_TERRACOTTA, 0, "Magenta Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::LIGHT_BLUE_GLAZED_TERRACOTTA, 0, "Light Blue Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::YELLOW_GLAZED_TERRACOTTA, 0, "Yellow Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::LIME_GLAZED_TERRACOTTA, 0, "Lime Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::PINK_GLAZED_TERRACOTTA, 0, "Pink Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::GRAY_GLAZED_TERRACOTTA, 0, "Grey Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::SILVER_GLAZED_TERRACOTTA, 0, "Light Grey Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::CYAN_GLAZED_TERRACOTTA, 0, "Cyan Glazed Terracotta")); - - self::registerBlock(new GlazedTerracotta(Block::BLUE_GLAZED_TERRACOTTA, 0, "Blue Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::BROWN_GLAZED_TERRACOTTA, 0, "Brown Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::GREEN_GLAZED_TERRACOTTA, 0, "Green Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::RED_GLAZED_TERRACOTTA, 0, "Red Glazed Terracotta")); - self::registerBlock(new GlazedTerracotta(Block::BLACK_GLAZED_TERRACOTTA, 0, "Black Glazed Terracotta")); - self::registerBlock(new Concrete()); - self::registerBlock(new ConcretePowder()); - - //TODO: CHORUS_PLANT - self::registerBlock(new StainedGlass()); - - self::registerBlock(new Podzol()); - self::registerBlock(new Beetroot()); - self::registerBlock(new Stonecutter()); - self::registerBlock(new GlowingObsidian()); - self::registerBlock(new NetherReactor()); - self::registerBlock(new InfoUpdate(Block::INFO_UPDATE, 0, "update!")); - self::registerBlock(new InfoUpdate(Block::INFO_UPDATE2, 0, "ate!upd")); - //TODO: MOVINGBLOCK - //TODO: OBSERVER - //TODO: STRUCTURE_BLOCK - - self::registerBlock(new Reserved6(Block::RESERVED6, 0, "reserved6")); - - for($id = 0, $size = self::$fullList->getSize() >> 4; $id < $size; ++$id){ - if(self::$fullList[$id << 4] === null){ - self::registerBlock(new UnknownBlock($id)); - } - } - } - - public static function isInit() : bool{ - return self::$fullList !== null; - } - - /** - * Registers a block type into the index. Plugins may use this method to register new block types or override - * existing ones. - * - * NOTE: If you are registering a new block type, you will need to add it to the creative inventory yourself - it - * will not automatically appear there. - * - * @param bool $override Whether to override existing registrations - * - * @throws \RuntimeException if something attempted to override an already-registered block without specifying the - * $override parameter. - */ - public static function registerBlock(Block $block, bool $override = false) : void{ - $id = $block->getId(); - - if(!$override and self::isRegistered($id)){ - throw new \RuntimeException("Trying to overwrite an already registered block"); - } - - for($meta = 0; $meta < 16; ++$meta){ - $variant = clone $block; - $variant->setDamage($meta); - self::$fullList[($id << 4) | $meta] = $variant; - } - - self::$solid[$id] = $block->isSolid(); - self::$transparent[$id] = $block->isTransparent(); - self::$hardness[$id] = $block->getHardness(); - self::$light[$id] = $block->getLightLevel(); - self::$lightFilter[$id] = min(15, $block->getLightFilter() + 1); //opacity plus 1 standard light filter - self::$diffusesSkyLight[$id] = $block->diffusesSkyLight(); - self::$blastResistance[$id] = $block->getBlastResistance(); - } - - /** - * Returns a new Block instance with the specified ID, meta and position. - */ - public static function get(int $id, int $meta = 0, Position $pos = null) : Block{ - if($meta < 0 or $meta > 0xf){ - throw new \InvalidArgumentException("Block meta value $meta is out of bounds"); - } - - try{ - if(self::$fullList !== null){ - $block = clone self::$fullList[($id << 4) | $meta]; - }else{ - $block = new UnknownBlock($id, $meta); - } - }catch(\RuntimeException $e){ - throw new \InvalidArgumentException("Block ID $id is out of bounds"); - } - - if($pos !== null){ - $block->x = $pos->getFloorX(); - $block->y = $pos->getFloorY(); - $block->z = $pos->getFloorZ(); - $block->level = $pos->level; - } - - return $block; - } - - /** - * @internal - * @phpstan-return \SplFixedArray - */ - public static function getBlockStatesArray() : \SplFixedArray{ - return self::$fullList; - } - - /** - * Returns whether a specified block ID is already registered in the block factory. - */ - public static function isRegistered(int $id) : bool{ - $b = self::$fullList[$id << 4]; - return $b !== null and !($b instanceof UnknownBlock); - } - - /** - * @internal - * @deprecated - */ - public static function toStaticRuntimeId(int $id, int $meta = 0) : int{ - return RuntimeBlockMapping::toStaticRuntimeId($id, $meta); - } - - /** - * @deprecated - * @internal - * - * @return int[] [id, meta] - */ - public static function fromStaticRuntimeId(int $runtimeId) : array{ - return RuntimeBlockMapping::fromStaticRuntimeId($runtimeId); - } -} diff --git a/src/pocketmine/block/BoneBlock.php b/src/pocketmine/block/BoneBlock.php deleted file mode 100644 index 364e8332b9..0000000000 --- a/src/pocketmine/block/BoneBlock.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Bone Block"; - } - - public function getHardness() : float{ - return 2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta = PillarRotationHelper::getMetaFromFace($this->meta, $face); - return $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - } - - public function getVariantBitmask() : int{ - return 0x03; - } -} diff --git a/src/pocketmine/block/BurningFurnace.php b/src/pocketmine/block/BurningFurnace.php deleted file mode 100644 index fdb61536c1..0000000000 --- a/src/pocketmine/block/BurningFurnace.php +++ /dev/null @@ -1,101 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Burning Furnace"; - } - - public function getHardness() : float{ - return 3.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getLightLevel() : int{ - return 13; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $faces = [ - 0 => 4, - 1 => 2, - 2 => 5, - 3 => 3 - ]; - $this->meta = $faces[$player instanceof Player ? $player->getDirection() : 0]; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - Tile::createTile(Tile::FURNACE, $this->getLevelNonNull(), TileFurnace::createNBT($this, $face, $item, $player)); - - return true; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - $furnace = $this->getLevelNonNull()->getTile($this); - if(!($furnace instanceof TileFurnace)){ - $furnace = Tile::createTile(Tile::FURNACE, $this->getLevelNonNull(), TileFurnace::createNBT($this)); - if(!($furnace instanceof TileFurnace)){ - return true; - } - } - - if(!$furnace->canOpenWith($item->getCustomName())){ - return true; - } - - $player->addWindow($furnace->getInventory()); - } - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Cactus.php b/src/pocketmine/block/Cactus.php deleted file mode 100644 index 2fd5fbff73..0000000000 --- a/src/pocketmine/block/Cactus.php +++ /dev/null @@ -1,136 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.4; - } - - public function hasEntityCollision() : bool{ - return true; - } - - public function getName() : string{ - return "Cactus"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - return new AxisAlignedBB( - $this->x + 0.0625, - $this->y + 0.0625, - $this->z + 0.0625, - $this->x + 0.9375, - $this->y + 0.9375, - $this->z + 0.9375 - ); - } - - public function onEntityCollide(Entity $entity) : void{ - $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_CONTACT, 1); - $entity->attack($ev); - } - - public function onNearbyBlockChange() : void{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() !== self::SAND and $down->getId() !== self::CACTUS){ - $this->getLevelNonNull()->useBreakOn($this); - }else{ - for($side = 2; $side <= 5; ++$side){ - $b = $this->getSide($side); - if($b->isSolid()){ - $this->getLevelNonNull()->useBreakOn($this); - break; - } - } - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() !== self::CACTUS){ - if($this->meta === 0x0f){ - for($y = 1; $y < 3; ++$y){ - $b = $this->getLevelNonNull()->getBlockAt($this->x, $this->y + $y, $this->z); - if($b->getId() === self::AIR){ - $ev = new BlockGrowEvent($b, BlockFactory::get(Block::CACTUS)); - $ev->call(); - if($ev->isCancelled()){ - break; - } - $this->getLevelNonNull()->setBlock($b, $ev->getNewState(), true); - }else{ - break; - } - } - $this->meta = 0; - $this->getLevelNonNull()->setBlock($this, $this); - }else{ - ++$this->meta; - $this->getLevelNonNull()->setBlock($this, $this); - } - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === self::SAND or $down->getId() === self::CACTUS){ - $block0 = $this->getSide(Vector3::SIDE_NORTH); - $block1 = $this->getSide(Vector3::SIDE_SOUTH); - $block2 = $this->getSide(Vector3::SIDE_WEST); - $block3 = $this->getSide(Vector3::SIDE_EAST); - if(!$block0->isSolid() and !$block1->isSolid() and !$block2->isSolid() and !$block3->isSolid()){ - $this->getLevelNonNull()->setBlock($this, $this, true); - - return true; - } - } - - return false; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Cake.php b/src/pocketmine/block/Cake.php deleted file mode 100644 index 0ed09f8d19..0000000000 --- a/src/pocketmine/block/Cake.php +++ /dev/null @@ -1,138 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getName() : string{ - return "Cake"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - $f = $this->getDamage() * 0.125; //1 slice width - - return new AxisAlignedBB( - $this->x + 0.0625 + $f, - $this->y, - $this->z + 0.0625, - $this->x + 1 - 0.0625, - $this->y + 0.5, - $this->z + 1 - 0.0625 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() !== self::AIR){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() === self::AIR){ //Replace with common break method - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), true); - } - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return []; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player !== null){ - $player->consumeObject($this); - return true; - } - - return false; - } - - public function getFoodRestore() : int{ - return 2; - } - - public function getSaturationRestore() : float{ - return 0.4; - } - - public function requiresHunger() : bool{ - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - /** - * @return Block - */ - public function getResidue(){ - $clone = clone $this; - $clone->meta++; - if($clone->meta > 0x06){ - $clone = BlockFactory::get(Block::AIR); - } - return $clone; - } - - /** - * @return EffectInstance[] - */ - public function getAdditionalEffects() : array{ - return []; - } - - public function onConsume(Living $consumer) : void{ - $this->level->setBlock($this, $this->getResidue()); - } -} diff --git a/src/pocketmine/block/Chest.php b/src/pocketmine/block/Chest.php deleted file mode 100644 index af2b8e4984..0000000000 --- a/src/pocketmine/block/Chest.php +++ /dev/null @@ -1,138 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 2.5; - } - - public function getName() : string{ - return "Chest"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - //these are slightly bigger than in PC - return new AxisAlignedBB( - $this->x + 0.025, - $this->y, - $this->z + 0.025, - $this->x + 0.975, - $this->y + 0.95, - $this->z + 0.975 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $faces = [ - 0 => 4, - 1 => 2, - 2 => 5, - 3 => 3 - ]; - - $chest = null; - $this->meta = $faces[$player instanceof Player ? $player->getDirection() : 0]; - - for($side = 2; $side <= 5; ++$side){ - if(($this->meta === 4 or $this->meta === 5) and ($side === 4 or $side === 5)){ - continue; - }elseif(($this->meta === 3 or $this->meta === 2) and ($side === 2 or $side === 3)){ - continue; - } - $c = $this->getSide($side); - if($c->getId() === $this->id and $c->getDamage() === $this->meta){ - $tile = $this->getLevelNonNull()->getTile($c); - if($tile instanceof TileChest and !$tile->isPaired()){ - $chest = $tile; - break; - } - } - } - - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - $tile = Tile::createTile(Tile::CHEST, $this->getLevelNonNull(), TileChest::createNBT($this, $face, $item, $player)); - - if($chest instanceof TileChest and $tile instanceof TileChest){ - $chest->pairWith($tile); - $tile->pairWith($chest); - } - - return true; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - - $t = $this->getLevelNonNull()->getTile($this); - $chest = null; - if($t instanceof TileChest){ - $chest = $t; - }else{ - $chest = Tile::createTile(Tile::CHEST, $this->getLevelNonNull(), TileChest::createNBT($this)); - if(!($chest instanceof TileChest)){ - return true; - } - } - - if( - !$this->getSide(Vector3::SIDE_UP)->isTransparent() or - (($pair = $chest->getPair()) !== null and !$pair->getBlock()->getSide(Vector3::SIDE_UP)->isTransparent()) or - !$chest->canOpenWith($item->getCustomName()) - ){ - return true; - } - - $player->addWindow($chest->getInventory()); - } - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getFuelTime() : int{ - return 300; - } -} diff --git a/src/pocketmine/block/Coal.php b/src/pocketmine/block/Coal.php deleted file mode 100644 index 68ccfe2396..0000000000 --- a/src/pocketmine/block/Coal.php +++ /dev/null @@ -1,63 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Coal Block"; - } - - public function getFuelTime() : int{ - return 16000; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 5; - } -} diff --git a/src/pocketmine/block/CoalOre.php b/src/pocketmine/block/CoalOre.php deleted file mode 100644 index b8c9634c4d..0000000000 --- a/src/pocketmine/block/CoalOre.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Coal Ore"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::COAL) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(0, 2); - } -} diff --git a/src/pocketmine/block/CobblestoneWall.php b/src/pocketmine/block/CobblestoneWall.php deleted file mode 100644 index 09699c1cd6..0000000000 --- a/src/pocketmine/block/CobblestoneWall.php +++ /dev/null @@ -1,120 +0,0 @@ -meta = $meta; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 2; - } - - public function getName() : string{ - static $names = [ - self::NONE_MOSSY_WALL => "Cobblestone", - self::MOSSY_WALL => "Mossy Cobblestone", - self::GRANITE_WALL => "Granite", - self::DIORITE_WALL => "Diorite", - self::ANDESITE_WALL => "Andesite", - self::SANDSTONE_WALL => "Sandstone", - self::BRICK_WALL => "Brick", - self::STONE_BRICK_WALL => "Stone Brick", - self::MOSSY_STONE_BRICK_WALL => "Mossy Stone Brick", - self::NETHER_BRICK_WALL => "Nether Brick", - self::END_STONE_BRICK_WALL => "End Stone Brick", - self::PRISMARINE_WALL => "Prismarine", - self::RED_SANDSTONE_WALL => "Red Sandstone", - self::RED_NETHER_BRICK_WALL => "Red Nether Brick" - ]; - return ($names[$this->getVariant()] ?? "Unknown") . " Wall"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - //walls don't have any special collision boxes like fences do - - $north = $this->canConnect($this->getSide(Vector3::SIDE_NORTH)); - $south = $this->canConnect($this->getSide(Vector3::SIDE_SOUTH)); - $west = $this->canConnect($this->getSide(Vector3::SIDE_WEST)); - $east = $this->canConnect($this->getSide(Vector3::SIDE_EAST)); - - $inset = 0.25; - if( - $this->getSide(Vector3::SIDE_UP)->getId() === Block::AIR and //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) - ) - ){ - //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 - $inset = 0.3125; - } - - return new AxisAlignedBB( - $this->x + ($west ? 0 : $inset), - $this->y, - $this->z + ($north ? 0 : $inset), - $this->x + 1 - ($east ? 0 : $inset), - $this->y + 1.5, - $this->z + 1 - ($south ? 0 : $inset) - ); - } - - /** - * @return bool - */ - public function canConnect(Block $block){ - return $block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent()); - } -} diff --git a/src/pocketmine/block/Cobweb.php b/src/pocketmine/block/Cobweb.php deleted file mode 100644 index c20cc35f57..0000000000 --- a/src/pocketmine/block/Cobweb.php +++ /dev/null @@ -1,71 +0,0 @@ -meta = $meta; - } - - public function hasEntityCollision() : bool{ - return true; - } - - public function getName() : string{ - return "Cobweb"; - } - - public function getHardness() : float{ - return 4; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SWORD | BlockToolType::TYPE_SHEARS; - } - - public function getToolHarvestLevel() : int{ - return 1; - } - - public function onEntityCollide(Entity $entity) : void{ - $entity->resetFallDistance(); - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::STRING) - ]; - } - - public function diffusesSkyLight() : bool{ - return true; - } -} diff --git a/src/pocketmine/block/Concrete.php b/src/pocketmine/block/Concrete.php deleted file mode 100644 index e00f0d68a1..0000000000 --- a/src/pocketmine/block/Concrete.php +++ /dev/null @@ -1,52 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return ColorBlockMetaHelper::getColorFromMeta($this->getVariant()) . " Concrete"; - } - - public function getHardness() : float{ - return 1.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/CraftingTable.php b/src/pocketmine/block/CraftingTable.php deleted file mode 100644 index 0d898384ca..0000000000 --- a/src/pocketmine/block/CraftingTable.php +++ /dev/null @@ -1,77 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 2.5; - } - - public function getName() : string{ - return "Crafting Table"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - $player->setCraftingGrid(new CraftingGrid($player, CraftingGrid::SIZE_BIG)); - - if(!array_key_exists($windowId = Player::HARDCODED_CRAFTING_GRID_WINDOW_ID, $player->openHardcodedWindows)){ - //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 - $pk = new ContainerOpenPacket(); - $pk->windowId = $windowId; - $pk->type = WindowTypes::WORKBENCH; - $pk->x = $this->getFloorX(); - $pk->y = $this->getFloorY(); - $pk->z = $this->getFloorZ(); - $player->sendDataPacket($pk); - $player->openHardcodedWindows[$windowId] = true; - } - } - - return true; - } - - public function getFuelTime() : int{ - return 300; - } -} diff --git a/src/pocketmine/block/Crops.php b/src/pocketmine/block/Crops.php deleted file mode 100644 index 81afd0c17d..0000000000 --- a/src/pocketmine/block/Crops.php +++ /dev/null @@ -1,93 +0,0 @@ -getSide(Vector3::SIDE_DOWN)->getId() === Block::FARMLAND){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - return false; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($this->meta < 7 and $item->getId() === Item::DYE and $item->getDamage() === 0x0F){ //Bonemeal - $block = clone $this; - $block->meta += mt_rand(2, 5); - if($block->meta > 7){ - $block->meta = 7; - } - - $ev = new BlockGrowEvent($this, $block); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($this, $ev->getNewState(), true, true); - } - - $item->pop(); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() !== Block::FARMLAND){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if(mt_rand(0, 2) === 1){ - if($this->meta < 0x07){ - $block = clone $this; - ++$block->meta; - $ev = new BlockGrowEvent($this, $block); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($this, $ev->getNewState(), true, true); - } - } - } - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } -} diff --git a/src/pocketmine/block/Diamond.php b/src/pocketmine/block/Diamond.php deleted file mode 100644 index 8deea0468b..0000000000 --- a/src/pocketmine/block/Diamond.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 5; - } - - public function getName() : string{ - return "Diamond Block"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } -} diff --git a/src/pocketmine/block/DiamondOre.php b/src/pocketmine/block/DiamondOre.php deleted file mode 100644 index 0fa957dc0c..0000000000 --- a/src/pocketmine/block/DiamondOre.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 3; - } - - public function getName() : string{ - return "Diamond Ore"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::DIAMOND) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(3, 7); - } -} diff --git a/src/pocketmine/block/Dirt.php b/src/pocketmine/block/Dirt.php deleted file mode 100644 index 9b00df1e5e..0000000000 --- a/src/pocketmine/block/Dirt.php +++ /dev/null @@ -1,67 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getName() : string{ - if($this->meta === 1){ - return "Coarse Dirt"; - } - return "Dirt"; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($item instanceof Hoe){ - $item->applyDamage(1); - if($this->meta === 1){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::DIRT), true); - }else{ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::FARMLAND), true); - } - - return true; - } - - return false; - } -} diff --git a/src/pocketmine/block/Door.php b/src/pocketmine/block/Door.php deleted file mode 100644 index d4de03dc40..0000000000 --- a/src/pocketmine/block/Door.php +++ /dev/null @@ -1,292 +0,0 @@ -getDamage(); - $isUp = ($damage & 0x08) > 0; - - if($isUp){ - $down = $this->getSide(Vector3::SIDE_DOWN)->getDamage(); - $up = $damage; - }else{ - $down = $damage; - $up = $this->getSide(Vector3::SIDE_UP)->getDamage(); - } - - $isRight = ($up & 0x01) > 0; - - return $down & 0x07 | ($isUp ? 8 : 0) | ($isRight ? 0x10 : 0); - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - $f = 0.1875; - $damage = $this->getFullDamage(); - - $bb = new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 2, - $this->z + 1 - ); - - $j = $damage & 0x03; - $isOpen = (($damage & 0x04) > 0); - $isRight = (($damage & 0x10) > 0); - - if($j === 0){ - if($isOpen){ - if(!$isRight){ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + $f - ); - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z + 1 - $f, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + $f, - $this->y + 1, - $this->z + 1 - ); - } - }elseif($j === 1){ - if($isOpen){ - if(!$isRight){ - $bb->setBounds( - $this->x + 1 - $f, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + $f, - $this->y + 1, - $this->z + 1 - ); - } - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + $f - ); - } - }elseif($j === 2){ - if($isOpen){ - if(!$isRight){ - $bb->setBounds( - $this->x, - $this->y, - $this->z + 1 - $f, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + $f - ); - } - }else{ - $bb->setBounds( - $this->x + 1 - $f, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - }elseif($j === 3){ - if($isOpen){ - if(!$isRight){ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + $f, - $this->y + 1, - $this->z + 1 - ); - }else{ - $bb->setBounds( - $this->x + 1 - $f, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - }else{ - $bb->setBounds( - $this->x, - $this->y, - $this->z + 1 - $f, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - } - - return $bb; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() === self::AIR){ //Replace with common break method - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), false); - if($this->getSide(Vector3::SIDE_UP) instanceof Door){ - $this->getLevelNonNull()->setBlock($this->getSide(Vector3::SIDE_UP), BlockFactory::get(Block::AIR), false); - } - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face === Vector3::SIDE_UP){ - $blockUp = $this->getSide(Vector3::SIDE_UP); - $blockDown = $this->getSide(Vector3::SIDE_DOWN); - if(!$blockUp->canBeReplaced() or $blockDown->isTransparent()){ - return false; - } - $direction = $player instanceof Player ? $player->getDirection() : 0; - $faces = [ - 0 => 3, - 1 => 4, - 2 => 2, - 3 => 5 - ]; - $next = $this->getSide($faces[($direction + 2) % 4]); - $next2 = $this->getSide($faces[$direction]); - $metaUp = 0x08; - if($next->getId() === $this->getId() or (!$next2->isTransparent() and $next->isTransparent())){ //Door hinge - $metaUp |= 0x01; - } - - $this->setDamage($player->getDirection() & 0x03); - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); //Bottom - $this->getLevelNonNull()->setBlock($blockUp, BlockFactory::get($this->getId(), $metaUp), true); //Top - return true; - } - - return false; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if(($this->getDamage() & 0x08) === 0x08){ //Top - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === $this->getId()){ - $meta = $down->getDamage() ^ 0x04; - $this->level->setBlock($down, BlockFactory::get($this->getId(), $meta), true); - $this->level->addSound(new DoorSound($this)); - return true; - } - - return false; - }else{ - $this->meta ^= 0x04; - $this->level->setBlock($this, $this, true); - $this->level->addSound(new DoorSound($this)); - } - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - if(($this->meta & 0x08) === 0){ //bottom half only - return parent::getDropsForCompatibleTool($item); - } - - return []; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function getAffectedBlocks() : array{ - if(($this->getDamage() & 0x08) === 0x08){ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === $this->getId()){ - return [$this, $down]; - } - }else{ - $up = $this->getSide(Vector3::SIDE_UP); - if($up->getId() === $this->getId()){ - return [$this, $up]; - } - } - - return parent::getAffectedBlocks(); - } -} diff --git a/src/pocketmine/block/DoublePlant.php b/src/pocketmine/block/DoublePlant.php deleted file mode 100644 index c27b53ef10..0000000000 --- a/src/pocketmine/block/DoublePlant.php +++ /dev/null @@ -1,135 +0,0 @@ -meta = $meta; - } - - public function canBeReplaced() : bool{ - return $this->getVariant() === 2 or $this->getVariant() === 3; //grass or fern - } - - public function getName() : string{ - static $names = [ - 0 => "Sunflower", - 1 => "Lilac", - 2 => "Double Tallgrass", - 3 => "Large Fern", - 4 => "Rose Bush", - 5 => "Peony" - ]; - return $names[$this->getVariant()] ?? ""; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $id = $blockReplace->getSide(Vector3::SIDE_DOWN)->getId(); - if(($id === Block::GRASS or $id === Block::DIRT) and $blockReplace->getSide(Vector3::SIDE_UP)->canBeReplaced()){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, false, false); - $this->getLevelNonNull()->setBlock($blockReplace->getSide(Vector3::SIDE_UP), BlockFactory::get($this->id, $this->meta | self::BITFLAG_TOP), false, false); - - return true; - } - - return false; - } - - /** - * Returns whether this double-plant has a corresponding other half. - */ - public function isValidHalfPlant() : bool{ - if(($this->meta & self::BITFLAG_TOP) !== 0){ - $other = $this->getSide(Vector3::SIDE_DOWN); - }else{ - $other = $this->getSide(Vector3::SIDE_UP); - } - - return ( - $other->getId() === $this->getId() and - $other->getVariant() === $this->getVariant() and - ($other->getDamage() & self::BITFLAG_TOP) !== ($this->getDamage() & self::BITFLAG_TOP) - ); - } - - public function onNearbyBlockChange() : void{ - if(!$this->isValidHalfPlant() or (($this->meta & self::BITFLAG_TOP) === 0 and $this->getSide(Vector3::SIDE_DOWN)->isTransparent())){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function getVariantBitmask() : int{ - return 0x07; - } - - public function getToolType() : int{ - return ($this->getVariant() === 2 or $this->getVariant() === 3) ? BlockToolType::TYPE_SHEARS : BlockToolType::TYPE_NONE; - } - - public function getToolHarvestLevel() : int{ - return ($this->getVariant() === 2 or $this->getVariant() === 3) ? 1 : 0; //only grass or fern require shears - } - - public function getDrops(Item $item) : array{ - if(($this->meta & self::BITFLAG_TOP) !== 0){ - if($this->isCompatibleWithTool($item)){ - return parent::getDrops($item); - } - - if(mt_rand(0, 24) === 0){ - return [ - ItemFactory::get(Item::SEEDS) - ]; - } - } - - return []; - } - - public function getAffectedBlocks() : array{ - if($this->isValidHalfPlant()){ - return [$this, $this->getSide(($this->meta & self::BITFLAG_TOP) !== 0 ? Vector3::SIDE_DOWN : Vector3::SIDE_UP)]; - } - - return parent::getAffectedBlocks(); - } - - public function getFlameEncouragement() : int{ - return 60; - } - - public function getFlammability() : int{ - return 100; - } -} diff --git a/src/pocketmine/block/Emerald.php b/src/pocketmine/block/Emerald.php deleted file mode 100644 index 7c5051051f..0000000000 --- a/src/pocketmine/block/Emerald.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } - - public function getName() : string{ - return "Emerald Block"; - } -} diff --git a/src/pocketmine/block/EmeraldOre.php b/src/pocketmine/block/EmeraldOre.php deleted file mode 100644 index 4daaece69c..0000000000 --- a/src/pocketmine/block/EmeraldOre.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Emerald Ore"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } - - public function getHardness() : float{ - return 3; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::EMERALD) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(3, 7); - } -} diff --git a/src/pocketmine/block/EnchantingTable.php b/src/pocketmine/block/EnchantingTable.php deleted file mode 100644 index 2724ff51e0..0000000000 --- a/src/pocketmine/block/EnchantingTable.php +++ /dev/null @@ -1,79 +0,0 @@ -meta = $meta; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - Tile::createTile(Tile::ENCHANT_TABLE, $this->getLevelNonNull(), TileEnchantTable::createNBT($this, $face, $item, $player)); - - return true; - } - - public function getHardness() : float{ - return 5; - } - - public function getBlastResistance() : float{ - return 6000; - } - - public function getName() : string{ - return "Enchanting Table"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - //TODO lock - - $player->addWindow(new EnchantInventory($this)); - } - - return true; - } -} diff --git a/src/pocketmine/block/EndPortalFrame.php b/src/pocketmine/block/EndPortalFrame.php deleted file mode 100644 index 5ecf295560..0000000000 --- a/src/pocketmine/block/EndPortalFrame.php +++ /dev/null @@ -1,68 +0,0 @@ -meta = $meta; - } - - public function getLightLevel() : int{ - return 1; - } - - public function getName() : string{ - return "End Portal Frame"; - } - - public function getHardness() : float{ - return -1; - } - - public function getBlastResistance() : float{ - return 18000000; - } - - public function isBreakable(Item $item) : bool{ - return false; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + (($this->getDamage() & 0x04) > 0 ? 1 : 0.8125), - $this->z + 1 - ); - } -} diff --git a/src/pocketmine/block/EndRod.php b/src/pocketmine/block/EndRod.php deleted file mode 100644 index 91251ab759..0000000000 --- a/src/pocketmine/block/EndRod.php +++ /dev/null @@ -1,104 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "End Rod"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face === Vector3::SIDE_UP or $face === Vector3::SIDE_DOWN){ - $this->meta = $face; - }else{ - $this->meta = $face ^ 0x01; - } - if($blockClicked instanceof EndRod and $blockClicked->getDamage() === $this->meta){ - $this->meta ^= 0x01; - } - - return $this->level->setBlock($blockReplace, $this, true, true); - } - - public function isSolid() : bool{ - return true; - } - - public function getLightLevel() : int{ - return 14; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - $m = $this->meta & ~0x01; - $width = 0.375; - - switch($m){ - case 0x00: //up/down - return new AxisAlignedBB( - $this->x + $width, - $this->y, - $this->z + $width, - $this->x + 1 - $width, - $this->y + 1, - $this->z + 1 - $width - ); - case 0x02: //north/south - return new AxisAlignedBB( - $this->x, - $this->y + $width, - $this->z + $width, - $this->x + 1, - $this->y + 1 - $width, - $this->z + 1 - $width - ); - case 0x04: //east/west - return new AxisAlignedBB( - $this->x + $width, - $this->y + $width, - $this->z, - $this->x + 1 - $width, - $this->y + 1 - $width, - $this->z + 1 - ); - } - - return null; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/EndStone.php b/src/pocketmine/block/EndStone.php deleted file mode 100644 index d7147f8de5..0000000000 --- a/src/pocketmine/block/EndStone.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "End Stone"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 3; - } -} diff --git a/src/pocketmine/block/EndStoneBricks.php b/src/pocketmine/block/EndStoneBricks.php deleted file mode 100644 index 87ba1cb29d..0000000000 --- a/src/pocketmine/block/EndStoneBricks.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "End Stone Bricks"; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/EnderChest.php b/src/pocketmine/block/EnderChest.php deleted file mode 100644 index 00438b4724..0000000000 --- a/src/pocketmine/block/EnderChest.php +++ /dev/null @@ -1,112 +0,0 @@ - 4, - 1 => 2, - 2 => 5, - 3 => 3 - ]; - - $this->meta = $faces[$player instanceof Player ? $player->getDirection() : 0]; - - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - Tile::createTile(Tile::ENDER_CHEST, $this->getLevelNonNull(), TileEnderChest::createNBT($this, $face, $item, $player)); - - return true; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($player instanceof Player){ - - $t = $this->getLevelNonNull()->getTile($this); - $enderChest = null; - if($t instanceof TileEnderChest){ - $enderChest = $t; - }else{ - $enderChest = Tile::createTile(Tile::ENDER_CHEST, $this->getLevelNonNull(), TileEnderChest::createNBT($this)); - if(!($enderChest instanceof TileEnderChest)){ - return true; - } - } - - if(!$this->getSide(Vector3::SIDE_UP)->isTransparent()){ - return true; - } - - $player->getEnderChestInventory()->setHolderPosition($enderChest); - $player->addWindow($player->getEnderChestInventory()); - } - - return true; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::OBSIDIAN, 0, 8) - ]; - } - - public function getFuelTime() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Farmland.php b/src/pocketmine/block/Farmland.php deleted file mode 100644 index 243f8af3cd..0000000000 --- a/src/pocketmine/block/Farmland.php +++ /dev/null @@ -1,117 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Farmland"; - } - - public function getHardness() : float{ - return 0.6; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, //TODO: this should be 0.9375, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109) - $this->z + 1 - ); - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_UP)->isSolid()){ - $this->level->setBlock($this, BlockFactory::get(Block::DIRT), true); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if(!$this->canHydrate()){ - if($this->meta > 0){ - $this->meta--; - $this->level->setBlock($this, $this, false, false); - }else{ - $this->level->setBlock($this, BlockFactory::get(Block::DIRT), false, true); - } - }elseif($this->meta < 7){ - $this->meta = 7; - $this->level->setBlock($this, $this, false, false); - } - } - - protected function canHydrate() : bool{ - //TODO: check rain - $start = $this->add(-4, 0, -4); - $end = $this->add(4, 1, 4); - for($y = $start->y; $y <= $end->y; ++$y){ - for($z = $start->z; $z <= $end->z; ++$z){ - for($x = $start->x; $x <= $end->x; ++$x){ - $id = $this->level->getBlockIdAt($x, $y, $z); - if($id === Block::STILL_WATER or $id === Block::FLOWING_WATER){ - return true; - } - } - } - } - - return false; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::DIRT) - ]; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function getPickedItem() : Item{ - return ItemFactory::get(Item::DIRT); - } -} diff --git a/src/pocketmine/block/Fence.php b/src/pocketmine/block/Fence.php deleted file mode 100644 index 8fd322c887..0000000000 --- a/src/pocketmine/block/Fence.php +++ /dev/null @@ -1,112 +0,0 @@ -meta = $meta; - } - - public function getThickness() : float{ - return 0.25; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - $width = 0.5 - $this->getThickness() / 2; - - return new AxisAlignedBB( - $this->x + ($this->canConnect($this->getSide(Vector3::SIDE_WEST)) ? 0 : $width), - $this->y, - $this->z + ($this->canConnect($this->getSide(Vector3::SIDE_NORTH)) ? 0 : $width), - $this->x + 1 - ($this->canConnect($this->getSide(Vector3::SIDE_EAST)) ? 0 : $width), - $this->y + 1.5, - $this->z + 1 - ($this->canConnect($this->getSide(Vector3::SIDE_SOUTH)) ? 0 : $width) - ); - } - - protected function recalculateCollisionBoxes() : array{ - $inset = 0.5 - $this->getThickness() / 2; - - /** @var AxisAlignedBB[] $bbs */ - $bbs = []; - - $connectWest = $this->canConnect($this->getSide(Vector3::SIDE_WEST)); - $connectEast = $this->canConnect($this->getSide(Vector3::SIDE_EAST)); - - if($connectWest or $connectEast){ - //X axis (west/east) - $bbs[] = new AxisAlignedBB( - $this->x + ($connectWest ? 0 : $inset), - $this->y, - $this->z + $inset, - $this->x + 1 - ($connectEast ? 0 : $inset), - $this->y + 1.5, - $this->z + 1 - $inset - ); - } - - $connectNorth = $this->canConnect($this->getSide(Vector3::SIDE_NORTH)); - $connectSouth = $this->canConnect($this->getSide(Vector3::SIDE_SOUTH)); - - if($connectNorth or $connectSouth){ - //Z axis (north/south) - $bbs[] = new AxisAlignedBB( - $this->x + $inset, - $this->y, - $this->z + ($connectNorth ? 0 : $inset), - $this->x + 1 - $inset, - $this->y + 1.5, - $this->z + 1 - ($connectSouth ? 0 : $inset) - ); - } - - if(count($bbs) === 0){ - //centre post AABB (only needed if not connected on any axis - other BBs overlapping will do this if any connections are made) - return [ - new AxisAlignedBB( - $this->x + $inset, - $this->y, - $this->z + $inset, - $this->x + 1 - $inset, - $this->y + 1.5, - $this->z + 1 - $inset - ) - ]; - } - - return $bbs; - } - - /** - * @return bool - */ - public function canConnect(Block $block){ - return $block instanceof static or $block instanceof FenceGate or ($block->isSolid() and !$block->isTransparent()); - } -} diff --git a/src/pocketmine/block/FenceGate.php b/src/pocketmine/block/FenceGate.php deleted file mode 100644 index 66b135f705..0000000000 --- a/src/pocketmine/block/FenceGate.php +++ /dev/null @@ -1,104 +0,0 @@ -getDamage() & 0x04) > 0){ - return null; - } - - $i = ($this->getDamage() & 0x03); - if($i === 2 or $i === 0){ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z + 0.375, - $this->x + 1, - $this->y + 1.5, - $this->z + 0.625 - ); - }else{ - return new AxisAlignedBB( - $this->x + 0.375, - $this->y, - $this->z, - $this->x + 0.625, - $this->y + 1.5, - $this->z + 1 - ); - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta = ($player instanceof Player ? ($player->getDirection() - 1) & 0x03 : 0); - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - $this->meta = (($this->meta ^ 0x04) & ~0x02); - - if($player !== null){ - $this->meta |= (($player->getDirection() - 1) & 0x02); - } - - $this->getLevelNonNull()->setBlock($this, $this, true); - $this->level->addSound(new DoorSound($this)); - return true; - } - - public function getFuelTime() : int{ - return 300; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 20; - } -} diff --git a/src/pocketmine/block/Flower.php b/src/pocketmine/block/Flower.php deleted file mode 100644 index 7a1b5baf94..0000000000 --- a/src/pocketmine/block/Flower.php +++ /dev/null @@ -1,86 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - static $names = [ - self::TYPE_POPPY => "Poppy", - self::TYPE_BLUE_ORCHID => "Blue Orchid", - self::TYPE_ALLIUM => "Allium", - self::TYPE_AZURE_BLUET => "Azure Bluet", - self::TYPE_RED_TULIP => "Red Tulip", - self::TYPE_ORANGE_TULIP => "Orange Tulip", - self::TYPE_WHITE_TULIP => "White Tulip", - self::TYPE_PINK_TULIP => "Pink Tulip", - self::TYPE_OXEYE_DAISY => "Oxeye Daisy" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === Block::GRASS or $down->getId() === Block::DIRT or $down->getId() === Block::FARMLAND){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function getFlameEncouragement() : int{ - return 60; - } - - public function getFlammability() : int{ - return 100; - } -} diff --git a/src/pocketmine/block/FlowerPot.php b/src/pocketmine/block/FlowerPot.php deleted file mode 100644 index 5e1a41d483..0000000000 --- a/src/pocketmine/block/FlowerPot.php +++ /dev/null @@ -1,113 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Flower Pot"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x + 0.3125, - $this->y, - $this->z + 0.3125, - $this->x + 0.6875, - $this->y + 0.375, - $this->z + 0.6875 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - return false; - } - - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - Tile::createTile(Tile::FLOWER_POT, $this->getLevelNonNull(), TileFlowerPot::createNBT($this, $face, $item, $player)); - return true; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function onActivate(Item $item, Player $player = null) : bool{ - $pot = $this->getLevelNonNull()->getTile($this); - if(!($pot instanceof TileFlowerPot)){ - return false; - } - if(!$pot->canAddItem($item)){ - return true; - } - - $this->setDamage(self::STATE_FULL); //specific damage value is unnecessary, it just needs to be non-zero to show an item. - $this->getLevelNonNull()->setBlock($this, $this, true, false); - $pot->setItem($item->pop()); - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - $items = parent::getDropsForCompatibleTool($item); - - $tile = $this->getLevelNonNull()->getTile($this); - if($tile instanceof TileFlowerPot){ - $item = $tile->getItem(); - if($item->getId() !== Item::AIR){ - $items[] = $item; - } - } - - return $items; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } -} diff --git a/src/pocketmine/block/Gold.php b/src/pocketmine/block/Gold.php deleted file mode 100644 index d9b7457953..0000000000 --- a/src/pocketmine/block/Gold.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Gold Block"; - } - - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } -} diff --git a/src/pocketmine/block/GoldOre.php b/src/pocketmine/block/GoldOre.php deleted file mode 100644 index a776e01fd7..0000000000 --- a/src/pocketmine/block/GoldOre.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Gold Ore"; - } - - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } -} diff --git a/src/pocketmine/block/Grass.php b/src/pocketmine/block/Grass.php deleted file mode 100644 index 8313cc78cb..0000000000 --- a/src/pocketmine/block/Grass.php +++ /dev/null @@ -1,120 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Grass"; - } - - public function getHardness() : float{ - return 0.6; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::DIRT) - ]; - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - $lightAbove = $this->level->getFullLightAt($this->x, $this->y + 1, $this->z); - if($lightAbove < 4 and BlockFactory::$lightFilter[$this->level->getBlockIdAt($this->x, $this->y + 1, $this->z)] >= 3){ //2 plus 1 standard filter amount - //grass dies - $ev = new BlockSpreadEvent($this, $this, BlockFactory::get(Block::DIRT)); - $ev->call(); - if(!$ev->isCancelled()){ - $this->level->setBlock($this, $ev->getNewState(), false, false); - } - }elseif($lightAbove >= 9){ - //try grass spread - for($i = 0; $i < 4; ++$i){ - $x = mt_rand($this->x - 1, $this->x + 1); - $y = mt_rand($this->y - 3, $this->y + 1); - $z = mt_rand($this->z - 1, $this->z + 1); - if( - $this->level->getBlockIdAt($x, $y, $z) !== Block::DIRT or - $this->level->getBlockDataAt($x, $y, $z) === 1 or - $this->level->getFullLightAt($x, $y + 1, $z) < 4 or - BlockFactory::$lightFilter[$this->level->getBlockIdAt($x, $y + 1, $z)] >= 3 - ){ - continue; - } - - $ev = new BlockSpreadEvent($b = $this->level->getBlockAt($x, $y, $z), $this, BlockFactory::get(Block::GRASS)); - $ev->call(); - if(!$ev->isCancelled()){ - $this->level->setBlock($b, $ev->getNewState(), false, false); - } - } - } - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($item->getId() === Item::DYE and $item->getDamage() === 0x0F){ - $item->pop(); - TallGrassObject::growGrass($this->getLevelNonNull(), $this, new Random(mt_rand()), 8, 2); - - return true; - }elseif($item instanceof Hoe){ - $item->applyDamage(1); - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::FARMLAND)); - - return true; - }elseif($item instanceof Shovel and $this->getSide(Vector3::SIDE_UP)->getId() === Block::AIR){ - $item->applyDamage(1); - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::GRASS_PATH)); - - return true; - } - - return false; - } -} diff --git a/src/pocketmine/block/GrassPath.php b/src/pocketmine/block/GrassPath.php deleted file mode 100644 index 712f3e5473..0000000000 --- a/src/pocketmine/block/GrassPath.php +++ /dev/null @@ -1,73 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Grass Path"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, //TODO: this should be 0.9375, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109) - $this->z + 1 - ); - } - - public function getHardness() : float{ - return 0.6; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_UP)->isSolid()){ - $this->level->setBlock($this, BlockFactory::get(Block::DIRT), true); - } - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::DIRT) - ]; - } -} diff --git a/src/pocketmine/block/HardenedClay.php b/src/pocketmine/block/HardenedClay.php deleted file mode 100644 index 3a3e6896e6..0000000000 --- a/src/pocketmine/block/HardenedClay.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Hardened Clay"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 1.25; - } -} diff --git a/src/pocketmine/block/Iron.php b/src/pocketmine/block/Iron.php deleted file mode 100644 index 3f0a0786f3..0000000000 --- a/src/pocketmine/block/Iron.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Iron Block"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_STONE; - } - - public function getHardness() : float{ - return 5; - } -} diff --git a/src/pocketmine/block/IronBars.php b/src/pocketmine/block/IronBars.php deleted file mode 100644 index 52b4869ce2..0000000000 --- a/src/pocketmine/block/IronBars.php +++ /dev/null @@ -1,55 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Iron Bars"; - } - - public function getHardness() : float{ - return 5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/IronDoor.php b/src/pocketmine/block/IronDoor.php deleted file mode 100644 index 2d263faed5..0000000000 --- a/src/pocketmine/block/IronDoor.php +++ /dev/null @@ -1,54 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Iron Door"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 5; - } -} diff --git a/src/pocketmine/block/IronOre.php b/src/pocketmine/block/IronOre.php deleted file mode 100644 index 003e8384d0..0000000000 --- a/src/pocketmine/block/IronOre.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Iron Ore"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_STONE; - } - - public function getHardness() : float{ - return 3; - } -} diff --git a/src/pocketmine/block/IronTrapdoor.php b/src/pocketmine/block/IronTrapdoor.php deleted file mode 100644 index 72edd577a7..0000000000 --- a/src/pocketmine/block/IronTrapdoor.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Item Frame"; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - $tile = $this->level->getTile($this); - if(!($tile instanceof TileItemFrame)){ - $tile = Tile::createTile(Tile::ITEM_FRAME, $this->getLevelNonNull(), TileItemFrame::createNBT($this)); - if(!($tile instanceof TileItemFrame)){ - return true; - } - } - - if($tile->hasItem()){ - $tile->setItemRotation(($tile->getItemRotation() + 1) % 8); - }elseif(!$item->isNull()){ - $tile->setItem($item->pop()); - } - - return true; - } - - public function onNearbyBlockChange() : void{ - $sides = [ - 0 => Vector3::SIDE_WEST, - 1 => Vector3::SIDE_EAST, - 2 => Vector3::SIDE_NORTH, - 3 => Vector3::SIDE_SOUTH - ]; - if(isset($sides[$this->meta]) and !$this->getSide($sides[$this->meta])->isSolid()){ - $this->level->useBreakOn($this); - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face === Vector3::SIDE_DOWN or $face === Vector3::SIDE_UP or !$blockClicked->isSolid()){ - return false; - } - - $faces = [ - Vector3::SIDE_NORTH => 3, - Vector3::SIDE_SOUTH => 2, - Vector3::SIDE_WEST => 1, - Vector3::SIDE_EAST => 0 - ]; - - $this->meta = $faces[$face]; - $this->level->setBlock($blockReplace, $this, true, true); - - Tile::createTile(Tile::ITEM_FRAME, $this->getLevelNonNull(), TileItemFrame::createNBT($this, $face, $item, $player)); - - return true; - - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - $drops = parent::getDropsForCompatibleTool($item); - - $tile = $this->level->getTile($this); - if($tile instanceof TileItemFrame){ - $tileItem = $tile->getItem(); - if(lcg_value() <= $tile->getItemDropChance() and !$tileItem->isNull()){ - $drops[] = $tileItem; - } - } - - return $drops; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function getHardness() : float{ - return 0.25; - } -} diff --git a/src/pocketmine/block/Ladder.php b/src/pocketmine/block/Ladder.php deleted file mode 100644 index a84258e4c1..0000000000 --- a/src/pocketmine/block/Ladder.php +++ /dev/null @@ -1,126 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Ladder"; - } - - public function hasEntityCollision() : bool{ - return true; - } - - public function isSolid() : bool{ - return false; - } - - public function getHardness() : float{ - return 0.4; - } - - public function canClimb() : bool{ - return true; - } - - public function onEntityCollide(Entity $entity) : void{ - if($entity instanceof Living and $entity->asVector3()->floor()->distanceSquared($this) < 1){ //entity coordinates must be inside block - $entity->resetFallDistance(); - $entity->onGround = true; - } - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - $f = 0.1875; - - $minX = $minZ = 0; - $maxX = $maxZ = 1; - - if($this->meta === 2){ - $minZ = 1 - $f; - }elseif($this->meta === 3){ - $maxZ = $f; - }elseif($this->meta === 4){ - $minX = 1 - $f; - }elseif($this->meta === 5){ - $maxX = $f; - } - - return new AxisAlignedBB( - $this->x + $minX, - $this->y, - $this->z + $minZ, - $this->x + $maxX, - $this->y + 1, - $this->z + $maxZ - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if(!$blockClicked->isTransparent()){ - $faces = [ - 2 => 2, - 3 => 3, - 4 => 4, - 5 => 5 - ]; - if(isset($faces[$face])){ - $this->meta = $faces[$face]; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if(!$this->getSide($this->meta ^ 0x01)->isSolid()){ //Replace with common break method - $this->level->useBreakOn($this); - } - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Lapis.php b/src/pocketmine/block/Lapis.php deleted file mode 100644 index 664a4a4db9..0000000000 --- a/src/pocketmine/block/Lapis.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Lapis Lazuli Block"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_STONE; - } - - public function getHardness() : float{ - return 3; - } -} diff --git a/src/pocketmine/block/LapisOre.php b/src/pocketmine/block/LapisOre.php deleted file mode 100644 index 2025507f35..0000000000 --- a/src/pocketmine/block/LapisOre.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_STONE; - } - - public function getName() : string{ - return "Lapis Lazuli Ore"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::DYE, 4, mt_rand(4, 8)) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(2, 5); - } -} diff --git a/src/pocketmine/block/Leaves.php b/src/pocketmine/block/Leaves.php deleted file mode 100644 index 6eeb83e491..0000000000 --- a/src/pocketmine/block/Leaves.php +++ /dev/null @@ -1,205 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHEARS; - } - - public function getName() : string{ - static $names = [ - self::OAK => "Oak Leaves", - self::SPRUCE => "Spruce Leaves", - self::BIRCH => "Birch Leaves", - self::JUNGLE => "Jungle Leaves" - ]; - return $names[$this->getVariant()]; - } - - public function diffusesSkyLight() : bool{ - return true; - } - - /** - * @param true[] $visited reference parameter - * @phpstan-param array $visited - */ - protected function findLog(Block $pos, array &$visited, int $distance, ?int $fromSide = null) : bool{ - $index = $pos->x . "." . $pos->y . "." . $pos->z; - if(isset($visited[$index])){ - return false; - } - if($pos->getId() === $this->woodType){ - return true; - }elseif($pos->getId() === $this->id and $distance < 3){ - $visited[$index] = true; - $down = $pos->getSide(Vector3::SIDE_DOWN)->getId(); - if($down === $this->woodType){ - return true; - } - if($fromSide === null){ - for($side = 2; $side <= 5; ++$side){ - if($this->findLog($pos->getSide($side), $visited, $distance + 1, $side)){ - return true; - } - } - }else{ //No more loops - switch($fromSide){ - case 2: - if($this->findLog($pos->getSide(Vector3::SIDE_NORTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_WEST), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_EAST), $visited, $distance + 1, $fromSide)){ - return true; - } - break; - case 3: - if($this->findLog($pos->getSide(Vector3::SIDE_SOUTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_WEST), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_EAST), $visited, $distance + 1, $fromSide)){ - return true; - } - break; - case 4: - if($this->findLog($pos->getSide(Vector3::SIDE_NORTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_SOUTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_WEST), $visited, $distance + 1, $fromSide)){ - return true; - } - break; - case 5: - if($this->findLog($pos->getSide(Vector3::SIDE_NORTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_SOUTH), $visited, $distance + 1, $fromSide)){ - return true; - }elseif($this->findLog($pos->getSide(Vector3::SIDE_EAST), $visited, $distance + 1, $fromSide)){ - return true; - } - break; - } - } - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if(($this->meta & 0b00001100) === 0){ - $this->meta |= 0x08; - $this->getLevelNonNull()->setBlock($this, $this, true, false); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if(($this->meta & 0b00001100) === 0x08){ - $this->meta &= 0x03; - $visited = []; - - $ev = new LeavesDecayEvent($this); - $ev->call(); - if($ev->isCancelled() or $this->findLog($this, $visited, 0)){ - $this->getLevelNonNull()->setBlock($this, $this, false, false); - }else{ - $this->getLevelNonNull()->useBreakOn($this); - } - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta |= 0x04; - return $this->getLevelNonNull()->setBlock($this, $this, true); - } - - public function getVariantBitmask() : int{ - return 0x03; - } - - public function getDrops(Item $item) : array{ - if(($item->getBlockToolType() & BlockToolType::TYPE_SHEARS) !== 0){ - return $this->getDropsForCompatibleTool($item); - } - - $drops = []; - if(mt_rand(1, 20) === 1){ //Saplings - $drops[] = $this->getSaplingItem(); - } - if($this->canDropApples() and mt_rand(1, 200) === 1){ //Apples - $drops[] = ItemFactory::get(Item::APPLE); - } - - return $drops; - } - - public function getSaplingItem() : Item{ - return ItemFactory::get(Item::SAPLING, $this->getVariant()); - } - - public function canDropApples() : bool{ - return $this->getVariant() === self::OAK; - } - - public function getFlameEncouragement() : int{ - return 30; - } - - public function getFlammability() : int{ - return 60; - } -} diff --git a/src/pocketmine/block/Leaves2.php b/src/pocketmine/block/Leaves2.php deleted file mode 100644 index 3abe14aec5..0000000000 --- a/src/pocketmine/block/Leaves2.php +++ /dev/null @@ -1,50 +0,0 @@ - "Acacia Leaves", - self::DARK_OAK => "Dark Oak Leaves" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getSaplingItem() : Item{ - return ItemFactory::get(Item::SAPLING, $this->getVariant() + 4); - } - - public function canDropApples() : bool{ - return $this->getVariant() === self::DARK_OAK; - } -} diff --git a/src/pocketmine/block/Lever.php b/src/pocketmine/block/Lever.php deleted file mode 100644 index 10b6f3042f..0000000000 --- a/src/pocketmine/block/Lever.php +++ /dev/null @@ -1,93 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Lever"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if(!$blockClicked->isSolid()){ - return false; - } - - if($face === Vector3::SIDE_DOWN){ - $this->meta = 0; - }else{ - $this->meta = 6 - $face; - } - - if($player !== null){ - if(($player->getDirection() & 0x01) === 0){ - if($face === Vector3::SIDE_UP){ - $this->meta = 6; - } - }else{ - if($face === Vector3::SIDE_DOWN){ - $this->meta = 7; - } - } - } - - return $this->level->setBlock($blockReplace, $this, true, true); - } - - public function onNearbyBlockChange() : void{ - $faces = [ - 0 => Vector3::SIDE_UP, - 1 => Vector3::SIDE_WEST, - 2 => Vector3::SIDE_EAST, - 3 => Vector3::SIDE_NORTH, - 4 => Vector3::SIDE_SOUTH, - 5 => Vector3::SIDE_DOWN, - 6 => Vector3::SIDE_DOWN, - 7 => Vector3::SIDE_UP - ]; - if(!$this->getSide($faces[$this->meta & 0x07])->isSolid()){ - $this->level->useBreakOn($this); - } - } - - //TODO -} diff --git a/src/pocketmine/block/Liquid.php b/src/pocketmine/block/Liquid.php deleted file mode 100644 index ce12ef74e3..0000000000 --- a/src/pocketmine/block/Liquid.php +++ /dev/null @@ -1,458 +0,0 @@ -meta; - if($d >= 8){ - $d = 0; - } - - return ($d + 1) / 9; - } - - protected function getFlowDecay(Block $block) : int{ - if($block->getId() !== $this->getId()){ - return -1; - } - - return $block->getDamage(); - } - - protected function getEffectiveFlowDecay(Block $block) : int{ - if($block->getId() !== $this->getId()){ - return -1; - } - - $decay = $block->getDamage(); - - if($decay >= 8){ - $decay = 0; - } - - return $decay; - } - - public function clearCaches() : void{ - parent::clearCaches(); - $this->flowVector = null; - } - - public function getFlowVector() : Vector3{ - if($this->flowVector !== null){ - return $this->flowVector; - } - - $vector = new Vector3(0, 0, 0); - - $decay = $this->getEffectiveFlowDecay($this); - - for($j = 0; $j < 4; ++$j){ - - $x = $this->x; - $y = $this->y; - $z = $this->z; - - if($j === 0){ - --$x; - }elseif($j === 1){ - ++$x; - }elseif($j === 2){ - --$z; - }elseif($j === 3){ - ++$z; - } - $sideBlock = $this->level->getBlockAt($x, $y, $z); - $blockDecay = $this->getEffectiveFlowDecay($sideBlock); - - if($blockDecay < 0){ - if(!$sideBlock->canBeFlowedInto()){ - continue; - } - - $blockDecay = $this->getEffectiveFlowDecay($this->level->getBlockAt($x, $y - 1, $z)); - - if($blockDecay >= 0){ - $realDecay = $blockDecay - ($decay - 8); - $vector->x += ($sideBlock->x - $this->x) * $realDecay; - $vector->y += ($sideBlock->y - $this->y) * $realDecay; - $vector->z += ($sideBlock->z - $this->z) * $realDecay; - } - - continue; - }else{ - $realDecay = $blockDecay - $decay; - $vector->x += ($sideBlock->x - $this->x) * $realDecay; - $vector->y += ($sideBlock->y - $this->y) * $realDecay; - $vector->z += ($sideBlock->z - $this->z) * $realDecay; - } - } - - if($this->getDamage() >= 8){ - if( - !$this->canFlowInto($this->level->getBlockAt($this->x, $this->y, $this->z - 1)) or - !$this->canFlowInto($this->level->getBlockAt($this->x, $this->y, $this->z + 1)) or - !$this->canFlowInto($this->level->getBlockAt($this->x - 1, $this->y, $this->z)) or - !$this->canFlowInto($this->level->getBlockAt($this->x + 1, $this->y, $this->z)) or - !$this->canFlowInto($this->level->getBlockAt($this->x, $this->y + 1, $this->z - 1)) or - !$this->canFlowInto($this->level->getBlockAt($this->x, $this->y + 1, $this->z + 1)) or - !$this->canFlowInto($this->level->getBlockAt($this->x - 1, $this->y + 1, $this->z)) or - !$this->canFlowInto($this->level->getBlockAt($this->x + 1, $this->y + 1, $this->z)) - ){ - $vector = $vector->normalize()->add(0, -6, 0); - } - } - - return $this->flowVector = $vector->normalize(); - } - - public function addVelocityToEntity(Entity $entity, Vector3 $vector) : void{ - if($entity->canBeMovedByCurrents()){ - $flow = $this->getFlowVector(); - $vector->x += $flow->x; - $vector->y += $flow->y; - $vector->z += $flow->z; - } - } - - abstract public function tickRate() : int; - - /** - * Returns how many liquid levels are lost per block flowed horizontally. Affects how far the liquid can flow. - */ - public function getFlowDecayPerBlock() : int{ - return 1; - } - - public function onNearbyBlockChange() : void{ - $this->checkForHarden(); - $this->level->scheduleDelayedBlockUpdate($this, $this->tickRate()); - } - - public function onScheduledUpdate() : void{ - $decay = $this->getFlowDecay($this); - $multiplier = $this->getFlowDecayPerBlock(); - - if($decay > 0){ - $smallestFlowDecay = -100; - $this->adjacentSources = 0; - $smallestFlowDecay = $this->getSmallestFlowDecay($this->level->getBlockAt($this->x, $this->y, $this->z - 1), $smallestFlowDecay); - $smallestFlowDecay = $this->getSmallestFlowDecay($this->level->getBlockAt($this->x, $this->y, $this->z + 1), $smallestFlowDecay); - $smallestFlowDecay = $this->getSmallestFlowDecay($this->level->getBlockAt($this->x - 1, $this->y, $this->z), $smallestFlowDecay); - $smallestFlowDecay = $this->getSmallestFlowDecay($this->level->getBlockAt($this->x + 1, $this->y, $this->z), $smallestFlowDecay); - - $newDecay = $smallestFlowDecay + $multiplier; - - if($newDecay >= 8 or $smallestFlowDecay < 0){ - $newDecay = -1; - } - - if(($topFlowDecay = $this->getFlowDecay($this->level->getBlockAt($this->x, $this->y + 1, $this->z))) >= 0){ - $newDecay = $topFlowDecay | 0x08; - } - - if($this->adjacentSources >= 2 and $this instanceof Water){ - $bottomBlock = $this->level->getBlockAt($this->x, $this->y - 1, $this->z); - if($bottomBlock->isSolid()){ - $newDecay = 0; - }elseif($bottomBlock instanceof Water and $bottomBlock->getDamage() === 0){ - $newDecay = 0; - } - } - - if($newDecay !== $decay){ - $decay = $newDecay; - if($decay < 0){ - $this->level->setBlock($this, BlockFactory::get(Block::AIR), true, true); - }else{ - $this->level->setBlock($this, BlockFactory::get($this->id, $decay), true, true); - $this->level->scheduleDelayedBlockUpdate($this, $this->tickRate()); - } - } - } - - if($decay >= 0){ - $bottomBlock = $this->level->getBlockAt($this->x, $this->y - 1, $this->z); - - $this->flowIntoBlock($bottomBlock, $decay | 0x08); - - if($decay === 0 or !$bottomBlock->canBeFlowedInto()){ - if($decay >= 8){ - $adjacentDecay = 1; - }else{ - $adjacentDecay = $decay + $multiplier; - } - - if($adjacentDecay < 8){ - $flags = $this->getOptimalFlowDirections(); - - if($flags[0]){ - $this->flowIntoBlock($this->level->getBlockAt($this->x - 1, $this->y, $this->z), $adjacentDecay); - } - - if($flags[1]){ - $this->flowIntoBlock($this->level->getBlockAt($this->x + 1, $this->y, $this->z), $adjacentDecay); - } - - if($flags[2]){ - $this->flowIntoBlock($this->level->getBlockAt($this->x, $this->y, $this->z - 1), $adjacentDecay); - } - - if($flags[3]){ - $this->flowIntoBlock($this->level->getBlockAt($this->x, $this->y, $this->z + 1), $adjacentDecay); - } - } - } - - $this->checkForHarden(); - } - } - - protected function flowIntoBlock(Block $block, int $newFlowDecay) : void{ - if($this->canFlowInto($block) and !($block instanceof Liquid)){ - $ev = new BlockSpreadEvent($block, $this, BlockFactory::get($this->getId(), $newFlowDecay)); - $ev->call(); - if(!$ev->isCancelled()){ - if($block->getId() > 0){ - $this->level->useBreakOn($block); - } - - $this->level->setBlock($block, $ev->getNewState(), true, true); - $this->level->scheduleDelayedBlockUpdate($block, $this->tickRate()); - } - } - } - - private function calculateFlowCost(int $blockX, int $blockY, int $blockZ, int $accumulatedCost, int $maxCost, int $originOpposite, int $lastOpposite) : int{ - $cost = 1000; - - for($j = 0; $j < 4; ++$j){ - if($j === $originOpposite or $j === $lastOpposite){ - continue; - } - - $x = $blockX; - $y = $blockY; - $z = $blockZ; - - if($j === 0){ - --$x; - }elseif($j === 1){ - ++$x; - }elseif($j === 2){ - --$z; - }elseif($j === 3){ - ++$z; - } - - if(!isset($this->flowCostVisited[$hash = Level::blockHash($x, $y, $z)])){ - $blockSide = $this->level->getBlockAt($x, $y, $z); - if(!$this->canFlowInto($blockSide)){ - $this->flowCostVisited[$hash] = self::BLOCKED; - }elseif($this->level->getBlockAt($x, $y - 1, $z)->canBeFlowedInto()){ - $this->flowCostVisited[$hash] = self::CAN_FLOW_DOWN; - }else{ - $this->flowCostVisited[$hash] = self::CAN_FLOW; - } - } - - $status = $this->flowCostVisited[$hash]; - - if($status === self::BLOCKED){ - continue; - }elseif($status === self::CAN_FLOW_DOWN){ - return $accumulatedCost; - } - - if($accumulatedCost >= $maxCost){ - continue; - } - - $realCost = $this->calculateFlowCost($x, $y, $z, $accumulatedCost + 1, $maxCost, $originOpposite, $j ^ 0x01); - - if($realCost < $cost){ - $cost = $realCost; - } - } - - return $cost; - } - - /** - * @return bool[] - */ - private function getOptimalFlowDirections() : array{ - $flowCost = array_fill(0, 4, 1000); - $maxCost = intdiv(4, $this->getFlowDecayPerBlock()); - for($j = 0; $j < 4; ++$j){ - $x = $this->x; - $y = $this->y; - $z = $this->z; - - if($j === 0){ - --$x; - }elseif($j === 1){ - ++$x; - }elseif($j === 2){ - --$z; - }elseif($j === 3){ - ++$z; - } - $block = $this->level->getBlockAt($x, $y, $z); - - if(!$this->canFlowInto($block)){ - $this->flowCostVisited[Level::blockHash($x, $y, $z)] = self::BLOCKED; - continue; - }elseif($this->level->getBlockAt($x, $y - 1, $z)->canBeFlowedInto()){ - $this->flowCostVisited[Level::blockHash($x, $y, $z)] = self::CAN_FLOW_DOWN; - $flowCost[$j] = $maxCost = 0; - }elseif($maxCost > 0){ - $this->flowCostVisited[Level::blockHash($x, $y, $z)] = self::CAN_FLOW; - $flowCost[$j] = $this->calculateFlowCost($x, $y, $z, 1, $maxCost, $j ^ 0x01, $j ^ 0x01); - $maxCost = min($maxCost, $flowCost[$j]); - } - } - - $this->flowCostVisited = []; - - $minCost = min($flowCost); - - $isOptimalFlowDirection = []; - - for($i = 0; $i < 4; ++$i){ - $isOptimalFlowDirection[$i] = ($flowCost[$i] === $minCost); - } - - return $isOptimalFlowDirection; - } - - /** - * @phpstan-impure This function modifies the adjacent sources count (premature optimisation) - */ - private function getSmallestFlowDecay(Block $block, int $decay) : int{ - $blockDecay = $this->getFlowDecay($block); - - if($blockDecay < 0){ - return $decay; - }elseif($blockDecay === 0){ - ++$this->adjacentSources; - }elseif($blockDecay >= 8){ - $blockDecay = 0; - } - - return ($decay >= 0 && $blockDecay >= $decay) ? $decay : $blockDecay; - } - - /** - * @return void - */ - protected function checkForHarden(){ - - } - - protected function liquidCollide(Block $cause, Block $result) : bool{ - $ev = new BlockFormEvent($this, $result); - $ev->call(); - if(!$ev->isCancelled()){ - $this->level->setBlock($this, $ev->getNewState(), true, true); - $this->level->addSound(new FizzSound($this->add(0.5, 0.5, 0.5), 2.6 + (lcg_value() - lcg_value()) * 0.8)); - } - return true; - } - - protected function canFlowInto(Block $block) : bool{ - return $block->canBeFlowedInto() and !($block instanceof Liquid and $block->meta === 0); //TODO: I think this should only be liquids of the same type - } -} diff --git a/src/pocketmine/block/Melon.php b/src/pocketmine/block/Melon.php deleted file mode 100644 index f2364611f5..0000000000 --- a/src/pocketmine/block/Melon.php +++ /dev/null @@ -1,55 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Melon Block"; - } - - public function getHardness() : float{ - return 1; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::MELON_SLICE, 0, mt_rand(3, 7)) - ]; - } -} diff --git a/src/pocketmine/block/MonsterSpawner.php b/src/pocketmine/block/MonsterSpawner.php deleted file mode 100644 index 5a20d87940..0000000000 --- a/src/pocketmine/block/MonsterSpawner.php +++ /dev/null @@ -1,65 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Monster Spawner"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return []; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - protected function getXpDropAmount() : int{ - return mt_rand(15, 43); - } -} diff --git a/src/pocketmine/block/NetherBrickFence.php b/src/pocketmine/block/NetherBrickFence.php deleted file mode 100644 index 099518fd25..0000000000 --- a/src/pocketmine/block/NetherBrickFence.php +++ /dev/null @@ -1,47 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Nether Brick Stairs"; - } - - public function getHardness() : float{ - return 2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/NetherQuartzOre.php b/src/pocketmine/block/NetherQuartzOre.php deleted file mode 100644 index 4777dde947..0000000000 --- a/src/pocketmine/block/NetherQuartzOre.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Nether Quartz Ore"; - } - - public function getHardness() : float{ - return 3; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::QUARTZ) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(2, 5); - } -} diff --git a/src/pocketmine/block/NetherReactor.php b/src/pocketmine/block/NetherReactor.php deleted file mode 100644 index bf3f924fb7..0000000000 --- a/src/pocketmine/block/NetherReactor.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - static $prefixes = [ - "", - "Active ", - "Used " - ]; - return ($prefixes[$this->meta] ?? "") . "Nether Reactor Core"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 3; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::IRON_INGOT, 0, 6), - ItemFactory::get(Item::DIAMOND, 0, 3) - ]; - } -} diff --git a/src/pocketmine/block/NetherWartPlant.php b/src/pocketmine/block/NetherWartPlant.php deleted file mode 100644 index 3ab129b57b..0000000000 --- a/src/pocketmine/block/NetherWartPlant.php +++ /dev/null @@ -1,88 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Nether Wart"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === Block::SOUL_SAND){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, false, true); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() !== Block::SOUL_SAND){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if($this->meta < 3 and mt_rand(0, 10) === 0){ //Still growing - $block = clone $this; - $block->meta++; - $ev = new BlockGrowEvent($this, $block); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($this, $ev->getNewState(), false, true); - } - } - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get($this->getItemId(), 0, ($this->getDamage() === 3 ? mt_rand(2, 4) : 1)) - ]; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } -} diff --git a/src/pocketmine/block/Obsidian.php b/src/pocketmine/block/Obsidian.php deleted file mode 100644 index 1363b490fe..0000000000 --- a/src/pocketmine/block/Obsidian.php +++ /dev/null @@ -1,55 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Obsidian"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_DIAMOND; - } - - public function getHardness() : float{ - return 35; //50 in PC - } - - public function getBlastResistance() : float{ - return 6000; - } -} diff --git a/src/pocketmine/block/Planks.php b/src/pocketmine/block/Planks.php deleted file mode 100644 index 85de56ea77..0000000000 --- a/src/pocketmine/block/Planks.php +++ /dev/null @@ -1,71 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getName() : string{ - static $names = [ - self::OAK => "Oak Wood Planks", - self::SPRUCE => "Spruce Wood Planks", - self::BIRCH => "Birch Wood Planks", - self::JUNGLE => "Jungle Wood Planks", - self::ACACIA => "Acacia Wood Planks", - self::DARK_OAK => "Dark Oak Wood Planks" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getFuelTime() : int{ - return 300; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 20; - } -} diff --git a/src/pocketmine/block/Prismarine.php b/src/pocketmine/block/Prismarine.php deleted file mode 100644 index 61de1b3b37..0000000000 --- a/src/pocketmine/block/Prismarine.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getName() : string{ - static $names = [ - self::NORMAL => "Prismarine", - self::DARK => "Dark Prismarine", - self::BRICKS => "Prismarine Bricks" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getVariantBitmask() : int{ - return 0x03; - } -} diff --git a/src/pocketmine/block/PumpkinStem.php b/src/pocketmine/block/PumpkinStem.php deleted file mode 100644 index aa06ac3670..0000000000 --- a/src/pocketmine/block/PumpkinStem.php +++ /dev/null @@ -1,83 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Pumpkin Stem"; - } - - public function onRandomTick() : void{ - if(mt_rand(0, 2) === 1){ - if($this->meta < 0x07){ - $block = clone $this; - ++$block->meta; - $ev = new BlockGrowEvent($this, $block); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($this, $ev->getNewState(), true); - } - }else{ - for($side = 2; $side <= 5; ++$side){ - $b = $this->getSide($side); - if($b->getId() === self::PUMPKIN){ - return; - } - } - $side = $this->getSide(mt_rand(2, 5)); - $d = $side->getSide(Vector3::SIDE_DOWN); - if($side->getId() === self::AIR and ($d->getId() === self::FARMLAND or $d->getId() === self::GRASS or $d->getId() === self::DIRT)){ - $ev = new BlockGrowEvent($side, BlockFactory::get(Block::PUMPKIN)); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($side, $ev->getNewState(), true); - } - } - } - } - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::PUMPKIN_SEEDS, 0, mt_rand(0, 2)) - ]; - } - - public function getPickedItem() : Item{ - return ItemFactory::get(Item::PUMPKIN_SEEDS); - } -} diff --git a/src/pocketmine/block/PurpurStairs.php b/src/pocketmine/block/PurpurStairs.php deleted file mode 100644 index 39b3ace269..0000000000 --- a/src/pocketmine/block/PurpurStairs.php +++ /dev/null @@ -1,55 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Purpur Stairs"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getBlastResistance() : float{ - return 30; - } -} diff --git a/src/pocketmine/block/Quartz.php b/src/pocketmine/block/Quartz.php deleted file mode 100644 index bfaaea46f6..0000000000 --- a/src/pocketmine/block/Quartz.php +++ /dev/null @@ -1,75 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getName() : string{ - static $names = [ - self::NORMAL => "Quartz Block", - self::CHISELED => "Chiseled Quartz Block", - self::PILLAR => "Quartz Pillar" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($this->getVariant() !== self::NORMAL){ - $this->meta = PillarRotationHelper::getMetaFromFace($this->meta, $face); - } - return $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getVariantBitmask() : int{ - return 0x03; - } -} diff --git a/src/pocketmine/block/QuartzStairs.php b/src/pocketmine/block/QuartzStairs.php deleted file mode 100644 index 9ef08373d8..0000000000 --- a/src/pocketmine/block/QuartzStairs.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Quartz Stairs"; - } -} diff --git a/src/pocketmine/block/Rail.php b/src/pocketmine/block/Rail.php deleted file mode 100644 index 52f2d328cb..0000000000 --- a/src/pocketmine/block/Rail.php +++ /dev/null @@ -1,94 +0,0 @@ - [ - Vector3::SIDE_SOUTH, - Vector3::SIDE_EAST - ], - self::CURVE_SOUTHWEST => [ - Vector3::SIDE_SOUTH, - Vector3::SIDE_WEST - ], - self::CURVE_NORTHWEST => [ - Vector3::SIDE_NORTH, - Vector3::SIDE_WEST - ], - self::CURVE_NORTHEAST => [ - Vector3::SIDE_NORTH, - Vector3::SIDE_EAST - ] - ]; - - protected $id = self::RAIL; - - public function getName() : string{ - return "Rail"; - } - - protected function getMetaForState(array $connections) : int{ - try{ - return self::searchState($connections, self::CURVE_CONNECTIONS); - }catch(\InvalidArgumentException $e){ - return parent::getMetaForState($connections); - } - } - - protected function getConnectionsForState() : array{ - return self::CURVE_CONNECTIONS[$this->meta] ?? self::CONNECTIONS[$this->meta]; - } - - protected function getPossibleConnectionDirectionsOneConstraint(int $constraint) : array{ - /** @var int[] $horizontal */ - static $horizontal = [ - Vector3::SIDE_NORTH, - Vector3::SIDE_SOUTH, - Vector3::SIDE_WEST, - Vector3::SIDE_EAST - ]; - - $possible = parent::getPossibleConnectionDirectionsOneConstraint($constraint); - - if(($constraint & self::FLAG_ASCEND) === 0){ - foreach($horizontal as $d){ - if($constraint !== $d){ - $possible[$d] = true; - } - } - } - - return $possible; - } -} diff --git a/src/pocketmine/block/RedSandstone.php b/src/pocketmine/block/RedSandstone.php deleted file mode 100644 index 8c2372ccc5..0000000000 --- a/src/pocketmine/block/RedSandstone.php +++ /dev/null @@ -1,37 +0,0 @@ - "Red Sandstone", - self::CHISELED => "Chiseled Red Sandstone", - self::SMOOTH => "Smooth Red Sandstone" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } -} diff --git a/src/pocketmine/block/RedSandstoneStairs.php b/src/pocketmine/block/RedSandstoneStairs.php deleted file mode 100644 index 5c1471f692..0000000000 --- a/src/pocketmine/block/RedSandstoneStairs.php +++ /dev/null @@ -1,33 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Redstone Block"; - } -} diff --git a/src/pocketmine/block/RedstoneOre.php b/src/pocketmine/block/RedstoneOre.php deleted file mode 100644 index a9fab687cc..0000000000 --- a/src/pocketmine/block/RedstoneOre.php +++ /dev/null @@ -1,79 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Redstone Ore"; - } - - public function getHardness() : float{ - return 3; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - return $this->getLevelNonNull()->setBlock($this, $this, true, false); - } - - public function onActivate(Item $item, Player $player = null) : bool{ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::GLOWING_REDSTONE_ORE, $this->meta)); - return false; //this shouldn't prevent block placement - } - - public function onNearbyBlockChange() : void{ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::GLOWING_REDSTONE_ORE, $this->meta)); - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_IRON; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::REDSTONE_DUST, 0, mt_rand(4, 5)) - ]; - } - - protected function getXpDropAmount() : int{ - return mt_rand(1, 5); - } -} diff --git a/src/pocketmine/block/RedstoneRail.php b/src/pocketmine/block/RedstoneRail.php deleted file mode 100644 index 7d3b9cd4c0..0000000000 --- a/src/pocketmine/block/RedstoneRail.php +++ /dev/null @@ -1,32 +0,0 @@ -meta & ~self::FLAG_POWERED]; - } -} diff --git a/src/pocketmine/block/Sandstone.php b/src/pocketmine/block/Sandstone.php deleted file mode 100644 index 5c941c319a..0000000000 --- a/src/pocketmine/block/Sandstone.php +++ /dev/null @@ -1,64 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getName() : string{ - static $names = [ - self::NORMAL => "Sandstone", - self::CHISELED => "Chiseled Sandstone", - self::SMOOTH => "Smooth Sandstone" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getVariantBitmask() : int{ - return 0x03; - } -} diff --git a/src/pocketmine/block/SandstoneStairs.php b/src/pocketmine/block/SandstoneStairs.php deleted file mode 100644 index 7c73e6147e..0000000000 --- a/src/pocketmine/block/SandstoneStairs.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Sandstone Stairs"; - } -} diff --git a/src/pocketmine/block/Sapling.php b/src/pocketmine/block/Sapling.php deleted file mode 100644 index 2fdeb3ebff..0000000000 --- a/src/pocketmine/block/Sapling.php +++ /dev/null @@ -1,111 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - static $names = [ - 0 => "Oak Sapling", - 1 => "Spruce Sapling", - 2 => "Birch Sapling", - 3 => "Jungle Sapling", - 4 => "Acacia Sapling", - 5 => "Dark Oak Sapling" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === self::GRASS or $down->getId() === self::DIRT or $down->getId() === self::FARMLAND){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - return false; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($item->getId() === Item::DYE and $item->getDamage() === 0x0F){ //Bonemeal - //TODO: change log type - Tree::growTree($this->getLevelNonNull(), $this->x, $this->y, $this->z, new Random(mt_rand()), $this->getVariant()); - - $item->pop(); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->isTransparent()){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if($this->level->getFullLightAt($this->x, $this->y, $this->z) >= 8 and mt_rand(1, 7) === 1){ - if(($this->meta & 0x08) === 0x08){ - Tree::growTree($this->getLevelNonNull(), $this->x, $this->y, $this->z, new Random(mt_rand()), $this->getVariant()); - }else{ - $this->meta |= 0x08; - $this->getLevelNonNull()->setBlock($this, $this, true); - } - } - } - - public function getVariantBitmask() : int{ - return 0x07; - } - - public function getFuelTime() : int{ - return 100; - } -} diff --git a/src/pocketmine/block/SignPost.php b/src/pocketmine/block/SignPost.php deleted file mode 100644 index 945d20a7e0..0000000000 --- a/src/pocketmine/block/SignPost.php +++ /dev/null @@ -1,95 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1; - } - - public function isSolid() : bool{ - return false; - } - - public function getName() : string{ - return "Sign Post"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return null; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face !== Vector3::SIDE_DOWN){ - - if($face === Vector3::SIDE_UP){ - $this->meta = $player !== null ? (floor((($player->yaw + 180) * 16 / 360) + 0.5) & 0x0f) : 0; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - }else{ - $this->meta = $face; - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get(Block::WALL_SIGN, $this->meta), true); - } - - $sign = Tile::createTile(Tile::SIGN, $this->getLevelNonNull(), TileSign::createNBT($this, $face, $item, $player)); - if($player !== null && $sign instanceof TileSign){ - $sign->setEditorEntityRuntimeId($player->getId()); - } - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() === self::AIR){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Skull.php b/src/pocketmine/block/Skull.php deleted file mode 100644 index dfeb2f7114..0000000000 --- a/src/pocketmine/block/Skull.php +++ /dev/null @@ -1,90 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1; - } - - public function getName() : string{ - return "Mob Head"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - //TODO: different bounds depending on attached face (meta) - return new AxisAlignedBB( - $this->x + 0.25, - $this->y, - $this->z + 0.25, - $this->x + 0.75, - $this->y + 0.5, - $this->z + 0.75 - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face === Vector3::SIDE_DOWN){ - return false; - } - - $this->meta = $face; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - Tile::createTile(Tile::SKULL, $this->getLevelNonNull(), TileSkull::createNBT($this, $face, $item, $player)); - - return true; - } - - private function getItem() : Item{ - $tile = $this->level->getTile($this); - return ItemFactory::get(Item::SKULL, $tile instanceof TileSkull ? $tile->getType() : 0); - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [$this->getItem()]; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } - - public function getPickedItem() : Item{ - return $this->getItem(); - } -} diff --git a/src/pocketmine/block/Slab.php b/src/pocketmine/block/Slab.php deleted file mode 100644 index 3657cb1d38..0000000000 --- a/src/pocketmine/block/Slab.php +++ /dev/null @@ -1,129 +0,0 @@ -meta = $meta; - } - - abstract public function getDoubleSlabId() : int; - - public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ - if(parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock)){ - return true; - } - - if($blockReplace->getId() === $this->getId() and $blockReplace->getVariant() === $this->getVariant()){ - if(($blockReplace->getDamage() & 0x08) !== 0){ //Trying to combine with top slab - return $clickVector->y <= 0.5 or (!$isClickedBlock and $face === Vector3::SIDE_UP); - }else{ - return $clickVector->y >= 0.5 or (!$isClickedBlock and $face === Vector3::SIDE_DOWN); - } - } - - return false; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta &= 0x07; - if($face === Vector3::SIDE_DOWN){ - if($blockClicked->getId() === $this->id and ($blockClicked->getDamage() & 0x08) === 0x08 and $blockClicked->getVariant() === $this->getVariant()){ - $this->getLevelNonNull()->setBlock($blockClicked, BlockFactory::get($this->getDoubleSlabId(), $this->getVariant()), true); - - return true; - }elseif($blockReplace->getId() === $this->id and $blockReplace->getVariant() === $this->getVariant()){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get($this->getDoubleSlabId(), $this->getVariant()), true); - - return true; - }else{ - $this->meta |= 0x08; - } - }elseif($face === Vector3::SIDE_UP){ - if($blockClicked->getId() === $this->id and ($blockClicked->getDamage() & 0x08) === 0 and $blockClicked->getVariant() === $this->getVariant()){ - $this->getLevelNonNull()->setBlock($blockClicked, BlockFactory::get($this->getDoubleSlabId(), $this->getVariant()), true); - - return true; - }elseif($blockReplace->getId() === $this->id and $blockReplace->getVariant() === $this->getVariant()){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get($this->getDoubleSlabId(), $this->getVariant()), true); - - return true; - } - }else{ //TODO: collision - if($blockReplace->getId() === $this->id){ - if($blockReplace->getVariant() === $this->getVariant()){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get($this->getDoubleSlabId(), $this->getVariant()), true); - - return true; - } - - return false; - }else{ - if($clickVector->y > 0.5){ - $this->meta |= 0x08; - } - } - } - - if($blockReplace->getId() === $this->id and $blockClicked->getVariant() !== $this->getVariant()){ - return false; - } - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - public function getVariantBitmask() : int{ - return 0x07; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - if(($this->meta & 0x08) > 0){ - return new AxisAlignedBB( - $this->x, - $this->y + 0.5, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - }else{ - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 0.5, - $this->z + 1 - ); - } - } -} diff --git a/src/pocketmine/block/Snow.php b/src/pocketmine/block/Snow.php deleted file mode 100644 index b6e00b3bd0..0000000000 --- a/src/pocketmine/block/Snow.php +++ /dev/null @@ -1,59 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.2; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - return "Snow Block"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::SNOWBALL, 0, 4) - ]; - } -} diff --git a/src/pocketmine/block/SnowLayer.php b/src/pocketmine/block/SnowLayer.php deleted file mode 100644 index 8d11cab2af..0000000000 --- a/src/pocketmine/block/SnowLayer.php +++ /dev/null @@ -1,102 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Snow Layer"; - } - - public function canBeReplaced() : bool{ - return $this->meta < 7; //8 snow layers - } - - public function getHardness() : float{ - return 0.1; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - private function canBeSupportedBy(Block $b) : bool{ - return $b->isSolid() or ($b->getId() === $this->getId() and $b->getDamage() === 7); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($blockReplace->getId() === $this->getId() and $blockReplace->getDamage() < 7){ - $this->setDamage($blockReplace->getDamage() + 1); - } - if($this->canBeSupportedBy($blockReplace->getSide(Vector3::SIDE_DOWN))){ - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if(!$this->canBeSupportedBy($this->getSide(Vector3::SIDE_DOWN))){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), false, false); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if($this->level->getBlockLightAt($this->x, $this->y, $this->z) >= 12){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), false, false); - } - } - - public function getDropsForCompatibleTool(Item $item) : array{ - return [ - ItemFactory::get(Item::SNOWBALL) //TODO: check layer count - ]; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } -} diff --git a/src/pocketmine/block/SoulSand.php b/src/pocketmine/block/SoulSand.php deleted file mode 100644 index dbe6deadc7..0000000000 --- a/src/pocketmine/block/SoulSand.php +++ /dev/null @@ -1,59 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Soul Sand"; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHOVEL; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - return new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1 - 0.125, - $this->z + 1 - ); - } -} diff --git a/src/pocketmine/block/Sponge.php b/src/pocketmine/block/Sponge.php deleted file mode 100644 index 0927af584a..0000000000 --- a/src/pocketmine/block/Sponge.php +++ /dev/null @@ -1,45 +0,0 @@ -meta = $meta; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_HOE; - } - - public function getHardness() : float{ - return 0.6; - } - - public function getName() : string{ - return "Sponge"; - } -} diff --git a/src/pocketmine/block/StainedClay.php b/src/pocketmine/block/StainedClay.php deleted file mode 100644 index 365c8790c4..0000000000 --- a/src/pocketmine/block/StainedClay.php +++ /dev/null @@ -1,35 +0,0 @@ -getVariant()) . " Stained Clay"; - } -} diff --git a/src/pocketmine/block/StainedGlassPane.php b/src/pocketmine/block/StainedGlassPane.php deleted file mode 100644 index f9f45a4c1e..0000000000 --- a/src/pocketmine/block/StainedGlassPane.php +++ /dev/null @@ -1,35 +0,0 @@ -getVariant()) . " Stained Glass Pane"; - } -} diff --git a/src/pocketmine/block/Stair.php b/src/pocketmine/block/Stair.php deleted file mode 100644 index 352bacd829..0000000000 --- a/src/pocketmine/block/Stair.php +++ /dev/null @@ -1,104 +0,0 @@ -meta & 0x04) === 0 ? 0 : 0.5; - $maxYSlab = $minYSlab + 0.5; - - $bbs = [ - new AxisAlignedBB( - $this->x, - $this->y + $minYSlab, - $this->z, - $this->x + 1, - $this->y + $maxYSlab, - $this->z + 1 - ) - ]; - - $minY = ($this->meta & 0x04) === 0 ? 0.5 : 0; - $maxY = $minY + 0.5; - - $rotationMeta = $this->meta & 0x03; - - $minX = $minZ = 0; - $maxX = $maxZ = 1; - - switch($rotationMeta){ - case 0: - $minX = 0.5; - break; - case 1: - $maxX = 0.5; - break; - case 2: - $minZ = 0.5; - break; - case 3: - $maxZ = 0.5; - break; - } - - $bbs[] = new AxisAlignedBB( - $this->x + $minX, - $this->y + $minY, - $this->z + $minZ, - $this->x + $maxX, - $this->y + $maxY, - $this->z + $maxZ - ); - - return $bbs; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $faces = [ - 0 => 0, - 1 => 2, - 2 => 1, - 3 => 3 - ]; - $this->meta = $player !== null ? $faces[$player->getDirection()] & 0x03 : 0; - if(($clickVector->y > 0.5 and $face !== Vector3::SIDE_UP) or $face === Vector3::SIDE_DOWN){ - $this->meta |= 0x04; //Upside-down stairs - } - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/StandingBanner.php b/src/pocketmine/block/StandingBanner.php deleted file mode 100644 index 090f5c83a8..0000000000 --- a/src/pocketmine/block/StandingBanner.php +++ /dev/null @@ -1,106 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1; - } - - public function isSolid() : bool{ - return false; - } - - public function getName() : string{ - return "Standing Banner"; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - return null; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if($face !== Vector3::SIDE_DOWN){ - if($face === Vector3::SIDE_UP and $player !== null){ - $this->meta = floor((($player->yaw + 180) * 16 / 360) + 0.5) & 0x0f; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true); - }else{ - $this->meta = $face; - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get(Block::WALL_BANNER, $this->meta), true); - } - - Tile::createTile(Tile::BANNER, $this->getLevelNonNull(), TileBanner::createNBT($this, $face, $item, $player)); - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() === self::AIR){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - $tile = $this->level->getTile($this); - - $drop = ItemFactory::get(Item::BANNER, ($tile instanceof TileBanner ? $tile->getBaseColor() : 0)); - if($tile instanceof TileBanner and !($patterns = $tile->getPatterns())->empty()){ - $drop->setNamedTagEntry(clone $patterns); - } - - return [$drop]; - } - - public function isAffectedBySilkTouch() : bool{ - return false; - } -} diff --git a/src/pocketmine/block/Stone.php b/src/pocketmine/block/Stone.php deleted file mode 100644 index c3a6d8c211..0000000000 --- a/src/pocketmine/block/Stone.php +++ /dev/null @@ -1,79 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - static $names = [ - self::NORMAL => "Stone", - self::GRANITE => "Granite", - self::POLISHED_GRANITE => "Polished Granite", - self::DIORITE => "Diorite", - self::POLISHED_DIORITE => "Polished Diorite", - self::ANDESITE => "Andesite", - self::POLISHED_ANDESITE => "Polished Andesite" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getDropsForCompatibleTool(Item $item) : array{ - if($this->getDamage() === self::NORMAL){ - return [ - ItemFactory::get(Item::COBBLESTONE, $this->getDamage()) - ]; - } - - return parent::getDropsForCompatibleTool($item); - } -} diff --git a/src/pocketmine/block/StoneBrickStairs.php b/src/pocketmine/block/StoneBrickStairs.php deleted file mode 100644 index dc12203001..0000000000 --- a/src/pocketmine/block/StoneBrickStairs.php +++ /dev/null @@ -1,51 +0,0 @@ -meta = $meta; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getName() : string{ - return "Stone Brick Stairs"; - } -} diff --git a/src/pocketmine/block/StoneBricks.php b/src/pocketmine/block/StoneBricks.php deleted file mode 100644 index 3b17797ba3..0000000000 --- a/src/pocketmine/block/StoneBricks.php +++ /dev/null @@ -1,61 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 1.5; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } - - public function getName() : string{ - static $names = [ - self::NORMAL => "Stone Bricks", - self::MOSSY => "Mossy Stone Bricks", - self::CRACKED => "Cracked Stone Bricks", - self::CHISELED => "Chiseled Stone Bricks" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } -} diff --git a/src/pocketmine/block/StonePressurePlate.php b/src/pocketmine/block/StonePressurePlate.php deleted file mode 100644 index dc72f72a53..0000000000 --- a/src/pocketmine/block/StonePressurePlate.php +++ /dev/null @@ -1,59 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Stone Pressure Plate"; - } - - public function isSolid() : bool{ - return false; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/StoneSlab.php b/src/pocketmine/block/StoneSlab.php deleted file mode 100644 index 19d3dbe5cc..0000000000 --- a/src/pocketmine/block/StoneSlab.php +++ /dev/null @@ -1,69 +0,0 @@ - "Stone", - self::SANDSTONE => "Sandstone", - self::WOODEN => "Wooden", - self::COBBLESTONE => "Cobblestone", - self::BRICK => "Brick", - self::STONE_BRICK => "Stone Brick", - self::QUARTZ => "Quartz", - self::NETHER_BRICK => "Nether Brick" - ]; - return (($this->meta & 0x08) > 0 ? "Upper " : "") . ($names[$this->getVariant()] ?? "") . " Slab"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/StoneSlab2.php b/src/pocketmine/block/StoneSlab2.php deleted file mode 100644 index 37962f7c2b..0000000000 --- a/src/pocketmine/block/StoneSlab2.php +++ /dev/null @@ -1,56 +0,0 @@ - "Red Sandstone", - self::TYPE_PURPUR => "Purpur", - self::TYPE_PRISMARINE => "Prismarine", - self::TYPE_DARK_PRISMARINE => "Dark Prismarine", - self::TYPE_PRISMARINE_BRICKS => "Prismarine Bricks", - self::TYPE_MOSSY_COBBLESTONE => "Mossy Cobblestone", - self::TYPE_SMOOTH_SANDSTONE => "Smooth Sandstone", - self::TYPE_RED_NETHER_BRICK => "Red Nether Brick" - ]; - - return (($this->meta & 0x08) > 0 ? "Upper " : "") . ($names[$this->getVariant()] ?? "") . " Slab"; - } -} diff --git a/src/pocketmine/block/Sugarcane.php b/src/pocketmine/block/Sugarcane.php deleted file mode 100644 index a8fa2e8f1b..0000000000 --- a/src/pocketmine/block/Sugarcane.php +++ /dev/null @@ -1,132 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Sugarcane"; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($item->getId() === Item::DYE and $item->getDamage() === 0x0F){ //Bonemeal - if($this->getSide(Vector3::SIDE_DOWN)->getId() !== self::SUGARCANE_BLOCK){ - for($y = 1; $y < 3; ++$y){ - $b = $this->getLevelNonNull()->getBlockAt($this->x, $this->y + $y, $this->z); - if($b->getId() === self::AIR){ - $ev = new BlockGrowEvent($b, BlockFactory::get(Block::SUGARCANE_BLOCK)); - $ev->call(); - if($ev->isCancelled()){ - break; - } - $this->getLevelNonNull()->setBlock($b, $ev->getNewState(), true); - }else{ - break; - } - } - $this->meta = 0; - $this->getLevelNonNull()->setBlock($this, $this, true); - } - - $item->pop(); - - return true; - } - - return false; - } - - public function onNearbyBlockChange() : void{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->isTransparent() and $down->getId() !== self::SUGARCANE_BLOCK){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - if($this->getSide(Vector3::SIDE_DOWN)->getId() !== self::SUGARCANE_BLOCK){ - if($this->meta === 0x0F){ - for($y = 1; $y < 3; ++$y){ - $b = $this->getLevelNonNull()->getBlockAt($this->x, $this->y + $y, $this->z); - if($b->getId() === self::AIR){ - $ev = new BlockGrowEvent($b, BlockFactory::get(Block::SUGARCANE_BLOCK)); - $ev->call(); - if($ev->isCancelled()){ - break; - } - $this->getLevelNonNull()->setBlock($b, $ev->getNewState(), true); - break; - } - } - $this->meta = 0; - $this->getLevelNonNull()->setBlock($this, $this, true); - }else{ - ++$this->meta; - $this->getLevelNonNull()->setBlock($this, $this, true); - } - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $down = $this->getSide(Vector3::SIDE_DOWN); - if($down->getId() === self::SUGARCANE_BLOCK){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get(Block::SUGARCANE_BLOCK), true); - - return true; - }elseif($down->getId() === self::GRASS or $down->getId() === self::DIRT or $down->getId() === self::SAND){ - $block0 = $down->getSide(Vector3::SIDE_NORTH); - $block1 = $down->getSide(Vector3::SIDE_SOUTH); - $block2 = $down->getSide(Vector3::SIDE_WEST); - $block3 = $down->getSide(Vector3::SIDE_EAST); - if(($block0 instanceof Water) or ($block1 instanceof Water) or ($block2 instanceof Water) or ($block3 instanceof Water)){ - $this->getLevelNonNull()->setBlock($blockReplace, BlockFactory::get(Block::SUGARCANE_BLOCK), true); - - return true; - } - } - - return false; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/TNT.php b/src/pocketmine/block/TNT.php deleted file mode 100644 index 6c5f8252fe..0000000000 --- a/src/pocketmine/block/TNT.php +++ /dev/null @@ -1,105 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "TNT"; - } - - public function getHardness() : float{ - return 0; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - if($item instanceof FlintSteel or $item->hasEnchantment(Enchantment::FIRE_ASPECT)){ - if($item instanceof Durable){ - $item->applyDamage(1); - } - $this->ignite(); - return true; - } - - return false; - } - - public function hasEntityCollision() : bool{ - return true; - } - - public function onEntityCollide(Entity $entity) : void{ - if($entity instanceof Arrow and $entity->isOnFire()){ - $this->ignite(); - } - } - - /** - * @return void - */ - public function ignite(int $fuse = 80){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::AIR), true); - - $mot = (new Random())->nextSignedFloat() * M_PI * 2; - $nbt = Entity::createBaseNBT($this->add(0.5, 0, 0.5), new Vector3(-sin($mot) * 0.02, 0.2, -cos($mot) * 0.02)); - $nbt->setShort("Fuse", $fuse); - - $tnt = Entity::createEntity("PrimedTNT", $this->getLevelNonNull(), $nbt); - - if($tnt !== null){ - $tnt->spawnToAll(); - } - } - - public function getFlameEncouragement() : int{ - return 15; - } - - public function getFlammability() : int{ - return 100; - } - - public function onIncinerate() : void{ - $this->ignite(); - } -} diff --git a/src/pocketmine/block/Thin.php b/src/pocketmine/block/Thin.php deleted file mode 100644 index a84ec34c15..0000000000 --- a/src/pocketmine/block/Thin.php +++ /dev/null @@ -1,107 +0,0 @@ -x + ($this->canConnect($this->getSide(Vector3::SIDE_WEST)) ? 0 : $width), - $this->y, - $this->z + ($this->canConnect($this->getSide(Vector3::SIDE_NORTH)) ? 0 : $width), - $this->x + 1 - ($this->canConnect($this->getSide(Vector3::SIDE_EAST)) ? 0 : $width), - $this->y + 1, - $this->z + 1 - ($this->canConnect($this->getSide(Vector3::SIDE_SOUTH)) ? 0 : $width) - ); - } - - protected function recalculateCollisionBoxes() : array{ - $inset = 0.5 - 0.125 / 2; - - /** @var AxisAlignedBB[] $bbs */ - $bbs = []; - - $connectWest = $this->canConnect($this->getSide(Vector3::SIDE_WEST)); - $connectEast = $this->canConnect($this->getSide(Vector3::SIDE_EAST)); - - if($connectWest or $connectEast){ - //X axis (west/east) - $bbs[] = new AxisAlignedBB( - $this->x + ($connectWest ? 0 : $inset), - $this->y, - $this->z + $inset, - $this->x + 1 - ($connectEast ? 0 : $inset), - $this->y + 1, - $this->z + 1 - $inset - ); - } - - $connectNorth = $this->canConnect($this->getSide(Vector3::SIDE_NORTH)); - $connectSouth = $this->canConnect($this->getSide(Vector3::SIDE_SOUTH)); - - if($connectNorth or $connectSouth){ - //Z axis (north/south) - $bbs[] = new AxisAlignedBB( - $this->x + $inset, - $this->y, - $this->z + ($connectNorth ? 0 : $inset), - $this->x + 1 - $inset, - $this->y + 1, - $this->z + 1 - ($connectSouth ? 0 : $inset) - ); - } - - if(count($bbs) === 0){ - //centre post AABB (only needed if not connected on any axis - other BBs overlapping will do this if any connections are made) - return [ - new AxisAlignedBB( - $this->x + $inset, - $this->y, - $this->z + $inset, - $this->x + 1 - $inset, - $this->y + 1, - $this->z + 1 - $inset - ) - ]; - } - - return $bbs; - } - - public function canConnect(Block $block) : bool{ - if($block instanceof Thin){ - return true; - } - - //FIXME: currently there's no proper way to tell if a block is a full-block, so we check the bounding box size - $bb = $block->getBoundingBox(); - return $bb !== null and $bb->getAverageEdgeLength() >= 1; - } -} diff --git a/src/pocketmine/block/Torch.php b/src/pocketmine/block/Torch.php deleted file mode 100644 index d95f3b119e..0000000000 --- a/src/pocketmine/block/Torch.php +++ /dev/null @@ -1,92 +0,0 @@ -meta = $meta; - } - - public function getLightLevel() : int{ - return 14; - } - - public function getName() : string{ - return "Torch"; - } - - public function onNearbyBlockChange() : void{ - $below = $this->getSide(Vector3::SIDE_DOWN); - $meta = $this->getDamage(); - static $faces = [ - 0 => Vector3::SIDE_DOWN, - 1 => Vector3::SIDE_WEST, - 2 => Vector3::SIDE_EAST, - 3 => Vector3::SIDE_NORTH, - 4 => Vector3::SIDE_SOUTH, - 5 => Vector3::SIDE_DOWN - ]; - $face = $faces[$meta] ?? Vector3::SIDE_DOWN; - - if($this->getSide($face)->isTransparent() and !($face === Vector3::SIDE_DOWN and ($below->getId() === self::FENCE or $below->getId() === self::COBBLESTONE_WALL))){ - $this->getLevelNonNull()->useBreakOn($this); - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $below = $this->getSide(Vector3::SIDE_DOWN); - - if(!$blockClicked->isTransparent() and $face !== Vector3::SIDE_DOWN){ - $faces = [ - Vector3::SIDE_UP => 5, - Vector3::SIDE_NORTH => 4, - Vector3::SIDE_SOUTH => 3, - Vector3::SIDE_WEST => 2, - Vector3::SIDE_EAST => 1 - ]; - $this->meta = $faces[$face]; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - }elseif(!$below->isTransparent() or $below->getId() === self::FENCE or $below->getId() === self::COBBLESTONE_WALL){ - $this->meta = 0; - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - - return true; - } - - return false; - } - - public function getVariantBitmask() : int{ - return 0; - } -} diff --git a/src/pocketmine/block/Trapdoor.php b/src/pocketmine/block/Trapdoor.php deleted file mode 100644 index 0cbbd1564c..0000000000 --- a/src/pocketmine/block/Trapdoor.php +++ /dev/null @@ -1,161 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Wooden Trapdoor"; - } - - public function getHardness() : float{ - return 3; - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - $damage = $this->getDamage(); - - $f = 0.1875; - - if(($damage & self::MASK_UPPER) > 0){ - $bb = new AxisAlignedBB( - $this->x, - $this->y + 1 - $f, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - }else{ - $bb = new AxisAlignedBB( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + $f, - $this->z + 1 - ); - } - - if(($damage & self::MASK_OPENED) > 0){ - if(($damage & 0x03) === self::MASK_SIDE_NORTH){ - $bb->setBounds( - $this->x, - $this->y, - $this->z + 1 - $f, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - }elseif(($damage & 0x03) === self::MASK_SIDE_SOUTH){ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + $f - ); - } - if(($damage & 0x03) === self::MASK_SIDE_WEST){ - $bb->setBounds( - $this->x + 1 - $f, - $this->y, - $this->z, - $this->x + 1, - $this->y + 1, - $this->z + 1 - ); - } - if(($damage & 0x03) === self::MASK_SIDE_EAST){ - $bb->setBounds( - $this->x, - $this->y, - $this->z, - $this->x + $f, - $this->y + 1, - $this->z + 1 - ); - } - } - - return $bb; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $directions = [ - 0 => 1, - 1 => 3, - 2 => 0, - 3 => 2 - ]; - if($player !== null){ - $this->meta = $directions[$player->getDirection() & 0x03]; - } - if(($clickVector->y > 0.5 and $face !== self::SIDE_UP) or $face === self::SIDE_DOWN){ - $this->meta |= self::MASK_UPPER; //top half of block - } - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - return true; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function onActivate(Item $item, Player $player = null) : bool{ - $this->meta ^= self::MASK_OPENED; - $this->getLevelNonNull()->setBlock($this, $this, true); - $this->level->addSound(new DoorSound($this)); - return true; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getFuelTime() : int{ - return 300; - } -} diff --git a/src/pocketmine/block/Vine.php b/src/pocketmine/block/Vine.php deleted file mode 100644 index 5473fd247c..0000000000 --- a/src/pocketmine/block/Vine.php +++ /dev/null @@ -1,218 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Vines"; - } - - public function getHardness() : float{ - return 0.2; - } - - public function canPassThrough() : bool{ - return true; - } - - public function hasEntityCollision() : bool{ - return true; - } - - public function canClimb() : bool{ - return true; - } - - public function canBeReplaced() : bool{ - return true; - } - - public function onEntityCollide(Entity $entity) : void{ - $entity->resetFallDistance(); - } - - protected function recalculateBoundingBox() : ?AxisAlignedBB{ - - $minX = 1; - $minY = 1; - $minZ = 1; - $maxX = 0; - $maxY = 0; - $maxZ = 0; - - $flag = $this->meta > 0; - - if(($this->meta & self::FLAG_WEST) > 0){ - $maxX = max($maxX, 0.0625); - $minX = 0; - $minY = 0; - $maxY = 1; - $minZ = 0; - $maxZ = 1; - $flag = true; - } - - if(($this->meta & self::FLAG_EAST) > 0){ - $minX = min($minX, 0.9375); - $maxX = 1; - $minY = 0; - $maxY = 1; - $minZ = 0; - $maxZ = 1; - $flag = true; - } - - if(($this->meta & self::FLAG_SOUTH) > 0){ - $minZ = min($minZ, 0.9375); - $maxZ = 1; - $minX = 0; - $maxX = 1; - $minY = 0; - $maxY = 1; - $flag = true; - } - - //TODO: Missing NORTH check - - if(!$flag and $this->getSide(Vector3::SIDE_UP)->isSolid()){ - $minY = min($minY, 0.9375); - $maxY = 1; - $minX = 0; - $maxX = 1; - $minZ = 0; - $maxZ = 1; - } - - return new AxisAlignedBB( - $this->x + $minX, - $this->y + $minY, - $this->z + $minZ, - $this->x + $maxX, - $this->y + $maxY, - $this->z + $maxZ - ); - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - if(!$blockClicked->isSolid() or $face === Vector3::SIDE_UP or $face === Vector3::SIDE_DOWN){ - return false; - } - - $faces = [ - Vector3::SIDE_NORTH => self::FLAG_SOUTH, - Vector3::SIDE_SOUTH => self::FLAG_NORTH, - Vector3::SIDE_WEST => self::FLAG_EAST, - Vector3::SIDE_EAST => self::FLAG_WEST - ]; - - $this->meta = $faces[$face] ?? 0; - if($blockReplace->getId() === $this->getId()){ - $this->meta |= $blockReplace->meta; - } - - $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - return true; - } - - public function onNearbyBlockChange() : void{ - $sides = [ - self::FLAG_SOUTH => Vector3::SIDE_SOUTH, - self::FLAG_WEST => Vector3::SIDE_WEST, - self::FLAG_NORTH => Vector3::SIDE_NORTH, - self::FLAG_EAST => Vector3::SIDE_EAST - ]; - - $meta = $this->meta; - - foreach($sides as $flag => $side){ - if(($meta & $flag) === 0){ - continue; - } - - if(!$this->getSide($side)->isSolid()){ - $meta &= ~$flag; - } - } - - if($meta !== $this->meta){ - if($meta === 0){ - $this->level->useBreakOn($this); - }else{ - $this->meta = $meta; - $this->level->setBlock($this, $this); - } - } - } - - public function ticksRandomly() : bool{ - return true; - } - - public function onRandomTick() : void{ - //TODO: vine growth - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getDrops(Item $item) : array{ - if(($item->getBlockToolType() & BlockToolType::TYPE_SHEARS) !== 0){ - return $this->getDropsForCompatibleTool($item); - } - - return []; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getFlameEncouragement() : int{ - return 15; - } - - public function getFlammability() : int{ - return 100; - } -} diff --git a/src/pocketmine/block/Water.php b/src/pocketmine/block/Water.php deleted file mode 100644 index 8a17a2ae8c..0000000000 --- a/src/pocketmine/block/Water.php +++ /dev/null @@ -1,81 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Water"; - } - - public function getLightFilter() : int{ - return 2; - } - - public function getStillForm() : Block{ - return BlockFactory::get(Block::STILL_WATER, $this->meta); - } - - public function getFlowingForm() : Block{ - return BlockFactory::get(Block::FLOWING_WATER, $this->meta); - } - - public function getBucketFillSound() : int{ - return LevelSoundEventPacket::SOUND_BUCKET_FILL_WATER; - } - - public function getBucketEmptySound() : int{ - return LevelSoundEventPacket::SOUND_BUCKET_EMPTY_WATER; - } - - public function tickRate() : int{ - return 5; - } - - public function onEntityCollide(Entity $entity) : void{ - $entity->resetFallDistance(); - if($entity->isOnFire()){ - $entity->extinguish(); - } - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $ret = $this->getLevelNonNull()->setBlock($this, $this, true, false); - $this->getLevelNonNull()->scheduleDelayedBlockUpdate($this, $this->tickRate()); - - return $ret; - } -} diff --git a/src/pocketmine/block/WeightedPressurePlateLight.php b/src/pocketmine/block/WeightedPressurePlateLight.php deleted file mode 100644 index c36d107f6b..0000000000 --- a/src/pocketmine/block/WeightedPressurePlateLight.php +++ /dev/null @@ -1,59 +0,0 @@ -meta = $meta; - } - - public function getName() : string{ - return "Weighted Pressure Plate Light"; - } - - public function isSolid() : bool{ - return false; - } - - public function getHardness() : float{ - return 0.5; - } - - public function getVariantBitmask() : int{ - return 0; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_PICKAXE; - } - - public function getToolHarvestLevel() : int{ - return TieredTool::TIER_WOODEN; - } -} diff --git a/src/pocketmine/block/Wood.php b/src/pocketmine/block/Wood.php deleted file mode 100644 index 707256c6ed..0000000000 --- a/src/pocketmine/block/Wood.php +++ /dev/null @@ -1,81 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 2; - } - - public function getName() : string{ - static $names = [ - self::OAK => "Oak Wood", - self::SPRUCE => "Spruce Wood", - self::BIRCH => "Birch Wood", - self::JUNGLE => "Jungle Wood" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function place(Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, Player $player = null) : bool{ - $this->meta = PillarRotationHelper::getMetaFromFace($this->meta, $face); - return $this->getLevelNonNull()->setBlock($blockReplace, $this, true, true); - } - - public function getVariantBitmask() : int{ - return 0x03; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getFuelTime() : int{ - return 300; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 5; - } -} diff --git a/src/pocketmine/block/WoodenFence.php b/src/pocketmine/block/WoodenFence.php deleted file mode 100644 index f0df471f64..0000000000 --- a/src/pocketmine/block/WoodenFence.php +++ /dev/null @@ -1,67 +0,0 @@ - "Oak Fence", - self::FENCE_SPRUCE => "Spruce Fence", - self::FENCE_BIRCH => "Birch Fence", - self::FENCE_JUNGLE => "Jungle Fence", - self::FENCE_ACACIA => "Acacia Fence", - self::FENCE_DARKOAK => "Dark Oak Fence" - ]; - return $names[$this->getVariant()] ?? "Unknown"; - } - - public function getFuelTime() : int{ - return 300; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 20; - } -} diff --git a/src/pocketmine/block/WoodenPressurePlate.php b/src/pocketmine/block/WoodenPressurePlate.php deleted file mode 100644 index 8602ff3f21..0000000000 --- a/src/pocketmine/block/WoodenPressurePlate.php +++ /dev/null @@ -1,45 +0,0 @@ - "Oak", - 1 => "Spruce", - 2 => "Birch", - 3 => "Jungle", - 4 => "Acacia", - 5 => "Dark Oak" - ]; - return (($this->meta & 0x08) === 0x08 ? "Upper " : "") . ($names[$this->getVariant()] ?? "") . " Wooden Slab"; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_AXE; - } - - public function getFuelTime() : int{ - return 300; - } - - public function getFlameEncouragement() : int{ - return 5; - } - - public function getFlammability() : int{ - return 20; - } -} diff --git a/src/pocketmine/block/Wool.php b/src/pocketmine/block/Wool.php deleted file mode 100644 index 4463957164..0000000000 --- a/src/pocketmine/block/Wool.php +++ /dev/null @@ -1,65 +0,0 @@ -meta = $meta; - } - - public function getHardness() : float{ - return 0.8; - } - - public function getToolType() : int{ - return BlockToolType::TYPE_SHEARS; - } - - public function getName() : string{ - return ColorBlockMetaHelper::getColorFromMeta($this->getVariant()) . " Wool"; - } - - public function getBreakTime(Item $item) : float{ - $time = parent::getBreakTime($item); - if($item->getBlockToolType() === BlockToolType::TYPE_SHEARS){ - $time *= 3; //shears break compatible blocks 15x faster, but wool 5x - } - - return $time; - } - - public function getFlameEncouragement() : int{ - return 30; - } - - public function getFlammability() : int{ - return 60; - } -} diff --git a/src/pocketmine/command/CommandReader.php b/src/pocketmine/command/CommandReader.php deleted file mode 100644 index 450668e759..0000000000 --- a/src/pocketmine/command/CommandReader.php +++ /dev/null @@ -1,178 +0,0 @@ -buffer = new \Threaded; - $this->notifier = $notifier; - } - - /** - * @return void - */ - public function shutdown(){ - $this->shutdown = true; - } - - public function quit(){ - $wait = microtime(true) + 0.5; - while(microtime(true) < $wait){ - if($this->isRunning()){ - usleep(100000); - }else{ - parent::quit(); - return; - } - } - - $message = "Thread blocked for unknown reason"; - if($this->type === self::TYPE_PIPED){ - $message = "STDIN is being piped from another location and the pipe is blocked, cannot stop safely"; - } - - throw new \ThreadException($message); - } - - private function initStdin() : void{ - if(is_resource(self::$stdin)){ - fclose(self::$stdin); - } - - self::$stdin = fopen("php://stdin", "r"); - if($this->isPipe(self::$stdin)){ - $this->type = self::TYPE_PIPED; - }else{ - $this->type = self::TYPE_STREAM; - } - } - - /** - * Checks if the specified stream is a FIFO pipe. - * - * @param resource $stream - */ - private function isPipe($stream) : bool{ - return is_resource($stream) and (!stream_isatty($stream) or ((fstat($stream)["mode"] & 0170000) === 0010000)); - } - - /** - * Reads a line from the console and adds it to the buffer. This method may block the thread. - * - * @return bool if the main execution should continue reading lines - */ - private function readLine() : bool{ - if(!is_resource(self::$stdin)){ - $this->initStdin(); - } - - $r = [self::$stdin]; - $w = $e = null; - if(($count = stream_select($r, $w, $e, 0, 200000)) === 0){ //nothing changed in 200000 microseconds - return true; - }elseif($count === false){ //stream error - $this->initStdin(); - } - - if(($raw = fgets(self::$stdin)) === false){ //broken pipe or EOF - $this->initStdin(); - $this->synchronized(function() : void{ - $this->wait(200000); - }); //prevent CPU waste if it's end of pipe - return true; //loop back round - } - - $line = trim($raw); - - if($line !== ""){ - $this->buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $line); - if($this->notifier !== null){ - $this->notifier->wakeupSleeper(); - } - } - - return true; - } - - /** - * Reads a line from console, if available. Returns null if not available - * - * @return string|null - */ - public function getLine(){ - if($this->buffer->count() !== 0){ - return (string) $this->buffer->shift(); - } - - return null; - } - - /** - * @return void - */ - public function run(){ - $this->registerClassLoader(); - $this->initStdin(); - - while(!$this->shutdown and $this->readLine()); - - fclose(self::$stdin); - } - - public function getThreadName() : string{ - return "Console"; - } -} diff --git a/src/pocketmine/command/ConsoleCommandSender.php b/src/pocketmine/command/ConsoleCommandSender.php deleted file mode 100644 index 2ce7fd9ce4..0000000000 --- a/src/pocketmine/command/ConsoleCommandSender.php +++ /dev/null @@ -1,135 +0,0 @@ -perm = new PermissibleBase($this); - } - - /** - * @param Permission|string $name - */ - public function isPermissionSet($name) : bool{ - return $this->perm->isPermissionSet($name); - } - - /** - * @param Permission|string $name - */ - public function hasPermission($name) : bool{ - return $this->perm->hasPermission($name); - } - - public function addAttachment(Plugin $plugin, string $name = null, bool $value = null) : PermissionAttachment{ - return $this->perm->addAttachment($plugin, $name, $value); - } - - /** - * @return void - */ - public function removeAttachment(PermissionAttachment $attachment){ - $this->perm->removeAttachment($attachment); - } - - public function recalculatePermissions(){ - $this->perm->recalculatePermissions(); - } - - /** - * @return PermissionAttachmentInfo[] - */ - public function getEffectivePermissions() : array{ - return $this->perm->getEffectivePermissions(); - } - - /** - * @return Server - */ - public function getServer(){ - return Server::getInstance(); - } - - /** - * @param TextContainer|string $message - * - * @return void - */ - public function sendMessage($message){ - if($message instanceof TextContainer){ - $message = $this->getServer()->getLanguage()->translate($message); - }else{ - $message = $this->getServer()->getLanguage()->translateString($message); - } - - foreach(explode("\n", trim($message)) as $line){ - MainLogger::getLogger()->info($line); - } - } - - public function getName() : string{ - return "CONSOLE"; - } - - public function isOp() : bool{ - return true; - } - - /** - * @return void - */ - public function setOp(bool $value){ - - } - - public function getScreenLineHeight() : int{ - return $this->lineHeight ?? PHP_INT_MAX; - } - - public function setScreenLineHeight(int $height = null){ - if($height !== null and $height < 1){ - throw new \InvalidArgumentException("Line height must be at least 1"); - } - $this->lineHeight = $height; - } -} diff --git a/src/pocketmine/command/RemoteConsoleCommandSender.php b/src/pocketmine/command/RemoteConsoleCommandSender.php deleted file mode 100644 index 48e33a1275..0000000000 --- a/src/pocketmine/command/RemoteConsoleCommandSender.php +++ /dev/null @@ -1,54 +0,0 @@ -getServer()->getLanguage()->translate($message); - }else{ - $message = $this->getServer()->getLanguage()->translateString($message); - } - - $this->messages .= trim($message, "\r\n") . "\n"; - } - - /** - * @return string - */ - public function getMessage(){ - return $this->messages; - } - - public function getName() : string{ - return "Rcon"; - } -} diff --git a/src/pocketmine/command/defaults/GarbageCollectorCommand.php b/src/pocketmine/command/defaults/GarbageCollectorCommand.php deleted file mode 100644 index f6cbc9af7e..0000000000 --- a/src/pocketmine/command/defaults/GarbageCollectorCommand.php +++ /dev/null @@ -1,76 +0,0 @@ -setPermission("pocketmine.command.gc"); - } - - public function execute(CommandSender $sender, string $commandLabel, array $args){ - if(!$this->testPermission($sender)){ - return true; - } - - $chunksCollected = 0; - $entitiesCollected = 0; - $tilesCollected = 0; - - $memory = memory_get_usage(); - - foreach($sender->getServer()->getLevels() as $level){ - $diff = [count($level->getChunks()), count($level->getEntities()), count($level->getTiles())]; - $level->doChunkGarbageCollection(); - $level->unloadChunks(true); - $chunksCollected += $diff[0] - count($level->getChunks()); - $entitiesCollected += $diff[1] - count($level->getEntities()); - $tilesCollected += $diff[2] - count($level->getTiles()); - $level->clearCache(true); - } - - $cyclesCollected = $sender->getServer()->getMemoryManager()->triggerGarbageCollector(); - - $sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::WHITE . "Garbage collection result" . TextFormat::GREEN . " ----"); - $sender->sendMessage(TextFormat::GOLD . "Chunks: " . TextFormat::RED . number_format($chunksCollected)); - $sender->sendMessage(TextFormat::GOLD . "Entities: " . TextFormat::RED . number_format($entitiesCollected)); - $sender->sendMessage(TextFormat::GOLD . "Tiles: " . TextFormat::RED . number_format($tilesCollected)); - - $sender->sendMessage(TextFormat::GOLD . "Cycles: " . TextFormat::RED . number_format($cyclesCollected)); - $sender->sendMessage(TextFormat::GOLD . "Memory freed: " . TextFormat::RED . number_format(round((($memory - memory_get_usage()) / 1024) / 1024, 2), 2) . " MB"); - return true; - } -} diff --git a/src/pocketmine/command/defaults/ParticleCommand.php b/src/pocketmine/command/defaults/ParticleCommand.php deleted file mode 100644 index 3c74249527..0000000000 --- a/src/pocketmine/command/defaults/ParticleCommand.php +++ /dev/null @@ -1,231 +0,0 @@ -setPermission("pocketmine.command.particle"); - } - - public function execute(CommandSender $sender, string $commandLabel, array $args){ - if(!$this->testPermission($sender)){ - return true; - } - - if(count($args) < 7){ - throw new InvalidCommandSyntaxException(); - } - - if($sender instanceof Player){ - $level = $sender->getLevelNonNull(); - $pos = new Vector3( - $this->getRelativeDouble($sender->getX(), $sender, $args[1]), - $this->getRelativeDouble($sender->getY(), $sender, $args[2], 0, Level::Y_MAX), - $this->getRelativeDouble($sender->getZ(), $sender, $args[3]) - ); - }else{ - $level = $sender->getServer()->getDefaultLevel(); - $pos = new Vector3((float) $args[1], (float) $args[2], (float) $args[3]); - } - - $name = strtolower($args[0]); - - $xd = (float) $args[4]; - $yd = (float) $args[5]; - $zd = (float) $args[6]; - - $count = isset($args[7]) ? max(1, (int) $args[7]) : 1; - - $data = isset($args[8]) ? (int) $args[8] : null; - - $particle = $this->getParticle($name, $pos, $xd, $yd, $zd, $data); - - if($particle === null){ - $sender->sendMessage(new TranslationContainer(TextFormat::RED . "%commands.particle.notFound", [$name])); - return true; - } - - $sender->sendMessage(new TranslationContainer("commands.particle.success", [$name, $count])); - - $random = new Random((int) (microtime(true) * 1000) + mt_rand()); - - for($i = 0; $i < $count; ++$i){ - $particle->setComponents( - $pos->x + $random->nextSignedFloat() * $xd, - $pos->y + $random->nextSignedFloat() * $yd, - $pos->z + $random->nextSignedFloat() * $zd - ); - $level->addParticle($particle); - } - - return true; - } - - /** - * @return Particle|null - */ - private function getParticle(string $name, Vector3 $pos, float $xd, float $yd, float $zd, int $data = null){ - switch($name){ - case "explode": - return new ExplodeParticle($pos); - case "hugeexplosion": - return new HugeExplodeParticle($pos); - case "hugeexplosionseed": - return new HugeExplodeSeedParticle($pos); - case "bubble": - return new BubbleParticle($pos); - case "splash": - return new SplashParticle($pos); - case "wake": - case "water": - return new WaterParticle($pos); - case "crit": - return new CriticalParticle($pos); - case "smoke": - return new SmokeParticle($pos, $data ?? 0); - case "spell": - return new EnchantParticle($pos); - case "instantspell": - return new InstantEnchantParticle($pos); - case "dripwater": - return new WaterDripParticle($pos); - case "driplava": - return new LavaDripParticle($pos); - case "townaura": - case "spore": - return new SporeParticle($pos); - case "portal": - return new PortalParticle($pos); - case "flame": - return new FlameParticle($pos); - case "lava": - return new LavaParticle($pos); - case "reddust": - return new RedstoneParticle($pos, $data ?? 1); - case "snowballpoof": - return new ItemBreakParticle($pos, ItemFactory::get(Item::SNOWBALL)); - case "slime": - return new ItemBreakParticle($pos, ItemFactory::get(Item::SLIMEBALL)); - case "itembreak": - if($data !== null and $data !== 0){ - return new ItemBreakParticle($pos, ItemFactory::get($data)); - } - break; - case "terrain": - if($data !== null and $data !== 0){ - return new TerrainParticle($pos, BlockFactory::get($data)); - } - break; - case "heart": - return new HeartParticle($pos, $data ?? 0); - case "ink": - return new InkParticle($pos, $data ?? 0); - case "droplet": - return new RainSplashParticle($pos); - case "enchantmenttable": - return new EnchantmentTableParticle($pos); - case "happyvillager": - return new HappyVillagerParticle($pos); - case "angryvillager": - return new AngryVillagerParticle($pos); - case "forcefield": - return new BlockForceFieldParticle($pos, $data ?? 0); - case "mobflame": - return new EntityFlameParticle($pos); - } - - if(strpos($name, "iconcrack_") === 0){ - $d = explode("_", $name); - if(count($d) === 3){ - return new ItemBreakParticle($pos, ItemFactory::get((int) $d[1], (int) $d[2])); - } - }elseif(strpos($name, "blockcrack_") === 0){ - $d = explode("_", $name); - if(count($d) === 2){ - return new TerrainParticle($pos, BlockFactory::get(((int) $d[1]) & 0xff, ((int) $d[1]) >> 12)); - } - }elseif(strpos($name, "blockdust_") === 0){ - $d = explode("_", $name); - if(count($d) >= 4){ - return new DustParticle($pos, ((int) $d[1]) & 0xff, ((int) $d[2]) & 0xff, ((int) $d[3]) & 0xff, isset($d[4]) ? ((int) $d[4]) & 0xff : 255); - } - } - - return null; - } -} diff --git a/src/pocketmine/command/defaults/ReloadCommand.php b/src/pocketmine/command/defaults/ReloadCommand.php deleted file mode 100644 index 33e498ac8f..0000000000 --- a/src/pocketmine/command/defaults/ReloadCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -setPermission("pocketmine.command.reload"); - } - - public function execute(CommandSender $sender, string $commandLabel, array $args){ - if(!$this->testPermission($sender)){ - return true; - } - - Command::broadcastCommandMessage($sender, new TranslationContainer(TextFormat::YELLOW . "%pocketmine.command.reload.reloading")); - - $sender->getServer()->reload(); - Command::broadcastCommandMessage($sender, new TranslationContainer(TextFormat::YELLOW . "%pocketmine.command.reload.reloaded")); - - return true; - } -} diff --git a/src/pocketmine/command/defaults/TimeCommand.php b/src/pocketmine/command/defaults/TimeCommand.php deleted file mode 100644 index 41c2903bc4..0000000000 --- a/src/pocketmine/command/defaults/TimeCommand.php +++ /dev/null @@ -1,145 +0,0 @@ -setPermission("pocketmine.command.time.add;pocketmine.command.time.set;pocketmine.command.time.start;pocketmine.command.time.stop"); - } - - public function execute(CommandSender $sender, string $commandLabel, array $args){ - if(count($args) < 1){ - throw new InvalidCommandSyntaxException(); - } - - if($args[0] === "start"){ - if(!$sender->hasPermission("pocketmine.command.time.start")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - foreach($sender->getServer()->getLevels() as $level){ - $level->startTime(); - } - Command::broadcastCommandMessage($sender, "Restarted the time"); - return true; - }elseif($args[0] === "stop"){ - if(!$sender->hasPermission("pocketmine.command.time.stop")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - foreach($sender->getServer()->getLevels() as $level){ - $level->stopTime(); - } - Command::broadcastCommandMessage($sender, "Stopped the time"); - return true; - }elseif($args[0] === "query"){ - if(!$sender->hasPermission("pocketmine.command.time.query")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - if($sender instanceof Player){ - $level = $sender->getLevelNonNull(); - }else{ - $level = $sender->getServer()->getDefaultLevel(); - } - $sender->sendMessage($sender->getServer()->getLanguage()->translateString("commands.time.query", [$level->getTime()])); - return true; - } - - if(count($args) < 2){ - throw new InvalidCommandSyntaxException(); - } - - if($args[0] === "set"){ - if(!$sender->hasPermission("pocketmine.command.time.set")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - - switch($args[1]){ - case "day": - $value = Level::TIME_DAY; - break; - case "noon": - $value = Level::TIME_NOON; - break; - case "sunset": - $value = Level::TIME_SUNSET; - break; - case "night": - $value = Level::TIME_NIGHT; - break; - case "midnight": - $value = Level::TIME_MIDNIGHT; - break; - case "sunrise": - $value = Level::TIME_SUNRISE; - break; - default: - $value = $this->getInteger($sender, $args[1], 0); - break; - } - - foreach($sender->getServer()->getLevels() as $level){ - $level->setTime($value); - } - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.set", [$value])); - }elseif($args[0] === "add"){ - if(!$sender->hasPermission("pocketmine.command.time.add")){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - - $value = $this->getInteger($sender, $args[1], 0); - foreach($sender->getServer()->getLevels() as $level){ - $level->setTime($level->getTime() + $value); - } - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.added", [$value])); - }else{ - throw new InvalidCommandSyntaxException(); - } - - return true; - } -} diff --git a/src/pocketmine/command/defaults/WhitelistCommand.php b/src/pocketmine/command/defaults/WhitelistCommand.php deleted file mode 100644 index 117adfe627..0000000000 --- a/src/pocketmine/command/defaults/WhitelistCommand.php +++ /dev/null @@ -1,130 +0,0 @@ -setPermission("pocketmine.command.whitelist.reload;pocketmine.command.whitelist.enable;pocketmine.command.whitelist.disable;pocketmine.command.whitelist.list;pocketmine.command.whitelist.add;pocketmine.command.whitelist.remove"); - } - - public function execute(CommandSender $sender, string $commandLabel, array $args){ - if(!$this->testPermission($sender)){ - return true; - } - - if(count($args) === 1){ - if($this->badPerm($sender, strtolower($args[0]))){ - return false; - } - switch(strtolower($args[0])){ - case "reload": - $sender->getServer()->reloadWhitelist(); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.whitelist.reloaded")); - - return true; - case "on": - $sender->getServer()->setConfigBool("white-list", true); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.whitelist.enabled")); - - return true; - case "off": - $sender->getServer()->setConfigBool("white-list", false); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.whitelist.disabled")); - - return true; - case "list": - $entries = $sender->getServer()->getWhitelisted()->getAll(true); - sort($entries, SORT_STRING); - $result = implode(", ", $entries); - $count = count($entries); - - $sender->sendMessage(new TranslationContainer("commands.whitelist.list", [$count, $count])); - $sender->sendMessage($result); - - return true; - - case "add": - $sender->sendMessage(new TranslationContainer("commands.generic.usage", ["%commands.whitelist.add.usage"])); - return true; - - case "remove": - $sender->sendMessage(new TranslationContainer("commands.generic.usage", ["%commands.whitelist.remove.usage"])); - return true; - } - }elseif(count($args) === 2){ - if($this->badPerm($sender, strtolower($args[0]))){ - return false; - } - if(!Player::isValidUserName($args[1])){ - throw new InvalidCommandSyntaxException(); - } - switch(strtolower($args[0])){ - case "add": - $sender->getServer()->getOfflinePlayer($args[1])->setWhitelisted(true); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.whitelist.add.success", [$args[1]])); - - return true; - case "remove": - $sender->getServer()->getOfflinePlayer($args[1])->setWhitelisted(false); - Command::broadcastCommandMessage($sender, new TranslationContainer("commands.whitelist.remove.success", [$args[1]])); - - return true; - } - } - - throw new InvalidCommandSyntaxException(); - } - - private function badPerm(CommandSender $sender, string $subcommand) : bool{ - static $map = [ - "on" => "enable", - "off" => "disable" - ]; - if(!$sender->hasPermission("pocketmine.command.whitelist." . ($map[$subcommand] ?? $subcommand))){ - $sender->sendMessage($sender->getServer()->getLanguage()->translateString(TextFormat::RED . "%commands.generic.permission")); - - return true; - } - - return false; - } -} diff --git a/src/pocketmine/entity/DataPropertyManager.php b/src/pocketmine/entity/DataPropertyManager.php deleted file mode 100644 index d3f3afcb1b..0000000000 --- a/src/pocketmine/entity/DataPropertyManager.php +++ /dev/null @@ -1,209 +0,0 @@ - - */ - private $properties = []; - - /** - * @var mixed[][] - * @phpstan-var array - */ - private $dirtyProperties = []; - - public function __construct(){ - - } - - public function getByte(int $key) : ?int{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_BYTE); - assert(is_int($value) or $value === null); - return $value; - } - - public function setByte(int $key, int $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_BYTE, $value, $force); - } - - public function getShort(int $key) : ?int{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_SHORT); - assert(is_int($value) or $value === null); - return $value; - } - - public function setShort(int $key, int $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_SHORT, $value, $force); - } - - public function getInt(int $key) : ?int{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_INT); - assert(is_int($value) or $value === null); - return $value; - } - - public function setInt(int $key, int $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_INT, $value, $force); - } - - public function getFloat(int $key) : ?float{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_FLOAT); - assert(is_float($value) or $value === null); - return $value; - } - - public function setFloat(int $key, float $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_FLOAT, $value, $force); - } - - public function getString(int $key) : ?string{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_STRING); - assert(is_string($value) or $value === null); - return $value; - } - - public function setString(int $key, string $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_STRING, $value, $force); - } - - public function getCompoundTag(int $key) : ?CompoundTag{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_COMPOUND_TAG); - assert($value instanceof CompoundTag or $value === null); - return $value; - } - - public function setCompoundTag(int $key, CompoundTag $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_COMPOUND_TAG, $value, $force); - } - - public function getBlockPos(int $key) : ?Vector3{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_POS); - assert($value instanceof Vector3 or $value === null); - return $value; - } - - public function setBlockPos(int $key, ?Vector3 $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_POS, $value !== null ? $value->floor() : null, $force); - } - - public function getLong(int $key) : ?int{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_LONG); - assert(is_int($value) or $value === null); - return $value; - } - - public function setLong(int $key, int $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_LONG, $value, $force); - } - - public function getVector3(int $key) : ?Vector3{ - $value = $this->getPropertyValue($key, Entity::DATA_TYPE_VECTOR3F); - assert($value instanceof Vector3 or $value === null); - return $value; - } - - public function setVector3(int $key, ?Vector3 $value, bool $force = false) : void{ - $this->setPropertyValue($key, Entity::DATA_TYPE_VECTOR3F, $value !== null ? $value->asVector3() : null, $force); - } - - public function removeProperty(int $key) : void{ - unset($this->properties[$key]); - } - - public function hasProperty(int $key) : bool{ - return isset($this->properties[$key]); - } - - public function getPropertyType(int $key) : int{ - if(isset($this->properties[$key])){ - return $this->properties[$key][0]; - } - - return -1; - } - - private function checkType(int $key, int $type) : void{ - if(isset($this->properties[$key]) and $this->properties[$key][0] !== $type){ - throw new \RuntimeException("Expected type $type, but have " . $this->properties[$key][0]); - } - } - - /** - * @return mixed - */ - public function getPropertyValue(int $key, int $type){ - if($type !== -1){ - $this->checkType($key, $type); - } - return isset($this->properties[$key]) ? $this->properties[$key][1] : null; - } - - /** - * @param mixed $value - */ - public function setPropertyValue(int $key, int $type, $value, bool $force = false) : void{ - if(!$force){ - $this->checkType($key, $type); - } - $this->properties[$key] = $this->dirtyProperties[$key] = [$type, $value]; - } - - /** - * Returns all properties. - * - * @return mixed[][] - * @phpstan-return array - */ - public function getAll() : array{ - return $this->properties; - } - - /** - * Returns properties that have changed and need to be broadcasted. - * - * @return mixed[][] - * @phpstan-return array - */ - public function getDirty() : array{ - return $this->dirtyProperties; - } - - /** - * Clears records of dirty properties. - */ - public function clearDirtyProperties() : void{ - $this->dirtyProperties = []; - } -} diff --git a/src/pocketmine/entity/Effect.php b/src/pocketmine/entity/Effect.php deleted file mode 100644 index 47447a7c93..0000000000 --- a/src/pocketmine/entity/Effect.php +++ /dev/null @@ -1,343 +0,0 @@ -getId()] = $effect; - } - - public static function getEffect(int $id) : ?Effect{ - return self::$effects[$id] ?? null; - } - - public static function getEffectByName(string $name) : ?Effect{ - $const = self::class . "::" . mb_strtoupper($name); - if(defined($const)){ - return self::getEffect(constant($const)); - } - return null; - } - - /** @var int */ - protected $id; - /** @var string */ - protected $name; - /** @var Color */ - protected $color; - /** @var bool */ - protected $bad; - /** @var int */ - protected $defaultDuration; - /** @var bool */ - protected $hasBubbles; - - /** - * @param int $id Effect ID as per Minecraft PE - * @param string $name Translation key used for effect name - * @param Color $color Color of bubbles given by this effect - * @param bool $isBad Whether the effect is harmful - * @param int $defaultDuration Duration in ticks the effect will last for by default if applied without a duration. - * @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(int $id, string $name, Color $color, bool $isBad = false, int $defaultDuration = 300 * 20, bool $hasBubbles = true){ - $this->id = $id; - $this->name = $name; - $this->color = $color; - $this->bad = $isBad; - $this->defaultDuration = $defaultDuration; - $this->hasBubbles = $hasBubbles; - } - - /** - * Returns the effect ID as per Minecraft PE - */ - public function getId() : int{ - return $this->id; - } - - /** - * Returns the translation key used to translate this effect's name. - */ - public function getName() : string{ - return $this->name; - } - - /** - * Returns a Color object representing this effect's particle colour. - */ - public function getColor() : Color{ - return clone $this->color; - } - - /** - * Returns whether this effect is harmful. - * TODO: implement inverse effect results for undead mobs - */ - public function isBad() : bool{ - return $this->bad; - } - - /** - * Returns whether the effect is by default an instant effect. - */ - public function isInstantEffect() : bool{ - return $this->defaultDuration <= 1; - } - - /** - * Returns the default duration (in ticks) this effect will apply for if a duration is not specified. - */ - public function getDefaultDuration() : int{ - return $this->defaultDuration; - } - - /** - * Returns whether this effect will give the subject potion bubbles. - */ - public function hasBubbles() : bool{ - return $this->hasBubbles; - } - - /** - * Returns whether the effect will do something on the current tick. - */ - public function canTick(EffectInstance $instance) : bool{ - switch($this->id){ - case Effect::POISON: - case Effect::FATAL_POISON: - if(($interval = (25 >> $instance->getAmplifier())) > 0){ - return ($instance->getDuration() % $interval) === 0; - } - return true; - case Effect::WITHER: - if(($interval = (50 >> $instance->getAmplifier())) > 0){ - return ($instance->getDuration() % $interval) === 0; - } - return true; - case Effect::REGENERATION: - if(($interval = (40 >> $instance->getAmplifier())) > 0){ - return ($instance->getDuration() % $interval) === 0; - } - return true; - case Effect::HUNGER: - return true; - case Effect::INSTANT_DAMAGE: - case Effect::INSTANT_HEALTH: - case Effect::SATURATION: - //If forced to last longer than 1 tick, these apply every tick. - return true; - } - return false; - } - - /** - * Applies effect results to an entity. This will not be called unless canTick() returns true. - */ - public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null, ?Entity $sourceOwner = null) : void{ - switch($this->id){ - /** @noinspection PhpMissingBreakStatementInspection */ - case Effect::POISON: - if($entity->getHealth() <= 1){ - break; - } - case Effect::FATAL_POISON: - $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, 1); - $entity->attack($ev); - break; - - case Effect::WITHER: - $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, 1); - $entity->attack($ev); - break; - - case Effect::REGENERATION: - if($entity->getHealth() < $entity->getMaxHealth()){ - $ev = new EntityRegainHealthEvent($entity, 1, EntityRegainHealthEvent::CAUSE_MAGIC); - $entity->heal($ev); - } - break; - - case Effect::HUNGER: - if($entity instanceof Human){ - $entity->exhaust(0.025 * $instance->getEffectLevel(), PlayerExhaustEvent::CAUSE_POTION); - } - break; - case Effect::INSTANT_HEALTH: - //TODO: add particles (witch spell) - if($entity->getHealth() < $entity->getMaxHealth()){ - $entity->heal(new EntityRegainHealthEvent($entity, (4 << $instance->getAmplifier()) * $potency, EntityRegainHealthEvent::CAUSE_MAGIC)); - } - break; - case Effect::INSTANT_DAMAGE: - //TODO: add particles (witch spell) - $damage = (4 << $instance->getAmplifier()) * $potency; - if($source !== null and $sourceOwner !== null){ - $ev = new EntityDamageByChildEntityEvent($sourceOwner, $source, $entity, EntityDamageEvent::CAUSE_MAGIC, $damage); - }elseif($source !== null){ - $ev = new EntityDamageByEntityEvent($source, $entity, EntityDamageEvent::CAUSE_MAGIC, $damage); - }else{ - $ev = new EntityDamageEvent($entity, EntityDamageEvent::CAUSE_MAGIC, $damage); - } - $entity->attack($ev); - - break; - case Effect::SATURATION: - if($entity instanceof Human){ - $entity->addFood($instance->getEffectLevel()); - $entity->addSaturation($instance->getEffectLevel() * 2); - } - break; - } - } - - /** - * Applies effects to the entity when the effect is first added. - */ - public function add(Living $entity, EffectInstance $instance) : void{ - switch($this->id){ - case Effect::INVISIBILITY: - $entity->setInvisible(); - $entity->setNameTagVisible(false); - break; - case Effect::SPEED: - $attr = $entity->getAttributeMap()->getAttribute(Attribute::MOVEMENT_SPEED); - $attr->setValue($attr->getValue() * (1 + 0.2 * $instance->getEffectLevel())); - break; - case Effect::SLOWNESS: - $attr = $entity->getAttributeMap()->getAttribute(Attribute::MOVEMENT_SPEED); - $attr->setValue($attr->getValue() * (1 - 0.15 * $instance->getEffectLevel()), true); - break; - - case Effect::HEALTH_BOOST: - $entity->setMaxHealth($entity->getMaxHealth() + 4 * $instance->getEffectLevel()); - break; - case Effect::ABSORPTION: - $new = (4 * $instance->getEffectLevel()); - if($new > $entity->getAbsorption()){ - $entity->setAbsorption($new); - } - break; - } - } - - /** - * Removes the effect from the entity, resetting any changed values back to their original defaults. - */ - public function remove(Living $entity, EffectInstance $instance) : void{ - switch($this->id){ - case Effect::INVISIBILITY: - $entity->setInvisible(false); - $entity->setNameTagVisible(true); - break; - case Effect::SPEED: - $attr = $entity->getAttributeMap()->getAttribute(Attribute::MOVEMENT_SPEED); - $attr->setValue($attr->getValue() / (1 + 0.2 * $instance->getEffectLevel())); - break; - case Effect::SLOWNESS: - $attr = $entity->getAttributeMap()->getAttribute(Attribute::MOVEMENT_SPEED); - $attr->setValue($attr->getValue() / (1 - 0.15 * $instance->getEffectLevel())); - break; - case Effect::HEALTH_BOOST: - $entity->setMaxHealth($entity->getMaxHealth() - 4 * $instance->getEffectLevel()); - break; - case Effect::ABSORPTION: - $entity->setAbsorption(0); - break; - } - } - - public function __clone(){ - $this->color = clone $this->color; - } -} diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php deleted file mode 100644 index 39e1a28fcd..0000000000 --- a/src/pocketmine/entity/Entity.php +++ /dev/null @@ -1,2193 +0,0 @@ -> - */ - private static $knownEntities = []; - /** - * @var string[] - * @phpstan-var array, string> - */ - private static $saveNames = []; - - /** - * Called on server startup to register default entity types. - */ - public static function init() : void{ - //define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC - //TODO: index them by version to allow proper multi-save compatibility - - Entity::registerEntity(Arrow::class, false, ['Arrow', 'minecraft:arrow']); - Entity::registerEntity(Egg::class, false, ['Egg', 'minecraft:egg']); - Entity::registerEntity(EnderPearl::class, false, ['ThrownEnderpearl', 'minecraft:ender_pearl']); - Entity::registerEntity(ExperienceBottle::class, false, ['ThrownExpBottle', 'minecraft:xp_bottle']); - Entity::registerEntity(ExperienceOrb::class, false, ['XPOrb', 'minecraft:xp_orb']); - Entity::registerEntity(FallingBlock::class, false, ['FallingSand', 'minecraft:falling_block']); - Entity::registerEntity(ItemEntity::class, false, ['Item', 'minecraft:item']); - Entity::registerEntity(Painting::class, false, ['Painting', 'minecraft:painting']); - Entity::registerEntity(PrimedTNT::class, false, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt']); - Entity::registerEntity(Snowball::class, false, ['Snowball', 'minecraft:snowball']); - Entity::registerEntity(SplashPotion::class, false, ['ThrownPotion', 'minecraft:potion', 'thrownpotion']); - Entity::registerEntity(Squid::class, false, ['Squid', 'minecraft:squid']); - Entity::registerEntity(Villager::class, false, ['Villager', 'minecraft:villager']); - Entity::registerEntity(Zombie::class, false, ['Zombie', 'minecraft:zombie']); - - Entity::registerEntity(Human::class, true); - - Attribute::init(); - Effect::init(); - PaintingMotive::init(); - } - - /** - * Creates an entity with the specified type, level and NBT, with optional additional arguments to pass to the - * entity's constructor - * - * @param int|string $type - * @param mixed ...$args - */ - public static function createEntity($type, Level $level, CompoundTag $nbt, ...$args) : ?Entity{ - if(isset(self::$knownEntities[$type])){ - $class = self::$knownEntities[$type]; - /** @see Entity::__construct() */ - return new $class($level, $nbt, ...$args); - } - - return null; - } - - /** - * Registers an entity type into the index. - * - * @param string $className Class that extends Entity - * @param bool $force Force registration even if the entity does not have a valid network ID - * @param string[] $saveNames An array of save names which this entity might be saved under. Defaults to the short name of the class itself if empty. - * @phpstan-param class-string $className - * - * NOTE: The first save name in the $saveNames array will be used when saving the entity to disk. The reflection - * name of the class will be appended to the end and only used if no other save names are specified. - */ - public static function registerEntity(string $className, bool $force = false, array $saveNames = []) : bool{ - $class = new \ReflectionClass($className); - if(is_a($className, Entity::class, true) and !$class->isAbstract()){ - if($className::NETWORK_ID !== -1){ - self::$knownEntities[$className::NETWORK_ID] = $className; - }elseif(!$force){ - return false; - } - - $shortName = $class->getShortName(); - if(!in_array($shortName, $saveNames, true)){ - $saveNames[] = $shortName; - } - - foreach($saveNames as $name){ - self::$knownEntities[$name] = $className; - } - - self::$saveNames[$className] = reset($saveNames); - - return true; - } - - return false; - } - - /** - * Helper function which creates minimal NBT needed to spawn an entity. - */ - public static function createBaseNBT(Vector3 $pos, ?Vector3 $motion = null, float $yaw = 0.0, float $pitch = 0.0) : CompoundTag{ - return new CompoundTag("", [ - new ListTag("Pos", [ - new DoubleTag("", $pos->x), - new DoubleTag("", $pos->y), - new DoubleTag("", $pos->z) - ]), - new ListTag("Motion", [ - new DoubleTag("", $motion !== null ? $motion->x : 0.0), - new DoubleTag("", $motion !== null ? $motion->y : 0.0), - new DoubleTag("", $motion !== null ? $motion->z : 0.0) - ]), - new ListTag("Rotation", [ - new FloatTag("", $yaw), - new FloatTag("", $pitch) - ]) - ]); - } - - /** @var Player[] */ - protected $hasSpawned = []; - - /** @var int */ - protected $id; - - /** @var DataPropertyManager */ - protected $propertyManager; - - /** @var Chunk|null */ - public $chunk; - - /** @var EntityDamageEvent|null */ - protected $lastDamageCause = null; - - /** @var Block[]|null */ - protected $blocksAround = null; - - /** @var float */ - public $lastX; - /** @var float */ - public $lastY; - /** @var float */ - public $lastZ; - - /** @var Vector3 */ - protected $motion; - /** @var Vector3 */ - protected $lastMotion; - /** @var bool */ - protected $forceMovementUpdate = false; - - /** @var Vector3 */ - public $temporalVector; - - /** @var float */ - public $lastYaw; - /** @var float */ - public $lastPitch; - - /** @var AxisAlignedBB */ - public $boundingBox; - /** @var bool */ - public $onGround; - - /** @var float */ - public $eyeHeight = null; - - /** @var float */ - public $height; - /** @var float */ - public $width; - - /** @var float */ - protected $baseOffset = 0.0; - - /** @var float */ - private $health = 20.0; - /** @var int */ - private $maxHealth = 20; - - /** @var float */ - protected $ySize = 0.0; - /** @var float */ - protected $stepHeight = 0.0; - /** @var bool */ - public $keepMovement = false; - - /** @var float */ - public $fallDistance = 0.0; - /** @var int */ - public $ticksLived = 0; - /** @var int */ - public $lastUpdate; - /** @var int */ - protected $fireTicks = 0; - /** @var CompoundTag */ - public $namedtag; - /** @var bool */ - public $canCollide = true; - - /** @var bool */ - protected $isStatic = false; - - /** @var bool */ - private $savedWithChunk = true; - - /** @var bool */ - public $isCollided = false; - /** @var bool */ - public $isCollidedHorizontally = false; - /** @var bool */ - public $isCollidedVertically = false; - - /** @var int */ - public $noDamageTicks; - /** @var bool */ - protected $justCreated = true; - /** @var bool */ - private $invulnerable; - - /** @var AttributeMap */ - protected $attributeMap; - - /** @var float */ - protected $gravity; - /** @var float */ - protected $drag; - - /** @var Server */ - protected $server; - - /** @var bool */ - protected $closed = false; - /** @var bool */ - private $needsDespawn = false; - - /** @var TimingsHandler */ - protected $timings; - - /** @var bool */ - protected $constructed = false; - - /** @var bool */ - private $closeInFlight = false; - - public function __construct(Level $level, CompoundTag $nbt){ - $this->constructed = true; - $this->timings = Timings::getEntityTimings($this); - - $this->temporalVector = new Vector3(); - - if($this->eyeHeight === null){ - $this->eyeHeight = $this->height / 2 + 0.1; - } - - $this->id = Entity::$entityCount++; - $this->namedtag = $nbt; - $this->server = $level->getServer(); - - /** @var float[] $pos */ - $pos = $this->namedtag->getListTag("Pos")->getAllValues(); - /** @var float[] $rotation */ - $rotation = $this->namedtag->getListTag("Rotation")->getAllValues(); - - parent::__construct($pos[0], $pos[1], $pos[2], $rotation[0], $rotation[1], $level); - assert(!is_nan($this->x) and !is_infinite($this->x) and !is_nan($this->y) and !is_infinite($this->y) and !is_nan($this->z) and !is_infinite($this->z)); - - $this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0); - $this->recalculateBoundingBox(); - - $this->chunk = $this->level->getChunkAtPosition($this, false); - if($this->chunk === null){ - throw new \InvalidStateException("Cannot create entities in unloaded chunks"); - } - - $this->motion = new Vector3(0, 0, 0); - if($this->namedtag->hasTag("Motion", ListTag::class)){ - /** @var float[] $motion */ - $motion = $this->namedtag->getListTag("Motion")->getAllValues(); - $this->setMotion($this->temporalVector->setComponents(...$motion)); - } - - $this->resetLastMovements(); - - $this->fallDistance = $this->namedtag->getFloat("FallDistance", 0.0); - - $this->propertyManager = new DataPropertyManager(); - - $this->propertyManager->setLong(self::DATA_FLAGS, 0); - $this->propertyManager->setShort(self::DATA_MAX_AIR, 400); - $this->propertyManager->setString(self::DATA_NAMETAG, ""); - $this->propertyManager->setLong(self::DATA_LEAD_HOLDER_EID, -1); - $this->propertyManager->setFloat(self::DATA_SCALE, 1); - $this->propertyManager->setFloat(self::DATA_BOUNDING_BOX_WIDTH, $this->width); - $this->propertyManager->setFloat(self::DATA_BOUNDING_BOX_HEIGHT, $this->height); - - $this->fireTicks = $this->namedtag->getShort("Fire", 0); - if($this->isOnFire()){ - $this->setGenericFlag(self::DATA_FLAG_ONFIRE); - } - - $this->propertyManager->setShort(self::DATA_AIR, $this->namedtag->getShort("Air", 300)); - $this->onGround = $this->namedtag->getByte("OnGround", 0) !== 0; - $this->invulnerable = $this->namedtag->getByte("Invulnerable", 0) !== 0; - - $this->attributeMap = new AttributeMap(); - $this->addAttributes(); - - $this->setGenericFlag(self::DATA_FLAG_AFFECTED_BY_GRAVITY, true); - $this->setGenericFlag(self::DATA_FLAG_HAS_COLLISION, true); - - $this->initEntity(); - $this->propertyManager->clearDirtyProperties(); //Prevents resending properties that were set during construction - - $this->chunk->addEntity($this); - $this->level->addEntity($this); - - $this->lastUpdate = $this->server->getTick(); - (new EntitySpawnEvent($this))->call(); - - $this->scheduleUpdate(); - - } - - public function getNameTag() : string{ - return $this->propertyManager->getString(self::DATA_NAMETAG); - } - - public function isNameTagVisible() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_CAN_SHOW_NAMETAG); - } - - public function isNameTagAlwaysVisible() : bool{ - return $this->propertyManager->getByte(self::DATA_ALWAYS_SHOW_NAMETAG) === 1; - } - - public function setNameTag(string $name) : void{ - $this->propertyManager->setString(self::DATA_NAMETAG, $name); - } - - public function setNameTagVisible(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_CAN_SHOW_NAMETAG, $value); - } - - public function setNameTagAlwaysVisible(bool $value = true) : void{ - $this->propertyManager->setByte(self::DATA_ALWAYS_SHOW_NAMETAG, $value ? 1 : 0); - } - - public function getScoreTag() : ?string{ - return $this->propertyManager->getString(self::DATA_SCORE_TAG); - } - - public function setScoreTag(string $score) : void{ - $this->propertyManager->setString(self::DATA_SCORE_TAG, $score); - } - - public function getScale() : float{ - return $this->propertyManager->getFloat(self::DATA_SCALE); - } - - public function setScale(float $value) : void{ - if($value <= 0){ - throw new \InvalidArgumentException("Scale must be greater than 0"); - } - $multiplier = $value / $this->getScale(); - - $this->width *= $multiplier; - $this->height *= $multiplier; - $this->eyeHeight *= $multiplier; - - $this->recalculateBoundingBox(); - - $this->propertyManager->setFloat(self::DATA_SCALE, $value); - } - - public function getBoundingBox() : AxisAlignedBB{ - return $this->boundingBox; - } - - protected function recalculateBoundingBox() : void{ - $halfWidth = $this->width / 2; - - $this->boundingBox->setBounds( - $this->x - $halfWidth, - $this->y + $this->ySize, - $this->z - $halfWidth, - $this->x + $halfWidth, - $this->y + $this->height + $this->ySize, - $this->z + $halfWidth - ); - } - - public function isSneaking() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_SNEAKING); - } - - public function setSneaking(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_SNEAKING, $value); - } - - public function isSprinting() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_SPRINTING); - } - - public function setSprinting(bool $value = true) : void{ - if($value !== $this->isSprinting()){ - $this->setGenericFlag(self::DATA_FLAG_SPRINTING, $value); - $attr = $this->attributeMap->getAttribute(Attribute::MOVEMENT_SPEED); - $attr->setValue($value ? ($attr->getValue() * 1.3) : ($attr->getValue() / 1.3), false, true); - } - } - - public function isImmobile() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_IMMOBILE); - } - - public function setImmobile(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_IMMOBILE, $value); - } - - public function isInvisible() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_INVISIBLE); - } - - public function setInvisible(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_INVISIBLE, $value); - } - - /** - * Returns whether the entity is able to climb blocks such as ladders or vines. - */ - public function canClimb() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_CAN_CLIMB); - } - - /** - * Sets whether the entity is able to climb climbable blocks. - */ - public function setCanClimb(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_CAN_CLIMB, $value); - } - - /** - * Returns whether this entity is climbing a block. By default this is only true if the entity is climbing a ladder or vine or similar block. - */ - public function canClimbWalls() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_WALLCLIMBING); - } - - /** - * Sets whether the entity is climbing a block. If true, the entity can climb anything. - */ - public function setCanClimbWalls(bool $value = true) : void{ - $this->setGenericFlag(self::DATA_FLAG_WALLCLIMBING, $value); - } - - /** - * Returns the entity ID of the owning entity, or null if the entity doesn't have an owner. - */ - public function getOwningEntityId() : ?int{ - return $this->propertyManager->getLong(self::DATA_OWNER_EID); - } - - /** - * Returns the owning entity, or null if the entity was not found. - */ - public function getOwningEntity() : ?Entity{ - $eid = $this->getOwningEntityId(); - if($eid !== null){ - return $this->server->findEntity($eid); - } - - return null; - } - - /** - * Sets the owner of the entity. Passing null will remove the current owner. - * - * @throws \InvalidArgumentException if the supplied entity is not valid - */ - public function setOwningEntity(?Entity $owner) : void{ - if($owner === null){ - $this->propertyManager->removeProperty(self::DATA_OWNER_EID); - }elseif($owner->closed){ - throw new \InvalidArgumentException("Supplied owning entity is garbage and cannot be used"); - }else{ - $this->propertyManager->setLong(self::DATA_OWNER_EID, $owner->getId()); - } - } - - /** - * Returns the entity ID of the entity's target, or null if it doesn't have a target. - */ - public function getTargetEntityId() : ?int{ - return $this->propertyManager->getLong(self::DATA_TARGET_EID); - } - - /** - * Returns the entity's target entity, or null if not found. - * This is used for things like hostile mobs attacking entities, and for fishing rods reeling hit entities in. - */ - public function getTargetEntity() : ?Entity{ - $eid = $this->getTargetEntityId(); - if($eid !== null){ - return $this->server->findEntity($eid); - } - - return null; - } - - /** - * Sets the entity's target entity. Passing null will remove the current target. - * - * @throws \InvalidArgumentException if the target entity is not valid - */ - public function setTargetEntity(?Entity $target) : void{ - if($target === null){ - $this->propertyManager->removeProperty(self::DATA_TARGET_EID); - }elseif($target->closed){ - throw new \InvalidArgumentException("Supplied target entity is garbage and cannot be used"); - }else{ - $this->propertyManager->setLong(self::DATA_TARGET_EID, $target->getId()); - } - } - - /** - * Returns whether this entity will be saved when its chunk is unloaded. - */ - public function canSaveWithChunk() : bool{ - return $this->savedWithChunk; - } - - /** - * Sets whether this entity will be saved when its chunk is unloaded. This can be used to prevent the entity being - * saved to disk. - */ - public function setCanSaveWithChunk(bool $value) : void{ - $this->savedWithChunk = $value; - } - - /** - * Returns the short save name - */ - public function getSaveId() : string{ - if(!isset(self::$saveNames[static::class])){ - throw new \InvalidStateException("Entity " . static::class . " is not registered"); - } - return self::$saveNames[static::class]; - } - - public function saveNBT() : void{ - if(!($this instanceof Player)){ - $this->namedtag->setString("id", $this->getSaveId(), true); - - if($this->getNameTag() !== ""){ - $this->namedtag->setString("CustomName", $this->getNameTag()); - $this->namedtag->setByte("CustomNameVisible", $this->isNameTagVisible() ? 1 : 0); - }else{ - $this->namedtag->removeTag("CustomName", "CustomNameVisible"); - } - } - - $this->namedtag->setTag(new ListTag("Pos", [ - new DoubleTag("", $this->x), - new DoubleTag("", $this->y), - new DoubleTag("", $this->z) - ])); - - $this->namedtag->setTag(new ListTag("Motion", [ - new DoubleTag("", $this->motion->x), - new DoubleTag("", $this->motion->y), - new DoubleTag("", $this->motion->z) - ])); - - $this->namedtag->setTag(new ListTag("Rotation", [ - new FloatTag("", $this->yaw), - new FloatTag("", $this->pitch) - ])); - - $this->namedtag->setFloat("FallDistance", $this->fallDistance); - $this->namedtag->setShort("Fire", $this->fireTicks); - $this->namedtag->setShort("Air", $this->propertyManager->getShort(self::DATA_AIR)); - $this->namedtag->setByte("OnGround", $this->onGround ? 1 : 0); - $this->namedtag->setByte("Invulnerable", $this->invulnerable ? 1 : 0); - } - - protected function initEntity() : void{ - if($this->namedtag->hasTag("CustomName", StringTag::class)){ - $this->setNameTag($this->namedtag->getString("CustomName")); - - if($this->namedtag->hasTag("CustomNameVisible", StringTag::class)){ - //Older versions incorrectly saved this as a string (see 890f72dbf23a77f294169b79590770470041adc4) - $this->setNameTagVisible($this->namedtag->getString("CustomNameVisible") !== ""); - $this->namedtag->removeTag("CustomNameVisible"); - }else{ - $this->setNameTagVisible($this->namedtag->getByte("CustomNameVisible", 1) !== 0); - } - } - } - - protected function addAttributes() : void{ - - } - - public function attack(EntityDamageEvent $source) : void{ - $source->call(); - if($source->isCancelled()){ - return; - } - - $this->setLastDamageCause($source); - - $this->setHealth($this->getHealth() - $source->getFinalDamage()); - } - - public function heal(EntityRegainHealthEvent $source) : void{ - $source->call(); - if($source->isCancelled()){ - return; - } - - $this->setHealth($this->getHealth() + $source->getAmount()); - } - - public function kill() : void{ - $this->health = 0; - $this->scheduleUpdate(); - } - - /** - * Called to tick entities while dead. Returns whether the entity should be flagged for despawn yet. - */ - protected function onDeathUpdate(int $tickDiff) : bool{ - return true; - } - - public function isAlive() : bool{ - return $this->health > 0; - } - - public function getHealth() : float{ - return $this->health; - } - - /** - * Sets the health of the Entity. This won't send any update to the players - */ - public function setHealth(float $amount) : void{ - if($amount == $this->health){ - return; - } - - if($amount <= 0){ - if($this->isAlive()){ - $this->health = 0; - $this->kill(); - } - }elseif($amount <= $this->getMaxHealth() or $amount < $this->health){ - $this->health = $amount; - }else{ - $this->health = $this->getMaxHealth(); - } - } - - public function getMaxHealth() : int{ - return $this->maxHealth; - } - - public function setMaxHealth(int $amount) : void{ - $this->maxHealth = $amount; - } - - public function setLastDamageCause(EntityDamageEvent $type) : void{ - $this->lastDamageCause = $type; - } - - public function getLastDamageCause() : ?EntityDamageEvent{ - return $this->lastDamageCause; - } - - public function getAttributeMap() : AttributeMap{ - return $this->attributeMap; - } - - public function getDataPropertyManager() : DataPropertyManager{ - return $this->propertyManager; - } - - public function entityBaseTick(int $tickDiff = 1) : bool{ - //TODO: check vehicles - - $this->justCreated = false; - - $changedProperties = $this->propertyManager->getDirty(); - if(count($changedProperties) > 0){ - $this->sendData($this->hasSpawned, $changedProperties); - $this->propertyManager->clearDirtyProperties(); - } - - $hasUpdate = false; - - $this->checkBlockCollision(); - - if($this->y <= -16 and $this->isAlive()){ - $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10); - $this->attack($ev); - $hasUpdate = true; - } - - if($this->isOnFire() and $this->doOnFireTick($tickDiff)){ - $hasUpdate = true; - } - - if($this->noDamageTicks > 0){ - $this->noDamageTicks -= $tickDiff; - if($this->noDamageTicks < 0){ - $this->noDamageTicks = 0; - } - } - - $this->ticksLived += $tickDiff; - - return $hasUpdate; - } - - public function isOnFire() : bool{ - return $this->fireTicks > 0; - } - - public function setOnFire(int $seconds) : void{ - $ticks = $seconds * 20; - if($ticks > $this->getFireTicks()){ - $this->setFireTicks($ticks); - } - - $this->setGenericFlag(self::DATA_FLAG_ONFIRE, $this->isOnFire()); - } - - public function getFireTicks() : int{ - return $this->fireTicks; - } - - /** - * @throws \InvalidArgumentException - */ - public function setFireTicks(int $fireTicks) : void{ - if($fireTicks < 0 or $fireTicks > 0x7fff){ - throw new \InvalidArgumentException("Fire ticks must be in range 0 ... " . 0x7fff . ", got $fireTicks"); - } - $this->fireTicks = $fireTicks; - } - - public function extinguish() : void{ - $this->fireTicks = 0; - $this->setGenericFlag(self::DATA_FLAG_ONFIRE, false); - } - - public function isFireProof() : bool{ - return false; - } - - protected function doOnFireTick(int $tickDiff = 1) : bool{ - if($this->isFireProof() and $this->fireTicks > 1){ - $this->fireTicks = 1; - }else{ - $this->fireTicks -= $tickDiff; - } - - if(($this->fireTicks % 20 === 0) or $tickDiff > 20){ - $this->dealFireDamage(); - } - - if(!$this->isOnFire()){ - $this->extinguish(); - }else{ - return true; - } - - return false; - } - - /** - * Called to deal damage to entities when they are on fire. - */ - protected function dealFireDamage() : void{ - $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_FIRE_TICK, 1); - $this->attack($ev); - } - - public function canCollideWith(Entity $entity) : bool{ - return !$this->justCreated and $entity !== $this; - } - - public function canBeCollidedWith() : bool{ - return $this->isAlive(); - } - - protected function updateMovement(bool $teleport = false) : void{ - $diffPosition = ($this->x - $this->lastX) ** 2 + ($this->y - $this->lastY) ** 2 + ($this->z - $this->lastZ) ** 2; - $diffRotation = ($this->yaw - $this->lastYaw) ** 2 + ($this->pitch - $this->lastPitch) ** 2; - - $diffMotion = $this->motion->subtract($this->lastMotion)->lengthSquared(); - - $still = $this->motion->lengthSquared() == 0.0; - $wasStill = $this->lastMotion->lengthSquared() == 0.0; - if($wasStill !== $still){ - //TODO: hack for client-side AI interference: prevent client sided movement when motion is 0 - $this->setImmobile($still); - } - - if($teleport or $diffPosition > 0.0001 or $diffRotation > 1.0 or (!$wasStill and $still)){ - $this->lastX = $this->x; - $this->lastY = $this->y; - $this->lastZ = $this->z; - - $this->lastYaw = $this->yaw; - $this->lastPitch = $this->pitch; - - $this->broadcastMovement($teleport); - } - - if($diffMotion > 0.0025 or $wasStill !== $still){ //0.05 ** 2 - $this->lastMotion = clone $this->motion; - - $this->broadcastMotion(); - } - } - - public function getOffsetPosition(Vector3 $vector3) : Vector3{ - return new Vector3($vector3->x, $vector3->y + $this->baseOffset, $vector3->z); - } - - protected function broadcastMovement(bool $teleport = false) : void{ - $pk = new MoveActorAbsolutePacket(); - $pk->entityRuntimeId = $this->id; - $pk->position = $this->getOffsetPosition($this); - - //this looks very odd but is correct as of 1.5.0.7 - //for arrows this is actually x/y/z rotation - //for mobs x and z are used for pitch and yaw, and y is used for headyaw - $pk->xRot = $this->pitch; - $pk->yRot = $this->yaw; //TODO: head yaw - $pk->zRot = $this->yaw; - - if($teleport){ - $pk->flags |= MoveActorAbsolutePacket::FLAG_TELEPORT; - } - if($this->onGround){ - $pk->flags |= MoveActorAbsolutePacket::FLAG_GROUND; - } - - $this->level->broadcastPacketToViewers($this, $pk); - } - - protected function broadcastMotion() : void{ - $pk = new SetActorMotionPacket(); - $pk->entityRuntimeId = $this->id; - $pk->motion = $this->getMotion(); - - $this->level->broadcastPacketToViewers($this, $pk); - } - - protected function applyDragBeforeGravity() : bool{ - return false; - } - - protected function applyGravity() : void{ - $this->motion->y -= $this->gravity; - } - - protected function tryChangeMovement() : void{ - $friction = 1 - $this->drag; - - if($this->applyDragBeforeGravity()){ - $this->motion->y *= $friction; - } - - $this->applyGravity(); - - if(!$this->applyDragBeforeGravity()){ - $this->motion->y *= $friction; - } - - if($this->onGround){ - $friction *= $this->level->getBlockAt((int) floor($this->x), (int) floor($this->y - 1), (int) floor($this->z))->getFrictionFactor(); - } - - $this->motion->x *= $friction; - $this->motion->z *= $friction; - } - - protected function checkObstruction(float $x, float $y, float $z) : bool{ - if(count($this->level->getCollisionCubes($this, $this->getBoundingBox(), false)) === 0){ - return false; - } - - $floorX = (int) floor($x); - $floorY = (int) floor($y); - $floorZ = (int) floor($z); - - $diffX = $x - $floorX; - $diffY = $y - $floorY; - $diffZ = $z - $floorZ; - - if(BlockFactory::$solid[$this->level->getBlockIdAt($floorX, $floorY, $floorZ)]){ - $westNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX - 1, $floorY, $floorZ)]; - $eastNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX + 1, $floorY, $floorZ)]; - $downNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX, $floorY - 1, $floorZ)]; - $upNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX, $floorY + 1, $floorZ)]; - $northNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX, $floorY, $floorZ - 1)]; - $southNonSolid = !BlockFactory::$solid[$this->level->getBlockIdAt($floorX, $floorY, $floorZ + 1)]; - - $direction = -1; - $limit = 9999; - - if($westNonSolid){ - $limit = $diffX; - $direction = Vector3::SIDE_WEST; - } - - if($eastNonSolid and 1 - $diffX < $limit){ - $limit = 1 - $diffX; - $direction = Vector3::SIDE_EAST; - } - - if($downNonSolid and $diffY < $limit){ - $limit = $diffY; - $direction = Vector3::SIDE_DOWN; - } - - if($upNonSolid and 1 - $diffY < $limit){ - $limit = 1 - $diffY; - $direction = Vector3::SIDE_UP; - } - - if($northNonSolid and $diffZ < $limit){ - $limit = $diffZ; - $direction = Vector3::SIDE_NORTH; - } - - if($southNonSolid and 1 - $diffZ < $limit){ - $direction = Vector3::SIDE_SOUTH; - } - - $force = lcg_value() * 0.2 + 0.1; - - if($direction === Vector3::SIDE_WEST){ - $this->motion->x = -$force; - - return true; - } - - if($direction === Vector3::SIDE_EAST){ - $this->motion->x = $force; - - return true; - } - - if($direction === Vector3::SIDE_DOWN){ - $this->motion->y = -$force; - - return true; - } - - if($direction === Vector3::SIDE_UP){ - $this->motion->y = $force; - - return true; - } - - if($direction === Vector3::SIDE_NORTH){ - $this->motion->z = -$force; - - return true; - } - - if($direction === Vector3::SIDE_SOUTH){ - $this->motion->z = $force; - - return true; - } - } - - return false; - } - - public function getDirection() : ?int{ - $rotation = fmod($this->yaw - 90, 360); - if($rotation < 0){ - $rotation += 360.0; - } - if((0 <= $rotation and $rotation < 45) or (315 <= $rotation and $rotation < 360)){ - return 2; //North - }elseif(45 <= $rotation and $rotation < 135){ - return 3; //East - }elseif(135 <= $rotation and $rotation < 225){ - return 0; //South - }elseif(225 <= $rotation and $rotation < 315){ - return 1; //West - }else{ - return null; - } - } - - public function getDirectionVector() : Vector3{ - $y = -sin(deg2rad($this->pitch)); - $xz = cos(deg2rad($this->pitch)); - $x = -$xz * sin(deg2rad($this->yaw)); - $z = $xz * cos(deg2rad($this->yaw)); - - return $this->temporalVector->setComponents($x, $y, $z)->normalize(); - } - - public function getDirectionPlane() : Vector2{ - return (new Vector2(-cos(deg2rad($this->yaw) - M_PI_2), -sin(deg2rad($this->yaw) - M_PI_2)))->normalize(); - } - - public function onUpdate(int $currentTick) : bool{ - if($this->closed){ - return false; - } - - $tickDiff = $currentTick - $this->lastUpdate; - if($tickDiff <= 0){ - if(!$this->justCreated){ - $this->server->getLogger()->debug("Expected tick difference of at least 1, got $tickDiff for " . get_class($this)); - } - - return true; - } - - $this->lastUpdate = $currentTick; - - if(!$this->isAlive()){ - if($this->onDeathUpdate($tickDiff)){ - $this->flagForDespawn(); - } - - return true; - } - - $this->timings->startTiming(); - - if($this->hasMovementUpdate()){ - $this->tryChangeMovement(); - - if(abs($this->motion->x) <= self::MOTION_THRESHOLD){ - $this->motion->x = 0; - } - if(abs($this->motion->y) <= self::MOTION_THRESHOLD){ - $this->motion->y = 0; - } - if(abs($this->motion->z) <= self::MOTION_THRESHOLD){ - $this->motion->z = 0; - } - - if($this->motion->x != 0 or $this->motion->y != 0 or $this->motion->z != 0){ - $this->move($this->motion->x, $this->motion->y, $this->motion->z); - } - - $this->forceMovementUpdate = false; - } - - $this->updateMovement(); - - Timings::$timerEntityBaseTick->startTiming(); - $hasUpdate = $this->entityBaseTick($tickDiff); - Timings::$timerEntityBaseTick->stopTiming(); - - $this->timings->stopTiming(); - - //if($this->isStatic()) - return ($hasUpdate or $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)); - } - $this->level->updateEntities[$this->id] = $this; - } - - public function onNearbyBlockChange() : void{ - $this->setForceMovementUpdate(); - $this->scheduleUpdate(); - } - - /** - * Flags the entity as needing a movement update on the next tick. Setting this forces a movement update even if the - * entity's motion is zero. Used to trigger movement updates when blocks change near entities. - */ - final public function setForceMovementUpdate(bool $value = true) : void{ - $this->forceMovementUpdate = $value; - - $this->blocksAround = null; - } - - /** - * Returns whether the entity needs a movement update on the next tick. - */ - public function hasMovementUpdate() : bool{ - return ( - $this->forceMovementUpdate or - $this->motion->x != 0 or - $this->motion->y != 0 or - $this->motion->z != 0 or - !$this->onGround - ); - } - - public function canTriggerWalking() : bool{ - return true; - } - - public function resetFallDistance() : void{ - $this->fallDistance = 0.0; - } - - protected function updateFallState(float $distanceThisTick, bool $onGround) : void{ - if($onGround){ - if($this->fallDistance > 0){ - $this->fall($this->fallDistance); - $this->resetFallDistance(); - } - }elseif($distanceThisTick < $this->fallDistance){ - //we've fallen some distance (distanceThisTick is negative) - //or we ascended back towards where fall distance was measured from initially (distanceThisTick is positive but less than existing fallDistance) - $this->fallDistance -= $distanceThisTick; - }else{ - //we ascended past the apex where fall distance was originally being measured from - //reset it so it will be measured starting from the new, higher position - $this->fallDistance = 0; - } - } - - /** - * Called when a falling entity hits the ground. - */ - public function fall(float $fallDistance) : void{ - - } - - public function getEyeHeight() : float{ - return $this->eyeHeight; - } - - public function onCollideWithPlayer(Player $player) : void{ - - } - - public function isUnderwater() : bool{ - $block = $this->level->getBlockAt((int) floor($this->x), (int) floor($y = ($this->y + $this->getEyeHeight())), (int) floor($this->z)); - - if($block instanceof Water){ - $f = ($block->y + 1) - ($block->getFluidHeightPercent() - 0.1111111); - return $y < $f; - } - - return false; - } - - public function isInsideOfSolid() : bool{ - $block = $this->level->getBlockAt((int) floor($this->x), (int) floor($y = ($this->y + $this->getEyeHeight())), (int) floor($this->z)); - - return $block->isSolid() and !$block->isTransparent() and $block->collidesWithBB($this->getBoundingBox()); - } - - public function fastMove(float $dx, float $dy, float $dz) : bool{ - $this->blocksAround = null; - - if($dx == 0 and $dz == 0 and $dy == 0){ - return true; - } - - Timings::$entityMoveTimer->startTiming(); - - $newBB = $this->boundingBox->offsetCopy($dx, $dy, $dz); - - $list = $this->level->getCollisionCubes($this, $newBB, false); - - if(count($list) === 0){ - $this->boundingBox = $newBB; - } - - $this->x = ($this->boundingBox->minX + $this->boundingBox->maxX) / 2; - $this->y = $this->boundingBox->minY - $this->ySize; - $this->z = ($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2; - - $this->checkChunks(); - - if(!$this->onGround or $dy != 0){ - $bb = clone $this->boundingBox; - $bb->minY -= 0.75; - $this->onGround = false; - - if(count($this->level->getCollisionBlocks($bb)) > 0){ - $this->onGround = true; - } - } - $this->isCollided = $this->onGround; - $this->updateFallState($dy, $this->onGround); - - Timings::$entityMoveTimer->stopTiming(); - - return true; - } - - public function move(float $dx, float $dy, float $dz) : void{ - $this->blocksAround = null; - - Timings::$entityMoveTimer->startTiming(); - - $movX = $dx; - $movY = $dy; - $movZ = $dz; - - if($this->keepMovement){ - $this->boundingBox->offset($dx, $dy, $dz); - }else{ - $this->ySize *= self::STEP_CLIP_MULTIPLIER; - - /* - if($this->isColliding){ //With cobweb? - $this->isColliding = false; - $dx *= 0.25; - $dy *= 0.05; - $dz *= 0.25; - $this->motionX = 0; - $this->motionY = 0; - $this->motionZ = 0; - } - */ - - $axisalignedbb = clone $this->boundingBox; - - /*$sneakFlag = $this->onGround and $this instanceof Player; - - if($sneakFlag){ - for($mov = 0.05; $dx != 0.0 and count($this->level->getCollisionCubes($this, $this->boundingBox->getOffsetBoundingBox($dx, -1, 0))) === 0; $movX = $dx){ - if($dx < $mov and $dx >= -$mov){ - $dx = 0; - }elseif($dx > 0){ - $dx -= $mov; - }else{ - $dx += $mov; - } - } - - for(; $dz != 0.0 and count($this->level->getCollisionCubes($this, $this->boundingBox->getOffsetBoundingBox(0, -1, $dz))) === 0; $movZ = $dz){ - if($dz < $mov and $dz >= -$mov){ - $dz = 0; - }elseif($dz > 0){ - $dz -= $mov; - }else{ - $dz += $mov; - } - } - - //TODO: big messy loop - }*/ - - assert(abs($dx) <= 20 and abs($dy) <= 20 and abs($dz) <= 20, "Movement distance is excessive: dx=$dx, dy=$dy, dz=$dz"); - - //TODO: bad hack here will cause unexpected behaviour under heavy lag - $list = $this->level->getCollisionCubes($this, $this->level->getTickRateTime() > 50 ? $this->boundingBox->offsetCopy($dx, $dy, $dz) : $this->boundingBox->addCoord($dx, $dy, $dz), false); - - foreach($list as $bb){ - $dy = $bb->calculateYOffset($this->boundingBox, $dy); - } - - $this->boundingBox->offset(0, $dy, 0); - - $fallingFlag = ($this->onGround or ($dy != $movY and $movY < 0)); - - foreach($list as $bb){ - $dx = $bb->calculateXOffset($this->boundingBox, $dx); - } - - $this->boundingBox->offset($dx, 0, 0); - - foreach($list as $bb){ - $dz = $bb->calculateZOffset($this->boundingBox, $dz); - } - - $this->boundingBox->offset(0, 0, $dz); - - if($this->stepHeight > 0 and $fallingFlag and ($movX != $dx or $movZ != $dz)){ - $cx = $dx; - $cy = $dy; - $cz = $dz; - $dx = $movX; - $dy = $this->stepHeight; - $dz = $movZ; - - $axisalignedbb1 = clone $this->boundingBox; - - $this->boundingBox->setBB($axisalignedbb); - - $list = $this->level->getCollisionCubes($this, $this->boundingBox->addCoord($dx, $dy, $dz), false); - - foreach($list as $bb){ - $dy = $bb->calculateYOffset($this->boundingBox, $dy); - } - - $this->boundingBox->offset(0, $dy, 0); - - foreach($list as $bb){ - $dx = $bb->calculateXOffset($this->boundingBox, $dx); - } - - $this->boundingBox->offset($dx, 0, 0); - - foreach($list as $bb){ - $dz = $bb->calculateZOffset($this->boundingBox, $dz); - } - - $this->boundingBox->offset(0, 0, $dz); - - $reverseDY = -$dy; - foreach($list as $bb){ - $reverseDY = $bb->calculateYOffset($this->boundingBox, $reverseDY); - } - $dy += $reverseDY; - $this->boundingBox->offset(0, $reverseDY, 0); - - if(($cx ** 2 + $cz ** 2) >= ($dx ** 2 + $dz ** 2)){ - $dx = $cx; - $dy = $cy; - $dz = $cz; - $this->boundingBox->setBB($axisalignedbb1); - }else{ - $this->ySize += $dy; - } - } - } - - $this->x = ($this->boundingBox->minX + $this->boundingBox->maxX) / 2; - $this->y = $this->boundingBox->minY - $this->ySize; - $this->z = ($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2; - - $this->checkChunks(); - $this->checkBlockCollision(); - $this->checkGroundState($movX, $movY, $movZ, $dx, $dy, $dz); - $this->updateFallState($dy, $this->onGround); - - if($movX != $dx){ - $this->motion->x = 0; - } - - if($movY != $dy){ - $this->motion->y = 0; - } - - if($movZ != $dz){ - $this->motion->z = 0; - } - - //TODO: vehicle collision events (first we need to spawn them!) - - Timings::$entityMoveTimer->stopTiming(); - } - - protected function checkGroundState(float $movX, float $movY, float $movZ, float $dx, float $dy, float $dz) : void{ - $this->isCollidedVertically = $movY != $dy; - $this->isCollidedHorizontally = ($movX != $dx or $movZ != $dz); - $this->isCollided = ($this->isCollidedHorizontally or $this->isCollidedVertically); - $this->onGround = ($movY != $dy and $movY < 0); - } - - /** - * @deprecated WARNING: Despite what its name implies, this function DOES NOT return all the blocks around the entity. - * Instead, it returns blocks which have reactions for an entity intersecting with them. - * - * @return Block[] - */ - public function getBlocksAround() : 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 = []; - - for($z = $minZ; $z <= $maxZ; ++$z){ - for($x = $minX; $x <= $maxX; ++$x){ - for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->level->getBlockAt($x, $y, $z); - if($block->hasEntityCollision()){ - $this->blocksAround[] = $block; - } - } - } - } - } - - return $this->blocksAround; - } - - /** - * Returns whether this entity can be moved by currents in liquids. - */ - public function canBeMovedByCurrents() : bool{ - return true; - } - - protected function checkBlockCollision() : void{ - $vector = $this->temporalVector->setComponents(0, 0, 0); - - foreach($this->getBlocksAround() as $block){ - $block->onEntityCollide($this); - $block->addVelocityToEntity($this, $vector); - } - - if($vector->lengthSquared() > 0){ - $vector = $vector->normalize(); - $d = 0.014; - $this->motion->x += $vector->x * $d; - $this->motion->y += $vector->y * $d; - $this->motion->z += $vector->z * $d; - } - } - - public function getPosition() : Position{ - return $this->asPosition(); - } - - public function getLocation() : Location{ - return $this->asLocation(); - } - - public function setPosition(Vector3 $pos) : bool{ - if($this->closed){ - return false; - } - - if($pos instanceof Position and $pos->level !== null and $pos->level !== $this->level){ - if(!$this->switchLevel($pos->getLevelNonNull())){ - return false; - } - } - - $this->x = $pos->x; - $this->y = $pos->y; - $this->z = $pos->z; - - $this->recalculateBoundingBox(); - - $this->blocksAround = null; - - $this->checkChunks(); - - return true; - } - - public function setRotation(float $yaw, float $pitch) : void{ - $this->yaw = $yaw; - $this->pitch = $pitch; - $this->scheduleUpdate(); - } - - public function setPositionAndRotation(Vector3 $pos, float $yaw, float $pitch) : bool{ - if($this->setPosition($pos)){ - $this->setRotation($yaw, $pitch); - - return true; - } - - return false; - } - - protected function checkChunks() : void{ - $chunkX = $this->getFloorX() >> 4; - $chunkZ = $this->getFloorZ() >> 4; - if($this->chunk === null or ($this->chunk->getX() !== $chunkX or $this->chunk->getZ() !== $chunkZ)){ - if($this->chunk !== null){ - $this->chunk->removeEntity($this); - } - $this->chunk = $this->level->getChunk($chunkX, $chunkZ, true); - - if(!$this->justCreated){ - $newChunk = $this->level->getViewersForPosition($this); - foreach($this->hasSpawned as $player){ - if(!isset($newChunk[$player->getLoaderId()])){ - $this->despawnFrom($player); - }else{ - unset($newChunk[$player->getLoaderId()]); - } - } - foreach($newChunk as $player){ - $this->spawnTo($player); - } - } - - if($this->chunk === null){ - return; - } - - $this->chunk->addEntity($this); - } - } - - protected function resetLastMovements() : void{ - list($this->lastX, $this->lastY, $this->lastZ) = [$this->x, $this->y, $this->z]; - list($this->lastYaw, $this->lastPitch) = [$this->yaw, $this->pitch]; - $this->lastMotion = clone $this->motion; - } - - public function getMotion() : Vector3{ - return clone $this->motion; - } - - public function setMotion(Vector3 $motion) : bool{ - if(!$this->justCreated){ - $ev = new EntityMotionEvent($this, $motion); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - } - - $this->motion = clone $motion; - - if(!$this->justCreated){ - $this->updateMovement(); - } - - return true; - } - - public function isOnGround() : bool{ - return $this->onGround; - } - - /** - * @param Vector3|Position|Location $pos - */ - public function teleport(Vector3 $pos, ?float $yaw = null, ?float $pitch = null) : bool{ - if($pos instanceof Location){ - $yaw = $yaw ?? $pos->yaw; - $pitch = $pitch ?? $pos->pitch; - } - $from = Position::fromObject($this, $this->level); - $to = Position::fromObject($pos, $pos instanceof Position ? $pos->getLevelNonNull() : $this->level); - $ev = new EntityTeleportEvent($this, $from, $to); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - $this->ySize = 0; - $pos = $ev->getTo(); - - $this->setMotion($this->temporalVector->setComponents(0, 0, 0)); - if($this->setPositionAndRotation($pos, $yaw ?? $this->yaw, $pitch ?? $this->pitch)){ - $this->resetFallDistance(); - $this->setForceMovementUpdate(); - - $this->updateMovement(true); - - return true; - } - - return false; - } - - protected function switchLevel(Level $targetLevel) : bool{ - if($this->closed){ - return false; - } - - if($this->isValid()){ - $ev = new EntityLevelChangeEvent($this, $this->level, $targetLevel); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - $this->level->removeEntity($this); - if($this->chunk !== null){ - $this->chunk->removeEntity($this); - } - $this->despawnFromAll(); - } - - $this->setLevel($targetLevel); - $this->level->addEntity($this); - $this->chunk = null; - - return true; - } - - public function getId() : int{ - return $this->id; - } - - /** - * @return Player[] - */ - public function getViewers() : array{ - return $this->hasSpawned; - } - - /** - * 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 = AddActorPacket::LEGACY_ID_MAP_BC[static::NETWORK_ID]; - $pk->position = $this->asVector3(); - $pk->motion = $this->getMotion(); - $pk->yaw = $this->yaw; - $pk->headYaw = $this->yaw; //TODO - $pk->pitch = $this->pitch; - $pk->attributes = $this->attributeMap->getAll(); - $pk->metadata = $this->propertyManager->getAll(); - - $player->dataPacket($pk); - } - - public function spawnTo(Player $player) : void{ - if( - !isset($this->hasSpawned[$player->getLoaderId()]) and - $this->chunk !== null and - $player->getLevelNonNull() === $this->level and - isset($player->usedChunks[$chunkHash = Level::chunkHash($this->chunk->getX(), $this->chunk->getZ())]) and - $player->usedChunks[$chunkHash] === true - ){ - $this->hasSpawned[$player->getLoaderId()] = $player; - - $this->sendSpawnPacket($player); - } - } - - public function spawnToAll() : void{ - if($this->chunk === null or $this->closed){ - return; - } - foreach($this->level->getViewersForPosition($this) as $player){ - if($player->isOnline()){ - $this->spawnTo($player); - } - } - } - - public function respawnToAll() : void{ - foreach($this->hasSpawned as $key => $player){ - unset($this->hasSpawned[$key]); - $this->spawnTo($player); - } - } - - /** - * @deprecated WARNING: This function DOES NOT permanently hide the entity from the player. As soon as the entity or - * player moves, the player will once again be able to see the entity. - */ - public function despawnFrom(Player $player, bool $send = true) : void{ - if(isset($this->hasSpawned[$player->getLoaderId()])){ - if($send){ - $pk = new RemoveActorPacket(); - $pk->entityUniqueId = $this->id; - $player->dataPacket($pk); - } - unset($this->hasSpawned[$player->getLoaderId()]); - } - } - - /** - * @deprecated WARNING: This function DOES NOT permanently hide the entity from viewers. As soon as the entity or - * player moves, viewers will once again be able to see the entity. - */ - public function despawnFromAll() : void{ - foreach($this->hasSpawned as $player){ - $this->despawnFrom($player); - } - } - - /** - * Flags the entity to be removed from the world on the next tick. - */ - public function flagForDespawn() : void{ - $this->needsDespawn = true; - $this->scheduleUpdate(); - } - - public function isFlaggedForDespawn() : bool{ - return $this->needsDespawn; - } - - /** - * Returns whether the entity has been "closed". - */ - public function isClosed() : bool{ - return $this->closed; - } - - /** - * Closes the entity and frees attached references. - * - * WARNING: Entities are unusable after this has been executed! - */ - public function close() : void{ - if($this->closeInFlight){ - return; - } - - if(!$this->closed){ - $this->closeInFlight = true; - (new EntityDespawnEvent($this))->call(); - $this->closed = true; - - $this->despawnFromAll(); - $this->hasSpawned = []; - - if($this->chunk !== null){ - $this->chunk->removeEntity($this); - $this->chunk = null; - } - - if($this->isValid()){ - $this->level->removeEntity($this); - $this->setLevel(null); - } - - $this->namedtag = null; - $this->lastDamageCause = null; - $this->closeInFlight = false; - } - } - - public function setDataFlag(int $propertyId, int $flagId, bool $value = true, int $propertyType = self::DATA_TYPE_LONG) : void{ - if($this->getDataFlag($propertyId, $flagId) !== $value){ - $flags = (int) $this->propertyManager->getPropertyValue($propertyId, $propertyType); - $flags ^= 1 << $flagId; - $this->propertyManager->setPropertyValue($propertyId, $propertyType, $flags); - } - } - - public function getDataFlag(int $propertyId, int $flagId) : bool{ - return (((int) $this->propertyManager->getPropertyValue($propertyId, -1)) & (1 << $flagId)) > 0; - } - - /** - * Wrapper around {@link Entity#getDataFlag} for generic data flag reading. - */ - public function getGenericFlag(int $flagId) : bool{ - return $this->getDataFlag($flagId >= 64 ? self::DATA_FLAGS2 : self::DATA_FLAGS, $flagId % 64); - } - - /** - * Wrapper around {@link Entity#setDataFlag} for generic data flag setting. - */ - public function setGenericFlag(int $flagId, bool $value = true) : void{ - $this->setDataFlag($flagId >= 64 ? self::DATA_FLAGS2 : self::DATA_FLAGS, $flagId % 64, $value, self::DATA_TYPE_LONG); - } - - /** - * @param Player[]|Player $player - * @param mixed[][] $data Properly formatted entity data, defaults to everything - * @phpstan-param array $data - */ - public function sendData($player, ?array $data = null) : void{ - if(!is_array($player)){ - $player = [$player]; - } - - $pk = new SetActorDataPacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->metadata = $data ?? $this->propertyManager->getAll(); - - foreach($player as $p){ - if($p === $this){ - continue; - } - $p->dataPacket(clone $pk); - } - - if($this instanceof Player){ - $this->dataPacket($pk); - } - } - - /** - * @param Player[]|null $players - */ - public function broadcastEntityEvent(int $eventId, ?int $eventData = null, ?array $players = null) : void{ - $pk = new ActorEventPacket(); - $pk->entityRuntimeId = $this->id; - $pk->event = $eventId; - $pk->data = $eventData ?? 0; - - $this->server->broadcastPacket($players ?? $this->getViewers(), $pk); - } - - public function __destruct(){ - $this->close(); - } - - public function setMetadata(string $metadataKey, MetadataValue $newMetadataValue){ - $this->server->getEntityMetadata()->setMetadata($this, $metadataKey, $newMetadataValue); - } - - public function getMetadata(string $metadataKey){ - return $this->server->getEntityMetadata()->getMetadata($this, $metadataKey); - } - - public function hasMetadata(string $metadataKey) : bool{ - return $this->server->getEntityMetadata()->hasMetadata($this, $metadataKey); - } - - public function removeMetadata(string $metadataKey, Plugin $owningPlugin){ - $this->server->getEntityMetadata()->removeMetadata($this, $metadataKey, $owningPlugin); - } - - public function __toString(){ - return (new \ReflectionClass($this))->getShortName() . "(" . $this->getId() . ")"; - } - - /** - * TODO: remove this BC hack in 4.0 - * - * @param string $name - * - * @return mixed - * @throws \ErrorException - */ - public function __get($name){ - if($name === "fireTicks"){ - return $this->fireTicks; - } - throw new \ErrorException("Undefined property: " . get_class($this) . "::\$" . $name); - } - - /** - * TODO: remove this BC hack in 4.0 - * - * @param string $name - * @param mixed $value - * - * @return void - * @throws \ErrorException - * @throws \InvalidArgumentException - */ - public function __set($name, $value){ - if($name === "fireTicks"){ - $this->setFireTicks($value); - }else{ - throw new \ErrorException("Undefined property: " . get_class($this) . "::\$" . $name); - } - } - - /** - * TODO: remove this BC hack in 4.0 - * - * @param string $name - * - * @return bool - */ - public function __isset($name){ - return $name === "fireTicks"; - } -} diff --git a/src/pocketmine/entity/Human.php b/src/pocketmine/entity/Human.php deleted file mode 100644 index 835b7baf13..0000000000 --- a/src/pocketmine/entity/Human.php +++ /dev/null @@ -1,868 +0,0 @@ -skin === null){ - $skinTag = $nbt->getCompoundTag("Skin"); - if($skinTag === null){ - throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); - } - $this->skin = self::deserializeSkinNBT($skinTag); //this throws if the skin is invalid - } - - parent::__construct($level, $nbt); - } - - /** - * @throws \InvalidArgumentException - */ - protected static function deserializeSkinNBT(CompoundTag $skinTag) : Skin{ - $skin = new Skin( - $skinTag->getString("Name"), - $skinTag->hasTag("Data", StringTag::class) ? $skinTag->getString("Data") : $skinTag->getByteArray("Data"), //old data (this used to be saved as a StringTag in older versions of PM) - $skinTag->getByteArray("CapeData", ""), - $skinTag->getString("GeometryName", ""), - $skinTag->getByteArray("GeometryData", "") - ); - $skin->validate(); - return $skin; - } - - /** - * @deprecated - * - * Checks the length of a supplied skin bitmap and returns whether the length is valid. - */ - public static function isValidSkin(string $skin) : bool{ - return in_array(strlen($skin), Skin::ACCEPTED_SKIN_SIZES, true); - } - - public function getUniqueId() : ?UUID{ - return $this->uuid; - } - - public function getRawUniqueId() : string{ - return $this->rawUUID; - } - - /** - * Returns a Skin object containing information about this human's skin. - */ - public function getSkin() : Skin{ - return $this->skin; - } - - /** - * Sets the human's skin. This will not send any update to viewers, you need to do that manually using - * {@link sendSkin}. - */ - public function setSkin(Skin $skin) : void{ - $skin->validate(); - $this->skin = $skin; - $this->skin->debloatGeometryData(); - } - - /** - * Sends the human's skin to the specified list of players. If null is given for targets, the skin will be sent to - * all viewers. - * - * @param Player[]|null $targets - */ - public function sendSkin(?array $targets = null) : void{ - $pk = new PlayerSkinPacket(); - $pk->uuid = $this->getUniqueId(); - $pk->skin = SkinAdapterSingleton::get()->toSkinData($this->skin); - $this->server->broadcastPacket($targets ?? $this->hasSpawned, $pk); - } - - public function jump() : void{ - parent::jump(); - if($this->isSprinting()){ - $this->exhaust(0.8, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING); - }else{ - $this->exhaust(0.2, PlayerExhaustEvent::CAUSE_JUMPING); - } - } - - public function getFood() : float{ - return $this->attributeMap->getAttribute(Attribute::HUNGER)->getValue(); - } - - /** - * WARNING: This method does not check if full and may throw an exception if out of bounds. - * Use {@link Human::addFood()} for this purpose - * - * @throws \InvalidArgumentException - */ - public function setFood(float $new) : void{ - $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); - $old = $attr->getValue(); - $attr->setValue($new); - - // ranges: 18-20 (regen), 7-17 (none), 1-6 (no sprint), 0 (health depletion) - foreach([17, 6, 0] as $bound){ - if(($old > $bound) !== ($new > $bound)){ - $this->foodTickTimer = 0; - break; - } - } - } - - public function getMaxFood() : float{ - return $this->attributeMap->getAttribute(Attribute::HUNGER)->getMaxValue(); - } - - public function addFood(float $amount) : void{ - $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); - $amount += $attr->getValue(); - $amount = max(min($amount, $attr->getMaxValue()), $attr->getMinValue()); - $this->setFood($amount); - } - - /** - * Returns whether this Human may consume objects requiring hunger. - */ - public function isHungry() : bool{ - return $this->getFood() < $this->getMaxFood(); - } - - public function getSaturation() : float{ - return $this->attributeMap->getAttribute(Attribute::SATURATION)->getValue(); - } - - /** - * WARNING: This method does not check if saturated and may throw an exception if out of bounds. - * Use {@link Human::addSaturation()} for this purpose - * - * @throws \InvalidArgumentException - */ - public function setSaturation(float $saturation) : void{ - $this->attributeMap->getAttribute(Attribute::SATURATION)->setValue($saturation); - } - - public function addSaturation(float $amount) : void{ - $attr = $this->attributeMap->getAttribute(Attribute::SATURATION); - $attr->setValue($attr->getValue() + $amount, true); - } - - public function getExhaustion() : float{ - return $this->attributeMap->getAttribute(Attribute::EXHAUSTION)->getValue(); - } - - /** - * WARNING: This method does not check if exhausted and does not consume saturation/food. - * Use {@link Human::exhaust()} for this purpose. - */ - public function setExhaustion(float $exhaustion) : void{ - $this->attributeMap->getAttribute(Attribute::EXHAUSTION)->setValue($exhaustion); - } - - /** - * Increases a human's exhaustion level. - * - * @return float the amount of exhaustion level increased - */ - public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{ - $ev = new PlayerExhaustEvent($this, $amount, $cause); - $ev->call(); - if($ev->isCancelled()){ - return 0.0; - } - - $exhaustion = $this->getExhaustion(); - $exhaustion += $ev->getAmount(); - - while($exhaustion >= 4.0){ - $exhaustion -= 4.0; - - $saturation = $this->getSaturation(); - if($saturation > 0){ - $saturation = max(0, $saturation - 1.0); - $this->setSaturation($saturation); - }else{ - $food = $this->getFood(); - if($food > 0){ - $food--; - $this->setFood(max($food, 0)); - } - } - } - $this->setExhaustion($exhaustion); - - return $ev->getAmount(); - } - - public function consumeObject(Consumable $consumable) : bool{ - if($consumable instanceof MaybeConsumable and !$consumable->canBeConsumed()){ - return false; - } - if($consumable instanceof FoodSource && $consumable->requiresHunger() and !$this->isHungry()){ - return false; - } - - return parent::consumeObject($consumable); - } - - protected function applyConsumptionResults(Consumable $consumable) : void{ - if($consumable instanceof FoodSource){ - $this->addFood($consumable->getFoodRestore()); - $this->addSaturation($consumable->getSaturationRestore()); - } - - parent::applyConsumptionResults($consumable); - } - - /** - * Returns the player's experience level. - */ - public function getXpLevel() : int{ - return (int) $this->attributeMap->getAttribute(Attribute::EXPERIENCE_LEVEL)->getValue(); - } - - /** - * Sets the player's experience level. This does not affect their total XP or their XP progress. - */ - public function setXpLevel(int $level) : bool{ - return $this->setXpAndProgress($level, null); - } - - /** - * Adds a number of XP levels to the player. - */ - public function addXpLevels(int $amount, bool $playSound = true) : bool{ - $oldLevel = $this->getXpLevel(); - if($this->setXpLevel($oldLevel + $amount)){ - if($playSound){ - $newLevel = $this->getXpLevel(); - if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ - $this->playLevelUpSound($newLevel); - } - } - - return true; - } - - return false; - } - - /** - * Subtracts a number of XP levels from the player. - */ - public function subtractXpLevels(int $amount) : bool{ - return $this->addXpLevels(-$amount); - } - - /** - * Returns a value between 0.0 and 1.0 to indicate how far through the current level the player is. - */ - public function getXpProgress() : float{ - return $this->attributeMap->getAttribute(Attribute::EXPERIENCE)->getValue(); - } - - /** - * Sets the player's progress through the current level to a value between 0.0 and 1.0. - */ - public function setXpProgress(float $progress) : bool{ - return $this->setXpAndProgress(null, $progress); - } - - /** - * Returns the number of XP points the player has progressed into their current level. - */ - public function getRemainderXp() : int{ - return (int) (ExperienceUtils::getXpToCompleteLevel($this->getXpLevel()) * $this->getXpProgress()); - } - - /** - * Returns the amount of XP points the player currently has, calculated from their current level and progress - * through their current level. This will be reduced by enchanting deducting levels and is used to calculate the - * amount of XP the player drops on death. - */ - public function getCurrentTotalXp() : int{ - return ExperienceUtils::getXpToReachLevel($this->getXpLevel()) + $this->getRemainderXp(); - } - - /** - * Sets the current total of XP the player has, recalculating their XP level and progress. - * Note that this DOES NOT update the player's lifetime total XP. - */ - public function setCurrentTotalXp(int $amount) : bool{ - $newLevel = ExperienceUtils::getLevelFromXp($amount); - - $xpLevel = (int) $newLevel; - $xpProgress = $newLevel - (int) $newLevel; - if($xpProgress > 1.0){ - throw new AssumptionFailedError(sprintf("newLevel - (int) newLevel should never be bigger than 1, but have %.53f (newLevel=%.53f)", $xpProgress, $newLevel)); - } - return $this->setXpAndProgress($xpLevel, $xpProgress); - } - - /** - * Adds an amount of XP to the player, recalculating their XP level and progress. XP amount will be added to the - * player's lifetime XP. - * - * @param bool $playSound Whether to play level-up and XP gained sounds. - */ - public function addXp(int $amount, bool $playSound = true) : bool{ - $amount = min($amount, INT32_MAX - $this->totalXp); - $oldLevel = $this->getXpLevel(); - $oldTotal = $this->getCurrentTotalXp(); - - if($this->setCurrentTotalXp($oldTotal + $amount)){ - if($amount > 0){ - $this->totalXp += $amount; - } - - if($playSound){ - $newLevel = $this->getXpLevel(); - if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ - $this->playLevelUpSound($newLevel); - }elseif($this->getCurrentTotalXp() > $oldTotal){ - $this->level->broadcastLevelEvent($this, LevelEventPacket::EVENT_SOUND_ORB, mt_rand()); - } - } - - return true; - } - - return false; - } - - private function playLevelUpSound(int $newLevel) : void{ - $volume = 0x10000000 * (min(30, $newLevel) / 5); //No idea why such odd numbers, but this works... - $this->level->broadcastLevelSoundEvent($this, LevelSoundEventPacket::SOUND_LEVELUP, (int) $volume); - } - - /** - * Takes an amount of XP from the player, recalculating their XP level and progress. - */ - public function subtractXp(int $amount) : bool{ - return $this->addXp(-$amount); - } - - protected function setXpAndProgress(?int $level, ?float $progress) : bool{ - if(!$this->justCreated){ - $ev = new PlayerExperienceChangeEvent($this, $this->getXpLevel(), $this->getXpProgress(), $level, $progress); - $ev->call(); - - if($ev->isCancelled()){ - return false; - } - - $level = $ev->getNewLevel(); - $progress = $ev->getNewProgress(); - } - - if($level !== null){ - $this->getAttributeMap()->getAttribute(Attribute::EXPERIENCE_LEVEL)->setValue($level); - } - - if($progress !== null){ - $this->getAttributeMap()->getAttribute(Attribute::EXPERIENCE)->setValue($progress); - } - - return true; - } - - /** - * Returns the total XP the player has collected in their lifetime. Resets when the player dies. - * XP levels being removed in enchanting do not reduce this number. - */ - public function getLifetimeTotalXp() : int{ - return $this->totalXp; - } - - /** - * Sets the lifetime total XP of the player. This does not recalculate their level or progress. Used for player - * score when they die. (TODO: add this when MCPE supports it) - */ - public function setLifetimeTotalXp(int $amount) : void{ - if($amount < 0 || $amount > INT32_MAX){ - throw new \InvalidArgumentException("XP must be greater than 0 and less than " . INT32_MAX); - } - - $this->totalXp = $amount; - } - - /** - * Returns whether the human can pickup XP orbs (checks cooldown time) - */ - public function canPickupXp() : bool{ - return $this->xpCooldown === 0; - } - - public function onPickupXp(int $xpValue) : void{ - static $mainHandIndex = -1; - - //TODO: replace this with a more generic equipment getting/setting interface - /** @var Durable[] $equipment */ - $equipment = []; - - if(($item = $this->inventory->getItemInHand()) instanceof Durable and $item->hasEnchantment(Enchantment::MENDING)){ - $equipment[$mainHandIndex] = $item; - } - //TODO: check offhand - foreach($this->armorInventory->getContents() as $k => $armorItem){ - if($armorItem instanceof Durable and $armorItem->hasEnchantment(Enchantment::MENDING)){ - $equipment[$k] = $armorItem; - } - } - - if(count($equipment) > 0){ - $repairItem = $equipment[$k = array_rand($equipment)]; - if($repairItem->getDamage() > 0){ - $repairAmount = min($repairItem->getDamage(), $xpValue * 2); - $repairItem->setDamage($repairItem->getDamage() - $repairAmount); - $xpValue -= (int) ceil($repairAmount / 2); - - if($k === $mainHandIndex){ - $this->inventory->setItemInHand($repairItem); - }else{ - $this->armorInventory->setItem($k, $repairItem); - } - } - } - - $this->addXp($xpValue); //this will still get fired even if the value is 0 due to mending, to play sounds - $this->resetXpCooldown(); - } - - /** - * Sets the duration in ticks until the human can pick up another XP orb. - */ - public function resetXpCooldown(int $value = 2) : void{ - $this->xpCooldown = $value; - } - - public function getXpDropAmount() : int{ - //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for - //about 7.5 levels of XP. - return min(100, 7 * $this->getXpLevel()); - } - - /** - * @return PlayerInventory - */ - public function getInventory(){ - return $this->inventory; - } - - public function getEnderChestInventory() : EnderChestInventory{ - return $this->enderChestInventory; - } - - /** - * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. - */ - protected function initHumanData() : void{ - if($this->namedtag->hasTag("NameTag", StringTag::class)){ - $this->setNameTag($this->namedtag->getString("NameTag")); - } - - $this->uuid = UUID::fromData((string) $this->getId(), $this->skin->getSkinData(), $this->getNameTag()); - } - - protected function initEntity() : void{ - parent::initEntity(); - - $this->setPlayerFlag(self::DATA_PLAYER_FLAG_SLEEP, false); - $this->propertyManager->setBlockPos(self::DATA_PLAYER_BED_POSITION, null); - - $this->inventory = new PlayerInventory($this); - $this->enderChestInventory = new EnderChestInventory(); - $this->initHumanData(); - - $inventoryTag = $this->namedtag->getListTag("Inventory"); - if($inventoryTag !== null){ - $armorListener = $this->armorInventory->getEventProcessor(); - $this->armorInventory->setEventProcessor(null); - - /** @var CompoundTag $item */ - foreach($inventoryTag as $i => $item){ - $slot = $item->getByte("Slot"); - if($slot >= 0 and $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)); - } - } - - $this->armorInventory->setEventProcessor($armorListener); - } - - $enderChestInventoryTag = $this->namedtag->getListTag("EnderChestInventory"); - if($enderChestInventoryTag !== null){ - /** @var CompoundTag $item */ - foreach($enderChestInventoryTag as $i => $item){ - $this->enderChestInventory->setItem($item->getByte("Slot"), Item::nbtDeserialize($item)); - } - } - - $this->inventory->setHeldItemIndex($this->namedtag->getInt("SelectedInventorySlot", 0), false); - - $this->inventory->setEventProcessor(new EntityInventoryEventProcessor($this)); - - $this->setFood((float) $this->namedtag->getInt("foodLevel", (int) $this->getFood(), true)); - $this->setExhaustion($this->namedtag->getFloat("foodExhaustionLevel", $this->getExhaustion(), true)); - $this->setSaturation($this->namedtag->getFloat("foodSaturationLevel", $this->getSaturation(), true)); - $this->foodTickTimer = $this->namedtag->getInt("foodTickTimer", $this->foodTickTimer, true); - - $this->setXpLevel($this->namedtag->getInt("XpLevel", $this->getXpLevel(), true)); - $this->setXpProgress($this->namedtag->getFloat("XpP", $this->getXpProgress(), true)); - $this->totalXp = $this->namedtag->getInt("XpTotal", $this->totalXp, true); - - if($this->namedtag->hasTag("XpSeed", IntTag::class)){ - $this->xpSeed = $this->namedtag->getInt("XpSeed"); - }else{ - $this->xpSeed = random_int(INT32_MIN, INT32_MAX); - } - } - - protected function addAttributes() : void{ - parent::addAttributes(); - - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::SATURATION)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXHAUSTION)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::HUNGER)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE_LEVEL)); - $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE)); - } - - public function entityBaseTick(int $tickDiff = 1) : bool{ - $hasUpdate = parent::entityBaseTick($tickDiff); - - $this->doFoodTick($tickDiff); - - if($this->xpCooldown > 0){ - $this->xpCooldown--; - } - - return $hasUpdate; - } - - protected function doFoodTick(int $tickDiff = 1) : void{ - if($this->isAlive()){ - $food = $this->getFood(); - $health = $this->getHealth(); - $difficulty = $this->level->getDifficulty(); - - $this->foodTickTimer += $tickDiff; - if($this->foodTickTimer >= 80){ - $this->foodTickTimer = 0; - } - - if($difficulty === Level::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){ - if($food < $this->getMaxFood()){ - $this->addFood(1.0); - $food = $this->getFood(); - } - if($this->foodTickTimer % 20 === 0 and $health < $this->getMaxHealth()){ - $this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); - } - } - - if($this->foodTickTimer === 0){ - if($food >= 18){ - if($health < $this->getMaxHealth()){ - $this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); - $this->exhaust(3.0, PlayerExhaustEvent::CAUSE_HEALTH_REGEN); - } - }elseif($food <= 0){ - if(($difficulty === Level::DIFFICULTY_EASY and $health > 10) or ($difficulty === Level::DIFFICULTY_NORMAL and $health > 1) or $difficulty === Level::DIFFICULTY_HARD){ - $this->attack(new EntityDamageEvent($this, EntityDamageEvent::CAUSE_STARVATION, 1)); - } - } - } - - if($food <= 6){ - $this->setSprinting(false); - } - } - } - - public function getName() : string{ - return $this->getNameTag(); - } - - public function applyDamageModifiers(EntityDamageEvent $source) : void{ - parent::applyDamageModifiers($source); - - $type = $source->getCause(); - if($type !== EntityDamageEvent::CAUSE_SUICIDE and $type !== EntityDamageEvent::CAUSE_VOID - and $this->inventory->getItemInHand() instanceof Totem){ //TODO: check offhand as well (when it's implemented) - - $compensation = $this->getHealth() - $source->getFinalDamage() - 1; - if($compensation < 0){ - $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM); - } - } - } - - protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ - parent::applyPostDamageEffects($source); - $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM); - if($totemModifier < 0){ //Totem prevented death - $this->removeAllEffects(); - - $this->addEffect(new EffectInstance(Effect::getEffect(Effect::REGENERATION), 40 * 20, 1)); - $this->addEffect(new EffectInstance(Effect::getEffect(Effect::FIRE_RESISTANCE), 40 * 20, 1)); - $this->addEffect(new EffectInstance(Effect::getEffect(Effect::ABSORPTION), 5 * 20, 1)); - - $this->broadcastEntityEvent(ActorEventPacket::CONSUME_TOTEM); - $this->level->broadcastLevelEvent($this->add(0, $this->eyeHeight, 0), LevelEventPacket::EVENT_SOUND_TOTEM); - - $hand = $this->inventory->getItemInHand(); - if($hand instanceof Totem){ - $hand->pop(); //Plugins could alter max stack size - $this->inventory->setItemInHand($hand); - } - } - } - - public function getDrops() : array{ - return array_filter(array_merge( - $this->inventory !== null ? array_values($this->inventory->getContents()) : [], - $this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [] - ), function(Item $item) : bool{ return !$item->hasEnchantment(Enchantment::VANISHING); }); - } - - public function saveNBT() : void{ - parent::saveNBT(); - - $this->namedtag->setInt("foodLevel", (int) $this->getFood(), true); - $this->namedtag->setFloat("foodExhaustionLevel", $this->getExhaustion(), true); - $this->namedtag->setFloat("foodSaturationLevel", $this->getSaturation(), true); - $this->namedtag->setInt("foodTickTimer", $this->foodTickTimer); - - $this->namedtag->setInt("XpLevel", $this->getXpLevel()); - $this->namedtag->setFloat("XpP", $this->getXpProgress()); - $this->namedtag->setInt("XpTotal", $this->totalXp); - $this->namedtag->setInt("XpSeed", $this->xpSeed); - - $inventoryTag = new ListTag("Inventory", [], NBT::TAG_Compound); - $this->namedtag->setTag($inventoryTag); - if($this->inventory !== null){ - //Normal inventory - $slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize(); - for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){ - $item = $this->inventory->getItem($slot - 9); - if(!$item->isNull()){ - $inventoryTag->push($item->nbtSerialize($slot)); - } - } - - //Armor - for($slot = 100; $slot < 104; ++$slot){ - $item = $this->armorInventory->getItem($slot - 100); - if(!$item->isNull()){ - $inventoryTag->push($item->nbtSerialize($slot)); - } - } - - $this->namedtag->setInt("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); - } - - if($this->enderChestInventory !== null){ - /** @var CompoundTag[] $items */ - $items = []; - - $slotCount = $this->enderChestInventory->getSize(); - for($slot = 0; $slot < $slotCount; ++$slot){ - $item = $this->enderChestInventory->getItem($slot); - if(!$item->isNull()){ - $items[] = $item->nbtSerialize($slot); - } - } - - $this->namedtag->setTag(new ListTag("EnderChestInventory", $items, NBT::TAG_Compound)); - } - - if($this->skin !== null){ - $this->namedtag->setTag(new CompoundTag("Skin", [ - new StringTag("Name", $this->skin->getSkinId()), - new ByteArrayTag("Data", $this->skin->getSkinData()), - new ByteArrayTag("CapeData", $this->skin->getCapeData()), - new StringTag("GeometryName", $this->skin->getGeometryName()), - new ByteArrayTag("GeometryData", $this->skin->getGeometryData()) - ])); - } - } - - public function spawnTo(Player $player) : void{ - if($player !== $this){ - parent::spawnTo($player); - } - } - - protected function sendSpawnPacket(Player $player) : void{ - $this->skin->validate(); - - if(!($this instanceof Player)){ - /* we don't use Server->updatePlayerListData() because that uses batches, which could cause race conditions in async compression mode */ - $pk = new PlayerListPacket(); - $pk->type = PlayerListPacket::TYPE_ADD; - $pk->entries = [PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), SkinAdapterSingleton::get()->toSkinData($this->skin))]; - $player->dataPacket($pk); - } - - $pk = new AddPlayerPacket(); - $pk->uuid = $this->getUniqueId(); - $pk->username = $this->getName(); - $pk->entityRuntimeId = $this->getId(); - $pk->position = $this->asVector3(); - $pk->motion = $this->getMotion(); - $pk->yaw = $this->yaw; - $pk->pitch = $this->pitch; - $pk->item = ItemStackWrapper::legacy($this->getInventory()->getItemInHand()); - $pk->metadata = $this->propertyManager->getAll(); - $player->dataPacket($pk); - - //TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately - $this->sendData($player, [self::DATA_NAMETAG => [self::DATA_TYPE_STRING, $this->getNameTag()]]); - - $this->armorInventory->sendContents($player); - - if(!($this instanceof Player)){ - $pk = new PlayerListPacket(); - $pk->type = PlayerListPacket::TYPE_REMOVE; - $pk->entries = [PlayerListEntry::createRemovalEntry($this->uuid)]; - $player->dataPacket($pk); - } - } - - public function close() : void{ - if(!$this->closed){ - if($this->inventory !== null){ - $this->inventory->removeAllViewers(true); - $this->inventory = null; - } - if($this->enderChestInventory !== null){ - $this->enderChestInventory->removeAllViewers(true); - $this->enderChestInventory = null; - } - parent::close(); - } - } - - /** - * Wrapper around {@link Entity#getDataFlag} for player-specific data flag reading. - */ - public function getPlayerFlag(int $flagId) : bool{ - return $this->getDataFlag(self::DATA_PLAYER_FLAGS, $flagId); - } - - /** - * Wrapper around {@link Entity#setDataFlag} for player-specific data flag setting. - */ - public function setPlayerFlag(int $flagId, bool $value = true) : void{ - $this->setDataFlag(self::DATA_PLAYER_FLAGS, $flagId, $value, self::DATA_TYPE_BYTE); - } -} diff --git a/src/pocketmine/entity/Vehicle.php b/src/pocketmine/entity/Vehicle.php deleted file mode 100644 index 2929c183b6..0000000000 --- a/src/pocketmine/entity/Vehicle.php +++ /dev/null @@ -1,28 +0,0 @@ -namedtag->getInt("Profession", self::PROFESSION_FARMER); - - if($profession > 4 or $profession < 0){ - $profession = self::PROFESSION_FARMER; - } - - $this->setProfession($profession); - } - - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setInt("Profession", $this->getProfession()); - } - - /** - * Sets the villager profession - */ - public function setProfession(int $profession) : void{ - $this->propertyManager->setInt(self::DATA_VARIANT, $profession); - } - - public function getProfession() : int{ - return $this->propertyManager->getInt(self::DATA_VARIANT); - } - - public function isBaby() : bool{ - return $this->getGenericFlag(self::DATA_FLAG_BABY); - } -} diff --git a/src/pocketmine/entity/object/FallingBlock.php b/src/pocketmine/entity/object/FallingBlock.php deleted file mode 100644 index 37affc309e..0000000000 --- a/src/pocketmine/entity/object/FallingBlock.php +++ /dev/null @@ -1,144 +0,0 @@ -namedtag->hasTag("TileID", IntTag::class)){ - $blockId = $this->namedtag->getInt("TileID"); - }elseif($this->namedtag->hasTag("Tile", ByteTag::class)){ - $blockId = $this->namedtag->getByte("Tile"); - $this->namedtag->removeTag("Tile"); - } - - if($blockId === 0){ - throw new \UnexpectedValueException("Invalid " . get_class($this) . " entity: block ID is 0 or missing"); - } - - $damage = $this->namedtag->getByte("Data", 0); - - $this->block = BlockFactory::get($blockId, $damage); - - $this->propertyManager->setInt(self::DATA_VARIANT, $this->block->getRuntimeId()); - } - - public function canCollideWith(Entity $entity) : bool{ - return false; - } - - public function canBeMovedByCurrents() : bool{ - return false; - } - - public function attack(EntityDamageEvent $source) : void{ - if($source->getCause() === EntityDamageEvent::CAUSE_VOID){ - parent::attack($source); - } - } - - public function entityBaseTick(int $tickDiff = 1) : bool{ - if($this->closed){ - return false; - } - - $hasUpdate = parent::entityBaseTick($tickDiff); - - if(!$this->isFlaggedForDespawn()){ - $pos = Position::fromObject($this->add(-$this->width / 2, $this->height, -$this->width / 2)->floor(), $this->getLevelNonNull()); - - $this->block->position($pos); - - $blockTarget = null; - if($this->block instanceof Fallable){ - $blockTarget = $this->block->tickFalling(); - } - - if($this->onGround or $blockTarget !== null){ - $this->flagForDespawn(); - - $block = $this->level->getBlock($pos); - if(!$block->canBeReplaced() or ($this->onGround and abs($this->y - $this->getFloorY()) > 0.001)){ - //FIXME: anvils are supposed to destroy torches - $this->getLevelNonNull()->dropItem($this, ItemFactory::get($this->getBlock(), $this->getDamage())); - }else{ - $ev = new EntityBlockChangeEvent($this, $block, $blockTarget ?? $this->block); - $ev->call(); - if(!$ev->isCancelled()){ - $this->getLevelNonNull()->setBlock($pos, $ev->getTo(), true); - } - } - $hasUpdate = true; - } - } - - return $hasUpdate; - } - - public function getBlock() : int{ - return $this->block->getId(); - } - - public function getDamage() : int{ - return $this->block->getDamage(); - } - - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setInt("TileID", $this->block->getId(), true); - $this->namedtag->setByte("Data", $this->block->getDamage()); - } -} diff --git a/src/pocketmine/entity/object/ItemEntity.php b/src/pocketmine/entity/object/ItemEntity.php deleted file mode 100644 index 0ea2af38c1..0000000000 --- a/src/pocketmine/entity/object/ItemEntity.php +++ /dev/null @@ -1,218 +0,0 @@ -setMaxHealth(5); - $this->setHealth($this->namedtag->getShort("Health", (int) $this->getHealth())); - $this->age = $this->namedtag->getShort("Age", $this->age); - $this->pickupDelay = $this->namedtag->getShort("PickupDelay", $this->pickupDelay); - $this->owner = $this->namedtag->getString("Owner", $this->owner); - $this->thrower = $this->namedtag->getString("Thrower", $this->thrower); - - $itemTag = $this->namedtag->getCompoundTag("Item"); - if($itemTag === null){ - throw new \UnexpectedValueException("Invalid " . get_class($this) . " entity: expected \"Item\" NBT tag not found"); - } - - $this->item = Item::nbtDeserialize($itemTag); - if($this->item->isNull()){ - throw new \UnexpectedValueException("Item for " . get_class($this) . " is invalid"); - } - - (new ItemSpawnEvent($this))->call(); - } - - public function entityBaseTick(int $tickDiff = 1) : bool{ - if($this->closed){ - return false; - } - - $hasUpdate = parent::entityBaseTick($tickDiff); - - if(!$this->isFlaggedForDespawn() and $this->pickupDelay > -1 and $this->pickupDelay < 32767){ //Infinite delay - $this->pickupDelay -= $tickDiff; - if($this->pickupDelay < 0){ - $this->pickupDelay = 0; - } - - $this->age += $tickDiff; - if($this->age > 6000){ - $ev = new ItemDespawnEvent($this); - $ev->call(); - if($ev->isCancelled()){ - $this->age = 0; - }else{ - $this->flagForDespawn(); - $hasUpdate = true; - } - } - } - - return $hasUpdate; - } - - protected function tryChangeMovement() : void{ - $this->checkObstruction($this->x, $this->y, $this->z); - parent::tryChangeMovement(); - } - - protected function applyDragBeforeGravity() : bool{ - return true; - } - - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setTag($this->item->nbtSerialize(-1, "Item")); - $this->namedtag->setShort("Health", (int) $this->getHealth()); - $this->namedtag->setShort("Age", $this->age); - $this->namedtag->setShort("PickupDelay", $this->pickupDelay); - if($this->owner !== null){ - $this->namedtag->setString("Owner", $this->owner); - } - if($this->thrower !== null){ - $this->namedtag->setString("Thrower", $this->thrower); - } - } - - public function getItem() : Item{ - return $this->item; - } - - public function canCollideWith(Entity $entity) : bool{ - return false; - } - - public function canBeCollidedWith() : bool{ - return false; - } - - public function getPickupDelay() : int{ - return $this->pickupDelay; - } - - public function setPickupDelay(int $delay) : void{ - $this->pickupDelay = $delay; - } - - public function getOwner() : string{ - return $this->owner; - } - - public function setOwner(string $owner) : void{ - $this->owner = $owner; - } - - public function getThrower() : string{ - return $this->thrower; - } - - public function setThrower(string $thrower) : void{ - $this->thrower = $thrower; - } - - protected function sendSpawnPacket(Player $player) : void{ - $pk = new AddItemActorPacket(); - $pk->entityRuntimeId = $this->getId(); - $pk->position = $this->asVector3(); - $pk->motion = $this->getMotion(); - $pk->item = ItemStackWrapper::legacy($this->getItem()); - $pk->metadata = $this->propertyManager->getAll(); - - $player->dataPacket($pk); - } - - public function onCollideWithPlayer(Player $player) : void{ - if($this->getPickupDelay() !== 0){ - return; - } - - $item = $this->getItem(); - $playerInventory = $player->getInventory(); - - if($player->isSurvival() and !$playerInventory->canAddItem($item)){ - return; - } - - $ev = new InventoryPickupItemEvent($playerInventory, $this); - $ev->call(); - if($ev->isCancelled()){ - return; - } - - switch($item->getId()){ - case Item::WOOD: - $player->awardAchievement("mineWood"); - break; - case Item::DIAMOND: - $player->awardAchievement("diamond"); - break; - } - - $pk = new TakeItemActorPacket(); - $pk->eid = $player->getId(); - $pk->target = $this->getId(); - $this->server->broadcastPacket($this->getViewers(), $pk); - - $playerInventory->addItem(clone $item); - $this->flagForDespawn(); - } -} diff --git a/src/pocketmine/entity/object/Painting.php b/src/pocketmine/entity/object/Painting.php deleted file mode 100644 index f9767b211e..0000000000 --- a/src/pocketmine/entity/object/Painting.php +++ /dev/null @@ -1,289 +0,0 @@ -motive = $nbt->getString("Motive"); - $this->blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ")); - if($nbt->hasTag("Direction", ByteTag::class)){ - $this->direction = $nbt->getByte("Direction"); - }elseif($nbt->hasTag("Facing", ByteTag::class)){ - $this->direction = $nbt->getByte("Facing"); - } - parent::__construct($level, $nbt); - } - - protected function initEntity() : void{ - $this->setMaxHealth(1); - $this->setHealth(1); - parent::initEntity(); - } - - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setInt("TileX", (int) $this->blockIn->x); - $this->namedtag->setInt("TileY", (int) $this->blockIn->y); - $this->namedtag->setInt("TileZ", (int) $this->blockIn->z); - - $this->namedtag->setByte("Facing", $this->direction); - $this->namedtag->setByte("Direction", $this->direction); //Save both for full compatibility - - $this->namedtag->setString("Motive", $this->motive); - } - - public function kill() : void{ - if(!$this->isAlive()){ - return; - } - parent::kill(); - - $drops = true; - - if($this->lastDamageCause instanceof EntityDamageByEntityEvent){ - $killer = $this->lastDamageCause->getDamager(); - if($killer instanceof Player and $killer->isCreative()){ - $drops = false; - } - } - - if($drops){ - //non-living entities don't have a way to create drops generically yet - $this->level->dropItem($this, ItemFactory::get(Item::PAINTING)); - } - $this->level->addParticle(new DestroyBlockParticle($this->add(0.5, 0.5, 0.5), BlockFactory::get(Block::PLANKS))); - } - - protected function recalculateBoundingBox() : void{ - static $directions = [ - 0 => Vector3::SIDE_SOUTH, - 1 => Vector3::SIDE_WEST, - 2 => Vector3::SIDE_NORTH, - 3 => Vector3::SIDE_EAST - ]; - - $facing = $directions[$this->direction]; - - $this->boundingBox->setBB(self::getPaintingBB($this->blockIn->getSide($facing), $facing, $this->getMotive())); - } - - public function onNearbyBlockChange() : void{ - parent::onNearbyBlockChange(); - - static $directions = [ - 0 => Vector3::SIDE_SOUTH, - 1 => Vector3::SIDE_WEST, - 2 => Vector3::SIDE_NORTH, - 3 => Vector3::SIDE_EAST - ]; - - $face = $directions[$this->direction]; - if(!self::canFit($this->level, $this->blockIn->getSide($face), $face, false, $this->getMotive())){ - $this->kill(); - } - } - - public function hasMovementUpdate() : bool{ - return false; - } - - protected function updateMovement(bool $teleport = false) : void{ - - } - - public function canBeCollidedWith() : bool{ - return false; - } - - 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 = $this->direction; - $pk->title = $this->motive; - - $player->dataPacket($pk); - } - - /** - * Returns the painting motive (which image is displayed on the painting) - */ - public function getMotive() : PaintingMotive{ - return PaintingMotive::getMotiveByName($this->motive); - } - - public function getDirection() : ?int{ - return $this->direction; - } - - /** - * Returns the bounding-box a painting with the specified motive would have at the given position and direction. - */ - private static function getPaintingBB(Vector3 $blockIn, int $facing, PaintingMotive $motive) : AxisAlignedBB{ - $width = $motive->getWidth(); - $height = $motive->getHeight(); - - $horizontalStart = (int) (ceil($width / 2) - 1); - $verticalStart = (int) (ceil($height / 2) - 1); - - $thickness = 1 / 16; - - $minX = $maxX = 0; - $minZ = $maxZ = 0; - - $minY = -$verticalStart; - $maxY = $minY + $height; - - switch($facing){ - case Vector3::SIDE_NORTH: - $minZ = 1 - $thickness; - $maxZ = 1; - $maxX = $horizontalStart + 1; - $minX = $maxX - $width; - break; - case Vector3::SIDE_SOUTH: - $minZ = 0; - $maxZ = $thickness; - $minX = -$horizontalStart; - $maxX = $minX + $width; - break; - case Vector3::SIDE_WEST: - $minX = 1 - $thickness; - $maxX = 1; - $minZ = -$horizontalStart; - $maxZ = $minZ + $width; - break; - case Vector3::SIDE_EAST: - $minX = 0; - $maxX = $thickness; - $maxZ = $horizontalStart + 1; - $minZ = $maxZ - $width; - break; - } - - return new AxisAlignedBB( - $blockIn->x + $minX, - $blockIn->y + $minY, - $blockIn->z + $minZ, - $blockIn->x + $maxX, - $blockIn->y + $maxY, - $blockIn->z + $maxZ - ); - } - - /** - * Returns whether a painting with the specified motive can be placed at the given position. - */ - public static function canFit(Level $level, Vector3 $blockIn, int $facing, bool $checkOverlap, PaintingMotive $motive) : bool{ - $width = $motive->getWidth(); - $height = $motive->getHeight(); - - $horizontalStart = (int) (ceil($width / 2) - 1); - $verticalStart = (int) (ceil($height / 2) - 1); - - switch($facing){ - case Vector3::SIDE_NORTH: - $rotatedFace = Vector3::SIDE_WEST; - break; - case Vector3::SIDE_WEST: - $rotatedFace = Vector3::SIDE_SOUTH; - break; - case Vector3::SIDE_SOUTH: - $rotatedFace = Vector3::SIDE_EAST; - break; - case Vector3::SIDE_EAST: - $rotatedFace = Vector3::SIDE_NORTH; - break; - default: - return false; - } - - $oppositeSide = Vector3::getOppositeSide($facing); - - $startPos = $blockIn->asVector3()->getSide(Vector3::getOppositeSide($rotatedFace), $horizontalStart)->getSide(Vector3::SIDE_DOWN, $verticalStart); - - for($w = 0; $w < $width; ++$w){ - for($h = 0; $h < $height; ++$h){ - $pos = $startPos->getSide($rotatedFace, $w)->getSide(Vector3::SIDE_UP, $h); - - $block = $level->getBlockAt($pos->x, $pos->y, $pos->z); - if($block->isSolid() or !$block->getSide($oppositeSide)->isSolid()){ - return false; - } - } - } - - if($checkOverlap){ - $bb = self::getPaintingBB($blockIn, $facing, $motive); - - foreach($level->getNearbyEntities($bb) as $entity){ - if($entity instanceof self){ - return false; - } - } - } - - return true; - } -} diff --git a/src/pocketmine/entity/object/PrimedTNT.php b/src/pocketmine/entity/object/PrimedTNT.php deleted file mode 100644 index c00c6f1543..0000000000 --- a/src/pocketmine/entity/object/PrimedTNT.php +++ /dev/null @@ -1,115 +0,0 @@ -getCause() === EntityDamageEvent::CAUSE_VOID){ - parent::attack($source); - } - } - - protected function initEntity() : void{ - parent::initEntity(); - - if($this->namedtag->hasTag("Fuse", ShortTag::class)){ - $this->fuse = $this->namedtag->getShort("Fuse"); - }else{ - $this->fuse = 80; - } - - $this->setGenericFlag(self::DATA_FLAG_IGNITED, true); - $this->propertyManager->setInt(self::DATA_FUSE_LENGTH, $this->fuse); - - $this->level->broadcastLevelEvent($this, LevelEventPacket::EVENT_SOUND_IGNITE); - } - - public function canCollideWith(Entity $entity) : bool{ - return false; - } - - public function saveNBT() : void{ - parent::saveNBT(); - $this->namedtag->setShort("Fuse", $this->fuse, true); //older versions incorrectly saved this as a byte - } - - public function entityBaseTick(int $tickDiff = 1) : bool{ - if($this->closed){ - return false; - } - - $hasUpdate = parent::entityBaseTick($tickDiff); - - if($this->fuse % 5 === 0){ //don't spam it every tick, it's not necessary - $this->propertyManager->setInt(self::DATA_FUSE_LENGTH, $this->fuse); - } - - if(!$this->isFlaggedForDespawn()){ - $this->fuse -= $tickDiff; - - if($this->fuse <= 0){ - $this->flagForDespawn(); - $this->explode(); - } - } - - return $hasUpdate or $this->fuse >= 0; - } - - public function explode() : void{ - $ev = new ExplosionPrimeEvent($this, 4); - $ev->call(); - if(!$ev->isCancelled()){ - $explosion = new Explosion(Position::fromObject($this->add(0, $this->height / 2, 0), $this->level), $ev->getForce(), $this); - if($ev->isBlockBreaking()){ - $explosion->explodeA(); - } - $explosion->explodeB(); - } - } -} diff --git a/src/pocketmine/event/block/SignChangeEvent.php b/src/pocketmine/event/block/SignChangeEvent.php deleted file mode 100644 index d72c6b5d56..0000000000 --- a/src/pocketmine/event/block/SignChangeEvent.php +++ /dev/null @@ -1,98 +0,0 @@ -player = $thePlayer; - $this->setLines($theLines); - } - - public function getPlayer() : Player{ - return $this->player; - } - - /** - * @return string[] - */ - public function getLines() : array{ - return $this->lines; - } - - /** - * @param int $index 0-3 - * - * @throws \InvalidArgumentException if the index is out of bounds - */ - public function getLine(int $index) : string{ - if($index < 0 or $index > 3){ - throw new \InvalidArgumentException("Index must be in the range 0-3!"); - } - - return $this->lines[$index]; - } - - /** - * @param string[] $lines - * - * @throws \InvalidArgumentException if there are more or less than 4 lines in the passed array - */ - public function setLines(array $lines) : void{ - if(count($lines) !== 4){ - throw new \InvalidArgumentException("Array size must be 4!"); - } - Utils::validateArrayValueType($lines, function(string $_) : void{}); - $this->lines = $lines; - } - - /** - * @param int $index 0-3 - * - * @throws \InvalidArgumentException if the index is out of bounds - */ - public function setLine(int $index, string $line) : void{ - if($index < 0 or $index > 3){ - throw new \InvalidArgumentException("Index must be in the range 0-3!"); - } - $this->lines[$index] = $line; - } -} diff --git a/src/pocketmine/event/entity/EntityDespawnEvent.php b/src/pocketmine/event/entity/EntityDespawnEvent.php deleted file mode 100644 index 298b1dc8b3..0000000000 --- a/src/pocketmine/event/entity/EntityDespawnEvent.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ -class EntityDespawnEvent extends EntityEvent{ - /** @var int */ - private $entityType; - - public function __construct(Entity $entity){ - $this->entity = $entity; - $this->entityType = $entity::NETWORK_ID; - } - - /** - * @deprecated - */ - public function getType() : int{ - return $this->entityType; - } - - /** - * @deprecated - */ - public function isCreature() : bool{ - return $this->entity instanceof Creature; - } - - /** - * @deprecated - */ - public function isHuman() : bool{ - return $this->entity instanceof Human; - } - - /** - * @deprecated - */ - public function isProjectile() : bool{ - return $this->entity instanceof Projectile; - } - - /** - * @deprecated - */ - public function isVehicle() : bool{ - return $this->entity instanceof Vehicle; - } - - /** - * @deprecated - */ - public function isItem() : bool{ - return $this->entity instanceof ItemEntity; - } -} diff --git a/src/pocketmine/event/entity/EntitySpawnEvent.php b/src/pocketmine/event/entity/EntitySpawnEvent.php deleted file mode 100644 index d1e45649f1..0000000000 --- a/src/pocketmine/event/entity/EntitySpawnEvent.php +++ /dev/null @@ -1,95 +0,0 @@ - - */ -class EntitySpawnEvent extends EntityEvent{ - /** @var int */ - private $entityType; - - public function __construct(Entity $entity){ - $this->entity = $entity; - $this->entityType = $entity::NETWORK_ID; - } - - /** - * @deprecated - */ - public function getPosition() : Position{ - return $this->entity->getPosition(); - } - - /** - * @deprecated - */ - public function getType() : int{ - return $this->entityType; - } - - /** - * @deprecated - */ - public function isCreature() : bool{ - return $this->entity instanceof Creature; - } - - /** - * @deprecated - */ - public function isHuman() : bool{ - return $this->entity instanceof Human; - } - - /** - * @deprecated - */ - public function isProjectile() : bool{ - return $this->entity instanceof Projectile; - } - - /** - * @deprecated - */ - public function isVehicle() : bool{ - return $this->entity instanceof Vehicle; - } - - /** - * @deprecated - */ - public function isItem() : bool{ - return $this->entity instanceof ItemEntity; - } -} diff --git a/src/pocketmine/event/player/PlayerPreLoginEvent.php b/src/pocketmine/event/player/PlayerPreLoginEvent.php deleted file mode 100644 index 4d9cc4ae97..0000000000 --- a/src/pocketmine/event/player/PlayerPreLoginEvent.php +++ /dev/null @@ -1,58 +0,0 @@ -player = $player; - $this->kickMessage = $kickMessage; - } - - public function setKickMessage(string $kickMessage) : void{ - $this->kickMessage = $kickMessage; - } - - public function getKickMessage() : string{ - return $this->kickMessage; - } -} diff --git a/src/pocketmine/event/player/cheat/PlayerIllegalMoveEvent.php b/src/pocketmine/event/player/cheat/PlayerIllegalMoveEvent.php deleted file mode 100644 index c46e91a410..0000000000 --- a/src/pocketmine/event/player/cheat/PlayerIllegalMoveEvent.php +++ /dev/null @@ -1,63 +0,0 @@ -player = $player; - $this->attemptedPosition = $attemptedPosition; - $this->originalPosition = $originalPosition; - $this->expectedPosition = $player->asVector3(); - } - - /** - * Returns the position the player attempted to move to. - */ - public function getAttemptedPosition() : Vector3{ - return $this->attemptedPosition; - } - - public function getOriginalPosition() : Vector3{ - return $this->originalPosition; - } - - public function getExpectedPosition() : Vector3{ - return $this->expectedPosition; - } -} diff --git a/src/pocketmine/event/server/NetworkInterfaceCrashEvent.php b/src/pocketmine/event/server/NetworkInterfaceCrashEvent.php deleted file mode 100644 index 6f54a5a44c..0000000000 --- a/src/pocketmine/event/server/NetworkInterfaceCrashEvent.php +++ /dev/null @@ -1,44 +0,0 @@ -exception = $throwable; - } - - public function getCrashInformation() : \Throwable{ - return $this->exception; - } -} diff --git a/src/pocketmine/event/server/ServerCommandEvent.php b/src/pocketmine/event/server/ServerCommandEvent.php deleted file mode 100644 index 9e0767036b..0000000000 --- a/src/pocketmine/event/server/ServerCommandEvent.php +++ /dev/null @@ -1,62 +0,0 @@ -sender = $sender; - $this->command = $command; - } - - public function getSender() : CommandSender{ - return $this->sender; - } - - public function getCommand() : string{ - return $this->command; - } - - public function setCommand(string $command) : void{ - $this->command = $command; - } -} diff --git a/src/pocketmine/inventory/AnvilInventory.php b/src/pocketmine/inventory/AnvilInventory.php deleted file mode 100644 index c13d872513..0000000000 --- a/src/pocketmine/inventory/AnvilInventory.php +++ /dev/null @@ -1,67 +0,0 @@ -asPosition()); - } - - public function getNetworkType() : int{ - return WindowTypes::ANVIL; - } - - public function getName() : string{ - return "Anvil"; - } - - public function getDefaultSize() : int{ - return 2; //1 input, 1 material - } - - /** - * This override is here for documentation and code completion purposes only. - * @return Position - */ - public function getHolder(){ - return $this->holder; - } - - public function onClose(Player $who) : void{ - parent::onClose($who); - - foreach($this->getContents() as $item){ - $who->dropItem($item); - } - $this->clearAll(); - } -} diff --git a/src/pocketmine/inventory/ArmorInventory.php b/src/pocketmine/inventory/ArmorInventory.php deleted file mode 100644 index 95041c5025..0000000000 --- a/src/pocketmine/inventory/ArmorInventory.php +++ /dev/null @@ -1,153 +0,0 @@ -holder = $holder; - parent::__construct(); - } - - public function getHolder() : Living{ - return $this->holder; - } - - public function getName() : string{ - return "Armor"; - } - - public function getDefaultSize() : int{ - return 4; - } - - public function getHelmet() : Item{ - return $this->getItem(self::SLOT_HEAD); - } - - public function getChestplate() : Item{ - return $this->getItem(self::SLOT_CHEST); - } - - public function getLeggings() : Item{ - return $this->getItem(self::SLOT_LEGS); - } - - public function getBoots() : Item{ - return $this->getItem(self::SLOT_FEET); - } - - public function setHelmet(Item $helmet) : bool{ - return $this->setItem(self::SLOT_HEAD, $helmet); - } - - public function setChestplate(Item $chestplate) : bool{ - return $this->setItem(self::SLOT_CHEST, $chestplate); - } - - public function setLeggings(Item $leggings) : bool{ - return $this->setItem(self::SLOT_LEGS, $leggings); - } - - public function setBoots(Item $boots) : bool{ - return $this->setItem(self::SLOT_FEET, $boots); - } - - public function sendSlot(int $index, $target) : void{ - if($target instanceof Player){ - $target = [$target]; - } - - $pk = new MobArmorEquipmentPacket(); - $pk->entityRuntimeId = $this->getHolder()->getId(); - $pk->head = ItemStackWrapper::legacy($this->getHelmet()); - $pk->chest = ItemStackWrapper::legacy($this->getChestplate()); - $pk->legs = ItemStackWrapper::legacy($this->getLeggings()); - $pk->feet = ItemStackWrapper::legacy($this->getBoots()); - $pk->encode(); - - foreach($target as $player){ - if($player === $this->getHolder()){ - /** @var Player $player */ - - $pk2 = new InventorySlotPacket(); - $pk2->windowId = $player->getWindowId($this); - $pk2->inventorySlot = $index; - $pk2->item = ItemStackWrapper::legacy($this->getItem($index)); - $player->dataPacket($pk2); - }else{ - $player->dataPacket($pk); - } - } - } - - public function sendContents($target) : void{ - if($target instanceof Player){ - $target = [$target]; - } - - $pk = new MobArmorEquipmentPacket(); - $pk->entityRuntimeId = $this->getHolder()->getId(); - $pk->head = ItemStackWrapper::legacy($this->getHelmet()); - $pk->chest = ItemStackWrapper::legacy($this->getChestplate()); - $pk->legs = ItemStackWrapper::legacy($this->getLeggings()); - $pk->feet = ItemStackWrapper::legacy($this->getBoots()); - $pk->encode(); - - foreach($target as $player){ - if($player === $this->getHolder()){ - $pk2 = new InventoryContentPacket(); - $pk2->windowId = $player->getWindowId($this); - $pk2->items = array_map([ItemStackWrapper::class, 'legacy'], $this->getContents(true)); - $player->dataPacket($pk2); - }else{ - $player->dataPacket($pk); - } - } - } - - /** - * @return Player[] - */ - public function getViewers() : array{ - return array_merge(parent::getViewers(), $this->holder->getViewers()); - } -} diff --git a/src/pocketmine/inventory/BaseInventory.php b/src/pocketmine/inventory/BaseInventory.php deleted file mode 100644 index cde92cc4ae..0000000000 --- a/src/pocketmine/inventory/BaseInventory.php +++ /dev/null @@ -1,483 +0,0 @@ - - */ - protected $slots; - /** @var Player[] */ - protected $viewers = []; - /** @var InventoryEventProcessor|null */ - protected $eventProcessor; - - /** - * @param Item[] $items - */ - public function __construct(array $items = [], int $size = null, string $title = null){ - $this->slots = new \SplFixedArray($size ?? $this->getDefaultSize()); - $this->title = $title ?? $this->getName(); - - $this->setContents($items, false); - } - - abstract public function getName() : string; - - public function getTitle() : string{ - return $this->title; - } - - /** - * Returns the size of the inventory. - */ - public function getSize() : int{ - return $this->slots->getSize(); - } - - /** - * Sets the new size of the inventory. - * WARNING: If the size is smaller, any items past the new size will be lost. - * - * @return void - */ - public function setSize(int $size){ - $this->slots->setSize($size); - } - - abstract public function getDefaultSize() : int; - - public function getMaxStackSize() : int{ - return $this->maxStackSize; - } - - public function getItem(int $index) : Item{ - return $this->slots[$index] !== null ? clone $this->slots[$index] : ItemFactory::get(Item::AIR, 0, 0); - } - - /** - * @return Item[] - */ - public function getContents(bool $includeEmpty = false) : array{ - $contents = []; - $air = null; - - foreach($this->slots as $i => $slot){ - if($slot !== null){ - $contents[$i] = clone $slot; - }elseif($includeEmpty){ - $contents[$i] = $air ?? ($air = ItemFactory::get(Item::AIR, 0, 0)); - } - } - - return $contents; - } - - /** - * @param Item[] $items - */ - public function setContents(array $items, bool $send = true) : void{ - if(count($items) > $this->getSize()){ - $items = array_slice($items, 0, $this->getSize(), true); - } - - for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - if(!isset($items[$i])){ - if($this->slots[$i] !== null){ - $this->clear($i, false); - } - }else{ - if(!$this->setItem($i, $items[$i], false)){ - $this->clear($i, false); - } - } - } - - if($send){ - $this->sendContents($this->getViewers()); - } - } - - /** - * Drops the contents of the inventory into the specified Level at the specified position and clears the inventory - * contents. - */ - public function dropContents(Level $level, Vector3 $position) : void{ - foreach($this->getContents() as $item){ - $level->dropItem($position, $item); - } - - $this->clearAll(); - } - - public function setItem(int $index, Item $item, bool $send = true) : bool{ - if($item->isNull()){ - $item = ItemFactory::get(Item::AIR, 0, 0); - }else{ - $item = clone $item; - } - - $oldItem = $this->getItem($index); - if($this->eventProcessor !== null){ - $newItem = $this->eventProcessor->onSlotChange($this, $index, $oldItem, $item); - if($newItem === null){ - return false; - } - }else{ - $newItem = $item; - } - - $this->slots[$index] = $newItem->isNull() ? null : $newItem; - $this->onSlotChange($index, $oldItem, $send); - - return true; - } - - public function contains(Item $item) : bool{ - $count = max(1, $item->getCount()); - $checkDamage = !$item->hasAnyDamageValue(); - $checkTags = $item->hasCompoundTag(); - foreach($this->getContents() as $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $count -= $i->getCount(); - if($count <= 0){ - return true; - } - } - } - - return false; - } - - public function all(Item $item) : array{ - $slots = []; - $checkDamage = !$item->hasAnyDamageValue(); - $checkTags = $item->hasCompoundTag(); - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $slots[$index] = $i; - } - } - - return $slots; - } - - public function remove(Item $item) : void{ - $checkDamage = !$item->hasAnyDamageValue(); - $checkTags = $item->hasCompoundTag(); - - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags)){ - $this->clear($index); - } - } - } - - public function first(Item $item, bool $exact = false) : int{ - $count = $exact ? $item->getCount() : max(1, $item->getCount()); - $checkDamage = $exact || !$item->hasAnyDamageValue(); - $checkTags = $exact || $item->hasCompoundTag(); - - foreach($this->getContents() as $index => $i){ - if($item->equals($i, $checkDamage, $checkTags) and ($i->getCount() === $count or (!$exact and $i->getCount() > $count))){ - return $index; - } - } - - return -1; - } - - public function firstEmpty() : int{ - foreach($this->slots as $i => $slot){ - if($slot === null or $slot->isNull()){ - return $i; - } - } - - return -1; - } - - public function isSlotEmpty(int $index) : bool{ - return $this->slots[$index] === null or $this->slots[$index]->isNull(); - } - - public function canAddItem(Item $item) : bool{ - $count = $item->getCount(); - for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $slot = $this->getItem($i); - if($item->equals($slot)){ - if(($diff = min($slot->getMaxStackSize(), $item->getMaxStackSize()) - $slot->getCount()) > 0){ - $count -= $diff; - } - }elseif($slot->isNull()){ - $count -= min($this->getMaxStackSize(), $item->getMaxStackSize()); - } - - if($count <= 0){ - return true; - } - } - - return false; - } - - public function addItem(Item ...$slots) : array{ - /** @var Item[] $itemSlots */ - /** @var Item[] $slots */ - $itemSlots = []; - foreach($slots as $slot){ - if(!$slot->isNull()){ - $itemSlots[] = clone $slot; - } - } - - $emptySlots = []; - - for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $item = $this->getItem($i); - if($item->isNull()){ - $emptySlots[] = $i; - } - - foreach($itemSlots as $index => $slot){ - if($slot->equals($item) and $item->getCount() < $item->getMaxStackSize()){ - $amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize()); - if($amount > 0){ - $slot->setCount($slot->getCount() - $amount); - $item->setCount($item->getCount() + $amount); - $this->setItem($i, $item); - if($slot->getCount() <= 0){ - unset($itemSlots[$index]); - } - } - } - } - - if(count($itemSlots) === 0){ - break; - } - } - - if(count($itemSlots) > 0 and count($emptySlots) > 0){ - foreach($emptySlots as $slotIndex){ - //This loop only gets the first item, then goes to the next empty slot - foreach($itemSlots as $index => $slot){ - $amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize()); - $slot->setCount($slot->getCount() - $amount); - $item = clone $slot; - $item->setCount($amount); - $this->setItem($slotIndex, $item); - if($slot->getCount() <= 0){ - unset($itemSlots[$index]); - } - break; - } - } - } - - return $itemSlots; - } - - public function removeItem(Item ...$slots) : array{ - /** @var Item[] $itemSlots */ - /** @var Item[] $slots */ - $itemSlots = []; - foreach($slots as $slot){ - if(!$slot->isNull()){ - $itemSlots[] = clone $slot; - } - } - - for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $item = $this->getItem($i); - if($item->isNull()){ - continue; - } - - foreach($itemSlots as $index => $slot){ - if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasCompoundTag())){ - $amount = min($item->getCount(), $slot->getCount()); - $slot->setCount($slot->getCount() - $amount); - $item->setCount($item->getCount() - $amount); - $this->setItem($i, $item); - if($slot->getCount() <= 0){ - unset($itemSlots[$index]); - } - } - } - - if(count($itemSlots) === 0){ - break; - } - } - - return $itemSlots; - } - - public function clear(int $index, bool $send = true) : bool{ - return $this->setItem($index, ItemFactory::get(Item::AIR, 0, 0), $send); - } - - public function clearAll(bool $send = true) : void{ - for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ - $this->clear($i, false); - } - - if($send){ - $this->sendContents($this->getViewers()); - } - } - - /** - * @return Player[] - */ - public function getViewers() : array{ - return $this->viewers; - } - - /** - * Removes the inventory window from all players currently viewing it. - * - * @param bool $force Force removal of permanent windows such as the player's own inventory. Used internally. - */ - public function removeAllViewers(bool $force = false) : void{ - foreach($this->viewers as $hash => $viewer){ - $viewer->removeWindow($this, $force); - unset($this->viewers[$hash]); - } - } - - public function setMaxStackSize(int $size) : void{ - $this->maxStackSize = $size; - } - - public function open(Player $who) : bool{ - $ev = new InventoryOpenEvent($this, $who); - $ev->call(); - if($ev->isCancelled()){ - return false; - } - $this->onOpen($who); - - return true; - } - - public function close(Player $who) : void{ - $this->onClose($who); - } - - public function onOpen(Player $who) : void{ - $this->viewers[spl_object_hash($who)] = $who; - } - - public function onClose(Player $who) : void{ - unset($this->viewers[spl_object_hash($who)]); - } - - public function onSlotChange(int $index, Item $before, bool $send) : void{ - if($send){ - $this->sendSlot($index, $this->getViewers()); - } - } - - /** - * @param Player|Player[] $target - */ - public function sendContents($target) : void{ - if($target instanceof Player){ - $target = [$target]; - } - - $pk = new InventoryContentPacket(); - $pk->items = array_map([ItemStackWrapper::class, 'legacy'], $this->getContents(true)); - - foreach($target as $player){ - if(($id = $player->getWindowId($this)) === ContainerIds::NONE){ - $this->close($player); - continue; - } - $pk->windowId = $id; - $player->dataPacket($pk); - } - } - - /** - * @param Player|Player[] $target - */ - public function sendSlot(int $index, $target) : void{ - if($target instanceof Player){ - $target = [$target]; - } - - $pk = new InventorySlotPacket(); - $pk->inventorySlot = $index; - $pk->item = ItemStackWrapper::legacy($this->getItem($index)); - - foreach($target as $player){ - if(($id = $player->getWindowId($this)) === ContainerIds::NONE){ - $this->close($player); - continue; - } - $pk->windowId = $id; - $player->dataPacket($pk); - } - } - - public function slotExists(int $slot) : bool{ - return $slot >= 0 and $slot < $this->slots->getSize(); - } - - public function getEventProcessor() : ?InventoryEventProcessor{ - return $this->eventProcessor; - } - - public function setEventProcessor(?InventoryEventProcessor $eventProcessor) : void{ - $this->eventProcessor = $eventProcessor; - } -} diff --git a/src/pocketmine/inventory/ChestInventory.php b/src/pocketmine/inventory/ChestInventory.php deleted file mode 100644 index 0f45ef431d..0000000000 --- a/src/pocketmine/inventory/ChestInventory.php +++ /dev/null @@ -1,101 +0,0 @@ -holder; - } - - protected function getOpenSound() : int{ - return LevelSoundEventPacket::SOUND_CHEST_OPEN; - } - - protected function getCloseSound() : int{ - return LevelSoundEventPacket::SOUND_CHEST_CLOSED; - } - - public function onOpen(Player $who) : void{ - parent::onOpen($who); - - if(count($this->getViewers()) === 1 and $this->getHolder()->isValid()){ - //TODO: this crap really shouldn't be managed by the inventory - $this->broadcastBlockEventPacket(true); - $this->getHolder()->getLevelNonNull()->broadcastLevelSoundEvent($this->getHolder()->add(0.5, 0.5, 0.5), $this->getOpenSound()); - } - } - - public function onClose(Player $who) : void{ - if(count($this->getViewers()) === 1 and $this->getHolder()->isValid()){ - //TODO: this crap really shouldn't be managed by the inventory - $this->broadcastBlockEventPacket(false); - $this->getHolder()->getLevelNonNull()->broadcastLevelSoundEvent($this->getHolder()->add(0.5, 0.5, 0.5), $this->getCloseSound()); - } - parent::onClose($who); - } - - protected function broadcastBlockEventPacket(bool $isOpen) : void{ - $holder = $this->getHolder(); - - $pk = new BlockEventPacket(); - $pk->x = (int) $holder->x; - $pk->y = (int) $holder->y; - $pk->z = (int) $holder->z; - $pk->eventType = 1; //it's always 1 for a chest - $pk->eventData = $isOpen ? 1 : 0; - $holder->getLevelNonNull()->broadcastPacketToViewers($holder, $pk); - } -} diff --git a/src/pocketmine/inventory/ContainerInventory.php b/src/pocketmine/inventory/ContainerInventory.php deleted file mode 100644 index a6231fc011..0000000000 --- a/src/pocketmine/inventory/ContainerInventory.php +++ /dev/null @@ -1,83 +0,0 @@ -holder = $holder; - parent::__construct($items, $size, $title); - } - - public function onOpen(Player $who) : void{ - parent::onOpen($who); - $pk = new ContainerOpenPacket(); - $pk->windowId = $who->getWindowId($this); - $pk->type = $this->getNetworkType(); - $holder = $this->getHolder(); - - $pk->x = $pk->y = $pk->z = 0; - $pk->entityUniqueId = -1; - - if($holder instanceof Entity){ - $pk->entityUniqueId = $holder->getId(); - }else{ - $pk->x = $holder->getFloorX(); - $pk->y = $holder->getFloorY(); - $pk->z = $holder->getFloorZ(); - } - - $who->dataPacket($pk); - - $this->sendContents($who); - } - - public function onClose(Player $who) : void{ - $pk = new ContainerClosePacket(); - $pk->windowId = $who->getWindowId($this); - $pk->server = $who->getClosingWindowId() !== $pk->windowId; - $who->dataPacket($pk); - parent::onClose($who); - } - - /** - * Returns the Minecraft PE inventory type used to show the inventory window to clients. - */ - abstract public function getNetworkType() : int; - - /** - * @return Vector3 - */ - public function getHolder(){ - return $this->holder; - } -} diff --git a/src/pocketmine/inventory/CraftingManager.php b/src/pocketmine/inventory/CraftingManager.php deleted file mode 100644 index 6e068cde19..0000000000 --- a/src/pocketmine/inventory/CraftingManager.php +++ /dev/null @@ -1,290 +0,0 @@ -init(); - } - - public function init() : void{ - $recipes = json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla" . DIRECTORY_SEPARATOR . "recipes.json"), true); - if(!is_array($recipes)){ - throw new AssumptionFailedError("recipes.json root should contain a map of recipe types"); - } - - $itemDeserializerFunc = \Closure::fromCallable([Item::class, 'jsonDeserialize']); - - foreach($recipes["shapeless"] as $recipe){ - if($recipe["block"] !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics - continue; - } - $this->registerShapelessRecipe(new ShapelessRecipe( - array_map($itemDeserializerFunc, $recipe["input"]), - array_map($itemDeserializerFunc, $recipe["output"]) - )); - } - foreach($recipes["shaped"] as $recipe){ - if($recipe["block"] !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics - continue; - } - $this->registerShapedRecipe(new ShapedRecipe( - $recipe["shape"], - array_map($itemDeserializerFunc, $recipe["input"]), - array_map($itemDeserializerFunc, $recipe["output"]) - )); - } - foreach($recipes["smelting"] as $recipe){ - if($recipe["block"] !== "furnace"){ //TODO: filter others out for now to avoid breaking economics - continue; - } - $this->registerFurnaceRecipe(new FurnaceRecipe( - Item::jsonDeserialize($recipe["output"]), - Item::jsonDeserialize($recipe["input"])) - ); - } - - $this->buildCraftingDataCache(); - } - - /** - * Rebuilds the cached CraftingDataPacket. - */ - public function buildCraftingDataCache() : void{ - Timings::$craftingDataCacheRebuildTimer->startTiming(); - $pk = new CraftingDataPacket(); - $pk->cleanRecipes = true; - - foreach($this->shapelessRecipes as $list){ - foreach($list as $recipe){ - $pk->addShapelessRecipe($recipe); - } - } - foreach($this->shapedRecipes as $list){ - foreach($list as $recipe){ - $pk->addShapedRecipe($recipe); - } - } - - foreach($this->furnaceRecipes as $recipe){ - $pk->addFurnaceRecipe($recipe); - } - - $pk->encode(); - - $batch = new BatchPacket(); - $batch->addPacket($pk); - $batch->setCompressionLevel(Server::getInstance()->networkCompressionLevel); - $batch->encode(); - - $this->craftingDataCache = $batch; - Timings::$craftingDataCacheRebuildTimer->stopTiming(); - } - - /** - * Returns a pre-compressed CraftingDataPacket for sending to players. Rebuilds the cache if it is not found. - */ - public function getCraftingDataPacket() : BatchPacket{ - if($this->craftingDataCache === null){ - $this->buildCraftingDataCache(); - } - - return $this->craftingDataCache; - } - - /** - * Function used to arrange Shapeless Recipe ingredient lists into a consistent order. - * - * @return int - */ - public static function sort(Item $i1, Item $i2){ - //Use spaceship operator to compare each property, then try the next one if they are equivalent. - ($retval = $i1->getId() <=> $i2->getId()) === 0 && ($retval = $i1->getDamage() <=> $i2->getDamage()) === 0 && ($retval = $i1->getCount() <=> $i2->getCount()) === 0; - - return $retval; - } - - /** - * @param Item[] $items - * - * @return Item[] - */ - private static function pack(array $items) : array{ - /** @var Item[] $result */ - $result = []; - - foreach($items as $i => $item){ - foreach($result as $otherItem){ - if($item->equals($otherItem)){ - $otherItem->setCount($otherItem->getCount() + $item->getCount()); - continue 2; - } - } - - //No matching item found - $result[] = clone $item; - } - - return $result; - } - - /** - * @param Item[] $outputs - */ - private static function hashOutputs(array $outputs) : string{ - $outputs = self::pack($outputs); - usort($outputs, [self::class, "sort"]); - foreach($outputs as $o){ - //this reduces accuracy of hash, but it's necessary to deal with recipe book shift-clicking stupidity - $o->setCount(1); - } - - return json_encode($outputs); - } - - /** - * @return ShapelessRecipe[][] - */ - public function getShapelessRecipes() : array{ - return $this->shapelessRecipes; - } - - /** - * @return ShapedRecipe[][] - */ - public function getShapedRecipes() : array{ - return $this->shapedRecipes; - } - - /** - * @return FurnaceRecipe[] - */ - public function getFurnaceRecipes() : array{ - return $this->furnaceRecipes; - } - - public function registerShapedRecipe(ShapedRecipe $recipe) : void{ - $this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe; - - $this->craftingDataCache = null; - } - - public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{ - $this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe; - - $this->craftingDataCache = null; - } - - public function registerFurnaceRecipe(FurnaceRecipe $recipe) : void{ - $input = $recipe->getInput(); - $this->furnaceRecipes[$input->getId() . ":" . ($input->hasAnyDamageValue() ? "?" : $input->getDamage())] = $recipe; - $this->craftingDataCache = null; - } - - /** - * @param Item[] $outputs - */ - public function matchRecipe(CraftingGrid $grid, array $outputs) : ?CraftingRecipe{ - //TODO: try to match special recipes before anything else (first they need to be implemented!) - - $outputHash = self::hashOutputs($outputs); - - if(isset($this->shapedRecipes[$outputHash])){ - foreach($this->shapedRecipes[$outputHash] as $recipe){ - if($recipe->matchesCraftingGrid($grid)){ - return $recipe; - } - } - } - - if(isset($this->shapelessRecipes[$outputHash])){ - foreach($this->shapelessRecipes[$outputHash] as $recipe){ - if($recipe->matchesCraftingGrid($grid)){ - return $recipe; - } - } - } - - return null; - } - - /** - * @param Item[] $outputs - * - * @return CraftingRecipe[]|\Generator - * @phpstan-return \Generator - */ - public function matchRecipeByOutputs(array $outputs) : \Generator{ - //TODO: try to match special recipes before anything else (first they need to be implemented!) - - $outputHash = self::hashOutputs($outputs); - - if(isset($this->shapedRecipes[$outputHash])){ - foreach($this->shapedRecipes[$outputHash] as $recipe){ - yield $recipe; - } - } - - if(isset($this->shapelessRecipes[$outputHash])){ - foreach($this->shapelessRecipes[$outputHash] as $recipe){ - yield $recipe; - } - } - } - - public function matchFurnaceRecipe(Item $input) : ?FurnaceRecipe{ - return $this->furnaceRecipes[$input->getId() . ":" . $input->getDamage()] ?? $this->furnaceRecipes[$input->getId() . ":?"] ?? null; - } - - /** - * @deprecated - */ - public function registerRecipe(Recipe $recipe) : void{ - $recipe->registerToCraftingManager($this); - } -} diff --git a/src/pocketmine/inventory/DoubleChestInventory.php b/src/pocketmine/inventory/DoubleChestInventory.php deleted file mode 100644 index bf91477dbe..0000000000 --- a/src/pocketmine/inventory/DoubleChestInventory.php +++ /dev/null @@ -1,146 +0,0 @@ -left = $left->getRealInventory(); - $this->right = $right->getRealInventory(); - $items = array_merge($this->left->getContents(true), $this->right->getContents(true)); - BaseInventory::__construct($items); - } - - public function getName() : string{ - return "Double Chest"; - } - - public function getDefaultSize() : int{ - return $this->left->getDefaultSize() + $this->right->getDefaultSize(); - } - - public function getInventory(){ - return $this; - } - - /** - * @return Chest|Position - */ - public function getHolder(){ - return $this->left->getHolder(); - } - - public function getItem(int $index) : Item{ - return $index < $this->left->getSize() ? $this->left->getItem($index) : $this->right->getItem($index - $this->left->getSize()); - } - - public function setItem(int $index, Item $item, bool $send = true) : bool{ - $old = $this->getItem($index); - if($index < $this->left->getSize() ? $this->left->setItem($index, $item, $send) : $this->right->setItem($index - $this->left->getSize(), $item, $send)){ - $this->onSlotChange($index, $old, $send); - return true; - } - return false; - } - - public function getContents(bool $includeEmpty = false) : array{ - $result = $this->left->getContents($includeEmpty); - $leftSize = $this->left->getSize(); - - foreach($this->right->getContents($includeEmpty) as $i => $item){ - $result[$i + $leftSize] = $item; - } - - return $result; - } - - /** - * @param Item[] $items - */ - public function setContents(array $items, bool $send = true) : void{ - $size = $this->getSize(); - if(count($items) > $size){ - $items = array_slice($items, 0, $size, true); - } - - $leftSize = $this->left->getSize(); - - for($i = 0; $i < $size; ++$i){ - if(!isset($items[$i])){ - if(($i < $leftSize and isset($this->left->slots[$i])) or isset($this->right->slots[$i - $leftSize])){ - $this->clear($i, false); - } - }elseif(!$this->setItem($i, $items[$i], false)){ - $this->clear($i, false); - } - } - - if($send){ - $this->sendContents($this->getViewers()); - } - } - - public function onOpen(Player $who) : void{ - parent::onOpen($who); - - if(count($this->getViewers()) === 1 and $this->right->getHolder()->isValid()){ - $this->right->broadcastBlockEventPacket(true); - } - } - - public function onClose(Player $who) : void{ - if(count($this->getViewers()) === 1 and $this->right->getHolder()->isValid()){ - $this->right->broadcastBlockEventPacket(false); - } - parent::onClose($who); - } - - public function getLeftSide() : ChestInventory{ - return $this->left; - } - - public function getRightSide() : ChestInventory{ - return $this->right; - } - - /** - * @return void - */ - public function invalidate(){ - $this->left = null; - $this->right = null; - } -} diff --git a/src/pocketmine/inventory/EnchantInventory.php b/src/pocketmine/inventory/EnchantInventory.php deleted file mode 100644 index 059b229b64..0000000000 --- a/src/pocketmine/inventory/EnchantInventory.php +++ /dev/null @@ -1,67 +0,0 @@ -asPosition()); - } - - public function getNetworkType() : int{ - return WindowTypes::ENCHANTMENT; - } - - public function getName() : string{ - return "Enchantment Table"; - } - - public function getDefaultSize() : int{ - return 2; //1 input, 1 lapis - } - - /** - * This override is here for documentation and code completion purposes only. - * @return Position - */ - public function getHolder(){ - return $this->holder; - } - - public function onClose(Player $who) : void{ - parent::onClose($who); - - foreach($this->getContents() as $item){ - $who->dropItem($item); - } - $this->clearAll(); - } -} diff --git a/src/pocketmine/inventory/EnderChestInventory.php b/src/pocketmine/inventory/EnderChestInventory.php deleted file mode 100644 index 6c8a7c6cef..0000000000 --- a/src/pocketmine/inventory/EnderChestInventory.php +++ /dev/null @@ -1,77 +0,0 @@ -holder->setComponents($enderChest->getFloorX(), $enderChest->getFloorY(), $enderChest->getFloorZ()); - $this->holder->setLevel($enderChest->getLevelNonNull()); - } - - protected function getOpenSound() : int{ - return LevelSoundEventPacket::SOUND_ENDERCHEST_OPEN; - } - - protected function getCloseSound() : int{ - return LevelSoundEventPacket::SOUND_ENDERCHEST_CLOSED; - } - - /** - * This override is here for documentation and code completion purposes only. - * @return Position - */ - public function getHolder(){ - return $this->holder; - } -} diff --git a/src/pocketmine/inventory/FurnaceInventory.php b/src/pocketmine/inventory/FurnaceInventory.php deleted file mode 100644 index 761d14514d..0000000000 --- a/src/pocketmine/inventory/FurnaceInventory.php +++ /dev/null @@ -1,81 +0,0 @@ -holder; - } - - public function getResult() : Item{ - return $this->getItem(2); - } - - public function getFuel() : Item{ - return $this->getItem(1); - } - - public function getSmelting() : Item{ - return $this->getItem(0); - } - - public function setResult(Item $item) : bool{ - return $this->setItem(2, $item); - } - - public function setFuel(Item $item) : bool{ - return $this->setItem(1, $item); - } - - public function setSmelting(Item $item) : bool{ - return $this->setItem(0, $item); - } -} diff --git a/src/pocketmine/inventory/MultiRecipe.php b/src/pocketmine/inventory/MultiRecipe.php deleted file mode 100644 index 8309c821f9..0000000000 --- a/src/pocketmine/inventory/MultiRecipe.php +++ /dev/null @@ -1,48 +0,0 @@ -uuid = $uuid; - } -} diff --git a/src/pocketmine/inventory/PlayerCursorInventory.php b/src/pocketmine/inventory/PlayerCursorInventory.php deleted file mode 100644 index 1107382f28..0000000000 --- a/src/pocketmine/inventory/PlayerCursorInventory.php +++ /dev/null @@ -1,65 +0,0 @@ -holder = $holder; - parent::__construct(); - } - - public function getName() : string{ - return "Cursor"; - } - - public function getDefaultSize() : int{ - return 1; - } - - public function setSize(int $size){ - throw new \BadMethodCallException("Cursor can only carry one item at a time"); - } - - /** - * This override is here for documentation and code completion purposes only. - * @return Player - */ - public function getHolder(){ - return $this->holder; - } - - public function sendContents($target) : void{ - //TODO: HACK! - //Since 1.13, this is now part of a larger "UI inventory", and sending contents for this larger inventory does - //not work the way it's intended to. Even if it did, it would be necessary to send all 51 slots just to update - //this one, which is just not worth it. - //This workaround isn't great, but it's at least simple. - $this->sendSlot(0, $target); - } -} diff --git a/src/pocketmine/inventory/PlayerInventory.php b/src/pocketmine/inventory/PlayerInventory.php deleted file mode 100644 index edfd0ea9ca..0000000000 --- a/src/pocketmine/inventory/PlayerInventory.php +++ /dev/null @@ -1,216 +0,0 @@ -holder = $player; - parent::__construct(); - } - - public function getName() : string{ - return "Player"; - } - - public function getDefaultSize() : int{ - return 36; - } - - /** - * Called when a client equips a hotbar slot. This method should not be used by plugins. - * This method will call PlayerItemHeldEvent. - * - * @param int $hotbarSlot Number of the hotbar slot to equip. - * - * @return bool if the equipment change was successful, false if not. - */ - public function equipItem(int $hotbarSlot) : bool{ - $holder = $this->getHolder(); - if(!$this->isHotbarSlot($hotbarSlot)){ - if($holder instanceof Player){ - $this->sendContents($holder); - } - return false; - } - - if($holder instanceof Player){ - $ev = new PlayerItemHeldEvent($holder, $this->getItem($hotbarSlot), $hotbarSlot); - $ev->call(); - - if($ev->isCancelled()){ - $this->sendHeldItem($holder); - return false; - } - } - $this->setHeldItemIndex($hotbarSlot, false); - - return true; - } - - private function isHotbarSlot(int $slot) : bool{ - return $slot >= 0 and $slot <= $this->getHotbarSize(); - } - - /** - * @throws \InvalidArgumentException - */ - private function throwIfNotHotbarSlot(int $slot) : void{ - if(!$this->isHotbarSlot($slot)){ - throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getHotbarSize() - 1) . ")"); - } - } - - /** - * Returns the item in the specified hotbar slot. - * - * @throws \InvalidArgumentException if the hotbar slot index is out of range - */ - public function getHotbarSlotItem(int $hotbarSlot) : Item{ - $this->throwIfNotHotbarSlot($hotbarSlot); - return $this->getItem($hotbarSlot); - } - - /** - * Returns the hotbar slot number the holder is currently holding. - */ - public function getHeldItemIndex() : int{ - return $this->itemInHandIndex; - } - - /** - * Sets which hotbar slot the player is currently loading. - * - * @param int $hotbarSlot 0-8 index of the hotbar slot to hold - * @param bool $send Whether to send updates back to the inventory holder. This should usually be true for plugin calls. - * It should only be false to prevent feedback loops of equipment packets between client and server. - * - * @return void - * @throws \InvalidArgumentException if the hotbar slot is out of range - */ - public function setHeldItemIndex(int $hotbarSlot, bool $send = true){ - $this->throwIfNotHotbarSlot($hotbarSlot); - - $this->itemInHandIndex = $hotbarSlot; - - if($this->getHolder() instanceof Player and $send){ - $this->sendHeldItem($this->getHolder()); - } - - $this->sendHeldItem($this->getHolder()->getViewers()); - } - - /** - * Returns the currently-held item. - */ - public function getItemInHand() : Item{ - return $this->getHotbarSlotItem($this->itemInHandIndex); - } - - /** - * Sets the item in the currently-held slot to the specified item. - */ - public function setItemInHand(Item $item) : bool{ - return $this->setItem($this->getHeldItemIndex(), $item); - } - - /** - * Sends the currently-held item to specified targets. - * - * @param Player|Player[] $target - * - * @return void - */ - public function sendHeldItem($target){ - $item = $this->getItemInHand(); - - $pk = new MobEquipmentPacket(); - $pk->entityRuntimeId = $this->getHolder()->getId(); - $pk->item = ItemStackWrapper::legacy($item); - $pk->inventorySlot = $pk->hotbarSlot = $this->getHeldItemIndex(); - $pk->windowId = ContainerIds::INVENTORY; - - if(!is_array($target)){ - $target->dataPacket($pk); - if($target === $this->getHolder()){ - $this->sendSlot($this->getHeldItemIndex(), $target); - } - }else{ - $this->getHolder()->getLevelNonNull()->getServer()->broadcastPacket($target, $pk); - if(in_array($this->getHolder(), $target, true)){ - $this->sendSlot($this->getHeldItemIndex(), $this->getHolder()); - } - } - } - - /** - * Returns the number of slots in the hotbar. - */ - public function getHotbarSize() : int{ - return 9; - } - - /** - * @return void - */ - public function sendCreativeContents(){ - //TODO: this mess shouldn't be in here - $holder = $this->getHolder(); - if(!($holder instanceof Player)){ - throw new \LogicException("Cannot send creative inventory contents to non-player inventory holder"); - } - - $nextEntryId = 1; - $holder->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use (&$nextEntryId) : CreativeContentEntry{ - return new CreativeContentEntry($nextEntryId++, clone $item); - }, $holder->isSpectator() ? [] : Item::getCreativeItems()))); //fill it for all gamemodes except spectator - } - - /** - * This override is here for documentation and code completion purposes only. - * @return Human|Player - */ - public function getHolder(){ - return $this->holder; - } -} diff --git a/src/pocketmine/inventory/transaction/action/CreativeInventoryAction.php b/src/pocketmine/inventory/transaction/action/CreativeInventoryAction.php deleted file mode 100644 index 7e5b94e0f1..0000000000 --- a/src/pocketmine/inventory/transaction/action/CreativeInventoryAction.php +++ /dev/null @@ -1,73 +0,0 @@ -actionType = $actionType; - } - - /** - * Checks that the player is in creative, and (if creating an item) that the item exists in the creative inventory. - */ - public function isValid(Player $source) : bool{ - return $source->isCreative(true) and - ($this->actionType === self::TYPE_DELETE_ITEM or Item::getCreativeItemIndex($this->sourceItem) !== -1); - } - - /** - * Returns the type of the action. - */ - public function getActionType() : int{ - return $this->actionType; - } - - /** - * No need to do anything extra here: this type just provides a place for items to disappear or appear from. - */ - public function execute(Player $source) : bool{ - return true; - } - - public function onExecuteSuccess(Player $source) : void{ - - } - - public function onExecuteFail(Player $source) : void{ - - } -} diff --git a/src/pocketmine/item/Armor.php b/src/pocketmine/item/Armor.php deleted file mode 100644 index ba6b15e575..0000000000 --- a/src/pocketmine/item/Armor.php +++ /dev/null @@ -1,94 +0,0 @@ -getNamedTag()->hasTag(self::TAG_CUSTOM_COLOR, IntTag::class)){ - return Color::fromARGB(Binary::unsignInt($this->getNamedTag()->getInt(self::TAG_CUSTOM_COLOR))); - } - - return null; - } - - /** - * Sets the dyed colour of this armour piece. This generally only applies to leather armour. - */ - public function setCustomColor(Color $color) : void{ - $this->setNamedTagEntry(new IntTag(self::TAG_CUSTOM_COLOR, Binary::signInt($color->toARGB()))); - } - - /** - * Returns the total enchantment protection factor this armour piece offers from all applicable protection - * enchantments on the item. - */ - public function getEnchantmentProtectionFactor(EntityDamageEvent $event) : int{ - $epf = 0; - - foreach($this->getEnchantments() as $enchantment){ - $type = $enchantment->getType(); - if($type instanceof ProtectionEnchantment and $type->isApplicable($event)){ - $epf += $type->getProtectionFactor($enchantment->getLevel()); - } - } - - return $epf; - } - - protected function getUnbreakingDamageReduction(int $amount) : int{ - if(($unbreakingLevel = $this->getEnchantmentLevel(Enchantment::UNBREAKING)) > 0){ - $negated = 0; - - $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 - $negated++; - } - } - - return $negated; - } - - return 0; - } -} diff --git a/src/pocketmine/item/Banner.php b/src/pocketmine/item/Banner.php deleted file mode 100644 index 4af379872d..0000000000 --- a/src/pocketmine/item/Banner.php +++ /dev/null @@ -1,205 +0,0 @@ -getNamedTag()->getInt(self::TAG_BASE, 0); - } - - /** - * Sets the color of the banner base. - * Banner items have to be resent to see the changes in the inventory. - */ - public function setBaseColor(int $color) : void{ - $namedTag = $this->getNamedTag(); - $namedTag->setInt(self::TAG_BASE, $color & 0x0f); - $this->setNamedTag($namedTag); - } - - /** - * Applies a new pattern on the banner with the given color. - * Banner items have to be resent to see the changes in the inventory. - * - * @return int ID of pattern. - */ - public function addPattern(string $pattern, int $color) : int{ - $patternsTag = $this->getNamedTag()->getListTag(self::TAG_PATTERNS); - assert($patternsTag !== null); - - $patternsTag->push(new CompoundTag("", [ - new IntTag(self::TAG_PATTERN_COLOR, $color & 0x0f), - new StringTag(self::TAG_PATTERN_NAME, $pattern) - ])); - - $this->setNamedTagEntry($patternsTag); - - return $patternsTag->count() - 1; - } - - /** - * Returns whether a pattern with the given ID exists on the banner or not. - */ - public function patternExists(int $patternId) : bool{ - $this->correctNBT(); - return $this->getNamedTag()->getListTag(self::TAG_PATTERNS)->isset($patternId); - } - - /** - * Returns the data of a pattern with the given ID. - * - * @return mixed[] - * @phpstan-return array{Color?: int, Pattern?: string} - */ - public function getPatternData(int $patternId) : array{ - if(!$this->patternExists($patternId)){ - return []; - } - - $patternsTag = $this->getNamedTag()->getListTag(self::TAG_PATTERNS); - assert($patternsTag !== null); - $pattern = $patternsTag->get($patternId); - assert($pattern instanceof CompoundTag); - - return [ - self::TAG_PATTERN_COLOR => $pattern->getInt(self::TAG_PATTERN_COLOR), - self::TAG_PATTERN_NAME => $pattern->getString(self::TAG_PATTERN_NAME) - ]; - } - - /** - * Changes the pattern of a previously existing pattern. - * Banner items have to be resent to see the changes in the inventory. - * - * @return bool indicating success. - */ - public function changePattern(int $patternId, string $pattern, int $color) : bool{ - if(!$this->patternExists($patternId)){ - return false; - } - - $patternsTag = $this->getNamedTag()->getListTag(self::TAG_PATTERNS); - assert($patternsTag !== null); - - $patternsTag->set($patternId, new CompoundTag("", [ - new IntTag(self::TAG_PATTERN_COLOR, $color & 0x0f), - new StringTag(self::TAG_PATTERN_NAME, $pattern) - ])); - - $this->setNamedTagEntry($patternsTag); - return true; - } - - /** - * Deletes a pattern from the banner with the given ID. - * Banner items have to be resent to see the changes in the inventory. - * - * @return bool indicating whether the pattern existed or not. - */ - public function deletePattern(int $patternId) : bool{ - if(!$this->patternExists($patternId)){ - return false; - } - - $patternsTag = $this->getNamedTag()->getListTag(self::TAG_PATTERNS); - if($patternsTag instanceof ListTag){ - $patternsTag->remove($patternId); - $this->setNamedTagEntry($patternsTag); - } - - return true; - } - - /** - * Deletes the top most pattern of the banner. - * Banner items have to be resent to see the changes in the inventory. - * - * @return bool indicating whether the banner was empty or not. - */ - public function deleteTopPattern() : bool{ - return $this->deletePattern($this->getPatternCount() - 1); - } - - /** - * Deletes the bottom pattern of the banner. - * Banner items have to be resent to see the changes in the inventory. - * - * @return bool indicating whether the banner was empty or not. - */ - public function deleteBottomPattern() : bool{ - return $this->deletePattern(0); - } - - /** - * Returns the total count of patterns on this banner. - */ - public function getPatternCount() : int{ - return $this->getNamedTag()->getListTag(self::TAG_PATTERNS)->count(); - } - - public function correctNBT() : void{ - $tag = $this->getNamedTag(); - if(!$tag->hasTag(self::TAG_BASE, IntTag::class)){ - $tag->setInt(self::TAG_BASE, 0); - } - - if(!$tag->hasTag(self::TAG_PATTERNS, ListTag::class)){ - $tag->setTag(new ListTag(self::TAG_PATTERNS)); - } - $this->setNamedTag($tag); - } - - public function getFuelTime() : int{ - return 300; - } -} diff --git a/src/pocketmine/item/Bow.php b/src/pocketmine/item/Bow.php deleted file mode 100644 index 2d1d026e70..0000000000 --- a/src/pocketmine/item/Bow.php +++ /dev/null @@ -1,125 +0,0 @@ -isSurvival() and !$player->getInventory()->contains(ItemFactory::get(Item::ARROW, 0, 1))){ - $player->getInventory()->sendContents($player); - return false; - } - - $nbt = Entity::createBaseNBT( - $player->add(0, $player->getEyeHeight(), 0), - $player->getDirectionVector(), - ($player->yaw > 180 ? 360 : 0) - $player->yaw, - -$player->pitch - ); - - $diff = $player->getItemUseDuration(); - $p = $diff / 20; - $baseForce = min((($p ** 2) + $p * 2) / 3, 1); - - $entity = Entity::createEntity("Arrow", $player->getLevelNonNull(), $nbt, $player, $baseForce >= 1); - if($entity instanceof Projectile){ - $infinity = $this->hasEnchantment(Enchantment::INFINITY); - if($entity instanceof ArrowEntity){ - if($infinity){ - $entity->setPickupMode(ArrowEntity::PICKUP_CREATIVE); - } - if(($punchLevel = $this->getEnchantmentLevel(Enchantment::PUNCH)) > 0){ - $entity->setPunchKnockback($punchLevel); - } - } - if(($powerLevel = $this->getEnchantmentLevel(Enchantment::POWER)) > 0){ - $entity->setBaseDamage($entity->getBaseDamage() + (($powerLevel + 1) / 2)); - } - if($this->hasEnchantment(Enchantment::FLAME)){ - $entity->setOnFire(intdiv($entity->getFireTicks(), 20) + 100); - } - $ev = new EntityShootBowEvent($player, $this, $entity, $baseForce * 3); - - if($baseForce < 0.1 or $diff < 5 or $player->isSpectator()){ - $ev->setCancelled(); - } - - $ev->call(); - - $entity = $ev->getProjectile(); //This might have been changed by plugins - - if($ev->isCancelled()){ - $entity->flagForDespawn(); - $player->getInventory()->sendContents($player); - }else{ - $entity->setMotion($entity->getMotion()->multiply($ev->getForce())); - if($player->isSurvival()){ - if(!$infinity){ //TODO: tipped arrows are still consumed when Infinity is applied - $player->getInventory()->removeItem(ItemFactory::get(Item::ARROW, 0, 1)); - } - $this->applyDamage(1); - } - - if($entity instanceof Projectile){ - $projectileEv = new ProjectileLaunchEvent($entity); - $projectileEv->call(); - if($projectileEv->isCancelled()){ - $ev->getProjectile()->flagForDespawn(); - }else{ - $ev->getProjectile()->spawnToAll(); - $player->getLevelNonNull()->broadcastLevelSoundEvent($player, LevelSoundEventPacket::SOUND_BOW); - } - }else{ - $entity->spawnToAll(); - } - } - }else{ - $entity->spawnToAll(); - } - - return true; - } -} diff --git a/src/pocketmine/item/Bucket.php b/src/pocketmine/item/Bucket.php deleted file mode 100644 index af516e8d1b..0000000000 --- a/src/pocketmine/item/Bucket.php +++ /dev/null @@ -1,125 +0,0 @@ -meta === Block::AIR ? 16 : 1; //empty buckets stack to 16 - } - - public function getFuelTime() : int{ - if($this->meta === Block::LAVA or $this->meta === Block::FLOWING_LAVA){ - return 20000; - } - - return 0; - } - - public function getFuelResidue() : Item{ - if($this->meta === Block::LAVA or $this->meta === Block::FLOWING_LAVA){ - return ItemFactory::get(Item::BUCKET); - } - - return parent::getFuelResidue(); - } - - public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{ - $resultBlock = BlockFactory::get($this->meta); - - if($resultBlock instanceof Air){ - if($blockClicked instanceof Liquid and $blockClicked->getDamage() === 0){ - $stack = clone $this; - - $stack->pop(); - $resultItem = ItemFactory::get(Item::BUCKET, $blockClicked->getFlowingForm()->getId()); - $ev = new PlayerBucketFillEvent($player, $blockReplace, $face, $this, $resultItem); - $ev->call(); - if(!$ev->isCancelled()){ - $player->getLevelNonNull()->setBlock($blockClicked, BlockFactory::get(Block::AIR), true, true); - $player->getLevelNonNull()->broadcastLevelSoundEvent($blockClicked->add(0.5, 0.5, 0.5), $blockClicked->getBucketFillSound()); - if($player->isSurvival()){ - if($stack->getCount() === 0){ - $player->getInventory()->setItemInHand($ev->getItem()); - }else{ - $player->getInventory()->setItemInHand($stack); - $player->getInventory()->addItem($ev->getItem()); - } - }else{ - $player->getInventory()->addItem($ev->getItem()); - } - - return true; - }else{ - $player->getInventory()->sendContents($player); - } - } - }elseif($resultBlock instanceof Liquid and $blockReplace->canBeReplaced()){ - $ev = new PlayerBucketEmptyEvent($player, $blockReplace, $face, $this, ItemFactory::get(Item::BUCKET)); - $ev->call(); - if(!$ev->isCancelled()){ - $player->getLevelNonNull()->setBlock($blockReplace, $resultBlock->getFlowingForm(), true, true); - $player->getLevelNonNull()->broadcastLevelSoundEvent($blockReplace->add(0.5, 0.5, 0.5), $resultBlock->getBucketEmptySound()); - - if($player->isSurvival()){ - $player->getInventory()->setItemInHand($ev->getItem()); - } - return true; - }else{ - $player->getInventory()->sendContents($player); - } - } - - return false; - } - - public function getResidue(){ - return ItemFactory::get(Item::BUCKET, 0, 1); - } - - public function getAdditionalEffects() : array{ - return []; - } - - public function canBeConsumed() : bool{ - return $this->meta === 1; //Milk - } - - public function onConsume(Living $consumer){ - $consumer->removeAllEffects(); - } -} diff --git a/src/pocketmine/item/ChainChestplate.php b/src/pocketmine/item/ChainChestplate.php deleted file mode 100644 index 5952d8386f..0000000000 --- a/src/pocketmine/item/ChainChestplate.php +++ /dev/null @@ -1,38 +0,0 @@ -read($tag); - if(!($data instanceof CompoundTag)){ - throw new \InvalidArgumentException("Invalid item NBT string given, it could not be deserialized"); - } - - return $data; - } - - private static function writeCompoundTag(CompoundTag $tag) : string{ - if(self::$cachedParser === null){ - self::$cachedParser = new LittleEndianNBTStream(); - } - - return self::$cachedParser->write($tag); - } - - /** - * Returns a new Item instance with the specified ID, damage, count and NBT. - * - * This function redirects to {@link ItemFactory#get}. - * - * @param CompoundTag|string $tags - */ - public static function get(int $id, int $meta = 0, int $count = 1, $tags = "") : Item{ - return ItemFactory::get($id, $meta, $count, $tags); - } - - /** - * Tries to parse the specified string into Item ID/meta identifiers, and returns Item instances it created. - * - * This function redirects to {@link ItemFactory#fromString}. - * - * @return Item[]|Item - */ - public static function fromString(string $str, bool $multiple = false){ - return ItemFactory::fromString($str, $multiple); - } - - /** @var Item[] */ - private static $creative = []; - - /** - * @return void - */ - public static function initCreativeItems(){ - self::clearCreativeItems(); - - $creativeItems = json_decode(file_get_contents(\pocketmine\RESOURCE_PATH . "vanilla" . DIRECTORY_SEPARATOR . "creativeitems.json"), true); - - foreach($creativeItems as $data){ - $item = Item::jsonDeserialize($data); - if($item->getName() === "Unknown"){ - continue; - } - self::addCreativeItem($item); - } - } - - /** - * Removes all previously added items from the creative menu. - * Note: Players who are already online when this is called will not see this change. - * - * @return void - */ - public static function clearCreativeItems(){ - Item::$creative = []; - } - - /** - * @return Item[] - */ - public static function getCreativeItems() : array{ - return Item::$creative; - } - - /** - * Adds an item to the creative menu. - * Note: Players who are already online when this is called will not see this change. - * - * @return void - */ - public static function addCreativeItem(Item $item){ - Item::$creative[] = clone $item; - } - - /** - * Removes an item from the creative menu. - * Note: Players who are already online when this is called will not see this change. - * - * @return void - */ - public static function removeCreativeItem(Item $item){ - $index = self::getCreativeItemIndex($item); - if($index !== -1){ - unset(Item::$creative[$index]); - } - } - - public static function isCreativeItem(Item $item) : bool{ - return Item::getCreativeItemIndex($item) !== -1; - } - - /** - * @return Item|null - */ - public static function getCreativeItem(int $index){ - return Item::$creative[$index] ?? null; - } - - public static function getCreativeItemIndex(Item $item) : int{ - foreach(Item::$creative as $i => $d){ - if($item->equals($d, !($item instanceof Durable))){ - return $i; - } - } - - return -1; - } - - /** @var int */ - protected $id; - /** @var int */ - protected $meta; - /** @var CompoundTag|null */ - private $nbt = null; - /** @var int */ - public $count = 1; - /** @var string */ - protected $name; - - /** - * Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register - * into the index. - * - * NOTE: This should NOT BE USED for creating items to set into an inventory. Use {@link ItemFactory#get} for that - * purpose. - */ - public function __construct(int $id, int $meta = 0, string $name = "Unknown"){ - if($id < -0x8000 or $id > 0x7fff){ //signed short range - throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); - } - $this->id = $id; - $this->setDamage($meta); - $this->name = $name; - } - - /** - * @deprecated This method accepts NBT serialized in a network-dependent format. - * @see Item::setNamedTag() - * - * @param CompoundTag|string|null $tags - * - * @return $this - */ - public function setCompoundTag($tags) : Item{ - if($tags instanceof CompoundTag){ - $this->setNamedTag($tags); - }elseif(is_string($tags) and strlen($tags) > 0){ - $this->setNamedTag(self::parseCompoundTag($tags)); - }else{ - $this->clearNamedTag(); - } - - return $this; - } - - /** - * @deprecated This method returns NBT serialized in a network-dependent format. Prefer use of getNamedTag() instead. - * @see Item::getNamedTag() - * - * Returns the serialized NBT of the Item - */ - public function getCompoundTag() : string{ - return $this->nbt !== null ? self::writeCompoundTag($this->nbt) : ""; - } - - /** - * Returns whether this Item has a non-empty NBT. - */ - public function hasCompoundTag() : bool{ - return $this->nbt !== null and $this->nbt->getCount() > 0; - } - - public function hasCustomBlockData() : bool{ - return $this->getNamedTagEntry(self::TAG_BLOCK_ENTITY_TAG) instanceof CompoundTag; - } - - /** - * @return $this - */ - public function clearCustomBlockData(){ - $this->removeNamedTagEntry(self::TAG_BLOCK_ENTITY_TAG); - return $this; - } - - /** - * @return $this - */ - public function setCustomBlockData(CompoundTag $compound) : Item{ - $tags = clone $compound; - $tags->setName(self::TAG_BLOCK_ENTITY_TAG); - $this->setNamedTagEntry($tags); - - return $this; - } - - public function getCustomBlockData() : ?CompoundTag{ - $tag = $this->getNamedTagEntry(self::TAG_BLOCK_ENTITY_TAG); - return $tag instanceof CompoundTag ? $tag : null; - } - - public function hasEnchantments() : bool{ - return $this->getNamedTagEntry(self::TAG_ENCH) instanceof ListTag; - } - - public function hasEnchantment(int $id, int $level = -1) : bool{ - $ench = $this->getNamedTagEntry(self::TAG_ENCH); - if(!($ench instanceof ListTag)){ - return false; - } - - /** @var CompoundTag $entry */ - foreach($ench as $entry){ - if($entry->getShort("id") === $id and ($level === -1 or $entry->getShort("lvl") === $level)){ - return true; - } - } - - return false; - } - - public function getEnchantment(int $id) : ?EnchantmentInstance{ - $ench = $this->getNamedTagEntry(self::TAG_ENCH); - if(!($ench instanceof ListTag)){ - return null; - } - - /** @var CompoundTag $entry */ - foreach($ench as $entry){ - if($entry->getShort("id") === $id){ - $e = Enchantment::getEnchantment($entry->getShort("id")); - if($e !== null){ - return new EnchantmentInstance($e, $entry->getShort("lvl")); - } - } - } - - return null; - } - - public function removeEnchantment(int $id, int $level = -1) : void{ - $ench = $this->getNamedTagEntry(self::TAG_ENCH); - if(!($ench instanceof ListTag)){ - return; - } - - /** @var CompoundTag $entry */ - foreach($ench as $k => $entry){ - if($entry->getShort("id") === $id and ($level === -1 or $entry->getShort("lvl") === $level)){ - $ench->remove($k); - break; - } - } - - if($ench->getCount() > 0){ - $this->setNamedTagEntry($ench); - }else{ - $this->removeNamedTagEntry(self::TAG_ENCH); - } - } - - public function removeEnchantments() : void{ - $this->removeNamedTagEntry(self::TAG_ENCH); - } - - public function addEnchantment(EnchantmentInstance $enchantment) : void{ - $found = false; - - $ench = $this->getNamedTagEntry(self::TAG_ENCH); - if(!($ench instanceof ListTag)){ - $ench = new ListTag(self::TAG_ENCH, [], NBT::TAG_Compound); - }else{ - /** @var CompoundTag $entry */ - foreach($ench as $k => $entry){ - if($entry->getShort("id") === $enchantment->getId()){ - $ench->set($k, new CompoundTag("", [ - new ShortTag("id", $enchantment->getId()), - new ShortTag("lvl", $enchantment->getLevel()) - ])); - $found = true; - break; - } - } - } - - if(!$found){ - $ench->push(new CompoundTag("", [ - new ShortTag("id", $enchantment->getId()), - new ShortTag("lvl", $enchantment->getLevel()) - ])); - } - - $this->setNamedTagEntry($ench); - } - - /** - * @return EnchantmentInstance[] - */ - public function getEnchantments() : array{ - /** @var EnchantmentInstance[] $enchantments */ - $enchantments = []; - - $ench = $this->getNamedTagEntry(self::TAG_ENCH); - if($ench instanceof ListTag){ - /** @var CompoundTag $entry */ - foreach($ench as $entry){ - $e = Enchantment::getEnchantment($entry->getShort("id")); - if($e !== null){ - $enchantments[] = new EnchantmentInstance($e, $entry->getShort("lvl")); - } - } - } - - return $enchantments; - } - - /** - * Returns the level of the enchantment on this item with the specified ID, or 0 if the item does not have the - * enchantment. - */ - public function getEnchantmentLevel(int $enchantmentId) : int{ - $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); - if($ench !== null){ - /** @var CompoundTag $entry */ - foreach($ench as $entry){ - if($entry->getShort("id") === $enchantmentId){ - return $entry->getShort("lvl"); - } - } - } - - return 0; - } - - public function hasCustomName() : bool{ - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if($display instanceof CompoundTag){ - return $display->hasTag(self::TAG_DISPLAY_NAME); - } - - return false; - } - - public function getCustomName() : string{ - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if($display instanceof CompoundTag){ - return $display->getString(self::TAG_DISPLAY_NAME, ""); - } - - return ""; - } - - /** - * @return $this - */ - public function setCustomName(string $name) : Item{ - if($name === ""){ - return $this->clearCustomName(); - } - - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if(!($display instanceof CompoundTag)){ - $display = new CompoundTag(self::TAG_DISPLAY); - } - - $display->setString(self::TAG_DISPLAY_NAME, $name); - $this->setNamedTagEntry($display); - - return $this; - } - - /** - * @return $this - */ - public function clearCustomName() : Item{ - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if($display instanceof CompoundTag){ - $display->removeTag(self::TAG_DISPLAY_NAME); - - if($display->getCount() === 0){ - $this->removeNamedTagEntry($display->getName()); - }else{ - $this->setNamedTagEntry($display); - } - } - - return $this; - } - - /** - * @return string[] - */ - public function getLore() : array{ - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if($display instanceof CompoundTag and ($lore = $display->getListTag(self::TAG_DISPLAY_LORE)) !== null){ - return array_map(function(NamedTag $line) : string{ - if(!($line instanceof StringTag)){ - throw new AssumptionFailedError("Nobody bothered to handle this error case and we can't fix it until PM4, oops ... #blameshoghi"); - } - return $line->getValue(); - }, $lore->getValue()); - } - - return []; - } - - /** - * @param string[] $lines - * - * @return $this - */ - public function setLore(array $lines) : Item{ - $display = $this->getNamedTagEntry(self::TAG_DISPLAY); - if(!($display instanceof CompoundTag)){ - $display = new CompoundTag(self::TAG_DISPLAY, []); - } - - $display->setTag(new ListTag(self::TAG_DISPLAY_LORE, array_map(function(string $str) : StringTag{ - return new StringTag("", $str); - }, $lines), NBT::TAG_String)); - - $this->setNamedTagEntry($display); - - return $this; - } - - public function getNamedTagEntry(string $name) : ?NamedTag{ - return $this->getNamedTag()->getTag($name); - } - - public function setNamedTagEntry(NamedTag $new) : void{ - $tag = $this->getNamedTag(); - $tag->setTag($new); - $this->setNamedTag($tag); - } - - public function removeNamedTagEntry(string $name) : void{ - $tag = $this->getNamedTag(); - $tag->removeTag($name); - $this->setNamedTag($tag); - } - - /** - * Returns a tree of Tag objects representing the Item's NBT. If the item does not have any NBT, an empty CompoundTag - * object is returned to allow the caller to manipulate and apply back to the item. - */ - public function getNamedTag() : CompoundTag{ - return $this->nbt ?? ($this->nbt = new CompoundTag()); - } - - /** - * Sets the Item's NBT from the supplied CompoundTag object. - * - * @return $this - */ - public function setNamedTag(CompoundTag $tag) : Item{ - if($tag->getCount() === 0){ - return $this->clearNamedTag(); - } - - $this->nbt = clone $tag; - - return $this; - } - - /** - * Removes the Item's NBT. - * @return $this - */ - public function clearNamedTag() : Item{ - $this->nbt = null; - return $this; - } - - public function getCount() : int{ - return $this->count; - } - - /** - * @return $this - */ - public function setCount(int $count) : Item{ - $this->count = $count; - - return $this; - } - - /** - * Pops an item from the stack and returns it, decreasing the stack count of this item stack by one. - * - * @return static A clone of this itemstack containing the amount of items that were removed from this stack. - * @throws \InvalidArgumentException if trying to pop more items than are on the stack - */ - public function pop(int $count = 1) : Item{ - if($count > $this->count){ - throw new \InvalidArgumentException("Cannot pop $count items from a stack of $this->count"); - } - - $item = clone $this; - $item->count = $count; - - $this->count -= $count; - - return $item; - } - - public function isNull() : bool{ - return $this->count <= 0 or $this->id === Item::AIR; - } - - /** - * Returns the name of the item, or the custom name if it is set. - */ - final public function getName() : string{ - return $this->hasCustomName() ? $this->getCustomName() : $this->getVanillaName(); - } - - /** - * Returns the vanilla name of the item, disregarding custom names. - */ - public function getVanillaName() : string{ - return $this->name; - } - - final public function canBePlaced() : bool{ - return $this->getBlock()->canBePlaced(); - } - - /** - * Returns the block corresponding to this Item. - */ - public function getBlock() : Block{ - return BlockFactory::get(self::AIR); - } - - final public function getId() : int{ - return $this->id; - } - - final public function getDamage() : int{ - return $this->meta; - } - - /** - * @return $this - */ - public function setDamage(int $meta) : Item{ - $this->meta = $meta !== -1 ? $meta & 0x7FFF : -1; - - return $this; - } - - /** - * Returns whether this item can match any item with an equivalent ID with any meta value. - * Used in crafting recipes which accept multiple variants of the same item, for example crafting tables recipes. - */ - public function hasAnyDamageValue() : bool{ - return $this->meta === -1; - } - - /** - * Returns the highest amount of this item which will fit into one inventory slot. - */ - public function getMaxStackSize() : int{ - return 64; - } - - /** - * Returns the time in ticks which the item will fuel a furnace for. - */ - public function getFuelTime() : int{ - return 0; - } - - /** - * Returns an item after burning fuel - */ - public function getFuelResidue() : Item{ - $item = clone $this; - $item->pop(); - - return $item; - } - - /** - * Returns how many points of damage this item will deal to an entity when used as a weapon. - */ - public function getAttackPoints() : int{ - return 1; - } - - /** - * Returns how many armor points can be gained by wearing this item. - */ - public function getDefensePoints() : int{ - return 0; - } - - /** - * Returns what type of block-breaking tool this is. Blocks requiring the same tool type as the item will break - * faster (except for blocks requiring no tool, which break at the same speed regardless of the tool used) - */ - public function getBlockToolType() : int{ - return BlockToolType::TYPE_NONE; - } - - /** - * Returns the harvesting power that this tool has. This affects what blocks it can mine when the tool type matches - * the mined block. - * This should return 1 for non-tiered tools, and the tool tier for tiered tools. - * - * @see Block::getToolHarvestLevel() - */ - public function getBlockToolHarvestLevel() : int{ - return 0; - } - - public function getMiningEfficiency(Block $block) : float{ - return 1; - } - - /** - * Called when a player uses this item on a block. - */ - public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{ - return false; - } - - /** - * Called when a player uses the item on air, for example throwing a projectile. - * Returns whether the item was changed, for example count decrease or durability change. - */ - public function onClickAir(Player $player, Vector3 $directionVector) : bool{ - return false; - } - - /** - * Called when a player is using this item and releases it. Used to handle bow shoot actions. - * Returns whether the item was changed, for example count decrease or durability change. - */ - public function onReleaseUsing(Player $player) : bool{ - return false; - } - - /** - * Called when this item is used to destroy a block. Usually used to update durability. - */ - public function onDestroyBlock(Block $block) : bool{ - return false; - } - - /** - * Called when this item is used to attack an entity. Usually used to update durability. - */ - public function onAttackEntity(Entity $victim) : bool{ - return false; - } - - /** - * Returns the number of ticks a player must wait before activating this item again. - */ - public function getCooldownTicks() : int{ - return 0; - } - - /** - * Compares an Item to this Item and check if they match. - * - * @param bool $checkDamage Whether to verify that the damage values match. - * @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->id === $item->getId() and - (!$checkDamage or $this->getDamage() === $item->getDamage()) and - (!$checkCompound or $this->getNamedTag()->equals($item->getNamedTag())); - } - - /** - * 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; - } - - final public function __toString() : string{ - return "Item " . $this->name . " (" . $this->id . ":" . ($this->hasAnyDamageValue() ? "?" : $this->meta) . ")x" . $this->count . ($this->hasCompoundTag() ? " tags:" . base64_encode($this->getCompoundTag()) : ""); - } - - /** - * Returns an array of item stack properties that can be serialized to json. - * - * @return mixed[] - * @phpstan-return array{id: int, damage?: int, count?: int, nbt_b64?: string} - */ - final public function jsonSerialize() : array{ - $data = [ - "id" => $this->getId() - ]; - - if($this->getDamage() !== 0){ - $data["damage"] = $this->getDamage(); - } - - if($this->getCount() !== 1){ - $data["count"] = $this->getCount(); - } - - if($this->hasCompoundTag()){ - $data["nbt_b64"] = base64_encode($this->getCompoundTag()); - } - - return $data; - } - - /** - * Returns an Item from properties created in an array by {@link Item#jsonSerialize} - * @param mixed[] $data - * @phpstan-param array{ - * id: int, - * damage?: int, - * count?: int, - * nbt?: string, - * nbt_hex?: string, - * nbt_b64?: string - * } $data - */ - final public static function jsonDeserialize(array $data) : Item{ - $nbt = ""; - - //Backwards compatibility - if(isset($data["nbt"])){ - $nbt = $data["nbt"]; - }elseif(isset($data["nbt_hex"])){ - $nbt = hex2bin($data["nbt_hex"]); - }elseif(isset($data["nbt_b64"])){ - $nbt = base64_decode($data["nbt_b64"], true); - } - return ItemFactory::get( - (int) $data["id"], - (int) ($data["damage"] ?? 0), - (int) ($data["count"] ?? 1), - (string) $nbt - ); - } - - /** - * Serializes the item to an NBT CompoundTag - * - * @param int $slot optional, the inventory slot of the item - * @param string $tagName the name to assign to the CompoundTag object - */ - public function nbtSerialize(int $slot = -1, string $tagName = "") : CompoundTag{ - $result = new CompoundTag($tagName, [ - new ShortTag("id", $this->id), - new ByteTag("Count", Binary::signByte($this->count)), - new ShortTag("Damage", $this->meta) - ]); - - if($this->hasCompoundTag()){ - $itemNBT = clone $this->getNamedTag(); - $itemNBT->setName("tag"); - $result->setTag($itemNBT); - } - - if($slot !== -1){ - $result->setByte("Slot", $slot); - } - - return $result; - } - - /** - * Deserializes an Item from an NBT CompoundTag - */ - public static function nbtDeserialize(CompoundTag $tag) : Item{ - if(!$tag->hasTag("id") or !$tag->hasTag("Count")){ - return ItemFactory::get(0); - } - - $count = Binary::unsignByte($tag->getByte("Count")); - $meta = $tag->getShort("Damage", 0); - - $idTag = $tag->getTag("id"); - if($idTag instanceof ShortTag){ - $item = ItemFactory::get($idTag->getValue(), $meta, $count); - }elseif($idTag instanceof StringTag){ //PC item save format - try{ - $item = ItemFactory::fromStringSingle($idTag->getValue()); - }catch(\InvalidArgumentException $e){ - //TODO: improve error handling - return ItemFactory::get(Item::AIR, 0, 0); - } - $item->setDamage($meta); - $item->setCount($count); - }else{ - throw new \InvalidArgumentException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given"); - } - - $itemNBT = $tag->getCompoundTag("tag"); - if($itemNBT instanceof CompoundTag){ - /** @var CompoundTag $t */ - $t = clone $itemNBT; - $t->setName(""); - $item->setNamedTag($t); - } - - return $item; - } - - public function __clone(){ - if($this->nbt !== null){ - $this->nbt = clone $this->nbt; - } - } -} diff --git a/src/pocketmine/item/ItemFactory.php b/src/pocketmine/item/ItemFactory.php deleted file mode 100644 index 4993b454d5..0000000000 --- a/src/pocketmine/item/ItemFactory.php +++ /dev/null @@ -1,403 +0,0 @@ - - */ - private static $list; - - /** - * @return void - */ - public static function init(){ - self::$list = new \SplFixedArray(65536); - - self::registerItem(new Shovel(Item::IRON_SHOVEL, 0, "Iron Shovel", TieredTool::TIER_IRON)); - self::registerItem(new Pickaxe(Item::IRON_PICKAXE, 0, "Iron Pickaxe", TieredTool::TIER_IRON)); - self::registerItem(new Axe(Item::IRON_AXE, 0, "Iron Axe", TieredTool::TIER_IRON)); - self::registerItem(new FlintSteel()); - self::registerItem(new Apple()); - self::registerItem(new Bow()); - self::registerItem(new Arrow()); - self::registerItem(new Coal()); - self::registerItem(new Item(Item::DIAMOND, 0, "Diamond")); - self::registerItem(new Item(Item::IRON_INGOT, 0, "Iron Ingot")); - self::registerItem(new Item(Item::GOLD_INGOT, 0, "Gold Ingot")); - self::registerItem(new Sword(Item::IRON_SWORD, 0, "Iron Sword", TieredTool::TIER_IRON)); - self::registerItem(new Sword(Item::WOODEN_SWORD, 0, "Wooden Sword", TieredTool::TIER_WOODEN)); - self::registerItem(new Shovel(Item::WOODEN_SHOVEL, 0, "Wooden Shovel", TieredTool::TIER_WOODEN)); - self::registerItem(new Pickaxe(Item::WOODEN_PICKAXE, 0, "Wooden Pickaxe", TieredTool::TIER_WOODEN)); - self::registerItem(new Axe(Item::WOODEN_AXE, 0, "Wooden Axe", TieredTool::TIER_WOODEN)); - self::registerItem(new Sword(Item::STONE_SWORD, 0, "Stone Sword", TieredTool::TIER_STONE)); - self::registerItem(new Shovel(Item::STONE_SHOVEL, 0, "Stone Shovel", TieredTool::TIER_STONE)); - self::registerItem(new Pickaxe(Item::STONE_PICKAXE, 0, "Stone Pickaxe", TieredTool::TIER_STONE)); - self::registerItem(new Axe(Item::STONE_AXE, 0, "Stone Axe", TieredTool::TIER_STONE)); - self::registerItem(new Sword(Item::DIAMOND_SWORD, 0, "Diamond Sword", TieredTool::TIER_DIAMOND)); - self::registerItem(new Shovel(Item::DIAMOND_SHOVEL, 0, "Diamond Shovel", TieredTool::TIER_DIAMOND)); - self::registerItem(new Pickaxe(Item::DIAMOND_PICKAXE, 0, "Diamond Pickaxe", TieredTool::TIER_DIAMOND)); - self::registerItem(new Axe(Item::DIAMOND_AXE, 0, "Diamond Axe", TieredTool::TIER_DIAMOND)); - self::registerItem(new Stick()); - self::registerItem(new Bowl()); - self::registerItem(new MushroomStew()); - self::registerItem(new Sword(Item::GOLDEN_SWORD, 0, "Gold Sword", TieredTool::TIER_GOLD)); - self::registerItem(new Shovel(Item::GOLDEN_SHOVEL, 0, "Gold Shovel", TieredTool::TIER_GOLD)); - self::registerItem(new Pickaxe(Item::GOLDEN_PICKAXE, 0, "Gold Pickaxe", TieredTool::TIER_GOLD)); - self::registerItem(new Axe(Item::GOLDEN_AXE, 0, "Gold Axe", TieredTool::TIER_GOLD)); - self::registerItem(new StringItem()); - self::registerItem(new Item(Item::FEATHER, 0, "Feather")); - self::registerItem(new Item(Item::GUNPOWDER, 0, "Gunpowder")); - self::registerItem(new Hoe(Item::WOODEN_HOE, 0, "Wooden Hoe", TieredTool::TIER_WOODEN)); - self::registerItem(new Hoe(Item::STONE_HOE, 0, "Stone Hoe", TieredTool::TIER_STONE)); - self::registerItem(new Hoe(Item::IRON_HOE, 0, "Iron Hoe", TieredTool::TIER_IRON)); - self::registerItem(new Hoe(Item::DIAMOND_HOE, 0, "Diamond Hoe", TieredTool::TIER_DIAMOND)); - self::registerItem(new Hoe(Item::GOLDEN_HOE, 0, "Golden Hoe", TieredTool::TIER_GOLD)); - self::registerItem(new WheatSeeds()); - self::registerItem(new Item(Item::WHEAT, 0, "Wheat")); - self::registerItem(new Bread()); - self::registerItem(new LeatherCap()); - self::registerItem(new LeatherTunic()); - self::registerItem(new LeatherPants()); - self::registerItem(new LeatherBoots()); - self::registerItem(new ChainHelmet()); - self::registerItem(new ChainChestplate()); - self::registerItem(new ChainLeggings()); - self::registerItem(new ChainBoots()); - self::registerItem(new IronHelmet()); - self::registerItem(new IronChestplate()); - self::registerItem(new IronLeggings()); - self::registerItem(new IronBoots()); - self::registerItem(new DiamondHelmet()); - self::registerItem(new DiamondChestplate()); - self::registerItem(new DiamondLeggings()); - self::registerItem(new DiamondBoots()); - self::registerItem(new GoldHelmet()); - self::registerItem(new GoldChestplate()); - self::registerItem(new GoldLeggings()); - self::registerItem(new GoldBoots()); - self::registerItem(new Item(Item::FLINT, 0, "Flint")); - self::registerItem(new RawPorkchop()); - self::registerItem(new CookedPorkchop()); - self::registerItem(new PaintingItem()); - self::registerItem(new GoldenApple()); - self::registerItem(new Sign()); - self::registerItem(new ItemBlock(Block::OAK_DOOR_BLOCK, 0, Item::OAK_DOOR)); - self::registerItem(new Bucket()); - - self::registerItem(new Minecart()); - //TODO: SADDLE - self::registerItem(new ItemBlock(Block::IRON_DOOR_BLOCK, 0, Item::IRON_DOOR)); - self::registerItem(new Redstone()); - self::registerItem(new Snowball()); - self::registerItem(new Boat()); - self::registerItem(new Item(Item::LEATHER, 0, "Leather")); - //TODO: KELP - self::registerItem(new Item(Item::BRICK, 0, "Brick")); - self::registerItem(new Item(Item::CLAY_BALL, 0, "Clay")); - self::registerItem(new ItemBlock(Block::SUGARCANE_BLOCK, 0, Item::SUGARCANE)); - self::registerItem(new Item(Item::PAPER, 0, "Paper")); - self::registerItem(new Book()); - self::registerItem(new Item(Item::SLIME_BALL, 0, "Slimeball")); - //TODO: CHEST_MINECART - - self::registerItem(new Egg()); - self::registerItem(new Compass()); - self::registerItem(new FishingRod()); - self::registerItem(new Clock()); - self::registerItem(new Item(Item::GLOWSTONE_DUST, 0, "Glowstone Dust")); - self::registerItem(new RawFish()); - self::registerItem(new CookedFish()); - self::registerItem(new Dye()); - self::registerItem(new Item(Item::BONE, 0, "Bone")); - self::registerItem(new Item(Item::SUGAR, 0, "Sugar")); - self::registerItem(new ItemBlock(Block::CAKE_BLOCK, 0, Item::CAKE)); - self::registerItem(new Bed()); - self::registerItem(new ItemBlock(Block::REPEATER_BLOCK, 0, Item::REPEATER)); - self::registerItem(new Cookie()); - //TODO: FILLED_MAP - self::registerItem(new Shears()); - self::registerItem(new Melon()); - self::registerItem(new PumpkinSeeds()); - self::registerItem(new MelonSeeds()); - self::registerItem(new RawBeef()); - self::registerItem(new Steak()); - self::registerItem(new RawChicken()); - self::registerItem(new CookedChicken()); - self::registerItem(new RottenFlesh()); - self::registerItem(new EnderPearl()); - self::registerItem(new BlazeRod()); - self::registerItem(new Item(Item::GHAST_TEAR, 0, "Ghast Tear")); - self::registerItem(new Item(Item::GOLD_NUGGET, 0, "Gold Nugget")); - self::registerItem(new ItemBlock(Block::NETHER_WART_PLANT, 0, Item::NETHER_WART)); - self::registerItem(new Potion()); - self::registerItem(new GlassBottle()); - self::registerItem(new SpiderEye()); - self::registerItem(new Item(Item::FERMENTED_SPIDER_EYE, 0, "Fermented Spider Eye")); - self::registerItem(new Item(Item::BLAZE_POWDER, 0, "Blaze Powder")); - self::registerItem(new Item(Item::MAGMA_CREAM, 0, "Magma Cream")); - self::registerItem(new ItemBlock(Block::BREWING_STAND_BLOCK, 0, Item::BREWING_STAND)); - self::registerItem(new ItemBlock(Block::CAULDRON_BLOCK, 0, Item::CAULDRON)); - //TODO: ENDER_EYE - self::registerItem(new Item(Item::GLISTERING_MELON, 0, "Glistering Melon")); - self::registerItem(new SpawnEgg()); - self::registerItem(new ExperienceBottle()); - //TODO: FIREBALL - self::registerItem(new WritableBook()); - self::registerItem(new WrittenBook()); - self::registerItem(new Item(Item::EMERALD, 0, "Emerald")); - self::registerItem(new ItemBlock(Block::ITEM_FRAME_BLOCK, 0, Item::ITEM_FRAME)); - self::registerItem(new ItemBlock(Block::FLOWER_POT_BLOCK, 0, Item::FLOWER_POT)); - self::registerItem(new Carrot()); - self::registerItem(new Potato()); - self::registerItem(new BakedPotato()); - self::registerItem(new PoisonousPotato()); - //TODO: EMPTYMAP - self::registerItem(new GoldenCarrot()); - self::registerItem(new ItemBlock(Block::SKULL_BLOCK, 0, Item::SKULL)); - //TODO: CARROTONASTICK - self::registerItem(new Item(Item::NETHER_STAR, 0, "Nether Star")); - self::registerItem(new PumpkinPie()); - //TODO: FIREWORKS - //TODO: FIREWORKSCHARGE - //TODO: ENCHANTED_BOOK - self::registerItem(new ItemBlock(Block::COMPARATOR_BLOCK, 0, Item::COMPARATOR)); - self::registerItem(new Item(Item::NETHER_BRICK, 0, "Nether Brick")); - self::registerItem(new Item(Item::NETHER_QUARTZ, 0, "Nether Quartz")); - //TODO: MINECART_WITH_TNT - //TODO: HOPPER_MINECART - self::registerItem(new Item(Item::PRISMARINE_SHARD, 0, "Prismarine Shard")); - self::registerItem(new ItemBlock(Block::HOPPER_BLOCK, 0, Item::HOPPER)); - self::registerItem(new RawRabbit()); - self::registerItem(new CookedRabbit()); - self::registerItem(new RabbitStew()); - self::registerItem(new Item(Item::RABBIT_FOOT, 0, "Rabbit's Foot")); - self::registerItem(new Item(Item::RABBIT_HIDE, 0, "Rabbit Hide")); - //TODO: HORSEARMORLEATHER - //TODO: HORSEARMORIRON - //TODO: GOLD_HORSE_ARMOR - //TODO: DIAMOND_HORSE_ARMOR - //TODO: LEAD - //TODO: NAMETAG - self::registerItem(new Item(Item::PRISMARINE_CRYSTALS, 0, "Prismarine Crystals")); - self::registerItem(new RawMutton()); - self::registerItem(new CookedMutton()); - //TODO: ARMOR_STAND - //TODO: END_CRYSTAL - self::registerItem(new ItemBlock(Block::SPRUCE_DOOR_BLOCK, 0, Item::SPRUCE_DOOR)); - self::registerItem(new ItemBlock(Block::BIRCH_DOOR_BLOCK, 0, Item::BIRCH_DOOR)); - self::registerItem(new ItemBlock(Block::JUNGLE_DOOR_BLOCK, 0, Item::JUNGLE_DOOR)); - self::registerItem(new ItemBlock(Block::ACACIA_DOOR_BLOCK, 0, Item::ACACIA_DOOR)); - self::registerItem(new ItemBlock(Block::DARK_OAK_DOOR_BLOCK, 0, Item::DARK_OAK_DOOR)); - self::registerItem(new ChorusFruit()); - self::registerItem(new Item(Item::CHORUS_FRUIT_POPPED, 0, "Popped Chorus Fruit")); - - self::registerItem(new Item(Item::DRAGON_BREATH, 0, "Dragon's Breath")); - self::registerItem(new SplashPotion()); - - //TODO: LINGERING_POTION - //TODO: SPARKLER - //TODO: COMMAND_BLOCK_MINECART - //TODO: ELYTRA - self::registerItem(new Item(Item::SHULKER_SHELL, 0, "Shulker Shell")); - self::registerItem(new Banner()); - //TODO: MEDICINE - //TODO: BALLOON - //TODO: RAPID_FERTILIZER - self::registerItem(new Totem()); - self::registerItem(new Item(Item::BLEACH, 0, "Bleach")); //EDU - self::registerItem(new Item(Item::IRON_NUGGET, 0, "Iron Nugget")); - //TODO: ICE_BOMB - - //TODO: TRIDENT - - self::registerItem(new Beetroot()); - self::registerItem(new BeetrootSeeds()); - self::registerItem(new BeetrootSoup()); - self::registerItem(new RawSalmon()); - self::registerItem(new Clownfish()); - self::registerItem(new Pufferfish()); - self::registerItem(new CookedSalmon()); - self::registerItem(new DriedKelp()); - self::registerItem(new Item(Item::NAUTILUS_SHELL, 0, "Nautilus Shell")); - self::registerItem(new GoldenAppleEnchanted()); - self::registerItem(new Item(Item::HEART_OF_THE_SEA, 0, "Heart of the Sea")); - self::registerItem(new Item(Item::TURTLE_SHELL_PIECE, 0, "Scute")); - //TODO: TURTLE_HELMET - - //TODO: COMPOUND - //TODO: RECORD_13 - //TODO: RECORD_CAT - //TODO: RECORD_BLOCKS - //TODO: RECORD_CHIRP - //TODO: RECORD_FAR - //TODO: RECORD_MALL - //TODO: RECORD_MELLOHI - //TODO: RECORD_STAL - //TODO: RECORD_STRAD - //TODO: RECORD_WARD - //TODO: RECORD_11 - //TODO: RECORD_WAIT - } - - /** - * Registers an item type into the index. Plugins may use this method to register new item types or override existing - * ones. - * - * NOTE: If you are registering a new item type, you will need to add it to the creative inventory yourself - it - * will not automatically appear there. - * - * @return void - * @throws \RuntimeException if something attempted to override an already-registered item without specifying the - * $override parameter. - */ - public static function registerItem(Item $item, bool $override = false){ - $id = $item->getId(); - if(!$override and self::isRegistered($id)){ - throw new \RuntimeException("Trying to overwrite an already registered item"); - } - - self::$list[self::getListOffset($id)] = clone $item; - } - - /** - * Returns an instance of the Item with the specified id, meta, count and NBT. - * - * @param CompoundTag|string|null $tags - * - * @throws \TypeError - */ - public static function get(int $id, int $meta = 0, int $count = 1, $tags = null) : Item{ - if(!is_string($tags) and !($tags instanceof CompoundTag) and $tags !== null){ - throw new \TypeError("`tags` argument must be a string or CompoundTag instance, " . (is_object($tags) ? "instance of " . get_class($tags) : gettype($tags)) . " given"); - } - - try{ - /** @var Item|null $listed */ - $listed = self::$list[self::getListOffset($id)]; - if($listed !== null){ - $item = clone $listed; - }elseif($id >= 0 and $id < 256){ //intentionally excludes negatives because extended blocks aren't supported yet - /* Blocks must have a damage value 0-15, but items can have damage value -1 to indicate that they are - * crafting ingredients with any-damage. */ - $item = new ItemBlock($id, $meta); - }else{ - $item = new Item($id, $meta); - } - }catch(\RuntimeException $e){ - throw new \InvalidArgumentException("Item ID $id is invalid or out of bounds"); - } - - $item->setDamage($meta); - $item->setCount($count); - $item->setCompoundTag($tags); - return $item; - } - - /** - * Tries to parse the specified string into Item ID/meta identifiers, and returns Item instances it created. - * - * Example accepted formats: - * - `diamond_pickaxe:5` - * - `minecraft:string` - * - `351:4 (lapis lazuli ID:meta)` - * - * If multiple item instances are to be created, their identifiers must be comma-separated, for example: - * `diamond_pickaxe,wooden_shovel:18,iron_ingot` - * - * @return Item[]|Item - * - * @throws \InvalidArgumentException if the given string cannot be parsed as an item identifier - */ - public static function fromString(string $str, bool $multiple = false){ - if($multiple){ - $blocks = []; - foreach(explode(",", $str) as $b){ - $blocks[] = self::fromStringSingle($b); - } - - return $blocks; - }else{ - return self::fromStringSingle($str); - } - } - - public static function fromStringSingle(string $str) : Item{ - $b = explode(":", str_replace([" ", "minecraft:"], ["_", ""], trim($str))); - if(!isset($b[1])){ - $meta = 0; - }elseif(is_numeric($b[1])){ - $meta = (int) $b[1]; - }else{ - throw new \InvalidArgumentException("Unable to parse \"" . $b[1] . "\" from \"" . $str . "\" as a valid meta value"); - } - - if(is_numeric($b[0])){ - $item = self::get((int) $b[0], $meta); - }elseif(defined(ItemIds::class . "::" . mb_strtoupper($b[0]))){ - $item = self::get(constant(ItemIds::class . "::" . mb_strtoupper($b[0])), $meta); - }else{ - throw new \InvalidArgumentException("Unable to resolve \"" . $str . "\" to a valid item"); - } - - return $item; - } - - /** - * Returns whether the specified item ID is already registered in the item factory. - */ - public static function isRegistered(int $id) : bool{ - if($id < 256){ - return BlockFactory::isRegistered($id); - } - return self::$list[self::getListOffset($id)] !== null; - } - - private static function getListOffset(int $id) : int{ - if($id < -0x8000 or $id > 0x7fff){ - throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); - } - return $id & 0xffff; - } -} diff --git a/src/pocketmine/item/ItemIds.php b/src/pocketmine/item/ItemIds.php deleted file mode 100644 index 4adcb1d7c9..0000000000 --- a/src/pocketmine/item/ItemIds.php +++ /dev/null @@ -1,266 +0,0 @@ -meta); - } - - public function getResidue(){ - return ItemFactory::get(Item::GLASS_BOTTLE); - } -} diff --git a/src/pocketmine/item/ProjectileItem.php b/src/pocketmine/item/ProjectileItem.php deleted file mode 100644 index fccc4c3bf0..0000000000 --- a/src/pocketmine/item/ProjectileItem.php +++ /dev/null @@ -1,77 +0,0 @@ -add(0, $player->getEyeHeight(), 0), $directionVector, $player->yaw, $player->pitch); - $this->addExtraTags($nbt); - - $projectile = Entity::createEntity($this->getProjectileEntityType(), $player->getLevelNonNull(), $nbt, $player); - if($projectile !== null){ - $projectile->setMotion($projectile->getMotion()->multiply($this->getThrowForce())); - } - - $this->pop(); - - if($projectile instanceof Projectile){ - $projectileEv = new ProjectileLaunchEvent($projectile); - $projectileEv->call(); - if($projectileEv->isCancelled()){ - $projectile->flagForDespawn(); - }else{ - $projectile->spawnToAll(); - - $player->getLevelNonNull()->broadcastLevelSoundEvent($player, LevelSoundEventPacket::SOUND_THROW, 0, EntityIds::PLAYER); - } - }elseif($projectile !== null){ - $projectile->spawnToAll(); - }else{ - return false; - } - - return true; - } -} diff --git a/src/pocketmine/item/TieredTool.php b/src/pocketmine/item/TieredTool.php deleted file mode 100644 index 7fa2898895..0000000000 --- a/src/pocketmine/item/TieredTool.php +++ /dev/null @@ -1,108 +0,0 @@ -tier = $tier; - } - - public function getMaxDurability() : int{ - return self::getDurabilityFromTier($this->tier); - } - - public function getTier() : int{ - return $this->tier; - } - - public static function getDurabilityFromTier(int $tier) : int{ - static $levels = [ - self::TIER_GOLD => 33, - self::TIER_WOODEN => 60, - self::TIER_STONE => 132, - self::TIER_IRON => 251, - self::TIER_DIAMOND => 1562 - ]; - - if(!isset($levels[$tier])){ - throw new \InvalidArgumentException("Unknown tier '$tier'"); - } - - return $levels[$tier]; - } - - protected static function getBaseDamageFromTier(int $tier) : int{ - static $levels = [ - self::TIER_WOODEN => 5, - self::TIER_GOLD => 5, - self::TIER_STONE => 6, - self::TIER_IRON => 7, - self::TIER_DIAMOND => 8 - ]; - - if(!isset($levels[$tier])){ - throw new \InvalidArgumentException("Unknown tier '$tier'"); - } - - return $levels[$tier]; - } - - public static function getBaseMiningEfficiencyFromTier(int $tier) : float{ - static $levels = [ - self::TIER_WOODEN => 2, - self::TIER_STONE => 4, - self::TIER_IRON => 6, - self::TIER_DIAMOND => 8, - self::TIER_GOLD => 12 - ]; - - if(!isset($levels[$tier])){ - throw new \InvalidArgumentException("Unknown tier '$tier'"); - } - - return $levels[$tier]; - } - - protected function getBaseMiningEfficiency() : float{ - return self::getBaseMiningEfficiencyFromTier($this->tier); - } - - public function getFuelTime() : int{ - if($this->tier === self::TIER_WOODEN){ - return 200; - } - - return 0; - } -} diff --git a/src/pocketmine/item/WritableBook.php b/src/pocketmine/item/WritableBook.php deleted file mode 100644 index 22731aba40..0000000000 --- a/src/pocketmine/item/WritableBook.php +++ /dev/null @@ -1,190 +0,0 @@ - - public const TAG_PAGE_TEXT = "text"; //TAG_String - public const TAG_PAGE_PHOTONAME = "photoname"; //TAG_String - TODO - - public function __construct(int $meta = 0){ - parent::__construct(self::WRITABLE_BOOK, $meta, "Book & Quill"); - } - - /** - * Returns whether the given page exists in this book. - */ - public function pageExists(int $pageId) : bool{ - return $this->getPagesTag()->isset($pageId); - } - - /** - * Returns a string containing the content of a page (which could be empty), or null if the page doesn't exist. - */ - public function getPageText(int $pageId) : ?string{ - $pages = $this->getNamedTag()->getListTag(self::TAG_PAGES); - if($pages === null or !$pages->isset($pageId)){ - return null; - } - - $page = $pages->get($pageId); - if($page instanceof CompoundTag){ - return $page->getString(self::TAG_PAGE_TEXT, ""); - } - - return null; - } - - /** - * Sets the text of a page in the book. Adds the page if the page does not yet exist. - * - * @return bool indicating whether the page was created or not. - */ - public function setPageText(int $pageId, string $pageText) : bool{ - $created = false; - if(!$this->pageExists($pageId)){ - $this->addPage($pageId); - $created = true; - } - - /** @var CompoundTag[]|ListTag $pagesTag */ - $pagesTag = $this->getPagesTag(); - /** @var CompoundTag $page */ - $page = $pagesTag->get($pageId); - $page->setString(self::TAG_PAGE_TEXT, $pageText); - - $this->setNamedTagEntry($pagesTag); - - return $created; - } - - /** - * Adds a new page with the given page ID. - * Creates a new page for every page between the given ID and existing pages that doesn't yet exist. - */ - public function addPage(int $pageId) : void{ - if($pageId < 0){ - throw new \InvalidArgumentException("Page number \"$pageId\" is out of range"); - } - - $pagesTag = $this->getPagesTag(); - - for($current = $pagesTag->count(); $current <= $pageId; $current++){ - $pagesTag->push(new CompoundTag("", [ - new StringTag(self::TAG_PAGE_TEXT, ""), - new StringTag(self::TAG_PAGE_PHOTONAME, "") - ])); - } - - $this->setNamedTagEntry($pagesTag); - } - - /** - * Deletes an existing page with the given page ID. - * - * @return bool indicating success - */ - public function deletePage(int $pageId) : bool{ - $pagesTag = $this->getPagesTag(); - $pagesTag->remove($pageId); - $this->setNamedTagEntry($pagesTag); - - return true; - } - - /** - * Inserts a new page with the given text and moves other pages upwards. - * - * @return bool indicating success - */ - public function insertPage(int $pageId, string $pageText = "") : bool{ - $pagesTag = $this->getPagesTag(); - - $pagesTag->insert($pageId, new CompoundTag("", [ - new StringTag(self::TAG_PAGE_TEXT, $pageText), - new StringTag(self::TAG_PAGE_PHOTONAME, "") - ])); - - $this->setNamedTagEntry($pagesTag); - - return true; - } - - /** - * Switches the text of two pages with each other. - * - * @return bool indicating success - */ - public function swapPages(int $pageId1, int $pageId2) : bool{ - if(!$this->pageExists($pageId1) or !$this->pageExists($pageId2)){ - return false; - } - - $pageContents1 = $this->getPageText($pageId1); - $pageContents2 = $this->getPageText($pageId2); - $this->setPageText($pageId1, $pageContents2); - $this->setPageText($pageId2, $pageContents1); - - return true; - } - - public function getMaxStackSize() : int{ - return 1; - } - - /** - * Returns an array containing all pages of this book. - * - * @return CompoundTag[] - */ - public function getPages() : array{ - /** @var CompoundTag[] $pages */ - $pages = $this->getPagesTag()->getValue(); - - return $pages; - } - - protected function getPagesTag() : ListTag{ - $pagesTag = $this->getNamedTag()->getListTag(self::TAG_PAGES); - if($pagesTag !== null and $pagesTag->getTagType() === NBT::TAG_Compound){ - return $pagesTag; - } - return new ListTag(self::TAG_PAGES, [], NBT::TAG_Compound); - } - - /** - * @param CompoundTag[] $pages - */ - public function setPages(array $pages) : void{ - $nbt = $this->getNamedTag(); - $nbt->setTag(new ListTag(self::TAG_PAGES, $pages, NBT::TAG_Compound)); - $this->setNamedTag($nbt); - } -} diff --git a/src/pocketmine/item/enchantment/Enchantment.php b/src/pocketmine/item/enchantment/Enchantment.php deleted file mode 100644 index 3775d7cfe8..0000000000 --- a/src/pocketmine/item/enchantment/Enchantment.php +++ /dev/null @@ -1,252 +0,0 @@ - - */ - protected static $enchantments; - - public static function init() : void{ - self::$enchantments = new \SplFixedArray(256); - - self::registerEnchantment(new ProtectionEnchantment(self::PROTECTION, "%enchantment.protect.all", self::RARITY_COMMON, self::SLOT_ARMOR, self::SLOT_NONE, 4, 0.75, null)); - self::registerEnchantment(new ProtectionEnchantment(self::FIRE_PROTECTION, "%enchantment.protect.fire", self::RARITY_UNCOMMON, self::SLOT_ARMOR, self::SLOT_NONE, 4, 1.25, [ - EntityDamageEvent::CAUSE_FIRE, - EntityDamageEvent::CAUSE_FIRE_TICK, - EntityDamageEvent::CAUSE_LAVA - //TODO: check fireballs - ])); - self::registerEnchantment(new ProtectionEnchantment(self::FEATHER_FALLING, "%enchantment.protect.fall", self::RARITY_UNCOMMON, self::SLOT_FEET, self::SLOT_NONE, 4, 2.5, [ - EntityDamageEvent::CAUSE_FALL - ])); - self::registerEnchantment(new ProtectionEnchantment(self::BLAST_PROTECTION, "%enchantment.protect.explosion", self::RARITY_RARE, self::SLOT_ARMOR, self::SLOT_NONE, 4, 1.5, [ - EntityDamageEvent::CAUSE_BLOCK_EXPLOSION, - EntityDamageEvent::CAUSE_ENTITY_EXPLOSION - ])); - self::registerEnchantment(new ProtectionEnchantment(self::PROJECTILE_PROTECTION, "%enchantment.protect.projectile", self::RARITY_UNCOMMON, self::SLOT_ARMOR, self::SLOT_NONE, 4, 1.5, [ - EntityDamageEvent::CAUSE_PROJECTILE - ])); - self::registerEnchantment(new Enchantment(self::THORNS, "%enchantment.thorns", self::RARITY_MYTHIC, self::SLOT_TORSO, self::SLOT_HEAD | self::SLOT_LEGS | self::SLOT_FEET, 3)); - self::registerEnchantment(new Enchantment(self::RESPIRATION, "%enchantment.oxygen", self::RARITY_RARE, self::SLOT_HEAD, self::SLOT_NONE, 3)); - - self::registerEnchantment(new SharpnessEnchantment(self::SHARPNESS, "%enchantment.damage.all", self::RARITY_COMMON, self::SLOT_SWORD, self::SLOT_AXE, 5)); - //TODO: smite, bane of arthropods (these don't make sense now because their applicable mobs don't exist yet) - - self::registerEnchantment(new KnockbackEnchantment(self::KNOCKBACK, "%enchantment.knockback", self::RARITY_UNCOMMON, self::SLOT_SWORD, self::SLOT_NONE, 2)); - self::registerEnchantment(new FireAspectEnchantment(self::FIRE_ASPECT, "%enchantment.fire", self::RARITY_RARE, self::SLOT_SWORD, self::SLOT_NONE, 2)); - - self::registerEnchantment(new Enchantment(self::EFFICIENCY, "%enchantment.digging", self::RARITY_COMMON, self::SLOT_DIG, self::SLOT_SHEARS, 5)); - self::registerEnchantment(new Enchantment(self::SILK_TOUCH, "%enchantment.untouching", self::RARITY_MYTHIC, self::SLOT_DIG, self::SLOT_SHEARS, 1)); - self::registerEnchantment(new Enchantment(self::UNBREAKING, "%enchantment.durability", self::RARITY_UNCOMMON, self::SLOT_DIG | self::SLOT_ARMOR | self::SLOT_FISHING_ROD | self::SLOT_BOW, self::SLOT_TOOL | self::SLOT_CARROT_STICK | self::SLOT_ELYTRA, 3)); - - self::registerEnchantment(new Enchantment(self::POWER, "%enchantment.arrowDamage", self::RARITY_COMMON, self::SLOT_BOW, self::SLOT_NONE, 5)); - self::registerEnchantment(new Enchantment(self::PUNCH, "%enchantment.arrowKnockback", self::RARITY_RARE, self::SLOT_BOW, self::SLOT_NONE, 2)); - self::registerEnchantment(new Enchantment(self::FLAME, "%enchantment.arrowFire", self::RARITY_RARE, self::SLOT_BOW, self::SLOT_NONE, 1)); - self::registerEnchantment(new Enchantment(self::INFINITY, "%enchantment.arrowInfinite", self::RARITY_MYTHIC, self::SLOT_BOW, self::SLOT_NONE, 1)); - - self::registerEnchantment(new Enchantment(self::MENDING, "%enchantment.mending", self::RARITY_RARE, self::SLOT_NONE, self::SLOT_ALL, 1)); - - self::registerEnchantment(new Enchantment(self::VANISHING, "%enchantment.curse.vanishing", self::RARITY_MYTHIC, self::SLOT_NONE, self::SLOT_ALL, 1)); - } - - /** - * Registers an enchantment type. - */ - public static function registerEnchantment(Enchantment $enchantment) : void{ - self::$enchantments[$enchantment->getId()] = clone $enchantment; - } - - public static function getEnchantment(int $id) : ?Enchantment{ - if($id < 0 or $id >= self::$enchantments->getSize()){ - return null; - } - return self::$enchantments[$id] ?? null; - } - - public static function getEnchantmentByName(string $name) : ?Enchantment{ - $const = Enchantment::class . "::" . mb_strtoupper($name); - if(defined($const)){ - return self::getEnchantment(constant($const)); - } - return null; - } - - /** @var int */ - private $id; - /** @var string */ - private $name; - /** @var int */ - private $rarity; - /** @var int */ - private $primaryItemFlags; - /** @var int */ - private $secondaryItemFlags; - /** @var int */ - private $maxLevel; - - public function __construct(int $id, string $name, int $rarity, int $primaryItemFlags, int $secondaryItemFlags, int $maxLevel){ - $this->id = $id; - $this->name = $name; - $this->rarity = $rarity; - $this->primaryItemFlags = $primaryItemFlags; - $this->secondaryItemFlags = $secondaryItemFlags; - $this->maxLevel = $maxLevel; - } - - /** - * Returns the ID of this enchantment as per Minecraft PE - */ - public function getId() : int{ - return $this->id; - } - - /** - * Returns a translation key for this enchantment's name. - */ - public function getName() : string{ - return $this->name; - } - - /** - * Returns an int constant indicating how rare this enchantment type is. - */ - public function getRarity() : int{ - return $this->rarity; - } - - /** - * Returns a bitset indicating what item types can have this item applied from an enchanting table. - */ - public function getPrimaryItemFlags() : int{ - return $this->primaryItemFlags; - } - - /** - * Returns a bitset indicating what item types cannot have this item applied from an enchanting table, but can from - * an anvil. - */ - public function getSecondaryItemFlags() : int{ - return $this->secondaryItemFlags; - } - - /** - * Returns whether this enchantment can apply to the item type from an enchanting table. - */ - public function hasPrimaryItemType(int $flag) : bool{ - return ($this->primaryItemFlags & $flag) !== 0; - } - - /** - * Returns whether this enchantment can apply to the item type from an anvil, if it is not a primary item. - */ - public function hasSecondaryItemType(int $flag) : bool{ - return ($this->secondaryItemFlags & $flag) !== 0; - } - - /** - * Returns the maximum level of this enchantment that can be found on an enchantment table. - */ - public function getMaxLevel() : int{ - return $this->maxLevel; - } - - //TODO: methods for min/max XP cost bounds based on enchantment level (not needed yet - enchanting is client-side) -} diff --git a/src/pocketmine/item/enchantment/EnchantmentList.php b/src/pocketmine/item/enchantment/EnchantmentList.php deleted file mode 100644 index 103d3c1a63..0000000000 --- a/src/pocketmine/item/enchantment/EnchantmentList.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ - private $enchantments; - - public function __construct(int $size){ - $this->enchantments = new \SplFixedArray($size); - } - - public function setSlot(int $slot, EnchantmentEntry $entry) : void{ - $this->enchantments[$slot] = $entry; - } - - public function getSlot(int $slot) : EnchantmentEntry{ - return $this->enchantments[$slot]; - } - - public function getSize() : int{ - return $this->enchantments->getSize(); - } -} diff --git a/src/pocketmine/lang/locale b/src/pocketmine/lang/locale deleted file mode 160000 index c85a7b79f3..0000000000 --- a/src/pocketmine/lang/locale +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c85a7b79f35a3e4d55b85656dfd90be2864ebc8a diff --git a/src/pocketmine/level/ChunkLoader.php b/src/pocketmine/level/ChunkLoader.php deleted file mode 100644 index da3baff0b8..0000000000 --- a/src/pocketmine/level/ChunkLoader.php +++ /dev/null @@ -1,111 +0,0 @@ -registerChunkLoader($this, $chunkX, $chunkZ) - * Unregister Level->unregisterChunkLoader($this, $chunkX, $chunkZ) - * - * WARNING: When moving this object around in the world or destroying it, - * be sure to free the existing references from Level, otherwise you'll leak memory. - */ -interface ChunkLoader{ - - /** - * Returns the ChunkLoader id. - * Call Level::generateChunkLoaderId($this) to generate and save it - */ - public function getLoaderId() : int; - - /** - * Returns if the chunk loader is currently active - */ - public function isLoaderActive() : bool; - - /** - * @return Position - */ - public function getPosition(); - - /** - * @return float - */ - public function getX(); - - /** - * @return float - */ - public function getZ(); - - /** - * @return Level - */ - public function getLevel(); - - /** - * This method will be called when a Chunk is replaced by a new one - * - * @return void - */ - public function onChunkChanged(Chunk $chunk); - - /** - * This method will be called when a registered chunk is loaded - * - * @return void - */ - public function onChunkLoaded(Chunk $chunk); - - /** - * This method will be called when a registered chunk is unloaded - * - * @return void - */ - public function onChunkUnloaded(Chunk $chunk); - - /** - * This method will be called when a registered chunk is populated - * Usually it'll be sent with another call to onChunkChanged() - * - * @return void - */ - public function onChunkPopulated(Chunk $chunk); - - /** - * This method will be called when a block changes in a registered chunk - * - * @param Block|Vector3 $block - * - * @return void - */ - public function onBlockChanged(Vector3 $block); - -} diff --git a/src/pocketmine/level/ChunkManager.php b/src/pocketmine/level/ChunkManager.php deleted file mode 100644 index ea526e0bcc..0000000000 --- a/src/pocketmine/level/ChunkManager.php +++ /dev/null @@ -1,110 +0,0 @@ - - -class Level implements ChunkManager, Metadatable{ - - /** @var int */ - private static $levelIdCounter = 1; - /** @var int */ - private static $chunkLoaderCounter = 1; - - public const Y_MASK = 0xFF; - public const Y_MAX = 0x100; //256 - - public const TIME_DAY = 1000; - public const TIME_NOON = 6000; - public const TIME_SUNSET = 12000; - public const TIME_NIGHT = 13000; - public const TIME_MIDNIGHT = 18000; - public const TIME_SUNRISE = 23000; - - public const TIME_FULL = 24000; - - public const DIFFICULTY_PEACEFUL = 0; - public const DIFFICULTY_EASY = 1; - public const DIFFICULTY_NORMAL = 2; - public const DIFFICULTY_HARD = 3; - - /** @var Tile[] */ - private $tiles = []; - - /** @var Player[] */ - private $players = []; - - /** @var Entity[] */ - private $entities = []; - - /** @var Entity[] */ - public $updateEntities = []; - /** @var Tile[] */ - public $updateTiles = []; - /** @var Block[][] */ - private $blockCache = []; - - /** @var BatchPacket[] */ - private $chunkCache = []; - - /** @var int */ - private $sendTimeTicker = 0; - - /** @var Server */ - private $server; - - /** @var int */ - private $levelId; - - /** @var LevelProvider */ - private $provider; - /** @var int */ - private $providerGarbageCollectionTicker = 0; - - /** @var int */ - private $worldHeight; - - /** @var ChunkLoader[] */ - private $loaders = []; - /** @var int[] */ - private $loaderCounter = []; - /** @var ChunkLoader[][] */ - private $chunkLoaders = []; - /** @var Player[][] */ - private $playerLoaders = []; - - /** @var DataPacket[][] */ - private $chunkPackets = []; - /** @var DataPacket[] */ - private $globalPackets = []; - - /** @var float[] */ - private $unloadQueue = []; - - /** @var int */ - private $time; - /** @var bool */ - public $stopTime = false; - - /** @var float */ - private $sunAnglePercentage = 0.0; - /** @var int */ - private $skyLightReduction = 0; - - /** @var string */ - private $folderName; - /** @var string */ - private $displayName; - - /** @var Chunk[] */ - private $chunks = []; - - /** @var Vector3[][] */ - private $changedBlocks = []; - - /** - * @var ReversePriorityQueue - * @phpstan-var ReversePriorityQueue - */ - private $scheduledBlockUpdateQueue; - /** @var int[] */ - private $scheduledBlockUpdateQueueIndex = []; - - /** - * @var \SplQueue - * @phpstan-var \SplQueue - */ - private $neighbourBlockUpdateQueue; - - /** @var Player[][] */ - private $chunkSendQueue = []; - /** @var ChunkRequestTask[] */ - private $chunkSendTasks = []; - - /** @var bool[] */ - private $chunkPopulationQueue = []; - /** @var bool[] */ - private $chunkPopulationLock = []; - /** @var int */ - private $chunkPopulationQueueSize = 2; - /** @var bool[] */ - private $generatorRegisteredWorkers = []; - - /** @var bool */ - private $autoSave = true; - - /** @var BlockMetadataStore */ - private $blockMetadata; - - /** @var Position */ - private $temporalPosition; - /** @var Vector3 */ - private $temporalVector; - - /** - * @var \SplFixedArray - * @phpstan-var \SplFixedArray - */ - private $blockStates; - - /** @var int */ - private $sleepTicks = 0; - - /** @var int */ - private $chunkTickRadius; - /** @var int[] */ - private $chunkTickList = []; - /** @var int */ - private $chunksPerTick; - /** @var bool */ - private $clearChunksOnTick; - /** @var \SplFixedArray */ - private $randomTickBlocks; - - /** @var LevelTimings */ - public $timings; - - /** @var float */ - public $tickRateTime = 0; - /** - * @deprecated - * @var int - */ - public $tickRateCounter = 0; - - /** @var bool */ - private $doingTick = false; - - /** - * @var string - * @phpstan-var class-string<\pocketmine\level\generator\Generator> - */ - private $generator; - - /** @var bool */ - private $closed = false; - - /** @var BlockLightUpdate|null */ - private $blockLightUpdate = null; - /** @var SkyLightUpdate|null */ - private $skyLightUpdate = null; - - public static function chunkHash(int $x, int $z) : int{ - return (($x & 0xFFFFFFFF) << 32) | ($z & 0xFFFFFFFF); - } - - public static function blockHash(int $x, int $y, int $z) : int{ - if($y < 0 or $y >= Level::Y_MAX){ - throw new \InvalidArgumentException("Y coordinate $y is out of range!"); - } - return (($x & 0xFFFFFFF) << 36) | (($y & Level::Y_MASK) << 28) | ($z & 0xFFFFFFF); - } - - /** - * Computes a small index relative to chunk base from the given coordinates. - */ - public static function chunkBlockHash(int $x, int $y, int $z) : int{ - return ($y << 8) | (($z & 0xf) << 4) | ($x & 0xf); - } - - public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{ - $x = $hash >> 36; - $y = ($hash >> 28) & Level::Y_MASK; //it's always positive - $z = ($hash & 0xFFFFFFF) << 36 >> 36; - } - - public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{ - $x = $hash >> 32; - $z = ($hash & 0xFFFFFFFF) << 32 >> 32; - } - - public static function generateChunkLoaderId(ChunkLoader $loader) : int{ - if($loader->getLoaderId() === 0){ - return self::$chunkLoaderCounter++; - }else{ - throw new \InvalidStateException("ChunkLoader has a loader id already assigned: " . $loader->getLoaderId()); - } - } - - public static function getDifficultyFromString(string $str) : int{ - switch(strtolower(trim($str))){ - case "0": - case "peaceful": - case "p": - return Level::DIFFICULTY_PEACEFUL; - - case "1": - case "easy": - case "e": - return Level::DIFFICULTY_EASY; - - case "2": - case "normal": - case "n": - return Level::DIFFICULTY_NORMAL; - - case "3": - case "hard": - case "h": - return Level::DIFFICULTY_HARD; - } - - return -1; - } - - /** - * Init the default level data - */ - public function __construct(Server $server, string $name, LevelProvider $provider){ - $this->blockStates = BlockFactory::getBlockStatesArray(); - $this->levelId = self::$levelIdCounter++; - $this->blockMetadata = new BlockMetadataStore($this); - $this->server = $server; - $this->autoSave = $server->getAutoSave(); - - $this->provider = $provider; - - $this->displayName = $this->provider->getName(); - $this->worldHeight = $this->provider->getWorldHeight(); - - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.preparing", [$this->displayName])); - $this->generator = GeneratorManager::getGenerator($this->provider->getGenerator(), true); - //TODO: validate generator options - - $this->folderName = $name; - - $this->scheduledBlockUpdateQueue = new ReversePriorityQueue(); - $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); - - $this->neighbourBlockUpdateQueue = new \SplQueue(); - - $this->time = $this->provider->getTime(); - - $this->chunkTickRadius = min($this->server->getViewDistance(), max(1, (int) $this->server->getProperty("chunk-ticking.tick-radius", 4))); - $this->chunksPerTick = (int) $this->server->getProperty("chunk-ticking.per-tick", 40); - $this->chunkPopulationQueueSize = (int) $this->server->getProperty("chunk-generation.population-queue-size", 2); - $this->clearChunksOnTick = (bool) $this->server->getProperty("chunk-ticking.clear-tick-list", true); - - $dontTickBlocks = array_fill_keys($this->server->getProperty("chunk-ticking.disable-block-ticking", []), true); - - $this->randomTickBlocks = new \SplFixedArray(256); - foreach($this->randomTickBlocks as $id => $null){ - $block = BlockFactory::get($id); //Make sure it's a copy - if(!isset($dontTickBlocks[$id]) and $block->ticksRandomly()){ - $this->randomTickBlocks[$id] = $block; - } - } - - $this->timings = new LevelTimings($this); - $this->temporalPosition = new Position(0, 0, 0, $this); - $this->temporalVector = new Vector3(0, 0, 0); - } - - /** - * @deprecated - */ - public function getTickRate() : int{ - return 1; - } - - public function getTickRateTime() : float{ - return $this->tickRateTime; - } - - /** - * @deprecated does nothing - * - * @return void - */ - public function setTickRate(int $tickRate){ - - } - - public function registerGeneratorToWorker(int $worker) : void{ - $this->generatorRegisteredWorkers[$worker] = true; - $this->server->getAsyncPool()->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getGeneratorOptions()), $worker); - } - - /** - * @return void - */ - public function unregisterGenerator(){ - $pool = $this->server->getAsyncPool(); - foreach($pool->getRunningWorkers() as $i){ - if(isset($this->generatorRegisteredWorkers[$i])){ - $pool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i); - } - } - $this->generatorRegisteredWorkers = []; - } - - public function getBlockMetadata() : BlockMetadataStore{ - return $this->blockMetadata; - } - - public function getServer() : Server{ - return $this->server; - } - - final public function getProvider() : LevelProvider{ - return $this->provider; - } - - /** - * Returns the unique level identifier - */ - final public function getId() : int{ - return $this->levelId; - } - - public function isClosed() : bool{ - return $this->closed; - } - - /** - * @return void - */ - public function close(){ - if($this->closed){ - throw new \InvalidStateException("Tried to close a world which is already closed"); - } - - foreach($this->chunks as $chunk){ - $this->unloadChunk($chunk->getX(), $chunk->getZ(), false); - } - - $this->save(); - - $this->unregisterGenerator(); - - $this->provider->close(); - $this->provider = null; - $this->blockMetadata = null; - $this->blockCache = []; - $this->temporalPosition = null; - - $this->closed = true; - } - - /** - * @param Player[]|null $players - * - * @return void - */ - public function addSound(Sound $sound, array $players = null){ - $pk = $sound->encode(); - if(!is_array($pk)){ - $pk = [$pk]; - } - if(count($pk) > 0){ - if($players === null){ - foreach($pk as $e){ - $this->broadcastPacketToViewers($sound, $e); - } - }else{ - $this->server->batchPackets($players, $pk, false); - } - } - } - - /** - * @param Player[]|null $players - * - * @return void - */ - public function addParticle(Particle $particle, array $players = null){ - $pk = $particle->encode(); - if(!is_array($pk)){ - $pk = [$pk]; - } - if(count($pk) > 0){ - if($players === null){ - foreach($pk as $e){ - $this->broadcastPacketToViewers($particle, $e); - } - }else{ - $this->server->batchPackets($players, $pk, false); - } - } - } - - /** - * Broadcasts a LevelEvent to players in the area. This could be sound, particles, weather changes, etc. - * - * @param Vector3|null $pos If null, broadcasts to every player in the Level - * - * @return void - */ - public function broadcastLevelEvent(?Vector3 $pos, int $evid, int $data = 0){ - $pk = new LevelEventPacket(); - $pk->evid = $evid; - $pk->data = $data; - if($pos !== null){ - $pk->position = $pos->asVector3(); - $this->broadcastPacketToViewers($pos, $pk); - }else{ - $pk->position = null; - $this->broadcastGlobalPacket($pk); - } - } - - /** - * Broadcasts a LevelSoundEvent to players in the area. - * - * @param bool $disableRelativeVolume If true, all players receiving this sound-event will hear the sound at full volume regardless of distance - * - * @return void - */ - public function broadcastLevelSoundEvent(Vector3 $pos, int $soundId, int $extraData = -1, int $entityTypeId = -1, bool $isBabyMob = false, bool $disableRelativeVolume = false){ - $pk = new LevelSoundEventPacket(); - $pk->sound = $soundId; - $pk->extraData = $extraData; - $pk->entityType = AddActorPacket::LEGACY_ID_MAP_BC[$entityTypeId] ?? ":"; - $pk->isBabyMob = $isBabyMob; - $pk->disableRelativeVolume = $disableRelativeVolume; - $pk->position = $pos->asVector3(); - $this->broadcastPacketToViewers($pos, $pk); - } - - public function getAutoSave() : bool{ - return $this->autoSave; - } - - /** - * @return void - */ - public function setAutoSave(bool $value){ - $this->autoSave = $value; - } - - /** - * @internal - * @see Server::unloadLevel() - * - * Unloads the current level from memory safely - * - * @param bool $force default false, force unload of default level - * - * @throws \InvalidStateException if trying to unload a level during level tick - */ - public function unload(bool $force = false) : bool{ - if($this->doingTick and !$force){ - throw new \InvalidStateException("Cannot unload a world during world tick"); - } - - $ev = new LevelUnloadEvent($this); - - if($this === $this->server->getDefaultLevel() and !$force){ - $ev->setCancelled(true); - } - - $ev->call(); - - if(!$force and $ev->isCancelled()){ - return false; - } - - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$this->getName()])); - $defaultLevel = $this->server->getDefaultLevel(); - foreach($this->getPlayers() as $player){ - if($this === $defaultLevel or $defaultLevel === null){ - $player->close($player->getLeaveMessage(), "Forced default world unload"); - }else{ - $player->teleport($defaultLevel->getSafeSpawn()); - } - } - - if($this === $defaultLevel){ - $this->server->setDefaultLevel(null); - } - - $this->server->removeLevel($this); - - $this->close(); - - return true; - } - - /** - * @deprecated WARNING: This function has a misleading name. Contrary to what the name might imply, this function - * DOES NOT return players who are IN a chunk, rather, it returns players who can SEE the chunk. - * - * Returns a list of players who have the target chunk within their view distance. - * - * @return Player[] - */ - public function getChunkPlayers(int $chunkX, int $chunkZ) : array{ - return $this->playerLoaders[Level::chunkHash($chunkX, $chunkZ)] ?? []; - } - - /** - * Gets the chunk loaders being used in a specific chunk - * - * @return ChunkLoader[] - */ - public function getChunkLoaders(int $chunkX, int $chunkZ) : array{ - return $this->chunkLoaders[Level::chunkHash($chunkX, $chunkZ)] ?? []; - } - - /** - * Returns an array of players who have the target position within their view distance. - * - * @return Player[] - */ - public function getViewersForPosition(Vector3 $pos) : array{ - return $this->getChunkPlayers($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4); - } - - /** - * Queues a DataPacket to be sent to all players using the chunk at the specified X/Z coordinates at the end of the - * current tick. - * - * @return void - */ - public function addChunkPacket(int $chunkX, int $chunkZ, DataPacket $packet){ - if(!isset($this->chunkPackets[$index = Level::chunkHash($chunkX, $chunkZ)])){ - $this->chunkPackets[$index] = [$packet]; - }else{ - $this->chunkPackets[$index][] = $packet; - } - } - - /** - * Broadcasts a packet to every player who has the target position within their view distance. - */ - public function broadcastPacketToViewers(Vector3 $pos, DataPacket $packet) : void{ - $this->addChunkPacket($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4, $packet); - } - - /** - * Broadcasts a packet to every player in the level. - */ - public function broadcastGlobalPacket(DataPacket $packet) : void{ - $this->globalPackets[] = $packet; - } - - /** - * @deprecated - * @see Level::broadcastGlobalPacket() - */ - public function addGlobalPacket(DataPacket $packet) : void{ - $this->globalPackets[] = $packet; - } - - /** - * @return void - */ - public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true){ - $loaderId = $loader->getLoaderId(); - - if(!isset($this->chunkLoaders[$chunkHash = Level::chunkHash($chunkX, $chunkZ)])){ - $this->chunkLoaders[$chunkHash] = []; - $this->playerLoaders[$chunkHash] = []; - }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){ - return; - } - - $this->chunkLoaders[$chunkHash][$loaderId] = $loader; - if($loader instanceof Player){ - $this->playerLoaders[$chunkHash][$loaderId] = $loader; - } - - if(!isset($this->loaders[$loaderId])){ - $this->loaderCounter[$loaderId] = 1; - $this->loaders[$loaderId] = $loader; - }else{ - ++$this->loaderCounter[$loaderId]; - } - - $this->cancelUnloadChunkRequest($chunkX, $chunkZ); - - if($autoLoad){ - $this->loadChunk($chunkX, $chunkZ); - } - } - - /** - * @return void - */ - public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ){ - $chunkHash = Level::chunkHash($chunkX, $chunkZ); - $loaderId = $loader->getLoaderId(); - if(isset($this->chunkLoaders[$chunkHash][$loaderId])){ - unset($this->chunkLoaders[$chunkHash][$loaderId]); - unset($this->playerLoaders[$chunkHash][$loaderId]); - if(count($this->chunkLoaders[$chunkHash]) === 0){ - unset($this->chunkLoaders[$chunkHash]); - unset($this->playerLoaders[$chunkHash]); - $this->unloadChunkRequest($chunkX, $chunkZ, true); - } - - if(--$this->loaderCounter[$loaderId] === 0){ - unset($this->loaderCounter[$loaderId]); - unset($this->loaders[$loaderId]); - } - } - } - - /** - * @internal - * - * @param Player ...$targets If empty, will send to all players in the level. - * - * @return void - */ - public function sendTime(Player ...$targets){ - $pk = new SetTimePacket(); - $pk->time = $this->time & 0xffffffff; //avoid overflowing the field, since the packet uses an int32 - - $this->server->broadcastPacket(count($targets) > 0 ? $targets : $this->players, $pk); - } - - /** - * @internal - * - * @return void - */ - public function doTick(int $currentTick){ - if($this->closed){ - throw new \InvalidStateException("Attempted to tick a world which has been closed"); - } - - $this->timings->doTick->startTiming(); - $this->doingTick = true; - try{ - $this->actuallyDoTick($currentTick); - }finally{ - $this->doingTick = false; - $this->timings->doTick->stopTiming(); - } - } - - protected function actuallyDoTick(int $currentTick) : void{ - if(!$this->stopTime){ - //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types - if($this->time === PHP_INT_MAX){ - $this->time = PHP_INT_MIN; - }else{ - $this->time++; - } - } - - $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time - $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle - - if(++$this->sendTimeTicker === 200){ - $this->sendTime(); - $this->sendTimeTicker = 0; - } - - $this->unloadChunks(); - if(++$this->providerGarbageCollectionTicker >= 6000){ - $this->provider->doGarbageCollection(); - $this->providerGarbageCollectionTicker = 0; - } - - //Do block updates - $this->timings->doTickPending->startTiming(); - - //Delayed updates - while($this->scheduledBlockUpdateQueue->count() > 0 and $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){ - /** @var Vector3 $vec */ - $vec = $this->scheduledBlockUpdateQueue->extract()["data"]; - unset($this->scheduledBlockUpdateQueueIndex[Level::blockHash($vec->x, $vec->y, $vec->z)]); - if(!$this->isInLoadedTerrain($vec)){ - continue; - } - $block = $this->getBlock($vec); - $block->onScheduledUpdate(); - } - - //Normal updates - while($this->neighbourBlockUpdateQueue->count() > 0){ - $index = $this->neighbourBlockUpdateQueue->dequeue(); - Level::getBlockXYZ($index, $x, $y, $z); - - $block = $this->getBlockAt($x, $y, $z); - $block->clearCaches(); //for blocks like fences, force recalculation of connected AABBs - - $ev = new BlockUpdateEvent($block); - $ev->call(); - if(!$ev->isCancelled()){ - $block->onNearbyBlockChange(); - } - } - - $this->timings->doTickPending->stopTiming(); - - $this->timings->entityTick->startTiming(); - //Update entities that need update - Timings::$tickEntityTimer->startTiming(); - foreach($this->updateEntities as $id => $entity){ - if($entity->isClosed() or !$entity->onUpdate($currentTick)){ - unset($this->updateEntities[$id]); - } - if($entity->isFlaggedForDespawn()){ - $entity->close(); - } - } - Timings::$tickEntityTimer->stopTiming(); - $this->timings->entityTick->stopTiming(); - - $this->timings->tileEntityTick->startTiming(); - Timings::$tickTileEntityTimer->startTiming(); - //Update tiles that need update - foreach($this->updateTiles as $id => $tile){ - if(!$tile->onUpdate()){ - unset($this->updateTiles[$id]); - } - } - Timings::$tickTileEntityTimer->stopTiming(); - $this->timings->tileEntityTick->stopTiming(); - - $this->timings->doTickTiles->startTiming(); - $this->tickChunks(); - $this->timings->doTickTiles->stopTiming(); - - $this->executeQueuedLightUpdates(); - - if(count($this->changedBlocks) > 0){ - if(count($this->players) > 0){ - foreach($this->changedBlocks as $index => $blocks){ - if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send - continue; - } - unset($this->chunkCache[$index]); - Level::getXZ($index, $chunkX, $chunkZ); - if(count($blocks) > 512){ - $chunk = $this->getChunk($chunkX, $chunkZ); - foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){ - $p->onChunkChanged($chunk); - } - }else{ - $this->sendBlocks($this->getChunkPlayers($chunkX, $chunkZ), $blocks, UpdateBlockPacket::FLAG_ALL); - } - } - }else{ - $this->chunkCache = []; - } - - $this->changedBlocks = []; - - } - - $this->processChunkRequest(); - - if($this->sleepTicks > 0 and --$this->sleepTicks <= 0){ - $this->checkSleep(); - } - - if(count($this->globalPackets) > 0){ - if(count($this->players) > 0){ - $this->server->batchPackets($this->players, $this->globalPackets); - } - $this->globalPackets = []; - } - - foreach($this->chunkPackets as $index => $entries){ - Level::getXZ($index, $chunkX, $chunkZ); - $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ); - if(count($chunkPlayers) > 0){ - $this->server->batchPackets($chunkPlayers, $entries, false, false); - } - } - - $this->chunkPackets = []; - } - - /** - * @return void - */ - public function checkSleep(){ - if(count($this->players) === 0){ - return; - } - - $resetTime = true; - foreach($this->getPlayers() as $p){ - if(!$p->isSleeping()){ - $resetTime = false; - break; - } - } - - if($resetTime){ - $time = $this->getTimeOfDay(); - - if($time >= Level::TIME_NIGHT and $time < Level::TIME_SUNRISE){ - $this->setTime($this->getTime() + Level::TIME_FULL - $time); - - foreach($this->getPlayers() as $p){ - $p->stopSleep(); - } - } - } - } - - public function setSleepTicks(int $ticks) : void{ - $this->sleepTicks = $ticks; - } - - /** - * @param Player[] $target - * @param Vector3[] $blocks - * - * @return void - */ - public function sendBlocks(array $target, array $blocks, int $flags = UpdateBlockPacket::FLAG_NONE, bool $optimizeRebuilds = false){ - $packets = []; - if($optimizeRebuilds){ - $chunks = []; - 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))); - } - $pk = new UpdateBlockPacket(); - - $first = false; - if(!isset($chunks[$index = Level::chunkHash($b->x >> 4, $b->z >> 4)])){ - $chunks[$index] = true; - $first = true; - } - - $pk->x = $b->x; - $pk->y = $b->y; - $pk->z = $b->z; - - if($b instanceof Block){ - $pk->blockRuntimeId = $b->getRuntimeId(); - }else{ - $fullBlock = $this->getFullBlock($b->x, $b->y, $b->z); - $pk->blockRuntimeId = RuntimeBlockMapping::toStaticRuntimeId($fullBlock >> 4, $fullBlock & 0xf); - } - - $pk->flags = $first ? $flags : UpdateBlockPacket::FLAG_NONE; - - $packets[] = $pk; - } - }else{ - 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))); - } - $pk = new UpdateBlockPacket(); - - $pk->x = $b->x; - $pk->y = $b->y; - $pk->z = $b->z; - - if($b instanceof Block){ - $pk->blockRuntimeId = $b->getRuntimeId(); - }else{ - $fullBlock = $this->getFullBlock($b->x, $b->y, $b->z); - $pk->blockRuntimeId = RuntimeBlockMapping::toStaticRuntimeId($fullBlock >> 4, $fullBlock & 0xf); - } - - $pk->flags = $flags; - - $packets[] = $pk; - } - } - - $this->server->batchPackets($target, $packets, false, false); - } - - /** - * @return void - */ - public function clearCache(bool $force = false){ - if($force){ - $this->chunkCache = []; - $this->blockCache = []; - }else{ - $count = 0; - foreach($this->blockCache as $list){ - $count += count($list); - if($count > 2048){ - $this->blockCache = []; - break; - } - } - } - } - - /** - * @return void - */ - public function clearChunkCache(int $chunkX, int $chunkZ){ - unset($this->chunkCache[Level::chunkHash($chunkX, $chunkZ)]); - } - - /** - * @phpstan-return \SplFixedArray - */ - public function getRandomTickedBlocks() : \SplFixedArray{ - return $this->randomTickBlocks; - } - - /** - * @return void - */ - public function addRandomTickedBlock(int $id){ - $this->randomTickBlocks[$id] = BlockFactory::get($id); - } - - /** - * @return void - */ - public function removeRandomTickedBlock(int $id){ - $this->randomTickBlocks[$id] = null; - } - - private function tickChunks() : void{ - if($this->chunksPerTick <= 0 or count($this->loaders) === 0){ - $this->chunkTickList = []; - return; - } - - $chunksPerLoader = min(200, max(1, (int) ((($this->chunksPerTick - count($this->loaders)) / count($this->loaders)) + 0.5))); - $randRange = 3 + $chunksPerLoader / 30; - $randRange = (int) ($randRange > $this->chunkTickRadius ? $this->chunkTickRadius : $randRange); - - foreach($this->loaders as $loader){ - $chunkX = (int) floor($loader->getX()) >> 4; - $chunkZ = (int) floor($loader->getZ()) >> 4; - - $index = Level::chunkHash($chunkX, $chunkZ); - $existingLoaders = max(0, $this->chunkTickList[$index] ?? 0); - $this->chunkTickList[$index] = $existingLoaders + 1; - for($chunk = 0; $chunk < $chunksPerLoader; ++$chunk){ - $dx = mt_rand(-$randRange, $randRange); - $dz = mt_rand(-$randRange, $randRange); - $hash = Level::chunkHash($dx + $chunkX, $dz + $chunkZ); - if(!isset($this->chunkTickList[$hash]) and isset($this->chunks[$hash])){ - $this->chunkTickList[$hash] = -1; - } - } - } - - foreach($this->chunkTickList as $index => $loaders){ - Level::getXZ($index, $chunkX, $chunkZ); - - for($cx = -1; $cx <= 1; ++$cx){ - for($cz = -1; $cz <= 1; ++$cz){ - if(!isset($this->chunks[Level::chunkHash($chunkX + $cx, $chunkZ + $cz)])){ - unset($this->chunkTickList[$index]); - goto skip_to_next; //no "continue 3" thanks! - } - } - } - - if($loaders <= 0){ - unset($this->chunkTickList[$index]); - } - - $chunk = $this->chunks[$index]; - foreach($chunk->getEntities() as $entity){ - $entity->scheduleUpdate(); - } - - foreach($chunk->getSubChunks() as $Y => $subChunk){ - if(!($subChunk instanceof EmptySubChunk)){ - $k = mt_rand(0, 0xfffffffff); //36 bits - for($i = 0; $i < 3; ++$i){ - $x = $k & 0x0f; - $y = ($k >> 4) & 0x0f; - $z = ($k >> 8) & 0x0f; - $k >>= 12; - - $blockId = $subChunk->getBlockId($x, $y, $z); - if($this->randomTickBlocks[$blockId] !== null){ - /** @var Block $block */ - $block = clone $this->randomTickBlocks[$blockId]; - $block->setDamage($subChunk->getBlockData($x, $y, $z)); - - $block->x = $chunkX * 16 + $x; - $block->y = ($Y << 4) + $y; - $block->z = $chunkZ * 16 + $z; - $block->level = $this; - $block->onRandomTick(); - } - } - } - } - - skip_to_next: //dummy label to break out of nested loops - } - - if($this->clearChunksOnTick){ - $this->chunkTickList = []; - } - } - - /** - * @return mixed[] - */ - public function __debugInfo() : array{ - return []; - } - - public function save(bool $force = false) : bool{ - - if(!$this->getAutoSave() and !$force){ - return false; - } - - (new LevelSaveEvent($this))->call(); - - $this->provider->setTime($this->time); - $this->saveChunks(); - if($this->provider instanceof BaseLevelProvider){ - $this->provider->saveLevelData(); - } - - return true; - } - - /** - * @return void - */ - public function saveChunks(){ - $this->timings->syncChunkSaveTimer->startTiming(); - try{ - foreach($this->chunks as $chunk){ - if(($chunk->hasChanged() or count($chunk->getTiles()) > 0 or count($chunk->getSavableEntities()) > 0) and $chunk->isGenerated()){ - $this->provider->saveChunk($chunk); - $chunk->setChanged(false); - } - } - }finally{ - $this->timings->syncChunkSaveTimer->stopTiming(); - } - } - - /** - * Schedules a block update to be executed after the specified number of ticks. - * Blocks will be updated with the scheduled update type. - * - * @return void - */ - public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay){ - if( - !$this->isInWorld($pos->x, $pos->y, $pos->z) or - (isset($this->scheduledBlockUpdateQueueIndex[$index = Level::blockHash($pos->x, $pos->y, $pos->z)]) and $this->scheduledBlockUpdateQueueIndex[$index] <= $delay) - ){ - return; - } - $this->scheduledBlockUpdateQueueIndex[$index] = $delay; - $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick()); - } - - /** - * Schedules the blocks around the specified position to be updated at the end of this tick. - * Blocks will be updated with the normal update type. - * - * @return void - */ - public function scheduleNeighbourBlockUpdates(Vector3 $pos){ - $pos = $pos->floor(); - - for($i = 0; $i <= 5; ++$i){ - $side = $pos->getSide($i); - if($this->isInWorld($side->x, $side->y, $side->z)){ - $this->neighbourBlockUpdateQueue->enqueue(Level::blockHash($side->x, $side->y, $side->z)); - } - } - } - - /** - * @return Block[] - */ - public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{ - $minX = (int) floor($bb->minX - 1); - $minY = (int) floor($bb->minY - 1); - $minZ = (int) floor($bb->minZ - 1); - $maxX = (int) floor($bb->maxX + 1); - $maxY = (int) floor($bb->maxY + 1); - $maxZ = (int) floor($bb->maxZ + 1); - - $collides = []; - - if($targetFirst){ - for($z = $minZ; $z <= $maxZ; ++$z){ - for($x = $minX; $x <= $maxX; ++$x){ - for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->getBlockAt($x, $y, $z); - if(!$block->canPassThrough() and $block->collidesWithBB($bb)){ - return [$block]; - } - } - } - } - }else{ - for($z = $minZ; $z <= $maxZ; ++$z){ - for($x = $minX; $x <= $maxX; ++$x){ - for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->getBlockAt($x, $y, $z); - if(!$block->canPassThrough() and $block->collidesWithBB($bb)){ - $collides[] = $block; - } - } - } - } - } - - return $collides; - } - - public function isFullBlock(Vector3 $pos) : bool{ - if($pos instanceof Block){ - if($pos->isSolid()){ - return true; - } - $bb = $pos->getBoundingBox(); - }else{ - $bb = $this->getBlock($pos)->getBoundingBox(); - } - - return $bb !== null and $bb->getAverageEdgeLength() >= 1; - } - - /** - * @return AxisAlignedBB[] - */ - public function getCollisionCubes(Entity $entity, AxisAlignedBB $bb, bool $entities = true) : array{ - $minX = (int) floor($bb->minX - 1); - $minY = (int) floor($bb->minY - 1); - $minZ = (int) floor($bb->minZ - 1); - $maxX = (int) floor($bb->maxX + 1); - $maxY = (int) floor($bb->maxY + 1); - $maxZ = (int) floor($bb->maxZ + 1); - - $collides = []; - - for($z = $minZ; $z <= $maxZ; ++$z){ - for($x = $minX; $x <= $maxX; ++$x){ - for($y = $minY; $y <= $maxY; ++$y){ - $block = $this->getBlockAt($x, $y, $z); - if(!$block->canPassThrough()){ - foreach($block->getCollisionBoxes() as $blockBB){ - if($blockBB->intersectsWith($bb)){ - $collides[] = $blockBB; - } - } - } - } - } - } - - if($entities){ - foreach($this->getCollidingEntities($bb->expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){ - $collides[] = clone $ent->boundingBox; - } - } - - return $collides; - } - - public function getFullLight(Vector3 $pos) : int{ - return $this->getFullLightAt($pos->x, $pos->y, $pos->z); - } - - public function getFullLightAt(int $x, int $y, int $z) : int{ - $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z); - if($skyLight < 15){ - return max($skyLight, $this->getBlockLightAt($x, $y, $z)); - }else{ - return $skyLight; - } - } - - /** - * Computes the percentage of a circle away from noon the sun is currently at. This can be multiplied by 2 * M_PI to - * get an angle in radians, or by 360 to get an angle in degrees. - */ - public function computeSunAnglePercentage() : float{ - $timeProgress = ($this->time % 24000) / 24000; - - //0.0 needs to be high noon, not dusk - $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25); - - //Offset the sun progress to be above the horizon longer at dusk and dawn - //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn - $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3); - - return $sunProgress + $diff; - } - - /** - * Returns the percentage of a circle away from noon the sun is currently at. - */ - public function getSunAnglePercentage() : float{ - return $this->sunAnglePercentage; - } - - /** - * Returns the current sun angle in radians. - */ - public function getSunAngleRadians() : float{ - return $this->sunAnglePercentage * 2 * M_PI; - } - - /** - * Returns the current sun angle in degrees. - */ - public function getSunAngleDegrees() : float{ - return $this->sunAnglePercentage * 360.0; - } - - /** - * Computes how many points of sky light is subtracted based on the current time. Used to offset raw chunk sky light - * to get a real light value. - */ - public function computeSkyLightReduction() : int{ - $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5))); - - //TODO: check rain and thunder level - - return (int) ($percentage * 11); - } - - /** - * Returns how many points of sky light is subtracted based on the current time. - */ - public function getSkyLightReduction() : int{ - return $this->skyLightReduction; - } - - /** - * Returns the sky light level at the specified coordinates, offset by the current time and weather. - * - * @return int 0-15 - */ - public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{ - $light = $this->getBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction; - return $light < 0 ? 0 : $light; - } - - /** - * @return int bitmap, (id << 4) | data - */ - public function getFullBlock(int $x, int $y, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, false)->getFullBlock($x & 0x0f, $y, $z & 0x0f); - } - - public function isInWorld(int $x, int $y, int $z) : bool{ - return ( - $x <= INT32_MAX and $x >= INT32_MIN and - $y < $this->worldHeight and $y >= 0 and - $z <= INT32_MAX and $z >= INT32_MIN - ); - } - - /** - * Gets the Block object at the Vector3 location. This method wraps around {@link getBlockAt}, converting the - * vector components to integers. - * - * Note: If you're using this for performance-sensitive code, and you're guaranteed to be supplying ints in the - * specified vector, consider using {@link getBlockAt} instead for better performance. - * - * @param bool $cached Whether to use the block cache for getting the block (faster, but may be inaccurate) - * @param bool $addToCache Whether to cache the block object created by this method call. - */ - public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{ - return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache); - } - - /** - * Gets the Block object at the specified coordinates. - * - * Note for plugin developers: If you are using this method a lot (thousands of times for many positions for - * example), you may want to set addToCache to false to avoid using excessive amounts of memory. - * - * @param bool $cached Whether to use the block cache for getting the block (faster, but may be inaccurate) - * @param bool $addToCache Whether to cache the block object created by this method call. - */ - public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{ - $fullState = 0; - $relativeBlockHash = null; - $chunkHash = Level::chunkHash($x >> 4, $z >> 4); - - if($this->isInWorld($x, $y, $z)){ - $relativeBlockHash = Level::chunkBlockHash($x, $y, $z); - - if($cached and isset($this->blockCache[$chunkHash][$relativeBlockHash])){ - return $this->blockCache[$chunkHash][$relativeBlockHash]; - } - - $chunk = $this->chunks[$chunkHash] ?? null; - if($chunk !== null){ - $fullState = $chunk->getFullBlock($x & 0x0f, $y, $z & 0x0f); - }else{ - $addToCache = false; - } - } - - $block = clone $this->blockStates[$fullState & 0xfff]; - - $block->x = $x; - $block->y = $y; - $block->z = $z; - $block->level = $this; - - if($addToCache and $relativeBlockHash !== null){ - $this->blockCache[$chunkHash][$relativeBlockHash] = $block; - } - - return $block; - } - - /** - * @return void - */ - public function updateAllLight(Vector3 $pos){ - $this->updateBlockSkyLight($pos->x, $pos->y, $pos->z); - $this->updateBlockLight($pos->x, $pos->y, $pos->z); - } - - /** - * Returns the highest block light level available in the positions adjacent to the specified block coordinates. - */ - public function getHighestAdjacentBlockSkyLight(int $x, int $y, int $z) : int{ - return max([ - $this->getBlockSkyLightAt($x + 1, $y, $z), - $this->getBlockSkyLightAt($x - 1, $y, $z), - $this->getBlockSkyLightAt($x, $y + 1, $z), - $this->getBlockSkyLightAt($x, $y - 1, $z), - $this->getBlockSkyLightAt($x, $y, $z + 1), - $this->getBlockSkyLightAt($x, $y, $z - 1) - ]); - } - - /** - * @return void - */ - public function updateBlockSkyLight(int $x, int $y, int $z){ - $this->timings->doBlockSkyLightUpdates->startTiming(); - - $oldHeightMap = $this->getHeightMap($x, $z); - $sourceId = $this->getBlockIdAt($x, $y, $z); - - $yPlusOne = $y + 1; - - if($yPlusOne === $oldHeightMap){ //Block changed directly beneath the heightmap. Check if a block was removed or changed to a different light-filter. - $newHeightMap = $this->getChunk($x >> 4, $z >> 4)->recalculateHeightMapColumn($x & 0x0f, $z & 0x0f); - }elseif($yPlusOne > $oldHeightMap){ //Block changed above the heightmap. - if(BlockFactory::$lightFilter[$sourceId] > 1 or BlockFactory::$diffusesSkyLight[$sourceId]){ - $this->setHeightMap($x, $z, $yPlusOne); - $newHeightMap = $yPlusOne; - }else{ //Block changed which has no effect on direct sky light, for example placing or removing glass. - $this->timings->doBlockSkyLightUpdates->stopTiming(); - return; - } - }else{ //Block changed below heightmap - $newHeightMap = $oldHeightMap; - } - - if($this->skyLightUpdate === null){ - $this->skyLightUpdate = new SkyLightUpdate($this); - } - if($newHeightMap > $oldHeightMap){ //Heightmap increase, block placed, remove sky light - for($i = $y; $i >= $oldHeightMap; --$i){ - $this->skyLightUpdate->setAndUpdateLight($x, $i, $z, 0); //Remove all light beneath, adjacent recalculation will handle the rest. - } - }elseif($newHeightMap < $oldHeightMap){ //Heightmap decrease, block changed or removed, add sky light - for($i = $y; $i >= $newHeightMap; --$i){ - $this->skyLightUpdate->setAndUpdateLight($x, $i, $z, 15); - } - }else{ //No heightmap change, block changed "underground" - $this->skyLightUpdate->setAndUpdateLight($x, $y, $z, max(0, $this->getHighestAdjacentBlockSkyLight($x, $y, $z) - BlockFactory::$lightFilter[$sourceId])); - } - - $this->timings->doBlockSkyLightUpdates->stopTiming(); - } - - /** - * Returns the highest block light level available in the positions adjacent to the specified block coordinates. - */ - public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{ - return max([ - $this->getBlockLightAt($x + 1, $y, $z), - $this->getBlockLightAt($x - 1, $y, $z), - $this->getBlockLightAt($x, $y + 1, $z), - $this->getBlockLightAt($x, $y - 1, $z), - $this->getBlockLightAt($x, $y, $z + 1), - $this->getBlockLightAt($x, $y, $z - 1) - ]); - } - - /** - * @return void - */ - public function updateBlockLight(int $x, int $y, int $z){ - $this->timings->doBlockLightUpdates->startTiming(); - - $id = $this->getBlockIdAt($x, $y, $z); - $newLevel = max(BlockFactory::$light[$id], $this->getHighestAdjacentBlockLight($x, $y, $z) - BlockFactory::$lightFilter[$id]); - - if($this->blockLightUpdate === null){ - $this->blockLightUpdate = new BlockLightUpdate($this); - } - $this->blockLightUpdate->setAndUpdateLight($x, $y, $z, $newLevel); - - $this->timings->doBlockLightUpdates->stopTiming(); - } - - public function executeQueuedLightUpdates() : void{ - if($this->blockLightUpdate !== null){ - $this->timings->doBlockLightUpdates->startTiming(); - $this->blockLightUpdate->execute(); - $this->blockLightUpdate = null; - $this->timings->doBlockLightUpdates->stopTiming(); - } - - if($this->skyLightUpdate !== null){ - $this->timings->doBlockSkyLightUpdates->startTiming(); - $this->skyLightUpdate->execute(); - $this->skyLightUpdate = null; - $this->timings->doBlockSkyLightUpdates->stopTiming(); - } - } - - /** - * Sets on Vector3 the data from a Block object, - * does block updates and puts the changes to the send queue. - * - * If $direct is true, it'll send changes directly to players. if false, it'll be queued - * and the best way to send queued changes will be done in the next tick. - * This way big changes can be sent on a single chunk update packet instead of thousands of packets. - * - * If $update is true, it'll get the neighbour blocks (6 sides) and update them. - * If you are doing big changes, you might want to set this to false, then update manually. - * - * @param bool $direct @deprecated - * - * @return bool Whether the block has been updated or not - */ - public function setBlock(Vector3 $pos, Block $block, bool $direct = false, bool $update = true) : bool{ - $pos = $pos->floor(); - if(!$this->isInWorld($pos->x, $pos->y, $pos->z)){ - return false; - } - - $this->timings->setBlock->startTiming(); - - if($this->getChunkAtPosition($pos, true)->setBlock($pos->x & 0x0f, $pos->y, $pos->z & 0x0f, $block->getId(), $block->getDamage())){ - if(!($pos instanceof Position)){ - $pos = $this->temporalPosition->setComponents($pos->x, $pos->y, $pos->z); - } - - $block = clone $block; - - $block->position($pos); - $block->clearCaches(); - - $chunkHash = Level::chunkHash($pos->x >> 4, $pos->z >> 4); - $relativeBlockHash = Level::chunkBlockHash($pos->x, $pos->y, $pos->z); - - unset($this->blockCache[$chunkHash][$relativeBlockHash]); - - if($direct){ - $this->sendBlocks($this->getChunkPlayers($pos->x >> 4, $pos->z >> 4), [$block], UpdateBlockPacket::FLAG_ALL_PRIORITY); - unset($this->chunkCache[$chunkHash], $this->changedBlocks[$chunkHash][$relativeBlockHash]); - }else{ - if(!isset($this->changedBlocks[$chunkHash])){ - $this->changedBlocks[$chunkHash] = []; - } - - $this->changedBlocks[$chunkHash][$relativeBlockHash] = $block; - } - - foreach($this->getChunkLoaders($pos->x >> 4, $pos->z >> 4) as $loader){ - $loader->onBlockChanged($block); - } - - if($update){ - $this->updateAllLight($block); - - $ev = new BlockUpdateEvent($block); - $ev->call(); - if(!$ev->isCancelled()){ - foreach($this->getNearbyEntities(new AxisAlignedBB($block->x - 1, $block->y - 1, $block->z - 1, $block->x + 2, $block->y + 2, $block->z + 2)) as $entity){ - $entity->onNearbyBlockChange(); - } - $ev->getBlock()->onNearbyBlockChange(); - $this->scheduleNeighbourBlockUpdates($pos); - } - } - - $this->timings->setBlock->stopTiming(); - - return true; - } - - $this->timings->setBlock->stopTiming(); - - return false; - } - - /** - * @return ItemEntity|null - */ - public function dropItem(Vector3 $source, Item $item, Vector3 $motion = null, int $delay = 10){ - $motion = $motion ?? new Vector3(lcg_value() * 0.2 - 0.1, 0.2, lcg_value() * 0.2 - 0.1); - $itemTag = $item->nbtSerialize(); - $itemTag->setName("Item"); - - if(!$item->isNull()){ - $nbt = Entity::createBaseNBT($source, $motion, lcg_value() * 360, 0); - $nbt->setShort("Health", 5); - $nbt->setShort("PickupDelay", $delay); - $nbt->setTag($itemTag); - $itemEntity = Entity::createEntity("Item", $this, $nbt); - - if($itemEntity instanceof ItemEntity){ - $itemEntity->spawnToAll(); - - return $itemEntity; - } - } - return null; - } - - /** - * Drops XP orbs into the world for the specified amount, splitting the amount into several orbs if necessary. - * - * @return ExperienceOrb[] - */ - public function dropExperience(Vector3 $pos, int $amount) : array{ - /** @var ExperienceOrb[] $orbs */ - $orbs = []; - - foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){ - $nbt = Entity::createBaseNBT( - $pos, - $this->temporalVector->setComponents((lcg_value() * 0.2 - 0.1) * 2, lcg_value() * 0.4, (lcg_value() * 0.2 - 0.1) * 2), - lcg_value() * 360, - 0 - ); - $nbt->setShort(ExperienceOrb::TAG_VALUE_PC, $split); - - $orb = Entity::createEntity("XPOrb", $this, $nbt); - if($orb === null){ - continue; - } - - $orb->spawnToAll(); - if($orb instanceof ExperienceOrb){ - $orbs[] = $orb; - } - } - - return $orbs; - } - - /** - * Checks if the level spawn protection radius will prevent the player from using items or building at the specified - * Vector3 position. - * - * @return bool true if spawn protection cancelled the action, false if not. - */ - public function checkSpawnProtection(Player $player, Vector3 $vector) : bool{ - if(!$player->hasPermission("pocketmine.spawnprotect.bypass") and ($distance = $this->server->getSpawnRadius()) > -1){ - $t = new Vector2($vector->x, $vector->z); - - $spawnLocation = $this->getSpawnLocation(); - $s = new Vector2($spawnLocation->x, $spawnLocation->z); - if($t->distance($s) <= $distance){ - return true; - } - } - - return false; - } - - /** - * Tries to break a block using a item, including Player time checks if available - * It'll try to lower the durability if Item is a tool, and set it to Air if broken. - * - * @param Item $item reference parameter (if null, can break anything) - */ - public function useBreakOn(Vector3 $vector, Item &$item = null, Player $player = null, bool $createParticles = false) : bool{ - $target = $this->getBlock($vector); - $affectedBlocks = $target->getAffectedBlocks(); - - if($item === null){ - $item = ItemFactory::get(Item::AIR, 0, 0); - } - - $drops = []; - if($player === null or !$player->isCreative()){ - $drops = array_merge(...array_map(function(Block $block) use ($item) : array{ return $block->getDrops($item); }, $affectedBlocks)); - } - - $xpDrop = 0; - if($player !== null and !$player->isCreative()){ - $xpDrop = array_sum(array_map(function(Block $block) use ($item) : int{ return $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->isBreakable($item)) or $player->isSpectator()){ - $ev->setCancelled(); - }elseif($this->checkSpawnProtection($player, $target)){ - $ev->setCancelled(); //set it to cancelled so plugins can bypass this - } - - if($player->isAdventure(true) and !$ev->isCancelled()){ - $tag = $item->getNamedTagEntry("CanDestroy"); - $canBreak = false; - if($tag instanceof ListTag){ - foreach($tag as $v){ - if($v instanceof StringTag){ - $entry = ItemFactory::fromStringSingle($v->getValue()); - if($entry->getId() > 0 and $entry->getBlock()->getId() === $target->getId()){ - $canBreak = true; - break; - } - } - } - } - - $ev->setCancelled(!$canBreak); - } - - $ev->call(); - if($ev->isCancelled()){ - return false; - } - - $drops = $ev->getDrops(); - $xpDrop = $ev->getXpDropAmount(); - - }elseif(!$target->isBreakable($item)){ - return false; - } - - foreach($affectedBlocks as $t){ - $this->destroyBlockInternal($t, $item, $player, $createParticles); - } - - $item->onDestroyBlock($target); - - if(count($drops) > 0){ - $dropPos = $target->add(0.5, 0.5, 0.5); - foreach($drops as $drop){ - if(!$drop->isNull()){ - $this->dropItem($dropPos, $drop); - } - } - } - - if($xpDrop > 0){ - $this->dropExperience($target->add(0.5, 0.5, 0.5), $xpDrop); - } - - return true; - } - - private function destroyBlockInternal(Block $target, Item $item, ?Player $player = null, bool $createParticles = false) : void{ - if($createParticles){ - $this->addParticle(new DestroyBlockParticle($target->add(0.5, 0.5, 0.5), $target)); - } - - $target->onBreak($item, $player); - - $tile = $this->getTile($target); - if($tile !== null){ - if($tile instanceof Container){ - if($tile instanceof Chest){ - $tile->unpair(); - } - - $tile->getInventory()->dropContents($this, $target); - } - - $tile->close(); - } - } - - /** - * Uses a item on a position and face, placing it or activating the block - * - * @param Player|null $player default null - * @param bool $playSound Whether to play a block-place sound if the block was placed successfully. - */ - public function useItemOn(Vector3 $vector, Item &$item, int $face, Vector3 $clickVector = null, Player $player = null, bool $playSound = false) : bool{ - $blockClicked = $this->getBlock($vector); - $blockReplace = $blockClicked->getSide($face); - - if($clickVector === null){ - $clickVector = new Vector3(0.0, 0.0, 0.0); - } - - if(!$this->isInWorld($blockReplace->x, $blockReplace->y, $blockReplace->z)){ - //TODO: build height limit messages for custom world heights and mcregion cap - return false; - } - - if($blockClicked->getId() === Block::AIR){ - return false; - } - - if($player !== null){ - $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK); - if($this->checkSpawnProtection($player, $blockClicked) or $player->isSpectator()){ - $ev->setCancelled(); //set it to cancelled so plugins can bypass this - } - - $ev->call(); - if(!$ev->isCancelled()){ - if((!$player->isSneaking() or $item->isNull()) and $blockClicked->onActivate($item, $player)){ - return true; - } - - if($item->onActivate($player, $blockReplace, $blockClicked, $face, $clickVector)){ - return true; - } - }else{ - return false; - } - }elseif($blockClicked->onActivate($item, $player)){ - return true; - } - - if($item->canBePlaced()){ - $hand = $item->getBlock(); - $hand->position($blockReplace); - }else{ - return false; - } - - if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){ - $blockReplace = $blockClicked; - $hand->position($blockReplace); - }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){ - return false; - } - - if($hand->isSolid()){ - foreach($hand->getCollisionBoxes() as $collisionBox){ - if(count($this->getCollidingEntities($collisionBox)) > 0){ - return false; //Entity in block - } - } - } - - if($player !== null){ - $ev = new BlockPlaceEvent($player, $hand, $blockReplace, $blockClicked, $item); - if($this->checkSpawnProtection($player, $blockReplace) or $player->isSpectator()){ - $ev->setCancelled(); - } - - if($player->isAdventure(true) and !$ev->isCancelled()){ - $canPlace = false; - $tag = $item->getNamedTagEntry("CanPlaceOn"); - if($tag instanceof ListTag){ - foreach($tag as $v){ - if($v instanceof StringTag){ - $entry = ItemFactory::fromStringSingle($v->getValue()); - if($entry->getId() > 0 and $entry->getBlock()->getId() === $blockClicked->getId()){ - $canPlace = true; - break; - } - } - } - } - - $ev->setCancelled(!$canPlace); - } - - $ev->call(); - if($ev->isCancelled()){ - return false; - } - } - - if(!$hand->place($item, $blockReplace, $blockClicked, $face, $clickVector, $player)){ - return false; - } - - if($playSound){ - $this->broadcastLevelSoundEvent($hand, LevelSoundEventPacket::SOUND_PLACE, $hand->getRuntimeId()); - } - - $item->pop(); - - return true; - } - - /** - * @return Entity|null - */ - public function getEntity(int $entityId){ - return $this->entities[$entityId] ?? null; - } - - /** - * Gets the list of all the entities in this level - * - * @return Entity[] - */ - public function getEntities() : array{ - return $this->entities; - } - - /** - * Returns the entities colliding the current one inside the AxisAlignedBB - * - * @return Entity[] - */ - public function getCollidingEntities(AxisAlignedBB $bb, Entity $entity = null) : array{ - $nearby = []; - - if($entity === null or $entity->canCollide){ - $minX = ((int) floor($bb->minX - 2)) >> 4; - $maxX = ((int) floor($bb->maxX + 2)) >> 4; - $minZ = ((int) floor($bb->minZ - 2)) >> 4; - $maxZ = ((int) floor($bb->maxZ + 2)) >> 4; - - for($x = $minX; $x <= $maxX; ++$x){ - for($z = $minZ; $z <= $maxZ; ++$z){ - foreach($this->getChunkEntities($x, $z) as $ent){ - /** @var Entity|null $entity */ - if($ent->canBeCollidedWith() and ($entity === null or ($ent !== $entity and $entity->canCollideWith($ent))) and $ent->boundingBox->intersectsWith($bb)){ - $nearby[] = $ent; - } - } - } - } - } - - return $nearby; - } - - /** - * Returns the entities near the current one inside the AxisAlignedBB - * - * @return Entity[] - */ - public function getNearbyEntities(AxisAlignedBB $bb, Entity $entity = null) : array{ - $nearby = []; - - $minX = ((int) floor($bb->minX - 2)) >> 4; - $maxX = ((int) floor($bb->maxX + 2)) >> 4; - $minZ = ((int) floor($bb->minZ - 2)) >> 4; - $maxZ = ((int) floor($bb->maxZ + 2)) >> 4; - - for($x = $minX; $x <= $maxX; ++$x){ - for($z = $minZ; $z <= $maxZ; ++$z){ - foreach($this->getChunkEntities($x, $z) as $ent){ - if($ent !== $entity and $ent->boundingBox->intersectsWith($bb)){ - $nearby[] = $ent; - } - } - } - } - - return $nearby; - } - - /** - * Returns the closest Entity to the specified position, within the given radius. - * - * @param string $entityType Class of entity to use for instanceof - * @param bool $includeDead Whether to include entitites which are dead - * @phpstan-template TEntity of Entity - * @phpstan-param class-string $entityType - * - * @return Entity|null an entity of type $entityType, or null if not found - * @phpstan-return TEntity - */ - public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{ - assert(is_a($entityType, Entity::class, true)); - - $minX = ((int) floor($pos->x - $maxDistance)) >> 4; - $maxX = ((int) floor($pos->x + $maxDistance)) >> 4; - $minZ = ((int) floor($pos->z - $maxDistance)) >> 4; - $maxZ = ((int) floor($pos->z + $maxDistance)) >> 4; - - $currentTargetDistSq = $maxDistance ** 2; - - /** - * @var Entity|null $currentTarget - * @phpstan-var TEntity|null $currentTarget - */ - $currentTarget = null; - - for($x = $minX; $x <= $maxX; ++$x){ - for($z = $minZ; $z <= $maxZ; ++$z){ - foreach($this->getChunkEntities($x, $z) as $entity){ - if(!($entity instanceof $entityType) or $entity->isClosed() or $entity->isFlaggedForDespawn() or (!$includeDead and !$entity->isAlive())){ - continue; - } - $distSq = $entity->distanceSquared($pos); - if($distSq < $currentTargetDistSq){ - $currentTargetDistSq = $distSq; - $currentTarget = $entity; - } - } - } - } - - return $currentTarget; - } - - /** - * Returns a list of the Tile entities in this level - * - * @return Tile[] - */ - public function getTiles() : array{ - return $this->tiles; - } - - /** - * @return Tile|null - */ - public function getTileById(int $tileId){ - return $this->tiles[$tileId] ?? null; - } - - /** - * Returns a list of the players in this level - * - * @return Player[] - */ - public function getPlayers() : array{ - return $this->players; - } - - /** - * @return ChunkLoader[] - */ - public function getLoaders() : array{ - return $this->loaders; - } - - /** - * Returns the Tile in a position, or null if not found. - * - * Note: This method wraps getTileAt(). If you're guaranteed to be passing integers, and you're using this method - * in performance-sensitive code, consider using getTileAt() instead of this method for better performance. - */ - public function getTile(Vector3 $pos) : ?Tile{ - return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z)); - } - - /** - * Returns the tile at the specified x,y,z coordinates, or null if it does not exist. - */ - public function getTileAt(int $x, int $y, int $z) : ?Tile{ - $chunk = $this->getChunk($x >> 4, $z >> 4); - - if($chunk !== null){ - return $chunk->getTile($x & 0x0f, $y, $z & 0x0f); - } - - return null; - } - - /** - * Returns a list of the entities on a given chunk - * - * @return Entity[] - */ - public function getChunkEntities(int $X, int $Z) : array{ - return ($chunk = $this->getChunk($X, $Z)) !== null ? $chunk->getEntities() : []; - } - - /** - * Gives a list of the Tile entities on a given chunk - * - * @return Tile[] - */ - public function getChunkTiles(int $X, int $Z) : array{ - return ($chunk = $this->getChunk($X, $Z)) !== null ? $chunk->getTiles() : []; - } - - /** - * Gets the raw block id. - * - * @return int 0-255 - */ - public function getBlockIdAt(int $x, int $y, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getBlockId($x & 0x0f, $y, $z & 0x0f); - } - - /** - * Sets the raw block id. - * - * @param int $id 0-255 - * - * @return void - */ - public function setBlockIdAt(int $x, int $y, int $z, int $id){ - if(!$this->isInWorld($x, $y, $z)){ //TODO: bad hack but fixing this requires BC breaks to do properly :( - return; - } - $chunkHash = Level::chunkHash($x >> 4, $z >> 4); - $relativeBlockHash = Level::chunkBlockHash($x, $y, $z); - unset($this->blockCache[$chunkHash][$relativeBlockHash]); - $this->getChunk($x >> 4, $z >> 4, true)->setBlockId($x & 0x0f, $y, $z & 0x0f, $id & 0xff); - - if(!isset($this->changedBlocks[$chunkHash])){ - $this->changedBlocks[$chunkHash] = []; - } - $this->changedBlocks[$chunkHash][$relativeBlockHash] = $v = new Vector3($x, $y, $z); - foreach($this->getChunkLoaders($x >> 4, $z >> 4) as $loader){ - $loader->onBlockChanged($v); - } - } - - /** - * Gets the raw block metadata - * - * @return int 0-15 - */ - public function getBlockDataAt(int $x, int $y, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getBlockData($x & 0x0f, $y, $z & 0x0f); - } - - /** - * Sets the raw block metadata. - * - * @param int $data 0-15 - * - * @return void - */ - public function setBlockDataAt(int $x, int $y, int $z, int $data){ - if(!$this->isInWorld($x, $y, $z)){ //TODO: bad hack but fixing this requires BC breaks to do properly :( - return; - } - $chunkHash = Level::chunkHash($x >> 4, $z >> 4); - $relativeBlockHash = Level::chunkBlockHash($x, $y, $z); - unset($this->blockCache[$chunkHash][$relativeBlockHash]); - - $this->getChunk($x >> 4, $z >> 4, true)->setBlockData($x & 0x0f, $y, $z & 0x0f, $data & 0x0f); - - if(!isset($this->changedBlocks[$chunkHash])){ - $this->changedBlocks[$chunkHash] = []; - } - $this->changedBlocks[$chunkHash][$relativeBlockHash] = $v = new Vector3($x, $y, $z); - foreach($this->getChunkLoaders($x >> 4, $z >> 4) as $loader){ - $loader->onBlockChanged($v); - } - } - - /** - * Gets the raw block skylight level - * - * @return int 0-15 - */ - public function getBlockSkyLightAt(int $x, int $y, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getBlockSkyLight($x & 0x0f, $y, $z & 0x0f); - } - - /** - * Sets the raw block skylight level. - * - * @param int $level 0-15 - * - * @return void - */ - public function setBlockSkyLightAt(int $x, int $y, int $z, int $level){ - $this->getChunk($x >> 4, $z >> 4, true)->setBlockSkyLight($x & 0x0f, $y, $z & 0x0f, $level & 0x0f); - } - - /** - * Gets the raw block light level - * - * @return int 0-15 - */ - public function getBlockLightAt(int $x, int $y, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getBlockLight($x & 0x0f, $y, $z & 0x0f); - } - - /** - * Sets the raw block light level. - * - * @param int $level 0-15 - * - * @return void - */ - public function setBlockLightAt(int $x, int $y, int $z, int $level){ - $this->getChunk($x >> 4, $z >> 4, true)->setBlockLight($x & 0x0f, $y, $z & 0x0f, $level & 0x0f); - } - - public function getBiomeId(int $x, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getBiomeId($x & 0x0f, $z & 0x0f); - } - - public function getBiome(int $x, int $z) : Biome{ - return Biome::getBiome($this->getBiomeId($x, $z)); - } - - /** - * @return void - */ - public function setBiomeId(int $x, int $z, int $biomeId){ - $this->getChunk($x >> 4, $z >> 4, true)->setBiomeId($x & 0x0f, $z & 0x0f, $biomeId); - } - - public function getHeightMap(int $x, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getHeightMap($x & 0x0f, $z & 0x0f); - } - - /** - * @return void - */ - public function setHeightMap(int $x, int $z, int $value){ - $this->getChunk($x >> 4, $z >> 4, true)->setHeightMap($x & 0x0f, $z & 0x0f, $value); - } - - /** - * @return Chunk[] - */ - public function getChunks() : array{ - return $this->chunks; - } - - /** - * Returns the chunk at the specified X/Z coordinates. If the chunk is not loaded, attempts to (synchronously!!!) - * load it. - * - * @param bool $create Whether to create an empty chunk as a placeholder if the chunk does not exist - * - * @return Chunk|null - */ - public function getChunk(int $x, int $z, bool $create = false){ - if(isset($this->chunks[$index = Level::chunkHash($x, $z)])){ - return $this->chunks[$index]; - }elseif($this->loadChunk($x, $z, $create)){ - return $this->chunks[$index]; - } - - return null; - } - - /** - * Returns the chunk containing the given Vector3 position. - */ - public function getChunkAtPosition(Vector3 $pos, bool $create = false) : ?Chunk{ - return $this->getChunk($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4, $create); - } - - /** - * Returns the chunks adjacent to the specified chunk. - * - * @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){ - continue; //center chunk - } - $result[$i] = $this->getChunk($x + $xx - 1, $z + $zz - 1, false); - } - } - - return $result; - } - - /** - * @return void - */ - public function generateChunkCallback(int $x, int $z, ?Chunk $chunk){ - Timings::$generationCallbackTimer->startTiming(); - if(isset($this->chunkPopulationQueue[$index = Level::chunkHash($x, $z)])){ - for($xx = -1; $xx <= 1; ++$xx){ - for($zz = -1; $zz <= 1; ++$zz){ - unset($this->chunkPopulationLock[Level::chunkHash($x + $xx, $z + $zz)]); - } - } - unset($this->chunkPopulationQueue[$index]); - - if($chunk !== null){ - $oldChunk = $this->getChunk($x, $z, false); - $this->setChunk($x, $z, $chunk, false); - if(($oldChunk === null or !$oldChunk->isPopulated()) and $chunk->isPopulated()){ - (new ChunkPopulateEvent($this, $chunk))->call(); - - foreach($this->getChunkLoaders($x, $z) as $loader){ - $loader->onChunkPopulated($chunk); - } - } - } - }elseif(isset($this->chunkPopulationLock[$index])){ - unset($this->chunkPopulationLock[$index]); - if($chunk !== null){ - $this->setChunk($x, $z, $chunk, false); - } - }elseif($chunk !== null){ - $this->setChunk($x, $z, $chunk, false); - } - Timings::$generationCallbackTimer->stopTiming(); - } - - /** - * @param bool $deleteEntitiesAndTiles Whether to delete entities and tiles on the old chunk, or transfer them to the new one - * - * @return void - */ - public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk = null, bool $deleteEntitiesAndTiles = true){ - if($chunk === null){ - return; - } - - $chunk->setX($chunkX); - $chunk->setZ($chunkZ); - - $chunkHash = Level::chunkHash($chunkX, $chunkZ); - $oldChunk = $this->getChunk($chunkX, $chunkZ, false); - if($oldChunk !== null and $oldChunk !== $chunk){ - if($deleteEntitiesAndTiles){ - foreach($oldChunk->getEntities() as $player){ - if(!($player instanceof Player)){ - continue; - } - $chunk->addEntity($player); - $oldChunk->removeEntity($player); - $player->chunk = $chunk; - } - //TODO: this causes chunkloaders to receive false "unloaded" notifications - $this->unloadChunk($chunkX, $chunkZ, false, false); - }else{ - foreach($oldChunk->getEntities() as $entity){ - $chunk->addEntity($entity); - $oldChunk->removeEntity($entity); - $entity->chunk = $chunk; - } - - foreach($oldChunk->getTiles() as $tile){ - $chunk->addTile($tile); - $oldChunk->removeTile($tile); - } - } - } - - $this->chunks[$chunkHash] = $chunk; - - unset($this->blockCache[$chunkHash]); - unset($this->chunkCache[$chunkHash]); - unset($this->changedBlocks[$chunkHash]); - if(isset($this->chunkSendTasks[$chunkHash])){ //invalidate pending caches - $this->chunkSendTasks[$chunkHash]->cancelRun(); - unset($this->chunkSendTasks[$chunkHash]); - } - $chunk->setChanged(); - - if(!$this->isChunkInUse($chunkX, $chunkZ)){ - $this->unloadChunkRequest($chunkX, $chunkZ); - }else{ - foreach($this->getChunkLoaders($chunkX, $chunkZ) as $loader){ - $loader->onChunkChanged($chunk); - } - } - } - - /** - * Gets the highest block Y value at a specific $x and $z - * - * @return int 0-255 - */ - public function getHighestBlockAt(int $x, int $z) : int{ - return $this->getChunk($x >> 4, $z >> 4, true)->getHighestBlockAt($x & 0x0f, $z & 0x0f); - } - - /** - * Returns whether the given position is in a loaded area of terrain. - */ - public function isInLoadedTerrain(Vector3 $pos) : bool{ - return $this->isChunkLoaded($pos->getFloorX() >> 4, $pos->getFloorZ() >> 4); - } - - public function isChunkLoaded(int $x, int $z) : bool{ - return isset($this->chunks[Level::chunkHash($x, $z)]); - } - - public function isChunkGenerated(int $x, int $z) : bool{ - $chunk = $this->getChunk($x, $z); - return $chunk !== null ? $chunk->isGenerated() : false; - } - - public function isChunkPopulated(int $x, int $z) : bool{ - $chunk = $this->getChunk($x, $z); - return $chunk !== null ? $chunk->isPopulated() : false; - } - - /** - * Returns a Position pointing to the spawn - */ - public function getSpawnLocation() : Position{ - return Position::fromObject($this->provider->getSpawn(), $this); - } - - /** - * Sets the level spawn location - * - * @return void - */ - public function setSpawnLocation(Vector3 $pos){ - $previousSpawn = $this->getSpawnLocation(); - $this->provider->setSpawn($pos); - (new SpawnChangeEvent($this, $previousSpawn))->call(); - } - - /** - * @return void - */ - public function requestChunk(int $x, int $z, Player $player){ - $index = Level::chunkHash($x, $z); - if(!isset($this->chunkSendQueue[$index])){ - $this->chunkSendQueue[$index] = []; - } - - $this->chunkSendQueue[$index][$player->getLoaderId()] = $player; - } - - private function sendChunkFromCache(int $x, int $z) : void{ - if(isset($this->chunkSendQueue[$index = Level::chunkHash($x, $z)])){ - foreach($this->chunkSendQueue[$index] as $player){ - /** @var Player $player */ - if($player->isConnected() and isset($player->usedChunks[$index])){ - $player->sendChunk($x, $z, $this->chunkCache[$index]); - } - } - unset($this->chunkSendQueue[$index]); - } - } - - private function processChunkRequest() : void{ - if(count($this->chunkSendQueue) > 0){ - $this->timings->syncChunkSendTimer->startTiming(); - - foreach($this->chunkSendQueue as $index => $players){ - Level::getXZ($index, $x, $z); - - if(isset($this->chunkSendTasks[$index])){ - if($this->chunkSendTasks[$index]->isCrashed()){ - unset($this->chunkSendTasks[$index]); - $this->server->getLogger()->error("Failed to prepare chunk $x $z for sending, retrying"); - }else{ - //Not ready for sending yet - continue; - } - } - if(isset($this->chunkCache[$index])){ - $this->sendChunkFromCache($x, $z); - continue; - } - $this->timings->syncChunkSendPrepareTimer->startTiming(); - - $chunk = $this->chunks[$index] ?? null; - if(!($chunk instanceof Chunk)){ - throw new ChunkException("Invalid Chunk sent"); - } - assert($chunk->getX() === $x and $chunk->getZ() === $z, "Chunk coordinate mismatch: expected $x $z, but chunk has coordinates " . $chunk->getX() . " " . $chunk->getZ() . ", did you forget to clone a chunk before setting?"); - - $this->server->getAsyncPool()->submitTask($task = new ChunkRequestTask($this, $x, $z, $chunk)); - $this->chunkSendTasks[$index] = $task; - - $this->timings->syncChunkSendPrepareTimer->stopTiming(); - } - - $this->timings->syncChunkSendTimer->stopTiming(); - } - } - - /** - * @return void - */ - public function chunkRequestCallback(int $x, int $z, BatchPacket $payload){ - $this->timings->syncChunkSendTimer->startTiming(); - - $index = Level::chunkHash($x, $z); - unset($this->chunkSendTasks[$index]); - - $this->chunkCache[$index] = $payload; - $this->sendChunkFromCache($x, $z); - if(!$this->server->getMemoryManager()->canUseChunkCache()){ - unset($this->chunkCache[$index]); - } - - $this->timings->syncChunkSendTimer->stopTiming(); - } - - /** - * @return void - * @throws LevelException - */ - public function addEntity(Entity $entity){ - if($entity->isClosed()){ - throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world"); - } - if($entity->getLevel() !== $this){ - throw new LevelException("Invalid Entity world"); - } - - if($entity instanceof Player){ - $this->players[$entity->getId()] = $entity; - } - $this->entities[$entity->getId()] = $entity; - } - - /** - * Removes the entity from the level index - * - * @return void - * @throws LevelException - */ - public function removeEntity(Entity $entity){ - if($entity->getLevel() !== $this){ - throw new LevelException("Invalid Entity world"); - } - - if($entity instanceof Player){ - unset($this->players[$entity->getId()]); - $this->checkSleep(); - } - - unset($this->entities[$entity->getId()]); - unset($this->updateEntities[$entity->getId()]); - } - - /** - * @return void - * @throws LevelException - */ - public function addTile(Tile $tile){ - if($tile->isClosed()){ - throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world"); - } - if($tile->getLevel() !== $this){ - throw new LevelException("Invalid Tile world"); - } - - $chunkX = $tile->getFloorX() >> 4; - $chunkZ = $tile->getFloorZ() >> 4; - - if(isset($this->chunks[$hash = Level::chunkHash($chunkX, $chunkZ)])){ - $this->chunks[$hash]->addTile($tile); - }else{ - throw new \InvalidStateException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ"); - } - - $this->tiles[$tile->getId()] = $tile; - $this->clearChunkCache($chunkX, $chunkZ); - } - - /** - * @return void - * @throws LevelException - */ - public function removeTile(Tile $tile){ - if($tile->getLevel() !== $this){ - throw new LevelException("Invalid Tile world"); - } - - unset($this->tiles[$tile->getId()], $this->updateTiles[$tile->getId()]); - - $chunkX = $tile->getFloorX() >> 4; - $chunkZ = $tile->getFloorZ() >> 4; - - if(isset($this->chunks[$hash = Level::chunkHash($chunkX, $chunkZ)])){ - $this->chunks[$hash]->removeTile($tile); - } - $this->clearChunkCache($chunkX, $chunkZ); - } - - public function isChunkInUse(int $x, int $z) : bool{ - return isset($this->chunkLoaders[$index = Level::chunkHash($x, $z)]) and count($this->chunkLoaders[$index]) > 0; - } - - /** - * Attempts to load a chunk from the level provider (if not already loaded). - * - * @param bool $create Whether to create an empty chunk to load if the chunk cannot be loaded from disk. - * - * @return bool if loading the chunk was successful - * - * @throws \InvalidStateException - */ - public function loadChunk(int $x, int $z, bool $create = true) : bool{ - if(isset($this->chunks[$chunkHash = Level::chunkHash($x, $z)])){ - return true; - } - - $this->timings->syncChunkLoadTimer->startTiming(); - - $this->cancelUnloadChunkRequest($x, $z); - - $this->timings->syncChunkLoadDataTimer->startTiming(); - - $chunk = null; - - try{ - $chunk = $this->provider->loadChunk($x, $z); - }catch(CorruptedChunkException | UnsupportedChunkFormatException $e){ - $logger = $this->server->getLogger(); - $logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage()); - } - - if($chunk === null and $create){ - $chunk = new Chunk($x, $z); - } - - $this->timings->syncChunkLoadDataTimer->stopTiming(); - - if($chunk === null){ - $this->timings->syncChunkLoadTimer->stopTiming(); - return false; - } - - $this->chunks[$chunkHash] = $chunk; - unset($this->blockCache[$chunkHash]); - - $chunk->initChunk($this); - - (new ChunkLoadEvent($this, $chunk, !$chunk->isGenerated()))->call(); - - if(!$chunk->isLightPopulated() and $chunk->isPopulated() and $this->getServer()->getProperty("chunk-ticking.light-updates", false)){ - $this->getServer()->getAsyncPool()->submitTask(new LightPopulationTask($this, $chunk)); - } - - if($this->isChunkInUse($x, $z)){ - foreach($this->getChunkLoaders($x, $z) as $loader){ - $loader->onChunkLoaded($chunk); - } - }else{ - $this->server->getLogger()->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity"); - $this->unloadChunkRequest($x, $z); - } - - $this->timings->syncChunkLoadTimer->stopTiming(); - - return true; - } - - private function queueUnloadChunk(int $x, int $z) : void{ - $this->unloadQueue[$index = Level::chunkHash($x, $z)] = microtime(true); - unset($this->chunkTickList[$index]); - } - - /** - * @return bool - */ - public function unloadChunkRequest(int $x, int $z, bool $safe = true){ - if(($safe and $this->isChunkInUse($x, $z)) or $this->isSpawnChunk($x, $z)){ - return false; - } - - $this->queueUnloadChunk($x, $z); - - return true; - } - - /** - * @return void - */ - public function cancelUnloadChunkRequest(int $x, int $z){ - unset($this->unloadQueue[Level::chunkHash($x, $z)]); - } - - public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{ - if($safe and $this->isChunkInUse($x, $z)){ - return false; - } - - if(!$this->isChunkLoaded($x, $z)){ - return true; - } - - $this->timings->doChunkUnload->startTiming(); - - $chunkHash = Level::chunkHash($x, $z); - - $chunk = $this->chunks[$chunkHash] ?? null; - - if($chunk !== null){ - $ev = new ChunkUnloadEvent($this, $chunk); - $ev->call(); - if($ev->isCancelled()){ - $this->timings->doChunkUnload->stopTiming(); - - return false; - } - - if($trySave and $this->getAutoSave() and $chunk->isGenerated()){ - if($chunk->hasChanged() or count($chunk->getTiles()) > 0 or count($chunk->getSavableEntities()) > 0){ - $this->timings->syncChunkSaveTimer->startTiming(); - try{ - $this->provider->saveChunk($chunk); - }finally{ - $this->timings->syncChunkSaveTimer->stopTiming(); - } - } - } - - foreach($this->getChunkLoaders($x, $z) as $loader){ - $loader->onChunkUnloaded($chunk); - } - - $chunk->onUnload(); - } - - unset($this->chunks[$chunkHash]); - unset($this->chunkTickList[$chunkHash]); - unset($this->chunkCache[$chunkHash]); - unset($this->blockCache[$chunkHash]); - unset($this->changedBlocks[$chunkHash]); - unset($this->chunkSendQueue[$chunkHash]); - unset($this->chunkSendTasks[$chunkHash]); - - $this->timings->doChunkUnload->stopTiming(); - - return true; - } - - /** - * Returns whether the chunk at the specified coordinates is a spawn chunk - */ - public function isSpawnChunk(int $X, int $Z) : bool{ - $spawn = $this->provider->getSpawn(); - $spawnX = $spawn->x >> 4; - $spawnZ = $spawn->z >> 4; - - return abs($X - $spawnX) <= 1 and abs($Z - $spawnZ) <= 1; - } - - public function getSafeSpawn(?Vector3 $spawn = null) : Position{ - if(!($spawn instanceof Vector3) or $spawn->y < 1){ - $spawn = $this->getSpawnLocation(); - } - - $max = $this->worldHeight; - $v = $spawn->floor(); - $chunk = $this->getChunkAtPosition($v, false); - $x = (int) $v->x; - $z = (int) $v->z; - if($chunk !== null and $chunk->isGenerated()){ - $y = (int) min($max - 2, $v->y); - $wasAir = ($chunk->getBlockId($x & 0x0f, $y - 1, $z & 0x0f) === 0); - for(; $y > 0; --$y){ - if($this->isFullBlock($this->getBlockAt($x, $y, $z))){ - if($wasAir){ - $y++; - break; - } - }else{ - $wasAir = true; - } - } - - for(; $y >= 0 and $y < $max; ++$y){ - if(!$this->isFullBlock($this->getBlockAt($x, $y + 1, $z))){ - if(!$this->isFullBlock($this->getBlockAt($x, $y, $z))){ - return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this); - } - }else{ - ++$y; - } - } - - $v->y = $y; - } - - return new Position($spawn->x, $v->y, $spawn->z, $this); - } - - /** - * Gets the current time - */ - public function getTime() : int{ - return $this->time; - } - - /** - * Returns the current time of day - */ - public function getTimeOfDay() : int{ - return $this->time % self::TIME_FULL; - } - - /** - * Returns the Level name - */ - public function getName() : string{ - return $this->displayName; - } - - /** - * Returns the Level folder name - */ - public function getFolderName() : string{ - return $this->folderName; - } - - /** - * Sets the current time on the level - * - * @return void - */ - public function setTime(int $time){ - $this->time = $time; - $this->sendTime(); - } - - /** - * Stops the time for the level, will not save the lock state to disk - * - * @return void - */ - public function stopTime(){ - $this->stopTime = true; - $this->sendTime(); - } - - /** - * Start the time again, if it was stopped - * - * @return void - */ - public function startTime(){ - $this->stopTime = false; - $this->sendTime(); - } - - /** - * Gets the level seed - */ - public function getSeed() : int{ - return $this->provider->getSeed(); - } - - /** - * Sets the seed for the level - * - * @return void - */ - public function setSeed(int $seed){ - $this->provider->setSeed($seed); - } - - public function getWorldHeight() : int{ - return $this->worldHeight; - } - - public function getDifficulty() : int{ - return $this->provider->getDifficulty(); - } - - /** - * @return void - */ - public function setDifficulty(int $difficulty){ - if($difficulty < 0 or $difficulty > 3){ - throw new \InvalidArgumentException("Invalid difficulty level $difficulty"); - } - $this->provider->setDifficulty($difficulty); - - $this->sendDifficulty(); - } - - /** - * @param Player ...$targets - * - * @return void - */ - public function sendDifficulty(Player ...$targets){ - if(count($targets) === 0){ - $targets = $this->getPlayers(); - } - - $pk = new SetDifficultyPacket(); - $pk->difficulty = $this->getDifficulty(); - $this->server->broadcastPacket($targets, $pk); - } - - public function populateChunk(int $x, int $z, bool $force = false) : bool{ - if(isset($this->chunkPopulationQueue[$index = Level::chunkHash($x, $z)]) or (count($this->chunkPopulationQueue) >= $this->chunkPopulationQueueSize and !$force)){ - return false; - } - for($xx = -1; $xx <= 1; ++$xx){ - for($zz = -1; $zz <= 1; ++$zz){ - if(isset($this->chunkPopulationLock[Level::chunkHash($x + $xx, $z + $zz)])){ - return false; - } - } - } - - $chunk = $this->getChunk($x, $z, true); - if(!$chunk->isPopulated()){ - Timings::$populationTimer->startTiming(); - - $this->chunkPopulationQueue[$index] = true; - for($xx = -1; $xx <= 1; ++$xx){ - for($zz = -1; $zz <= 1; ++$zz){ - $this->chunkPopulationLock[Level::chunkHash($x + $xx, $z + $zz)] = true; - } - } - - $task = new PopulationTask($this, $chunk); - $workerId = $this->server->getAsyncPool()->selectWorker(); - if(!isset($this->generatorRegisteredWorkers[$workerId])){ - $this->registerGeneratorToWorker($workerId); - } - $this->server->getAsyncPool()->submitTaskToWorker($task, $workerId); - - Timings::$populationTimer->stopTiming(); - return false; - } - - return true; - } - - /** - * @return void - */ - public function doChunkGarbageCollection(){ - $this->timings->doChunkGC->startTiming(); - - foreach($this->chunks as $index => $chunk){ - if(!isset($this->unloadQueue[$index])){ - Level::getXZ($index, $X, $Z); - if(!$this->isSpawnChunk($X, $Z)){ - $this->unloadChunkRequest($X, $Z, true); - } - } - $chunk->collectGarbage(); - } - - $this->provider->doGarbageCollection(); - - $this->timings->doChunkGC->stopTiming(); - } - - /** - * @return void - */ - public function unloadChunks(bool $force = false){ - if(count($this->unloadQueue) > 0){ - $maxUnload = 96; - $now = microtime(true); - foreach($this->unloadQueue as $index => $time){ - Level::getXZ($index, $X, $Z); - - if(!$force){ - if($maxUnload <= 0){ - break; - }elseif($time > ($now - 30)){ - continue; - } - } - - //If the chunk can't be unloaded, it stays on the queue - if($this->unloadChunk($X, $Z, true)){ - unset($this->unloadQueue[$index]); - --$maxUnload; - } - } - } - } - - public function setMetadata(string $metadataKey, MetadataValue $newMetadataValue){ - $this->server->getLevelMetadata()->setMetadata($this, $metadataKey, $newMetadataValue); - } - - public function getMetadata(string $metadataKey){ - return $this->server->getLevelMetadata()->getMetadata($this, $metadataKey); - } - - public function hasMetadata(string $metadataKey) : bool{ - return $this->server->getLevelMetadata()->hasMetadata($this, $metadataKey); - } - - public function removeMetadata(string $metadataKey, Plugin $owningPlugin){ - $this->server->getLevelMetadata()->removeMetadata($this, $metadataKey, $owningPlugin); - } -} diff --git a/src/pocketmine/level/LevelTimings.php b/src/pocketmine/level/LevelTimings.php deleted file mode 100644 index 5b7defdbed..0000000000 --- a/src/pocketmine/level/LevelTimings.php +++ /dev/null @@ -1,95 +0,0 @@ -getFolderName() . " - "; - - $this->setBlock = new TimingsHandler("** " . $name . "setBlock"); - $this->doBlockLightUpdates = new TimingsHandler("** " . $name . "doBlockLightUpdates"); - $this->doBlockSkyLightUpdates = new TimingsHandler("** " . $name . "doBlockSkyLightUpdates"); - - $this->doChunkUnload = new TimingsHandler("** " . $name . "doChunkUnload"); - $this->doTickPending = new TimingsHandler("** " . $name . "doTickPending"); - $this->doTickTiles = new TimingsHandler("** " . $name . "doTickTiles"); - $this->doChunkGC = new TimingsHandler("** " . $name . "doChunkGC"); - $this->entityTick = new TimingsHandler("** " . $name . "entityTick"); - $this->tileEntityTick = new TimingsHandler("** " . $name . "tileEntityTick"); - - Timings::init(); //make sure the timers we want are available - $this->syncChunkSendTimer = new TimingsHandler("** " . $name . "syncChunkSend", Timings::$playerChunkSendTimer); - $this->syncChunkSendPrepareTimer = new TimingsHandler("** " . $name . "syncChunkSendPrepare", Timings::$playerChunkSendTimer); - - $this->syncChunkLoadTimer = new TimingsHandler("** " . $name . "syncChunkLoad", Timings::$worldLoadTimer); - $this->syncChunkLoadDataTimer = new TimingsHandler("** " . $name . "syncChunkLoad - Data"); - $this->syncChunkLoadEntitiesTimer = new TimingsHandler("** " . $name . "syncChunkLoad - Entities"); - $this->syncChunkLoadTileEntitiesTimer = new TimingsHandler("** " . $name . "syncChunkLoad - TileEntities"); - $this->syncChunkSaveTimer = new TimingsHandler("** " . $name . "syncChunkSave", Timings::$worldSaveTimer); - - $this->doTick = new TimingsHandler($name . "doTick"); - } -} diff --git a/src/pocketmine/level/SimpleChunkManager.php b/src/pocketmine/level/SimpleChunkManager.php deleted file mode 100644 index 3f8b46ea3c..0000000000 --- a/src/pocketmine/level/SimpleChunkManager.php +++ /dev/null @@ -1,169 +0,0 @@ -seed = $seed; - $this->worldHeight = $worldHeight; - } - - /** - * Gets the raw block id. - * - * @return int 0-255 - */ - public function getBlockIdAt(int $x, int $y, int $z) : int{ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - return $chunk->getBlockId($x & 0xf, $y, $z & 0xf); - } - return 0; - } - - /** - * Sets the raw block id. - * - * @param int $id 0-255 - * - * @return void - */ - public function setBlockIdAt(int $x, int $y, int $z, int $id){ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - $chunk->setBlockId($x & 0xf, $y, $z & 0xf, $id); - } - } - - /** - * Gets the raw block metadata - * - * @return int 0-15 - */ - public function getBlockDataAt(int $x, int $y, int $z) : int{ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - return $chunk->getBlockData($x & 0xf, $y, $z & 0xf); - } - return 0; - } - - /** - * Sets the raw block metadata. - * - * @param int $data 0-15 - * - * @return void - */ - public function setBlockDataAt(int $x, int $y, int $z, int $data){ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - $chunk->setBlockData($x & 0xf, $y, $z & 0xf, $data); - } - } - - public function getBlockLightAt(int $x, int $y, int $z) : int{ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - return $chunk->getBlockLight($x & 0xf, $y, $z & 0xf); - } - - return 0; - } - - public function setBlockLightAt(int $x, int $y, int $z, int $level){ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - $chunk->setBlockLight($x & 0xf, $y, $z & 0xf, $level); - } - } - - public function getBlockSkyLightAt(int $x, int $y, int $z) : int{ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - return $chunk->getBlockSkyLight($x & 0xf, $y, $z & 0xf); - } - - return 0; - } - - public function setBlockSkyLightAt(int $x, int $y, int $z, int $level){ - if(($chunk = $this->getChunk($x >> 4, $z >> 4)) !== null){ - $chunk->setBlockSkyLight($x & 0xf, $y, $z & 0xf, $level); - } - } - - /** - * @return Chunk|null - */ - public function getChunk(int $chunkX, int $chunkZ){ - return $this->chunks[Level::chunkHash($chunkX, $chunkZ)] ?? null; - } - - /** - * @return void - */ - public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk = null){ - if($chunk === null){ - unset($this->chunks[Level::chunkHash($chunkX, $chunkZ)]); - return; - } - $this->chunks[Level::chunkHash($chunkX, $chunkZ)] = $chunk; - } - - /** - * @return void - */ - public function cleanChunks(){ - $this->chunks = []; - } - - /** - * Gets the level seed - */ - public function getSeed() : int{ - return $this->seed; - } - - public function getWorldHeight() : int{ - return $this->worldHeight; - } - - public function isInWorld(int $x, int $y, int $z) : bool{ - return ( - $x <= INT32_MAX and $x >= INT32_MIN and - $y < $this->worldHeight and $y >= 0 and - $z <= INT32_MAX and $z >= INT32_MIN - ); - } -} diff --git a/src/pocketmine/level/biome/Biome.php b/src/pocketmine/level/biome/Biome.php deleted file mode 100644 index 09a38bcaaa..0000000000 --- a/src/pocketmine/level/biome/Biome.php +++ /dev/null @@ -1,203 +0,0 @@ - - */ - private static $biomes; - - /** @var int */ - private $id; - /** @var bool */ - private $registered = false; - - /** @var Populator[] */ - private $populators = []; - - /** @var int */ - private $minElevation; - /** @var int */ - private $maxElevation; - - /** @var Block[] */ - private $groundCover = []; - - /** @var float */ - protected $rainfall = 0.5; - /** @var float */ - protected $temperature = 0.5; - - /** - * @return void - */ - protected static function register(int $id, Biome $biome){ - self::$biomes[$id] = $biome; - $biome->setId($id); - } - - /** - * @return void - */ - public static function init(){ - self::$biomes = new \SplFixedArray(self::MAX_BIOMES); - - self::register(self::OCEAN, new OceanBiome()); - self::register(self::PLAINS, new PlainBiome()); - self::register(self::DESERT, new DesertBiome()); - self::register(self::MOUNTAINS, new MountainsBiome()); - self::register(self::FOREST, new ForestBiome()); - self::register(self::TAIGA, new TaigaBiome()); - self::register(self::SWAMP, new SwampBiome()); - self::register(self::RIVER, new RiverBiome()); - - self::register(self::HELL, new HellBiome()); - - self::register(self::ICE_PLAINS, new IcePlainsBiome()); - - self::register(self::SMALL_MOUNTAINS, new SmallMountainsBiome()); - - self::register(self::BIRCH_FOREST, new ForestBiome(ForestBiome::TYPE_BIRCH)); - } - - public static function getBiome(int $id) : Biome{ - if(self::$biomes[$id] === null){ - self::register($id, new UnknownBiome()); - } - return self::$biomes[$id]; - } - - /** - * @return void - */ - public function clearPopulators(){ - $this->populators = []; - } - - /** - * @return void - */ - public function addPopulator(Populator $populator){ - $this->populators[] = $populator; - } - - /** - * @return void - */ - public function populateChunk(ChunkManager $level, int $chunkX, int $chunkZ, Random $random){ - foreach($this->populators as $populator){ - $populator->populate($level, $chunkX, $chunkZ, $random); - } - } - - /** - * @return Populator[] - */ - public function getPopulators() : array{ - return $this->populators; - } - - /** - * @return void - */ - public function setId(int $id){ - if(!$this->registered){ - $this->registered = true; - $this->id = $id; - } - } - - public function getId() : int{ - return $this->id; - } - - abstract public function getName() : string; - - public function getMinElevation() : int{ - return $this->minElevation; - } - - public function getMaxElevation() : int{ - return $this->maxElevation; - } - - /** - * @return void - */ - public function setElevation(int $min, int $max){ - $this->minElevation = $min; - $this->maxElevation = $max; - } - - /** - * @return Block[] - */ - public function getGroundCover() : array{ - return $this->groundCover; - } - - /** - * @param Block[] $covers - * - * @return void - */ - public function setGroundCover(array $covers){ - $this->groundCover = $covers; - } - - public function getTemperature() : float{ - return $this->temperature; - } - - public function getRainfall() : float{ - return $this->rainfall; - } -} diff --git a/src/pocketmine/level/format/Chunk.php b/src/pocketmine/level/format/Chunk.php deleted file mode 100644 index d20f08725c..0000000000 --- a/src/pocketmine/level/format/Chunk.php +++ /dev/null @@ -1,1011 +0,0 @@ - - */ - protected $subChunks; - - /** @var EmptySubChunk */ - protected $emptySubChunk; - - /** @var Tile[] */ - protected $tiles = []; - /** @var Tile[] */ - protected $tileList = []; - - /** @var Entity[] */ - protected $entities = []; - - /** - * @var \SplFixedArray|int[] - * @phpstan-var \SplFixedArray - */ - protected $heightMap; - - /** @var string */ - protected $biomeIds; - - /** @var CompoundTag[] */ - protected $NBTtiles = []; - - /** @var CompoundTag[] */ - protected $NBTentities = []; - - /** - * @param SubChunkInterface[] $subChunks - * @param CompoundTag[] $entities - * @param CompoundTag[] $tiles - * @param int[] $heightMap - * @phpstan-param list $heightMap - */ - public function __construct(int $chunkX, int $chunkZ, array $subChunks = [], array $entities = [], array $tiles = [], string $biomeIds = "", array $heightMap = []){ - $this->x = $chunkX; - $this->z = $chunkZ; - - $this->height = Chunk::MAX_SUBCHUNKS; //TODO: add a way of changing this - - $this->subChunks = new \SplFixedArray($this->height); - $this->emptySubChunk = EmptySubChunk::getInstance(); - - foreach($this->subChunks as $y => $null){ - $this->subChunks[$y] = $subChunks[$y] ?? $this->emptySubChunk; - } - - if(count($heightMap) === 256){ - $this->heightMap = \SplFixedArray::fromArray($heightMap); - }else{ - assert(count($heightMap) === 0, "Wrong HeightMap value count, expected 256, got " . count($heightMap)); - $val = ($this->height * 16); - $this->heightMap = \SplFixedArray::fromArray(array_fill(0, 256, $val)); - } - - if(strlen($biomeIds) === 256){ - $this->biomeIds = $biomeIds; - }else{ - assert($biomeIds === "", "Wrong BiomeIds value count, expected 256, got " . strlen($biomeIds)); - $this->biomeIds = str_repeat("\x00", 256); - } - - $this->NBTtiles = $tiles; - $this->NBTentities = $entities; - } - - public function getX() : int{ - return $this->x; - } - - public function getZ() : int{ - return $this->z; - } - - /** - * @return void - */ - public function setX(int $x){ - $this->x = $x; - } - - /** - * @return void - */ - public function setZ(int $z){ - $this->z = $z; - } - - /** - * Returns the chunk height in count of subchunks. - */ - public function getHeight() : int{ - return $this->height; - } - - /** - * Returns a bitmap of block ID and meta at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return int bitmap, (id << 4) | meta - */ - public function getFullBlock(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getFullBlock($x, $y & 0x0f, $z); - } - - /** - * Sets block ID and meta in one call at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * @param int|null $blockId 0-255 if null, does not change - * @param int|null $meta 0-15 if null, does not change - */ - public function setBlock(int $x, int $y, int $z, ?int $blockId = null, ?int $meta = null) : bool{ - if($this->getSubChunk($y >> 4, true)->setBlock($x, $y & 0x0f, $z, $blockId !== null ? ($blockId & 0xff) : null, $meta !== null ? ($meta & 0x0f) : null)){ - $this->hasChanged = true; - return true; - } - return false; - } - - /** - * Returns the block ID at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return int 0-255 - */ - public function getBlockId(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockId($x, $y & 0x0f, $z); - } - - /** - * Sets the block ID at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * @param int $id 0-255 - * - * @return void - */ - public function setBlockId(int $x, int $y, int $z, int $id){ - if($this->getSubChunk($y >> 4, true)->setBlockId($x, $y & 0x0f, $z, $id)){ - $this->hasChanged = true; - } - } - - /** - * Returns the block meta value at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return int 0-15 - */ - public function getBlockData(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockData($x, $y & 0x0f, $z); - } - - /** - * Sets the block meta value at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * @param int $data 0-15 - * - * @return void - */ - public function setBlockData(int $x, int $y, int $z, int $data){ - if($this->getSubChunk($y >> 4, true)->setBlockData($x, $y & 0x0f, $z, $data)){ - $this->hasChanged = true; - } - } - - /** - * Returns the sky light level at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return int 0-15 - */ - public function getBlockSkyLight(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockSkyLight($x, $y & 0x0f, $z); - } - - /** - * Sets the sky light level at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * @param int $level 0-15 - * - * @return void - */ - public function setBlockSkyLight(int $x, int $y, int $z, int $level){ - if($this->getSubChunk($y >> 4, true)->setBlockSkyLight($x, $y & 0x0f, $z, $level)){ - $this->hasChanged = true; - } - } - - /** - * @return void - */ - public function setAllBlockSkyLight(int $level){ - $char = chr(($level & 0x0f) | ($level << 4)); - $data = str_repeat($char, 2048); - for($y = $this->getHighestSubChunkIndex(); $y >= 0; --$y){ - $this->getSubChunk($y, true)->setBlockSkyLightArray($data); - } - } - - /** - * Returns the block light level at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return int 0-15 - */ - public function getBlockLight(int $x, int $y, int $z) : int{ - return $this->getSubChunk($y >> 4)->getBlockLight($x, $y & 0x0f, $z); - } - - /** - * Sets the block light level at the specified chunk block coordinates - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * @param int $level 0-15 - * - * @return void - */ - public function setBlockLight(int $x, int $y, int $z, int $level){ - if($this->getSubChunk($y >> 4, true)->setBlockLight($x, $y & 0x0f, $z, $level)){ - $this->hasChanged = true; - } - } - - /** - * @return void - */ - public function setAllBlockLight(int $level){ - $char = chr(($level & 0x0f) | ($level << 4)); - $data = str_repeat($char, 2048); - for($y = $this->getHighestSubChunkIndex(); $y >= 0; --$y){ - $this->getSubChunk($y, true)->setBlockLightArray($data); - } - } - - /** - * Returns the Y coordinate of the highest non-air block at the specified X/Z chunk block coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - * - * @return int 0-255, or -1 if there are no blocks in the column - */ - public function getHighestBlockAt(int $x, int $z) : int{ - $index = $this->getHighestSubChunkIndex(); - if($index === -1){ - return -1; - } - - for($y = $index; $y >= 0; --$y){ - $height = $this->getSubChunk($y)->getHighestBlockAt($x, $z) | ($y << 4); - if($height !== -1){ - return $height; - } - } - - return -1; - } - - public function getMaxY() : int{ - return ($this->getHighestSubChunkIndex() << 4) | 0x0f; - } - - /** - * Returns the heightmap value at the specified X/Z chunk block coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - */ - public function getHeightMap(int $x, int $z) : int{ - return $this->heightMap[($z << 4) | $x]; - } - - /** - * Returns the heightmap value at the specified X/Z chunk block coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - * - * @return void - */ - public function setHeightMap(int $x, int $z, int $value){ - $this->heightMap[($z << 4) | $x] = $value; - } - - /** - * Recalculates the heightmap for the whole chunk. - * - * @return void - */ - public function recalculateHeightMap(){ - for($z = 0; $z < 16; ++$z){ - for($x = 0; $x < 16; ++$x){ - $this->recalculateHeightMapColumn($x, $z); - } - } - } - - /** - * Recalculates the heightmap for the block column at the specified X/Z chunk coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - * - * @return int New calculated heightmap value (0-256 inclusive) - */ - public function recalculateHeightMapColumn(int $x, int $z) : int{ - $y = $this->getHighestBlockAt($x, $z); - for(; $y >= 0; --$y){ - if(BlockFactory::$lightFilter[$id = $this->getBlockId($x, $y, $z)] > 1 or BlockFactory::$diffusesSkyLight[$id]){ - break; - } - } - - $this->setHeightMap($x, $z, $y + 1); - return $y + 1; - } - - /** - * Performs basic sky light population on the chunk. - * This does not cater for adjacent sky light, this performs direct sky light population only. This may cause some strange visual artifacts - * if the chunk is light-populated after being terrain-populated. - * - * TODO: fast adjacent light spread - * - * @return void - */ - public function populateSkyLight(){ - $maxY = $this->getMaxY(); - - $this->setAllBlockSkyLight(0); - - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - $y = $maxY; - $heightMap = $this->getHeightMap($x, $z); - for(; $y >= $heightMap; --$y){ - $this->setBlockSkyLight($x, $y, $z, 15); - } - - $light = 15; - for(; $y >= 0; --$y){ - $light -= BlockFactory::$lightFilter[$this->getBlockId($x, $y, $z)]; - if($light <= 0){ - break; - } - $this->setBlockSkyLight($x, $y, $z, $light); - } - } - } - } - - /** - * Returns the biome ID at the specified X/Z chunk block coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - * - * @return int 0-255 - */ - public function getBiomeId(int $x, int $z) : int{ - return ord($this->biomeIds[($z << 4) | $x]); - } - - /** - * Sets the biome ID at the specified X/Z chunk block coordinates - * - * @param int $x 0-15 - * @param int $z 0-15 - * @param int $biomeId 0-255 - * - * @return void - */ - public function setBiomeId(int $x, int $z, int $biomeId){ - $this->hasChanged = true; - $this->biomeIds[($z << 4) | $x] = chr($biomeId & 0xff); - } - - /** - * Returns a column of block IDs from bottom to top at the specified X/Z chunk block coordinates. - * - * @param int $x 0-15 - * @param int $z 0-15 - */ - public function getBlockIdColumn(int $x, int $z) : string{ - $result = ""; - foreach($this->subChunks as $subChunk){ - $result .= $subChunk->getBlockIdColumn($x, $z); - } - - return $result; - } - - /** - * Returns a column of block meta values from bottom to top at the specified X/Z chunk block coordinates. - * - * @param int $x 0-15 - * @param int $z 0-15 - */ - public function getBlockDataColumn(int $x, int $z) : string{ - $result = ""; - foreach($this->subChunks as $subChunk){ - $result .= $subChunk->getBlockDataColumn($x, $z); - } - return $result; - } - - /** - * Returns a column of sky light values from bottom to top at the specified X/Z chunk block coordinates. - * - * @param int $x 0-15 - * @param int $z 0-15 - */ - public function getBlockSkyLightColumn(int $x, int $z) : string{ - $result = ""; - foreach($this->subChunks as $subChunk){ - $result .= $subChunk->getBlockSkyLightColumn($x, $z); - } - return $result; - } - - /** - * Returns a column of block light values from bottom to top at the specified X/Z chunk block coordinates. - * - * @param int $x 0-15 - * @param int $z 0-15 - */ - public function getBlockLightColumn(int $x, int $z) : string{ - $result = ""; - foreach($this->subChunks as $subChunk){ - $result .= $subChunk->getBlockLightColumn($x, $z); - } - return $result; - } - - public function isLightPopulated() : bool{ - return $this->lightPopulated; - } - - /** - * @return void - */ - public function setLightPopulated(bool $value = true){ - $this->lightPopulated = $value; - $this->hasChanged = true; - } - - public function isPopulated() : bool{ - return $this->terrainPopulated; - } - - /** - * @return void - */ - public function setPopulated(bool $value = true){ - $this->terrainPopulated = $value; - $this->hasChanged = true; - } - - public function isGenerated() : bool{ - return $this->terrainGenerated; - } - - /** - * @return void - */ - public function setGenerated(bool $value = true){ - $this->terrainGenerated = $value; - $this->hasChanged = true; - } - - /** - * @return void - */ - public function addEntity(Entity $entity){ - if($entity->isClosed()){ - throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to a chunk"); - } - $this->entities[$entity->getId()] = $entity; - if(!($entity instanceof Player) and $this->isInit){ - $this->hasChanged = true; - } - } - - /** - * @return void - */ - public function removeEntity(Entity $entity){ - unset($this->entities[$entity->getId()]); - if(!($entity instanceof Player) and $this->isInit){ - $this->hasChanged = true; - } - } - - /** - * @return void - */ - public function addTile(Tile $tile){ - if($tile->isClosed()){ - throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to a chunk"); - } - $this->tiles[$tile->getId()] = $tile; - if(isset($this->tileList[$index = (($tile->x & 0x0f) << 12) | (($tile->z & 0x0f) << 8) | ($tile->y & 0xff)]) and $this->tileList[$index] !== $tile){ - $this->tileList[$index]->close(); - } - $this->tileList[$index] = $tile; - if($this->isInit){ - $this->hasChanged = true; - } - } - - /** - * @return void - */ - public function removeTile(Tile $tile){ - unset($this->tiles[$tile->getId()]); - unset($this->tileList[(($tile->x & 0x0f) << 12) | (($tile->z & 0x0f) << 8) | ($tile->y & 0xff)]); - if($this->isInit){ - $this->hasChanged = true; - } - } - - /** - * Returns an array of entities currently using this chunk. - * - * @return Entity[] - */ - public function getEntities() : array{ - return $this->entities; - } - - /** - * @return Entity[] - */ - public function getSavableEntities() : array{ - return array_filter($this->entities, function(Entity $entity) : bool{ return $entity->canSaveWithChunk() and !$entity->isClosed(); }); - } - - /** - * @return Tile[] - */ - public function getTiles() : array{ - return $this->tiles; - } - - /** - * Returns the tile at the specified chunk block coordinates, or null if no tile exists. - * - * @param int $x 0-15 - * @param int $y 0-255 - * @param int $z 0-15 - * - * @return Tile|null - */ - public function getTile(int $x, int $y, int $z){ - $index = ($x << 12) | ($z << 8) | $y; - return $this->tileList[$index] ?? null; - } - - /** - * Called when the chunk is unloaded, closing entities and tiles. - */ - public function onUnload() : void{ - foreach($this->getEntities() as $entity){ - if($entity instanceof Player){ - continue; - } - $entity->close(); - } - - foreach($this->getTiles() as $tile){ - $tile->close(); - } - } - - /** - * Deserializes tiles and entities from NBT - * - * @return void - */ - public function initChunk(Level $level){ - if(!$this->isInit){ - $changed = false; - - $level->timings->syncChunkLoadEntitiesTimer->startTiming(); - foreach($this->NBTentities as $nbt){ - $idTag = $nbt->getTag("id"); - if(!($idTag instanceof IntTag) && !($idTag instanceof StringTag)){ //allow mixed types (because of leveldb) - $changed = true; - continue; - } - - try{ - $entity = Entity::createEntity($idTag->getValue(), $level, $nbt); - if(!($entity instanceof Entity)){ - $changed = true; - continue; - } - }catch(\Throwable $t){ - $level->getServer()->getLogger()->logException($t); - $changed = true; - continue; - } - } - $this->NBTentities = []; - $level->timings->syncChunkLoadEntitiesTimer->stopTiming(); - - $level->timings->syncChunkLoadTileEntitiesTimer->startTiming(); - foreach($this->NBTtiles as $nbt){ - if(!$nbt->hasTag(Tile::TAG_ID, StringTag::class)){ - $changed = true; - continue; - } - - if(Tile::createTile($nbt->getString(Tile::TAG_ID), $level, $nbt) === null){ - $changed = true; - continue; - } - } - - $this->NBTtiles = []; - $level->timings->syncChunkLoadTileEntitiesTimer->stopTiming(); - - $this->hasChanged = $changed; - - $this->isInit = true; - } - } - - public function getBiomeIdArray() : string{ - return $this->biomeIds; - } - - /** - * @return int[] - */ - public function getHeightMapArray() : array{ - return $this->heightMap->toArray(); - } - - public function hasChanged() : bool{ - return $this->hasChanged; - } - - /** - * @return void - */ - public function setChanged(bool $value = true){ - $this->hasChanged = $value; - } - - /** - * Returns the subchunk at the specified subchunk Y coordinate, or an empty, unmodifiable stub if it does not exist or the coordinate is out of range. - * - * @param bool $generateNew Whether to create a new, modifiable subchunk if there is not one in place - */ - public function getSubChunk(int $y, bool $generateNew = false) : SubChunkInterface{ - if($y < 0 or $y >= $this->height){ - return $this->emptySubChunk; - }elseif($generateNew and $this->subChunks[$y] instanceof EmptySubChunk){ - $this->subChunks[$y] = new SubChunk(); - } - - return $this->subChunks[$y]; - } - - /** - * Sets a subchunk in the chunk index - * - * @param bool $allowEmpty Whether to check if the chunk is empty, and if so replace it with an empty stub - */ - public function setSubChunk(int $y, SubChunkInterface $subChunk = null, bool $allowEmpty = false) : bool{ - if($y < 0 or $y >= $this->height){ - return false; - } - if($subChunk === null or ($subChunk->isEmpty() and !$allowEmpty)){ - $this->subChunks[$y] = $this->emptySubChunk; - }else{ - $this->subChunks[$y] = $subChunk; - } - $this->hasChanged = true; - return true; - } - - /** - * @return \SplFixedArray|SubChunkInterface[] - * @phpstan-return \SplFixedArray - */ - public function getSubChunks() : \SplFixedArray{ - return $this->subChunks; - } - - /** - * Returns the Y coordinate of the highest non-empty subchunk in this chunk. - */ - public function getHighestSubChunkIndex() : int{ - for($y = $this->subChunks->count() - 1; $y >= 0; --$y){ - if($this->subChunks[$y] instanceof EmptySubChunk){ - //No need to thoroughly prune empties at runtime, this will just reduce performance. - continue; - } - return $y; - } - - return -1; - } - - /** - * Returns the count of subchunks that need sending to players - */ - public function getSubChunkSendCount() : int{ - return $this->getHighestSubChunkIndex() + 1; - } - - /** - * Disposes of empty subchunks and frees data where possible - */ - public function collectGarbage() : void{ - foreach($this->subChunks as $y => $subChunk){ - if($subChunk instanceof SubChunk){ - if($subChunk->isEmpty()){ - $this->subChunks[$y] = $this->emptySubChunk; - }else{ - $subChunk->collectGarbage(); - } - } - } - } - - /** - * Serializes the chunk for sending to players - */ - public function networkSerialize(?string $networkSerializedTiles) : string{ - $result = ""; - $subChunkCount = $this->getSubChunkSendCount(); - - //TODO: HACK! fill in fake subchunks to make up for the new negative space client-side - for($y = 0; $y < 4; ++$y){ - $result .= chr(8); //subchunk version 8 - $result .= chr(0); //0 layers - client will treat this as all-air - } - for($y = 0; $y < $subChunkCount; ++$y){ - $result .= $this->subChunks[$y]->networkSerialize(); - } - - //TODO: right now we don't support 3D natively, so we just 3Dify our 2D biomes so they fill the column - $encodedBiomePalette = $this->networkSerializeBiomesAsPalette(); - $result .= str_repeat($encodedBiomePalette, 25); - - $result .= chr(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. - - $result .= $networkSerializedTiles ?? $this->networkSerializeTiles(); - - return $result; - } - - /** - * Serializes all tiles in network format for chunk sending. This is necessary because fastSerialize() doesn't - * include tiles; they have to be encoded on the main thread. - */ - public function networkSerializeTiles() : string{ - $result = ""; - foreach($this->tiles as $tile){ - if($tile instanceof Spawnable){ - $result .= $tile->getSerializedSpawnCompound(); - } - } - - return $result; - } - - private function networkSerializeBiomesAsPalette() : string{ - /** @var string[]|null $biomeIdMap */ - static $biomeIdMap = null; - if($biomeIdMap === null){ - $biomeIdMapRaw = file_get_contents(\pocketmine\RESOURCE_PATH . '/vanilla/biome_id_map.json'); - if($biomeIdMapRaw === false) throw new AssumptionFailedError(); - $biomeIdMapDecoded = json_decode($biomeIdMapRaw, true); - if(!is_array($biomeIdMapDecoded)) throw new AssumptionFailedError(); - $biomeIdMap = array_flip($biomeIdMapDecoded); - } - $biomePalette = new PalettedBlockArray($this->getBiomeId(0, 0)); - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - $biomeId = $this->getBiomeId($x, $z); - if(!isset($biomeIdMap[$biomeId])){ - //make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this - $biomeId = Biome::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; - } - - /** - * Fast-serializes the chunk for passing between threads - * TODO: tiles and entities - */ - public function fastSerialize() : string{ - $stream = new BinaryStream(); - $stream->putInt($this->x); - $stream->putInt($this->z); - $stream->putByte(($this->lightPopulated ? 4 : 0) | ($this->terrainPopulated ? 2 : 0) | ($this->terrainGenerated ? 1 : 0)); - if($this->terrainGenerated){ - //subchunks - $count = 0; - $subChunks = ""; - foreach($this->subChunks as $y => $subChunk){ - if($subChunk instanceof EmptySubChunk){ - continue; - } - ++$count; - $subChunks .= chr($y) . $subChunk->getBlockIdArray() . $subChunk->getBlockDataArray(); - if($this->lightPopulated){ - $subChunks .= $subChunk->getBlockSkyLightArray() . $subChunk->getBlockLightArray(); - } - } - $stream->putByte($count); - $stream->put($subChunks); - - //biomes - $stream->put($this->biomeIds); - if($this->lightPopulated){ - $stream->put(pack("v*", ...$this->heightMap)); - } - } - - return $stream->getBuffer(); - } - - /** - * Deserializes a fast-serialized chunk - */ - public static function fastDeserialize(string $data) : Chunk{ - $stream = new BinaryStream($data); - - $x = $stream->getInt(); - $z = $stream->getInt(); - $flags = $stream->getByte(); - $lightPopulated = (bool) ($flags & 4); - $terrainPopulated = (bool) ($flags & 2); - $terrainGenerated = (bool) ($flags & 1); - - $subChunks = []; - $biomeIds = ""; - $heightMap = []; - if($terrainGenerated){ - $count = $stream->getByte(); - for($y = 0; $y < $count; ++$y){ - $subChunks[$stream->getByte()] = new SubChunk( - $stream->get(4096), //blockids - $stream->get(2048), //blockdata - $lightPopulated ? $stream->get(2048) : "", //skylight - $lightPopulated ? $stream->get(2048) : "" //blocklight - ); - } - - $biomeIds = $stream->get(256); - if($lightPopulated){ - /** @var int[] $unpackedHeightMap */ - $unpackedHeightMap = unpack("v*", $stream->get(512)); //unpack() will never fail here - $heightMap = array_values($unpackedHeightMap); - } - } - - $chunk = new Chunk($x, $z, $subChunks, [], [], $biomeIds, $heightMap); - $chunk->setGenerated($terrainGenerated); - $chunk->setPopulated($terrainPopulated); - $chunk->setLightPopulated($lightPopulated); - $chunk->setChanged(false); - - return $chunk; - } -} diff --git a/src/pocketmine/level/format/EmptySubChunk.php b/src/pocketmine/level/format/EmptySubChunk.php deleted file mode 100644 index 0c5eeb5d57..0000000000 --- a/src/pocketmine/level/format/EmptySubChunk.php +++ /dev/null @@ -1,131 +0,0 @@ -ids = self::assignData($ids, 4096); - $this->data = self::assignData($data, 2048); - $this->skyLight = self::assignData($skyLight, 2048, "\xff"); - $this->blockLight = self::assignData($blockLight, 2048); - $this->collectGarbage(); - } - - public function isEmpty(bool $checkLight = true) : bool{ - return ( - substr_count($this->ids, "\x00") === 4096 and - (!$checkLight or ( - substr_count($this->skyLight, "\xff") === 2048 and - $this->blockLight === self::ZERO_NIBBLE_ARRAY - )) - ); - } - - public function getBlockId(int $x, int $y, int $z) : int{ - return ord($this->ids[($x << 8) | ($z << 4) | $y]); - } - - public function setBlockId(int $x, int $y, int $z, int $id) : bool{ - $this->ids[($x << 8) | ($z << 4) | $y] = chr($id); - return true; - } - - public function getBlockData(int $x, int $y, int $z) : int{ - return (ord($this->data[($x << 7) | ($z << 3) | ($y >> 1)]) >> (($y & 1) << 2)) & 0xf; - } - - public function setBlockData(int $x, int $y, int $z, int $data) : bool{ - $i = ($x << 7) | ($z << 3) | ($y >> 1); - - $shift = ($y & 1) << 2; - $byte = ord($this->data[$i]); - $this->data[$i] = chr(($byte & ~(0xf << $shift)) | (($data & 0xf) << $shift)); - - return true; - } - - public function getFullBlock(int $x, int $y, int $z) : int{ - $i = ($x << 8) | ($z << 4) | $y; - return (ord($this->ids[$i]) << 4) | ((ord($this->data[$i >> 1]) >> (($y & 1) << 2)) & 0xf); - } - - public function setBlock(int $x, int $y, int $z, ?int $id = null, ?int $data = null) : bool{ - $i = ($x << 8) | ($z << 4) | $y; - $changed = false; - if($id !== null){ - $block = chr($id); - if($this->ids[$i] !== $block){ - $this->ids[$i] = $block; - $changed = true; - } - } - - if($data !== null){ - $i >>= 1; - - $shift = ($y & 1) << 2; - $oldPair = ord($this->data[$i]); - $newPair = ($oldPair & ~(0xf << $shift)) | (($data & 0xf) << $shift); - if($newPair !== $oldPair){ - $this->data[$i] = chr($newPair); - $changed = true; - } - } - - return $changed; - } - - public function getBlockLight(int $x, int $y, int $z) : int{ - return (ord($this->blockLight[($x << 7) | ($z << 3) | ($y >> 1)]) >> (($y & 1) << 2)) & 0xf; - } - - public function setBlockLight(int $x, int $y, int $z, int $level) : bool{ - $i = ($x << 7) | ($z << 3) | ($y >> 1); - - $shift = ($y & 1) << 2; - $byte = ord($this->blockLight[$i]); - $this->blockLight[$i] = chr(($byte & ~(0xf << $shift)) | (($level & 0xf) << $shift)); - - return true; - } - - public function getBlockSkyLight(int $x, int $y, int $z) : int{ - return (ord($this->skyLight[($x << 7) | ($z << 3) | ($y >> 1)]) >> (($y & 1) << 2)) & 0xf; - } - - public function setBlockSkyLight(int $x, int $y, int $z, int $level) : bool{ - $i = ($x << 7) | ($z << 3) | ($y >> 1); - - $shift = ($y & 1) << 2; - $byte = ord($this->skyLight[$i]); - $this->skyLight[$i] = chr(($byte & ~(0xf << $shift)) | (($level & 0xf) << $shift)); - - return true; - } - - public function getHighestBlockAt(int $x, int $z) : int{ - $low = ($x << 8) | ($z << 4); - $i = $low | 0x0f; - for(; $i >= $low; --$i){ - if($this->ids[$i] !== "\x00"){ - return $i & 0x0f; - } - } - - return -1; //highest block not in this subchunk - } - - public function getBlockIdColumn(int $x, int $z) : string{ - return substr($this->ids, ($x << 8) | ($z << 4), 16); - } - - public function getBlockDataColumn(int $x, int $z) : string{ - return substr($this->data, ($x << 7) | ($z << 3), 8); - } - - public function getBlockLightColumn(int $x, int $z) : string{ - return substr($this->blockLight, ($x << 7) | ($z << 3), 8); - } - - public function getBlockSkyLightColumn(int $x, int $z) : string{ - return substr($this->skyLight, ($x << 7) | ($z << 3), 8); - } - - public function getBlockIdArray() : string{ - assert(strlen($this->ids) === 4096, "Wrong length of ID array, expecting 4096 bytes, got " . strlen($this->ids)); - return $this->ids; - } - - public function getBlockDataArray() : string{ - assert(strlen($this->data) === 2048, "Wrong length of data array, expecting 2048 bytes, got " . strlen($this->data)); - return $this->data; - } - - public function getBlockSkyLightArray() : string{ - assert(strlen($this->skyLight) === 2048, "Wrong length of skylight array, expecting 2048 bytes, got " . strlen($this->skyLight)); - return $this->skyLight; - } - - public function setBlockSkyLightArray(string $data){ - assert(strlen($data) === 2048, "Wrong length of skylight array, expecting 2048 bytes, got " . strlen($data)); - $this->skyLight = $data; - } - - public function getBlockLightArray() : string{ - assert(strlen($this->blockLight) === 2048, "Wrong length of light array, expecting 2048 bytes, got " . strlen($this->blockLight)); - return $this->blockLight; - } - - public function setBlockLightArray(string $data){ - assert(strlen($data) === 2048, "Wrong length of light array, expecting 2048 bytes, got " . strlen($data)); - $this->blockLight = $data; - } - - public function networkSerialize() : string{ - return "\x00" . $this->ids . $this->data; - } - - /** - * @return mixed[] - */ - public function __debugInfo(){ - return []; - } - - public function collectGarbage() : void{ - /* - * This strange looking code is designed to exploit PHP's copy-on-write behaviour. Assigning will copy a - * reference to the const instead of duplicating the whole string. The string will only be duplicated when - * modified, which is perfect for this purpose. - */ - if($this->data === self::ZERO_NIBBLE_ARRAY){ - $this->data = self::ZERO_NIBBLE_ARRAY; - } - if($this->skyLight === self::ZERO_NIBBLE_ARRAY){ - $this->skyLight = self::ZERO_NIBBLE_ARRAY; - } - if($this->blockLight === self::ZERO_NIBBLE_ARRAY){ - $this->blockLight = self::ZERO_NIBBLE_ARRAY; - } - } -} diff --git a/src/pocketmine/level/format/SubChunkInterface.php b/src/pocketmine/level/format/SubChunkInterface.php deleted file mode 100644 index 6b5cd1433f..0000000000 --- a/src/pocketmine/level/format/SubChunkInterface.php +++ /dev/null @@ -1,79 +0,0 @@ -path = $path; - if(!file_exists($this->path)){ - mkdir($this->path, 0777, true); - } - - $this->loadLevelData(); - $this->fixLevelData(); - } - - protected function loadLevelData() : void{ - $compressedLevelData = @file_get_contents($this->getPath() . "level.dat"); - if($compressedLevelData === false){ - throw new LevelException("Failed to read level.dat (permission denied or doesn't exist)"); - } - $rawLevelData = @zlib_decode($compressedLevelData); - if($rawLevelData === false){ - throw new LevelException("Failed to decompress level.dat contents (probably corrupted)"); - } - $nbt = new BigEndianNBTStream(); - try{ - $levelData = $nbt->read($rawLevelData); - }catch(\UnexpectedValueException $e){ - throw new LevelException("Failed to decode level.dat (" . $e->getMessage() . ")", 0, $e); - } - - if(!($levelData instanceof CompoundTag) or !$levelData->hasTag("Data", CompoundTag::class)){ - throw new LevelException("Invalid level.dat"); - } - - $this->levelData = $levelData->getCompoundTag("Data"); - } - - protected function fixLevelData() : void{ - if(!$this->levelData->hasTag("generatorName", StringTag::class)){ - $this->levelData->setString("generatorName", "default", true); - }elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($this->levelData->getString("generatorName"))) !== null){ - $this->levelData->setString("generatorName", $generatorName); - } - - if(!$this->levelData->hasTag("generatorOptions", StringTag::class)){ - $this->levelData->setString("generatorOptions", ""); - } - } - - /** - * Hack to fix worlds broken previously by older versions of PocketMine-MP which incorrectly saved classpaths of - * generators into level.dat on imported (not generated) worlds. - * - * This should only have affected leveldb worlds as far as I know, because PC format worlds include the - * generatorName tag by default. However, MCPE leveldb ones didn't, and so they would get filled in with something - * broken. - * - * This bug took a long time to get found because previously the generator manager would just return the default - * generator silently on failure to identify the correct generator, which caused lots of unexpected bugs. - * - * Only classnames which were written into the level.dat from "fixing" the level data are included here. These are - * hardcoded to avoid problems fixing broken worlds in the future if these classes get moved, renamed or removed. - * - * @param string $className Classname saved in level.dat - * - * @return null|string Name of the correct generator to replace the broken value - */ - protected static function hackyFixForGeneratorClasspathInLevelDat(string $className) : ?string{ - //THESE ARE DELIBERATELY HARDCODED, DO NOT CHANGE! - switch($className){ - case 'pocketmine\level\generator\normal\Normal': - return "normal"; - case 'pocketmine\level\generator\Flat': - return "flat"; - } - - return null; - } - - public function getPath() : string{ - return $this->path; - } - - public function getName() : string{ - return $this->levelData->getString("LevelName"); - } - - public function getTime() : int{ - return $this->levelData->getLong("Time", 0, true); - } - - public function setTime(int $value){ - $this->levelData->setLong("Time", $value, true); //some older PM worlds had this in the wrong format - } - - public function getSeed() : int{ - return $this->levelData->getLong("RandomSeed"); - } - - public function setSeed(int $value){ - $this->levelData->setLong("RandomSeed", $value); - } - - public function getSpawn() : Vector3{ - return new Vector3($this->levelData->getInt("SpawnX"), $this->levelData->getInt("SpawnY"), $this->levelData->getInt("SpawnZ")); - } - - public function setSpawn(Vector3 $pos){ - $this->levelData->setInt("SpawnX", $pos->getFloorX()); - $this->levelData->setInt("SpawnY", $pos->getFloorY()); - $this->levelData->setInt("SpawnZ", $pos->getFloorZ()); - } - - public function doGarbageCollection(){ - - } - - public function getLevelData() : CompoundTag{ - return $this->levelData; - } - - /** - * @return void - */ - public function saveLevelData(){ - $nbt = new BigEndianNBTStream(); - $buffer = $nbt->writeCompressed(new CompoundTag("", [ - $this->levelData - ])); - file_put_contents($this->getPath() . "level.dat", $buffer); - } - - /** - * @throws CorruptedChunkException - * @throws UnsupportedChunkFormatException - */ - public function loadChunk(int $chunkX, int $chunkZ) : ?Chunk{ - return $this->readChunk($chunkX, $chunkZ); - } - - public function saveChunk(Chunk $chunk) : void{ - if(!$chunk->isGenerated()){ - throw new \InvalidStateException("Cannot save un-generated chunk"); - } - $this->writeChunk($chunk); - } - - /** - * @throws UnsupportedChunkFormatException - * @throws CorruptedChunkException - */ - abstract protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk; - - abstract protected function writeChunk(Chunk $chunk) : void; -} diff --git a/src/pocketmine/level/format/io/ChunkRequestTask.php b/src/pocketmine/level/format/io/ChunkRequestTask.php deleted file mode 100644 index 1d54a2454d..0000000000 --- a/src/pocketmine/level/format/io/ChunkRequestTask.php +++ /dev/null @@ -1,89 +0,0 @@ -levelId = $level->getId(); - $this->compressionLevel = $level->getServer()->networkCompressionLevel; - - $this->chunk = $chunk->fastSerialize(); - $this->tiles = $chunk->networkSerializeTiles(); - - $this->chunkX = $chunkX; - $this->chunkZ = $chunkZ; - } - - public function onRun(){ - $chunk = Chunk::fastDeserialize($this->chunk); - $pk = LevelChunkPacket::withoutCache($this->chunkX, $this->chunkZ, $chunk->getSubChunkSendCount() + 4, $chunk->networkSerialize($this->tiles)); - - $batch = new BatchPacket(); - $batch->addPacket($pk); - $batch->setCompressionLevel($this->compressionLevel); - $batch->encode(); - - $this->setResult($batch->buffer); - } - - public function onCompletion(Server $server){ - $level = $server->getLevel($this->levelId); - if($level instanceof Level){ - if($this->hasResult()){ - $batch = new BatchPacket($this->getResult()); - assert(strlen($batch->buffer) > 0); - $batch->isEncoded = true; - $level->chunkRequestCallback($this->chunkX, $this->chunkZ, $batch); - }else{ - $server->getLogger()->error("Chunk request for world #" . $this->levelId . ", x=" . $this->chunkX . ", z=" . $this->chunkZ . " doesn't have any result data"); - } - }else{ - $server->getLogger()->debug("Dropped chunk task due to world not loaded"); - } - } -} diff --git a/src/pocketmine/level/format/io/ChunkUtils.php b/src/pocketmine/level/format/io/ChunkUtils.php deleted file mode 100644 index 0bfb52fdc5..0000000000 --- a/src/pocketmine/level/format/io/ChunkUtils.php +++ /dev/null @@ -1,112 +0,0 @@ - XZY and vice versa) - * - * @param string $array length 4096 - * - * @return string length 4096 - */ - final public static function reorderByteArray(string $array) : string{ - $result = str_repeat("\x00", 4096); - if($array !== $result){ - $i = 0; - for($x = 0; $x < 16; ++$x){ - $zM = $x + 256; - for($z = $x; $z < $zM; $z += 16){ - $yM = $z + 4096; - for($y = $z; $y < $yM; $y += 256){ - $result[$i] = $array[$y]; - ++$i; - } - } - } - } - - return $result; - } - - /** - * Re-orders a nibble array (YZX -> XZY and vice versa) - * - * @param string $array length 2048 - * @param string $commonValue length 1 common value to fill the default array with and to expect, may improve sort time - * - * @return string length 2048 - */ - final public static function reorderNibbleArray(string $array, string $commonValue = "\x00") : string{ - $result = str_repeat($commonValue, 2048); - - if($array !== $result){ - $i = 0; - for($x = 0; $x < 8; ++$x){ - for($z = 0; $z < 16; ++$z){ - $zx = (($z << 3) | $x); - for($y = 0; $y < 8; ++$y){ - $j = (($y << 8) | $zx); - $j80 = ($j | 0x80); - if($array[$j] === $commonValue and $array[$j80] === $commonValue){ - //values are already filled - }else{ - $i1 = ord($array[$j]); - $i2 = ord($array[$j80]); - $result[$i] = chr(($i2 << 4) | ($i1 & 0x0f)); - $result[$i | 0x80] = chr(($i1 >> 4) | ($i2 & 0xf0)); - } - $i++; - } - } - $i += 128; - } - } - - return $result; - } - - /** - * Converts pre-MCPE-1.0 biome color array to biome ID array. - * - * @param int[] $array of biome color values - * @phpstan-param list $array - */ - public static function convertBiomeColors(array $array) : string{ - $result = str_repeat("\x00", 256); - foreach($array as $i => $color){ - $result[$i] = chr(($color >> 24) & 0xff); - } - return $result; - } - - } -} diff --git a/src/pocketmine/level/format/io/LevelProvider.php b/src/pocketmine/level/format/io/LevelProvider.php deleted file mode 100644 index b35613de69..0000000000 --- a/src/pocketmine/level/format/io/LevelProvider.php +++ /dev/null @@ -1,138 +0,0 @@ - $generator - * @phpstan-param array $options - * - * @return void - */ - public static function generate(string $path, string $name, int $seed, string $generator, array $options = []); - - /** - * Returns the generator name - */ - public function getGenerator() : string; - - /** - * @return mixed[] - * @phpstan-return array - */ - public function getGeneratorOptions() : array; - - /** - * Saves a chunk (usually to disk). - */ - public function saveChunk(Chunk $chunk) : void; - - /** - * Loads a chunk (usually from disk storage) and returns it. If the chunk does not exist, null is returned. - * - * @throws CorruptedChunkException - * @throws UnsupportedChunkFormatException - */ - public function loadChunk(int $chunkX, int $chunkZ) : ?Chunk; - - public function getName() : string; - - public function getTime() : int; - - /** - * @return void - */ - public function setTime(int $value); - - public function getSeed() : int; - - /** - * @return void - */ - public function setSeed(int $value); - - public function getSpawn() : Vector3; - - /** - * @return void - */ - public function setSpawn(Vector3 $pos); - - /** - * Returns the world difficulty. This will be one of the Level constants. - */ - public function getDifficulty() : int; - - /** - * Sets the world difficulty. - * - * @return void - */ - public function setDifficulty(int $difficulty); - - /** - * Performs garbage collection in the level provider, such as cleaning up regions in Region-based worlds. - * - * @return void - */ - public function doGarbageCollection(); - - /** - * Performs cleanups necessary when the level provider is closed and no longer needed. - * - * @return void - */ - public function close(); - -} diff --git a/src/pocketmine/level/format/io/LevelProviderManager.php b/src/pocketmine/level/format/io/LevelProviderManager.php deleted file mode 100644 index bec1e6ce15..0000000000 --- a/src/pocketmine/level/format/io/LevelProviderManager.php +++ /dev/null @@ -1,95 +0,0 @@ -> - */ - protected static $providers = []; - - public static function init() : void{ - self::addProvider(Anvil::class); - self::addProvider(McRegion::class); - self::addProvider(PMAnvil::class); - self::addProvider(LevelDB::class); - } - - /** - * @phpstan-param class-string $class - * - * @return void - * @throws \InvalidArgumentException - */ - public static function addProvider(string $class){ - try{ - $reflection = new \ReflectionClass($class); - }catch(\ReflectionException $e){ - throw new \InvalidArgumentException("Class $class does not exist"); - } - if(!$reflection->implementsInterface(LevelProvider::class)){ - throw new \InvalidArgumentException("Class $class does not implement " . LevelProvider::class); - } - if(!$reflection->isInstantiable()){ - throw new \InvalidArgumentException("Class $class cannot be constructed"); - } - - self::$providers[strtolower($class::getProviderName())] = $class; - } - - /** - * Returns a LevelProvider class for this path, or null - * - * @return string|null - * @phpstan-return class-string|null - */ - public static function getProvider(string $path){ - foreach(self::$providers as $provider){ - /** @phpstan-var class-string $provider */ - if($provider::isValid($path)){ - return $provider; - } - } - - return null; - } - - /** - * Returns a LevelProvider by name, or null if not found - * - * @return string|null - * @phpstan-return class-string|null - */ - public static function getProviderByName(string $name){ - return self::$providers[trim(strtolower($name))] ?? null; - } -} diff --git a/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php b/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php deleted file mode 100644 index e88ba4f833..0000000000 --- a/src/pocketmine/level/format/io/exception/UnsupportedChunkFormatException.php +++ /dev/null @@ -1,30 +0,0 @@ - LEVELDB_ZLIB_RAW_COMPRESSION - ]); - } - - public function __construct(string $path){ - self::checkForLevelDBExtension(); - parent::__construct($path); - - $this->db = self::createDB($path); - } - - protected function loadLevelData() : void{ - $rawLevelData = file_get_contents($this->getPath() . "level.dat"); - if($rawLevelData === false or strlen($rawLevelData) <= 8){ - throw new LevelException("Truncated level.dat"); - } - $nbt = new LittleEndianNBTStream(); - try{ - $levelData = $nbt->read(substr($rawLevelData, 8)); - }catch(\UnexpectedValueException $e){ - throw new LevelException("Invalid level.dat (" . $e->getMessage() . ")", 0, $e); - } - if($levelData instanceof CompoundTag){ - $this->levelData = $levelData; - }else{ - throw new LevelException("Invalid level.dat"); - } - - $version = $this->levelData->getInt("StorageVersion", INT32_MAX, true); - if($version > self::CURRENT_STORAGE_VERSION){ - throw new LevelException("Specified LevelDB world format version ($version) is not supported"); - } - } - - protected function fixLevelData() : void{ - $db = self::createDB($this->path); - - if(!$this->levelData->hasTag("generatorName", StringTag::class)){ - if($this->levelData->hasTag("Generator", IntTag::class)){ - switch($this->levelData->getInt("Generator")){ //Detect correct generator from MCPE data - case self::GENERATOR_FLAT: - $this->levelData->setString("generatorName", "flat"); - if(($layers = $db->get(self::ENTRY_FLAT_WORLD_LAYERS)) !== false){ //Detect existing custom flat layers - $layers = trim($layers, "[]"); - }else{ - $layers = "7,3,3,2"; - } - $this->levelData->setString("generatorOptions", "2;" . $layers . ";1"); - break; - case self::GENERATOR_INFINITE: - //TODO: add a null generator which does not generate missing chunks (to allow importing back to MCPE and generating more normal terrain without PocketMine messing things up) - $this->levelData->setString("generatorName", "default"); - $this->levelData->setString("generatorOptions", ""); - break; - case self::GENERATOR_LIMITED: - throw new LevelException("Limited worlds are not currently supported"); - default: - throw new LevelException("Unknown LevelDB world format type, this level cannot be loaded"); - } - }else{ - $this->levelData->setString("generatorName", "default"); - } - }elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($this->levelData->getString("generatorName"))) !== null){ - $this->levelData->setString("generatorName", $generatorName); - } - - if(!$this->levelData->hasTag("generatorOptions", StringTag::class)){ - $this->levelData->setString("generatorOptions", ""); - } - } - - public static function getProviderName() : string{ - return "leveldb"; - } - - public function getWorldHeight() : int{ - return 256; - } - - public static function isValid(string $path) : bool{ - return file_exists($path . "/level.dat") and is_dir($path . "/db/"); - } - - public static function generate(string $path, string $name, int $seed, string $generator, array $options = []){ - self::checkForLevelDBExtension(); - - if(!file_exists($path . "/db")){ - mkdir($path . "/db", 0777, true); - } - - switch($generator){ - case Flat::class: - $generatorType = self::GENERATOR_FLAT; - break; - default: - $generatorType = self::GENERATOR_INFINITE; - //TODO: add support for limited worlds - } - - $levelData = new CompoundTag("", [ - //Vanilla fields - new IntTag("DayCycleStopTime", -1), - new IntTag("Difficulty", Level::getDifficultyFromString((string) ($options["difficulty"] ?? "normal"))), - new ByteTag("ForceGameType", 0), - new IntTag("GameType", 0), - new IntTag("Generator", $generatorType), - new LongTag("LastPlayed", time()), - new StringTag("LevelName", $name), - new IntTag("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL), - //new IntTag("Platform", 2), //TODO: find out what the possible values are for - new LongTag("RandomSeed", $seed), - new IntTag("SpawnX", 0), - new IntTag("SpawnY", 32767), - new IntTag("SpawnZ", 0), - new IntTag("StorageVersion", self::CURRENT_STORAGE_VERSION), - new LongTag("Time", 0), - new ByteTag("eduLevel", 0), - new ByteTag("falldamage", 1), - new ByteTag("firedamage", 1), - new ByteTag("hasBeenLoadedInCreative", 1), //badly named, this actually determines whether achievements can be earned in this world... - new ByteTag("immutableWorld", 0), - new FloatTag("lightningLevel", 0.0), - new IntTag("lightningTime", 0), - new ByteTag("pvp", 1), - new FloatTag("rainLevel", 0.0), - new IntTag("rainTime", 0), - new ByteTag("spawnMobs", 1), - new ByteTag("texturePacksRequired", 0), //TODO - - //Additional PocketMine-MP fields - new CompoundTag("GameRules", []), - new ByteTag("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0), - new StringTag("generatorName", GeneratorManager::getGeneratorName($generator)), - new StringTag("generatorOptions", $options["preset"] ?? "") - ]); - - $nbt = new LittleEndianNBTStream(); - $buffer = $nbt->write($levelData); - file_put_contents($path . "level.dat", Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer); - - $db = self::createDB($path); - - if($generatorType === self::GENERATOR_FLAT and isset($options["preset"])){ - $layers = explode(";", $options["preset"])[1] ?? ""; - if($layers !== ""){ - $out = "["; - foreach(Flat::parseLayers($layers) as $result){ - $out .= $result[0] . ","; //only id, meta will unfortunately not survive :( - } - $out = rtrim($out, ",") . "]"; //remove trailing comma - $db->put(self::ENTRY_FLAT_WORLD_LAYERS, $out); //Add vanilla flatworld layers to allow terrain generation by MCPE to continue seamlessly - } - } - } - - public function saveLevelData(){ - $this->levelData->setInt("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL); - $this->levelData->setInt("StorageVersion", self::CURRENT_STORAGE_VERSION); - - $nbt = new LittleEndianNBTStream(); - $buffer = $nbt->write($this->levelData); - file_put_contents($this->getPath() . "level.dat", Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer); - } - - public function getGenerator() : string{ - return $this->levelData->getString("generatorName", ""); - } - - public function getGeneratorOptions() : array{ - return ["preset" => $this->levelData->getString("generatorOptions", "")]; - } - - public function getDifficulty() : int{ - return $this->levelData->getInt("Difficulty", Level::DIFFICULTY_NORMAL); - } - - public function setDifficulty(int $difficulty){ - $this->levelData->setInt("Difficulty", $difficulty); //yes, this is intended! (in PE: int, PC: byte) - } - - /** - * @throws UnsupportedChunkFormatException - */ - protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{ - $index = LevelDB::chunkIndex($chunkX, $chunkZ); - - $chunkVersionRaw = $this->db->get($index . self::TAG_VERSION); - if($chunkVersionRaw === false){ - return null; - } - - /** @var SubChunk[] $subChunks */ - $subChunks = []; - - /** @var int[] $heightMap */ - $heightMap = []; - /** @var string $biomeIds */ - $biomeIds = ""; - - /** @var bool $lightPopulated */ - $lightPopulated = true; - - $chunkVersion = ord($chunkVersionRaw); - $hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION; - - $binaryStream = new BinaryStream(); - - switch($chunkVersion){ - case 7: //MCPE 1.2 (???) - case 4: //MCPE 1.1 - //TODO: check beds - case 3: //MCPE 1.0 - for($y = 0; $y < Chunk::MAX_SUBCHUNKS; ++$y){ - if(($data = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y))) === false){ - continue; - } - - $binaryStream->setBuffer($data, 0); - $subChunkVersion = $binaryStream->getByte(); - if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){ - $hasBeenUpgraded = true; - } - - switch($subChunkVersion){ - case 0: - $blocks = $binaryStream->get(4096); - $blockData = $binaryStream->get(2048); - if($chunkVersion < 4){ - $blockSkyLight = $binaryStream->get(2048); - $blockLight = $binaryStream->get(2048); - $hasBeenUpgraded = true; //drop saved light - }else{ - //Mojang didn't bother changing the subchunk version when they stopped saving sky light -_- - $blockSkyLight = ""; - $blockLight = ""; - $lightPopulated = false; - } - - $subChunks[$y] = new SubChunk($blocks, $blockData, $blockSkyLight, $blockLight); - break; - default: - //TODO: set chunks read-only so the version on disk doesn't get overwritten - throw new UnsupportedChunkFormatException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); - } - } - - if(($maps2d = $this->db->get($index . self::TAG_DATA_2D)) !== false){ - $binaryStream->setBuffer($maps2d, 0); - - /** @var int[] $unpackedHeightMap */ - $unpackedHeightMap = unpack("v*", $binaryStream->get(512)); //unpack() will never fail here - $heightMap = array_values($unpackedHeightMap); - $biomeIds = $binaryStream->get(256); - } - break; - case 2: // < MCPE 1.0 - $legacyTerrain = $this->db->get($index . self::TAG_LEGACY_TERRAIN); - if($legacyTerrain === false){ - throw new CorruptedChunkException("Expected to find a LEGACY_TERRAIN key for this chunk version, but none found"); - } - $binaryStream->setBuffer($legacyTerrain); - $fullIds = $binaryStream->get(32768); - $fullData = $binaryStream->get(16384); - $fullSkyLight = $binaryStream->get(16384); - $fullBlockLight = $binaryStream->get(16384); - - for($yy = 0; $yy < 8; ++$yy){ - $subOffset = ($yy << 4); - $ids = ""; - for($i = 0; $i < 256; ++$i){ - $ids .= substr($fullIds, $subOffset, 16); - $subOffset += 128; - } - $data = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $data .= substr($fullData, $subOffset, 8); - $subOffset += 64; - } - $skyLight = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $skyLight .= substr($fullSkyLight, $subOffset, 8); - $subOffset += 64; - } - $blockLight = ""; - $subOffset = ($yy << 3); - for($i = 0; $i < 256; ++$i){ - $blockLight .= substr($fullBlockLight, $subOffset, 8); - $subOffset += 64; - } - $subChunks[$yy] = new SubChunk($ids, $data, $skyLight, $blockLight); - } - - /** @var int[] $unpackedHeightMap */ - $unpackedHeightMap = unpack("C*", $binaryStream->get(256)); //unpack() will never fail here, but static analysers don't know that - $heightMap = array_values($unpackedHeightMap); - - /** @var int[] $unpackedBiomeIds */ - $unpackedBiomeIds = unpack("N*", $binaryStream->get(1024)); //nor here - $biomeIds = ChunkUtils::convertBiomeColors(array_values($unpackedBiomeIds)); - break; - default: - //TODO: set chunks read-only so the version on disk doesn't get overwritten - throw new UnsupportedChunkFormatException("don't know how to decode chunk format version $chunkVersion"); - } - - $nbt = new LittleEndianNBTStream(); - - /** @var CompoundTag[] $entities */ - $entities = []; - if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false and $entityData !== ""){ - $entityTags = $nbt->read($entityData, true); - foreach((is_array($entityTags) ? $entityTags : [$entityTags]) as $entityTag){ - if(!($entityTag instanceof CompoundTag)){ - throw new CorruptedChunkException("Entity root tag should be TAG_Compound"); - } - if($entityTag->hasTag("id", IntTag::class)){ - $entityTag->setInt("id", $entityTag->getInt("id") & 0xff); //remove type flags - TODO: use these instead of removing them) - } - $entities[] = $entityTag; - } - } - - /** @var CompoundTag[] $tiles */ - $tiles = []; - if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false and $tileData !== ""){ - $tileTags = $nbt->read($tileData, true); - foreach((is_array($tileTags) ? $tileTags : [$tileTags]) as $tileTag){ - if(!($tileTag instanceof CompoundTag)){ - throw new CorruptedChunkException("Tile root tag should be TAG_Compound"); - } - $tiles[] = $tileTag; - } - } - - //TODO: extra data should be converted into blockstorage layers (first they need to be implemented!) - /* - $extraData = []; - if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) !== false and $extraRawData !== ""){ - $binaryStream->setBuffer($extraRawData, 0); - $count = $binaryStream->getLInt(); - for($i = 0; $i < $count; ++$i){ - $key = $binaryStream->getLInt(); - $value = $binaryStream->getLShort(); - $extraData[$key] = $value; - } - }*/ - - $chunk = new Chunk( - $chunkX, - $chunkZ, - $subChunks, - $entities, - $tiles, - $biomeIds, - $heightMap - ); - - //TODO: tile ticks, biome states (?) - - $chunk->setGenerated(true); - $chunk->setPopulated(true); - $chunk->setLightPopulated($lightPopulated); - $chunk->setChanged($hasBeenUpgraded); //trigger rewriting chunk to disk if it was converted from an older format - - return $chunk; - } - - protected function writeChunk(Chunk $chunk) : void{ - $index = LevelDB::chunkIndex($chunk->getX(), $chunk->getZ()); - $this->db->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION)); - - $subChunks = $chunk->getSubChunks(); - foreach($subChunks as $y => $subChunk){ - $key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y); - if($subChunk->isEmpty(false)){ //MCPE doesn't save light anymore as of 1.1 - $this->db->delete($key); - }else{ - $this->db->put($key, - chr(self::CURRENT_LEVEL_SUBCHUNK_VERSION) . - $subChunk->getBlockIdArray() . - $subChunk->getBlockDataArray() - ); - } - } - - $this->db->put($index . self::TAG_DATA_2D, pack("v*", ...$chunk->getHeightMapArray()) . $chunk->getBiomeIdArray()); - - //TODO: use this properly - $this->db->put($index . self::TAG_STATE_FINALISATION, chr(self::FINALISATION_DONE)); - - /** @var CompoundTag[] $tiles */ - $tiles = []; - foreach($chunk->getTiles() as $tile){ - $tiles[] = $tile->saveNBT(); - } - $this->writeTags($tiles, $index . self::TAG_BLOCK_ENTITY); - - /** @var CompoundTag[] $entities */ - $entities = []; - foreach($chunk->getSavableEntities() as $entity){ - $entity->saveNBT(); - $entities[] = $entity->namedtag; - } - $this->writeTags($entities, $index . self::TAG_ENTITY); - - $this->db->delete($index . self::TAG_DATA_2D_LEGACY); - $this->db->delete($index . self::TAG_LEGACY_TERRAIN); - } - - /** - * @param CompoundTag[] $targets - */ - private function writeTags(array $targets, string $index) : void{ - if(count($targets) > 0){ - $nbt = new LittleEndianNBTStream(); - $this->db->put($index, $nbt->write($targets)); - }else{ - $this->db->delete($index); - } - } - - public function getDatabase() : \LevelDB{ - return $this->db; - } - - public static function chunkIndex(int $chunkX, int $chunkZ) : string{ - return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ); - } - - public function close(){ - unset($this->db); - } -} diff --git a/src/pocketmine/level/format/io/region/Anvil.php b/src/pocketmine/level/format/io/region/Anvil.php deleted file mode 100644 index bf1e69f0de..0000000000 --- a/src/pocketmine/level/format/io/region/Anvil.php +++ /dev/null @@ -1,163 +0,0 @@ -setInt("xPos", $chunk->getX()); - $nbt->setInt("zPos", $chunk->getZ()); - - $nbt->setByte("V", 1); - $nbt->setLong("LastUpdate", 0); //TODO - $nbt->setLong("InhabitedTime", 0); //TODO - $nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0); - $nbt->setByte("LightPopulated", $chunk->isLightPopulated() ? 1 : 0); - - $subChunks = []; - foreach($chunk->getSubChunks() as $y => $subChunk){ - if(!($subChunk instanceof SubChunk) or $subChunk->isEmpty()){ - continue; - } - - $tag = $this->serializeSubChunk($subChunk); - $tag->setByte("Y", $y); - $subChunks[] = $tag; - } - $nbt->setTag(new ListTag("Sections", $subChunks, NBT::TAG_Compound)); - - $nbt->setByteArray("Biomes", $chunk->getBiomeIdArray()); - $nbt->setIntArray("HeightMap", $chunk->getHeightMapArray()); - - $entities = []; - - foreach($chunk->getSavableEntities() as $entity){ - $entity->saveNBT(); - $entities[] = $entity->namedtag; - } - - $nbt->setTag(new ListTag("Entities", $entities, NBT::TAG_Compound)); - - $tiles = []; - foreach($chunk->getTiles() as $tile){ - $tiles[] = $tile->saveNBT(); - } - - $nbt->setTag(new ListTag("TileEntities", $tiles, NBT::TAG_Compound)); - - //TODO: TileTicks - - $writer = new BigEndianNBTStream(); - return $writer->writeCompressed(new CompoundTag("", [$nbt]), ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL); - } - - protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{ - return new CompoundTag("", [ - new ByteArrayTag("Blocks", ChunkUtils::reorderByteArray($subChunk->getBlockIdArray())), //Generic in-memory chunks are currently always XZY - new ByteArrayTag("Data", ChunkUtils::reorderNibbleArray($subChunk->getBlockDataArray())), - new ByteArrayTag("SkyLight", ChunkUtils::reorderNibbleArray($subChunk->getBlockSkyLightArray(), "\xff")), - new ByteArrayTag("BlockLight", ChunkUtils::reorderNibbleArray($subChunk->getBlockLightArray())) - ]); - } - - protected function nbtDeserialize(string $data) : Chunk{ - $data = @zlib_decode($data); - if($data === false){ - throw new CorruptedChunkException("Failed to decompress chunk data"); - } - $nbt = new BigEndianNBTStream(); - $chunk = $nbt->read($data); - if(!($chunk instanceof CompoundTag) or !$chunk->hasTag("Level")){ - throw new CorruptedChunkException("'Level' key is missing from chunk NBT"); - } - - $chunk = $chunk->getCompoundTag("Level"); - - $subChunks = []; - $subChunksTag = $chunk->getListTag("Sections") ?? []; - foreach($subChunksTag as $subChunk){ - if($subChunk instanceof CompoundTag){ - $subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk); - } - } - - if($chunk->hasTag("BiomeColors", IntArrayTag::class)){ - $biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format - }else{ - $biomeIds = $chunk->getByteArray("Biomes", "", true); - } - - $result = new Chunk( - $chunk->getInt("xPos"), - $chunk->getInt("zPos"), - $subChunks, - $chunk->hasTag("Entities", ListTag::class) ? self::getCompoundList("Entities", $chunk->getListTag("Entities")) : [], - $chunk->hasTag("TileEntities", ListTag::class) ? self::getCompoundList("TileEntities", $chunk->getListTag("TileEntities")) : [], - $biomeIds, - $chunk->getIntArray("HeightMap", []) - ); - $result->setLightPopulated($chunk->getByte("LightPopulated", 0) !== 0); - $result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0); - $result->setGenerated(); - $result->setChanged(false); - return $result; - } - - protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{ - return new SubChunk( - ChunkUtils::reorderByteArray($subChunk->getByteArray("Blocks")), - ChunkUtils::reorderNibbleArray($subChunk->getByteArray("Data")), - ChunkUtils::reorderNibbleArray($subChunk->getByteArray("SkyLight"), "\xff"), - ChunkUtils::reorderNibbleArray($subChunk->getByteArray("BlockLight")) - ); - } - - public static function getProviderName() : string{ - return "anvil"; - } - - public static function getPcWorldFormatVersion() : int{ - return 19133; //anvil - } - - public function getWorldHeight() : int{ - //TODO: add world height options - return 256; - } -} diff --git a/src/pocketmine/level/format/io/region/McRegion.php b/src/pocketmine/level/format/io/region/McRegion.php deleted file mode 100644 index fb39a8e4ca..0000000000 --- a/src/pocketmine/level/format/io/region/McRegion.php +++ /dev/null @@ -1,419 +0,0 @@ -setInt("xPos", $chunk->getX()); - $nbt->setInt("zPos", $chunk->getZ()); - - $nbt->setLong("LastUpdate", 0); //TODO - $nbt->setByte("TerrainPopulated", $chunk->isPopulated() ? 1 : 0); - $nbt->setByte("LightPopulated", $chunk->isLightPopulated() ? 1 : 0); - - $ids = ""; - $data = ""; - $skyLight = ""; - $blockLight = ""; - $subChunks = $chunk->getSubChunks(); - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - for($y = 0; $y < 8; ++$y){ - $subChunk = $subChunks[$y]; - $ids .= $subChunk->getBlockIdColumn($x, $z); - $data .= $subChunk->getBlockDataColumn($x, $z); - $skyLight .= $subChunk->getBlockSkyLightColumn($x, $z); - $blockLight .= $subChunk->getBlockLightColumn($x, $z); - } - } - } - - $nbt->setByteArray("Blocks", $ids); - $nbt->setByteArray("Data", $data); - $nbt->setByteArray("SkyLight", $skyLight); - $nbt->setByteArray("BlockLight", $blockLight); - - $nbt->setByteArray("Biomes", $chunk->getBiomeIdArray()); //doesn't exist in regular McRegion, this is here for PocketMine-MP only - $nbt->setByteArray("HeightMap", pack("C*", ...$chunk->getHeightMapArray())); //this is ByteArray in McRegion, but IntArray in Anvil (due to raised build height) - - $entities = []; - - foreach($chunk->getSavableEntities() as $entity){ - $entity->saveNBT(); - $entities[] = $entity->namedtag; - } - - $nbt->setTag(new ListTag("Entities", $entities, NBT::TAG_Compound)); - - $tiles = []; - foreach($chunk->getTiles() as $tile){ - $tiles[] = $tile->saveNBT(); - } - - $nbt->setTag(new ListTag("TileEntities", $tiles, NBT::TAG_Compound)); - - $writer = new BigEndianNBTStream(); - return $writer->writeCompressed(new CompoundTag("", [$nbt]), ZLIB_ENCODING_DEFLATE, RegionLoader::$COMPRESSION_LEVEL); - } - - /** - * @throws CorruptedChunkException - */ - protected function nbtDeserialize(string $data) : Chunk{ - $data = @zlib_decode($data); - if($data === false){ - throw new CorruptedChunkException("Failed to decompress chunk data"); - } - $nbt = new BigEndianNBTStream(); - $chunk = $nbt->read($data); - if(!($chunk instanceof CompoundTag) or !$chunk->hasTag("Level")){ - throw new CorruptedChunkException("'Level' key is missing from chunk NBT"); - } - - $chunk = $chunk->getCompoundTag("Level"); - - $subChunks = []; - $fullIds = $chunk->hasTag("Blocks", ByteArrayTag::class) ? $chunk->getByteArray("Blocks") : str_repeat("\x00", 32768); - $fullData = $chunk->hasTag("Data", ByteArrayTag::class) ? $chunk->getByteArray("Data") : str_repeat("\x00", 16384); - $fullSkyLight = $chunk->hasTag("SkyLight", ByteArrayTag::class) ? $chunk->getByteArray("SkyLight") : str_repeat("\xff", 16384); - $fullBlockLight = $chunk->hasTag("BlockLight", ByteArrayTag::class) ? $chunk->getByteArray("BlockLight") : str_repeat("\x00", 16384); - - for($y = 0; $y < 8; ++$y){ - $offset = ($y << 4); - $ids = ""; - for($i = 0; $i < 256; ++$i){ - $ids .= substr($fullIds, $offset, 16); - $offset += 128; - } - $data = ""; - $offset = ($y << 3); - for($i = 0; $i < 256; ++$i){ - $data .= substr($fullData, $offset, 8); - $offset += 64; - } - $skyLight = ""; - $offset = ($y << 3); - for($i = 0; $i < 256; ++$i){ - $skyLight .= substr($fullSkyLight, $offset, 8); - $offset += 64; - } - $blockLight = ""; - $offset = ($y << 3); - for($i = 0; $i < 256; ++$i){ - $blockLight .= substr($fullBlockLight, $offset, 8); - $offset += 64; - } - $subChunks[$y] = new SubChunk($ids, $data, $skyLight, $blockLight); - } - - if($chunk->hasTag("BiomeColors", IntArrayTag::class)){ - $biomeIds = ChunkUtils::convertBiomeColors($chunk->getIntArray("BiomeColors")); //Convert back to original format - }elseif($chunk->hasTag("Biomes", ByteArrayTag::class)){ - $biomeIds = $chunk->getByteArray("Biomes"); - }else{ - $biomeIds = ""; - } - - $heightMap = []; - if($chunk->hasTag("HeightMap", ByteArrayTag::class)){ - /** @var int[] $unpackedHeightMap */ - $unpackedHeightMap = unpack("C*", $chunk->getByteArray("HeightMap")); //unpack() will never fail here - $heightMap = array_values($unpackedHeightMap); - }elseif($chunk->hasTag("HeightMap", IntArrayTag::class)){ - $heightMap = $chunk->getIntArray("HeightMap"); #blameshoghicp - } - - $result = new Chunk( - $chunk->getInt("xPos"), - $chunk->getInt("zPos"), - $subChunks, - $chunk->hasTag("Entities", ListTag::class) ? self::getCompoundList("Entities", $chunk->getListTag("Entities")) : [], - $chunk->hasTag("TileEntities", ListTag::class) ? self::getCompoundList("TileEntities", $chunk->getListTag("TileEntities")) : [], - $biomeIds, - $heightMap - ); - $result->setLightPopulated($chunk->getByte("LightPopulated", 0) !== 0); - $result->setPopulated($chunk->getByte("TerrainPopulated", 0) !== 0); - $result->setGenerated(true); - $result->setChanged(false); - return $result; - } - - /** - * @return CompoundTag[] - * @throws CorruptedChunkException - */ - protected static function getCompoundList(string $context, ListTag $list) : array{ - if($list->count() === 0){ //empty lists might have wrong types, we don't care - return []; - } - if($list->getTagType() !== NBT::TAG_Compound){ - throw new CorruptedChunkException("Expected TAG_List for '$context'"); - } - $result = []; - foreach($list as $tag){ - if(!($tag instanceof CompoundTag)){ - //this should never happen, but it's still possible due to lack of native type safety - throw new CorruptedChunkException("Expected TAG_List for '$context'"); - } - $result[] = $tag; - } - return $result; - } - - public static function getProviderName() : string{ - return "mcregion"; - } - - /** - * Returns the storage version as per Minecraft PC world formats. - */ - public static function getPcWorldFormatVersion() : int{ - return 19132; //mcregion - } - - public function getWorldHeight() : int{ - //TODO: add world height options - return 128; - } - - public static function isValid(string $path) : bool{ - $isValid = (file_exists($path . "/level.dat") and is_dir($path . "/region/")); - - if($isValid){ - $files = array_filter(scandir($path . "/region/", SCANDIR_SORT_NONE), function(string $file) : bool{ - $extPos = strrpos($file, "."); - return $extPos !== false && substr($file, $extPos + 1, 2) === "mc"; //region file - }); - - foreach($files as $f){ - $extPos = strrpos($f, "."); - if($extPos !== false && substr($f, $extPos + 1) !== static::REGION_FILE_EXTENSION){ - $isValid = false; - break; - } - } - } - - return $isValid; - } - - public static function generate(string $path, string $name, int $seed, string $generator, array $options = []){ - if(!file_exists($path)){ - mkdir($path, 0777, true); - } - - if(!file_exists($path . "/region")){ - mkdir($path . "/region", 0777); - } - //TODO, add extra details - $levelData = new CompoundTag("Data", [ - new ByteTag("hardcore", ($options["hardcore"] ?? false) === true ? 1 : 0), - new ByteTag("Difficulty", Level::getDifficultyFromString((string) ($options["difficulty"] ?? "normal"))), - new ByteTag("initialized", 1), - new IntTag("GameType", 0), - new IntTag("generatorVersion", 1), //2 in MCPE - new IntTag("SpawnX", 256), - new IntTag("SpawnY", 70), - new IntTag("SpawnZ", 256), - new IntTag("version", static::getPcWorldFormatVersion()), - new IntTag("DayTime", 0), - new LongTag("LastPlayed", (int) (microtime(true) * 1000)), - new LongTag("RandomSeed", $seed), - new LongTag("SizeOnDisk", 0), - new LongTag("Time", 0), - new StringTag("generatorName", GeneratorManager::getGeneratorName($generator)), - new StringTag("generatorOptions", $options["preset"] ?? ""), - new StringTag("LevelName", $name), - new CompoundTag("GameRules", []) - ]); - $nbt = new BigEndianNBTStream(); - $buffer = $nbt->writeCompressed(new CompoundTag("", [ - $levelData - ])); - file_put_contents($path . "level.dat", $buffer); - } - - public function getGenerator() : string{ - return $this->levelData->getString("generatorName", "DEFAULT"); - } - - public function getGeneratorOptions() : array{ - return ["preset" => $this->levelData->getString("generatorOptions", "")]; - } - - public function getDifficulty() : int{ - return $this->levelData->getByte("Difficulty", Level::DIFFICULTY_NORMAL); - } - - public function setDifficulty(int $difficulty){ - $this->levelData->setByte("Difficulty", $difficulty); - } - - public function doGarbageCollection(){ - $limit = time() - 300; - foreach($this->regions as $index => $region){ - if($region->lastUsed <= $limit){ - $region->close(); - unset($this->regions[$index]); - } - } - } - - /** - * @param int $regionX reference parameter - * @param int $regionZ reference parameter - * - * @return void - */ - public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ){ - $regionX = $chunkX >> 5; - $regionZ = $chunkZ >> 5; - } - - /** - * @return RegionLoader|null - */ - protected function getRegion(int $regionX, int $regionZ){ - return $this->regions[Level::chunkHash($regionX, $regionZ)] ?? null; - } - - /** - * Returns the path to a specific region file based on its X/Z coordinates - */ - protected function pathToRegion(int $regionX, int $regionZ) : string{ - return $this->path . "region/r.$regionX.$regionZ." . static::REGION_FILE_EXTENSION; - } - - /** - * @return void - */ - protected function loadRegion(int $regionX, int $regionZ){ - if(!isset($this->regions[$index = Level::chunkHash($regionX, $regionZ)])){ - $path = $this->pathToRegion($regionX, $regionZ); - - $region = new RegionLoader($path, $regionX, $regionZ); - try{ - $region->open(); - }catch(CorruptedRegionException $e){ - $logger = MainLogger::getLogger(); - $logger->error("Corrupted region file detected: " . $e->getMessage()); - - $region->close(false); //Do not write anything to the file - - $backupPath = $path . ".bak." . time(); - rename($path, $backupPath); - $logger->error("Corrupted region file has been backed up to " . $backupPath); - - $region = new RegionLoader($path, $regionX, $regionZ); - $region->open(); //this will create a new empty region to replace the corrupted one - } - - $this->regions[$index] = $region; - } - } - - public function close(){ - foreach($this->regions as $index => $region){ - $region->close(); - unset($this->regions[$index]); - } - } - - /** - * @throws CorruptedChunkException - */ - protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{ - $regionX = $regionZ = null; - self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ); - assert(is_int($regionX) and is_int($regionZ)); - - if(!file_exists($this->pathToRegion($regionX, $regionZ))){ - return null; - } - $this->loadRegion($regionX, $regionZ); - - $chunkData = $this->getRegion($regionX, $regionZ)->readChunk($chunkX & 0x1f, $chunkZ & 0x1f); - if($chunkData !== null){ - return $this->nbtDeserialize($chunkData); - } - - return null; - } - - protected function writeChunk(Chunk $chunk) : void{ - $chunkX = $chunk->getX(); - $chunkZ = $chunk->getZ(); - - self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ); - $this->loadRegion($regionX, $regionZ); - - $this->getRegion($regionX, $regionZ)->writeChunk($chunkX & 0x1f, $chunkZ & 0x1f, $this->nbtSerialize($chunk)); - } -} diff --git a/src/pocketmine/level/generator/Flat.php b/src/pocketmine/level/generator/Flat.php deleted file mode 100644 index 681b4ad0a6..0000000000 --- a/src/pocketmine/level/generator/Flat.php +++ /dev/null @@ -1,210 +0,0 @@ - - */ - private $structure; - /** @var int */ - private $floorLevel; - /** @var int */ - private $biome; - /** - * @var mixed[] - * @phpstan-var array - */ - private $options; - /** @var string */ - private $preset; - - public function getSettings() : array{ - return $this->options; - } - - public function getName() : string{ - return "flat"; - } - - /** - * @param mixed[] $options - * @phpstan-param array $options - * - * @throws InvalidGeneratorOptionsException - */ - public function __construct(array $options = []){ - $this->options = $options; - if(isset($this->options["preset"]) and $this->options["preset"] != ""){ - $this->preset = $this->options["preset"]; - }else{ - $this->preset = "2;7,2x3,2;1;"; - //$this->preset = "2;7,59x1,3x3,2;1;spawn(radius=10 block=89),decoration(treecount=80 grasscount=45)"; - } - - $this->parsePreset(); - - if(isset($this->options["decoration"])){ - $ores = new Ore(); - $ores->setOreTypes([ - new OreType(BlockFactory::get(Block::COAL_ORE), 20, 16, 0, 128), - new OreType(BlockFactory::get(Block::IRON_ORE), 20, 8, 0, 64), - new OreType(BlockFactory::get(Block::REDSTONE_ORE), 8, 7, 0, 16), - new OreType(BlockFactory::get(Block::LAPIS_ORE), 1, 6, 0, 32), - new OreType(BlockFactory::get(Block::GOLD_ORE), 2, 8, 0, 32), - new OreType(BlockFactory::get(Block::DIAMOND_ORE), 1, 7, 0, 16), - new OreType(BlockFactory::get(Block::DIRT), 20, 32, 0, 128), - new OreType(BlockFactory::get(Block::GRAVEL), 10, 16, 0, 128) - ]); - $this->populators[] = $ores; - } - } - - /** - * @return int[][] - * @phpstan-return array - * - * @throws InvalidGeneratorOptionsException - */ - public static function parseLayers(string $layers) : array{ - $result = []; - $split = array_map('\trim', explode(',', $layers)); - $y = 0; - 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 = ItemFactory::fromStringSingle($matches[2])->getBlock(); - }catch(\InvalidArgumentException $e){ - throw new InvalidGeneratorOptionsException("Invalid preset layer \"$line\": " . $e->getMessage(), 0, $e); - } - for($cY = $y, $y += $cnt; $cY < $y; ++$cY){ - $result[$cY] = [$b->getId(), $b->getDamage()]; - } - } - - 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); - - $this->floorLevel = count($this->structure); - - //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(0, 0); - $this->chunk->setGenerated(); - - for($Z = 0; $Z < 16; ++$Z){ - for($X = 0; $X < 16; ++$X){ - $this->chunk->setBiomeId($X, $Z, $this->biome); - } - } - - $count = count($this->structure); - for($sy = 0; $sy < $count; $sy += 16){ - $subchunk = $this->chunk->getSubChunk($sy >> 4, true); - for($y = 0; $y < 16 and isset($this->structure[$y | $sy]); ++$y){ - list($id, $meta) = $this->structure[$y | $sy]; - - for($Z = 0; $Z < 16; ++$Z){ - for($X = 0; $X < 16; ++$X){ - $subchunk->setBlock($X, $y, $Z, $id, $meta); - } - } - } - } - } - - public function init(ChunkManager $level, Random $random) : void{ - parent::init($level, $random); - $this->generateBaseChunk(); - } - - public function generateChunk(int $chunkX, int $chunkZ) : void{ - $chunk = clone $this->chunk; - $chunk->setX($chunkX); - $chunk->setZ($chunkZ); - $this->level->setChunk($chunkX, $chunkZ, $chunk); - } - - public function populateChunk(int $chunkX, int $chunkZ) : void{ - $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed()); - foreach($this->populators as $populator){ - $populator->populate($this->level, $chunkX, $chunkZ, $this->random); - } - - } - - public function getSpawn() : Vector3{ - return new Vector3(128, $this->floorLevel, 128); - } -} diff --git a/src/pocketmine/level/generator/GeneratorManager.php b/src/pocketmine/level/generator/GeneratorManager.php deleted file mode 100644 index 1f370f26cf..0000000000 --- a/src/pocketmine/level/generator/GeneratorManager.php +++ /dev/null @@ -1,117 +0,0 @@ - classname mapping - * @phpstan-var array> - */ - private static $list = []; - - /** - * Registers the default known generators. - */ - public static function registerDefaultGenerators() : void{ - self::addGenerator(Flat::class, "flat"); - self::addGenerator(Normal::class, "normal"); - self::addGenerator(Normal::class, "default"); - self::addGenerator(Nether::class, "hell"); - self::addGenerator(Nether::class, "nether"); - } - - /** - * @param string $class Fully qualified name of class that extends \pocketmine\level\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 - * @phpstan-param class-string $class - */ - public static function addGenerator(string $class, string $name, bool $overwrite = false) : void{ - if(!is_subclass_of($class, Generator::class)){ - throw new \InvalidArgumentException("Class $class does not extend " . Generator::class); - } - - if(!$overwrite and isset(self::$list[$name = strtolower($name)])){ - throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); - } - - self::$list[$name] = $class; - } - - /** - * Returns a list of names for registered generators. - * - * @return string[] - */ - public static function getGeneratorList() : array{ - return array_keys(self::$list); - } - - /** - * 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 - */ - public static function getGenerator(string $name, bool $throwOnMissing = false){ - if(isset(self::$list[$name = strtolower($name)])){ - return self::$list[$name]; - } - - if($throwOnMissing){ - throw new \InvalidArgumentException("Alias \"$name\" does not map to any known generator"); - } - return Normal::class; - } - - /** - * Returns the registered name of the given Generator class. - * - * @param string $class Fully qualified name of class that extends \pocketmine\level\generator\Generator - * @phpstan-param class-string $class - */ - public static function getGeneratorName(string $class) : string{ - foreach(self::$list as $name => $c){ - if($c === $class){ - return $name; - } - } - - return "unknown"; - } - - private function __construct(){ - //NOOP - } -} diff --git a/src/pocketmine/level/generator/PopulationTask.php b/src/pocketmine/level/generator/PopulationTask.php deleted file mode 100644 index 8ff8d86e91..0000000000 --- a/src/pocketmine/level/generator/PopulationTask.php +++ /dev/null @@ -1,154 +0,0 @@ -state = true; - $this->levelId = $level->getId(); - $this->chunk = $chunk->fastSerialize(); - - foreach($level->getAdjacentChunks($chunk->getX(), $chunk->getZ()) as $i => $c){ - $this->{"chunk$i"} = $c !== null ? $c->fastSerialize() : null; - } - } - - public function onRun(){ - $manager = $this->getFromThreadStore("generation.level{$this->levelId}.manager"); - $generator = $this->getFromThreadStore("generation.level{$this->levelId}.generator"); - if(!($manager instanceof SimpleChunkManager) or !($generator instanceof Generator)){ - $this->state = false; - return; - } - - /** @var Chunk[] $chunks */ - $chunks = []; - - $chunk = Chunk::fastDeserialize($this->chunk); - - for($i = 0; $i < 9; ++$i){ - if($i === 4){ - continue; - } - $xx = -1 + $i % 3; - $zz = -1 + (int) ($i / 3); - $ck = $this->{"chunk$i"}; - if($ck === null){ - $chunks[$i] = new Chunk($chunk->getX() + $xx, $chunk->getZ() + $zz); - }else{ - $chunks[$i] = Chunk::fastDeserialize($ck); - } - } - - $manager->setChunk($chunk->getX(), $chunk->getZ(), $chunk); - if(!$chunk->isGenerated()){ - $generator->generateChunk($chunk->getX(), $chunk->getZ()); - $chunk = $manager->getChunk($chunk->getX(), $chunk->getZ()); - $chunk->setGenerated(); - } - - foreach($chunks as $i => $c){ - $manager->setChunk($c->getX(), $c->getZ(), $c); - if(!$c->isGenerated()){ - $generator->generateChunk($c->getX(), $c->getZ()); - $chunks[$i] = $manager->getChunk($c->getX(), $c->getZ()); - $chunks[$i]->setGenerated(); - } - } - - $generator->populateChunk($chunk->getX(), $chunk->getZ()); - $chunk = $manager->getChunk($chunk->getX(), $chunk->getZ()); - $chunk->setPopulated(); - - $chunk->recalculateHeightMap(); - $chunk->populateSkyLight(); - $chunk->setLightPopulated(); - - $this->chunk = $chunk->fastSerialize(); - - foreach($chunks as $i => $c){ - $this->{"chunk$i"} = $c->hasChanged() ? $c->fastSerialize() : null; - } - - $manager->cleanChunks(); - } - - public function onCompletion(Server $server){ - $level = $server->getLevel($this->levelId); - if($level !== null){ - if(!$this->state){ - $level->registerGeneratorToWorker($this->worker->getAsyncWorkerId()); - } - - $chunk = Chunk::fastDeserialize($this->chunk); - - for($i = 0; $i < 9; ++$i){ - if($i === 4){ - continue; - } - $c = $this->{"chunk$i"}; - if($c !== null){ - $c = Chunk::fastDeserialize($c); - $level->generateChunkCallback($c->getX(), $c->getZ(), $this->state ? $c : null); - } - } - - $level->generateChunkCallback($chunk->getX(), $chunk->getZ(), $this->state ? $chunk : null); - } - } -} diff --git a/src/pocketmine/level/generator/hell/Nether.php b/src/pocketmine/level/generator/hell/Nether.php deleted file mode 100644 index 4b1d8aaab0..0000000000 --- a/src/pocketmine/level/generator/hell/Nether.php +++ /dev/null @@ -1,142 +0,0 @@ - $options - * - * @throws InvalidGeneratorOptionsException - */ - public function __construct(array $options = []){ - - } - - public function getName() : string{ - return "nether"; - } - - public function getSettings() : array{ - return []; - } - - public function init(ChunkManager $level, Random $random) : void{ - parent::init($level, $random); - $this->random->setSeed($this->level->getSeed()); - $this->noiseBase = new Simplex($this->random, 4, 1 / 4, 1 / 64); - $this->random->setSeed($this->level->getSeed()); - - /*$ores = new Ore(); - $ores->setOreTypes([ - new OreType(new CoalOre(), 20, 16, 0, 128), - new OreType(New IronOre(), 20, 8, 0, 64), - new OreType(new RedstoneOre(), 8, 7, 0, 16), - new OreType(new LapisOre(), 1, 6, 0, 32), - new OreType(new GoldOre(), 2, 8, 0, 32), - new OreType(new DiamondOre(), 1, 7, 0, 16), - new OreType(new Dirt(), 20, 32, 0, 128), - new OreType(new Gravel(), 10, 16, 0, 128) - ]); - $this->populators[] = $ores;*/ - } - - public function generateChunk(int $chunkX, int $chunkZ) : void{ - $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed()); - - $noise = $this->noiseBase->getFastNoise3D(16, 128, 16, 4, 8, 4, $chunkX * 16, 0, $chunkZ * 16); - - $chunk = $this->level->getChunk($chunkX, $chunkZ); - - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - - $biome = Biome::getBiome(Biome::HELL); - $chunk->setBiomeId($x, $z, $biome->getId()); - - for($y = 0; $y < 128; ++$y){ - if($y === 0 or $y === 127){ - $chunk->setBlockId($x, $y, $z, Block::BEDROCK); - continue; - } - $noiseValue = (abs($this->emptyHeight - $y) / $this->emptyHeight) * $this->emptyAmplitude - $noise[$x][$z][$y]; - $noiseValue -= 1 - $this->density; - - if($noiseValue > 0){ - $chunk->setBlockId($x, $y, $z, Block::NETHERRACK); - }elseif($y <= $this->waterHeight){ - $chunk->setBlockId($x, $y, $z, Block::STILL_LAVA); - } - } - } - } - - foreach($this->generationPopulators as $populator){ - $populator->populate($this->level, $chunkX, $chunkZ, $this->random); - } - } - - public function populateChunk(int $chunkX, int $chunkZ) : void{ - $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed()); - foreach($this->populators as $populator){ - $populator->populate($this->level, $chunkX, $chunkZ, $this->random); - } - - $chunk = $this->level->getChunk($chunkX, $chunkZ); - $biome = Biome::getBiome($chunk->getBiomeId(7, 7)); - $biome->populateChunk($this->level, $chunkX, $chunkZ, $this->random); - } - - public function getSpawn() : Vector3{ - return new Vector3(127.5, 128, 127.5); - } -} diff --git a/src/pocketmine/level/generator/noise/Perlin.php b/src/pocketmine/level/generator/noise/Perlin.php deleted file mode 100644 index e0b37f3972..0000000000 --- a/src/pocketmine/level/generator/noise/Perlin.php +++ /dev/null @@ -1,161 +0,0 @@ -octaves = $octaves; - $this->persistence = $persistence; - $this->expansion = $expansion; - $this->offsetX = $random->nextFloat() * 256; - $this->offsetY = $random->nextFloat() * 256; - $this->offsetZ = $random->nextFloat() * 256; - - for($i = 0; $i < 512; ++$i){ - $this->perm[$i] = 0; - } - - for($i = 0; $i < 256; ++$i){ - $this->perm[$i] = $random->nextBoundedInt(256); - } - - for($i = 0; $i < 256; ++$i){ - $pos = $random->nextBoundedInt(256 - $i) + $i; - $old = $this->perm[$i]; - - $this->perm[$i] = $this->perm[$pos]; - $this->perm[$pos] = $old; - $this->perm[$i + 256] = $this->perm[$i]; - } - - } - - public function getNoise3D($x, $y, $z){ - $x += $this->offsetX; - $y += $this->offsetY; - $z += $this->offsetZ; - - $floorX = (int) $x; - $floorY = (int) $y; - $floorZ = (int) $z; - - $X = $floorX & 0xFF; - $Y = $floorY & 0xFF; - $Z = $floorZ & 0xFF; - - $x -= $floorX; - $y -= $floorY; - $z -= $floorZ; - - //Fade curves - //$fX = self::fade($x); - //$fY = self::fade($y); - //$fZ = self::fade($z); - $fX = $x * $x * $x * ($x * ($x * 6 - 15) + 10); - $fY = $y * $y * $y * ($y * ($y * 6 - 15) + 10); - $fZ = $z * $z * $z * ($z * ($z * 6 - 15) + 10); - - //Cube corners - $A = $this->perm[$X] + $Y; - $B = $this->perm[$X + 1] + $Y; - - $AA = $this->perm[$A] + $Z; - $AB = $this->perm[$A + 1] + $Z; - $BA = $this->perm[$B] + $Z; - $BB = $this->perm[$B + 1] + $Z; - - $AA1 = self::grad($this->perm[$AA], $x, $y, $z); - $BA1 = self::grad($this->perm[$BA], $x - 1, $y, $z); - $AB1 = self::grad($this->perm[$AB], $x, $y - 1, $z); - $BB1 = self::grad($this->perm[$BB], $x - 1, $y - 1, $z); - $AA2 = self::grad($this->perm[$AA + 1], $x, $y, $z - 1); - $BA2 = self::grad($this->perm[$BA + 1], $x - 1, $y, $z - 1); - $AB2 = self::grad($this->perm[$AB + 1], $x, $y - 1, $z - 1); - $BB2 = self::grad($this->perm[$BB + 1], $x - 1, $y - 1, $z - 1); - - $xLerp11 = $AA1 + $fX * ($BA1 - $AA1); - - $zLerp1 = $xLerp11 + $fY * ($AB1 + $fX * ($BB1 - $AB1) - $xLerp11); - - $xLerp21 = $AA2 + $fX * ($BA2 - $AA2); - - return $zLerp1 + $fZ * ($xLerp21 + $fY * ($AB2 + $fX * ($BB2 - $AB2) - $xLerp21) - $zLerp1); - - /* - return self::lerp( - $fZ, - self::lerp( - $fY, - self::lerp( - $fX, - self::grad($this->perm[$AA], $x, $y, $z), - self::grad($this->perm[$BA], $x - 1, $y, $z) - ), - self::lerp( - $fX, - self::grad($this->perm[$AB], $x, $y - 1, $z), - self::grad($this->perm[$BB], $x - 1, $y - 1, $z) - ) - ), - self::lerp( - $fY, - self::lerp( - $fX, - self::grad($this->perm[$AA + 1], $x, $y, $z - 1), - self::grad($this->perm[$BA + 1], $x - 1, $y, $z - 1) - ), - self::lerp( - $fX, - self::grad($this->perm[$AB + 1], $x, $y - 1, $z - 1), - self::grad($this->perm[$BB + 1], $x - 1, $y - 1, $z - 1) - ) - ) - ); - */ - } - - /** - * @param float $x - * @param float $y - * - * @return float - */ - public function getNoise2D($x, $y){ - return $this->getNoise3D($x, $y, 0); - } -} diff --git a/src/pocketmine/level/generator/noise/Simplex.php b/src/pocketmine/level/generator/noise/Simplex.php deleted file mode 100644 index 47c5e7b814..0000000000 --- a/src/pocketmine/level/generator/noise/Simplex.php +++ /dev/null @@ -1,494 +0,0 @@ -offsetW = $random->nextFloat() * 256; - self::$SQRT_3 = sqrt(3); - self::$SQRT_5 = sqrt(5); - self::$F2 = 0.5 * (self::$SQRT_3 - 1); - self::$G2 = (3 - self::$SQRT_3) / 6; - self::$G22 = self::$G2 * 2.0 - 1; - self::$F3 = 1.0 / 3.0; - self::$G3 = 1.0 / 6.0; - self::$F4 = (self::$SQRT_5 - 1.0) / 4.0; - self::$G4 = (5.0 - self::$SQRT_5) / 20.0; - self::$G42 = self::$G4 * 2.0; - self::$G43 = self::$G4 * 3.0; - self::$G44 = self::$G4 * 4.0 - 1.0; - } - - /** - * @param int[] $g - * @param float $x - * @param float $y - * - * @return float - */ - protected static function dot2D($g, $x, $y){ - return $g[0] * $x + $g[1] * $y; - } - - /** - * @param int[] $g - * @param float $x - * @param float $y - * @param float $z - * - * @return float - */ - protected static function dot3D($g, $x, $y, $z){ - return $g[0] * $x + $g[1] * $y + $g[2] * $z; - } - - /** - * @param int[] $g - * @param float $x - * @param float $y - * @param float $z - * @param float $w - * - * @return float - */ - protected static function dot4D($g, $x, $y, $z, $w){ - return $g[0] * $x + $g[1] * $y + $g[2] * $z + $g[3] * $w; - } - - public function getNoise3D($x, $y, $z){ - $x += $this->offsetX; - $y += $this->offsetY; - $z += $this->offsetZ; - - // Skew the input space to determine which simplex cell we're in - $s = ($x + $y + $z) * self::$F3; // Very nice and simple skew factor for 3D - $i = (int) ($x + $s); - $j = (int) ($y + $s); - $k = (int) ($z + $s); - $t = ($i + $j + $k) * self::$G3; - // Unskew the cell origin back to (x,y,z) space - $x0 = $x - ($i - $t); // The x,y,z distances from the cell origin - $y0 = $y - ($j - $t); - $z0 = $z - ($k - $t); - - // For the 3D case, the simplex shape is a slightly irregular tetrahedron. - - // Determine which simplex we are in. - if($x0 >= $y0){ - if($y0 >= $z0){ - $i1 = 1; - $j1 = 0; - $k1 = 0; - $i2 = 1; - $j2 = 1; - $k2 = 0; - } // X Y Z order - elseif($x0 >= $z0){ - $i1 = 1; - $j1 = 0; - $k1 = 0; - $i2 = 1; - $j2 = 0; - $k2 = 1; - } // X Z Y order - else{ - $i1 = 0; - $j1 = 0; - $k1 = 1; - $i2 = 1; - $j2 = 0; - $k2 = 1; - } - // Z X Y order - }else{ // x0 0){ - $gi0 = self::$grad3[$this->perm[$ii + $this->perm[$jj + $this->perm[$kk]]] % 12]; - $n += $t0 * $t0 * $t0 * $t0 * ($gi0[0] * $x0 + $gi0[1] * $y0 + $gi0[2] * $z0); - } - - $t1 = 0.6 - $x1 * $x1 - $y1 * $y1 - $z1 * $z1; - if($t1 > 0){ - $gi1 = self::$grad3[$this->perm[$ii + $i1 + $this->perm[$jj + $j1 + $this->perm[$kk + $k1]]] % 12]; - $n += $t1 * $t1 * $t1 * $t1 * ($gi1[0] * $x1 + $gi1[1] * $y1 + $gi1[2] * $z1); - } - - $t2 = 0.6 - $x2 * $x2 - $y2 * $y2 - $z2 * $z2; - if($t2 > 0){ - $gi2 = self::$grad3[$this->perm[$ii + $i2 + $this->perm[$jj + $j2 + $this->perm[$kk + $k2]]] % 12]; - $n += $t2 * $t2 * $t2 * $t2 * ($gi2[0] * $x2 + $gi2[1] * $y2 + $gi2[2] * $z2); - } - - $t3 = 0.6 - $x3 * $x3 - $y3 * $y3 - $z3 * $z3; - if($t3 > 0){ - $gi3 = self::$grad3[$this->perm[$ii + 1 + $this->perm[$jj + 1 + $this->perm[$kk + 1]]] % 12]; - $n += $t3 * $t3 * $t3 * $t3 * ($gi3[0] * $x3 + $gi3[1] * $y3 + $gi3[2] * $z3); - } - - // Add contributions from each corner to get the noise value. - // The result is scaled to stay just inside [-1,1] - return 32.0 * $n; - } - - /** - * @param float $x - * @param float $y - * - * @return float - */ - public function getNoise2D($x, $y){ - $x += $this->offsetX; - $y += $this->offsetY; - - // Skew the input space to determine which simplex cell we're in - $s = ($x + $y) * self::$F2; // Hairy factor for 2D - $i = (int) ($x + $s); - $j = (int) ($y + $s); - $t = ($i + $j) * self::$G2; - // Unskew the cell origin back to (x,y) space - $x0 = $x - ($i - $t); // The x,y distances from the cell origin - $y0 = $y - ($j - $t); - - // For the 2D case, the simplex shape is an equilateral triangle. - - // Determine which simplex we are in. - if($x0 > $y0){ - $i1 = 1; - $j1 = 0; - } // lower triangle, XY order: (0,0)->(1,0)->(1,1) - else{ - $i1 = 0; - $j1 = 1; - } - // upper triangle, YX order: (0,0)->(0,1)->(1,1) - - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - - $x1 = $x0 - $i1 + self::$G2; // Offsets for middle corner in (x,y) unskewed coords - $y1 = $y0 - $j1 + self::$G2; - $x2 = $x0 + self::$G22; // Offsets for last corner in (x,y) unskewed coords - $y2 = $y0 + self::$G22; - - // Work out the hashed gradient indices of the three simplex corners - $ii = $i & 255; - $jj = $j & 255; - - $n = 0; - - // Calculate the contribution from the three corners - $t0 = 0.5 - $x0 * $x0 - $y0 * $y0; - if($t0 > 0){ - $gi0 = self::$grad3[$this->perm[$ii + $this->perm[$jj]] % 12]; - $n += $t0 * $t0 * $t0 * $t0 * ($gi0[0] * $x0 + $gi0[1] * $y0); // (x,y) of grad3 used for 2D gradient - } - - $t1 = 0.5 - $x1 * $x1 - $y1 * $y1; - if($t1 > 0){ - $gi1 = self::$grad3[$this->perm[$ii + $i1 + $this->perm[$jj + $j1]] % 12]; - $n += $t1 * $t1 * $t1 * $t1 * ($gi1[0] * $x1 + $gi1[1] * $y1); - } - - $t2 = 0.5 - $x2 * $x2 - $y2 * $y2; - if($t2 > 0){ - $gi2 = self::$grad3[$this->perm[$ii + 1 + $this->perm[$jj + 1]] % 12]; - $n += $t2 * $t2 * $t2 * $t2 * ($gi2[0] * $x2 + $gi2[1] * $y2); - } - - // Add contributions from each corner to get the noise value. - // The result is scaled to return values in the interval [-1,1]. - return 70.0 * $n; - } - - /** - * Computes and returns the 4D simplex noise for the given coordinates in - * 4D space - * - * @param float $x X coordinate - * @param float $y Y coordinate - * @param float $z Z coordinate - * @param float $w W coordinate - * - * @return float Noise at given location, from range -1 to 1 - */ - /*public function getNoise4D($x, $y, $z, $w){ - x += offsetX; - y += offsetY; - z += offsetZ; - w += offsetW; - - n0, n1, n2, n3, n4; // Noise contributions from the five corners - - // Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in - s = (x + y + z + w) * self::$F4; // Factor for 4D skewing - i = floor(x + s); - j = floor(y + s); - k = floor(z + s); - l = floor(w + s); - - t = (i + j + k + l) * self::$G4; // Factor for 4D unskewing - X0 = i - t; // Unskew the cell origin back to (x,y,z,w) space - Y0 = j - t; - Z0 = k - t; - W0 = l - t; - x0 = x - X0; // The x,y,z,w distances from the cell origin - y0 = y - Y0; - z0 = z - Z0; - w0 = w - W0; - - // For the 4D case, the simplex is a 4D shape I won't even try to describe. - // To find out which of the 24 possible simplices we're in, we need to - // determine the magnitude ordering of x0, y0, z0 and w0. - // The method below is a good way of finding the ordering of x,y,z,w and - // then find the correct traversal order for the simplex we’re in. - // First, six pair-wise comparisons are performed between each possible pair - // of the four coordinates, and the results are used to add up binary bits - // for an integer index. - c1 = (x0 > y0) ? 32 : 0; - c2 = (x0 > z0) ? 16 : 0; - c3 = (y0 > z0) ? 8 : 0; - c4 = (x0 > w0) ? 4 : 0; - c5 = (y0 > w0) ? 2 : 0; - c6 = (z0 > w0) ? 1 : 0; - c = c1 + c2 + c3 + c4 + c5 + c6; - i1, j1, k1, l1; // The integer offsets for the second simplex corner - i2, j2, k2, l2; // The integer offsets for the third simplex corner - i3, j3, k3, l3; // The integer offsets for the fourth simplex corner - - // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. - // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0; - j1 = simplex[c][1] >= 3 ? 1 : 0; - k1 = simplex[c][2] >= 3 ? 1 : 0; - l1 = simplex[c][3] >= 3 ? 1 : 0; - - // The number 2 in the "simplex" array is at the second largest coordinate. - i2 = simplex[c][0] >= 2 ? 1 : 0; - j2 = simplex[c][1] >= 2 ? 1 : 0; - k2 = simplex[c][2] >= 2 ? 1 : 0; - l2 = simplex[c][3] >= 2 ? 1 : 0; - - // The number 1 in the "simplex" array is at the second smallest coordinate. - i3 = simplex[c][0] >= 1 ? 1 : 0; - j3 = simplex[c][1] >= 1 ? 1 : 0; - k3 = simplex[c][2] >= 1 ? 1 : 0; - l3 = simplex[c][3] >= 1 ? 1 : 0; - - // The fifth corner has all coordinate offsets = 1, so no need to look that up. - - x1 = x0 - i1 + self::$G4; // Offsets for second corner in (x,y,z,w) coords - y1 = y0 - j1 + self::$G4; - z1 = z0 - k1 + self::$G4; - w1 = w0 - l1 + self::$G4; - - x2 = x0 - i2 + self::$G42; // Offsets for third corner in (x,y,z,w) coords - y2 = y0 - j2 + self::$G42; - z2 = z0 - k2 + self::$G42; - w2 = w0 - l2 + self::$G42; - - x3 = x0 - i3 + self::$G43; // Offsets for fourth corner in (x,y,z,w) coords - y3 = y0 - j3 + self::$G43; - z3 = z0 - k3 + self::$G43; - w3 = w0 - l3 + self::$G43; - - x4 = x0 + self::$G44; // Offsets for last corner in (x,y,z,w) coords - y4 = y0 + self::$G44; - z4 = z0 + self::$G44; - w4 = w0 + self::$G44; - - // Work out the hashed gradient indices of the five simplex corners - ii = i & 255; - jj = j & 255; - kk = k & 255; - ll = l & 255; - - gi0 = $this->perm[ii + $this->perm[jj + $this->perm[kk + $this->perm[ll]]]] % 32; - gi1 = $this->perm[ii + i1 + $this->perm[jj + j1 + $this->perm[kk + k1 + $this->perm[ll + l1]]]] % 32; - gi2 = $this->perm[ii + i2 + $this->perm[jj + j2 + $this->perm[kk + k2 + $this->perm[ll + l2]]]] % 32; - gi3 = $this->perm[ii + i3 + $this->perm[jj + j3 + $this->perm[kk + k3 + $this->perm[ll + l3]]]] % 32; - gi4 = $this->perm[ii + 1 + $this->perm[jj + 1 + $this->perm[kk + 1 + $this->perm[ll + 1]]]] % 32; - - // Calculate the contribution from the five corners - t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0; - if(t0 < 0){ - n0 = 0.0; - }else{ - t0 *= t0; - n0 = t0 * t0 * dot(grad4[gi0], x0, y0, z0, w0); - } - - t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1; - if(t1 < 0){ - n1 = 0.0; - }else{ - t1 *= t1; - n1 = t1 * t1 * dot(grad4[gi1], x1, y1, z1, w1); - } - - t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2; - if(t2 < 0){ - n2 = 0.0; - }else{ - t2 *= t2; - n2 = t2 * t2 * dot(grad4[gi2], x2, y2, z2, w2); - } - - t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3; - if(t3 < 0){ - n3 = 0.0; - }else{ - t3 *= t3; - n3 = t3 * t3 * dot(grad4[gi3], x3, y3, z3, w3); - } - - t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4; - if(t4 < 0){ - n4 = 0.0; - }else{ - t4 *= t4; - n4 = t4 * t4 * dot(grad4[gi4], x4, y4, z4, w4); - } - - // Sum up and scale the result to cover the range [-1,1] - return 27.0 * (n0 + n1 + n2 + n3 + n4); - }*/ -} diff --git a/src/pocketmine/level/generator/normal/Normal.php b/src/pocketmine/level/generator/normal/Normal.php deleted file mode 100644 index 2531fe256e..0000000000 --- a/src/pocketmine/level/generator/normal/Normal.php +++ /dev/null @@ -1,254 +0,0 @@ - $options - * - * @throws InvalidGeneratorOptionsException - */ - public function __construct(array $options = []){ - if(self::$GAUSSIAN_KERNEL === null){ - self::generateKernel(); - } - } - - private static function generateKernel() : void{ - self::$GAUSSIAN_KERNEL = []; - - $bellSize = 1 / self::$SMOOTH_SIZE; - $bellHeight = 2 * self::$SMOOTH_SIZE; - - for($sx = -self::$SMOOTH_SIZE; $sx <= self::$SMOOTH_SIZE; ++$sx){ - self::$GAUSSIAN_KERNEL[$sx + self::$SMOOTH_SIZE] = []; - - for($sz = -self::$SMOOTH_SIZE; $sz <= self::$SMOOTH_SIZE; ++$sz){ - $bx = $bellSize * $sx; - $bz = $bellSize * $sz; - self::$GAUSSIAN_KERNEL[$sx + self::$SMOOTH_SIZE][$sz + self::$SMOOTH_SIZE] = $bellHeight * exp(-($bx * $bx + $bz * $bz) / 2); - } - } - } - - public function getName() : string{ - return "normal"; - } - - public function getSettings() : array{ - return []; - } - - private function pickBiome(int $x, int $z) : Biome{ - $hash = $x * 2345803 ^ $z * 9236449 ^ $this->level->getSeed(); - $hash *= $hash + 223; - $xNoise = $hash >> 20 & 3; - $zNoise = $hash >> 22 & 3; - if($xNoise == 3){ - $xNoise = 1; - } - if($zNoise == 3){ - $zNoise = 1; - } - - return $this->selector->pickBiome($x + $xNoise - 1, $z + $zNoise - 1); - } - - public function init(ChunkManager $level, Random $random) : void{ - parent::init($level, $random); - $this->random->setSeed($this->level->getSeed()); - $this->noiseBase = new Simplex($this->random, 4, 1 / 4, 1 / 32); - $this->random->setSeed($this->level->getSeed()); - $this->selector = new class($this->random) extends BiomeSelector{ - protected function lookup(float $temperature, float $rainfall) : int{ - if($rainfall < 0.25){ - if($temperature < 0.7){ - return Biome::OCEAN; - }elseif($temperature < 0.85){ - return Biome::RIVER; - }else{ - return Biome::SWAMP; - } - }elseif($rainfall < 0.60){ - if($temperature < 0.25){ - return Biome::ICE_PLAINS; - }elseif($temperature < 0.75){ - return Biome::PLAINS; - }else{ - return Biome::DESERT; - } - }elseif($rainfall < 0.80){ - if($temperature < 0.25){ - return Biome::TAIGA; - }elseif($temperature < 0.75){ - return Biome::FOREST; - }else{ - return Biome::BIRCH_FOREST; - } - }else{ - //Previously here, we had a (broken) condition to generate mountains, but fixing it would have - //caused generation changes on a patch release, so we can't keep it here for now. - return Biome::RIVER; - } - } - }; - - $this->selector->recalculate(); - - $cover = new GroundCover(); - $this->generationPopulators[] = $cover; - - $ores = new Ore(); - $ores->setOreTypes([ - new OreType(BlockFactory::get(Block::COAL_ORE), 20, 16, 0, 128), - new OreType(BlockFactory::get(Block::IRON_ORE), 20, 8, 0, 64), - new OreType(BlockFactory::get(Block::REDSTONE_ORE), 8, 7, 0, 16), - new OreType(BlockFactory::get(Block::LAPIS_ORE), 1, 6, 0, 32), - new OreType(BlockFactory::get(Block::GOLD_ORE), 2, 8, 0, 32), - new OreType(BlockFactory::get(Block::DIAMOND_ORE), 1, 7, 0, 16), - new OreType(BlockFactory::get(Block::DIRT), 20, 32, 0, 128), - new OreType(BlockFactory::get(Block::GRAVEL), 10, 16, 0, 128) - ]); - $this->populators[] = $ores; - } - - public function generateChunk(int $chunkX, int $chunkZ) : void{ - $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed()); - - $noise = $this->noiseBase->getFastNoise3D(16, 128, 16, 4, 8, 4, $chunkX * 16, 0, $chunkZ * 16); - - $chunk = $this->level->getChunk($chunkX, $chunkZ); - - $biomeCache = []; - - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - $minSum = 0; - $maxSum = 0; - $weightSum = 0; - - $biome = $this->pickBiome($chunkX * 16 + $x, $chunkZ * 16 + $z); - $chunk->setBiomeId($x, $z, $biome->getId()); - - for($sx = -self::$SMOOTH_SIZE; $sx <= self::$SMOOTH_SIZE; ++$sx){ - for($sz = -self::$SMOOTH_SIZE; $sz <= self::$SMOOTH_SIZE; ++$sz){ - - $weight = self::$GAUSSIAN_KERNEL[$sx + self::$SMOOTH_SIZE][$sz + self::$SMOOTH_SIZE]; - - if($sx === 0 and $sz === 0){ - $adjacent = $biome; - }else{ - $index = Level::chunkHash($chunkX * 16 + $x + $sx, $chunkZ * 16 + $z + $sz); - if(isset($biomeCache[$index])){ - $adjacent = $biomeCache[$index]; - }else{ - $biomeCache[$index] = $adjacent = $this->pickBiome($chunkX * 16 + $x + $sx, $chunkZ * 16 + $z + $sz); - } - } - - $minSum += ($adjacent->getMinElevation() - 1) * $weight; - $maxSum += $adjacent->getMaxElevation() * $weight; - - $weightSum += $weight; - } - } - - $minSum /= $weightSum; - $maxSum /= $weightSum; - - $smoothHeight = ($maxSum - $minSum) / 2; - - for($y = 0; $y < 128; ++$y){ - if($y === 0){ - $chunk->setBlockId($x, $y, $z, Block::BEDROCK); - continue; - } - $noiseValue = $noise[$x][$z][$y] - 1 / $smoothHeight * ($y - $smoothHeight - $minSum); - - if($noiseValue > 0){ - $chunk->setBlockId($x, $y, $z, Block::STONE); - }elseif($y <= $this->waterHeight){ - $chunk->setBlockId($x, $y, $z, Block::STILL_WATER); - } - } - } - } - - foreach($this->generationPopulators as $populator){ - $populator->populate($this->level, $chunkX, $chunkZ, $this->random); - } - } - - public function populateChunk(int $chunkX, int $chunkZ) : void{ - $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->level->getSeed()); - foreach($this->populators as $populator){ - $populator->populate($this->level, $chunkX, $chunkZ, $this->random); - } - - $chunk = $this->level->getChunk($chunkX, $chunkZ); - $biome = Biome::getBiome($chunk->getBiomeId(7, 7)); - $biome->populateChunk($this->level, $chunkX, $chunkZ, $this->random); - } - - public function getSpawn() : Vector3{ - return new Vector3(127.5, 128, 127.5); - } -} diff --git a/src/pocketmine/level/generator/object/OreType.php b/src/pocketmine/level/generator/object/OreType.php deleted file mode 100644 index 84269d0727..0000000000 --- a/src/pocketmine/level/generator/object/OreType.php +++ /dev/null @@ -1,47 +0,0 @@ -material = $material; - $this->clusterCount = $clusterCount; - $this->clusterSize = $clusterSize; - $this->maxHeight = $maxHeight; - $this->minHeight = $minHeight; - } -} diff --git a/src/pocketmine/level/generator/object/PopulatorObject.php b/src/pocketmine/level/generator/object/PopulatorObject.php deleted file mode 100644 index 63a11647d2..0000000000 --- a/src/pocketmine/level/generator/object/PopulatorObject.php +++ /dev/null @@ -1,31 +0,0 @@ - true, - Block::SAPLING => true, - Block::LEAVES => true, - Block::SNOW_LAYER => true, - Block::LEAVES2 => true - ]; - - /** @var int */ - public $type = 0; - /** @var int */ - public $trunkBlock = Block::LOG; - /** @var int */ - public $leafBlock = Block::LEAVES; - /** @var int */ - public $treeHeight = 7; - - /** - * @return void - */ - public static function growTree(ChunkManager $level, int $x, int $y, int $z, Random $random, int $type = 0){ - switch($type){ - case Sapling::SPRUCE: - $tree = new SpruceTree(); - break; - case Sapling::BIRCH: - if($random->nextBoundedInt(39) === 0){ - $tree = new BirchTree(true); - }else{ - $tree = new BirchTree(); - } - break; - case Sapling::JUNGLE: - $tree = new JungleTree(); - break; - case Sapling::ACACIA: - case Sapling::DARK_OAK: - return; //TODO - default: - $tree = new OakTree(); - /*if($random->nextRange(0, 9) === 0){ - $tree = new BigTree(); - }else{*/ - - //} - break; - } - if($tree->canPlaceObject($level, $x, $y, $z, $random)){ - $tree->placeObject($level, $x, $y, $z, $random); - } - } - - public function canPlaceObject(ChunkManager $level, 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){ - ++$radiusToCheck; - } - for($xx = -$radiusToCheck; $xx < ($radiusToCheck + 1); ++$xx){ - for($zz = -$radiusToCheck; $zz < ($radiusToCheck + 1); ++$zz){ - if(!isset($this->overridable[$level->getBlockIdAt($x + $xx, $y + $yy, $z + $zz)])){ - return false; - } - } - } - } - - return true; - } - - /** - * @return void - */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z, Random $random){ - - $this->placeTrunk($level, $x, $y, $z, $random, $this->treeHeight - 1); - - for($yy = $y - 3 + $this->treeHeight; $yy <= $y + $this->treeHeight; ++$yy){ - $yOff = $yy - ($y + $this->treeHeight); - $mid = (int) (1 - $yOff / 2); - for($xx = $x - $mid; $xx <= $x + $mid; ++$xx){ - $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)){ - continue; - } - if(!BlockFactory::$solid[$level->getBlockIdAt($xx, $yy, $zz)]){ - $level->setBlockIdAt($xx, $yy, $zz, $this->leafBlock); - $level->setBlockDataAt($xx, $yy, $zz, $this->type); - } - } - } - } - } - - /** - * @return void - */ - protected function placeTrunk(ChunkManager $level, int $x, int $y, int $z, Random $random, int $trunkHeight){ - // The base dirt block - $level->setBlockIdAt($x, $y - 1, $z, Block::DIRT); - - for($yy = 0; $yy < $trunkHeight; ++$yy){ - $blockId = $level->getBlockIdAt($x, $y + $yy, $z); - if(isset($this->overridable[$blockId])){ - $level->setBlockIdAt($x, $y + $yy, $z, $this->trunkBlock); - $level->setBlockDataAt($x, $y + $yy, $z, $this->type); - } - } - } -} diff --git a/src/pocketmine/level/generator/populator/Pond.php b/src/pocketmine/level/generator/populator/Pond.php deleted file mode 100644 index 7c926dbbc0..0000000000 --- a/src/pocketmine/level/generator/populator/Pond.php +++ /dev/null @@ -1,72 +0,0 @@ -nextRange(0, $this->waterOdd) === 0){ - $x = $random->nextRange($chunkX << 4, ($chunkX << 4) + 16); - $y = $random->nextBoundedInt(128); - $z = $random->nextRange($chunkZ << 4, ($chunkZ << 4) + 16); - $pond = new \pocketmine\level\generator\object\Pond($random, BlockFactory::get(Block::WATER)); - if($pond->canPlaceObject($level, $v = new Vector3($x, $y, $z))){ - $pond->placeObject($level, $v); - } - } - } - - /** - * @return void - */ - public function setWaterOdd(int $waterOdd){ - $this->waterOdd = $waterOdd; - } - - /** - * @return void - */ - public function setLavaOdd(int $lavaOdd){ - $this->lavaOdd = $lavaOdd; - } - - /** - * @return void - */ - public function setLavaSurfaceOdd(int $lavaSurfaceOdd){ - $this->lavaSurfaceOdd = $lavaSurfaceOdd; - } -} diff --git a/src/pocketmine/level/generator/populator/TallGrass.php b/src/pocketmine/level/generator/populator/TallGrass.php deleted file mode 100644 index 73f8e1f636..0000000000 --- a/src/pocketmine/level/generator/populator/TallGrass.php +++ /dev/null @@ -1,86 +0,0 @@ -randomAmount = $amount; - } - - /** - * @param int $amount - * - * @return void - */ - public function setBaseAmount($amount){ - $this->baseAmount = $amount; - } - - public function populate(ChunkManager $level, int $chunkX, int $chunkZ, Random $random){ - $this->level = $level; - $amount = $random->nextRange(0, $this->randomAmount) + $this->baseAmount; - for($i = 0; $i < $amount; ++$i){ - $x = $random->nextRange($chunkX * 16, $chunkX * 16 + 15); - $z = $random->nextRange($chunkZ * 16, $chunkZ * 16 + 15); - $y = $this->getHighestWorkableBlock($x, $z); - - if($y !== -1 and $this->canTallGrassStay($x, $y, $z)){ - $this->level->setBlockIdAt($x, $y, $z, Block::TALL_GRASS); - $this->level->setBlockDataAt($x, $y, $z, 1); - } - } - } - - private function canTallGrassStay(int $x, int $y, int $z) : bool{ - $b = $this->level->getBlockIdAt($x, $y, $z); - return ($b === Block::AIR or $b === Block::SNOW_LAYER) and $this->level->getBlockIdAt($x, $y - 1, $z) === Block::GRASS; - } - - private function getHighestWorkableBlock(int $x, int $z) : int{ - for($y = 127; $y >= 0; --$y){ - $b = $this->level->getBlockIdAt($x, $y, $z); - if($b !== Block::AIR and $b !== Block::LEAVES and $b !== Block::LEAVES2 and $b !== Block::SNOW_LAYER){ - return $y + 1; - } - } - - return -1; - } -} diff --git a/src/pocketmine/level/generator/populator/Tree.php b/src/pocketmine/level/generator/populator/Tree.php deleted file mode 100644 index 997107cf39..0000000000 --- a/src/pocketmine/level/generator/populator/Tree.php +++ /dev/null @@ -1,94 +0,0 @@ -type = $type; - } - - /** - * @param int $amount - * - * @return void - */ - public function setRandomAmount($amount){ - $this->randomAmount = $amount; - } - - /** - * @param int $amount - * - * @return void - */ - public function setBaseAmount($amount){ - $this->baseAmount = $amount; - } - - public function populate(ChunkManager $level, int $chunkX, int $chunkZ, Random $random){ - $this->level = $level; - $amount = $random->nextRange(0, $this->randomAmount) + $this->baseAmount; - for($i = 0; $i < $amount; ++$i){ - $x = $random->nextRange($chunkX << 4, ($chunkX << 4) + 15); - $z = $random->nextRange($chunkZ << 4, ($chunkZ << 4) + 15); - $y = $this->getHighestWorkableBlock($x, $z); - if($y === -1){ - continue; - } - ObjectTree::growTree($this->level, $x, $y, $z, $random, $this->type); - } - } - - private function getHighestWorkableBlock(int $x, int $z) : int{ - for($y = 127; $y >= 0; --$y){ - $b = $this->level->getBlockIdAt($x, $y, $z); - if($b === Block::DIRT or $b === Block::GRASS){ - return $y + 1; - }elseif($b !== Block::AIR and $b !== Block::SNOW_LAYER){ - return -1; - } - } - - return -1; - } -} diff --git a/src/pocketmine/level/light/BlockLightUpdate.php b/src/pocketmine/level/light/BlockLightUpdate.php deleted file mode 100644 index 0f7020f011..0000000000 --- a/src/pocketmine/level/light/BlockLightUpdate.php +++ /dev/null @@ -1,35 +0,0 @@ -subChunkHandler->currentSubChunk->getBlockLight($x & 0x0f, $y & 0x0f, $z & 0x0f); - } - - public function setLight(int $x, int $y, int $z, int $level){ - $this->subChunkHandler->currentSubChunk->setBlockLight($x & 0x0f, $y & 0x0f, $z & 0x0f, $level); - } -} diff --git a/src/pocketmine/level/light/LightPopulationTask.php b/src/pocketmine/level/light/LightPopulationTask.php deleted file mode 100644 index 26a3913032..0000000000 --- a/src/pocketmine/level/light/LightPopulationTask.php +++ /dev/null @@ -1,66 +0,0 @@ -levelId = $level->getId(); - $this->chunk = $chunk->fastSerialize(); - } - - public function onRun(){ - if(!BlockFactory::isInit()){ - BlockFactory::init(); - } - /** @var Chunk $chunk */ - $chunk = Chunk::fastDeserialize($this->chunk); - - $chunk->recalculateHeightMap(); - $chunk->populateSkyLight(); - $chunk->setLightPopulated(); - - $this->chunk = $chunk->fastSerialize(); - } - - public function onCompletion(Server $server){ - $level = $server->getLevel($this->levelId); - if($level !== null){ - /** @var Chunk $chunk */ - $chunk = Chunk::fastDeserialize($this->chunk); - $level->generateChunkCallback($chunk->getX(), $chunk->getZ(), $chunk); - } - } -} diff --git a/src/pocketmine/level/light/LightUpdate.php b/src/pocketmine/level/light/LightUpdate.php deleted file mode 100644 index 4e4cf5e26f..0000000000 --- a/src/pocketmine/level/light/LightUpdate.php +++ /dev/null @@ -1,203 +0,0 @@ - [x, y, z, new light level] - * @phpstan-var array - */ - protected $updateNodes = []; - - /** - * @var \SplQueue - * @phpstan-var \SplQueue - */ - protected $spreadQueue; - /** - * @var true[] - * @phpstan-var array - */ - protected $spreadVisited = []; - - /** - * @var \SplQueue - * @phpstan-var \SplQueue - */ - protected $removalQueue; - /** - * @var true[] - * @phpstan-var array - */ - protected $removalVisited = []; - /** @var SubChunkIteratorManager */ - protected $subChunkHandler; - - public function __construct(ChunkManager $level){ - $this->level = $level; - $this->removalQueue = new \SplQueue(); - $this->spreadQueue = new \SplQueue(); - - $this->subChunkHandler = new SubChunkIteratorManager($this->level); - } - - abstract protected function getLight(int $x, int $y, int $z) : int; - - /** - * @return void - */ - abstract protected function setLight(int $x, int $y, int $z, int $level); - - /** - * @return void - */ - public function setAndUpdateLight(int $x, int $y, int $z, int $newLevel){ - $this->updateNodes[Level::blockHash($x, $y, $z)] = [$x, $y, $z, $newLevel]; - } - - private function prepareNodes() : void{ - foreach($this->updateNodes as $blockHash => [$x, $y, $z, $newLevel]){ - if($this->subChunkHandler->moveTo($x, $y, $z)){ - $oldLevel = $this->getLight($x, $y, $z); - - if($oldLevel !== $newLevel){ - $this->setLight($x, $y, $z, $newLevel); - if($oldLevel < $newLevel){ //light increased - $this->spreadVisited[$blockHash] = true; - $this->spreadQueue->enqueue([$x, $y, $z]); - }else{ //light removed - $this->removalVisited[$blockHash] = true; - $this->removalQueue->enqueue([$x, $y, $z, $oldLevel]); - } - } - } - } - } - - /** - * @return void - */ - public function execute(){ - $this->prepareNodes(); - - while(!$this->removalQueue->isEmpty()){ - list($x, $y, $z, $oldAdjacentLight) = $this->removalQueue->dequeue(); - - $points = [ - [$x + 1, $y, $z], - [$x - 1, $y, $z], - [$x, $y + 1, $z], - [$x, $y - 1, $z], - [$x, $y, $z + 1], - [$x, $y, $z - 1] - ]; - - foreach($points as list($cx, $cy, $cz)){ - if($this->subChunkHandler->moveTo($cx, $cy, $cz)){ - $this->computeRemoveLight($cx, $cy, $cz, $oldAdjacentLight); - } - } - } - - while(!$this->spreadQueue->isEmpty()){ - list($x, $y, $z) = $this->spreadQueue->dequeue(); - - unset($this->spreadVisited[Level::blockHash($x, $y, $z)]); - - if(!$this->subChunkHandler->moveTo($x, $y, $z)){ - continue; - } - - $newAdjacentLight = $this->getLight($x, $y, $z); - if($newAdjacentLight <= 0){ - continue; - } - - $points = [ - [$x + 1, $y, $z], - [$x - 1, $y, $z], - [$x, $y + 1, $z], - [$x, $y - 1, $z], - [$x, $y, $z + 1], - [$x, $y, $z - 1] - ]; - - foreach($points as list($cx, $cy, $cz)){ - if($this->subChunkHandler->moveTo($cx, $cy, $cz)){ - $this->computeSpreadLight($cx, $cy, $cz, $newAdjacentLight); - } - } - } - } - - /** - * @return void - */ - protected function computeRemoveLight(int $x, int $y, int $z, int $oldAdjacentLevel){ - $current = $this->getLight($x, $y, $z); - - if($current !== 0 and $current < $oldAdjacentLevel){ - $this->setLight($x, $y, $z, 0); - - if(!isset($this->removalVisited[$index = Level::blockHash($x, $y, $z)])){ - $this->removalVisited[$index] = true; - if($current > 1){ - $this->removalQueue->enqueue([$x, $y, $z, $current]); - } - } - }elseif($current >= $oldAdjacentLevel){ - if(!isset($this->spreadVisited[$index = Level::blockHash($x, $y, $z)])){ - $this->spreadVisited[$index] = true; - $this->spreadQueue->enqueue([$x, $y, $z]); - } - } - } - - /** - * @return void - */ - protected function computeSpreadLight(int $x, int $y, int $z, int $newAdjacentLevel){ - $current = $this->getLight($x, $y, $z); - $potentialLight = $newAdjacentLevel - BlockFactory::$lightFilter[$this->subChunkHandler->currentSubChunk->getBlockId($x & 0x0f, $y & 0x0f, $z & 0x0f)]; - - if($current < $potentialLight){ - $this->setLight($x, $y, $z, $potentialLight); - - if(!isset($this->spreadVisited[$index = Level::blockHash($x, $y, $z)]) and $potentialLight > 1){ - $this->spreadVisited[$index] = true; - $this->spreadQueue->enqueue([$x, $y, $z]); - } - } - } -} diff --git a/src/pocketmine/level/light/SkyLightUpdate.php b/src/pocketmine/level/light/SkyLightUpdate.php deleted file mode 100644 index d38b997714..0000000000 --- a/src/pocketmine/level/light/SkyLightUpdate.php +++ /dev/null @@ -1,35 +0,0 @@ -subChunkHandler->currentSubChunk->getBlockSkyLight($x & 0x0f, $y & 0x0f, $z & 0x0f); - } - - public function setLight(int $x, int $y, int $z, int $level){ - $this->subChunkHandler->currentSubChunk->setBlockSkyLight($x & 0x0f, $y & 0x0f, $z & 0x0f, $level); - } -} diff --git a/src/pocketmine/level/particle/CriticalParticle.php b/src/pocketmine/level/particle/CriticalParticle.php deleted file mode 100644 index 9ecfff529c..0000000000 --- a/src/pocketmine/level/particle/CriticalParticle.php +++ /dev/null @@ -1,32 +0,0 @@ -toARGB() : 0); - } -} diff --git a/src/pocketmine/level/particle/ExplodeParticle.php b/src/pocketmine/level/particle/ExplodeParticle.php deleted file mode 100644 index c4c8ce0026..0000000000 --- a/src/pocketmine/level/particle/ExplodeParticle.php +++ /dev/null @@ -1,32 +0,0 @@ -toARGB() : 0); - } -} diff --git a/src/pocketmine/level/particle/ItemBreakParticle.php b/src/pocketmine/level/particle/ItemBreakParticle.php deleted file mode 100644 index 8a5e655a08..0000000000 --- a/src/pocketmine/level/particle/ItemBreakParticle.php +++ /dev/null @@ -1,33 +0,0 @@ -getId() << 16) | $item->getDamage()); - } -} diff --git a/src/pocketmine/level/particle/LavaDripParticle.php b/src/pocketmine/level/particle/LavaDripParticle.php deleted file mode 100644 index 9d71da477e..0000000000 --- a/src/pocketmine/level/particle/LavaDripParticle.php +++ /dev/null @@ -1,32 +0,0 @@ -level = $level; - $this->allocateEmptySubs = $allocateEmptySubs; - } - - public function moveTo(int $x, int $y, int $z) : bool{ - if($this->currentChunk === null or $this->currentX !== ($x >> 4) or $this->currentZ !== ($z >> 4)){ - $this->currentX = $x >> 4; - $this->currentZ = $z >> 4; - $this->currentSubChunk = null; - - $this->currentChunk = $this->level->getChunk($this->currentX, $this->currentZ); - if($this->currentChunk === null){ - return false; - } - } - - if($this->currentSubChunk === null or $this->currentY !== ($y >> 4)){ - $this->currentY = $y >> 4; - - $this->currentSubChunk = $this->currentChunk->getSubChunk($y >> 4, $this->allocateEmptySubs); - if($this->currentSubChunk instanceof EmptySubChunk){ - return false; - } - } - - return true; - } - - public function invalidate() : void{ - $this->currentChunk = null; - $this->currentSubChunk = null; - } -} diff --git a/src/pocketmine/metadata/BlockMetadataStore.php b/src/pocketmine/metadata/BlockMetadataStore.php deleted file mode 100644 index 04c69b21aa..0000000000 --- a/src/pocketmine/metadata/BlockMetadataStore.php +++ /dev/null @@ -1,69 +0,0 @@ -owningLevel = $owningLevel; - } - - private function disambiguate(Block $block, string $metadataKey) : string{ - if($block->getLevel() !== $this->owningLevel){ - throw new \InvalidStateException("Block does not belong to world " . $this->owningLevel->getName()); - } - return $block->x . ":" . $block->y . ":" . $block->z . ":" . $metadataKey; - } - - /** - * @return MetadataValue[] - */ - public function getMetadata(Block $subject, string $metadataKey){ - return $this->getMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - public function hasMetadata(Block $subject, string $metadataKey) : bool{ - return $this->hasMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - /** - * @return void - */ - public function removeMetadata(Block $subject, string $metadataKey, Plugin $owningPlugin){ - $this->removeMetadataInternal($this->disambiguate($subject, $metadataKey), $owningPlugin); - } - - /** - * @return void - */ - public function setMetadata(Block $subject, string $metadataKey, MetadataValue $newMetadataValue){ - $this->setMetadataInternal($this->disambiguate($subject, $metadataKey), $newMetadataValue); - } -} diff --git a/src/pocketmine/metadata/EntityMetadataStore.php b/src/pocketmine/metadata/EntityMetadataStore.php deleted file mode 100644 index e040f9f4c9..0000000000 --- a/src/pocketmine/metadata/EntityMetadataStore.php +++ /dev/null @@ -1,59 +0,0 @@ -getId() . ":" . $metadataKey; - } - - /** - * @return MetadataValue[] - */ - public function getMetadata(Entity $subject, string $metadataKey){ - return $this->getMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - public function hasMetadata(Entity $subject, string $metadataKey) : bool{ - return $this->hasMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - /** - * @return void - */ - public function removeMetadata(Entity $subject, string $metadataKey, Plugin $owningPlugin){ - $this->removeMetadataInternal($this->disambiguate($subject, $metadataKey), $owningPlugin); - } - - /** - * @return void - */ - public function setMetadata(Entity $subject, string $metadataKey, MetadataValue $newMetadataValue){ - $this->setMetadataInternal($this->disambiguate($subject, $metadataKey), $newMetadataValue); - } -} diff --git a/src/pocketmine/metadata/LevelMetadataStore.php b/src/pocketmine/metadata/LevelMetadataStore.php deleted file mode 100644 index 8866bca9d0..0000000000 --- a/src/pocketmine/metadata/LevelMetadataStore.php +++ /dev/null @@ -1,60 +0,0 @@ -getName()) . ":" . $metadataKey; - } - - /** - * @return MetadataValue[] - */ - public function getMetadata(Level $subject, string $metadataKey){ - return $this->getMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - public function hasMetadata(Level $subject, string $metadataKey) : bool{ - return $this->hasMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - /** - * @return void - */ - public function removeMetadata(Level $subject, string $metadataKey, Plugin $owningPlugin){ - $this->removeMetadataInternal($this->disambiguate($subject, $metadataKey), $owningPlugin); - } - - /** - * @return void - */ - public function setMetadata(Level $subject, string $metadataKey, MetadataValue $newMetadataValue){ - $this->setMetadataInternal($this->disambiguate($subject, $metadataKey), $newMetadataValue); - } -} diff --git a/src/pocketmine/metadata/MetadataStore.php b/src/pocketmine/metadata/MetadataStore.php deleted file mode 100644 index 5882ea6f45..0000000000 --- a/src/pocketmine/metadata/MetadataStore.php +++ /dev/null @@ -1,105 +0,0 @@ -> - */ - private $metadataMap; - - /** - * Adds a metadata value to an object. - * - * @return void - */ - protected function setMetadataInternal(string $key, MetadataValue $newMetadataValue){ - $owningPlugin = $newMetadataValue->getOwningPlugin(); - - if(!isset($this->metadataMap[$key])){ - /** @phpstan-var \SplObjectStorage $entry */ - $entry = new \SplObjectStorage(); - $this->metadataMap[$key] = $entry; - }else{ - $entry = $this->metadataMap[$key]; - } - $entry[$owningPlugin] = $newMetadataValue; - } - - /** - * Returns all metadata values attached to an object. If multiple - * have attached metadata, each will value will be included. - * - * @return MetadataValue[] - */ - protected function getMetadataInternal(string $key){ - if(isset($this->metadataMap[$key])){ - return $this->metadataMap[$key]; - }else{ - return []; - } - } - - /** - * Tests to see if a metadata attribute has been set on an object. - */ - protected function hasMetadataInternal(string $key) : bool{ - return isset($this->metadataMap[$key]); - } - - /** - * Removes a metadata item owned by a plugin from a subject. - * - * @return void - */ - protected function removeMetadataInternal(string $key, Plugin $owningPlugin){ - if(isset($this->metadataMap[$key])){ - unset($this->metadataMap[$key][$owningPlugin]); - if($this->metadataMap[$key]->count() === 0){ - unset($this->metadataMap[$key]); - } - } - } - - /** - * Invalidates all metadata in the metadata store that originates from the - * given plugin. Doing this will force each invalidated metadata item to - * be recalculated the next time it is accessed. - * - * @return void - */ - public function invalidateAll(Plugin $owningPlugin){ - foreach($this->metadataMap as $values){ - if(isset($values[$owningPlugin])){ - $values[$owningPlugin]->invalidate(); - } - } - } -} diff --git a/src/pocketmine/metadata/Metadatable.php b/src/pocketmine/metadata/Metadatable.php deleted file mode 100644 index 297c2ee652..0000000000 --- a/src/pocketmine/metadata/Metadatable.php +++ /dev/null @@ -1,59 +0,0 @@ -getName()) . ":" . $metadataKey; - } - - /** - * @return MetadataValue[] - */ - public function getMetadata(IPlayer $subject, string $metadataKey){ - return $this->getMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - public function hasMetadata(IPlayer $subject, string $metadataKey) : bool{ - return $this->hasMetadataInternal($this->disambiguate($subject, $metadataKey)); - } - - /** - * @return void - */ - public function removeMetadata(IPlayer $subject, string $metadataKey, Plugin $owningPlugin){ - $this->removeMetadataInternal($this->disambiguate($subject, $metadataKey), $owningPlugin); - } - - /** - * @return void - */ - public function setMetadata(IPlayer $subject, string $metadataKey, MetadataValue $newMetadataValue){ - $this->setMetadataInternal($this->disambiguate($subject, $metadataKey), $newMetadataValue); - } -} diff --git a/src/pocketmine/network/CompressBatchedTask.php b/src/pocketmine/network/CompressBatchedTask.php deleted file mode 100644 index a170a1563d..0000000000 --- a/src/pocketmine/network/CompressBatchedTask.php +++ /dev/null @@ -1,66 +0,0 @@ -data = $batch->payload; - $this->level = $batch->getCompressionLevel(); - $this->storeLocal($targets); - } - - public function onRun(){ - $batch = new BatchPacket(); - $batch->payload = $this->data; - - $batch->setCompressionLevel($this->level); - $batch->encode(); - - $this->setResult($batch->buffer); - } - - public function onCompletion(Server $server){ - $pk = new BatchPacket($this->getResult()); - $pk->isEncoded = true; - - /** @var Player[] $targets */ - $targets = $this->fetchLocal(); - - $server->broadcastPacketsCallback($pk, $targets); - } -} diff --git a/src/pocketmine/network/Network.php b/src/pocketmine/network/Network.php deleted file mode 100644 index 0a8b27c99f..0000000000 --- a/src/pocketmine/network/Network.php +++ /dev/null @@ -1,202 +0,0 @@ -server = $server; - - } - - /** - * @param float $upload - * @param float $download - * - * @return void - */ - public function addStatistics($upload, $download){ - $this->upload += $upload; - $this->download += $download; - } - - /** - * @return float - */ - public function getUpload(){ - return $this->upload; - } - - /** - * @return float - */ - public function getDownload(){ - return $this->download; - } - - /** - * @return void - */ - public function resetStatistics(){ - $this->upload = 0; - $this->download = 0; - } - - /** - * @return SourceInterface[] - */ - public function getInterfaces() : array{ - return $this->interfaces; - } - - /** - * @return void - */ - public function processInterfaces(){ - foreach($this->interfaces as $interface){ - $interface->process(); - } - } - - /** - * @deprecated - */ - public function processInterface(SourceInterface $interface) : void{ - $interface->process(); - } - - /** - * @return void - */ - public function registerInterface(SourceInterface $interface){ - $ev = new NetworkInterfaceRegisterEvent($interface); - $ev->call(); - if(!$ev->isCancelled()){ - $interface->start(); - $this->interfaces[$hash = spl_object_hash($interface)] = $interface; - if($interface instanceof AdvancedSourceInterface){ - $this->advancedInterfaces[$hash] = $interface; - $interface->setNetwork($this); - } - $interface->setName($this->name); - } - } - - /** - * @return void - */ - public function unregisterInterface(SourceInterface $interface){ - (new NetworkInterfaceUnregisterEvent($interface))->call(); - unset($this->interfaces[$hash = spl_object_hash($interface)], $this->advancedInterfaces[$hash]); - } - - /** - * Sets the server name shown on each interface Query - * - * @return void - */ - public function setName(string $name){ - $this->name = $name; - foreach($this->interfaces as $interface){ - $interface->setName($this->name); - } - } - - public function getName() : string{ - return $this->name; - } - - /** - * @return void - */ - public function updateName(){ - foreach($this->interfaces as $interface){ - $interface->setName($this->name); - } - } - - public function getServer() : Server{ - return $this->server; - } - - /** - * @return void - */ - public function sendPacket(string $address, int $port, string $payload){ - foreach($this->advancedInterfaces as $interface){ - $interface->sendRawPacket($address, $port, $payload); - } - } - - /** - * Blocks an IP address from the main interface. Setting timeout to -1 will block it forever - * - * @return void - */ - public function blockAddress(string $address, int $timeout = 300){ - foreach($this->advancedInterfaces as $interface){ - $interface->blockAddress($address, $timeout); - } - } - - /** - * @return void - */ - public function unblockAddress(string $address){ - foreach($this->advancedInterfaces as $interface){ - $interface->unblockAddress($address); - } - } -} diff --git a/src/pocketmine/network/mcpe/NetworkBinaryStream.php b/src/pocketmine/network/mcpe/NetworkBinaryStream.php deleted file mode 100644 index d07644f5fa..0000000000 --- a/src/pocketmine/network/mcpe/NetworkBinaryStream.php +++ /dev/null @@ -1,871 +0,0 @@ - - -use pocketmine\block\BlockIds; -use pocketmine\entity\Attribute; -use pocketmine\entity\Entity; -use pocketmine\item\Durable; -use pocketmine\item\Item; -use pocketmine\item\ItemFactory; -use pocketmine\item\ItemIds; -use pocketmine\math\Vector3; -use pocketmine\nbt\LittleEndianNBTStream; -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\nbt\tag\IntTag; -use pocketmine\nbt\tag\NamedTag; -use pocketmine\network\mcpe\convert\ItemTranslator; -use pocketmine\network\mcpe\convert\ItemTypeDictionary; -use pocketmine\network\mcpe\convert\RuntimeBlockMapping; -use pocketmine\network\mcpe\protocol\types\CommandOriginData; -use pocketmine\network\mcpe\protocol\types\EntityLink; -use pocketmine\network\mcpe\protocol\types\GameRuleType; -use pocketmine\network\mcpe\protocol\types\PersonaPieceTintColor; -use pocketmine\network\mcpe\protocol\types\PersonaSkinPiece; -use pocketmine\network\mcpe\protocol\types\SkinAnimation; -use pocketmine\network\mcpe\protocol\types\SkinData; -use pocketmine\network\mcpe\protocol\types\SkinImage; -use pocketmine\network\mcpe\protocol\types\StructureEditorData; -use pocketmine\network\mcpe\protocol\types\StructureSettings; -use pocketmine\utils\BinaryStream; -use pocketmine\utils\UUID; -use function assert; -use function count; -use function strlen; - -class NetworkBinaryStream extends BinaryStream{ - - private const DAMAGE_TAG = "Damage"; //TAG_Int - private const DAMAGE_TAG_CONFLICT_RESOLUTION = "___Damage_ProtocolCollisionResolution___"; - private const PM_META_TAG = "___Meta___"; - - public function getString() : string{ - return $this->get($this->getUnsignedVarInt()); - } - - public function putString(string $v) : void{ - $this->putUnsignedVarInt(strlen($v)); - $this->put($v); - } - - public function getUUID() : UUID{ - //This is actually two little-endian longs: UUID Most followed by UUID Least - $part1 = $this->getLInt(); - $part0 = $this->getLInt(); - $part3 = $this->getLInt(); - $part2 = $this->getLInt(); - - return new UUID($part0, $part1, $part2, $part3); - } - - public function putUUID(UUID $uuid) : void{ - $this->putLInt($uuid->getPart(1)); - $this->putLInt($uuid->getPart(0)); - $this->putLInt($uuid->getPart(3)); - $this->putLInt($uuid->getPart(2)); - } - - public function getSkin() : SkinData{ - $skinId = $this->getString(); - $skinPlayFabId = $this->getString(); - $skinResourcePatch = $this->getString(); - $skinData = $this->getSkinImage(); - $animationCount = $this->getLInt(); - $animations = []; - for($i = 0; $i < $animationCount; ++$i){ - $skinImage = $this->getSkinImage(); - $animationType = $this->getLInt(); - $animationFrames = $this->getLFloat(); - $expressionType = $this->getLInt(); - $animations[] = new SkinAnimation($skinImage, $animationType, $animationFrames, $expressionType); - } - $capeData = $this->getSkinImage(); - $geometryData = $this->getString(); - $geometryDataVersion = $this->getString(); - $animationData = $this->getString(); - $capeId = $this->getString(); - $fullSkinId = $this->getString(); - $armSize = $this->getString(); - $skinColor = $this->getString(); - $personaPieceCount = $this->getLInt(); - $personaPieces = []; - for($i = 0; $i < $personaPieceCount; ++$i){ - $pieceId = $this->getString(); - $pieceType = $this->getString(); - $packId = $this->getString(); - $isDefaultPiece = $this->getBool(); - $productId = $this->getString(); - $personaPieces[] = new PersonaSkinPiece($pieceId, $pieceType, $packId, $isDefaultPiece, $productId); - } - $pieceTintColorCount = $this->getLInt(); - $pieceTintColors = []; - for($i = 0; $i < $pieceTintColorCount; ++$i){ - $pieceType = $this->getString(); - $colorCount = $this->getLInt(); - $colors = []; - for($j = 0; $j < $colorCount; ++$j){ - $colors[] = $this->getString(); - } - $pieceTintColors[] = new PersonaPieceTintColor( - $pieceType, - $colors - ); - } - $premium = $this->getBool(); - $persona = $this->getBool(); - $capeOnClassic = $this->getBool(); - $isPrimaryUser = $this->getBool(); - - return new SkinData($skinId, $skinPlayFabId, $skinResourcePatch, $skinData, $animations, $capeData, $geometryData, $geometryDataVersion, $animationData, $capeId, $fullSkinId, $armSize, $skinColor, $personaPieces, $pieceTintColors, true, $premium, $persona, $capeOnClassic, $isPrimaryUser); - } - - /** - * @return void - */ - public function putSkin(SkinData $skin){ - $this->putString($skin->getSkinId()); - $this->putString($skin->getPlayFabId()); - $this->putString($skin->getResourcePatch()); - $this->putSkinImage($skin->getSkinImage()); - $this->putLInt(count($skin->getAnimations())); - foreach($skin->getAnimations() as $animation){ - $this->putSkinImage($animation->getImage()); - $this->putLInt($animation->getType()); - $this->putLFloat($animation->getFrames()); - $this->putLInt($animation->getExpressionType()); - } - $this->putSkinImage($skin->getCapeImage()); - $this->putString($skin->getGeometryData()); - $this->putString($skin->getGeometryDataEngineVersion()); - $this->putString($skin->getAnimationData()); - $this->putString($skin->getCapeId()); - $this->putString($skin->getFullSkinId()); - $this->putString($skin->getArmSize()); - $this->putString($skin->getSkinColor()); - $this->putLInt(count($skin->getPersonaPieces())); - foreach($skin->getPersonaPieces() as $piece){ - $this->putString($piece->getPieceId()); - $this->putString($piece->getPieceType()); - $this->putString($piece->getPackId()); - $this->putBool($piece->isDefaultPiece()); - $this->putString($piece->getProductId()); - } - $this->putLInt(count($skin->getPieceTintColors())); - foreach($skin->getPieceTintColors() as $tint){ - $this->putString($tint->getPieceType()); - $this->putLInt(count($tint->getColors())); - foreach($tint->getColors() as $color){ - $this->putString($color); - } - } - $this->putBool($skin->isPremium()); - $this->putBool($skin->isPersona()); - $this->putBool($skin->isPersonaCapeOnClassic()); - $this->putBool($skin->isPrimaryUser()); - } - - private function getSkinImage() : SkinImage{ - $width = $this->getLInt(); - $height = $this->getLInt(); - $data = $this->getString(); - return new SkinImage($height, $width, $data); - } - - private function putSkinImage(SkinImage $image) : void{ - $this->putLInt($image->getWidth()); - $this->putLInt($image->getHeight()); - $this->putString($image->getData()); - } - - public function getItemStackWithoutStackId() : Item{ - return $this->getItemStack(function() : void{ - //NOOP - }); - } - - public function putItemStackWithoutStackId(Item $item) : void{ - $this->putItemStack($item, function() : void{ - //NOOP - }); - } - - /** - * @phpstan-param \Closure(NetworkBinaryStream) : void $readExtraCrapInTheMiddle - */ - public function getItemStack(\Closure $readExtraCrapInTheMiddle) : Item{ - $netId = $this->getVarInt(); - if($netId === 0){ - return ItemFactory::get(0, 0, 0); - } - - $cnt = $this->getLShort(); - $netData = $this->getUnsignedVarInt(); - - [$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($netId, $netData); - - $readExtraCrapInTheMiddle($this); - - $this->getVarInt(); - - $extraData = new NetworkBinaryStream($this->getString()); - return (static function() use ($extraData, $netId, $id, $meta, $cnt) : Item{ - $nbtLen = $extraData->getLShort(); - - /** @var CompoundTag|null $nbt */ - $nbt = null; - if($nbtLen === 0xffff){ - $nbtDataVersion = $extraData->getByte(); - if($nbtDataVersion !== 1){ - throw new \UnexpectedValueException("Unexpected NBT data version $nbtDataVersion"); - } - $decodedNBT = (new LittleEndianNBTStream())->read($extraData->buffer, false, $extraData->offset, 512); - if(!($decodedNBT instanceof CompoundTag)){ - throw new \UnexpectedValueException("Unexpected root tag type for itemstack"); - } - $nbt = $decodedNBT; - }elseif($nbtLen !== 0){ - throw new \UnexpectedValueException("Unexpected fake NBT length $nbtLen"); - } - - //TODO - for($i = 0, $canPlaceOn = $extraData->getLInt(); $i < $canPlaceOn; ++$i){ - $extraData->get($extraData->getLShort()); - } - - //TODO - for($i = 0, $canDestroy = $extraData->getLInt(); $i < $canDestroy; ++$i){ - $extraData->get($extraData->getLShort()); - } - - if($netId === ItemTypeDictionary::getInstance()->fromStringId("minecraft:shield")){ - $extraData->getLLong(); //"blocking tick" (ffs mojang) - } - - if(!$extraData->feof()){ - throw new \UnexpectedValueException("Unexpected trailing extradata for network item $netId"); - } - - if($nbt !== null){ - if($nbt->hasTag(self::DAMAGE_TAG, IntTag::class)){ - $meta = $nbt->getInt(self::DAMAGE_TAG); - $nbt->removeTag(self::DAMAGE_TAG); - if(($conflicted = $nbt->getTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION)) !== null){ - $nbt->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION); - $conflicted->setName(self::DAMAGE_TAG); - $nbt->setTag($conflicted); - }elseif($nbt->count() === 0){ - $nbt = null; - } - }elseif(($metaTag = $nbt->getTag(self::PM_META_TAG)) instanceof IntTag){ - //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. - $meta = $metaTag->getValue(); - $nbt->removeTag(self::PM_META_TAG); - if($nbt->count() === 0){ - $nbt = null; - } - } - } - return ItemFactory::get($id, $meta, $cnt, $nbt); - })(); - } - - /** - * @phpstan-param \Closure(NetworkBinaryStream) : void $writeExtraCrapInTheMiddle - */ - public function putItemStack(Item $item, \Closure $writeExtraCrapInTheMiddle) : void{ - if($item->getId() === 0){ - $this->putVarInt(0); - - return; - } - - $coreData = $item->getDamage(); - [$netId, $netData] = ItemTranslator::getInstance()->toNetworkId($item->getId(), $coreData); - - $this->putVarInt($netId); - $this->putLShort($item->getCount()); - $this->putUnsignedVarInt($netData); - - $writeExtraCrapInTheMiddle($this); - - $blockRuntimeId = 0; - $isBlockItem = $item->getId() < 256; - if($isBlockItem){ - $block = $item->getBlock(); - if($block->getId() !== BlockIds::AIR){ - $blockRuntimeId = RuntimeBlockMapping::toStaticRuntimeId($block->getId(), $block->getDamage()); - } - } - $this->putVarInt($blockRuntimeId); - - $nbt = null; - if($item->hasCompoundTag()){ - $nbt = clone $item->getNamedTag(); - } - if($item instanceof Durable and $coreData > 0){ - if($nbt !== null){ - if(($existing = $nbt->getTag(self::DAMAGE_TAG)) !== null){ - $nbt->removeTag(self::DAMAGE_TAG); - $existing->setName(self::DAMAGE_TAG_CONFLICT_RESOLUTION); - $nbt->setTag($existing); - } - }else{ - $nbt = new CompoundTag(); - } - $nbt->setInt(self::DAMAGE_TAG, $coreData); - }elseif($isBlockItem && $coreData !== 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, $coreData); - } - - $this->putString( - (static function() use ($nbt, $netId) : string{ - $extraData = new NetworkBinaryStream(); - - if($nbt !== null){ - $extraData->putLShort(0xffff); - $extraData->putByte(1); //TODO: NBT data version (?) - $extraData->put((new LittleEndianNBTStream())->write($nbt)); - }else{ - $extraData->putLShort(0); - } - - $extraData->putLInt(0); //CanPlaceOn entry count (TODO) - $extraData->putLInt(0); //CanDestroy entry count (TODO) - - if($netId === ItemTypeDictionary::getInstance()->fromStringId("minecraft:shield")){ - $extraData->putLLong(0); //"blocking tick" (ffs mojang) - } - return $extraData->getBuffer(); - })()); - } - - public function getRecipeIngredient() : Item{ - $netId = $this->getVarInt(); - if($netId === 0){ - return ItemFactory::get(ItemIds::AIR, 0, 0); - } - $netData = $this->getVarInt(); - [$id, $meta] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($netId, $netData); - $count = $this->getVarInt(); - return ItemFactory::get($id, $meta, $count); - } - - public function putRecipeIngredient(Item $item) : void{ - if($item->isNull()){ - $this->putVarInt(0); - }else{ - if($item->hasAnyDamageValue()){ - [$netId, ] = ItemTranslator::getInstance()->toNetworkId($item->getId(), 0); - $netData = 0x7fff; - }else{ - [$netId, $netData] = ItemTranslator::getInstance()->toNetworkId($item->getId(), $item->getDamage()); - } - $this->putVarInt($netId); - $this->putVarInt($netData); - $this->putVarInt($item->getCount()); - } - } - - /** - * Decodes entity metadata from the stream. - * - * @param bool $types Whether to include metadata types along with values in the returned array - * - * @return mixed[]|mixed[][] - * @phpstan-return array|array - */ - public function getEntityMetadata(bool $types = true) : array{ - $count = $this->getUnsignedVarInt(); - $data = []; - for($i = 0; $i < $count; ++$i){ - $key = $this->getUnsignedVarInt(); - $type = $this->getUnsignedVarInt(); - $value = null; - switch($type){ - case Entity::DATA_TYPE_BYTE: - $value = $this->getByte(); - break; - case Entity::DATA_TYPE_SHORT: - $value = $this->getSignedLShort(); - break; - case Entity::DATA_TYPE_INT: - $value = $this->getVarInt(); - break; - case Entity::DATA_TYPE_FLOAT: - $value = $this->getLFloat(); - break; - case Entity::DATA_TYPE_STRING: - $value = $this->getString(); - break; - case Entity::DATA_TYPE_COMPOUND_TAG: - $value = (new NetworkLittleEndianNBTStream())->read($this->buffer, false, $this->offset, 512); - break; - case Entity::DATA_TYPE_POS: - $value = new Vector3(); - $this->getSignedBlockPosition($value->x, $value->y, $value->z); - break; - case Entity::DATA_TYPE_LONG: - $value = $this->getVarLong(); - break; - case Entity::DATA_TYPE_VECTOR3F: - $value = $this->getVector3(); - break; - default: - throw new \UnexpectedValueException("Invalid data type " . $type); - } - if($types){ - $data[$key] = [$type, $value]; - }else{ - $data[$key] = $value; - } - } - - return $data; - } - - /** - * Writes entity metadata to the packet buffer. - * - * @param mixed[][] $metadata - * @phpstan-param array $metadata - */ - public function putEntityMetadata(array $metadata) : void{ - $this->putUnsignedVarInt(count($metadata)); - foreach($metadata as $key => $d){ - $this->putUnsignedVarInt($key); //data key - $this->putUnsignedVarInt($d[0]); //data type - switch($d[0]){ - case Entity::DATA_TYPE_BYTE: - $this->putByte($d[1]); - break; - case Entity::DATA_TYPE_SHORT: - $this->putLShort($d[1]); //SIGNED short! - break; - case Entity::DATA_TYPE_INT: - $this->putVarInt($d[1]); - break; - case Entity::DATA_TYPE_FLOAT: - $this->putLFloat($d[1]); - break; - case Entity::DATA_TYPE_STRING: - $this->putString($d[1]); - break; - case Entity::DATA_TYPE_COMPOUND_TAG: - $this->put((new NetworkLittleEndianNBTStream())->write($d[1])); - break; - case Entity::DATA_TYPE_POS: - $v = $d[1]; - if($v !== null){ - $this->putSignedBlockPosition($v->x, $v->y, $v->z); - }else{ - $this->putSignedBlockPosition(0, 0, 0); - } - break; - case Entity::DATA_TYPE_LONG: - $this->putVarLong($d[1]); - break; - case Entity::DATA_TYPE_VECTOR3F: - $this->putVector3Nullable($d[1]); - break; - default: - throw new \UnexpectedValueException("Invalid data type " . $d[0]); - } - } - } - - /** - * Reads a list of Attributes from the stream. - * @return Attribute[] - * - * @throws \UnexpectedValueException if reading an attribute with an unrecognized name - */ - public function getAttributeList() : array{ - $list = []; - $count = $this->getUnsignedVarInt(); - - for($i = 0; $i < $count; ++$i){ - $min = $this->getLFloat(); - $max = $this->getLFloat(); - $current = $this->getLFloat(); - $default = $this->getLFloat(); - $name = $this->getString(); - - $attr = Attribute::getAttributeByName($name); - if($attr !== null){ - $attr->setMinValue($min); - $attr->setMaxValue($max); - $attr->setValue($current); - $attr->setDefaultValue($default); - - $list[] = $attr; - }else{ - throw new \UnexpectedValueException("Unknown attribute type \"$name\""); - } - } - - return $list; - } - - /** - * Writes a list of Attributes to the packet buffer using the standard format. - * - * @param Attribute ...$attributes - */ - public function putAttributeList(Attribute ...$attributes) : void{ - $this->putUnsignedVarInt(count($attributes)); - foreach($attributes as $attribute){ - $this->putLFloat($attribute->getMinValue()); - $this->putLFloat($attribute->getMaxValue()); - $this->putLFloat($attribute->getValue()); - $this->putLFloat($attribute->getDefaultValue()); - $this->putString($attribute->getName()); - } - } - - /** - * Reads and returns an EntityUniqueID - */ - final public function getEntityUniqueId() : int{ - return $this->getVarLong(); - } - - /** - * Writes an EntityUniqueID - */ - public function putEntityUniqueId(int $eid) : void{ - $this->putVarLong($eid); - } - - /** - * Reads and returns an EntityRuntimeID - */ - final public function getEntityRuntimeId() : int{ - return $this->getUnsignedVarLong(); - } - - /** - * Writes an EntityRuntimeID - */ - public function putEntityRuntimeId(int $eid) : void{ - $this->putUnsignedVarLong($eid); - } - - /** - * Reads an block position with unsigned Y coordinate. - * - * @param int $x reference parameter - * @param int $y reference parameter - * @param int $z reference parameter - */ - public function getBlockPosition(&$x, &$y, &$z) : void{ - $x = $this->getVarInt(); - $y = $this->getUnsignedVarInt(); - $z = $this->getVarInt(); - } - - /** - * Writes a block position with unsigned Y coordinate. - */ - public function putBlockPosition(int $x, int $y, int $z) : void{ - $this->putVarInt($x); - $this->putUnsignedVarInt($y); - $this->putVarInt($z); - } - - /** - * Reads a block position with a signed Y coordinate. - * - * @param int $x reference parameter - * @param int $y reference parameter - * @param int $z reference parameter - */ - public function getSignedBlockPosition(&$x, &$y, &$z) : void{ - $x = $this->getVarInt(); - $y = $this->getVarInt(); - $z = $this->getVarInt(); - } - - /** - * Writes a block position with a signed Y coordinate. - */ - public function putSignedBlockPosition(int $x, int $y, int $z) : void{ - $this->putVarInt($x); - $this->putVarInt($y); - $this->putVarInt($z); - } - - /** - * Reads a floating-point Vector3 object with coordinates rounded to 4 decimal places. - */ - public function getVector3() : Vector3{ - $x = $this->getLFloat(); - $y = $this->getLFloat(); - $z = $this->getLFloat(); - return new Vector3($x, $y, $z); - } - - /** - * Writes a floating-point Vector3 object, or 3x zero if null is given. - * - * Note: ONLY use this where it is reasonable to allow not specifying the vector. - * For all other purposes, use the non-nullable version. - * - * @see NetworkBinaryStream::putVector3() - */ - public function putVector3Nullable(?Vector3 $vector) : void{ - if($vector !== null){ - $this->putVector3($vector); - }else{ - $this->putLFloat(0.0); - $this->putLFloat(0.0); - $this->putLFloat(0.0); - } - } - - /** - * Writes a floating-point Vector3 object - */ - public function putVector3(Vector3 $vector) : void{ - $this->putLFloat($vector->x); - $this->putLFloat($vector->y); - $this->putLFloat($vector->z); - } - - public function getByteRotation() : float{ - return ($this->getByte() * (360 / 256)); - } - - public function putByteRotation(float $rotation) : void{ - $this->putByte((int) ($rotation / (360 / 256))); - } - - /** - * Reads gamerules - * TODO: implement this properly - * - * @return mixed[][], members are in the structure [name => [type, value, isPlayerModifiable]] - * @phpstan-return array - */ - public function getGameRules() : array{ - $count = $this->getUnsignedVarInt(); - $rules = []; - for($i = 0; $i < $count; ++$i){ - $name = $this->getString(); - $isPlayerModifiable = $this->getBool(); - $type = $this->getUnsignedVarInt(); - $value = null; - switch($type){ - case GameRuleType::BOOL: - $value = $this->getBool(); - break; - case GameRuleType::INT: - $value = $this->getUnsignedVarInt(); - break; - case GameRuleType::FLOAT: - $value = $this->getLFloat(); - break; - } - - $rules[$name] = [$type, $value, $isPlayerModifiable]; - } - - return $rules; - } - - /** - * Writes a gamerule array, members should be in the structure [name => [type, value, isPlayerModifiable]] - * TODO: implement this properly - * - * @param mixed[][] $rules - * @phpstan-param array $rules - */ - public function putGameRules(array $rules) : void{ - $this->putUnsignedVarInt(count($rules)); - foreach($rules as $name => $rule){ - $this->putString($name); - $this->putBool($rule[2]); - $this->putUnsignedVarInt($rule[0]); - switch($rule[0]){ - case GameRuleType::BOOL: - $this->putBool($rule[1]); - break; - case GameRuleType::INT: - $this->putUnsignedVarInt($rule[1]); - break; - case GameRuleType::FLOAT: - $this->putLFloat($rule[1]); - break; - } - } - } - - protected function getEntityLink() : EntityLink{ - $fromEntityUniqueId = $this->getEntityUniqueId(); - $toEntityUniqueId = $this->getEntityUniqueId(); - $type = $this->getByte(); - $immediate = $this->getBool(); - $causedByRider = $this->getBool(); - return new EntityLink($fromEntityUniqueId, $toEntityUniqueId, $type, $immediate, $causedByRider); - } - - protected function putEntityLink(EntityLink $link) : void{ - $this->putEntityUniqueId($link->fromEntityUniqueId); - $this->putEntityUniqueId($link->toEntityUniqueId); - $this->putByte($link->type); - $this->putBool($link->immediate); - $this->putBool($link->causedByRider); - } - - protected function getCommandOriginData() : CommandOriginData{ - $result = new CommandOriginData(); - - $result->type = $this->getUnsignedVarInt(); - $result->uuid = $this->getUUID(); - $result->requestId = $this->getString(); - - if($result->type === CommandOriginData::ORIGIN_DEV_CONSOLE or $result->type === CommandOriginData::ORIGIN_TEST){ - $result->playerEntityUniqueId = $this->getVarLong(); - } - - return $result; - } - - protected function putCommandOriginData(CommandOriginData $data) : void{ - $this->putUnsignedVarInt($data->type); - $this->putUUID($data->uuid); - $this->putString($data->requestId); - - if($data->type === CommandOriginData::ORIGIN_DEV_CONSOLE or $data->type === CommandOriginData::ORIGIN_TEST){ - $this->putVarLong($data->playerEntityUniqueId); - } - } - - protected function getStructureSettings() : StructureSettings{ - $result = new StructureSettings(); - - $result->paletteName = $this->getString(); - - $result->ignoreEntities = $this->getBool(); - $result->ignoreBlocks = $this->getBool(); - - $this->getBlockPosition($result->structureSizeX, $result->structureSizeY, $result->structureSizeZ); - $this->getBlockPosition($result->structureOffsetX, $result->structureOffsetY, $result->structureOffsetZ); - - $result->lastTouchedByPlayerID = $this->getEntityUniqueId(); - $result->rotation = $this->getByte(); - $result->mirror = $this->getByte(); - $result->animationMode = $this->getByte(); - $result->animationSeconds = $this->getLFloat(); - $result->integrityValue = $this->getLFloat(); - $result->integritySeed = $this->getLInt(); - $result->pivot = $this->getVector3(); - - return $result; - } - - protected function putStructureSettings(StructureSettings $structureSettings) : void{ - $this->putString($structureSettings->paletteName); - - $this->putBool($structureSettings->ignoreEntities); - $this->putBool($structureSettings->ignoreBlocks); - - $this->putBlockPosition($structureSettings->structureSizeX, $structureSettings->structureSizeY, $structureSettings->structureSizeZ); - $this->putBlockPosition($structureSettings->structureOffsetX, $structureSettings->structureOffsetY, $structureSettings->structureOffsetZ); - - $this->putEntityUniqueId($structureSettings->lastTouchedByPlayerID); - $this->putByte($structureSettings->rotation); - $this->putByte($structureSettings->mirror); - $this->putByte($structureSettings->animationMode); - $this->putLFloat($structureSettings->animationSeconds); - $this->putLFloat($structureSettings->integrityValue); - $this->putLInt($structureSettings->integritySeed); - $this->putVector3($structureSettings->pivot); - } - - protected function getStructureEditorData() : StructureEditorData{ - $result = new StructureEditorData(); - - $result->structureName = $this->getString(); - $result->structureDataField = $this->getString(); - - $result->includePlayers = $this->getBool(); - $result->showBoundingBox = $this->getBool(); - - $result->structureBlockType = $this->getVarInt(); - $result->structureSettings = $this->getStructureSettings(); - $result->structureRedstoneSaveMove = $this->getVarInt(); - - return $result; - } - - protected function putStructureEditorData(StructureEditorData $structureEditorData) : void{ - $this->putString($structureEditorData->structureName); - $this->putString($structureEditorData->structureDataField); - - $this->putBool($structureEditorData->includePlayers); - $this->putBool($structureEditorData->showBoundingBox); - - $this->putVarInt($structureEditorData->structureBlockType); - $this->putStructureSettings($structureEditorData->structureSettings); - $this->putVarInt($structureEditorData->structureRedstoneSaveMove); - } - - public function getNbtRoot() : NamedTag{ - $offset = $this->getOffset(); - try{ - $result = (new NetworkLittleEndianNBTStream())->read($this->getBuffer(), false, $offset, 512); - assert($result instanceof NamedTag, "doMultiple is false so we should definitely have a NamedTag here"); - return $result; - }finally{ - $this->setOffset($offset); - } - } - - public function getNbtCompoundRoot() : CompoundTag{ - $root = $this->getNbtRoot(); - if(!($root instanceof CompoundTag)){ - throw new \UnexpectedValueException("Expected TAG_Compound root"); - } - return $root; - } - - public function readGenericTypeNetworkId() : int{ - return $this->getVarInt(); - } - - public function writeGenericTypeNetworkId(int $id) : void{ - $this->putVarInt($id); - } -} diff --git a/src/pocketmine/network/mcpe/NetworkSession.php b/src/pocketmine/network/mcpe/NetworkSession.php deleted file mode 100644 index f505e1ec1d..0000000000 --- a/src/pocketmine/network/mcpe/NetworkSession.php +++ /dev/null @@ -1,884 +0,0 @@ -server = $server; - $this->player = $player; - } - - public function handleDataPacket(DataPacket $packet){ - if(!$this->player->isConnected()){ - return; - } - - $timings = Timings::getReceiveDataPacketTimings($packet); - $timings->startTiming(); - - $packet->decode(); - if(!$packet->feof() and !$packet->mayHaveUnreadBytes()){ - $remains = substr($packet->buffer, $packet->offset); - $this->server->getLogger()->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": 0x" . bin2hex($remains)); - } - - $ev = new DataPacketReceiveEvent($this->player, $packet); - $ev->call(); - if(!$ev->isCancelled() and !$packet->handle($this)){ - $this->server->getLogger()->debug("Unhandled " . $packet->getName() . " received from " . $this->player->getName() . ": " . base64_encode($packet->buffer)); - } - - $timings->stopTiming(); - } - - public function handleLogin(LoginPacket $packet) : bool{ - return $this->player->handleLogin($packet); - } - - public function handleClientToServerHandshake(ClientToServerHandshakePacket $packet) : bool{ - return false; //TODO - } - - public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{ - return $this->player->handleResourcePackClientResponse($packet); - } - - public function handleText(TextPacket $packet) : bool{ - if($packet->type === TextPacket::TYPE_CHAT){ - return $this->player->chat($packet->message); - } - - return false; - } - - public function handleMovePlayer(MovePlayerPacket $packet) : bool{ - return $this->player->handleMovePlayer($packet); - } - - public function handleLevelSoundEventPacketV1(LevelSoundEventPacketV1 $packet) : bool{ - return true; //useless leftover from 1.8 - } - - public function handleActorEvent(ActorEventPacket $packet) : bool{ - return $this->player->handleEntityEvent($packet); - } - - public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{ - return $this->player->handleInventoryTransaction($packet); - } - - public function handleMobEquipment(MobEquipmentPacket $packet) : bool{ - return $this->player->handleMobEquipment($packet); - } - - public function handleMobArmorEquipment(MobArmorEquipmentPacket $packet) : bool{ - return true; //Not used - } - - public function handleInteract(InteractPacket $packet) : bool{ - return $this->player->handleInteract($packet); - } - - public function handleBlockPickRequest(BlockPickRequestPacket $packet) : bool{ - return $this->player->handleBlockPickRequest($packet); - } - - public function handleActorPickRequest(ActorPickRequestPacket $packet) : bool{ - return false; //TODO - } - - public function handlePlayerAction(PlayerActionPacket $packet) : bool{ - return $this->player->handlePlayerAction($packet); - } - - public function handleAnimate(AnimatePacket $packet) : bool{ - return $this->player->handleAnimate($packet); - } - - public function handleRespawn(RespawnPacket $packet) : bool{ - return $this->player->handleRespawn($packet); - } - - public function handleContainerClose(ContainerClosePacket $packet) : bool{ - return $this->player->handleContainerClose($packet); - } - - public function handlePlayerHotbar(PlayerHotbarPacket $packet) : bool{ - return true; //this packet is useless - } - - public function handleCraftingEvent(CraftingEventPacket $packet) : bool{ - return true; //this is a broken useless packet, so we don't use it - } - - public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{ - return $this->player->handleAdventureSettings($packet); - } - - public function handleBlockActorData(BlockActorDataPacket $packet) : bool{ - return $this->player->handleBlockEntityData($packet); - } - - public function handlePlayerInput(PlayerInputPacket $packet) : bool{ - return false; //TODO - } - - public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{ - return $this->player->handleSetPlayerGameType($packet); - } - - public function handleSpawnExperienceOrb(SpawnExperienceOrbPacket $packet) : bool{ - return false; //TODO - } - - public function handleMapInfoRequest(MapInfoRequestPacket $packet) : bool{ - return false; //TODO - } - - public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{ - $this->player->setViewDistance($packet->radius); - - return true; - } - - public function handleItemFrameDropItem(ItemFrameDropItemPacket $packet) : bool{ - return $this->player->handleItemFrameDropItem($packet); - } - - public function handleBossEvent(BossEventPacket $packet) : bool{ - return false; //TODO - } - - public function handleShowCredits(ShowCreditsPacket $packet) : bool{ - return false; //TODO: handle resume - } - - public function handleCommandRequest(CommandRequestPacket $packet) : bool{ - return $this->player->chat($packet->command); - } - - public function handleCommandBlockUpdate(CommandBlockUpdatePacket $packet) : bool{ - return false; //TODO - } - - public function handleResourcePackChunkRequest(ResourcePackChunkRequestPacket $packet) : bool{ - return $this->player->handleResourcePackChunkRequest($packet); - } - - public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{ - return $this->player->changeSkin(SkinAdapterSingleton::get()->fromSkinData($packet->skin), $packet->newSkinName, $packet->oldSkinName); - } - - public function handleBookEdit(BookEditPacket $packet) : bool{ - return $this->player->handleBookEdit($packet); - } - - public function handleModalFormResponse(ModalFormResponsePacket $packet) : bool{ - return $this->player->onFormSubmit($packet->formId, self::stupid_json_decode($packet->formData, true)); - } - - /** - * Hack to work around a stupid bug in Minecraft W10 which causes empty strings to be sent unquoted in form responses. - * - * @return mixed - */ - private static function stupid_json_decode(string $json, bool $assoc = false){ - if(preg_match('/^\[(.+)\]$/s', $json, $matches) > 0){ - $raw = $matches[1]; - $lastComma = -1; - $newParts = []; - $inQuotes = false; - for($i = 0, $len = strlen($raw); $i <= $len; ++$i){ - if($i === $len or ($raw[$i] === "," and !$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 = '""'; - } - $newParts[] = $part; - $lastComma = $i; - }elseif($raw[$i] === '"'){ - if(!$inQuotes){ - $inQuotes = true; - }else{ - $backslashes = 0; - for(; $backslashes < $i && $raw[$i - $backslashes - 1] === "\\"; ++$backslashes){} - if(($backslashes % 2) === 0){ //unescaped quote - $inQuotes = false; - } - } - } - } - - $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)"); - } - - return $ret; - } - - return json_decode($json, $assoc); - } - - public function handleServerSettingsRequest(ServerSettingsRequestPacket $packet) : bool{ - return false; //TODO: GUI stuff - } - - public function handleSetLocalPlayerAsInitialized(SetLocalPlayerAsInitializedPacket $packet) : bool{ - $this->player->doFirstSpawn(); - return true; - } - - public function handleLevelSoundEvent(LevelSoundEventPacket $packet) : bool{ - return $this->player->handleLevelSoundEvent($packet); - } - - public function handleNetworkStackLatency(NetworkStackLatencyPacket $packet) : bool{ - return true; //TODO: implement this properly - this is here to silence debug spam from MCPE dev builds - } -} diff --git a/src/pocketmine/network/mcpe/RakLibInterface.php b/src/pocketmine/network/mcpe/RakLibInterface.php deleted file mode 100644 index 4097340b40..0000000000 --- a/src/pocketmine/network/mcpe/RakLibInterface.php +++ /dev/null @@ -1,281 +0,0 @@ -server = $server; - - $this->sleeper = new SleeperNotifier(); - $this->rakLib = new RakLibServer( - $this->server->getLogger(), - \pocketmine\COMPOSER_AUTOLOADER_PATH, - new InternetAddress($this->server->getIp(), $this->server->getPort(), 4), - (int) $this->server->getProperty("network.max-mtu-size", 1492), - self::MCPE_RAKNET_PROTOCOL_VERSION, - $this->sleeper - ); - $this->interface = new ServerHandler($this->rakLib, $this); - } - - public function start(){ - $this->server->getTickSleeper()->addNotifier($this->sleeper, function() : void{ - $this->process(); - }); - $this->rakLib->start(PTHREADS_INHERIT_CONSTANTS); //HACK: MainLogger needs constants for exception logging - } - - public function setNetwork(Network $network){ - $this->network = $network; - } - - public function process() : void{ - while($this->interface->handlePacket()){} - - if(!$this->rakLib->isRunning() and !$this->rakLib->isShutdown()){ - throw new \Exception("RakLib Thread crashed"); - } - } - - public function closeSession(string $identifier, string $reason) : void{ - if(isset($this->players[$identifier])){ - $player = $this->players[$identifier]; - unset($this->identifiers[spl_object_hash($player)]); - unset($this->players[$identifier]); - unset($this->identifiersACK[$identifier]); - $player->close($player->getLeaveMessage(), $reason); - } - } - - public function close(Player $player, string $reason = "unknown reason"){ - if(isset($this->identifiers[$h = spl_object_hash($player)])){ - unset($this->players[$this->identifiers[$h]]); - unset($this->identifiersACK[$this->identifiers[$h]]); - $this->interface->closeSession($this->identifiers[$h], $reason); - unset($this->identifiers[$h]); - } - } - - public function shutdown(){ - $this->server->getTickSleeper()->removeNotifier($this->sleeper); - $this->interface->shutdown(); - } - - public function emergencyShutdown(){ - $this->server->getTickSleeper()->removeNotifier($this->sleeper); - $this->interface->emergencyShutdown(); - } - - public function openSession(string $identifier, string $address, int $port, int $clientID) : void{ - $ev = new PlayerCreationEvent($this, Player::class, Player::class, $address, $port); - $ev->call(); - $class = $ev->getPlayerClass(); - - /** - * @var Player $player - * @see Player::__construct() - */ - $player = new $class($this, $ev->getAddress(), $ev->getPort()); - $this->players[$identifier] = $player; - $this->identifiersACK[$identifier] = 0; - $this->identifiers[spl_object_hash($player)] = $identifier; - $this->server->addPlayer($player); - } - - public function handleEncapsulated(string $identifier, EncapsulatedPacket $packet, int $flags) : void{ - if(isset($this->players[$identifier])){ - //get this now for blocking in case the player was closed before the exception was raised - $player = $this->players[$identifier]; - $address = $player->getAddress(); - try{ - if($packet->buffer !== ""){ - $pk = new BatchPacket($packet->buffer); - $player->handleDataPacket($pk); - } - }catch(\Throwable $e){ - $logger = $this->server->getLogger(); - $logger->debug("Packet " . (isset($pk) ? get_class($pk) : "unknown") . ": " . base64_encode($packet->buffer)); - $logger->logException($e); - - $player->close($player->getLeaveMessage(), "Internal server error"); - $this->interface->blockAddress($address, 5); - } - } - } - - public function blockAddress(string $address, int $timeout = 300){ - $this->interface->blockAddress($address, $timeout); - } - - public function unblockAddress(string $address){ - $this->interface->unblockAddress($address); - } - - public function handleRaw(string $address, int $port, string $payload) : void{ - $this->server->handlePacket($this, $address, $port, $payload); - } - - public function sendRawPacket(string $address, int $port, string $payload){ - $this->interface->sendRaw($address, $port, $payload); - } - - public function notifyACK(string $identifier, int $identifierACK) : void{ - - } - - public function setName(string $name){ - $info = $this->server->getQueryInformation(); - - $this->interface->sendOption("name", implode(";", - [ - "MCPE", - rtrim(addcslashes($name, ";"), '\\'), - ProtocolInfo::CURRENT_PROTOCOL, - ProtocolInfo::MINECRAFT_VERSION_NETWORK, - $info->getPlayerCount(), - $info->getMaxPlayerCount(), - $this->rakLib->getServerId(), - $this->server->getName(), - Server::getGamemodeName(Player::getClientFriendlyGamemode($this->server->getGamemode())) - ]) . ";" - ); - } - - /** - * @param bool $name - * - * @return void - */ - public function setPortCheck($name){ - $this->interface->sendOption("portChecking", $name); - } - - public function setPacketLimit(int $limit) : void{ - $this->interface->sendOption("packetLimit", $limit); - } - - public function handleOption(string $option, string $value) : void{ - if($option === "bandwidth"){ - $v = unserialize($value); - $this->network->addStatistics($v["up"], $v["down"]); - } - } - - public function putPacket(Player $player, DataPacket $packet, bool $needACK = false, bool $immediate = true){ - if(isset($this->identifiers[$h = spl_object_hash($player)])){ - $identifier = $this->identifiers[$h]; - if(!$packet->isEncoded){ - $packet->encode(); - } - - if($packet instanceof BatchPacket){ - if($needACK){ - $pk = new EncapsulatedPacket(); - $pk->identifierACK = $this->identifiersACK[$identifier]++; - $pk->buffer = $packet->buffer; - $pk->reliability = PacketReliability::RELIABLE_ORDERED; - $pk->orderChannel = 0; - }else{ - if(!isset($packet->__encapsulatedPacket)){ - $packet->__encapsulatedPacket = new CachedEncapsulatedPacket; - $packet->__encapsulatedPacket->identifierACK = null; - $packet->__encapsulatedPacket->buffer = $packet->buffer; - $packet->__encapsulatedPacket->reliability = PacketReliability::RELIABLE_ORDERED; - $packet->__encapsulatedPacket->orderChannel = 0; - } - $pk = $packet->__encapsulatedPacket; - } - - $this->interface->sendEncapsulated($identifier, $pk, ($needACK ? RakLib::FLAG_NEED_ACK : 0) | ($immediate ? RakLib::PRIORITY_IMMEDIATE : RakLib::PRIORITY_NORMAL)); - return $pk->identifierACK; - }else{ - $this->server->batchPackets([$player], [$packet], true, $immediate); - return null; - } - } - - return null; - } - - public function updatePing(string $identifier, int $pingMS) : void{ - if(isset($this->players[$identifier])){ - $this->players[$identifier]->updatePing($pingMS); - } - } -} diff --git a/src/pocketmine/network/mcpe/VerifyLoginTask.php b/src/pocketmine/network/mcpe/VerifyLoginTask.php deleted file mode 100644 index c1ccb540a2..0000000000 --- a/src/pocketmine/network/mcpe/VerifyLoginTask.php +++ /dev/null @@ -1,183 +0,0 @@ -storeLocal([$player, $packet]); - $this->chainJwts = serialize($packet->chainData["chain"]); - $this->clientDataJwt = $packet->clientDataJwt; - } - - public function onRun(){ - /** @var string[] $chainJwts */ - $chainJwts = unserialize($this->chainJwts); //Get it in a local variable to make sure it stays unserialized - - try{ - $currentKey = null; - $first = true; - - foreach($chainJwts as $jwt){ - $this->validateToken($jwt, $currentKey, $first); - $first = false; - } - - $this->validateToken($this->clientDataJwt, $currentKey); - - $this->error = null; - }catch(VerifyLoginException $e){ - $this->error = $e->getMessage(); - } - } - - /** - * @throws VerifyLoginException if errors are encountered - */ - private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{ - $rawParts = explode('.', $jwt); - if(count($rawParts) !== 3){ - throw new VerifyLoginException("Wrong number of JWT parts, expected 3, got " . count($rawParts)); - } - [$headB64, $payloadB64, $sigB64] = $rawParts; - - $headers = json_decode(base64_decode(strtr($headB64, '-_', '+/'), true), true); - - if($currentPublicKey === null){ - if(!$first){ - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.missingKey"); - } - - //First link, check that it is self-signed - $currentPublicKey = $headers["x5u"]; - }elseif($headers["x5u"] !== $currentPublicKey){ - //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature"); - } - - $plainSignature = base64_decode(strtr($sigB64, '-_', '+/'), true); - - //OpenSSL wants a DER-encoded signature, so we extract R and S from the plain signature and crudely serialize it. - - if(strlen($plainSignature) !== 96){ - throw new VerifyLoginException("Wrong signature length, expected 96, got " . strlen($plainSignature)); - } - - [$rString, $sString] = str_split($plainSignature, 48); - - $rString = ltrim($rString, "\x00"); - if(ord($rString[0]) >= 128){ //Would be considered signed, pad it with an extra zero - $rString = "\x00" . $rString; - } - - $sString = ltrim($sString, "\x00"); - if(ord($sString[0]) >= 128){ //Would be considered signed, pad it with an extra zero - $sString = "\x00" . $sString; - } - - //0x02 = Integer ASN.1 tag - $sequence = "\x02" . chr(strlen($rString)) . $rString . "\x02" . chr(strlen($sString)) . $sString; - //0x30 = Sequence ASN.1 tag - $derSignature = "\x30" . chr(strlen($sequence)) . $sequence; - - $v = openssl_verify("$headB64.$payloadB64", $derSignature, "-----BEGIN PUBLIC KEY-----\n" . wordwrap($currentPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----\n", OPENSSL_ALGO_SHA384); - if($v !== 1){ - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.badSignature"); - } - - if($currentPublicKey === self::MOJANG_ROOT_PUBLIC_KEY){ - $this->authenticated = true; //we're signed into xbox live - } - - $claims = json_decode(base64_decode(strtr($payloadB64, '-_', '+/'), true), true); - - $time = time(); - if(isset($claims["nbf"]) and $claims["nbf"] > $time + self::CLOCK_DRIFT_MAX){ - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.tooEarly"); - } - - if(isset($claims["exp"]) and $claims["exp"] < $time - self::CLOCK_DRIFT_MAX){ - throw new VerifyLoginException("%pocketmine.disconnect.invalidSession.tooLate"); - } - - $currentPublicKey = $claims["identityPublicKey"] ?? null; //if there are further links, the next link should be signed with this - } - - public function onCompletion(Server $server){ - /** - * @var Player $player - * @var LoginPacket $packet - */ - [$player, $packet] = $this->fetchLocal(); - if(!$player->isConnected()){ - $server->getLogger()->error("Player " . $player->getName() . " was disconnected before their login could be verified"); - }else{ - $player->onVerifyCompleted($packet, $this->error, $this->authenticated); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ActorEventPacket.php b/src/pocketmine/network/mcpe/protocol/ActorEventPacket.php deleted file mode 100644 index b49e47a35b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ActorEventPacket.php +++ /dev/null @@ -1,116 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ActorEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ACTOR_EVENT_PACKET; - - public const JUMP = 1; - public const HURT_ANIMATION = 2; - public const DEATH_ANIMATION = 3; - public const ARM_SWING = 4; - public const STOP_ATTACK = 5; - public const TAME_FAIL = 6; - public const TAME_SUCCESS = 7; - public const SHAKE_WET = 8; - public const USE_ITEM = 9; - public const EAT_GRASS_ANIMATION = 10; - public const FISH_HOOK_BUBBLE = 11; - public const FISH_HOOK_POSITION = 12; - public const FISH_HOOK_HOOK = 13; - public const FISH_HOOK_TEASE = 14; - public const SQUID_INK_CLOUD = 15; - public const ZOMBIE_VILLAGER_CURE = 16; - - public const RESPAWN = 18; - public const IRON_GOLEM_OFFER_FLOWER = 19; - public const IRON_GOLEM_WITHDRAW_FLOWER = 20; - public const LOVE_PARTICLES = 21; //breeding - public const VILLAGER_ANGRY = 22; - public const VILLAGER_HAPPY = 23; - public const WITCH_SPELL_PARTICLES = 24; - public const FIREWORK_PARTICLES = 25; - public const IN_LOVE_PARTICLES = 26; - public const SILVERFISH_SPAWN_ANIMATION = 27; - public const GUARDIAN_ATTACK = 28; - public const WITCH_DRINK_POTION = 29; - public const WITCH_THROW_POTION = 30; - public const MINECART_TNT_PRIME_FUSE = 31; - public const CREEPER_PRIME_FUSE = 32; - public const AIR_SUPPLY_EXPIRED = 33; - public const PLAYER_ADD_XP_LEVELS = 34; - public const ELDER_GUARDIAN_CURSE = 35; - public const AGENT_ARM_SWING = 36; - public const ENDER_DRAGON_DEATH = 37; - public const DUST_PARTICLES = 38; //not sure what this is - public const ARROW_SHAKE = 39; - - public const EATING_ITEM = 57; - - public const BABY_ANIMAL_FEED = 60; //green particles, like bonemeal on crops - public const DEATH_SMOKE_CLOUD = 61; - public const COMPLETE_TRADE = 62; - public const REMOVE_LEASH = 63; //data 1 = cut leash - - public const CONSUME_TOTEM = 65; - public const PLAYER_CHECK_TREASURE_HUNTER_ACHIEVEMENT = 66; //mojang... - public const ENTITY_SPAWN = 67; //used for MinecraftEventing stuff, not needed - public const DRAGON_PUKE = 68; //they call this puke particles - public const ITEM_ENTITY_MERGE = 69; - public const START_SWIM = 70; - public const BALLOON_POP = 71; - public const TREASURE_HUNT = 72; - public const AGENT_SUMMON = 73; - public const CHARGED_CROSSBOW = 74; - public const FALL = 75; - - //TODO: add more events - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $event; - /** @var int */ - public $data = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->event = $this->getByte(); - $this->data = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putByte($this->event); - $this->putVarInt($this->data); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleActorEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ActorPickRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ActorPickRequestPacket.php deleted file mode 100644 index ecb07296f9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ActorPickRequestPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ActorPickRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ACTOR_PICK_REQUEST_PACKET; - - /** @var int */ - public $entityUniqueId; - /** @var int */ - public $hotbarSlot; - /** @var bool */ - public $addUserData; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getLLong(); - $this->hotbarSlot = $this->getByte(); - $this->addUserData = $this->getBool(); - } - - protected function encodePayload(){ - $this->putLLong($this->entityUniqueId); - $this->putByte($this->hotbarSlot); - $this->putBool($this->addUserData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleActorPickRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddActorPacket.php b/src/pocketmine/network/mcpe/protocol/AddActorPacket.php deleted file mode 100644 index fd4802b685..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddActorPacket.php +++ /dev/null @@ -1,236 +0,0 @@ - - -use pocketmine\entity\Attribute; -use pocketmine\entity\EntityIds; -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\EntityLink; -use function count; - -class AddActorPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_ACTOR_PACKET; - - /* - * Really really really really really nasty hack, to preserve backwards compatibility. - * We can't transition to string IDs within 3.x because the network IDs (the integer ones) are exposed - * to the API in some places (for god's sake shoghi). - * - * TODO: remove this on 4.0 - */ - public const LEGACY_ID_MAP_BC = [ - EntityIds::NPC => "minecraft:npc", - EntityIds::PLAYER => "minecraft:player", - EntityIds::WITHER_SKELETON => "minecraft:wither_skeleton", - EntityIds::HUSK => "minecraft:husk", - EntityIds::STRAY => "minecraft:stray", - EntityIds::WITCH => "minecraft:witch", - EntityIds::ZOMBIE_VILLAGER => "minecraft:zombie_villager", - EntityIds::BLAZE => "minecraft:blaze", - EntityIds::MAGMA_CUBE => "minecraft:magma_cube", - EntityIds::GHAST => "minecraft:ghast", - EntityIds::CAVE_SPIDER => "minecraft:cave_spider", - EntityIds::SILVERFISH => "minecraft:silverfish", - EntityIds::ENDERMAN => "minecraft:enderman", - EntityIds::SLIME => "minecraft:slime", - EntityIds::ZOMBIE_PIGMAN => "minecraft:zombie_pigman", - EntityIds::SPIDER => "minecraft:spider", - EntityIds::SKELETON => "minecraft:skeleton", - EntityIds::CREEPER => "minecraft:creeper", - EntityIds::ZOMBIE => "minecraft:zombie", - EntityIds::SKELETON_HORSE => "minecraft:skeleton_horse", - EntityIds::MULE => "minecraft:mule", - EntityIds::DONKEY => "minecraft:donkey", - EntityIds::DOLPHIN => "minecraft:dolphin", - EntityIds::TROPICALFISH => "minecraft:tropicalfish", - EntityIds::WOLF => "minecraft:wolf", - EntityIds::SQUID => "minecraft:squid", - EntityIds::DROWNED => "minecraft:drowned", - EntityIds::SHEEP => "minecraft:sheep", - EntityIds::MOOSHROOM => "minecraft:mooshroom", - EntityIds::PANDA => "minecraft:panda", - EntityIds::SALMON => "minecraft:salmon", - EntityIds::PIG => "minecraft:pig", - EntityIds::VILLAGER => "minecraft:villager", - EntityIds::COD => "minecraft:cod", - EntityIds::PUFFERFISH => "minecraft:pufferfish", - EntityIds::COW => "minecraft:cow", - EntityIds::CHICKEN => "minecraft:chicken", - EntityIds::BALLOON => "minecraft:balloon", - EntityIds::LLAMA => "minecraft:llama", - EntityIds::IRON_GOLEM => "minecraft:iron_golem", - EntityIds::RABBIT => "minecraft:rabbit", - EntityIds::SNOW_GOLEM => "minecraft:snow_golem", - EntityIds::BAT => "minecraft:bat", - EntityIds::OCELOT => "minecraft:ocelot", - EntityIds::HORSE => "minecraft:horse", - EntityIds::CAT => "minecraft:cat", - EntityIds::POLAR_BEAR => "minecraft:polar_bear", - EntityIds::ZOMBIE_HORSE => "minecraft:zombie_horse", - EntityIds::TURTLE => "minecraft:turtle", - EntityIds::PARROT => "minecraft:parrot", - EntityIds::GUARDIAN => "minecraft:guardian", - EntityIds::ELDER_GUARDIAN => "minecraft:elder_guardian", - EntityIds::VINDICATOR => "minecraft:vindicator", - EntityIds::WITHER => "minecraft:wither", - EntityIds::ENDER_DRAGON => "minecraft:ender_dragon", - EntityIds::SHULKER => "minecraft:shulker", - EntityIds::ENDERMITE => "minecraft:endermite", - EntityIds::MINECART => "minecraft:minecart", - EntityIds::HOPPER_MINECART => "minecraft:hopper_minecart", - EntityIds::TNT_MINECART => "minecraft:tnt_minecart", - EntityIds::CHEST_MINECART => "minecraft:chest_minecart", - EntityIds::COMMAND_BLOCK_MINECART => "minecraft:command_block_minecart", - EntityIds::ARMOR_STAND => "minecraft:armor_stand", - EntityIds::ITEM => "minecraft:item", - EntityIds::TNT => "minecraft:tnt", - EntityIds::FALLING_BLOCK => "minecraft:falling_block", - EntityIds::XP_BOTTLE => "minecraft:xp_bottle", - EntityIds::XP_ORB => "minecraft:xp_orb", - EntityIds::EYE_OF_ENDER_SIGNAL => "minecraft:eye_of_ender_signal", - EntityIds::ENDER_CRYSTAL => "minecraft:ender_crystal", - EntityIds::SHULKER_BULLET => "minecraft:shulker_bullet", - EntityIds::FISHING_HOOK => "minecraft:fishing_hook", - EntityIds::DRAGON_FIREBALL => "minecraft:dragon_fireball", - EntityIds::ARROW => "minecraft:arrow", - EntityIds::SNOWBALL => "minecraft:snowball", - EntityIds::EGG => "minecraft:egg", - EntityIds::PAINTING => "minecraft:painting", - EntityIds::THROWN_TRIDENT => "minecraft:thrown_trident", - EntityIds::FIREBALL => "minecraft:fireball", - EntityIds::SPLASH_POTION => "minecraft:splash_potion", - EntityIds::ENDER_PEARL => "minecraft:ender_pearl", - EntityIds::LEASH_KNOT => "minecraft:leash_knot", - EntityIds::WITHER_SKULL => "minecraft:wither_skull", - EntityIds::WITHER_SKULL_DANGEROUS => "minecraft:wither_skull_dangerous", - EntityIds::BOAT => "minecraft:boat", - EntityIds::LIGHTNING_BOLT => "minecraft:lightning_bolt", - EntityIds::SMALL_FIREBALL => "minecraft:small_fireball", - EntityIds::LLAMA_SPIT => "minecraft:llama_spit", - EntityIds::AREA_EFFECT_CLOUD => "minecraft:area_effect_cloud", - EntityIds::LINGERING_POTION => "minecraft:lingering_potion", - EntityIds::FIREWORKS_ROCKET => "minecraft:fireworks_rocket", - EntityIds::EVOCATION_FANG => "minecraft:evocation_fang", - EntityIds::EVOCATION_ILLAGER => "minecraft:evocation_illager", - EntityIds::VEX => "minecraft:vex", - EntityIds::AGENT => "minecraft:agent", - EntityIds::ICE_BOMB => "minecraft:ice_bomb", - EntityIds::PHANTOM => "minecraft:phantom", - EntityIds::TRIPOD_CAMERA => "minecraft:tripod_camera" - ]; - - /** @var int|null */ - public $entityUniqueId = null; //TODO - /** @var int */ - public $entityRuntimeId; - /** @var string */ - public $type; - /** @var Vector3 */ - public $position; - /** @var Vector3|null */ - public $motion; - /** @var float */ - public $pitch = 0.0; - /** @var float */ - public $yaw = 0.0; - /** @var float */ - public $headYaw = 0.0; - - /** @var Attribute[] */ - public $attributes = []; - /** - * @var mixed[][] - * @phpstan-var array - */ - public $metadata = []; - /** @var EntityLink[] */ - public $links = []; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->type = $this->getString(); - $this->position = $this->getVector3(); - $this->motion = $this->getVector3(); - $this->pitch = $this->getLFloat(); - $this->yaw = $this->getLFloat(); - $this->headYaw = $this->getLFloat(); - - $attrCount = $this->getUnsignedVarInt(); - for($i = 0; $i < $attrCount; ++$i){ - $name = $this->getString(); - $min = $this->getLFloat(); - $current = $this->getLFloat(); - $max = $this->getLFloat(); - $attr = Attribute::getAttributeByName($name); - - if($attr !== null){ - $attr->setMinValue($min); - $attr->setMaxValue($max); - $attr->setValue($current); - $this->attributes[] = $attr; - }else{ - throw new \UnexpectedValueException("Unknown attribute type \"$name\""); - } - } - - $this->metadata = $this->getEntityMetadata(); - $linkCount = $this->getUnsignedVarInt(); - for($i = 0; $i < $linkCount; ++$i){ - $this->links[] = $this->getEntityLink(); - } - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->entityUniqueId ?? $this->entityRuntimeId); - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putString($this->type); - $this->putVector3($this->position); - $this->putVector3Nullable($this->motion); - $this->putLFloat($this->pitch); - $this->putLFloat($this->yaw); - $this->putLFloat($this->headYaw); - - $this->putUnsignedVarInt(count($this->attributes)); - foreach($this->attributes as $attribute){ - $this->putString($attribute->getName()); - $this->putLFloat($attribute->getMinValue()); - $this->putLFloat($attribute->getValue()); - $this->putLFloat($attribute->getMaxValue()); - } - - $this->putEntityMetadata($this->metadata); - $this->putUnsignedVarInt(count($this->links)); - foreach($this->links as $link){ - $this->putEntityLink($link); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAddActor($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddBehaviorTreePacket.php b/src/pocketmine/network/mcpe/protocol/AddBehaviorTreePacket.php deleted file mode 100644 index 135b80a66b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddBehaviorTreePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class AddBehaviorTreePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_BEHAVIOR_TREE_PACKET; - - /** @var string */ - public $behaviorTreeJson; - - protected function decodePayload(){ - $this->behaviorTreeJson = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->behaviorTreeJson); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAddBehaviorTree($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddEntityPacket.php b/src/pocketmine/network/mcpe/protocol/AddEntityPacket.php deleted file mode 100644 index a2c4e57dd6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddEntityPacket.php +++ /dev/null @@ -1,57 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class AddEntityPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ADD_ENTITY_PACKET; - - /** @var int */ - private $entityNetId; - - public static function create(int $entityNetId) : self{ - $result = new self; - $result->entityNetId = $entityNetId; - return $result; - } - - public function getEntityNetId() : int{ - return $this->entityNetId; - } - - protected function decodePayload() : void{ - $this->entityNetId = $this->getUnsignedVarInt(); - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt($this->entityNetId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleAddEntity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddItemActorPacket.php b/src/pocketmine/network/mcpe/protocol/AddItemActorPacket.php deleted file mode 100644 index dbe1fec4ef..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddItemActorPacket.php +++ /dev/null @@ -1,76 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; - -class AddItemActorPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_ITEM_ACTOR_PACKET; - - /** @var int|null */ - public $entityUniqueId = null; //TODO - /** @var int */ - public $entityRuntimeId; - /** @var ItemStackWrapper */ - public $item; - /** @var Vector3 */ - public $position; - /** @var Vector3|null */ - public $motion; - /** - * @var mixed[][] - * @phpstan-var array - */ - public $metadata = []; - /** @var bool */ - public $isFromFishing = false; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->item = ItemStackWrapper::read($this); - $this->position = $this->getVector3(); - $this->motion = $this->getVector3(); - $this->metadata = $this->getEntityMetadata(); - $this->isFromFishing = $this->getBool(); - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->entityUniqueId ?? $this->entityRuntimeId); - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->item->write($this); - $this->putVector3($this->position); - $this->putVector3Nullable($this->motion); - $this->putEntityMetadata($this->metadata); - $this->putBool($this->isFromFishing); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAddItemActor($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddPaintingPacket.php b/src/pocketmine/network/mcpe/protocol/AddPaintingPacket.php deleted file mode 100644 index a5f20b94c2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddPaintingPacket.php +++ /dev/null @@ -1,64 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class AddPaintingPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_PAINTING_PACKET; - - /** @var int|null */ - public $entityUniqueId = null; - /** @var int */ - public $entityRuntimeId; - /** @var Vector3 */ - public $position; - /** @var int */ - public $direction; - /** @var string */ - public $title; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->position = $this->getVector3(); - $this->direction = $this->getVarInt(); - $this->title = $this->getString(); - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->entityUniqueId ?? $this->entityRuntimeId); - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVector3($this->position); - $this->putVarInt($this->direction); - $this->putString($this->title); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAddPainting($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddPlayerPacket.php b/src/pocketmine/network/mcpe/protocol/AddPlayerPacket.php deleted file mode 100644 index 297469964f..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddPlayerPacket.php +++ /dev/null @@ -1,155 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\DeviceOS; -use pocketmine\network\mcpe\protocol\types\EntityLink; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; -use pocketmine\utils\UUID; -use function count; - -class AddPlayerPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_PLAYER_PACKET; - - /** @var UUID */ - public $uuid; - /** @var string */ - public $username; - /** @var int|null */ - public $entityUniqueId = null; //TODO - /** @var int */ - public $entityRuntimeId; - /** @var string */ - public $platformChatId = ""; - /** @var Vector3 */ - public $position; - /** @var Vector3|null */ - public $motion; - /** @var float */ - public $pitch = 0.0; - /** @var float */ - public $yaw = 0.0; - /** @var float|null */ - public $headYaw = null; //TODO - /** @var ItemStackWrapper */ - public $item; - /** - * @var mixed[][] - * @phpstan-var array - */ - public $metadata = []; - - //TODO: adventure settings stuff - /** @var int */ - public $uvarint1 = 0; - /** @var int */ - public $uvarint2 = 0; - /** @var int */ - public $uvarint3 = 0; - /** @var int */ - public $uvarint4 = 0; - /** @var int */ - public $uvarint5 = 0; - - /** @var int */ - public $long1 = 0; - - /** @var EntityLink[] */ - public $links = []; - - /** @var string */ - public $deviceId = ""; //TODO: fill player's device ID (???) - /** @var int */ - public $buildPlatform = DeviceOS::UNKNOWN; - - protected function decodePayload(){ - $this->uuid = $this->getUUID(); - $this->username = $this->getString(); - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->platformChatId = $this->getString(); - $this->position = $this->getVector3(); - $this->motion = $this->getVector3(); - $this->pitch = $this->getLFloat(); - $this->yaw = $this->getLFloat(); - $this->headYaw = $this->getLFloat(); - $this->item = ItemStackWrapper::read($this); - $this->metadata = $this->getEntityMetadata(); - - $this->uvarint1 = $this->getUnsignedVarInt(); - $this->uvarint2 = $this->getUnsignedVarInt(); - $this->uvarint3 = $this->getUnsignedVarInt(); - $this->uvarint4 = $this->getUnsignedVarInt(); - $this->uvarint5 = $this->getUnsignedVarInt(); - - $this->long1 = $this->getLLong(); - - $linkCount = $this->getUnsignedVarInt(); - for($i = 0; $i < $linkCount; ++$i){ - $this->links[$i] = $this->getEntityLink(); - } - - $this->deviceId = $this->getString(); - $this->buildPlatform = $this->getLInt(); - } - - protected function encodePayload(){ - $this->putUUID($this->uuid); - $this->putString($this->username); - $this->putEntityUniqueId($this->entityUniqueId ?? $this->entityRuntimeId); - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putString($this->platformChatId); - $this->putVector3($this->position); - $this->putVector3Nullable($this->motion); - $this->putLFloat($this->pitch); - $this->putLFloat($this->yaw); - $this->putLFloat($this->headYaw ?? $this->yaw); - $this->item->write($this); - $this->putEntityMetadata($this->metadata); - - $this->putUnsignedVarInt($this->uvarint1); - $this->putUnsignedVarInt($this->uvarint2); - $this->putUnsignedVarInt($this->uvarint3); - $this->putUnsignedVarInt($this->uvarint4); - $this->putUnsignedVarInt($this->uvarint5); - - $this->putLLong($this->long1); - - $this->putUnsignedVarInt(count($this->links)); - foreach($this->links as $link){ - $this->putEntityLink($link); - } - - $this->putString($this->deviceId); - $this->putLInt($this->buildPlatform); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAddPlayer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AddVolumeEntityPacket.php b/src/pocketmine/network/mcpe/protocol/AddVolumeEntityPacket.php deleted file mode 100644 index 033c3c1e55..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AddVolumeEntityPacket.php +++ /dev/null @@ -1,71 +0,0 @@ - - -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\NetworkSession; - -class AddVolumeEntityPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADD_VOLUME_ENTITY_PACKET; - - /** @var int */ - private $entityNetId; - /** @var CompoundTag */ - private $data; - /** @var string */ - private $engineVersion; - - public static function create(int $entityNetId, CompoundTag $data, string $engineVersion) : self{ - $result = new self; - $result->entityNetId = $entityNetId; - $result->data = $data; - $result->engineVersion = $engineVersion; - return $result; - } - - public function getEntityNetId() : int{ return $this->entityNetId; } - - public function getData() : CompoundTag{ return $this->data; } - - public function getEngineVersion() : string{ return $this->engineVersion; } - - protected function decodePayload() : void{ - $this->entityNetId = $this->getUnsignedVarInt(); - $this->data = $this->getNbtCompoundRoot(); - $this->engineVersion = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt($this->entityNetId); - $this->put((new NetworkLittleEndianNBTStream())->write($this->data)); - $this->putString($this->engineVersion); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleAddVolumeEntity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AdventureSettingsPacket.php b/src/pocketmine/network/mcpe/protocol/AdventureSettingsPacket.php deleted file mode 100644 index 427784d325..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AdventureSettingsPacket.php +++ /dev/null @@ -1,126 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\PlayerPermissions; - -class AdventureSettingsPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ADVENTURE_SETTINGS_PACKET; - - public const PERMISSION_NORMAL = 0; - public const PERMISSION_OPERATOR = 1; - public const PERMISSION_HOST = 2; - public const PERMISSION_AUTOMATION = 3; - public const PERMISSION_ADMIN = 4; - - /** - * This constant is used to identify flags that should be set on the second field. In a sensible world, these - * flags would all be set on the same packet field, but as of MCPE 1.2, the new abilities flags have for some - * reason been assigned a separate field. - */ - public const BITFLAG_SECOND_SET = 1 << 16; - - public const WORLD_IMMUTABLE = 0x01; - public const NO_PVP = 0x02; - - public const AUTO_JUMP = 0x20; - public const ALLOW_FLIGHT = 0x40; - public const NO_CLIP = 0x80; - public const WORLD_BUILDER = 0x100; - public const FLYING = 0x200; - public const MUTED = 0x400; - - public const MINE = 0x01 | self::BITFLAG_SECOND_SET; - public const DOORS_AND_SWITCHES = 0x02 | self::BITFLAG_SECOND_SET; - public const OPEN_CONTAINERS = 0x04 | self::BITFLAG_SECOND_SET; - public const ATTACK_PLAYERS = 0x08 | self::BITFLAG_SECOND_SET; - public const ATTACK_MOBS = 0x10 | self::BITFLAG_SECOND_SET; - public const OPERATOR = 0x20 | self::BITFLAG_SECOND_SET; - public const TELEPORT = 0x80 | self::BITFLAG_SECOND_SET; - public const BUILD = 0x100 | self::BITFLAG_SECOND_SET; - public const DEFAULT = 0x200 | self::BITFLAG_SECOND_SET; - - /** @var int */ - public $flags = 0; - /** @var int */ - public $commandPermission = self::PERMISSION_NORMAL; - /** @var int */ - public $flags2 = -1; - /** @var int */ - public $playerPermission = PlayerPermissions::MEMBER; - /** @var int */ - public $customFlags = 0; //... - /** @var int */ - public $entityUniqueId; //This is a little-endian long, NOT a var-long. (WTF Mojang) - - protected function decodePayload(){ - $this->flags = $this->getUnsignedVarInt(); - $this->commandPermission = $this->getUnsignedVarInt(); - $this->flags2 = $this->getUnsignedVarInt(); - $this->playerPermission = $this->getUnsignedVarInt(); - $this->customFlags = $this->getUnsignedVarInt(); - $this->entityUniqueId = $this->getLLong(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->flags); - $this->putUnsignedVarInt($this->commandPermission); - $this->putUnsignedVarInt($this->flags2); - $this->putUnsignedVarInt($this->playerPermission); - $this->putUnsignedVarInt($this->customFlags); - $this->putLLong($this->entityUniqueId); - } - - public function getFlag(int $flag) : bool{ - if(($flag & self::BITFLAG_SECOND_SET) !== 0){ - return ($this->flags2 & $flag) !== 0; - } - - return ($this->flags & $flag) !== 0; - } - - /** - * @return void - */ - public function setFlag(int $flag, bool $value){ - if(($flag & self::BITFLAG_SECOND_SET) !== 0){ - $flagSet =& $this->flags2; - }else{ - $flagSet =& $this->flags; - } - - if($value){ - $flagSet |= $flag; - }else{ - $flagSet &= ~$flag; - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAdventureSettings($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AnimateEntityPacket.php b/src/pocketmine/network/mcpe/protocol/AnimateEntityPacket.php deleted file mode 100644 index 4534376651..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AnimateEntityPacket.php +++ /dev/null @@ -1,115 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class AnimateEntityPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ANIMATE_ENTITY_PACKET; - - /** @var string */ - private $animation; - /** @var string */ - private $nextState; - /** @var string */ - private $stopExpression; - /** @var int */ - private $stopExpressionVersion; - /** @var string */ - private $controller; - /** @var float */ - private $blendOutTime; - /** - * @var int[] - * @phpstan-var list - */ - private $actorRuntimeIds; - - /** - * @param int[] $actorRuntimeIds - * @phpstan-param list $actorRuntimeIds - */ - public static function create(string $animation, string $nextState, string $stopExpression, int $stopExpressionVersion, string $controller, float $blendOutTime, array $actorRuntimeIds) : self{ - $result = new self; - $result->animation = $animation; - $result->nextState = $nextState; - $result->stopExpression = $stopExpression; - $result->stopExpressionVersion = $stopExpressionVersion; - $result->controller = $controller; - $result->blendOutTime = $blendOutTime; - $result->actorRuntimeIds = $actorRuntimeIds; - return $result; - } - - public function getAnimation() : string{ return $this->animation; } - - public function getNextState() : string{ return $this->nextState; } - - public function getStopExpression() : string{ return $this->stopExpression; } - - public function getStopExpressionVersion() : int{ return $this->stopExpressionVersion; } - - public function getController() : string{ return $this->controller; } - - public function getBlendOutTime() : float{ return $this->blendOutTime; } - - /** - * @return int[] - * @phpstan-return list - */ - public function getActorRuntimeIds() : array{ return $this->actorRuntimeIds; } - - protected function decodePayload() : void{ - $this->animation = $this->getString(); - $this->nextState = $this->getString(); - $this->stopExpression = $this->getString(); - $this->stopExpressionVersion = $this->getLInt(); - $this->controller = $this->getString(); - $this->blendOutTime = $this->getLFloat(); - $this->actorRuntimeIds = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->actorRuntimeIds[] = $this->getEntityRuntimeId(); - } - } - - protected function encodePayload() : void{ - $this->putString($this->animation); - $this->putString($this->nextState); - $this->putString($this->stopExpression); - $this->putLInt($this->stopExpressionVersion); - $this->putString($this->controller); - $this->putLFloat($this->blendOutTime); - $this->putUnsignedVarInt(count($this->actorRuntimeIds)); - foreach($this->actorRuntimeIds as $id){ - $this->putEntityRuntimeId($id); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleAnimateEntity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AnimatePacket.php b/src/pocketmine/network/mcpe/protocol/AnimatePacket.php deleted file mode 100644 index 38c14ceb84..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AnimatePacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class AnimatePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ANIMATE_PACKET; - - public const ACTION_SWING_ARM = 1; - - public const ACTION_STOP_SLEEP = 3; - public const ACTION_CRITICAL_HIT = 4; - public const ACTION_MAGICAL_CRITICAL_HIT = 5; - public const ACTION_ROW_RIGHT = 128; - public const ACTION_ROW_LEFT = 129; - - /** @var int */ - public $action; - /** @var int */ - public $entityRuntimeId; - /** @var float */ - public $float = 0.0; //TODO (Boat rowing time?) - - protected function decodePayload(){ - $this->action = $this->getVarInt(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - if(($this->action & 0x80) !== 0){ - $this->float = $this->getLFloat(); - } - } - - protected function encodePayload(){ - $this->putVarInt($this->action); - $this->putEntityRuntimeId($this->entityRuntimeId); - if(($this->action & 0x80) !== 0){ - $this->putLFloat($this->float); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAnimate($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AnvilDamagePacket.php b/src/pocketmine/network/mcpe/protocol/AnvilDamagePacket.php deleted file mode 100644 index f43c596e42..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AnvilDamagePacket.php +++ /dev/null @@ -1,78 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class AnvilDamagePacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ANVIL_DAMAGE_PACKET; - - /** @var int */ - private $x; - /** @var int */ - private $y; - /** @var int */ - private $z; - /** @var int */ - private $damageAmount; - - public static function create(int $x, int $y, int $z, int $damageAmount) : self{ - $result = new self; - [$result->x, $result->y, $result->z] = [$x, $y, $z]; - $result->damageAmount = $damageAmount; - return $result; - } - - public function getDamageAmount() : int{ - return $this->damageAmount; - } - - public function getX() : int{ - return $this->x; - } - - public function getY() : int{ - return $this->y; - } - - public function getZ() : int{ - return $this->z; - } - - protected function decodePayload() : void{ - $this->damageAmount = $this->getByte(); - $this->getBlockPosition($this->x, $this->y, $this->z); - } - - protected function encodePayload() : void{ - $this->putByte($this->damageAmount); - $this->putBlockPosition($this->x, $this->y, $this->z); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleAnvilDamage($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AutomationClientConnectPacket.php b/src/pocketmine/network/mcpe/protocol/AutomationClientConnectPacket.php deleted file mode 100644 index bca014048c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AutomationClientConnectPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class AutomationClientConnectPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::AUTOMATION_CLIENT_CONNECT_PACKET; - - /** @var string */ - public $serverUri; - - protected function decodePayload(){ - $this->serverUri = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->serverUri); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAutomationClientConnect($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php b/src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php deleted file mode 100644 index 933b212f7e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function file_get_contents; - -class AvailableActorIdentifiersPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::AVAILABLE_ACTOR_IDENTIFIERS_PACKET; - - /** @var string|null */ - private static $DEFAULT_NBT_CACHE = null; - - /** @var string */ - public $namedtag; - - protected function decodePayload(){ - $this->namedtag = $this->getRemaining(); - } - - protected function encodePayload(){ - $this->put( - $this->namedtag ?? - self::$DEFAULT_NBT_CACHE ?? - (self::$DEFAULT_NBT_CACHE = file_get_contents(\pocketmine\RESOURCE_PATH . '/vanilla/entity_identifiers.nbt')) - ); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAvailableActorIdentifiers($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php b/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php deleted file mode 100644 index 5b2d4c3d94..0000000000 --- a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php +++ /dev/null @@ -1,436 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\CommandData; -use pocketmine\network\mcpe\protocol\types\CommandEnum; -use pocketmine\network\mcpe\protocol\types\CommandEnumConstraint; -use pocketmine\network\mcpe\protocol\types\CommandParameter; -use pocketmine\utils\BinaryDataException; -use function array_search; -use function count; -use function dechex; - -class AvailableCommandsPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::AVAILABLE_COMMANDS_PACKET; - - /** - * This flag is set on all types EXCEPT the POSTFIX type. Not completely sure what this is for, but it is required - * for the argtype to work correctly. VALID seems as good a name as any. - */ - public const ARG_FLAG_VALID = 0x100000; - - /** - * Basic parameter types. These must be combined with the ARG_FLAG_VALID constant. - * ARG_FLAG_VALID | (type const) - */ - public const ARG_TYPE_INT = 0x01; - public const ARG_TYPE_FLOAT = 0x03; - public const ARG_TYPE_VALUE = 0x04; - public const ARG_TYPE_WILDCARD_INT = 0x05; - public const ARG_TYPE_OPERATOR = 0x06; - public const ARG_TYPE_TARGET = 0x07; - public const ARG_TYPE_WILDCARD_TARGET = 0x08; - - public const ARG_TYPE_FILEPATH = 0x10; - - public const ARG_TYPE_STRING = 0x20; - - public const ARG_TYPE_POSITION = 0x28; - - public const ARG_TYPE_MESSAGE = 0x2c; - - public const ARG_TYPE_RAWTEXT = 0x2e; - - public const ARG_TYPE_JSON = 0x32; - - public const ARG_TYPE_COMMAND = 0x3f; - - /** - * Enums are a little different: they are composed as follows: - * ARG_FLAG_ENUM | ARG_FLAG_VALID | (enum index) - */ - public const ARG_FLAG_ENUM = 0x200000; - - /** This is used for /xp L. It can only be applied to integer parameters. */ - public const ARG_FLAG_POSTFIX = 0x1000000; - - public const HARDCODED_ENUM_NAMES = [ - "CommandName" => true - ]; - - /** - * @var CommandData[] - * List of command data, including name, description, alias indexes and parameters. - */ - public $commandData = []; - - /** - * @var CommandEnum[] - * List of enums which aren't directly referenced by any vanilla command. - * This is used for the `CommandName` enum, which is a magic enum used by the `command` argument type. - */ - public $hardcodedEnums = []; - - /** - * @var CommandEnum[] - * List of dynamic command enums, also referred to as "soft" enums. These can by dynamically updated mid-game - * without resending this packet. - */ - public $softEnums = []; - - /** - * @var CommandEnumConstraint[] - * List of constraints for enum members. Used to constrain gamerules that can bechanged in nocheats mode and more. - */ - public $enumConstraints = []; - - protected function decodePayload(){ - /** @var string[] $enumValues */ - $enumValues = []; - for($i = 0, $enumValuesCount = $this->getUnsignedVarInt(); $i < $enumValuesCount; ++$i){ - $enumValues[] = $this->getString(); - } - - /** @var string[] $postfixes */ - $postfixes = []; - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $postfixes[] = $this->getString(); - } - - /** @var CommandEnum[] $enums */ - $enums = []; - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $enums[] = $enum = $this->getEnum($enumValues); - if(isset(self::HARDCODED_ENUM_NAMES[$enum->enumName])){ - $this->hardcodedEnums[] = $enum; - } - } - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->commandData[] = $this->getCommandData($enums, $postfixes); - } - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->softEnums[] = $this->getSoftEnum(); - } - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->enumConstraints[] = $this->getEnumConstraint($enums, $enumValues); - } - } - - /** - * @param string[] $enumValueList - * - * @throws \UnexpectedValueException - * @throws BinaryDataException - */ - protected function getEnum(array $enumValueList) : CommandEnum{ - $retval = new CommandEnum(); - $retval->enumName = $this->getString(); - - $listSize = count($enumValueList); - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $index = $this->getEnumValueIndex($listSize); - if(!isset($enumValueList[$index])){ - throw new \UnexpectedValueException("Invalid enum value index $index"); - } - //Get the enum value from the initial pile of mess - $retval->enumValues[] = $enumValueList[$index]; - } - - return $retval; - } - - protected function getSoftEnum() : CommandEnum{ - $retval = new CommandEnum(); - $retval->enumName = $this->getString(); - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - //Get the enum value from the initial pile of mess - $retval->enumValues[] = $this->getString(); - } - - return $retval; - } - - /** - * @param int[] $enumValueMap string enum name -> int index - */ - protected function putEnum(CommandEnum $enum, array $enumValueMap) : void{ - $this->putString($enum->enumName); - - $this->putUnsignedVarInt(count($enum->enumValues)); - $listSize = count($enumValueMap); - foreach($enum->enumValues as $value){ - $index = $enumValueMap[$value] ?? -1; - if($index === -1){ - throw new \InvalidStateException("Enum value '$value' not found"); - } - $this->putEnumValueIndex($index, $listSize); - } - } - - protected function putSoftEnum(CommandEnum $enum) : void{ - $this->putString($enum->enumName); - - $this->putUnsignedVarInt(count($enum->enumValues)); - foreach($enum->enumValues as $value){ - $this->putString($value); - } - } - - /** - * @throws BinaryDataException - */ - protected function getEnumValueIndex(int $valueCount) : int{ - if($valueCount < 256){ - return $this->getByte(); - }elseif($valueCount < 65536){ - return $this->getLShort(); - }else{ - return $this->getLInt(); - } - } - - protected function putEnumValueIndex(int $index, int $valueCount) : void{ - if($valueCount < 256){ - $this->putByte($index); - }elseif($valueCount < 65536){ - $this->putLShort($index); - }else{ - $this->putLInt($index); - } - } - - /** - * @param CommandEnum[] $enums - * @param string[] $enumValues - */ - protected function getEnumConstraint(array $enums, array $enumValues) : CommandEnumConstraint{ - //wtf, what was wrong with an offset inside the enum? :( - $valueIndex = $this->getLInt(); - if(!isset($enumValues[$valueIndex])){ - throw new \UnexpectedValueException("Enum constraint refers to unknown enum value index $valueIndex"); - } - $enumIndex = $this->getLInt(); - if(!isset($enums[$enumIndex])){ - throw new \UnexpectedValueException("Enum constraint refers to unknown enum index $enumIndex"); - } - $enum = $enums[$enumIndex]; - $valueOffset = array_search($enumValues[$valueIndex], $enum->enumValues, true); - if($valueOffset === false){ - throw new \UnexpectedValueException("Value \"" . $enumValues[$valueIndex] . "\" does not belong to enum \"$enum->enumName\""); - } - - $constraintIds = []; - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $constraintIds[] = $this->getByte(); - } - - return new CommandEnumConstraint($enum, $valueOffset, $constraintIds); - } - - /** - * @param int[] $enumIndexes string enum name -> int index - * @param int[] $enumValueIndexes string value -> int index - */ - protected function putEnumConstraint(CommandEnumConstraint $constraint, array $enumIndexes, array $enumValueIndexes) : void{ - $this->putLInt($enumValueIndexes[$constraint->getAffectedValue()]); - $this->putLInt($enumIndexes[$constraint->getEnum()->enumName]); - $this->putUnsignedVarInt(count($constraint->getConstraints())); - foreach($constraint->getConstraints() as $v){ - $this->putByte($v); - } - } - - /** - * @param CommandEnum[] $enums - * @param string[] $postfixes - * - * @throws \UnexpectedValueException - * @throws BinaryDataException - */ - protected function getCommandData(array $enums, array $postfixes) : CommandData{ - $retval = new CommandData(); - $retval->commandName = $this->getString(); - $retval->commandDescription = $this->getString(); - $retval->flags = $this->getLShort(); - $retval->permission = $this->getByte(); - $retval->aliases = $enums[$this->getLInt()] ?? null; - - for($overloadIndex = 0, $overloadCount = $this->getUnsignedVarInt(); $overloadIndex < $overloadCount; ++$overloadIndex){ - $retval->overloads[$overloadIndex] = []; - for($paramIndex = 0, $paramCount = $this->getUnsignedVarInt(); $paramIndex < $paramCount; ++$paramIndex){ - $parameter = new CommandParameter(); - $parameter->paramName = $this->getString(); - $parameter->paramType = $this->getLInt(); - $parameter->isOptional = $this->getBool(); - $parameter->flags = $this->getByte(); - - if(($parameter->paramType & self::ARG_FLAG_ENUM) !== 0){ - $index = ($parameter->paramType & 0xffff); - $parameter->enum = $enums[$index] ?? null; - if($parameter->enum === null){ - throw new \UnexpectedValueException("deserializing $retval->commandName parameter $parameter->paramName: expected enum at $index, but got none"); - } - }elseif(($parameter->paramType & self::ARG_FLAG_POSTFIX) !== 0){ - $index = ($parameter->paramType & 0xffff); - $parameter->postfix = $postfixes[$index] ?? null; - if($parameter->postfix === null){ - throw new \UnexpectedValueException("deserializing $retval->commandName parameter $parameter->paramName: expected postfix at $index, but got none"); - } - }elseif(($parameter->paramType & self::ARG_FLAG_VALID) === 0){ - throw new \UnexpectedValueException("deserializing $retval->commandName parameter $parameter->paramName: Invalid parameter type 0x" . dechex($parameter->paramType)); - } - - $retval->overloads[$overloadIndex][$paramIndex] = $parameter; - } - } - - return $retval; - } - - /** - * @param int[] $enumIndexes string enum name -> int index - * @param int[] $postfixIndexes - */ - protected function putCommandData(CommandData $data, array $enumIndexes, array $postfixIndexes) : void{ - $this->putString($data->commandName); - $this->putString($data->commandDescription); - $this->putLShort($data->flags); - $this->putByte($data->permission); - - if($data->aliases !== null){ - $this->putLInt($enumIndexes[$data->aliases->enumName] ?? -1); - }else{ - $this->putLInt(-1); - } - - $this->putUnsignedVarInt(count($data->overloads)); - foreach($data->overloads as $overload){ - /** @var CommandParameter[] $overload */ - $this->putUnsignedVarInt(count($overload)); - foreach($overload as $parameter){ - $this->putString($parameter->paramName); - - if($parameter->enum !== null){ - $type = self::ARG_FLAG_ENUM | self::ARG_FLAG_VALID | ($enumIndexes[$parameter->enum->enumName] ?? -1); - }elseif($parameter->postfix !== null){ - $key = $postfixIndexes[$parameter->postfix] ?? -1; - if($key === -1){ - throw new \InvalidStateException("Postfix '$parameter->postfix' not in postfixes array"); - } - $type = self::ARG_FLAG_POSTFIX | $key; - }else{ - $type = $parameter->paramType; - } - - $this->putLInt($type); - $this->putBool($parameter->isOptional); - $this->putByte($parameter->flags); - } - } - } - - protected function encodePayload(){ - /** @var int[] $enumValueIndexes */ - $enumValueIndexes = []; - /** @var int[] $postfixIndexes */ - $postfixIndexes = []; - /** @var int[] $enumIndexes */ - $enumIndexes = []; - /** @var CommandEnum[] $enums */ - $enums = []; - - $addEnumFn = static function(CommandEnum $enum) use (&$enums, &$enumIndexes, &$enumValueIndexes) : void{ - if(!isset($enumIndexes[$enum->enumName])){ - $enums[$enumIndexes[$enum->enumName] = count($enumIndexes)] = $enum; - } - foreach($enum->enumValues as $str){ - $enumValueIndexes[$str] = $enumValueIndexes[$str] ?? count($enumValueIndexes); - } - }; - foreach($this->hardcodedEnums as $enum){ - $addEnumFn($enum); - } - foreach($this->commandData as $commandData){ - if($commandData->aliases !== null){ - $addEnumFn($commandData->aliases); - } - /** @var CommandParameter[] $overload */ - foreach($commandData->overloads as $overload){ - /** @var CommandParameter $parameter */ - foreach($overload as $parameter){ - if($parameter->enum !== null){ - $addEnumFn($parameter->enum); - } - - if($parameter->postfix !== null){ - $postfixIndexes[$parameter->postfix] = $postfixIndexes[$parameter->postfix] ?? count($postfixIndexes); - } - } - } - } - - $this->putUnsignedVarInt(count($enumValueIndexes)); - foreach($enumValueIndexes as $enumValue => $index){ - $this->putString((string) $enumValue); //stupid PHP key casting D: - } - - $this->putUnsignedVarInt(count($postfixIndexes)); - foreach($postfixIndexes as $postfix => $index){ - $this->putString((string) $postfix); //stupid PHP key casting D: - } - - $this->putUnsignedVarInt(count($enums)); - foreach($enums as $enum){ - $this->putEnum($enum, $enumValueIndexes); - } - - $this->putUnsignedVarInt(count($this->commandData)); - foreach($this->commandData as $data){ - $this->putCommandData($data, $enumIndexes, $postfixIndexes); - } - - $this->putUnsignedVarInt(count($this->softEnums)); - foreach($this->softEnums as $enum){ - $this->putSoftEnum($enum); - } - - $this->putUnsignedVarInt(count($this->enumConstraints)); - foreach($this->enumConstraints as $constraint){ - $this->putEnumConstraint($constraint, $enumIndexes, $enumValueIndexes); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleAvailableCommands($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BatchPacket.php b/src/pocketmine/network/mcpe/protocol/BatchPacket.php deleted file mode 100644 index 230153744f..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BatchPacket.php +++ /dev/null @@ -1,138 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkBinaryStream; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\utils\AssumptionFailedError; -use function assert; -use function get_class; -use function strlen; -use function zlib_decode; -use function zlib_encode; -use const ZLIB_ENCODING_RAW; -#ifndef COMPILE -use pocketmine\utils\Binary; -#endif - -class BatchPacket extends DataPacket{ - public const NETWORK_ID = 0xfe; - - /** @var string */ - public $payload = ""; - /** @var int */ - protected $compressionLevel = 7; - - public function canBeBatched() : bool{ - return false; - } - - public function canBeSentBeforeLogin() : bool{ - return true; - } - - protected function decodeHeader(){ - $pid = $this->getByte(); - assert($pid === static::NETWORK_ID); - } - - protected function decodePayload(){ - $data = $this->getRemaining(); - try{ - $this->payload = zlib_decode($data, 1024 * 1024 * 2); //Max 2MB - }catch(\ErrorException $e){ //zlib decode error - $this->payload = ""; - } - } - - protected function encodeHeader(){ - $this->putByte(static::NETWORK_ID); - } - - protected function encodePayload(){ - $encoded = zlib_encode($this->payload, ZLIB_ENCODING_RAW, $this->compressionLevel); - if($encoded === false) throw new AssumptionFailedError("ZLIB compression failed"); - $this->put($encoded); - } - - /** - * @return void - */ - public function addPacket(DataPacket $packet){ - if(!$packet->canBeBatched()){ - throw new \InvalidArgumentException(get_class($packet) . " cannot be put inside a BatchPacket"); - } - if(!$packet->isEncoded){ - $packet->encode(); - } - - $this->payload .= Binary::writeUnsignedVarInt(strlen($packet->buffer)) . $packet->buffer; - } - - /** - * @return \Generator - * @phpstan-return \Generator - */ - public function getPackets(){ - $stream = new NetworkBinaryStream($this->payload); - $count = 0; - while(!$stream->feof()){ - if($count++ >= 500){ - throw new \UnexpectedValueException("Too many packets in a single batch"); - } - yield $stream->getString(); - } - } - - public function getCompressionLevel() : int{ - return $this->compressionLevel; - } - - /** - * @return void - */ - public function setCompressionLevel(int $level){ - $this->compressionLevel = $level; - } - - public function handle(NetworkSession $session) : bool{ - if($this->payload === ""){ - return false; - } - - foreach($this->getPackets() as $buf){ - $pk = PacketPool::getPacket($buf); - - if(!$pk->canBeBatched()){ - throw new \UnexpectedValueException("Received invalid " . get_class($pk) . " inside BatchPacket"); - } - - $session->handleDataPacket($pk); - } - - return true; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php b/src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php deleted file mode 100644 index 32599533ad..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function file_get_contents; - -class BiomeDefinitionListPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BIOME_DEFINITION_LIST_PACKET; - - /** @var string|null */ - private static $DEFAULT_NBT_CACHE = null; - - /** @var string */ - public $namedtag; - - protected function decodePayload(){ - $this->namedtag = $this->getRemaining(); - } - - protected function encodePayload(){ - $this->put( - $this->namedtag ?? - self::$DEFAULT_NBT_CACHE ?? - (self::$DEFAULT_NBT_CACHE = file_get_contents(\pocketmine\RESOURCE_PATH . '/vanilla/biome_definitions.nbt')) - ); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBiomeDefinitionList($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BlockActorDataPacket.php b/src/pocketmine/network/mcpe/protocol/BlockActorDataPacket.php deleted file mode 100644 index f842873e91..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BlockActorDataPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class BlockActorDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BLOCK_ACTOR_DATA_PACKET; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var string */ - public $namedtag; - - protected function decodePayload(){ - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->namedtag = $this->getRemaining(); - } - - protected function encodePayload(){ - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->put($this->namedtag); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBlockActorData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BlockEventPacket.php b/src/pocketmine/network/mcpe/protocol/BlockEventPacket.php deleted file mode 100644 index b9dffe108e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BlockEventPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class BlockEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BLOCK_EVENT_PACKET; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $eventType; - /** @var int */ - public $eventData; - - protected function decodePayload(){ - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->eventType = $this->getVarInt(); - $this->eventData = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putVarInt($this->eventType); - $this->putVarInt($this->eventData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBlockEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BlockPickRequestPacket.php b/src/pocketmine/network/mcpe/protocol/BlockPickRequestPacket.php deleted file mode 100644 index 683104fef6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BlockPickRequestPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class BlockPickRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BLOCK_PICK_REQUEST_PACKET; - - /** @var int */ - public $blockX; - /** @var int */ - public $blockY; - /** @var int */ - public $blockZ; - /** @var bool */ - public $addUserData = false; - /** @var int */ - public $hotbarSlot; - - protected function decodePayload(){ - $this->getSignedBlockPosition($this->blockX, $this->blockY, $this->blockZ); - $this->addUserData = $this->getBool(); - $this->hotbarSlot = $this->getByte(); - } - - protected function encodePayload(){ - $this->putSignedBlockPosition($this->blockX, $this->blockY, $this->blockZ); - $this->putBool($this->addUserData); - $this->putByte($this->hotbarSlot); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBlockPickRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BookEditPacket.php b/src/pocketmine/network/mcpe/protocol/BookEditPacket.php deleted file mode 100644 index 72c8dc0322..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BookEditPacket.php +++ /dev/null @@ -1,119 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class BookEditPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BOOK_EDIT_PACKET; - - public const TYPE_REPLACE_PAGE = 0; - public const TYPE_ADD_PAGE = 1; - public const TYPE_DELETE_PAGE = 2; - public const TYPE_SWAP_PAGES = 3; - public const TYPE_SIGN_BOOK = 4; - - /** @var int */ - public $type; - /** @var int */ - public $inventorySlot; - /** @var int */ - public $pageNumber; - /** @var int */ - public $secondaryPageNumber; - - /** @var string */ - public $text; - /** @var string */ - public $photoName; - - /** @var string */ - public $title; - /** @var string */ - public $author; - /** @var string */ - public $xuid; - - protected function decodePayload(){ - $this->type = $this->getByte(); - $this->inventorySlot = $this->getByte(); - - switch($this->type){ - case self::TYPE_REPLACE_PAGE: - case self::TYPE_ADD_PAGE: - $this->pageNumber = $this->getByte(); - $this->text = $this->getString(); - $this->photoName = $this->getString(); - break; - case self::TYPE_DELETE_PAGE: - $this->pageNumber = $this->getByte(); - break; - case self::TYPE_SWAP_PAGES: - $this->pageNumber = $this->getByte(); - $this->secondaryPageNumber = $this->getByte(); - break; - case self::TYPE_SIGN_BOOK: - $this->title = $this->getString(); - $this->author = $this->getString(); - $this->xuid = $this->getString(); - break; - default: - throw new \UnexpectedValueException("Unknown book edit type $this->type!"); - } - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putByte($this->inventorySlot); - - switch($this->type){ - case self::TYPE_REPLACE_PAGE: - case self::TYPE_ADD_PAGE: - $this->putByte($this->pageNumber); - $this->putString($this->text); - $this->putString($this->photoName); - break; - case self::TYPE_DELETE_PAGE: - $this->putByte($this->pageNumber); - break; - case self::TYPE_SWAP_PAGES: - $this->putByte($this->pageNumber); - $this->putByte($this->secondaryPageNumber); - break; - case self::TYPE_SIGN_BOOK: - $this->putString($this->title); - $this->putString($this->author); - $this->putString($this->xuid); - break; - default: - throw new \InvalidArgumentException("Unknown book edit type $this->type!"); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBookEdit($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/BossEventPacket.php b/src/pocketmine/network/mcpe/protocol/BossEventPacket.php deleted file mode 100644 index 0369e1f7ef..0000000000 --- a/src/pocketmine/network/mcpe/protocol/BossEventPacket.php +++ /dev/null @@ -1,131 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class BossEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::BOSS_EVENT_PACKET; - - /* S2C: Shows the boss-bar to the player. */ - public const TYPE_SHOW = 0; - /* C2S: Registers a player to a boss fight. */ - public const TYPE_REGISTER_PLAYER = 1; - /* S2C: Removes the boss-bar from the client. */ - public const TYPE_HIDE = 2; - /* C2S: Unregisters a player from a boss fight. */ - public const TYPE_UNREGISTER_PLAYER = 3; - /* S2C: Sets the bar percentage. */ - public const TYPE_HEALTH_PERCENT = 4; - /* S2C: Sets title of the bar. */ - public const TYPE_TITLE = 5; - /* S2C: Not sure on this. Includes color and overlay fields, plus an unknown short. TODO: check this */ - public const TYPE_UNKNOWN_6 = 6; - /* S2C: Not implemented :( Intended to alter bar appearance, but these currently produce no effect on client-side whatsoever. */ - public const TYPE_TEXTURE = 7; - - /** @var int */ - public $bossEid; - /** @var int */ - public $eventType; - - /** @var int (long) */ - public $playerEid; - /** @var float */ - public $healthPercent; - /** @var string */ - public $title; - /** @var int */ - public $unknownShort; - /** @var int */ - public $color; - /** @var int */ - public $overlay; - - protected function decodePayload(){ - $this->bossEid = $this->getEntityUniqueId(); - $this->eventType = $this->getUnsignedVarInt(); - switch($this->eventType){ - case self::TYPE_REGISTER_PLAYER: - case self::TYPE_UNREGISTER_PLAYER: - $this->playerEid = $this->getEntityUniqueId(); - break; - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_SHOW: - $this->title = $this->getString(); - $this->healthPercent = $this->getLFloat(); - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_UNKNOWN_6: - $this->unknownShort = $this->getLShort(); - case self::TYPE_TEXTURE: - $this->color = $this->getUnsignedVarInt(); - $this->overlay = $this->getUnsignedVarInt(); - break; - case self::TYPE_HEALTH_PERCENT: - $this->healthPercent = $this->getLFloat(); - break; - case self::TYPE_TITLE: - $this->title = $this->getString(); - break; - default: - break; - } - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->bossEid); - $this->putUnsignedVarInt($this->eventType); - switch($this->eventType){ - case self::TYPE_REGISTER_PLAYER: - case self::TYPE_UNREGISTER_PLAYER: - $this->putEntityUniqueId($this->playerEid); - break; - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_SHOW: - $this->putString($this->title); - $this->putLFloat($this->healthPercent); - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_UNKNOWN_6: - $this->putLShort($this->unknownShort); - case self::TYPE_TEXTURE: - $this->putUnsignedVarInt($this->color); - $this->putUnsignedVarInt($this->overlay); - break; - case self::TYPE_HEALTH_PERCENT: - $this->putLFloat($this->healthPercent); - break; - case self::TYPE_TITLE: - $this->putString($this->title); - break; - default: - break; - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleBossEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CameraPacket.php b/src/pocketmine/network/mcpe/protocol/CameraPacket.php deleted file mode 100644 index ebee825cfb..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CameraPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CameraPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CAMERA_PACKET; - - /** @var int */ - public $cameraUniqueId; - /** @var int */ - public $playerUniqueId; - - protected function decodePayload(){ - $this->cameraUniqueId = $this->getEntityUniqueId(); - $this->playerUniqueId = $this->getEntityUniqueId(); - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->cameraUniqueId); - $this->putEntityUniqueId($this->playerUniqueId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCamera($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CameraShakePacket.php b/src/pocketmine/network/mcpe/protocol/CameraShakePacket.php deleted file mode 100644 index 9f6a7acd43..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CameraShakePacket.php +++ /dev/null @@ -1,82 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CameraShakePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CAMERA_SHAKE_PACKET; - - public const TYPE_POSITIONAL = 0; - public const TYPE_ROTATIONAL = 1; - - public const ACTION_ADD = 0; - public const ACTION_STOP = 1; - - /** @var float */ - private $intensity; - /** @var float */ - private $duration; - /** @var int */ - private $shakeType; - /** @var int */ - private $shakeAction; - - public static function create(float $intensity, float $duration, int $shakeType, int $shakeAction) : self{ - $result = new self; - $result->intensity = $intensity; - $result->duration = $duration; - $result->shakeType = $shakeType; - $result->shakeAction = $shakeAction; - return $result; - } - - public function getIntensity() : float{ return $this->intensity; } - - public function getDuration() : float{ return $this->duration; } - - public function getShakeType() : int{ return $this->shakeType; } - - public function getShakeAction() : int{ return $this->shakeAction; } - - protected function decodePayload() : void{ - $this->intensity = $this->getLFloat(); - $this->duration = $this->getLFloat(); - $this->shakeType = $this->getByte(); - $this->shakeAction = $this->getByte(); - } - - protected function encodePayload() : void{ - $this->putLFloat($this->intensity); - $this->putLFloat($this->duration); - $this->putByte($this->shakeType); - $this->putByte($this->shakeAction); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleCameraShake($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ChangeDimensionPacket.php b/src/pocketmine/network/mcpe/protocol/ChangeDimensionPacket.php deleted file mode 100644 index 9af2db5fb1..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ChangeDimensionPacket.php +++ /dev/null @@ -1,56 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class ChangeDimensionPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CHANGE_DIMENSION_PACKET; - - /** @var int */ - public $dimension; - /** @var Vector3 */ - public $position; - /** @var bool */ - public $respawn = false; - - protected function decodePayload(){ - $this->dimension = $this->getVarInt(); - $this->position = $this->getVector3(); - $this->respawn = $this->getBool(); - } - - protected function encodePayload(){ - $this->putVarInt($this->dimension); - $this->putVector3($this->position); - $this->putBool($this->respawn); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleChangeDimension($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ClientCacheBlobStatusPacket.php b/src/pocketmine/network/mcpe/protocol/ClientCacheBlobStatusPacket.php deleted file mode 100644 index b9d389e6ed..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ClientCacheBlobStatusPacket.php +++ /dev/null @@ -1,93 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class ClientCacheBlobStatusPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CLIENT_CACHE_BLOB_STATUS_PACKET; - - /** @var int[] xxHash64 subchunk data hashes */ - private $hitHashes = []; - /** @var int[] xxHash64 subchunk data hashes */ - private $missHashes = []; - - /** - * @param int[] $hitHashes - * @param int[] $missHashes - */ - public static function create(array $hitHashes, array $missHashes) : self{ - //type checks - (static function(int ...$hashes) : void{})(...$hitHashes); - (static function(int ...$hashes) : void{})(...$missHashes); - - $result = new self; - $result->hitHashes = $hitHashes; - $result->missHashes = $missHashes; - return $result; - } - - /** - * @return int[] - */ - public function getHitHashes() : array{ - return $this->hitHashes; - } - - /** - * @return int[] - */ - public function getMissHashes() : array{ - return $this->missHashes; - } - - protected function decodePayload() : void{ - $missCount = $this->getUnsignedVarInt(); - $hitCount = $this->getUnsignedVarInt(); - for($i = 0; $i < $missCount; ++$i){ - $this->missHashes[] = $this->getLLong(); - } - for($i = 0; $i < $hitCount; ++$i){ - $this->hitHashes[] = $this->getLLong(); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->missHashes)); - $this->putUnsignedVarInt(count($this->hitHashes)); - foreach($this->missHashes as $hash){ - $this->putLLong($hash); - } - foreach($this->hitHashes as $hash){ - $this->putLLong($hash); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleClientCacheBlobStatus($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ClientCacheMissResponsePacket.php b/src/pocketmine/network/mcpe/protocol/ClientCacheMissResponsePacket.php deleted file mode 100644 index 28e823cbcd..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ClientCacheMissResponsePacket.php +++ /dev/null @@ -1,76 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ChunkCacheBlob; -use function count; - -class ClientCacheMissResponsePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CLIENT_CACHE_MISS_RESPONSE_PACKET; - - /** @var ChunkCacheBlob[] */ - private $blobs = []; - - /** - * @param ChunkCacheBlob[] $blobs - */ - public static function create(array $blobs) : self{ - //type check - (static function(ChunkCacheBlob ...$blobs) : void{})(...$blobs); - - $result = new self; - $result->blobs = $blobs; - return $result; - } - - /** - * @return ChunkCacheBlob[] - */ - public function getBlobs() : array{ - return $this->blobs; - } - - protected function decodePayload() : void{ - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $hash = $this->getLLong(); - $payload = $this->getString(); - $this->blobs[] = new ChunkCacheBlob($hash, $payload); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->blobs)); - foreach($this->blobs as $blob){ - $this->putLLong($blob->getHash()); - $this->putString($blob->getPayload()); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleClientCacheMissResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ClientCacheStatusPacket.php b/src/pocketmine/network/mcpe/protocol/ClientCacheStatusPacket.php deleted file mode 100644 index 3a4faf0530..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ClientCacheStatusPacket.php +++ /dev/null @@ -1,57 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ClientCacheStatusPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CLIENT_CACHE_STATUS_PACKET; - - /** @var bool */ - private $enabled; - - public static function create(bool $enabled) : self{ - $result = new self; - $result->enabled = $enabled; - return $result; - } - - public function isEnabled() : bool{ - return $this->enabled; - } - - protected function decodePayload() : void{ - $this->enabled = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putBool($this->enabled); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleClientCacheStatus($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ClientboundDebugRendererPacket.php b/src/pocketmine/network/mcpe/protocol/ClientboundDebugRendererPacket.php deleted file mode 100644 index 30f78fd0f6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ClientboundDebugRendererPacket.php +++ /dev/null @@ -1,137 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class ClientboundDebugRendererPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CLIENTBOUND_DEBUG_RENDERER_PACKET; - - public const TYPE_CLEAR = 1; - public const TYPE_ADD_CUBE = 2; - - /** @var int */ - private $type; - - //TODO: if more types are added, we'll probably want to make a separate data type and interfaces - /** @var string */ - private $text; - /** @var Vector3 */ - private $position; - /** @var float */ - private $red; - /** @var float */ - private $green; - /** @var float */ - private $blue; - /** @var float */ - private $alpha; - /** @var int */ - private $durationMillis; - - private static function base(int $type) : self{ - $result = new self; - $result->type = $type; - return $result; - } - - public static function clear() : self{ return self::base(self::TYPE_CLEAR); } - - public static function addCube(string $text, Vector3 $position, float $red, float $green, float $blue, float $alpha, int $durationMillis) : self{ - $result = self::base(self::TYPE_ADD_CUBE); - $result->text = $text; - $result->position = $position; - $result->red = $red; - $result->green = $green; - $result->blue = $blue; - $result->alpha = $alpha; - $result->durationMillis = $durationMillis; - return $result; - } - - public function getType() : int{ return $this->type; } - - public function getText() : string{ return $this->text; } - - public function getPosition() : Vector3{ return $this->position; } - - public function getRed() : float{ return $this->red; } - - public function getGreen() : float{ return $this->green; } - - public function getBlue() : float{ return $this->blue; } - - public function getAlpha() : float{ return $this->alpha; } - - public function getDurationMillis() : int{ return $this->durationMillis; } - - protected function decodePayload() : void{ - $this->type = $this->getLInt(); - - switch($this->type){ - case self::TYPE_CLEAR: - //NOOP - break; - case self::TYPE_ADD_CUBE: - $this->text = $this->getString(); - $this->position = $this->getVector3(); - $this->red = $this->getLFloat(); - $this->green = $this->getLFloat(); - $this->blue = $this->getLFloat(); - $this->alpha = $this->getLFloat(); - $this->durationMillis = $this->getLLong(); - break; - default: - throw new \UnexpectedValueException("Unknown type " . $this->type); - } - } - - protected function encodePayload() : void{ - $this->putLInt($this->type); - - switch($this->type){ - case self::TYPE_CLEAR: - //NOOP - break; - case self::TYPE_ADD_CUBE: - $this->putString($this->text); - $this->putVector3($this->position); - $this->putLFloat($this->red); - $this->putLFloat($this->green); - $this->putLFloat($this->blue); - $this->putLFloat($this->alpha); - $this->putLLong($this->durationMillis); - break; - default: - throw new \InvalidArgumentException("Unknown type " . $this->type); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleClientboundDebugRenderer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ClientboundMapItemDataPacket.php b/src/pocketmine/network/mcpe/protocol/ClientboundMapItemDataPacket.php deleted file mode 100644 index b0559be583..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ClientboundMapItemDataPacket.php +++ /dev/null @@ -1,204 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\DimensionIds; -use pocketmine\network\mcpe\protocol\types\MapDecoration; -use pocketmine\network\mcpe\protocol\types\MapTrackedObject; -use pocketmine\utils\Color; -use function count; - -class ClientboundMapItemDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CLIENTBOUND_MAP_ITEM_DATA_PACKET; - - public const BITFLAG_TEXTURE_UPDATE = 0x02; - public const BITFLAG_DECORATION_UPDATE = 0x04; - - /** @var int */ - public $mapId; - /** @var int */ - public $type; - /** @var int */ - public $dimensionId = DimensionIds::OVERWORLD; - /** @var bool */ - public $isLocked = false; - - /** @var int[] */ - public $eids = []; - /** @var int */ - public $scale; - - /** @var MapTrackedObject[] */ - public $trackedEntities = []; - /** @var MapDecoration[] */ - public $decorations = []; - - /** @var int */ - public $width; - /** @var int */ - public $height; - /** @var int */ - public $xOffset = 0; - /** @var int */ - public $yOffset = 0; - /** @var Color[][] */ - public $colors = []; - - protected function decodePayload(){ - $this->mapId = $this->getEntityUniqueId(); - $this->type = $this->getUnsignedVarInt(); - $this->dimensionId = $this->getByte(); - $this->isLocked = $this->getBool(); - - if(($this->type & 0x08) !== 0){ - $count = $this->getUnsignedVarInt(); - for($i = 0; $i < $count; ++$i){ - $this->eids[] = $this->getEntityUniqueId(); - } - } - - if(($this->type & (0x08 | self::BITFLAG_DECORATION_UPDATE | self::BITFLAG_TEXTURE_UPDATE)) !== 0){ //Decoration bitflag or colour bitflag - $this->scale = $this->getByte(); - } - - if(($this->type & self::BITFLAG_DECORATION_UPDATE) !== 0){ - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $object = new MapTrackedObject(); - $object->type = $this->getLInt(); - if($object->type === MapTrackedObject::TYPE_BLOCK){ - $this->getBlockPosition($object->x, $object->y, $object->z); - }elseif($object->type === MapTrackedObject::TYPE_ENTITY){ - $object->entityUniqueId = $this->getEntityUniqueId(); - }else{ - throw new \UnexpectedValueException("Unknown map object type $object->type"); - } - $this->trackedEntities[] = $object; - } - - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $icon = $this->getByte(); - $rotation = $this->getByte(); - $xOffset = $this->getByte(); - $yOffset = $this->getByte(); - $label = $this->getString(); - $color = Color::fromABGR($this->getUnsignedVarInt()); - $this->decorations[] = new MapDecoration($icon, $rotation, $xOffset, $yOffset, $label, $color); - } - } - - if(($this->type & self::BITFLAG_TEXTURE_UPDATE) !== 0){ - $this->width = $this->getVarInt(); - $this->height = $this->getVarInt(); - $this->xOffset = $this->getVarInt(); - $this->yOffset = $this->getVarInt(); - - $count = $this->getUnsignedVarInt(); - if($count !== $this->width * $this->height){ - throw new \UnexpectedValueException("Expected colour count of " . ($this->height * $this->width) . " (height $this->height * width $this->width), got $count"); - } - - for($y = 0; $y < $this->height; ++$y){ - for($x = 0; $x < $this->width; ++$x){ - $this->colors[$y][$x] = Color::fromABGR($this->getUnsignedVarInt()); - } - } - } - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->mapId); - - $type = 0; - if(($eidsCount = count($this->eids)) > 0){ - $type |= 0x08; - } - if(($decorationCount = count($this->decorations)) > 0){ - $type |= self::BITFLAG_DECORATION_UPDATE; - } - if(count($this->colors) > 0){ - $type |= self::BITFLAG_TEXTURE_UPDATE; - } - - $this->putUnsignedVarInt($type); - $this->putByte($this->dimensionId); - $this->putBool($this->isLocked); - - if(($type & 0x08) !== 0){ //TODO: find out what these are for - $this->putUnsignedVarInt($eidsCount); - foreach($this->eids as $eid){ - $this->putEntityUniqueId($eid); - } - } - - if(($type & (0x08 | self::BITFLAG_TEXTURE_UPDATE | self::BITFLAG_DECORATION_UPDATE)) !== 0){ - $this->putByte($this->scale); - } - - if(($type & self::BITFLAG_DECORATION_UPDATE) !== 0){ - $this->putUnsignedVarInt(count($this->trackedEntities)); - foreach($this->trackedEntities as $object){ - $this->putLInt($object->type); - if($object->type === MapTrackedObject::TYPE_BLOCK){ - $this->putBlockPosition($object->x, $object->y, $object->z); - }elseif($object->type === MapTrackedObject::TYPE_ENTITY){ - $this->putEntityUniqueId($object->entityUniqueId); - }else{ - throw new \InvalidArgumentException("Unknown map object type $object->type"); - } - } - - $this->putUnsignedVarInt($decorationCount); - foreach($this->decorations as $decoration){ - $this->putByte($decoration->getIcon()); - $this->putByte($decoration->getRotation()); - $this->putByte($decoration->getXOffset()); - $this->putByte($decoration->getYOffset()); - $this->putString($decoration->getLabel()); - $this->putUnsignedVarInt($decoration->getColor()->toABGR()); - } - } - - if(($type & self::BITFLAG_TEXTURE_UPDATE) !== 0){ - $this->putVarInt($this->width); - $this->putVarInt($this->height); - $this->putVarInt($this->xOffset); - $this->putVarInt($this->yOffset); - - $this->putUnsignedVarInt($this->width * $this->height); //list count, but we handle it as a 2D array... thanks for the confusion mojang - - for($y = 0; $y < $this->height; ++$y){ - for($x = 0; $x < $this->width; ++$x){ - $this->putUnsignedVarInt($this->colors[$y][$x]->toABGR()); - } - } - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleClientboundMapItemData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CodeBuilderPacket.php b/src/pocketmine/network/mcpe/protocol/CodeBuilderPacket.php deleted file mode 100644 index a05b07765f..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CodeBuilderPacket.php +++ /dev/null @@ -1,66 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CodeBuilderPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CODE_BUILDER_PACKET; - - /** @var string */ - private $url; - /** @var bool */ - private $openCodeBuilder; - - public static function create(string $url, bool $openCodeBuilder) : self{ - $result = new self; - $result->url = $url; - $result->openCodeBuilder = $openCodeBuilder; - return $result; - } - - public function getUrl() : string{ - return $this->url; - } - - public function openCodeBuilder() : bool{ - return $this->openCodeBuilder; - } - - protected function decodePayload() : void{ - $this->url = $this->getString(); - $this->openCodeBuilder = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putString($this->url); - $this->putBool($this->openCodeBuilder); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleCodeBuilder($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CommandBlockUpdatePacket.php b/src/pocketmine/network/mcpe/protocol/CommandBlockUpdatePacket.php deleted file mode 100644 index 01f6bae8ab..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CommandBlockUpdatePacket.php +++ /dev/null @@ -1,111 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CommandBlockUpdatePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::COMMAND_BLOCK_UPDATE_PACKET; - - /** @var bool */ - public $isBlock; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $commandBlockMode; - /** @var bool */ - public $isRedstoneMode; - /** @var bool */ - public $isConditional; - - /** @var int */ - public $minecartEid; - - /** @var string */ - public $command; - /** @var string */ - public $lastOutput; - /** @var string */ - public $name; - /** @var bool */ - public $shouldTrackOutput; - /** @var int */ - public $tickDelay; - /** @var bool */ - public $executeOnFirstTick; - - protected function decodePayload(){ - $this->isBlock = $this->getBool(); - - if($this->isBlock){ - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->commandBlockMode = $this->getUnsignedVarInt(); - $this->isRedstoneMode = $this->getBool(); - $this->isConditional = $this->getBool(); - }else{ - //Minecart with command block - $this->minecartEid = $this->getEntityRuntimeId(); - } - - $this->command = $this->getString(); - $this->lastOutput = $this->getString(); - $this->name = $this->getString(); - - $this->shouldTrackOutput = $this->getBool(); - $this->tickDelay = $this->getLInt(); - $this->executeOnFirstTick = $this->getBool(); - } - - protected function encodePayload(){ - $this->putBool($this->isBlock); - - if($this->isBlock){ - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putUnsignedVarInt($this->commandBlockMode); - $this->putBool($this->isRedstoneMode); - $this->putBool($this->isConditional); - }else{ - $this->putEntityRuntimeId($this->minecartEid); - } - - $this->putString($this->command); - $this->putString($this->lastOutput); - $this->putString($this->name); - - $this->putBool($this->shouldTrackOutput); - $this->putLInt($this->tickDelay); - $this->putBool($this->executeOnFirstTick); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCommandBlockUpdate($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CommandOutputPacket.php b/src/pocketmine/network/mcpe/protocol/CommandOutputPacket.php deleted file mode 100644 index ac7549d728..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CommandOutputPacket.php +++ /dev/null @@ -1,110 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\CommandOriginData; -use pocketmine\network\mcpe\protocol\types\CommandOutputMessage; -use function count; - -class CommandOutputPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::COMMAND_OUTPUT_PACKET; - - public const TYPE_LAST = 1; - public const TYPE_SILENT = 2; - public const TYPE_ALL = 3; - public const TYPE_DATA_SET = 4; - - /** @var CommandOriginData */ - public $originData; - /** @var int */ - public $outputType; - /** @var int */ - public $successCount; - /** @var CommandOutputMessage[] */ - public $messages = []; - /** @var string */ - public $unknownString; - - protected function decodePayload(){ - $this->originData = $this->getCommandOriginData(); - $this->outputType = $this->getByte(); - $this->successCount = $this->getUnsignedVarInt(); - - for($i = 0, $size = $this->getUnsignedVarInt(); $i < $size; ++$i){ - $this->messages[] = $this->getCommandMessage(); - } - - if($this->outputType === self::TYPE_DATA_SET){ - $this->unknownString = $this->getString(); - } - } - - protected function getCommandMessage() : CommandOutputMessage{ - $message = new CommandOutputMessage(); - - $message->isInternal = $this->getBool(); - $message->messageId = $this->getString(); - - for($i = 0, $size = $this->getUnsignedVarInt(); $i < $size; ++$i){ - $message->parameters[] = $this->getString(); - } - - return $message; - } - - protected function encodePayload(){ - $this->putCommandOriginData($this->originData); - $this->putByte($this->outputType); - $this->putUnsignedVarInt($this->successCount); - - $this->putUnsignedVarInt(count($this->messages)); - foreach($this->messages as $message){ - $this->putCommandMessage($message); - } - - if($this->outputType === self::TYPE_DATA_SET){ - $this->putString($this->unknownString); - } - } - - /** - * @return void - */ - protected function putCommandMessage(CommandOutputMessage $message){ - $this->putBool($message->isInternal); - $this->putString($message->messageId); - - $this->putUnsignedVarInt(count($message->parameters)); - foreach($message->parameters as $parameter){ - $this->putString($parameter); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCommandOutput($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CommandRequestPacket.php b/src/pocketmine/network/mcpe/protocol/CommandRequestPacket.php deleted file mode 100644 index 2159086776..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CommandRequestPacket.php +++ /dev/null @@ -1,56 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\CommandOriginData; - -class CommandRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::COMMAND_REQUEST_PACKET; - - /** @var string */ - public $command; - /** @var CommandOriginData */ - public $originData; - /** @var bool */ - public $isInternal; - - protected function decodePayload(){ - $this->command = $this->getString(); - $this->originData = $this->getCommandOriginData(); - $this->isInternal = $this->getBool(); - } - - protected function encodePayload(){ - $this->putString($this->command); - $this->putCommandOriginData($this->originData); - $this->putBool($this->isInternal); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCommandRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CompletedUsingItemPacket.php b/src/pocketmine/network/mcpe/protocol/CompletedUsingItemPacket.php deleted file mode 100644 index 1f044f7219..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CompletedUsingItemPacket.php +++ /dev/null @@ -1,68 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CompletedUsingItemPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::COMPLETED_USING_ITEM_PACKET; - - public const ACTION_UNKNOWN = -1; - public const ACTION_EQUIP_ARMOR = 0; - public const ACTION_EAT = 1; - public const ACTION_ATTACK = 2; - public const ACTION_CONSUME = 3; - public const ACTION_THROW = 4; - public const ACTION_SHOOT = 5; - public const ACTION_PLACE = 6; - public const ACTION_FILL_BOTTLE = 7; - public const ACTION_FILL_BUCKET = 8; - public const ACTION_POUR_BUCKET = 9; - public const ACTION_USE_TOOL = 10; - public const ACTION_INTERACT = 11; - public const ACTION_RETRIEVED = 12; - public const ACTION_DYED = 13; - public const ACTION_TRADED = 14; - - /** @var int */ - public $itemId; - /** @var int */ - public $action; - - public function decodePayload() : void{ - $this->itemId = $this->getShort(); - $this->action = $this->getLInt(); - } - - public function encodePayload() : void{ - $this->putShort($this->itemId); - $this->putLInt($this->action); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCompletedUsingItem($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ContainerOpenPacket.php b/src/pocketmine/network/mcpe/protocol/ContainerOpenPacket.php deleted file mode 100644 index be86a18189..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ContainerOpenPacket.php +++ /dev/null @@ -1,63 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ContainerOpenPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CONTAINER_OPEN_PACKET; - - /** @var int */ - public $windowId; - /** @var int */ - public $type; - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $entityUniqueId = -1; - - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->type = $this->getByte(); - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->entityUniqueId = $this->getEntityUniqueId(); - } - - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putByte($this->type); - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putEntityUniqueId($this->entityUniqueId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleContainerOpen($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ContainerSetDataPacket.php b/src/pocketmine/network/mcpe/protocol/ContainerSetDataPacket.php deleted file mode 100644 index 71c324b235..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ContainerSetDataPacket.php +++ /dev/null @@ -1,65 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ContainerSetDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CONTAINER_SET_DATA_PACKET; - - public const PROPERTY_FURNACE_TICK_COUNT = 0; - public const PROPERTY_FURNACE_LIT_TIME = 1; - public const PROPERTY_FURNACE_LIT_DURATION = 2; - public const PROPERTY_FURNACE_STORED_XP = 3; - public const PROPERTY_FURNACE_FUEL_AUX = 4; - - public const PROPERTY_BREWING_STAND_BREW_TIME = 0; - public const PROPERTY_BREWING_STAND_FUEL_AMOUNT = 1; - public const PROPERTY_BREWING_STAND_FUEL_TOTAL = 2; - - /** @var int */ - public $windowId; - /** @var int */ - public $property; - /** @var int */ - public $value; - - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->property = $this->getVarInt(); - $this->value = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putVarInt($this->property); - $this->putVarInt($this->value); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleContainerSetData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CorrectPlayerMovePredictionPacket.php b/src/pocketmine/network/mcpe/protocol/CorrectPlayerMovePredictionPacket.php deleted file mode 100644 index bc9af99030..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CorrectPlayerMovePredictionPacket.php +++ /dev/null @@ -1,77 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class CorrectPlayerMovePredictionPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CORRECT_PLAYER_MOVE_PREDICTION_PACKET; - - /** @var Vector3 */ - private $position; - /** @var Vector3 */ - private $delta; - /** @var bool */ - private $onGround; - /** @var int */ - private $tick; - - public static function create(Vector3 $position, Vector3 $delta, bool $onGround, int $tick) : self{ - $result = new self; - $result->position = $position; - $result->delta = $delta; - $result->onGround = $onGround; - $result->tick = $tick; - return $result; - } - - public function getPosition() : Vector3{ return $this->position; } - - public function getDelta() : Vector3{ return $this->delta; } - - public function isOnGround() : bool{ return $this->onGround; } - - public function getTick() : int{ return $this->tick; } - - protected function decodePayload() : void{ - $this->position = $this->getVector3(); - $this->delta = $this->getVector3(); - $this->onGround = $this->getBool(); - $this->tick = $this->getUnsignedVarLong(); - } - - protected function encodePayload() : void{ - $this->putVector3($this->position); - $this->putVector3($this->delta); - $this->putBool($this->onGround); - $this->putUnsignedVarLong($this->tick); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleCorrectPlayerMovePrediction($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php b/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php deleted file mode 100644 index 8714616c41..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php +++ /dev/null @@ -1,335 +0,0 @@ - - -use pocketmine\inventory\FurnaceRecipe; -use pocketmine\inventory\ShapedRecipe; -use pocketmine\inventory\ShapelessRecipe; -use pocketmine\item\ItemFactory; -use pocketmine\network\mcpe\convert\ItemTranslator; -use pocketmine\network\mcpe\NetworkBinaryStream; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\MaterialReducerRecipe; -use pocketmine\network\mcpe\protocol\types\MaterialReducerRecipeOutput; -use pocketmine\network\mcpe\protocol\types\PotionContainerChangeRecipe; -use pocketmine\network\mcpe\protocol\types\PotionTypeRecipe; -#ifndef COMPILE -use pocketmine\utils\Binary; -#endif -use function count; -use function str_repeat; - -class CraftingDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CRAFTING_DATA_PACKET; - - public const ENTRY_SHAPELESS = 0; - public const ENTRY_SHAPED = 1; - public const ENTRY_FURNACE = 2; - public const ENTRY_FURNACE_DATA = 3; - public const ENTRY_MULTI = 4; //TODO - public const ENTRY_SHULKER_BOX = 5; //TODO - public const ENTRY_SHAPELESS_CHEMISTRY = 6; //TODO - public const ENTRY_SHAPED_CHEMISTRY = 7; //TODO - - /** @var object[] */ - public $entries = []; - /** @var PotionTypeRecipe[] */ - public $potionTypeRecipes = []; - /** @var PotionContainerChangeRecipe[] */ - public $potionContainerRecipes = []; - /** @var MaterialReducerRecipe[] */ - public $materialReducerRecipes = []; - /** @var bool */ - public $cleanRecipes = false; - - /** @var mixed[][] */ - public $decodedEntries = []; - - public function clean(){ - $this->entries = []; - $this->decodedEntries = []; - return parent::clean(); - } - - protected function decodePayload(){ - $this->decodedEntries = []; - $recipeCount = $this->getUnsignedVarInt(); - for($i = 0; $i < $recipeCount; ++$i){ - $entry = []; - $entry["type"] = $recipeType = $this->getVarInt(); - - switch($recipeType){ - case self::ENTRY_SHAPELESS: - case self::ENTRY_SHULKER_BOX: - case self::ENTRY_SHAPELESS_CHEMISTRY: - $entry["recipe_id"] = $this->getString(); - $ingredientCount = $this->getUnsignedVarInt(); - $entry["input"] = []; - for($j = 0; $j < $ingredientCount; ++$j){ - $entry["input"][] = $in = $this->getRecipeIngredient(); - $in->setCount(1); //TODO HACK: they send a useless count field which breaks the PM crafting system because it isn't always 1 - } - $resultCount = $this->getUnsignedVarInt(); - $entry["output"] = []; - for($k = 0; $k < $resultCount; ++$k){ - $entry["output"][] = $this->getItemStackWithoutStackId(); - } - $entry["uuid"] = $this->getUUID()->toString(); - $entry["block"] = $this->getString(); - $entry["priority"] = $this->getVarInt(); - $entry["net_id"] = $this->readGenericTypeNetworkId(); - - break; - case self::ENTRY_SHAPED: - case self::ENTRY_SHAPED_CHEMISTRY: - $entry["recipe_id"] = $this->getString(); - $entry["width"] = $this->getVarInt(); - $entry["height"] = $this->getVarInt(); - $count = $entry["width"] * $entry["height"]; - $entry["input"] = []; - for($j = 0; $j < $count; ++$j){ - $entry["input"][] = $in = $this->getRecipeIngredient(); - $in->setCount(1); //TODO HACK: they send a useless count field which breaks the PM crafting system - } - $resultCount = $this->getUnsignedVarInt(); - $entry["output"] = []; - for($k = 0; $k < $resultCount; ++$k){ - $entry["output"][] = $this->getItemStackWithoutStackId(); - } - $entry["uuid"] = $this->getUUID()->toString(); - $entry["block"] = $this->getString(); - $entry["priority"] = $this->getVarInt(); - $entry["net_id"] = $this->readGenericTypeNetworkId(); - - break; - case self::ENTRY_FURNACE: - case self::ENTRY_FURNACE_DATA: - $inputIdNet = $this->getVarInt(); - if($recipeType === self::ENTRY_FURNACE){ - [$inputId, $inputData] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($inputIdNet, 0x7fff); - }else{ - $inputMetaNet = $this->getVarInt(); - [$inputId, $inputData] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($inputIdNet, $inputMetaNet); - } - $entry["input"] = ItemFactory::get($inputId, $inputData); - $entry["output"] = $out = $this->getItemStackWithoutStackId(); - if($out->getDamage() === 0x7fff){ - $out->setDamage(0); //TODO HACK: some 1.12 furnace recipe outputs have wildcard damage values - } - $entry["block"] = $this->getString(); - - break; - case self::ENTRY_MULTI: - $entry["uuid"] = $this->getUUID()->toString(); - $entry["net_id"] = $this->readGenericTypeNetworkId(); - break; - default: - throw new \UnexpectedValueException("Unhandled recipe type $recipeType!"); //do not continue attempting to decode - } - $this->decodedEntries[] = $entry; - } - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $inputIdNet = $this->getVarInt(); - $inputMetaNet = $this->getVarInt(); - [$input, $inputMeta] = ItemTranslator::getInstance()->fromNetworkId($inputIdNet, $inputMetaNet); - $ingredientIdNet = $this->getVarInt(); - $ingredientMetaNet = $this->getVarInt(); - [$ingredient, $ingredientMeta] = ItemTranslator::getInstance()->fromNetworkId($ingredientIdNet, $ingredientMetaNet); - $outputIdNet = $this->getVarInt(); - $outputMetaNet = $this->getVarInt(); - [$output, $outputMeta] = ItemTranslator::getInstance()->fromNetworkId($outputIdNet, $outputMetaNet); - $this->potionTypeRecipes[] = new PotionTypeRecipe($input, $inputMeta, $ingredient, $ingredientMeta, $output, $outputMeta); - } - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - //TODO: we discard inbound ID here, not safe because netID on its own might map to internalID+internalMeta for us - $inputIdNet = $this->getVarInt(); - [$input, ] = ItemTranslator::getInstance()->fromNetworkId($inputIdNet, 0); - $ingredientIdNet = $this->getVarInt(); - [$ingredient, ] = ItemTranslator::getInstance()->fromNetworkId($ingredientIdNet, 0); - $outputIdNet = $this->getVarInt(); - [$output, ] = ItemTranslator::getInstance()->fromNetworkId($outputIdNet, 0); - $this->potionContainerRecipes[] = new PotionContainerChangeRecipe($input, $ingredient, $output); - } - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $inputIdAndData = $this->getVarInt(); - [$inputId, $inputMeta] = [$inputIdAndData >> 16, $inputIdAndData & 0x7fff]; - $outputs = []; - for($j = 0, $outputCount = $this->getUnsignedVarInt(); $j < $outputCount; ++$j){ - $outputItemId = $this->getVarInt(); - $outputItemCount = $this->getVarInt(); - $outputs[] = new MaterialReducerRecipeOutput($outputItemId, $outputItemCount); - } - $this->materialReducerRecipes[] = new MaterialReducerRecipe($inputId, $inputMeta, $outputs); - } - $this->cleanRecipes = $this->getBool(); - } - - /** - * @param object $entry - */ - private static function writeEntry($entry, NetworkBinaryStream $stream, int $pos) : int{ - if($entry instanceof ShapelessRecipe){ - return self::writeShapelessRecipe($entry, $stream, $pos); - }elseif($entry instanceof ShapedRecipe){ - return self::writeShapedRecipe($entry, $stream, $pos); - }elseif($entry instanceof FurnaceRecipe){ - return self::writeFurnaceRecipe($entry, $stream); - } - //TODO: add MultiRecipe - - return -1; - } - - private static function writeShapelessRecipe(ShapelessRecipe $recipe, NetworkBinaryStream $stream, int $pos) : int{ - $stream->putString(Binary::writeInt($pos)); //some kind of recipe ID, doesn't matter what it is as long as it's unique - $stream->putUnsignedVarInt($recipe->getIngredientCount()); - foreach($recipe->getIngredientList() as $item){ - $stream->putRecipeIngredient($item); - } - - $results = $recipe->getResults(); - $stream->putUnsignedVarInt(count($results)); - foreach($results as $item){ - $stream->putItemStackWithoutStackId($item); - } - - $stream->put(str_repeat("\x00", 16)); //Null UUID - $stream->putString("crafting_table"); //TODO: blocktype (no prefix) (this might require internal API breaks) - $stream->putVarInt(50); //TODO: priority - $stream->writeGenericTypeNetworkId($pos); //TODO: ANOTHER recipe ID, only used on the network - - return CraftingDataPacket::ENTRY_SHAPELESS; - } - - private static function writeShapedRecipe(ShapedRecipe $recipe, NetworkBinaryStream $stream, int $pos) : int{ - $stream->putString(Binary::writeInt($pos)); //some kind of recipe ID, doesn't matter what it is as long as it's unique - $stream->putVarInt($recipe->getWidth()); - $stream->putVarInt($recipe->getHeight()); - - for($z = 0; $z < $recipe->getHeight(); ++$z){ - for($x = 0; $x < $recipe->getWidth(); ++$x){ - $stream->putRecipeIngredient($recipe->getIngredient($x, $z)); - } - } - - $results = $recipe->getResults(); - $stream->putUnsignedVarInt(count($results)); - foreach($results as $item){ - $stream->putItemStackWithoutStackId($item); - } - - $stream->put(str_repeat("\x00", 16)); //Null UUID - $stream->putString("crafting_table"); //TODO: blocktype (no prefix) (this might require internal API breaks) - $stream->putVarInt(50); //TODO: priority - $stream->writeGenericTypeNetworkId($pos); //TODO: ANOTHER recipe ID, only used on the network - - return CraftingDataPacket::ENTRY_SHAPED; - } - - private static function writeFurnaceRecipe(FurnaceRecipe $recipe, NetworkBinaryStream $stream) : int{ - $input = $recipe->getInput(); - if($input->hasAnyDamageValue()){ - [$netId, ] = ItemTranslator::getInstance()->toNetworkId($input->getId(), 0); - $netData = 0x7fff; - }else{ - [$netId, $netData] = ItemTranslator::getInstance()->toNetworkId($input->getId(), $input->getDamage()); - } - $stream->putVarInt($netId); - $stream->putVarInt($netData); - $stream->putItemStackWithoutStackId($recipe->getResult()); - $stream->putString("furnace"); //TODO: blocktype (no prefix) (this might require internal API breaks) - return CraftingDataPacket::ENTRY_FURNACE_DATA; - } - - /** - * @return void - */ - public function addShapelessRecipe(ShapelessRecipe $recipe){ - $this->entries[] = $recipe; - } - - /** - * @return void - */ - public function addShapedRecipe(ShapedRecipe $recipe){ - $this->entries[] = $recipe; - } - - /** - * @return void - */ - public function addFurnaceRecipe(FurnaceRecipe $recipe){ - $this->entries[] = $recipe; - } - - protected function encodePayload(){ - $this->putUnsignedVarInt(count($this->entries)); - - $writer = new NetworkBinaryStream(); - $counter = 0; - foreach($this->entries as $d){ - $entryType = self::writeEntry($d, $writer, ++$counter); - if($entryType >= 0){ - $this->putVarInt($entryType); - $this->put($writer->getBuffer()); - }else{ - $this->putVarInt(-1); - } - - $writer->reset(); - } - $this->putUnsignedVarInt(count($this->potionTypeRecipes)); - foreach($this->potionTypeRecipes as $recipe){ - $this->putVarInt($recipe->getInputItemId()); - $this->putVarInt($recipe->getInputItemMeta()); - $this->putVarInt($recipe->getIngredientItemId()); - $this->putVarInt($recipe->getIngredientItemMeta()); - $this->putVarInt($recipe->getOutputItemId()); - $this->putVarInt($recipe->getOutputItemMeta()); - } - $this->putUnsignedVarInt(count($this->potionContainerRecipes)); - foreach($this->potionContainerRecipes as $recipe){ - $this->putVarInt($recipe->getInputItemId()); - $this->putVarInt($recipe->getIngredientItemId()); - $this->putVarInt($recipe->getOutputItemId()); - } - $this->putUnsignedVarInt(count($this->materialReducerRecipes)); - foreach($this->materialReducerRecipes as $recipe){ - $this->putVarInt(($recipe->getInputItemId() << 16) | $recipe->getInputItemMeta()); - $this->putUnsignedVarInt(count($recipe->getOutputs())); - foreach($recipe->getOutputs() as $output){ - $this->putVarInt($output->getItemId()); - $this->putVarInt($output->getCount()); - } - } - - $this->putBool($this->cleanRecipes); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCraftingData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CraftingEventPacket.php b/src/pocketmine/network/mcpe/protocol/CraftingEventPacket.php deleted file mode 100644 index b937741b2e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CraftingEventPacket.php +++ /dev/null @@ -1,88 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; -use pocketmine\utils\UUID; -use function count; - -class CraftingEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CRAFTING_EVENT_PACKET; - - /** @var int */ - public $windowId; - /** @var int */ - public $type; - /** @var UUID */ - public $id; - /** @var ItemStackWrapper[] */ - public $input = []; - /** @var ItemStackWrapper[] */ - public $output = []; - - public function clean(){ - $this->input = []; - $this->output = []; - return parent::clean(); - } - - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->type = $this->getVarInt(); - $this->id = $this->getUUID(); - - $size = $this->getUnsignedVarInt(); - for($i = 0; $i < $size and $i < 128; ++$i){ - $this->input[] = ItemStackWrapper::read($this); - } - - $size = $this->getUnsignedVarInt(); - for($i = 0; $i < $size and $i < 128; ++$i){ - $this->output[] = ItemStackWrapper::read($this); - } - } - - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putVarInt($this->type); - $this->putUUID($this->id); - - $this->putUnsignedVarInt(count($this->input)); - foreach($this->input as $item){ - $item->write($this); - } - - $this->putUnsignedVarInt(count($this->output)); - foreach($this->output as $item){ - $item->write($this); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleCraftingEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CreatePhotoPacket.php b/src/pocketmine/network/mcpe/protocol/CreatePhotoPacket.php deleted file mode 100644 index ab9fb9205a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CreatePhotoPacket.php +++ /dev/null @@ -1,69 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class CreatePhotoPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::CREATE_PHOTO_PACKET; - - private int $entityUniqueId; - private string $photoName; - private string $photoItemName; - - public static function create(int $actorUniqueId, string $photoName, string $photoItemName) : self{ - $result = new self; - $result->entityUniqueId = $actorUniqueId; - $result->photoName = $photoName; - $result->photoItemName = $photoItemName; - return $result; - } - - /** - * TODO: rename this to getEntityUniqueId() on PM4 (shit architecture, thanks shoghi) - */ - public function getEntityUniqueIdField() : int{ return $this->entityUniqueId; } - - public function getPhotoName() : string{ return $this->photoName; } - - public function getPhotoItemName() : string{ return $this->photoItemName; } - - protected function decodePayload() : void{ - $this->entityUniqueId = $this->getLLong(); //why be consistent mojang ????? - $this->photoName = $this->getString(); - $this->photoItemName = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putLLong($this->entityUniqueId); - $this->putString($this->photoName); - $this->putString($this->photoItemName); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleCreatePhoto($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/CreativeContentPacket.php b/src/pocketmine/network/mcpe/protocol/CreativeContentPacket.php deleted file mode 100644 index 0bb599f6a4..0000000000 --- a/src/pocketmine/network/mcpe/protocol/CreativeContentPacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; -use function count; - -class CreativeContentPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::CREATIVE_CONTENT_PACKET; - - /** @var CreativeContentEntry[] */ - private $entries; - - /** - * @param CreativeContentEntry[] $entries - */ - public static function create(array $entries) : self{ - $result = new self; - $result->entries = $entries; - return $result; - } - - /** @return CreativeContentEntry[] */ - public function getEntries() : array{ return $this->entries; } - - protected function decodePayload() : void{ - $this->entries = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->entries[] = CreativeContentEntry::read($this); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - $entry->write($this); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleCreativeContent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/DataPacket.php b/src/pocketmine/network/mcpe/protocol/DataPacket.php deleted file mode 100644 index 57482d01f9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/DataPacket.php +++ /dev/null @@ -1,207 +0,0 @@ - - -use pocketmine\network\mcpe\CachedEncapsulatedPacket; -use pocketmine\network\mcpe\NetworkBinaryStream; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\utils\Utils; -use function bin2hex; -use function get_class; -use function is_object; -use function is_string; -use function method_exists; - -abstract class DataPacket extends NetworkBinaryStream{ - - public const NETWORK_ID = 0; - - public const PID_MASK = 0x3ff; //10 bits - - private const SUBCLIENT_ID_MASK = 0x03; //2 bits - private const SENDER_SUBCLIENT_ID_SHIFT = 10; - private const RECIPIENT_SUBCLIENT_ID_SHIFT = 12; - - /** @var bool */ - public $isEncoded = false; - /** @var CachedEncapsulatedPacket|null */ - public $__encapsulatedPacket = null; - - /** @var int */ - public $senderSubId = 0; - /** @var int */ - public $recipientSubId = 0; - - /** - * @return int - */ - public function pid(){ - return $this::NETWORK_ID; - } - - public function getName() : string{ - return (new \ReflectionClass($this))->getShortName(); - } - - public function canBeBatched() : bool{ - return true; - } - - public function canBeSentBeforeLogin() : bool{ - return false; - } - - /** - * Returns whether the packet may legally have unread bytes left in the buffer. - */ - public function mayHaveUnreadBytes() : bool{ - return false; - } - - /** - * @return void - * @throws \OutOfBoundsException - * @throws \UnexpectedValueException - */ - public function decode(){ - $this->offset = 0; - $this->decodeHeader(); - $this->decodePayload(); - } - - /** - * @return void - * @throws \OutOfBoundsException - * @throws \UnexpectedValueException - */ - protected function decodeHeader(){ - $header = $this->getUnsignedVarInt(); - $pid = $header & self::PID_MASK; - if($pid !== static::NETWORK_ID){ - throw new \UnexpectedValueException("Expected " . static::NETWORK_ID . " for packet ID, got $pid"); - } - $this->senderSubId = ($header >> self::SENDER_SUBCLIENT_ID_SHIFT) & self::SUBCLIENT_ID_MASK; - $this->recipientSubId = ($header >> self::RECIPIENT_SUBCLIENT_ID_SHIFT) & self::SUBCLIENT_ID_MASK; - } - - /** - * Note for plugin developers: If you're adding your own packets, you should perform decoding in here. - * - * @return void - * @throws \OutOfBoundsException - * @throws \UnexpectedValueException - */ - protected function decodePayload(){ - - } - - /** - * @return void - */ - public function encode(){ - $this->reset(); - $this->encodeHeader(); - $this->encodePayload(); - $this->isEncoded = true; - } - - /** - * @return void - */ - protected function encodeHeader(){ - $this->putUnsignedVarInt( - static::NETWORK_ID | - ($this->senderSubId << self::SENDER_SUBCLIENT_ID_SHIFT) | - ($this->recipientSubId << self::RECIPIENT_SUBCLIENT_ID_SHIFT) - ); - } - - /** - * Note for plugin developers: If you're adding your own packets, you should perform encoding in here. - * - * @return void - */ - protected function encodePayload(){ - - } - - /** - * Performs handling for this packet. Usually you'll want an appropriately named method in the NetworkSession for this. - * - * This method returns a bool to indicate whether the packet was handled or not. If the packet was unhandled, a debug message will be logged with a hexdump of the packet. - * Typically this method returns the return value of the handler in the supplied NetworkSession. See other packets for examples how to implement this. - * - * @return bool true if the packet was handled successfully, false if not. - */ - abstract public function handle(NetworkSession $session) : bool; - - /** - * @return $this - */ - public function clean(){ - $this->buffer = ""; - $this->isEncoded = false; - $this->offset = 0; - return $this; - } - - /** - * @return mixed[] - */ - public function __debugInfo(){ - $data = []; - foreach((array) $this as $k => $v){ - if($k === "buffer"){ - $data[$k] = bin2hex($v); - }elseif(is_string($v) or (is_object($v) and method_exists($v, "__toString"))){ - $data[$k] = Utils::printable((string) $v); - }else{ - $data[$k] = $v; - } - } - - return $data; - } - - /** - * @param string $name - * - * @return mixed - */ - public function __get($name){ - throw new \Error("Undefined property: " . get_class($this) . "::\$" . $name); - } - - /** - * @param string $name - * @param mixed $value - * - * @return void - */ - public function __set($name, $value){ - throw new \Error("Undefined property: " . get_class($this) . "::\$" . $name); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/DebugInfoPacket.php b/src/pocketmine/network/mcpe/protocol/DebugInfoPacket.php deleted file mode 100644 index 531d00f993..0000000000 --- a/src/pocketmine/network/mcpe/protocol/DebugInfoPacket.php +++ /dev/null @@ -1,65 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class DebugInfoPacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::DEBUG_INFO_PACKET; - - /** @var int */ - private $entityUniqueId; - /** @var string */ - private $data; - - public static function create(int $entityUniqueId, string $data) : self{ - $result = new self; - $result->entityUniqueId = $entityUniqueId; - $result->data = $data; - return $result; - } - - /** - * TODO: we can't call this getEntityRuntimeId() because of base class collision (crap architecture, thanks Shoghi) - */ - public function getEntityUniqueIdField() : int{ return $this->entityUniqueId; } - - public function getData() : string{ return $this->data; } - - protected function decodePayload() : void{ - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->data = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putEntityUniqueId($this->entityUniqueId); - $this->putString($this->data); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleDebugInfo($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/DisconnectPacket.php b/src/pocketmine/network/mcpe/protocol/DisconnectPacket.php deleted file mode 100644 index 3374f14f77..0000000000 --- a/src/pocketmine/network/mcpe/protocol/DisconnectPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class DisconnectPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::DISCONNECT_PACKET; - - /** @var bool */ - public $hideDisconnectionScreen = false; - /** @var string */ - public $message = ""; - - public function canBeSentBeforeLogin() : bool{ - return true; - } - - protected function decodePayload(){ - $this->hideDisconnectionScreen = $this->getBool(); - if(!$this->hideDisconnectionScreen){ - $this->message = $this->getString(); - } - } - - protected function encodePayload(){ - $this->putBool($this->hideDisconnectionScreen); - if(!$this->hideDisconnectionScreen){ - $this->putString($this->message); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleDisconnect($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/EduUriResourcePacket.php b/src/pocketmine/network/mcpe/protocol/EduUriResourcePacket.php deleted file mode 100644 index a4ee92f8b9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/EduUriResourcePacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\EducationUriResource; - -class EduUriResourcePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::EDU_URI_RESOURCE_PACKET; - - private EducationUriResource $resource; - - public static function create(EducationUriResource $resource) : self{ - $result = new self; - $result->resource = $resource; - return $result; - } - - public function getResource() : EducationUriResource{ return $this->resource; } - - protected function decodePayload() : void{ - $this->resource = EducationUriResource::read($this); - } - - protected function encodePayload() : void{ - $this->resource->write($this); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleEduUriResource($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/EducationSettingsPacket.php b/src/pocketmine/network/mcpe/protocol/EducationSettingsPacket.php deleted file mode 100644 index cb8f35d538..0000000000 --- a/src/pocketmine/network/mcpe/protocol/EducationSettingsPacket.php +++ /dev/null @@ -1,160 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\EducationSettingsAgentCapabilities; -use pocketmine\network\mcpe\protocol\types\EducationSettingsExternalLinkSettings; - -class EducationSettingsPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::EDUCATION_SETTINGS_PACKET; - - /** @var string */ - private $codeBuilderDefaultUri; - /** @var string */ - private $codeBuilderTitle; - /** @var bool */ - private $canResizeCodeBuilder; - /** @var bool */ - private $disableLegacyTitleBar; - /** @var string */ - private $postProcessFilter; - /** @var string */ - private $screenshotBorderResourcePath; - /** @var EducationSettingsAgentCapabilities|null */ - private $agentCapabilities; - /** @var string|null */ - private $codeBuilderOverrideUri; - /** @var bool */ - private $hasQuiz; - /** @var EducationSettingsExternalLinkSettings|null */ - private $linkSettings; - - public static function create( - string $codeBuilderDefaultUri, - string $codeBuilderTitle, - bool $canResizeCodeBuilder, - bool $disableLegacyTitleBar, - string $postProcessFilter, - string $screenshotBorderResourcePath, - ?EducationSettingsAgentCapabilities $agentCapabilities, - ?string $codeBuilderOverrideUri, - bool $hasQuiz, - ?EducationSettingsExternalLinkSettings $linkSettings - ) : self{ - $result = new self; - $result->codeBuilderDefaultUri = $codeBuilderDefaultUri; - $result->codeBuilderTitle = $codeBuilderTitle; - $result->canResizeCodeBuilder = $canResizeCodeBuilder; - $result->disableLegacyTitleBar = $disableLegacyTitleBar; - $result->postProcessFilter = $postProcessFilter; - $result->screenshotBorderResourcePath = $screenshotBorderResourcePath; - $result->agentCapabilities = $agentCapabilities; - $result->codeBuilderOverrideUri = $codeBuilderOverrideUri; - $result->hasQuiz = $hasQuiz; - $result->linkSettings = $linkSettings; - return $result; - } - - public function getCodeBuilderDefaultUri() : string{ - return $this->codeBuilderDefaultUri; - } - - public function getCodeBuilderTitle() : string{ - return $this->codeBuilderTitle; - } - - public function canResizeCodeBuilder() : bool{ - return $this->canResizeCodeBuilder; - } - - public function disableLegacyTitleBar() : bool{ return $this->disableLegacyTitleBar; } - - public function getPostProcessFilter() : string{ return $this->postProcessFilter; } - - public function getScreenshotBorderResourcePath() : string{ return $this->screenshotBorderResourcePath; } - - public function getAgentCapabilities() : ?EducationSettingsAgentCapabilities{ return $this->agentCapabilities; } - - public function getCodeBuilderOverrideUri() : ?string{ - return $this->codeBuilderOverrideUri; - } - - public function getHasQuiz() : bool{ - return $this->hasQuiz; - } - - public function getLinkSettings() : ?EducationSettingsExternalLinkSettings{ return $this->linkSettings; } - - protected function decodePayload() : void{ - $this->codeBuilderDefaultUri = $this->getString(); - $this->codeBuilderTitle = $this->getString(); - $this->canResizeCodeBuilder = $this->getBool(); - $this->disableLegacyTitleBar = $this->getBool(); - $this->postProcessFilter = $this->getString(); - $this->screenshotBorderResourcePath = $this->getString(); - $this->agentCapabilities = $this->getBool() ? EducationSettingsAgentCapabilities::read($this) : null; - if($this->getBool()){ - $this->codeBuilderOverrideUri = $this->getString(); - }else{ - $this->codeBuilderOverrideUri = null; - } - $this->hasQuiz = $this->getBool(); - $this->linkSettings = $this->getBool() ? EducationSettingsExternalLinkSettings::read($this) : null; - } - - protected function encodePayload() : void{ - $this->putString($this->codeBuilderDefaultUri); - $this->putString($this->codeBuilderTitle); - $this->putBool($this->canResizeCodeBuilder); - $this->putBool($this->disableLegacyTitleBar); - $this->putString($this->postProcessFilter); - $this->putString($this->screenshotBorderResourcePath); - $agentCapabilities = $this->agentCapabilities; - if($agentCapabilities !== null){ - $this->putBool(true); - $agentCapabilities->write($this); - }else{ - $this->putBool(false); - } - $this->putBool($this->codeBuilderOverrideUri !== null); - if($this->codeBuilderOverrideUri !== null){ - $this->putString($this->codeBuilderOverrideUri); - } - $this->putBool($this->hasQuiz); - $linkSettings = $this->linkSettings; - if($linkSettings !== null){ - $this->putBool(true); - $linkSettings->write($this); - }else{ - $this->putBool(false); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleEducationSettings($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/EmoteListPacket.php b/src/pocketmine/network/mcpe/protocol/EmoteListPacket.php deleted file mode 100644 index d900012c93..0000000000 --- a/src/pocketmine/network/mcpe/protocol/EmoteListPacket.php +++ /dev/null @@ -1,74 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\utils\UUID; -use function count; - -class EmoteListPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::EMOTE_LIST_PACKET; - - /** @var int */ - private $playerEntityRuntimeId; - /** @var UUID[] */ - private $emoteIds; - - /** - * @param UUID[] $emoteIds - */ - public static function create(int $playerEntityRuntimeId, array $emoteIds) : self{ - $result = new self; - $result->playerEntityRuntimeId = $playerEntityRuntimeId; - $result->emoteIds = $emoteIds; - return $result; - } - - public function getPlayerEntityRuntimeId() : int{ return $this->playerEntityRuntimeId; } - - /** @return UUID[] */ - public function getEmoteIds() : array{ return $this->emoteIds; } - - protected function decodePayload() : void{ - $this->playerEntityRuntimeId = $this->getEntityRuntimeId(); - $this->emoteIds = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->emoteIds[] = $this->getUUID(); - } - } - - protected function encodePayload() : void{ - $this->putEntityRuntimeId($this->playerEntityRuntimeId); - $this->putUnsignedVarInt(count($this->emoteIds)); - foreach($this->emoteIds as $emoteId){ - $this->putUUID($emoteId); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleEmoteList($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/EmotePacket.php b/src/pocketmine/network/mcpe/protocol/EmotePacket.php deleted file mode 100644 index 69011508a7..0000000000 --- a/src/pocketmine/network/mcpe/protocol/EmotePacket.php +++ /dev/null @@ -1,80 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class EmotePacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::EMOTE_PACKET; - - public const FLAG_SERVER = 1 << 0; - - /** @var int */ - private $entityRuntimeId; - /** @var string */ - private $emoteId; - /** @var int */ - private $flags; - - public static function create(int $entityRuntimeId, string $emoteId, int $flags) : self{ - $result = new self; - $result->entityRuntimeId = $entityRuntimeId; - $result->emoteId = $emoteId; - $result->flags = $flags; - return $result; - } - - /** - * TODO: we can't call this getEntityRuntimeId() because of base class collision (crap architecture, thanks Shoghi) - */ - public function getEntityRuntimeIdField() : int{ - return $this->entityRuntimeId; - } - - public function getEmoteId() : string{ - return $this->emoteId; - } - - public function getFlags() : int{ - return $this->flags; - } - - protected function decodePayload() : void{ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->emoteId = $this->getString(); - $this->flags = $this->getByte(); - } - - protected function encodePayload() : void{ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putString($this->emoteId); - $this->putByte($this->flags); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleEmote($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/EventPacket.php b/src/pocketmine/network/mcpe/protocol/EventPacket.php deleted file mode 100644 index 7e4246ddf4..0000000000 --- a/src/pocketmine/network/mcpe/protocol/EventPacket.php +++ /dev/null @@ -1,85 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class EventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::EVENT_PACKET; - - public const TYPE_ACHIEVEMENT_AWARDED = 0; - public const TYPE_ENTITY_INTERACT = 1; - public const TYPE_PORTAL_BUILT = 2; - public const TYPE_PORTAL_USED = 3; - public const TYPE_MOB_KILLED = 4; - public const TYPE_CAULDRON_USED = 5; - public const TYPE_PLAYER_DEATH = 6; - public const TYPE_BOSS_KILLED = 7; - public const TYPE_AGENT_COMMAND = 8; - public const TYPE_AGENT_CREATED = 9; - public const TYPE_PATTERN_REMOVED = 10; //??? - public const TYPE_COMMANED_EXECUTED = 11; - public const TYPE_FISH_BUCKETED = 12; - public const TYPE_MOB_BORN = 13; - public const TYPE_PET_DIED = 14; - public const TYPE_CAULDRON_BLOCK_USED = 15; - public const TYPE_COMPOSTER_BLOCK_USED = 16; - public const TYPE_BELL_BLOCK_USED = 17; - public const TYPE_ACTOR_DEFINITION = 18; - public const TYPE_RAID_UPDATE = 19; - public const TYPE_PLAYER_MOVEMENT_ANOMALY = 20; //anti cheat - public const TYPE_PLAYER_MOVEMENT_CORRECTED = 21; - public const TYPE_HONEY_HARVESTED = 22; - public const TYPE_TARGET_BLOCK_HIT = 23; - public const TYPE_PIGLIN_BARTER = 24; - - /** @var int */ - public $playerRuntimeId; - /** @var int */ - public $eventData; - /** @var int */ - public $type; - - protected function decodePayload(){ - $this->playerRuntimeId = $this->getEntityRuntimeId(); - $this->eventData = $this->getVarInt(); - $this->type = $this->getByte(); - - //TODO: nice confusing mess - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->playerRuntimeId); - $this->putVarInt($this->eventData); - $this->putByte($this->type); - - //TODO: also nice confusing mess - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/FilterTextPacket.php b/src/pocketmine/network/mcpe/protocol/FilterTextPacket.php deleted file mode 100644 index 84f81dc81b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/FilterTextPacket.php +++ /dev/null @@ -1,62 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class FilterTextPacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::FILTER_TEXT_PACKET; - - /** @var string */ - private $text; - /** @var bool */ - private $fromServer; - - public static function create(string $text, bool $server) : self{ - $result = new self; - $result->text = $text; - $result->fromServer = $server; - return $result; - } - - public function getText() : string{ return $this->text; } - - public function isFromServer() : bool{ return $this->fromServer; } - - protected function decodePayload() : void{ - $this->text = $this->getString(); - $this->fromServer = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putString($this->text); - $this->putBool($this->fromServer); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleFilterText($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/GameRulesChangedPacket.php b/src/pocketmine/network/mcpe/protocol/GameRulesChangedPacket.php deleted file mode 100644 index c099dfbd82..0000000000 --- a/src/pocketmine/network/mcpe/protocol/GameRulesChangedPacket.php +++ /dev/null @@ -1,50 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class GameRulesChangedPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::GAME_RULES_CHANGED_PACKET; - - /** - * @var mixed[][] - * @phpstan-var array - */ - public $gameRules = []; - - protected function decodePayload(){ - $this->gameRules = $this->getGameRules(); - } - - protected function encodePayload(){ - $this->putGameRules($this->gameRules); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleGameRulesChanged($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/GuiDataPickItemPacket.php b/src/pocketmine/network/mcpe/protocol/GuiDataPickItemPacket.php deleted file mode 100644 index 7d583cb113..0000000000 --- a/src/pocketmine/network/mcpe/protocol/GuiDataPickItemPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class GuiDataPickItemPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::GUI_DATA_PICK_ITEM_PACKET; - - /** @var string */ - public $itemDescription; - /** @var string */ - public $itemEffects; - /** @var int */ - public $hotbarSlot; - - protected function decodePayload(){ - $this->itemDescription = $this->getString(); - $this->itemEffects = $this->getString(); - $this->hotbarSlot = $this->getLInt(); - } - - protected function encodePayload(){ - $this->putString($this->itemDescription); - $this->putString($this->itemEffects); - $this->putLInt($this->hotbarSlot); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleGuiDataPickItem($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/HurtArmorPacket.php b/src/pocketmine/network/mcpe/protocol/HurtArmorPacket.php deleted file mode 100644 index 51078ce81a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/HurtArmorPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class HurtArmorPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::HURT_ARMOR_PACKET; - - /** @var int */ - public $cause; - /** @var int */ - public $health; - /** @var int */ - public $armorSlotFlags; - - protected function decodePayload(){ - $this->cause = $this->getVarInt(); - $this->health = $this->getVarInt(); - $this->armorSlotFlags = $this->getUnsignedVarLong(); - } - - protected function encodePayload(){ - $this->putVarInt($this->cause); - $this->putVarInt($this->health); - $this->putUnsignedVarLong($this->armorSlotFlags); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleHurtArmor($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/InteractPacket.php b/src/pocketmine/network/mcpe/protocol/InteractPacket.php deleted file mode 100644 index a2f7d2fe6b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/InteractPacket.php +++ /dev/null @@ -1,76 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class InteractPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::INTERACT_PACKET; - - public const ACTION_LEAVE_VEHICLE = 3; - public const ACTION_MOUSEOVER = 4; - public const ACTION_OPEN_NPC = 5; - public const ACTION_OPEN_INVENTORY = 6; - - /** @var int */ - public $action; - /** @var int */ - public $target; - - /** @var float */ - public $x; - /** @var float */ - public $y; - /** @var float */ - public $z; - - protected function decodePayload(){ - $this->action = $this->getByte(); - $this->target = $this->getEntityRuntimeId(); - - if($this->action === self::ACTION_MOUSEOVER){ - //TODO: should this be a vector3? - $this->x = $this->getLFloat(); - $this->y = $this->getLFloat(); - $this->z = $this->getLFloat(); - } - } - - protected function encodePayload(){ - $this->putByte($this->action); - $this->putEntityRuntimeId($this->target); - - if($this->action === self::ACTION_MOUSEOVER){ - $this->putLFloat($this->x); - $this->putLFloat($this->y); - $this->putLFloat($this->z); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleInteract($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/InventoryContentPacket.php b/src/pocketmine/network/mcpe/protocol/InventoryContentPacket.php deleted file mode 100644 index f7142cfe3c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/InventoryContentPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; -use function count; - -class InventoryContentPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::INVENTORY_CONTENT_PACKET; - - /** @var int */ - public $windowId; - /** @var ItemStackWrapper[] */ - public $items = []; - - protected function decodePayload(){ - $this->windowId = $this->getUnsignedVarInt(); - $count = $this->getUnsignedVarInt(); - for($i = 0; $i < $count; ++$i){ - $this->items[] = ItemStackWrapper::read($this); - } - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->windowId); - $this->putUnsignedVarInt(count($this->items)); - foreach($this->items as $item){ - $item->write($this); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleInventoryContent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/InventorySlotPacket.php b/src/pocketmine/network/mcpe/protocol/InventorySlotPacket.php deleted file mode 100644 index 905148205a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/InventorySlotPacket.php +++ /dev/null @@ -1,56 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; - -class InventorySlotPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::INVENTORY_SLOT_PACKET; - - /** @var int */ - public $windowId; - /** @var int */ - public $inventorySlot; - /** @var ItemStackWrapper */ - public $item; - - protected function decodePayload(){ - $this->windowId = $this->getUnsignedVarInt(); - $this->inventorySlot = $this->getUnsignedVarInt(); - $this->item = ItemStackWrapper::read($this); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->windowId); - $this->putUnsignedVarInt($this->inventorySlot); - $this->item->write($this); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleInventorySlot($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php b/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php deleted file mode 100644 index aee312b45e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php +++ /dev/null @@ -1,108 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession as PacketHandlerInterface; -use pocketmine\network\mcpe\protocol\types\inventory\InventoryTransactionChangedSlotsHack; -use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData; -use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData; -use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData; -use pocketmine\network\mcpe\protocol\types\inventory\TransactionData; -use pocketmine\network\mcpe\protocol\types\inventory\UseItemOnEntityTransactionData; -use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData; -use UnexpectedValueException as PacketDecodeException; -use function count; - -class InventoryTransactionPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::INVENTORY_TRANSACTION_PACKET; - - public const TYPE_NORMAL = 0; - public const TYPE_MISMATCH = 1; - public const TYPE_USE_ITEM = 2; - public const TYPE_USE_ITEM_ON_ENTITY = 3; - public const TYPE_RELEASE_ITEM = 4; - - /** @var int */ - public $requestId; - /** @var InventoryTransactionChangedSlotsHack[] */ - public $requestChangedSlots; - /** @var TransactionData */ - public $trData; - - protected function decodePayload() : void{ - $in = $this; - $this->requestId = $in->readGenericTypeNetworkId(); - $this->requestChangedSlots = []; - if($this->requestId !== 0){ - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $this->requestChangedSlots[] = InventoryTransactionChangedSlotsHack::read($in); - } - } - - $transactionType = $in->getUnsignedVarInt(); - - switch($transactionType){ - case self::TYPE_NORMAL: - $this->trData = new NormalTransactionData(); - break; - case self::TYPE_MISMATCH: - $this->trData = new MismatchTransactionData(); - break; - case self::TYPE_USE_ITEM: - $this->trData = new UseItemTransactionData(); - break; - case self::TYPE_USE_ITEM_ON_ENTITY: - $this->trData = new UseItemOnEntityTransactionData(); - break; - case self::TYPE_RELEASE_ITEM: - $this->trData = new ReleaseItemTransactionData(); - break; - default: - throw new PacketDecodeException("Unknown transaction type $transactionType"); - } - - $this->trData->decode($in); - } - - protected function encodePayload() : void{ - $out = $this; - $out->writeGenericTypeNetworkId($this->requestId); - if($this->requestId !== 0){ - $out->putUnsignedVarInt(count($this->requestChangedSlots)); - foreach($this->requestChangedSlots as $changedSlots){ - $changedSlots->write($out); - } - } - - $out->putUnsignedVarInt($this->trData->getTypeId()); - - $this->trData->encode($out); - } - - public function handle(PacketHandlerInterface $handler) : bool{ - return $handler->handleInventoryTransaction($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ItemComponentPacket.php b/src/pocketmine/network/mcpe/protocol/ItemComponentPacket.php deleted file mode 100644 index 26a7ac9dd3..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ItemComponentPacket.php +++ /dev/null @@ -1,78 +0,0 @@ - - -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ItemComponentPacketEntry; -use function count; - -class ItemComponentPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ITEM_COMPONENT_PACKET; - - /** - * @var ItemComponentPacketEntry[] - * @phpstan-var list - */ - private $entries; - - /** - * @param ItemComponentPacketEntry[] $entries - * @phpstan-param list $entries - */ - public static function create(array $entries) : self{ - $result = new self; - $result->entries = $entries; - return $result; - } - - /** - * @return ItemComponentPacketEntry[] - * @phpstan-return list - */ - public function getEntries() : array{ return $this->entries; } - - protected function decodePayload() : void{ - $this->entries = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $name = $this->getString(); - $nbt = $this->getNbtCompoundRoot(); - $this->entries[] = new ItemComponentPacketEntry($name, $nbt); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - $this->putString($entry->getName()); - $this->put((new NetworkLittleEndianNBTStream())->write($entry->getComponentNbt())); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleItemComponent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ItemStackRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ItemStackRequestPacket.php deleted file mode 100644 index 11b827d7c0..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ItemStackRequestPacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest; -use function count; - -class ItemStackRequestPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ITEM_STACK_REQUEST_PACKET; - - /** @var ItemStackRequest[] */ - private $requests; - - /** - * @param ItemStackRequest[] $requests - */ - public static function create(array $requests) : self{ - $result = new self; - $result->requests = $requests; - return $result; - } - - /** @return ItemStackRequest[] */ - public function getRequests() : array{ return $this->requests; } - - protected function decodePayload() : void{ - $this->requests = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->requests[] = ItemStackRequest::read($this); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->requests)); - foreach($this->requests as $request){ - $request->write($this); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleItemStackRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ItemStackResponsePacket.php b/src/pocketmine/network/mcpe/protocol/ItemStackResponsePacket.php deleted file mode 100644 index c5bfcb69ae..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ItemStackResponsePacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse; -use function count; - -class ItemStackResponsePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::ITEM_STACK_RESPONSE_PACKET; - - /** @var ItemStackResponse[] */ - private $responses; - - /** - * @param ItemStackResponse[] $responses - */ - public static function create(array $responses) : self{ - $result = new self; - $result->responses = $responses; - return $result; - } - - /** @return ItemStackResponse[] */ - public function getResponses() : array{ return $this->responses; } - - protected function decodePayload() : void{ - $this->responses = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->responses[] = ItemStackResponse::read($this); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->responses)); - foreach($this->responses as $response){ - $response->write($this); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleItemStackResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LabTablePacket.php b/src/pocketmine/network/mcpe/protocol/LabTablePacket.php deleted file mode 100644 index 54a3fd346d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LabTablePacket.php +++ /dev/null @@ -1,65 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class LabTablePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LAB_TABLE_PACKET; - - public const TYPE_START_COMBINE = 0; - public const TYPE_START_REACTION = 1; - public const TYPE_RESET = 2; - - /** @var int */ - public $type; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - - /** @var int */ - public $reactionType; - - protected function decodePayload(){ - $this->type = $this->getByte(); - $this->getSignedBlockPosition($this->x, $this->y, $this->z); - $this->reactionType = $this->getByte(); - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putSignedBlockPosition($this->x, $this->y, $this->z); - $this->putByte($this->reactionType); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLabTable($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LecternUpdatePacket.php b/src/pocketmine/network/mcpe/protocol/LecternUpdatePacket.php deleted file mode 100644 index 0242b77a90..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LecternUpdatePacket.php +++ /dev/null @@ -1,63 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class LecternUpdatePacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::LECTERN_UPDATE_PACKET; - - /** @var int */ - public $page; - /** @var int */ - public $totalPages; - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var bool */ - public $dropBook; - - protected function decodePayload() : void{ - $this->page = $this->getByte(); - $this->totalPages = $this->getByte(); - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->dropBook = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putByte($this->page); - $this->putByte($this->totalPages); - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putBool($this->dropBook); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLecternUpdate($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelChunkPacket.php b/src/pocketmine/network/mcpe/protocol/LevelChunkPacket.php deleted file mode 100644 index 43c5ee894d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelChunkPacket.php +++ /dev/null @@ -1,133 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class LevelChunkPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::LEVEL_CHUNK_PACKET; - - /** @var int */ - private $chunkX; - /** @var int */ - private $chunkZ; - /** @var int */ - private $subChunkCount; - /** @var bool */ - private $cacheEnabled; - /** @var int[] */ - private $usedBlobHashes = []; - /** @var string */ - private $extraPayload; - - public static function withoutCache(int $chunkX, int $chunkZ, int $subChunkCount, string $payload) : self{ - $result = new self; - $result->chunkX = $chunkX; - $result->chunkZ = $chunkZ; - $result->subChunkCount = $subChunkCount; - $result->extraPayload = $payload; - - $result->cacheEnabled = false; - - return $result; - } - - /** - * @param int[] $usedBlobHashes - */ - public static function withCache(int $chunkX, int $chunkZ, int $subChunkCount, array $usedBlobHashes, string $extraPayload) : self{ - (static function(int ...$hashes) : void{})(...$usedBlobHashes); - $result = new self; - $result->chunkX = $chunkX; - $result->chunkZ = $chunkZ; - $result->subChunkCount = $subChunkCount; - $result->extraPayload = $extraPayload; - - $result->cacheEnabled = true; - $result->usedBlobHashes = $usedBlobHashes; - - return $result; - } - - public function getChunkX() : int{ - return $this->chunkX; - } - - public function getChunkZ() : int{ - return $this->chunkZ; - } - - public function getSubChunkCount() : int{ - return $this->subChunkCount; - } - - public function isCacheEnabled() : bool{ - return $this->cacheEnabled; - } - - /** - * @return int[] - */ - public function getUsedBlobHashes() : array{ - return $this->usedBlobHashes; - } - - public function getExtraPayload() : string{ - return $this->extraPayload; - } - - protected function decodePayload() : void{ - $this->chunkX = $this->getVarInt(); - $this->chunkZ = $this->getVarInt(); - $this->subChunkCount = $this->getUnsignedVarInt(); - $this->cacheEnabled = $this->getBool(); - if($this->cacheEnabled){ - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->usedBlobHashes[] = $this->getLLong(); - } - } - $this->extraPayload = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->chunkX); - $this->putVarInt($this->chunkZ); - $this->putUnsignedVarInt($this->subChunkCount); - $this->putBool($this->cacheEnabled); - if($this->cacheEnabled){ - $this->putUnsignedVarInt(count($this->usedBlobHashes)); - foreach($this->usedBlobHashes as $hash){ - $this->putLLong($hash); - } - } - $this->putString($this->extraPayload); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleLevelChunk($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelEventGenericPacket.php b/src/pocketmine/network/mcpe/protocol/LevelEventGenericPacket.php deleted file mode 100644 index 7c250e7012..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelEventGenericPacket.php +++ /dev/null @@ -1,68 +0,0 @@ - - -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\NetworkSession; - -class LevelEventGenericPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::LEVEL_EVENT_GENERIC_PACKET; - - /** @var int */ - private $eventId; - /** @var string network-format NBT */ - private $eventData; - - public static function create(int $eventId, CompoundTag $data) : self{ - $result = new self; - $result->eventId = $eventId; - $result->eventData = (new NetworkLittleEndianNBTStream())->write($data); - return $result; - } - - public function getEventId() : int{ - return $this->eventId; - } - - public function getEventData() : string{ - return $this->eventData; - } - - protected function decodePayload() : void{ - $this->eventId = $this->getVarInt(); - $this->eventData = $this->getRemaining(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->eventId); - $this->put($this->eventData); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleLevelEventGeneric($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelEventPacket.php b/src/pocketmine/network/mcpe/protocol/LevelEventPacket.php deleted file mode 100644 index d126724b0c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelEventPacket.php +++ /dev/null @@ -1,138 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class LevelEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LEVEL_EVENT_PACKET; - - public const EVENT_SOUND_CLICK = 1000; - public const EVENT_SOUND_CLICK_FAIL = 1001; - public const EVENT_SOUND_SHOOT = 1002; - public const EVENT_SOUND_DOOR = 1003; - public const EVENT_SOUND_FIZZ = 1004; - public const EVENT_SOUND_IGNITE = 1005; - - public const EVENT_SOUND_GHAST = 1007; - public const EVENT_SOUND_GHAST_SHOOT = 1008; - public const EVENT_SOUND_BLAZE_SHOOT = 1009; - public const EVENT_SOUND_DOOR_BUMP = 1010; - - public const EVENT_SOUND_DOOR_CRASH = 1012; - - public const EVENT_SOUND_ENDERMAN_TELEPORT = 1018; - - public const EVENT_SOUND_ANVIL_BREAK = 1020; - public const EVENT_SOUND_ANVIL_USE = 1021; - public const EVENT_SOUND_ANVIL_FALL = 1022; - - public const EVENT_SOUND_POP = 1030; - - public const EVENT_SOUND_PORTAL = 1032; - - public const EVENT_SOUND_ITEMFRAME_ADD_ITEM = 1040; - public const EVENT_SOUND_ITEMFRAME_REMOVE = 1041; - public const EVENT_SOUND_ITEMFRAME_PLACE = 1042; - public const EVENT_SOUND_ITEMFRAME_REMOVE_ITEM = 1043; - public const EVENT_SOUND_ITEMFRAME_ROTATE_ITEM = 1044; - - public const EVENT_SOUND_CAMERA = 1050; - public const EVENT_SOUND_ORB = 1051; - public const EVENT_SOUND_TOTEM = 1052; - - public const EVENT_SOUND_ARMOR_STAND_BREAK = 1060; - public const EVENT_SOUND_ARMOR_STAND_HIT = 1061; - public const EVENT_SOUND_ARMOR_STAND_FALL = 1062; - public const EVENT_SOUND_ARMOR_STAND_PLACE = 1063; - - //TODO: check 2000-2017 - public const EVENT_PARTICLE_SHOOT = 2000; - public const EVENT_PARTICLE_DESTROY = 2001; - public const EVENT_PARTICLE_SPLASH = 2002; - public const EVENT_PARTICLE_EYE_DESPAWN = 2003; - public const EVENT_PARTICLE_SPAWN = 2004; - - public const EVENT_GUARDIAN_CURSE = 2006; - - public const EVENT_PARTICLE_BLOCK_FORCE_FIELD = 2008; - public const EVENT_PARTICLE_PROJECTILE_HIT = 2009; - - public const EVENT_PARTICLE_ENDERMAN_TELEPORT = 2013; - public const EVENT_PARTICLE_PUNCH_BLOCK = 2014; - - public const EVENT_START_RAIN = 3001; - public const EVENT_START_THUNDER = 3002; - public const EVENT_STOP_RAIN = 3003; - public const EVENT_STOP_THUNDER = 3004; - public const EVENT_PAUSE_GAME = 3005; //data: 1 to pause, 0 to resume - public const EVENT_PAUSE_GAME_NO_SCREEN = 3006; //data: 1 to pause, 0 to resume - same effect as normal pause but without screen - public const EVENT_SET_GAME_SPEED = 3007; //x coordinate of pos = scale factor (default 1.0) - - public const EVENT_REDSTONE_TRIGGER = 3500; - public const EVENT_CAULDRON_EXPLODE = 3501; - public const EVENT_CAULDRON_DYE_ARMOR = 3502; - public const EVENT_CAULDRON_CLEAN_ARMOR = 3503; - public const EVENT_CAULDRON_FILL_POTION = 3504; - public const EVENT_CAULDRON_TAKE_POTION = 3505; - public const EVENT_CAULDRON_FILL_WATER = 3506; - public const EVENT_CAULDRON_TAKE_WATER = 3507; - public const EVENT_CAULDRON_ADD_DYE = 3508; - public const EVENT_CAULDRON_CLEAN_BANNER = 3509; - - public const EVENT_BLOCK_START_BREAK = 3600; - public const EVENT_BLOCK_STOP_BREAK = 3601; - - public const EVENT_SET_DATA = 4000; - - public const EVENT_PLAYERS_SLEEPING = 9800; - - public const EVENT_ADD_PARTICLE_MASK = 0x4000; - - /** @var int */ - public $evid; - /** @var Vector3|null */ - public $position; - /** @var int */ - public $data; - - protected function decodePayload(){ - $this->evid = $this->getVarInt(); - $this->position = $this->getVector3(); - $this->data = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->evid); - $this->putVector3Nullable($this->position); - $this->putVarInt($this->data); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLevelEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacket.php b/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacket.php deleted file mode 100644 index 35d9e4a258..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacket.php +++ /dev/null @@ -1,397 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class LevelSoundEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LEVEL_SOUND_EVENT_PACKET; - - public const SOUND_ITEM_USE_ON = 0; - public const SOUND_HIT = 1; - public const SOUND_STEP = 2; - public const SOUND_FLY = 3; - public const SOUND_JUMP = 4; - public const SOUND_BREAK = 5; - public const SOUND_PLACE = 6; - public const SOUND_HEAVY_STEP = 7; - public const SOUND_GALLOP = 8; - public const SOUND_FALL = 9; - public const SOUND_AMBIENT = 10; - public const SOUND_AMBIENT_BABY = 11; - public const SOUND_AMBIENT_IN_WATER = 12; - public const SOUND_BREATHE = 13; - public const SOUND_DEATH = 14; - public const SOUND_DEATH_IN_WATER = 15; - public const SOUND_DEATH_TO_ZOMBIE = 16; - public const SOUND_HURT = 17; - public const SOUND_HURT_IN_WATER = 18; - public const SOUND_MAD = 19; - public const SOUND_BOOST = 20; - public const SOUND_BOW = 21; - public const SOUND_SQUISH_BIG = 22; - public const SOUND_SQUISH_SMALL = 23; - public const SOUND_FALL_BIG = 24; - public const SOUND_FALL_SMALL = 25; - public const SOUND_SPLASH = 26; - public const SOUND_FIZZ = 27; - public const SOUND_FLAP = 28; - public const SOUND_SWIM = 29; - public const SOUND_DRINK = 30; - public const SOUND_EAT = 31; - public const SOUND_TAKEOFF = 32; - public const SOUND_SHAKE = 33; - public const SOUND_PLOP = 34; - public const SOUND_LAND = 35; - public const SOUND_SADDLE = 36; - public const SOUND_ARMOR = 37; - public const SOUND_MOB_ARMOR_STAND_PLACE = 38; - public const SOUND_ADD_CHEST = 39; - public const SOUND_THROW = 40; - public const SOUND_ATTACK = 41; - public const SOUND_ATTACK_NODAMAGE = 42; - public const SOUND_ATTACK_STRONG = 43; - public const SOUND_WARN = 44; - public const SOUND_SHEAR = 45; - public const SOUND_MILK = 46; - public const SOUND_THUNDER = 47; - public const SOUND_EXPLODE = 48; - public const SOUND_FIRE = 49; - public const SOUND_IGNITE = 50; - public const SOUND_FUSE = 51; - public const SOUND_STARE = 52; - public const SOUND_SPAWN = 53; - public const SOUND_SHOOT = 54; - public const SOUND_BREAK_BLOCK = 55; - public const SOUND_LAUNCH = 56; - public const SOUND_BLAST = 57; - public const SOUND_LARGE_BLAST = 58; - public const SOUND_TWINKLE = 59; - public const SOUND_REMEDY = 60; - public const SOUND_UNFECT = 61; - public const SOUND_LEVELUP = 62; - public const SOUND_BOW_HIT = 63; - public const SOUND_BULLET_HIT = 64; - public const SOUND_EXTINGUISH_FIRE = 65; - public const SOUND_ITEM_FIZZ = 66; - public const SOUND_CHEST_OPEN = 67; - public const SOUND_CHEST_CLOSED = 68; - public const SOUND_SHULKERBOX_OPEN = 69; - public const SOUND_SHULKERBOX_CLOSED = 70; - public const SOUND_ENDERCHEST_OPEN = 71; - public const SOUND_ENDERCHEST_CLOSED = 72; - public const SOUND_POWER_ON = 73; - public const SOUND_POWER_OFF = 74; - public const SOUND_ATTACH = 75; - public const SOUND_DETACH = 76; - public const SOUND_DENY = 77; - public const SOUND_TRIPOD = 78; - public const SOUND_POP = 79; - public const SOUND_DROP_SLOT = 80; - public const SOUND_NOTE = 81; - public const SOUND_THORNS = 82; - public const SOUND_PISTON_IN = 83; - public const SOUND_PISTON_OUT = 84; - public const SOUND_PORTAL = 85; - public const SOUND_WATER = 86; - public const SOUND_LAVA_POP = 87; - public const SOUND_LAVA = 88; - public const SOUND_BURP = 89; - public const SOUND_BUCKET_FILL_WATER = 90; - public const SOUND_BUCKET_FILL_LAVA = 91; - public const SOUND_BUCKET_EMPTY_WATER = 92; - public const SOUND_BUCKET_EMPTY_LAVA = 93; - public const SOUND_ARMOR_EQUIP_CHAIN = 94; - public const SOUND_ARMOR_EQUIP_DIAMOND = 95; - public const SOUND_ARMOR_EQUIP_GENERIC = 96; - public const SOUND_ARMOR_EQUIP_GOLD = 97; - public const SOUND_ARMOR_EQUIP_IRON = 98; - public const SOUND_ARMOR_EQUIP_LEATHER = 99; - public const SOUND_ARMOR_EQUIP_ELYTRA = 100; - public const SOUND_RECORD_13 = 101; - public const SOUND_RECORD_CAT = 102; - public const SOUND_RECORD_BLOCKS = 103; - public const SOUND_RECORD_CHIRP = 104; - public const SOUND_RECORD_FAR = 105; - public const SOUND_RECORD_MALL = 106; - public const SOUND_RECORD_MELLOHI = 107; - public const SOUND_RECORD_STAL = 108; - public const SOUND_RECORD_STRAD = 109; - public const SOUND_RECORD_WARD = 110; - public const SOUND_RECORD_11 = 111; - public const SOUND_RECORD_WAIT = 112; - public const SOUND_STOP_RECORD = 113; //Not really a sound - public const SOUND_FLOP = 114; - public const SOUND_ELDERGUARDIAN_CURSE = 115; - public const SOUND_MOB_WARNING = 116; - public const SOUND_MOB_WARNING_BABY = 117; - public const SOUND_TELEPORT = 118; - public const SOUND_SHULKER_OPEN = 119; - public const SOUND_SHULKER_CLOSE = 120; - public const SOUND_HAGGLE = 121; - public const SOUND_HAGGLE_YES = 122; - public const SOUND_HAGGLE_NO = 123; - public const SOUND_HAGGLE_IDLE = 124; - public const SOUND_CHORUSGROW = 125; - public const SOUND_CHORUSDEATH = 126; - public const SOUND_GLASS = 127; - public const SOUND_POTION_BREWED = 128; - public const SOUND_CAST_SPELL = 129; - public const SOUND_PREPARE_ATTACK = 130; - public const SOUND_PREPARE_SUMMON = 131; - public const SOUND_PREPARE_WOLOLO = 132; - public const SOUND_FANG = 133; - public const SOUND_CHARGE = 134; - public const SOUND_CAMERA_TAKE_PICTURE = 135; - public const SOUND_LEASHKNOT_PLACE = 136; - public const SOUND_LEASHKNOT_BREAK = 137; - public const SOUND_GROWL = 138; - public const SOUND_WHINE = 139; - public const SOUND_PANT = 140; - public const SOUND_PURR = 141; - public const SOUND_PURREOW = 142; - public const SOUND_DEATH_MIN_VOLUME = 143; - public const SOUND_DEATH_MID_VOLUME = 144; - public const SOUND_IMITATE_BLAZE = 145; - public const SOUND_IMITATE_CAVE_SPIDER = 146; - public const SOUND_IMITATE_CREEPER = 147; - public const SOUND_IMITATE_ELDER_GUARDIAN = 148; - public const SOUND_IMITATE_ENDER_DRAGON = 149; - public const SOUND_IMITATE_ENDERMAN = 150; - - public const SOUND_IMITATE_EVOCATION_ILLAGER = 152; - public const SOUND_IMITATE_GHAST = 153; - public const SOUND_IMITATE_HUSK = 154; - public const SOUND_IMITATE_ILLUSION_ILLAGER = 155; - public const SOUND_IMITATE_MAGMA_CUBE = 156; - public const SOUND_IMITATE_POLAR_BEAR = 157; - public const SOUND_IMITATE_SHULKER = 158; - public const SOUND_IMITATE_SILVERFISH = 159; - public const SOUND_IMITATE_SKELETON = 160; - public const SOUND_IMITATE_SLIME = 161; - public const SOUND_IMITATE_SPIDER = 162; - public const SOUND_IMITATE_STRAY = 163; - public const SOUND_IMITATE_VEX = 164; - public const SOUND_IMITATE_VINDICATION_ILLAGER = 165; - public const SOUND_IMITATE_WITCH = 166; - public const SOUND_IMITATE_WITHER = 167; - public const SOUND_IMITATE_WITHER_SKELETON = 168; - public const SOUND_IMITATE_WOLF = 169; - public const SOUND_IMITATE_ZOMBIE = 170; - public const SOUND_IMITATE_ZOMBIE_PIGMAN = 171; - public const SOUND_IMITATE_ZOMBIE_VILLAGER = 172; - public const SOUND_BLOCK_END_PORTAL_FRAME_FILL = 173; - public const SOUND_BLOCK_END_PORTAL_SPAWN = 174; - public const SOUND_RANDOM_ANVIL_USE = 175; - public const SOUND_BOTTLE_DRAGONBREATH = 176; - public const SOUND_PORTAL_TRAVEL = 177; - public const SOUND_ITEM_TRIDENT_HIT = 178; - public const SOUND_ITEM_TRIDENT_RETURN = 179; - public const SOUND_ITEM_TRIDENT_RIPTIDE_1 = 180; - public const SOUND_ITEM_TRIDENT_RIPTIDE_2 = 181; - public const SOUND_ITEM_TRIDENT_RIPTIDE_3 = 182; - public const SOUND_ITEM_TRIDENT_THROW = 183; - public const SOUND_ITEM_TRIDENT_THUNDER = 184; - public const SOUND_ITEM_TRIDENT_HIT_GROUND = 185; - public const SOUND_DEFAULT = 186; - public const SOUND_BLOCK_FLETCHING_TABLE_USE = 187; - public const SOUND_ELEMCONSTRUCT_OPEN = 188; - public const SOUND_ICEBOMB_HIT = 189; - public const SOUND_BALLOONPOP = 190; - public const SOUND_LT_REACTION_ICEBOMB = 191; - public const SOUND_LT_REACTION_BLEACH = 192; - public const SOUND_LT_REACTION_EPASTE = 193; - public const SOUND_LT_REACTION_EPASTE2 = 194; - - public const SOUND_LT_REACTION_FERTILIZER = 199; - public const SOUND_LT_REACTION_FIREBALL = 200; - public const SOUND_LT_REACTION_MGSALT = 201; - public const SOUND_LT_REACTION_MISCFIRE = 202; - public const SOUND_LT_REACTION_FIRE = 203; - public const SOUND_LT_REACTION_MISCEXPLOSION = 204; - public const SOUND_LT_REACTION_MISCMYSTICAL = 205; - public const SOUND_LT_REACTION_MISCMYSTICAL2 = 206; - public const SOUND_LT_REACTION_PRODUCT = 207; - public const SOUND_SPARKLER_USE = 208; - public const SOUND_GLOWSTICK_USE = 209; - public const SOUND_SPARKLER_ACTIVE = 210; - public const SOUND_CONVERT_TO_DROWNED = 211; - public const SOUND_BUCKET_FILL_FISH = 212; - public const SOUND_BUCKET_EMPTY_FISH = 213; - public const SOUND_BUBBLE_UP = 214; - public const SOUND_BUBBLE_DOWN = 215; - public const SOUND_BUBBLE_POP = 216; - public const SOUND_BUBBLE_UPINSIDE = 217; - public const SOUND_BUBBLE_DOWNINSIDE = 218; - public const SOUND_HURT_BABY = 219; - public const SOUND_DEATH_BABY = 220; - public const SOUND_STEP_BABY = 221; - - public const SOUND_BORN = 223; - public const SOUND_BLOCK_TURTLE_EGG_BREAK = 224; - public const SOUND_BLOCK_TURTLE_EGG_CRACK = 225; - public const SOUND_BLOCK_TURTLE_EGG_HATCH = 226; - public const SOUND_LAY_EGG = 227; - public const SOUND_BLOCK_TURTLE_EGG_ATTACK = 228; - public const SOUND_BEACON_ACTIVATE = 229; - public const SOUND_BEACON_AMBIENT = 230; - public const SOUND_BEACON_DEACTIVATE = 231; - public const SOUND_BEACON_POWER = 232; - public const SOUND_CONDUIT_ACTIVATE = 233; - public const SOUND_CONDUIT_AMBIENT = 234; - public const SOUND_CONDUIT_ATTACK = 235; - public const SOUND_CONDUIT_DEACTIVATE = 236; - public const SOUND_CONDUIT_SHORT = 237; - public const SOUND_SWOOP = 238; - public const SOUND_BLOCK_BAMBOO_SAPLING_PLACE = 239; - public const SOUND_PRESNEEZE = 240; - public const SOUND_SNEEZE = 241; - public const SOUND_AMBIENT_TAME = 242; - public const SOUND_SCARED = 243; - public const SOUND_BLOCK_SCAFFOLDING_CLIMB = 244; - public const SOUND_CROSSBOW_LOADING_START = 245; - public const SOUND_CROSSBOW_LOADING_MIDDLE = 246; - public const SOUND_CROSSBOW_LOADING_END = 247; - public const SOUND_CROSSBOW_SHOOT = 248; - public const SOUND_CROSSBOW_QUICK_CHARGE_START = 249; - public const SOUND_CROSSBOW_QUICK_CHARGE_MIDDLE = 250; - public const SOUND_CROSSBOW_QUICK_CHARGE_END = 251; - public const SOUND_AMBIENT_AGGRESSIVE = 252; - public const SOUND_AMBIENT_WORRIED = 253; - public const SOUND_CANT_BREED = 254; - public const SOUND_ITEM_SHIELD_BLOCK = 255; - public const SOUND_ITEM_BOOK_PUT = 256; - public const SOUND_BLOCK_GRINDSTONE_USE = 257; - public const SOUND_BLOCK_BELL_HIT = 258; - public const SOUND_BLOCK_CAMPFIRE_CRACKLE = 259; - public const SOUND_ROAR = 260; - public const SOUND_STUN = 261; - public const SOUND_BLOCK_SWEET_BERRY_BUSH_HURT = 262; - public const SOUND_BLOCK_SWEET_BERRY_BUSH_PICK = 263; - public const SOUND_BLOCK_CARTOGRAPHY_TABLE_USE = 264; - public const SOUND_BLOCK_STONECUTTER_USE = 265; - public const SOUND_BLOCK_COMPOSTER_EMPTY = 266; - public const SOUND_BLOCK_COMPOSTER_FILL = 267; - public const SOUND_BLOCK_COMPOSTER_FILL_SUCCESS = 268; - public const SOUND_BLOCK_COMPOSTER_READY = 269; - public const SOUND_BLOCK_BARREL_OPEN = 270; - public const SOUND_BLOCK_BARREL_CLOSE = 271; - public const SOUND_RAID_HORN = 272; - public const SOUND_BLOCK_LOOM_USE = 273; - public const SOUND_AMBIENT_IN_RAID = 274; - public const SOUND_UI_CARTOGRAPHY_TABLE_TAKE_RESULT = 275; - public const SOUND_UI_STONECUTTER_TAKE_RESULT = 276; - public const SOUND_UI_LOOM_TAKE_RESULT = 277; - public const SOUND_BLOCK_SMOKER_SMOKE = 278; - public const SOUND_BLOCK_BLASTFURNACE_FIRE_CRACKLE = 279; - public const SOUND_BLOCK_SMITHING_TABLE_USE = 280; - public const SOUND_SCREECH = 281; - public const SOUND_SLEEP = 282; - public const SOUND_BLOCK_FURNACE_LIT = 283; - public const SOUND_CONVERT_MOOSHROOM = 284; - public const SOUND_MILK_SUSPICIOUSLY = 285; - public const SOUND_CELEBRATE = 286; - public const SOUND_JUMP_PREVENT = 287; - public const SOUND_AMBIENT_POLLINATE = 288; - public const SOUND_BLOCK_BEEHIVE_DRIP = 289; - public const SOUND_BLOCK_BEEHIVE_ENTER = 290; - public const SOUND_BLOCK_BEEHIVE_EXIT = 291; - public const SOUND_BLOCK_BEEHIVE_WORK = 292; - public const SOUND_BLOCK_BEEHIVE_SHEAR = 293; - public const SOUND_DRINK_HONEY = 294; - public const SOUND_AMBIENT_CAVE = 295; - public const SOUND_RETREAT = 296; - public const SOUND_CONVERTED_TO_ZOMBIFIED = 297; - public const SOUND_ADMIRE = 298; - public const SOUND_STEP_LAVA = 299; - public const SOUND_TEMPT = 300; - public const SOUND_PANIC = 301; - public const SOUND_ANGRY = 302; - public const SOUND_AMBIENT_WARPED_FOREST_MOOD = 303; - public const SOUND_AMBIENT_SOULSAND_VALLEY_MOOD = 304; - public const SOUND_AMBIENT_NETHER_WASTES_MOOD = 305; - public const SOUND_RESPAWN_ANCHOR_BASALT_DELTAS_MOOD = 306; - public const SOUND_AMBIENT_CRIMSON_FOREST_MOOD = 307; - public const SOUND_RESPAWN_ANCHOR_CHARGE = 308; - public const SOUND_RESPAWN_ANCHOR_DEPLETE = 309; - public const SOUND_RESPAWN_ANCHOR_SET_SPAWN = 310; - public const SOUND_RESPAWN_ANCHOR_AMBIENT = 311; - public const SOUND_PARTICLE_SOUL_ESCAPE_QUIET = 312; - public const SOUND_PARTICLE_SOUL_ESCAPE_LOUD = 313; - public const SOUND_RECORD_PIGSTEP = 314; - public const SOUND_LODESTONE_COMPASS_LINK_COMPASS_TO_LODESTONE = 315; - public const SOUND_SMITHING_TABLE_USE = 316; - public const SOUND_ARMOR_EQUIP_NETHERITE = 317; - public const SOUND_AMBIENT_WARPED_FOREST_LOOP = 318; - public const SOUND_AMBIENT_SOULSAND_VALLEY_LOOP = 319; - public const SOUND_AMBIENT_NETHER_WASTES_LOOP = 320; - public const SOUND_AMBIENT_BASALT_DELTAS_LOOP = 321; - public const SOUND_AMBIENT_CRIMSON_FOREST_LOOP = 322; - public const SOUND_AMBIENT_WARPED_FOREST_ADDITIONS = 323; - public const SOUND_AMBIENT_SOULSAND_VALLEY_ADDITIONS = 324; - public const SOUND_AMBIENT_NETHER_WASTES_ADDITIONS = 325; - public const SOUND_AMBIENT_BASALT_DELTAS_ADDITIONS = 326; - public const SOUND_AMBIENT_CRIMSON_FOREST_ADDITIONS = 327; - public const SOUND_BUCKET_FILL_POWDER_SNOW = 328; - public const SOUND_BUCKET_EMPTY_POWDER_SNOW = 329; - public const SOUND_UNDEFINED = 330; - - /** @var int */ - public $sound; - /** @var Vector3 */ - public $position; - /** @var int */ - public $extraData = -1; - /** @var string */ - public $entityType = ":"; //??? - /** @var bool */ - public $isBabyMob = false; //... - /** @var bool */ - public $disableRelativeVolume = false; - - protected function decodePayload(){ - $this->sound = $this->getUnsignedVarInt(); - $this->position = $this->getVector3(); - $this->extraData = $this->getVarInt(); - $this->entityType = $this->getString(); - $this->isBabyMob = $this->getBool(); - $this->disableRelativeVolume = $this->getBool(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->sound); - $this->putVector3($this->position); - $this->putVarInt($this->extraData); - $this->putString($this->entityType); - $this->putBool($this->isBabyMob); - $this->putBool($this->disableRelativeVolume); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLevelSoundEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV1.php b/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV1.php deleted file mode 100644 index 3342e6e3db..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV1.php +++ /dev/null @@ -1,71 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -/** - * Useless leftover from a 1.8 refactor, does nothing - */ -class LevelSoundEventPacketV1 extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LEVEL_SOUND_EVENT_PACKET_V1; - - /** @var int */ - public $sound; - /** @var Vector3 */ - public $position; - /** @var int */ - public $extraData = 0; - /** @var int */ - public $entityType = 1; - /** @var bool */ - public $isBabyMob = false; //... - /** @var bool */ - public $disableRelativeVolume = false; - - protected function decodePayload(){ - $this->sound = $this->getByte(); - $this->position = $this->getVector3(); - $this->extraData = $this->getVarInt(); - $this->entityType = $this->getVarInt(); - $this->isBabyMob = $this->getBool(); - $this->disableRelativeVolume = $this->getBool(); - } - - protected function encodePayload(){ - $this->putByte($this->sound); - $this->putVector3($this->position); - $this->putVarInt($this->extraData); - $this->putVarInt($this->entityType); - $this->putBool($this->isBabyMob); - $this->putBool($this->disableRelativeVolume); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLevelSoundEventPacketV1($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV2.php b/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV2.php deleted file mode 100644 index 5740ce4e45..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LevelSoundEventPacketV2.php +++ /dev/null @@ -1,71 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -/** - * Useless leftover from a 1.9 refactor, does nothing - */ -class LevelSoundEventPacketV2 extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LEVEL_SOUND_EVENT_PACKET_V2; - - /** @var int */ - public $sound; - /** @var Vector3 */ - public $position; - /** @var int */ - public $extraData = -1; - /** @var string */ - public $entityType = ":"; //??? - /** @var bool */ - public $isBabyMob = false; //... - /** @var bool */ - public $disableRelativeVolume = false; - - protected function decodePayload(){ - $this->sound = $this->getByte(); - $this->position = $this->getVector3(); - $this->extraData = $this->getVarInt(); - $this->entityType = $this->getString(); - $this->isBabyMob = $this->getBool(); - $this->disableRelativeVolume = $this->getBool(); - } - - protected function encodePayload(){ - $this->putByte($this->sound); - $this->putVector3($this->position); - $this->putVarInt($this->extraData); - $this->putString($this->entityType); - $this->putBool($this->isBabyMob); - $this->putBool($this->disableRelativeVolume); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLevelSoundEventPacketV2($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/LoginPacket.php b/src/pocketmine/network/mcpe/protocol/LoginPacket.php deleted file mode 100644 index e27aaa7711..0000000000 --- a/src/pocketmine/network/mcpe/protocol/LoginPacket.php +++ /dev/null @@ -1,147 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\utils\BinaryStream; -use pocketmine\utils\MainLogger; -use pocketmine\utils\Utils; -use function get_class; -use function json_decode; - -class LoginPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::LOGIN_PACKET; - - /** @var string */ - public $username; - /** @var int */ - public $protocol; - /** @var string */ - public $clientUUID; - /** @var int */ - public $clientId; - /** @var string|null */ - public $xuid = null; - /** @var string */ - public $identityPublicKey; - /** @var string */ - public $serverAddress; - /** @var string */ - public $locale; - - /** - * @var string[][] (the "chain" index contains one or more JWTs) - * @phpstan-var array{chain?: list} - */ - public $chainData = []; - /** @var string */ - public $clientDataJwt; - /** - * @var mixed[] decoded payload of the clientData JWT - * @phpstan-var array - */ - public $clientData = []; - - /** - * This field may be used by plugins to bypass keychain verification. It should only be used for plugins such as - * Specter where passing verification would take too much time and not be worth it. - * - * @var bool - */ - public $skipVerification = false; - - public function canBeSentBeforeLogin() : bool{ - return true; - } - - public function mayHaveUnreadBytes() : bool{ - return $this->protocol !== ProtocolInfo::CURRENT_PROTOCOL; - } - - protected function decodePayload(){ - $this->protocol = $this->getInt(); - - try{ - $this->decodeConnectionRequest(); - }catch(\Throwable $e){ - if($this->protocol === ProtocolInfo::CURRENT_PROTOCOL){ - throw $e; - } - - $logger = MainLogger::getLogger(); - $logger->debug(get_class($e) . " was thrown while decoding connection request in login (protocol version $this->protocol): " . $e->getMessage()); - foreach(Utils::printableTrace($e->getTrace()) as $line){ - $logger->debug($line); - } - } - } - - protected function decodeConnectionRequest() : void{ - $buffer = new BinaryStream($this->getString()); - - $this->chainData = json_decode($buffer->get($buffer->getLInt()), true); - - $hasExtraData = false; - foreach($this->chainData["chain"] as $chain){ - $webtoken = Utils::decodeJWT($chain); - if(isset($webtoken["extraData"])){ - if($hasExtraData){ - throw new \RuntimeException("Found 'extraData' multiple times in key chain"); - } - $hasExtraData = true; - if(isset($webtoken["extraData"]["displayName"])){ - $this->username = $webtoken["extraData"]["displayName"]; - } - if(isset($webtoken["extraData"]["identity"])){ - $this->clientUUID = $webtoken["extraData"]["identity"]; - } - if(isset($webtoken["extraData"]["XUID"])){ - $this->xuid = $webtoken["extraData"]["XUID"]; - } - } - - if(isset($webtoken["identityPublicKey"])){ - $this->identityPublicKey = $webtoken["identityPublicKey"]; - } - } - - $this->clientDataJwt = $buffer->get($buffer->getLInt()); - $this->clientData = Utils::decodeJWT($this->clientDataJwt); - - $this->clientId = $this->clientData["ClientRandomId"] ?? null; - $this->serverAddress = $this->clientData["ServerAddress"] ?? null; - - $this->locale = $this->clientData["LanguageCode"] ?? null; - } - - protected function encodePayload(){ - //TODO - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleLogin($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MapCreateLockedCopyPacket.php b/src/pocketmine/network/mcpe/protocol/MapCreateLockedCopyPacket.php deleted file mode 100644 index e8014a1c6a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MapCreateLockedCopyPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class MapCreateLockedCopyPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MAP_CREATE_LOCKED_COPY_PACKET; - - /** @var int */ - public $originalMapId; - /** @var int */ - public $newMapId; - - protected function decodePayload() : void{ - $this->originalMapId = $this->getEntityUniqueId(); - $this->newMapId = $this->getEntityUniqueId(); - } - - protected function encodePayload() : void{ - $this->putEntityUniqueId($this->originalMapId); - $this->putEntityUniqueId($this->newMapId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleMapCreateLockedCopy($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MobArmorEquipmentPacket.php b/src/pocketmine/network/mcpe/protocol/MobArmorEquipmentPacket.php deleted file mode 100644 index 0a01441c1f..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MobArmorEquipmentPacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; - -class MobArmorEquipmentPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOB_ARMOR_EQUIPMENT_PACKET; - - /** @var int */ - public $entityRuntimeId; - - //this intentionally doesn't use an array because we don't want any implicit dependencies on internal order - - /** @var ItemStackWrapper */ - public $head; - /** @var ItemStackWrapper */ - public $chest; - /** @var ItemStackWrapper */ - public $legs; - /** @var ItemStackWrapper */ - public $feet; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->head = ItemStackWrapper::read($this); - $this->chest = ItemStackWrapper::read($this); - $this->legs = ItemStackWrapper::read($this); - $this->feet = ItemStackWrapper::read($this); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->head->write($this); - $this->chest->write($this); - $this->legs->write($this); - $this->feet->write($this); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMobArmorEquipment($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MobEffectPacket.php b/src/pocketmine/network/mcpe/protocol/MobEffectPacket.php deleted file mode 100644 index a17de4d176..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MobEffectPacket.php +++ /dev/null @@ -1,71 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class MobEffectPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOB_EFFECT_PACKET; - - public const EVENT_ADD = 1; - public const EVENT_MODIFY = 2; - public const EVENT_REMOVE = 3; - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $eventId; - /** @var int */ - public $effectId; - /** @var int */ - public $amplifier = 0; - /** @var bool */ - public $particles = true; - /** @var int */ - public $duration = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->eventId = $this->getByte(); - $this->effectId = $this->getVarInt(); - $this->amplifier = $this->getVarInt(); - $this->particles = $this->getBool(); - $this->duration = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putByte($this->eventId); - $this->putVarInt($this->effectId); - $this->putVarInt($this->amplifier); - $this->putBool($this->particles); - $this->putVarInt($this->duration); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMobEffect($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MobEquipmentPacket.php b/src/pocketmine/network/mcpe/protocol/MobEquipmentPacket.php deleted file mode 100644 index 9d5c72f861..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MobEquipmentPacket.php +++ /dev/null @@ -1,64 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; - -class MobEquipmentPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOB_EQUIPMENT_PACKET; - - /** @var int */ - public $entityRuntimeId; - /** @var ItemStackWrapper */ - public $item; - /** @var int */ - public $inventorySlot; - /** @var int */ - public $hotbarSlot; - /** @var int */ - public $windowId = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->item = ItemStackWrapper::read($this); - $this->inventorySlot = $this->getByte(); - $this->hotbarSlot = $this->getByte(); - $this->windowId = $this->getByte(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->item->write($this); - $this->putByte($this->inventorySlot); - $this->putByte($this->hotbarSlot); - $this->putByte($this->windowId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMobEquipment($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ModalFormRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ModalFormRequestPacket.php deleted file mode 100644 index 4a0bd2cb9e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ModalFormRequestPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ModalFormRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MODAL_FORM_REQUEST_PACKET; - - /** @var int */ - public $formId; - /** @var string */ - public $formData; //json - - protected function decodePayload(){ - $this->formId = $this->getUnsignedVarInt(); - $this->formData = $this->getString(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->formId); - $this->putString($this->formData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleModalFormRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ModalFormResponsePacket.php b/src/pocketmine/network/mcpe/protocol/ModalFormResponsePacket.php deleted file mode 100644 index 9303ec17dd..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ModalFormResponsePacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ModalFormResponsePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MODAL_FORM_RESPONSE_PACKET; - - /** @var int */ - public $formId; - /** @var string */ - public $formData; //json - - protected function decodePayload(){ - $this->formId = $this->getUnsignedVarInt(); - $this->formData = $this->getString(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->formId); - $this->putString($this->formData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleModalFormResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MotionPredictionHintsPacket.php b/src/pocketmine/network/mcpe/protocol/MotionPredictionHintsPacket.php deleted file mode 100644 index 4b36075535..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MotionPredictionHintsPacket.php +++ /dev/null @@ -1,70 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class MotionPredictionHintsPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::MOTION_PREDICTION_HINTS_PACKET; - - /** @var int */ - private $entityRuntimeId; - /** @var Vector3 */ - private $motion; - /** @var bool */ - private $onGround; - - public static function create(int $entityRuntimeId, Vector3 $motion, bool $onGround) : self{ - $result = new self; - $result->entityRuntimeId = $entityRuntimeId; - $result->motion = $motion; - $result->onGround = $onGround; - return $result; - } - - public function getEntityRuntimeIdField() : int{ return $this->entityRuntimeId; } //TODO: rename this on PM4 (crap architecture, thanks shoghi) - - public function getMotion() : Vector3{ return $this->motion; } - - public function isOnGround() : bool{ return $this->onGround; } - - protected function decodePayload() : void{ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->motion = $this->getVector3(); - $this->onGround = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVector3($this->motion); - $this->putBool($this->onGround); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleMotionPredictionHints($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MoveActorAbsolutePacket.php b/src/pocketmine/network/mcpe/protocol/MoveActorAbsolutePacket.php deleted file mode 100644 index 71766cc487..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MoveActorAbsolutePacket.php +++ /dev/null @@ -1,72 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class MoveActorAbsolutePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOVE_ACTOR_ABSOLUTE_PACKET; - - public const FLAG_GROUND = 0x01; - public const FLAG_TELEPORT = 0x02; - public const FLAG_FORCE_MOVE_LOCAL_ENTITY = 0x04; - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $flags = 0; - /** @var Vector3 */ - public $position; - /** @var float */ - public $xRot; - /** @var float */ - public $yRot; - /** @var float */ - public $zRot; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->flags = $this->getByte(); - $this->position = $this->getVector3(); - $this->xRot = $this->getByteRotation(); - $this->yRot = $this->getByteRotation(); - $this->zRot = $this->getByteRotation(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putByte($this->flags); - $this->putVector3($this->position); - $this->putByteRotation($this->xRot); - $this->putByteRotation($this->yRot); - $this->putByteRotation($this->zRot); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMoveActorAbsolute($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php b/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php deleted file mode 100644 index d3d2573230..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php +++ /dev/null @@ -1,111 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class MoveActorDeltaPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOVE_ACTOR_DELTA_PACKET; - - public const FLAG_HAS_X = 0x01; - public const FLAG_HAS_Y = 0x02; - public const FLAG_HAS_Z = 0x04; - public const FLAG_HAS_ROT_X = 0x08; - public const FLAG_HAS_ROT_Y = 0x10; - public const FLAG_HAS_ROT_Z = 0x20; - public const FLAG_GROUND = 0x40; - public const FLAG_TELEPORT = 0x80; - public const FLAG_FORCE_MOVE_LOCAL_ENTITY = 0x100; - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $flags; - /** @var float */ - public $xPos = 0; - /** @var float */ - public $yPos = 0; - /** @var float */ - public $zPos = 0; - /** @var float */ - public $xRot = 0.0; - /** @var float */ - public $yRot = 0.0; - /** @var float */ - public $zRot = 0.0; - - private function maybeReadCoord(int $flag) : float{ - if(($this->flags & $flag) !== 0){ - return $this->getLFloat(); - } - return 0; - } - - private function maybeReadRotation(int $flag) : float{ - if(($this->flags & $flag) !== 0){ - return $this->getByteRotation(); - } - return 0.0; - } - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->flags = $this->getLShort(); - $this->xPos = $this->maybeReadCoord(self::FLAG_HAS_X); - $this->yPos = $this->maybeReadCoord(self::FLAG_HAS_Y); - $this->zPos = $this->maybeReadCoord(self::FLAG_HAS_Z); - $this->xRot = $this->maybeReadRotation(self::FLAG_HAS_ROT_X); - $this->yRot = $this->maybeReadRotation(self::FLAG_HAS_ROT_Y); - $this->zRot = $this->maybeReadRotation(self::FLAG_HAS_ROT_Z); - } - - private function maybeWriteCoord(int $flag, float $val) : void{ - if(($this->flags & $flag) !== 0){ - $this->putLFloat($val); - } - } - - private function maybeWriteRotation(int $flag, float $val) : void{ - if(($this->flags & $flag) !== 0){ - $this->putByteRotation($val); - } - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putLShort($this->flags); - $this->maybeWriteCoord(self::FLAG_HAS_X, $this->xPos); - $this->maybeWriteCoord(self::FLAG_HAS_Y, $this->yPos); - $this->maybeWriteCoord(self::FLAG_HAS_Z, $this->zPos); - $this->maybeWriteRotation(self::FLAG_HAS_ROT_X, $this->xRot); - $this->maybeWriteRotation(self::FLAG_HAS_ROT_Y, $this->yRot); - $this->maybeWriteRotation(self::FLAG_HAS_ROT_Z, $this->zRot); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMoveActorDelta($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MovePlayerPacket.php b/src/pocketmine/network/mcpe/protocol/MovePlayerPacket.php deleted file mode 100644 index 7fa8eb1e39..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MovePlayerPacket.php +++ /dev/null @@ -1,97 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class MovePlayerPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MOVE_PLAYER_PACKET; - - public const MODE_NORMAL = 0; - public const MODE_RESET = 1; - public const MODE_TELEPORT = 2; - public const MODE_PITCH = 3; //facepalm Mojang - - /** @var int */ - public $entityRuntimeId; - /** @var Vector3 */ - public $position; - /** @var float */ - public $pitch; - /** @var float */ - public $yaw; - /** @var float */ - public $headYaw; - /** @var int */ - public $mode = self::MODE_NORMAL; - /** @var bool */ - public $onGround = false; //TODO - /** @var int */ - public $ridingEid = 0; - /** @var int */ - public $teleportCause = 0; - /** @var int */ - public $teleportItem = 0; - /** @var int */ - public $tick = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->position = $this->getVector3(); - $this->pitch = $this->getLFloat(); - $this->yaw = $this->getLFloat(); - $this->headYaw = $this->getLFloat(); - $this->mode = $this->getByte(); - $this->onGround = $this->getBool(); - $this->ridingEid = $this->getEntityRuntimeId(); - if($this->mode === MovePlayerPacket::MODE_TELEPORT){ - $this->teleportCause = $this->getLInt(); - $this->teleportItem = $this->getLInt(); - } - $this->tick = $this->getUnsignedVarLong(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVector3($this->position); - $this->putLFloat($this->pitch); - $this->putLFloat($this->yaw); - $this->putLFloat($this->headYaw); //TODO - $this->putByte($this->mode); - $this->putBool($this->onGround); - $this->putEntityRuntimeId($this->ridingEid); - if($this->mode === MovePlayerPacket::MODE_TELEPORT){ - $this->putLInt($this->teleportCause); - $this->putLInt($this->teleportItem); - } - $this->putUnsignedVarLong($this->tick); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleMovePlayer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/MultiplayerSettingsPacket.php b/src/pocketmine/network/mcpe/protocol/MultiplayerSettingsPacket.php deleted file mode 100644 index c46918f885..0000000000 --- a/src/pocketmine/network/mcpe/protocol/MultiplayerSettingsPacket.php +++ /dev/null @@ -1,61 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class MultiplayerSettingsPacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::MULTIPLAYER_SETTINGS_PACKET; - - public const ACTION_ENABLE_MULTIPLAYER = 0; - public const ACTION_DISABLE_MULTIPLAYER = 1; - public const ACTION_REFRESH_JOIN_CODE = 2; - - /** @var int */ - private $action; - - public static function create(int $action) : self{ - $result = new self; - $result->action = $action; - return $result; - } - - public function getAction() : int{ - return $this->action; - } - - protected function decodePayload() : void{ - $this->action = $this->getVarInt(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->action); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleMultiplayerSettings($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/NetworkChunkPublisherUpdatePacket.php b/src/pocketmine/network/mcpe/protocol/NetworkChunkPublisherUpdatePacket.php deleted file mode 100644 index 223681cba3..0000000000 --- a/src/pocketmine/network/mcpe/protocol/NetworkChunkPublisherUpdatePacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class NetworkChunkPublisherUpdatePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::NETWORK_CHUNK_PUBLISHER_UPDATE_PACKET; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $radius; - - protected function decodePayload(){ - $this->getSignedBlockPosition($this->x, $this->y, $this->z); - $this->radius = $this->getUnsignedVarInt(); - } - - protected function encodePayload(){ - $this->putSignedBlockPosition($this->x, $this->y, $this->z); - $this->putUnsignedVarInt($this->radius); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleNetworkChunkPublisherUpdate($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/NetworkSettingsPacket.php b/src/pocketmine/network/mcpe/protocol/NetworkSettingsPacket.php deleted file mode 100644 index f372921294..0000000000 --- a/src/pocketmine/network/mcpe/protocol/NetworkSettingsPacket.php +++ /dev/null @@ -1,60 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class NetworkSettingsPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::NETWORK_SETTINGS_PACKET; - - public const COMPRESS_NOTHING = 0; - public const COMPRESS_EVERYTHING = 1; - - /** @var int */ - private $compressionThreshold; - - public static function create(int $compressionThreshold) : self{ - $result = new self; - $result->compressionThreshold = $compressionThreshold; - return $result; - } - - public function getCompressionThreshold() : int{ - return $this->compressionThreshold; - } - - protected function decodePayload() : void{ - $this->compressionThreshold = $this->getLShort(); - } - - protected function encodePayload() : void{ - $this->putLShort($this->compressionThreshold); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleNetworkSettings($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/NetworkStackLatencyPacket.php b/src/pocketmine/network/mcpe/protocol/NetworkStackLatencyPacket.php deleted file mode 100644 index 9fe2abd503..0000000000 --- a/src/pocketmine/network/mcpe/protocol/NetworkStackLatencyPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class NetworkStackLatencyPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::NETWORK_STACK_LATENCY_PACKET; - - /** @var int */ - public $timestamp; - /** @var bool */ - public $needResponse; - - protected function decodePayload(){ - $this->timestamp = $this->getLLong(); - $this->needResponse = $this->getBool(); - } - - protected function encodePayload(){ - $this->putLLong($this->timestamp); - $this->putBool($this->needResponse); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleNetworkStackLatency($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/NpcDialoguePacket.php b/src/pocketmine/network/mcpe/protocol/NpcDialoguePacket.php deleted file mode 100644 index 6aaa484f2a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/NpcDialoguePacket.php +++ /dev/null @@ -1,87 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class NpcDialoguePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::NPC_DIALOGUE_PACKET; - - public const ACTION_OPEN = 0; - public const ACTION_CLOSE = 1; - - private int $npcActorUniqueId; - private int $actionType; - private string $dialogue; - private string $sceneName; - private string $npcName; - private string $actionJson; - - public static function create(int $npcActorUniqueId, int $actionType, string $dialogue, string $sceneName, string $npcName, string $actionJson) : self{ - $result = new self; - $result->npcActorUniqueId = $npcActorUniqueId; - $result->actionType = $actionType; - $result->dialogue = $dialogue; - $result->sceneName = $sceneName; - $result->npcName = $npcName; - $result->actionJson = $actionJson; - return $result; - } - - public function getNpcActorUniqueId() : int{ return $this->npcActorUniqueId; } - - public function getActionType() : int{ return $this->actionType; } - - public function getDialogue() : string{ return $this->dialogue; } - - public function getSceneName() : string{ return $this->sceneName; } - - public function getNpcName() : string{ return $this->npcName; } - - public function getActionJson() : string{ return $this->actionJson; } - - protected function decodePayload() : void{ - $this->npcActorUniqueId = $this->getLLong(); //WHY NOT USING STANDARD METHODS, MOJANG - $this->actionType = $this->getVarInt(); - $this->dialogue = $this->getString(); - $this->sceneName = $this->getString(); - $this->npcName = $this->getString(); - $this->actionJson = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putLLong($this->npcActorUniqueId); - $this->putVarInt($this->actionType); - $this->putString($this->dialogue); - $this->putString($this->sceneName); - $this->putString($this->npcName); - $this->putString($this->actionJson); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleNpcDialogue($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/NpcRequestPacket.php b/src/pocketmine/network/mcpe/protocol/NpcRequestPacket.php deleted file mode 100644 index b8def7d140..0000000000 --- a/src/pocketmine/network/mcpe/protocol/NpcRequestPacket.php +++ /dev/null @@ -1,70 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class NpcRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::NPC_REQUEST_PACKET; - - public const REQUEST_SET_ACTIONS = 0; - public const REQUEST_EXECUTE_ACTION = 1; - public const REQUEST_EXECUTE_CLOSING_COMMANDS = 2; - public const REQUEST_SET_NAME = 3; - public const REQUEST_SET_SKIN = 4; - public const REQUEST_SET_INTERACTION_TEXT = 5; - public const REQUEST_EXECUTE_OPENING_COMMANDS = 6; - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $requestType; - /** @var string */ - public $commandString; - /** @var int */ - public $actionType; - public string $sceneName; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->requestType = $this->getByte(); - $this->commandString = $this->getString(); - $this->actionType = $this->getByte(); - $this->sceneName = $this->getString(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putByte($this->requestType); - $this->putString($this->commandString); - $this->putByte($this->actionType); - $this->putString($this->sceneName); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleNpcRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/OnScreenTextureAnimationPacket.php b/src/pocketmine/network/mcpe/protocol/OnScreenTextureAnimationPacket.php deleted file mode 100644 index a48ec01085..0000000000 --- a/src/pocketmine/network/mcpe/protocol/OnScreenTextureAnimationPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class OnScreenTextureAnimationPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::ON_SCREEN_TEXTURE_ANIMATION_PACKET; - - /** @var int */ - public $effectId; - - protected function decodePayload() : void{ - $this->effectId = $this->getLInt(); //unsigned - } - - protected function encodePayload() : void{ - $this->putLInt($this->effectId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleOnScreenTextureAnimation($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PacketPool.php b/src/pocketmine/network/mcpe/protocol/PacketPool.php deleted file mode 100644 index 0ea0ff53aa..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PacketPool.php +++ /dev/null @@ -1,232 +0,0 @@ - */ - protected static $pool; - - /** - * @return void - */ - public static function init(){ - static::$pool = new \SplFixedArray(256); - - static::registerPacket(new LoginPacket()); - static::registerPacket(new PlayStatusPacket()); - static::registerPacket(new ServerToClientHandshakePacket()); - static::registerPacket(new ClientToServerHandshakePacket()); - static::registerPacket(new DisconnectPacket()); - static::registerPacket(new ResourcePacksInfoPacket()); - static::registerPacket(new ResourcePackStackPacket()); - static::registerPacket(new ResourcePackClientResponsePacket()); - static::registerPacket(new TextPacket()); - static::registerPacket(new SetTimePacket()); - static::registerPacket(new StartGamePacket()); - static::registerPacket(new AddPlayerPacket()); - static::registerPacket(new AddActorPacket()); - static::registerPacket(new RemoveActorPacket()); - static::registerPacket(new AddItemActorPacket()); - static::registerPacket(new TakeItemActorPacket()); - static::registerPacket(new MoveActorAbsolutePacket()); - static::registerPacket(new MovePlayerPacket()); - static::registerPacket(new PassengerJumpPacket()); - static::registerPacket(new UpdateBlockPacket()); - static::registerPacket(new AddPaintingPacket()); - static::registerPacket(new TickSyncPacket()); - static::registerPacket(new LevelSoundEventPacketV1()); - static::registerPacket(new LevelEventPacket()); - static::registerPacket(new BlockEventPacket()); - static::registerPacket(new ActorEventPacket()); - static::registerPacket(new MobEffectPacket()); - static::registerPacket(new UpdateAttributesPacket()); - static::registerPacket(new InventoryTransactionPacket()); - static::registerPacket(new MobEquipmentPacket()); - static::registerPacket(new MobArmorEquipmentPacket()); - static::registerPacket(new InteractPacket()); - static::registerPacket(new BlockPickRequestPacket()); - static::registerPacket(new ActorPickRequestPacket()); - static::registerPacket(new PlayerActionPacket()); - static::registerPacket(new HurtArmorPacket()); - static::registerPacket(new SetActorDataPacket()); - static::registerPacket(new SetActorMotionPacket()); - static::registerPacket(new SetActorLinkPacket()); - static::registerPacket(new SetHealthPacket()); - static::registerPacket(new SetSpawnPositionPacket()); - static::registerPacket(new AnimatePacket()); - static::registerPacket(new RespawnPacket()); - static::registerPacket(new ContainerOpenPacket()); - static::registerPacket(new ContainerClosePacket()); - static::registerPacket(new PlayerHotbarPacket()); - static::registerPacket(new InventoryContentPacket()); - static::registerPacket(new InventorySlotPacket()); - static::registerPacket(new ContainerSetDataPacket()); - static::registerPacket(new CraftingDataPacket()); - static::registerPacket(new CraftingEventPacket()); - static::registerPacket(new GuiDataPickItemPacket()); - static::registerPacket(new AdventureSettingsPacket()); - static::registerPacket(new BlockActorDataPacket()); - static::registerPacket(new PlayerInputPacket()); - static::registerPacket(new LevelChunkPacket()); - static::registerPacket(new SetCommandsEnabledPacket()); - static::registerPacket(new SetDifficultyPacket()); - static::registerPacket(new ChangeDimensionPacket()); - static::registerPacket(new SetPlayerGameTypePacket()); - static::registerPacket(new PlayerListPacket()); - static::registerPacket(new SimpleEventPacket()); - static::registerPacket(new EventPacket()); - static::registerPacket(new SpawnExperienceOrbPacket()); - static::registerPacket(new ClientboundMapItemDataPacket()); - static::registerPacket(new MapInfoRequestPacket()); - static::registerPacket(new RequestChunkRadiusPacket()); - static::registerPacket(new ChunkRadiusUpdatedPacket()); - static::registerPacket(new ItemFrameDropItemPacket()); - static::registerPacket(new GameRulesChangedPacket()); - static::registerPacket(new CameraPacket()); - static::registerPacket(new BossEventPacket()); - static::registerPacket(new ShowCreditsPacket()); - static::registerPacket(new AvailableCommandsPacket()); - static::registerPacket(new CommandRequestPacket()); - static::registerPacket(new CommandBlockUpdatePacket()); - static::registerPacket(new CommandOutputPacket()); - static::registerPacket(new UpdateTradePacket()); - static::registerPacket(new UpdateEquipPacket()); - static::registerPacket(new ResourcePackDataInfoPacket()); - static::registerPacket(new ResourcePackChunkDataPacket()); - static::registerPacket(new ResourcePackChunkRequestPacket()); - static::registerPacket(new TransferPacket()); - static::registerPacket(new PlaySoundPacket()); - static::registerPacket(new StopSoundPacket()); - static::registerPacket(new SetTitlePacket()); - static::registerPacket(new AddBehaviorTreePacket()); - static::registerPacket(new StructureBlockUpdatePacket()); - static::registerPacket(new ShowStoreOfferPacket()); - static::registerPacket(new PurchaseReceiptPacket()); - static::registerPacket(new PlayerSkinPacket()); - static::registerPacket(new SubClientLoginPacket()); - static::registerPacket(new AutomationClientConnectPacket()); - static::registerPacket(new SetLastHurtByPacket()); - static::registerPacket(new BookEditPacket()); - static::registerPacket(new NpcRequestPacket()); - static::registerPacket(new PhotoTransferPacket()); - static::registerPacket(new ModalFormRequestPacket()); - static::registerPacket(new ModalFormResponsePacket()); - static::registerPacket(new ServerSettingsRequestPacket()); - static::registerPacket(new ServerSettingsResponsePacket()); - static::registerPacket(new ShowProfilePacket()); - static::registerPacket(new SetDefaultGameTypePacket()); - static::registerPacket(new RemoveObjectivePacket()); - static::registerPacket(new SetDisplayObjectivePacket()); - static::registerPacket(new SetScorePacket()); - static::registerPacket(new LabTablePacket()); - static::registerPacket(new UpdateBlockSyncedPacket()); - static::registerPacket(new MoveActorDeltaPacket()); - static::registerPacket(new SetScoreboardIdentityPacket()); - static::registerPacket(new SetLocalPlayerAsInitializedPacket()); - static::registerPacket(new UpdateSoftEnumPacket()); - static::registerPacket(new NetworkStackLatencyPacket()); - static::registerPacket(new ScriptCustomEventPacket()); - static::registerPacket(new SpawnParticleEffectPacket()); - static::registerPacket(new AvailableActorIdentifiersPacket()); - static::registerPacket(new LevelSoundEventPacketV2()); - static::registerPacket(new NetworkChunkPublisherUpdatePacket()); - static::registerPacket(new BiomeDefinitionListPacket()); - static::registerPacket(new LevelSoundEventPacket()); - static::registerPacket(new LevelEventGenericPacket()); - static::registerPacket(new LecternUpdatePacket()); - static::registerPacket(new AddEntityPacket()); - static::registerPacket(new RemoveEntityPacket()); - static::registerPacket(new ClientCacheStatusPacket()); - static::registerPacket(new OnScreenTextureAnimationPacket()); - static::registerPacket(new MapCreateLockedCopyPacket()); - static::registerPacket(new StructureTemplateDataRequestPacket()); - static::registerPacket(new StructureTemplateDataResponsePacket()); - static::registerPacket(new ClientCacheBlobStatusPacket()); - static::registerPacket(new ClientCacheMissResponsePacket()); - static::registerPacket(new EducationSettingsPacket()); - static::registerPacket(new EmotePacket()); - static::registerPacket(new MultiplayerSettingsPacket()); - static::registerPacket(new SettingsCommandPacket()); - static::registerPacket(new AnvilDamagePacket()); - static::registerPacket(new CompletedUsingItemPacket()); - static::registerPacket(new NetworkSettingsPacket()); - static::registerPacket(new PlayerAuthInputPacket()); - static::registerPacket(new CreativeContentPacket()); - static::registerPacket(new PlayerEnchantOptionsPacket()); - static::registerPacket(new ItemStackRequestPacket()); - static::registerPacket(new ItemStackResponsePacket()); - static::registerPacket(new PlayerArmorDamagePacket()); - static::registerPacket(new CodeBuilderPacket()); - static::registerPacket(new UpdatePlayerGameTypePacket()); - static::registerPacket(new EmoteListPacket()); - static::registerPacket(new PositionTrackingDBServerBroadcastPacket()); - static::registerPacket(new PositionTrackingDBClientRequestPacket()); - static::registerPacket(new DebugInfoPacket()); - static::registerPacket(new PacketViolationWarningPacket()); - static::registerPacket(new MotionPredictionHintsPacket()); - static::registerPacket(new AnimateEntityPacket()); - static::registerPacket(new CameraShakePacket()); - static::registerPacket(new PlayerFogPacket()); - static::registerPacket(new CorrectPlayerMovePredictionPacket()); - static::registerPacket(new ItemComponentPacket()); - static::registerPacket(new FilterTextPacket()); - static::registerPacket(new ClientboundDebugRendererPacket()); - static::registerPacket(new SyncActorPropertyPacket()); - static::registerPacket(new AddVolumeEntityPacket()); - static::registerPacket(new RemoveVolumeEntityPacket()); - static::registerPacket(new SimulationTypePacket()); - static::registerPacket(new NpcDialoguePacket()); - static::registerPacket(new EduUriResourcePacket()); - static::registerPacket(new CreatePhotoPacket()); - static::registerPacket(new UpdateSubChunkBlocksPacket()); - static::registerPacket(new PhotoInfoRequestPacket()); - static::registerPacket(new SubChunkPacket()); - static::registerPacket(new SubChunkRequestPacket()); - } - - /** - * @return void - */ - public static function registerPacket(DataPacket $packet){ - static::$pool[$packet->pid()] = clone $packet; - } - - public static function getPacketById(int $pid) : DataPacket{ - return isset(static::$pool[$pid]) ? clone static::$pool[$pid] : new UnknownPacket(); - } - - /** - * @throws BinaryDataException - */ - public static function getPacket(string $buffer) : DataPacket{ - $offset = 0; - $pk = static::getPacketById(Binary::readUnsignedVarInt($buffer, $offset) & DataPacket::PID_MASK); - $pk->setBuffer($buffer, $offset); - - return $pk; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PacketViolationWarningPacket.php b/src/pocketmine/network/mcpe/protocol/PacketViolationWarningPacket.php deleted file mode 100644 index 51167bc0c4..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PacketViolationWarningPacket.php +++ /dev/null @@ -1,84 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PacketViolationWarningPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::PACKET_VIOLATION_WARNING_PACKET; - - public const TYPE_MALFORMED = 0; - - public const SEVERITY_WARNING = 0; - public const SEVERITY_FINAL_WARNING = 1; - public const SEVERITY_TERMINATING_CONNECTION = 2; - - /** @var int */ - private $type; - /** @var int */ - private $severity; - /** @var int */ - private $packetId; - /** @var string */ - private $message; - - public static function create(int $type, int $severity, int $packetId, string $message) : self{ - $result = new self; - - $result->type = $type; - $result->severity = $severity; - $result->packetId = $packetId; - $result->message = $message; - - return $result; - } - - public function getType() : int{ return $this->type; } - - public function getSeverity() : int{ return $this->severity; } - - public function getPacketId() : int{ return $this->packetId; } - - public function getMessage() : string{ return $this->message; } - - protected function decodePayload() : void{ - $this->type = $this->getVarInt(); - $this->severity = $this->getVarInt(); - $this->packetId = $this->getVarInt(); - $this->message = $this->getString(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->type); - $this->putVarInt($this->severity); - $this->putVarInt($this->packetId); - $this->putString($this->message); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePacketViolationWarning($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PassengerJumpPacket.php b/src/pocketmine/network/mcpe/protocol/PassengerJumpPacket.php deleted file mode 100644 index 11b286c99b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PassengerJumpPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PassengerJumpPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PASSENGER_JUMP_PACKET; - - /** @var int */ - public $jumpStrength; //percentage - - protected function decodePayload(){ - $this->jumpStrength = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->jumpStrength); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePassengerJump($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PhotoInfoRequestPacket.php b/src/pocketmine/network/mcpe/protocol/PhotoInfoRequestPacket.php deleted file mode 100644 index a13783172f..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PhotoInfoRequestPacket.php +++ /dev/null @@ -1,52 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PhotoInfoRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PHOTO_INFO_REQUEST_PACKET; - - private int $photoId; - - public static function create(int $photoId) : self{ - $result = new self; - $result->photoId = $photoId; - return $result; - } - - protected function decodePayload() : void{ - $this->photoId = $this->getEntityUniqueId(); - } - - protected function encodePayload() : void{ - $this->putEntityUniqueId($this->photoId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePhotoInfoRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PhotoTransferPacket.php b/src/pocketmine/network/mcpe/protocol/PhotoTransferPacket.php deleted file mode 100644 index 1d3194b9e4..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PhotoTransferPacket.php +++ /dev/null @@ -1,71 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PhotoTransferPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PHOTO_TRANSFER_PACKET; - - /** @var string */ - public $photoName; - /** @var string */ - public $photoData; - /** @var string */ - public $bookId; //photos are stored in a sibling directory to the games folder (screenshots/(some UUID)/bookID/example.png) - /** @var int */ - public $type; - /** @var int */ - public $sourceType; - /** @var int */ - public $ownerEntityUniqueId; - /** @var string */ - public $newPhotoName; //??? - - protected function decodePayload(){ - $this->photoName = $this->getString(); - $this->photoData = $this->getString(); - $this->bookId = $this->getString(); - $this->type = $this->getByte(); - $this->sourceType = $this->getByte(); - $this->ownerEntityUniqueId = $this->getLLong(); //............... - $this->newPhotoName = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->photoName); - $this->putString($this->photoData); - $this->putString($this->bookId); - $this->putByte($this->type); - $this->putByte($this->sourceType); - $this->putLLong($this->ownerEntityUniqueId); - $this->putString($this->newPhotoName); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePhotoTransfer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlaySoundPacket.php b/src/pocketmine/network/mcpe/protocol/PlaySoundPacket.php deleted file mode 100644 index 94ef54ee79..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlaySoundPacket.php +++ /dev/null @@ -1,66 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PlaySoundPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAY_SOUND_PACKET; - - /** @var string */ - public $soundName; - /** @var float */ - public $x; - /** @var float */ - public $y; - /** @var float */ - public $z; - /** @var float */ - public $volume; - /** @var float */ - public $pitch; - - protected function decodePayload(){ - $this->soundName = $this->getString(); - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->x /= 8; - $this->y /= 8; - $this->z /= 8; - $this->volume = $this->getLFloat(); - $this->pitch = $this->getLFloat(); - } - - protected function encodePayload(){ - $this->putString($this->soundName); - $this->putBlockPosition((int) ($this->x * 8), (int) ($this->y * 8), (int) ($this->z * 8)); - $this->putLFloat($this->volume); - $this->putLFloat($this->pitch); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlaySound($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayStatusPacket.php b/src/pocketmine/network/mcpe/protocol/PlayStatusPacket.php deleted file mode 100644 index 1c81f5049b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayStatusPacket.php +++ /dev/null @@ -1,60 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PlayStatusPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAY_STATUS_PACKET; - - public const LOGIN_SUCCESS = 0; - public const LOGIN_FAILED_CLIENT = 1; - public const LOGIN_FAILED_SERVER = 2; - public const PLAYER_SPAWN = 3; - public const LOGIN_FAILED_INVALID_TENANT = 4; - public const LOGIN_FAILED_VANILLA_EDU = 5; - public const LOGIN_FAILED_EDU_VANILLA = 6; - public const LOGIN_FAILED_SERVER_FULL = 7; - - /** @var int */ - public $status; - - protected function decodePayload(){ - $this->status = $this->getInt(); - } - - public function canBeSentBeforeLogin() : bool{ - return true; - } - - protected function encodePayload(){ - $this->putInt($this->status); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayStatus($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerActionPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerActionPacket.php deleted file mode 100644 index 394a7232d6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerActionPacket.php +++ /dev/null @@ -1,92 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PlayerActionPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAYER_ACTION_PACKET; - - public const ACTION_START_BREAK = 0; - public const ACTION_ABORT_BREAK = 1; - public const ACTION_STOP_BREAK = 2; - public const ACTION_GET_UPDATED_BLOCK = 3; - public const ACTION_DROP_ITEM = 4; - public const ACTION_START_SLEEPING = 5; - public const ACTION_STOP_SLEEPING = 6; - public const ACTION_RESPAWN = 7; - public const ACTION_JUMP = 8; - public const ACTION_START_SPRINT = 9; - public const ACTION_STOP_SPRINT = 10; - public const ACTION_START_SNEAK = 11; - public const ACTION_STOP_SNEAK = 12; - public const ACTION_CREATIVE_PLAYER_DESTROY_BLOCK = 13; - public const ACTION_DIMENSION_CHANGE_ACK = 14; //sent when spawning in a different dimension to tell the server we spawned - public const ACTION_START_GLIDE = 15; - public const ACTION_STOP_GLIDE = 16; - public const ACTION_BUILD_DENIED = 17; - public const ACTION_CRACK_BREAK = 18; - public const ACTION_CHANGE_SKIN = 19; - public const ACTION_SET_ENCHANTMENT_SEED = 20; //no longer used - public const ACTION_START_SWIMMING = 21; - public const ACTION_STOP_SWIMMING = 22; - public const ACTION_START_SPIN_ATTACK = 23; - public const ACTION_STOP_SPIN_ATTACK = 24; - public const ACTION_INTERACT_BLOCK = 25; - public const ACTION_PREDICT_DESTROY_BLOCK = 26; - public const ACTION_CONTINUE_DESTROY_BLOCK = 27; - - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $action; - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $face; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->action = $this->getVarInt(); - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->face = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVarInt($this->action); - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putVarInt($this->face); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayerAction($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerArmorDamagePacket.php b/src/pocketmine/network/mcpe/protocol/PlayerArmorDamagePacket.php deleted file mode 100644 index 4554c16669..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerArmorDamagePacket.php +++ /dev/null @@ -1,108 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PlayerArmorDamagePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::PLAYER_ARMOR_DAMAGE_PACKET; - - private const FLAG_HEAD = 0; - private const FLAG_CHEST = 1; - private const FLAG_LEGS = 2; - private const FLAG_FEET = 3; - - /** @var int|null */ - private $headSlotDamage; - /** @var int|null */ - private $chestSlotDamage; - /** @var int|null */ - private $legsSlotDamage; - /** @var int|null */ - private $feetSlotDamage; - - public static function create(?int $headSlotDamage, ?int $chestSlotDamage, ?int $legsSlotDamage, ?int $feetSlotDamage) : self{ - $result = new self; - $result->headSlotDamage = $headSlotDamage; - $result->chestSlotDamage = $chestSlotDamage; - $result->legsSlotDamage = $legsSlotDamage; - $result->feetSlotDamage = $feetSlotDamage; - - return $result; - } - - public function getHeadSlotDamage() : ?int{ return $this->headSlotDamage; } - - public function getChestSlotDamage() : ?int{ return $this->chestSlotDamage; } - - public function getLegsSlotDamage() : ?int{ return $this->legsSlotDamage; } - - public function getFeetSlotDamage() : ?int{ return $this->feetSlotDamage; } - - private function maybeReadDamage(int $flags, int $flag) : ?int{ - if(($flags & (1 << $flag)) !== 0){ - return $this->getVarInt(); - } - return null; - } - - protected function decodePayload() : void{ - $flags = $this->getByte(); - - $this->headSlotDamage = $this->maybeReadDamage($flags, self::FLAG_HEAD); - $this->chestSlotDamage = $this->maybeReadDamage($flags, self::FLAG_CHEST); - $this->legsSlotDamage = $this->maybeReadDamage($flags, self::FLAG_LEGS); - $this->feetSlotDamage = $this->maybeReadDamage($flags, self::FLAG_FEET); - } - - private function composeFlag(?int $field, int $flag) : int{ - return $field !== null ? (1 << $flag) : 0; - } - - private function maybeWriteDamage(?int $field) : void{ - if($field !== null){ - $this->putVarInt($field); - } - } - - protected function encodePayload() : void{ - $this->putByte( - $this->composeFlag($this->headSlotDamage, self::FLAG_HEAD) | - $this->composeFlag($this->chestSlotDamage, self::FLAG_CHEST) | - $this->composeFlag($this->legsSlotDamage, self::FLAG_LEGS) | - $this->composeFlag($this->feetSlotDamage, self::FLAG_FEET) - ); - - $this->maybeWriteDamage($this->headSlotDamage); - $this->maybeWriteDamage($this->chestSlotDamage); - $this->maybeWriteDamage($this->legsSlotDamage); - $this->maybeWriteDamage($this->feetSlotDamage); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePlayerArmorDamage($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerAuthInputPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerAuthInputPacket.php deleted file mode 100644 index 16d8e7c6dc..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerAuthInputPacket.php +++ /dev/null @@ -1,183 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\InputMode; -use pocketmine\network\mcpe\protocol\types\PlayerAuthInputFlags; -use pocketmine\network\mcpe\protocol\types\PlayMode; -use function assert; - -class PlayerAuthInputPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::PLAYER_AUTH_INPUT_PACKET; - - /** @var Vector3 */ - private $position; - /** @var float */ - private $pitch; - /** @var float */ - private $yaw; - /** @var float */ - private $headYaw; - /** @var float */ - private $moveVecX; - /** @var float */ - private $moveVecZ; - /** @var int */ - private $inputFlags; - /** @var int */ - private $inputMode; - /** @var int */ - private $playMode; - /** @var Vector3|null */ - private $vrGazeDirection = null; - /** @var int */ - private $tick; - /** @var Vector3 */ - private $delta; - - /** - * @param int $inputFlags @see InputFlags - * @param int $inputMode @see InputMode - * @param int $playMode @see PlayMode - * @param Vector3|null $vrGazeDirection only used when PlayMode::VR - */ - public static function create(Vector3 $position, float $pitch, float $yaw, float $headYaw, float $moveVecX, float $moveVecZ, int $inputFlags, int $inputMode, int $playMode, ?Vector3 $vrGazeDirection, int $tick, Vector3 $delta) : self{ - if($playMode === PlayMode::VR and $vrGazeDirection === null){ - //yuck, can we get a properly written packet just once? ... - throw new \InvalidArgumentException("Gaze direction must be provided for VR play mode"); - } - $result = new self; - $result->position = $position->asVector3(); - $result->pitch = $pitch; - $result->yaw = $yaw; - $result->headYaw = $headYaw; - $result->moveVecX = $moveVecX; - $result->moveVecZ = $moveVecZ; - $result->inputFlags = $inputFlags; - $result->inputMode = $inputMode; - $result->playMode = $playMode; - if($vrGazeDirection !== null){ - $result->vrGazeDirection = $vrGazeDirection->asVector3(); - } - $result->tick = $tick; - $result->delta = $delta; - return $result; - } - - public function getPosition() : Vector3{ - return $this->position; - } - - public function getPitch() : float{ - return $this->pitch; - } - - public function getYaw() : float{ - return $this->yaw; - } - - public function getHeadYaw() : float{ - return $this->headYaw; - } - - public function getMoveVecX() : float{ - return $this->moveVecX; - } - - public function getMoveVecZ() : float{ - return $this->moveVecZ; - } - - /** - * @see PlayerAuthInputFlags - */ - public function getInputFlags() : int{ - return $this->inputFlags; - } - - /** - * @see InputMode - */ - public function getInputMode() : int{ - return $this->inputMode; - } - - /** - * @see PlayMode - */ - public function getPlayMode() : int{ - return $this->playMode; - } - - public function getVrGazeDirection() : ?Vector3{ - return $this->vrGazeDirection; - } - - public function getTick() : int{ return $this->tick; } - - public function getDelta() : Vector3{ return $this->delta; } - - protected function decodePayload() : void{ - $this->pitch = $this->getLFloat(); - $this->yaw = $this->getLFloat(); - $this->position = $this->getVector3(); - $this->moveVecX = $this->getLFloat(); - $this->moveVecZ = $this->getLFloat(); - $this->headYaw = $this->getLFloat(); - $this->inputFlags = $this->getUnsignedVarLong(); - $this->inputMode = $this->getUnsignedVarInt(); - $this->playMode = $this->getUnsignedVarInt(); - if($this->playMode === PlayMode::VR){ - $this->vrGazeDirection = $this->getVector3(); - } - $this->tick = $this->getUnsignedVarLong(); - $this->delta = $this->getVector3(); - } - - protected function encodePayload() : void{ - $this->putLFloat($this->pitch); - $this->putLFloat($this->yaw); - $this->putVector3($this->position); - $this->putLFloat($this->moveVecX); - $this->putLFloat($this->moveVecZ); - $this->putLFloat($this->headYaw); - $this->putUnsignedVarLong($this->inputFlags); - $this->putUnsignedVarInt($this->inputMode); - $this->putUnsignedVarInt($this->playMode); - if($this->playMode === PlayMode::VR){ - assert($this->vrGazeDirection !== null); - $this->putVector3($this->vrGazeDirection); - } - $this->putUnsignedVarLong($this->tick); - $this->putVector3($this->delta); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePlayerAuthInput($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerEnchantOptionsPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerEnchantOptionsPacket.php deleted file mode 100644 index c31f7bb5b6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerEnchantOptionsPacket.php +++ /dev/null @@ -1,69 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\EnchantOption; -use function count; - -class PlayerEnchantOptionsPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::PLAYER_ENCHANT_OPTIONS_PACKET; - - /** @var EnchantOption[] */ - private $options; - - /** - * @param EnchantOption[] $options - */ - public static function create(array $options) : self{ - $result = new self; - $result->options = $options; - return $result; - } - - /** - * @return EnchantOption[] - */ - public function getOptions() : array{ return $this->options; } - - protected function decodePayload() : void{ - $this->options = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->options[] = EnchantOption::read($this); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->options)); - foreach($this->options as $option){ - $option->write($this); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePlayerEnchantOptions($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerFogPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerFogPacket.php deleted file mode 100644 index 19f0b0fb2c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerFogPacket.php +++ /dev/null @@ -1,73 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class PlayerFogPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::PLAYER_FOG_PACKET; - - /** - * @var string[] - * @phpstan-var list - */ - private $fogLayers; - - /** - * @param string[] $fogLayers - * @phpstan-param list $fogLayers - */ - public static function create(array $fogLayers) : self{ - $result = new self; - $result->fogLayers = $fogLayers; - return $result; - } - - /** - * @return string[] - * @phpstan-return list - */ - public function getFogLayers() : array{ return $this->fogLayers; } - - protected function decodePayload() : void{ - $this->fogLayers = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $this->fogLayers[] = $this->getString(); - } - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt(count($this->fogLayers)); - foreach($this->fogLayers as $fogLayer){ - $this->putString($fogLayer); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePlayerFog($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerHotbarPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerHotbarPacket.php deleted file mode 100644 index 82269f7503..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerHotbarPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ContainerIds; - -/** - * One of the most useless packets. - */ -class PlayerHotbarPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAYER_HOTBAR_PACKET; - - /** @var int */ - public $selectedHotbarSlot; - /** @var int */ - public $windowId = ContainerIds::INVENTORY; - /** @var bool */ - public $selectHotbarSlot = true; - - protected function decodePayload(){ - $this->selectedHotbarSlot = $this->getUnsignedVarInt(); - $this->windowId = $this->getByte(); - $this->selectHotbarSlot = $this->getBool(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->selectedHotbarSlot); - $this->putByte($this->windowId); - $this->putBool($this->selectHotbarSlot); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayerHotbar($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerInputPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerInputPacket.php deleted file mode 100644 index 65274393b2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerInputPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PlayerInputPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAYER_INPUT_PACKET; - - /** @var float */ - public $motionX; - /** @var float */ - public $motionY; - /** @var bool */ - public $jumping; - /** @var bool */ - public $sneaking; - - protected function decodePayload(){ - $this->motionX = $this->getLFloat(); - $this->motionY = $this->getLFloat(); - $this->jumping = $this->getBool(); - $this->sneaking = $this->getBool(); - } - - protected function encodePayload(){ - $this->putLFloat($this->motionX); - $this->putLFloat($this->motionY); - $this->putBool($this->jumping); - $this->putBool($this->sneaking); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayerInput($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php deleted file mode 100644 index 44646bf52d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerListPacket.php +++ /dev/null @@ -1,105 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\PlayerListEntry; -use function count; - -class PlayerListPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAYER_LIST_PACKET; - - public const TYPE_ADD = 0; - public const TYPE_REMOVE = 1; - - /** @var PlayerListEntry[] */ - public $entries = []; - /** @var int */ - public $type; - - public function clean(){ - $this->entries = []; - return parent::clean(); - } - - protected function decodePayload(){ - $this->type = $this->getByte(); - $count = $this->getUnsignedVarInt(); - for($i = 0; $i < $count; ++$i){ - $entry = new PlayerListEntry(); - - if($this->type === self::TYPE_ADD){ - $entry->uuid = $this->getUUID(); - $entry->entityUniqueId = $this->getEntityUniqueId(); - $entry->username = $this->getString(); - $entry->xboxUserId = $this->getString(); - $entry->platformChatId = $this->getString(); - $entry->buildPlatform = $this->getLInt(); - $entry->skinData = $this->getSkin(); - $entry->isTeacher = $this->getBool(); - $entry->isHost = $this->getBool(); - }else{ - $entry->uuid = $this->getUUID(); - } - - $this->entries[$i] = $entry; - } - if($this->type === self::TYPE_ADD){ - for($i = 0; $i < $count; ++$i){ - $this->entries[$i]->skinData->setVerified($this->getBool()); - } - } - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - if($this->type === self::TYPE_ADD){ - $this->putUUID($entry->uuid); - $this->putEntityUniqueId($entry->entityUniqueId); - $this->putString($entry->username); - $this->putString($entry->xboxUserId); - $this->putString($entry->platformChatId); - $this->putLInt($entry->buildPlatform); - $this->putSkin($entry->skinData); - $this->putBool($entry->isTeacher); - $this->putBool($entry->isHost); - }else{ - $this->putUUID($entry->uuid); - } - } - if($this->type === self::TYPE_ADD){ - foreach($this->entries as $entry){ - $this->putBool($entry->skinData->isVerified()); - } - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayerList($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php b/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php deleted file mode 100644 index 661f73e033..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PlayerSkinPacket.php +++ /dev/null @@ -1,63 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\SkinData; -use pocketmine\utils\UUID; - -class PlayerSkinPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PLAYER_SKIN_PACKET; - - /** @var UUID */ - public $uuid; - /** @var string */ - public $oldSkinName = ""; - /** @var string */ - public $newSkinName = ""; - /** @var SkinData */ - public $skin; - - protected function decodePayload(){ - $this->uuid = $this->getUUID(); - $this->skin = $this->getSkin(); - $this->newSkinName = $this->getString(); - $this->oldSkinName = $this->getString(); - $this->skin->setVerified($this->getBool()); - } - - protected function encodePayload(){ - $this->putUUID($this->uuid); - $this->putSkin($this->skin); - $this->putString($this->newSkinName); - $this->putString($this->oldSkinName); - $this->putBool($this->skin->isVerified()); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePlayerSkin($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PositionTrackingDBClientRequestPacket.php b/src/pocketmine/network/mcpe/protocol/PositionTrackingDBClientRequestPacket.php deleted file mode 100644 index 2a83e0eb17..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PositionTrackingDBClientRequestPacket.php +++ /dev/null @@ -1,64 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class PositionTrackingDBClientRequestPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::POSITION_TRACKING_D_B_CLIENT_REQUEST_PACKET; - - public const ACTION_QUERY = 0; - - /** @var int */ - private $action; - /** @var int */ - private $trackingId; - - public static function create(int $action, int $trackingId) : self{ - $result = new self; - $result->action = $action; - $result->trackingId = $trackingId; - return $result; - } - - public function getAction() : int{ return $this->action; } - - public function getTrackingId() : int{ return $this->trackingId; } - - protected function decodePayload() : void{ - $this->action = $this->getByte(); - $this->trackingId = $this->getVarInt(); - } - - protected function encodePayload() : void{ - $this->putByte($this->action); - $this->putVarInt($this->trackingId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePositionTrackingDBClientRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/PositionTrackingDBServerBroadcastPacket.php b/src/pocketmine/network/mcpe/protocol/PositionTrackingDBServerBroadcastPacket.php deleted file mode 100644 index 2131a8001c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/PositionTrackingDBServerBroadcastPacket.php +++ /dev/null @@ -1,81 +0,0 @@ - - -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\NetworkSession; - -class PositionTrackingDBServerBroadcastPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::POSITION_TRACKING_D_B_SERVER_BROADCAST_PACKET; - - public const ACTION_UPDATE = 0; - public const ACTION_DESTROY = 1; - public const ACTION_NOT_FOUND = 2; - - /** @var int */ - private $action; - /** @var int */ - private $trackingId; - /** @var CompoundTag */ - private $nbt; - - public static function create(int $action, int $trackingId, CompoundTag $nbt) : self{ - $result = new self; - $result->action = $action; - $result->trackingId = $trackingId; - $result->nbt = $nbt; - return $result; - } - - public function getAction() : int{ return $this->action; } - - public function getTrackingId() : int{ return $this->trackingId; } - - public function getNbt() : CompoundTag{ return $this->nbt; } - - protected function decodePayload() : void{ - $this->action = $this->getByte(); - $this->trackingId = $this->getVarInt(); - $offset = $this->getOffset(); - $nbt = (new NetworkLittleEndianNBTStream())->read($this->getBuffer(), false, $offset); - $this->setOffset($offset); - if(!($nbt instanceof CompoundTag)){ - throw new \UnexpectedValueException("Expected TAG_Compound"); - } - $this->nbt = $nbt; - } - - protected function encodePayload() : void{ - $this->putByte($this->action); - $this->putVarInt($this->trackingId); - $this->put((new NetworkLittleEndianNBTStream())->write($this->nbt)); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handlePositionTrackingDBServerBroadcast($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ProtocolInfo.php b/src/pocketmine/network/mcpe/protocol/ProtocolInfo.php deleted file mode 100644 index e43718e020..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ProtocolInfo.php +++ /dev/null @@ -1,222 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class PurchaseReceiptPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::PURCHASE_RECEIPT_PACKET; - - /** @var string[] */ - public $entries = []; - - protected function decodePayload(){ - $count = $this->getUnsignedVarInt(); - for($i = 0; $i < $count; ++$i){ - $this->entries[] = $this->getString(); - } - } - - protected function encodePayload(){ - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - $this->putString($entry); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handlePurchaseReceipt($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RemoveActorPacket.php b/src/pocketmine/network/mcpe/protocol/RemoveActorPacket.php deleted file mode 100644 index d888106bcb..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RemoveActorPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class RemoveActorPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::REMOVE_ACTOR_PACKET; - - /** @var int */ - public $entityUniqueId; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getEntityUniqueId(); - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->entityUniqueId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleRemoveActor($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RemoveEntityPacket.php b/src/pocketmine/network/mcpe/protocol/RemoveEntityPacket.php deleted file mode 100644 index 9b301c064b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RemoveEntityPacket.php +++ /dev/null @@ -1,57 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class RemoveEntityPacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::REMOVE_ENTITY_PACKET; - - /** @var int */ - private $entityNetId; - - public static function create(int $entityNetId) : self{ - $result = new self; - $result->entityNetId = $entityNetId; - return $result; - } - - public function getEntityNetId() : int{ - return $this->entityNetId; - } - - protected function decodePayload() : void{ - $this->entityNetId = $this->getUnsignedVarInt(); - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt($this->entityNetId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleRemoveEntity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RemoveObjectivePacket.php b/src/pocketmine/network/mcpe/protocol/RemoveObjectivePacket.php deleted file mode 100644 index 6216c43ead..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RemoveObjectivePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class RemoveObjectivePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::REMOVE_OBJECTIVE_PACKET; - - /** @var string */ - public $objectiveName; - - protected function decodePayload(){ - $this->objectiveName = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->objectiveName); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleRemoveObjective($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RemoveVolumeEntityPacket.php b/src/pocketmine/network/mcpe/protocol/RemoveVolumeEntityPacket.php deleted file mode 100644 index 3f50a3a92b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RemoveVolumeEntityPacket.php +++ /dev/null @@ -1,55 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class RemoveVolumeEntityPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::REMOVE_VOLUME_ENTITY_PACKET; - - /** @var int */ - private $entityNetId; - - public static function create(int $entityNetId) : self{ - $result = new self; - $result->entityNetId = $entityNetId; - return $result; - } - - public function getEntityNetId() : int{ return $this->entityNetId; } - - protected function decodePayload() : void{ - $this->entityNetId = $this->getUnsignedVarInt(); - } - - protected function encodePayload() : void{ - $this->putUnsignedVarInt($this->entityNetId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleRemoveVolumeEntity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RequestChunkRadiusPacket.php b/src/pocketmine/network/mcpe/protocol/RequestChunkRadiusPacket.php deleted file mode 100644 index 497b39973c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RequestChunkRadiusPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class RequestChunkRadiusPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::REQUEST_CHUNK_RADIUS_PACKET; - - /** @var int */ - public $radius; - - protected function decodePayload(){ - $this->radius = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->radius); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleRequestChunkRadius($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php deleted file mode 100644 index 0365539be8..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkDataPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ResourcePackChunkDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_CHUNK_DATA_PACKET; - - /** @var string */ - public $packId; - /** @var int */ - public $chunkIndex; - /** @var int */ - public $progress; - /** @var string */ - public $data; - - protected function decodePayload(){ - $this->packId = $this->getString(); - $this->chunkIndex = $this->getLInt(); - $this->progress = $this->getLLong(); - $this->data = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->packId); - $this->putLInt($this->chunkIndex); - $this->putLLong($this->progress); - $this->putString($this->data); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePackChunkData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php deleted file mode 100644 index 88f8313baa..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackChunkRequestPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ResourcePackChunkRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_CHUNK_REQUEST_PACKET; - - /** @var string */ - public $packId; - /** @var int */ - public $chunkIndex; - - protected function decodePayload(){ - $this->packId = $this->getString(); - $this->chunkIndex = $this->getLInt(); - } - - protected function encodePayload(){ - $this->putString($this->packId); - $this->putLInt($this->chunkIndex); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePackChunkRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackClientResponsePacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackClientResponsePacket.php deleted file mode 100644 index 2306545ef9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackClientResponsePacket.php +++ /dev/null @@ -1,64 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class ResourcePackClientResponsePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_CLIENT_RESPONSE_PACKET; - - public const STATUS_REFUSED = 1; - public const STATUS_SEND_PACKS = 2; - public const STATUS_HAVE_ALL_PACKS = 3; - public const STATUS_COMPLETED = 4; - - /** @var int */ - public $status; - /** @var string[] */ - public $packIds = []; - - protected function decodePayload(){ - $this->status = $this->getByte(); - $entryCount = $this->getLShort(); - $this->packIds = []; - while($entryCount-- > 0){ - $this->packIds[] = $this->getString(); - } - } - - protected function encodePayload(){ - $this->putByte($this->status); - $this->putLShort(count($this->packIds)); - foreach($this->packIds as $id){ - $this->putString($id); - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePackClientResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php deleted file mode 100644 index 2a1ae104ee..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackDataInfoPacket.php +++ /dev/null @@ -1,72 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ResourcePackType; - -class ResourcePackDataInfoPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_DATA_INFO_PACKET; - - /** @var string */ - public $packId; - /** @var int */ - public $maxChunkSize; - /** @var int */ - public $chunkCount; - /** @var int */ - public $compressedPackSize; - /** @var string */ - public $sha256; - /** @var bool */ - public $isPremium = false; - /** @var int */ - public $packType = ResourcePackType::RESOURCES; //TODO: check the values for this - - protected function decodePayload(){ - $this->packId = $this->getString(); - $this->maxChunkSize = $this->getLInt(); - $this->chunkCount = $this->getLInt(); - $this->compressedPackSize = $this->getLLong(); - $this->sha256 = $this->getString(); - $this->isPremium = $this->getBool(); - $this->packType = $this->getByte(); - } - - protected function encodePayload(){ - $this->putString($this->packId); - $this->putLInt($this->maxChunkSize); - $this->putLInt($this->chunkCount); - $this->putLLong($this->compressedPackSize); - $this->putString($this->sha256); - $this->putBool($this->isPremium); - $this->putByte($this->packType); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePackDataInfo($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php deleted file mode 100644 index 58a676aeef..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePackStackPacket.php +++ /dev/null @@ -1,94 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\Experiments; -use pocketmine\resourcepacks\ResourcePack; -use function count; - -class ResourcePackStackPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACK_STACK_PACKET; - - /** @var bool */ - public $mustAccept = false; - - /** @var ResourcePack[] */ - public $behaviorPackStack = []; - /** @var ResourcePack[] */ - public $resourcePackStack = []; - - /** @var string */ - public $baseGameVersion = ProtocolInfo::MINECRAFT_VERSION_NETWORK; - - /** @var Experiments */ - public $experiments; - - protected function decodePayload(){ - $this->mustAccept = $this->getBool(); - $behaviorPackCount = $this->getUnsignedVarInt(); - while($behaviorPackCount-- > 0){ - $this->getString(); - $this->getString(); - $this->getString(); - } - - $resourcePackCount = $this->getUnsignedVarInt(); - while($resourcePackCount-- > 0){ - $this->getString(); - $this->getString(); - $this->getString(); - } - - $this->baseGameVersion = $this->getString(); - $this->experiments = Experiments::read($this); - } - - protected function encodePayload(){ - $this->putBool($this->mustAccept); - - $this->putUnsignedVarInt(count($this->behaviorPackStack)); - foreach($this->behaviorPackStack as $entry){ - $this->putString($entry->getPackId()); - $this->putString($entry->getPackVersion()); - $this->putString(""); //TODO: subpack name - } - - $this->putUnsignedVarInt(count($this->resourcePackStack)); - foreach($this->resourcePackStack as $entry){ - $this->putString($entry->getPackId()); - $this->putString($entry->getPackVersion()); - $this->putString(""); //TODO: subpack name - } - - $this->putString($this->baseGameVersion); - $this->experiments->write($this); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePackStack($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php b/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php deleted file mode 100644 index c795dd7ba9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ResourcePacksInfoPacket.php +++ /dev/null @@ -1,104 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\resourcepacks\ResourcePack; -use function count; - -class ResourcePacksInfoPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESOURCE_PACKS_INFO_PACKET; - - /** @var bool */ - public $mustAccept = false; //if true, forces client to choose between accepting packs or being disconnected - /** @var bool */ - public $hasScripts = false; //if true, causes disconnect for any platform that doesn't support scripts yet - - public bool $forceServerPacks = false; - /** @var ResourcePack[] */ - public $behaviorPackEntries = []; - /** @var ResourcePack[] */ - public $resourcePackEntries = []; - - protected function decodePayload(){ - $this->mustAccept = $this->getBool(); - $this->hasScripts = $this->getBool(); - $this->forceServerPacks = $this->getBool(); - $behaviorPackCount = $this->getLShort(); - while($behaviorPackCount-- > 0){ - $this->getString(); - $this->getString(); - $this->getLLong(); - $this->getString(); - $this->getString(); - $this->getString(); - $this->getBool(); - } - - $resourcePackCount = $this->getLShort(); - while($resourcePackCount-- > 0){ - $this->getString(); - $this->getString(); - $this->getLLong(); - $this->getString(); - $this->getString(); - $this->getString(); - $this->getBool(); - $this->getBool(); - } - } - - protected function encodePayload(){ - $this->putBool($this->mustAccept); - $this->putBool($this->hasScripts); - $this->putBool($this->forceServerPacks); - $this->putLShort(count($this->behaviorPackEntries)); - foreach($this->behaviorPackEntries as $entry){ - $this->putString($entry->getPackId()); - $this->putString($entry->getPackVersion()); - $this->putLLong($entry->getPackSize()); - $this->putString(""); //TODO: encryption key - $this->putString(""); //TODO: subpack name - $this->putString(""); //TODO: content identity - $this->putBool(false); //TODO: has scripts (?) - } - $this->putLShort(count($this->resourcePackEntries)); - foreach($this->resourcePackEntries as $entry){ - $this->putString($entry->getPackId()); - $this->putString($entry->getPackVersion()); - $this->putLLong($entry->getPackSize()); - $this->putString(""); //TODO: encryption key - $this->putString(""); //TODO: subpack name - $this->putString(""); //TODO: content identity - $this->putBool(false); //TODO: seems useless for resource packs - $this->putBool(false); //TODO: supports RTX - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleResourcePacksInfo($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/RespawnPacket.php b/src/pocketmine/network/mcpe/protocol/RespawnPacket.php deleted file mode 100644 index 58a52bf2ff..0000000000 --- a/src/pocketmine/network/mcpe/protocol/RespawnPacket.php +++ /dev/null @@ -1,60 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class RespawnPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::RESPAWN_PACKET; - - public const SEARCHING_FOR_SPAWN = 0; - public const READY_TO_SPAWN = 1; - public const CLIENT_READY_TO_SPAWN = 2; - - /** @var Vector3 */ - public $position; - /** @var int */ - public $respawnState = self::SEARCHING_FOR_SPAWN; - /** @var int */ - public $entityRuntimeId; - - protected function decodePayload(){ - $this->position = $this->getVector3(); - $this->respawnState = $this->getByte(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - } - - protected function encodePayload(){ - $this->putVector3($this->position); - $this->putByte($this->respawnState); - $this->putEntityRuntimeId($this->entityRuntimeId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleRespawn($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ScriptCustomEventPacket.php b/src/pocketmine/network/mcpe/protocol/ScriptCustomEventPacket.php deleted file mode 100644 index 78e9df9051..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ScriptCustomEventPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ScriptCustomEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SCRIPT_CUSTOM_EVENT_PACKET; - - /** @var string */ - public $eventName; - /** @var string json data */ - public $eventData; - - protected function decodePayload(){ - $this->eventName = $this->getString(); - $this->eventData = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->eventName); - $this->putString($this->eventData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleScriptCustomEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ServerSettingsRequestPacket.php b/src/pocketmine/network/mcpe/protocol/ServerSettingsRequestPacket.php deleted file mode 100644 index 5404151cba..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ServerSettingsRequestPacket.php +++ /dev/null @@ -1,44 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ServerSettingsRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SERVER_SETTINGS_REQUEST_PACKET; - - protected function decodePayload(){ - //No payload - } - - protected function encodePayload(){ - //No payload - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleServerSettingsRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ServerSettingsResponsePacket.php b/src/pocketmine/network/mcpe/protocol/ServerSettingsResponsePacket.php deleted file mode 100644 index e8f997901d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ServerSettingsResponsePacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ServerSettingsResponsePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SERVER_SETTINGS_RESPONSE_PACKET; - - /** @var int */ - public $formId; - /** @var string */ - public $formData; //json - - protected function decodePayload(){ - $this->formId = $this->getUnsignedVarInt(); - $this->formData = $this->getString(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->formId); - $this->putString($this->formData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleServerSettingsResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetActorDataPacket.php b/src/pocketmine/network/mcpe/protocol/SetActorDataPacket.php deleted file mode 100644 index 0e05562dd6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetActorDataPacket.php +++ /dev/null @@ -1,59 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetActorDataPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_ACTOR_DATA_PACKET; - - /** @var int */ - public $entityRuntimeId; - /** - * @var mixed[][] - * @phpstan-var array - */ - public $metadata; - - /** @var int */ - public $tick = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->metadata = $this->getEntityMetadata(); - $this->tick = $this->getUnsignedVarLong(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putEntityMetadata($this->metadata); - $this->putUnsignedVarLong($this->tick); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetActorData($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetActorLinkPacket.php b/src/pocketmine/network/mcpe/protocol/SetActorLinkPacket.php deleted file mode 100644 index 576d2abb54..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetActorLinkPacket.php +++ /dev/null @@ -1,48 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\EntityLink; - -class SetActorLinkPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_ACTOR_LINK_PACKET; - - /** @var EntityLink */ - public $link; - - protected function decodePayload(){ - $this->link = $this->getEntityLink(); - } - - protected function encodePayload(){ - $this->putEntityLink($this->link); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetActorLink($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetActorMotionPacket.php b/src/pocketmine/network/mcpe/protocol/SetActorMotionPacket.php deleted file mode 100644 index f4e1dcc2c7..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetActorMotionPacket.php +++ /dev/null @@ -1,52 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; - -class SetActorMotionPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_ACTOR_MOTION_PACKET; - - /** @var int */ - public $entityRuntimeId; - /** @var Vector3 */ - public $motion; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->motion = $this->getVector3(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVector3($this->motion); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetActorMotion($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetDefaultGameTypePacket.php b/src/pocketmine/network/mcpe/protocol/SetDefaultGameTypePacket.php deleted file mode 100644 index 3284704b78..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetDefaultGameTypePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetDefaultGameTypePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_DEFAULT_GAME_TYPE_PACKET; - - /** @var int */ - public $gamemode; - - protected function decodePayload(){ - $this->gamemode = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->gamemode); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetDefaultGameType($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetDifficultyPacket.php b/src/pocketmine/network/mcpe/protocol/SetDifficultyPacket.php deleted file mode 100644 index c8354bf1b2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetDifficultyPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetDifficultyPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_DIFFICULTY_PACKET; - - /** @var int */ - public $difficulty; - - protected function decodePayload(){ - $this->difficulty = $this->getUnsignedVarInt(); - } - - protected function encodePayload(){ - $this->putUnsignedVarInt($this->difficulty); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetDifficulty($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetDisplayObjectivePacket.php b/src/pocketmine/network/mcpe/protocol/SetDisplayObjectivePacket.php deleted file mode 100644 index e61e0cd73e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetDisplayObjectivePacket.php +++ /dev/null @@ -1,70 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetDisplayObjectivePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_DISPLAY_OBJECTIVE_PACKET; - - public const DISPLAY_SLOT_LIST = "list"; - public const DISPLAY_SLOT_SIDEBAR = "sidebar"; - public const DISPLAY_SLOT_BELOW_NAME = "belowname"; - - public const SORT_ORDER_ASCENDING = 0; - public const SORT_ORDER_DESCENDING = 1; - - /** @var string */ - public $displaySlot; - /** @var string */ - public $objectiveName; - /** @var string */ - public $displayName; - /** @var string */ - public $criteriaName; - /** @var int */ - public $sortOrder; - - protected function decodePayload(){ - $this->displaySlot = $this->getString(); - $this->objectiveName = $this->getString(); - $this->displayName = $this->getString(); - $this->criteriaName = $this->getString(); - $this->sortOrder = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putString($this->displaySlot); - $this->putString($this->objectiveName); - $this->putString($this->displayName); - $this->putString($this->criteriaName); - $this->putVarInt($this->sortOrder); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetDisplayObjective($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetHealthPacket.php b/src/pocketmine/network/mcpe/protocol/SetHealthPacket.php deleted file mode 100644 index 5daca24a39..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetHealthPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetHealthPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_HEALTH_PACKET; - - /** @var int */ - public $health; - - protected function decodePayload(){ - $this->health = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->health); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetHealth($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetLastHurtByPacket.php b/src/pocketmine/network/mcpe/protocol/SetLastHurtByPacket.php deleted file mode 100644 index b29b9ac2d9..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetLastHurtByPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetLastHurtByPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_LAST_HURT_BY_PACKET; - - /** @var int */ - public $entityTypeId; - - protected function decodePayload(){ - $this->entityTypeId = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->entityTypeId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetLastHurtBy($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetLocalPlayerAsInitializedPacket.php b/src/pocketmine/network/mcpe/protocol/SetLocalPlayerAsInitializedPacket.php deleted file mode 100644 index 12918f114c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetLocalPlayerAsInitializedPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetLocalPlayerAsInitializedPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_LOCAL_PLAYER_AS_INITIALIZED_PACKET; - - /** @var int */ - public $entityRuntimeId; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetLocalPlayerAsInitialized($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetPlayerGameTypePacket.php b/src/pocketmine/network/mcpe/protocol/SetPlayerGameTypePacket.php deleted file mode 100644 index 835c10fd97..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetPlayerGameTypePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetPlayerGameTypePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_PLAYER_GAME_TYPE_PACKET; - - /** @var int */ - public $gamemode; - - protected function decodePayload(){ - $this->gamemode = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->gamemode); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetPlayerGameType($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetScorePacket.php b/src/pocketmine/network/mcpe/protocol/SetScorePacket.php deleted file mode 100644 index 4a557f5829..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetScorePacket.php +++ /dev/null @@ -1,95 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ScorePacketEntry; -use function count; - -class SetScorePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_SCORE_PACKET; - - public const TYPE_CHANGE = 0; - public const TYPE_REMOVE = 1; - - /** @var int */ - public $type; - /** @var ScorePacketEntry[] */ - public $entries = []; - - protected function decodePayload(){ - $this->type = $this->getByte(); - for($i = 0, $i2 = $this->getUnsignedVarInt(); $i < $i2; ++$i){ - $entry = new ScorePacketEntry(); - $entry->scoreboardId = $this->getVarLong(); - $entry->objectiveName = $this->getString(); - $entry->score = $this->getLInt(); - if($this->type !== self::TYPE_REMOVE){ - $entry->type = $this->getByte(); - switch($entry->type){ - case ScorePacketEntry::TYPE_PLAYER: - case ScorePacketEntry::TYPE_ENTITY: - $entry->entityUniqueId = $this->getEntityUniqueId(); - break; - case ScorePacketEntry::TYPE_FAKE_PLAYER: - $entry->customName = $this->getString(); - break; - default: - throw new \UnexpectedValueException("Unknown entry type $entry->type"); - } - } - $this->entries[] = $entry; - } - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - $this->putVarLong($entry->scoreboardId); - $this->putString($entry->objectiveName); - $this->putLInt($entry->score); - if($this->type !== self::TYPE_REMOVE){ - $this->putByte($entry->type); - switch($entry->type){ - case ScorePacketEntry::TYPE_PLAYER: - case ScorePacketEntry::TYPE_ENTITY: - $this->putEntityUniqueId($entry->entityUniqueId); - break; - case ScorePacketEntry::TYPE_FAKE_PLAYER: - $this->putString($entry->customName); - break; - default: - throw new \InvalidArgumentException("Unknown entry type $entry->type"); - } - } - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetScore($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetScoreboardIdentityPacket.php b/src/pocketmine/network/mcpe/protocol/SetScoreboardIdentityPacket.php deleted file mode 100644 index c6dbc90fdf..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetScoreboardIdentityPacket.php +++ /dev/null @@ -1,70 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\ScoreboardIdentityPacketEntry; -use function count; - -class SetScoreboardIdentityPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_SCOREBOARD_IDENTITY_PACKET; - - public const TYPE_REGISTER_IDENTITY = 0; - public const TYPE_CLEAR_IDENTITY = 1; - - /** @var int */ - public $type; - /** @var ScoreboardIdentityPacketEntry[] */ - public $entries = []; - - protected function decodePayload(){ - $this->type = $this->getByte(); - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $entry = new ScoreboardIdentityPacketEntry(); - $entry->scoreboardId = $this->getVarLong(); - if($this->type === self::TYPE_REGISTER_IDENTITY){ - $entry->entityUniqueId = $this->getEntityUniqueId(); - } - - $this->entries[] = $entry; - } - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putUnsignedVarInt(count($this->entries)); - foreach($this->entries as $entry){ - $this->putVarLong($entry->scoreboardId); - if($this->type === self::TYPE_REGISTER_IDENTITY){ - $this->putEntityUniqueId($entry->entityUniqueId); - } - } - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetScoreboardIdentity($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetSpawnPositionPacket.php b/src/pocketmine/network/mcpe/protocol/SetSpawnPositionPacket.php deleted file mode 100644 index d45ca3553d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetSpawnPositionPacket.php +++ /dev/null @@ -1,70 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetSpawnPositionPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_SPAWN_POSITION_PACKET; - - public const TYPE_PLAYER_SPAWN = 0; - public const TYPE_WORLD_SPAWN = 1; - - /** @var int */ - public $spawnType; - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var int */ - public $dimension; - /** @var int */ - public $x2; - /** @var int */ - public $y2; - /** @var int */ - public $z2; - - protected function decodePayload(){ - $this->spawnType = $this->getVarInt(); - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->dimension = $this->getVarInt(); - $this->getBlockPosition($this->x2, $this->y2, $this->z2); - } - - protected function encodePayload(){ - $this->putVarInt($this->spawnType); - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putVarInt($this->dimension); - $this->putBlockPosition($this->x2, $this->y2, $this->z2); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetSpawnPosition($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetTimePacket.php b/src/pocketmine/network/mcpe/protocol/SetTimePacket.php deleted file mode 100644 index 66a97fba82..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetTimePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetTimePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_TIME_PACKET; - - /** @var int */ - public $time; - - protected function decodePayload(){ - $this->time = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putVarInt($this->time); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetTime($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SetTitlePacket.php b/src/pocketmine/network/mcpe/protocol/SetTitlePacket.php deleted file mode 100644 index b179caabed..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SetTitlePacket.php +++ /dev/null @@ -1,79 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SetTitlePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SET_TITLE_PACKET; - - public const TYPE_CLEAR_TITLE = 0; - public const TYPE_RESET_TITLE = 1; - public const TYPE_SET_TITLE = 2; - public const TYPE_SET_SUBTITLE = 3; - public const TYPE_SET_ACTIONBAR_MESSAGE = 4; - public const TYPE_SET_ANIMATION_TIMES = 5; - public const TYPE_SET_TITLE_JSON = 6; - public const TYPE_SET_SUBTITLE_JSON = 7; - public const TYPE_SET_ACTIONBAR_MESSAGE_JSON = 8; - - /** @var int */ - public $type; - /** @var string */ - public $text = ""; - /** @var int */ - public $fadeInTime = 0; - /** @var int */ - public $stayTime = 0; - /** @var int */ - public $fadeOutTime = 0; - public string $xuid = ""; - public string $platformOnlineId = ""; - - protected function decodePayload(){ - $this->type = $this->getVarInt(); - $this->text = $this->getString(); - $this->fadeInTime = $this->getVarInt(); - $this->stayTime = $this->getVarInt(); - $this->fadeOutTime = $this->getVarInt(); - $this->xuid = $this->getString(); - $this->platformOnlineId = $this->getString(); - } - - protected function encodePayload(){ - $this->putVarInt($this->type); - $this->putString($this->text); - $this->putVarInt($this->fadeInTime); - $this->putVarInt($this->stayTime); - $this->putVarInt($this->fadeOutTime); - $this->putString($this->xuid); - $this->putString($this->platformOnlineId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSetTitle($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SettingsCommandPacket.php b/src/pocketmine/network/mcpe/protocol/SettingsCommandPacket.php deleted file mode 100644 index d0ac8f5f56..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SettingsCommandPacket.php +++ /dev/null @@ -1,66 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SettingsCommandPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::SETTINGS_COMMAND_PACKET; - - /** @var string */ - private $command; - /** @var bool */ - private $suppressOutput; - - public static function create(string $command, bool $suppressOutput) : self{ - $result = new self; - $result->command = $command; - $result->suppressOutput = $suppressOutput; - return $result; - } - - public function getCommand() : string{ - return $this->command; - } - - public function getSuppressOutput() : bool{ - return $this->suppressOutput; - } - - protected function decodePayload() : void{ - $this->command = $this->getString(); - $this->suppressOutput = $this->getBool(); - } - - protected function encodePayload() : void{ - $this->putString($this->command); - $this->putBool($this->suppressOutput); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleSettingsCommand($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ShowCreditsPacket.php b/src/pocketmine/network/mcpe/protocol/ShowCreditsPacket.php deleted file mode 100644 index fa7f0cb5bf..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ShowCreditsPacket.php +++ /dev/null @@ -1,54 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ShowCreditsPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SHOW_CREDITS_PACKET; - - public const STATUS_START_CREDITS = 0; - public const STATUS_END_CREDITS = 1; - - /** @var int */ - public $playerEid; - /** @var int */ - public $status; - - protected function decodePayload(){ - $this->playerEid = $this->getEntityRuntimeId(); - $this->status = $this->getVarInt(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->playerEid); - $this->putVarInt($this->status); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleShowCredits($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ShowProfilePacket.php b/src/pocketmine/network/mcpe/protocol/ShowProfilePacket.php deleted file mode 100644 index 54f8cc1de8..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ShowProfilePacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ShowProfilePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SHOW_PROFILE_PACKET; - - /** @var string */ - public $xuid; - - protected function decodePayload(){ - $this->xuid = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->xuid); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleShowProfile($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/ShowStoreOfferPacket.php b/src/pocketmine/network/mcpe/protocol/ShowStoreOfferPacket.php deleted file mode 100644 index a3f5a22c30..0000000000 --- a/src/pocketmine/network/mcpe/protocol/ShowStoreOfferPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class ShowStoreOfferPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SHOW_STORE_OFFER_PACKET; - - /** @var string */ - public $offerId; - /** @var bool */ - public $showAll; - - protected function decodePayload(){ - $this->offerId = $this->getString(); - $this->showAll = $this->getBool(); - } - - protected function encodePayload(){ - $this->putString($this->offerId); - $this->putBool($this->showAll); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleShowStoreOffer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SimpleEventPacket.php b/src/pocketmine/network/mcpe/protocol/SimpleEventPacket.php deleted file mode 100644 index dbca01336b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SimpleEventPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SimpleEventPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SIMPLE_EVENT_PACKET; - - public const TYPE_ENABLE_COMMANDS = 1; - public const TYPE_DISABLE_COMMANDS = 2; - public const TYPE_UNLOCK_WORLD_TEMPLATE_SETTINGS = 3; - - /** @var int */ - public $eventType; - - protected function decodePayload(){ - $this->eventType = $this->getLShort(); - } - - protected function encodePayload(){ - $this->putLShort($this->eventType); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSimpleEvent($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SimulationTypePacket.php b/src/pocketmine/network/mcpe/protocol/SimulationTypePacket.php deleted file mode 100644 index 86c5121bba..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SimulationTypePacket.php +++ /dev/null @@ -1,58 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SimulationTypePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::SIMULATION_TYPE_PACKET; - - public const GAME = 0; - public const EDITOR = 1; - public const TEST = 2; - - private int $type; - - public static function create(int $type) : self{ - $result = new self; - $result->type = $type; - return $result; - } - - public function getType() : int{ return $this->type; } - - protected function decodePayload() : void{ - $this->type = $this->getByte(); - } - - protected function encodePayload() : void{ - $this->putByte($this->type); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleSimulationType($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SpawnParticleEffectPacket.php b/src/pocketmine/network/mcpe/protocol/SpawnParticleEffectPacket.php deleted file mode 100644 index 20a3b5498a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SpawnParticleEffectPacket.php +++ /dev/null @@ -1,61 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\DimensionIds; - -class SpawnParticleEffectPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SPAWN_PARTICLE_EFFECT_PACKET; - - /** @var int */ - public $dimensionId = DimensionIds::OVERWORLD; //wtf mojang - /** @var int */ - public $entityUniqueId = -1; //default none - /** @var Vector3 */ - public $position; - /** @var string */ - public $particleName; - - protected function decodePayload(){ - $this->dimensionId = $this->getByte(); - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->position = $this->getVector3(); - $this->particleName = $this->getString(); - } - - protected function encodePayload(){ - $this->putByte($this->dimensionId); - $this->putEntityUniqueId($this->entityUniqueId); - $this->putVector3($this->position); - $this->putString($this->particleName); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSpawnParticleEffect($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/StartGamePacket.php b/src/pocketmine/network/mcpe/protocol/StartGamePacket.php deleted file mode 100644 index d702732df3..0000000000 --- a/src/pocketmine/network/mcpe/protocol/StartGamePacket.php +++ /dev/null @@ -1,358 +0,0 @@ - - -use pocketmine\math\Vector3; -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\BlockPaletteEntry; -use pocketmine\network\mcpe\protocol\types\EducationEditionOffer; -use pocketmine\network\mcpe\protocol\types\EducationUriResource; -use pocketmine\network\mcpe\protocol\types\Experiments; -use pocketmine\network\mcpe\protocol\types\GameRuleType; -use pocketmine\network\mcpe\protocol\types\GeneratorType; -use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; -use pocketmine\network\mcpe\protocol\types\MultiplayerGameVisibility; -use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings; -use pocketmine\network\mcpe\protocol\types\PlayerPermissions; -use pocketmine\network\mcpe\protocol\types\SpawnSettings; -use function count; - -class StartGamePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::START_GAME_PACKET; - - /** @var int */ - public $entityUniqueId; - /** @var int */ - public $entityRuntimeId; - /** @var int */ - public $playerGamemode; - - /** @var Vector3 */ - public $playerPosition; - - /** @var float */ - public $pitch; - /** @var float */ - public $yaw; - - /** @var int */ - public $seed; - /** @var SpawnSettings */ - public $spawnSettings; - /** @var int */ - public $generator = GeneratorType::OVERWORLD; - /** @var int */ - public $worldGamemode; - /** @var int */ - public $difficulty; - /** @var int */ - public $spawnX; - /** @var int */ - public $spawnY; - /** @var int */ - public $spawnZ; - /** @var bool */ - public $hasAchievementsDisabled = true; - /** @var int */ - public $time = -1; - /** @var int */ - public $eduEditionOffer = EducationEditionOffer::NONE; - /** @var bool */ - public $hasEduFeaturesEnabled = false; - /** @var string */ - public $eduProductUUID = ""; - /** @var float */ - public $rainLevel; - /** @var float */ - public $lightningLevel; - /** @var bool */ - public $hasConfirmedPlatformLockedContent = false; - /** @var bool */ - public $isMultiplayerGame = true; - /** @var bool */ - public $hasLANBroadcast = true; - /** @var int */ - public $xboxLiveBroadcastMode = MultiplayerGameVisibility::PUBLIC; - /** @var int */ - public $platformBroadcastMode = MultiplayerGameVisibility::PUBLIC; - /** @var bool */ - public $commandsEnabled; - /** @var bool */ - public $isTexturePacksRequired = true; - /** - * @var mixed[][] - * @phpstan-var array - */ - public $gameRules = [ //TODO: implement this - "naturalregeneration" => [GameRuleType::BOOL, false, false] //Hack for client side regeneration - ]; - /** @var Experiments */ - public $experiments; - /** @var bool */ - public $hasBonusChestEnabled = false; - /** @var bool */ - public $hasStartWithMapEnabled = false; - /** @var int */ - public $defaultPlayerPermission = PlayerPermissions::MEMBER; //TODO - - /** @var int */ - public $serverChunkTickRadius = 4; //TODO (leave as default for now) - - /** @var bool */ - public $hasLockedBehaviorPack = false; - /** @var bool */ - public $hasLockedResourcePack = false; - /** @var bool */ - public $isFromLockedWorldTemplate = false; - /** @var bool */ - public $useMsaGamertagsOnly = false; - /** @var bool */ - public $isFromWorldTemplate = false; - /** @var bool */ - public $isWorldTemplateOptionLocked = false; - /** @var bool */ - public $onlySpawnV1Villagers = false; - /** @var string */ - public $vanillaVersion = ProtocolInfo::MINECRAFT_VERSION_NETWORK; - /** @var int */ - public $limitedWorldWidth = 0; - /** @var int */ - public $limitedWorldLength = 0; - /** @var bool */ - public $isNewNether = true; - /** @var EducationUriResource|null */ - public $eduSharedUriResource = null; - /** @var bool|null */ - public $experimentalGameplayOverride = null; - - /** @var string */ - public $levelId = ""; //base64 string, usually the same as world folder name in vanilla - /** @var string */ - public $worldName; - /** @var string */ - public $premiumWorldTemplateId = ""; - /** @var bool */ - public $isTrial = false; - /** @var PlayerMovementSettings */ - public $playerMovementSettings; - /** @var int */ - public $currentTick = 0; //only used if isTrial is true - /** @var int */ - public $enchantmentSeed = 0; - /** @var string */ - public $multiplayerCorrelationId = ""; //TODO: this should be filled with a UUID of some sort - - /** - * @var BlockPaletteEntry[] - * @phpstan-var list - */ - public $blockPalette = []; - - /** - * @var ItemTypeEntry[] - * @phpstan-var list - */ - public $itemTable; - /** @var bool */ - public $enableNewInventorySystem = false; //TODO - /** @var string */ - public $serverSoftwareVersion; - - public int $blockPaletteChecksum; - - protected function decodePayload(){ - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->playerGamemode = $this->getVarInt(); - - $this->playerPosition = $this->getVector3(); - - $this->pitch = $this->getLFloat(); - $this->yaw = $this->getLFloat(); - - //Level settings - $this->seed = $this->getVarInt(); - $this->spawnSettings = SpawnSettings::read($this); - $this->generator = $this->getVarInt(); - $this->worldGamemode = $this->getVarInt(); - $this->difficulty = $this->getVarInt(); - $this->getBlockPosition($this->spawnX, $this->spawnY, $this->spawnZ); - $this->hasAchievementsDisabled = $this->getBool(); - $this->time = $this->getVarInt(); - $this->eduEditionOffer = $this->getVarInt(); - $this->hasEduFeaturesEnabled = $this->getBool(); - $this->eduProductUUID = $this->getString(); - $this->rainLevel = $this->getLFloat(); - $this->lightningLevel = $this->getLFloat(); - $this->hasConfirmedPlatformLockedContent = $this->getBool(); - $this->isMultiplayerGame = $this->getBool(); - $this->hasLANBroadcast = $this->getBool(); - $this->xboxLiveBroadcastMode = $this->getVarInt(); - $this->platformBroadcastMode = $this->getVarInt(); - $this->commandsEnabled = $this->getBool(); - $this->isTexturePacksRequired = $this->getBool(); - $this->gameRules = $this->getGameRules(); - $this->experiments = Experiments::read($this); - $this->hasBonusChestEnabled = $this->getBool(); - $this->hasStartWithMapEnabled = $this->getBool(); - $this->defaultPlayerPermission = $this->getVarInt(); - $this->serverChunkTickRadius = $this->getLInt(); - $this->hasLockedBehaviorPack = $this->getBool(); - $this->hasLockedResourcePack = $this->getBool(); - $this->isFromLockedWorldTemplate = $this->getBool(); - $this->useMsaGamertagsOnly = $this->getBool(); - $this->isFromWorldTemplate = $this->getBool(); - $this->isWorldTemplateOptionLocked = $this->getBool(); - $this->onlySpawnV1Villagers = $this->getBool(); - $this->vanillaVersion = $this->getString(); - $this->limitedWorldWidth = $this->getLInt(); - $this->limitedWorldLength = $this->getLInt(); - $this->isNewNether = $this->getBool(); - $this->eduSharedUriResource = EducationUriResource::read($this); - if($this->getBool()){ - $this->experimentalGameplayOverride = $this->getBool(); - }else{ - $this->experimentalGameplayOverride = null; - } - - $this->levelId = $this->getString(); - $this->worldName = $this->getString(); - $this->premiumWorldTemplateId = $this->getString(); - $this->isTrial = $this->getBool(); - $this->playerMovementSettings = PlayerMovementSettings::read($this); - $this->currentTick = $this->getLLong(); - - $this->enchantmentSeed = $this->getVarInt(); - - $this->blockPalette = []; - for($i = 0, $len = $this->getUnsignedVarInt(); $i < $len; ++$i){ - $blockName = $this->getString(); - $state = $this->getNbtCompoundRoot(); - $this->blockPalette[] = new BlockPaletteEntry($blockName, $state); - } - - $this->itemTable = []; - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $stringId = $this->getString(); - $numericId = $this->getSignedLShort(); - $isComponentBased = $this->getBool(); - - $this->itemTable[] = new ItemTypeEntry($stringId, $numericId, $isComponentBased); - } - - $this->multiplayerCorrelationId = $this->getString(); - $this->enableNewInventorySystem = $this->getBool(); - $this->serverSoftwareVersion = $this->getString(); - $this->blockPaletteChecksum = $this->getLLong(); - } - - protected function encodePayload(){ - $this->putEntityUniqueId($this->entityUniqueId); - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putVarInt($this->playerGamemode); - - $this->putVector3($this->playerPosition); - - $this->putLFloat($this->pitch); - $this->putLFloat($this->yaw); - - //Level settings - $this->putVarInt($this->seed); - $this->spawnSettings->write($this); - $this->putVarInt($this->generator); - $this->putVarInt($this->worldGamemode); - $this->putVarInt($this->difficulty); - $this->putBlockPosition($this->spawnX, $this->spawnY, $this->spawnZ); - $this->putBool($this->hasAchievementsDisabled); - $this->putVarInt($this->time); - $this->putVarInt($this->eduEditionOffer); - $this->putBool($this->hasEduFeaturesEnabled); - $this->putString($this->eduProductUUID); - $this->putLFloat($this->rainLevel); - $this->putLFloat($this->lightningLevel); - $this->putBool($this->hasConfirmedPlatformLockedContent); - $this->putBool($this->isMultiplayerGame); - $this->putBool($this->hasLANBroadcast); - $this->putVarInt($this->xboxLiveBroadcastMode); - $this->putVarInt($this->platformBroadcastMode); - $this->putBool($this->commandsEnabled); - $this->putBool($this->isTexturePacksRequired); - $this->putGameRules($this->gameRules); - $this->experiments->write($this); - $this->putBool($this->hasBonusChestEnabled); - $this->putBool($this->hasStartWithMapEnabled); - $this->putVarInt($this->defaultPlayerPermission); - $this->putLInt($this->serverChunkTickRadius); - $this->putBool($this->hasLockedBehaviorPack); - $this->putBool($this->hasLockedResourcePack); - $this->putBool($this->isFromLockedWorldTemplate); - $this->putBool($this->useMsaGamertagsOnly); - $this->putBool($this->isFromWorldTemplate); - $this->putBool($this->isWorldTemplateOptionLocked); - $this->putBool($this->onlySpawnV1Villagers); - $this->putString($this->vanillaVersion); - $this->putLInt($this->limitedWorldWidth); - $this->putLInt($this->limitedWorldLength); - $this->putBool($this->isNewNether); - ($this->eduSharedUriResource ?? new EducationUriResource("", ""))->write($this); - $this->putBool($this->experimentalGameplayOverride !== null); - if($this->experimentalGameplayOverride !== null){ - $this->putBool($this->experimentalGameplayOverride); - } - - $this->putString($this->levelId); - $this->putString($this->worldName); - $this->putString($this->premiumWorldTemplateId); - $this->putBool($this->isTrial); - $this->playerMovementSettings->write($this); - $this->putLLong($this->currentTick); - - $this->putVarInt($this->enchantmentSeed); - - $this->putUnsignedVarInt(count($this->blockPalette)); - $nbtWriter = new NetworkLittleEndianNBTStream(); - foreach($this->blockPalette as $entry){ - $this->putString($entry->getName()); - $this->put($nbtWriter->write($entry->getStates())); - } - $this->putUnsignedVarInt(count($this->itemTable)); - foreach($this->itemTable as $entry){ - $this->putString($entry->getStringId()); - $this->putLShort($entry->getNumericId()); - $this->putBool($entry->isComponentBased()); - } - - $this->putString($this->multiplayerCorrelationId); - $this->putBool($this->enableNewInventorySystem); - $this->putString($this->serverSoftwareVersion); - $this->putLLong($this->blockPaletteChecksum); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleStartGame($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/StopSoundPacket.php b/src/pocketmine/network/mcpe/protocol/StopSoundPacket.php deleted file mode 100644 index 979ff6b297..0000000000 --- a/src/pocketmine/network/mcpe/protocol/StopSoundPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class StopSoundPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::STOP_SOUND_PACKET; - - /** @var string */ - public $soundName; - /** @var bool */ - public $stopAll; - - protected function decodePayload(){ - $this->soundName = $this->getString(); - $this->stopAll = $this->getBool(); - } - - protected function encodePayload(){ - $this->putString($this->soundName); - $this->putBool($this->stopAll); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleStopSound($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/StructureBlockUpdatePacket.php b/src/pocketmine/network/mcpe/protocol/StructureBlockUpdatePacket.php deleted file mode 100644 index fa748f2a8c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/StructureBlockUpdatePacket.php +++ /dev/null @@ -1,60 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\StructureEditorData; - -class StructureBlockUpdatePacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::STRUCTURE_BLOCK_UPDATE_PACKET; - - /** @var int */ - public $x; - /** @var int */ - public $y; - /** @var int */ - public $z; - /** @var StructureEditorData */ - public $structureEditorData; - /** @var bool */ - public $isPowered; - - protected function decodePayload(){ - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->structureEditorData = $this->getStructureEditorData(); - $this->isPowered = $this->getBool(); - } - - protected function encodePayload(){ - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putStructureEditorData($this->structureEditorData); - $this->putBool($this->isPowered); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleStructureBlockUpdate($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/StructureTemplateDataRequestPacket.php b/src/pocketmine/network/mcpe/protocol/StructureTemplateDataRequestPacket.php deleted file mode 100644 index ba100bb607..0000000000 --- a/src/pocketmine/network/mcpe/protocol/StructureTemplateDataRequestPacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\StructureSettings; - -class StructureTemplateDataRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::STRUCTURE_TEMPLATE_DATA_REQUEST_PACKET; - - public const TYPE_ALWAYS_LOAD = 1; - public const TYPE_CREATE_AND_LOAD = 2; - - /** @var string */ - public $structureTemplateName; - /** @var int */ - public $structureBlockX; - /** @var int */ - public $structureBlockY; - /** @var int */ - public $structureBlockZ; - /** @var StructureSettings */ - public $structureSettings; - /** @var int */ - public $structureTemplateResponseType; - - protected function decodePayload() : void{ - $this->structureTemplateName = $this->getString(); - $this->getBlockPosition($this->structureBlockX, $this->structureBlockY, $this->structureBlockZ); - $this->structureSettings = $this->getStructureSettings(); - $this->structureTemplateResponseType = $this->getByte(); - } - - protected function encodePayload() : void{ - $this->putString($this->structureTemplateName); - $this->putBlockPosition($this->structureBlockX, $this->structureBlockY, $this->structureBlockZ); - $this->putStructureSettings($this->structureSettings); - $this->putByte($this->structureTemplateResponseType); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleStructureTemplateDataRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/StructureTemplateDataResponsePacket.php b/src/pocketmine/network/mcpe/protocol/StructureTemplateDataResponsePacket.php deleted file mode 100644 index 98afaaed78..0000000000 --- a/src/pocketmine/network/mcpe/protocol/StructureTemplateDataResponsePacket.php +++ /dev/null @@ -1,56 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class StructureTemplateDataResponsePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::STRUCTURE_TEMPLATE_DATA_RESPONSE_PACKET; - - /** @var string */ - public $structureTemplateName; - /** @var string|null */ - public $namedtag; - - protected function decodePayload() : void{ - $this->structureTemplateName = $this->getString(); - if($this->getBool()){ - $this->namedtag = $this->getRemaining(); - } - } - - protected function encodePayload() : void{ - $this->putString($this->structureTemplateName); - $this->putBool($this->namedtag !== null); - if($this->namedtag !== null){ - $this->put($this->namedtag); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleStructureTemplateDataResponse($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SubChunkPacket.php b/src/pocketmine/network/mcpe/protocol/SubChunkPacket.php deleted file mode 100644 index ade9163d4c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SubChunkPacket.php +++ /dev/null @@ -1,119 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\SubChunkPacketHeightMapInfo; -use pocketmine\network\mcpe\protocol\types\SubChunkPacketHeightMapType; - -class SubChunkPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SUB_CHUNK_PACKET; - - private int $dimension; - private int $subChunkX; - private int $subChunkY; - private int $subChunkZ; - private string $data; - private int $requestResult; - private ?SubChunkPacketHeightMapInfo $heightMapData = null; - private ?int $usedBlobHash = null; - - public static function create(int $dimension, int $subChunkX, int $subChunkY, int $subChunkZ, string $data, int $requestResult, ?SubChunkPacketHeightMapInfo $heightMapData, ?int $usedBlobHash) : self{ - $result = new self; - $result->dimension = $dimension; - $result->subChunkX = $subChunkX; - $result->subChunkY = $subChunkY; - $result->subChunkZ = $subChunkZ; - $result->data = $data; - $result->requestResult = $requestResult; - $result->heightMapData = $heightMapData; - $result->usedBlobHash = $usedBlobHash; - return $result; - } - - public function getDimension() : int{ return $this->dimension; } - - public function getSubChunkX() : int{ return $this->subChunkX; } - - public function getSubChunkY() : int{ return $this->subChunkY; } - - public function getSubChunkZ() : int{ return $this->subChunkZ; } - - public function getData() : string{ return $this->data; } - - public function getRequestResult() : int{ return $this->requestResult; } - - public function getHeightMapData() : ?SubChunkPacketHeightMapInfo{ return $this->heightMapData; } - - public function getUsedBlobHash() : ?int{ return $this->usedBlobHash; } - - protected function decodePayload() : void{ - $this->dimension = $this->getVarInt(); - $this->subChunkX = $this->getVarInt(); - $this->subChunkY = $this->getVarInt(); - $this->subChunkZ = $this->getVarInt(); - $this->data = $this->getString(); - $this->requestResult = $this->getVarInt(); - $heightMapDataType = $this->getByte(); - $this->heightMapData = match($heightMapDataType){ - SubChunkPacketHeightMapType::NO_DATA => null, - SubChunkPacketHeightMapType::DATA => SubChunkPacketHeightMapInfo::read($this), - SubChunkPacketHeightMapType::ALL_TOO_HIGH => SubChunkPacketHeightMapInfo::allTooHigh(), - SubChunkPacketHeightMapType::ALL_TOO_LOW => SubChunkPacketHeightMapInfo::allTooLow(), - default => throw new \UnexpectedValueException("Unknown heightmap data type $heightMapDataType") - }; - $this->usedBlobHash = $this->getBool() ? $this->getLLong() : null; - } - - protected function encodePayload() : void{ - $this->putVarInt($this->dimension); - $this->putVarInt($this->subChunkX); - $this->putVarInt($this->subChunkY); - $this->putVarInt($this->subChunkZ); - $this->putString($this->data); - $this->putVarInt($this->requestResult); - if($this->heightMapData === null){ - $this->putByte(SubChunkPacketHeightMapType::NO_DATA); - }elseif($this->heightMapData->isAllTooLow()){ - $this->putByte(SubChunkPacketHeightMapType::ALL_TOO_LOW); - }elseif($this->heightMapData->isAllTooHigh()){ - $this->putByte(SubChunkPacketHeightMapType::ALL_TOO_HIGH); - }else{ - $heightMapData = $this->heightMapData; //avoid PHPStan purity issue - $this->putByte(SubChunkPacketHeightMapType::DATA); - $heightMapData->write($this); - } - $usedBlobHash = $this->usedBlobHash; - $this->putBool($usedBlobHash !== null); - if($usedBlobHash !== null){ - $this->putLLong($usedBlobHash); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleSubChunk($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SubChunkRequestPacket.php b/src/pocketmine/network/mcpe/protocol/SubChunkRequestPacket.php deleted file mode 100644 index cd35945764..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SubChunkRequestPacket.php +++ /dev/null @@ -1,72 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SubChunkRequestPacket extends DataPacket/* implements ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::SUB_CHUNK_REQUEST_PACKET; - - private int $dimension; - private int $subChunkX; - private int $subChunkY; - private int $subChunkZ; - - public static function create(int $dimension, int $subChunkX, int $subChunkY, int $subChunkZ) : self{ - $result = new self; - $result->dimension = $dimension; - $result->subChunkX = $subChunkX; - $result->subChunkY = $subChunkY; - $result->subChunkZ = $subChunkZ; - return $result; - } - - public function getDimension() : int{ return $this->dimension; } - - public function getSubChunkX() : int{ return $this->subChunkX; } - - public function getSubChunkY() : int{ return $this->subChunkY; } - - public function getSubChunkZ() : int{ return $this->subChunkZ; } - - protected function decodePayload() : void{ - $this->dimension = $this->getVarInt(); - $this->subChunkX = $this->getVarInt(); - $this->subChunkY = $this->getVarInt(); - $this->subChunkZ = $this->getVarInt(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->dimension); - $this->putVarInt($this->subChunkX); - $this->putVarInt($this->subChunkY); - $this->putVarInt($this->subChunkZ); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleSubChunkRequest($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SubClientLoginPacket.php b/src/pocketmine/network/mcpe/protocol/SubClientLoginPacket.php deleted file mode 100644 index f699e0c6c2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SubClientLoginPacket.php +++ /dev/null @@ -1,47 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class SubClientLoginPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SUB_CLIENT_LOGIN_PACKET; - - /** @var string */ - public $connectionRequestData; - - protected function decodePayload(){ - $this->connectionRequestData = $this->getString(); - } - - protected function encodePayload(){ - $this->putString($this->connectionRequestData); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleSubClientLogin($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/SyncActorPropertyPacket.php b/src/pocketmine/network/mcpe/protocol/SyncActorPropertyPacket.php deleted file mode 100644 index e95e508504..0000000000 --- a/src/pocketmine/network/mcpe/protocol/SyncActorPropertyPacket.php +++ /dev/null @@ -1,57 +0,0 @@ - - -use pocketmine\nbt\NetworkLittleEndianNBTStream; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\network\mcpe\NetworkSession; - -class SyncActorPropertyPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SYNC_ACTOR_PROPERTY_PACKET; - - /** @var CompoundTag */ - private $data; - - public static function create(CompoundTag $data) : self{ - $result = new self; - $result->data = $data; - return $result; - } - - public function getData() : CompoundTag{ return $this->data; } - - protected function decodePayload() : void{ - $this->data = $this->getNbtCompoundRoot(); - } - - protected function encodePayload() : void{ - $this->put((new NetworkLittleEndianNBTStream())->write($this->data)); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleSyncActorProperty($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/TakeItemActorPacket.php b/src/pocketmine/network/mcpe/protocol/TakeItemActorPacket.php deleted file mode 100644 index bb61123a24..0000000000 --- a/src/pocketmine/network/mcpe/protocol/TakeItemActorPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class TakeItemActorPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::TAKE_ITEM_ACTOR_PACKET; - - /** @var int */ - public $target; - /** @var int */ - public $eid; - - protected function decodePayload(){ - $this->target = $this->getEntityRuntimeId(); - $this->eid = $this->getEntityRuntimeId(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->target); - $this->putEntityRuntimeId($this->eid); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleTakeItemActor($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/TextPacket.php b/src/pocketmine/network/mcpe/protocol/TextPacket.php deleted file mode 100644 index 6b35b1f09e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/TextPacket.php +++ /dev/null @@ -1,128 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class TextPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::TEXT_PACKET; - - public const TYPE_RAW = 0; - public const TYPE_CHAT = 1; - public const TYPE_TRANSLATION = 2; - public const TYPE_POPUP = 3; - public const TYPE_JUKEBOX_POPUP = 4; - public const TYPE_TIP = 5; - public const TYPE_SYSTEM = 6; - public const TYPE_WHISPER = 7; - public const TYPE_ANNOUNCEMENT = 8; - public const TYPE_JSON_WHISPER = 9; - public const TYPE_JSON = 10; - - /** @var int */ - public $type; - /** @var bool */ - public $needsTranslation = false; - /** @var string */ - public $sourceName; - /** @var string */ - public $message; - /** @var string[] */ - public $parameters = []; - /** @var string */ - public $xboxUserId = ""; - /** @var string */ - public $platformChatId = ""; - - protected function decodePayload(){ - $this->type = $this->getByte(); - $this->needsTranslation = $this->getBool(); - switch($this->type){ - case self::TYPE_CHAT: - case self::TYPE_WHISPER: - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_ANNOUNCEMENT: - $this->sourceName = $this->getString(); - case self::TYPE_RAW: - case self::TYPE_TIP: - case self::TYPE_SYSTEM: - case self::TYPE_JSON_WHISPER: - case self::TYPE_JSON: - $this->message = $this->getString(); - break; - - case self::TYPE_TRANSLATION: - case self::TYPE_POPUP: - case self::TYPE_JUKEBOX_POPUP: - $this->message = $this->getString(); - $count = $this->getUnsignedVarInt(); - for($i = 0; $i < $count; ++$i){ - $this->parameters[] = $this->getString(); - } - break; - } - - $this->xboxUserId = $this->getString(); - $this->platformChatId = $this->getString(); - } - - protected function encodePayload(){ - $this->putByte($this->type); - $this->putBool($this->needsTranslation); - switch($this->type){ - case self::TYPE_CHAT: - case self::TYPE_WHISPER: - /** @noinspection PhpMissingBreakStatementInspection */ - case self::TYPE_ANNOUNCEMENT: - $this->putString($this->sourceName); - case self::TYPE_RAW: - case self::TYPE_TIP: - case self::TYPE_SYSTEM: - case self::TYPE_JSON_WHISPER: - case self::TYPE_JSON: - $this->putString($this->message); - break; - - case self::TYPE_TRANSLATION: - case self::TYPE_POPUP: - case self::TYPE_JUKEBOX_POPUP: - $this->putString($this->message); - $this->putUnsignedVarInt(count($this->parameters)); - foreach($this->parameters as $p){ - $this->putString($p); - } - break; - } - - $this->putString($this->xboxUserId); - $this->putString($this->platformChatId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleText($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/TickSyncPacket.php b/src/pocketmine/network/mcpe/protocol/TickSyncPacket.php deleted file mode 100644 index 4c14fc8aac..0000000000 --- a/src/pocketmine/network/mcpe/protocol/TickSyncPacket.php +++ /dev/null @@ -1,73 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class TickSyncPacket extends DataPacket/* implements ClientboundPacket, ServerboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::TICK_SYNC_PACKET; - - /** @var int */ - private $clientSendTime; - /** @var int */ - private $serverReceiveTime; - - public static function request(int $clientTime) : self{ - $result = new self; - $result->clientSendTime = $clientTime; - $result->serverReceiveTime = 0; //useless - return $result; - } - - public static function response(int $clientSendTime, int $serverReceiveTime) : self{ - $result = new self; - $result->clientSendTime = $clientSendTime; - $result->serverReceiveTime = $serverReceiveTime; - return $result; - } - - public function getClientSendTime() : int{ - return $this->clientSendTime; - } - - public function getServerReceiveTime() : int{ - return $this->serverReceiveTime; - } - - protected function decodePayload() : void{ - $this->clientSendTime = $this->getLLong(); - $this->serverReceiveTime = $this->getLLong(); - } - - protected function encodePayload() : void{ - $this->putLLong($this->clientSendTime); - $this->putLLong($this->serverReceiveTime); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleTickSync($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/TransferPacket.php b/src/pocketmine/network/mcpe/protocol/TransferPacket.php deleted file mode 100644 index e5729d1daf..0000000000 --- a/src/pocketmine/network/mcpe/protocol/TransferPacket.php +++ /dev/null @@ -1,51 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class TransferPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::TRANSFER_PACKET; - - /** @var string */ - public $address; - /** @var int */ - public $port = 19132; - - protected function decodePayload(){ - $this->address = $this->getString(); - $this->port = $this->getLShort(); - } - - protected function encodePayload(){ - $this->putString($this->address); - $this->putLShort($this->port); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleTransfer($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UnknownPacket.php b/src/pocketmine/network/mcpe/protocol/UnknownPacket.php deleted file mode 100644 index 73de061462..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UnknownPacket.php +++ /dev/null @@ -1,59 +0,0 @@ -payload ?? "") > 0){ - return ord($this->payload[0]); - } - return self::NETWORK_ID; - } - - public function getName() : string{ - return "unknown packet"; - } - - public function decode(){ - $this->payload = $this->getRemaining(); - } - - public function encode(){ - //Do not reset the buffer, this class does not have a valid NETWORK_ID constant. - $this->put($this->payload); - } - - public function handle(NetworkSession $session) : bool{ - return false; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateAttributesPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateAttributesPacket.php deleted file mode 100644 index d42c24eb93..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateAttributesPacket.php +++ /dev/null @@ -1,56 +0,0 @@ - - -use pocketmine\entity\Attribute; -use pocketmine\network\mcpe\NetworkSession; - -class UpdateAttributesPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_ATTRIBUTES_PACKET; - - /** @var int */ - public $entityRuntimeId; - /** @var Attribute[] */ - public $entries = []; - /** @var int */ - public $tick = 0; - - protected function decodePayload(){ - $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->entries = $this->getAttributeList(); - $this->tick = $this->getUnsignedVarLong(); - } - - protected function encodePayload(){ - $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putAttributeList(...$this->entries); - $this->putUnsignedVarLong($this->tick); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateAttributes($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateBlockPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateBlockPacket.php deleted file mode 100644 index 9e7ca6c322..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateBlockPacket.php +++ /dev/null @@ -1,75 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class UpdateBlockPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_BLOCK_PACKET; - - public const FLAG_NONE = 0b0000; - public const FLAG_NEIGHBORS = 0b0001; - public const FLAG_NETWORK = 0b0010; - public const FLAG_NOGRAPHIC = 0b0100; - public const FLAG_PRIORITY = 0b1000; - - public const FLAG_ALL = self::FLAG_NEIGHBORS | self::FLAG_NETWORK; - public const FLAG_ALL_PRIORITY = self::FLAG_ALL | self::FLAG_PRIORITY; - - public const DATA_LAYER_NORMAL = 0; - public const DATA_LAYER_LIQUID = 1; - - /** @var int */ - public $x; - /** @var int */ - public $z; - /** @var int */ - public $y; - /** @var int */ - public $blockRuntimeId; - /** @var int */ - public $flags; - /** @var int */ - public $dataLayerId = self::DATA_LAYER_NORMAL; - - protected function decodePayload(){ - $this->getBlockPosition($this->x, $this->y, $this->z); - $this->blockRuntimeId = $this->getUnsignedVarInt(); - $this->flags = $this->getUnsignedVarInt(); - $this->dataLayerId = $this->getUnsignedVarInt(); - } - - protected function encodePayload(){ - $this->putBlockPosition($this->x, $this->y, $this->z); - $this->putUnsignedVarInt($this->blockRuntimeId); - $this->putUnsignedVarInt($this->flags); - $this->putUnsignedVarInt($this->dataLayerId); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateBlock($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateBlockSyncedPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateBlockSyncedPacket.php deleted file mode 100644 index a98f2f59ed..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateBlockSyncedPacket.php +++ /dev/null @@ -1,57 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class UpdateBlockSyncedPacket extends UpdateBlockPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_BLOCK_SYNCED_PACKET; - - public const TYPE_NONE = 0; - public const TYPE_CREATE = 1; - public const TYPE_DESTROY = 2; - - /** @var int */ - public $entityUniqueId; - /** @var int */ - public $updateType; - - protected function decodePayload(){ - parent::decodePayload(); - $this->entityUniqueId = $this->getUnsignedVarLong(); - $this->updateType = $this->getUnsignedVarLong(); - } - - protected function encodePayload(){ - parent::encodePayload(); - $this->putUnsignedVarLong($this->entityUniqueId); - $this->putUnsignedVarLong($this->updateType); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateBlockSynced($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateEquipPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateEquipPacket.php deleted file mode 100644 index 7923dae7c0..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateEquipPacket.php +++ /dev/null @@ -1,63 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; - -class UpdateEquipPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_EQUIP_PACKET; - - /** @var int */ - public $windowId; - /** @var int */ - public $windowType; - /** @var int */ - public $windowSlotCount; //useless, seems to be part of a standard container header - /** @var int */ - public $entityUniqueId; - /** @var string */ - public $namedtag; - - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->windowType = $this->getByte(); - $this->windowSlotCount = $this->getVarInt(); - $this->entityUniqueId = $this->getEntityUniqueId(); - $this->namedtag = $this->getRemaining(); - } - - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putByte($this->windowType); - $this->putVarInt($this->windowSlotCount); - $this->putEntityUniqueId($this->entityUniqueId); - $this->put($this->namedtag); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateEquip($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdatePlayerGameTypePacket.php b/src/pocketmine/network/mcpe/protocol/UpdatePlayerGameTypePacket.php deleted file mode 100644 index d5070f620e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdatePlayerGameTypePacket.php +++ /dev/null @@ -1,67 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\GameMode; - -class UpdatePlayerGameTypePacket extends DataPacket/* implements ClientboundPacket*/{ - public const NETWORK_ID = ProtocolInfo::UPDATE_PLAYER_GAME_TYPE_PACKET; - - /** - * @var int - * @see GameMode - */ - private $gameMode; - - /** @var int */ - private $playerEntityUniqueId; - - public static function create(int $gameMode, int $playerEntityUniqueId) : self{ - $result = new self; - $result->gameMode = $gameMode; - $result->playerEntityUniqueId = $playerEntityUniqueId; - return $result; - } - - public function getGameMode() : int{ return $this->gameMode; } - - public function getPlayerEntityUniqueId() : int{ return $this->playerEntityUniqueId; } - - protected function decodePayload() : void{ - $this->gameMode = $this->getVarInt(); - $this->playerEntityUniqueId = $this->getEntityUniqueId(); - } - - protected function encodePayload() : void{ - $this->putVarInt($this->gameMode); - $this->putEntityUniqueId($this->playerEntityUniqueId); - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleUpdatePlayerGameType($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateSoftEnumPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateSoftEnumPacket.php deleted file mode 100644 index 807367accd..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateSoftEnumPacket.php +++ /dev/null @@ -1,65 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use function count; - -class UpdateSoftEnumPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_SOFT_ENUM_PACKET; - - public const TYPE_ADD = 0; - public const TYPE_REMOVE = 1; - public const TYPE_SET = 2; - - /** @var string */ - public $enumName; - /** @var string[] */ - public $values = []; - /** @var int */ - public $type; - - protected function decodePayload(){ - $this->enumName = $this->getString(); - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->values[] = $this->getString(); - } - $this->type = $this->getByte(); - } - - protected function encodePayload(){ - $this->putString($this->enumName); - $this->putUnsignedVarInt(count($this->values)); - foreach($this->values as $v){ - $this->putString($v); - } - $this->putByte($this->type); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateSoftEnum($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateSubChunkBlocksPacket.php b/src/pocketmine/network/mcpe/protocol/UpdateSubChunkBlocksPacket.php deleted file mode 100644 index 3512e0d616..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateSubChunkBlocksPacket.php +++ /dev/null @@ -1,97 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\UpdateSubChunkBlocksPacketEntry; -use function count; - -class UpdateSubChunkBlocksPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_SUB_CHUNK_BLOCKS_PACKET; - - private int $subChunkX; - private int $subChunkY; - private int $subChunkZ; - - /** @var UpdateSubChunkBlocksPacketEntry[] */ - private array $layer0Updates; - /** @var UpdateSubChunkBlocksPacketEntry[] */ - private array $layer1Updates; - - /** - * @param UpdateSubChunkBlocksPacketEntry[] $layer0 - * @param UpdateSubChunkBlocksPacketEntry[] $layer1 - */ - public static function create(int $subChunkX, int $subChunkY, int $subChunkZ, array $layer0, array $layer1) : self{ - $result = new self; - $result->subChunkX = $subChunkX; - $result->subChunkY = $subChunkY; - $result->subChunkZ = $subChunkZ; - $result->layer0Updates = $layer0; - $result->layer1Updates = $layer1; - return $result; - } - - public function getSubChunkX() : int{ return $this->subChunkX; } - - public function getSubChunkY() : int{ return $this->subChunkY; } - - public function getSubChunkZ() : int{ return $this->subChunkZ; } - - /** @return UpdateSubChunkBlocksPacketEntry[] */ - public function getLayer0Updates() : array{ return $this->layer0Updates; } - - /** @return UpdateSubChunkBlocksPacketEntry[] */ - public function getLayer1Updates() : array{ return $this->layer1Updates; } - - protected function decodePayload() : void{ - $this->subChunkX = $this->subChunkY = $this->subChunkZ = 0; - $this->getBlockPosition($this->subChunkX, $this->subChunkY, $this->subChunkZ); - $this->layer0Updates = []; - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->layer0Updates[] = UpdateSubChunkBlocksPacketEntry::read($this); - } - for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $this->layer1Updates[] = UpdateSubChunkBlocksPacketEntry::read($this); - } - } - - protected function encodePayload() : void{ - $this->putBlockPosition($this->subChunkX, $this->subChunkY, $this->subChunkZ); - $this->putUnsignedVarInt(count($this->layer0Updates)); - foreach($this->layer0Updates as $update){ - $update->write($this); - } - $this->putUnsignedVarInt(count($this->layer1Updates)); - foreach($this->layer1Updates as $update){ - $update->write($this); - } - } - - public function handle(NetworkSession $handler) : bool{ - return $handler->handleUpdateSubChunkBlocks($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/UpdateTradePacket.php b/src/pocketmine/network/mcpe/protocol/UpdateTradePacket.php deleted file mode 100644 index ad99f43617..0000000000 --- a/src/pocketmine/network/mcpe/protocol/UpdateTradePacket.php +++ /dev/null @@ -1,86 +0,0 @@ - - -use pocketmine\network\mcpe\NetworkSession; -use pocketmine\network\mcpe\protocol\types\WindowTypes; - -class UpdateTradePacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::UPDATE_TRADE_PACKET; - - //TODO: find fields - - /** @var int */ - public $windowId; - /** @var int */ - public $windowType = WindowTypes::TRADING; //Mojang hardcoded this -_- - /** @var int */ - public $windowSlotCount = 0; //useless, seems to be part of a standard container header - /** @var int */ - public $tradeTier; - /** @var int */ - public $traderEid; - /** @var int */ - public $playerEid; - /** @var string */ - public $displayName; - /** @var bool */ - public $isV2Trading; - /** @var bool */ - public $isWilling; - /** @var string */ - public $offers; - - protected function decodePayload(){ - $this->windowId = $this->getByte(); - $this->windowType = $this->getByte(); - $this->windowSlotCount = $this->getVarInt(); - $this->tradeTier = $this->getVarInt(); - $this->traderEid = $this->getEntityUniqueId(); - $this->playerEid = $this->getEntityUniqueId(); - $this->displayName = $this->getString(); - $this->isV2Trading = $this->getBool(); - $this->isWilling = $this->getBool(); - $this->offers = $this->getRemaining(); - } - - protected function encodePayload(){ - $this->putByte($this->windowId); - $this->putByte($this->windowType); - $this->putVarInt($this->windowSlotCount); - $this->putVarInt($this->tradeTier); - $this->putEntityUniqueId($this->traderEid); - $this->putEntityUniqueId($this->playerEid); - $this->putString($this->displayName); - $this->putBool($this->isV2Trading); - $this->putBool($this->isWilling); - $this->put($this->offers); - } - - public function handle(NetworkSession $session) : bool{ - return $session->handleUpdateTrade($this); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/ChunkCacheBlob.php b/src/pocketmine/network/mcpe/protocol/types/ChunkCacheBlob.php deleted file mode 100644 index a20137620e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/ChunkCacheBlob.php +++ /dev/null @@ -1,47 +0,0 @@ -hash = $hash; - $this->payload = $payload; - } - - public function getHash() : int{ - return $this->hash; - } - - public function getPayload() : string{ - return $this->payload; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandData.php b/src/pocketmine/network/mcpe/protocol/types/CommandData.php deleted file mode 100644 index cf98b90fc3..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/CommandData.php +++ /dev/null @@ -1,40 +0,0 @@ -enumValues[$valueOffset])){ - throw new \InvalidArgumentException("Invalid enum value offset $valueOffset"); - } - $this->enum = $enum; - $this->valueOffset = $valueOffset; - $this->constraints = $constraints; - } - - public function getEnum() : CommandEnum{ - return $this->enum; - } - - public function getValueOffset() : int{ - return $this->valueOffset; - } - - public function getAffectedValue() : string{ - return $this->enum->enumValues[$this->valueOffset]; - } - - /** - * @return int[] - */ - public function getConstraints() : array{ - return $this->constraints; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php b/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php deleted file mode 100644 index a2f1a26ed6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php +++ /dev/null @@ -1,42 +0,0 @@ -canModifyBlocks = $canModifyBlocks; - } - - public function getCanModifyBlocks() : ?bool{ return $this->canModifyBlocks; } - - public static function read(NetworkBinaryStream $in) : self{ - $canModifyBlocks = $in->getBool() ? $in->getBool() : null; - return new self($canModifyBlocks); - } - - public function write(NetworkBinaryStream $out) : void{ - if($this->canModifyBlocks !== null){ - $out->putBool(true); - $out->putBool($this->canModifyBlocks); - }else{ - $out->putBool(false); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/EducationSettingsExternalLinkSettings.php b/src/pocketmine/network/mcpe/protocol/types/EducationSettingsExternalLinkSettings.php deleted file mode 100644 index 56cca23605..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/EducationSettingsExternalLinkSettings.php +++ /dev/null @@ -1,52 +0,0 @@ -displayName = $displayName; - $this->url = $url; - } - - public function getUrl() : string{ return $this->url; } - - public function getDisplayName() : string{ return $this->displayName; } - - public static function read(NetworkBinaryStream $in) : self{ - $url = $in->getString(); - $displayName = $in->getString(); - return new self($displayName, $url); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putString($this->url); - $out->putString($this->displayName); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/EducationUriResource.php b/src/pocketmine/network/mcpe/protocol/types/EducationUriResource.php deleted file mode 100644 index 9633a78429..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/EducationUriResource.php +++ /dev/null @@ -1,51 +0,0 @@ -buttonName = $buttonName; - $this->linkUri = $linkUri; - } - - public function getButtonName() : string{ return $this->buttonName; } - - public function getLinkUri() : string{ return $this->linkUri; } - - public static function read(NetworkBinaryStream $in) : self{ - $buttonName = $in->getString(); - $linkUri = $in->getString(); - return new self($buttonName, $linkUri); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putString($this->buttonName); - $out->putString($this->linkUri); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/Enchant.php b/src/pocketmine/network/mcpe/protocol/types/Enchant.php deleted file mode 100644 index e9e2f1dcc6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/Enchant.php +++ /dev/null @@ -1,53 +0,0 @@ -id = $id; - $this->level = $level; - } - - public function getId() : int{ return $this->id; } - - public function getLevel() : int{ return $this->level; } - - public static function read(NetworkBinaryStream $in) : self{ - $id = $in->getByte(); - $level = $in->getByte(); - return new self($id, $level); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->id); - $out->putByte($this->level); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/EnchantOption.php b/src/pocketmine/network/mcpe/protocol/types/EnchantOption.php deleted file mode 100644 index c066b78e23..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/EnchantOption.php +++ /dev/null @@ -1,126 +0,0 @@ -cost = $cost; - $this->slotFlags = $slotFlags; - $this->equipActivatedEnchantments = $equipActivatedEnchantments; - $this->heldActivatedEnchantments = $heldActivatedEnchantments; - $this->selfActivatedEnchantments = $selfActivatedEnchantments; - $this->name = $name; - $this->optionId = $optionId; - } - - public function getCost() : int{ return $this->cost; } - - public function getSlotFlags() : int{ return $this->slotFlags; } - - /** @return Enchant[] */ - public function getEquipActivatedEnchantments() : array{ return $this->equipActivatedEnchantments; } - - /** @return Enchant[] */ - public function getHeldActivatedEnchantments() : array{ return $this->heldActivatedEnchantments; } - - /** @return Enchant[] */ - public function getSelfActivatedEnchantments() : array{ return $this->selfActivatedEnchantments; } - - public function getName() : string{ return $this->name; } - - public function getOptionId() : int{ return $this->optionId; } - - /** - * @return Enchant[] - */ - private static function readEnchantList(NetworkBinaryStream $in) : array{ - $result = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $result[] = Enchant::read($in); - } - return $result; - } - - /** - * @param Enchant[] $list - */ - private static function writeEnchantList(NetworkBinaryStream $out, array $list) : void{ - $out->putUnsignedVarInt(count($list)); - foreach($list as $item){ - $item->write($out); - } - } - - public static function read(NetworkBinaryStream $in) : self{ - $cost = $in->getUnsignedVarInt(); - - $slotFlags = $in->getLInt(); - $equipActivatedEnchants = self::readEnchantList($in); - $heldActivatedEnchants = self::readEnchantList($in); - $selfActivatedEnchants = self::readEnchantList($in); - - $name = $in->getString(); - $optionId = $in->readGenericTypeNetworkId(); - - return new self($cost, $slotFlags, $equipActivatedEnchants, $heldActivatedEnchants, $selfActivatedEnchants, $name, $optionId); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putUnsignedVarInt($this->cost); - - $out->putLInt($this->slotFlags); - self::writeEnchantList($out, $this->equipActivatedEnchantments); - self::writeEnchantList($out, $this->heldActivatedEnchantments); - self::writeEnchantList($out, $this->selfActivatedEnchantments); - - $out->putString($this->name); - $out->writeGenericTypeNetworkId($this->optionId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/EntityLink.php b/src/pocketmine/network/mcpe/protocol/types/EntityLink.php deleted file mode 100644 index bbd5a478f4..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/EntityLink.php +++ /dev/null @@ -1,50 +0,0 @@ -fromEntityUniqueId = $fromEntityUniqueId; - $this->toEntityUniqueId = $toEntityUniqueId; - $this->type = $type; - $this->immediate = $immediate; - $this->causedByRider = $causedByRider; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/Experiments.php b/src/pocketmine/network/mcpe/protocol/types/Experiments.php deleted file mode 100644 index 8c3196dbb2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/Experiments.php +++ /dev/null @@ -1,72 +0,0 @@ - - */ - private $experiments; - /** @var bool */ - private $hasPreviouslyUsedExperiments; - - /** - * @param bool[] $experiments - * @phpstan-param array $experiments - */ - public function __construct(array $experiments, bool $hasPreviouslyUsedExperiments){ - $this->experiments = $experiments; - $this->hasPreviouslyUsedExperiments = $hasPreviouslyUsedExperiments; - } - - /** @return bool[] */ - public function getExperiments() : array{ return $this->experiments; } - - public function hasPreviouslyUsedExperiments() : bool{ return $this->hasPreviouslyUsedExperiments; } - - public static function read(NetworkBinaryStream $in) : self{ - $experiments = []; - for($i = 0, $len = $in->getLInt(); $i < $len; ++$i){ - $experimentName = $in->getString(); - $enabled = $in->getBool(); - $experiments[$experimentName] = $enabled; - } - $hasPreviouslyUsedExperiments = $in->getBool(); - return new self($experiments, $hasPreviouslyUsedExperiments); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putLInt(count($this->experiments)); - foreach($this->experiments as $experimentName => $enabled){ - $out->putString($experimentName); - $out->putBool($enabled); - } - $out->putBool($this->hasPreviouslyUsedExperiments); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/GameMode.php b/src/pocketmine/network/mcpe/protocol/types/GameMode.php deleted file mode 100644 index 616c92259a..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/GameMode.php +++ /dev/null @@ -1,38 +0,0 @@ -stringId = $stringId; - $this->numericId = $numericId; - $this->componentBased = $componentBased; - } - - public function getStringId() : string{ return $this->stringId; } - - public function getNumericId() : int{ return $this->numericId; } - - public function isComponentBased() : bool{ return $this->componentBased; } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/MapDecoration.php b/src/pocketmine/network/mcpe/protocol/types/MapDecoration.php deleted file mode 100644 index 564cdf4bf6..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/MapDecoration.php +++ /dev/null @@ -1,74 +0,0 @@ -icon = $icon; - $this->rotation = $rotation; - $this->xOffset = $xOffset; - $this->yOffset = $yOffset; - $this->label = $label; - $this->color = $color; - } - - public function getIcon() : int{ - return $this->icon; - } - - public function getRotation() : int{ - return $this->rotation; - } - - public function getXOffset() : int{ - return $this->xOffset; - } - - public function getYOffset() : int{ - return $this->yOffset; - } - - public function getLabel() : string{ - return $this->label; - } - - public function getColor() : Color{ - return $this->color; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipe.php b/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipe.php deleted file mode 100644 index 057c2124cf..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipe.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - private array $outputs; - - /** - * @param MaterialReducerRecipeOutput[] $outputs - * @phpstan-param list $outputs - */ - public function __construct(int $inputItemId, int $inputItemMeta, array $outputs){ - $this->inputItemId = $inputItemId; - $this->inputItemMeta = $inputItemMeta; - $this->outputs = $outputs; - } - - public function getInputItemId() : int{ return $this->inputItemId; } - - public function getInputItemMeta() : int{ return $this->inputItemMeta; } - - /** @return MaterialReducerRecipeOutput[] */ - public function getOutputs() : array{ return $this->outputs; } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipeOutput.php b/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipeOutput.php deleted file mode 100644 index 4d06366c07..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/MaterialReducerRecipeOutput.php +++ /dev/null @@ -1,39 +0,0 @@ -itemId = $itemId; - $this->count = $count; - } - - public function getItemId() : int{ return $this->itemId; } - - public function getCount() : int{ return $this->count; } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/MultiplayerGameVisibility.php b/src/pocketmine/network/mcpe/protocol/types/MultiplayerGameVisibility.php deleted file mode 100644 index 5af4382e4c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/MultiplayerGameVisibility.php +++ /dev/null @@ -1,37 +0,0 @@ -sourceType = $packet->getUnsignedVarInt(); - - switch($this->sourceType){ - case self::SOURCE_CONTAINER: - $this->windowId = $packet->getVarInt(); - break; - case self::SOURCE_WORLD: - $this->sourceFlags = $packet->getUnsignedVarInt(); - break; - case self::SOURCE_CREATIVE: - break; - case self::SOURCE_TODO: - $this->windowId = $packet->getVarInt(); - break; - default: - throw new \UnexpectedValueException("Unknown inventory action source type $this->sourceType"); - } - - $this->inventorySlot = $packet->getUnsignedVarInt(); - $this->oldItem = ItemStackWrapper::read($packet); - $this->newItem = ItemStackWrapper::read($packet); - - return $this; - } - - /** - * @return void - */ - public function write(NetworkBinaryStream $packet){ - $packet->putUnsignedVarInt($this->sourceType); - - switch($this->sourceType){ - case self::SOURCE_CONTAINER: - $packet->putVarInt($this->windowId); - break; - case self::SOURCE_WORLD: - $packet->putUnsignedVarInt($this->sourceFlags); - break; - case self::SOURCE_CREATIVE: - break; - case self::SOURCE_TODO: - $packet->putVarInt($this->windowId); - break; - default: - throw new \InvalidArgumentException("Unknown inventory action source type $this->sourceType"); - } - - $packet->putUnsignedVarInt($this->inventorySlot); - $this->oldItem->write($packet); - $this->newItem->write($packet); - } - - /** - * @return InventoryAction|null - * - * @throws \UnexpectedValueException - */ - public function createInventoryAction(Player $player){ - $oldItem = $this->oldItem->getItemStack(); - $newItem = $this->newItem->getItemStack(); - if($oldItem->equalsExact($newItem)){ - //filter out useless noise in 1.13 - return null; - } - switch($this->sourceType){ - case self::SOURCE_CONTAINER: - if($this->windowId === ContainerIds::UI and $this->inventorySlot > 0){ - if($this->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){ - return null; //useless noise - } - if(array_key_exists($this->inventorySlot, UIInventorySlotOffset::CRAFTING2X2_INPUT)){ - $window = $player->getCraftingGrid(); - if($window->getGridWidth() !== CraftingGrid::SIZE_SMALL){ - throw new \UnexpectedValueException("Expected small crafting grid"); - } - $slot = UIInventorySlotOffset::CRAFTING2X2_INPUT[$this->inventorySlot]; - }elseif(array_key_exists($this->inventorySlot, UIInventorySlotOffset::CRAFTING3X3_INPUT)){ - $window = $player->getCraftingGrid(); - if($window->getGridWidth() !== CraftingGrid::SIZE_BIG){ - throw new \UnexpectedValueException("Expected big crafting grid"); - } - $slot = UIInventorySlotOffset::CRAFTING3X3_INPUT[$this->inventorySlot]; - }else{ - throw new \UnexpectedValueException("Unhandled magic UI slot offset $this->inventorySlot"); - } - }else{ - $window = $player->getWindow($this->windowId); - $slot = $this->inventorySlot; - } - if($window !== null){ - return new SlotChangeAction($window, $slot, $oldItem, $newItem); - } - - throw new \UnexpectedValueException("Player " . $player->getName() . " has no open container with window ID $this->windowId"); - case self::SOURCE_WORLD: - if($this->inventorySlot !== self::ACTION_MAGIC_SLOT_DROP_ITEM){ - throw new \UnexpectedValueException("Only expecting drop-item world actions from the client!"); - } - - return new DropItemAction($newItem); - case self::SOURCE_CREATIVE: - switch($this->inventorySlot){ - case self::ACTION_MAGIC_SLOT_CREATIVE_DELETE_ITEM: - $type = CreativeInventoryAction::TYPE_DELETE_ITEM; - break; - case self::ACTION_MAGIC_SLOT_CREATIVE_CREATE_ITEM: - $type = CreativeInventoryAction::TYPE_CREATE_ITEM; - break; - default: - throw new \UnexpectedValueException("Unexpected creative action type $this->inventorySlot"); - - } - - return new CreativeInventoryAction($oldItem, $newItem, $type); - case self::SOURCE_TODO: - //These types need special handling. - switch($this->windowId){ - case self::SOURCE_TYPE_CRAFTING_RESULT: - case self::SOURCE_TYPE_CRAFTING_USE_INGREDIENT: - return null; - } - - //TODO: more stuff - throw new \UnexpectedValueException("Player " . $player->getName() . " has no open container with window ID $this->windowId"); - default: - throw new \UnexpectedValueException("Unknown inventory source type $this->sourceType"); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/PersonaSkinPiece.php b/src/pocketmine/network/mcpe/protocol/types/PersonaSkinPiece.php deleted file mode 100644 index bb22a13507..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/PersonaSkinPiece.php +++ /dev/null @@ -1,77 +0,0 @@ -pieceId = $pieceId; - $this->pieceType = $pieceType; - $this->packId = $packId; - $this->isDefaultPiece = $isDefaultPiece; - $this->productId = $productId; - } - - public function getPieceId() : string{ - return $this->pieceId; - } - - public function getPieceType() : string{ - return $this->pieceType; - } - - public function getPackId() : string{ - return $this->packId; - } - - public function isDefaultPiece() : bool{ - return $this->isDefaultPiece; - } - - public function getProductId() : string{ - return $this->productId; - } -} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/protocol/types/PlayMode.php b/src/pocketmine/network/mcpe/protocol/types/PlayMode.php deleted file mode 100644 index 37ccc066d2..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/PlayMode.php +++ /dev/null @@ -1,45 +0,0 @@ -uuid = $uuid; - - return $entry; - } - - public static function createAdditionEntry(UUID $uuid, int $entityUniqueId, string $username, SkinData $skinData, string $xboxUserId = "", string $platformChatId = "", int $buildPlatform = -1, bool $isTeacher = false, bool $isHost = false) : PlayerListEntry{ - $entry = new PlayerListEntry(); - $entry->uuid = $uuid; - $entry->entityUniqueId = $entityUniqueId; - $entry->username = $username; - $entry->skinData = $skinData; - $entry->xboxUserId = $xboxUserId; - $entry->platformChatId = $platformChatId; - $entry->buildPlatform = $buildPlatform; - $entry->isTeacher = $isTeacher; - $entry->isHost = $isHost; - - return $entry; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/PlayerMovementSettings.php b/src/pocketmine/network/mcpe/protocol/types/PlayerMovementSettings.php deleted file mode 100644 index 67f2015caf..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/PlayerMovementSettings.php +++ /dev/null @@ -1,61 +0,0 @@ -movementType = $movementType; - $this->rewindHistorySize = $rewindHistorySize; - //do not ask me what the F this is doing here - $this->serverAuthoritativeBlockBreaking = $serverAuthoritativeBlockBreaking; - } - - public function getMovementType() : int{ return $this->movementType; } - - public function getRewindHistorySize() : int{ return $this->rewindHistorySize; } - - public function isServerAuthoritativeBlockBreaking() : bool{ return $this->serverAuthoritativeBlockBreaking; } - - public static function read(NetworkBinaryStream $in) : self{ - $movementType = $in->getVarInt(); - $rewindHistorySize = $in->getVarInt(); - $serverAuthBlockBreaking = $in->getBool(); - return new self($movementType, $rewindHistorySize, $serverAuthBlockBreaking); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putVarInt($this->movementType); - $out->putVarInt($this->rewindHistorySize); - $out->putBool($this->serverAuthoritativeBlockBreaking); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/PlayerMovementType.php b/src/pocketmine/network/mcpe/protocol/types/PlayerMovementType.php deleted file mode 100644 index 0521742ba1..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/PlayerMovementType.php +++ /dev/null @@ -1,31 +0,0 @@ -inputItemId = $inputItemId; - $this->ingredientItemId = $ingredientItemId; - $this->outputItemId = $outputItemId; - } - - public function getInputItemId() : int{ - return $this->inputItemId; - } - - public function getIngredientItemId() : int{ - return $this->ingredientItemId; - } - - public function getOutputItemId() : int{ - return $this->outputItemId; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php b/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php deleted file mode 100644 index 6ad42cc6fe..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php +++ /dev/null @@ -1,72 +0,0 @@ -inputItemId = $inputItemId; - $this->inputItemMeta = $inputItemMeta; - $this->ingredientItemId = $ingredientItemId; - $this->ingredientItemMeta = $ingredientItemMeta; - $this->outputItemId = $outputItemId; - $this->outputItemMeta = $outputItemMeta; - } - - public function getInputItemId() : int{ - return $this->inputItemId; - } - - public function getInputItemMeta() : int{ - return $this->inputItemMeta; - } - - public function getIngredientItemId() : int{ - return $this->ingredientItemId; - } - - public function getIngredientItemMeta() : int{ - return $this->ingredientItemMeta; - } - - public function getOutputItemId() : int{ - return $this->outputItemId; - } - - public function getOutputItemMeta() : int{ - return $this->outputItemMeta; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/ScorePacketEntry.php b/src/pocketmine/network/mcpe/protocol/types/ScorePacketEntry.php deleted file mode 100644 index ce8c0d07fc..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/ScorePacketEntry.php +++ /dev/null @@ -1,45 +0,0 @@ -image = $image; - $this->type = $type; - $this->frames = $frames; - $this->expressionType = $expressionType; - } - - /** - * Image of the animation. - */ - public function getImage() : SkinImage{ - return $this->image; - } - - /** - * The type of animation you are applying. - */ - public function getType() : int{ - return $this->type; - } - - /** - * The total amount of frames in an animation. - */ - public function getFrames() : float{ - return $this->frames; - } - - public function getExpressionType() : int{ return $this->expressionType; } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/SkinData.php b/src/pocketmine/network/mcpe/protocol/types/SkinData.php deleted file mode 100644 index 2291646bee..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/SkinData.php +++ /dev/null @@ -1,192 +0,0 @@ -skinId = $skinId; - $this->playFabId = $playFabId; - $this->resourcePatch = $resourcePatch; - $this->skinImage = $skinImage; - $this->animations = $animations; - $this->capeImage = $capeImage ?? new SkinImage(0, 0, ""); - $this->geometryData = $geometryData; - $this->geometryDataEngineVersion = $geometryDataEngineVersion; - $this->animationData = $animationData; - $this->capeId = $capeId; - //this has to be unique or the client will do stupid things - $this->fullSkinId = $fullSkinId ?? UUID::fromRandom()->toString(); - $this->armSize = $armSize; - $this->skinColor = $skinColor; - $this->personaPieces = $personaPieces; - $this->pieceTintColors = $pieceTintColors; - $this->isVerified = $isVerified; - $this->premium = $premium; - $this->persona = $persona; - $this->personaCapeOnClassic = $personaCapeOnClassic; - $this->isPrimaryUser = $isPrimaryUser; - } - - public function getSkinId() : string{ - return $this->skinId; - } - - public function getPlayFabId() : string{ return $this->playFabId; } - - public function getResourcePatch() : string{ - return $this->resourcePatch; - } - - public function getSkinImage() : SkinImage{ - return $this->skinImage; - } - - /** - * @return SkinAnimation[] - */ - public function getAnimations() : array{ - return $this->animations; - } - - public function getCapeImage() : SkinImage{ - return $this->capeImage; - } - - public function getGeometryData() : string{ - return $this->geometryData; - } - - public function getGeometryDataEngineVersion() : string{ return $this->geometryDataEngineVersion; } - - public function getAnimationData() : string{ - return $this->animationData; - } - - public function getCapeId() : string{ - return $this->capeId; - } - - public function getFullSkinId() : string{ - return $this->fullSkinId; - } - - public function getArmSize() : string{ - return $this->armSize; - } - - public function getSkinColor() : string{ - return $this->skinColor; - } - - /** - * @return PersonaSkinPiece[] - */ - public function getPersonaPieces() : array{ - return $this->personaPieces; - } - - /** - * @return PersonaPieceTintColor[] - */ - public function getPieceTintColors() : array{ - return $this->pieceTintColors; - } - - public function isPersona() : bool{ - return $this->persona; - } - - public function isPremium() : bool{ - return $this->premium; - } - - public function isPersonaCapeOnClassic() : bool{ - return $this->personaCapeOnClassic; - } - - public function isPrimaryUser() : bool{ return $this->isPrimaryUser; } - - public function isVerified() : bool{ - return $this->isVerified; - } - - /** - * @internal - */ - public function setVerified(bool $verified) : void{ - $this->isVerified = $verified; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/SkinImage.php b/src/pocketmine/network/mcpe/protocol/types/SkinImage.php deleted file mode 100644 index e6201524df..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/SkinImage.php +++ /dev/null @@ -1,73 +0,0 @@ -height = $height; - $this->width = $width; - $this->data = $data; - } - - public static function fromLegacy(string $data) : SkinImage{ - switch(strlen($data)){ - case 64 * 32 * 4: - return new self(32, 64, $data); - case 64 * 64 * 4: - return new self(64, 64, $data); - case 128 * 128 * 4: - return new self(128, 128, $data); - } - - throw new \InvalidArgumentException("Unknown size"); - } - - public function getHeight() : int{ - return $this->height; - } - - public function getWidth() : int{ - return $this->width; - } - - public function getData() : string{ - return $this->data; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/SpawnSettings.php b/src/pocketmine/network/mcpe/protocol/types/SpawnSettings.php deleted file mode 100644 index f592d40533..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/SpawnSettings.php +++ /dev/null @@ -1,73 +0,0 @@ -biomeType = $biomeType; - $this->biomeName = $biomeName; - $this->dimension = $dimension; - } - - public function getBiomeType() : int{ - return $this->biomeType; - } - - public function getBiomeName() : string{ - return $this->biomeName; - } - - /** - * @see DimensionIds - */ - public function getDimension() : int{ - return $this->dimension; - } - - public static function read(NetworkBinaryStream $in) : self{ - $biomeType = $in->getLShort(); - $biomeName = $in->getString(); - $dimension = $in->getVarInt(); - - return new self($biomeType, $biomeName, $dimension); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putLShort($this->biomeType); - $out->putString($this->biomeName); - $out->putVarInt($this->dimension); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/StructureEditorData.php b/src/pocketmine/network/mcpe/protocol/types/StructureEditorData.php deleted file mode 100644 index 9e7f042631..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/StructureEditorData.php +++ /dev/null @@ -1,48 +0,0 @@ - $heights - */ - public function __construct(private array $heights){ - if(count($heights) !== 256){ - throw new \InvalidArgumentException("Expected exactly 256 heightmap values"); - } - } - - /** @return int[] */ - public function getHeights() : array{ return $this->heights; } - - public function getHeight(int $x, int $z) : int{ - return $this->heights[(($z & 0xf) << 4) | ($x & 0xf)]; - } - - public static function read(NetworkBinaryStream $in) : self{ - $heights = []; - for($i = 0; $i < 256; ++$i){ - $heights[] = Binary::signByte($in->getByte()); - } - return new self($heights); - } - - public function write(NetworkBinaryStream $out) : void{ - for($i = 0; $i < 256; ++$i){ - $out->putByte(Binary::unsignByte($this->heights[$i])); - } - } - - public static function allTooLow() : self{ - return new self(array_fill(0, 256, -1)); - } - - public static function allTooHigh() : self{ - return new self(array_fill(0, 256, 16)); - } - - public function isAllTooLow() : bool{ - foreach($this->heights as $height){ - if($height >= 0){ - return false; - } - } - return true; - } - - public function isAllTooHigh() : bool{ - foreach($this->heights as $height){ - if($height <= 15){ - return false; - } - } - return true; - } -} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/protocol/types/SubChunkPacketHeightMapType.php b/src/pocketmine/network/mcpe/protocol/types/SubChunkPacketHeightMapType.php deleted file mode 100644 index 7b1d4105d0..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/SubChunkPacketHeightMapType.php +++ /dev/null @@ -1,32 +0,0 @@ -x = $x; - $this->y = $y; - $this->z = $z; - $this->blockRuntimeId = $blockRuntimeId; - $this->flags = $flags; - $this->syncedUpdateEntityUniqueId = $syncedUpdateEntityUniqueId; - $this->syncedUpdateType = $syncedUpdateType; - } - - public static function simple(int $x, int $y, int $z, int $blockRuntimeId) : self{ - return new self($x, $y, $z, $blockRuntimeId, UpdateBlockPacket::FLAG_NETWORK, 0, 0); - } - - public function getX() : int{ return $this->x; } - - public function getY() : int{ return $this->y; } - - public function getZ() : int{ return $this->z; } - - public function getBlockRuntimeId() : int{ return $this->blockRuntimeId; } - - public function getFlags() : int{ return $this->flags; } - - public function getSyncedUpdateEntityUniqueId() : int{ return $this->syncedUpdateEntityUniqueId; } - - public function getSyncedUpdateType() : int{ return $this->syncedUpdateType; } - - public static function read(NetworkBinaryStream $in) : self{ - $x = $y = $z = 0; - $in->getBlockPosition($x, $y, $z); - $blockRuntimeId = $in->getUnsignedVarInt(); - $updateFlags = $in->getUnsignedVarInt(); - $syncedUpdateEntityUniqueId = $in->getUnsignedVarLong(); //this can't use the standard method because it's unsigned as opposed to the usual signed... !!!!!! - $syncedUpdateType = $in->getUnsignedVarInt(); //this isn't even consistent with UpdateBlockSyncedPacket?! - - return new self($x, $y, $z, $blockRuntimeId, $updateFlags, $syncedUpdateEntityUniqueId, $syncedUpdateType); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putBlockPosition($this->x, $this->y, $this->z); - $out->putUnsignedVarInt($this->blockRuntimeId); - $out->putUnsignedVarInt($this->flags); - $out->putUnsignedVarLong($this->syncedUpdateEntityUniqueId); - $out->putUnsignedVarInt($this->syncedUpdateType); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/WindowTypes.php b/src/pocketmine/network/mcpe/protocol/types/WindowTypes.php deleted file mode 100644 index 583a3384c3..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/WindowTypes.php +++ /dev/null @@ -1,66 +0,0 @@ -containerId = $containerId; - $this->changedSlotIndexes = $changedSlotIndexes; - } - - public function getContainerId() : int{ return $this->containerId; } - - /** @return int[] */ - public function getChangedSlotIndexes() : array{ return $this->changedSlotIndexes; } - - public static function read(NetworkBinaryStream $in) : self{ - $containerId = $in->getByte(); - $changedSlots = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $changedSlots[] = $in->getByte(); - } - return new self($containerId, $changedSlots); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->containerId); - $out->putUnsignedVarInt(count($this->changedSlotIndexes)); - foreach($this->changedSlotIndexes as $index){ - $out->putByte($index); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/ItemStackWrapper.php b/src/pocketmine/network/mcpe/protocol/types/inventory/ItemStackWrapper.php deleted file mode 100644 index 07ce6bf73d..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/ItemStackWrapper.php +++ /dev/null @@ -1,68 +0,0 @@ -stackId = $stackId; - $this->itemStack = $itemStack; - } - - public static function legacy(Item $itemStack) : self{ - return new self($itemStack->isNull() ? 0 : 1, $itemStack); - } - - public function getStackId() : int{ return $this->stackId; } - - public function getItemStack() : Item{ return $this->itemStack; } - - public static function read(NetworkBinaryStream $in) : self{ - $stackId = 0; - $stack = $in->getItemStack(function(NetworkBinaryStream $in) use (&$stackId) : void{ - $hasNetId = $in->getBool(); - if($hasNetId){ - $stackId = $in->readGenericTypeNetworkId(); - } - }); - return new self($stackId, $stack); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putItemStack($this->itemStack, function(NetworkBinaryStream $out) : void{ - $out->putBool($this->stackId !== 0); - if($this->stackId !== 0){ - $out->writeGenericTypeNetworkId($this->stackId); - } - }); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/MismatchTransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/MismatchTransactionData.php deleted file mode 100644 index 2ee19c64d7..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/MismatchTransactionData.php +++ /dev/null @@ -1,50 +0,0 @@ -actions) > 0){ - throw new PacketDecodeException("Mismatch transaction type should not have any actions associated with it, but got " . count($this->actions)); - } - } - - protected function encodeData(PacketSerializer $stream) : void{ - - } - - public static function new() : self{ - return new self; //no arguments - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/NormalTransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/NormalTransactionData.php deleted file mode 100644 index 757bf9d629..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/NormalTransactionData.php +++ /dev/null @@ -1,54 +0,0 @@ -actions = $actions; - return $result; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/ReleaseItemTransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/ReleaseItemTransactionData.php deleted file mode 100644 index 5f05af8363..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/ReleaseItemTransactionData.php +++ /dev/null @@ -1,91 +0,0 @@ -actionType; - } - - public function getHotbarSlot() : int{ - return $this->hotbarSlot; - } - - public function getItemInHand() : ItemStackWrapper{ - return $this->itemInHand; - } - - public function getHeadPos() : Vector3{ - return $this->headPos; - } - - public function getTypeId() : int{ - return InventoryTransactionPacket::TYPE_RELEASE_ITEM; - } - - protected function decodeData(PacketSerializer $stream) : void{ - $this->actionType = $stream->getUnsignedVarInt(); - $this->hotbarSlot = $stream->getVarInt(); - $this->itemInHand = ItemStackWrapper::read($stream); - $this->headPos = $stream->getVector3(); - } - - protected function encodeData(PacketSerializer $stream) : void{ - $stream->putUnsignedVarInt($this->actionType); - $stream->putVarInt($this->hotbarSlot); - $this->itemInHand->write($stream); - $stream->putVector3($this->headPos); - } - - /** - * @param NetworkInventoryAction[] $actions - */ - public static function new(array $actions, int $actionType, int $hotbarSlot, ItemStackWrapper $itemInHand, Vector3 $headPos) : self{ - $result = new self; - $result->actions = $actions; - $result->actionType = $actionType; - $result->hotbarSlot = $hotbarSlot; - $result->itemInHand = $itemInHand; - $result->headPos = $headPos; - - return $result; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/TransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/TransactionData.php deleted file mode 100644 index 1f4cc13047..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/TransactionData.php +++ /dev/null @@ -1,72 +0,0 @@ -actions; - } - - abstract public function getTypeId() : int; - - /** - * @throws BinaryDataException - * @throws PacketDecodeException - */ - final public function decode(PacketSerializer $stream) : void{ - $actionCount = $stream->getUnsignedVarInt(); - for($i = 0; $i < $actionCount; ++$i){ - $this->actions[] = (new NetworkInventoryAction())->read($stream); - } - $this->decodeData($stream); - } - - /** - * @throws BinaryDataException - * @throws PacketDecodeException - */ - abstract protected function decodeData(PacketSerializer $stream) : void; - - final public function encode(PacketSerializer $stream) : void{ - $stream->putUnsignedVarInt(count($this->actions)); - foreach($this->actions as $action){ - $action->write($stream); - } - $this->encodeData($stream); - } - - abstract protected function encodeData(PacketSerializer $stream) : void; -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/UIInventorySlotOffset.php b/src/pocketmine/network/mcpe/protocol/types/inventory/UIInventorySlotOffset.php deleted file mode 100644 index 12e2c60eb7..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/UIInventorySlotOffset.php +++ /dev/null @@ -1,105 +0,0 @@ - 0, - 2 => 1, - ]; - public const STONE_CUTTER_INPUT = 3; - public const TRADE2_INGREDIENT = [ - 4 => 0, - 5 => 1, - ]; - public const TRADE_INGREDIENT = [ - 6 => 0, - 7 => 1, - ]; - public const MATERIAL_REDUCER_INPUT = 8; - public const LOOM = [ - 9 => 0, - 10 => 1, - 11 => 2, - ]; - public const CARTOGRAPHY_TABLE = [ - 12 => 0, - 13 => 1, - ]; - public const ENCHANTING_TABLE = [ - 14 => 0, - 15 => 1, - ]; - public const GRINDSTONE = [ - 16 => 0, - 17 => 1, - ]; - public const COMPOUND_CREATOR_INPUT = [ - 18 => 0, - 19 => 1, - 20 => 2, - 21 => 3, - 22 => 4, - 23 => 5, - 24 => 6, - 25 => 7, - 26 => 8, - ]; - public const BEACON_PAYMENT = 27; - public const CRAFTING2X2_INPUT = [ - 28 => 0, - 29 => 1, - 30 => 2, - 31 => 3, - ]; - public const CRAFTING3X3_INPUT = [ - 32 => 0, - 33 => 1, - 34 => 2, - 35 => 3, - 36 => 4, - 37 => 5, - 38 => 6, - 39 => 7, - 40 => 8, - ]; - public const MATERIAL_REDUCER_OUTPUT = [ - 41 => 0, - 42 => 1, - 43 => 2, - 44 => 3, - 45 => 4, - 46 => 5, - 47 => 6, - 48 => 7, - 49 => 8, - ]; - public const CREATED_ITEM_OUTPUT = 50; -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemOnEntityTransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemOnEntityTransactionData.php deleted file mode 100644 index d7fafbe9e1..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemOnEntityTransactionData.php +++ /dev/null @@ -1,109 +0,0 @@ -entityRuntimeId; - } - - public function getActionType() : int{ - return $this->actionType; - } - - public function getHotbarSlot() : int{ - return $this->hotbarSlot; - } - - public function getItemInHand() : ItemStackWrapper{ - return $this->itemInHand; - } - - public function getPlayerPos() : Vector3{ - return $this->playerPos; - } - - public function getClickPos() : Vector3{ - return $this->clickPos; - } - - public function getTypeId() : int{ - return InventoryTransactionPacket::TYPE_USE_ITEM_ON_ENTITY; - } - - protected function decodeData(PacketSerializer $stream) : void{ - $this->entityRuntimeId = $stream->getEntityRuntimeId(); - $this->actionType = $stream->getUnsignedVarInt(); - $this->hotbarSlot = $stream->getVarInt(); - $this->itemInHand = ItemStackWrapper::read($stream); - $this->playerPos = $stream->getVector3(); - $this->clickPos = $stream->getVector3(); - } - - protected function encodeData(PacketSerializer $stream) : void{ - $stream->putEntityRuntimeId($this->entityRuntimeId); - $stream->putUnsignedVarInt($this->actionType); - $stream->putVarInt($this->hotbarSlot); - $this->itemInHand->write($stream); - $stream->putVector3($this->playerPos); - $stream->putVector3($this->clickPos); - } - - /** - * @param NetworkInventoryAction[] $actions - */ - public static function new(array $actions, int $entityRuntimeId, int $actionType, int $hotbarSlot, ItemStackWrapper $itemInHand, Vector3 $playerPos, Vector3 $clickPos) : self{ - $result = new self; - $result->actions = $actions; - $result->entityRuntimeId = $entityRuntimeId; - $result->actionType = $actionType; - $result->hotbarSlot = $hotbarSlot; - $result->itemInHand = $itemInHand; - $result->playerPos = $playerPos; - $result->clickPos = $clickPos; - return $result; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php b/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php deleted file mode 100644 index 4eeb041b16..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php +++ /dev/null @@ -1,129 +0,0 @@ -actionType; - } - - public function getBlockPos() : Vector3{ - return $this->blockPos; - } - - public function getFace() : int{ - return $this->face; - } - - public function getHotbarSlot() : int{ - return $this->hotbarSlot; - } - - public function getItemInHand() : ItemStackWrapper{ - return $this->itemInHand; - } - - public function getPlayerPos() : Vector3{ - return $this->playerPos; - } - - public function getClickPos() : Vector3{ - return $this->clickPos; - } - - public function getBlockRuntimeId() : int{ - return $this->blockRuntimeId; - } - - public function getTypeId() : int{ - return InventoryTransactionPacket::TYPE_USE_ITEM; - } - - protected function decodeData(PacketSerializer $stream) : void{ - $this->actionType = $stream->getUnsignedVarInt(); - $x = $y = $z = 0; - $stream->getBlockPosition($x, $y, $z); - $this->blockPos = new Vector3($x, $y, $z); - $this->face = $stream->getVarInt(); - $this->hotbarSlot = $stream->getVarInt(); - $this->itemInHand = ItemStackWrapper::read($stream); - $this->playerPos = $stream->getVector3(); - $this->clickPos = $stream->getVector3(); - $this->blockRuntimeId = $stream->getUnsignedVarInt(); - } - - protected function encodeData(PacketSerializer $stream) : void{ - $stream->putUnsignedVarInt($this->actionType); - $stream->putBlockPosition($this->blockPos->x, $this->blockPos->y, $this->blockPos->z); - $stream->putVarInt($this->face); - $stream->putVarInt($this->hotbarSlot); - $this->itemInHand->write($stream); - $stream->putVector3($this->playerPos); - $stream->putVector3($this->clickPos); - $stream->putUnsignedVarInt($this->blockRuntimeId); - } - - /** - * @param NetworkInventoryAction[] $actions - */ - public static function new(array $actions, int $actionType, Vector3 $blockPos, int $face, int $hotbarSlot, ItemStackWrapper $itemInHand, Vector3 $playerPos, Vector3 $clickPos, int $blockRuntimeId) : self{ - $result = new self; - $result->actions = $actions; - $result->actionType = $actionType; - $result->blockPos = $blockPos; - $result->face = $face; - $result->hotbarSlot = $hotbarSlot; - $result->itemInHand = $itemInHand; - $result->playerPos = $playerPos; - $result->clickPos = $clickPos; - $result->blockRuntimeId = $blockRuntimeId; - return $result; - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/BeaconPaymentStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/BeaconPaymentStackRequestAction.php deleted file mode 100644 index c698073bdb..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/BeaconPaymentStackRequestAction.php +++ /dev/null @@ -1,59 +0,0 @@ -primaryEffectId = $primaryEffectId; - $this->secondaryEffectId = $secondaryEffectId; - } - - public function getPrimaryEffectId() : int{ return $this->primaryEffectId; } - - public function getSecondaryEffectId() : int{ return $this->secondaryEffectId; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::BEACON_PAYMENT; } - - public static function read(NetworkBinaryStream $in) : self{ - $primary = $in->getVarInt(); - $secondary = $in->getVarInt(); - return new self($primary, $secondary); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putVarInt($this->primaryEffectId); - $out->putVarInt($this->secondaryEffectId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeAutoStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeAutoStackRequestAction.php deleted file mode 100644 index e146afec07..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeAutoStackRequestAction.php +++ /dev/null @@ -1,60 +0,0 @@ -recipeId = $recipeId; - $this->repetitions = $repetitions; - } - - public function getRecipeId() : int{ return $this->recipeId; } - - public function getRepetitions() : int{ return $this->repetitions; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::CRAFTING_RECIPE_AUTO; } - - public static function read(NetworkBinaryStream $in) : self{ - $recipeId = $in->readGenericTypeNetworkId(); - $repetitions = $in->getByte(); - return new self($recipeId, $repetitions); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->recipeId); - $out->putByte($this->repetitions); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeOptionalStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeOptionalStackRequestAction.php deleted file mode 100644 index 9047947c89..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeOptionalStackRequestAction.php +++ /dev/null @@ -1,59 +0,0 @@ -recipeId = $type; - $this->filterStringIndex = $filterStringIndex; - } - - public function getRecipeId() : int{ return $this->recipeId; } - - public function getFilterStringIndex() : int{ return $this->filterStringIndex; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::CRAFTING_RECIPE_OPTIONAL; } - - public static function read(NetworkBinaryStream $in) : self{ - $recipeId = $in->readGenericTypeNetworkId(); - $filterStringIndex = $in->getLInt(); - return new self($recipeId, $filterStringIndex); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->recipeId); - $out->putLInt($this->filterStringIndex); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeStackRequestAction.php deleted file mode 100644 index 0a79642def..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftRecipeStackRequestAction.php +++ /dev/null @@ -1,52 +0,0 @@ -recipeId = $recipeId; - } - - public function getRecipeId() : int{ return $this->recipeId; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::CRAFTING_RECIPE; } - - public static function read(NetworkBinaryStream $in) : self{ - $recipeId = $in->readGenericTypeNetworkId(); - return new self($recipeId); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->recipeId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftingConsumeInputStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftingConsumeInputStackRequestAction.php deleted file mode 100644 index 26f85f7a79..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CraftingConsumeInputStackRequestAction.php +++ /dev/null @@ -1,33 +0,0 @@ -craftingGridSlot = $craftingGridSlot; - } - - public function getCraftingGridSlot() : int{ return $this->craftingGridSlot; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::CRAFTING_MARK_SECONDARY_RESULT_SLOT; } - - public static function read(NetworkBinaryStream $in) : self{ - $slot = $in->getByte(); - return new self($slot); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->craftingGridSlot); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CreativeCreateStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CreativeCreateStackRequestAction.php deleted file mode 100644 index 64b4aa7dfe..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/CreativeCreateStackRequestAction.php +++ /dev/null @@ -1,52 +0,0 @@ -creativeItemId = $creativeItemId; - } - - public function getCreativeItemId() : int{ return $this->creativeItemId; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::CREATIVE_CREATE; } - - public static function read(NetworkBinaryStream $in) : self{ - $creativeItemId = $in->readGenericTypeNetworkId(); - return new self($creativeItemId); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->creativeItemId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DeprecatedCraftingNonImplementedStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DeprecatedCraftingNonImplementedStackRequestAction.php deleted file mode 100644 index 031b410d8e..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DeprecatedCraftingNonImplementedStackRequestAction.php +++ /dev/null @@ -1,45 +0,0 @@ -results = $results; - $this->iterations = $iterations; - } - - /** @return Item[] */ - public function getResults() : array{ return $this->results; } - - public function getIterations() : int{ return $this->iterations; } - - public static function getTypeId() : int{ - return ItemStackRequestActionType::CRAFTING_RESULTS_DEPRECATED_ASK_TY_LAING; - } - - public static function read(NetworkBinaryStream $in) : self{ - $results = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $results[] = $in->getItemStackWithoutStackId(); - } - $iterations = $in->getByte(); - return new self($results, $iterations); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putUnsignedVarInt(count($this->results)); - foreach($this->results as $result){ - $out->putItemStackWithoutStackId($result); - } - $out->putByte($this->iterations); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DestroyStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DestroyStackRequestAction.php deleted file mode 100644 index 36f55e9a49..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DestroyStackRequestAction.php +++ /dev/null @@ -1,34 +0,0 @@ -count = $count; - $this->source = $source; - } - - final public function getCount() : int{ return $this->count; } - - final public function getSource() : ItemStackRequestSlotInfo{ return $this->source; } - - public static function read(NetworkBinaryStream $in) : self{ - $count = $in->getByte(); - $source = ItemStackRequestSlotInfo::read($in); - return new self($count, $source); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->count); - $this->source->write($out); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DropStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DropStackRequestAction.php deleted file mode 100644 index 329b4ec981..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/DropStackRequestAction.php +++ /dev/null @@ -1,66 +0,0 @@ -count = $count; - $this->source = $source; - $this->randomly = $randomly; - } - - public function getCount() : int{ return $this->count; } - - public function getSource() : ItemStackRequestSlotInfo{ return $this->source; } - - public function isRandomly() : bool{ return $this->randomly; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::DROP; } - - public static function read(NetworkBinaryStream $in) : self{ - $count = $in->getByte(); - $source = ItemStackRequestSlotInfo::read($in); - $random = $in->getBool(); - return new self($count, $source, $random); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->count); - $this->source->write($out); - $out->putBool($this->randomly); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequest.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequest.php deleted file mode 100644 index ddd883020b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequest.php +++ /dev/null @@ -1,111 +0,0 @@ - - */ - private $filterStrings; - - /** - * @param ItemStackRequestAction[] $actions - * @param string[] $filterStrings - * @phpstan-param list $filterStrings - */ - public function __construct(int $requestId, array $actions, array $filterStrings){ - $this->requestId = $requestId; - $this->actions = $actions; - $this->filterStrings = $filterStrings; - } - - public function getRequestId() : int{ return $this->requestId; } - - /** @return ItemStackRequestAction[] */ - public function getActions() : array{ return $this->actions; } - - /** - * @return string[] - * @phpstan-return list - */ - public function getFilterStrings() : array{ return $this->filterStrings; } - - private static function readAction(NetworkBinaryStream $in, int $typeId) : ItemStackRequestAction{ - switch($typeId){ - case TakeStackRequestAction::getTypeId(): return TakeStackRequestAction::read($in); - case PlaceStackRequestAction::getTypeId(): return PlaceStackRequestAction::read($in); - case SwapStackRequestAction::getTypeId(): return SwapStackRequestAction::read($in); - case DropStackRequestAction::getTypeId(): return DropStackRequestAction::read($in); - case DestroyStackRequestAction::getTypeId(): return DestroyStackRequestAction::read($in); - case CraftingConsumeInputStackRequestAction::getTypeId(): return CraftingConsumeInputStackRequestAction::read($in); - case CraftingMarkSecondaryResultStackRequestAction::getTypeId(): return CraftingMarkSecondaryResultStackRequestAction::read($in); - case LabTableCombineStackRequestAction::getTypeId(): return LabTableCombineStackRequestAction::read($in); - case BeaconPaymentStackRequestAction::getTypeId(): return BeaconPaymentStackRequestAction::read($in); - case MineBlockStackRequestAction::getTypeId(): return MineBlockStackRequestAction::read($in); - case CraftRecipeStackRequestAction::getTypeId(): return CraftRecipeStackRequestAction::read($in); - case CraftRecipeAutoStackRequestAction::getTypeId(): return CraftRecipeAutoStackRequestAction::read($in); - case CreativeCreateStackRequestAction::getTypeId(): return CreativeCreateStackRequestAction::read($in); - case CraftRecipeOptionalStackRequestAction::getTypeId(): return CraftRecipeOptionalStackRequestAction::read($in); - case DeprecatedCraftingNonImplementedStackRequestAction::getTypeId(): return DeprecatedCraftingNonImplementedStackRequestAction::read($in); - case DeprecatedCraftingResultsStackRequestAction::getTypeId(): return DeprecatedCraftingResultsStackRequestAction::read($in); - } - throw new \UnexpectedValueException("Unhandled item stack request action type $typeId"); - } - - public static function read(NetworkBinaryStream $in) : self{ - $requestId = $in->readGenericTypeNetworkId(); - $actions = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $typeId = $in->getByte(); - $actions[] = self::readAction($in, $typeId); - } - $filterStrings = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $filterStrings[] = $in->getString(); - } - return new self($requestId, $actions, $filterStrings); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->writeGenericTypeNetworkId($this->requestId); - $out->putUnsignedVarInt(count($this->actions)); - foreach($this->actions as $action){ - $out->putByte($action::getTypeId()); - $action->write($out); - } - $out->putUnsignedVarInt(count($this->filterStrings)); - foreach($this->filterStrings as $string){ - $out->putString($string); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequestAction.php deleted file mode 100644 index 26437fdd6b..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/ItemStackRequestAction.php +++ /dev/null @@ -1,33 +0,0 @@ -containerId = $containerId; - $this->slotId = $slotId; - $this->stackId = $stackId; - } - - public function getContainerId() : int{ return $this->containerId; } - - public function getSlotId() : int{ return $this->slotId; } - - public function getStackId() : int{ return $this->stackId; } - - public static function read(NetworkBinaryStream $in) : self{ - $containerId = $in->getByte(); - $slotId = $in->getByte(); - $stackId = $in->readGenericTypeNetworkId(); - return new self($containerId, $slotId, $stackId); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->containerId); - $out->putByte($this->slotId); - $out->writeGenericTypeNetworkId($this->stackId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/LabTableCombineStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/LabTableCombineStackRequestAction.php deleted file mode 100644 index 501bc31598..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/LabTableCombineStackRequestAction.php +++ /dev/null @@ -1,43 +0,0 @@ -unknown1 = $unknown1; - $this->predictedDurability = $predictedDurability; - $this->stackId = $stackId; - } - - public function getUnknown1() : int{ return $this->unknown1; } - - public function getPredictedDurability() : int{ return $this->predictedDurability; } - - public function getStackId() : int{ return $this->stackId; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::MINE_BLOCK; } - - public static function read(NetworkBinaryStream $in) : self{ - $unknown1 = $in->getVarInt(); - $predictedDurability = $in->getVarInt(); - $stackId = $in->readGenericTypeNetworkId(); - return new self($unknown1, $predictedDurability, $stackId); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putVarInt($this->unknown1); - $out->putVarInt($this->predictedDurability); - $out->writeGenericTypeNetworkId($this->stackId); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/SwapStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/SwapStackRequestAction.php deleted file mode 100644 index db3c011099..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/SwapStackRequestAction.php +++ /dev/null @@ -1,59 +0,0 @@ -slot1 = $slot1; - $this->slot2 = $slot2; - } - - public function getSlot1() : ItemStackRequestSlotInfo{ return $this->slot1; } - - public function getSlot2() : ItemStackRequestSlotInfo{ return $this->slot2; } - - public static function getTypeId() : int{ return ItemStackRequestActionType::SWAP; } - - public static function read(NetworkBinaryStream $in) : self{ - $slot1 = ItemStackRequestSlotInfo::read($in); - $slot2 = ItemStackRequestSlotInfo::read($in); - return new self($slot1, $slot2); - } - - public function write(NetworkBinaryStream $out) : void{ - $this->slot1->write($out); - $this->slot2->write($out); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeOrPlaceStackRequestActionTrait.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeOrPlaceStackRequestActionTrait.php deleted file mode 100644 index 63388d792c..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeOrPlaceStackRequestActionTrait.php +++ /dev/null @@ -1,61 +0,0 @@ -count = $count; - $this->source = $source; - $this->destination = $destination; - } - - final public function getCount() : int{ return $this->count; } - - final public function getSource() : ItemStackRequestSlotInfo{ return $this->source; } - - final public function getDestination() : ItemStackRequestSlotInfo{ return $this->destination; } - - public static function read(NetworkBinaryStream $in) : self{ - $count = $in->getByte(); - $src = ItemStackRequestSlotInfo::read($in); - $dst = ItemStackRequestSlotInfo::read($in); - return new self($count, $src, $dst); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->count); - $this->source->write($out); - $this->destination->write($out); - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeStackRequestAction.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeStackRequestAction.php deleted file mode 100644 index f2946def27..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackrequest/TakeStackRequestAction.php +++ /dev/null @@ -1,33 +0,0 @@ -result = $result; - $this->requestId = $requestId; - $this->containerInfos = $containerInfos; - } - - public function getResult() : int{ return $this->result; } - - public function getRequestId() : int{ return $this->requestId; } - - /** @return ItemStackResponseContainerInfo[] */ - public function getContainerInfos() : array{ return $this->containerInfos; } - - public static function read(NetworkBinaryStream $in) : self{ - $result = $in->getByte(); - $requestId = $in->readGenericTypeNetworkId(); - $containerInfos = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $containerInfos[] = ItemStackResponseContainerInfo::read($in); - } - return new self($result, $requestId, $containerInfos); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->result); - $out->writeGenericTypeNetworkId($this->requestId); - $out->putUnsignedVarInt(count($this->containerInfos)); - foreach($this->containerInfos as $containerInfo){ - $containerInfo->write($out); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseContainerInfo.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseContainerInfo.php deleted file mode 100644 index 50c9565966..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseContainerInfo.php +++ /dev/null @@ -1,65 +0,0 @@ -containerId = $containerId; - $this->slots = $slots; - } - - public function getContainerId() : int{ return $this->containerId; } - - /** @return ItemStackResponseSlotInfo[] */ - public function getSlots() : array{ return $this->slots; } - - public static function read(NetworkBinaryStream $in) : self{ - $containerId = $in->getByte(); - $slots = []; - for($i = 0, $len = $in->getUnsignedVarInt(); $i < $len; ++$i){ - $slots[] = ItemStackResponseSlotInfo::read($in); - } - return new self($containerId, $slots); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->containerId); - $out->putUnsignedVarInt(count($this->slots)); - foreach($this->slots as $slot){ - $slot->write($out); - } - } -} diff --git a/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseSlotInfo.php b/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseSlotInfo.php deleted file mode 100644 index fb152ecd76..0000000000 --- a/src/pocketmine/network/mcpe/protocol/types/inventory/stackresponse/ItemStackResponseSlotInfo.php +++ /dev/null @@ -1,82 +0,0 @@ -slot = $slot; - $this->hotbarSlot = $hotbarSlot; - $this->count = $count; - $this->itemStackId = $itemStackId; - $this->customName = $customName; - $this->durabilityCorrection = $durabilityCorrection; - } - - public function getSlot() : int{ return $this->slot; } - - public function getHotbarSlot() : int{ return $this->hotbarSlot; } - - public function getCount() : int{ return $this->count; } - - public function getItemStackId() : int{ return $this->itemStackId; } - - public function getCustomName() : string{ return $this->customName; } - - public function getDurabilityCorrection() : int{ return $this->durabilityCorrection; } - - public static function read(NetworkBinaryStream $in) : self{ - $slot = $in->getByte(); - $hotbarSlot = $in->getByte(); - $count = $in->getByte(); - $itemStackId = $in->readGenericTypeNetworkId(); - $customName = $in->getString(); - $durabilityCorrection = $in->getVarInt(); - return new self($slot, $hotbarSlot, $count, $itemStackId, $customName, $durabilityCorrection); - } - - public function write(NetworkBinaryStream $out) : void{ - $out->putByte($this->slot); - $out->putByte($this->hotbarSlot); - $out->putByte($this->count); - $out->writeGenericTypeNetworkId($this->itemStackId); - $out->putString($this->customName); - $out->putVarInt($this->durabilityCorrection); - } -} diff --git a/src/pocketmine/network/query/QueryHandler.php b/src/pocketmine/network/query/QueryHandler.php deleted file mode 100644 index 9343b67654..0000000000 --- a/src/pocketmine/network/query/QueryHandler.php +++ /dev/null @@ -1,137 +0,0 @@ -server = Server::getInstance(); - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.server.query.start")); - $addr = $this->server->getIp(); - $port = $this->server->getPort(); - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.server.query.info", [$port])); - /* - The Query protocol is built on top of the existing Minecraft PE UDP network stack. - Because the 0xFE packet does not exist in the MCPE protocol, - we can identify Query packets and remove them from the packet queue. - - Then, the Query class handles itself sending the packets in raw form, because - packets can conflict with the MCPE ones. - */ - - $this->regenerateToken(); - $this->lastToken = $this->token; - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.server.query.running", [$addr, $port])); - } - - private function debug(string $message) : void{ - //TODO: replace this with a proper prefixed logger - $this->server->getLogger()->debug("[Query] $message"); - } - - /** - * @deprecated - * - * @return void - */ - public function regenerateInfo(){ - - } - - /** - * @return void - */ - public function regenerateToken(){ - $this->lastToken = $this->token; - $this->token = random_bytes(16); - } - - public static function getTokenString(string $token, string $salt) : int{ - return Binary::readInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4)); - } - - /** - * @return void - */ - public function handle(AdvancedSourceInterface $interface, string $address, int $port, string $packet){ - $offset = 2; - $packetType = ord($packet[$offset++]); - $sessionID = Binary::readInt(substr($packet, $offset, 4)); - $offset += 4; - $payload = substr($packet, $offset); - - switch($packetType){ - case self::HANDSHAKE: //Handshake - $reply = chr(self::HANDSHAKE); - $reply .= Binary::writeInt($sessionID); - $reply .= self::getTokenString($this->token, $address) . "\x00"; - - $interface->sendRawPacket($address, $port, $reply); - break; - case self::STATISTICS: //Stat - $token = Binary::readInt(substr($payload, 0, 4)); - if($token !== ($t1 = self::getTokenString($this->token, $address)) and $token !== ($t2 = self::getTokenString($this->lastToken, $address))){ - $this->debug("Bad token $token from $address $port, expected $t1 or $t2"); - break; - } - $reply = chr(self::STATISTICS); - $reply .= Binary::writeInt($sessionID); - - if(strlen($payload) === 8){ - $reply .= $this->server->getQueryInformation()->getLongQuery(); - }else{ - $reply .= $this->server->getQueryInformation()->getShortQuery(); - } - $interface->sendRawPacket($address, $port, $reply); - break; - default: - $this->debug("Unhandled packet from $address $port: " . base64_encode($packet)); - break; - } - } -} diff --git a/src/pocketmine/network/rcon/RCON.php b/src/pocketmine/network/rcon/RCON.php deleted file mode 100644 index 13dec46cb6..0000000000 --- a/src/pocketmine/network/rcon/RCON.php +++ /dev/null @@ -1,146 +0,0 @@ -server = $server; - $this->server->getLogger()->info("Starting remote control listener"); - if($password === ""){ - throw new \InvalidArgumentException("Empty password"); - } - - $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - if($socket === false){ - throw new \RuntimeException("Failed to create socket:" . trim(socket_strerror(socket_last_error()))); - } - $this->socket = $socket; - - if(!socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1)){ - throw new \RuntimeException("Unable to set option on socket: " . trim(socket_strerror(socket_last_error()))); - } - - if(!@socket_bind($this->socket, $interface, $port) or !@socket_listen($this->socket, 5)){ - throw new \RuntimeException(trim(socket_strerror(socket_last_error()))); - } - - socket_set_block($this->socket); - - $ret = @socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $ipc); - if(!$ret){ - $err = socket_last_error(); - if(($err !== SOCKET_EPROTONOSUPPORT and $err !== SOCKET_ENOPROTOOPT) or !@socket_create_pair(AF_INET, SOCK_STREAM, 0, $ipc)){ - throw new \RuntimeException(trim(socket_strerror(socket_last_error()))); - } - } - - [$this->ipcMainSocket, $this->ipcThreadSocket] = $ipc; - - $notifier = new SleeperNotifier(); - $this->server->getTickSleeper()->addNotifier($notifier, function() : void{ - $this->check(); - }); - $this->instance = new RCONInstance($this->socket, $password, max(1, $maxClients), $this->server->getLogger(), $this->ipcThreadSocket, $notifier); - - socket_getsockname($this->socket, $addr, $port); - $this->server->getLogger()->info("RCON running on $addr:$port"); - } - - /** - * @return void - */ - public function stop(){ - $this->instance->close(); - socket_write($this->ipcMainSocket, "\x00"); //make select() return - $this->instance->quit(); - - @socket_close($this->socket); - @socket_close($this->ipcMainSocket); - @socket_close($this->ipcThreadSocket); - } - - /** - * @return void - */ - public function check(){ - $response = new RemoteConsoleCommandSender(); - $command = $this->instance->cmd; - - $ev = new RemoteServerCommandEvent($response, $command); - $ev->call(); - - if(!$ev->isCancelled()){ - $this->server->dispatchCommand($ev->getSender(), $ev->getCommand()); - } - - $this->instance->response = TextFormat::clean($response->getMessage()); - $this->instance->synchronized(function(RCONInstance $thread) : void{ - $thread->notify(); - }, $this->instance); - } -} diff --git a/src/pocketmine/network/rcon/RCONInstance.php b/src/pocketmine/network/rcon/RCONInstance.php deleted file mode 100644 index ab60c745fd..0000000000 --- a/src/pocketmine/network/rcon/RCONInstance.php +++ /dev/null @@ -1,276 +0,0 @@ -stop = false; - $this->cmd = ""; - $this->response = ""; - $this->socket = $socket; - $this->password = $password; - $this->maxClients = $maxClients; - $this->logger = $logger; - $this->ipcSocket = $ipcSocket; - $this->notifier = $notifier; - - $this->start(PTHREADS_INHERIT_NONE); - } - - /** - * @return int|false - */ - private function writePacket(\Socket $client, int $requestID, int $packetType, string $payload){ - $pk = Binary::writeLInt($requestID) - . Binary::writeLInt($packetType) - . $payload - . "\x00\x00"; //Terminate payload and packet - return socket_write($client, Binary::writeLInt(strlen($pk)) . $pk); - } - - /** - * @param int $requestID reference parameter - * @param int $packetType reference parameter - * @param string $payload reference parameter - * - * @return bool - */ - private function readPacket(\Socket $client, ?int &$requestID, ?int &$packetType, ?string &$payload){ - $d = @socket_read($client, 4); - - socket_getpeername($client, $ip, $port); - if($d === false){ - $err = socket_last_error($client); - if($err !== SOCKET_ECONNRESET){ - $this->logger->debug("Connection error with $ip $port: " . trim(socket_strerror($err))); - } - return false; - } - if(strlen($d) !== 4){ - if($d !== ""){ //empty data is returned on disconnection - $this->logger->debug("Truncated packet from $ip $port (want 4 bytes, have " . strlen($d) . "), disconnecting"); - } - return false; - } - $size = Binary::readLInt($d); - if($size < 0 or $size > 65535){ - $this->logger->debug("Packet with too-large length header $size from $ip $port, disconnecting"); - return false; - } - $buf = @socket_read($client, $size); - if($buf === false){ - $err = socket_last_error($client); - if($err !== SOCKET_ECONNRESET){ - $this->logger->debug("Connection error with $ip $port: " . trim(socket_strerror($err))); - } - return false; - } - if(strlen($buf) !== $size){ - $this->logger->debug("Truncated packet from $ip $port (want $size bytes, have " . strlen($buf) . "), disconnecting"); - return false; - } - $requestID = Binary::readLInt(substr($buf, 0, 4)); - $packetType = Binary::readLInt(substr($buf, 4, 4)); - $payload = substr($buf, 8, -2); //Strip two null bytes - return true; - } - - /** - * @return void - */ - public function close(){ - $this->stop = true; - } - - /** - * @return void - */ - public function run(){ - $this->registerClassLoader(); - - /** - * @var \Socket[] $clients - * @phpstan-var array $clients - */ - $clients = []; - /** @var bool[] $authenticated */ - $authenticated = []; - /** @var float[] $timeouts */ - $timeouts = []; - - /** @var int $nextClientId */ - $nextClientId = 0; - - while(!$this->stop){ - $r = $clients; - $r["main"] = $this->socket; //this is ugly, but we need to be able to mass-select() - $r["ipc"] = $this->ipcSocket; - $w = null; - $e = null; - - $disconnect = []; - - if(socket_select($r, $w, $e, 5, 0) > 0){ - foreach($r as $id => $sock){ - if($sock === $this->socket){ - if(($client = socket_accept($this->socket)) !== false){ - if(count($clients) >= $this->maxClients){ - @socket_close($client); - }else{ - socket_set_nonblock($client); - socket_set_option($client, SOL_SOCKET, SO_KEEPALIVE, 1); - - $id = $nextClientId++; - $clients[$id] = $client; - $authenticated[$id] = false; - $timeouts[$id] = microtime(true) + 5; - } - } - }elseif($sock === $this->ipcSocket){ - //read dummy data - socket_read($sock, 65535); - }else{ - $p = $this->readPacket($sock, $requestID, $packetType, $payload); - if($p === false){ - $disconnect[$id] = $sock; - continue; - } - - switch($packetType){ - case 3: //Login - if($authenticated[$id]){ - $disconnect[$id] = $sock; - break; - } - if($payload === $this->password){ - socket_getpeername($sock, $addr, $port); - $this->logger->info("Successful Rcon connection from: /$addr:$port"); - $this->writePacket($sock, $requestID, 2, ""); - $authenticated[$id] = true; - }else{ - $disconnect[$id] = $sock; - $this->writePacket($sock, -1, 2, ""); - } - break; - case 2: //Command - if(!$authenticated[$id]){ - $disconnect[$id] = $sock; - break; - } - if($payload !== ""){ - $this->cmd = ltrim($payload); - $this->synchronized(function() : void{ - $this->notifier->wakeupSleeper(); - $this->wait(); - }); - $this->writePacket($sock, $requestID, 0, str_replace("\n", "\r\n", trim($this->response))); - $this->response = ""; - $this->cmd = ""; - } - break; - } - } - } - } - - foreach($authenticated as $id => $status){ - if(!isset($disconnect[$id]) and !$authenticated[$id] and $timeouts[$id] < microtime(true)){ //Timeout - $disconnect[$id] = $clients[$id]; - } - } - - foreach($disconnect as $id => $client){ - $this->disconnectClient($client); - unset($clients[$id], $authenticated[$id], $timeouts[$id]); - } - } - - foreach($clients as $client){ - $this->disconnectClient($client); - } - } - - private function disconnectClient(\Socket $client) : void{ - socket_getpeername($client, $ip, $port); - @socket_set_option($client, SOL_SOCKET, SO_LINGER, ["l_onoff" => 1, "l_linger" => 1]); - @socket_shutdown($client, 2); - @socket_set_block($client); - @socket_read($client, 1); - @socket_close($client); - $this->logger->info("Disconnected client: /$ip:$port"); - } - - public function getThreadName() : string{ - return "RCON"; - } -} diff --git a/src/pocketmine/permission/DefaultPermissions.php b/src/pocketmine/permission/DefaultPermissions.php deleted file mode 100644 index fa9e73ed52..0000000000 --- a/src/pocketmine/permission/DefaultPermissions.php +++ /dev/null @@ -1,132 +0,0 @@ -getChildren()[$perm->getName()] = true; - - return self::registerPermission($perm); - } - PermissionManager::getInstance()->addPermission($perm); - - return PermissionManager::getInstance()->getPermission($perm->getName()); - } - - /** - * @return void - */ - public static function registerCorePermissions(){ - $parent = self::registerPermission(new Permission(self::ROOT, "Allows using all PocketMine commands and utilities")); - - $broadcasts = self::registerPermission(new Permission(self::ROOT . ".broadcast", "Allows the user to receive all broadcast messages"), $parent); - self::registerPermission(new Permission(self::ROOT . ".broadcast.admin", "Allows the user to receive administrative broadcasts", Permission::DEFAULT_OP), $broadcasts); - self::registerPermission(new Permission(self::ROOT . ".broadcast.user", "Allows the user to receive user broadcasts", Permission::DEFAULT_TRUE), $broadcasts); - $broadcasts->recalculatePermissibles(); - - $spawnprotect = self::registerPermission(new Permission(self::ROOT . ".spawnprotect.bypass", "Allows the user to edit blocks within the protected spawn radius", Permission::DEFAULT_OP), $parent); - $spawnprotect->recalculatePermissibles(); - - $commands = self::registerPermission(new Permission(self::ROOT . ".command", "Allows using all PocketMine commands"), $parent); - - $whitelist = self::registerPermission(new Permission(self::ROOT . ".command.whitelist", "Allows the user to modify the server whitelist", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.add", "Allows the user to add a player to the server whitelist"), $whitelist); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.remove", "Allows the user to remove a player from the server whitelist"), $whitelist); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.reload", "Allows the user to reload the server whitelist"), $whitelist); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.enable", "Allows the user to enable the server whitelist"), $whitelist); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.disable", "Allows the user to disable the server whitelist"), $whitelist); - self::registerPermission(new Permission(self::ROOT . ".command.whitelist.list", "Allows the user to list all players on the server whitelist"), $whitelist); - $whitelist->recalculatePermissibles(); - - $ban = self::registerPermission(new Permission(self::ROOT . ".command.ban", "Allows the user to ban people", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.ban.player", "Allows the user to ban players"), $ban); - self::registerPermission(new Permission(self::ROOT . ".command.ban.ip", "Allows the user to ban IP addresses"), $ban); - self::registerPermission(new Permission(self::ROOT . ".command.ban.list", "Allows the user to list banned players"), $ban); - $ban->recalculatePermissibles(); - - $unban = self::registerPermission(new Permission(self::ROOT . ".command.unban", "Allows the user to unban people", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.unban.player", "Allows the user to unban players"), $unban); - self::registerPermission(new Permission(self::ROOT . ".command.unban.ip", "Allows the user to unban IP addresses"), $unban); - $unban->recalculatePermissibles(); - - $op = self::registerPermission(new Permission(self::ROOT . ".command.op", "Allows the user to change operators", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.op.give", "Allows the user to give a player operator status"), $op); - self::registerPermission(new Permission(self::ROOT . ".command.op.take", "Allows the user to take a player's operator status"), $op); - $op->recalculatePermissibles(); - - $save = self::registerPermission(new Permission(self::ROOT . ".command.save", "Allows the user to save the worlds", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.save.enable", "Allows the user to enable automatic saving"), $save); - self::registerPermission(new Permission(self::ROOT . ".command.save.disable", "Allows the user to disable automatic saving"), $save); - self::registerPermission(new Permission(self::ROOT . ".command.save.perform", "Allows the user to perform a manual save"), $save); - $save->recalculatePermissibles(); - - $time = self::registerPermission(new Permission(self::ROOT . ".command.time", "Allows the user to alter the time", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.time.add", "Allows the user to fast-forward time"), $time); - self::registerPermission(new Permission(self::ROOT . ".command.time.set", "Allows the user to change the time"), $time); - self::registerPermission(new Permission(self::ROOT . ".command.time.start", "Allows the user to restart the time"), $time); - self::registerPermission(new Permission(self::ROOT . ".command.time.stop", "Allows the user to stop the time"), $time); - self::registerPermission(new Permission(self::ROOT . ".command.time.query", "Allows the user query the time"), $time); - $time->recalculatePermissibles(); - - $kill = self::registerPermission(new Permission(self::ROOT . ".command.kill", "Allows the user to kill players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.kill.self", "Allows the user to commit suicide", Permission::DEFAULT_TRUE), $kill); - self::registerPermission(new Permission(self::ROOT . ".command.kill.other", "Allows the user to kill other players"), $kill); - $kill->recalculatePermissibles(); - - self::registerPermission(new Permission(self::ROOT . ".command.me", "Allows the user to perform a chat action", Permission::DEFAULT_TRUE), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.tell", "Allows the user to privately message another player", Permission::DEFAULT_TRUE), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.say", "Allows the user to talk as the console", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.give", "Allows the user to give items to players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.effect", "Allows the user to give/take potion effects", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.enchant", "Allows the user to enchant items", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.particle", "Allows the user to create particle effects", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.teleport", "Allows the user to teleport players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.kick", "Allows the user to kick players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.stop", "Allows the user to stop the server", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.list", "Allows the user to list all online players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.help", "Allows the user to view the help menu", Permission::DEFAULT_TRUE), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.plugins", "Allows the user to view the list of plugins", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.reload", "Allows the user to reload the server settings", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.version", "Allows the user to view the version of the server", Permission::DEFAULT_TRUE), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.gamemode", "Allows the user to change the gamemode of players", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.defaultgamemode", "Allows the user to change the default gamemode", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.seed", "Allows the user to view the seed of the world", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.status", "Allows the user to view the server performance", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.gc", "Allows the user to fire garbage collection tasks", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.dumpmemory", "Allows the user to dump memory contents", Permission::DEFAULT_FALSE), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.timings", "Allows the user to records timings for all plugin events", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.spawnpoint", "Allows the user to change player's spawnpoint", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.setworldspawn", "Allows the user to change the world spawn", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.transferserver", "Allows the user to transfer self to another server", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.title", "Allows the user to send a title to the specified player", Permission::DEFAULT_OP), $commands); - self::registerPermission(new Permission(self::ROOT . ".command.difficulty", "Allows the user to change the game difficulty", Permission::DEFAULT_OP), $commands); - - $commands->recalculatePermissibles(); - - $parent->recalculatePermissibles(); - } -} diff --git a/src/pocketmine/permission/Permissible.php b/src/pocketmine/permission/Permissible.php deleted file mode 100644 index a009cb0cf3..0000000000 --- a/src/pocketmine/permission/Permissible.php +++ /dev/null @@ -1,61 +0,0 @@ -opable = $opable; - if($opable instanceof Permissible){ - $this->parent = $opable; - } - } - - public function isOp() : bool{ - return $this->opable->isOp(); - } - - public function setOp(bool $value){ - $this->opable->setOp($value); - } - - public function isPermissionSet($name) : bool{ - return isset($this->permissions[$name instanceof Permission ? $name->getName() : $name]); - } - - public function hasPermission($name) : bool{ - if($name instanceof Permission){ - $name = $name->getName(); - } - - if($this->isPermissionSet($name)){ - return $this->permissions[$name]->getValue(); - } - - if(($perm = PermissionManager::getInstance()->getPermission($name)) !== null){ - $perm = $perm->getDefault(); - - return $perm === Permission::DEFAULT_TRUE or ($this->isOp() and $perm === Permission::DEFAULT_OP) or (!$this->isOp() and $perm === Permission::DEFAULT_NOT_OP); - }else{ - return Permission::$DEFAULT_PERMISSION === Permission::DEFAULT_TRUE or ($this->isOp() and Permission::$DEFAULT_PERMISSION === Permission::DEFAULT_OP) or (!$this->isOp() and Permission::$DEFAULT_PERMISSION === Permission::DEFAULT_NOT_OP); - } - - } - - /** - * //TODO: tick scheduled attachments - */ - public function addAttachment(Plugin $plugin, string $name = null, bool $value = null) : PermissionAttachment{ - if(!$plugin->isEnabled()){ - throw new PluginException("Plugin " . $plugin->getDescription()->getName() . " is disabled"); - } - - $result = new PermissionAttachment($plugin, $this->parent ?? $this); - $this->attachments[spl_object_hash($result)] = $result; - if($name !== null and $value !== null){ - $result->setPermission($name, $value); - } - - $this->recalculatePermissions(); - - return $result; - } - - public function removeAttachment(PermissionAttachment $attachment){ - if(isset($this->attachments[spl_object_hash($attachment)])){ - unset($this->attachments[spl_object_hash($attachment)]); - if(($ex = $attachment->getRemovalCallback()) !== null){ - $ex->attachmentRemoved($attachment); - } - - $this->recalculatePermissions(); - - } - - } - - public function recalculatePermissions(){ - Timings::$permissibleCalculationTimer->startTiming(); - - $this->clearPermissions(); - $permManager = PermissionManager::getInstance(); - $defaults = $permManager->getDefaultPermissions($this->isOp()); - $permManager->subscribeToDefaultPerms($this->isOp(), $this->parent ?? $this); - - foreach($defaults as $perm){ - $name = $perm->getName(); - $this->permissions[$name] = new PermissionAttachmentInfo($this->parent ?? $this, $name, null, true); - $permManager->subscribeToPermission($name, $this->parent ?? $this); - $this->calculateChildPermissions($perm->getChildren(), false, null); - } - - foreach($this->attachments as $attachment){ - $this->calculateChildPermissions($attachment->getPermissions(), false, $attachment); - } - - Timings::$permissibleCalculationTimer->stopTiming(); - } - - /** - * @return void - */ - public function clearPermissions(){ - $permManager = PermissionManager::getInstance(); - $permManager->unsubscribeFromAllPermissions($this->parent ?? $this); - - $permManager->unsubscribeFromDefaultPerms(false, $this->parent ?? $this); - $permManager->unsubscribeFromDefaultPerms(true, $this->parent ?? $this); - - $this->permissions = []; - } - - /** - * @param bool[] $children - */ - private function calculateChildPermissions(array $children, bool $invert, ?PermissionAttachment $attachment) : void{ - $permManager = PermissionManager::getInstance(); - foreach($children as $name => $v){ - $perm = $permManager->getPermission($name); - $value = ($v xor $invert); - $this->permissions[$name] = new PermissionAttachmentInfo($this->parent ?? $this, $name, $attachment, $value); - $permManager->subscribeToPermission($name, $this->parent ?? $this); - - if($perm instanceof Permission){ - $this->calculateChildPermissions($perm->getChildren(), !$value, $attachment); - } - } - } - - /** - * @return PermissionAttachmentInfo[] - */ - public function getEffectivePermissions() : array{ - return $this->permissions; - } -} diff --git a/src/pocketmine/permission/Permission.php b/src/pocketmine/permission/Permission.php deleted file mode 100644 index d2c64d900d..0000000000 --- a/src/pocketmine/permission/Permission.php +++ /dev/null @@ -1,243 +0,0 @@ -> $data - * - * @return Permission[] - */ - public static function loadPermissions(array $data, string $default = self::DEFAULT_OP) : array{ - $result = []; - foreach($data as $key => $entry){ - $result[] = self::loadPermission($key, $entry, $default, $result); - } - - return $result; - } - - /** - * @param mixed[] $data - * @param Permission[] $output reference parameter - * @phpstan-param array $data - * - * @throws \Exception - */ - public static function loadPermission(string $name, array $data, string $default = self::DEFAULT_OP, array &$output = []) : Permission{ - $desc = null; - $children = []; - if(isset($data["default"])){ - $default = Permission::getByName($data["default"]); - } - - if(isset($data["children"])){ - if(is_array($data["children"])){ - foreach($data["children"] as $k => $v){ - if(is_array($v)){ - $output[] = self::loadPermission($k, $v, $default, $output); - } - $children[$k] = true; - } - }else{ - throw new \InvalidStateException("'children' key is of wrong type"); - } - } - - if(isset($data["description"])){ - $desc = $data["description"]; - } - - return new Permission($name, $desc, $default, $children); - } - - /** @var string */ - private $name; - - /** @var string */ - private $description; - - /** - * @var bool[] - * @phpstan-var array - */ - private $children; - - /** @var string */ - private $defaultValue; - - /** - * Creates a new Permission object to be attached to Permissible objects - * - * @param bool[] $children - * @phpstan-param array $children - */ - public function __construct(string $name, string $description = null, string $defaultValue = null, array $children = []){ - $this->name = $name; - $this->description = $description ?? ""; - $this->defaultValue = $defaultValue ?? self::$DEFAULT_PERMISSION; - $this->children = $children; - - $this->recalculatePermissibles(); - } - - public function getName() : string{ - return $this->name; - } - - /** - * @return bool[] - * @phpstan-return array - */ - public function &getChildren() : array{ - return $this->children; - } - - public function getDefault() : string{ - return $this->defaultValue; - } - - /** - * @return void - */ - public function setDefault(string $value){ - if($value !== $this->defaultValue){ - $this->defaultValue = $value; - $this->recalculatePermissibles(); - } - } - - public function getDescription() : string{ - return $this->description; - } - - /** - * @return void - */ - public function setDescription(string $value){ - $this->description = $value; - } - - /** - * @return Permissible[] - */ - public function getPermissibles() : array{ - return PermissionManager::getInstance()->getPermissionSubscriptions($this->name); - } - - /** - * @return void - */ - public function recalculatePermissibles(){ - $perms = $this->getPermissibles(); - - PermissionManager::getInstance()->recalculatePermissionDefaults($this); - - foreach($perms as $p){ - $p->recalculatePermissions(); - } - } - - /** - * @param string|Permission $name - * - * @return Permission|null Permission if $name is a string, null if it's a Permission - */ - public function addParent($name, bool $value){ - if($name instanceof Permission){ - $name->getChildren()[$this->getName()] = $value; - $name->recalculatePermissibles(); - return null; - }else{ - $perm = PermissionManager::getInstance()->getPermission($name); - if($perm === null){ - $perm = new Permission($name); - PermissionManager::getInstance()->addPermission($perm); - } - - $this->addParent($perm, $value); - - return $perm; - } - } -} diff --git a/src/pocketmine/permission/PermissionManager.php b/src/pocketmine/permission/PermissionManager.php deleted file mode 100644 index 6987310944..0000000000 --- a/src/pocketmine/permission/PermissionManager.php +++ /dev/null @@ -1,211 +0,0 @@ -permissions[$name] ?? null; - } - - public function addPermission(Permission $permission) : bool{ - if(!isset($this->permissions[$permission->getName()])){ - $this->permissions[$permission->getName()] = $permission; - $this->calculatePermissionDefault($permission); - - return true; - } - - return false; - } - - /** - * @param string|Permission $permission - * - * @return void - */ - public function removePermission($permission){ - if($permission instanceof Permission){ - unset($this->permissions[$permission->getName()]); - }else{ - unset($this->permissions[$permission]); - } - } - - /** - * @return Permission[] - */ - public function getDefaultPermissions(bool $op) : array{ - if($op){ - return $this->defaultPermsOp; - }else{ - return $this->defaultPerms; - } - } - - /** - * @return void - */ - public function recalculatePermissionDefaults(Permission $permission){ - if(isset($this->permissions[$permission->getName()])){ - unset($this->defaultPermsOp[$permission->getName()]); - unset($this->defaultPerms[$permission->getName()]); - $this->calculatePermissionDefault($permission); - } - } - - private function calculatePermissionDefault(Permission $permission) : void{ - Timings::$permissionDefaultTimer->startTiming(); - if($permission->getDefault() === Permission::DEFAULT_OP or $permission->getDefault() === Permission::DEFAULT_TRUE){ - $this->defaultPermsOp[$permission->getName()] = $permission; - $this->dirtyPermissibles(true); - } - - if($permission->getDefault() === Permission::DEFAULT_NOT_OP or $permission->getDefault() === Permission::DEFAULT_TRUE){ - $this->defaultPerms[$permission->getName()] = $permission; - $this->dirtyPermissibles(false); - } - Timings::$permissionDefaultTimer->stopTiming(); - } - - private function dirtyPermissibles(bool $op) : void{ - foreach($this->getDefaultPermSubscriptions($op) as $p){ - $p->recalculatePermissions(); - } - } - - /** - * @return void - */ - public function subscribeToPermission(string $permission, Permissible $permissible){ - if(!isset($this->permSubs[$permission])){ - $this->permSubs[$permission] = []; - } - $this->permSubs[$permission][spl_object_hash($permissible)] = $permissible; - } - - /** - * @return void - */ - public function unsubscribeFromPermission(string $permission, Permissible $permissible){ - if(isset($this->permSubs[$permission])){ - unset($this->permSubs[$permission][spl_object_hash($permissible)]); - if(count($this->permSubs[$permission]) === 0){ - unset($this->permSubs[$permission]); - } - } - } - - public function unsubscribeFromAllPermissions(Permissible $permissible) : void{ - foreach($this->permSubs as $permission => &$subs){ - unset($subs[spl_object_hash($permissible)]); - if(count($subs) === 0){ - unset($this->permSubs[$permission]); - } - } - } - - /** - * @return Permissible[] - */ - public function getPermissionSubscriptions(string $permission) : array{ - return $this->permSubs[$permission] ?? []; - } - - /** - * @return void - */ - public function subscribeToDefaultPerms(bool $op, Permissible $permissible){ - if($op){ - $this->defSubsOp[spl_object_hash($permissible)] = $permissible; - }else{ - $this->defSubs[spl_object_hash($permissible)] = $permissible; - } - } - - /** - * @return void - */ - public function unsubscribeFromDefaultPerms(bool $op, Permissible $permissible){ - if($op){ - unset($this->defSubsOp[spl_object_hash($permissible)]); - }else{ - unset($this->defSubs[spl_object_hash($permissible)]); - } - } - - /** - * @return Permissible[] - */ - public function getDefaultPermSubscriptions(bool $op) : array{ - if($op){ - return $this->defSubsOp; - } - - return $this->defSubs; - } - - /** - * @return Permission[] - */ - public function getPermissions() : array{ - return $this->permissions; - } - - public function clearPermissions() : void{ - $this->permissions = []; - $this->defaultPerms = []; - $this->defaultPermsOp = []; - } -} diff --git a/src/pocketmine/plugin/PluginLogger.php b/src/pocketmine/plugin/PluginLogger.php deleted file mode 100644 index a7cfcdbbdb..0000000000 --- a/src/pocketmine/plugin/PluginLogger.php +++ /dev/null @@ -1,101 +0,0 @@ -attachments[spl_object_hash($attachment)] = $attachment; - } - - public function removeAttachment(\LoggerAttachment $attachment){ - unset($this->attachments[spl_object_hash($attachment)]); - } - - public function removeAttachments(){ - $this->attachments = []; - } - - public function getAttachments(){ - return $this->attachments; - } - - public function __construct(Plugin $context){ - $prefix = $context->getDescription()->getPrefix(); - $this->pluginName = $prefix != null ? "[$prefix] " : "[" . $context->getDescription()->getName() . "] "; - } - - public function emergency($message){ - $this->log(LogLevel::EMERGENCY, $message); - } - - public function alert($message){ - $this->log(LogLevel::ALERT, $message); - } - - public function critical($message){ - $this->log(LogLevel::CRITICAL, $message); - } - - public function error($message){ - $this->log(LogLevel::ERROR, $message); - } - - public function warning($message){ - $this->log(LogLevel::WARNING, $message); - } - - public function notice($message){ - $this->log(LogLevel::NOTICE, $message); - } - - public function info($message){ - $this->log(LogLevel::INFO, $message); - } - - public function debug($message){ - $this->log(LogLevel::DEBUG, $message); - } - - public function logException(\Throwable $e, $trace = null){ - Server::getInstance()->getLogger()->logException($e, $trace); - } - - public function log($level, $message){ - Server::getInstance()->getLogger()->log($level, $this->pluginName . $message); - foreach($this->attachments as $attachment){ - $attachment->log($level, $message); - } - } -} diff --git a/src/pocketmine/plugin/PluginManager.php b/src/pocketmine/plugin/PluginManager.php deleted file mode 100644 index 6ee6fa1ecf..0000000000 --- a/src/pocketmine/plugin/PluginManager.php +++ /dev/null @@ -1,781 +0,0 @@ -, PluginLoader> - */ - protected $fileAssociations = []; - - /** @var string|null */ - private $pluginDataDirectory; - - public function __construct(Server $server, SimpleCommandMap $commandMap, ?string $pluginDataDirectory){ - $this->server = $server; - $this->commandMap = $commandMap; - $this->pluginDataDirectory = $pluginDataDirectory; - if($this->pluginDataDirectory !== null){ - if(!file_exists($this->pluginDataDirectory)){ - @mkdir($this->pluginDataDirectory, 0777, true); - }elseif(!is_dir($this->pluginDataDirectory)){ - throw new \RuntimeException("Plugin data path $this->pluginDataDirectory exists and is not a directory"); - } - } - } - - /** - * @return null|Plugin - */ - public function getPlugin(string $name){ - if(isset($this->plugins[$name])){ - return $this->plugins[$name]; - } - - return null; - } - - public function registerInterface(PluginLoader $loader) : void{ - $this->fileAssociations[get_class($loader)] = $loader; - } - - /** - * @return Plugin[] - */ - public function getPlugins() : array{ - return $this->plugins; - } - - private function getDataDirectory(string $pluginPath, string $pluginName) : string{ - if($this->pluginDataDirectory !== null){ - return $this->pluginDataDirectory . $pluginName; - } - return dirname($pluginPath) . DIRECTORY_SEPARATOR . $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()->translateString("pocketmine.plugin.load", [$description->getFullName()])); - try{ - $description->checkRequiredExtensions(); - }catch(PluginException $ex){ - $this->server->getLogger()->error($ex->getMessage()); - return null; - } - - $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); - } - - $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; - } - - try{ - /** - * @var Plugin $plugin - * @see Plugin::__construct() - */ - $plugin = new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed); - $plugin->onLoad(); - $this->plugins[$plugin->getDescription()->getName()] = $plugin; - - $pluginCommands = $this->parseYamlCommands($plugin); - - if(count($pluginCommands) > 0){ - $this->commandMap->registerAll($plugin->getDescription()->getName(), $pluginCommands); - } - - return $plugin; - }catch(\Throwable $e){ - $this->server->getLogger()->logException($e); - return null; - } - } - } - } - - return null; - } - - /** - * @param string[]|null $newLoaders - * @phpstan-param list> $newLoaders - * - * @return Plugin[] - */ - public function loadPlugins(string $directory, array $newLoaders = null){ - if(!is_dir($directory)){ - return []; - } - - $plugins = []; - $loadedPlugins = []; - $dependencies = []; - $softDependencies = []; - if(is_array($newLoaders)){ - $loaders = []; - foreach($newLoaders as $key){ - if(isset($this->fileAssociations[$key])){ - $loaders[$key] = $this->fileAssociations[$key]; - } - } - }else{ - $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 - 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"); - if(!$loader->canLoadPlugin($file)){ - continue; - } - try{ - $description = $loader->getPluginDescription($file); - if($description === null){ - continue; - } - - $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()->translateString("pocketmine.plugin.loadError", [$name, "%pocketmine.plugin.restrictedName"])); - continue; - }elseif(strpos($name, " ") !== false){ - $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.plugin.spacesDiscouraged", [$name])); - } - - if(isset($plugins[$name]) or $this->getPlugin($name) instanceof Plugin){ - $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.duplicateError", [$name])); - continue; - } - - if(!$this->isCompatibleApi(...$description->getCompatibleApis())){ - $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [ - $name, - $this->server->getLanguage()->translateString("%pocketmine.plugin.incompatibleAPI", [implode(", ", $description->getCompatibleApis())]) - ])); - continue; - } - - if(count($description->getCompatibleOperatingSystems()) > 0 and !in_array(Utils::getOS(), $description->getCompatibleOperatingSystems(), true)) { - $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [ - $name, - $this->server->getLanguage()->translateString("%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()->translateString("pocketmine.plugin.loadError", [ - $name, - $this->server->getLanguage()->translateString("%pocketmine.plugin.incompatibleProtocol", [implode(", ", $pluginMcpeProtocols)]) - ])); - continue; - } - } - - $plugins[$name] = $file; - - $softDependencies[$name] = array_merge($softDependencies[$name] ?? [], $description->getSoftDepend()); - $dependencies[$name] = $description->getDepend(); - - foreach($description->getLoadBefore() as $before){ - if(isset($softDependencies[$before])){ - $softDependencies[$before][] = $name; - }else{ - $softDependencies[$before] = [$name]; - } - } - }catch(\Throwable $e){ - $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.plugin.fileError", [$file, $directory, $e->getMessage()])); - $this->server->getLogger()->logException($e); - } - } - } - - while(count($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()->translateString("pocketmine.plugin.loadError", [ - $name, - $this->server->getLanguage()->translateString("%pocketmine.plugin.unknownDependency", [$dependency]) - ])); - unset($plugins[$name]); - continue 2; - } - } - - 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]); - $loadedThisLoop++; - if(($plugin = $this->loadPlugin($file, $loaders)) instanceof Plugin){ - $loadedPlugins[$name] = $plugin; - }else{ - $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.genericLoadError", [$name])); - } - } - } - - if($loadedThisLoop === 0){ - //No plugins loaded :( - foreach($plugins as $name => $file){ - $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.loadError", [$name, "%pocketmine.plugin.circularDependency"])); - } - $plugins = []; - } - } - - return $loadedPlugins; - } - - /** - * Returns whether a specified API version string is considered compatible with the server's API version. - * - * @param string ...$versions - */ - public function isCompatibleApi(string ...$versions) : bool{ - $serverString = $this->server->getApiVersion(); - $serverApi = array_pad(explode("-", $serverString, 2), 2, ""); - $serverNumbers = array_map("\intval", explode(".", $serverApi[0])); - - foreach($versions as $version){ - //Format: majorVersion.minorVersion.patch (3.0.0) - // or: majorVersion.minorVersion.patch-devBuild (3.0.0-alpha1) - if($version !== $serverString){ - $pluginApi = array_pad(explode("-", $version, 2), 2, ""); //0 = version, 1 = suffix (optional) - - if(mb_strtoupper($pluginApi[1]) !== mb_strtoupper($serverApi[1])){ //Different release phase (alpha vs. beta) or phase build (alpha.1 vs alpha.2) - continue; - } - - $pluginNumbers = array_map("\intval", array_pad(explode(".", $pluginApi[0]), 3, "0")); //plugins might specify API like "3.0" or "3" - - if($pluginNumbers[0] !== $serverNumbers[0]){ //Completely different API version - continue; - } - - if($pluginNumbers[1] > $serverNumbers[1]){ //If the plugin requires new API features, being backwards compatible - continue; - } - - if($pluginNumbers[1] === $serverNumbers[1] and $pluginNumbers[2] > $serverNumbers[2]){ //If the plugin requires bug fixes in patches, being backwards compatible - continue; - } - } - - return true; - } - - return false; - } - - /** - * @deprecated - * @see PermissionManager::getPermission() - * - * @return null|Permission - */ - public function getPermission(string $name){ - return PermissionManager::getInstance()->getPermission($name); - } - - /** - * @deprecated - * @see PermissionManager::addPermission() - */ - public function addPermission(Permission $permission) : bool{ - return PermissionManager::getInstance()->addPermission($permission); - } - - /** - * @deprecated - * @see PermissionManager::removePermission() - * - * @param string|Permission $permission - * - * @return void - */ - public function removePermission($permission){ - PermissionManager::getInstance()->removePermission($permission); - } - - /** - * @deprecated - * @see PermissionManager::getDefaultPermissions() - * - * @return Permission[] - */ - public function getDefaultPermissions(bool $op) : array{ - return PermissionManager::getInstance()->getDefaultPermissions($op); - } - - /** - * @deprecated - * @see PermissionManager::recalculatePermissionDefaults() - * - * @return void - */ - public function recalculatePermissionDefaults(Permission $permission){ - PermissionManager::getInstance()->recalculatePermissionDefaults($permission); - } - - /** - * @deprecated - * @see PermissionManager::subscribeToPermission() - * - * @return void - */ - public function subscribeToPermission(string $permission, Permissible $permissible){ - PermissionManager::getInstance()->subscribeToPermission($permission, $permissible); - } - - /** - * @deprecated - * @see PermissionManager::unsubscribeFromPermission() - * - * @return void - */ - public function unsubscribeFromPermission(string $permission, Permissible $permissible){ - PermissionManager::getInstance()->unsubscribeFromPermission($permission, $permissible); - } - - /** - * @deprecated - * @see PermissionManager::unsubscribeFromAllPermissions() - */ - public function unsubscribeFromAllPermissions(Permissible $permissible) : void{ - PermissionManager::getInstance()->unsubscribeFromAllPermissions($permissible); - } - - /** - * @deprecated - * @see PermissionManager::getPermissionSubscriptions() - * - * @return array|Permissible[] - */ - public function getPermissionSubscriptions(string $permission) : array{ - return PermissionManager::getInstance()->getPermissionSubscriptions($permission); - } - - /** - * @deprecated - * @see PermissionManager::subscribeToDefaultPerms() - * - * @return void - */ - public function subscribeToDefaultPerms(bool $op, Permissible $permissible){ - PermissionManager::getInstance()->subscribeToDefaultPerms($op, $permissible); - } - - /** - * @deprecated - * @see PermissionManager::unsubscribeFromDefaultPerms() - * - * @return void - */ - public function unsubscribeFromDefaultPerms(bool $op, Permissible $permissible){ - PermissionManager::getInstance()->unsubscribeFromDefaultPerms($op, $permissible); - } - - /** - * @deprecated - * @see PermissionManager::getDefaultPermSubscriptions() - * - * @return Permissible[] - */ - public function getDefaultPermSubscriptions(bool $op) : array{ - return PermissionManager::getInstance()->getDefaultPermSubscriptions($op); - } - - /** - * @deprecated - * @see PermissionManager::getPermissions() - * - * @return Permission[] - */ - public function getPermissions() : array{ - return PermissionManager::getInstance()->getPermissions(); - } - - public function isPluginEnabled(Plugin $plugin) : bool{ - return isset($this->plugins[$plugin->getDescription()->getName()]) and $plugin->isEnabled(); - } - - /** - * @return void - */ - public function enablePlugin(Plugin $plugin){ - if(!$plugin->isEnabled()){ - try{ - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.plugin.enable", [$plugin->getDescription()->getFullName()])); - - $permManager = PermissionManager::getInstance(); - foreach($plugin->getDescription()->getPermissions() as $perm){ - $permManager->addPermission($perm); - } - $plugin->getScheduler()->setEnabled(true); - $plugin->setEnabled(true); - - $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin; - - (new PluginEnableEvent($plugin))->call(); - }catch(\Throwable $e){ - $this->server->getLogger()->logException($e); - $this->disablePlugin($plugin); - } - } - } - - /** - * @return PluginCommand[] - */ - protected function parseYamlCommands(Plugin $plugin) : array{ - $pluginCmds = []; - - foreach($plugin->getDescription()->getCommands() as $key => $data){ - if(strpos($key, ":") !== false){ - $this->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.commandError", [$key, $plugin->getDescription()->getFullName()])); - continue; - } - if(is_array($data)){ - $newCmd = new PluginCommand($key, $plugin); - 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->server->getLogger()->critical($this->server->getLanguage()->translateString("pocketmine.plugin.aliasError", [$alias, $plugin->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{ - throw new \InvalidArgumentException("Permission must be a string or boolean, " . gettype($data["permission"]) . " given"); - } - } - - if(isset($data["permission-message"])){ - $newCmd->setPermissionMessage($data["permission-message"]); - } - - $pluginCmds[] = $newCmd; - } - } - - return $pluginCmds; - } - - /** - * @return void - */ - public function disablePlugins(){ - foreach($this->getPlugins() as $plugin){ - $this->disablePlugin($plugin); - } - } - - /** - * @return void - */ - public function disablePlugin(Plugin $plugin){ - if($plugin->isEnabled()){ - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.plugin.disable", [$plugin->getDescription()->getFullName()])); - (new PluginDisableEvent($plugin))->call(); - - unset($this->enabledPlugins[$plugin->getDescription()->getName()]); - - try{ - $plugin->setEnabled(false); - }catch(\Throwable $e){ - $this->server->getLogger()->logException($e); - } - $plugin->getScheduler()->shutdown(); - HandlerList::unregisterAll($plugin); - $permManager = PermissionManager::getInstance(); - foreach($plugin->getDescription()->getPermissions() as $perm){ - $permManager->removePermission($perm); - } - } - } - - public function tickSchedulers(int $currentTick) : void{ - foreach($this->enabledPlugins as $p){ - $p->getScheduler()->mainThreadHeartbeat($currentTick); - } - } - - /** - * @return void - */ - public function clearPlugins(){ - $this->disablePlugins(); - $this->plugins = []; - $this->enabledPlugins = []; - $this->fileAssociations = []; - } - - /** - * Calls an event - * - * @deprecated - * @see Event::call() - * - * @return void - */ - public function callEvent(Event $event){ - $event->call(); - } - - /** - * Registers all the events in the given Listener class - * - * @throws PluginException - */ - public function registerEvents(Listener $listener, Plugin $plugin) : void{ - if(!$plugin->isEnabled()){ - throw new PluginException("Plugin attempted to register " . get_class($listener) . " while not enabled"); - } - - $reflection = new \ReflectionClass(get_class($listener)); - foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){ - if(!$method->isStatic() and $method->getDeclaringClass()->implementsInterface(Listener::class)){ - $tags = Utils::parseDocComment((string) $method->getDocComment()); - if(isset($tags["notHandler"])){ - continue; - } - - $parameters = $method->getParameters(); - if(count($parameters) !== 1){ - continue; - } - - $handlerClosure = $method->getClosure($listener); - if($handlerClosure === null) throw new AssumptionFailedError("This should never happen"); - - try{ - $paramType = $parameters[0]->getType(); - //isBuiltin() returns false for builtin classes .................. - if($paramType instanceof \ReflectionNamedType && !$paramType->isBuiltin()){ - /** @phpstan-var class-string $paramClass */ - $paramClass = $paramType->getName(); - $eventClass = new \ReflectionClass($paramClass); - }else{ - $eventClass = null; - } - }catch(\ReflectionException $e){ //class doesn't exist - if(isset($tags["softDepend"]) && !isset($this->plugins[$tags["softDepend"]])){ - $this->server->getLogger()->debug("Not registering @softDepend listener " . Utils::getNiceClosureName($handlerClosure) . "() because plugin \"" . $tags["softDepend"] . "\" not found"); - continue; - } - - throw $e; - } - if($eventClass === null or !$eventClass->isSubclassOf(Event::class)){ - continue; - } - - try{ - $priority = isset($tags["priority"]) ? EventPriority::fromString($tags["priority"]) : EventPriority::NORMAL; - }catch(\InvalidArgumentException $e){ - throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid/unknown priority \"" . $tags["priority"] . "\""); - } - - $ignoreCancelled = false; - if(isset($tags["ignoreCancelled"])){ - switch(strtolower($tags["ignoreCancelled"])){ - case "true": - case "": - $ignoreCancelled = true; - break; - case "false": - $ignoreCancelled = false; - break; - default: - throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @ignoreCancelled value \"" . $tags["ignoreCancelled"] . "\""); - } - } - - $this->registerEvent($eventClass->getName(), $listener, $priority, new MethodEventExecutor($method->getName()), $plugin, $ignoreCancelled); - } - } - } - - /** - * @param string $event Class name that extends Event - * @phpstan-param class-string $event - * - * @throws PluginException - */ - public function registerEvent(string $event, Listener $listener, int $priority, EventExecutor $executor, Plugin $plugin, bool $ignoreCancelled = false) : void{ - if(!is_subclass_of($event, Event::class)){ - throw new PluginException($event . " is not an Event"); - } - - if(!$plugin->isEnabled()){ - throw new PluginException("Plugin attempted to register " . $event . " while not enabled"); - } - - $timings = new TimingsHandler("Plugin: " . $plugin->getDescription()->getFullName() . " Event: " . get_class($listener) . "::" . ($executor instanceof MethodEventExecutor ? $executor->getMethod() : "???") . "(" . (new \ReflectionClass($event))->getShortName() . ")"); - - $this->getEventListeners($event)->register(new RegisteredListener($listener, $executor, $priority, $plugin, $ignoreCancelled, $timings)); - } - - private function getEventListeners(string $event) : HandlerList{ - $list = HandlerList::getHandlerListFor($event); - if($list === null){ - throw new PluginException("Abstract events not declaring @allowHandle cannot be handled (tried to register listener for $event)"); - } - return $list; - } -} diff --git a/src/pocketmine/resourcepacks/ResourcePackInfoEntry.php b/src/pocketmine/resourcepacks/ResourcePackInfoEntry.php deleted file mode 100644 index 1da3b40b32..0000000000 --- a/src/pocketmine/resourcepacks/ResourcePackInfoEntry.php +++ /dev/null @@ -1,51 +0,0 @@ -packId = $packId; - $this->version = $version; - $this->packSize = $packSize; - } - - public function getPackId() : string{ - return $this->packId; - } - - public function getVersion() : string{ - return $this->version; - } - - public function getPackSize() : int{ - return $this->packSize; - } -} diff --git a/src/pocketmine/resources/vanilla b/src/pocketmine/resources/vanilla deleted file mode 160000 index 482c679aa5..0000000000 --- a/src/pocketmine/resources/vanilla +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 482c679aa5ed0b81c088c2b1ff0b8110a94c8a6c diff --git a/src/pocketmine/scheduler/AsyncTask.php b/src/pocketmine/scheduler/AsyncTask.php deleted file mode 100644 index fbb6d00db7..0000000000 --- a/src/pocketmine/scheduler/AsyncTask.php +++ /dev/null @@ -1,317 +0,0 @@ - - * Used to store objects on the main thread which should not be serialized. - */ - private static $threadLocalStorage; - - /** @var AsyncWorker|null $worker */ - public $worker = null; - - /** @var \Threaded */ - public $progressUpdates; - - /** @var scalar|null */ - private $result = null; - /** @var bool */ - private $serialized = false; - /** @var bool */ - private $cancelRun = false; - /** @var int|null */ - private $taskId = null; - - /** @var bool */ - private $crashed = false; - - /** - * @return void - */ - public function run(){ - $this->result = null; - - if(!$this->cancelRun){ - try{ - $this->onRun(); - }catch(\Throwable $e){ - $this->crashed = true; - $this->worker->handleException($e); - } - } - - $this->setGarbage(); - } - - public function isCrashed() : bool{ - return $this->crashed or $this->isTerminated(); - } - - /** - * @return mixed - */ - public function getResult(){ - if($this->serialized){ - if(!is_string($this->result)) throw new AssumptionFailedError("Result expected to be a serialized string"); - return unserialize($this->result); - } - return $this->result; - } - - /** - * @return void - */ - public function cancelRun(){ - $this->cancelRun = true; - } - - public function hasCancelledRun() : bool{ - return $this->cancelRun; - } - - public function hasResult() : bool{ - return $this->result !== null; - } - - /** - * @param mixed $result - * - * @return void - */ - public function setResult($result){ - $this->result = ($this->serialized = !is_scalar($result)) ? serialize($result) : $result; - } - - /** - * @return void - */ - public function setTaskId(int $taskId){ - $this->taskId = $taskId; - } - - /** - * @return int|null - */ - public function getTaskId(){ - return $this->taskId; - } - - /** - * @deprecated - * @see AsyncWorker::getFromThreadStore() - * - * @return mixed - */ - public function getFromThreadStore(string $identifier){ - if($this->worker === null or $this->isGarbage()){ - throw new \BadMethodCallException("Objects stored in AsyncWorker thread-local storage can only be retrieved during task execution"); - } - return $this->worker->getFromThreadStore($identifier); - } - - /** - * @deprecated - * @see AsyncWorker::saveToThreadStore() - * - * @param mixed $value - * - * @return void - */ - public function saveToThreadStore(string $identifier, $value){ - if($this->worker === null or $this->isGarbage()){ - throw new \BadMethodCallException("Objects can only be added to AsyncWorker thread-local storage during task execution"); - } - $this->worker->saveToThreadStore($identifier, $value); - } - - /** - * @deprecated - * @see AsyncWorker::removeFromThreadStore() - */ - public function removeFromThreadStore(string $identifier) : void{ - if($this->worker === null or $this->isGarbage()){ - throw new \BadMethodCallException("Objects can only be removed from AsyncWorker thread-local storage during task execution"); - } - $this->worker->removeFromThreadStore($identifier); - } - - /** - * Actions to execute when run - * - * @return void - */ - abstract public function onRun(); - - /** - * Actions to execute when completed (on main thread) - * Implement this if you want to handle the data in your AsyncTask after it has been processed - * - * @return void - */ - public function onCompletion(Server $server){ - - } - - /** - * Call this method from {@link AsyncTask::onRun} (AsyncTask execution thread) to schedule a call to - * {@link AsyncTask::onProgressUpdate} from the main thread with the given progress parameter. - * - * @param mixed $progress A value that can be safely serialize()'ed. - * - * @return void - */ - public function publishProgress($progress){ - $this->progressUpdates[] = serialize($progress); - } - - /** - * @internal Only call from AsyncPool.php on the main thread - * - * @return void - */ - public function checkProgressUpdates(Server $server){ - while($this->progressUpdates->count() !== 0){ - $progress = $this->progressUpdates->shift(); - $this->onProgressUpdate($server, unserialize($progress)); - } - } - - /** - * Called from the main thread after {@link AsyncTask::publishProgress} is called. - * All {@link AsyncTask::publishProgress} calls should result in {@link AsyncTask::onProgressUpdate} calls before - * {@link AsyncTask::onCompletion} is called. - * - * @param mixed $progress The parameter passed to {@link AsyncTask::publishProgress}. It is serialize()'ed - * and then unserialize()'ed, as if it has been cloned. - * - * @return void - */ - public function onProgressUpdate(Server $server, $progress){ - - } - - /** - * Saves mixed data in thread-local storage on the parent thread. You may use this to retain references to objects - * or arrays which you need to access in {@link AsyncTask::onCompletion} which cannot be stored as a property of - * your task (due to them becoming serialized). - * - * Scalar types can be stored directly in class properties instead of using this storage. - * - * WARNING: THIS METHOD SHOULD ONLY BE CALLED FROM THE MAIN THREAD! - * - * @param mixed $complexData the data to store - * - * @return void - * @throws \BadMethodCallException if called from any thread except the main thread - */ - protected function storeLocal($complexData){ - if($this->worker !== null and $this->worker === \Thread::getCurrentThread()){ - throw new \BadMethodCallException("Objects can only be stored from the parent thread"); - } - - if(self::$threadLocalStorage === null){ - /** @phpstan-var \SplObjectStorage $storage */ - $storage = new \SplObjectStorage(); - self::$threadLocalStorage = $storage; //lazy init - } - - if(isset(self::$threadLocalStorage[$this])){ - throw new \InvalidStateException("Already storing complex data for this async task"); - } - self::$threadLocalStorage[$this] = $complexData; - } - - /** - * Returns data previously stored in thread-local storage on the parent thread. Use this during progress updates or - * task completion to retrieve data you stored using {@link AsyncTask::storeLocal}. - * - * WARNING: THIS METHOD SHOULD ONLY BE CALLED FROM THE MAIN THREAD! - * - * @return mixed - * - * @throws \RuntimeException if no data were stored by this AsyncTask instance. - * @throws \BadMethodCallException if called from any thread except the main thread - */ - protected function fetchLocal(){ - if($this->worker !== null and $this->worker === \Thread::getCurrentThread()){ - throw new \BadMethodCallException("Objects can only be retrieved from the parent thread"); - } - - if(self::$threadLocalStorage === null or !isset(self::$threadLocalStorage[$this])){ - throw new \InvalidStateException("No complex data stored for this async task"); - } - - return self::$threadLocalStorage[$this]; - } - - /** - * @deprecated - * @see AsyncTask::fetchLocal() - * - * @return mixed - * - * @throws \RuntimeException if no data were stored by this AsyncTask instance - * @throws \BadMethodCallException if called from any thread except the main thread - */ - protected function peekLocal(){ - return $this->fetchLocal(); - } - - /** - * @internal Called by the AsyncPool to destroy any leftover stored objects that this task failed to retrieve. - */ - public function removeDanglingStoredObjects() : void{ - if(self::$threadLocalStorage !== null and isset(self::$threadLocalStorage[$this])){ - unset(self::$threadLocalStorage[$this]); - } - } -} diff --git a/src/pocketmine/tile/Banner.php b/src/pocketmine/tile/Banner.php deleted file mode 100644 index 2037b2ce0a..0000000000 --- a/src/pocketmine/tile/Banner.php +++ /dev/null @@ -1,264 +0,0 @@ -baseColor = $nbt->getInt(self::TAG_BASE, self::COLOR_BLACK, true); - $this->patterns = $nbt->getListTag(self::TAG_PATTERNS) ?? new ListTag(self::TAG_PATTERNS); - $this->loadName($nbt); - } - - protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setInt(self::TAG_BASE, $this->baseColor); - $nbt->setTag($this->patterns); - $this->saveName($nbt); - } - - protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setInt(self::TAG_BASE, $this->baseColor); - $nbt->setTag($this->patterns); - $this->addNameSpawnData($nbt); - } - - /** - * Returns the color of the banner base. - */ - public function getBaseColor() : int{ - return $this->baseColor; - } - - /** - * Sets the color of the banner base. - */ - public function setBaseColor(int $color) : void{ - $this->baseColor = $color; - $this->onChanged(); - } - - /** - * Applies a new pattern on the banner with the given color. - * - * @return int ID of pattern. - */ - public function addPattern(string $pattern, int $color) : int{ - $this->patterns->push(new CompoundTag("", [ - new IntTag(self::TAG_PATTERN_COLOR, $color & 0x0f), - new StringTag(self::TAG_PATTERN_NAME, $pattern) - ])); - - $this->onChanged(); - return $this->patterns->count() - 1; //Last offset in the list - } - - /** - * Returns whether a pattern with the given ID exists on the banner or not. - */ - public function patternExists(int $patternId) : bool{ - return $this->patterns->isset($patternId); - } - - /** - * Returns the data of a pattern with the given ID. - * - * @return mixed[] - * @phpstan-return array{Color?: int, Pattern?: string} - */ - public function getPatternData(int $patternId) : array{ - if(!$this->patternExists($patternId)){ - return []; - } - - $patternTag = $this->patterns->get($patternId); - assert($patternTag instanceof CompoundTag); - - return [ - self::TAG_PATTERN_COLOR => $patternTag->getInt(self::TAG_PATTERN_COLOR), - self::TAG_PATTERN_NAME => $patternTag->getString(self::TAG_PATTERN_NAME) - ]; - } - - /** - * Changes the pattern of a previously existing pattern. - * - * @return bool indicating success. - */ - public function changePattern(int $patternId, string $pattern, int $color) : bool{ - if(!$this->patternExists($patternId)){ - return false; - } - - $this->patterns->set($patternId, new CompoundTag("", [ - new IntTag(self::TAG_PATTERN_COLOR, $color & 0x0f), - new StringTag(self::TAG_PATTERN_NAME, $pattern) - ])); - - $this->onChanged(); - return true; - } - - /** - * Deletes a pattern from the banner with the given ID. - * - * @return bool indicating whether the pattern existed or not. - */ - public function deletePattern(int $patternId) : bool{ - if(!$this->patternExists($patternId)){ - return false; - } - - $this->patterns->remove($patternId); - - $this->onChanged(); - return true; - } - - /** - * Deletes the top most pattern of the banner. - * - * @return bool indicating whether the banner was empty or not. - */ - public function deleteTopPattern() : bool{ - return $this->deletePattern($this->getPatternCount() - 1); - } - - /** - * Deletes the bottom pattern of the banner. - * - * @return bool indicating whether the banner was empty or not. - */ - public function deleteBottomPattern() : bool{ - return $this->deletePattern(0); - } - - /** - * Returns the total count of patterns on this banner. - */ - public function getPatternCount() : int{ - return $this->patterns->count(); - } - - public function getPatterns() : ListTag{ - return $this->patterns; - } - - protected static function createAdditionalNBT(CompoundTag $nbt, Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : void{ - $nbt->setInt(self::TAG_BASE, $item !== null ? $item->getDamage() & 0x0f : 0); - - if($item !== null){ - if($item->getNamedTag()->hasTag(self::TAG_PATTERNS, ListTag::class)){ - $nbt->setTag($item->getNamedTag()->getListTag(self::TAG_PATTERNS)); - } - - self::createNameNBT($nbt, $pos, $face, $item, $player); - } - } - - public function getDefaultName() : string{ - return "Banner"; - } -} diff --git a/src/pocketmine/tile/FlowerPot.php b/src/pocketmine/tile/FlowerPot.php deleted file mode 100644 index 1070d0d70d..0000000000 --- a/src/pocketmine/tile/FlowerPot.php +++ /dev/null @@ -1,96 +0,0 @@ -item = ItemFactory::get($nbt->getShort(self::TAG_ITEM, 0, true), $nbt->getInt(self::TAG_ITEM_DATA, 0, true), 1); - } - - protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setShort(self::TAG_ITEM, $this->item->getId()); - $nbt->setInt(self::TAG_ITEM_DATA, $this->item->getDamage()); - } - - public function canAddItem(Item $item) : bool{ - if(!$this->isEmpty()){ - return false; - } - switch($item->getId()){ - /** @noinspection PhpMissingBreakStatementInspection */ - case Item::TALL_GRASS: - if($item->getDamage() === 1){ - return false; - } - case Item::SAPLING: - case Item::DEAD_BUSH: - case Item::DANDELION: - case Item::RED_FLOWER: - case Item::BROWN_MUSHROOM: - case Item::RED_MUSHROOM: - case Item::CACTUS: - return true; - default: - return false; - } - } - - public function getItem() : Item{ - return clone $this->item; - } - - /** - * @return void - */ - public function setItem(Item $item){ - $this->item = clone $item; - $this->onChanged(); - } - - /** - * @return void - */ - public function removeItem(){ - $this->setItem(ItemFactory::get(Item::AIR, 0, 0)); - } - - public function isEmpty() : bool{ - return $this->getItem()->isNull(); - } - - protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setShort(self::TAG_ITEM, $this->item->getId()); - $nbt->setInt(self::TAG_ITEM_DATA, $this->item->getDamage()); - } -} diff --git a/src/pocketmine/tile/Furnace.php b/src/pocketmine/tile/Furnace.php deleted file mode 100644 index 22ab6adf67..0000000000 --- a/src/pocketmine/tile/Furnace.php +++ /dev/null @@ -1,258 +0,0 @@ -burnTime > 0){ - $this->scheduleUpdate(); - } - } - - protected function readSaveData(CompoundTag $nbt) : void{ - $this->burnTime = max(0, $nbt->getShort(self::TAG_BURN_TIME, 0, true)); - - $this->cookTime = $nbt->getShort(self::TAG_COOK_TIME, 0, true); - if($this->burnTime === 0){ - $this->cookTime = 0; - } - - $this->maxTime = $nbt->getShort(self::TAG_MAX_TIME, 0, true); - if($this->maxTime === 0){ - $this->maxTime = $this->burnTime; - } - - $this->loadName($nbt); - - $this->inventory = new FurnaceInventory($this); - $this->loadItems($nbt); - - $this->inventory->setEventProcessor(new class($this) implements InventoryEventProcessor{ - /** @var Furnace */ - private $furnace; - - public function __construct(Furnace $furnace){ - $this->furnace = $furnace; - } - - public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem, Item $newItem) : ?Item{ - $this->furnace->scheduleUpdate(); - return $newItem; - } - }); - } - - protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setShort(self::TAG_BURN_TIME, $this->burnTime); - $nbt->setShort(self::TAG_COOK_TIME, $this->cookTime); - $nbt->setShort(self::TAG_MAX_TIME, $this->maxTime); - $this->saveName($nbt); - $this->saveItems($nbt); - } - - public function getDefaultName() : string{ - return "Furnace"; - } - - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(true); - $this->inventory = null; - - parent::close(); - } - } - - /** - * @return FurnaceInventory - */ - public function getInventory(){ - return $this->inventory; - } - - /** - * @return FurnaceInventory - */ - public function getRealInventory(){ - return $this->getInventory(); - } - - /** - * @return void - */ - protected function checkFuel(Item $fuel){ - $ev = new FurnaceBurnEvent($this, $fuel, $fuel->getFuelTime()); - $ev->call(); - if($ev->isCancelled()){ - return; - } - - $this->maxTime = $this->burnTime = $ev->getBurnTime(); - - if($this->getBlock()->getId() === Block::FURNACE){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::BURNING_FURNACE, $this->getBlock()->getDamage()), true); - } - - if($this->burnTime > 0 and $ev->isBurning()){ - $this->inventory->setFuel($fuel->getFuelResidue()); - } - } - - protected function getFuelTicksLeft() : int{ - return $this->maxTime > 0 ? (int) ceil($this->burnTime / $this->maxTime * 200) : 0; - } - - public function onUpdate() : bool{ - if($this->closed){ - return false; - } - - $this->timings->startTiming(); - - $prevCookTime = $this->cookTime; - $prevFuelTicksLeft = $this->getFuelTicksLeft(); - - $ret = false; - - $fuel = $this->inventory->getFuel(); - $raw = $this->inventory->getSmelting(); - $product = $this->inventory->getResult(); - $smelt = $this->server->getCraftingManager()->matchFurnaceRecipe($raw); - $canSmelt = ($smelt instanceof FurnaceRecipe and $raw->getCount() > 0 and (($smelt->getResult()->equals($product) and $product->getCount() < $product->getMaxStackSize()) or $product->isNull())); - - if($this->burnTime <= 0 and $canSmelt and $fuel->getFuelTime() > 0 and $fuel->getCount() > 0){ - $this->checkFuel($fuel); - } - - if($this->burnTime > 0){ - --$this->burnTime; - - if($smelt instanceof FurnaceRecipe and $canSmelt){ - ++$this->cookTime; - - if($this->cookTime >= 200){ //10 seconds - $product = ItemFactory::get($smelt->getResult()->getId(), $smelt->getResult()->getDamage(), $product->getCount() + 1); - - $ev = new FurnaceSmeltEvent($this, $raw, $product); - $ev->call(); - - if(!$ev->isCancelled()){ - $this->inventory->setResult($ev->getResult()); - $raw->pop(); - $this->inventory->setSmelting($raw); - } - - $this->cookTime -= 200; - } - }elseif($this->burnTime <= 0){ - $this->burnTime = $this->cookTime = $this->maxTime = 0; - }else{ - $this->cookTime = 0; - } - $ret = true; - }else{ - if($this->getBlock()->getId() === Block::BURNING_FURNACE){ - $this->getLevelNonNull()->setBlock($this, BlockFactory::get(Block::FURNACE, $this->getBlock()->getDamage()), true); - } - $this->burnTime = $this->cookTime = $this->maxTime = 0; - } - - /** @var ContainerSetDataPacket[] $packets */ - $packets = []; - if($prevCookTime !== $this->cookTime){ - $pk = new ContainerSetDataPacket(); - $pk->property = ContainerSetDataPacket::PROPERTY_FURNACE_TICK_COUNT; - $pk->value = $this->cookTime; - $packets[] = $pk; - } - - $fuelTicksLeft = $this->getFuelTicksLeft(); - if($prevFuelTicksLeft !== $fuelTicksLeft){ - $pk = new ContainerSetDataPacket(); - $pk->property = ContainerSetDataPacket::PROPERTY_FURNACE_LIT_TIME; - $pk->value = $fuelTicksLeft; - $packets[] = $pk; - } - - if(count($packets) > 0){ - foreach($this->getInventory()->getViewers() as $player){ - $windowId = $player->getWindowId($this->getInventory()); - if($windowId > 0){ - foreach($packets as $pk){ - $pk->windowId = $windowId; - $player->dataPacket(clone $pk); - } - } - } - } - - $this->timings->stopTiming(); - - return $ret; - } - - protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setShort(self::TAG_BURN_TIME, $this->burnTime); - $nbt->setShort(self::TAG_COOK_TIME, $this->cookTime); - - $this->addNameSpawnData($nbt); - } -} diff --git a/src/pocketmine/tile/Sign.php b/src/pocketmine/tile/Sign.php deleted file mode 100644 index 747172be31..0000000000 --- a/src/pocketmine/tile/Sign.php +++ /dev/null @@ -1,193 +0,0 @@ -hasTag(self::TAG_TEXT_BLOB, StringTag::class)){ //MCPE 1.2 save format - $this->text = self::fixTextBlob($nbt->getString(self::TAG_TEXT_BLOB)); - }else{ - for($i = 1; $i <= 4; ++$i){ - $textKey = sprintf(self::TAG_TEXT_LINE, $i); - if($nbt->hasTag($textKey, StringTag::class)){ - $this->text[$i - 1] = $nbt->getString($textKey); - } - } - } - $this->text = array_map(function(string $line) : string{ - return mb_scrub($line, 'UTF-8'); - }, $this->text); - } - - protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text)); - - for($i = 1; $i <= 4; ++$i){ //Backwards-compatibility - $textKey = sprintf(self::TAG_TEXT_LINE, $i); - $nbt->setString($textKey, $this->getLine($i - 1)); - } - } - - /** - * Changes contents of the specific lines to the string provided. - * Leaves contents of the specific lines as is if null is provided. - */ - public function setText(?string $line1 = "", ?string $line2 = "", ?string $line3 = "", ?string $line4 = "") : void{ - if($line1 !== null){ - $this->setLine(0, $line1, false); - } - if($line2 !== null){ - $this->setLine(1, $line2, false); - } - if($line3 !== null){ - $this->setLine(2, $line3, false); - } - if($line4 !== null){ - $this->setLine(3, $line4, false); - } - - $this->onChanged(); - } - - /** - * @param int $index 0-3 - */ - public function setLine(int $index, string $line, bool $update = true) : void{ - if($index < 0 or $index > 3){ - throw new \InvalidArgumentException("Index must be in the range 0-3!"); - } - if(!mb_check_encoding($line, 'UTF-8')){ - throw new \InvalidArgumentException("Text must be valid UTF-8"); - } - - $this->text[$index] = $line; - if($update){ - $this->onChanged(); - } - } - - /** - * @param int $index 0-3 - */ - public function getLine(int $index) : string{ - if($index < 0 or $index > 3){ - throw new \InvalidArgumentException("Index must be in the range 0-3!"); - } - return $this->text[$index]; - } - - /** - * @return string[] - */ - public function getText() : array{ - return $this->text; - } - - /** - * Returns the entity runtime ID of the player who placed this sign. Only the player whose entity ID matches this - * one may edit the sign text. - * This is needed because as of 1.16.220, there is still no reliable way to detect when the MCPE client closed the - * sign edit GUI, so we have no way to know when the text is finalized. This limits editing of the text to only the - * player who placed it, and only while that player is online. - * We can say for sure that the sign is finalized if either of the following occurs: - * - The player quits (after rejoin, the player's entity runtimeID will be different). - * - The chunk is unloaded (on next load, the entity runtimeID will be null, because it's not saved). - */ - public function getEditorEntityRuntimeId() : ?int{ return $this->editorEntityRuntimeId; } - - public function setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : void{ - $this->editorEntityRuntimeId = $editorEntityRuntimeId; - } - - protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text)); - } - - public function updateCompoundTag(CompoundTag $nbt, Player $player) : bool{ - if($nbt->getString("id") !== Tile::SIGN){ - return false; - } - - if($nbt->hasTag(self::TAG_TEXT_BLOB, StringTag::class)){ - $lines = self::fixTextBlob($nbt->getString(self::TAG_TEXT_BLOB)); - }else{ - return false; - } - $size = 0; - foreach($lines as $line){ - $size += strlen($line); - } - if($size > 1000){ - //trigger kick + IP ban - TODO: on 4.0 this will require a better fix - throw new \UnexpectedValueException($player->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)"); - } - - $removeFormat = $player->getRemoveFormat(); - - $ev = new SignChangeEvent($this->getBlock(), $player, array_map(function(string $line) use ($removeFormat) : string{ return TextFormat::clean($line, $removeFormat); }, $lines)); - if($this->editorEntityRuntimeId === null || $this->editorEntityRuntimeId !== $player->getId()){ - $ev->setCancelled(); - } - $ev->call(); - - if(!$ev->isCancelled()){ - $this->setText(...$ev->getLines()); - - return true; - }else{ - return false; - } - } -} diff --git a/src/pocketmine/tile/Skull.php b/src/pocketmine/tile/Skull.php deleted file mode 100644 index 0568c8373c..0000000000 --- a/src/pocketmine/tile/Skull.php +++ /dev/null @@ -1,86 +0,0 @@ -skullType = $nbt->getByte(self::TAG_SKULL_TYPE, self::TYPE_SKELETON, true); - $this->skullRotation = $nbt->getByte(self::TAG_ROT, 0, true); - } - - protected function writeSaveData(CompoundTag $nbt) : void{ - $nbt->setByte(self::TAG_SKULL_TYPE, $this->skullType); - $nbt->setByte(self::TAG_ROT, $this->skullRotation); - } - - /** - * @return void - */ - public function setType(int $type){ - $this->skullType = $type; - $this->onChanged(); - } - - public function getType() : int{ - return $this->skullType; - } - - protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ - $nbt->setByte(self::TAG_SKULL_TYPE, $this->skullType); - $nbt->setByte(self::TAG_ROT, $this->skullRotation); - } - - protected static function createAdditionalNBT(CompoundTag $nbt, Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : void{ - $nbt->setByte(self::TAG_SKULL_TYPE, $item !== null ? $item->getDamage() : self::TYPE_SKELETON); - - $rot = 0; - if($face === Vector3::SIDE_UP and $player !== null){ - $rot = floor(($player->yaw * 16 / 360) + 0.5) & 0x0F; - } - $nbt->setByte(self::TAG_ROT, $rot); - } -} diff --git a/src/pocketmine/tile/Spawnable.php b/src/pocketmine/tile/Spawnable.php deleted file mode 100644 index 41727905b1..0000000000 --- a/src/pocketmine/tile/Spawnable.php +++ /dev/null @@ -1,134 +0,0 @@ -x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->namedtag = $this->getSerializedSpawnCompound(); - - return $pk; - } - - public function spawnTo(Player $player) : bool{ - if($this->closed){ - return false; - } - - $player->dataPacket($this->createSpawnPacket()); - - return true; - } - - public function __construct(Level $level, CompoundTag $nbt){ - parent::__construct($level, $nbt); - $this->spawnToAll(); - } - - /** - * @return void - */ - public function spawnToAll(){ - if($this->closed){ - return; - } - - $this->level->broadcastPacketToViewers($this, $this->createSpawnPacket()); - } - - /** - * Performs actions needed when the tile is modified, such as clearing caches and respawning the tile to players. - * WARNING: This MUST be called to clear spawn-compound and chunk caches when the tile's spawn compound has changed! - */ - protected function onChanged() : void{ - $this->spawnCompoundCache = null; - $this->spawnToAll(); - - $this->level->clearChunkCache($this->getFloorX() >> 4, $this->getFloorZ() >> 4); - } - - /** - * Returns encoded NBT (varint, little-endian) used to spawn this tile to clients. Uses cache where possible, - * populates cache if it is null. - * - * @return string encoded NBT - */ - final public function getSerializedSpawnCompound() : string{ - if($this->spawnCompoundCache === null){ - if(self::$nbtWriter === null){ - self::$nbtWriter = new NetworkLittleEndianNBTStream(); - } - - $spawnCompoundCache = self::$nbtWriter->write($this->getSpawnCompound()); - if($spawnCompoundCache === false) throw new AssumptionFailedError("NBTStream->write() should not return false when given a CompoundTag"); - $this->spawnCompoundCache = $spawnCompoundCache; - } - - return $this->spawnCompoundCache; - } - - final public function getSpawnCompound() : CompoundTag{ - $nbt = new CompoundTag("", [ - new StringTag(self::TAG_ID, static::getSaveId()), - new IntTag(self::TAG_X, $this->x), - new IntTag(self::TAG_Y, $this->y), - new IntTag(self::TAG_Z, $this->z) - ]); - $this->addAdditionalSpawnData($nbt); - return $nbt; - } - - /** - * An extension to getSpawnCompound() for - * further modifying the generic tile NBT. - */ - abstract protected function addAdditionalSpawnData(CompoundTag $nbt) : void; - - /** - * Called when a player updates a block entity's NBT data - * for example when writing on a sign. - * - * @return bool indication of success, will respawn the tile to the player if false. - */ - public function updateCompoundTag(CompoundTag $nbt, Player $player) : bool{ - return false; - } -} diff --git a/src/pocketmine/tile/Tile.php b/src/pocketmine/tile/Tile.php deleted file mode 100644 index cdeee29928..0000000000 --- a/src/pocketmine/tile/Tile.php +++ /dev/null @@ -1,274 +0,0 @@ -> - */ - private static $knownTiles = []; - /** - * @var string[] - * @phpstan-var array, string> - */ - private static $saveNames = []; - - /** @var string */ - public $name; - /** @var int */ - public $id; - /** @var bool */ - public $closed = false; - /** @var Server */ - protected $server; - /** @var TimingsHandler */ - protected $timings; - - /** - * @return void - */ - public static function init(){ - self::registerTile(Banner::class, [self::BANNER, "minecraft:banner"]); - self::registerTile(Bed::class, [self::BED, "minecraft:bed"]); - self::registerTile(Chest::class, [self::CHEST, "minecraft:chest"]); - self::registerTile(EnchantTable::class, [self::ENCHANT_TABLE, "minecraft:enchanting_table"]); - self::registerTile(EnderChest::class, [self::ENDER_CHEST, "minecraft:ender_chest"]); - self::registerTile(FlowerPot::class, [self::FLOWER_POT, "minecraft:flower_pot"]); - self::registerTile(Furnace::class, [self::FURNACE, "minecraft:furnace"]); - self::registerTile(ItemFrame::class, [self::ITEM_FRAME]); //this is an entity in PC - self::registerTile(Sign::class, [self::SIGN, "minecraft:sign"]); - self::registerTile(Skull::class, [self::SKULL, "minecraft:skull"]); - } - - /** - * @param string $type - * @param mixed ...$args - */ - public static function createTile($type, Level $level, CompoundTag $nbt, ...$args) : ?Tile{ - if(isset(self::$knownTiles[$type])){ - $class = self::$knownTiles[$type]; - /** @see Tile::__construct() */ - return new $class($level, $nbt, ...$args); - } - - return null; - } - - /** - * @param string[] $saveNames - * @phpstan-param class-string $className - * - * @throws \ReflectionException - */ - public static function registerTile(string $className, array $saveNames = []) : bool{ - $class = new \ReflectionClass($className); - if(is_a($className, Tile::class, true) and !$class->isAbstract()){ - $shortName = $class->getShortName(); - if(!in_array($shortName, $saveNames, true)){ - $saveNames[] = $shortName; - } - - foreach($saveNames as $name){ - self::$knownTiles[$name] = $className; - } - - self::$saveNames[$className] = reset($saveNames); - - return true; - } - - return false; - } - - /** - * Returns the short save name - */ - public static function getSaveId() : string{ - if(!isset(self::$saveNames[static::class])){ - throw new \InvalidStateException("Tile is not registered"); - } - - return self::$saveNames[static::class]; - } - - public function __construct(Level $level, CompoundTag $nbt){ - $this->timings = Timings::getTileEntityTimings($this); - - $this->server = $level->getServer(); - $this->name = ""; - $this->id = Tile::$tileCount++; - - parent::__construct($nbt->getInt(self::TAG_X), $nbt->getInt(self::TAG_Y), $nbt->getInt(self::TAG_Z), $level); - $this->readSaveData($nbt); - - $this->getLevelNonNull()->addTile($this); - } - - public function getId() : int{ - return $this->id; - } - - /** - * Reads additional data from the CompoundTag on tile creation. - */ - abstract protected function readSaveData(CompoundTag $nbt) : void; - - /** - * Writes additional save data to a CompoundTag, not including generic things like ID and coordinates. - */ - abstract protected function writeSaveData(CompoundTag $nbt) : void; - - public function saveNBT() : CompoundTag{ - $nbt = new CompoundTag(); - $nbt->setString(self::TAG_ID, static::getSaveId()); - $nbt->setInt(self::TAG_X, $this->x); - $nbt->setInt(self::TAG_Y, $this->y); - $nbt->setInt(self::TAG_Z, $this->z); - $this->writeSaveData($nbt); - - return $nbt; - } - - public function getCleanedNBT() : ?CompoundTag{ - $this->writeSaveData($tag = new CompoundTag()); - return $tag->getCount() > 0 ? $tag : null; - } - - /** - * Creates and returns a CompoundTag containing the necessary information to spawn a tile of this type. - */ - public static function createNBT(Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : CompoundTag{ - if(static::class === self::class){ - throw new \BadMethodCallException(__METHOD__ . " must be called from the scope of a child class"); - } - $nbt = new CompoundTag("", [ - new StringTag(self::TAG_ID, static::getSaveId()), - new IntTag(self::TAG_X, (int) $pos->x), - new IntTag(self::TAG_Y, (int) $pos->y), - new IntTag(self::TAG_Z, (int) $pos->z) - ]); - - static::createAdditionalNBT($nbt, $pos, $face, $item, $player); - - if($item !== null){ - $customBlockData = $item->getCustomBlockData(); - if($customBlockData !== null){ - foreach($customBlockData as $customBlockDataTag){ - $nbt->setTag(clone $customBlockDataTag); - } - } - } - - return $nbt; - } - - /** - * Called by createNBT() to allow descendent classes to add their own base NBT using the parameters provided. - */ - protected static function createAdditionalNBT(CompoundTag $nbt, Vector3 $pos, ?int $face = null, ?Item $item = null, ?Player $player = null) : void{ - - } - - public function getBlock() : Block{ - return $this->level->getBlockAt($this->x, $this->y, $this->z); - } - - public function onUpdate() : bool{ - return false; - } - - final public function scheduleUpdate() : void{ - if($this->closed){ - throw new \InvalidStateException("Cannot schedule update on garbage tile " . get_class($this)); - } - $this->level->updateTiles[$this->id] = $this; - } - - public function isClosed() : bool{ - return $this->closed; - } - - public function __destruct(){ - $this->close(); - } - - public function close() : void{ - if(!$this->closed){ - $this->closed = true; - - if($this->isValid()){ - $this->level->removeTile($this); - $this->setLevel(null); - } - } - } - - public function getName() : string{ - return $this->name; - } -} diff --git a/src/pocketmine/timings/Timings.php b/src/pocketmine/timings/Timings.php deleted file mode 100644 index 6fd16e2464..0000000000 --- a/src/pocketmine/timings/Timings.php +++ /dev/null @@ -1,209 +0,0 @@ -getOwnerName() . " Runnable: " . $task->getTaskName(); - - if($period > 0){ - $name .= "(interval:" . $period . ")"; - }else{ - $name .= "(Single)"; - } - - if(!isset(self::$pluginTaskTimingMap[$name])){ - self::$pluginTaskTimingMap[$name] = new TimingsHandler($name, self::$schedulerSyncTimer); - } - - return self::$pluginTaskTimingMap[$name]; - } - - public static function getEntityTimings(Entity $entity) : TimingsHandler{ - $entityType = (new \ReflectionClass($entity))->getShortName(); - if(!isset(self::$entityTypeTimingMap[$entityType])){ - if($entity instanceof Player){ - self::$entityTypeTimingMap[$entityType] = new TimingsHandler("** tickEntity - EntityPlayer", self::$tickEntityTimer); - }else{ - self::$entityTypeTimingMap[$entityType] = new TimingsHandler("** tickEntity - " . $entityType, self::$tickEntityTimer); - } - } - - return self::$entityTypeTimingMap[$entityType]; - } - - public static function getTileEntityTimings(Tile $tile) : TimingsHandler{ - $tileType = (new \ReflectionClass($tile))->getShortName(); - if(!isset(self::$tileEntityTypeTimingMap[$tileType])){ - self::$tileEntityTypeTimingMap[$tileType] = new TimingsHandler("** tickTileEntity - " . $tileType, self::$tickTileEntityTimer); - } - - return self::$tileEntityTypeTimingMap[$tileType]; - } - - public static function getReceiveDataPacketTimings(DataPacket $pk) : TimingsHandler{ - if(!isset(self::$packetReceiveTimingMap[$pk::NETWORK_ID])){ - $pkName = (new \ReflectionClass($pk))->getShortName(); - self::$packetReceiveTimingMap[$pk::NETWORK_ID] = new TimingsHandler("** receivePacket - " . $pkName . " [0x" . dechex($pk::NETWORK_ID) . "]", self::$playerNetworkReceiveTimer); - } - - return self::$packetReceiveTimingMap[$pk::NETWORK_ID]; - } - - public static function getSendDataPacketTimings(DataPacket $pk) : TimingsHandler{ - if(!isset(self::$packetSendTimingMap[$pk::NETWORK_ID])){ - $pkName = (new \ReflectionClass($pk))->getShortName(); - self::$packetSendTimingMap[$pk::NETWORK_ID] = new TimingsHandler("** sendPacket - " . $pkName . " [0x" . dechex($pk::NETWORK_ID) . "]", self::$playerNetworkTimer); - } - - return self::$packetSendTimingMap[$pk::NETWORK_ID]; - } -} diff --git a/src/pocketmine/timings/TimingsHandler.php b/src/pocketmine/timings/TimingsHandler.php deleted file mode 100644 index 5538c070f8..0000000000 --- a/src/pocketmine/timings/TimingsHandler.php +++ /dev/null @@ -1,232 +0,0 @@ -totalTime; - $count = $timings->count; - if($count === 0){ - continue; - } - - $avg = $time / $count; - - fwrite($fp, " " . $timings->name . " Time: " . round($time * 1000000000) . " Count: " . $count . " Avg: " . round($avg * 1000000000) . " Violations: " . $timings->violations . PHP_EOL); - } - - fwrite($fp, "# Version " . Server::getInstance()->getVersion() . PHP_EOL); - fwrite($fp, "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion() . PHP_EOL); - - $entities = 0; - $livingEntities = 0; - foreach(Server::getInstance()->getLevels() as $level){ - $entities += count($level->getEntities()); - foreach($level->getEntities() as $e){ - if($e instanceof Living){ - ++$livingEntities; - } - } - } - - fwrite($fp, "# Entities " . $entities . PHP_EOL); - fwrite($fp, "# LivingEntities " . $livingEntities . PHP_EOL); - - $sampleTime = microtime(true) - self::$timingStart; - fwrite($fp, "Sample time " . round($sampleTime * 1000000000) . " (" . $sampleTime . "s)" . PHP_EOL); - } - - public static function isEnabled() : bool{ - return self::$enabled; - } - - public static function setEnabled(bool $enable = true) : void{ - self::$enabled = $enable; - self::reload(); - } - - public static function getStartTime() : float{ - return self::$timingStart; - } - - /** - * @return void - */ - public static function reload(){ - if(self::$enabled){ - foreach(self::$HANDLERS as $timings){ - $timings->reset(); - } - self::$timingStart = microtime(true); - } - } - - /** - * @return void - */ - public static function tick(bool $measure = true){ - if(self::$enabled){ - if($measure){ - foreach(self::$HANDLERS as $timings){ - if($timings->curTickTotal > 0.05){ - $timings->violations += (int) round($timings->curTickTotal / 0.05); - } - $timings->curTickTotal = 0; - $timings->curCount = 0; - $timings->timingDepth = 0; - } - }else{ - foreach(self::$HANDLERS as $timings){ - $timings->totalTime -= $timings->curTickTotal; - $timings->count -= $timings->curCount; - - $timings->curTickTotal = 0; - $timings->curCount = 0; - $timings->timingDepth = 0; - } - } - } - } - - /** @var string */ - private $name; - /** @var TimingsHandler|null */ - private $parent = null; - - /** @var int */ - private $count = 0; - /** @var int */ - private $curCount = 0; - /** @var float */ - private $start = 0; - /** @var int */ - private $timingDepth = 0; - /** @var float */ - private $totalTime = 0; - /** @var float */ - private $curTickTotal = 0; - /** @var int */ - private $violations = 0; - - public function __construct(string $name, TimingsHandler $parent = null){ - $this->name = $name; - $this->parent = $parent; - - self::$HANDLERS[spl_object_hash($this)] = $this; - } - - /** - * @return void - */ - public function startTiming(){ - if(self::$enabled){ - $this->internalStartTiming(microtime(true)); - } - } - - private function internalStartTiming(float $now) : void{ - if(++$this->timingDepth === 1){ - $this->start = $now; - if($this->parent !== null){ - $this->parent->internalStartTiming($now); - } - } - } - - /** - * @return void - */ - public function stopTiming(){ - if(self::$enabled){ - $this->internalStopTiming(microtime(true)); - } - } - - private function internalStopTiming(float $now) : void{ - if($this->timingDepth === 0){ - //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets - //and enable/disable, it would have a performance impact. Therefore, considering the limited - //usefulness of bailing here anyway, we don't currently bother. - return; - } - if(--$this->timingDepth !== 0 or $this->start == 0){ - return; - } - - $diff = $now - $this->start; - $this->totalTime += $diff; - $this->curTickTotal += $diff; - ++$this->curCount; - ++$this->count; - $this->start = 0; - if($this->parent !== null){ - $this->parent->internalStopTiming($now); - } - } - - /** - * @return void - */ - public function reset(){ - $this->count = 0; - $this->curCount = 0; - $this->violations = 0; - $this->curTickTotal = 0; - $this->totalTime = 0; - $this->start = 0; - $this->timingDepth = 0; - } - - /** - * @return void - */ - public function remove(){ - unset(self::$HANDLERS[spl_object_hash($this)]); - } -} diff --git a/src/pocketmine/updater/AutoUpdater.php b/src/pocketmine/updater/AutoUpdater.php deleted file mode 100644 index e0d35adccd..0000000000 --- a/src/pocketmine/updater/AutoUpdater.php +++ /dev/null @@ -1,216 +0,0 @@ -|null - */ - protected $updateInfo = null; - /** @var VersionString|null */ - protected $newVersion; - - public function __construct(Server $server, string $endpoint){ - $this->server = $server; - $this->endpoint = "http://$endpoint/api/"; - - if((bool) $server->getProperty("auto-updater.enabled", true)){ - $this->doCheck(); - } - } - - /** - * Callback used at the end of the update checking task - * - * @param mixed[] $updateInfo - * @phpstan-param array $updateInfo - * - * @return void - */ - public function checkUpdateCallback(array $updateInfo){ - $this->updateInfo = $updateInfo; - $this->checkUpdate(); - if($this->hasUpdate()){ - (new UpdateNotifyEvent($this))->call(); - if((bool) $this->server->getProperty("auto-updater.on-update.warn-console", true)){ - $this->showConsoleUpdate(); - } - }else{ - if(!\pocketmine\IS_DEVELOPMENT_BUILD and $this->getChannel() !== "stable"){ - $this->showChannelSuggestionStable(); - }elseif(\pocketmine\IS_DEVELOPMENT_BUILD and $this->getChannel() === "stable"){ - $this->showChannelSuggestionBeta(); - } - } - } - - /** - * Returns whether there is an update available. - */ - public function hasUpdate() : bool{ - return $this->newVersion !== null; - } - - /** - * Posts a warning to the console to tell the user there is an update available - * - * @return void - */ - public function showConsoleUpdate(){ - $messages = [ - "Your version of " . $this->server->getName() . " is out of date. Version " . $this->newVersion->getFullVersion(true) . " was released on " . date("D M j h:i:s Y", $this->updateInfo["date"]) - ]; - if($this->updateInfo["details_url"] !== null){ - $messages[] = "Details: " . $this->updateInfo["details_url"]; - } - $messages[] = "Download: " . $this->updateInfo["download_url"]; - - $this->printConsoleMessage($messages, \LogLevel::WARNING); - } - - /** - * Shows a warning to a player to tell them there is an update available - * - * @return void - */ - public function showPlayerUpdate(Player $player){ - $player->sendMessage(TextFormat::DARK_PURPLE . "The version of " . $this->server->getName() . " that this server is running is out of date. Please consider updating to the latest version."); - $player->sendMessage(TextFormat::DARK_PURPLE . "Check the console for more details."); - } - - /** - * @return void - */ - protected function showChannelSuggestionStable(){ - $this->printConsoleMessage([ - "It appears you're running a Stable build, when you've specified that you prefer to run " . ucfirst($this->getChannel()) . " builds.", - "If you would like to be kept informed about new Stable builds only, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'stable'." - ]); - } - - /** - * @return void - */ - protected function showChannelSuggestionBeta(){ - $this->printConsoleMessage([ - "It appears you're running a Beta build, when you've specified that you prefer to run Stable builds.", - "If you would like to be kept informed about new Beta or Development builds, it is recommended that you change 'preferred-channel' in your pocketmine.yml to 'beta' or 'development'." - ]); - } - - /** - * @param string[] $lines - * - * @return void - */ - protected function printConsoleMessage(array $lines, string $logLevel = \LogLevel::INFO){ - $logger = $this->server->getLogger(); - - $title = $this->server->getName() . ' Auto Updater'; - $logger->log($logLevel, sprintf('----- %s -----', $title)); - foreach($lines as $line){ - $logger->log($logLevel, $line); - } - $logger->log($logLevel, sprintf('----- %s -----', str_repeat('-', strlen($title)))); - } - - /** - * Returns the last retrieved update data. - * - * @return mixed[]|null - * @phpstan-return array|null - */ - public function getUpdateInfo(){ - return $this->updateInfo; - } - - /** - * Schedules an AsyncTask to check for an update. - * - * @return void - */ - public function doCheck(){ - $this->server->getAsyncPool()->submitTask(new UpdateCheckTask($this->endpoint, $this->getChannel())); - } - - /** - * Checks the update information against the current server version to decide if there's an update - * - * @return void - */ - protected function checkUpdate(){ - if($this->updateInfo === null){ - return; - } - $currentVersion = new VersionString(\pocketmine\BASE_VERSION, \pocketmine\IS_DEVELOPMENT_BUILD, \pocketmine\BUILD_NUMBER); - try{ - $newVersion = new VersionString($this->updateInfo["base_version"], $this->updateInfo["is_dev"], $this->updateInfo["build"]); - }catch(\InvalidArgumentException $e){ - //Invalid version returned from API, assume there's no update - $this->server->getLogger()->debug("[AutoUpdater] Assuming no update because \"" . $e->getMessage() . "\""); - return; - } - - if($currentVersion->compare($newVersion) > 0 and ($currentVersion->getFullVersion() !== $newVersion->getFullVersion() or $currentVersion->getBuild() > 0)){ - $this->newVersion = $newVersion; - } - } - - /** - * Returns the channel used for update checking (stable, beta, dev) - */ - public function getChannel() : string{ - $channel = strtolower($this->server->getProperty("auto-updater.preferred-channel", "stable")); - if($channel !== "stable" and $channel !== "beta" and $channel !== "alpha" and $channel !== "development"){ - $channel = "stable"; - } - - return $channel; - } - - /** - * Returns the host used for update checks. - */ - public function getEndpoint() : string{ - return $this->endpoint; - } -} diff --git a/src/pocketmine/utils/Color.php b/src/pocketmine/utils/Color.php deleted file mode 100644 index 1522c20c12..0000000000 --- a/src/pocketmine/utils/Color.php +++ /dev/null @@ -1,185 +0,0 @@ -r = $r & 0xff; - $this->g = $g & 0xff; - $this->b = $b & 0xff; - $this->a = $a & 0xff; - } - - /** - * Returns the alpha (opacity) value of this colour. - */ - public function getA() : int{ - return $this->a; - } - - /** - * Sets the alpha (opacity) value of this colour, lower = more transparent - * - * @return void - */ - public function setA(int $a){ - $this->a = $a & 0xff; - } - - /** - * Retuns the red value of this colour. - */ - public function getR() : int{ - return $this->r; - } - - /** - * Sets the red value of this colour. - * - * @return void - */ - public function setR(int $r){ - $this->r = $r & 0xff; - } - - /** - * Returns the green value of this colour. - */ - public function getG() : int{ - return $this->g; - } - - /** - * Sets the green value of this colour. - * - * @return void - */ - public function setG(int $g){ - $this->g = $g & 0xff; - } - - /** - * Returns the blue value of this colour. - */ - public function getB() : int{ - return $this->b; - } - - /** - * Sets the blue value of this colour. - * - * @return void - */ - public function setB(int $b){ - $this->b = $b & 0xff; - } - - /** - * Mixes the supplied list of colours together to produce a result colour. - * - * @param Color ...$colors - */ - public static function mix(Color ...$colors) : Color{ - $count = count($colors); - if($count < 1){ - throw new \ArgumentCountError("No colors given"); - } - - $a = $r = $g = $b = 0; - - foreach($colors as $color){ - $a += $color->a; - $r += $color->r; - $g += $color->g; - $b += $color->b; - } - - return new Color((int) ($r / $count), (int) ($g / $count), (int) ($b / $count), (int) ($a / $count)); - } - - /** - * Returns a Color from the supplied RGB colour code (24-bit) - * - * @return Color - */ - public static function fromRGB(int $code){ - return new Color(($code >> 16) & 0xff, ($code >> 8) & 0xff, $code & 0xff); - } - - /** - * Returns a Color from the supplied ARGB colour code (32-bit) - * - * @return Color - */ - public static function fromARGB(int $code){ - return new Color(($code >> 16) & 0xff, ($code >> 8) & 0xff, $code & 0xff, ($code >> 24) & 0xff); - } - - /** - * Returns an ARGB 32-bit colour value. - */ - public function toARGB() : int{ - return ($this->a << 24) | ($this->r << 16) | ($this->g << 8) | $this->b; - } - - /** - * Returns a little-endian ARGB 32-bit colour value. - */ - public function toBGRA() : int{ - return ($this->b << 24) | ($this->g << 16) | ($this->r << 8) | $this->a; - } - - /** - * Returns an RGBA 32-bit colour value. - */ - public function toRGBA() : int{ - return ($this->r << 24) | ($this->g << 16) | ($this->b << 8) | $this->a; - } - - /** - * Returns a little-endian RGBA colour value. - */ - public function toABGR() : int{ - return ($this->a << 24) | ($this->b << 16) | ($this->g << 8) | $this->r; - } - - /** - * @return Color - */ - public static function fromABGR(int $code){ - return new Color($code & 0xff, ($code >> 8) & 0xff, ($code >> 16) & 0xff, ($code >> 24) & 0xff); - } -} diff --git a/src/pocketmine/utils/MainLogger.php b/src/pocketmine/utils/MainLogger.php deleted file mode 100644 index 2846e0bfd7..0000000000 --- a/src/pocketmine/utils/MainLogger.php +++ /dev/null @@ -1,385 +0,0 @@ -logFile = $logFile; - $this->logDebug = $logDebug; - $this->logStream = new \Threaded; - - //Child threads may not inherit command line arguments, so if there's an override it needs to be recorded here - $this->mainThreadHasFormattingCodes = Terminal::hasFormattingCodes(); - $this->timezone = Timezone::get(); - - $this->start(PTHREADS_INHERIT_NONE); - } - - public static function getLogger() : MainLogger{ - return static::$logger; - } - - /** - * Returns whether a MainLogger instance is statically registered on this thread. - */ - public static function isRegisteredStatic() : bool{ - return static::$logger !== null; - } - - /** - * Assigns the MainLogger instance to the {@link MainLogger#logger} static property. - * - * WARNING: Because static properties are thread-local, this MUST be called from the body of every Thread if you - * want the logger to be accessible via {@link MainLogger#getLogger}. - * - * @return void - */ - public function registerStatic(){ - if(static::$logger === null){ - static::$logger = $this; - } - } - - /** - * Returns the current logger format used for console output. - */ - public function getFormat() : string{ - return $this->format; - } - - /** - * Sets the logger format to use for outputting text to the console. - * It should be an sprintf()able string accepting 5 string arguments: - * - time - * - color - * - thread name - * - prefix (debug, info etc) - * - message - * - * @see http://php.net/manual/en/function.sprintf.php - */ - public function setFormat(string $format) : void{ - $this->format = $format; - } - - public function emergency($message){ - $this->send($message, \LogLevel::EMERGENCY, "EMERGENCY", TextFormat::RED); - } - - public function alert($message){ - $this->send($message, \LogLevel::ALERT, "ALERT", TextFormat::RED); - } - - public function critical($message){ - $this->send($message, \LogLevel::CRITICAL, "CRITICAL", TextFormat::RED); - } - - public function error($message){ - $this->send($message, \LogLevel::ERROR, "ERROR", TextFormat::DARK_RED); - } - - public function warning($message){ - $this->send($message, \LogLevel::WARNING, "WARNING", TextFormat::YELLOW); - } - - public function notice($message){ - $this->send($message, \LogLevel::NOTICE, "NOTICE", TextFormat::AQUA); - } - - public function info($message){ - $this->send($message, \LogLevel::INFO, "INFO", TextFormat::WHITE); - } - - public function debug($message, bool $force = false){ - if(!$this->logDebug and !$force){ - return; - } - $this->send($message, \LogLevel::DEBUG, "DEBUG", TextFormat::GRAY); - } - - /** - * @return void - */ - public function setLogDebug(bool $logDebug){ - $this->logDebug = $logDebug; - } - - /** - * @param mixed[][]|null $trace - * @phpstan-param list>|null $trace - * - * @return void - */ - public function logException(\Throwable $e, $trace = null){ - if($trace === null){ - $trace = $e->getTrace(); - } - - $this->synchronized(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->syncFlushBuffer(); - } - - private static function printExceptionMessage(\Throwable $e) : string{ - static $errorConversion = [ - 0 => "EXCEPTION", - E_ERROR => "E_ERROR", - E_WARNING => "E_WARNING", - E_PARSE => "E_PARSE", - E_NOTICE => "E_NOTICE", - E_CORE_ERROR => "E_CORE_ERROR", - E_CORE_WARNING => "E_CORE_WARNING", - E_COMPILE_ERROR => "E_COMPILE_ERROR", - E_COMPILE_WARNING => "E_COMPILE_WARNING", - E_USER_ERROR => "E_USER_ERROR", - E_USER_WARNING => "E_USER_WARNING", - E_USER_NOTICE => "E_USER_NOTICE", - E_STRICT => "E_STRICT", - E_RECOVERABLE_ERROR => "E_RECOVERABLE_ERROR", - E_DEPRECATED => "E_DEPRECATED", - E_USER_DEPRECATED => "E_USER_DEPRECATED" - ]; - - $errstr = preg_replace('/\s+/', ' ', trim($e->getMessage())); - - $errno = $e->getCode(); - $errno = $errorConversion[$errno] ?? $errno; - - $errfile = Utils::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: - $this->emergency($message); - break; - case LogLevel::ALERT: - $this->alert($message); - break; - case LogLevel::CRITICAL: - $this->critical($message); - break; - case LogLevel::ERROR: - $this->error($message); - break; - case LogLevel::WARNING: - $this->warning($message); - break; - case LogLevel::NOTICE: - $this->notice($message); - break; - case LogLevel::INFO: - $this->info($message); - break; - case LogLevel::DEBUG: - $this->debug($message); - break; - } - } - - /** - * @return void - */ - public function shutdown(){ - $this->synchronized(function() : void{ - $this->shutdown = true; - $this->notify(); - }); - } - - /** - * @param string $message - * @param string $level - * @param string $prefix - * @param string $color - * - * @return void - */ - protected function send($message, $level, $prefix, $color){ - /** @var \DateTime|null $time */ - static $time = null; - if($time === null){ //thread-local - $time = new \DateTime('now', new \DateTimeZone($this->timezone)); - } - $time->setTimestamp(time()); - - $thread = \Thread::getCurrentThread(); - if($thread === null){ - $threadName = "Server thread"; - }elseif($thread instanceof Thread or $thread instanceof Worker){ - $threadName = $thread->getThreadName() . " thread"; - }else{ - $threadName = (new \ReflectionClass($thread))->getShortName() . " thread"; - } - - $message = sprintf($this->format, $time->format("H:i:s"), $color, $threadName, $prefix, TextFormat::clean($message, false)); - - if(!Terminal::isInit()){ - Terminal::init($this->mainThreadHasFormattingCodes); //lazy-init colour codes because we don't know if they've been registered on this thread - } - - $this->synchronized(function() use ($message, $level, $time) : void{ - Terminal::writeLine($message); - - foreach($this->attachments as $attachment){ - $attachment->call($level, $message); - } - - $this->logStream[] = $time->format("Y-m-d") . " " . TextFormat::clean($message) . PHP_EOL; - $this->notify(); - }); - } - - /** - * @return void - */ - public function syncFlushBuffer(){ - $this->synchronized(function() : void{ - $this->syncFlush = true; - $this->notify(); //write immediately - }); - $this->synchronized(function() : void{ - while($this->syncFlush){ - $this->wait(); //block until it's all been written to disk - } - }); - } - - /** - * @param resource $logResource - */ - private function writeLogStream($logResource) : void{ - while($this->logStream->count() > 0){ - /** @var string $chunk */ - $chunk = $this->logStream->shift(); - fwrite($logResource, $chunk); - } - - $this->synchronized(function() : void{ - if($this->syncFlush){ - $this->syncFlush = false; - $this->notify(); //if this was due to a sync flush, tell the caller to stop waiting - } - }); - } - - /** - * @return void - */ - public function run(){ - $logResource = fopen($this->logFile, "ab"); - if(!is_resource($logResource)){ - throw new \RuntimeException("Couldn't open log file"); - } - - while(!$this->shutdown){ - $this->writeLogStream($logResource); - $this->synchronized(function() : void{ - if(!$this->shutdown && !$this->syncFlush){ - $this->wait(); - } - }); - } - - $this->writeLogStream($logResource); - - fclose($logResource); - } -} diff --git a/src/pocketmine/utils/UUID.php b/src/pocketmine/utils/UUID.php deleted file mode 100644 index b6d725d39c..0000000000 --- a/src/pocketmine/utils/UUID.php +++ /dev/null @@ -1,132 +0,0 @@ -parts = [$part1, $part2, $part3, $part4]; - - $this->version = $version ?? ($this->parts[1] & 0xf000) >> 12; - } - - public function getVersion() : int{ - return $this->version; - } - - public function equals(UUID $uuid) : bool{ - return $uuid->parts === $this->parts; - } - - /** - * Creates an UUID from an hexadecimal representation - */ - public static function fromString(string $uuid, int $version = null) : UUID{ - //TODO: should we be stricter about the notation (8-4-4-4-12)? - $binary = @hex2bin(str_replace("-", "", trim($uuid))); - if($binary === false){ - throw new \InvalidArgumentException("Invalid hex string UUID representation"); - } - return self::fromBinary($binary, $version); - } - - /** - * Creates an UUID from a binary representation - * - * @throws \InvalidArgumentException - */ - public static function fromBinary(string $uuid, int $version = null) : UUID{ - if(strlen($uuid) !== 16){ - throw new \InvalidArgumentException("Must have exactly 16 bytes"); - } - - return new UUID(Binary::readInt(substr($uuid, 0, 4)), Binary::readInt(substr($uuid, 4, 4)), Binary::readInt(substr($uuid, 8, 4)), Binary::readInt(substr($uuid, 12, 4)), $version); - } - - /** - * Creates an UUIDv3 from binary data or list of binary data - * - * @param string ...$data - */ - public static function fromData(string ...$data) : UUID{ - $hash = hash("md5", implode($data), true); - - return self::fromBinary($hash, 3); - } - - public static function fromRandom() : UUID{ - return self::fromData(Binary::writeInt(time()), Binary::writeShort(($pid = getmypid()) !== false ? $pid : 0), Binary::writeShort(($uid = getmyuid()) !== false ? $uid : 0), Binary::writeInt(mt_rand(-0x7fffffff, 0x7fffffff)), Binary::writeInt(mt_rand(-0x7fffffff, 0x7fffffff))); - } - - public function toBinary() : string{ - return Binary::writeInt($this->parts[0]) . Binary::writeInt($this->parts[1]) . Binary::writeInt($this->parts[2]) . Binary::writeInt($this->parts[3]); - } - - public function toString() : string{ - $hex = bin2hex($this->toBinary()); - - //xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx 8-4-4-4-12 - return substr($hex, 0, 8) . "-" . substr($hex, 8, 4) . "-" . substr($hex, 12, 4) . "-" . substr($hex, 16, 4) . "-" . substr($hex, 20, 12); - } - - public function __toString() : string{ - return $this->toString(); - } - - /** - * @return int - * @throws \InvalidArgumentException - */ - public function getPart(int $partNumber){ - if($partNumber < 0 or $partNumber > 3){ - throw new \InvalidArgumentException("Invalid UUID part index $partNumber"); - } - return $this->parts[$partNumber]; - } - - /** - * @return int[] - */ - public function getParts() : array{ - return $this->parts; - } -} diff --git a/src/promise/Promise.php b/src/promise/Promise.php new file mode 100644 index 0000000000..28666917e9 --- /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; + } +} \ No newline at end of file diff --git a/src/promise/PromiseResolver.php b/src/promise/PromiseResolver.php new file mode 100644 index 0000000000..0f447693fc --- /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; + } +} \ No newline at end of file diff --git a/src/promise/PromiseSharedData.php b/src/promise/PromiseSharedData.php new file mode 100644 index 0000000000..bccf56cc29 --- /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/pocketmine/resourcepacks/ResourcePack.php b/src/resourcepacks/ResourcePack.php similarity index 92% rename from src/pocketmine/resourcepacks/ResourcePack.php rename to src/resourcepacks/ResourcePack.php index aee9cf2c2c..a1abfc227e 100644 --- a/src/pocketmine/resourcepacks/ResourcePack.php +++ b/src/resourcepacks/ResourcePack.php @@ -25,11 +25,6 @@ namespace pocketmine\resourcepacks; interface ResourcePack{ - /** - * Returns the path to the resource pack. This might be a file or a directory, depending on the type of pack. - */ - public function getPath() : string; - /** * Returns the human-readable name of the resource pack */ diff --git a/src/pocketmine/resourcepacks/ResourcePackException.php b/src/resourcepacks/ResourcePackException.php similarity index 100% rename from src/pocketmine/resourcepacks/ResourcePackException.php rename to src/resourcepacks/ResourcePackException.php diff --git a/src/pocketmine/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php similarity index 90% rename from src/pocketmine/resourcepacks/ResourcePackManager.php rename to src/resourcepacks/ResourcePackManager.php index d1cd2803c5..657df537d8 100644 --- a/src/pocketmine/resourcepacks/ResourcePackManager.php +++ b/src/resourcepacks/ResourcePackManager.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\resourcepacks; use pocketmine\utils\Config; +use Webmozart\PathUtil\Path; use function array_keys; use function copy; use function count; @@ -65,11 +66,12 @@ class ResourcePackManager{ throw new \InvalidArgumentException("Resource packs path $path exists and is not a directory"); } - if(!file_exists($this->path . "resource_packs.yml")){ - copy(\pocketmine\RESOURCE_PATH . "resource_packs.yml", $this->path . "resource_packs.yml"); + $resourcePacksYml = Path::join($this->path, "resource_packs.yml"); + if(!file_exists($resourcePacksYml)){ + copy(Path::join(\pocketmine\RESOURCE_PATH, "resource_packs.yml"), $resourcePacksYml); } - $resourcePacksConfig = new Config($this->path . "resource_packs.yml", Config::YAML, []); + $resourcePacksConfig = new Config($resourcePacksYml, Config::YAML, []); $this->serverForceResources = (bool) $resourcePacksConfig->get("force_resources", false); @@ -87,7 +89,7 @@ class ResourcePackManager{ } $pack = (string) $pack; try{ - $packPath = $this->path . DIRECTORY_SEPARATOR . $pack; + $packPath = Path::join($this->path, $pack); if(!file_exists($packPath)){ throw new ResourcePackException("File or directory not found"); } @@ -123,7 +125,7 @@ class ResourcePackManager{ * Returns the directory which resource packs are loaded from. */ public function getPath() : string{ - return $this->path; + return $this->path . DIRECTORY_SEPARATOR; } /** @@ -143,10 +145,8 @@ class ResourcePackManager{ /** * Returns the resource pack matching the specified UUID string, or null if the ID was not recognized. - * - * @return ResourcePack|null */ - public function getPackById(string $id){ + public function getPackById(string $id) : ?ResourcePack{ return $this->uuidList[strtolower($id)] ?? null; } diff --git a/src/pocketmine/resourcepacks/ZippedResourcePack.php b/src/resourcepacks/ZippedResourcePack.php similarity index 83% rename from src/pocketmine/resourcepacks/ZippedResourcePack.php rename to src/resourcepacks/ZippedResourcePack.php index 89b9a97a77..9f2593c845 100644 --- a/src/pocketmine/resourcepacks/ZippedResourcePack.php +++ b/src/resourcepacks/ZippedResourcePack.php @@ -24,8 +24,8 @@ declare(strict_types=1); namespace pocketmine\resourcepacks; use Ahc\Json\Comment as CommentedJsonDecoder; +use pocketmine\resourcepacks\json\Manifest; use function assert; -use function count; use function fclose; use function feof; use function file_exists; @@ -41,28 +41,10 @@ use function strlen; class ZippedResourcePack implements ResourcePack{ - /** - * Performs basic validation checks on a resource pack's manifest.json. - * TODO: add more manifest validation - */ - public static function verifyManifest(\stdClass $manifest) : bool{ - if(!isset($manifest->format_version) or !isset($manifest->header) or !isset($manifest->modules)){ - return false; - } - - //Right now we don't care about anything else, only the stuff we're sending to clients. - return - isset($manifest->header->description) and - isset($manifest->header->name) and - isset($manifest->header->uuid) and - isset($manifest->header->version) and - count($manifest->header->version) === 3; - } - /** @var string */ protected $path; - /** @var \stdClass */ + /** @var Manifest */ protected $manifest; /** @var string|null */ @@ -121,8 +103,16 @@ class ZippedResourcePack implements ResourcePack{ if(!($manifest instanceof \stdClass)){ throw new ResourcePackException("manifest.json should contain a JSON object, not " . gettype($manifest)); } - if(!self::verifyManifest($manifest)){ - throw new ResourcePackException("manifest.json is missing required fields"); + + $mapper = new \JsonMapper(); + $mapper->bExceptionOnUndefinedProperty = true; + $mapper->bExceptionOnMissingData = true; + + try{ + /** @var Manifest $manifest */ + $manifest = $mapper->map($manifest, new Manifest()); + }catch(\JsonMapper_Exception $e){ + throw new ResourcePackException("Invalid manifest.json contents: " . $e->getMessage(), 0, $e); } $this->manifest = $manifest; diff --git a/src/resourcepacks/json/Manifest.php b/src/resourcepacks/json/Manifest.php new file mode 100644 index 0000000000..cd6f1c9139 --- /dev/null +++ b/src/resourcepacks/json/Manifest.php @@ -0,0 +1,49 @@ + + * @var int[] + * @phpstan-var array{int, int, int} + * @required */ - public $enumValues = []; - + public array $version; } diff --git a/src/pocketmine/permission/ServerOperator.php b/src/resourcepacks/json/ManifestHeader.php similarity index 67% rename from src/pocketmine/permission/ServerOperator.php rename to src/resourcepacks/json/ManifestHeader.php index ca6c9e8a0d..60568a4db9 100644 --- a/src/pocketmine/permission/ServerOperator.php +++ b/src/resourcepacks/json/ManifestHeader.php @@ -21,18 +21,28 @@ declare(strict_types=1); -namespace pocketmine\permission; +namespace pocketmine\resourcepacks\json; -interface ServerOperator{ - /** - * Checks if the current object has operator permissions - */ - public function isOp() : bool; +final class ManifestHeader{ + + public string $description; + + /** @required */ + public string $name; + + /** @required */ + public string $uuid; /** - * Sets the operator permission for the current object - * - * @return void + * @var int[] + * @phpstan-var array{int, int, int} + * @required */ - public function setOp(bool $value); + public array $version; + + /** + * @var int[] + * @phpstan-var array{int, int, int} + */ + public array $min_engine_version; } diff --git a/src/resourcepacks/json/ManifestMetadata.php b/src/resourcepacks/json/ManifestMetadata.php new file mode 100644 index 0000000000..d44c42167c --- /dev/null +++ b/src/resourcepacks/json/ManifestMetadata.php @@ -0,0 +1,33 @@ + + * @var \SplQueue[]|AsyncTask[][] + * @phpstan-var array> */ - private $tasks = []; - /** - * @var int[] - * @phpstan-var array - */ - private $taskWorkers = []; - /** @var int */ - private $nextTaskId = 1; + private $taskQueues = []; /** * @var AsyncWorker[] * @phpstan-var array */ private $workers = []; - /** - * @var int[] - * @phpstan-var array - */ - private $workerUsage = []; /** * @var int[] * @phpstan-var array @@ -88,12 +74,15 @@ class AsyncPool{ */ private $workerStartHooks = []; - public function __construct(Server $server, int $size, int $workerMemoryLimit, \ClassLoader $classLoader, \ThreadedLogger $logger){ - $this->server = $server; + /** @var SleeperHandler */ + private $eventLoop; + + public function __construct(int $size, int $workerMemoryLimit, \ClassLoader $classLoader, \ThreadedLogger $logger, SleeperHandler $eventLoop){ $this->size = $size; $this->workerMemoryLimit = $workerMemoryLimit; $this->classLoader = $classLoader; $this->logger = $logger; + $this->eventLoop = $eventLoop; } /** @@ -122,7 +111,7 @@ class AsyncPool{ */ public function addWorkerStartHook(\Closure $hook) : void{ Utils::validateCallableSignature(function(int $worker) : void{}, $hook); - $this->workerStartHooks[spl_object_hash($hook)] = $hook; + $this->workerStartHooks[spl_object_id($hook)] = $hook; foreach($this->workers as $i => $worker){ $hook($i); } @@ -134,7 +123,7 @@ class AsyncPool{ * @phpstan-param \Closure(int $workerId) : void $hook */ public function removeWorkerStartHook(\Closure $hook) : void{ - unset($this->workerStartHooks[spl_object_hash($hook)]); + unset($this->workerStartHooks[spl_object_id($hook)]); } /** @@ -152,11 +141,16 @@ class AsyncPool{ */ private function getWorker(int $worker) : AsyncWorker{ if(!isset($this->workers[$worker])){ - $this->workerUsage[$worker] = 0; - $this->workers[$worker] = new AsyncWorker($this->logger, $worker, $this->workerMemoryLimit); - $this->workers[$worker]->setClassLoader($this->classLoader); + $notifier = new SleeperNotifier(); + $this->workers[$worker] = new AsyncWorker($this->logger, $worker, $this->workerMemoryLimit, $notifier); + $this->eventLoop->addNotifier($notifier, function() use ($worker) : void{ + $this->collectTasksFromWorker($worker); + }); + $this->workers[$worker]->setClassLoaders([$this->classLoader]); $this->workers[$worker]->start(self::WORKER_START_OPTIONS); + $this->taskQueues[$worker] = new \SplQueue(); + foreach($this->workerStartHooks as $hook){ $hook($worker); } @@ -172,19 +166,15 @@ class AsyncPool{ if($worker < 0 or $worker >= $this->size){ throw new \InvalidArgumentException("Invalid worker $worker"); } - if($task->getTaskId() !== null){ + if($task->isSubmitted()){ throw new \InvalidArgumentException("Cannot submit the same AsyncTask instance more than once"); } $task->progressUpdates = new \Threaded; - $taskId = $this->nextTaskId++; - $task->setTaskId($taskId); - - $this->tasks[$taskId] = $task; + $task->setSubmitted(); $this->getWorker($worker)->stack($task); - $this->workerUsage[$worker]++; - $this->taskWorkers[$taskId] = $worker; + $this->taskQueues[$worker]->enqueue($task); $this->workerLastUsed[$worker] = time(); } @@ -198,8 +188,8 @@ class AsyncPool{ public function selectWorker() : int{ $worker = null; $minUsage = PHP_INT_MAX; - foreach($this->workerUsage as $i => $usage){ - if($usage < $minUsage){ + foreach($this->taskQueues as $i => $queue){ + if(($usage = $queue->count()) < $minUsage){ $worker = $i; $minUsage = $usage; if($usage === 0){ @@ -226,7 +216,7 @@ class AsyncPool{ * worker may be started. */ public function submitTask(AsyncTask $task) : int{ - if($task->getTaskId() !== null){ + if($task->isSubmitted()){ throw new \InvalidArgumentException("Cannot submit the same AsyncTask instance more than once"); } @@ -235,77 +225,43 @@ class AsyncPool{ return $worker; } - /** - * Removes a completed or crashed task from the pool. - */ - private function removeTask(AsyncTask $task, bool $force = false) : void{ - if(isset($this->taskWorkers[$task->getTaskId()])){ - if(!$force and ($task->isRunning() or !$task->isGarbage())){ - return; - } - $this->workerUsage[$this->taskWorkers[$task->getTaskId()]]--; - } - - $task->removeDanglingStoredObjects(); - unset($this->tasks[$task->getTaskId()]); - unset($this->taskWorkers[$task->getTaskId()]); - } - - /** - * Removes all tasks from the pool, cancelling where possible. This will block until all tasks have been - * successfully deleted. - */ - public function removeTasks() : void{ - foreach($this->workers as $worker){ - /** @var AsyncTask $task */ - while(($task = $worker->unstack()) !== null){ - //cancelRun() is not strictly necessary here, but it might be used to inform plugins of the task state - //(i.e. it never executed). - assert($task instanceof AsyncTask); - $task->cancelRun(); - $this->removeTask($task, true); - } - } - do{ - foreach($this->tasks as $task){ - $task->cancelRun(); - $this->removeTask($task); - } - - if(count($this->tasks) > 0){ - Server::microSleep(25000); - } - }while(count($this->tasks) > 0); - - for($i = 0; $i < $this->size; ++$i){ - $this->workerUsage[$i] = 0; - } - - $this->taskWorkers = []; - $this->tasks = []; - - $this->collectWorkers(); - } - - /** - * Collects garbage from running workers. - */ - private function collectWorkers() : void{ - foreach($this->workers as $worker){ - $worker->collect(); - } - } - /** * Collects finished and/or crashed tasks from the workers, firing their on-completion hooks where appropriate. * * @throws \ReflectionException + * @return bool whether there are tasks left to be collected */ - public function collectTasks() : void{ - foreach($this->tasks as $task){ - $task->checkProgressUpdates($this->server); - if($task->isGarbage() and !$task->isRunning() and !$task->isCrashed()){ - if(!$task->hasCancelledRun()){ + public function collectTasks() : bool{ + foreach($this->taskQueues as $worker => $queue){ + $this->collectTasksFromWorker($worker); + } + + //we check this in a second loop, because task collection could have caused new tasks to be added to the queues + foreach($this->taskQueues as $queue){ + if(!$queue->isEmpty()){ + return true; + } + } + return false; + } + + public function collectTasksFromWorker(int $worker) : bool{ + if(!isset($this->taskQueues[$worker])){ + throw new \InvalidArgumentException("No such worker $worker"); + } + $queue = $this->taskQueues[$worker]; + $more = false; + while(!$queue->isEmpty()){ + /** @var AsyncTask $task */ + $task = $queue->bottom(); + $task->checkProgressUpdates(); + if($task->isFinished()){ //make sure the task actually executed before trying to collect + $queue->dequeue(); + + if($task->isCrashed()){ + $this->logger->critical("Could not execute asynchronous task " . (new \ReflectionClass($task))->getShortName() . ": Task crashed"); + $task->onError(); + }elseif(!$task->hasCancelledRun()){ /* * It's possible for a task to submit a progress update and then finish before the progress * update is detected by the parent thread, so here we consume any missed updates. @@ -315,18 +271,16 @@ class AsyncPool{ * lost. Thus, it's necessary to do one last check here to make sure all progress updates have * been consumed before completing. */ - $task->checkProgressUpdates($this->server); - $task->onCompletion($this->server); + $task->checkProgressUpdates(); + $task->onCompletion(); } - - $this->removeTask($task); - }elseif($task->isCrashed()){ - $this->logger->critical("Could not execute asynchronous task " . (new \ReflectionClass($task))->getShortName() . ": Task crashed"); - $this->removeTask($task, true); + }else{ + $more = true; + break; //current task is still running, skip to next worker } } - - $this->collectWorkers(); + $this->workers[$worker]->collect(); + return $more; } /** @@ -336,16 +290,17 @@ class AsyncPool{ * @phpstan-return array */ public function getTaskQueueSizes() : array{ - return $this->workerUsage; + return array_map(function(\SplQueue $queue) : int{ return $queue->count(); }, $this->taskQueues); } public function shutdownUnusedWorkers() : int{ $ret = 0; $time = time(); - foreach($this->workerUsage as $i => $usage){ - if($usage === 0 and (!isset($this->workerLastUsed[$i]) or $this->workerLastUsed[$i] + 300 < $time)){ + foreach($this->taskQueues as $i => $queue){ + if((!isset($this->workerLastUsed[$i]) or $this->workerLastUsed[$i] + 300 < $time) and $queue->isEmpty()){ $this->workers[$i]->quit(); - unset($this->workers[$i], $this->workerUsage[$i], $this->workerLastUsed[$i]); + $this->eventLoop->removeNotifier($this->workers[$i]->getNotifier()); + unset($this->workers[$i], $this->taskQueues[$i], $this->workerLastUsed[$i]); $ret++; } } @@ -357,12 +312,16 @@ class AsyncPool{ * Cancels all pending tasks and shuts down all the workers in the pool. */ public function shutdown() : void{ - $this->collectTasks(); - $this->removeTasks(); + while($this->collectTasks()){ + //NOOP + } + foreach($this->workers as $worker){ $worker->quit(); + $this->eventLoop->removeNotifier($worker->getNotifier()); } $this->workers = []; + $this->taskQueues = []; $this->workerLastUsed = []; } } diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php new file mode 100644 index 0000000000..2f1bedc6b5 --- /dev/null +++ b/src/scheduler/AsyncTask.php @@ -0,0 +1,262 @@ + mixed data + * @phpstan-var \ArrayObject>|null + * + * Used to store objects which are only needed on one thread and should not be serialized. + */ + private static $threadLocalStorage = null; + + /** @var AsyncWorker|null $worker */ + public $worker = null; + + /** @var \Threaded */ + public $progressUpdates; + + /** @var scalar|null */ + private $result = null; + /** @var bool */ + private $serialized = false; + /** @var bool */ + private $cancelRun = false; + /** @var bool */ + private $submitted = false; + + /** @var bool */ + private $crashed = false; + /** @var bool */ + private $finished = false; + + public function run() : void{ + $this->result = null; + + if(!$this->cancelRun){ + try{ + $this->onRun(); + }catch(\Throwable $e){ + $this->crashed = true; + $this->worker->handleException($e); + } + } + + $this->finished = true; + $this->worker->getNotifier()->wakeupSleeper(); + } + + public function isCrashed() : bool{ + return $this->crashed or $this->isTerminated(); + } + + /** + * Returns whether this task has finished executing, whether successfully or not. This differs from isRunning() + * because it is not true prior to task execution. + */ + public function isFinished() : bool{ + return $this->finished or $this->isCrashed(); + } + + public function hasResult() : bool{ + return $this->result !== null; + } + + /** + * @return mixed + */ + public function getResult(){ + if($this->serialized){ + if(!is_string($this->result)) throw new AssumptionFailedError("Result expected to be a serialized string"); + return igbinary_unserialize($this->result); + } + return $this->result; + } + + /** + * @param mixed $result + */ + public function setResult($result) : void{ + $this->result = ($this->serialized = !is_scalar($result)) ? igbinary_serialize($result) : $result; + } + + public function cancelRun() : void{ + $this->cancelRun = true; + } + + public function hasCancelledRun() : bool{ + return $this->cancelRun; + } + + public function setSubmitted() : void{ + $this->submitted = true; + } + + public function isSubmitted() : bool{ + return $this->submitted; + } + + /** + * Actions to execute when run + */ + abstract public function onRun() : void; + + /** + * Actions to execute when completed (on main thread) + * Implement this if you want to handle the data in your AsyncTask after it has been processed + */ + public function onCompletion() : void{ + + } + + /** + * Call this method from {@link AsyncTask::onRun} (AsyncTask execution thread) to schedule a call to + * {@link AsyncTask::onProgressUpdate} from the main thread with the given progress parameter. + * + * @param mixed $progress A value that can be safely serialize()'ed. + */ + public function publishProgress($progress) : void{ + $this->progressUpdates[] = igbinary_serialize($progress); + } + + /** + * @internal Only call from AsyncPool.php on the main thread + */ + public function checkProgressUpdates() : void{ + while($this->progressUpdates->count() !== 0){ + $progress = $this->progressUpdates->shift(); + $this->onProgressUpdate(igbinary_unserialize($progress)); + } + } + + /** + * Called from the main thread after {@link AsyncTask::publishProgress} is called. + * All {@link AsyncTask::publishProgress} calls should result in {@link AsyncTask::onProgressUpdate} calls before + * {@link AsyncTask::onCompletion} is called. + * + * @param mixed $progress The parameter passed to {@link AsyncTask#publishProgress}. It is serialize()'ed + * and then unserialize()'ed, as if it has been cloned. + */ + public function onProgressUpdate($progress) : void{ + + } + + /** + * Called from the main thread when the async task experiences an error during onRun(). Use this for things like + * promise rejection. + */ + public function onError() : void{ + + } + + /** + * Saves mixed data in thread-local storage. Data stored using this storage is **only accessible from the thread it + * was stored on**. Data stored using this method will **not** be serialized. + * This can be used to store references to variables which you need later on on the same thread, but not others. + * + * For example, plugin references could be stored in the constructor of the async task (which is called on the main + * thread) using this, and then fetched in onCompletion() (which is also called on the main thread), without them + * becoming serialized. + * + * Scalar types can be stored directly in class properties instead of using this storage. + * + * Objects stored in this storage can be retrieved using fetchLocal() on the same thread that this method was called + * from. + * + * @param mixed $complexData the data to store + */ + protected function storeLocal(string $key, $complexData) : void{ + if(self::$threadLocalStorage === null){ + /* + * It's necessary to use an object (not array) here because pthreads is stupid. Non-default array statics + * will be inherited when task classes are copied to the worker thread, which would cause unwanted + * inheritance of primitive thread-locals, which we really don't want for various reasons. + * It won't try to inherit objects though, so this is the easiest solution. + */ + self::$threadLocalStorage = new \ArrayObject(); + } + self::$threadLocalStorage[spl_object_id($this)][$key] = $complexData; + } + + /** + * Retrieves data stored in thread-local storage. + * + * If you used storeLocal(), you can use this on the same thread to fetch data stored. This should be used during + * onProgressUpdate() and onCompletion() to fetch thread-local data stored on the parent thread. + * + * @return mixed + * + * @throws \InvalidArgumentException if no data were stored by this AsyncTask instance. + */ + protected function fetchLocal(string $key){ + $id = spl_object_id($this); + if(self::$threadLocalStorage === null or !isset(self::$threadLocalStorage[$id][$key])){ + throw new \InvalidArgumentException("No matching thread-local data found on this thread"); + } + + return self::$threadLocalStorage[$id][$key]; + } + + final public function __destruct(){ + $this->reallyDestruct(); + if(self::$threadLocalStorage !== null and isset(self::$threadLocalStorage[$h = spl_object_id($this)])){ + unset(self::$threadLocalStorage[$h]); + if(self::$threadLocalStorage->count() === 0){ + self::$threadLocalStorage = null; + } + } + } + + /** + * Override this to do normal __destruct() cleanup from a child class. + */ + protected function reallyDestruct() : void{ + + } +} diff --git a/src/pocketmine/scheduler/AsyncWorker.php b/src/scheduler/AsyncWorker.php similarity index 75% rename from src/pocketmine/scheduler/AsyncWorker.php rename to src/scheduler/AsyncWorker.php index 16931ee04b..3dc8c2e688 100644 --- a/src/pocketmine/scheduler/AsyncWorker.php +++ b/src/scheduler/AsyncWorker.php @@ -23,13 +23,10 @@ declare(strict_types=1); namespace pocketmine\scheduler; -use pocketmine\utils\MainLogger; -use pocketmine\utils\Utils; -use pocketmine\Worker; -use function error_reporting; +use pocketmine\snooze\SleeperNotifier; +use pocketmine\thread\Worker; use function gc_enable; use function ini_set; -use function set_error_handler; class AsyncWorker extends Worker{ /** @var mixed[] */ @@ -43,26 +40,22 @@ class AsyncWorker extends Worker{ /** @var int */ private $memoryLimit; - public function __construct(\ThreadedLogger $logger, int $id, int $memoryLimit){ + /** @var SleeperNotifier */ + private $notifier; + + public function __construct(\ThreadedLogger $logger, int $id, int $memoryLimit, SleeperNotifier $notifier){ $this->logger = $logger; $this->id = $id; $this->memoryLimit = $memoryLimit; + $this->notifier = $notifier; } - /** - * @return void - */ - public function run(){ - error_reporting(-1); + public function getNotifier() : SleeperNotifier{ + return $this->notifier; + } - $this->registerClassLoader(); - - //set this after the autoloader is registered - set_error_handler([Utils::class, 'errorExceptionHandler']); - - if($this->logger instanceof MainLogger){ - $this->logger->registerStatic(); - } + protected function onRun() : void{ + \GlobalLogger::set($this->logger); gc_enable(); @@ -79,15 +72,12 @@ class AsyncWorker extends Worker{ return $this->logger; } - /** - * @return void - */ - public function handleException(\Throwable $e){ + public function handleException(\Throwable $e) : void{ $this->logger->logException($e); } public function getThreadName() : string{ - return "Asynchronous Worker #" . $this->id; + return "AsyncWorker#" . $this->id; } public function getAsyncWorkerId() : int{ @@ -101,6 +91,9 @@ class AsyncWorker extends Worker{ * @param mixed $value */ public function saveToThreadStore(string $identifier, $value) : void{ + if(\Thread::getCurrentThread() !== $this){ + throw new \LogicException("Thread-local data can only be stored in the thread context"); + } self::$store[$identifier] = $value; } @@ -115,6 +108,9 @@ class AsyncWorker extends Worker{ * @return mixed */ public function getFromThreadStore(string $identifier){ + if(\Thread::getCurrentThread() !== $this){ + throw new \LogicException("Thread-local data can only be fetched in the thread context"); + } return self::$store[$identifier] ?? null; } @@ -122,6 +118,9 @@ class AsyncWorker extends Worker{ * Removes previously-stored mixed data from the worker's thread-local object store. */ public function removeFromThreadStore(string $identifier) : void{ + if(\Thread::getCurrentThread() !== $this){ + throw new \LogicException("Thread-local data can only be removed in the thread context"); + } unset(self::$store[$identifier]); } } diff --git a/src/pocketmine/scheduler/BulkCurlTask.php b/src/scheduler/BulkCurlTask.php similarity index 55% rename from src/pocketmine/scheduler/BulkCurlTask.php rename to src/scheduler/BulkCurlTask.php index 0b1fb12e93..cd35471754 100644 --- a/src/pocketmine/scheduler/BulkCurlTask.php +++ b/src/scheduler/BulkCurlTask.php @@ -25,8 +25,9 @@ namespace pocketmine\scheduler; use pocketmine\utils\Internet; use pocketmine\utils\InternetException; -use function serialize; -use function unserialize; +use pocketmine\utils\InternetRequestResult; +use function igbinary_serialize; +use function igbinary_unserialize; /** * Executes a consecutive list of cURL operations. @@ -34,6 +35,8 @@ use function unserialize; * The result of this AsyncTask is an array of arrays (returned from {@link Internet::simpleCurl}) or InternetException objects. */ class BulkCurlTask extends AsyncTask{ + private const TLS_KEY_COMPLETION_CALLBACK = "completionCallback"; + /** @var string */ private $operations; @@ -42,28 +45,41 @@ class BulkCurlTask extends AsyncTask{ * * $operations accepts an array of arrays. Each member array must contain a string mapped to "page", and optionally, * "timeout", "extraHeaders" and "extraOpts". Documentation of these options are same as those in - * {@link Utils::simpleCurl}. + * {@link Internet::simpleCurl}. * - * @param mixed[][] $operations - * @param mixed|null $complexData - * @phpstan-param list, extraOpts?: array}> $operations + * @param BulkCurlTaskOperation[] $operations + * @phpstan-param \Closure(list $results) : void $onCompletion */ - public function __construct(array $operations, $complexData = null){ - $this->storeLocal($complexData); - $this->operations = serialize($operations); + public function __construct(array $operations, \Closure $onCompletion){ + $this->operations = igbinary_serialize($operations); + $this->storeLocal(self::TLS_KEY_COMPLETION_CALLBACK, $onCompletion); } - public function onRun(){ - /** @phpstan-var list, extraOpts?: array}> $operations */ - $operations = unserialize($this->operations); + public function onRun() : void{ + /** + * @var BulkCurlTaskOperation[] $operations + * @phpstan-var list $operations + */ + $operations = igbinary_unserialize($this->operations); $results = []; foreach($operations as $op){ try{ - $results[] = Internet::simpleCurl($op["page"], $op["timeout"] ?? 10, $op["extraHeaders"] ?? [], $op["extraOpts"] ?? []); + $results[] = Internet::simpleCurl($op->getPage(), $op->getTimeout(), $op->getExtraHeaders(), $op->getExtraOpts()); }catch(InternetException $e){ $results[] = $e; } } $this->setResult($results); } + + public function onCompletion() : void{ + /** + * @var \Closure + * @phpstan-var \Closure(list) : void + */ + $callback = $this->fetchLocal(self::TLS_KEY_COMPLETION_CALLBACK); + /** @var InternetRequestResult[]|InternetException[] $results */ + $results = $this->getResult(); + $callback($results); + } } diff --git a/src/scheduler/BulkCurlTaskOperation.php b/src/scheduler/BulkCurlTaskOperation.php new file mode 100644 index 0000000000..9050d90ad1 --- /dev/null +++ b/src/scheduler/BulkCurlTaskOperation.php @@ -0,0 +1,71 @@ + + */ + private $extraHeaders; + /** + * @var mixed[] + * @phpstan-var array + */ + private $extraOpts; + + /** + * @param string[] $extraHeaders + * @param mixed[] $extraOpts + * @phpstan-param list $extraHeaders + * @phpstan-param array $extraOpts + */ + public function __construct(string $page, float $timeout = 10, array $extraHeaders = [], array $extraOpts = []){ + $this->page = $page; + $this->timeout = $timeout; + $this->extraHeaders = $extraHeaders; + $this->extraOpts = $extraOpts; + } + + public function getPage() : string{ return $this->page; } + + public function getTimeout() : float{ return $this->timeout; } + + /** + * @return string[] + * @phpstan-return list + */ + public function getExtraHeaders() : array{ return $this->extraHeaders; } + + /** + * @return mixed[] + * @phpstan-return array + */ + public function getExtraOpts() : array{ return $this->extraOpts; } +} diff --git a/src/scheduler/CancelTaskException.php b/src/scheduler/CancelTaskException.php new file mode 100644 index 0000000000..a500c4d78f --- /dev/null +++ b/src/scheduler/CancelTaskException.php @@ -0,0 +1,32 @@ +scheduleTask(new ClosureTask(function(int $currentTick) : void{ - * echo "HI on $currentTick\n"; + * TaskScheduler->scheduleTask(new ClosureTask(function() : void{ + * echo "HI\n"; * }); * ``` */ @@ -40,16 +42,16 @@ class ClosureTask extends Task{ /** * @var \Closure - * @phpstan-var \Closure(int) : void + * @phpstan-var \Closure() : void */ private $closure; /** - * @param \Closure $closure Must accept only ONE parameter, $currentTick - * @phpstan-param \Closure(int) : void $closure + * @param \Closure $closure Must accept zero parameters + * @phpstan-param \Closure() : void $closure */ public function __construct(\Closure $closure){ - Utils::validateCallableSignature(function(int $currentTick) : void{}, $closure); + Utils::validateCallableSignature(new CallbackType(new ReturnType()), $closure); $this->closure = $closure; } @@ -57,7 +59,7 @@ class ClosureTask extends Task{ return Utils::getNiceClosureName($this->closure); } - public function onRun(int $currentTick){ - ($this->closure)($currentTick); + public function onRun() : void{ + ($this->closure)(); } } diff --git a/src/pocketmine/scheduler/DumpWorkerMemoryTask.php b/src/scheduler/DumpWorkerMemoryTask.php similarity index 86% rename from src/pocketmine/scheduler/DumpWorkerMemoryTask.php rename to src/scheduler/DumpWorkerMemoryTask.php index e45d421d2c..01ff114632 100644 --- a/src/pocketmine/scheduler/DumpWorkerMemoryTask.php +++ b/src/scheduler/DumpWorkerMemoryTask.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace pocketmine\scheduler; use pocketmine\MemoryManager; -use const DIRECTORY_SEPARATOR; +use Webmozart\PathUtil\Path; /** * Task used to dump memory from AsyncWorkers @@ -43,13 +43,13 @@ class DumpWorkerMemoryTask extends AsyncTask{ $this->maxStringSize = $maxStringSize; } - public function onRun(){ + public function onRun() : void{ MemoryManager::dumpMemory( $this->worker, - $this->outputFolder . DIRECTORY_SEPARATOR . "AsyncWorker#" . $this->worker->getAsyncWorkerId(), + Path::join($this->outputFolder, "AsyncWorker#" . $this->worker->getAsyncWorkerId()), $this->maxNesting, $this->maxStringSize, - $this->worker->getLogger() + new \PrefixedLogger($this->worker->getLogger(), "Memory Dump") ); } } diff --git a/src/pocketmine/scheduler/GarbageCollectionTask.php b/src/scheduler/GarbageCollectionTask.php similarity index 96% rename from src/pocketmine/scheduler/GarbageCollectionTask.php rename to src/scheduler/GarbageCollectionTask.php index 093843d4b1..97ea28a939 100644 --- a/src/pocketmine/scheduler/GarbageCollectionTask.php +++ b/src/scheduler/GarbageCollectionTask.php @@ -29,7 +29,7 @@ use function gc_mem_caches; class GarbageCollectionTask extends AsyncTask{ - public function onRun(){ + public function onRun() : void{ gc_enable(); gc_collect_cycles(); gc_mem_caches(); diff --git a/src/pocketmine/scheduler/Task.php b/src/scheduler/Task.php similarity index 78% rename from src/pocketmine/scheduler/Task.php rename to src/scheduler/Task.php index 3cdbe01158..0c4ea44696 100644 --- a/src/pocketmine/scheduler/Task.php +++ b/src/scheduler/Task.php @@ -37,22 +37,11 @@ abstract class Task{ return $this->taskHandler; } - final public function getTaskId() : int{ - if($this->taskHandler !== null){ - return $this->taskHandler->getTaskId(); - } - - return -1; - } - public function getName() : string{ return Utils::getNiceClassName($this); } - /** - * @return void - */ - final public function setHandler(TaskHandler $taskHandler = null){ + final public function setHandler(?TaskHandler $taskHandler) : void{ if($this->taskHandler === null or $taskHandler === null){ $this->taskHandler = $taskHandler; } @@ -61,16 +50,14 @@ abstract class Task{ /** * Actions to execute when run * - * @return void + * @throws CancelTaskException */ - abstract public function onRun(int $currentTick); + abstract public function onRun() : void; /** * Actions to execute if the Task is cancelled - * - * @return void */ - public function onCancel(){ + public function onCancel() : void{ } } diff --git a/src/pocketmine/scheduler/TaskHandler.php b/src/scheduler/TaskHandler.php similarity index 82% rename from src/pocketmine/scheduler/TaskHandler.php rename to src/scheduler/TaskHandler.php index 04b5f1a425..e95ee1fcb2 100644 --- a/src/pocketmine/scheduler/TaskHandler.php +++ b/src/scheduler/TaskHandler.php @@ -31,9 +31,6 @@ class TaskHandler{ /** @var Task */ protected $task; - /** @var int */ - protected $taskId; - /** @var int */ protected $delay; @@ -54,12 +51,11 @@ class TaskHandler{ /** @var string */ private $ownerName; - public function __construct(Task $task, int $taskId, int $delay = -1, int $period = -1, ?string $ownerName = null){ + public function __construct(Task $task, int $delay = -1, int $period = -1, ?string $ownerName = null){ if($task->getHandler() !== null){ throw new \InvalidArgumentException("Cannot assign multiple handlers to the same task"); } $this->task = $task; - $this->taskId = $taskId; $this->delay = $delay; $this->period = $period; $this->taskName = $task->getName(); @@ -76,17 +72,10 @@ class TaskHandler{ return $this->nextRun; } - /** - * @return void - */ - public function setNextRun(int $ticks){ + public function setNextRun(int $ticks) : void{ $this->nextRun = $ticks; } - public function getTaskId() : int{ - return $this->taskId; - } - public function getTask() : Task{ return $this->task; } @@ -107,10 +96,7 @@ class TaskHandler{ return $this->period; } - /** - * @return void - */ - public function cancel(){ + public function cancel() : void{ try{ if(!$this->isCancelled()){ $this->task->onCancel(); @@ -120,21 +106,17 @@ class TaskHandler{ } } - /** - * @return void - */ - public function remove(){ + public function remove() : void{ $this->cancelled = true; $this->task->setHandler(null); } - /** - * @return void - */ - public function run(int $currentTick){ + public function run() : void{ $this->timings->startTiming(); try{ - $this->task->onRun($currentTick); + $this->task->onRun(); + }catch(CancelTaskException $e){ + $this->cancel(); }finally{ $this->timings->stopTiming(); } diff --git a/src/pocketmine/scheduler/TaskScheduler.php b/src/scheduler/TaskScheduler.php similarity index 63% rename from src/pocketmine/scheduler/TaskScheduler.php rename to src/scheduler/TaskScheduler.php index c6eb4dd069..a1ed6a261b 100644 --- a/src/pocketmine/scheduler/TaskScheduler.php +++ b/src/scheduler/TaskScheduler.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace pocketmine\scheduler; +use pocketmine\utils\ObjectSet; use pocketmine\utils\ReversePriorityQueue; class TaskScheduler{ @@ -42,90 +43,54 @@ class TaskScheduler{ */ protected $queue; - /** @var TaskHandler[] */ - protected $tasks = []; - - /** @var int */ - private $ids = 1; + /** + * @var ObjectSet|TaskHandler[] + * @phpstan-var ObjectSet + */ + protected $tasks; /** @var int */ protected $currentTick = 0; - /** - * @param \Logger $logger @deprecated - */ - public function __construct(\Logger $logger, ?string $owner = null){ + public function __construct(?string $owner = null){ $this->owner = $owner; $this->queue = new ReversePriorityQueue(); + $this->tasks = new ObjectSet(); } - /** - * @return TaskHandler - */ - public function scheduleTask(Task $task){ + public function scheduleTask(Task $task) : TaskHandler{ return $this->addTask($task, -1, -1); } - /** - * @return TaskHandler - */ - public function scheduleDelayedTask(Task $task, int $delay){ + public function scheduleDelayedTask(Task $task, int $delay) : TaskHandler{ return $this->addTask($task, $delay, -1); } - /** - * @return TaskHandler - */ - public function scheduleRepeatingTask(Task $task, int $period){ + public function scheduleRepeatingTask(Task $task, int $period) : TaskHandler{ return $this->addTask($task, -1, $period); } - /** - * @return TaskHandler - */ - public function scheduleDelayedRepeatingTask(Task $task, int $delay, int $period){ + public function scheduleDelayedRepeatingTask(Task $task, int $delay, int $period) : TaskHandler{ return $this->addTask($task, $delay, $period); } - /** - * @return void - */ - public function cancelTask(int $taskId){ - if(isset($this->tasks[$taskId])){ - try{ - $this->tasks[$taskId]->cancel(); - }finally{ - unset($this->tasks[$taskId]); - } - } - } - - /** - * @return void - */ - public function cancelAllTasks(){ + public function cancelAllTasks() : void{ foreach($this->tasks as $id => $task){ - $this->cancelTask($id); + $task->cancel(); } - $this->tasks = []; + $this->tasks->clear(); while(!$this->queue->isEmpty()){ $this->queue->extract(); } - $this->ids = 1; } - public function isQueued(int $taskId) : bool{ - return isset($this->tasks[$taskId]); + public function isQueued(TaskHandler $task) : bool{ + return $this->tasks->contains($task); } - /** - * @return TaskHandler - * - * @throws \InvalidStateException - */ - private function addTask(Task $task, int $delay, int $period){ + 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){ @@ -138,7 +103,7 @@ class TaskScheduler{ $period = 1; } - return $this->handle(new TaskHandler($task, $this->nextId(), $delay, $period, $this->owner)); + return $this->handle(new TaskHandler($task, $delay, $period, $this->owner)); } private function handle(TaskHandler $handler) : TaskHandler{ @@ -149,7 +114,7 @@ class TaskScheduler{ } $handler->setNextRun($nextRun); - $this->tasks[$handler->getTaskId()] = $handler; + $this->tasks->add($handler); $this->queue->insert($handler, $nextRun); return $handler; @@ -164,25 +129,22 @@ class TaskScheduler{ $this->enabled = $enabled; } - /** - * @return void - */ - public function mainThreadHeartbeat(int $currentTick){ + public function mainThreadHeartbeat(int $currentTick) : void{ $this->currentTick = $currentTick; while($this->isReady($this->currentTick)){ /** @var TaskHandler $task */ $task = $this->queue->extract(); if($task->isCancelled()){ - unset($this->tasks[$task->getTaskId()]); + $this->tasks->remove($task); continue; } - $task->run($this->currentTick); + $task->run(); if(!$task->isCancelled() && $task->isRepeating()){ $task->setNextRun($this->currentTick + $task->getPeriod()); $this->queue->insert($task, $this->currentTick + $task->getPeriod()); }else{ $task->remove(); - unset($this->tasks[$task->getTaskId()]); + $this->tasks->remove($task); } } } @@ -190,8 +152,4 @@ class TaskScheduler{ private function isReady(int $currentTick) : bool{ return !$this->queue->isEmpty() and $this->queue->current()->getNextRun() <= $currentTick; } - - private function nextId() : int{ - return $this->ids++; - } } diff --git a/src/pocketmine/scheduler/SendUsageTask.php b/src/stats/SendUsageTask.php similarity index 85% rename from src/pocketmine/scheduler/SendUsageTask.php rename to src/stats/SendUsageTask.php index 3bb75a8601..9440ae7797 100644 --- a/src/pocketmine/scheduler/SendUsageTask.php +++ b/src/stats/SendUsageTask.php @@ -21,16 +21,19 @@ declare(strict_types=1); -namespace pocketmine\scheduler; +namespace pocketmine\stats; 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; -use pocketmine\utils\UUID; -use pocketmine\utils\VersionString; +use pocketmine\VersionInfo; +use Ramsey\Uuid\Uuid; +use function array_map; use function array_values; use function count; use function json_encode; @@ -57,18 +60,18 @@ class SendUsageTask extends AsyncTask{ * @phpstan-param array $playerList */ public function __construct(Server $server, int $type, array $playerList = []){ - $endpoint = "http://" . $server->getProperty("anonymous-statistics.host", "stats.pocketmine.net") . "/"; + $endpoint = "http://" . $server->getConfigGroup()->getPropertyString("anonymous-statistics.host", "stats.pocketmine.net") . "/"; $data = []; $data["uniqueServerId"] = $server->getServerUniqueId()->toString(); $data["uniqueMachineId"] = Utils::getMachineUniqueId()->toString(); - $data["uniqueRequestId"] = UUID::fromData($server->getServerUniqueId()->toString(), microtime(false))->toString(); + $data["uniqueRequestId"] = Uuid::uuid3($server->getServerUniqueId()->toString(), microtime(false))->toString(); switch($type){ case self::TYPE_OPEN: $data["event"] = "open"; - $version = new VersionString(\pocketmine\BASE_VERSION, \pocketmine\IS_DEVELOPMENT_BUILD, \pocketmine\BUILD_NUMBER); + $version = VersionInfo::VERSION(); $data["server"] = [ "port" => $server->getPort(), @@ -120,16 +123,9 @@ 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 = []; - foreach($server->getOnlinePlayers() as $p){ - if($p->isOnline()){ - $players[] = md5($p->getUniqueId()->toBinary()); - } - } + $players = array_map(function(Player $p) : string{ return md5($p->getUniqueId()->getBytes()); }, $server->getOnlinePlayers()); $data["players"] = [ "count" => count($players), @@ -159,7 +155,7 @@ class SendUsageTask extends AsyncTask{ $this->data = $data; } - public function onRun(){ + public function onRun() : void{ Internet::postURL($this->endpoint, $this->data, 5, [ "Content-Type: application/json", "Content-Length: " . strlen($this->data) diff --git a/src/pocketmine/Thread.php b/src/thread/CommonThreadPartsTrait.php similarity index 51% rename from src/pocketmine/Thread.php rename to src/thread/CommonThreadPartsTrait.php index 5a174c2dc1..3f609d8330 100644 --- a/src/pocketmine/Thread.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -21,17 +21,15 @@ declare(strict_types=1); -namespace pocketmine; +namespace pocketmine\thread; -use const PTHREADS_INHERIT_ALL; +use pocketmine\errorhandler\ErrorToExceptionHandler; +use pocketmine\Server; +use function error_reporting; -/** - * This class must be extended by all custom threading classes - */ -abstract class Thread extends \Thread{ - - /** @var \ClassLoader|null */ - protected $classLoader; +trait CommonThreadPartsTrait{ + /** @var \Threaded|\ClassLoader[]|null */ + private ?\Threaded $classLoaders = null; /** @var string|null */ protected $composerAutoloaderPath; @@ -39,69 +37,66 @@ abstract class Thread extends \Thread{ protected $isKilled = false; /** - * @return \ClassLoader|null + * @return \ClassLoader[] */ - public function getClassLoader(){ - return $this->classLoader; + public function getClassLoaders() : ?array{ + return $this->classLoaders !== null ? (array) $this->classLoaders : null; } /** - * @return void + * @param \ClassLoader[] $autoloaders */ - public function setClassLoader(\ClassLoader $loader = null){ + public function setClassLoaders(?array $autoloaders = null) : void{ $this->composerAutoloaderPath = \pocketmine\COMPOSER_AUTOLOADER_PATH; - if($loader === null){ - $loader = Server::getInstance()->getLoader(); + if($autoloaders === null){ + $autoloaders = [Server::getInstance()->getLoader()]; + } + + if($this->classLoaders === null){ + $this->classLoaders = new \Threaded(); + }else{ + foreach($this->classLoaders as $k => $autoloader){ + unset($this->classLoaders[$k]); + } + } + foreach($autoloaders as $autoloader){ + $this->classLoaders[] = $autoloader; } - $this->classLoader = $loader; } /** - * Registers the class loader for this thread. + * Registers the class loaders for this thread. * * WARNING: This method MUST be called from any descendent threads' run() method to make autoloading usable. * If you do not do this, you will not be able to use new classes that were not loaded when the thread was started * (unless you are using a custom autoloader). - * - * @return void */ - public function registerClassLoader(){ + public function registerClassLoaders() : void{ if($this->composerAutoloaderPath !== null){ require $this->composerAutoloaderPath; } - if($this->classLoader !== null){ - $this->classLoader->register(false); + $autoloaders = $this->classLoaders; + if($autoloaders !== null){ + foreach($autoloaders as $autoloader){ + /** @var \ClassLoader $autoloader */ + $autoloader->register(false); + } } } + final public function run() : void{ + error_reporting(-1); + $this->registerClassLoaders(); + //set this after the autoloader is registered + ErrorToExceptionHandler::set(); + $this->onRun(); + } + /** - * @return bool + * Runs code on the thread. */ - public function start(int $options = PTHREADS_INHERIT_ALL){ - ThreadManager::getInstance()->add($this); - - if($this->getClassLoader() === null){ - $this->setClassLoader(); - } - return parent::start($options); - } - - /** - * Stops the thread using the best way possible. Try to stop it yourself before calling this. - * - * @return void - */ - public function quit(){ - $this->isKilled = true; - - if(!$this->isJoined()){ - $this->notify(); - $this->join(); - } - - ThreadManager::getInstance()->remove($this); - } + abstract protected function onRun() : void; public function getThreadName() : string{ return (new \ReflectionClass($this))->getShortName(); diff --git a/src/thread/Thread.php b/src/thread/Thread.php new file mode 100644 index 0000000000..8448289a6a --- /dev/null +++ b/src/thread/Thread.php @@ -0,0 +1,64 @@ +add($this); + + if($this->getClassLoaders() === null){ + $this->setClassLoaders(); + } + return parent::start($options); + } + + /** + * Stops the thread using the best way possible. Try to stop it yourself before calling this. + */ + public function quit() : void{ + $this->isKilled = true; + + if(!$this->isJoined()){ + $this->notify(); + $this->join(); + } + + ThreadManager::getInstance()->remove($this); + } +} diff --git a/src/thread/ThreadException.php b/src/thread/ThreadException.php new file mode 100644 index 0000000000..2779c1795b --- /dev/null +++ b/src/thread/ThreadException.php @@ -0,0 +1,28 @@ +quit(); $logger->debug($thread->getThreadName() . " thread stopped successfully."); - }catch(\ThreadException $e){ + }catch(ThreadException $e){ ++$erroredThreads; $logger->debug("Could not stop " . $thread->getThreadName() . " thread: " . $e->getMessage()); } diff --git a/src/thread/Worker.php b/src/thread/Worker.php new file mode 100644 index 0000000000..e710787ccd --- /dev/null +++ b/src/thread/Worker.php @@ -0,0 +1,65 @@ +add($this); + + if($this->getClassLoaders() === null){ + $this->setClassLoaders(); + } + return parent::start($options); + } + + /** + * Stops the thread using the best way possible. Try to stop it yourself before calling this. + */ + public function quit() : void{ + $this->isKilled = true; + + if(!$this->isShutdown()){ + while($this->unstack() !== null); + $this->notify(); + $this->shutdown(); + } + + ThreadManager::getInstance()->remove($this); + } +} diff --git a/src/timings/Timings.php b/src/timings/Timings.php new file mode 100644 index 0000000000..c0176cc604 --- /dev/null +++ b/src/timings/Timings.php @@ -0,0 +1,242 @@ +getOwnerName() . " Runnable: " . $task->getTaskName(); + + if($period > 0){ + $name .= "(interval:" . $period . ")"; + }else{ + $name .= "(Single)"; + } + + if(!isset(self::$pluginTaskTimingMap[$name])){ + self::$pluginTaskTimingMap[$name] = new TimingsHandler($name, self::$schedulerSync); + } + + return self::$pluginTaskTimingMap[$name]; + } + + public static function getEntityTimings(Entity $entity) : TimingsHandler{ + $entityType = (new \ReflectionClass($entity))->getShortName(); + if(!isset(self::$entityTypeTimingMap[$entityType])){ + if($entity instanceof Player){ + self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickEntity - EntityPlayer", self::$tickEntity); + }else{ + self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickEntity - " . $entityType, self::$tickEntity); + } + } + + return self::$entityTypeTimingMap[$entityType]; + } + + public static function getTileEntityTimings(Tile $tile) : TimingsHandler{ + $tileType = (new \ReflectionClass($tile))->getShortName(); + if(!isset(self::$tileEntityTypeTimingMap[$tileType])){ + self::$tileEntityTypeTimingMap[$tileType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickTileEntity - " . $tileType, self::$tickTileEntity); + } + + return self::$tileEntityTypeTimingMap[$tileType]; + } + + public static function getReceiveDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{ + $pid = $pk->pid(); + if(!isset(self::$packetReceiveTimingMap[$pid])){ + $pkName = (new \ReflectionClass($pk))->getShortName(); + self::$packetReceiveTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "receivePacket - " . $pkName . " [0x" . dechex($pid) . "]", self::$playerNetworkReceive); + } + + return self::$packetReceiveTimingMap[$pid]; + } + + public static function getSendDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{ + $pid = $pk->pid(); + if(!isset(self::$packetSendTimingMap[$pid])){ + $pkName = (new \ReflectionClass($pk))->getShortName(); + self::$packetSendTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "sendPacket - " . $pkName . " [0x" . dechex($pid) . "]", self::$playerNetworkSend); + } + + return self::$packetSendTimingMap[$pid]; + } +} diff --git a/src/timings/TimingsHandler.php b/src/timings/TimingsHandler.php new file mode 100644 index 0000000000..eef7cd0913 --- /dev/null +++ b/src/timings/TimingsHandler.php @@ -0,0 +1,186 @@ +getTotalTime(); + $count = $timings->getCount(); + if($count === 0){ + //this should never happen - a timings record shouldn't exist if it hasn't been used + continue; + } + + $avg = $time / $count; + + $result[] = " " . $timings->getName() . " Time: $time Count: " . $count . " Avg: $avg Violations: " . $timings->getViolations(); + } + + $result[] = "# Version " . Server::getInstance()->getVersion(); + $result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion(); + + $entities = 0; + $livingEntities = 0; + foreach(Server::getInstance()->getWorldManager()->getWorlds() as $world){ + $entities += count($world->getEntities()); + foreach($world->getEntities() as $e){ + if($e instanceof Living){ + ++$livingEntities; + } + } + } + + $result[] = "# Entities " . $entities; + $result[] = "# LivingEntities " . $livingEntities; + + $sampleTime = hrtime(true) - self::$timingStart; + $result[] = "Sample time $sampleTime (" . ($sampleTime / 1000000000) . "s)"; + return $result; + } + + public static function isEnabled() : bool{ + return self::$enabled; + } + + public static function setEnabled(bool $enable = true) : void{ + self::$enabled = $enable; + self::reload(); + } + + public static function getStartTime() : float{ + return self::$timingStart; + } + + public static function reload() : void{ + TimingsRecord::clearRecords(); + if(self::$enabled){ + self::$timingStart = hrtime(true); + } + } + + public static function tick(bool $measure = true) : void{ + if(self::$enabled){ + TimingsRecord::tick($measure); + } + } + + /** @var string */ + private $name; + /** @var TimingsHandler|null */ + private $parent = null; + + /** @var TimingsRecord|null */ + private $record = null; + + /** @var int */ + private $timingDepth = 0; + + public function __construct(string $name, ?TimingsHandler $parent = null){ + $this->name = $name; + $this->parent = $parent; + } + + public function getName() : string{ return $this->name; } + + public function startTiming() : void{ + if(self::$enabled){ + $this->internalStartTiming(hrtime(true)); + } + } + + private function internalStartTiming(int $now) : void{ + if(++$this->timingDepth === 1){ + if($this->record === null){ + $this->record = new TimingsRecord($this); + } + $this->record->startTiming($now); + if($this->parent !== null){ + $this->parent->internalStartTiming($now); + } + } + } + + public function stopTiming() : void{ + if(self::$enabled){ + $this->internalStopTiming(hrtime(true)); + } + } + + private function internalStopTiming(int $now) : void{ + if($this->timingDepth === 0){ + //TODO: it would be nice to bail here, but since we'd have to track timing depth across resets + //and enable/disable, it would have a performance impact. Therefore, considering the limited + //usefulness of bailing here anyway, we don't currently bother. + return; + } + if(--$this->timingDepth !== 0){ + return; + } + + if($this->record !== null){ + //this might be null if a timings reset occurred while the timer was running + $this->record->stopTiming($now); + } + if($this->parent !== null){ + $this->parent->internalStopTiming($now); + } + } + + /** + * @return mixed the result of the given closure + * + * @phpstan-template TClosureReturn + * @phpstan-param \Closure() : TClosureReturn $closure + * @phpstan-return TClosureReturn + */ + public function time(\Closure $closure){ + $this->startTiming(); + try{ + return $closure(); + }finally{ + $this->stopTiming(); + } + } + + /** + * @internal + */ + public function destroyCycles() : void{ + $this->record = null; + } +} diff --git a/src/timings/TimingsRecord.php b/src/timings/TimingsRecord.php new file mode 100644 index 0000000000..1150e55f41 --- /dev/null +++ b/src/timings/TimingsRecord.php @@ -0,0 +1,125 @@ + + */ + private static $records = []; + + public static function clearRecords() : void{ + foreach(self::$records as $record){ + $record->handler->destroyCycles(); + } + self::$records = []; + } + + /** + * @return self[] + * @phpstan-return array + */ + public static function getAll() : array{ return self::$records; } + + public static function tick(bool $measure = true) : void{ + if($measure){ + foreach(self::$records as $record){ + if($record->curTickTotal > 50000000){ + $record->violations += (int) round($record->curTickTotal / 50000000); + } + $record->curTickTotal = 0; + $record->curCount = 0; + } + }else{ + foreach(self::$records as $record){ + $record->totalTime -= $record->curTickTotal; + $record->count -= $record->curCount; + + $record->curTickTotal = 0; + $record->curCount = 0; + } + } + } + + /** @var TimingsHandler */ + private $handler; + + /** @var int */ + private $count = 0; + /** @var int */ + private $curCount = 0; + /** @var int */ + private $start = 0; + /** @var int */ + private $totalTime = 0; + /** @var int */ + private $curTickTotal = 0; + /** @var int */ + private $violations = 0; + + public function __construct(TimingsHandler $handler){ + self::$records[spl_object_id($this)] = $this; + //I'm not the biggest fan of this cycle, but it seems to be the most effective way to avoid leaking anything. + $this->handler = $handler; + } + + public function getName() : string{ return $this->handler->getName(); } + + public function getCount() : int{ return $this->count; } + + public function getCurCount() : int{ return $this->curCount; } + + public function getStart() : float{ return $this->start; } + + public function getTotalTime() : float{ return $this->totalTime; } + + public function getCurTickTotal() : float{ return $this->curTickTotal; } + + public function getViolations() : int{ return $this->violations; } + + public function startTiming(int $now) : void{ + $this->start = $now; + } + + public function stopTiming(int $now) : void{ + if($this->start == 0){ + return; + } + $diff = $now - $this->start; + $this->totalTime += $diff; + $this->curTickTotal += $diff; + ++$this->curCount; + ++$this->count; + $this->start = 0; + } +} diff --git a/src/pocketmine/updater/UpdateCheckTask.php b/src/updater/UpdateCheckTask.php similarity index 56% rename from src/pocketmine/updater/UpdateCheckTask.php rename to src/updater/UpdateCheckTask.php index 23fc75f27b..6b571b2fed 100644 --- a/src/pocketmine/updater/UpdateCheckTask.php +++ b/src/updater/UpdateCheckTask.php @@ -24,12 +24,13 @@ declare(strict_types=1); namespace pocketmine\updater; use pocketmine\scheduler\AsyncTask; -use pocketmine\Server; use pocketmine\utils\Internet; use function is_array; +use function is_string; use function json_decode; class UpdateCheckTask extends AsyncTask{ + private const TLS_KEY_UPDATER = "updater"; /** @var string */ private $endpoint; @@ -38,32 +39,33 @@ class UpdateCheckTask extends AsyncTask{ /** @var string */ private $error = "Unknown error"; - public function __construct(string $endpoint, string $channel){ + public function __construct(UpdateChecker $updater, string $endpoint, string $channel){ + $this->storeLocal(self::TLS_KEY_UPDATER, $updater); $this->endpoint = $endpoint; $this->channel = $channel; } - public function onRun(){ + public function onRun() : void{ $error = ""; $response = Internet::getURL($this->endpoint . "?channel=" . $this->channel, 4, [], $error); $this->error = $error; - if($response !== false){ - $response = json_decode($response, true); + if($response !== null){ + $response = json_decode($response->getBody(), true); if(is_array($response)){ - if( - isset($response["base_version"]) and - isset($response["is_dev"]) and - isset($response["build"]) and - isset($response["date"]) and - isset($response["download_url"]) - ){ - $response["details_url"] = $response["details_url"] ?? null; - $this->setResult($response); - }elseif(isset($response["error"])){ + if(isset($response["error"]) and is_string($response["error"])){ $this->error = $response["error"]; }else{ - $this->error = "Invalid response data"; + $mapper = new \JsonMapper(); + $mapper->bExceptionOnMissingData = true; + $mapper->bEnforceMapType = false; + try{ + /** @var UpdateInfo $responseObj */ + $responseObj = $mapper->map($response, new UpdateInfo()); + $this->setResult($responseObj); + }catch(\JsonMapper_Exception $e){ + $this->error = "Invalid JSON response data: " . $e->getMessage(); + } } }else{ $this->error = "Invalid response data"; @@ -71,17 +73,15 @@ class UpdateCheckTask extends AsyncTask{ } } - public function onCompletion(Server $server){ - if($this->error !== ""){ - $server->getLogger()->debug("[AutoUpdater] Async update check failed due to \"$this->error\""); + public function onCompletion() : void{ + /** @var UpdateChecker $updater */ + $updater = $this->fetchLocal(self::TLS_KEY_UPDATER); + if($this->hasResult()){ + /** @var UpdateInfo $response */ + $response = $this->getResult(); + $updater->checkUpdateCallback($response); }else{ - $updateInfo = $this->getResult(); - if(is_array($updateInfo)){ - $server->getUpdater()->checkUpdateCallback($updateInfo); - }else{ - $server->getLogger()->debug("[AutoUpdater] Update info error"); - } - + $updater->checkUpdateError($this->error); } } } diff --git a/src/updater/UpdateChecker.php b/src/updater/UpdateChecker.php new file mode 100644 index 0000000000..f7459a1ce8 --- /dev/null +++ b/src/updater/UpdateChecker.php @@ -0,0 +1,177 @@ +server = $server; + $this->logger = new \PrefixedLogger($server->getLogger(), "Update Checker"); + $this->endpoint = "http://$endpoint/api/"; + + if($server->getConfigGroup()->getPropertyBool("auto-updater.enabled", true)){ + $this->doCheck(); + } + } + + public function checkUpdateError(string $error) : void{ + $this->logger->debug("Async update check failed due to \"$error\""); + } + + /** + * Callback used at the end of the update checking task + */ + public function checkUpdateCallback(UpdateInfo $updateInfo) : void{ + $this->checkUpdate($updateInfo); + if($this->hasUpdate()){ + (new UpdateNotifyEvent($this))->call(); + if($this->server->getConfigGroup()->getPropertyBool("auto-updater.on-update.warn-console", true)){ + $this->showConsoleUpdate(); + } + }else{ + if(!VersionInfo::IS_DEVELOPMENT_BUILD and $this->getChannel() !== "stable"){ + $this->showChannelSuggestionStable(); + }elseif(VersionInfo::IS_DEVELOPMENT_BUILD and $this->getChannel() === "stable"){ + $this->showChannelSuggestionBeta(); + } + } + } + + /** + * Returns whether there is an update available. + */ + public function hasUpdate() : bool{ + return $this->updateInfo !== null; + } + + /** + * Posts a warning to the console to tell the user there is an update available + */ + public function showConsoleUpdate() : void{ + if($this->updateInfo === null){ + return; + } + $newVersion = new VersionString($this->updateInfo->base_version, $this->updateInfo->is_dev, $this->updateInfo->build); + $messages = [ + "Your version of " . $this->server->getName() . " is out of date. Version " . $newVersion->getFullVersion(true) . " was released on " . date("D M j h:i:s Y", $this->updateInfo->date) + ]; + + $messages[] = "Details: " . $this->updateInfo->details_url; + $messages[] = "Download: " . $this->updateInfo->download_url; + + $this->printConsoleMessage($messages, \LogLevel::WARNING); + } + + protected function showChannelSuggestionStable() : void{ + $this->printConsoleMessage([ + "You're running a Stable build, but you're receiving update notifications for " . ucfirst($this->getChannel()) . " builds.", + "To get notified about new Stable builds only, change 'preferred-channel' in your pocketmine.yml to 'stable'." + ]); + } + + protected function showChannelSuggestionBeta() : void{ + $this->printConsoleMessage([ + "You're running a Beta build, but you're receiving update notifications for Stable builds.", + "To get notified about new Beta or Development builds, change 'preferred-channel' in your pocketmine.yml to 'beta' or 'development'." + ]); + } + + /** + * @param string[] $lines + */ + protected function printConsoleMessage(array $lines, string $logLevel = \LogLevel::INFO) : void{ + foreach($lines as $line){ + $this->logger->log($logLevel, $line); + } + } + + /** + * Returns the last retrieved update data. + */ + public function getUpdateInfo() : ?UpdateInfo{ + return $this->updateInfo; + } + + /** + * Schedules an AsyncTask to check for an update. + */ + public function doCheck() : void{ + $this->server->getAsyncPool()->submitTask(new UpdateCheckTask($this, $this->endpoint, $this->getChannel())); + } + + /** + * Checks the update information against the current server version to decide if there's an update + */ + protected function checkUpdate(UpdateInfo $updateInfo) : void{ + $currentVersion = VersionInfo::VERSION(); + try{ + $newVersion = new VersionString($updateInfo->base_version, $updateInfo->is_dev, $updateInfo->build); + }catch(\InvalidArgumentException $e){ + //Invalid version returned from API, assume there's no update + $this->logger->debug("Assuming no update because \"" . $e->getMessage() . "\""); + return; + } + + if($currentVersion->getBuild() > 0 && $currentVersion->compare($newVersion) > 0){ + $this->updateInfo = $updateInfo; + } + } + + /** + * 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; + } + + /** + * Returns the host used for update checks. + */ + public function getEndpoint() : string{ + return $this->endpoint; + } +} diff --git a/src/updater/UpdateInfo.php b/src/updater/UpdateInfo.php new file mode 100644 index 0000000000..ebbc393d0b --- /dev/null +++ b/src/updater/UpdateInfo.php @@ -0,0 +1,53 @@ + $default */ - public function __construct(string $file, int $type = Config::DETECT, array $default = [], &$correct = null){ + public function __construct(string $file, int $type = Config::DETECT, array $default = []){ $this->load($file, $type, $default); - $correct = $this->correct; } /** * Removes all the changes in memory and loads the file again - * - * @return void */ - public function reload(){ + public function reload() : void{ $this->config = []; $this->nestedCache = []; - $this->correct = false; $this->load($this->file, $this->type); } @@ -150,19 +143,19 @@ class Config{ /** * @param mixed[] $default * @phpstan-param array $default + * + * @throws \InvalidArgumentException if config type is invalid or could not be auto-detected */ - public function load(string $file, int $type = Config::DETECT, array $default = []) : bool{ - $this->correct = true; + private function load(string $file, int $type = Config::DETECT, array $default = []) : void{ $this->file = $file; $this->type = $type; if($this->type === Config::DETECT){ - $extension = explode(".", basename($this->file)); - $extension = strtolower(trim(array_pop($extension))); + $extension = strtolower(Path::getExtension($this->file)); if(isset(Config::$formats[$extension])){ $this->type = Config::$formats[$extension]; }else{ - $this->correct = false; + throw new \InvalidArgumentException("Cannot detect config type of " . $this->file); } } @@ -170,81 +163,35 @@ class Config{ $this->config = $default; $this->save(); }else{ - if($this->correct){ - $content = file_get_contents($this->file); - if($content === false){ - $this->correct = false; - return false; - } - $config = null; - switch($this->type){ - case Config::PROPERTIES: - $config = $this->parseProperties($content); - break; - case Config::JSON: - $config = json_decode($content, true); - break; - case Config::YAML: - $content = self::fixYAMLIndexes($content); - $config = yaml_parse($content); - break; - case Config::SERIALIZED: - $config = unserialize($content); - break; - case Config::ENUM: - $config = self::parseList($content); - break; - default: - $this->correct = false; - - return false; - } - $this->config = is_array($config) ? $config : $default; - if($this->fillDefaults($default, $this->config) > 0){ - $this->save(); - } - }else{ - return false; + $content = file_get_contents($this->file); + if($content === false){ + throw new \RuntimeException("Unable to load config file"); } - } - - return true; - } - - public function check() : bool{ - return $this->correct; - } - - public function save() : bool{ - if($this->correct){ - $content = null; + $config = null; switch($this->type){ case Config::PROPERTIES: - $content = $this->writeProperties(); + $config = self::parseProperties($content); break; case Config::JSON: - $content = json_encode($this->config, $this->jsonOptions); + $config = json_decode($content, true); break; case Config::YAML: - $content = yaml_emit($this->config, YAML_UTF8_ENCODING); + $content = self::fixYAMLIndexes($content); + $config = yaml_parse($content); break; case Config::SERIALIZED: - $content = serialize($this->config); + $config = unserialize($content); break; case Config::ENUM: - $content = implode("\r\n", array_keys($this->config)); + $config = array_fill_keys(self::parseList($content), true); break; default: - throw new \InvalidStateException("Config type is unknown, has not been set or not detected"); + throw new \InvalidArgumentException("Invalid config type specified"); + } + $this->config = is_array($config) ? $config : $default; + if($this->fillDefaults($default, $this->config) > 0){ + $this->save(); } - - file_put_contents($this->file, $content); - - $this->changed = false; - - return true; - }else{ - return false; } } @@ -255,6 +202,36 @@ class Config{ return $this->file; } + /** + * Flushes the config to disk in the appropriate format. + */ + public function save() : void{ + $content = null; + switch($this->type){ + case Config::PROPERTIES: + $content = self::writeProperties($this->config); + break; + case Config::JSON: + $content = json_encode($this->config, $this->jsonOptions); + break; + case Config::YAML: + $content = yaml_emit($this->config, YAML_UTF8_ENCODING); + break; + case Config::SERIALIZED: + $content = serialize($this->config); + break; + case Config::ENUM: + $content = self::writeList(array_keys($this->config)); + break; + default: + throw new AssumptionFailedError("Config type is unknown, has not been set or not detected"); + } + + file_put_contents($this->file, $content); + + $this->changed = false; + } + /** * Sets the options for the JSON encoding when saving * @@ -331,10 +308,8 @@ class Config{ /** * @param string $k * @param mixed $v - * - * @return void */ - public function __set($k, $v){ + public function __set($k, $v) : void{ $this->set($k, $v); } @@ -357,10 +332,8 @@ class Config{ /** * @param string $key * @param mixed $value - * - * @return void */ - public function setNested($key, $value){ + public function setNested($key, $value) : void{ $vars = explode(".", $key); $base = array_shift($vars); @@ -442,19 +415,17 @@ class Config{ * @return bool|mixed */ public function get($k, $default = false){ - return ($this->correct and isset($this->config[$k])) ? $this->config[$k] : $default; + return $this->config[$k] ?? $default; } /** * @param string $k key to be set * @param mixed $v value to set key - * - * @return void */ - public function set($k, $v = true){ + 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]); } @@ -464,10 +435,8 @@ class Config{ /** * @param mixed[] $v * @phpstan-param array $v - * - * @return void */ - public function setAll(array $v){ + public function setAll(array $v) : void{ $this->config = $v; $this->changed = true; } @@ -488,10 +457,8 @@ class Config{ /** * @param string $k - * - * @return void */ - public function remove($k){ + public function remove($k) : void{ unset($this->config[$k]); $this->changed = true; } @@ -507,10 +474,8 @@ class Config{ /** * @param mixed[] $defaults * @phpstan-param array $defaults - * - * @return void */ - public function setDefaults(array $defaults){ + public function setDefaults(array $defaults) : void{ $this->fillDefaults($defaults, $this->config); } @@ -522,7 +487,7 @@ 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])){ $data[$k] = []; @@ -542,28 +507,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"; } @@ -572,9 +547,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){ @@ -590,11 +566,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])){ - MainLogger::getLogger()->debug("[Config] Repeated property " . $k . " on file " . $this->file); - } - $result[$k] = $v; + $result[(string) $k] = $v; } } diff --git a/src/pocketmine/utils/TextFormatJsonObject.php b/src/utils/DestructorCallbackTrait.php similarity index 52% rename from src/pocketmine/utils/TextFormatJsonObject.php rename to src/utils/DestructorCallbackTrait.php index f6ebf02aa6..3653cfa05d 100644 --- a/src/pocketmine/utils/TextFormatJsonObject.php +++ b/src/utils/DestructorCallbackTrait.php @@ -24,37 +24,27 @@ declare(strict_types=1); namespace pocketmine\utils; /** - * @internal - * @see TextFormat::toJSON() + * This trait provides destructor callback functionality to objects which use it. This enables a weakmap-like system + * to function without actually having weak maps. + * TODO: remove this in PHP 8 */ -final class TextFormatJsonObject implements \JsonSerializable{ - /** @var string|null */ - public $text = null; - /** @var string|null */ - public $color = null; - /** @var bool|null */ - public $bold = null; - /** @var bool|null */ - public $italic = null; - /** @var bool|null */ - public $underlined = null; - /** @var bool|null */ - public $strikethrough = null; - /** @var bool|null */ - public $obfuscated = null; +trait DestructorCallbackTrait{ /** - * @var TextFormatJsonObject[]|null - * @phpstan-var array|null + * @var ObjectSet + * @phpstan-var ObjectSet<\Closure() : void>|null */ - public $extra = null; + private $destructorCallbacks = null; - public function jsonSerialize(){ - $result = (array) $this; - foreach($result as $k => $v){ - if($v === null){ - unset($result[$k]); + /** @phpstan-return ObjectSet<\Closure() : void> */ + public function getDestructorCallbacks() : ObjectSet{ + return $this->destructorCallbacks === null ? ($this->destructorCallbacks = new ObjectSet()) : $this->destructorCallbacks; + } + + public function __destruct(){ + if($this->destructorCallbacks !== null){ + foreach($this->destructorCallbacks as $callback){ + $callback(); } } - return $result; } } diff --git a/src/utils/EnumTrait.php b/src/utils/EnumTrait.php new file mode 100644 index 0000000000..40057b9ea4 --- /dev/null +++ b/src/utils/EnumTrait.php @@ -0,0 +1,102 @@ +name(), $member); + } + + protected static function registerAll(self ...$members) : void{ + foreach($members as $member){ + self::register($member); + } + } + + /** + * Returns all members of the enum. + * This is overridden to change the return typehint. + * + * @return self[] + */ + public static function getAll() : array{ + //phpstan doesn't support generic traits yet :( + /** @var self[] $result */ + $result = self::_registryGetAll(); + return $result; + } + + /** @var int|null */ + private static $nextId = null; + + /** @var string */ + private $enumName; + /** @var int */ + private $runtimeId; + + /** + * @throws \InvalidArgumentException + */ + private function __construct(string $enumName){ + if(preg_match('/^\D[A-Za-z\d_]+$/u', $enumName, $matches) === 0){ + throw new \InvalidArgumentException("Invalid enum member name \"$enumName\", should only contain letters, numbers and underscores, and must not start with a number"); + } + $this->enumName = $enumName; + if(self::$nextId === null){ + self::$nextId = Process::pid(); //this provides enough base entropy to prevent hardcoding + } + $this->runtimeId = self::$nextId++; + } + + public function name() : string{ + return $this->enumName; + } + + /** + * Returns a runtime-only identifier for this enum member. This will be different with each run, so don't try to + * hardcode it. + * This can be useful for switches or array indexing. + */ + public function id() : int{ + return $this->runtimeId; + } + + /** + * Returns whether the two objects are equivalent. + */ + public function equals(self $other) : bool{ + return $this->enumName === $other->enumName; + } +} diff --git a/src/utils/Filesystem.php b/src/utils/Filesystem.php new file mode 100644 index 0000000000..2d107af6e0 --- /dev/null +++ b/src/utils/Filesystem.php @@ -0,0 +1,224 @@ + + */ + private static $cleanedPaths = [ + \pocketmine\PATH => self::CLEAN_PATH_SRC_PREFIX + ]; + + public const CLEAN_PATH_SRC_PREFIX = "pmsrc"; + public const CLEAN_PATH_PLUGINS_PREFIX = "plugins"; + + private function __construct(){ + //NOOP + } + + 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"); + foreach($objects as $object){ + if($object !== "." and $object !== ".."){ + $fullObject = Path::join($dir, $object); + if(is_dir($fullObject)){ + self::recursiveUnlink($fullObject); + }else{ + unlink($fullObject); + } + } + } + rmdir($dir); + }elseif(is_file($dir)){ + unlink($dir); + } + } + + /** + * Recursively copies a directory to a new location. The parent directories for the destination must exist. + */ + public static function recursiveCopy(string $origin, string $destination) : void{ + if(!is_dir($origin)){ + throw new \RuntimeException("$origin does not exist, or is not a directory"); + } + if(!is_dir($destination)){ + if(file_exists($destination)){ + throw new \RuntimeException("$destination already exists, and is not a directory"); + } + if(!is_dir(dirname($destination))){ + //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?)"); + } + } + self::recursiveCopyInternal($origin, $destination); + } + + private static function recursiveCopyInternal(string $origin, string $destination) : void{ + if(is_dir($origin)){ + if(!is_dir($destination)){ + if(file_exists($destination)){ + throw new \RuntimeException("Path $destination does not exist, or is not a directory"); + } + 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"); + foreach($objects as $object){ + if($object === "." || $object === ".."){ + continue; + } + self::recursiveCopyInternal(Path::join($origin, $object), Path::join($destination, $object)); + } + }else{ + $dirName = dirname($destination); + if(!is_dir($dirName)){ //the destination folder should already exist + throw new AssumptionFailedError("The destination folder should have been created in the parent call"); + } + copy($origin, $destination); + } + } + + public static function addCleanedPath(string $path, string $replacement) : void{ + self::$cleanedPaths[$path] = $replacement; + uksort(self::$cleanedPaths, function(string $str1, string $str2) : int{ + return strlen($str2) <=> strlen($str1); //longest first + }); + } + + /** + * @return string[] + * @phpstan-return array + */ + public static function getCleanedPaths() : array{ return self::$cleanedPaths; } + + /** + * @param string $path + * + * @return string + */ + public static function cleanPath($path){ + $result = str_replace([DIRECTORY_SEPARATOR, ".php", "phar://"], ["/", "", ""], $path); + + //remove relative paths + //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), "/"); + } + } + return $result; + } + + /** + * Attempts to get a lock on the specified file, creating it if it does not exist. This is typically used for IPC to + * inform other processes that some file or folder is already in use, to avoid data corruption. + * If this function succeeds in gaining a lock on the file, it writes the current PID to the file. + * + * @return int|null process ID of the process currently holding the lock failure, null on success. + * @throws \InvalidArgumentException if the lock file path is invalid (e.g. parent directory doesn't exist, permission denied) + */ + public static function createLockFile(string $lockFilePath) : ?int{ + $resource = fopen($lockFilePath, "a+b"); + if($resource === false){ + throw new \InvalidArgumentException("Invalid lock file path or read/write permissions denied"); + } + if(!flock($resource, LOCK_EX | LOCK_NB)){ + //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); + if(preg_match('/^\d+$/', $pid) === 1){ + return (int) $pid; + } + return -1; + } + ftruncate($resource, 0); + fwrite($resource, (string) getmypid()); + fflush($resource); + flock($resource, LOCK_SH); //prevent acquiring an exclusive lock from another process, but allow reading + self::$lockFileHandles[realpath($lockFilePath)] = $resource; //keep the resource alive to preserve the lock + return null; + } + + /** + * Releases a file lock previously acquired by createLockFile() and deletes the lock file. + * + * @throws \InvalidArgumentException if the lock file path is invalid (e.g. parent directory doesn't exist, permission denied) + */ + public static function releaseLockFile(string $lockFilePath) : void{ + $lockFilePath = realpath($lockFilePath); + if($lockFilePath === false){ + throw new \InvalidArgumentException("Invalid lock file path"); + } + if(isset(self::$lockFileHandles[$lockFilePath])){ + flock(self::$lockFileHandles[$lockFilePath], LOCK_UN); + fclose(self::$lockFileHandles[$lockFilePath]); + unset(self::$lockFileHandles[$lockFilePath]); + @unlink($lockFilePath); + } + } +} diff --git a/src/pocketmine/utils/Git.php b/src/utils/Git.php similarity index 85% rename from src/pocketmine/utils/Git.php rename to src/utils/Git.php index f3e8fb1c1c..dd83eb0331 100644 --- a/src/pocketmine/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(Utils::execute("git -C \"$dir\" rev-parse HEAD", $out) === 0 and $out !== false and strlen($out = trim($out)) === 40){ - if(Utils::execute("git -C \"$dir\" diff --quiet") === 1 or Utils::execute("git -C \"$dir\" diff --cached --quiet") === 1){ //Locally-modified + 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 $dirty = true; } return $out; diff --git a/src/pocketmine/utils/Internet.php b/src/utils/Internet.php similarity index 76% rename from src/pocketmine/utils/Internet.php rename to src/utils/Internet.php index 5c3f06329e..329457b260 100644 --- a/src/pocketmine/utils/Internet.php +++ b/src/utils/Internet.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\utils; +use pocketmine\VersionInfo; use function array_merge; use function curl_close; use function curl_error; @@ -84,28 +85,28 @@ class Internet{ } $ip = self::getURL("http://api.ipify.org/"); - if($ip !== false){ - return self::$ip = $ip; + if($ip !== null){ + return self::$ip = $ip->getBody(); } $ip = self::getURL("http://checkip.dyndns.org/"); - if($ip !== false and preg_match('#Current IP Address\: ([0-9a-fA-F\:\.]*)#', trim(strip_tags($ip)), $matches) > 0){ + if($ip !== null and 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 !== false and preg_match('#">([0-9a-fA-F\:\.]*)#', $ip, $matches) > 0){ + if($ip !== null and preg_match('#">([0-9a-fA-F\:\.]*)#', $ip->getBody(), $matches) > 0){ return self::$ip = $matches[1]; } $ip = self::getURL("http://checkmyip.org/"); - if($ip !== false and preg_match('#Your IP address is ([0-9a-fA-F\:\.]*)#', $ip, $matches) > 0){ + if($ip !== null and 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 !== false and trim($ip) != ""){ - return self::$ip = trim($ip); + if($ip !== null and ($addr = trim($ip->getBody())) != ""){ + return self::$ip = $addr; } return false; @@ -139,23 +140,17 @@ class Internet{ * GETs an URL using cURL * NOTE: This is a blocking operation and can take a significant amount of time. It is inadvisable to use this method on the main thread. * - * @param int $timeout default 10 - * @param string[] $extraHeaders - * @param string $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. - * @param string[] $headers reference parameter - * @param int $httpCode reference parameter + * @param int $timeout default 10 + * @param string[] $extraHeaders + * @param string|null $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. * @phpstan-param list $extraHeaders - * @phpstan-param array $headers - * - * @return string|false */ - public static function getURL(string $page, int $timeout = 10, array $extraHeaders = [], &$err = null, &$headers = null, &$httpCode = null){ + public static function getURL(string $page, int $timeout = 10, array $extraHeaders = [], &$err = null) : ?InternetRequestResult{ try{ - list($ret, $headers, $httpCode) = self::simpleCurl($page, $timeout, $extraHeaders); - return $ret; + return self::simpleCurl($page, $timeout, $extraHeaders); }catch(InternetException $ex){ $err = $ex->getMessage(); - return false; + return null; } } @@ -165,25 +160,19 @@ class Internet{ * * @param string[]|string $args * @param string[] $extraHeaders - * @param string $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. - * @param string[] $headers reference parameter - * @param int $httpCode reference parameter + * @param string|null $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. * @phpstan-param string|array $args * @phpstan-param list $extraHeaders - * @phpstan-param array $headers - * - * @return string|false */ - public static function postURL(string $page, $args, int $timeout = 10, array $extraHeaders = [], &$err = null, &$headers = null, &$httpCode = null){ + public static function postURL(string $page, $args, int $timeout = 10, array $extraHeaders = [], &$err = null) : ?InternetRequestResult{ try{ - list($ret, $headers, $httpCode) = self::simpleCurl($page, $timeout, $extraHeaders, [ + return self::simpleCurl($page, $timeout, $extraHeaders, [ CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $args ]); - return $ret; }catch(InternetException $ex){ $err = $ex->getMessage(); - return false; + return null; } } @@ -191,20 +180,17 @@ class Internet{ * General cURL shorthand function. * NOTE: This is a blocking operation and can take a significant amount of time. It is inadvisable to use this method on the main thread. * - * @param float|int $timeout The maximum connect timeout and timeout in seconds, correct to ms. + * @param float|int $timeout The maximum connect timeout and timeout in seconds, correct to ms. * @param string[] $extraHeaders extra headers to send as a plain string array * @param array $extraOpts extra CURLOPT_* to set as an [opt => value] map - * @param callable|null $onSuccess function to be called if there is no error. Accepts a resource argument as the cURL handle. + * @param \Closure|null $onSuccess function to be called if there is no error. Accepts a resource argument as the cURL handle. * @phpstan-param array $extraOpts * @phpstan-param list $extraHeaders - * @phpstan-param (callable(\CurlHandle) : void)|null $onSuccess - * - * @return array a plain array of three [result body : string, headers : string[][], HTTP response code : int]. Headers are grouped by requests with strtolower(header name) as keys and header value as values - * @phpstan-return array{string, list>, int} + * @phpstan-param (\Closure(\CurlHandle) : void)|null $onSuccess * * @throws InternetException if a cURL error occurs */ - public static function simpleCurl(string $page, $timeout = 10, array $extraHeaders = [], array $extraOpts = [], callable $onSuccess = null){ + public static function simpleCurl(string $page, $timeout = 10, array $extraHeaders = [], array $extraOpts = [], ?\Closure $onSuccess = null) : InternetRequestResult{ if(!self::$online){ throw new InternetException("Cannot execute web request while offline"); } @@ -224,7 +210,7 @@ class Internet{ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT_MS => (int) ($timeout * 1000), CURLOPT_TIMEOUT_MS => (int) ($timeout * 1000), - CURLOPT_HTTPHEADER => array_merge(["User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0 " . \pocketmine\NAME . "/" . \pocketmine\VERSION], $extraHeaders), + CURLOPT_HTTPHEADER => array_merge(["User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0 " . VersionInfo::NAME . "/" . VersionInfo::VERSION()->getFullVersion(true)], $extraHeaders), CURLOPT_HEADER => true ]); try{ @@ -252,7 +238,7 @@ class Internet{ if($onSuccess !== null){ $onSuccess($ch); } - return [$body, $headers, $httpCode]; + return new InternetRequestResult($headers, $body, $httpCode); }finally{ curl_close($ch); } diff --git a/src/pocketmine/utils/InternetException.php b/src/utils/InternetException.php similarity index 100% rename from src/pocketmine/utils/InternetException.php rename to src/utils/InternetException.php diff --git a/src/pocketmine/item/enchantment/EnchantmentEntry.php b/src/utils/InternetRequestResult.php similarity index 54% rename from src/pocketmine/item/enchantment/EnchantmentEntry.php rename to src/utils/InternetRequestResult.php index 50fd4301e2..481ceedb38 100644 --- a/src/pocketmine/item/enchantment/EnchantmentEntry.php +++ b/src/utils/InternetRequestResult.php @@ -21,38 +21,37 @@ declare(strict_types=1); -namespace pocketmine\item\enchantment; +namespace pocketmine\utils; -class EnchantmentEntry{ +final class InternetRequestResult{ - /** @var Enchantment[] */ - private $enchantments; - /** @var int */ - private $cost; + /** + * @var string[][] + * @phpstan-var list> + */ + private $headers; /** @var string */ - private $randomName; + private $body; + /** @var int */ + private $code; /** - * @param Enchantment[] $enchantments + * @param string[][] $headers + * @phpstan-param list> $headers */ - public function __construct(array $enchantments, int $cost, string $randomName){ - $this->enchantments = $enchantments; - $this->cost = $cost; - $this->randomName = $randomName; + public function __construct(array $headers, string $body, int $code){ + $this->headers = $headers; + $this->body = $body; + $this->code = $code; } /** - * @return Enchantment[] + * @return string[][] + * @phpstan-return list> */ - public function getEnchantments() : array{ - return $this->enchantments; - } + public function getHeaders() : array{ return $this->headers; } - public function getCost() : int{ - return $this->cost; - } + public function getBody() : string{ return $this->body; } - public function getRandomName() : string{ - return $this->randomName; - } + public function getCode() : int{ return $this->code; } } diff --git a/src/utils/MainLogger.php b/src/utils/MainLogger.php new file mode 100644 index 0000000000..c7290f0d29 --- /dev/null +++ b/src/utils/MainLogger.php @@ -0,0 +1,227 @@ +logDebug = $logDebug; + + $this->useFormattingCodes = $useFormattingCodes; + $this->mainThreadName = $mainThreadName; + $this->timezone = $timezone->getName(); + + $this->logWriterThread = new MainLoggerThread($logFile); + $this->logWriterThread->start(PTHREADS_INHERIT_NONE); + } + + /** + * Returns the current logger format used for console output. + */ + public function getFormat() : string{ + return $this->format; + } + + /** + * Sets the logger format to use for outputting text to the console. + * It should be an sprintf()able string accepting 5 string arguments: + * - time + * - color + * - thread name + * - prefix (debug, info etc) + * - message + * + * @see http://php.net/manual/en/function.sprintf.php + */ + public function setFormat(string $format) : void{ + $this->format = $format; + } + + public function emergency($message){ + $this->send($message, \LogLevel::EMERGENCY, "EMERGENCY", TextFormat::RED); + } + + public function alert($message){ + $this->send($message, \LogLevel::ALERT, "ALERT", TextFormat::RED); + } + + public function critical($message){ + $this->send($message, \LogLevel::CRITICAL, "CRITICAL", TextFormat::RED); + } + + public function error($message){ + $this->send($message, \LogLevel::ERROR, "ERROR", TextFormat::DARK_RED); + } + + public function warning($message){ + $this->send($message, \LogLevel::WARNING, "WARNING", TextFormat::YELLOW); + } + + public function notice($message){ + $this->send($message, \LogLevel::NOTICE, "NOTICE", TextFormat::AQUA); + } + + public function info($message){ + $this->send($message, \LogLevel::INFO, "INFO", TextFormat::WHITE); + } + + public function debug($message, bool $force = false){ + if(!$this->logDebug and !$force){ + return; + } + $this->send($message, \LogLevel::DEBUG, "DEBUG", TextFormat::GRAY); + } + + public function setLogDebug(bool $logDebug) : void{ + $this->logDebug = $logDebug; + } + + /** + * @param mixed[][]|null $trace + * @phpstan-param list>|null $trace + * + * @return void + */ + public function logException(\Throwable $e, $trace = null){ + $this->critical(implode("\n", Utils::printableExceptionInfo($e, $trace))); + + $this->syncFlushBuffer(); + } + + public function log($level, $message){ + switch($level){ + case LogLevel::EMERGENCY: + $this->emergency($message); + break; + case LogLevel::ALERT: + $this->alert($message); + break; + case LogLevel::CRITICAL: + $this->critical($message); + break; + case LogLevel::ERROR: + $this->error($message); + break; + case LogLevel::WARNING: + $this->warning($message); + break; + case LogLevel::NOTICE: + $this->notice($message); + break; + case LogLevel::INFO: + $this->info($message); + break; + case LogLevel::DEBUG: + $this->debug($message); + break; + } + } + + /** + * @phpstan-param \Closure() : void $c + */ + public function buffer(\Closure $c) : void{ + $this->synchronized($c); + } + + public function shutdownLogWriterThread() : void{ + if(\Thread::getCurrentThreadId() === $this->logWriterThread->getCreatorId()){ + $this->logWriterThread->shutdown(); + }else{ + throw new \LogicException("Only the creator thread can shutdown the logger thread"); + } + } + + /** + * @param string $message + * @param string $level + * @param string $prefix + * @param string $color + */ + protected function send($message, $level, $prefix, $color) : void{ + $time = new \DateTime('now', new \DateTimeZone($this->timezone)); + + $thread = \Thread::getCurrentThread(); + if($thread === null){ + $threadName = $this->mainThreadName . " thread"; + }elseif($thread instanceof Thread or $thread instanceof Worker){ + $threadName = $thread->getThreadName() . " thread"; + }else{ + $threadName = (new \ReflectionClass($thread))->getShortName() . " thread"; + } + + $message = sprintf($this->format, $time->format("H:i:s.v"), $color, $threadName, $prefix, TextFormat::clean($message, false)); + + if(!Terminal::isInit()){ + Terminal::init($this->useFormattingCodes); //lazy-init colour codes because we don't know if they've been registered on this thread + } + + $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->log($level, $message); + } + }); + } + + public function syncFlushBuffer() : void{ + $this->logWriterThread->syncFlushBuffer(); + } + + public function __destruct(){ + if(!$this->logWriterThread->isJoined() && \Thread::getCurrentThreadId() === $this->logWriterThread->getCreatorId()){ + $this->shutdownLogWriterThread(); + } + } +} diff --git a/src/utils/MainLoggerThread.php b/src/utils/MainLoggerThread.php new file mode 100644 index 0000000000..49b98c1c40 --- /dev/null +++ b/src/utils/MainLoggerThread.php @@ -0,0 +1,109 @@ +buffer = new \Threaded(); + touch($logFile); + $this->logFile = $logFile; + } + + public function write(string $line) : void{ + $this->synchronized(function() use ($line) : void{ + $this->buffer[] = $line; + $this->notify(); + }); + } + + public function syncFlushBuffer() : void{ + $this->synchronized(function() : void{ + $this->syncFlush = true; + $this->notify(); //write immediately + }); + $this->synchronized(function() : void{ + while($this->syncFlush){ + $this->wait(); //block until it's all been written to disk + } + }); + } + + public function shutdown() : void{ + $this->synchronized(function() : void{ + $this->shutdown = true; + $this->notify(); + }); + $this->join(); + } + + /** + * @param resource $logResource + */ + private function writeLogStream($logResource) : void{ + while($this->buffer->count() > 0){ + /** @var string $chunk */ + $chunk = $this->buffer->shift(); + fwrite($logResource, $chunk); + } + + $this->synchronized(function() : void{ + if($this->syncFlush){ + $this->syncFlush = false; + $this->notify(); //if this was due to a sync flush, tell the caller to stop waiting + } + }); + } + + public function run() : void{ + $logResource = fopen($this->logFile, "ab"); + if(!is_resource($logResource)){ + throw new \RuntimeException("Couldn't open log file"); + } + + while(!$this->shutdown){ + $this->writeLogStream($logResource); + $this->synchronized(function() : void{ + if(!$this->shutdown && !$this->syncFlush){ + $this->wait(); + } + }); + } + + $this->writeLogStream($logResource); + + fclose($logResource); + } +} diff --git a/src/utils/NotCloneable.php b/src/utils/NotCloneable.php new file mode 100644 index 0000000000..23eb04754d --- /dev/null +++ b/src/utils/NotCloneable.php @@ -0,0 +1,31 @@ + + */ +final class ObjectSet implements \IteratorAggregate{ + /** + * @var object[] + * @phpstan-var array + */ + private array $objects = []; + + /** @phpstan-param T ...$objects */ + public function add(object ...$objects) : void{ + foreach($objects as $object){ + $this->objects[spl_object_id($object)] = $object; + } + } + + /** @phpstan-param T ...$objects */ + public function remove(object ...$objects) : void{ + foreach($objects as $object){ + unset($this->objects[spl_object_id($object)]); + } + } + + public function clear() : void{ + $this->objects = []; + } + + public function contains(object $object) : bool{ + return array_key_exists(spl_object_id($object), $this->objects); + } + + /** @phpstan-return \ArrayIterator */ + public function getIterator() : \ArrayIterator{ + return new \ArrayIterator($this->objects); + } + + /** + * @return object[] + * @phpstan-return array + */ + public function toArray() : array{ + return $this->objects; + } +} diff --git a/src/pocketmine/utils/Process.php b/src/utils/Process.php similarity index 91% rename from src/pocketmine/utils/Process.php rename to src/utils/Process.php index ef092f2428..8e93f9b1c4 100644 --- a/src/pocketmine/utils/Process.php +++ b/src/utils/Process.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\utils; -use pocketmine\ThreadManager; +use pocketmine\thread\ThreadManager; use function count; use function exec; use function fclose; @@ -122,23 +122,24 @@ final class Process{ //TODO: more OS - return count(ThreadManager::getInstance()->getAll()) + 3; //RakLib + MainLogger + Main Thread + return count(ThreadManager::getInstance()->getAll()) + 2; //MainLogger + Main Thread } - /** - * @param int $pid - */ - public static function kill($pid) : void{ - if(MainLogger::isRegisteredStatic()){ - MainLogger::getLogger()->syncFlushBuffer(); + 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/pocketmine/utils/Random.php b/src/utils/Random.php similarity index 98% rename from src/pocketmine/utils/Random.php rename to src/utils/Random.php index 5614534206..81598ac24f 100644 --- a/src/pocketmine/utils/Random.php +++ b/src/utils/Random.php @@ -63,10 +63,8 @@ class Random{ /** * @param int $seed Integer to be used as seed. - * - * @return void */ - public function setSeed(int $seed){ + public function setSeed(int $seed) : void{ $this->seed = $seed; $this->x = self::X ^ $seed; $this->y = self::Y ^ ($seed << 17) | (($seed >> 15) & 0x7fffffff) & 0xffffffff; diff --git a/src/utils/RegistryTrait.php b/src/utils/RegistryTrait.php new file mode 100644 index 0000000000..31c2116f10 --- /dev/null +++ b/src/utils/RegistryTrait.php @@ -0,0 +1,109 @@ + $arguments + * + * @return object + */ + public static function __callStatic($name, $arguments){ + if(count($arguments) > 0){ + throw new \ArgumentCountError("Expected exactly 0 arguments, " . count($arguments) . " passed"); + } + try{ + return self::_registryFromString($name); + }catch(\InvalidArgumentException $e){ + throw new \Error($e->getMessage(), 0, $e); + } + } + + /** + * @return object[] + */ + private static function _registryGetAll() : array{ + self::checkInit(); + return array_map(function(object $o) : object{ + return self::preprocessMember($o); + }, self::$members); + } +} diff --git a/src/pocketmine/utils/ReversePriorityQueue.php b/src/utils/ReversePriorityQueue.php similarity index 100% rename from src/pocketmine/utils/ReversePriorityQueue.php rename to src/utils/ReversePriorityQueue.php diff --git a/src/pocketmine/utils/ServerException.php b/src/utils/ServerException.php similarity index 100% rename from src/pocketmine/utils/ServerException.php rename to src/utils/ServerException.php diff --git a/src/pocketmine/utils/ServerKiller.php b/src/utils/ServerKiller.php similarity index 91% rename from src/pocketmine/utils/ServerKiller.php rename to src/utils/ServerKiller.php index 96a42ce8d9..24f8eb37a3 100644 --- a/src/pocketmine/utils/ServerKiller.php +++ b/src/utils/ServerKiller.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\utils; -use pocketmine\Thread; +use pocketmine\thread\Thread; use function time; class ServerKiller extends Thread{ @@ -41,11 +41,7 @@ class ServerKiller extends Thread{ $this->time = $time; } - /** - * @return void - */ - public function run(){ - $this->registerClassLoader(); + protected function onRun() : void{ $start = time(); $this->synchronized(function() : void{ if(!$this->stopped){ @@ -54,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 0000000000..de81a2b670 --- /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/pocketmine/utils/SingletonTrait.php b/src/utils/SingletonTrait.php similarity index 100% rename from src/pocketmine/utils/SingletonTrait.php rename to src/utils/SingletonTrait.php diff --git a/src/utils/StringToTParser.php b/src/utils/StringToTParser.php new file mode 100644 index 0000000000..a15204ba47 --- /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); + } +} \ No newline at end of file diff --git a/src/pocketmine/utils/Terminal.php b/src/utils/Terminal.php similarity index 54% rename from src/pocketmine/utils/Terminal.php rename to src/utils/Terminal.php index d667f0f4cf..73e350a2c2 100644 --- a/src/pocketmine/utils/Terminal.php +++ b/src/utils/Terminal.php @@ -27,66 +27,44 @@ use function fclose; use function fopen; use function function_exists; use function getenv; -use function is_array; use function is_string; use function sapi_windows_vt100_support; use function shell_exec; use function stream_isatty; +use const PHP_EOL; abstract class Terminal{ - /** @var string */ - public static $FORMAT_BOLD = ""; - /** @var string */ - public static $FORMAT_OBFUSCATED = ""; - /** @var string */ - public static $FORMAT_ITALIC = ""; - /** @var string */ - public static $FORMAT_UNDERLINE = ""; - /** @var string */ - public static $FORMAT_STRIKETHROUGH = ""; + public static string $FORMAT_BOLD = ""; + public static string $FORMAT_OBFUSCATED = ""; + public static string $FORMAT_ITALIC = ""; + public static string $FORMAT_UNDERLINE = ""; + public static string $FORMAT_STRIKETHROUGH = ""; - /** @var string */ - public static $FORMAT_RESET = ""; + public static string $FORMAT_RESET = ""; - /** @var string */ - public static $COLOR_BLACK = ""; - /** @var string */ - public static $COLOR_DARK_BLUE = ""; - /** @var string */ - public static $COLOR_DARK_GREEN = ""; - /** @var string */ - public static $COLOR_DARK_AQUA = ""; - /** @var string */ - public static $COLOR_DARK_RED = ""; - /** @var string */ - public static $COLOR_PURPLE = ""; - /** @var string */ - public static $COLOR_GOLD = ""; - /** @var string */ - public static $COLOR_GRAY = ""; - /** @var string */ - public static $COLOR_DARK_GRAY = ""; - /** @var string */ - public static $COLOR_BLUE = ""; - /** @var string */ - public static $COLOR_GREEN = ""; - /** @var string */ - public static $COLOR_AQUA = ""; - /** @var string */ - public static $COLOR_RED = ""; - /** @var string */ - public static $COLOR_LIGHT_PURPLE = ""; - /** @var string */ - public static $COLOR_YELLOW = ""; - /** @var string */ - public static $COLOR_WHITE = ""; + public static string $COLOR_BLACK = ""; + public static string $COLOR_DARK_BLUE = ""; + public static string $COLOR_DARK_GREEN = ""; + public static string $COLOR_DARK_AQUA = ""; + public static string $COLOR_DARK_RED = ""; + public static string $COLOR_PURPLE = ""; + public static string $COLOR_GOLD = ""; + public static string $COLOR_GRAY = ""; + public static string $COLOR_DARK_GRAY = ""; + public static string $COLOR_BLUE = ""; + public static string $COLOR_GREEN = ""; + public static string $COLOR_AQUA = ""; + public static string $COLOR_RED = ""; + public static string $COLOR_LIGHT_PURPLE = ""; + public static string $COLOR_YELLOW = ""; + public static string $COLOR_WHITE = ""; /** @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; } @@ -105,10 +83,7 @@ abstract class Terminal{ return $result; } - /** - * @return void - */ - protected static function getFallbackEscapeCodes(){ + protected static function getFallbackEscapeCodes() : void{ self::$FORMAT_BOLD = "\x1b[1m"; self::$FORMAT_OBFUSCATED = ""; self::$FORMAT_ITALIC = "\x1b[3m"; @@ -117,28 +92,27 @@ abstract class Terminal{ self::$FORMAT_RESET = "\x1b[m"; - self::$COLOR_BLACK = "\x1b[38;5;16m"; - self::$COLOR_DARK_BLUE = "\x1b[38;5;19m"; - self::$COLOR_DARK_GREEN = "\x1b[38;5;34m"; - self::$COLOR_DARK_AQUA = "\x1b[38;5;37m"; - self::$COLOR_DARK_RED = "\x1b[38;5;124m"; - self::$COLOR_PURPLE = "\x1b[38;5;127m"; - self::$COLOR_GOLD = "\x1b[38;5;214m"; - self::$COLOR_GRAY = "\x1b[38;5;145m"; - self::$COLOR_DARK_GRAY = "\x1b[38;5;59m"; - self::$COLOR_BLUE = "\x1b[38;5;63m"; - self::$COLOR_GREEN = "\x1b[38;5;83m"; - self::$COLOR_AQUA = "\x1b[38;5;87m"; - self::$COLOR_RED = "\x1b[38;5;203m"; - self::$COLOR_LIGHT_PURPLE = "\x1b[38;5;207m"; - self::$COLOR_YELLOW = "\x1b[38;5;227m"; - self::$COLOR_WHITE = "\x1b[38;5;231m"; + $color = fn(int $code) => "\x1b[38;5;${code}m"; + + self::$COLOR_BLACK = $color(16); + self::$COLOR_DARK_BLUE = $color(19); + self::$COLOR_DARK_GREEN = $color(34); + self::$COLOR_DARK_AQUA = $color(37); + self::$COLOR_DARK_RED = $color(124); + self::$COLOR_PURPLE = $color(127); + self::$COLOR_GOLD = $color(214); + self::$COLOR_GRAY = $color(145); + self::$COLOR_DARK_GRAY = $color(59); + self::$COLOR_BLUE = $color(63); + self::$COLOR_GREEN = $color(83); + self::$COLOR_AQUA = $color(87); + self::$COLOR_RED = $color(203); + self::$COLOR_LIGHT_PURPLE = $color(207); + self::$COLOR_YELLOW = $color(227); + self::$COLOR_WHITE = $color(231); } - /** - * @return void - */ - protected static function getEscapeCodes(){ + protected static function getEscapeCodes() : void{ $tput = fn(string $args) => is_string($result = shell_exec("tput $args")) ? $result : ""; $setaf = fn(int $code) => $tput("setaf $code"); @@ -209,89 +183,35 @@ abstract class Terminal{ /** * Returns a string with colorized ANSI Escape codes for the current terminal * Note that this is platform-dependent and might produce different results depending on the terminal type and/or OS. - * - * @param string|string[] $string */ - public static function toANSI($string) : string{ - if(!is_array($string)){ - $string = TextFormat::tokenize($string); - } - + public static function toANSI(string $string) : string{ $newString = ""; - foreach($string as $token){ - switch($token){ - case TextFormat::BOLD: - $newString .= Terminal::$FORMAT_BOLD; - break; - case TextFormat::OBFUSCATED: - $newString .= Terminal::$FORMAT_OBFUSCATED; - break; - case TextFormat::ITALIC: - $newString .= Terminal::$FORMAT_ITALIC; - break; - case TextFormat::UNDERLINE: - $newString .= Terminal::$FORMAT_UNDERLINE; - break; - case TextFormat::STRIKETHROUGH: - $newString .= Terminal::$FORMAT_STRIKETHROUGH; - break; - case TextFormat::RESET: - $newString .= Terminal::$FORMAT_RESET; - break; - - //Colors - case TextFormat::BLACK: - $newString .= Terminal::$COLOR_BLACK; - break; - case TextFormat::DARK_BLUE: - $newString .= Terminal::$COLOR_DARK_BLUE; - break; - case TextFormat::DARK_GREEN: - $newString .= Terminal::$COLOR_DARK_GREEN; - break; - case TextFormat::DARK_AQUA: - $newString .= Terminal::$COLOR_DARK_AQUA; - break; - case TextFormat::DARK_RED: - $newString .= Terminal::$COLOR_DARK_RED; - break; - case TextFormat::DARK_PURPLE: - $newString .= Terminal::$COLOR_PURPLE; - break; - case TextFormat::GOLD: - $newString .= Terminal::$COLOR_GOLD; - break; - case TextFormat::GRAY: - $newString .= Terminal::$COLOR_GRAY; - break; - case TextFormat::DARK_GRAY: - $newString .= Terminal::$COLOR_DARK_GRAY; - break; - case TextFormat::BLUE: - $newString .= Terminal::$COLOR_BLUE; - break; - case TextFormat::GREEN: - $newString .= Terminal::$COLOR_GREEN; - break; - case TextFormat::AQUA: - $newString .= Terminal::$COLOR_AQUA; - break; - case TextFormat::RED: - $newString .= Terminal::$COLOR_RED; - break; - case TextFormat::LIGHT_PURPLE: - $newString .= Terminal::$COLOR_LIGHT_PURPLE; - break; - case TextFormat::YELLOW: - $newString .= Terminal::$COLOR_YELLOW; - break; - case TextFormat::WHITE: - $newString .= Terminal::$COLOR_WHITE; - break; - default: - $newString .= $token; - break; - } + foreach(TextFormat::tokenize($string) as $token){ + $newString .= match($token){ + TextFormat::BOLD => Terminal::$FORMAT_BOLD, + TextFormat::OBFUSCATED => Terminal::$FORMAT_OBFUSCATED, + TextFormat::ITALIC => Terminal::$FORMAT_ITALIC, + TextFormat::UNDERLINE => Terminal::$FORMAT_UNDERLINE, + TextFormat::STRIKETHROUGH => Terminal::$FORMAT_STRIKETHROUGH, + TextFormat::RESET => Terminal::$FORMAT_RESET, + TextFormat::BLACK => Terminal::$COLOR_BLACK, + TextFormat::DARK_BLUE => Terminal::$COLOR_DARK_BLUE, + TextFormat::DARK_GREEN => Terminal::$COLOR_DARK_GREEN, + TextFormat::DARK_AQUA => Terminal::$COLOR_DARK_AQUA, + TextFormat::DARK_RED => Terminal::$COLOR_DARK_RED, + TextFormat::DARK_PURPLE => Terminal::$COLOR_PURPLE, + TextFormat::GOLD => Terminal::$COLOR_GOLD, + TextFormat::GRAY => Terminal::$COLOR_GRAY, + TextFormat::DARK_GRAY => Terminal::$COLOR_DARK_GRAY, + TextFormat::BLUE => Terminal::$COLOR_BLUE, + TextFormat::GREEN => Terminal::$COLOR_GREEN, + TextFormat::AQUA => Terminal::$COLOR_AQUA, + TextFormat::RED => Terminal::$COLOR_RED, + TextFormat::LIGHT_PURPLE => Terminal::$COLOR_LIGHT_PURPLE, + TextFormat::YELLOW => Terminal::$COLOR_YELLOW, + TextFormat::WHITE => Terminal::$COLOR_WHITE, + default => $token, + }; } return $newString; diff --git a/src/pocketmine/utils/TextFormat.php b/src/utils/TextFormat.php similarity index 61% rename from src/pocketmine/utils/TextFormat.php rename to src/utils/TextFormat.php index c848795821..66189078c2 100644 --- a/src/pocketmine/utils/TextFormat.php +++ b/src/utils/TextFormat.php @@ -23,9 +23,6 @@ declare(strict_types=1); namespace pocketmine\utils; -use function is_array; -use function json_encode; -use function json_last_error_msg; use function mb_scrub; use function preg_last_error; use function preg_quote; @@ -33,7 +30,6 @@ use function preg_replace; use function preg_split; use function str_repeat; use function str_replace; -use const JSON_UNESCAPED_SLASHES; use const PREG_BACKTRACK_LIMIT_ERROR; use const PREG_BAD_UTF8_ERROR; use const PREG_BAD_UTF8_OFFSET_ERROR; @@ -67,11 +63,39 @@ abstract class TextFormat{ public const YELLOW = TextFormat::ESCAPE . "e"; public const WHITE = TextFormat::ESCAPE . "f"; + public const COLORS = [ + self::BLACK => self::BLACK, + self::DARK_BLUE => self::DARK_BLUE, + self::DARK_GREEN => self::DARK_GREEN, + self::DARK_AQUA => self::DARK_AQUA, + self::DARK_RED => self::DARK_RED, + self::DARK_PURPLE => self::DARK_PURPLE, + self::GOLD => self::GOLD, + self::GRAY => self::GRAY, + self::DARK_GRAY => self::DARK_GRAY, + self::BLUE => self::BLUE, + self::GREEN => self::GREEN, + self::AQUA => self::AQUA, + self::RED => self::RED, + self::LIGHT_PURPLE => self::LIGHT_PURPLE, + self::YELLOW => self::YELLOW, + self::WHITE => self::WHITE, + ]; + public const OBFUSCATED = TextFormat::ESCAPE . "k"; public const BOLD = TextFormat::ESCAPE . "l"; public const STRIKETHROUGH = TextFormat::ESCAPE . "m"; public const UNDERLINE = TextFormat::ESCAPE . "n"; public const ITALIC = TextFormat::ESCAPE . "o"; + + public const FORMATS = [ + self::OBFUSCATED => self::OBFUSCATED, + self::BOLD => self::BOLD, + self::STRIKETHROUGH => self::STRIKETHROUGH, + self::UNDERLINE => self::UNDERLINE, + self::ITALIC => self::ITALIC, + ]; + public const RESET = TextFormat::ESCAPE . "r"; private static function makePcreError() : \InvalidArgumentException{ @@ -132,207 +156,13 @@ abstract class TextFormat{ return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-fk-or])/u', TextFormat::ESCAPE . '$1', $string); } - /** - * Returns an JSON-formatted string with colors/markup - * - * @param string|string[] $string - */ - public static function toJSON($string) : string{ - if(!is_array($string)){ - $string = self::tokenize($string); - } - $newString = new TextFormatJsonObject(); - $pointer = $newString; - $color = "white"; - $bold = false; - $italic = false; - $underlined = false; - $strikethrough = false; - $obfuscated = false; - $index = 0; - - foreach($string as $token){ - if($pointer->text !== null){ - if($newString->extra === null){ - $newString->extra = []; - } - $newString->extra[$index] = $pointer = new TextFormatJsonObject(); - if($color !== "white"){ - $pointer->color = $color; - } - if($bold){ - $pointer->bold = true; - } - if($italic){ - $pointer->italic = true; - } - if($underlined){ - $pointer->underlined = true; - } - if($strikethrough){ - $pointer->strikethrough = true; - } - if($obfuscated){ - $pointer->obfuscated = true; - } - ++$index; - } - switch($token){ - case TextFormat::BOLD: - if(!$bold){ - $pointer->bold = true; - $bold = true; - } - break; - case TextFormat::OBFUSCATED: - if(!$obfuscated){ - $pointer->obfuscated = true; - $obfuscated = true; - } - break; - case TextFormat::ITALIC: - if(!$italic){ - $pointer->italic = true; - $italic = true; - } - break; - case TextFormat::UNDERLINE: - if(!$underlined){ - $pointer->underlined = true; - $underlined = true; - } - break; - case TextFormat::STRIKETHROUGH: - if(!$strikethrough){ - $pointer->strikethrough = true; - $strikethrough = true; - } - break; - case TextFormat::RESET: - if($color !== "white"){ - $pointer->color = "white"; - $color = "white"; - } - if($bold){ - $pointer->bold = false; - $bold = false; - } - if($italic){ - $pointer->italic = false; - $italic = false; - } - if($underlined){ - $pointer->underlined = false; - $underlined = false; - } - if($strikethrough){ - $pointer->strikethrough = false; - $strikethrough = false; - } - if($obfuscated){ - $pointer->obfuscated = false; - $obfuscated = false; - } - break; - - //Colors - case TextFormat::BLACK: - $pointer->color = "black"; - $color = "black"; - break; - case TextFormat::DARK_BLUE: - $pointer->color = "dark_blue"; - $color = "dark_blue"; - break; - case TextFormat::DARK_GREEN: - $pointer->color = "dark_green"; - $color = "dark_green"; - break; - case TextFormat::DARK_AQUA: - $pointer->color = "dark_aqua"; - $color = "dark_aqua"; - break; - case TextFormat::DARK_RED: - $pointer->color = "dark_red"; - $color = "dark_red"; - break; - case TextFormat::DARK_PURPLE: - $pointer->color = "dark_purple"; - $color = "dark_purple"; - break; - case TextFormat::GOLD: - $pointer->color = "gold"; - $color = "gold"; - break; - case TextFormat::GRAY: - $pointer->color = "gray"; - $color = "gray"; - break; - case TextFormat::DARK_GRAY: - $pointer->color = "dark_gray"; - $color = "dark_gray"; - break; - case TextFormat::BLUE: - $pointer->color = "blue"; - $color = "blue"; - break; - case TextFormat::GREEN: - $pointer->color = "green"; - $color = "green"; - break; - case TextFormat::AQUA: - $pointer->color = "aqua"; - $color = "aqua"; - break; - case TextFormat::RED: - $pointer->color = "red"; - $color = "red"; - break; - case TextFormat::LIGHT_PURPLE: - $pointer->color = "light_purple"; - $color = "light_purple"; - break; - case TextFormat::YELLOW: - $pointer->color = "yellow"; - $color = "yellow"; - break; - case TextFormat::WHITE: - $pointer->color = "white"; - $color = "white"; - break; - default: - $pointer->text = $token; - break; - } - } - - if($newString->extra !== null){ - foreach($newString->extra as $k => $d){ - if($d->text === null){ - unset($newString->extra[$k]); - } - } - } - - $result = json_encode($newString, JSON_UNESCAPED_SLASHES); - if($result === false){ - throw new \InvalidArgumentException("Failed to encode result JSON: " . json_last_error_msg()); - } - return $result; - } - /** * Returns an HTML-formatted string with colors/markup - * - * @param string|string[] $string */ - public static function toHTML($string) : string{ - if(!is_array($string)){ - $string = self::tokenize($string); - } + public static function toHTML(string $string) : string{ $newString = ""; $tokens = 0; - foreach($string as $token){ + foreach(self::tokenize($string) as $token){ switch($token){ case TextFormat::BOLD: $newString .= ""; diff --git a/src/pocketmine/utils/Timezone.php b/src/utils/Timezone.php similarity index 88% rename from src/pocketmine/utils/Timezone.php rename to src/utils/Timezone.php index 54fe0049c4..fdd39c865f 100644 --- a/src/pocketmine/utils/Timezone.php +++ b/src/utils/Timezone.php @@ -54,12 +54,8 @@ abstract class Timezone{ return $tz; } - /** - * @return string[] - */ - public static function init() : array{ - $messages = []; - $timezone = self::get(); + public static function init() : void{ + $timezone = ini_get("date.timezone"); if($timezone !== ""){ /* * This is here so that people don't come to us complaining and fill up the issue tracker when they put @@ -70,13 +66,14 @@ abstract class Timezone{ if($default_timezone !== false){ ini_set("date.timezone", $default_timezone); date_default_timezone_set($default_timezone); - return $messages; + return; } + //Bad php.ini value, try another method to detect timezone - $messages[] = "Timezone \"$timezone\" could not be parsed as a valid timezone from php.ini, falling back to auto-detection"; + \GlobalLogger::get()->warning("Timezone \"$timezone\" could not be parsed as a valid timezone from php.ini, falling back to auto-detection"); }else{ date_default_timezone_set($timezone); - return $messages; + return; } } @@ -84,11 +81,11 @@ abstract class 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); - return $messages; + return; } - if(($response = Internet::getURL("http://ip-api.com/json")) !== false //If system timezone detection fails or timezone is an invalid value. - and is_array($ip_geolocation_data = json_decode($response, true)) + 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 isset($ip_geolocation_data['status']) and $ip_geolocation_data['status'] !== 'fail' and is_string($ip_geolocation_data['timezone']) @@ -96,14 +93,12 @@ abstract class Timezone{ ){ //Again, for redundancy. ini_set("date.timezone", $ip_geolocation_data['timezone']); - return $messages; + return; } ini_set("date.timezone", "UTC"); date_default_timezone_set("UTC"); - $messages[] = "Timezone could not be automatically determined or was set to an invalid value. An incorrect timezone will result in incorrect timestamps on console logs. It has been set to \"UTC\" by default. You can change it on the php.ini file."; - - return $messages; + \GlobalLogger::get()->warning("Timezone could not be automatically determined or was set to an invalid value. An incorrect timezone will result in incorrect timestamps on console logs. It has been set to \"UTC\" by default. You can change it on the php.ini file."); } /** diff --git a/src/pocketmine/utils/Utils.php b/src/utils/Utils.php similarity index 57% rename from src/pocketmine/utils/Utils.php rename to src/utils/Utils.php index 90a58aca76..d761f772f0 100644 --- a/src/pocketmine/utils/Utils.php +++ b/src/utils/Utils.php @@ -28,35 +28,39 @@ 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; use function array_map; use function array_reverse; use function array_values; -use function base64_decode; use function bin2hex; use function chunk_split; +use function class_exists; use function count; use function debug_zval_dump; use function dechex; -use function error_reporting; use function exec; use function explode; 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_dir; -use function is_file; +use function is_bool; +use function is_int; use function is_object; use function is_string; -use function json_decode; -use function ltrim; +use function mb_check_encoding; use function ob_end_clean; use function ob_get_contents; use function ob_start; @@ -67,38 +71,28 @@ use function preg_grep; use function preg_match; use function preg_match_all; use function preg_replace; -use function rmdir; -use function rtrim; -use function scandir; -use function sha1; use function shell_exec; -use function spl_object_hash; +use function spl_object_id; use function str_pad; -use function str_replace; use function str_split; use function stripos; use function strlen; use function strpos; -use function strtolower; -use function strtr; use function substr; use function sys_get_temp_dir; use function trim; -use function unlink; use function xdebug_get_function_stack; -use const DIRECTORY_SEPARATOR; use const PHP_EOL; use const PHP_INT_MAX; use const PHP_INT_SIZE; use const PHP_MAXPATHLEN; -use const SCANDIR_SORT_NONE; use const STR_PAD_LEFT; use const STR_PAD_RIGHT; /** * Big collection of functions */ -class Utils{ +final class Utils{ public const OS_WINDOWS = "win"; public const OS_IOS = "ios"; public const OS_MACOS = "mac"; @@ -107,31 +101,11 @@ class Utils{ public const OS_BSD = "bsd"; public const OS_UNKNOWN = "other"; - public const CLEAN_PATH_SRC_PREFIX = "pmsrc"; - public const CLEAN_PATH_PLUGINS_PREFIX = "plugins"; - /** @var string|null */ - public static $os; - /** @var UUID|null */ + private static $os; + /** @var UuidInterface|null */ private static $serverUniqueId = null; - /** - * Generates an unique identifier to a callable - * - * @phpstan-param anyCallable $variable - * - * @return string - */ - public static function getCallableIdentifier(callable $variable){ - if(is_array($variable)){ - return sha1(strtolower(spl_object_hash($variable[0])) . "::" . strtolower($variable[1])); - }elseif(is_string($variable)){ - return sha1(strtolower($variable)); - }else{ - throw new AssumptionFailedError("Unhandled callable type"); - } - } - /** * Returns a readable identifier for the given Closure, including file and line. * @@ -158,7 +132,7 @@ class Utils{ $filename = $func->getFileName(); return "closure@" . ($filename !== false ? - self::cleanPath($filename) . "#L" . $func->getStartLine() : + Filesystem::cleanPath($filename) . "#L" . $func->getStartLine() : "internal" ); } @@ -174,7 +148,7 @@ class Utils{ $filename = $reflect->getFileName(); return "anonymous@" . ($filename !== false ? - self::cleanPath($filename) . "#L" . $reflect->getStartLine() : + Filesystem::cleanPath($filename) . "#L" . $reflect->getStartLine() : "internal" ); } @@ -182,6 +156,30 @@ class Utils{ return $reflect->getName(); } + /** + * @phpstan-return \Closure(object) : object + */ + public static function cloneCallback() : \Closure{ + return static function(object $o){ + return clone $o; + }; + } + + /** + * @phpstan-template T of object + * + * @param object[] $array + * @phpstan-param T[] $array + * + * @return object[] + * @phpstan-return T[] + */ + public static function cloneObjectArray(array $array) : array{ + /** @phpstan-var \Closure(T) : T $callback */ + $callback = self::cloneCallback(); + return array_map($callback, $array); + } + /** * Gets this machine / server instance unique ID * Returns a hash, the first 32 characters (or 16 if raw) @@ -190,7 +188,7 @@ class Utils{ * * @param string $extra optional, additional data to identify the machine */ - public static function getMachineUniqueId(string $extra = "") : UUID{ + public static function getMachineUniqueId(string $extra = "") : UuidInterface{ if(self::$serverUniqueId !== null and $extra === ""){ return self::$serverUniqueId; } @@ -246,7 +244,8 @@ class Utils{ $data .= $ext . ":" . phpversion($ext); } - $uuid = UUID::fromData($machine, $data); + //TODO: use of NIL as namespace is a hack; it works for now, but we should have a proper namespace UUID + $uuid = Uuid::uuid3(Uuid::NIL, $data); if($extra === ""){ self::$serverUniqueId = $uuid; @@ -255,18 +254,6 @@ class Utils{ return $uuid; } - /** - * @deprecated - * @see Internet::getIP() - * - * @param bool $force default false, force IP check even when cached - * - * @return string|bool - */ - public static function getIP(bool $force = false){ - return Internet::getIP($force); - } - /** * Returns the current Operating System * Windows => win @@ -304,35 +291,6 @@ class Utils{ return self::$os; } - /** - * @deprecated - * @see Process::getRealMemoryUsage() - * - * @return int[] - */ - public static function getRealMemoryUsage() : array{ - return Process::getRealMemoryUsage(); - } - - /** - * @deprecated - * @see Process::getMemoryUsage() - * @see Process::getAdvancedMemoryUsage() - * - * @return int[]|int - */ - public static function getMemoryUsage(bool $advanced = false){ - return $advanced ? Process::getAdvancedMemoryUsage() : Process::getMemoryUsage(); - } - - /** - * @deprecated - * @see Process::getThreadCount() - */ - public static function getThreadCount() : int{ - return Process::getThreadCount(); - } - public static function getCoreCount(bool $recalculate = false) : int{ static $processors = 0; @@ -396,76 +354,6 @@ class Utils{ return preg_replace('#([^\x20-\x7E])#', '.', $str); } - /* - public static function angle3D($pos1, $pos2){ - $X = $pos1["x"] - $pos2["x"]; - $Z = $pos1["z"] - $pos2["z"]; - $dXZ = sqrt(pow($X, 2) + pow($Z, 2)); - $Y = $pos1["y"] - $pos2["y"]; - $hAngle = rad2deg(atan2($Z, $X) - M_PI_2); - $vAngle = rad2deg(-atan2($Y, $dXZ)); - - return array("yaw" => $hAngle, "pitch" => $vAngle); - }*/ - - /** - * @deprecated - * @see Internet::getURL() - * - * @param int $timeout default 10 - * @param string[] $extraHeaders - * @param string $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. - * @param string[] $headers reference parameter - * @param int $httpCode reference parameter - * @phpstan-param list $extraHeaders - * @phpstan-param array $headers - * - * @return string|false - */ - public static function getURL(string $page, int $timeout = 10, array $extraHeaders = [], &$err = null, &$headers = null, &$httpCode = null){ - return Internet::getURL($page, $timeout, $extraHeaders, $err, $headers, $httpCode); - } - - /** - * @deprecated - * @see Internet::postURL() - * - * @param string[]|string $args - * @param string[] $extraHeaders - * @param string $err reference parameter, will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. - * @param string[] $headers reference parameter - * @param int $httpCode reference parameter - * @phpstan-param string|array $args - * @phpstan-param list $extraHeaders - * @phpstan-param array $headers - * - * @return string|false - */ - public static function postURL(string $page, $args, int $timeout = 10, array $extraHeaders = [], &$err = null, &$headers = null, &$httpCode = null){ - return Internet::postURL($page, $args, $timeout, $extraHeaders, $err, $headers, $httpCode); - } - - /** - * @deprecated - * @see Internet::simpleCurl() - * - * @param float|int $timeout The maximum connect timeout and timeout in seconds, correct to ms. - * @param string[] $extraHeaders extra headers to send as a plain string array - * @param array $extraOpts extra CURLOPT_* to set as an [opt => value] map - * @param callable|null $onSuccess function to be called if there is no error. Accepts a resource argument as the cURL handle. - * @phpstan-param array $extraOpts - * @phpstan-param list $extraHeaders - * @phpstan-param (callable(\CurlHandle) : void)|null $onSuccess - * - * @return array a plain array of three [result body : string, headers : string[][], HTTP response code : int]. Headers are grouped by requests with strtolower(header name) as keys and header value as values - * @phpstan-return array{string, list>, int} - * - * @throws \RuntimeException if a cURL error occurs - */ - public static function simpleCurl(string $page, $timeout = 10, array $extraHeaders = [], array $extraOpts = [], callable $onSuccess = null){ - return Internet::simpleCurl($page, $timeout, $extraHeaders, $extraOpts, $onSuccess); - } - public static function javaStringHash(string $string) : int{ $hash = 0; for($i = 0, $len = strlen($string); $i < $len; $i++){ @@ -485,55 +373,10 @@ class Utils{ return $hash; } - /** - * @deprecated - * @see Process::execute() - * - * @param string $command Command to execute - * @param string|null $stdout Reference parameter to write stdout to - * @param string|null $stderr Reference parameter to write stderr to - * - * @return int process exit code - */ - public static function execute(string $command, string &$stdout = null, string &$stderr = null) : int{ - return Process::execute($command, $stdout, $stderr); - } - - /** - * @return mixed[] - * @phpstan-return array - */ - public static function decodeJWT(string $token) : array{ - [$headB64, $payloadB64, $sigB64] = explode(".", $token); - - $rawPayloadJSON = base64_decode(strtr($payloadB64, '-_', '+/'), true); - if($rawPayloadJSON === false){ - throw new \InvalidArgumentException("Payload base64 is invalid and cannot be decoded"); - } - $decodedPayload = json_decode($rawPayloadJSON, true); - if(!is_array($decodedPayload)){ - throw new \InvalidArgumentException("Decoded payload should be array, " . gettype($decodedPayload) . " received"); - } - - return $decodedPayload; - } - - /** - * @deprecated - * @see Process::kill() - * - * @param int $pid - */ - public static function kill($pid) : void{ - Process::kill($pid); - } - /** * @param object $value - * - * @return int */ - public static function getReferenceCount($value, bool $includeCurrent = true){ + public static function getReferenceCount($value, bool $includeCurrent = true) : int{ ob_start(); debug_zval_dump($value); $contents = ob_get_contents(); @@ -547,6 +390,49 @@ 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 @@ -566,7 +452,7 @@ class Utils{ $params = implode(", ", array_map(function($value) use($maxStringLength) : string{ if(is_object($value)){ - return "object " . self::getNiceClassName($value); + return "object " . self::getNiceClassName($value) . "#" . spl_object_id($value); } if(is_array($value)){ return "array[" . count($value) . "]"; @@ -574,10 +460,13 @@ class Utils{ if(is_string($value)){ return "string[" . strlen($value) . "] " . substr(Utils::printable($value), 0, $maxStringLength); } + if(is_bool($value)){ + return $value ? "true" : "false"; + } return gettype($value) . " " . Utils::printable((string) $value); }, $args)); } - $messages[] = "#$i " . (isset($trace[$i]["file"]) ? self::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" or $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")"; } return $messages; } @@ -607,29 +496,6 @@ class Utils{ return self::printableTrace(self::currentTrace(++$skipFrames)); } - /** - * @param string $path - * - * @return string - */ - public static function cleanPath($path){ - $result = str_replace([DIRECTORY_SEPARATOR, ".php", "phar://"], ["/", "", ""], $path); - - //remove relative paths - //TODO: make these paths dynamic so they can be unit-tested against - static $cleanPaths = [ - \pocketmine\PLUGIN_PATH => self::CLEAN_PATH_PLUGINS_PREFIX, //this has to come BEFORE \pocketmine\PATH because it's inside that by default on src installations - \pocketmine\PATH => self::CLEAN_PATH_SRC_PREFIX - ]; - foreach($cleanPaths as $cleanPath => $replacement){ - $cleanPath = rtrim(str_replace([DIRECTORY_SEPARATOR, "phar://"], ["/", ""], $cleanPath), "/"); - if(strpos($result, $cleanPath) === 0){ - $result = ltrim(str_replace($cleanPath, $replacement, $result), "/"); - } - } - return $result; - } - /** * Extracts one-line tags from the doc-comment * @@ -640,21 +506,9 @@ class Utils{ 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); - $result = array_combine($matches[1], $matches[2]); - return $result; - } - - /** - * @throws \ErrorException - */ - public static function errorExceptionHandler(int $severity, string $message, string $file, int $line) : bool{ - if((error_reporting() & $severity) !== 0){ - throw new \ErrorException($message, 0, $severity, $file, $line); - } - - return true; //stfu operator + return array_combine($matches[1], $matches[2]); } /** @@ -662,21 +516,20 @@ class Utils{ * @phpstan-param class-string $baseName */ public static function testValidInstance(string $className, string $baseName) : void{ - try{ - $base = new \ReflectionClass($baseName); - }catch(\ReflectionException $e){ - throw new \InvalidArgumentException("Base class $baseName does not exist"); + $baseInterface = false; + if(!class_exists($baseName)){ + if(!interface_exists($baseName)){ + throw new \InvalidArgumentException("Base class $baseName does not exist"); + } + $baseInterface = true; } - - try{ - $class = new \ReflectionClass($className); - }catch(\ReflectionException $e){ - throw new \InvalidArgumentException("Class $className does not exist"); + if(!class_exists($className)){ + throw new \InvalidArgumentException("Class $className does not exist or is not a class"); } - - if(!$class->isSubclassOf($baseName)){ - throw new \InvalidArgumentException("Class $className does not " . ($base->isInterface() ? "implement" : "extend") . " " . $baseName); + if(!is_a($className, $baseName, true)){ + throw new \InvalidArgumentException("Class $className does not " . ($baseInterface ? "implement" : "extend") . " $baseName"); } + $class = new \ReflectionClass($className); if(!$class->isInstantiable()){ throw new \InvalidArgumentException("Class $className cannot be constructed"); } @@ -686,17 +539,20 @@ 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 . "`"); } } @@ -715,22 +571,25 @@ class Utils{ } } - 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"); - foreach($objects as $object){ - if($object !== "." and $object !== ".."){ - if(is_dir($dir . "/" . $object)){ - self::recursiveUnlink($dir . "/" . $object); - }else{ - unlink($dir . "/" . $object); - } - } - } - rmdir($dir); - }elseif(is_file($dir)){ - unlink($dir); + /** + * 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"); } } } diff --git a/src/pocketmine/utils/VersionString.php b/src/utils/VersionString.php similarity index 81% rename from src/pocketmine/utils/VersionString.php rename to src/utils/VersionString.php index 59a4deddad..7dae029b54 100644 --- a/src/pocketmine/utils/VersionString.php +++ b/src/utils/VersionString.php @@ -63,6 +63,10 @@ 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); } @@ -117,18 +121,16 @@ class VersionString{ if($diff){ return $tNumber - $number; } - if($number > $tNumber){ - return -1; //Target is older - }elseif($number < $tNumber){ - return 1; //Target is newer - }elseif($target->isDev() and !$this->isDev()){ - return -1; //Dev builds of the same version are always considered older than a release - }elseif($target->getBuild() > $this->getBuild()){ - return 1; - }elseif($target->getBuild() < $this->getBuild()){ - return -1; - }else{ - return 0; //Same version + + if(($result = $tNumber <=> $number) !== 0){ + return $result; } + if($target->isDev() !== $this->isDev()){ + return $this->isDev() ? 1 : -1; //Dev builds of the same version are always considered older than a release + } + if(($target->getSuffix() === "") !== ($this->suffix === "")){ + return $this->suffix !== "" ? 1 : -1; //alpha/beta/whatever releases are always considered older than a non-suffixed version + } + return $target->getBuild() <=> $this->getBuild(); } } diff --git a/src/pocketmine/wizard/SetupWizard.php b/src/wizard/SetupWizard.php similarity index 51% rename from src/pocketmine/wizard/SetupWizard.php rename to src/wizard/SetupWizard.php index 782005bc29..6dfb3b6909 100644 --- a/src/pocketmine/wizard/SetupWizard.php +++ b/src/wizard/SetupWizard.php @@ -27,46 +27,50 @@ declare(strict_types=1); */ namespace pocketmine\wizard; -use pocketmine\lang\BaseLang; -use pocketmine\Player; +use pocketmine\data\java\GameModeIdMap; +use pocketmine\lang\KnownTranslationFactory; +use pocketmine\lang\Language; +use pocketmine\lang\LanguageNotFoundException; +use pocketmine\player\GameMode; use pocketmine\utils\Config; use pocketmine\utils\Internet; use pocketmine\utils\InternetException; -use function base64_encode; -use function count; +use pocketmine\utils\Utils; +use pocketmine\VersionInfo; +use Webmozart\PathUtil\Path; use function fgets; -use function random_bytes; use function sleep; use function strtolower; -use function substr; use function trim; use const PHP_EOL; use const STDIN; class SetupWizard{ - public const DEFAULT_NAME = \pocketmine\NAME . " Server"; + public const DEFAULT_NAME = VersionInfo::NAME . " Server"; public const DEFAULT_PORT = 19132; public const DEFAULT_PLAYERS = 20; - public const DEFAULT_GAMEMODE = Player::SURVIVAL; - /** @var BaseLang */ + /** @var Language */ private $lang; + /** @var string */ + private $dataPath; - public function __construct(){ - + public function __construct(string $dataPath){ + $this->dataPath = $dataPath; } public function run() : bool{ - $this->message(\pocketmine\NAME . " set-up wizard"); + $this->message(VersionInfo::NAME . " set-up wizard"); - $langs = BaseLang::getLanguageList(); - if(count($langs) === 0){ + try{ + $langs = Language::getLanguageList(); + }catch(LanguageNotFoundException $e){ $this->error("No language files found, please use provided builds or clone the repository recursively."); return false; } $this->message("Please select a language"); - foreach($langs as $short => $native){ + foreach(Utils::stringifyKeys($langs) as $short => $native){ $this->writeLine(" $native => $short"); } @@ -78,20 +82,20 @@ class SetupWizard{ } }while($lang === null); - $this->lang = new BaseLang($lang); + $this->lang = new Language($lang); - $this->message($this->lang->get("language_has_been_selected")); + $this->message($this->lang->translate(KnownTranslationFactory::language_has_been_selected())); if(!$this->showLicense()){ return false; } //this has to happen here to prevent user avoiding agreeing to license - $config = new Config(\pocketmine\DATA . "server.properties", Config::PROPERTIES); + $config = new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES); $config->set("language", $lang); $config->save(); - if(strtolower($this->getInput($this->lang->get("skip_installer"), "n", "y/N")) === "y"){ + if(strtolower($this->getInput($this->lang->translate(KnownTranslationFactory::skip_installer()), "n", "y/N")) === "y"){ $this->printIpDetails(); return true; } @@ -110,7 +114,7 @@ class SetupWizard{ } private function showLicense() : bool{ - $this->message($this->lang->translateString("welcome_to_pocketmine", [\pocketmine\NAME])); + $this->message($this->lang->translate(KnownTranslationFactory::welcome_to_pocketmine(VersionInfo::NAME))); echo <<writeLine(); - if(strtolower($this->getInput($this->lang->get("accept_license"), "n", "y/N")) !== "y"){ - $this->error($this->lang->translateString("you_have_to_accept_the_license", [\pocketmine\NAME])); + if(strtolower($this->getInput($this->lang->translate(KnownTranslationFactory::accept_license()), "n", "y/N")) !== "y"){ + $this->error($this->lang->translate(KnownTranslationFactory::you_have_to_accept_the_license(VersionInfo::NAME))); sleep(5); return false; @@ -131,23 +135,23 @@ LICENSE; } private function welcome() : void{ - $this->message($this->lang->get("setting_up_server_now")); - $this->message($this->lang->get("default_values_info")); - $this->message($this->lang->get("server_properties")); + $this->message($this->lang->translate(KnownTranslationFactory::setting_up_server_now())); + $this->message($this->lang->translate(KnownTranslationFactory::default_values_info())); + $this->message($this->lang->translate(KnownTranslationFactory::server_properties())); } private function generateBaseConfig() : void{ - $config = new Config(\pocketmine\DATA . "server.properties", Config::PROPERTIES); + $config = new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES); - $config->set("motd", ($name = $this->getInput($this->lang->get("name_your_server"), self::DEFAULT_NAME))); + $config->set("motd", ($name = $this->getInput($this->lang->translate(KnownTranslationFactory::name_your_server()), self::DEFAULT_NAME))); $config->set("server-name", $name); - $this->message($this->lang->get("port_warning")); + $this->message($this->lang->translate(KnownTranslationFactory::port_warning())); do{ - $port = (int) $this->getInput($this->lang->get("server_port"), (string) self::DEFAULT_PORT); + $port = (int) $this->getInput($this->lang->translate(KnownTranslationFactory::server_port()), (string) self::DEFAULT_PORT); if($port <= 0 or $port > 65535){ - $this->error($this->lang->get("invalid_port")); + $this->error($this->lang->translate(KnownTranslationFactory::invalid_port())); continue; } @@ -155,43 +159,35 @@ LICENSE; }while(true); $config->set("server-port", $port); - $this->message($this->lang->get("gamemode_info")); + $this->message($this->lang->translate(KnownTranslationFactory::gamemode_info())); do{ - $gamemode = (int) $this->getInput($this->lang->get("default_gamemode"), (string) self::DEFAULT_GAMEMODE); + $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); - $config->set("max-players", (int) $this->getInput($this->lang->get("max_players"), (string) self::DEFAULT_PLAYERS)); - - $this->message($this->lang->get("spawn_protection_info")); - - if(strtolower($this->getInput($this->lang->get("spawn_protection"), "n", "y/N")) === "n"){ - $config->set("spawn-protection", -1); - }else{ - $config->set("spawn-protection", 16); - } + $config->set("max-players", (int) $this->getInput($this->lang->translate(KnownTranslationFactory::max_players()), (string) self::DEFAULT_PLAYERS)); $config->save(); } private function generateUserFiles() : void{ - $this->message($this->lang->get("op_info")); + $this->message($this->lang->translate(KnownTranslationFactory::op_info())); - $op = strtolower($this->getInput($this->lang->get("op_who"), "")); + $op = strtolower($this->getInput($this->lang->translate(KnownTranslationFactory::op_who()), "")); if($op === ""){ - $this->error($this->lang->get("op_warning")); + $this->error($this->lang->translate(KnownTranslationFactory::op_warning())); }else{ - $ops = new Config(\pocketmine\DATA . "ops.txt", Config::ENUM); + $ops = new Config(Path::join($this->dataPath, "ops.txt"), Config::ENUM); $ops->set($op, true); $ops->save(); } - $this->message($this->lang->get("whitelist_info")); + $this->message($this->lang->translate(KnownTranslationFactory::whitelist_info())); - $config = new Config(\pocketmine\DATA . "server.properties", Config::PROPERTIES); - if(strtolower($this->getInput($this->lang->get("whitelist_enable"), "n", "y/N")) === "y"){ - $this->error($this->lang->get("whitelist_warning")); + $config = new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES); + if(strtolower($this->getInput($this->lang->translate(KnownTranslationFactory::whitelist_enable()), "n", "y/N")) === "y"){ + $this->error($this->lang->translate(KnownTranslationFactory::whitelist_warning())); $config->set("white-list", true); }else{ $config->set("white-list", false); @@ -200,30 +196,20 @@ LICENSE; } private function networkFunctions() : void{ - $config = new Config(\pocketmine\DATA . "server.properties", Config::PROPERTIES); - $this->error($this->lang->get("query_warning1")); - $this->error($this->lang->get("query_warning2")); - if(strtolower($this->getInput($this->lang->get("query_disable"), "n", "y/N")) === "y"){ + $config = new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES); + $this->error($this->lang->translate(KnownTranslationFactory::query_warning1())); + $this->error($this->lang->translate(KnownTranslationFactory::query_warning2())); + if(strtolower($this->getInput($this->lang->translate(KnownTranslationFactory::query_disable()), "n", "y/N")) === "y"){ $config->set("enable-query", false); }else{ $config->set("enable-query", true); } - $this->message($this->lang->get("rcon_info")); - if(strtolower($this->getInput($this->lang->get("rcon_enable"), "n", "y/N")) === "y"){ - $config->set("enable-rcon", true); - $password = substr(base64_encode(random_bytes(20)), 3, 10); - $config->set("rcon.password", $password); - $this->message($this->lang->get("rcon_password") . ": " . $password); - }else{ - $config->set("enable-rcon", false); - } - $config->save(); } private function printIpDetails() : void{ - $this->message($this->lang->get("ip_get")); + $this->message($this->lang->translate(KnownTranslationFactory::ip_get())); $externalIP = Internet::getIP(); if($externalIP === false){ @@ -235,15 +221,15 @@ LICENSE; $internalIP = "unknown (" . $e->getMessage() . ")"; } - $this->error($this->lang->translateString("ip_warning", ["EXTERNAL_IP" => $externalIP, "INTERNAL_IP" => $internalIP])); - $this->error($this->lang->get("ip_confirm")); + $this->error($this->lang->translate(KnownTranslationFactory::ip_warning($externalIP, $internalIP))); + $this->error($this->lang->translate(KnownTranslationFactory::ip_confirm())); $this->readLine(); } private function endWizard() : void{ - $this->message($this->lang->get("you_have_finished")); - $this->message($this->lang->get("pocketmine_plugins")); - $this->message($this->lang->translateString("pocketmine_will_start", [\pocketmine\NAME])); + $this->message($this->lang->translate(KnownTranslationFactory::you_have_finished())); + $this->message($this->lang->translate(KnownTranslationFactory::pocketmine_plugins())); + $this->message($this->lang->translate(KnownTranslationFactory::pocketmine_will_start(VersionInfo::NAME))); $this->writeLine(); $this->writeLine(); diff --git a/src/world/BlockTransaction.php b/src/world/BlockTransaction.php new file mode 100644 index 0000000000..80bf8445fb --- /dev/null +++ b/src/world/BlockTransaction.php @@ -0,0 +1,144 @@ +world = $world; + $this->addValidator(static function(ChunkManager $world, int $x, int $y, int $z) : bool{ + return $world->isInWorld($x, $y, $z); + }); + } + + /** + * Adds a block to the transaction at the given position. + * + * @return $this + */ + public function addBlock(Vector3 $pos, Block $state) : self{ + return $this->addBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), $state); + } + + /** + * Adds a block to the batch at the given coordinates. + * + * @return $this + */ + public function addBlockAt(int $x, int $y, int $z, Block $state) : self{ + $this->blocks[$x][$y][$z] = $state; + return $this; + } + + /** + * Reads a block from the given world, masked by the blocks in this transaction. This can be useful if you want to + * add blocks to the transaction that depend on previous blocks should they exist. + */ + public function fetchBlock(Vector3 $pos) : Block{ + return $this->fetchBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()); + } + + /** + * @see BlockTransaction::fetchBlock() + */ + public function fetchBlockAt(int $x, int $y, int $z) : Block{ + return $this->blocks[$x][$y][$z] ?? $this->world->getBlockAt($x, $y, $z); + } + + /** + * Validates and attempts to apply the transaction to the given world. If any part of the transaction fails to + * validate, no changes will be made to the world. + * + * @return bool if the application was successful + */ + public function apply() : bool{ + foreach($this->getBlocks() as [$x, $y, $z, $_]){ + foreach($this->validators as $validator){ + if(!$validator($this->world, $x, $y, $z)){ + return false; + } + } + } + $changedBlocks = 0; + foreach($this->getBlocks() as [$x, $y, $z, $block]){ + $oldBlock = $this->world->getBlockAt($x, $y, $z); + if(!$oldBlock->isSameState($block)){ + $this->world->setBlockAt($x, $y, $z, $block); + $changedBlocks++; + } + } + return $changedBlocks !== 0; + } + + /** + * @return \Generator|mixed[] [int $x, int $y, int $z, Block $block] + * @phpstan-return \Generator + */ + public function getBlocks() : \Generator{ + foreach($this->blocks as $x => $yLine){ + foreach($yLine as $y => $zLine){ + foreach($zLine as $z => $block){ + yield [$x, $y, $z, $block]; + } + } + } + } + + /** + * Add a validation predicate which will be used to validate every block. + * The callable signature should be the same as the below dummy function. + * @see BlockTransaction::dummyValidator() + * + * @phpstan-param \Closure(ChunkManager $world, int $x, int $y, int $z) : bool $validator + */ + public function addValidator(\Closure $validator) : void{ + Utils::validateCallableSignature([$this, 'dummyValidator'], $validator); + $this->validators[] = $validator; + } + + /** + * Dummy function demonstrating the required closure signature for validators. + * @see BlockTransaction::addValidator() + * + * @dummy + */ + public function dummyValidator(ChunkManager $world, int $x, int $y, int $z) : bool{ + return true; + } +} diff --git a/src/world/ChunkListener.php b/src/world/ChunkListener.php new file mode 100644 index 0000000000..8fafd73b00 --- /dev/null +++ b/src/world/ChunkListener.php @@ -0,0 +1,68 @@ +chunk = $chunk; - } - - public function getChunk() : Chunk{ - return $this->chunk; - } } diff --git a/src/world/ChunkLockId.php b/src/world/ChunkLockId.php new file mode 100644 index 0000000000..312b8c9109 --- /dev/null +++ b/src/world/ChunkLockId.php @@ -0,0 +1,38 @@ +source = $center; - $this->level = $center->getLevelNonNull(); + $this->world = $center->getWorld(); if($size <= 0){ throw new \InvalidArgumentException("Explosion radius must be greater than 0, got $size"); @@ -82,7 +81,7 @@ class Explosion{ $this->size = $size; $this->what = $what; - $this->subChunkHandler = new SubChunkIteratorManager($this->level, false); + $this->subChunkExplorer = new SubChunkExplorer($this->world); } /** @@ -94,8 +93,7 @@ class Explosion{ return false; } - $vector = new Vector3(0, 0, 0); - $vBlock = new Position(0, 0, 0, $this->level); + $blockFactory = BlockFactory::getInstance(); $currentChunk = null; $currentSubChunk = null; @@ -105,8 +103,10 @@ class Explosion{ 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){ - $vector->setComponents($i / $mRays * 2 - 1, $j / $mRays * 2 - 1, $k / $mRays * 2 - 1); - $vector->setComponents(($vector->x / ($len = $vector->length())) * $this->stepLen, ($vector->y / $len) * $this->stepLen, ($vector->z / $len) * $this->stepLen); + //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); + [$shiftX, $shiftY, $shiftZ] = [($shiftX / $len) * $this->stepLen, ($shiftY / $len) * $this->stepLen, ($shiftZ / $len) * $this->stepLen]; $pointerX = $this->source->x; $pointerY = $this->source->y; $pointerZ = $this->source->z; @@ -115,27 +115,29 @@ class Explosion{ $x = (int) $pointerX; $y = (int) $pointerY; $z = (int) $pointerZ; - $vBlock->x = $pointerX >= $x ? $x : $x - 1; - $vBlock->y = $pointerY >= $y ? $y : $y - 1; - $vBlock->z = $pointerZ >= $z ? $z : $z - 1; + $vBlockX = $pointerX >= $x ? $x : $x - 1; + $vBlockY = $pointerY >= $y ? $y : $y - 1; + $vBlockZ = $pointerZ >= $z ? $z : $z - 1; - $pointerX += $vector->x; - $pointerY += $vector->y; - $pointerZ += $vector->z; + $pointerX += $shiftX; + $pointerY += $shiftY; + $pointerZ += $shiftZ; - if(!$this->subChunkHandler->moveTo($vBlock->x, $vBlock->y, $vBlock->z)){ + if($this->subChunkExplorer->moveTo($vBlockX, $vBlockY, $vBlockZ) === SubChunkExplorerStatus::INVALID){ continue; } - $blockId = $this->subChunkHandler->currentSubChunk->getBlockId($vBlock->x & 0x0f, $vBlock->y & 0x0f, $vBlock->z & 0x0f); + $state = $this->subChunkExplorer->currentSubChunk->getFullBlock($vBlockX & SubChunk::COORD_MASK, $vBlockY & SubChunk::COORD_MASK, $vBlockZ & SubChunk::COORD_MASK); - if($blockId !== 0){ - $blastForce -= (BlockFactory::$blastResistance[$blockId] / 5 + 0.3) * $this->stepLen; + $blastResistance = $blockFactory->blastResistance[$state]; + if($blastResistance >= 0){ + $blastForce -= ($blastResistance / 5 + 0.3) * $this->stepLen; if($blastForce > 0){ - if(!isset($this->affectedBlocks[Level::blockHash($vBlock->x, $vBlock->y, $vBlock->z)])){ - $_block = BlockFactory::get($blockId, $this->subChunkHandler->currentSubChunk->getBlockData($vBlock->x & 0x0f, $vBlock->y & 0x0f, $vBlock->z & 0x0f), $vBlock); + if(!isset($this->affectedBlocks[World::blockHash($vBlockX, $vBlockY, $vBlockZ)])){ + $_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false); foreach($_block->getAffectedBlocks() as $_affectedBlock){ - $this->affectedBlocks[Level::blockHash($_affectedBlock->x, $_affectedBlock->y, $_affectedBlock->z)] = $_affectedBlock; + $_affectedBlockPos = $_affectedBlock->getPosition(); + $this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock; } } } @@ -180,12 +182,14 @@ class Explosion{ $explosionBB = new AxisAlignedBB($minX, $minY, $minZ, $maxX, $maxY, $maxZ); - $list = $this->level->getNearbyEntities($explosionBB, $this->what instanceof Entity ? $this->what : null); + /** @var Entity[] $list */ + $list = $this->world->getNearbyEntities($explosionBB, $this->what instanceof Entity ? $this->what : null); foreach($list as $entity){ - $distance = $entity->distance($this->source) / $explosionSize; + $entityPos = $entity->getPosition(); + $distance = $entityPos->distance($this->source) / $explosionSize; if($distance <= 1){ - $motion = $entity->subtract($this->source)->normalize(); + $motion = $entityPos->subtractVector($this->source)->normalize(); $impact = (1 - $distance) * ($exposure = 1); @@ -204,59 +208,28 @@ class Explosion{ } } - $air = ItemFactory::get(Item::AIR); + $air = ItemFactory::air(); + $airBlock = VanillaBlocks::AIR(); foreach($this->affectedBlocks as $block){ - $yieldDrops = false; - + $pos = $block->getPosition(); if($block instanceof TNT){ $block->ignite(mt_rand(10, 30)); - }elseif($yieldDrops = (mt_rand(0, 100) < $yield)){ - foreach($block->getDrops($air) as $drop){ - $this->level->dropItem($block->add(0.5, 0.5, 0.5), $drop); - } - } - - $this->level->setBlockIdAt($block->x, $block->y, $block->z, 0); - $this->level->setBlockDataAt($block->x, $block->y, $block->z, 0); - - $t = $this->level->getTileAt($block->x, $block->y, $block->z); - if($t instanceof Tile){ - if($t instanceof Chest){ - $t->unpair(); - } - if($yieldDrops and $t instanceof Container){ - $t->getInventory()->dropContents($this->level, $t->add(0.5, 0.5, 0.5)); - } - - $t->close(); - } - } - - foreach($this->affectedBlocks as $block){ - $pos = new Vector3($block->x, $block->y, $block->z); - - for($side = 0; $side <= 5; $side++){ - $sideBlock = $pos->getSide($side); - if(!$this->level->isInWorld($sideBlock->x, $sideBlock->y, $sideBlock->z)){ - continue; - } - if(!isset($this->affectedBlocks[$index = Level::blockHash($sideBlock->x, $sideBlock->y, $sideBlock->z)]) and !isset($updateBlocks[$index])){ - $ev = new BlockUpdateEvent($this->level->getBlockAt($sideBlock->x, $sideBlock->y, $sideBlock->z)); - $ev->call(); - if(!$ev->isCancelled()){ - foreach($this->level->getNearbyEntities(new AxisAlignedBB($sideBlock->x - 1, $sideBlock->y - 1, $sideBlock->z - 1, $sideBlock->x + 2, $sideBlock->y + 2, $sideBlock->z + 2)) as $entity){ - $entity->onNearbyBlockChange(); - } - $ev->getBlock()->onNearbyBlockChange(); + }else{ + if(mt_rand(0, 100) < $yield){ + foreach($block->getDrops($air) as $drop){ + $this->world->dropItem($pos->add(0.5, 0.5, 0.5), $drop); } - $updateBlocks[$index] = true; } + if(($t = $this->world->getTileAt($pos->x, $pos->y, $pos->z)) !== null){ + $t->onBlockDestroyed(); //needed to create drops for inventories + } + $this->world->setBlockAt($pos->x, $pos->y, $pos->z, $airBlock); } } - $this->level->addParticle(new HugeExplodeSeedParticle($source)); - $this->level->broadcastLevelSoundEvent($source, LevelSoundEventPacket::SOUND_EXPLODE); + $this->world->addParticle($source, new HugeExplodeSeedParticle()); + $this->world->addSound($source, new ExplodeSound()); return true; } diff --git a/src/pocketmine/level/Position.php b/src/world/Position.php similarity index 50% rename from src/pocketmine/level/Position.php rename to src/world/Position.php index c0792559c1..f3fe84caaf 100644 --- a/src/pocketmine/level/Position.php +++ b/src/world/Position.php @@ -21,97 +21,69 @@ declare(strict_types=1); -namespace pocketmine\level; +namespace pocketmine\world; use pocketmine\math\Vector3; use pocketmine\utils\AssumptionFailedError; -use pocketmine\utils\MainLogger; use function assert; class Position extends Vector3{ - /** @var Level|null */ - public $level = null; + /** @var World|null */ + public $world = null; /** * @param float|int $x * @param float|int $y * @param float|int $z */ - public function __construct($x = 0, $y = 0, $z = 0, Level $level = null){ + public function __construct($x, $y, $z, ?World $world){ parent::__construct($x, $y, $z); - $this->setLevel($level); + if($world !== null and !$world->isLoaded()){ + throw new \InvalidArgumentException("Specified world has been unloaded and cannot be used"); + } + + $this->world = $world; } /** * @return Position */ - public static function fromObject(Vector3 $pos, Level $level = null){ - return new Position($pos->x, $pos->y, $pos->z, $level); + public static function fromObject(Vector3 $pos, ?World $world){ + return new Position($pos->x, $pos->y, $pos->z, $world); } /** * Return a Position instance */ public function asPosition() : Position{ - return new Position($this->x, $this->y, $this->z, $this->level); + return new Position($this->x, $this->y, $this->z, $this->world); } /** - * Returns the target Level, or null if the target is not valid. - * If a reference exists to a Level which is closed, the reference will be destroyed and null will be returned. - * - * @return Level|null - */ - public function getLevel(){ - if($this->level !== null and $this->level->isClosed()){ - MainLogger::getLogger()->debug("Position was holding a reference to an unloaded world"); - $this->level = null; - } - - return $this->level; - } - - /** - * Returns the position's world if valid. Throws an error if the world is unexpectedly null. + * Returns the position's world if valid. Throws an error if the world is unexpectedly invalid. * * @throws AssumptionFailedError */ - public function getLevelNonNull() : Level{ - $world = $this->getLevel(); - if($world === null){ - throw new AssumptionFailedError("Position world is null"); + public function getWorld() : World{ + if($this->world === null || !$this->world->isLoaded()){ + throw new AssumptionFailedError("Position world is null or has been unloaded"); } - return $world; + + return $this->world; } /** - * Sets the target Level of the position. - * - * @return $this - * - * @throws \InvalidArgumentException if the specified Level has been closed - */ - public function setLevel(Level $level = null){ - if($level !== null and $level->isClosed()){ - throw new \InvalidArgumentException("Specified world has been unloaded and cannot be used"); - } - - $this->level = $level; - return $this; - } - - /** - * Checks if this object has a valid reference to a loaded Level + * Checks if this object has a valid reference to a loaded world */ public function isValid() : bool{ - if($this->level !== null and $this->level->isClosed()){ - $this->level = null; + if($this->world !== null and !$this->world->isLoaded()){ + $this->world = null; return false; } - return $this->level !== null; + return $this->world !== null; } /** @@ -122,16 +94,16 @@ class Position extends Vector3{ public function getSide(int $side, int $step = 1){ assert($this->isValid()); - return Position::fromObject(parent::getSide($side, $step), $this->level); + return Position::fromObject(parent::getSide($side, $step), $this->world); } public function __toString(){ - return "Position(level=" . ($this->isValid() ? $this->getLevelNonNull()->getName() : "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->getLevel() === $this->getLevel(); + return parent::equals($v) and $v->world === $this->world; } return parent::equals($v); } diff --git a/src/world/SimpleChunkManager.php b/src/world/SimpleChunkManager.php new file mode 100644 index 0000000000..105412bb50 --- /dev/null +++ b/src/world/SimpleChunkManager.php @@ -0,0 +1,89 @@ +minY = $minY; + $this->maxY = $maxY; + } + + public function getBlockAt(int $x, int $y, int $z) : Block{ + if($this->isInWorld($x, $y, $z) && ($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){ + return BlockFactory::getInstance()->fromFullBlock($chunk->getFullBlock($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK)); + } + return VanillaBlocks::AIR(); + } + + public function setBlockAt(int $x, int $y, int $z, Block $block) : void{ + if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){ + $chunk->setFullBlock($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK, $block->getFullId()); + }else{ + throw new \InvalidArgumentException("Cannot set block at coordinates x=$x,y=$y,z=$z, terrain is not loaded or out of bounds"); + } + } + + public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{ + return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null; + } + + public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + $this->chunks[World::chunkHash($chunkX, $chunkZ)] = $chunk; + } + + public function cleanChunks() : void{ + $this->chunks = []; + } + + public function getMinY() : int{ + return $this->minY; + } + + public function getMaxY() : int{ + return $this->maxY; + } + + 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 + ); + } +} diff --git a/src/world/TickingChunkLoader.php b/src/world/TickingChunkLoader.php new file mode 100644 index 0000000000..57281c90c2 --- /dev/null +++ b/src/world/TickingChunkLoader.php @@ -0,0 +1,36 @@ + + +class World implements ChunkManager{ + + /** @var int */ + private static $worldIdCounter = 1; + + public const Y_MAX = 256; + public const Y_MIN = 0; + + public const TIME_DAY = 1000; + public const TIME_NOON = 6000; + public const TIME_SUNSET = 12000; + public const TIME_NIGHT = 13000; + public const TIME_MIDNIGHT = 18000; + public const TIME_SUNRISE = 23000; + + public const TIME_FULL = 24000; + + public const DIFFICULTY_PEACEFUL = 0; + public const DIFFICULTY_EASY = 1; + public const DIFFICULTY_NORMAL = 2; + public const DIFFICULTY_HARD = 3; + + public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3; + + /** @var Player[] */ + private $players = []; + + /** @var Entity[] */ + private $entities = []; + /** + * @var Vector3[] + * @phpstan-var array + */ + private $entityLastKnownPositions = []; + + /** + * @var Entity[][] + * @phpstan-var array> + */ + private array $entitiesByChunk = []; + + /** @var Entity[] */ + public $updateEntities = []; + /** @var Block[][] */ + private $blockCache = []; + + /** @var int */ + private $sendTimeTicker = 0; + + /** @var Server */ + private $server; + + /** @var int */ + private $worldId; + + /** @var WritableWorldProvider */ + private $provider; + /** @var int */ + private $providerGarbageCollectionTicker = 0; + + /** @var int */ + private $minY; + /** @var int */ + private $maxY; + + /** @var TickingChunkLoader[] */ + private $tickingLoaders = []; + /** @var int[] */ + private $tickingLoaderCounter = []; + /** @var ChunkLoader[][] */ + private $chunkLoaders = []; + + /** @var ChunkListener[][] */ + private $chunkListeners = []; + /** @var Player[][] */ + private $playerChunkListeners = []; + + /** @var ClientboundPacket[][] */ + private $packetBuffersByChunk = []; + + /** @var float[] */ + private $unloadQueue = []; + + /** @var int */ + private $time; + /** @var bool */ + public $stopTime = false; + + /** @var float */ + private $sunAnglePercentage = 0.0; + /** @var int */ + private $skyLightReduction = 0; + + /** @var string */ + private $folderName; + /** @var string */ + private $displayName; + + /** @var Chunk[] */ + private $chunks = []; + + /** @var Vector3[][] */ + private $changedBlocks = []; + + /** + * @var ReversePriorityQueue + * @phpstan-var ReversePriorityQueue + */ + private $scheduledBlockUpdateQueue; + /** @var int[] */ + private $scheduledBlockUpdateQueueIndex = []; + + /** + * @var \SplQueue + * @phpstan-var \SplQueue + */ + private $neighbourBlockUpdateQueue; + /** @var bool[] blockhash => dummy */ + private $neighbourBlockUpdateQueueIndex = []; + + /** @var bool[] */ + private $activeChunkPopulationTasks = []; + /** @var ChunkLockId[] */ + private $chunkLock = []; + /** @var int */ + private $maxConcurrentChunkPopulationTasks = 2; + /** + * @var PromiseResolver[] chunkHash => promise + * @phpstan-var array> + */ + private array $chunkPopulationRequestMap = []; + /** + * @var \SplQueue (queue of chunkHashes) + * @phpstan-var \SplQueue + */ + private \SplQueue $chunkPopulationRequestQueue; + /** + * @var true[] chunkHash => dummy + * @phpstan-var array + */ + private array $chunkPopulationRequestQueueIndex = []; + + /** @var bool[] */ + private $generatorRegisteredWorkers = []; + + /** @var bool */ + private $autoSave = true; + + /** @var int */ + private $sleepTicks = 0; + + /** @var int */ + private $chunkTickRadius; + /** @var int */ + private $chunksPerTick; + /** @var int */ + private $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK; + /** @var bool[] */ + private $randomTickBlocks = []; + + /** @var WorldTimings */ + public $timings; + + /** @var float */ + public $tickRateTime = 0; + + /** @var bool */ + private $doingTick = false; + + /** + * @var string + * @phpstan-var class-string<\pocketmine\world\generator\Generator> + */ + private $generator; + + /** @var bool */ + private $unloaded = false; + /** + * @var \Closure[] + * @phpstan-var array + */ + private $unloadCallbacks = []; + + /** @var BlockLightUpdate|null */ + private $blockLightUpdate = null; + /** @var SkyLightUpdate|null */ + private $skyLightUpdate = null; + + /** @var \Logger */ + private $logger; + + /** @var AsyncPool */ + private $workerPool; + + public static function chunkHash(int $x, int $z) : int{ + return morton2d_encode($x, $z); + } + + private const MORTON3D_BIT_SIZE = 21; + private const BLOCKHASH_Y_BITS = 9; + private const BLOCKHASH_Y_PADDING = 128; //size (in blocks) of padding after both boundaries of the Y axis + 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 = (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; + private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS; + + public static function blockHash(int $x, int $y, int $z) : int{ + $shiftedY = $y + self::BLOCKHASH_Y_OFFSET; + if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){ + throw new \InvalidArgumentException("Y coordinate $y is out of range!"); + } + //morton3d gives us 21 bits on each axis, but the Y axis only requires 9 + //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead. + //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate. + return morton3d_encode( + $x & self::BLOCKHASH_XZ_MASK, + ($shiftedY /* & self::BLOCKHASH_Y_MASK */) | + ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) | + ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT), + $z & self::BLOCKHASH_XZ_MASK + ); + } + + /** + * Computes a small index relative to chunk base from the given coordinates. + */ + public static function chunkBlockHash(int $x, int $y, int $z) : int{ + return morton3d_encode($x, $y, $z); + } + + public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{ + [$baseX, $baseY, $baseZ] = morton3d_decode($hash); + + $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE); + $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE); + + $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT; + $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET; + $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT; + } + + public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{ + [$x, $z] = morton2d_decode($hash); + } + + public static function getDifficultyFromString(string $str) : int{ + switch(strtolower(trim($str))){ + case "0": + case "peaceful": + case "p": + return World::DIFFICULTY_PEACEFUL; + + case "1": + case "easy": + case "e": + return World::DIFFICULTY_EASY; + + case "2": + case "normal": + case "n": + return World::DIFFICULTY_NORMAL; + + case "3": + case "hard": + case "h": + return World::DIFFICULTY_HARD; + } + + return -1; + } + + /** + * Init the default world data + */ + public function __construct(Server $server, string $name, WritableWorldProvider $provider, AsyncPool $workerPool){ + $this->worldId = self::$worldIdCounter++; + $this->server = $server; + + $this->provider = $provider; + $this->workerPool = $workerPool; + + $this->displayName = $this->provider->getWorldData()->getName(); + $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName"); + + $this->minY = $this->provider->getWorldMinY(); + $this->maxY = $this->provider->getWorldMaxY(); + + $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName))); + $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"); + + foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){ + $promise->reject(); + unset($this->chunkPopulationRequestMap[$chunkHash]); + } + if(count($this->chunkPopulationRequestMap) !== 0){ + //TODO: this might actually get hit because generation rejection callbacks might try to schedule new + //requests, and we can't prevent that right now because there's no way to detect "unloading" state + throw new AssumptionFailedError("New generation requests scheduled during unload"); + } + }); + + $this->folderName = $name; + + $this->scheduledBlockUpdateQueue = new ReversePriorityQueue(); + $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + + $this->neighbourBlockUpdateQueue = new \SplQueue(); + + $this->time = $this->provider->getWorldData()->getTime(); + + $cfg = $this->server->getConfigGroup(); + $this->chunkTickRadius = min($this->server->getViewDistance(), max(1, $cfg->getPropertyInt("chunk-ticking.tick-radius", 4))); + $this->chunksPerTick = $cfg->getPropertyInt("chunk-ticking.per-tick", 40); + $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->timings = new WorldTimings($this); + + $this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{ + if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){ + $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered"); + unset($this->generatorRegisteredWorkers[$workerId]); + } + }); + $workerPool = $this->workerPool; + $this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{ + $workerPool->removeWorkerStartHook($workerStartHook); + }); + } + + public function getTickRateTime() : float{ + return $this->tickRateTime; + } + + public function registerGeneratorToWorker(int $worker) : void{ + $this->logger->debug("Registering generator on worker $worker"); + $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker); + $this->generatorRegisteredWorkers[$worker] = true; + } + + public function unregisterGenerator() : void{ + foreach($this->workerPool->getRunningWorkers() as $i){ + if(isset($this->generatorRegisteredWorkers[$i])){ + $this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i); + } + } + $this->generatorRegisteredWorkers = []; + } + + public function getServer() : Server{ + return $this->server; + } + + public function getLogger() : \Logger{ + return $this->logger; + } + + final public function getProvider() : WritableWorldProvider{ + return $this->provider; + } + + /** + * Returns the unique world identifier + */ + final public function getId() : int{ + return $this->worldId; + } + + public function isLoaded() : bool{ + return !$this->unloaded; + } + + /** + * @internal + */ + public function onUnload() : void{ + if($this->unloaded){ + throw new \LogicException("Tried to close a world which is already closed"); + } + + foreach($this->unloadCallbacks as $callback){ + $callback(); + } + $this->unloadCallbacks = []; + + foreach($this->chunks as $chunkHash => $chunk){ + self::getXZ($chunkHash, $chunkX, $chunkZ); + $this->unloadChunk($chunkX, $chunkZ, false); + } + foreach($this->entitiesByChunk as $chunkHash => $entities){ + self::getXZ($chunkHash, $chunkX, $chunkZ); + + $leakedEntities = 0; + foreach($entities as $entity){ + if(!$entity->isFlaggedForDespawn()){ + $leakedEntities++; + } + $entity->close(); + } + if($leakedEntities !== 0){ + $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!"); + } + } + + $this->save(); + + $this->unregisterGenerator(); + + $this->provider->close(); + $this->blockCache = []; + + $this->unloaded = true; + } + + /** @phpstan-param \Closure() : void $callback */ + public function addOnUnloadCallback(\Closure $callback) : void{ + $this->unloadCallbacks[spl_object_id($callback)] = $callback; + } + + /** @phpstan-param \Closure() : void $callback */ + public function removeOnUnloadCallback(\Closure $callback) : void{ + unset($this->unloadCallbacks[spl_object_id($callback)]); + } + + /** + * @param Player[]|null $players + */ + public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{ + $pk = $sound->encode($pos); + if(count($pk) > 0){ + if($players === null){ + foreach($pk as $e){ + $this->broadcastPacketToViewers($pos, $e); + } + }else{ + $this->server->broadcastPackets($players, $pk); + } + } + } + + /** + * @param Player[]|null $players + */ + public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{ + $pk = $particle->encode($pos); + if(count($pk) > 0){ + if($players === null){ + foreach($pk as $e){ + $this->broadcastPacketToViewers($pos, $e); + } + }else{ + $this->server->broadcastPackets($players, $pk); + } + } + } + + public function getAutoSave() : bool{ + return $this->autoSave; + } + + public function setAutoSave(bool $value) : void{ + $this->autoSave = $value; + } + + /** + * @deprecated WARNING: This function has a misleading name. Contrary to what the name might imply, this function + * DOES NOT return players who are IN a chunk, rather, it returns players who can SEE the chunk. + * + * Returns a list of players who have the target chunk within their view distance. + * + * @return Player[] + */ + public function getChunkPlayers(int $chunkX, int $chunkZ) : array{ + return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? []; + } + + /** + * Gets the chunk loaders being used in a specific chunk + * + * @return ChunkLoader[] + */ + public function getChunkLoaders(int $chunkX, int $chunkZ) : array{ + return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? []; + } + + /** + * Returns an array of players who have the target position within their view distance. + * + * @return Player[] + */ + public function getViewersForPosition(Vector3 $pos) : array{ + return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE); + } + + /** + * Broadcasts a packet to every player who has the target position within their view distance. + */ + public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{ + $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet); + } + + private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{ + if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){ + $this->packetBuffersByChunk[$index] = [$packet]; + }else{ + $this->packetBuffersByChunk[$index][] = $packet; + } + } + + public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{ + $loaderId = spl_object_id($loader); + + if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){ + $this->chunkLoaders[$chunkHash] = []; + }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){ + return; + } + + $this->chunkLoaders[$chunkHash][$loaderId] = $loader; + + if($loader instanceof TickingChunkLoader){ + if(!isset($this->tickingLoaders[$loaderId])){ + $this->tickingLoaderCounter[$loaderId] = 1; + $this->tickingLoaders[$loaderId] = $loader; + }else{ + ++$this->tickingLoaderCounter[$loaderId]; + } + } + + $this->cancelUnloadChunkRequest($chunkX, $chunkZ); + + if($autoLoad){ + $this->loadChunk($chunkX, $chunkZ); + } + } + + public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{ + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $loaderId = spl_object_id($loader); + if(isset($this->chunkLoaders[$chunkHash][$loaderId])){ + unset($this->chunkLoaders[$chunkHash][$loaderId]); + if(count($this->chunkLoaders[$chunkHash]) === 0){ + unset($this->chunkLoaders[$chunkHash]); + $this->unloadChunkRequest($chunkX, $chunkZ, true); + if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){ + $this->chunkPopulationRequestMap[$chunkHash]->reject(); + unset($this->chunkPopulationRequestMap[$chunkHash]); + } + } + + if(isset($this->tickingLoaderCounter[$loaderId]) && --$this->tickingLoaderCounter[$loaderId] === 0){ + unset($this->tickingLoaderCounter[$loaderId]); + unset($this->tickingLoaders[$loaderId]); + } + } + } + + /** + * Registers a listener to receive events on a chunk. + */ + public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{ + $hash = World::chunkHash($chunkX, $chunkZ); + if(isset($this->chunkListeners[$hash])){ + $this->chunkListeners[$hash][spl_object_id($listener)] = $listener; + }else{ + $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener]; + } + if($listener instanceof Player){ + $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener; + } + } + + /** + * Unregisters a chunk listener previously registered. + * + * @see World::registerChunkListener() + */ + public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{ + $hash = World::chunkHash($chunkX, $chunkZ); + if(isset($this->chunkListeners[$hash])){ + unset($this->chunkListeners[$hash][spl_object_id($listener)]); + unset($this->playerChunkListeners[$hash][spl_object_id($listener)]); + if(count($this->chunkListeners[$hash]) === 0){ + unset($this->chunkListeners[$hash]); + unset($this->playerChunkListeners[$hash]); + } + } + } + + /** + * Unregisters a chunk listener from all chunks it is listening on in this World. + */ + public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{ + foreach($this->chunkListeners as $hash => $listeners){ + World::getXZ($hash, $chunkX, $chunkZ); + $this->unregisterChunkListener($listener, $chunkX, $chunkZ); + } + } + + /** + * Returns all the listeners attached to this chunk. + * + * @return ChunkListener[] + */ + public function getChunkListeners(int $chunkX, int $chunkZ) : array{ + return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? []; + } + + /** + * @internal + * + * @param Player ...$targets If empty, will send to all players in the world. + */ + public function sendTime(Player ...$targets) : void{ + if(count($targets) === 0){ + $targets = $this->players; + } + foreach($targets as $player){ + $player->getNetworkSession()->syncWorldTime($this->time); + } + } + + public function isDoingTick() : bool{ + return $this->doingTick; + } + + /** + * @internal + */ + public function doTick(int $currentTick) : void{ + if($this->unloaded){ + throw new \LogicException("Attempted to tick a world which has been closed"); + } + + $this->timings->doTick->startTiming(); + $this->doingTick = true; + try{ + $this->actuallyDoTick($currentTick); + }finally{ + $this->doingTick = false; + $this->timings->doTick->stopTiming(); + } + } + + protected function actuallyDoTick(int $currentTick) : void{ + if(!$this->stopTime){ + //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types + if($this->time === PHP_INT_MAX){ + $this->time = PHP_INT_MIN; + }else{ + $this->time++; + } + } + + $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time + $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle + + if(++$this->sendTimeTicker === 200){ + $this->sendTime(); + $this->sendTimeTicker = 0; + } + + $this->unloadChunks(); + if(++$this->providerGarbageCollectionTicker >= 6000){ + $this->provider->doGarbageCollection(); + $this->providerGarbageCollectionTicker = 0; + } + + //Do block updates + $this->timings->scheduledBlockUpdates->startTiming(); + + //Delayed updates + while($this->scheduledBlockUpdateQueue->count() > 0 and $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){ + /** @var Vector3 $vec */ + $vec = $this->scheduledBlockUpdateQueue->extract()["data"]; + unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]); + if(!$this->isInLoadedTerrain($vec)){ + continue; + } + $block = $this->getBlock($vec); + $block->onScheduledUpdate(); + } + + //Normal updates + while($this->neighbourBlockUpdateQueue->count() > 0){ + $index = $this->neighbourBlockUpdateQueue->dequeue(); + unset($this->neighbourBlockUpdateQueueIndex[$index]); + World::getBlockXYZ($index, $x, $y, $z); + if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){ + continue; + } + + $block = $this->getBlockAt($x, $y, $z); + $block->readStateFromWorld(); //for blocks like fences, force recalculation of connected AABBs + + $ev = new BlockUpdateEvent($block); + $ev->call(); + if(!$ev->isCancelled()){ + foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){ + $entity->onNearbyBlockChange(); + } + $block->onNearbyBlockChange(); + } + } + + $this->timings->scheduledBlockUpdates->stopTiming(); + + $this->timings->entityTick->startTiming(); + //Update entities that need update + Timings::$tickEntity->startTiming(); + foreach($this->updateEntities as $id => $entity){ + if($entity->isClosed() or !$entity->onUpdate($currentTick)){ + unset($this->updateEntities[$id]); + } + if($entity->isFlaggedForDespawn()){ + $entity->close(); + } + } + Timings::$tickEntity->stopTiming(); + $this->timings->entityTick->stopTiming(); + + $this->timings->randomChunkUpdates->startTiming(); + $this->tickChunks(); + $this->timings->randomChunkUpdates->stopTiming(); + + $this->executeQueuedLightUpdates(); + + if(count($this->changedBlocks) > 0){ + if(count($this->players) > 0){ + foreach($this->changedBlocks as $index => $blocks){ + if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send + continue; + } + World::getXZ($index, $chunkX, $chunkZ); + if(count($blocks) > 512){ + $chunk = $this->getChunk($chunkX, $chunkZ); + foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){ + $p->onChunkChanged($chunkX, $chunkZ, $chunk); + } + }else{ + foreach($this->createBlockUpdatePackets($blocks) as $packet){ + $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet); + } + } + } + } + + $this->changedBlocks = []; + + } + + if($this->sleepTicks > 0 and --$this->sleepTicks <= 0){ + $this->checkSleep(); + } + + foreach($this->packetBuffersByChunk as $index => $entries){ + World::getXZ($index, $chunkX, $chunkZ); + $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ); + if(count($chunkPlayers) > 0){ + $this->server->broadcastPackets($chunkPlayers, $entries); + } + } + + $this->packetBuffersByChunk = []; + } + + public function checkSleep() : void{ + if(count($this->players) === 0){ + return; + } + + $resetTime = true; + foreach($this->getPlayers() as $p){ + if(!$p->isSleeping()){ + $resetTime = false; + break; + } + } + + if($resetTime){ + $time = $this->getTimeOfDay(); + + if($time >= World::TIME_NIGHT and $time < World::TIME_SUNRISE){ + $this->setTime($this->getTime() + World::TIME_FULL - $time); + + foreach($this->getPlayers() as $p){ + $p->stopSleep(); + } + } + } + } + + public function setSleepTicks(int $ticks) : void{ + $this->sleepTicks = $ticks; + } + + /** + * @param Vector3[] $blocks + * + * @return ClientboundPacket[] + */ + public function createBlockUpdatePackets(array $blocks) : array{ + $packets = []; + + 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); + $blockPosition = BlockPosition::fromVector3($b); + $packets[] = UpdateBlockPacket::create( + $blockPosition, + RuntimeBlockMapping::getInstance()->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($blockPosition, $tile->getSerializedSpawnCompound()); + } + } + + return $packets; + } + + public function clearCache(bool $force = false) : void{ + if($force){ + $this->blockCache = []; + }else{ + $count = 0; + foreach($this->blockCache as $list){ + $count += count($list); + if($count > 2048){ + $this->blockCache = []; + break; + } + } + } + } + + /** + * @return bool[] fullID => bool + */ + public function getRandomTickedBlocks() : array{ + return $this->randomTickBlocks; + } + + public function addRandomTickedBlock(Block $block) : void{ + if($block instanceof UnknownBlock){ + throw new \InvalidArgumentException("Cannot do random-tick on unknown block"); + } + $this->randomTickBlocks[$block->getFullId()] = true; + } + + public function removeRandomTickedBlock(Block $block) : void{ + unset($this->randomTickBlocks[$block->getFullId()]); + } + + private function tickChunks() : void{ + if($this->chunksPerTick <= 0 or count($this->tickingLoaders) === 0){ + return; + } + + $this->timings->randomChunkUpdatesChunkSelection->startTiming(); + + /** @var bool[] $chunkTickList chunkhash => dummy */ + $chunkTickList = []; + + $chunksPerLoader = min(200, max(1, (int) ((($this->chunksPerTick - count($this->tickingLoaders)) / count($this->tickingLoaders)) + 0.5))); + $randRange = 3 + $chunksPerLoader / 30; + $randRange = (int) ($randRange > $this->chunkTickRadius ? $this->chunkTickRadius : $randRange); + + foreach($this->tickingLoaders as $loader){ + $chunkX = (int) floor($loader->getX()) >> Chunk::COORD_BIT_SIZE; + $chunkZ = (int) floor($loader->getZ()) >> Chunk::COORD_BIT_SIZE; + + for($chunk = 0; $chunk < $chunksPerLoader; ++$chunk){ + $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)){ + $chunkTickList[$hash] = true; + } + } + } + + $this->timings->randomChunkUpdatesChunkSelection->stopTiming(); + + foreach($chunkTickList as $index => $_){ + World::getXZ($index, $chunkX, $chunkZ); + + $this->tickChunk($chunkX, $chunkZ); + } + } + + private function isChunkTickable(int $chunkX, int $chunkZ) : bool{ + for($cx = -1; $cx <= 1; ++$cx){ + for($cz = -1; $cz <= 1; ++$cz){ + if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){ + return false; + } + $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz); + if($adjacentChunk === null || !$adjacentChunk->isPopulated()){ + return false; + } + $lightPopulatedState = $adjacentChunk->isLightPopulated(); + if($lightPopulatedState !== true){ + if($lightPopulatedState === false){ + $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz); + } + return false; + } + } + } + + return true; + } + + private function orderLightPopulation(int $chunkX, int $chunkZ) : void{ + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated(); + if($lightPopulatedState === false){ + $this->chunks[$chunkHash]->setLightPopulated(null); + + $this->workerPool->submitTask(new LightPopulationTask( + $this->chunks[$chunkHash], + function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{ + /** + * TODO: phpstan can't infer these types yet :( + * @phpstan-var array $blockLight + * @phpstan-var array $skyLight + * @phpstan-var array $heightMap + */ + if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){ + return; + } + //TODO: calculated light information might not be valid if the terrain changed during light calculation + + $chunk->setHeightMapArray($heightMap); + foreach($blockLight as $y => $lightArray){ + $chunk->getSubChunk($y)->setBlockLightArray($lightArray); + } + foreach($skyLight as $y => $lightArray){ + $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray); + } + $chunk->setLightPopulated(true); + } + )); + } + } + + private function tickChunk(int $chunkX, int $chunkZ) : void{ + $chunk = $this->getChunk($chunkX, $chunkZ); + if($chunk === null){ + throw new \InvalidArgumentException("Chunk is not loaded"); + } + foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){ + $entity->onRandomUpdate(); + } + + foreach($chunk->getSubChunks() as $Y => $subChunk){ + if(!$subChunk->isEmptyFast()){ + $k = 0; + for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){ + if(($i % 5) === 0){ + //60 bits will be used by 5 blocks (12 bits each) + $k = mt_rand(0, (1 << 60) - 1); + } + $x = $k & SubChunk::COORD_MASK; + $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK; + $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK; + $k >>= (SubChunk::COORD_BIT_SIZE * 3); + + $state = $subChunk->getFullBlock($x, $y, $z); + + if(isset($this->randomTickBlocks[$state])){ + /** @var Block $block */ + $block = BlockFactory::getInstance()->fromFullBlock($state); + $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z); + $block->onRandomTick(); + } + } + } + } + } + + /** + * @return mixed[] + */ + public function __debugInfo() : array{ + return []; + } + + public function save(bool $force = false) : bool{ + + if(!$this->getAutoSave() and !$force){ + return false; + } + + (new WorldSaveEvent($this))->call(); + + $this->provider->getWorldData()->setTime($this->time); + $this->saveChunks(); + $this->provider->getWorldData()->save(); + + return true; + } + + public function saveChunks() : void{ + $this->timings->syncChunkSave->startTiming(); + try{ + foreach($this->chunks as $chunkHash => $chunk){ + self::getXZ($chunkHash, $chunkX, $chunkZ); + $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData( + $chunk, + array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk())), + array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()), + )); + $chunk->clearTerrainDirtyFlags(); + } + }finally{ + $this->timings->syncChunkSave->stopTiming(); + } + } + + /** + * Schedules a block update to be executed after the specified number of ticks. + * Blocks will be updated with the scheduled update type. + */ + 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) + ){ + return; + } + $this->scheduledBlockUpdateQueueIndex[$index] = $delay; + $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick()); + } + + private function tryAddToNeighbourUpdateQueue(Vector3 $pos) : void{ + if($this->isInWorld($pos->x, $pos->y, $pos->z)){ + $hash = World::blockHash($pos->x, $pos->y, $pos->z); + if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){ + $this->neighbourBlockUpdateQueue->enqueue($hash); + $this->neighbourBlockUpdateQueueIndex[$hash] = true; + } + } + } + + /** + * Notify the blocks at and around the position that the block at the position may have changed. + * This will cause onNeighbourBlockUpdate() to be called for these blocks. + */ + public function notifyNeighbourBlockUpdate(Vector3 $pos) : void{ + $this->tryAddToNeighbourUpdateQueue($pos); + foreach($pos->sides() as $side){ + $this->tryAddToNeighbourUpdateQueue($side); + } + } + + /** + * @return Block[] + */ + public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{ + $minX = (int) floor($bb->minX - 1); + $minY = (int) floor($bb->minY - 1); + $minZ = (int) floor($bb->minZ - 1); + $maxX = (int) floor($bb->maxX + 1); + $maxY = (int) floor($bb->maxY + 1); + $maxZ = (int) floor($bb->maxZ + 1); + + $collides = []; + + if($targetFirst){ + for($z = $minZ; $z <= $maxZ; ++$z){ + for($x = $minX; $x <= $maxX; ++$x){ + for($y = $minY; $y <= $maxY; ++$y){ + $block = $this->getBlockAt($x, $y, $z); + if($block->collidesWithBB($bb)){ + return [$block]; + } + } + } + } + }else{ + for($z = $minZ; $z <= $maxZ; ++$z){ + for($x = $minX; $x <= $maxX; ++$x){ + for($y = $minY; $y <= $maxY; ++$y){ + $block = $this->getBlockAt($x, $y, $z); + if($block->collidesWithBB($bb)){ + $collides[] = $block; + } + } + } + } + } + + return $collides; + } + + /** + * @return AxisAlignedBB[] + */ + public function getCollisionBoxes(Entity $entity, AxisAlignedBB $bb, bool $entities = true) : array{ + $minX = (int) floor($bb->minX - 1); + $minY = (int) floor($bb->minY - 1); + $minZ = (int) floor($bb->minZ - 1); + $maxX = (int) floor($bb->maxX + 1); + $maxY = (int) floor($bb->maxY + 1); + $maxZ = (int) floor($bb->maxZ + 1); + + $collides = []; + + for($z = $minZ; $z <= $maxZ; ++$z){ + for($x = $minX; $x <= $maxX; ++$x){ + for($y = $minY; $y <= $maxY; ++$y){ + $block = $this->getBlockAt($x, $y, $z); + foreach($block->getCollisionBoxes() as $blockBB){ + if($blockBB->intersectsWith($bb)){ + $collides[] = $blockBB; + } + } + } + } + } + + if($entities){ + foreach($this->getCollidingEntities($bb->expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){ + $collides[] = clone $ent->boundingBox; + } + } + + return $collides; + } + + /** + * Computes the percentage of a circle away from noon the sun is currently at. This can be multiplied by 2 * M_PI to + * get an angle in radians, or by 360 to get an angle in degrees. + */ + public function computeSunAnglePercentage() : float{ + $timeProgress = ($this->time % 24000) / 24000; + + //0.0 needs to be high noon, not dusk + $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25); + + //Offset the sun progress to be above the horizon longer at dusk and dawn + //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn + $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3); + + return $sunProgress + $diff; + } + + /** + * Returns the percentage of a circle away from noon the sun is currently at. + */ + public function getSunAnglePercentage() : float{ + return $this->sunAnglePercentage; + } + + /** + * Returns the current sun angle in radians. + */ + public function getSunAngleRadians() : float{ + return $this->sunAnglePercentage * 2 * M_PI; + } + + /** + * Returns the current sun angle in degrees. + */ + public function getSunAngleDegrees() : float{ + return $this->sunAnglePercentage * 360.0; + } + + /** + * Computes how many points of sky light is subtracted based on the current time. Used to offset raw chunk sky light + * to get a real light value. + */ + public function computeSkyLightReduction() : int{ + $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5))); + + //TODO: check rain and thunder level + + return (int) ($percentage * 11); + } + + /** + * Returns how many points of sky light is subtracted based on the current time. + */ + public function getSkyLightReduction() : int{ + return $this->skyLightReduction; + } + + /** + * Returns the highest available level of any type of light at the given coordinates, adjusted for the current + * weather and time of day. + */ + public function getFullLight(Vector3 $pos) : int{ + return $this->getFullLightAt($pos->x, $pos->y, $pos->z); + } + + /** + * Returns the highest available level of any type of light at the given coordinates, adjusted for the current + * weather and time of day. + */ + public function getFullLightAt(int $x, int $y, int $z) : int{ + $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z); + if($skyLight < 15){ + return max($skyLight, $this->getBlockLightAt($x, $y, $z)); + }else{ + return $skyLight; + } + } + + /** + * Returns the highest available level of any type of light at, or adjacent to, the given coordinates, adjusted for + * the current weather and time of day. + */ + public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{ + return $this->getHighestAdjacentLight($x, $y, $z, \Closure::fromCallable([$this, 'getFullLightAt'])); + } + + /** + * Returns the highest potential level of sky light at the target coordinates, regardless of the time of day or + * weather conditions. + * You usually don't want to use this for vanilla gameplay logic; prefer the real sky light instead. + * @see World::getRealBlockSkyLightAt() + * + * @return int 0-15 + */ + public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{ + if(!$this->isInWorld($x, $y, $z)){ + return $y >= self::Y_MAX ? 15 : 0; + } + if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){ + return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + } + return 0; //TODO: this should probably throw instead (light not calculated yet) + } + + /** + * Returns the sky light level at the specified coordinates, offset by the current time and weather. + * + * @return int 0-15 + */ + public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{ + $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction; + return $light < 0 ? 0 : $light; + } + + /** + * Gets the raw block light level + * + * @return int 0-15 + */ + public function getBlockLightAt(int $x, int $y, int $z) : int{ + if(!$this->isInWorld($x, $y, $z)){ + return 0; + } + if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){ + return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + } + return 0; //TODO: this should probably throw instead (light not calculated yet) + } + + 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){ + return; + } + + $blockFactory = BlockFactory::getInstance(); + $this->timings->doBlockSkyLightUpdates->startTiming(); + if($this->skyLightUpdate === null){ + $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight); + } + $this->skyLightUpdate->recalculateNode($x, $y, $z); + $this->timings->doBlockSkyLightUpdates->stopTiming(); + + $this->timings->doBlockLightUpdates->startTiming(); + if($this->blockLightUpdate === null){ + $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light); + } + $this->blockLightUpdate->recalculateNode($x, $y, $z); + $this->timings->doBlockLightUpdates->stopTiming(); + } + + /** + * @phpstan-param \Closure(int $x, int $y, int $z) : int $lightGetter + */ + private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{ + $max = 0; + foreach([ + [$x + 1, $y, $z], + [$x - 1, $y, $z], + [$x, $y + 1, $z], + [$x, $y - 1, $z], + [$x, $y, $z + 1], + [$x, $y, $z - 1] + ] as [$x1, $y1, $z1]){ + if( + !$this->isInWorld($x1, $y1, $z1) || + ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null || + $chunk->isLightPopulated() !== true + ){ + continue; + } + $max = max($max, $lightGetter($x1, $y1, $z1)); + } + return $max; + } + + /** + * Returns the highest potential level of sky light in the positions adjacent to the specified block coordinates. + */ + public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{ + return $this->getHighestAdjacentLight($x, $y, $z, \Closure::fromCallable([$this, 'getPotentialBlockSkyLightAt'])); + } + + /** + * Returns the highest block sky light available in the positions adjacent to the given coordinates, adjusted for + * the world's current time of day and weather conditions. + */ + public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{ + return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction; + } + + /** + * Returns the highest block light level available in the positions adjacent to the specified block coordinates. + */ + public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{ + return $this->getHighestAdjacentLight($x, $y, $z, \Closure::fromCallable([$this, 'getBlockLightAt'])); + } + + private function executeQueuedLightUpdates() : void{ + if($this->blockLightUpdate !== null){ + $this->timings->doBlockLightUpdates->startTiming(); + $this->blockLightUpdate->execute(); + $this->blockLightUpdate = null; + $this->timings->doBlockLightUpdates->stopTiming(); + } + + if($this->skyLightUpdate !== null){ + $this->timings->doBlockSkyLightUpdates->startTiming(); + $this->skyLightUpdate->execute(); + $this->skyLightUpdate = null; + $this->timings->doBlockSkyLightUpdates->stopTiming(); + } + } + + 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 + ); + } + + /** + * Gets the Block object at the Vector3 location. This method wraps around {@link getBlockAt}, converting the + * vector components to integers. + * + * Note: If you're using this for performance-sensitive code, and you're guaranteed to be supplying ints in the + * specified vector, consider using {@link getBlockAt} instead for better performance. + * + * @param bool $cached Whether to use the block cache for getting the block (faster, but may be inaccurate) + * @param bool $addToCache Whether to cache the block object created by this method call. + */ + public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{ + return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache); + } + + /** + * Gets the Block object at the specified coordinates. + * + * Note for plugin developers: If you are using this method a lot (thousands of times for many positions for + * example), you may want to set addToCache to false to avoid using excessive amounts of memory. + * + * @param bool $cached Whether to use the block cache for getting the block (faster, but may be inaccurate) + * @param bool $addToCache Whether to cache the block object created by this method call. + */ + public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{ + $relativeBlockHash = null; + $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE); + + if($this->isInWorld($x, $y, $z)){ + $relativeBlockHash = World::chunkBlockHash($x, $y, $z); + + if($cached and isset($this->blockCache[$chunkHash][$relativeBlockHash])){ + return $this->blockCache[$chunkHash][$relativeBlockHash]; + } + + $chunk = $this->chunks[$chunkHash] ?? null; + if($chunk !== null){ + $block = BlockFactory::getInstance()->fromFullBlock($chunk->getFullBlock($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK)); + }else{ + $addToCache = false; + $block = VanillaBlocks::AIR(); + } + }else{ + $block = VanillaBlocks::AIR(); + } + + $block->position($this, $x, $y, $z); + + static $dynamicStateRead = false; + + if($dynamicStateRead){ + //this call was generated by a parent getBlock() call calculating dynamic stateinfo + //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated). + //this ensures that it's impossible for dynamic state properties to recursively depend on each other. + $addToCache = false; + }else{ + $dynamicStateRead = true; + $block->readStateFromWorld(); + $dynamicStateRead = false; + } + + if($addToCache and $relativeBlockHash !== null){ + $this->blockCache[$chunkHash][$relativeBlockHash] = $block; + } + + return $block; + } + + /** + * Sets the block at the given Vector3 coordinates. + * + * @throws \InvalidArgumentException if the position is out of the world bounds + */ + public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{ + $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update); + } + + /** + * Sets the block at the given coordinates. + * + * If $update is true, it'll get the neighbour blocks (6 sides) and update them, and also update local lighting. + * If you are doing big changes, you might want to set this to false, then update manually. + * + * @throws \InvalidArgumentException if the position is out of the world bounds + */ + public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{ + if(!$this->isInWorld($x, $y, $z)){ + throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds"); + } + $chunkX = $x >> Chunk::COORD_BIT_SIZE; + $chunkZ = $z >> Chunk::COORD_BIT_SIZE; + 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(); + + $this->unlockChunk($chunkX, $chunkZ, null); + + $block = clone $block; + + $block->position($this, $x, $y, $z); + $block->writeStateToWorld(); + $pos = new Vector3($x, $y, $z); + + $chunkHash = World::chunkHash($chunkX, $chunkZ); + $relativeBlockHash = World::chunkBlockHash($x, $y, $z); + + unset($this->blockCache[$chunkHash][$relativeBlockHash]); + + if(!isset($this->changedBlocks[$chunkHash])){ + $this->changedBlocks[$chunkHash] = []; + } + $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos; + + foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){ + $listener->onBlockChanged($pos); + } + + if($update){ + $this->updateAllLight($x, $y, $z); + $this->tryAddToNeighbourUpdateQueue($pos); + foreach($pos->sides() as $side){ + $this->tryAddToNeighbourUpdateQueue($side); + } + } + + $this->timings->setBlock->stopTiming(); + } + + public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{ + if($item->isNull()){ + return null; + } + + $itemEntity = new ItemEntity(Location::fromObject($source, $this, lcg_value() * 360, 0), $item); + + $itemEntity->setPickupDelay($delay); + $itemEntity->setMotion($motion ?? new Vector3(lcg_value() * 0.2 - 0.1, 0.2, lcg_value() * 0.2 - 0.1)); + $itemEntity->spawnToAll(); + + return $itemEntity; + } + + /** + * Drops XP orbs into the world for the specified amount, splitting the amount into several orbs if necessary. + * + * @return ExperienceOrb[] + */ + public function dropExperience(Vector3 $pos, int $amount) : array{ + /** @var ExperienceOrb[] $orbs */ + $orbs = []; + + foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){ + $orb = new ExperienceOrb(Location::fromObject($pos, $this, lcg_value() * 360, 0), $split); + + $orb->setMotion(new Vector3((lcg_value() * 0.2 - 0.1) * 2, lcg_value() * 0.4, (lcg_value() * 0.2 - 0.1) * 2)); + $orb->spawnToAll(); + + $orbs[] = $orb; + } + + return $orbs; + } + + /** + * Tries to break a block using a item, including Player time checks if available + * It'll try to lower the durability if Item is a tool, and set it to Air if broken. + * + * @param Item $item reference parameter (if null, can break anything) + */ + public function useBreakOn(Vector3 $vector, Item &$item = null, ?Player $player = null, bool $createParticles = false) : bool{ + $vector = $vector->floor(); + + $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE; + $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE; + if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){ + return false; + } + + $target = $this->getBlock($vector); + $affectedBlocks = $target->getAffectedBlocks(); + + if($item === null){ + $item = ItemFactory::air(); + } + + $drops = []; + if($player === null or $player->hasFiniteResources()){ + $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks)); + } + + $xpDrop = 0; + if($player !== null and $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()){ + $ev->cancel(); + } + + if($player->isAdventure(true) and !$ev->isCancelled()){ + $canBreak = false; + $itemParser = LegacyStringToItemParser::getInstance(); + foreach($item->getCanDestroy() as $v){ + $entry = $itemParser->parse($v); + if($entry->getBlock()->isSameType($target)){ + $canBreak = true; + break; + } + } + + if(!$canBreak){ + $ev->cancel(); + } + } + + $ev->call(); + if($ev->isCancelled()){ + return false; + } + + $drops = $ev->getDrops(); + $xpDrop = $ev->getXpDropAmount(); + + }elseif(!$target->getBreakInfo()->isBreakable()){ + return false; + } + + foreach($affectedBlocks as $t){ + $this->destroyBlockInternal($t, $item, $player, $createParticles); + } + + $item->onDestroyBlock($target); + + if(count($drops) > 0){ + $dropPos = $vector->add(0.5, 0.5, 0.5); + foreach($drops as $drop){ + if(!$drop->isNull()){ + $this->dropItem($dropPos, $drop); + } + } + } + + if($xpDrop > 0){ + $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop); + } + + return true; + } + + private function destroyBlockInternal(Block $target, Item $item, ?Player $player = null, bool $createParticles = false) : void{ + if($createParticles){ + $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target)); + } + + $target->onBreak($item, $player); + + $tile = $this->getTile($target->getPosition()); + if($tile !== null){ + $tile->onBlockDestroyed(); + } + } + + /** + * Uses a item on a position and face, placing it or activating the block + * + * @param Player|null $player default null + * @param bool $playSound Whether to play a block-place sound if the block was placed successfully. + */ + public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false) : bool{ + $blockClicked = $this->getBlock($vector); + $blockReplace = $blockClicked->getSide($face); + + if($clickVector === null){ + $clickVector = new Vector3(0.0, 0.0, 0.0); + } + + if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){ + //TODO: build height limit messages for custom world heights and mcregion cap + return false; + } + $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE; + $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE; + if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){ + return false; + } + + if($blockClicked->getId() === BlockLegacyIds::AIR){ + return false; + } + + if($player !== null){ + $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK); + if($player->isSpectator()){ + $ev->cancel(); //set it to cancelled so plugins can bypass this + } + + $ev->call(); + if(!$ev->isCancelled()){ + if((!$player->isSneaking() or $item->isNull()) and $blockClicked->onInteract($item, $face, $clickVector, $player)){ + return true; + } + + $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector); + if(!$result->equals(ItemUseResult::NONE())){ + return $result->equals(ItemUseResult::SUCCESS()); + } + }else{ + return false; + } + }elseif($blockClicked->onInteract($item, $face, $clickVector, $player)){ + return true; + } + + if($item->isNull() or !$item->canBePlaced()){ + return false; + } + $hand = $item->getBlock($face); + $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z); + + if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){ + $blockReplace = $blockClicked; + $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z); + }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){ + return false; + } + + $tx = new BlockTransaction($this); + if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){ + return false; + } + + foreach($tx->getBlocks() as [$x, $y, $z, $block]){ + $block->position($this, $x, $y, $z); + foreach($block->getCollisionBoxes() as $collisionBox){ + if(count($this->getCollidingEntities($collisionBox)) > 0){ + return false; //Entity in block + } + } + } + + if($player !== null){ + $ev = new BlockPlaceEvent($player, $hand, $blockReplace, $blockClicked, $item); + if($player->isSpectator()){ + $ev->cancel(); + } + + if($player->isAdventure(true) and !$ev->isCancelled()){ + $canPlace = false; + $itemParser = LegacyStringToItemParser::getInstance(); + foreach($item->getCanPlaceOn() as $v){ + $entry = $itemParser->parse($v); + if($entry->getBlock()->isSameType($blockClicked)){ + $canPlace = true; + break; + } + } + + if(!$canPlace){ + $ev->cancel(); + } + } + + $ev->call(); + if($ev->isCancelled()){ + return false; + } + } + + if(!$tx->apply()){ + return false; + } + foreach($tx->getBlocks() as [$x, $y, $z, $_]){ + $tile = $this->getTileAt($x, $y, $z); + if($tile !== null){ + //TODO: seal this up inside block placement + $tile->copyDataFromItem($item); + } + + $this->getBlockAt($x, $y, $z)->onPostPlace(); + } + + if($playSound){ + $this->addSound($hand->getPosition(), new BlockPlaceSound($hand)); + } + + $item->pop(); + + return true; + } + + public function getEntity(int $entityId) : ?Entity{ + return $this->entities[$entityId] ?? null; + } + + /** + * Gets the list of all the entities in this world + * + * @return Entity[] + */ + public function getEntities() : array{ + return $this->entities; + } + + /** + * Returns the entities colliding the current one inside the AxisAlignedBB + * + * @return Entity[] + */ + public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{ + $nearby = []; + + if($entity === null or $entity->canCollide){ + foreach($this->getNearbyEntities($bb, $entity) as $ent){ + if($ent->canBeCollidedWith() and ($entity === null or $entity->canCollideWith($ent))){ + $nearby[] = $ent; + } + } + } + + return $nearby; + } + + /** + * Returns the entities near the current one inside the AxisAlignedBB + * + * @return Entity[] + */ + public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{ + $nearby = []; + + $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE; + $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE; + $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE; + $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE; + + for($x = $minX; $x <= $maxX; ++$x){ + for($z = $minZ; $z <= $maxZ; ++$z){ + if(!$this->isChunkLoaded($x, $z)){ + continue; + } + foreach($this->getChunkEntities($x, $z) as $ent){ + if($ent !== $entity and $ent->boundingBox->intersectsWith($bb)){ + $nearby[] = $ent; + } + } + } + } + + return $nearby; + } + + /** + * Returns the closest Entity to the specified position, within the given radius. + * + * @param string $entityType Class of entity to use for instanceof + * @param bool $includeDead Whether to include entitites which are dead + * @phpstan-template TEntity of Entity + * @phpstan-param class-string $entityType + * + * @return Entity|null an entity of type $entityType, or null if not found + * @phpstan-return TEntity + */ + public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{ + assert(is_a($entityType, Entity::class, true)); + + $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE; + $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE; + $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE; + $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE; + + $currentTargetDistSq = $maxDistance ** 2; + + /** + * @var Entity|null $currentTarget + * @phpstan-var TEntity|null $currentTarget + */ + $currentTarget = null; + + for($x = $minX; $x <= $maxX; ++$x){ + for($z = $minZ; $z <= $maxZ; ++$z){ + if(!$this->isChunkLoaded($x, $z)){ + continue; + } + foreach($this->getChunkEntities($x, $z) as $entity){ + if(!($entity instanceof $entityType) or $entity->isFlaggedForDespawn() or (!$includeDead and !$entity->isAlive())){ + continue; + } + $distSq = $entity->getPosition()->distanceSquared($pos); + if($distSq < $currentTargetDistSq){ + $currentTargetDistSq = $distSq; + $currentTarget = $entity; + } + } + } + } + + return $currentTarget; + } + + /** + * Returns a list of the players in this world + * + * @return Player[] + */ + public function getPlayers() : array{ + return $this->players; + } + + /** + * Returns the Tile in a position, or null if not found. + * + * Note: This method wraps getTileAt(). If you're guaranteed to be passing integers, and you're using this method + * in performance-sensitive code, consider using getTileAt() instead of this method for better performance. + */ + public function getTile(Vector3 $pos) : ?Tile{ + return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z)); + } + + /** + * Returns the tile at the specified x,y,z coordinates, or null if it does not exist. + */ + public function getTileAt(int $x, int $y, int $z) : ?Tile{ + return ($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null ? $chunk->getTile($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK) : null; + } + + public function getBiomeId(int $x, int $z) : int{ + if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){ + return $chunk->getBiomeId($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK); + } + return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet) + } + + public function getBiome(int $x, int $z) : Biome{ + return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $z)); + } + + public function setBiomeId(int $x, int $z, int $biomeId) : void{ + $chunkX = $x >> Chunk::COORD_BIT_SIZE; + $chunkZ = $z >> Chunk::COORD_BIT_SIZE; + $this->unlockChunk($chunkX, $chunkZ, null); + if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){ + $chunk->setBiomeId($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId); + }else{ + //if we allowed this, the modifications would be lost when the chunk is created + throw new WorldException("Cannot set biome in a non-generated chunk"); + } + } + + /** + * @return Chunk[] + */ + public function getLoadedChunks() : array{ + return $this->chunks; + } + + public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{ + return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null; + } + + /** + * @return Entity[] + */ + public function getChunkEntities(int $chunkX, int $chunkZ) : array{ + return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? []; + } + + /** + * Returns the chunk containing the given Vector3 position. + */ + public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{ + return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE); + } + + /** + * Returns the chunks adjacent to the specified chunk. + * + * @return Chunk[]|null[] + */ + public function getAdjacentChunks(int $x, int $z) : array{ + $result = []; + for($xx = -1; $xx <= 1; ++$xx){ + for($zz = -1; $zz <= 1; ++$zz){ + if($xx === 0 && $zz === 0){ + continue; //center chunk + } + $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz); + } + } + + return $result; + } + + /** + * 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] = $lockId; + } + + /** + * 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)]); + } + + 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){ + $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; + + unset($this->blockCache[$chunkHash]); + unset($this->changedBlocks[$chunkHash]); + $chunk->setTerrainDirty(); + + if(!$this->isChunkInUse($chunkX, $chunkZ)){ + $this->unloadChunkRequest($chunkX, $chunkZ); + } + + if($oldChunk === null){ + (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call(); + + foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){ + $listener->onChunkLoaded($chunkX, $chunkZ, $chunk); + } + }else{ + foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){ + $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(); + } + } + } + } + + /** + * Gets the highest block Y value at a specific $x and $z + * + * @return int|null 0-255, or null if the column is empty + * @throws WorldException if the terrain is not generated + */ + public function getHighestBlockAt(int $x, int $z) : ?int{ + if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){ + return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK); + } + throw new WorldException("Cannot get highest block in an ungenerated chunk"); + } + + /** + * Returns whether the given position is in a loaded area of terrain. + */ + public function isInLoadedTerrain(Vector3 $pos) : bool{ + return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE); + } + + public function isChunkLoaded(int $x, int $z) : bool{ + return isset($this->chunks[World::chunkHash($x, $z)]); + } + + public function isChunkGenerated(int $x, int $z) : bool{ + return $this->loadChunk($x, $z) !== null; + } + + public function isChunkPopulated(int $x, int $z) : bool{ + $chunk = $this->loadChunk($x, $z); + return $chunk !== null ? $chunk->isPopulated() : false; + } + + /** + * Returns a Position pointing to the spawn + */ + public function getSpawnLocation() : Position{ + return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this); + } + + /** + * Sets the world spawn location + */ + public function setSpawnLocation(Vector3 $pos) : void{ + $previousSpawn = $this->getSpawnLocation(); + $this->provider->getWorldData()->setSpawn($pos); + (new SpawnChangeEvent($this, $previousSpawn))->call(); + } + + /** + * @throws \InvalidArgumentException + */ + public function addEntity(Entity $entity) : void{ + if($entity->isClosed()){ + throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world"); + } + if($entity->getWorld() !== $this){ + throw new \InvalidArgumentException("Invalid Entity world"); + } + if(array_key_exists($entity->getId(), $this->entities)){ + if($this->entities[$entity->getId()] === $entity){ + throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world"); + }else{ + throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId()); + } + } + $pos = $entity->getPosition()->asVector3(); + $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity; + $this->entityLastKnownPositions[$entity->getId()] = $pos; + + if($entity instanceof Player){ + $this->players[$entity->getId()] = $entity; + } + $this->entities[$entity->getId()] = $entity; + } + + /** + * Removes the entity from the world index + * + * @throws \InvalidArgumentException + */ + public function removeEntity(Entity $entity) : void{ + if($entity->getWorld() !== $this){ + throw new \InvalidArgumentException("Invalid Entity world"); + } + if(!array_key_exists($entity->getId(), $this->entities)){ + throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)"); + } + $pos = $this->entityLastKnownPositions[$entity->getId()]; + $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE); + if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){ + unset($this->entitiesByChunk[$chunkHash][$entity->getId()]); + if(count($this->entitiesByChunk[$chunkHash]) === 0){ + unset($this->entitiesByChunk[$chunkHash]); + } + } + unset($this->entityLastKnownPositions[$entity->getId()]); + + if($entity instanceof Player){ + unset($this->players[$entity->getId()]); + $this->checkSleep(); + } + + unset($this->entities[$entity->getId()]); + unset($this->updateEntities[$entity->getId()]); + } + + /** + * @internal + */ + public function onEntityMoved(Entity $entity) : void{ + if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){ + //this can happen if the entity was teleported before addEntity() was called + return; + } + $oldPosition = $this->entityLastKnownPositions[$entity->getId()]; + $newPosition = $entity->getPosition(); + + $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE; + $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE; + $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE; + $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE; + + if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){ + $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ); + if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){ + unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]); + if(count($this->entitiesByChunk[$oldChunkHash]) === 0){ + unset($this->entitiesByChunk[$oldChunkHash]); + } + } + + $newViewers = $this->getViewersForPosition($newPosition); + foreach($entity->getViewers() as $player){ + if(!isset($newViewers[spl_object_id($player)])){ + $entity->despawnFrom($player); + }else{ + unset($newViewers[spl_object_id($player)]); + } + } + foreach($newViewers as $player){ + $entity->spawnTo($player); + } + + $newChunkHash = World::chunkHash($newChunkX, $newChunkZ); + $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity; + } + $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3(); + } + + /** + * @internal Tiles are now bound with blocks, and their creation is automatic. They should not be directly added. + * @throws \InvalidArgumentException + */ + public function addTile(Tile $tile) : void{ + if($tile->isClosed()){ + throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world"); + } + $pos = $tile->getPosition(); + if(!$pos->isValid() || $pos->getWorld() !== $this){ + throw new \InvalidArgumentException("Invalid Tile world"); + } + + $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE; + $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE; + + if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){ + $this->chunks[$hash]->addTile($tile); + }else{ + throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ"); + } + + //delegate tile ticking to the corresponding block + $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1); + } + + /** + * @internal Tiles are now bound with blocks, and their removal is automatic. They should not be directly removed. + * @throws \InvalidArgumentException + */ + public function removeTile(Tile $tile) : void{ + $pos = $tile->getPosition(); + if(!$pos->isValid() || $pos->getWorld() !== $this){ + throw new \InvalidArgumentException("Invalid Tile world"); + } + + $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE; + $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE; + + if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){ + $this->chunks[$hash]->removeTile($tile); + } + foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){ + $listener->onBlockChanged($pos->asVector3()); + } + } + + public function isChunkInUse(int $x, int $z) : bool{ + return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) and count($this->chunkLoaders[$index]) > 0; + } + + /** + * Attempts to load a chunk from the world provider (if not already loaded). If the chunk is already loaded, it is + * returned directly. + * + * @return Chunk|null the requested chunk, or null on failure. + */ + public function loadChunk(int $x, int $z) : ?Chunk{ + if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){ + return $this->chunks[$chunkHash]; + } + + $this->timings->syncChunkLoad->startTiming(); + + $this->cancelUnloadChunkRequest($x, $z); + + $this->timings->syncChunkLoadData->startTiming(); + + $chunk = null; + + try{ + $chunk = $this->provider->loadChunk($x, $z); + }catch(CorruptedChunkException $e){ + $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage()); + } + + $this->timings->syncChunkLoadData->stopTiming(); + + if($chunk === null){ + $this->timings->syncChunkLoad->stopTiming(); + return null; + } + + $this->chunks[$chunkHash] = $chunk->getChunk(); + unset($this->blockCache[$chunkHash]); + + $this->initChunk($x, $z, $chunk); + + (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call(); + + if(!$this->isChunkInUse($x, $z)){ + $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity"); + $this->unloadChunkRequest($x, $z); + } + foreach($this->getChunkListeners($x, $z) as $listener){ + $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]); + } + + $this->timings->syncChunkLoad->stopTiming(); + + return $this->chunks[$chunkHash]; + } + + 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(SavedDataLoadingException $e){ + $logger->error("Bad entity data at list position $k: " . $e->getMessage()); + $logger->logException($e); + continue; + } + if($entity === null){ + $saveIdTag = $nbt->getTag("id") ?? $nbt->getTag("identifier"); + $saveId = ""; + if($saveIdTag instanceof StringTag){ + $saveId = $saveIdTag->getValue(); + }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format + $saveId = "legacy(" . $saveIdTag->getValue() . ")"; + } + $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 + } + + $this->timings->syncChunkLoadEntities->stopTiming(); + } + + if(count($chunkData->getTileNBT()) !== 0){ + $this->timings->syncChunkLoadTileEntities->startTiming(); + $tileFactory = TileFactory::getInstance(); + foreach($chunkData->getTileNBT() as $k => $nbt){ + try{ + $tile = $tileFactory->createFromData($this, $nbt); + }catch(SavedDataLoadingException $e){ + $logger->error("Bad tile entity data at list position $k: " . $e->getMessage()); + $logger->logException($e); + continue; + } + if($tile === null){ + $logger->warning("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)){ + $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded"); + }elseif($this->getTile($tilePosition = $tile->getPosition()) !== 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); + } + } + + $this->timings->syncChunkLoadTileEntities->stopTiming(); + } + } + + private function queueUnloadChunk(int $x, int $z) : void{ + $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true); + } + + public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{ + if(($safe and $this->isChunkInUse($x, $z)) or $this->isSpawnChunk($x, $z)){ + return false; + } + + $this->queueUnloadChunk($x, $z); + + return true; + } + + public function cancelUnloadChunkRequest(int $x, int $z) : void{ + unset($this->unloadQueue[World::chunkHash($x, $z)]); + } + + public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{ + if($safe and $this->isChunkInUse($x, $z)){ + return false; + } + + if(!$this->isChunkLoaded($x, $z)){ + return true; + } + + $this->timings->doChunkUnload->startTiming(); + + $chunkHash = World::chunkHash($x, $z); + + $chunk = $this->chunks[$chunkHash] ?? null; + + if($chunk !== null){ + $ev = new ChunkUnloadEvent($this, $x, $z, $chunk); + $ev->call(); + if($ev->isCancelled()){ + $this->timings->doChunkUnload->stopTiming(); + + return false; + } + + if($trySave and $this->getAutoSave()){ + $this->timings->syncChunkSave->startTiming(); + try{ + $this->provider->saveChunk($x, $z, new ChunkData( + $chunk, + array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk())), + array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()), + )); + }finally{ + $this->timings->syncChunkSave->stopTiming(); + } + } + + foreach($this->getChunkListeners($x, $z) as $listener){ + $listener->onChunkUnloaded($x, $z, $chunk); + } + + foreach($this->getChunkEntities($x, $z) as $entity){ + if($entity instanceof Player){ + continue; + } + $entity->close(); + } + + $chunk->onUnload(); + } + + unset($this->chunks[$chunkHash]); + unset($this->blockCache[$chunkHash]); + unset($this->changedBlocks[$chunkHash]); + + if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){ + $this->chunkPopulationRequestMap[$chunkHash]->reject(); + unset($this->chunkPopulationRequestMap[$chunkHash]); + } + + $this->timings->doChunkUnload->stopTiming(); + + return true; + } + + /** + * Returns whether the chunk at the specified coordinates is a spawn chunk + */ + public function isSpawnChunk(int $X, int $Z) : bool{ + $spawn = $this->getSpawnLocation(); + $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE; + $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE; + + return abs($X - $spawnX) <= 1 and 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){ + $spawn = $this->getSpawnLocation(); + } + + $max = $this->maxY; + $v = $spawn->floor(); + $chunk = $this->getOrLoadChunkAtPosition($v); + if($chunk === null){ + throw new WorldException("Cannot find a safe spawn point in non-generated terrain"); + } + $x = (int) $v->x; + $z = (int) $v->z; + $y = (int) min($max - 2, $v->y); + $wasAir = $this->getBlockAt($x, $y - 1, $z)->getId() === BlockLegacyIds::AIR; //TODO: bad hack, clean up + for(; $y > $this->minY; --$y){ + if($this->getBlockAt($x, $y, $z)->isFullCube()){ + if($wasAir){ + $y++; + break; + } + }else{ + $wasAir = true; + } + } + + for(; $y >= $this->minY and $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); + } + }else{ + ++$y; + } + } + + return new Position($spawn->x, $y, $spawn->z, $this); + } + + /** + * Gets the current time + */ + public function getTime() : int{ + return $this->time; + } + + /** + * Returns the current time of day + */ + public function getTimeOfDay() : int{ + return $this->time % self::TIME_FULL; + } + + /** + * Returns the World display name. + * WARNING: This is NOT guaranteed to be unique. Multiple worlds at runtime may share the same display name. + */ + public function getDisplayName() : string{ + return $this->displayName; + } + + /** + * Returns the World folder name. This will not change at runtime and will be unique to a world per runtime. + */ + public function getFolderName() : string{ + return $this->folderName; + } + + /** + * Sets the current time on the world + */ + public function setTime(int $time) : void{ + $this->time = $time; + $this->sendTime(); + } + + /** + * Stops the time for the world, will not save the lock state to disk + */ + public function stopTime() : void{ + $this->stopTime = true; + $this->sendTime(); + } + + /** + * Start the time again, if it was stopped + */ + public function startTime() : void{ + $this->stopTime = false; + $this->sendTime(); + } + + /** + * Gets the world seed + */ + public function getSeed() : int{ + return $this->provider->getWorldData()->getSeed(); + } + + public function getMinY() : int{ + return $this->minY; + } + + public function getMaxY() : int{ + return $this->maxY; + } + + public function getDifficulty() : int{ + return $this->provider->getWorldData()->getDifficulty(); + } + + public function setDifficulty(int $difficulty) : void{ + if($difficulty < 0 or $difficulty > 3){ + throw new \InvalidArgumentException("Invalid difficulty level $difficulty"); + } + $this->provider->getWorldData()->setDifficulty($difficulty); + + foreach($this->players as $player){ + $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty()); + } + } + + 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->addChunkHashToPopulationRequestQueue($chunkHash); + $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver(); + if($associatedChunkLoader === null){ + $temporaryLoader = new class implements ChunkLoader{}; + $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ); + $resolver->getPromise()->onCompletion( + fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ), + static function() : void{} + ); + } + 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]; + } + + /** + * Attempts to initiate asynchronous generation/population of the target chunk, if it's currently reasonable to do + * so (and if it isn't already generated/populated). + * If the generator is busy, the request will be put into a queue and delayed until a better time. + * + * A ChunkLoader can be associated with the generation request to ensure that the generation request is cancelled if + * no loaders are attached to the target chunk. If no loader is provided, one will be assigned (and automatically + * removed when the generation request completes). + * + * @phpstan-return Promise + */ + public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : 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 $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader); + } + return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver); + } + + /** + * Initiates asynchronous generation/population of the target chunk, if it's not already generated/populated. + * If generation has already been requested for the target chunk, the promise for the already active request will be + * returned directly. + * + * If the chunk is currently locked (for example due to another chunk using it for async generation), the request + * will be queued and executed at the earliest opportunity. + * + * @phpstan-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($chunkX + $xx, $chunkZ + $zz)){ + //chunk is already in use by another generation request; queue the request for later + return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader); + } + } + } + + $this->activeChunkPopulationTasks[$chunkHash] = true; + if($resolver === null){ + $resolver = new PromiseResolver(); + $this->chunkPopulationRequestMap[$chunkHash] = $resolver; + } + + $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 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); + } + } + }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{ + $this->timings->doChunkGC->startTiming(); + + foreach($this->chunks as $index => $chunk){ + if(!isset($this->unloadQueue[$index])){ + World::getXZ($index, $X, $Z); + if(!$this->isSpawnChunk($X, $Z)){ + $this->unloadChunkRequest($X, $Z, true); + } + } + $chunk->collectGarbage(); + } + + $this->provider->doGarbageCollection(); + + $this->timings->doChunkGC->stopTiming(); + } + + public function unloadChunks(bool $force = false) : void{ + if(count($this->unloadQueue) > 0){ + $maxUnload = 96; + $now = microtime(true); + foreach($this->unloadQueue as $index => $time){ + World::getXZ($index, $X, $Z); + + if(!$force){ + if($maxUnload <= 0){ + break; + }elseif($time > ($now - 30)){ + continue; + } + } + + //If the chunk can't be unloaded, it stays on the queue + if($this->unloadChunk($X, $Z, true)){ + unset($this->unloadQueue[$index]); + --$maxUnload; + } + } + } + } +} diff --git a/src/world/WorldCreationOptions.php b/src/world/WorldCreationOptions.php new file mode 100644 index 0000000000..87304dd5ef --- /dev/null +++ b/src/world/WorldCreationOptions.php @@ -0,0 +1,98 @@ + */ + private string $generatorClass = Normal::class; + private int $seed; + private int $difficulty = World::DIFFICULTY_NORMAL; + private string $generatorOptions = ""; + private Vector3 $spawnPosition; + + public function __construct(){ + $this->seed = random_int(Limits::INT32_MIN, Limits::INT32_MAX); + $this->spawnPosition = new Vector3(256, 70, 256); + } + + public static function create() : self{ + return new self; + } + + /** @phpstan-return class-string */ + public function getGeneratorClass() : string{ return $this->generatorClass; } + + /** + * @phpstan-param class-string $generatorClass + * @return $this + */ + public function setGeneratorClass(string $generatorClass) : self{ + Utils::testValidInstance($generatorClass, Generator::class); + $this->generatorClass = $generatorClass; + return $this; + } + + public function getSeed() : int{ return $this->seed; } + + /** @return $this */ + public function setSeed(int $seed) : self{ + $this->seed = $seed; + return $this; + } + + public function getDifficulty() : int{ return $this->difficulty; } + + /** @return $this */ + public function setDifficulty(int $difficulty) : self{ + $this->difficulty = $difficulty; + return $this; + } + + public function getGeneratorOptions() : string{ return $this->generatorOptions; } + + /** @return $this */ + public function setGeneratorOptions(string $generatorOptions) : self{ + $this->generatorOptions = $generatorOptions; + return $this; + } + + public function getSpawnPosition() : Vector3{ return $this->spawnPosition; } + + /** @return $this */ + public function setSpawnPosition(Vector3 $spawnPosition) : self{ + $this->spawnPosition = $spawnPosition; + return $this; + } +} diff --git a/src/pocketmine/level/LevelException.php b/src/world/WorldException.php similarity index 91% rename from src/pocketmine/level/LevelException.php rename to src/world/WorldException.php index cda361f862..73966649f4 100644 --- a/src/pocketmine/level/LevelException.php +++ b/src/world/WorldException.php @@ -21,10 +21,10 @@ declare(strict_types=1); -namespace pocketmine\level; +namespace pocketmine\world; use pocketmine\utils\ServerException; -class LevelException extends ServerException{ +class WorldException extends ServerException{ } diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php new file mode 100644 index 0000000000..aa906e4ab4 --- /dev/null +++ b/src/world/WorldManager.php @@ -0,0 +1,415 @@ +server = $server; + $this->dataPath = $dataPath; + $this->providerManager = $providerManager; + } + + public function getProviderManager() : WorldProviderManager{ + return $this->providerManager; + } + + /** + * @return World[] + */ + public function getWorlds() : array{ + return $this->worlds; + } + + public function getDefaultWorld() : ?World{ + return $this->defaultWorld; + } + + /** + * Sets the default world to a different world + * This won't change the level-name property, + * 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)){ + $this->defaultWorld = $world; + } + } + + public function isWorldLoaded(string $name) : bool{ + return $this->getWorldByName($name) instanceof World; + } + + public function getWorld(int $worldId) : ?World{ + return $this->worlds[$worldId] ?? null; + } + + /** + * NOTE: This matches worlds based on the FOLDER name, NOT the display name. + */ + public function getWorldByName(string $name) : ?World{ + foreach($this->worlds as $world){ + if($world->getFolderName() === $name){ + return $world; + } + } + + return null; + } + + /** + * @throws \InvalidArgumentException + */ + public function unloadWorld(World $world, bool $forceUnload = false) : bool{ + if($world === $this->getDefaultWorld() and !$forceUnload){ + throw new \InvalidArgumentException("The default world cannot be unloaded while running, please switch worlds."); + } + if($world->isDoingTick()){ + throw new \InvalidArgumentException("Cannot unload a world during world tick"); + } + + $ev = new WorldUnloadEvent($world); + if($world === $this->defaultWorld and !$forceUnload){ + $ev->cancel(); + } + + $ev->call(); + + if(!$forceUnload and $ev->isCancelled()){ + return false; + } + + $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_unloading($world->getDisplayName()))); + try{ + $safeSpawn = $this->defaultWorld !== null ? $this->defaultWorld->getSafeSpawn() : null; + }catch(WorldException $e){ + $safeSpawn = null; + } + foreach($world->getPlayers() as $player){ + if($world === $this->defaultWorld or $safeSpawn === null){ + $player->disconnect("Forced default world unload"); + }else{ + $player->teleport($safeSpawn); + } + } + + if($world === $this->defaultWorld){ + $this->defaultWorld = null; + } + unset($this->worlds[$world->getId()]); + + $world->onUnload(); + return true; + } + + /** + * Loads a world from the data directory + * + * @param bool $autoUpgrade Converts worlds to the default format if the world's format is not writable / deprecated + * + * @throws WorldException + */ + public function loadWorld(string $name, bool $autoUpgrade = false) : bool{ + if(trim($name) === ""){ + throw new \InvalidArgumentException("Invalid empty world name"); + } + if($this->isWorldLoaded($name)){ + return true; + }elseif(!$this->isWorldGenerated($name)){ + return false; + } + + $path = $this->getWorldPath($name); + + $providers = $this->providerManager->getMatchingProviders($path); + if(count($providers) !== 1){ + $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError( + $name, + count($providers) === 0 ? + KnownTranslationFactory::pocketmine_level_unknownFormat() : + KnownTranslationFactory::pocketmine_level_ambiguousFormat(implode(", ", array_keys($providers))) + ))); + return false; + } + $providerClass = array_shift($providers); + + try{ + $provider = $providerClass->fromPath($path); + }catch(CorruptedWorldException $e){ + $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, + 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{ + $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($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($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_finish($name, $converter->getBackupPath()))); + } + + $world = new World($this->server, $name, $provider, $this->server->getAsyncPool()); + + $this->worlds[$world->getId()] = $world; + $world->setAutoSave($this->autoSave); + + (new WorldLoadEvent($world))->call(); + + return true; + } + + /** + * Generates a new world if it does not exist + * + * @throws \InvalidArgumentException + */ + public function generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration = true) : bool{ + if(trim($name) === "" or $this->isWorldGenerated($name)){ + return false; + } + + $providerEntry = $this->providerManager->getDefault(); + + $path = $this->getWorldPath($name); + $providerEntry->generate($path, $name, $options); + + $world = new World($this->server, $name, $providerEntry->fromPath($path), $this->server->getAsyncPool()); + $this->worlds[$world->getId()] = $world; + + $world->setAutoSave($this->autoSave); + + (new WorldInitEvent($world))->call(); + + (new WorldLoadEvent($world))->call(); + + if($backgroundGeneration){ + $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_backgroundGeneration($name))); + + $spawnLocation = $world->getSpawnLocation(); + $centerX = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE; + $centerZ = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE; + + $selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ)); + $done = 0; + $total = count($selected); + foreach($selected as $index){ + World::getXZ($index, $chunkX, $chunkZ); + $world->orderChunkPopulation($chunkX, $chunkZ, null)->onCompletion( + static function() use ($world, &$done, $total) : void{ + $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($world->getServer()->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_spawnTerrainGenerationProgress(strval($done), strval($total), strval($newProgress)))); + } + }, + static function() : void{ + //NOOP: we don't care if the world was unloaded + }); + } + } + + return true; + } + + private function getWorldPath(string $name) : string{ + return Path::join($this->dataPath, $name) . "/"; //TODO: check if we still need the trailing dirsep (I'm a little scared to remove it) + } + + public function isWorldGenerated(string $name) : bool{ + if(trim($name) === ""){ + return false; + } + $path = $this->getWorldPath($name); + if(!($this->getWorldByName($name) instanceof World)){ + return count($this->providerManager->getMatchingProviders($path)) > 0; + } + + return true; + } + + /** + * Searches all worlds for the entity with the specified ID. + * Useful for tracking entities across multiple worlds without needing strong references. + */ + public function findEntity(int $entityId) : ?Entity{ + foreach($this->worlds as $world){ + assert($world->isLoaded()); + if(($entity = $world->getEntity($entityId)) instanceof Entity){ + return $entity; + } + } + + return null; + } + + public function tick(int $currentTick) : void{ + foreach($this->worlds as $k => $world){ + if(!isset($this->worlds[$k])){ + // World unloaded during the tick of a world earlier in this loop, perhaps by plugin + continue; + } + + $worldTime = microtime(true); + $world->doTick($currentTick); + $tickMs = (microtime(true) - $worldTime) * 1000; + $world->tickRateTime = $tickMs; + if($tickMs >= 50){ + $world->getLogger()->debug(sprintf("Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / 50, 2))); + } + } + + if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){ + $this->autoSaveTicker = 0; + $this->server->getLogger()->debug("[Auto Save] Saving worlds..."); + $start = microtime(true); + $this->doAutoSave(); + $time = microtime(true) - $start; + $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); + } + } + + public function getAutoSave() : bool{ + return $this->autoSave; + } + + public function setAutoSave(bool $value) : void{ + $this->autoSave = $value; + foreach($this->worlds as $world){ + $world->setAutoSave($this->autoSave); + } + } + + /** + * Returns the period in ticks after which loaded worlds will be automatically saved to disk. + */ + public function getAutoSaveInterval() : int{ + return $this->autoSaveTicks; + } + + public function setAutoSaveInterval(int $autoSaveTicks) : void{ + if($autoSaveTicks <= 0){ + throw new \InvalidArgumentException("Autosave ticks must be positive"); + } + $this->autoSaveTicks = $autoSaveTicks; + } + + private function doAutoSave() : void{ + Timings::$worldSave->startTiming(); + foreach($this->worlds as $world){ + foreach($world->getPlayers() as $player){ + if($player->spawned){ + $player->save(); + } + } + $world->save(false); + } + Timings::$worldSave->stopTiming(); + } +} diff --git a/src/world/WorldTimings.php b/src/world/WorldTimings.php new file mode 100644 index 0000000000..874a5f5fdd --- /dev/null +++ b/src/world/WorldTimings.php @@ -0,0 +1,80 @@ +getFolderName() . " - "; + + $this->setBlock = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "setBlock"); + $this->doBlockLightUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Block Light Updates"); + $this->doBlockSkyLightUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Sky Light Updates"); + + $this->doChunkUnload = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Unload Chunks"); + $this->scheduledBlockUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Scheduled Block Updates"); + $this->randomChunkUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Random Chunk Updates"); + $this->randomChunkUpdatesChunkSelection = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Random Chunk Updates - Chunk Selection"); + $this->doChunkGC = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Garbage Collection"); + $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(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(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/biome/Biome.php b/src/world/biome/Biome.php new file mode 100644 index 0000000000..ea79a048e4 --- /dev/null +++ b/src/world/biome/Biome.php @@ -0,0 +1,124 @@ +populators = []; + } + + public function addPopulator(Populator $populator) : void{ + $this->populators[] = $populator; + } + + public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void{ + foreach($this->populators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $random); + } + } + + /** + * @return Populator[] + */ + public function getPopulators() : array{ + return $this->populators; + } + + public function setId(int $id) : void{ + if(!$this->registered){ + $this->registered = true; + $this->id = $id; + } + } + + public function getId() : int{ + return $this->id; + } + + abstract public function getName() : string; + + public function getMinElevation() : int{ + return $this->minElevation; + } + + public function getMaxElevation() : int{ + return $this->maxElevation; + } + + public function setElevation(int $min, int $max) : void{ + $this->minElevation = $min; + $this->maxElevation = $max; + } + + /** + * @return Block[] + */ + public function getGroundCover() : array{ + return $this->groundCover; + } + + /** + * @param Block[] $covers + */ + public function setGroundCover(array $covers) : void{ + $this->groundCover = $covers; + } + + public function getTemperature() : float{ + return $this->temperature; + } + + public function getRainfall() : float{ + return $this->rainfall; + } +} diff --git a/src/world/biome/BiomeRegistry.php b/src/world/biome/BiomeRegistry.php new file mode 100644 index 0000000000..9e7f3077b8 --- /dev/null +++ b/src/world/biome/BiomeRegistry.php @@ -0,0 +1,72 @@ + + */ + private $biomes; + + public function __construct(){ + $this->biomes = new \SplFixedArray(Biome::MAX_BIOMES); + + $this->register(BiomeIds::OCEAN, new OceanBiome()); + $this->register(BiomeIds::PLAINS, new PlainBiome()); + $this->register(BiomeIds::DESERT, new DesertBiome()); + $this->register(BiomeIds::EXTREME_HILLS, new MountainsBiome()); + $this->register(BiomeIds::FOREST, new ForestBiome()); + $this->register(BiomeIds::TAIGA, new TaigaBiome()); + $this->register(BiomeIds::SWAMPLAND, new SwampBiome()); + $this->register(BiomeIds::RIVER, new RiverBiome()); + + $this->register(BiomeIds::HELL, new HellBiome()); + + $this->register(BiomeIds::ICE_PLAINS, new IcePlainsBiome()); + + $this->register(BiomeIds::EXTREME_HILLS_EDGE, new SmallMountainsBiome()); + + $this->register(BiomeIds::BIRCH_FOREST, new ForestBiome(TreeType::BIRCH())); + } + + public function register(int $id, Biome $biome) : void{ + $this->biomes[$id] = $biome; + $biome->setId($id); + } + + public function getBiome(int $id) : Biome{ + if($this->biomes[$id] === null){ + $this->register($id, new UnknownBiome()); + } + + return $this->biomes[$id]; + } +} diff --git a/src/pocketmine/level/biome/DesertBiome.php b/src/world/biome/DesertBiome.php similarity index 96% rename from src/pocketmine/level/biome/DesertBiome.php rename to src/world/biome/DesertBiome.php index d863f47dd8..79e3cda63a 100644 --- a/src/pocketmine/level/biome/DesertBiome.php +++ b/src/world/biome/DesertBiome.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; class DesertBiome extends SandyBiome{ diff --git a/src/pocketmine/level/biome/ForestBiome.php b/src/world/biome/ForestBiome.php similarity index 68% rename from src/pocketmine/level/biome/ForestBiome.php rename to src/world/biome/ForestBiome.php index a756d21091..b791416a55 100644 --- a/src/pocketmine/level/biome/ForestBiome.php +++ b/src/world/biome/ForestBiome.php @@ -21,26 +21,23 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Sapling; -use pocketmine\level\generator\populator\TallGrass; -use pocketmine\level\generator\populator\Tree; +use pocketmine\block\utils\TreeType; +use pocketmine\world\generator\populator\TallGrass; +use pocketmine\world\generator\populator\Tree; class ForestBiome extends GrassyBiome{ - public const TYPE_NORMAL = 0; - public const TYPE_BIRCH = 1; + /** @var TreeType */ + private $type; - /** @var int */ - public $type; - - public function __construct(int $type = self::TYPE_NORMAL){ + public function __construct(?TreeType $type = null){ parent::__construct(); - $this->type = $type; + $this->type = $type ?? TreeType::OAK(); - $trees = new Tree($type === self::TYPE_BIRCH ? Sapling::BIRCH : Sapling::OAK); + $trees = new Tree($type); $trees->setBaseAmount(5); $this->addPopulator($trees); @@ -51,7 +48,7 @@ class ForestBiome extends GrassyBiome{ $this->setElevation(63, 81); - if($type === self::TYPE_BIRCH){ + if($this->type->equals(TreeType::BIRCH())){ $this->temperature = 0.6; $this->rainfall = 0.5; }else{ @@ -61,6 +58,6 @@ class ForestBiome extends GrassyBiome{ } public function getName() : string{ - return $this->type === self::TYPE_BIRCH ? "Birch Forest" : "Forest"; + return $this->type->getDisplayName() . " Forest"; } } diff --git a/src/pocketmine/level/biome/GrassyBiome.php b/src/world/biome/GrassyBiome.php similarity index 75% rename from src/pocketmine/level/biome/GrassyBiome.php rename to src/world/biome/GrassyBiome.php index ad327b6599..9520c1a2e0 100644 --- a/src/pocketmine/level/biome/GrassyBiome.php +++ b/src/world/biome/GrassyBiome.php @@ -21,20 +21,19 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; abstract class GrassyBiome extends Biome{ public function __construct(){ $this->setGroundCover([ - BlockFactory::get(Block::GRASS), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT) + VanillaBlocks::GRASS(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT() ]); } } diff --git a/src/pocketmine/level/biome/HellBiome.php b/src/world/biome/HellBiome.php similarity index 96% rename from src/pocketmine/level/biome/HellBiome.php rename to src/world/biome/HellBiome.php index 4fcb761927..38556ad258 100644 --- a/src/pocketmine/level/biome/HellBiome.php +++ b/src/world/biome/HellBiome.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; class HellBiome extends Biome{ diff --git a/src/pocketmine/level/biome/IcePlainsBiome.php b/src/world/biome/IcePlainsBiome.php similarity index 92% rename from src/pocketmine/level/biome/IcePlainsBiome.php rename to src/world/biome/IcePlainsBiome.php index 5d61c9ce67..5b7756971a 100644 --- a/src/pocketmine/level/biome/IcePlainsBiome.php +++ b/src/world/biome/IcePlainsBiome.php @@ -21,9 +21,9 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\level\generator\populator\TallGrass; +use pocketmine\world\generator\populator\TallGrass; class IcePlainsBiome extends SnowyBiome{ diff --git a/src/pocketmine/level/biome/MountainsBiome.php b/src/world/biome/MountainsBiome.php similarity index 73% rename from src/pocketmine/level/biome/MountainsBiome.php rename to src/world/biome/MountainsBiome.php index da3585b6db..47986a8aa3 100644 --- a/src/pocketmine/level/biome/MountainsBiome.php +++ b/src/world/biome/MountainsBiome.php @@ -21,10 +21,13 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\level\generator\populator\TallGrass; -use pocketmine\level\generator\populator\Tree; +use pocketmine\block\VanillaBlocks; +use pocketmine\world\generator\object\OreType; +use pocketmine\world\generator\populator\Ore; +use pocketmine\world\generator\populator\TallGrass; +use pocketmine\world\generator\populator\Tree; class MountainsBiome extends GrassyBiome{ @@ -40,7 +43,12 @@ class MountainsBiome extends GrassyBiome{ $this->addPopulator($tallGrass); - //TODO: add emerald + $ores = new Ore(); + $ores->setOreTypes([ + new OreType(VanillaBlocks::EMERALD_ORE(), VanillaBlocks::STONE(), 11, 1, 0, 32) + ]); + + $this->addPopulator($ores); $this->setElevation(63, 127); diff --git a/src/pocketmine/level/biome/OceanBiome.php b/src/world/biome/OceanBiome.php similarity index 76% rename from src/pocketmine/level/biome/OceanBiome.php rename to src/world/biome/OceanBiome.php index 174ac70e4e..e554ca7312 100644 --- a/src/pocketmine/level/biome/OceanBiome.php +++ b/src/world/biome/OceanBiome.php @@ -21,21 +21,20 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; -use pocketmine\level\generator\populator\TallGrass; +use pocketmine\block\VanillaBlocks; +use pocketmine\world\generator\populator\TallGrass; class OceanBiome extends Biome{ public function __construct(){ $this->setGroundCover([ - BlockFactory::get(Block::GRAVEL), - BlockFactory::get(Block::GRAVEL), - BlockFactory::get(Block::GRAVEL), - BlockFactory::get(Block::GRAVEL), - BlockFactory::get(Block::GRAVEL) + VanillaBlocks::GRAVEL(), + VanillaBlocks::GRAVEL(), + VanillaBlocks::GRAVEL(), + VanillaBlocks::GRAVEL(), + VanillaBlocks::GRAVEL() ]); $tallGrass = new TallGrass(); diff --git a/src/pocketmine/level/biome/PlainBiome.php b/src/world/biome/PlainBiome.php similarity index 92% rename from src/pocketmine/level/biome/PlainBiome.php rename to src/world/biome/PlainBiome.php index 0f927a5827..e6d0f07e1d 100644 --- a/src/pocketmine/level/biome/PlainBiome.php +++ b/src/world/biome/PlainBiome.php @@ -21,9 +21,9 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\level\generator\populator\TallGrass; +use pocketmine\world\generator\populator\TallGrass; class PlainBiome extends GrassyBiome{ diff --git a/src/pocketmine/level/biome/RiverBiome.php b/src/world/biome/RiverBiome.php similarity index 77% rename from src/pocketmine/level/biome/RiverBiome.php rename to src/world/biome/RiverBiome.php index f52c978617..534107b8c1 100644 --- a/src/pocketmine/level/biome/RiverBiome.php +++ b/src/world/biome/RiverBiome.php @@ -21,21 +21,20 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; -use pocketmine\level\generator\populator\TallGrass; +use pocketmine\block\VanillaBlocks; +use pocketmine\world\generator\populator\TallGrass; class RiverBiome extends Biome{ public function __construct(){ $this->setGroundCover([ - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT) + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT() ]); $tallGrass = new TallGrass(); diff --git a/src/pocketmine/level/biome/SandyBiome.php b/src/world/biome/SandyBiome.php similarity index 74% rename from src/pocketmine/level/biome/SandyBiome.php rename to src/world/biome/SandyBiome.php index e033c10e68..e809c62016 100644 --- a/src/pocketmine/level/biome/SandyBiome.php +++ b/src/world/biome/SandyBiome.php @@ -21,20 +21,19 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; abstract class SandyBiome extends Biome{ public function __construct(){ $this->setGroundCover([ - BlockFactory::get(Block::SAND), - BlockFactory::get(Block::SAND), - BlockFactory::get(Block::SANDSTONE), - BlockFactory::get(Block::SANDSTONE), - BlockFactory::get(Block::SANDSTONE) + VanillaBlocks::SAND(), + VanillaBlocks::SAND(), + VanillaBlocks::SANDSTONE(), + VanillaBlocks::SANDSTONE(), + VanillaBlocks::SANDSTONE() ]); } } diff --git a/src/pocketmine/level/biome/SmallMountainsBiome.php b/src/world/biome/SmallMountainsBiome.php similarity index 96% rename from src/pocketmine/level/biome/SmallMountainsBiome.php rename to src/world/biome/SmallMountainsBiome.php index 4e96de245f..6ba7127fb4 100644 --- a/src/pocketmine/level/biome/SmallMountainsBiome.php +++ b/src/world/biome/SmallMountainsBiome.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; class SmallMountainsBiome extends MountainsBiome{ diff --git a/src/pocketmine/level/biome/SnowyBiome.php b/src/world/biome/SnowyBiome.php similarity index 75% rename from src/pocketmine/level/biome/SnowyBiome.php rename to src/world/biome/SnowyBiome.php index 463deae4b2..ed1061d639 100644 --- a/src/pocketmine/level/biome/SnowyBiome.php +++ b/src/world/biome/SnowyBiome.php @@ -21,20 +21,19 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; +use pocketmine\block\VanillaBlocks; abstract class SnowyBiome extends Biome{ public function __construct(){ $this->setGroundCover([ - BlockFactory::get(Block::SNOW_LAYER), - BlockFactory::get(Block::GRASS), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT), - BlockFactory::get(Block::DIRT) + VanillaBlocks::SNOW_LAYER(), + VanillaBlocks::GRASS(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT(), + VanillaBlocks::DIRT() ]); } } diff --git a/src/pocketmine/level/biome/SwampBiome.php b/src/world/biome/SwampBiome.php similarity index 96% rename from src/pocketmine/level/biome/SwampBiome.php rename to src/world/biome/SwampBiome.php index e80dc72e95..a1db3621a3 100644 --- a/src/pocketmine/level/biome/SwampBiome.php +++ b/src/world/biome/SwampBiome.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; class SwampBiome extends GrassyBiome{ diff --git a/src/pocketmine/level/biome/TaigaBiome.php b/src/world/biome/TaigaBiome.php similarity index 84% rename from src/pocketmine/level/biome/TaigaBiome.php rename to src/world/biome/TaigaBiome.php index 90ac60b610..18a8ca3448 100644 --- a/src/pocketmine/level/biome/TaigaBiome.php +++ b/src/world/biome/TaigaBiome.php @@ -21,18 +21,18 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; -use pocketmine\block\Sapling; -use pocketmine\level\generator\populator\TallGrass; -use pocketmine\level\generator\populator\Tree; +use pocketmine\block\utils\TreeType; +use pocketmine\world\generator\populator\TallGrass; +use pocketmine\world\generator\populator\Tree; class TaigaBiome extends SnowyBiome{ public function __construct(){ parent::__construct(); - $trees = new Tree(Sapling::SPRUCE); + $trees = new Tree(TreeType::SPRUCE()); $trees->setBaseAmount(10); $this->addPopulator($trees); diff --git a/src/pocketmine/level/biome/UnknownBiome.php b/src/world/biome/UnknownBiome.php similarity index 96% rename from src/pocketmine/level/biome/UnknownBiome.php rename to src/world/biome/UnknownBiome.php index 7bed7718e1..c8ac018979 100644 --- a/src/pocketmine/level/biome/UnknownBiome.php +++ b/src/world/biome/UnknownBiome.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\biome; +namespace pocketmine\world\biome; /** * Polyfill class for biomes that are unknown to PocketMine-MP diff --git a/src/world/format/BiomeArray.php b/src/world/format/BiomeArray.php new file mode 100644 index 0000000000..30a5d2cb0f --- /dev/null +++ b/src/world/format/BiomeArray.php @@ -0,0 +1,74 @@ +payload = $payload; + } + + public static function fill(int $biomeId) : self{ + return new BiomeArray(str_repeat(chr($biomeId), 256)); + } + + private static function idx(int $x, int $z) : int{ + if($x < 0 or $x >= 16 or $z < 0 or $z >= 16){ + throw new \InvalidArgumentException("x and z must be in the range 0-15"); + } + return ($z << 4) | $x; + } + + public function get(int $x, int $z) : int{ + return ord($this->payload[self::idx($x, $z)]); + } + + public function set(int $x, int $z, int $biomeId) : void{ + if($biomeId < 0 or $biomeId >= 256){ + throw new \InvalidArgumentException("Biome ID must be in the range 0-255"); + } + $this->payload[self::idx($x, $z)] = chr($biomeId); + } + + /** + * @return string ZZZZXXXX key bits + */ + public function getData() : string{ + return $this->payload; + } +} diff --git a/src/world/format/Chunk.php b/src/world/format/Chunk.php new file mode 100644 index 0000000000..8bb5eed9f2 --- /dev/null +++ b/src/world/format/Chunk.php @@ -0,0 +1,345 @@ + + */ + protected $subChunks; + + /** @var Tile[] */ + protected $tiles = []; + + /** @var HeightArray */ + protected $heightMap; + + /** @var BiomeArray */ + protected $biomeIds; + + /** + * @param SubChunk[] $subChunks + */ + 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 + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []); + } + + $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; + } + + /** + * Returns the chunk height in count of subchunks. + */ + public function getHeight() : int{ + return $this->subChunks->getSize(); + } + + /** + * Returns the internal ID of the blockstate at the given coordinates. + * + * @param int $x 0-15 + * @param int $y 0-255 + * @param int $z 0-15 + * + * @return int bitmap, (id << 4) | meta + */ + public function getFullBlock(int $x, int $y, int $z) : int{ + return $this->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)->getFullBlock($x, $y & SubChunk::COORD_MASK, $z); + } + + /** + * Sets the blockstate at the given coordinate by internal ID. + */ + 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_BLOCKS; + } + + /** + * Returns the Y coordinate of the highest non-air block at the specified X/Z chunk block coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + * + * @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 = 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); + } + } + + return null; + } + + /** + * Returns the heightmap value at the specified X/Z chunk block coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + */ + public function getHeightMap(int $x, int $z) : int{ + return $this->heightMap->get($x, $z); + } + + /** + * Returns the heightmap value at the specified X/Z chunk block coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + */ + public function setHeightMap(int $x, int $z, int $value) : void{ + $this->heightMap->set($x, $z, $value); + } + + /** + * Returns the biome ID at the specified X/Z chunk block coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + * + * @return int 0-255 + */ + public function getBiomeId(int $x, int $z) : int{ + return $this->biomeIds->get($x, $z); + } + + /** + * Sets the biome ID at the specified X/Z chunk block coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + * @param int $biomeId 0-255 + */ + public function setBiomeId(int $x, int $z, int $biomeId) : void{ + $this->biomeIds->set($x, $z, $biomeId); + $this->terrainDirtyFlags |= self::DIRTY_FLAG_BIOMES; + } + + public function isLightPopulated() : ?bool{ + return $this->lightPopulated; + } + + public function setLightPopulated(?bool $value = true) : void{ + $this->lightPopulated = $value; + } + + public function isPopulated() : bool{ + return $this->terrainPopulated; + } + + public function setPopulated(bool $value = true) : void{ + $this->terrainPopulated = $value; + $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS; + } + + public function addTile(Tile $tile) : void{ + if($tile->isClosed()){ + throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to a chunk"); + } + + $pos = $tile->getPosition(); + if(isset($this->tiles[$index = Chunk::blockHash($pos->x, $pos->y, $pos->z)]) and $this->tiles[$index] !== $tile){ + throw new \InvalidArgumentException("Another tile is already at this location"); + } + $this->tiles[$index] = $tile; + } + + public function removeTile(Tile $tile) : void{ + $pos = $tile->getPosition(); + unset($this->tiles[Chunk::blockHash($pos->x, $pos->y, $pos->z)]); + } + + /** + * @return Tile[] + */ + public function getTiles() : array{ + return $this->tiles; + } + + /** + * Returns the tile at the specified chunk block coordinates, or null if no tile exists. + * + * @param int $x 0-15 + * @param int $y 0-255 + * @param int $z 0-15 + */ + public function getTile(int $x, int $y, int $z) : ?Tile{ + return $this->tiles[Chunk::blockHash($x, $y, $z)] ?? null; + } + + /** + * Called when the chunk is unloaded, closing entities and tiles. + */ + public function onUnload() : void{ + foreach($this->getTiles() as $tile){ + $tile->close(); + } + } + + public function getBiomeIdArray() : string{ + return $this->biomeIds->getData(); + } + + /** + * @return int[] + */ + public function getHeightMapArray() : array{ + return $this->heightMap->getValues(); + } + + /** + * @param int[] $values + */ + public function setHeightMapArray(array $values) : void{ + $this->heightMap = new HeightArray($values); + } + + public function isTerrainDirty() : bool{ + return $this->terrainDirtyFlags !== 0; + } + + public function getTerrainDirtyFlag(int $flag) : bool{ + return ($this->terrainDirtyFlags & $flag) !== 0; + } + + public function getTerrainDirtyFlags() : int{ + return $this->terrainDirtyFlags; + } + + public function setTerrainDirtyFlag(int $flag, bool $value) : void{ + if($value){ + $this->terrainDirtyFlags |= $flag; + }else{ + $this->terrainDirtyFlags &= ~$flag; + } + } + + public function setTerrainDirty() : void{ + $this->terrainDirtyFlags = ~0; + } + + public function clearTerrainDirtyFlags() : void{ + $this->terrainDirtyFlags = 0; + } + + public function getSubChunk(int $y) : SubChunk{ + if($y < self::MIN_SUBCHUNK_INDEX || $y > self::MAX_SUBCHUNK_INDEX){ + throw new \InvalidArgumentException("Invalid subchunk Y coordinate $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 < self::MIN_SUBCHUNK_INDEX or $y > self::MAX_SUBCHUNK_INDEX){ + throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y"); + } + + $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, []); + $this->setTerrainDirtyFlag(self::DIRTY_FLAG_BLOCKS, true); + } + + /** + * @return SubChunk[] + * @phpstan-return array + */ + public function getSubChunks() : array{ + $result = []; + foreach($this->subChunks as $yOffset => $subChunk){ + $result[$yOffset + self::MIN_SUBCHUNK_INDEX] = $subChunk; + } + return $result; + } + + /** + * Disposes of empty subchunks and frees data where possible + */ + public function collectGarbage() : void{ + foreach($this->subChunks as $y => $subChunk){ + $subChunk->collectGarbage(); + } + } + + public function __clone(){ + //we don't bother cloning entities or tiles since it's impractical to do so (too many dependencies) + $this->subChunks = \SplFixedArray::fromArray(array_map(function(SubChunk $subChunk) : SubChunk{ + return clone $subChunk; + }, $this->subChunks->toArray())); + $this->heightMap = clone $this->heightMap; + $this->biomeIds = clone $this->biomeIds; + } + + /** + * Hashes the given chunk block coordinates into a single integer. + * + * @param int $x 0-15 + * @param int $y 0-255 + * @param int $z 0-15 + */ + public static function blockHash(int $x, int $y, int $z) : int{ + return ($y << (2 * SubChunk::COORD_BIT_SIZE)) | + (($z & SubChunk::COORD_MASK) << SubChunk::COORD_BIT_SIZE) | + ($x & SubChunk::COORD_MASK); + } +} diff --git a/src/pocketmine/level/format/ChunkException.php b/src/world/format/ChunkException.php similarity index 95% rename from src/pocketmine/level/format/ChunkException.php rename to src/world/format/ChunkException.php index 0d373f4868..d8f97d057c 100644 --- a/src/pocketmine/level/format/ChunkException.php +++ b/src/world/format/ChunkException.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format; +namespace pocketmine\world\format; class ChunkException extends \RuntimeException{ diff --git a/src/world/format/HeightArray.php b/src/world/format/HeightArray.php new file mode 100644 index 0000000000..54c8e19f33 --- /dev/null +++ b/src/world/format/HeightArray.php @@ -0,0 +1,78 @@ + + */ + private $array; + + /** + * @param int[] $values ZZZZXXXX key bit order + * @phpstan-param list $values + */ + public function __construct(array $values){ + if(count($values) !== 256){ + throw new \InvalidArgumentException("Expected exactly 256 values"); + } + $this->array = \SplFixedArray::fromArray($values); + } + + public static function fill(int $value) : self{ + return new self(array_fill(0, 256, $value)); + } + + private static function idx(int $x, int $z) : int{ + if($x < 0 or $x >= 16 or $z < 0 or $z >= 16){ + throw new \InvalidArgumentException("x and z must be in the range 0-15"); + } + return ($z << 4) | $x; + } + + public function get(int $x, int $z) : int{ + return $this->array[self::idx($x, $z)]; + } + + public function set(int $x, int $z, int $height) : void{ + $this->array[self::idx($x, $z)] = $height; + } + + /** + * @return int[] ZZZZXXXX key bit order + * @phpstan-return list + */ + public function getValues() : array{ + return $this->array->toArray(); + } + + public function __clone(){ + $this->array = clone $this->array; + } +} diff --git a/src/world/format/SubChunk.php b/src/world/format/SubChunk.php new file mode 100644 index 0000000000..ea20b917f5 --- /dev/null +++ b/src/world/format/SubChunk.php @@ -0,0 +1,172 @@ +emptyBlockId = $emptyBlockId; + $this->blockLayers = $blocks; + + $this->skyLight = $skyLight; + $this->blockLight = $blockLight; + } + + /** + * Returns whether this subchunk contains any non-air blocks. + * This function will do a slow check, usually by garbage collecting first. + * This is typically useful for disk saving. + */ + public function isEmptyAuthoritative() : bool{ + $this->collectGarbage(); + return $this->isEmptyFast(); + } + + /** + * Returns a non-authoritative bool to indicate whether the chunk contains any blocks. + * This may report non-empty erroneously if the chunk has been modified and not garbage-collected. + */ + public function isEmptyFast() : bool{ + return count($this->blockLayers) === 0; + } + + /** + * Returns the block used as the default. This is assumed to refer to air. + * If all the blocks in a subchunk layer are equal to this block, the layer is assumed to be empty. + */ + public function getEmptyBlockId() : int{ return $this->emptyBlockId; } + + public function getFullBlock(int $x, int $y, int $z) : int{ + if(count($this->blockLayers) === 0){ + return $this->emptyBlockId; + } + return $this->blockLayers[0]->get($x, $y, $z); + } + + public function setFullBlock(int $x, int $y, int $z, int $block) : void{ + if(count($this->blockLayers) === 0){ + $this->blockLayers[] = new PalettedBlockArray($this->emptyBlockId); + } + $this->blockLayers[0]->set($x, $y, $z, $block); + } + + /** + * @return PalettedBlockArray[] + */ + public function getBlockLayers() : array{ + return $this->blockLayers; + } + + public function getHighestBlockAt(int $x, int $z) : ?int{ + if(count($this->blockLayers) === 0){ + return null; + } + for($y = self::EDGE_LENGTH - 1; $y >= 0; --$y){ + if($this->blockLayers[0]->get($x, $y, $z) !== $this->emptyBlockId){ + return $y; + } + } + + return null; //highest block not in this subchunk + } + + public function getBlockSkyLightArray() : LightArray{ + return $this->skyLight ??= LightArray::fill(0); + } + + public function setBlockSkyLightArray(LightArray $data) : void{ + $this->skyLight = $data; + } + + public function getBlockLightArray() : LightArray{ + return $this->blockLight ??= LightArray::fill(0); + } + + public function setBlockLightArray(LightArray $data) : void{ + $this->blockLight = $data; + } + + /** + * @return mixed[] + */ + public function __debugInfo() : array{ + return []; + } + + public function collectGarbage() : void{ + foreach($this->blockLayers as $k => $layer){ + $layer->collectGarbage(); + + foreach($layer->getPalette() as $p){ + if($p !== $this->emptyBlockId){ + continue 2; + } + } + unset($this->blockLayers[$k]); + } + $this->blockLayers = array_values($this->blockLayers); + + if($this->skyLight !== null && $this->skyLight->isUniform(0)){ + $this->skyLight = null; + } + if($this->blockLight !== null && $this->blockLight->isUniform(0)){ + $this->blockLight = null; + } + } + + public function __clone(){ + $this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{ + return clone $array; + }, $this->blockLayers); + + if($this->skyLight !== null){ + $this->skyLight = clone $this->skyLight; + } + if($this->blockLight !== null){ + $this->blockLight = clone $this->blockLight; + } + } +} diff --git a/src/world/format/io/BaseWorldProvider.php b/src/world/format/io/BaseWorldProvider.php new file mode 100644 index 0000000000..6404f641b8 --- /dev/null +++ b/src/world/format/io/BaseWorldProvider.php @@ -0,0 +1,59 @@ +path = $path; + $this->worldData = $this->loadLevelData(); + } + + /** + * @throws CorruptedWorldException + * @throws UnsupportedWorldFormatException + */ + abstract protected function loadLevelData() : WorldData; + + public function getPath() : string{ + return $this->path; + } + + public function getWorldData() : WorldData{ + return $this->worldData; + } +} diff --git a/src/pocketmine/scheduler/FileWriteTask.php b/src/world/format/io/ChunkData.php similarity index 59% rename from src/pocketmine/scheduler/FileWriteTask.php rename to src/world/format/io/ChunkData.php index 8ffa4901ba..93ce350958 100644 --- a/src/pocketmine/scheduler/FileWriteTask.php +++ b/src/world/format/io/ChunkData.php @@ -21,32 +21,28 @@ declare(strict_types=1); -namespace pocketmine\scheduler; +namespace pocketmine\world\format\io; -use function file_put_contents; +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\world\format\Chunk; -/** - * @deprecated - */ -class FileWriteTask extends AsyncTask{ - - /** @var string */ - private $path; - /** @var mixed */ - private $contents; - /** @var int */ - private $flags; +final class ChunkData{ /** - * @param mixed $contents + * @param CompoundTag[] $entityNBT + * @param CompoundTag[] $tileNBT */ - public function __construct(string $path, $contents, int $flags = 0){ - $this->path = $path; - $this->contents = $contents; - $this->flags = $flags; - } + public function __construct( + private Chunk $chunk, + private array $entityNBT, + private array $tileNBT + ){} - public function onRun(){ - file_put_contents($this->path, $this->contents, $this->flags); - } + public function getChunk() : Chunk{ return $this->chunk; } + + /** @return CompoundTag[] */ + public function getEntityNBT() : array{ return $this->entityNBT; } + + /** @return CompoundTag[] */ + public function getTileNBT() : array{ return $this->tileNBT; } } diff --git a/src/world/format/io/ChunkUtils.php b/src/world/format/io/ChunkUtils.php new file mode 100644 index 0000000000..4b49b7b48d --- /dev/null +++ b/src/world/format/io/ChunkUtils.php @@ -0,0 +1,45 @@ + $array + */ + public static function convertBiomeColors(array $array) : string{ + $result = str_repeat("\x00", 256); + foreach($array as $i => $color){ + $result[$i] = chr(($color >> 24) & 0xff); + } + return $result; + } + +} diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php new file mode 100644 index 0000000000..c8ab53a733 --- /dev/null +++ b/src/world/format/io/FastChunkSerializer.php @@ -0,0 +1,121 @@ +putByte( + ($chunk->isPopulated() ? self::FLAG_POPULATED : 0) + ); + + //subchunks + $subChunks = $chunk->getSubChunks(); + $count = count($subChunks); + $stream->putByte($count); + + foreach($subChunks as $y => $subChunk){ + $stream->putByte($y); + $stream->putInt($subChunk->getEmptyBlockId()); + $layers = $subChunk->getBlockLayers(); + $stream->putByte(count($layers)); + foreach($layers as $blocks){ + $wordArray = $blocks->getWordArray(); + $palette = $blocks->getPalette(); + + $stream->putByte($blocks->getBitsPerBlock()); + $stream->put($wordArray); + $serialPalette = pack("L*", ...$palette); + $stream->putInt(strlen($serialPalette)); + $stream->put($serialPalette); + } + } + + //biomes + $stream->put($chunk->getBiomeIdArray()); + + return $stream->getBuffer(); + } + + /** + * Deserializes a fast-serialized chunk + */ + public static function deserializeTerrain(string $data) : Chunk{ + $stream = new BinaryStream($data); + + $flags = $stream->getByte(); + $terrainPopulated = (bool) ($flags & self::FLAG_POPULATED); + + $subChunks = []; + + $count = $stream->getByte(); + for($subCount = 0; $subCount < $count; ++$subCount){ + $y = Binary::signByte($stream->getByte()); + $airBlockId = $stream->getInt(); + + /** @var PalettedBlockArray[] $layers */ + $layers = []; + for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){ + $bitsPerBlock = $stream->getByte(); + $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + /** @var int[] $unpackedPalette */ + $unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here + $palette = array_values($unpackedPalette); + + $layers[] = PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); + } + $subChunks[$y] = new SubChunk($airBlockId, $layers); + } + + $biomeIds = new BiomeArray($stream->get(256)); + + return new Chunk($subChunks, $biomeIds, $terrainPopulated); + } +} diff --git a/src/world/format/io/FormatConverter.php b/src/world/format/io/FormatConverter.php new file mode 100644 index 0000000000..2599415c49 --- /dev/null +++ b/src/world/format/io/FormatConverter.php @@ -0,0 +1,168 @@ +oldProvider = $oldProvider; + $this->newProvider = $newProvider; + $this->logger = new \PrefixedLogger($logger, "World Converter: " . $this->oldProvider->getWorldData()->getName()); + $this->chunksPerProgressUpdate = $chunksPerProgressUpdate; + + if(!file_exists($backupPath)){ + @mkdir($backupPath, 0777, true); + } + $nextSuffix = ""; + do{ + $this->backupPath = Path::join($backupPath, basename($this->oldProvider->getPath()) . $nextSuffix); + $nextSuffix = "_" . crc32(random_bytes(4)); + }while(file_exists($this->backupPath)); + } + + public function getBackupPath() : string{ + return $this->backupPath; + } + + public function execute() : WritableWorldProvider{ + $new = $this->generateNew(); + + $this->populateLevelData($new->getWorldData()); + $this->convertTerrain($new); + + $path = $this->oldProvider->getPath(); + $this->oldProvider->close(); + $new->close(); + + $this->logger->info("Backing up pre-conversion world to " . $this->backupPath); + if(!@rename($path, $this->backupPath)){ + $this->logger->warning("Moving old world files for backup failed, attempting copy instead. This might take a long time."); + Filesystem::recursiveCopy($path, $this->backupPath); + Filesystem::recursiveUnlink($path); + } + if(!@rename($new->getPath(), $path)){ + //we don't expect this to happen because worlds/ should most likely be all on the same FS, but just in case... + $this->logger->debug("Relocation of new world files to location failed, attempting copy and delete instead"); + Filesystem::recursiveCopy($new->getPath(), $path); + Filesystem::recursiveUnlink($new->getPath()); + } + + $this->logger->info("Conversion completed"); + return $this->newProvider->fromPath($path); + } + + private function generateNew() : WritableWorldProvider{ + $this->logger->info("Generating new world"); + $data = $this->oldProvider->getWorldData(); + + $convertedOutput = rtrim($this->oldProvider->getPath(), "/" . DIRECTORY_SEPARATOR) . "_converted" . DIRECTORY_SEPARATOR; + if(file_exists($convertedOutput)){ + $this->logger->info("Found previous conversion attempt, deleting..."); + Filesystem::recursiveUnlink($convertedOutput); + } + $this->newProvider->generate($convertedOutput, $data->getName(), WorldCreationOptions::create() + //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()) + ->setDifficulty($data->getDifficulty()) + ); + + return $this->newProvider->fromPath($convertedOutput); + } + + private function populateLevelData(WorldData $data) : void{ + $this->logger->info("Converting world manifest"); + $oldData = $this->oldProvider->getWorldData(); + $data->setDifficulty($oldData->getDifficulty()); + $data->setLightningLevel($oldData->getLightningLevel()); + $data->setLightningTime($oldData->getLightningTime()); + $data->setRainLevel($oldData->getRainLevel()); + $data->setRainTime($oldData->getRainTime()); + $data->setSpawn($oldData->getSpawn()); + $data->setTime($oldData->getTime()); + + $data->save(); + $this->logger->info("Finished converting manifest"); + //TODO: add more properties as-needed + } + + private function convertTerrain(WritableWorldProvider $new) : void{ + $this->logger->info("Calculating chunk count"); + $count = $this->oldProvider->calculateChunkCount(); + $this->logger->info("Discovered $count chunks"); + + $counter = 0; + + $start = microtime(true); + $thisRound = $start; + foreach($this->oldProvider->getAllChunks(true, $this->logger) as $coords => $chunk){ + [$chunkX, $chunkZ] = $coords; + $chunk->getChunk()->setTerrainDirty(); + $new->saveChunk($chunkX, $chunkZ, $chunk); + $counter++; + if(($counter % $this->chunksPerProgressUpdate) === 0){ + $time = microtime(true); + $diff = $time - $thisRound; + $thisRound = $time; + $this->logger->info("Converted $counter / $count chunks (" . floor($this->chunksPerProgressUpdate / $diff) . " chunks/sec)"); + } + } + $total = microtime(true) - $start; + $this->logger->info("Converted $counter / $counter chunks in " . round($total, 3) . " seconds (" . floor($counter / $total) . " chunks/sec)"); + } +} diff --git a/src/world/format/io/ReadOnlyWorldProviderManagerEntry.php b/src/world/format/io/ReadOnlyWorldProviderManagerEntry.php new file mode 100644 index 0000000000..beb4098821 --- /dev/null +++ b/src/world/format/io/ReadOnlyWorldProviderManagerEntry.php @@ -0,0 +1,41 @@ +fromPath = $fromPath; + } + + public function fromPath(string $path) : WorldProvider{ return ($this->fromPath)($path); } +} diff --git a/src/world/format/io/WorldData.php b/src/world/format/io/WorldData.php new file mode 100644 index 0000000000..606e18be21 --- /dev/null +++ b/src/world/format/io/WorldData.php @@ -0,0 +1,103 @@ + + * @throws CorruptedChunkException + */ + public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator; + + /** + * Returns the number of chunks in the provider. Used for world conversion time estimations. + */ + public function calculateChunkCount() : int; +} diff --git a/src/world/format/io/WorldProviderManager.php b/src/world/format/io/WorldProviderManager.php new file mode 100644 index 0000000000..64dad08168 --- /dev/null +++ b/src/world/format/io/WorldProviderManager.php @@ -0,0 +1,103 @@ + + */ + protected $providers = []; + + private WritableWorldProviderManagerEntry $default; + + public function __construct(){ + $leveldb = new WritableWorldProviderManagerEntry(\Closure::fromCallable([LevelDB::class, 'isValid']), fn(string $path) => new LevelDB($path), \Closure::fromCallable([LevelDB::class, 'generate'])); + $this->default = $leveldb; + $this->addProvider($leveldb, "leveldb"); + + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([Anvil::class, 'isValid']), fn(string $path) => new Anvil($path)), "anvil"); + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([McRegion::class, 'isValid']), fn(string $path) => new McRegion($path)), "mcregion"); + $this->addProvider(new ReadOnlyWorldProviderManagerEntry(\Closure::fromCallable([PMAnvil::class, 'isValid']), fn(string $path) => new PMAnvil($path)), "pmanvil"); + } + + /** + * Returns the default format used to generate new worlds. + */ + public function getDefault() : WritableWorldProviderManagerEntry{ + return $this->default; + } + + public function setDefault(WritableWorldProviderManagerEntry $class) : void{ + $this->default = $class; + } + + public function addProvider(WorldProviderManagerEntry $providerEntry, string $name, bool $overwrite = false) : void{ + $name = strtolower($name); + if(!$overwrite and isset($this->providers[$name])){ + throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); + } + + $this->providers[$name] = $providerEntry; + } + + /** + * Returns a WorldProvider class for this path, or null + * + * @return WorldProviderManagerEntry[] + * @phpstan-return array + */ + public function getMatchingProviders(string $path) : array{ + $result = []; + foreach(Utils::stringifyKeys($this->providers) as $alias => $providerEntry){ + if($providerEntry->isValid($path)){ + $result[$alias] = $providerEntry; + } + } + return $result; + } + + /** + * @return WorldProviderManagerEntry[] + * @phpstan-return array + */ + public function getAvailableProviders() : array{ + return $this->providers; + } + + /** + * Returns a WorldProvider by name, or null if not found + */ + public function getProviderByName(string $name) : ?WorldProviderManagerEntry{ + return $this->providers[trim(strtolower($name))] ?? null; + } +} diff --git a/src/world/format/io/WorldProviderManagerEntry.php b/src/world/format/io/WorldProviderManagerEntry.php new file mode 100644 index 0000000000..5c35ac09ec --- /dev/null +++ b/src/world/format/io/WorldProviderManagerEntry.php @@ -0,0 +1,53 @@ +isValid = $isValid; + } + + /** + * Tells if the path is a valid world. + * This must tell if the current format supports opening the files in the directory + */ + public function isValid(string $path) : bool{ return ($this->isValid)($path); } + + /** + * @throws CorruptedWorldException + * @throws UnsupportedWorldFormatException + */ + abstract public function fromPath(string $path) : WorldProvider; +} diff --git a/src/world/format/io/WritableWorldProvider.php b/src/world/format/io/WritableWorldProvider.php new file mode 100644 index 0000000000..eda481e3a3 --- /dev/null +++ b/src/world/format/io/WritableWorldProvider.php @@ -0,0 +1,31 @@ +fromPath = $fromPath; + $this->generate = $generate; + } + + public function fromPath(string $path) : WritableWorldProvider{ + return ($this->fromPath)($path); + } + + /** + * Generates world manifest files and any other things needed to initialize a new world on disk + */ + public function generate(string $path, string $name, WorldCreationOptions $options) : void{ + ($this->generate)($path, $name, $options); + } +} diff --git a/src/world/format/io/data/BaseNbtWorldData.php b/src/world/format/io/data/BaseNbtWorldData.php new file mode 100644 index 0000000000..43016af0a2 --- /dev/null +++ b/src/world/format/io/data/BaseNbtWorldData.php @@ -0,0 +1,148 @@ +dataPath = $dataPath; + + if(!file_exists($this->dataPath)){ + throw new CorruptedWorldException("World data not found at $dataPath"); + } + + try{ + $this->compoundTag = $this->load(); + }catch(CorruptedWorldException $e){ + throw new CorruptedWorldException("Corrupted world data: " . $e->getMessage(), 0, $e); + } + $this->fix(); + } + + /** + * @throws CorruptedWorldException + * @throws UnsupportedWorldFormatException + */ + abstract protected function load() : CompoundTag; + + /** + * @throws CorruptedWorldException + * @throws UnsupportedWorldFormatException + */ + abstract protected function fix() : void; + + /** + * Hack to fix worlds broken previously by older versions of PocketMine-MP which incorrectly saved classpaths of + * generators into level.dat on imported (not generated) worlds. + * + * This should only have affected leveldb worlds as far as I know, because PC format worlds include the + * generatorName tag by default. However, MCPE leveldb ones didn't, and so they would get filled in with something + * broken. + * + * This bug took a long time to get found because previously the generator manager would just return the default + * generator silently on failure to identify the correct generator, which caused lots of unexpected bugs. + * + * Only classnames which were written into the level.dat from "fixing" the level data are included here. These are + * hardcoded to avoid problems fixing broken worlds in the future if these classes get moved, renamed or removed. + * + * @param string $className Classname saved in level.dat + * + * @return null|string Name of the correct generator to replace the broken value + */ + protected static function hackyFixForGeneratorClasspathInLevelDat(string $className) : ?string{ + //THESE ARE DELIBERATELY HARDCODED, DO NOT CHANGE! + switch($className){ + /** @noinspection ClassConstantCanBeUsedInspection */ + case 'pocketmine\level\generator\normal\Normal': + return "normal"; + /** @noinspection ClassConstantCanBeUsedInspection */ + case 'pocketmine\level\generator\Flat': + return "flat"; + } + + return null; + } + + public function getCompoundTag() : CompoundTag{ + return $this->compoundTag; + } + + /* The below are common between PC and PE */ + + public function getName() : string{ + return $this->compoundTag->getString("LevelName"); + } + + public function getGenerator() : string{ + return $this->compoundTag->getString("generatorName", "DEFAULT"); + } + + public function getGeneratorOptions() : string{ + return $this->compoundTag->getString("generatorOptions", ""); + } + + public function getSeed() : int{ + return $this->compoundTag->getLong("RandomSeed"); + } + + public function getTime() : int{ + if(($timeTag = $this->compoundTag->getTag("Time")) instanceof IntTag){ //some older PM worlds had this in the wrong format + return $timeTag->getValue(); + } + return $this->compoundTag->getLong("Time", 0); + } + + public function setTime(int $value) : void{ + $this->compoundTag->setLong("Time", $value); + } + + public function getSpawn() : Vector3{ + return new Vector3($this->compoundTag->getInt("SpawnX"), $this->compoundTag->getInt("SpawnY"), $this->compoundTag->getInt("SpawnZ")); + } + + public function setSpawn(Vector3 $pos) : void{ + $this->compoundTag->setInt("SpawnX", $pos->getFloorX()); + $this->compoundTag->setInt("SpawnY", $pos->getFloorY()); + $this->compoundTag->setInt("SpawnZ", $pos->getFloorZ()); + } + +} diff --git a/src/world/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php new file mode 100644 index 0000000000..50a4184fec --- /dev/null +++ b/src/world/format/io/data/BedrockWorldData.php @@ -0,0 +1,210 @@ +getGeneratorClass()){ + case Flat::class: + $generatorType = self::GENERATOR_FLAT; + break; + default: + $generatorType = self::GENERATOR_INFINITE; + //TODO: add support for limited worlds + } + + $worldData = CompoundTag::create() + //Vanilla fields + ->setInt("DayCycleStopTime", -1) + ->setInt("Difficulty", $options->getDifficulty()) + ->setByte("ForceGameType", 0) + ->setInt("GameType", 0) + ->setInt("Generator", $generatorType) + ->setLong("LastPlayed", time()) + ->setString("LevelName", $name) + ->setInt("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL) + //->setInt("Platform", 2) //TODO: find out what the possible values are for + ->setLong("RandomSeed", $options->getSeed()) + ->setInt("SpawnX", $options->getSpawnPosition()->getFloorX()) + ->setInt("SpawnY", $options->getSpawnPosition()->getFloorY()) + ->setInt("SpawnZ", $options->getSpawnPosition()->getFloorZ()) + ->setInt("StorageVersion", self::CURRENT_STORAGE_VERSION) + ->setLong("Time", 0) + ->setByte("eduLevel", 0) + ->setByte("falldamage", 1) + ->setByte("firedamage", 1) + ->setByte("hasBeenLoadedInCreative", 1) //badly named, this actually determines whether achievements can be earned in this world... + ->setByte("immutableWorld", 0) + ->setFloat("lightningLevel", 0.0) + ->setInt("lightningTime", 0) + ->setByte("pvp", 1) + ->setFloat("rainLevel", 0.0) + ->setInt("rainTime", 0) + ->setByte("spawnMobs", 1) + ->setByte("texturePacksRequired", 0) //TODO + + //Additional PocketMine-MP fields + ->setTag("GameRules", new CompoundTag()) + ->setByte("hardcore", 0) + ->setString("generatorName", GeneratorManager::getInstance()->getGeneratorName($options->getGeneratorClass())) + ->setString("generatorOptions", $options->getGeneratorOptions()); + + $nbt = new LittleEndianNbtSerializer(); + $buffer = $nbt->write(new TreeRoot($worldData)); + file_put_contents(Path::join($path, "level.dat"), Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer); + } + + protected function load() : CompoundTag{ + $rawLevelData = @file_get_contents($this->dataPath); + if($rawLevelData === false){ + throw new CorruptedWorldException("Failed to read level.dat (permission denied or doesn't exist)"); + } + if(strlen($rawLevelData) <= 8){ + throw new CorruptedWorldException("Truncated level.dat"); + } + $nbt = new LittleEndianNbtSerializer(); + try{ + $worldData = $nbt->read(substr($rawLevelData, 8))->mustGetCompoundTag(); + }catch(NbtDataException $e){ + throw new CorruptedWorldException($e->getMessage(), 0, $e); + } + + $version = $worldData->getInt("StorageVersion", Limits::INT32_MAX); + if($version > self::CURRENT_STORAGE_VERSION){ + throw new UnsupportedWorldFormatException("LevelDB world format version $version is currently unsupported"); + } + + return $worldData; + } + + protected function fix() : void{ + $generatorNameTag = $this->compoundTag->getTag("generatorName"); + if(!($generatorNameTag instanceof StringTag)){ + if(($mcpeGeneratorTypeTag = $this->compoundTag->getTag("Generator")) instanceof IntTag){ + switch($mcpeGeneratorTypeTag->getValue()){ //Detect correct generator from MCPE data + case self::GENERATOR_FLAT: + $this->compoundTag->setString("generatorName", "flat"); + $this->compoundTag->setString("generatorOptions", "2;7,3,3,2;1"); + break; + case self::GENERATOR_INFINITE: + //TODO: add a null generator which does not generate missing chunks (to allow importing back to MCPE and generating more normal terrain without PocketMine messing things up) + $this->compoundTag->setString("generatorName", "default"); + $this->compoundTag->setString("generatorOptions", ""); + break; + case self::GENERATOR_LIMITED: + throw new UnsupportedWorldFormatException("Limited worlds are not currently supported"); + default: + throw new UnsupportedWorldFormatException("Unknown LevelDB generator type"); + } + }else{ + $this->compoundTag->setString("generatorName", "default"); + } + }elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($generatorNameTag->getValue())) !== null){ + $this->compoundTag->setString("generatorName", $generatorName); + } + + if(!($this->compoundTag->getTag("generatorOptions")) instanceof StringTag){ + $this->compoundTag->setString("generatorOptions", ""); + } + } + + public function save() : void{ + $this->compoundTag->setInt("NetworkVersion", ProtocolInfo::CURRENT_PROTOCOL); + $this->compoundTag->setInt("StorageVersion", self::CURRENT_STORAGE_VERSION); + + $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); + } + + public function getDifficulty() : int{ + return $this->compoundTag->getInt("Difficulty", World::DIFFICULTY_NORMAL); + } + + public function setDifficulty(int $difficulty) : void{ + $this->compoundTag->setInt("Difficulty", $difficulty); //yes, this is intended! (in PE: int, PC: byte) + } + + public function getRainTime() : int{ + return $this->compoundTag->getInt("rainTime", 0); + } + + public function setRainTime(int $ticks) : void{ + $this->compoundTag->setInt("rainTime", $ticks); + } + + public function getRainLevel() : float{ + return $this->compoundTag->getFloat("rainLevel", 0.0); + } + + public function setRainLevel(float $level) : void{ + $this->compoundTag->setFloat("rainLevel", $level); + } + + public function getLightningTime() : int{ + return $this->compoundTag->getInt("lightningTime", 0); + } + + public function setLightningTime(int $ticks) : void{ + $this->compoundTag->setInt("lightningTime", $ticks); + } + + public function getLightningLevel() : float{ + return $this->compoundTag->getFloat("lightningLevel", 0.0); + } + + public function setLightningLevel(float $level) : void{ + $this->compoundTag->setFloat("lightningLevel", $level); + } +} diff --git a/src/world/format/io/data/JavaWorldData.php b/src/world/format/io/data/JavaWorldData.php new file mode 100644 index 0000000000..c2f4888762 --- /dev/null +++ b/src/world/format/io/data/JavaWorldData.php @@ -0,0 +1,166 @@ +setByte("hardcore", 0) + ->setByte("Difficulty", $options->getDifficulty()) + ->setByte("initialized", 1) + ->setInt("GameType", 0) + ->setInt("generatorVersion", 1) //2 in MCPE + ->setInt("SpawnX", $options->getSpawnPosition()->getFloorX()) + ->setInt("SpawnY", $options->getSpawnPosition()->getFloorY()) + ->setInt("SpawnZ", $options->getSpawnPosition()->getFloorZ()) + ->setInt("version", $version) + ->setInt("DayTime", 0) + ->setLong("LastPlayed", (int) (microtime(true) * 1000)) + ->setLong("RandomSeed", $options->getSeed()) + ->setLong("SizeOnDisk", 0) + ->setLong("Time", 0) + ->setString("generatorName", GeneratorManager::getInstance()->getGeneratorName($options->getGeneratorClass())) + ->setString("generatorOptions", $options->getGeneratorOptions()) + ->setString("LevelName", $name) + ->setTag("GameRules", new CompoundTag()); + + $nbt = new BigEndianNbtSerializer(); + $buffer = zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $worldData))), ZLIB_ENCODING_GZIP); + file_put_contents(Path::join($path, "level.dat"), $buffer); + } + + protected function load() : CompoundTag{ + $rawLevelData = @file_get_contents($this->dataPath); + if($rawLevelData === false){ + throw new CorruptedWorldException("Failed to read level.dat (permission denied or doesn't exist)"); + } + $nbt = new BigEndianNbtSerializer(); + $decompressed = @zlib_decode($rawLevelData); + if($decompressed === false){ + throw new CorruptedWorldException("Failed to decompress level.dat contents"); + } + try{ + $worldData = $nbt->read($decompressed)->mustGetCompoundTag(); + }catch(NbtDataException $e){ + throw new CorruptedWorldException($e->getMessage(), 0, $e); + } + + $dataTag = $worldData->getTag("Data"); + if(!($dataTag instanceof CompoundTag)){ + throw new CorruptedWorldException("Missing 'Data' key or wrong type"); + } + return $dataTag; + } + + protected function fix() : void{ + $generatorNameTag = $this->compoundTag->getTag("generatorName"); + if(!($generatorNameTag instanceof StringTag)){ + $this->compoundTag->setString("generatorName", "default"); + }elseif(($generatorName = self::hackyFixForGeneratorClasspathInLevelDat($generatorNameTag->getValue())) !== null){ + $this->compoundTag->setString("generatorName", $generatorName); + } + + if(!($this->compoundTag->getTag("generatorOptions") instanceof StringTag)){ + $this->compoundTag->setString("generatorOptions", ""); + } + } + + 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); + } + + public function getDifficulty() : int{ + return $this->compoundTag->getByte("Difficulty", World::DIFFICULTY_NORMAL); + } + + public function setDifficulty(int $difficulty) : void{ + $this->compoundTag->setByte("Difficulty", $difficulty); + } + + public function getRainTime() : int{ + return $this->compoundTag->getInt("rainTime", 0); + } + + public function setRainTime(int $ticks) : void{ + $this->compoundTag->setInt("rainTime", $ticks); + } + + public function getRainLevel() : float{ + if(($rainLevelTag = $this->compoundTag->getTag("rainLevel")) instanceof FloatTag){ //PocketMine/MCPE + return $rainLevelTag->getValue(); + } + + return (float) $this->compoundTag->getByte("raining", 0); //PC vanilla + } + + public function setRainLevel(float $level) : void{ + $this->compoundTag->setFloat("rainLevel", $level); //PocketMine/MCPE + $this->compoundTag->setByte("raining", (int) ceil($level)); //PC vanilla + } + + public function getLightningTime() : int{ + return $this->compoundTag->getInt("thunderTime", 0); + } + + public function setLightningTime(int $ticks) : void{ + $this->compoundTag->setInt("thunderTime", $ticks); + } + + public function getLightningLevel() : float{ + if(($lightningLevelTag = $this->compoundTag->getTag("lightningLevel")) instanceof FloatTag){ //PocketMine/MCPE + return $lightningLevelTag->getValue(); + } + + return (float) $this->compoundTag->getByte("thundering", 0); //PC vanilla + } + + public function setLightningLevel(float $level) : void{ + $this->compoundTag->setFloat("lightningLevel", $level); //PocketMine/MCPE + $this->compoundTag->setByte("thundering", (int) ceil($level)); //PC vanilla + } +} diff --git a/src/pocketmine/level/format/io/exception/CorruptedChunkException.php b/src/world/format/io/exception/CorruptedChunkException.php similarity index 89% rename from src/pocketmine/level/format/io/exception/CorruptedChunkException.php rename to src/world/format/io/exception/CorruptedChunkException.php index 9c0705fbd8..38dd61f6d5 100644 --- a/src/pocketmine/level/format/io/exception/CorruptedChunkException.php +++ b/src/world/format/io/exception/CorruptedChunkException.php @@ -21,9 +21,9 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\exception; +namespace pocketmine\world\format\io\exception; -use pocketmine\level\format\ChunkException; +use pocketmine\world\format\ChunkException; class CorruptedChunkException extends ChunkException{ diff --git a/src/pocketmine/command/PluginIdentifiableCommand.php b/src/world/format/io/exception/CorruptedWorldException.php similarity index 84% rename from src/pocketmine/command/PluginIdentifiableCommand.php rename to src/world/format/io/exception/CorruptedWorldException.php index 02e85f7e92..982bc5e5f7 100644 --- a/src/pocketmine/command/PluginIdentifiableCommand.php +++ b/src/world/format/io/exception/CorruptedWorldException.php @@ -21,11 +21,10 @@ declare(strict_types=1); -namespace pocketmine\command; +namespace pocketmine\world\format\io\exception; -use pocketmine\plugin\Plugin; +use pocketmine\world\WorldException; -interface PluginIdentifiableCommand{ +class CorruptedWorldException extends WorldException{ - public function getPlugin() : Plugin; } diff --git a/src/pocketmine/event/entity/EntityArmorChangeEvent.php b/src/world/format/io/exception/UnsupportedWorldFormatException.php similarity index 83% rename from src/pocketmine/event/entity/EntityArmorChangeEvent.php rename to src/world/format/io/exception/UnsupportedWorldFormatException.php index 8d53832aa3..d97fb4f45b 100644 --- a/src/pocketmine/event/entity/EntityArmorChangeEvent.php +++ b/src/world/format/io/exception/UnsupportedWorldFormatException.php @@ -21,8 +21,10 @@ declare(strict_types=1); -namespace pocketmine\event\entity; +namespace pocketmine\world\format\io\exception; -class EntityArmorChangeEvent extends EntityInventoryChangeEvent{ +use pocketmine\world\WorldException; + +class UnsupportedWorldFormatException extends WorldException{ } diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php new file mode 100644 index 0000000000..3fe2e359c2 --- /dev/null +++ b/src/world/format/io/leveldb/LevelDB.php @@ -0,0 +1,554 @@ + LEVELDB_ZLIB_RAW_COMPRESSION, + "block_size" => 64 * 1024 //64KB, big enough for most chunks + ]); + } + + public function __construct(string $path){ + self::checkForLevelDBExtension(); + parent::__construct($path); + + try{ + $this->db = self::createDB($path); + }catch(\LevelDBException $e){ + //we can't tell the difference between errors caused by bad permissions and actual corruption :( + throw new CorruptedWorldException(trim($e->getMessage()), 0, $e); + } + } + + protected function loadLevelData() : WorldData{ + return new BedrockWorldData(Path::join($this->getPath(), "level.dat")); + } + + public function getWorldMinY() : int{ + return 0; + } + + public function getWorldMaxY() : int{ + return 256; + } + + public static function isValid(string $path) : bool{ + return file_exists(Path::join($path, "level.dat")) and is_dir(Path::join($path, "db")); + } + + public static function generate(string $path, string $name, WorldCreationOptions $options) : void{ + self::checkForLevelDBExtension(); + + $dbPath = Path::join($path, "db"); + if(!file_exists($dbPath)){ + mkdir($dbPath, 0777, true); + } + + BedrockWorldData::generate($path, $name, $options); + } + + protected function deserializePaletted(BinaryStream $stream) : PalettedBlockArray{ + $bitsPerBlock = $stream->getByte() >> 1; + + try{ + $words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock)); + }catch(\InvalidArgumentException $e){ + throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e); + } + $nbt = new LittleEndianNbtSerializer(); + $palette = []; + $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); + + $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; + } + $palette[] = ($id << Block::INTERNAL_METADATA_BITS) | $data; + } + + //TODO: exceptions + return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette); + } + + protected static function deserializeExtraDataKey(int $chunkVersion, int $key, ?int &$x, ?int &$y, ?int &$z) : void{ + if($chunkVersion >= 3){ + $x = ($key >> 12) & 0xf; + $z = ($key >> 8) & 0xf; + $y = $key & 0xff; + }else{ //pre-1.0, 7 bits were used because the build height limit was lower + $x = ($key >> 11) & 0xf; + $z = ($key >> 7) & 0xf; + $y = $key & 0x7f; + } + } + + /** + * @return PalettedBlockArray[] + */ + protected function deserializeLegacyExtraData(string $index, int $chunkVersion) : array{ + if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) === false or $extraRawData === ""){ + return []; + } + + /** @var PalettedBlockArray[] $extraDataLayers */ + $extraDataLayers = []; + $binaryStream = new BinaryStream($extraRawData); + $count = $binaryStream->getLInt(); + for($i = 0; $i < $count; ++$i){ + $key = $binaryStream->getLInt(); + $value = $binaryStream->getLShort(); + + self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z); + + $ySub = ($fullY >> SubChunk::COORD_BIT_SIZE); + $y = $key & SubChunk::COORD_MASK; + + $blockId = $value & 0xff; + $blockData = ($value >> 8) & 0xf; + if(!isset($extraDataLayers[$ySub])){ + $extraDataLayers[$ySub] = new PalettedBlockArray(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS); + } + $extraDataLayers[$ySub]->set($x, $y, $z, ($blockId << Block::INTERNAL_METADATA_BITS) | $blockData); + } + + return $extraDataLayers; + } + + /** + * @throws CorruptedChunkException + */ + public function loadChunk(int $chunkX, int $chunkZ) : ?ChunkData{ + $index = LevelDB::chunkIndex($chunkX, $chunkZ); + + $chunkVersionRaw = $this->db->get($index . self::TAG_VERSION); + if($chunkVersionRaw === false){ + return null; + } + + /** @var SubChunk[] $subChunks */ + $subChunks = []; + + /** @var BiomeArray|null $biomeArray */ + $biomeArray = null; + + $chunkVersion = ord($chunkVersionRaw); + $hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION; + + switch($chunkVersion){ + case 15: //MCPE 1.12.0.4 beta (???) + case 14: //MCPE 1.11.1.2 (???) + case 13: //MCPE 1.11.0.4 beta (???) + case 12: //MCPE 1.11.0.3 beta (???) + case 11: //MCPE 1.11.0.1 beta (???) + case 10: //MCPE 1.9 (???) + case 9: //MCPE 1.8 (???) + case 7: //MCPE 1.2 (???) + case 6: //MCPE 1.2.0.2 beta (???) + case 4: //MCPE 1.1 + //TODO: check beds + case 3: //MCPE 1.0 + $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion); + + 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; + } + + $binaryStream = new BinaryStream($data); + if($binaryStream->feof()){ + throw new CorruptedChunkException("Unexpected empty data for subchunk $y"); + } + $subChunkVersion = $binaryStream->getByte(); + if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){ + $hasBeenUpgraded = true; + } + + switch($subChunkVersion){ + case 0: + case 2: //these are all identical to version 0, but vanilla respects these so we should also + case 3: + case 4: + case 5: + case 6: + case 7: + try{ + $blocks = $binaryStream->get(4096); + $blockData = $binaryStream->get(2048); + + if($chunkVersion < 4){ + $binaryStream->get(4096); //legacy light info, discard it + $hasBeenUpgraded = true; + } + }catch(BinaryDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + + $storages = [SubChunkConverter::convertSubChunkXZY($blocks, $blockData)]; + if(isset($convertedLegacyExtraData[$y])){ + $storages[] = $convertedLegacyExtraData[$y]; + } + + $subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages); + break; + case 1: //paletted v1, has a single blockstorage + $storages = [$this->deserializePaletted($binaryStream)]; + if(isset($convertedLegacyExtraData[$y])){ + $storages[] = $convertedLegacyExtraData[$y]; + } + $subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages); + break; + case 8: + //legacy extradata layers intentionally ignored because they aren't supposed to exist in v8 + $storageCount = $binaryStream->getByte(); + if($storageCount > 0){ + $storages = []; + + for($k = 0; $k < $storageCount; ++$k){ + $storages[] = $this->deserializePaletted($binaryStream); + } + $subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages); + } + break; + default: + //TODO: set chunks read-only so the version on disk doesn't get overwritten + throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); + } + } + + if(($maps2d = $this->db->get($index . self::TAG_DATA_2D)) !== false){ + $binaryStream = new BinaryStream($maps2d); + + try{ + $binaryStream->get(512); //heightmap, discard it + $biomeArray = new BiomeArray($binaryStream->get(256)); //never throws + }catch(BinaryDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + } + break; + case 2: // < MCPE 1.0 + case 1: + case 0: //MCPE 0.9.0.1 beta (first version) + $convertedLegacyExtraData = $this->deserializeLegacyExtraData($index, $chunkVersion); + + $legacyTerrain = $this->db->get($index . self::TAG_LEGACY_TERRAIN); + if($legacyTerrain === false){ + throw new CorruptedChunkException("Missing expected LEGACY_TERRAIN tag for format version $chunkVersion"); + } + $binaryStream = new BinaryStream($legacyTerrain); + try{ + $fullIds = $binaryStream->get(32768); + $fullData = $binaryStream->get(16384); + $binaryStream->get(32768); //legacy light info, discard it + }catch(BinaryDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + + for($yy = 0; $yy < 8; ++$yy){ + $storages = [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $yy)]; + if(isset($convertedLegacyExtraData[$yy])){ + $storages[] = $convertedLegacyExtraData[$yy]; + } + $subChunks[$yy] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, $storages); + } + + try{ + $binaryStream->get(256); //heightmap, discard it + /** @var int[] $unpackedBiomeArray */ + $unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here + $biomeArray = new BiomeArray(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws + }catch(BinaryDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + break; + default: + //TODO: set chunks read-only so the version on disk doesn't get overwritten + throw new CorruptedChunkException("don't know how to decode chunk format version $chunkVersion"); + } + + $nbt = new LittleEndianNbtSerializer(); + + /** @var CompoundTag[] $entities */ + $entities = []; + if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false and $entityData !== ""){ + try{ + $entities = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($entityData)); + }catch(NbtDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + } + + /** @var CompoundTag[] $tiles */ + $tiles = []; + if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false and $tileData !== ""){ + try{ + $tiles = array_map(fn(TreeRoot $root) => $root->mustGetCompoundTag(), $nbt->readMultiple($tileData)); + }catch(NbtDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + } + + $finalisationChr = $this->db->get($index . self::TAG_STATE_FINALISATION); + if($finalisationChr !== false){ + $finalisation = ord($finalisationChr); + $terrainPopulated = $finalisation === self::FINALISATION_DONE; + }else{ //older versions didn't have this tag + $terrainPopulated = true; + } + + //TODO: tile ticks, biome states (?) + + $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 + } + + return new ChunkData($chunk, $entities, $tiles); + } + + public function saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{ + $idMap = LegacyBlockIdToStringIdMap::getInstance(); + $index = LevelDB::chunkIndex($chunkX, $chunkZ); + + $write = new \LevelDBWriteBatch(); + $write->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION)); + + $chunk = $chunkData->getChunk(); + if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BLOCKS)){ + $subChunks = $chunk->getSubChunks(); + foreach($subChunks as $y => $subChunk){ + $key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y); + if($subChunk->isEmptyAuthoritative()){ + $write->delete($key); + }else{ + $subStream = new BinaryStream(); + $subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION); + + $layers = $subChunk->getBlockLayers(); + $subStream->putByte(count($layers)); + foreach($layers as $blocks){ + if($blocks->getBitsPerBlock() !== 0){ + $subStream->putByte($blocks->getBitsPerBlock() << 1); + $subStream->put($blocks->getWordArray()); + }else{ + //TODO: we use these in-memory, but they aren't supported on disk by the game yet + //polyfill them with a zero'd 1-bpb instead + $subStream->putByte(1 << 1); + $subStream->put(str_repeat("\x00", PalettedBlockArray::getExpectedWordArraySize(1))); + } + + $palette = $blocks->getPalette(); + $subStream->putLInt(count($palette)); + $tags = []; + foreach($palette as $p){ + $tags[] = new TreeRoot(CompoundTag::create() + ->setString("name", $idMap->legacyToString($p >> Block::INTERNAL_METADATA_BITS) ?? "minecraft:info_update") + ->setInt("oldid", $p >> Block::INTERNAL_METADATA_BITS) //PM only (debugging), vanilla doesn't have this + ->setShort("val", $p & Block::INTERNAL_METADATA_MASK)); + } + + $subStream->put((new LittleEndianNbtSerializer())->writeMultiple($tags)); + } + + $write->put($key, $subStream->getBuffer()); + } + } + } + + if($chunk->getTerrainDirtyFlag(Chunk::DIRTY_FLAG_BIOMES)){ + $write->put($index . self::TAG_DATA_2D, str_repeat("\x00", 512) . $chunk->getBiomeIdArray()); + } + + //TODO: use this properly + $write->put($index . self::TAG_STATE_FINALISATION, chr($chunk->isPopulated() ? self::FINALISATION_DONE : self::FINALISATION_NEEDS_POPULATION)); + + $this->writeTags($chunkData->getTileNBT(), $index . self::TAG_BLOCK_ENTITY, $write); + $this->writeTags($chunkData->getEntityNBT(), $index . self::TAG_ENTITY, $write); + + $write->delete($index . self::TAG_DATA_2D_LEGACY); + $write->delete($index . self::TAG_LEGACY_TERRAIN); + + $this->db->write($write); + } + + /** + * @param CompoundTag[] $targets + */ + private function writeTags(array $targets, string $index, \LevelDBWriteBatch $write) : void{ + if(count($targets) > 0){ + $nbt = new LittleEndianNbtSerializer(); + $write->put($index, $nbt->writeMultiple(array_map(fn(CompoundTag $tag) => new TreeRoot($tag), $targets))); + }else{ + $write->delete($index); + } + } + + public function getDatabase() : \LevelDB{ + return $this->db; + } + + public static function chunkIndex(int $chunkX, int $chunkZ) : string{ + return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ); + } + + public function doGarbageCollection() : void{ + + } + + public function close() : void{ + unset($this->db); + } + + 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){ + $chunkX = Binary::readLInt(substr($key, 0, 4)); + $chunkZ = Binary::readLInt(substr($key, 4, 4)); + try{ + if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){ + yield [$chunkX, $chunkZ] => $chunk; + } + }catch(CorruptedChunkException $e){ + if(!$skipCorrupted){ + throw $e; + } + if($logger !== null){ + $logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")"); + } + } + } + } + } + + public function calculateChunkCount() : int{ + $count = 0; + foreach($this->db->getIterator() as $key => $_){ + if(strlen($key) === 9 and substr($key, -1) === self::TAG_VERSION){ + $count++; + } + } + return $count; + } +} diff --git a/src/world/format/io/region/Anvil.php b/src/world/format/io/region/Anvil.php new file mode 100644 index 0000000000..12e907d205 --- /dev/null +++ b/src/world/format/io/region/Anvil.php @@ -0,0 +1,59 @@ +read($decompressed)->mustGetCompoundTag(); + }catch(NbtDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + $chunk = $chunk->getTag("Level"); + if(!($chunk instanceof CompoundTag)){ + throw new CorruptedChunkException("'Level' key is missing from chunk NBT"); + } + + $subChunks = []; + $subChunksTag = $chunk->getListTag("Sections") ?? []; + foreach($subChunksTag as $subChunk){ + if($subChunk instanceof CompoundTag){ + $subChunks[$subChunk->getByte("Y")] = $this->deserializeSubChunk($subChunk); + } + } + + $makeBiomeArray = function(string $biomeIds) : BiomeArray{ + try{ + return new BiomeArray($biomeIds); + }catch(\InvalidArgumentException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + }; + $biomeArray = null; + if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){ + $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); + } + + return new ChunkData( + 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) : [], + ); + } + + abstract protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk; + +} diff --git a/src/world/format/io/region/McRegion.php b/src/world/format/io/region/McRegion.php new file mode 100644 index 0000000000..bf15973643 --- /dev/null +++ b/src/world/format/io/region/McRegion.php @@ -0,0 +1,124 @@ +read($decompressed)->mustGetCompoundTag(); + }catch(NbtDataException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + $chunk = $chunk->getTag("Level"); + if(!($chunk instanceof CompoundTag)){ + 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); + + for($y = 0; $y < 8; ++$y){ + $subChunks[$y] = new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [SubChunkConverter::convertSubChunkFromLegacyColumn($fullIds, $fullData, $y)]); + } + + $makeBiomeArray = function(string $biomeIds) : BiomeArray{ + try{ + return new BiomeArray($biomeIds); + }catch(\InvalidArgumentException $e){ + throw new CorruptedChunkException($e->getMessage(), 0, $e); + } + }; + $biomeIds = null; + if(($biomeColorsTag = $chunk->getTag("BiomeColors")) instanceof IntArrayTag){ + $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); + } + + return new ChunkData( + 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) : [], + ); + } + + protected static function getRegionFileExtension() : string{ + return "mcr"; + } + + protected static function getPcWorldFormatVersion() : int{ + return 19132; + } + + public function getWorldMinY() : int{ + return 0; + } + + public function getWorldMaxY() : int{ + //TODO: add world height options + return 128; + } +} diff --git a/src/pocketmine/level/format/io/region/PMAnvil.php b/src/world/format/io/region/PMAnvil.php similarity index 55% rename from src/pocketmine/level/format/io/region/PMAnvil.php rename to src/world/format/io/region/PMAnvil.php index 5ef0d665c4..299d597c94 100644 --- a/src/pocketmine/level/format/io/region/PMAnvil.php +++ b/src/world/format/io/region/PMAnvil.php @@ -21,43 +21,41 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; -use pocketmine\level\format\SubChunk; -use pocketmine\nbt\tag\ByteArrayTag; +use pocketmine\block\Block; +use pocketmine\block\BlockLegacyIds; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\world\format\io\SubChunkConverter; +use pocketmine\world\format\SubChunk; /** * This format is exactly the same as the PC Anvil format, with the only difference being that the stored data order * is XZY instead of YZX for more performance loading and saving worlds. */ -class PMAnvil extends Anvil{ - - public const REGION_FILE_EXTENSION = "mcapm"; - - protected function serializeSubChunk(SubChunk $subChunk) : CompoundTag{ - return new CompoundTag("", [ - new ByteArrayTag("Blocks", $subChunk->getBlockIdArray()), - new ByteArrayTag("Data", $subChunk->getBlockDataArray()), - new ByteArrayTag("SkyLight", $subChunk->getBlockSkyLightArray()), - new ByteArrayTag("BlockLight", $subChunk->getBlockLightArray()) - ]); - } +class PMAnvil extends RegionWorldProvider{ + use LegacyAnvilChunkTrait; protected function deserializeSubChunk(CompoundTag $subChunk) : SubChunk{ - return new SubChunk( - $subChunk->getByteArray("Blocks"), - $subChunk->getByteArray("Data"), - $subChunk->getByteArray("SkyLight"), - $subChunk->getByteArray("BlockLight") - ); + return new SubChunk(BlockLegacyIds::AIR << Block::INTERNAL_METADATA_BITS, [SubChunkConverter::convertSubChunkXZY( + self::readFixedSizeByteArray($subChunk, "Blocks", 4096), + self::readFixedSizeByteArray($subChunk, "Data", 2048) + )]); } - public static function getProviderName() : string{ - return "pmanvil"; + protected static function getRegionFileExtension() : string{ + return "mcapm"; } - public static function getPcWorldFormatVersion() : int{ + protected static function getPcWorldFormatVersion() : int{ return -1; //Not a PC format, only PocketMine-MP } + + public function getWorldMinY() : int{ + return 0; + } + + public function getWorldMaxY() : int{ + return 256; + } } diff --git a/src/pocketmine/level/format/io/region/RegionException.php b/src/world/format/io/region/RegionException.php similarity index 94% rename from src/pocketmine/level/format/io/region/RegionException.php rename to src/world/format/io/region/RegionException.php index d3e3a6bf17..44cb1ff5fc 100644 --- a/src/pocketmine/level/format/io/region/RegionException.php +++ b/src/world/format/io/region/RegionException.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; class RegionException extends \RuntimeException{ diff --git a/src/pocketmine/level/format/io/region/RegionGarbageMap.php b/src/world/format/io/region/RegionGarbageMap.php similarity index 99% rename from src/pocketmine/level/format/io/region/RegionGarbageMap.php rename to src/world/format/io/region/RegionGarbageMap.php index d076529331..a3dd8e5426 100644 --- a/src/pocketmine/level/format/io/region/RegionGarbageMap.php +++ b/src/world/format/io/region/RegionGarbageMap.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; use pocketmine\utils\AssumptionFailedError; use function end; diff --git a/src/pocketmine/level/format/io/region/RegionLoader.php b/src/world/format/io/region/RegionLoader.php similarity index 87% rename from src/pocketmine/level/format/io/region/RegionLoader.php rename to src/world/format/io/region/RegionLoader.php index 152d4aa670..dd50f6c38d 100644 --- a/src/pocketmine/level/format/io/region/RegionLoader.php +++ b/src/world/format/io/region/RegionLoader.php @@ -21,14 +21,14 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; -use pocketmine\level\format\ChunkException; -use pocketmine\level\format\io\exception\CorruptedChunkException; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Binary; use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryStream; +use pocketmine\world\format\ChunkException; +use pocketmine\world\format\io\exception\CorruptedChunkException; use function assert; use function ceil; use function chr; @@ -44,7 +44,6 @@ use function fwrite; use function is_resource; use function ksort; use function max; -use function pack; use function str_pad; use function str_repeat; use function stream_set_read_buffer; @@ -57,22 +56,17 @@ use const SORT_NUMERIC; use const STR_PAD_RIGHT; class RegionLoader{ - public const VERSION = 1; public const COMPRESSION_GZIP = 1; public const COMPRESSION_ZLIB = 2; - public const MAX_SECTOR_LENGTH = 255 << 12; //255 sectors (~0.996 MiB) - public const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps + private const MAX_SECTOR_LENGTH = 255 << 12; //255 sectors (~0.996 MiB) + private const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps public const FIRST_SECTOR = 2; //location table occupies 0 and 1 /** @var int */ public static $COMPRESSION_LEVEL = 7; - /** @var int */ - protected $x; - /** @var int */ - protected $z; /** @var string */ protected $filePath; /** @var resource */ @@ -86,43 +80,52 @@ class RegionLoader{ /** @var int */ public $lastUsed = 0; - public function __construct(string $filePath, int $regionX, int $regionZ){ - $this->x = $regionX; - $this->z = $regionZ; - $this->filePath = $filePath; - $this->garbageTable = new RegionGarbageMap([]); - } - /** - * @return void * @throws CorruptedRegionException */ - public function open(){ - clearstatcache(false, $this->filePath); - $exists = file_exists($this->filePath); - if(!$exists){ - touch($this->filePath); - }elseif(filesize($this->filePath) % 4096 !== 0){ - throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB"); - } + private function __construct(string $filePath){ + $this->filePath = $filePath; + $this->garbageTable = new RegionGarbageMap([]); + $this->lastUsed = time(); $filePointer = fopen($this->filePath, "r+b"); if($filePointer === false) throw new AssumptionFailedError("fopen() should not fail here"); $this->filePointer = $filePointer; stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB - if(!$exists){ - $this->createBlank(); - }else{ - $this->loadLocationTable(); + } + + /** + * @throws CorruptedRegionException + */ + public static function loadExisting(string $filePath) : self{ + clearstatcache(false, $filePath); + if(!file_exists($filePath)){ + throw new \RuntimeException("File $filePath does not exist"); + } + if(filesize($filePath) % 4096 !== 0){ + throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB"); } - $this->lastUsed = time(); + $result = new self($filePath); + $result->loadLocationTable(); + return $result; + } + + public static function createNew(string $filePath) : self{ + clearstatcache(false, $filePath); + if(file_exists($filePath)){ + throw new \RuntimeException("Region file $filePath already exists"); + } + touch($filePath); + + $result = new self($filePath); + $result->createBlank(); + return $result; } public function __destruct(){ if(is_resource($this->filePointer)){ - $this->writeLocationTable(); fclose($this->filePointer); } } @@ -201,11 +204,10 @@ class RegionLoader{ } /** - * @return void * @throws ChunkException * @throws \InvalidArgumentException */ - public function writeChunk(int $x, int $z, string $chunkData){ + public function writeChunk(int $x, int $z, string $chunkData) : void{ $this->lastUsed = time(); $length = strlen($chunkData) + 1; @@ -249,10 +251,9 @@ class RegionLoader{ } /** - * @return void * @throws \InvalidArgumentException */ - public function removeChunk(int $x, int $z){ + public function removeChunk(int $x, int $z) : void{ $index = self::getChunkOffset($x, $z); $oldLocation = $this->locationTable[$index]; $this->locationTable[$index] = null; @@ -282,25 +283,18 @@ class RegionLoader{ } /** - * Writes the region header and closes the file - * - * @return void + * Closes the file */ - public function close(bool $writeHeader = true){ + public function close() : void{ if(is_resource($this->filePointer)){ - if($writeHeader){ - $this->writeLocationTable(); - } - fclose($this->filePointer); } } /** - * @return void * @throws CorruptedRegionException */ - protected function loadLocationTable(){ + protected function loadLocationTable() : void{ fseek($this->filePointer, 0); $headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH); @@ -382,27 +376,7 @@ class RegionLoader{ } } - private function writeLocationTable() : void{ - $write = []; - - for($i = 0; $i < 1024; ++$i){ - $entry = $this->locationTable[$i]; - $write[] = $entry !== null ? (($entry->getFirstSector() << 8) | $entry->getSectorCount()) : 0; - } - for($i = 0; $i < 1024; ++$i){ - $entry = $this->locationTable[$i]; - $write[] = $entry !== null ? $entry->getTimestamp() : 0; - } - fseek($this->filePointer, 0); - fwrite($this->filePointer, pack("N*", ...$write), 4096 * 2); - } - - /** - * @param int $index - * - * @return void - */ - protected function writeLocationIndex($index){ + protected function writeLocationIndex(int $index) : void{ $entry = $this->locationTable[$index]; fseek($this->filePointer, $index << 2); fwrite($this->filePointer, Binary::writeInt($entry !== null ? ($entry->getFirstSector() << 8) | $entry->getSectorCount() : 0), 4); @@ -411,10 +385,7 @@ class RegionLoader{ clearstatcache(false, $this->filePath); } - /** - * @return void - */ - protected function createBlank(){ + protected function createBlank() : void{ fseek($this->filePointer, 0); ftruncate($this->filePointer, 8192); // this fills the file with the null byte for($i = 0; $i < 1024; ++$i){ @@ -462,15 +433,17 @@ class RegionLoader{ return 1 - ($used / $size); } - public function getX() : int{ - return $this->x; - } - - public function getZ() : int{ - return $this->z; - } - public function getFilePath() : string{ return $this->filePath; } + + public function calculateChunkCount() : int{ + $count = 0; + for($i = 0; $i < 1024; ++$i){ + if($this->isChunkGenerated($i)){ + $count++; + } + } + return $count; + } } diff --git a/src/pocketmine/level/format/io/region/RegionLocationTableEntry.php b/src/world/format/io/region/RegionLocationTableEntry.php similarity index 98% rename from src/pocketmine/level/format/io/region/RegionLocationTableEntry.php rename to src/world/format/io/region/RegionLocationTableEntry.php index 764c0eb9bf..8a7fdfaeb1 100644 --- a/src/pocketmine/level/format/io/region/RegionLocationTableEntry.php +++ b/src/world/format/io/region/RegionLocationTableEntry.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; use function range; diff --git a/src/world/format/io/region/RegionWorldProvider.php b/src/world/format/io/region/RegionWorldProvider.php new file mode 100644 index 0000000000..1fe62912c7 --- /dev/null +++ b/src/world/format/io/region/RegionWorldProvider.php @@ -0,0 +1,260 @@ +getPath(), "level.dat")); + } + + public function doGarbageCollection() : void{ + $limit = time() - 300; + foreach($this->regions as $index => $region){ + if($region->lastUsed <= $limit){ + $region->close(); + unset($this->regions[$index]); + } + } + } + + /** + * @param int $regionX reference parameter + * @param int $regionZ reference parameter + */ + public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ) : void{ + $regionX = $chunkX >> 5; + $regionZ = $chunkZ >> 5; + } + + protected function getRegion(int $regionX, int $regionZ) : ?RegionLoader{ + return $this->regions[morton2d_encode($regionX, $regionZ)] ?? null; + } + + /** + * Returns the path to a specific region file based on its X/Z coordinates + */ + protected function pathToRegion(int $regionX, int $regionZ) : string{ + return Path::join($this->path, "region", "r.$regionX.$regionZ." . static::getRegionFileExtension()); + } + + protected function loadRegion(int $regionX, int $regionZ) : RegionLoader{ + if(!isset($this->regions[$index = morton2d_encode($regionX, $regionZ)])){ + $path = $this->pathToRegion($regionX, $regionZ); + + try{ + $this->regions[$index] = RegionLoader::loadExisting($path); + }catch(CorruptedRegionException $e){ + $logger = \GlobalLogger::get(); + $logger->error("Corrupted region file detected: " . $e->getMessage()); + + $backupPath = $path . ".bak." . time(); + rename($path, $backupPath); + $logger->error("Corrupted region file has been backed up to " . $backupPath); + + $this->regions[$index] = RegionLoader::createNew($path); + } + } + return $this->regions[$index]; + } + + protected function unloadRegion(int $regionX, int $regionZ) : void{ + if(isset($this->regions[$hash = morton2d_encode($regionX, $regionZ)])){ + $this->regions[$hash]->close(); + unset($this->regions[$hash]); + } + } + + public function close() : void{ + foreach($this->regions as $index => $region){ + $region->close(); + unset($this->regions[$index]); + } + } + + /** + * @throws CorruptedChunkException + */ + abstract protected function deserializeChunk(string $data) : ?ChunkData; + + /** + * @return CompoundTag[] + * @throws CorruptedChunkException + */ + protected static function getCompoundList(string $context, ListTag $list) : array{ + if($list->count() === 0){ //empty lists might have wrong types, we don't care + return []; + } + if($list->getTagType() !== NBT::TAG_Compound){ + throw new CorruptedChunkException("Expected TAG_List for '$context'"); + } + $result = []; + foreach($list as $tag){ + if(!($tag instanceof CompoundTag)){ + //this should never happen, but it's still possible due to lack of native type safety + throw new CorruptedChunkException("Expected TAG_List for '$context'"); + } + $result[] = $tag; + } + return $result; + } + + 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(); + if(strlen($data) !== $length){ + throw new CorruptedChunkException("Expected '$tagName' payload to have exactly $length bytes, but have " . strlen($data)); + } + return $data; + } + + /** + * @throws CorruptedChunkException + */ + 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)); + + if(!file_exists($this->pathToRegion($regionX, $regionZ))){ + return null; + } + + $chunkData = $this->loadRegion($regionX, $regionZ)->readChunk($chunkX & 0x1f, $chunkZ & 0x1f); + if($chunkData !== null){ + return $this->deserializeChunk($chunkData); + } + + return null; + } + + private function createRegionIterator() : \RegexIterator{ + return new \RegexIterator( + new \FilesystemIterator( + Path::join($this->path, 'region'), + \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS + ), + '/\/r\.(-?\d+)\.(-?\d+)\.' . static::getRegionFileExtension() . '$/', + \RegexIterator::GET_MATCH + ); + } + + public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{ + $iterator = $this->createRegionIterator(); + + foreach($iterator as $region){ + $regionX = ((int) $region[1]); + $regionZ = ((int) $region[2]); + $rX = $regionX << 5; + $rZ = $regionZ << 5; + + for($chunkX = $rX; $chunkX < $rX + 32; ++$chunkX){ + for($chunkZ = $rZ; $chunkZ < $rZ + 32; ++$chunkZ){ + try{ + $chunk = $this->loadChunk($chunkX, $chunkZ); + if($chunk !== null){ + yield [$chunkX, $chunkZ] => $chunk; + } + }catch(CorruptedChunkException $e){ + if(!$skipCorrupted){ + throw $e; + } + if($logger !== null){ + $logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")"); + } + } + } + } + + $this->unloadRegion($regionX, $regionZ); + } + } + + public function calculateChunkCount() : int{ + $count = 0; + foreach($this->createRegionIterator() as $region){ + $regionX = ((int) $region[1]); + $regionZ = ((int) $region[2]); + $count += $this->loadRegion($regionX, $regionZ)->calculateChunkCount(); + $this->unloadRegion($regionX, $regionZ); + } + return $count; + } +} diff --git a/src/world/format/io/region/WritableRegionWorldProvider.php b/src/world/format/io/region/WritableRegionWorldProvider.php new file mode 100644 index 0000000000..ae4c5ee06e --- /dev/null +++ b/src/world/format/io/region/WritableRegionWorldProvider.php @@ -0,0 +1,60 @@ +loadRegion($regionX, $regionZ)->writeChunk($chunkX & 0x1f, $chunkZ & 0x1f, $this->serializeChunk($chunkData)); + } +} diff --git a/src/world/generator/Flat.php b/src/world/generator/Flat.php new file mode 100644 index 0000000000..26a658b6d6 --- /dev/null +++ b/src/world/generator/Flat.php @@ -0,0 +1,101 @@ +options = FlatGeneratorOptions::parsePreset($this->preset); + + if(isset($this->options->getExtraOptions()["decoration"])){ + $ores = new Ore(); + $stone = VanillaBlocks::STONE(); + $ores->setOreTypes([ + new OreType(VanillaBlocks::COAL_ORE(), $stone, 20, 16, 0, 128), + new OreType(VanillaBlocks::IRON_ORE(), $stone, 20, 8, 0, 64), + new OreType(VanillaBlocks::REDSTONE_ORE(), $stone, 8, 7, 0, 16), + new OreType(VanillaBlocks::LAPIS_LAZULI_ORE(), $stone, 1, 6, 0, 32), + new OreType(VanillaBlocks::GOLD_ORE(), $stone, 2, 8, 0, 32), + new OreType(VanillaBlocks::DIAMOND_ORE(), $stone, 1, 7, 0, 16), + new OreType(VanillaBlocks::DIRT(), $stone, 20, 32, 0, 128), + new OreType(VanillaBlocks::GRAVEL(), $stone, 10, 16, 0, 128) + ]); + $this->populators[] = $ores; + } + + $this->generateBaseChunk(); + } + + protected function generateBaseChunk() : void{ + $this->chunk = new Chunk([], BiomeArray::fill($this->options->getBiomeId()), false); + + $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($structure[$y | $sy]); ++$y){ + $id = $structure[$y | $sy]; + + for($Z = 0; $Z < SubChunk::EDGE_LENGTH; ++$Z){ + for($X = 0; $X < SubChunk::EDGE_LENGTH; ++$X){ + $subchunk->setFullBlock($X, $y, $Z, $id); + } + } + } + } + } + + public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $world->setChunk($chunkX, $chunkZ, clone $this->chunk); + } + + public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed); + foreach($this->populators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $this->random); + } + + } +} diff --git a/src/world/generator/FlatGeneratorOptions.php b/src/world/generator/FlatGeneratorOptions.php new file mode 100644 index 0000000000..01ca4a9146 --- /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); + } + +} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/protocol/MapInfoRequestPacket.php b/src/world/generator/Gaussian.php similarity index 53% rename from src/pocketmine/network/mcpe/protocol/MapInfoRequestPacket.php rename to src/world/generator/Gaussian.php index 58c23ffdab..61b0f86d96 100644 --- a/src/pocketmine/network/mcpe/protocol/MapInfoRequestPacket.php +++ b/src/world/generator/Gaussian.php @@ -21,27 +21,31 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\world\generator; -#include +use function exp; -use pocketmine\network\mcpe\NetworkSession; - -class MapInfoRequestPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::MAP_INFO_REQUEST_PACKET; +final class Gaussian{ /** @var int */ - public $mapId; + public $smoothSize; + /** @var float[][] */ + public $kernel = []; - protected function decodePayload(){ - $this->mapId = $this->getEntityUniqueId(); - } + public function __construct(int $smoothSize){ + $this->smoothSize = $smoothSize; - protected function encodePayload(){ - $this->putEntityUniqueId($this->mapId); - } + $bellSize = 1 / $this->smoothSize; + $bellHeight = 2 * $this->smoothSize; - public function handle(NetworkSession $session) : bool{ - return $session->handleMapInfoRequest($this); + for($sx = -$this->smoothSize; $sx <= $this->smoothSize; ++$sx){ + $this->kernel[$sx + $this->smoothSize] = []; + + for($sz = -$this->smoothSize; $sz <= $this->smoothSize; ++$sz){ + $bx = $bellSize * $sx; + $bz = $bellSize * $sz; + $this->kernel[$sx + $this->smoothSize][$sz + $this->smoothSize] = $bellHeight * exp(-($bx * $bx + $bz * $bz) / 2); + } + } } } diff --git a/src/pocketmine/level/generator/Generator.php b/src/world/generator/Generator.php similarity index 59% rename from src/pocketmine/level/generator/Generator.php rename to src/world/generator/Generator.php index fa44ad9d31..acc8cc88fc 100644 --- a/src/pocketmine/level/generator/Generator.php +++ b/src/world/generator/Generator.php @@ -22,20 +22,19 @@ declare(strict_types=1); /** - * Noise classes used in Levels + * Noise classes used in world generation */ -namespace pocketmine\level\generator; +namespace pocketmine\world\generator; -use pocketmine\level\ChunkManager; -use pocketmine\math\Vector3; use pocketmine\utils\Random; use pocketmine\utils\Utils; +use pocketmine\world\ChunkManager; use function preg_match; abstract class Generator{ /** - * Converts a string level seed into an integer for use by the generator. + * Converts a string world seed into an integer for use by the generator. */ public static function convertSeed(string $seed) : ?int{ if($seed === ""){ //empty seed should cause a random seed to be selected - can't use 0 here because 0 is a valid seed @@ -49,35 +48,21 @@ abstract class Generator{ return $convertedSeed; } - /** @var ChunkManager */ - protected $level; + /** @var int */ + protected $seed; + + protected string $preset; + /** @var Random */ protected $random; - /** - * @throws InvalidGeneratorOptionsException - * - * @param mixed[] $settings - * @phpstan-param array $settings - */ - abstract public function __construct(array $settings = []); - - public function init(ChunkManager $level, Random $random) : void{ - $this->level = $level; - $this->random = $random; + public function __construct(int $seed, string $preset){ + $this->seed = $seed; + $this->preset = $preset; + $this->random = new Random($seed); } - abstract public function generateChunk(int $chunkX, int $chunkZ) : void; + abstract public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void; - abstract public function populateChunk(int $chunkX, int $chunkZ) : void; - - /** - * @return mixed[] - * @phpstan-return array - */ - abstract public function getSettings() : array; - - abstract public function getName() : string; - - abstract public function getSpawn() : Vector3; + abstract public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void; } diff --git a/src/world/generator/GeneratorManager.php b/src/world/generator/GeneratorManager.php new file mode 100644 index 0000000000..0e29cc68b1 --- /dev/null +++ b/src/world/generator/GeneratorManager.php @@ -0,0 +1,116 @@ + classname mapping + * @phpstan-var array + */ + private $list = []; + + public function __construct(){ + $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 \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, \Closure $presetValidator, bool $overwrite = false) : void{ + Utils::testValidInstance($class, Generator::class); + + if(!$overwrite and isset($this->list[$name = strtolower($name)])){ + throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); + } + + $this->list[$name] = new GeneratorManagerEntry($class, $presetValidator); + } + + /** + * Returns a list of names for registered generators. + * + * @return string[] + */ + public function getGeneratorList() : array{ + return array_keys($this->list); + } + + /** + * Returns the generator entry of a registered Generator matching the given name, or null if not found. + */ + public function getGenerator(string $name) : ?GeneratorManagerEntry{ + return $this->list[strtolower($name)] ?? null; + } + + /** + * Returns the registered name of the given Generator class. + * + * @param string $class Fully qualified name of class that extends \pocketmine\world\generator\Generator + * @phpstan-param class-string $class + * + * @throws \InvalidArgumentException if the class type cannot be matched to a known alias + */ + public function getGeneratorName(string $class) : string{ + Utils::testValidInstance($class, Generator::class); + foreach(Utils::stringifyKeys($this->list) as $name => $c){ + if($c->getGeneratorClass() === $class){ + return $name; + } + } + + throw new \InvalidArgumentException("Generator class $class is not registered"); + } +} diff --git a/src/pocketmine/network/mcpe/protocol/types/PersonaPieceTintColor.php b/src/world/generator/GeneratorManagerEntry.php similarity index 52% rename from src/pocketmine/network/mcpe/protocol/types/PersonaPieceTintColor.php rename to src/world/generator/GeneratorManagerEntry.php index 05231d3f1b..2f7bbc16c5 100644 --- a/src/pocketmine/network/mcpe/protocol/types/PersonaPieceTintColor.php +++ b/src/world/generator/GeneratorManagerEntry.php @@ -21,35 +21,28 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\world\generator; -final class PersonaPieceTintColor{ - - public const PIECE_TYPE_PERSONA_EYES = "persona_eyes"; - public const PIECE_TYPE_PERSONA_HAIR = "persona_hair"; - public const PIECE_TYPE_PERSONA_MOUTH = "persona_mouth"; - - /** @var string */ - private $pieceType; - /** @var string[] */ - private $colors; +final class GeneratorManagerEntry{ /** - * @param string[] $colors + * @phpstan-param class-string $generatorClass + * @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator */ - public function __construct(string $pieceType, array $colors){ - $this->pieceType = $pieceType; - $this->colors = $colors; - } + public function __construct( + private string $generatorClass, + private \Closure $presetValidator + ){} - public function getPieceType() : string{ - return $this->pieceType; - } + /** @phpstan-return class-string */ + public function getGeneratorClass() : string{ return $this->generatorClass; } /** - * @return string[] + * @throws InvalidGeneratorOptionsException */ - public function getColors() : array{ - return $this->colors; + public function validateGeneratorOptions(string $generatorOptions) : void{ + if(($exception = ($this->presetValidator)($generatorOptions)) !== null){ + throw $exception; + } } -} +} \ No newline at end of file diff --git a/src/pocketmine/level/generator/GeneratorRegisterTask.php b/src/world/generator/GeneratorRegisterTask.php similarity index 50% rename from src/pocketmine/level/generator/GeneratorRegisterTask.php rename to src/world/generator/GeneratorRegisterTask.php index 03f58339f2..b8b2ea67d0 100644 --- a/src/pocketmine/level/generator/GeneratorRegisterTask.php +++ b/src/world/generator/GeneratorRegisterTask.php @@ -21,17 +21,10 @@ declare(strict_types=1); -namespace pocketmine\level\generator; +namespace pocketmine\world\generator; -use pocketmine\block\BlockFactory; -use pocketmine\item\ItemFactory; -use pocketmine\level\biome\Biome; -use pocketmine\level\Level; -use pocketmine\level\SimpleChunkManager; use pocketmine\scheduler\AsyncTask; -use pocketmine\utils\Random; -use function serialize; -use function unserialize; +use pocketmine\world\World; class GeneratorRegisterTask extends AsyncTask{ @@ -45,36 +38,30 @@ class GeneratorRegisterTask extends AsyncTask{ /** @var int */ public $seed; /** @var int */ - public $levelId; + public $worldId; /** @var int */ - public $worldHeight = Level::Y_MAX; + public $worldMinY; + /** @var int */ + public $worldMaxY; /** - * @param mixed[] $generatorSettings * @phpstan-param class-string $generatorClass - * @phpstan-param array $generatorSettings */ - public function __construct(Level $level, string $generatorClass, array $generatorSettings = []){ + public function __construct(World $world, string $generatorClass, string $generatorSettings){ $this->generatorClass = $generatorClass; - $this->settings = serialize($generatorSettings); - $this->seed = $level->getSeed(); - $this->levelId = $level->getId(); - $this->worldHeight = $level->getWorldHeight(); + $this->settings = $generatorSettings; + $this->seed = $world->getSeed(); + $this->worldId = $world->getId(); + $this->worldMinY = $world->getMinY(); + $this->worldMaxY = $world->getMaxY(); } - public function onRun(){ - BlockFactory::init(); - ItemFactory::init(); - Biome::init(); - $manager = new SimpleChunkManager($this->seed, $this->worldHeight); - $this->saveToThreadStore("generation.level{$this->levelId}.manager", $manager); - + public function onRun() : void{ /** * @var Generator $generator * @see Generator::__construct() */ - $generator = new $this->generatorClass(unserialize($this->settings)); - $generator->init($manager, new Random($manager->getSeed())); - $this->saveToThreadStore("generation.level{$this->levelId}.generator", $generator); + $generator = new $this->generatorClass($this->seed, $this->settings); + ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId); } } diff --git a/src/pocketmine/level/generator/GeneratorUnregisterTask.php b/src/world/generator/GeneratorUnregisterTask.php similarity index 71% rename from src/pocketmine/level/generator/GeneratorUnregisterTask.php rename to src/world/generator/GeneratorUnregisterTask.php index 597c2fe4f5..c9022e9e2b 100644 --- a/src/pocketmine/level/generator/GeneratorUnregisterTask.php +++ b/src/world/generator/GeneratorUnregisterTask.php @@ -21,22 +21,21 @@ declare(strict_types=1); -namespace pocketmine\level\generator; +namespace pocketmine\world\generator; -use pocketmine\level\Level; use pocketmine\scheduler\AsyncTask; +use pocketmine\world\World; class GeneratorUnregisterTask extends AsyncTask{ /** @var int */ - public $levelId; + public $worldId; - public function __construct(Level $level){ - $this->levelId = $level->getId(); + public function __construct(World $world){ + $this->worldId = $world->getId(); } - public function onRun(){ - $this->removeFromThreadStore("generation.level{$this->levelId}.manager"); - $this->removeFromThreadStore("generation.level{$this->levelId}.generator"); + public function onRun() : void{ + ThreadLocalGeneratorContext::unregister($this->worldId); } } diff --git a/src/pocketmine/level/generator/InvalidGeneratorOptionsException.php b/src/world/generator/InvalidGeneratorOptionsException.php similarity index 95% rename from src/pocketmine/level/generator/InvalidGeneratorOptionsException.php rename to src/world/generator/InvalidGeneratorOptionsException.php index 9489589f0d..b6274f91b8 100644 --- a/src/pocketmine/level/generator/InvalidGeneratorOptionsException.php +++ b/src/world/generator/InvalidGeneratorOptionsException.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\generator; +namespace pocketmine\world\generator; class InvalidGeneratorOptionsException extends \UnexpectedValueException{ diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php new file mode 100644 index 0000000000..a50f884fca --- /dev/null +++ b/src/world/generator/PopulationTask.php @@ -0,0 +1,153 @@ + $adjacentChunks) : void + */ +class PopulationTask extends AsyncTask{ + private const TLS_KEY_ON_COMPLETION = "onCompletion"; + + private int $worldId; + private int $chunkX; + private int $chunkZ; + + private ?string $chunk; + + private string $adjacentChunks; + + /** + * @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::serializeTerrain($chunk) : 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_ON_COMPLETION, $onCompletion); + } + + public function onRun() : void{ + $context = ThreadLocalGeneratorContext::fetch($this->worldId); + if($context === null){ + throw new AssumptionFailedError("Generator context should have been initialized before any PopulationTask execution"); + } + $generator = $context->getGenerator(); + $manager = new SimpleChunkManager($context->getWorldMinY(), $context->getWorldMaxY()); + + $chunk = $this->chunk !== null ? FastChunkSerializer::deserializeTerrain($this->chunk) : null; + + /** @var string[] $serialChunks */ + $serialChunks = igbinary_unserialize($this->adjacentChunks); + $chunks = array_map( + fn(?string $serialized) => $serialized !== null ? FastChunkSerializer::deserializeTerrain($serialized) : null, + $serialChunks + ); + + 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 $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::serializeTerrain($chunk); + + $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 \Closure $onCompletion + * @phpstan-var OnCompletion $onCompletion + */ + $onCompletion = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); + + $chunk = $this->chunk !== null ? + FastChunkSerializer::deserializeTerrain($this->chunk) : + throw new AssumptionFailedError("Center chunk should never be null"); + + /** + * @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); + } + } + + $onCompletion($chunk, $adjacentChunks); + } +} diff --git a/src/world/generator/ThreadLocalGeneratorContext.php b/src/world/generator/ThreadLocalGeneratorContext.php new file mode 100644 index 0000000000..56a9b78f6a --- /dev/null +++ b/src/world/generator/ThreadLocalGeneratorContext.php @@ -0,0 +1,67 @@ + + */ + private static $contexts = []; + + public static function register(self $context, int $worldId) : void{ + self::$contexts[$worldId] = $context; + } + + public static function unregister(int $worldId) : void{ + unset(self::$contexts[$worldId]); + } + + public static function fetch(int $worldId) : ?self{ + return self::$contexts[$worldId] ?? null; + } + + /** @var Generator */ + private $generator; + + /** @var int */ + private $worldMinY; + /** @var int */ + private $worldMaxY; + + public function __construct(Generator $generator, int $worldMinY, int $worldMaxY){ + $this->generator = $generator; + $this->worldMinY = $worldMinY; + $this->worldMaxY = $worldMaxY; + } + + public function getGenerator() : Generator{ return $this->generator; } + + public function getWorldMinY() : int{ return $this->worldMinY; } + + public function getWorldMaxY() : int{ return $this->worldMaxY; } +} diff --git a/src/pocketmine/level/generator/biome/BiomeSelector.php b/src/world/generator/biome/BiomeSelector.php similarity index 76% rename from src/pocketmine/level/generator/biome/BiomeSelector.php rename to src/world/generator/biome/BiomeSelector.php index 4688e9e88d..f61f534ce6 100644 --- a/src/pocketmine/level/generator/biome/BiomeSelector.php +++ b/src/world/generator/biome/BiomeSelector.php @@ -21,12 +21,13 @@ declare(strict_types=1); -namespace pocketmine\level\generator\biome; +namespace pocketmine\world\generator\biome; -use pocketmine\level\biome\Biome; -use pocketmine\level\biome\UnknownBiome; -use pocketmine\level\generator\noise\Simplex; use pocketmine\utils\Random; +use pocketmine\world\biome\Biome; +use pocketmine\world\biome\BiomeRegistry; +use pocketmine\world\biome\UnknownBiome; +use pocketmine\world\generator\noise\Simplex; abstract class BiomeSelector{ /** @var Simplex */ @@ -52,15 +53,13 @@ abstract class BiomeSelector{ */ abstract protected function lookup(float $temperature, float $rainfall) : int; - /** - * @return void - */ - public function recalculate(){ + public function recalculate() : void{ $this->map = new \SplFixedArray(64 * 64); + $biomeRegistry = BiomeRegistry::getInstance(); for($i = 0; $i < 64; ++$i){ for($j = 0; $j < 64; ++$j){ - $biome = Biome::getBiome($this->lookup($i / 63, $j / 63)); + $biome = $biomeRegistry->getBiome($this->lookup($i / 63, $j / 63)); if($biome instanceof UnknownBiome){ throw new \RuntimeException("Unknown biome returned by selector with ID " . $biome->getId()); } @@ -69,31 +68,15 @@ abstract class BiomeSelector{ } } - /** - * @param float $x - * @param float $z - * - * @return float - */ - public function getTemperature($x, $z){ + public function getTemperature(float $x, float $z) : float{ return ($this->temperature->noise2D($x, $z, true) + 1) / 2; } - /** - * @param float $x - * @param float $z - * - * @return float - */ - public function getRainfall($x, $z){ + public function getRainfall(float $x, float $z) : float{ return ($this->rainfall->noise2D($x, $z, true) + 1) / 2; } - /** - * @param int $x - * @param int $z - */ - public function pickBiome($x, $z) : Biome{ + public function pickBiome(float $x, float $z) : Biome{ $temperature = (int) ($this->getTemperature($x, $z) * 63); $rainfall = (int) ($this->getRainfall($x, $z) * 63); diff --git a/src/world/generator/hell/Nether.php b/src/world/generator/hell/Nether.php new file mode 100644 index 0000000000..8657e25808 --- /dev/null +++ b/src/world/generator/hell/Nether.php @@ -0,0 +1,120 @@ +noiseBase = new Simplex($this->random, 4, 1 / 4, 1 / 64); + $this->random->setSeed($this->seed); + + $ores = new Ore(); + $ores->setOreTypes([ + new OreType(VanillaBlocks::NETHER_QUARTZ_ORE(), VanillaBlocks::NETHERRACK(), 16, 14, 10, 117) + ]); + $this->populators[] = $ores; + } + + public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed); + + $noise = $this->noiseBase->getFastNoise3D(Chunk::EDGE_LENGTH, 128, Chunk::EDGE_LENGTH, 4, 8, 4, $chunkX * Chunk::EDGE_LENGTH, 0, $chunkZ * Chunk::EDGE_LENGTH); + + $chunk = $world->getChunk($chunkX, $chunkZ); + + $bedrock = VanillaBlocks::BEDROCK()->getFullId(); + $netherrack = VanillaBlocks::NETHERRACK()->getFullId(); + $stillLava = VanillaBlocks::LAVA()->getFullId(); + + for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){ + for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){ + $chunk->setBiomeId($x, $z, BiomeIds::HELL); + + for($y = 0; $y < 128; ++$y){ + if($y === 0 or $y === 127){ + $chunk->setFullBlock($x, $y, $z, $bedrock); + continue; + } + $noiseValue = (abs($this->emptyHeight - $y) / $this->emptyHeight) * $this->emptyAmplitude - $noise[$x][$z][$y]; + $noiseValue -= 1 - $this->density; + + if($noiseValue > 0){ + $chunk->setFullBlock($x, $y, $z, $netherrack); + }elseif($y <= $this->waterHeight){ + $chunk->setFullBlock($x, $y, $z, $stillLava); + } + } + } + } + + foreach($this->generationPopulators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $this->random); + } + } + + public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed); + foreach($this->populators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $this->random); + } + + $chunk = $world->getChunk($chunkX, $chunkZ); + $biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7)); + $biome->populateChunk($world, $chunkX, $chunkZ, $this->random); + } +} diff --git a/src/pocketmine/level/generator/noise/Noise.php b/src/world/generator/noise/Noise.php similarity index 83% rename from src/pocketmine/level/generator/noise/Noise.php rename to src/world/generator/noise/Noise.php index 727dd82998..fde81d10b1 100644 --- a/src/pocketmine/level/generator/noise/Noise.php +++ b/src/world/generator/noise/Noise.php @@ -22,55 +22,14 @@ declare(strict_types=1); /** - * Different noise generators for level generation + * Different noise generators for world generation */ -namespace pocketmine\level\generator\noise; +namespace pocketmine\world\generator\noise; use function array_fill; use function assert; abstract class Noise{ - /** @var int[] */ - protected $perm = []; - /** @var float */ - protected $offsetX = 0; - /** @var float */ - protected $offsetY = 0; - /** @var float */ - protected $offsetZ = 0; - /** @var int */ - protected $octaves = 8; - /** @var float */ - protected $persistence; - /** @var float */ - protected $expansion; - - /** - * @param float $x - */ - public static function floor($x) : int{ - return $x >= 0 ? (int) $x : (int) ($x - 1); - } - - /** - * @param float $x - * - * @return float - */ - public static function fade($x){ - return $x * $x * $x * ($x * ($x * 6 - 15) + 10); - } - - /** - * @param float $x - * @param float $y - * @param float $z - * - * @return float - */ - public static function lerp($x, $y, $z){ - return $y + $x * ($z - $y); - } /** * @param float $x @@ -152,20 +111,17 @@ abstract class Noise{ ); } - /** - * @param int $hash - * @param float $x - * @param float $y - * @param float $z - * - * @return float - */ - public static function grad($hash, $x, $y, $z){ - $hash &= 15; - $u = $hash < 8 ? $x : $y; - $v = $hash < 4 ? $y : (($hash === 12 or $hash === 14) ? $x : $z); + /** @var float */ + protected $persistence; + /** @var float */ + protected $expansion; + /** @var int */ + protected $octaves; - return (($hash & 1) === 0 ? $u : -$u) + (($hash & 2) === 0 ? $v : -$v); + public function __construct(int $octaves, float $persistence, float $expansion){ + $this->octaves = $octaves; + $this->persistence = $persistence; + $this->expansion = $expansion; } /** @@ -338,30 +294,46 @@ abstract class Noise{ } } + /** + * The following code originally called trilinearLerp() in a loop, but it was later inlined to elide function + * call overhead. + * Later, it became apparent that some of the logic was being repeated unnecessarily in the inner loop, so the + * code was changed further to avoid this, which produced visible performance improvements. + * + * In any language with a compiler, a compiler would most likely have noticed that these optimisations could be + * made and made these changes automatically, but in PHP we don't have a compiler, so the task falls to us. + * + * @see Noise::trilinearLerp() + */ for($xx = 0; $xx < $xSize; ++$xx){ + $nx = (int) ($xx / $xSamplingRate) * $xSamplingRate; + $nnx = $nx + $xSamplingRate; + + $dx1 = (($nnx - $xx) / ($nnx - $nx)); + $dx2 = (($xx - $nx) / ($nnx - $nx)); + for($zz = 0; $zz < $zSize; ++$zz){ + $nz = (int) ($zz / $zSamplingRate) * $zSamplingRate; + $nnz = $nz + $zSamplingRate; + + $dz1 = ($nnz - $zz) / ($nnz - $nz); + $dz2 = ($zz - $nz) / ($nnz - $nz); + for($yy = 0; $yy < $ySize; ++$yy){ if($xx % $xSamplingRate !== 0 or $zz % $zSamplingRate !== 0 or $yy % $ySamplingRate !== 0){ - $nx = (int) ($xx / $xSamplingRate) * $xSamplingRate; $ny = (int) ($yy / $ySamplingRate) * $ySamplingRate; - $nz = (int) ($zz / $zSamplingRate) * $zSamplingRate; - - $nnx = $nx + $xSamplingRate; $nny = $ny + $ySamplingRate; - $nnz = $nz + $zSamplingRate; - $dx1 = (($nnx - $xx) / ($nnx - $nx)); - $dx2 = (($xx - $nx) / ($nnx - $nx)); $dy1 = (($nny - $yy) / ($nny - $ny)); $dy2 = (($yy - $ny) / ($nny - $ny)); - $noiseArray[$xx][$zz][$yy] = (($nnz - $zz) / ($nnz - $nz)) * ( + $noiseArray[$xx][$zz][$yy] = $dz1 * ( $dy1 * ( $dx1 * $noiseArray[$nx][$nz][$ny] + $dx2 * $noiseArray[$nnx][$nz][$ny] ) + $dy2 * ( $dx1 * $noiseArray[$nx][$nz][$nny] + $dx2 * $noiseArray[$nnx][$nz][$nny] ) - ) + (($zz - $nz) / ($nnz - $nz)) * ( + ) + $dz2 * ( $dy1 * ( $dx1 * $noiseArray[$nx][$nnz][$ny] + $dx2 * $noiseArray[$nnx][$nnz][$ny] ) + $dy2 * ( @@ -375,17 +347,4 @@ abstract class Noise{ return $noiseArray; } - - /** - * @param float $x - * @param float $y - * @param float $z - * - * @return void - */ - public function setOffset($x, $y, $z){ - $this->offsetX = $x; - $this->offsetY = $y; - $this->offsetZ = $z; - } } diff --git a/src/world/generator/noise/Simplex.php b/src/world/generator/noise/Simplex.php new file mode 100644 index 0000000000..71ef32cffc --- /dev/null +++ b/src/world/generator/noise/Simplex.php @@ -0,0 +1,282 @@ +offsetX = $random->nextFloat() * 256; + $this->offsetY = $random->nextFloat() * 256; + $this->offsetZ = $random->nextFloat() * 256; + + for($i = 0; $i < 512; ++$i){ + $this->perm[$i] = 0; + } + + for($i = 0; $i < 256; ++$i){ + $this->perm[$i] = $random->nextBoundedInt(256); + } + + for($i = 0; $i < 256; ++$i){ + $pos = $random->nextBoundedInt(256 - $i) + $i; + $old = $this->perm[$i]; + + $this->perm[$i] = $this->perm[$pos]; + $this->perm[$pos] = $old; + $this->perm[$i + 256] = $this->perm[$i]; + } + + //this dummy call is necessary to produce the same RNG state as before latest refactors to this file + //previously this value would be used for offsetW + //TODO: this really needs to reset the RNG seed to avoid future RNG contamination + $random->nextSignedInt(); + } + + public function getNoise3D($x, $y, $z){ + $x += $this->offsetX; + $y += $this->offsetY; + $z += $this->offsetZ; + + // Skew the input space to determine which simplex cell we're in + $s = ($x + $y + $z) * self::F3; // Very nice and simple skew factor for 3D + $i = (int) ($x + $s); + $j = (int) ($y + $s); + $k = (int) ($z + $s); + $t = ($i + $j + $k) * self::G3; + // Unskew the cell origin back to (x,y,z) space + $x0 = $x - ($i - $t); // The x,y,z distances from the cell origin + $y0 = $y - ($j - $t); + $z0 = $z - ($k - $t); + + // For the 3D case, the simplex shape is a slightly irregular tetrahedron. + + // Determine which simplex we are in. + if($x0 >= $y0){ + if($y0 >= $z0){ + $i1 = 1; + $j1 = 0; + $k1 = 0; + $i2 = 1; + $j2 = 1; + $k2 = 0; + } // X Y Z order + elseif($x0 >= $z0){ + $i1 = 1; + $j1 = 0; + $k1 = 0; + $i2 = 1; + $j2 = 0; + $k2 = 1; + } // X Z Y order + else{ + $i1 = 0; + $j1 = 0; + $k1 = 1; + $i2 = 1; + $j2 = 0; + $k2 = 1; + } + // Z X Y order + }else{ // x0 0){ + $gi0 = self::grad3[$this->perm[$ii + $this->perm[$jj + $this->perm[$kk]]] % 12]; + $n += $t0 * $t0 * $t0 * $t0 * ($gi0[0] * $x0 + $gi0[1] * $y0 + $gi0[2] * $z0); + } + + $t1 = 0.6 - $x1 * $x1 - $y1 * $y1 - $z1 * $z1; + if($t1 > 0){ + $gi1 = self::grad3[$this->perm[$ii + $i1 + $this->perm[$jj + $j1 + $this->perm[$kk + $k1]]] % 12]; + $n += $t1 * $t1 * $t1 * $t1 * ($gi1[0] * $x1 + $gi1[1] * $y1 + $gi1[2] * $z1); + } + + $t2 = 0.6 - $x2 * $x2 - $y2 * $y2 - $z2 * $z2; + if($t2 > 0){ + $gi2 = self::grad3[$this->perm[$ii + $i2 + $this->perm[$jj + $j2 + $this->perm[$kk + $k2]]] % 12]; + $n += $t2 * $t2 * $t2 * $t2 * ($gi2[0] * $x2 + $gi2[1] * $y2 + $gi2[2] * $z2); + } + + $t3 = 0.6 - $x3 * $x3 - $y3 * $y3 - $z3 * $z3; + if($t3 > 0){ + $gi3 = self::grad3[$this->perm[$ii + 1 + $this->perm[$jj + 1 + $this->perm[$kk + 1]]] % 12]; + $n += $t3 * $t3 * $t3 * $t3 * ($gi3[0] * $x3 + $gi3[1] * $y3 + $gi3[2] * $z3); + } + + // Add contributions from each corner to get the noise value. + // The result is scaled to stay just inside [-1,1] + return 32.0 * $n; + } + + /** + * @param float $x + * @param float $y + * + * @return float + */ + public function getNoise2D($x, $y){ + $x += $this->offsetX; + $y += $this->offsetY; + + // Skew the input space to determine which simplex cell we're in + $s = ($x + $y) * self::F2; // Hairy factor for 2D + $i = (int) ($x + $s); + $j = (int) ($y + $s); + $t = ($i + $j) * self::G2; + // Unskew the cell origin back to (x,y) space + $x0 = $x - ($i - $t); // The x,y distances from the cell origin + $y0 = $y - ($j - $t); + + // For the 2D case, the simplex shape is an equilateral triangle. + + // Determine which simplex we are in. + if($x0 > $y0){ + $i1 = 1; + $j1 = 0; + } // lower triangle, XY order: (0,0)->(1,0)->(1,1) + else{ + $i1 = 0; + $j1 = 1; + } + // upper triangle, YX order: (0,0)->(0,1)->(1,1) + + // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + // c = (3-sqrt(3))/6 + + $x1 = $x0 - $i1 + self::G2; // Offsets for middle corner in (x,y) unskewed coords + $y1 = $y0 - $j1 + self::G2; + $x2 = $x0 + self::G22; // Offsets for last corner in (x,y) unskewed coords + $y2 = $y0 + self::G22; + + // Work out the hashed gradient indices of the three simplex corners + $ii = $i & 255; + $jj = $j & 255; + + $n = 0; + + // Calculate the contribution from the three corners + $t0 = 0.5 - $x0 * $x0 - $y0 * $y0; + if($t0 > 0){ + $gi0 = self::grad3[$this->perm[$ii + $this->perm[$jj]] % 12]; + $n += $t0 * $t0 * $t0 * $t0 * ($gi0[0] * $x0 + $gi0[1] * $y0); // (x,y) of grad3 used for 2D gradient + } + + $t1 = 0.5 - $x1 * $x1 - $y1 * $y1; + if($t1 > 0){ + $gi1 = self::grad3[$this->perm[$ii + $i1 + $this->perm[$jj + $j1]] % 12]; + $n += $t1 * $t1 * $t1 * $t1 * ($gi1[0] * $x1 + $gi1[1] * $y1); + } + + $t2 = 0.5 - $x2 * $x2 - $y2 * $y2; + if($t2 > 0){ + $gi2 = self::grad3[$this->perm[$ii + 1 + $this->perm[$jj + 1]] % 12]; + $n += $t2 * $t2 * $t2 * $t2 * ($gi2[0] * $x2 + $gi2[1] * $y2); + } + + // Add contributions from each corner to get the noise value. + // The result is scaled to return values in the interval [-1,1]. + return 70.0 * $n; + } +} diff --git a/src/world/generator/normal/Normal.php b/src/world/generator/normal/Normal.php new file mode 100644 index 0000000000..f8a006663c --- /dev/null +++ b/src/world/generator/normal/Normal.php @@ -0,0 +1,230 @@ +gaussian = new Gaussian(2); + + $this->noiseBase = new Simplex($this->random, 4, 1 / 4, 1 / 32); + $this->random->setSeed($this->seed); + + $this->selector = new class($this->random) extends BiomeSelector{ + protected function lookup(float $temperature, float $rainfall) : int{ + if($rainfall < 0.25){ + if($temperature < 0.7){ + return BiomeIds::OCEAN; + }elseif($temperature < 0.85){ + return BiomeIds::RIVER; + }else{ + return BiomeIds::SWAMPLAND; + } + }elseif($rainfall < 0.60){ + if($temperature < 0.25){ + return BiomeIds::ICE_PLAINS; + }elseif($temperature < 0.75){ + return BiomeIds::PLAINS; + }else{ + return BiomeIds::DESERT; + } + }elseif($rainfall < 0.80){ + if($temperature < 0.25){ + return BiomeIds::TAIGA; + }elseif($temperature < 0.75){ + return BiomeIds::FOREST; + }else{ + return BiomeIds::BIRCH_FOREST; + } + }else{ + if($temperature < 0.20){ + return BiomeIds::EXTREME_HILLS; + }elseif($temperature < 0.40){ + return BiomeIds::EXTREME_HILLS_EDGE; + }else{ + return BiomeIds::RIVER; + } + } + } + }; + + $this->selector->recalculate(); + + $cover = new GroundCover(); + $this->generationPopulators[] = $cover; + + $ores = new Ore(); + $stone = VanillaBlocks::STONE(); + $ores->setOreTypes([ + new OreType(VanillaBlocks::COAL_ORE(), $stone, 20, 16, 0, 128), + new OreType(VanillaBlocks::IRON_ORE(), $stone, 20, 8, 0, 64), + new OreType(VanillaBlocks::REDSTONE_ORE(), $stone, 8, 7, 0, 16), + new OreType(VanillaBlocks::LAPIS_LAZULI_ORE(), $stone, 1, 6, 0, 32), + new OreType(VanillaBlocks::GOLD_ORE(), $stone, 2, 8, 0, 32), + new OreType(VanillaBlocks::DIAMOND_ORE(), $stone, 1, 7, 0, 16), + new OreType(VanillaBlocks::DIRT(), $stone, 20, 32, 0, 128), + new OreType(VanillaBlocks::GRAVEL(), $stone, 10, 16, 0, 128) + ]); + $this->populators[] = $ores; + } + + private function pickBiome(int $x, int $z) : Biome{ + $hash = $x * 2345803 ^ $z * 9236449 ^ $this->seed; + $hash *= $hash + 223; + $xNoise = $hash >> 20 & 3; + $zNoise = $hash >> 22 & 3; + if($xNoise == 3){ + $xNoise = 1; + } + if($zNoise == 3){ + $zNoise = 1; + } + + return $this->selector->pickBiome($x + $xNoise - 1, $z + $zNoise - 1); + } + + public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed); + + $noise = $this->noiseBase->getFastNoise3D(Chunk::EDGE_LENGTH, 128, Chunk::EDGE_LENGTH, 4, 8, 4, $chunkX * Chunk::EDGE_LENGTH, 0, $chunkZ * Chunk::EDGE_LENGTH); + + $chunk = $world->getChunk($chunkX, $chunkZ); + + $biomeCache = []; + + $bedrock = VanillaBlocks::BEDROCK()->getFullId(); + $stillWater = VanillaBlocks::WATER()->getFullId(); + $stone = VanillaBlocks::STONE()->getFullId(); + + $baseX = $chunkX * Chunk::EDGE_LENGTH; + $baseZ = $chunkZ * Chunk::EDGE_LENGTH; + for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){ + $absoluteX = $baseX + $x; + for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){ + $absoluteZ = $baseZ + $z; + $minSum = 0; + $maxSum = 0; + $weightSum = 0; + + $biome = $this->pickBiome($absoluteX, $absoluteZ); + $chunk->setBiomeId($x, $z, $biome->getId()); + + for($sx = -$this->gaussian->smoothSize; $sx <= $this->gaussian->smoothSize; ++$sx){ + for($sz = -$this->gaussian->smoothSize; $sz <= $this->gaussian->smoothSize; ++$sz){ + + $weight = $this->gaussian->kernel[$sx + $this->gaussian->smoothSize][$sz + $this->gaussian->smoothSize]; + + if($sx === 0 and $sz === 0){ + $adjacent = $biome; + }else{ + $index = World::chunkHash($absoluteX + $sx, $absoluteZ + $sz); + if(isset($biomeCache[$index])){ + $adjacent = $biomeCache[$index]; + }else{ + $biomeCache[$index] = $adjacent = $this->pickBiome($absoluteX + $sx, $absoluteZ + $sz); + } + } + + $minSum += ($adjacent->getMinElevation() - 1) * $weight; + $maxSum += $adjacent->getMaxElevation() * $weight; + + $weightSum += $weight; + } + } + + $minSum /= $weightSum; + $maxSum /= $weightSum; + + $smoothHeight = ($maxSum - $minSum) / 2; + + for($y = 0; $y < 128; ++$y){ + if($y === 0){ + $chunk->setFullBlock($x, $y, $z, $bedrock); + continue; + } + $noiseValue = $noise[$x][$z][$y] - 1 / $smoothHeight * ($y - $smoothHeight - $minSum); + + if($noiseValue > 0){ + $chunk->setFullBlock($x, $y, $z, $stone); + }elseif($y <= $this->waterHeight){ + $chunk->setFullBlock($x, $y, $z, $stillWater); + } + } + } + } + + foreach($this->generationPopulators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $this->random); + } + } + + public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{ + $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed); + foreach($this->populators as $populator){ + $populator->populate($world, $chunkX, $chunkZ, $this->random); + } + + $chunk = $world->getChunk($chunkX, $chunkZ); + $biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7)); + $biome->populateChunk($world, $chunkX, $chunkZ, $this->random); + } +} diff --git a/src/pocketmine/level/generator/object/BirchTree.php b/src/world/generator/object/BirchTree.php similarity index 71% rename from src/pocketmine/level/generator/object/BirchTree.php rename to src/world/generator/object/BirchTree.php index 453973c2de..5bd0b98ffe 100644 --- a/src/pocketmine/level/generator/object/BirchTree.php +++ b/src/world/generator/object/BirchTree.php @@ -21,33 +21,27 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; -use pocketmine\block\Block; -use pocketmine\block\Wood; -use pocketmine\level\ChunkManager; +use pocketmine\block\VanillaBlocks; use pocketmine\utils\Random; +use pocketmine\world\BlockTransaction; +use pocketmine\world\ChunkManager; class BirchTree extends Tree{ - /** @var bool */ protected $superBirch = false; public function __construct(bool $superBirch = false){ - $this->trunkBlock = Block::LOG; - $this->leafBlock = Block::LEAVES; - $this->type = Wood::BIRCH; + parent::__construct(VanillaBlocks::BIRCH_LOG(), VanillaBlocks::BIRCH_LEAVES()); $this->superBirch = $superBirch; } - /** - * @return void - */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z, Random $random){ + public function getBlockTransaction(ChunkManager $world, int $x, int $y, int $z, Random $random) : ?BlockTransaction{ $this->treeHeight = $random->nextBoundedInt(3) + 5; if($this->superBirch){ $this->treeHeight += 5; } - parent::placeObject($level, $x, $y, $z, $random); + return parent::getBlockTransaction($world, $x, $y, $z, $random); } } diff --git a/src/pocketmine/level/generator/object/JungleTree.php b/src/world/generator/object/JungleTree.php similarity index 78% rename from src/pocketmine/level/generator/object/JungleTree.php rename to src/world/generator/object/JungleTree.php index a237e5303a..de687b9200 100644 --- a/src/pocketmine/level/generator/object/JungleTree.php +++ b/src/world/generator/object/JungleTree.php @@ -21,17 +21,13 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; -use pocketmine\block\Block; -use pocketmine\block\Wood; +use pocketmine\block\VanillaBlocks; class JungleTree extends Tree{ public function __construct(){ - $this->trunkBlock = Block::LOG; - $this->leafBlock = Block::LEAVES; - $this->type = Wood::JUNGLE; - $this->treeHeight = 8; + parent::__construct(VanillaBlocks::JUNGLE_LOG(), VanillaBlocks::JUNGLE_LEAVES(), 8); } } diff --git a/src/pocketmine/level/generator/object/OakTree.php b/src/world/generator/object/OakTree.php similarity index 67% rename from src/pocketmine/level/generator/object/OakTree.php rename to src/world/generator/object/OakTree.php index b11757ba5f..d273d2ae5b 100644 --- a/src/pocketmine/level/generator/object/OakTree.php +++ b/src/world/generator/object/OakTree.php @@ -21,26 +21,21 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; -use pocketmine\block\Block; -use pocketmine\block\Wood; -use pocketmine\level\ChunkManager; +use pocketmine\block\VanillaBlocks; use pocketmine\utils\Random; +use pocketmine\world\BlockTransaction; +use pocketmine\world\ChunkManager; class OakTree extends Tree{ public function __construct(){ - $this->trunkBlock = Block::LOG; - $this->leafBlock = Block::LEAVES; - $this->type = Wood::OAK; + parent::__construct(VanillaBlocks::OAK_LOG(), VanillaBlocks::OAK_LEAVES()); } - /** - * @return void - */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z, Random $random){ + public function getBlockTransaction(ChunkManager $world, int $x, int $y, int $z, Random $random) : ?BlockTransaction{ $this->treeHeight = $random->nextBoundedInt(3) + 4; - parent::placeObject($level, $x, $y, $z, $random); + return parent::getBlockTransaction($world, $x, $y, $z, $random); } } diff --git a/src/pocketmine/level/generator/object/Ore.php b/src/world/generator/object/Ore.php similarity index 79% rename from src/pocketmine/level/generator/object/Ore.php rename to src/world/generator/object/Ore.php index 92d9683951..aad9cc0349 100644 --- a/src/pocketmine/level/generator/object/Ore.php +++ b/src/world/generator/object/Ore.php @@ -21,12 +21,11 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; -use pocketmine\block\Block; -use pocketmine\level\ChunkManager; use pocketmine\math\VectorMath; use pocketmine\utils\Random; +use pocketmine\world\ChunkManager; use function sin; use const M_PI; @@ -45,14 +44,11 @@ class Ore{ return $this->type; } - public function canPlaceObject(ChunkManager $level, int $x, int $y, int $z) : bool{ - return $level->getBlockIdAt($x, $y, $z) === Block::STONE; + public function canPlaceObject(ChunkManager $world, int $x, int $y, int $z) : bool{ + return $world->getBlockAt($x, $y, $z)->isSameType($this->type->replaces); } - /** - * @return void - */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z){ + public function placeObject(ChunkManager $world, int $x, int $y, int $z) : void{ $clusterSize = $this->type->clusterSize; $angle = $this->random->nextFloat() * M_PI; $offset = VectorMath::getDirection2D($angle)->multiply($clusterSize / 8); @@ -89,11 +85,8 @@ class Ore{ $sizeZ = ($zz + 0.5 - $seedZ) / $size; $sizeZ *= $sizeZ; - if(($sizeX + $sizeY + $sizeZ) < 1 and $level->getBlockIdAt($xx, $yy, $zz) === Block::STONE){ - $level->setBlockIdAt($xx, $yy, $zz, $this->type->material->getId()); - if($this->type->material->getDamage() !== 0){ - $level->setBlockDataAt($xx, $yy, $zz, $this->type->material->getDamage()); - } + if(($sizeX + $sizeY + $sizeZ) < 1 and $world->getBlockAt($xx, $yy, $zz)->isSameType($this->type->replaces)){ + $world->setBlockAt($xx, $yy, $zz, $this->type->material); } } } diff --git a/src/pocketmine/level/particle/TerrainParticle.php b/src/world/generator/object/OreType.php similarity index 75% rename from src/pocketmine/level/particle/TerrainParticle.php rename to src/world/generator/object/OreType.php index 8295f3d1d4..96c46b8d7b 100644 --- a/src/pocketmine/level/particle/TerrainParticle.php +++ b/src/world/generator/object/OreType.php @@ -21,13 +21,17 @@ declare(strict_types=1); -namespace pocketmine\level\particle; +namespace pocketmine\world\generator\object; use pocketmine\block\Block; -use pocketmine\math\Vector3; -class TerrainParticle extends GenericParticle{ - public function __construct(Vector3 $pos, Block $b){ - parent::__construct($pos, Particle::TYPE_TERRAIN, $b->getRuntimeId()); - } +class OreType{ + public function __construct( + public Block $material, + public Block $replaces, + public int $clusterCount, + public int $clusterSize, + public int $minHeight, + public int $maxHeight + ){} } diff --git a/src/pocketmine/level/generator/object/SpruceTree.php b/src/world/generator/object/SpruceTree.php similarity index 66% rename from src/pocketmine/level/generator/object/SpruceTree.php rename to src/world/generator/object/SpruceTree.php index 5dddbd31c6..3439b03d4d 100644 --- a/src/pocketmine/level/generator/object/SpruceTree.php +++ b/src/world/generator/object/SpruceTree.php @@ -21,35 +21,32 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; -use pocketmine\block\Block; -use pocketmine\block\BlockFactory; -use pocketmine\block\Wood; -use pocketmine\level\ChunkManager; +use pocketmine\block\VanillaBlocks; use pocketmine\utils\Random; +use pocketmine\world\BlockTransaction; +use pocketmine\world\ChunkManager; use function abs; class SpruceTree extends Tree{ public function __construct(){ - $this->trunkBlock = Block::LOG; - $this->leafBlock = Block::LEAVES; - $this->type = Wood::SPRUCE; - $this->treeHeight = 10; + parent::__construct(VanillaBlocks::SPRUCE_LOG(), VanillaBlocks::SPRUCE_LEAVES(), 10); } - /** - * @return void - */ - public function placeObject(ChunkManager $level, int $x, int $y, int $z, Random $random){ - $this->treeHeight = $random->nextBoundedInt(4) + 6; + protected function generateTrunkHeight(Random $random) : int{ + return $this->treeHeight - $random->nextBoundedInt(3); + } + public function getBlockTransaction(ChunkManager $world, int $x, int $y, int $z, Random $random) : ?BlockTransaction{ + $this->treeHeight = $random->nextBoundedInt(4) + 6; + return parent::getBlockTransaction($world, $x, $y, $z, $random); + } + + protected function placeCanopy(int $x, int $y, int $z, Random $random, BlockTransaction $transaction) : void{ $topSize = $this->treeHeight - (1 + $random->nextBoundedInt(2)); $lRadius = 2 + $random->nextBoundedInt(2); - - $this->placeTrunk($level, $x, $y, $z, $random, $this->treeHeight - $random->nextBoundedInt(3)); - $radius = $random->nextBoundedInt(2); $maxR = 1; $minR = 0; @@ -65,9 +62,8 @@ class SpruceTree extends Tree{ continue; } - if(!BlockFactory::$solid[$level->getBlockIdAt($xx, $yyy, $zz)]){ - $level->setBlockIdAt($xx, $yyy, $zz, $this->leafBlock); - $level->setBlockDataAt($xx, $yyy, $zz, $this->type); + if(!$transaction->fetchBlockAt($xx, $yyy, $zz)->isSolid()){ + $transaction->addBlockAt($xx, $yyy, $zz, $this->leafBlock); } } } diff --git a/src/pocketmine/level/generator/object/TallGrass.php b/src/world/generator/object/TallGrass.php similarity index 61% rename from src/pocketmine/level/generator/object/TallGrass.php rename to src/world/generator/object/TallGrass.php index 7457ed0fda..80e75e5a1d 100644 --- a/src/pocketmine/level/generator/object/TallGrass.php +++ b/src/world/generator/object/TallGrass.php @@ -21,36 +21,34 @@ declare(strict_types=1); -namespace pocketmine\level\generator\object; +namespace pocketmine\world\generator\object; use pocketmine\block\Block; -use pocketmine\level\ChunkManager; +use pocketmine\block\BlockLegacyIds; +use pocketmine\block\VanillaBlocks; use pocketmine\math\Vector3; use pocketmine\utils\Random; +use pocketmine\world\ChunkManager; use function count; class TallGrass{ - /** - * @return void - */ - public static function growGrass(ChunkManager $level, Vector3 $pos, Random $random, int $count = 15, int $radius = 10){ + public static function growGrass(ChunkManager $world, Vector3 $pos, Random $random, int $count = 15, int $radius = 10) : void{ + /** @var Block[] $arr */ $arr = [ - [Block::DANDELION, 0], - [Block::POPPY, 0], - [Block::TALL_GRASS, 1], - [Block::TALL_GRASS, 1], - [Block::TALL_GRASS, 1], - [Block::TALL_GRASS, 1] + VanillaBlocks::DANDELION(), + VanillaBlocks::POPPY(), + $tallGrass = VanillaBlocks::TALL_GRASS(), + $tallGrass, + $tallGrass, + $tallGrass ]; $arrC = count($arr) - 1; 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($level->getBlockIdAt($x, $pos->y + 1, $z) === Block::AIR and $level->getBlockIdAt($x, $pos->y, $z) === Block::GRASS){ - $t = $arr[$random->nextRange(0, $arrC)]; - $level->setBlockIdAt($x, $pos->y + 1, $z, $t[0]); - $level->setBlockDataAt($x, $pos->y + 1, $z, $t[1]); + if($world->getBlockAt($x, $pos->y + 1, $z)->getId() === BlockLegacyIds::AIR and $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 new file mode 100644 index 0000000000..90eef6c3ab --- /dev/null +++ b/src/world/generator/object/Tree.php @@ -0,0 +1,122 @@ +trunkBlock = $trunkBlock; + $this->leafBlock = $leafBlock; + + $this->treeHeight = $treeHeight; + } + + 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){ + ++$radiusToCheck; + } + for($xx = -$radiusToCheck; $xx < ($radiusToCheck + 1); ++$xx){ + for($zz = -$radiusToCheck; $zz < ($radiusToCheck + 1); ++$zz){ + if(!$this->canOverride($world->getBlockAt($x + $xx, $y + $yy, $z + $zz))){ + return false; + } + } + } + } + + return true; + } + + /** + * Returns the BlockTransaction containing all the blocks the tree would change upon growing at the given coordinates + * or null if the tree can't be grown + */ + public function getBlockTransaction(ChunkManager $world, int $x, int $y, int $z, Random $random) : ?BlockTransaction{ + if(!$this->canPlaceObject($world, $x, $y, $z, $random)){ + return null; + } + + $transaction = new BlockTransaction($world); + $this->placeTrunk($x, $y, $z, $random, $this->generateTrunkHeight($random), $transaction); + $this->placeCanopy($x, $y, $z, $random, $transaction); + + return $transaction; + } + + protected function generateTrunkHeight(Random $random) : int{ + return $this->treeHeight - 1; + } + + protected function placeTrunk(int $x, int $y, int $z, Random $random, int $trunkHeight, BlockTransaction $transaction) : void{ + // The base dirt block + $transaction->addBlockAt($x, $y - 1, $z, VanillaBlocks::DIRT()); + + for($yy = 0; $yy < $trunkHeight; ++$yy){ + if($this->canOverride($transaction->fetchBlockAt($x, $y + $yy, $z))){ + $transaction->addBlockAt($x, $y + $yy, $z, $this->trunkBlock); + } + } + } + + protected function placeCanopy(int $x, int $y, int $z, Random $random, BlockTransaction $transaction) : void{ + for($yy = $y - 3 + $this->treeHeight; $yy <= $y + $this->treeHeight; ++$yy){ + $yOff = $yy - ($y + $this->treeHeight); + $mid = (int) (1 - $yOff / 2); + for($xx = $x - $mid; $xx <= $x + $mid; ++$xx){ + $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)){ + continue; + } + if(!$transaction->fetchBlockAt($xx, $yy, $zz)->isSolid()){ + $transaction->addBlockAt($xx, $yy, $zz, $this->leafBlock); + } + } + } + } + } + + protected function canOverride(Block $block) : bool{ + return $block->canBeReplaced() or $block instanceof Sapling or $block instanceof Leaves; + } +} diff --git a/src/world/generator/object/TreeFactory.php b/src/world/generator/object/TreeFactory.php new file mode 100644 index 0000000000..6921817386 --- /dev/null +++ b/src/world/generator/object/TreeFactory.php @@ -0,0 +1,56 @@ +equals(TreeType::SPRUCE())){ + return new SpruceTree(); + }elseif($type->equals(TreeType::BIRCH())){ + if($random->nextBoundedInt(39) === 0){ + return new BirchTree(true); + }else{ + return new BirchTree(); + } + }elseif($type->equals(TreeType::JUNGLE())){ + return new JungleTree(); + }elseif($type->equals(TreeType::OAK())){ //default + return new OakTree(); + /*if($random->nextRange(0, 9) === 0){ + $tree = new BigTree(); + }else{*/ + + //} + } + return null; + } +} diff --git a/src/pocketmine/level/generator/populator/GroundCover.php b/src/world/generator/populator/GroundCover.php similarity index 57% rename from src/pocketmine/level/generator/populator/GroundCover.php rename to src/world/generator/populator/GroundCover.php index 7858bd2dc5..da5a336a3e 100644 --- a/src/pocketmine/level/generator/populator/GroundCover.php +++ b/src/world/generator/populator/GroundCover.php @@ -21,24 +21,27 @@ declare(strict_types=1); -namespace pocketmine\level\generator\populator; +namespace pocketmine\world\generator\populator; use pocketmine\block\BlockFactory; +use pocketmine\block\BlockLegacyIds; use pocketmine\block\Liquid; -use pocketmine\level\biome\Biome; -use pocketmine\level\ChunkManager; use pocketmine\utils\Random; +use pocketmine\world\biome\BiomeRegistry; +use pocketmine\world\ChunkManager; +use pocketmine\world\format\Chunk; use function count; use function min; -use function ord; -class GroundCover extends Populator{ +class GroundCover implements Populator{ - public function populate(ChunkManager $level, int $chunkX, int $chunkZ, Random $random){ - $chunk = $level->getChunk($chunkX, $chunkZ); - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - $biome = Biome::getBiome($chunk->getBiomeId($x, $z)); + public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void{ + $chunk = $world->getChunk($chunkX, $chunkZ); + $factory = BlockFactory::getInstance(); + $biomeRegistry = BiomeRegistry::getInstance(); + for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){ + for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){ + $biome = $biomeRegistry->getBiome($chunk->getBiomeId($x, $z)); $cover = $biome->getGroundCover(); if(count($cover) > 0){ $diffY = 0; @@ -46,10 +49,9 @@ class GroundCover extends Populator{ $diffY = 1; } - $column = $chunk->getBlockIdColumn($x, $z); $startY = 127; for(; $startY > 0; --$startY){ - if($column[$startY] !== "\x00" and !BlockFactory::get(ord($column[$startY]))->isTransparent()){ + if(!$factory->fromFullBlock($chunk->getFullBlock($x, $startY, $z))->isTransparent()){ break; } } @@ -57,17 +59,15 @@ class GroundCover extends Populator{ $endY = $startY - count($cover); for($y = $startY; $y > $endY and $y >= 0; --$y){ $b = $cover[$startY - $y]; - if($column[$y] === "\x00" and $b->isSolid()){ + $id = $factory->fromFullBlock($chunk->getFullBlock($x, $y, $z)); + if($id->getId() === BlockLegacyIds::AIR and $b->isSolid()){ break; } - if($b->canBeFlowedInto() and BlockFactory::get(ord($column[$y])) instanceof Liquid){ + if($b->canBeFlowedInto() and $id instanceof Liquid){ continue; } - if($b->getDamage() === 0){ - $chunk->setBlockId($x, $y, $z, $b->getId()); - }else{ - $chunk->setBlock($x, $y, $z, $b->getId(), $b->getDamage()); - } + + $chunk->setFullBlock($x, $y, $z, $b->getFullId()); } } } diff --git a/src/pocketmine/level/generator/populator/Ore.php b/src/world/generator/populator/Ore.php similarity index 59% rename from src/pocketmine/level/generator/populator/Ore.php rename to src/world/generator/populator/Ore.php index a2d962c17b..7a0d6aff9c 100644 --- a/src/pocketmine/level/generator/populator/Ore.php +++ b/src/world/generator/populator/Ore.php @@ -21,26 +21,27 @@ declare(strict_types=1); -namespace pocketmine\level\generator\populator; +namespace pocketmine\world\generator\populator; -use pocketmine\level\ChunkManager; -use pocketmine\level\generator\object\Ore as ObjectOre; -use pocketmine\level\generator\object\OreType; use pocketmine\utils\Random; +use pocketmine\world\ChunkManager; +use pocketmine\world\format\Chunk; +use pocketmine\world\generator\object\Ore as ObjectOre; +use pocketmine\world\generator\object\OreType; -class Ore extends Populator{ +class Ore implements Populator{ /** @var OreType[] */ private $oreTypes = []; - public function populate(ChunkManager $level, int $chunkX, int $chunkZ, Random $random){ + public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void{ foreach($this->oreTypes as $type){ $ore = new ObjectOre($random, $type); for($i = 0; $i < $ore->type->clusterCount; ++$i){ - $x = $random->nextRange($chunkX << 4, ($chunkX << 4) + 15); + $x = $random->nextRange($chunkX << Chunk::COORD_BIT_SIZE, ($chunkX << Chunk::COORD_BIT_SIZE) + Chunk::EDGE_LENGTH - 1); $y = $random->nextRange($ore->type->minHeight, $ore->type->maxHeight); - $z = $random->nextRange($chunkZ << 4, ($chunkZ << 4) + 15); - if($ore->canPlaceObject($level, $x, $y, $z)){ - $ore->placeObject($level, $x, $y, $z); + $z = $random->nextRange($chunkZ << Chunk::COORD_BIT_SIZE, ($chunkZ << Chunk::COORD_BIT_SIZE) + Chunk::EDGE_LENGTH - 1); + if($ore->canPlaceObject($world, $x, $y, $z)){ + $ore->placeObject($world, $x, $y, $z); } } } @@ -48,10 +49,8 @@ class Ore extends Populator{ /** * @param OreType[] $types - * - * @return void */ - public function setOreTypes(array $types){ + public function setOreTypes(array $types) : void{ $this->oreTypes = $types; } } diff --git a/src/pocketmine/level/generator/populator/Populator.php b/src/world/generator/populator/Populator.php similarity index 77% rename from src/pocketmine/level/generator/populator/Populator.php rename to src/world/generator/populator/Populator.php index f97b591898..87e734ec0c 100644 --- a/src/pocketmine/level/generator/populator/Populator.php +++ b/src/world/generator/populator/Populator.php @@ -24,15 +24,13 @@ declare(strict_types=1); /** * All the Object populator classes */ -namespace pocketmine\level\generator\populator; -use pocketmine\level\ChunkManager; +namespace pocketmine\world\generator\populator; + use pocketmine\utils\Random; +use pocketmine\world\ChunkManager; -abstract class Populator{ +interface Populator{ - /** - * @return void - */ - abstract public function populate(ChunkManager $level, int $chunkX, int $chunkZ, Random $random); + public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void; } diff --git a/src/world/generator/populator/TallGrass.php b/src/world/generator/populator/TallGrass.php new file mode 100644 index 0000000000..6783e6537a --- /dev/null +++ b/src/world/generator/populator/TallGrass.php @@ -0,0 +1,76 @@ +randomAmount = $amount; + } + + public function setBaseAmount(int $amount) : void{ + $this->baseAmount = $amount; + } + + public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void{ + $amount = $random->nextRange(0, $this->randomAmount) + $this->baseAmount; + + $block = VanillaBlocks::TALL_GRASS(); + for($i = 0; $i < $amount; ++$i){ + $x = $random->nextRange($chunkX * Chunk::EDGE_LENGTH, $chunkX * Chunk::EDGE_LENGTH + (Chunk::EDGE_LENGTH - 1)); + $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)){ + $world->setBlockAt($x, $y, $z, $block); + } + } + } + + 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; + } + + 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){ + return $y + 1; + } + } + + return -1; + } +} diff --git a/src/world/generator/populator/Tree.php b/src/world/generator/populator/Tree.php new file mode 100644 index 0000000000..45eb64835c --- /dev/null +++ b/src/world/generator/populator/Tree.php @@ -0,0 +1,84 @@ +type = $type ?? TreeType::OAK(); + } + + public function setRandomAmount(int $amount) : void{ + $this->randomAmount = $amount; + } + + public function setBaseAmount(int $amount) : void{ + $this->baseAmount = $amount; + } + + public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random) : void{ + $amount = $random->nextRange(0, $this->randomAmount) + $this->baseAmount; + for($i = 0; $i < $amount; ++$i){ + $x = $random->nextRange($chunkX << Chunk::COORD_BIT_SIZE, ($chunkX << Chunk::COORD_BIT_SIZE) + Chunk::EDGE_LENGTH); + $z = $random->nextRange($chunkZ << Chunk::COORD_BIT_SIZE, ($chunkZ << Chunk::COORD_BIT_SIZE) + Chunk::EDGE_LENGTH); + $y = $this->getHighestWorkableBlock($world, $x, $z); + if($y === -1){ + continue; + } + $tree = TreeFactory::get($random, $this->type); + $transaction = $tree?->getBlockTransaction($world, $x, $y, $z, $random); + $transaction?->apply(); + } + } + + 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){ + return $y + 1; + }elseif($b !== BlockLegacyIds::AIR and $b !== BlockLegacyIds::SNOW_LAYER){ + return -1; + } + } + + return -1; + } +} diff --git a/src/world/light/BlockLightUpdate.php b/src/world/light/BlockLightUpdate.php new file mode 100644 index 0000000000..5036400d8f --- /dev/null +++ b/src/world/light/BlockLightUpdate.php @@ -0,0 +1,105 @@ + + */ + private $lightEmitters; + + /** + * @param \SplFixedArray|int[] $lightFilters + * @param \SplFixedArray|int[] $lightEmitters + * @phpstan-param \SplFixedArray $lightFilters + * @phpstan-param \SplFixedArray $lightEmitters + */ + public function __construct(SubChunkExplorer $subChunkExplorer, \SplFixedArray $lightFilters, \SplFixedArray $lightEmitters){ + parent::__construct($subChunkExplorer, $lightFilters); + $this->lightEmitters = $lightEmitters; + } + + protected function getCurrentLightArray() : LightArray{ + return $this->subChunkExplorer->currentSubChunk->getBlockLightArray(); + } + + public function recalculateNode(int $x, int $y, int $z) : void{ + if($this->subChunkExplorer->moveTo($x, $y, $z) !== SubChunkExplorerStatus::INVALID){ + $block = $this->subChunkExplorer->currentSubChunk->getFullBlock($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + $this->setAndUpdateLight($x, $y, $z, max($this->lightEmitters[$block], $this->getHighestAdjacentLight($x, $y, $z) - $this->lightFilters[$block])); + } + } + + public function recalculateChunk(int $chunkX, int $chunkZ) : int{ + if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ) === SubChunkExplorerStatus::INVALID){ + throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); + } + $chunk = $this->subChunkExplorer->currentChunk; + + $lightSources = 0; + foreach($chunk->getSubChunks() as $subChunkY => $subChunk){ + $subChunk->setBlockLightArray(LightArray::fill(0)); + + foreach($subChunk->getBlockLayers() as $layer){ + foreach($layer->getPalette() as $state){ + if($this->lightEmitters[$state] > 0){ + $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); + break 2; + } + } + } + } + + return $lightSources; + } + + private function scanForLightEmittingBlocks(SubChunk $subChunk, int $baseX, int $baseY, int $baseZ) : int{ + $lightSources = 0; + for($x = 0; $x < SubChunk::EDGE_LENGTH; ++$x){ + for($z = 0; $z < SubChunk::EDGE_LENGTH; ++$z){ + for($y = 0; $y < SubChunk::EDGE_LENGTH; ++$y){ + $light = $this->lightEmitters[$subChunk->getFullBlock($x, $y, $z)]; + if($light > 0){ + $this->setAndUpdateLight( + $baseX + $x, + $baseY + $y, + $baseZ + $z, + $light + ); + $lightSources++; + } + } + } + } + return $lightSources; + } +} diff --git a/src/world/light/LightPopulationTask.php b/src/world/light/LightPopulationTask.php new file mode 100644 index 0000000000..97d6344ede --- /dev/null +++ b/src/world/light/LightPopulationTask.php @@ -0,0 +1,102 @@ + $blockLight, array $skyLight, array $heightMap) : void $onCompletion + */ + public function __construct(Chunk $chunk, \Closure $onCompletion){ + $this->chunk = FastChunkSerializer::serializeTerrain($chunk); + $this->storeLocal(self::TLS_KEY_COMPLETION_CALLBACK, $onCompletion); + } + + public function onRun() : void{ + $chunk = FastChunkSerializer::deserializeTerrain($this->chunk); + + $manager = new SimpleChunkManager(World::Y_MIN, World::Y_MAX); + $manager->setChunk(0, 0, $chunk); + + $blockFactory = BlockFactory::getInstance(); + foreach([ + "Block" => new BlockLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->light), + "Sky" => new SkyLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight), + ] as $name => $update){ + $update->recalculateChunk(0, 0); + $update->execute(); + } + + $chunk->setLightPopulated(); + + $this->resultHeightMap = igbinary_serialize($chunk->getHeightMapArray()); + $skyLightArrays = []; + $blockLightArrays = []; + foreach($chunk->getSubChunks() as $y => $subChunk){ + $skyLightArrays[$y] = $subChunk->getBlockSkyLightArray(); + $blockLightArrays[$y] = $subChunk->getBlockLightArray(); + } + $this->resultSkyLightArrays = igbinary_serialize($skyLightArrays); + $this->resultBlockLightArrays = igbinary_serialize($blockLightArrays); + } + + public function onCompletion() : void{ + /** @var int[] $heightMapArray */ + $heightMapArray = igbinary_unserialize($this->resultHeightMap); + + /** @var LightArray[] $skyLightArrays */ + $skyLightArrays = igbinary_unserialize($this->resultSkyLightArrays); + /** @var LightArray[] $blockLightArrays */ + $blockLightArrays = igbinary_unserialize($this->resultBlockLightArrays); + + /** + * @var \Closure + * @phpstan-var \Closure(array $blockLight, array $skyLight, array $heightMap>) : void + */ + $callback = $this->fetchLocal(self::TLS_KEY_COMPLETION_CALLBACK); + $callback($blockLightArrays, $skyLightArrays, $heightMapArray); + } +} diff --git a/src/pocketmine/block/InvisibleBedrock.php b/src/world/light/LightPropagationContext.php similarity index 59% rename from src/pocketmine/block/InvisibleBedrock.php rename to src/world/light/LightPropagationContext.php index 689c90f6bd..a13e5ae200 100644 --- a/src/pocketmine/block/InvisibleBedrock.php +++ b/src/world/light/LightPropagationContext.php @@ -21,31 +21,34 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\world\light; -use pocketmine\item\Item; +final class LightPropagationContext{ -class InvisibleBedrock extends Transparent{ + /** + * @var \SplQueue + * @phpstan-var \SplQueue + */ + public $spreadQueue; + /** + * @var true[] + * @phpstan-var array + */ + public $spreadVisited = []; - protected $id = self::INVISIBLE_BEDROCK; + /** + * @var \SplQueue + * @phpstan-var \SplQueue + */ + public $removalQueue; + /** + * @var true[] + * @phpstan-var array + */ + public $removalVisited = []; public function __construct(){ - - } - - public function getName() : string{ - return "Invisible Bedrock"; - } - - public function getHardness() : float{ - return -1; - } - - public function getBlastResistance() : float{ - return 18000000; - } - - public function isBreakable(Item $item) : bool{ - return false; + $this->removalQueue = new \SplQueue(); + $this->spreadQueue = new \SplQueue(); } } diff --git a/src/world/light/LightUpdate.php b/src/world/light/LightUpdate.php new file mode 100644 index 0000000000..a8b196a520 --- /dev/null +++ b/src/world/light/LightUpdate.php @@ -0,0 +1,210 @@ + + */ + protected $lightFilters; + + /** + * @var int[][] blockhash => [x, y, z, new light level] + * @phpstan-var array + */ + protected $updateNodes = []; + + /** @var SubChunkExplorer */ + protected $subChunkExplorer; + + /** + * @param \SplFixedArray|int[] $lightFilters + * @phpstan-param \SplFixedArray $lightFilters + */ + public function __construct(SubChunkExplorer $subChunkExplorer, \SplFixedArray $lightFilters){ + $this->lightFilters = $lightFilters; + + $this->subChunkExplorer = $subChunkExplorer; + } + + abstract protected function getCurrentLightArray() : LightArray; + + abstract public function recalculateNode(int $x, int $y, int $z) : void; + + /** + * Scans for all light sources in the target chunk and adds them to the propagation queue. + * This erases preexisting light in the chunk. + */ + abstract public function recalculateChunk(int $chunkX, int $chunkZ) : int; + + protected function getEffectiveLight(int $x, int $y, int $z) : int{ + if($this->subChunkExplorer->moveTo($x, $y, $z) !== SubChunkExplorerStatus::INVALID){ + return $this->getCurrentLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + } + return 0; + } + + protected function getHighestAdjacentLight(int $x, int $y, int $z) : int{ + $adjacent = 0; + foreach(self::ADJACENTS as [$ox, $oy, $oz]){ + if(($adjacent = max($adjacent, $this->getEffectiveLight($x + $ox, $y + $oy, $z + $oz))) === 15){ + break; + } + } + return $adjacent; + } + + public function setAndUpdateLight(int $x, int $y, int $z, int $newLevel) : void{ + $this->updateNodes[World::blockHash($x, $y, $z)] = [$x, $y, $z, $newLevel]; + } + + private function prepareNodes() : LightPropagationContext{ + $context = new LightPropagationContext(); + foreach($this->updateNodes as $blockHash => [$x, $y, $z, $newLevel]){ + if($this->subChunkExplorer->moveTo($x, $y, $z) !== SubChunkExplorerStatus::INVALID){ + $lightArray = $this->getCurrentLightArray(); + $oldLevel = $lightArray->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + + if($oldLevel !== $newLevel){ + $lightArray->set($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK, $newLevel); + if($oldLevel < $newLevel){ //light increased + $context->spreadVisited[$blockHash] = true; + $context->spreadQueue->enqueue([$x, $y, $z]); + }else{ //light removed + $context->removalVisited[$blockHash] = true; + $context->removalQueue->enqueue([$x, $y, $z, $oldLevel]); + } + } + } + } + return $context; + } + + public function execute() : int{ + $context = $this->prepareNodes(); + + $touched = 0; + while(!$context->removalQueue->isEmpty()){ + $touched++; + [$x, $y, $z, $oldAdjacentLight] = $context->removalQueue->dequeue(); + + foreach(self::ADJACENTS as [$ox, $oy, $oz]){ + $cx = $x + $ox; + $cy = $y + $oy; + $cz = $z + $oz; + + 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)])){ + $context->spreadVisited[$index] = true; + $context->spreadQueue->enqueue([$cx, $cy, $cz]); + } + } + } + + while(!$context->spreadQueue->isEmpty()){ + $touched++; + [$x, $y, $z] = $context->spreadQueue->dequeue(); + + unset($context->spreadVisited[World::blockHash($x, $y, $z)]); + + $newAdjacentLight = $this->getEffectiveLight($x, $y, $z); + if($newAdjacentLight <= 0){ + continue; + } + + foreach(self::ADJACENTS as [$ox, $oy, $oz]){ + $cx = $x + $ox; + $cy = $y + $oy; + $cz = $z + $oz; + + if($this->subChunkExplorer->moveTo($cx, $cy, $cz) !== SubChunkExplorerStatus::INVALID){ + $this->computeSpreadLight($cx, $cy, $cz, $newAdjacentLight, $context); + } + } + } + + return $touched; + } + + protected function computeRemoveLight(int $x, int $y, int $z, int $oldAdjacentLevel, LightPropagationContext $context) : void{ + $lightArray = $this->getCurrentLightArray(); + $lx = $x & SubChunk::COORD_MASK; + $ly = $y & SubChunk::COORD_MASK; + $lz = $z & SubChunk::COORD_MASK; + $current = $lightArray->get($lx, $ly, $lz); + + if($current !== 0 and $current < $oldAdjacentLevel){ + $lightArray->set($lx, $ly, $lz, 0); + + if(!isset($context->removalVisited[$index = World::blockHash($x, $y, $z)])){ + $context->removalVisited[$index] = true; + if($current > 1){ + $context->removalQueue->enqueue([$x, $y, $z, $current]); + } + } + }elseif($current >= $oldAdjacentLevel){ + if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)])){ + $context->spreadVisited[$index] = true; + $context->spreadQueue->enqueue([$x, $y, $z]); + } + } + } + + protected function computeSpreadLight(int $x, int $y, int $z, int $newAdjacentLevel, LightPropagationContext $context) : void{ + $lightArray = $this->getCurrentLightArray(); + $lx = $x & SubChunk::COORD_MASK; + $ly = $y & SubChunk::COORD_MASK; + $lz = $z & SubChunk::COORD_MASK; + $current = $lightArray->get($lx, $ly, $lz); + $potentialLight = $newAdjacentLevel - $this->lightFilters[$this->subChunkExplorer->currentSubChunk->getFullBlock($lx, $ly, $lz)]; + + if($current < $potentialLight){ + $lightArray->set($lx, $ly, $lz, $potentialLight); + + if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)]) and $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 new file mode 100644 index 0000000000..a505fde025 --- /dev/null +++ b/src/world/light/SkyLightUpdate.php @@ -0,0 +1,235 @@ + + */ + private $directSkyLightBlockers; + + /** + * @param \SplFixedArray|int[] $lightFilters + * @param \SplFixedArray|bool[] $directSkyLightBlockers + * @phpstan-param \SplFixedArray $lightFilters + * @phpstan-param \SplFixedArray $directSkyLightBlockers + */ + public function __construct(SubChunkExplorer $subChunkExplorer, \SplFixedArray $lightFilters, \SplFixedArray $directSkyLightBlockers){ + parent::__construct($subChunkExplorer, $lightFilters); + $this->directSkyLightBlockers = $directSkyLightBlockers; + } + + protected function getCurrentLightArray() : LightArray{ + return $this->subChunkExplorer->currentSubChunk->getBlockSkyLightArray(); + } + + protected function getEffectiveLight(int $x, int $y, int $z) : int{ + if($y >= World::Y_MAX){ + $this->subChunkExplorer->invalidate(); + return 15; + } + return parent::getEffectiveLight($x, $y, $z); + } + + public function recalculateNode(int $x, int $y, int $z) : void{ + if($this->subChunkExplorer->moveTo($x, $y, $z) === SubChunkExplorerStatus::INVALID){ + return; + } + $chunk = $this->subChunkExplorer->currentChunk; + + $oldHeightMap = $chunk->getHeightMap($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK); + $source = $this->subChunkExplorer->currentSubChunk->getFullBlock($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK); + + $yPlusOne = $y + 1; + + if($yPlusOne === $oldHeightMap){ //Block changed directly beneath the heightmap. Check if a block was removed or changed to a different light-filter. + $newHeightMap = self::recalculateHeightMapColumn($chunk, $x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $this->directSkyLightBlockers); + $chunk->setHeightMap($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $newHeightMap); + }elseif($yPlusOne > $oldHeightMap){ //Block changed above the heightmap. + if($this->directSkyLightBlockers[$source]){ + $chunk->setHeightMap($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $yPlusOne); + $newHeightMap = $yPlusOne; + }else{ //Block changed which has no effect on direct sky light, for example placing or removing glass. + return; + } + }else{ //Block changed below heightmap + $newHeightMap = $oldHeightMap; + } + + if($newHeightMap > $oldHeightMap){ //Heightmap increase, block placed, remove sky light + for($i = $y; $i >= $oldHeightMap; --$i){ + $this->setAndUpdateLight($x, $i, $z, 0); //Remove all light beneath, adjacent recalculation will handle the rest. + } + }elseif($newHeightMap < $oldHeightMap){ //Heightmap decrease, block changed or removed, add sky light + for($i = $y; $i >= $newHeightMap; --$i){ + $this->setAndUpdateLight($x, $i, $z, 15); + } + }else{ //No heightmap change, block changed "underground" + $this->setAndUpdateLight($x, $y, $z, max(0, $this->getHighestAdjacentLight($x, $y, $z) - $this->lightFilters[$source])); + } + } + + public function recalculateChunk(int $chunkX, int $chunkZ) : int{ + if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ) === SubChunkExplorerStatus::INVALID){ + throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); + } + $chunk = $this->subChunkExplorer->currentChunk; + + $newHeightMap = self::recalculateHeightMap($chunk, $this->directSkyLightBlockers); + $chunk->setHeightMapArray($newHeightMap->getValues()); + + //setAndUpdateLight() won't bother propagating from nodes that are already what we want to change them to, so we + //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); + for($y = Chunk::MIN_SUBCHUNK_INDEX; $y < $lowestClearSubChunk && $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){ + $chunk->getSubChunk($y)->setBlockSkyLightArray(LightArray::fill(0)); + } + for($y = $lowestClearSubChunk; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){ + $chunk->getSubChunk($y)->setBlockSkyLightArray(LightArray::fill(15)); + } + + $lightSources = 0; + + $baseX = $chunkX << Chunk::COORD_BIT_SIZE; + $baseZ = $chunkZ << Chunk::COORD_BIT_SIZE; + for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){ + for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){ + $currentHeight = $chunk->getHeightMap($x, $z); + $maxAdjacentHeight = World::Y_MIN; + if($x !== 0){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x - 1, $z)); + } + if($x !== 15){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x + 1, $z)); + } + if($z !== 0){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x, $z - 1)); + } + if($z !== 15){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x, $z + 1)); + } + + /* + * We skip the top two blocks between current height and max adjacent (if there's a difference) because: + * - the block next to the highest adjacent will do nothing during propagation (it's surrounded by 15s) + * - the block below that block will do the same as the node in the highest adjacent + * NOTE: If block opacity becomes direction-aware in the future, the second point will become invalid. + */ + $nodeColumnEnd = max($currentHeight, $maxAdjacentHeight - 2); + + for($y = $currentHeight; $y <= $nodeColumnEnd; $y++){ + $this->setAndUpdateLight($x + $baseX, $y, $z + $baseZ, 15); + $lightSources++; + } + for($y = $nodeColumnEnd + 1, $yMax = $lowestClearSubChunk * SubChunk::EDGE_LENGTH; $y < $yMax; $y++){ + if($this->subChunkExplorer->moveTo($x + $baseX, $y, $z + $baseZ) !== SubChunkExplorerStatus::INVALID){ + $this->getCurrentLightArray()->set($x, $y & SubChunk::COORD_MASK, $z, 15); + } + } + } + } + + return $lightSources; + } + + /** + * Recalculates the heightmap for the whole chunk. + * + * @param \SplFixedArray|bool[] $directSkyLightBlockers + * @phpstan-param \SplFixedArray $directSkyLightBlockers + */ + private static function recalculateHeightMap(Chunk $chunk, \SplFixedArray $directSkyLightBlockers) : HeightArray{ + $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 < 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 >= Chunk::MIN_SUBCHUNK_INDEX; $subChunkY--){ + $subHighestBlockY = $chunk->getSubChunk($subChunkY)->getHighestBlockAt($x, $z); + if($subHighestBlockY !== null){ + $y = ($subChunkY * SubChunk::EDGE_LENGTH) + $subHighestBlockY; + break; + } + } + + if($y === null){ //no blocks in the column + $result->set($x, $z, World::Y_MIN); + }else{ + for(; $y >= World::Y_MIN; --$y){ + if($directSkyLightBlockers[$chunk->getFullBlock($x, $y, $z)]){ + $result->set($x, $z, $y + 1); + break; + } + } + } + } + } + return $result; + } + + /** + * Recalculates the heightmap for the block column at the specified X/Z chunk coordinates + * + * @param int $x 0-15 + * @param int $z 0-15 + * @param \SplFixedArray|bool[] $directSkyLightBlockers + * @phpstan-param \SplFixedArray $directSkyLightBlockers + * + * @return int New calculated heightmap value (0-256 inclusive) + */ + private static function recalculateHeightMapColumn(Chunk $chunk, int $x, int $z, \SplFixedArray $directSkyLightBlockers) : int{ + $y = $chunk->getHighestBlockAt($x, $z); + if($y === null){ + return World::Y_MIN; + } + for(; $y >= World::Y_MIN; --$y){ + if($directSkyLightBlockers[$chunk->getFullBlock($x, $y, $z)]){ + break; + } + } + + return $y + 1; + } +} diff --git a/src/world/particle/AngryVillagerParticle.php b/src/world/particle/AngryVillagerParticle.php new file mode 100644 index 0000000000..67e7805ab1 --- /dev/null +++ b/src/world/particle/AngryVillagerParticle.php @@ -0,0 +1,35 @@ +x, $pos->y, $pos->z); - $this->data = $b->getRuntimeId(); + public function __construct(Block $b){ + $this->block = $b; } - public function encode(){ - $pk = new LevelEventPacket; - $pk->evid = LevelEventPacket::EVENT_PARTICLE_DESTROY; - $pk->position = $this->asVector3(); - $pk->data = $this->data; - - return $pk; + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::PARTICLE_DESTROY, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()), $pos)]; } } diff --git a/src/world/particle/BlockForceFieldParticle.php b/src/world/particle/BlockForceFieldParticle.php new file mode 100644 index 0000000000..cfd2d0b2a4 --- /dev/null +++ b/src/world/particle/BlockForceFieldParticle.php @@ -0,0 +1,42 @@ +data = $data; //TODO: proper encode/decode of data + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::BLOCK_FORCE_FIELD, $this->data, $pos)]; + } +} diff --git a/src/pocketmine/level/sound/GenericSound.php b/src/world/particle/BlockPunchParticle.php similarity index 54% rename from src/pocketmine/level/sound/GenericSound.php rename to src/world/particle/BlockPunchParticle.php index 1aba871a3d..5ee0c33521 100644 --- a/src/pocketmine/level/sound/GenericSound.php +++ b/src/world/particle/BlockPunchParticle.php @@ -21,38 +21,30 @@ declare(strict_types=1); -namespace pocketmine\level\sound; +namespace pocketmine\world\particle; +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 GenericSound extends Sound{ +/** + * This particle appears when a player is attacking a block face in survival mode attempting to break it. + */ +class BlockPunchParticle implements Particle{ + /** @var Block */ + private $block; /** @var int */ - protected $id; - /** @var float */ - protected $pitch = 0; + private $face; - public function __construct(Vector3 $pos, int $id, float $pitch = 0){ - parent::__construct($pos->x, $pos->y, $pos->z); - $this->id = $id; - $this->pitch = $pitch * 1000; + public function __construct(Block $block, int $face){ + $this->block = $block; + $this->face = $face; } - public function getPitch() : float{ - return $this->pitch / 1000; - } - - public function setPitch(float $pitch) : void{ - $this->pitch = $pitch * 1000; - } - - public function encode(){ - $pk = new LevelEventPacket; - $pk->evid = $this->id; - $pk->position = $this->asVector3(); - $pk->data = (int) $this->pitch; - - return $pk; + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::PARTICLE_PUNCH_BLOCK, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()) | ($this->face << 24), $pos)]; } } diff --git a/src/world/particle/BubbleParticle.php b/src/world/particle/BubbleParticle.php new file mode 100644 index 0000000000..90336a9235 --- /dev/null +++ b/src/world/particle/BubbleParticle.php @@ -0,0 +1,35 @@ +scale = $scale; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::CRITICAL, $this->scale, $pos)]; + } +} diff --git a/src/world/particle/DragonEggTeleportParticle.php b/src/world/particle/DragonEggTeleportParticle.php new file mode 100644 index 0000000000..a2f2ae3ac2 --- /dev/null +++ b/src/world/particle/DragonEggTeleportParticle.php @@ -0,0 +1,63 @@ +xDiff = self::boundOrThrow($xDiff); + $this->yDiff = self::boundOrThrow($yDiff); + $this->zDiff = self::boundOrThrow($zDiff); + } + + private static function boundOrThrow(int $v) : int{ + if($v < -255 or $v > 255){ + throw new \InvalidArgumentException("Value must be between -255 and 255"); + } + return $v; + } + + public function encode(Vector3 $pos) : array{ + $data = ($this->zDiff < 0 ? 1 << 26 : 0) | + ($this->yDiff < 0 ? 1 << 25 : 0) | + ($this->xDiff < 0 ? 1 << 24 : 0) | + (abs($this->xDiff) << 16) | + (abs($this->yDiff) << 8) | + abs($this->zDiff); + + return [LevelEventPacket::create(LevelEvent::PARTICLE_DRAGON_EGG_TELEPORT, $data, $pos)]; + } +} diff --git a/src/world/particle/DustParticle.php b/src/world/particle/DustParticle.php new file mode 100644 index 0000000000..4e0564910a --- /dev/null +++ b/src/world/particle/DustParticle.php @@ -0,0 +1,42 @@ +color = $color; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::DUST, $this->color->toARGB(), $pos)]; + } +} diff --git a/src/world/particle/EnchantParticle.php b/src/world/particle/EnchantParticle.php new file mode 100644 index 0000000000..48f223ae92 --- /dev/null +++ b/src/world/particle/EnchantParticle.php @@ -0,0 +1,43 @@ +color = $color; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::MOB_SPELL, $this->color->toARGB(), $pos)]; + } +} diff --git a/src/world/particle/EnchantmentTableParticle.php b/src/world/particle/EnchantmentTableParticle.php new file mode 100644 index 0000000000..86a86f572c --- /dev/null +++ b/src/world/particle/EnchantmentTableParticle.php @@ -0,0 +1,35 @@ +x, $pos->y, $pos->z); + public function __construct(string $text, string $title = ""){ $this->text = $text; $this->title = $title; } @@ -79,48 +83,48 @@ class FloatingTextParticle extends Particle{ $this->invisible = $value; } - public function encode(){ + public function encode(Vector3 $pos) : array{ $p = []; if($this->entityId === null){ - $this->entityId = Entity::$entityCount++; + $this->entityId = Entity::nextRuntimeId(); }else{ - $pk0 = new RemoveActorPacket(); - $pk0->entityUniqueId = $this->entityId; - - $p[] = $pk0; + $p[] = RemoveActorPacket::create($this->entityId); } if(!$this->invisible){ - $uuid = UUID::fromRandom(); + $uuid = Uuid::uuid4(); $name = $this->title . ($this->text !== "" ? "\n" . $this->text : ""); - $add = new PlayerListPacket(); - $add->type = PlayerListPacket::TYPE_ADD; - $add->entries = [PlayerListEntry::createAdditionEntry($uuid, $this->entityId, $name, SkinAdapterSingleton::get()->toSkinData(new Skin("Standard_Custom", str_repeat("\x00", 8192))))]; - $p[] = $add; + $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 = $this->asVector3(); //TODO: check offset - $pk->item = ItemStackWrapper::legacy(ItemFactory::get(Item::AIR, 0, 0)); - - $flags = ( - 1 << Entity::DATA_FLAG_IMMOBILE + $actorFlags = ( + 1 << EntityMetadataFlags::IMMOBILE ); - $pk->metadata = [ - Entity::DATA_FLAGS => [Entity::DATA_TYPE_LONG, $flags], - Entity::DATA_SCALE => [Entity::DATA_TYPE_FLOAT, 0.01] //zero causes problems on debug builds + $actorMetadata = [ + EntityMetadataProperties::FLAGS => new LongMetadataProperty($actorFlags), + EntityMetadataProperties::SCALE => new FloatMetadataProperty(0.01) //zero causes problems on debug builds ]; + $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[] = $pk; - - $remove = new PlayerListPacket(); - $remove->type = PlayerListPacket::TYPE_REMOVE; - $remove->entries = [PlayerListEntry::createRemovalEntry($uuid)]; - $p[] = $remove; + $p[] = PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($uuid)]); } return $p; diff --git a/src/world/particle/HappyVillagerParticle.php b/src/world/particle/HappyVillagerParticle.php new file mode 100644 index 0000000000..d405a6037b --- /dev/null +++ b/src/world/particle/HappyVillagerParticle.php @@ -0,0 +1,35 @@ +scale = $scale; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::HEART, $this->scale, $pos)]; + } +} diff --git a/src/world/particle/HugeExplodeParticle.php b/src/world/particle/HugeExplodeParticle.php new file mode 100644 index 0000000000..3f1c109f93 --- /dev/null +++ b/src/world/particle/HugeExplodeParticle.php @@ -0,0 +1,35 @@ +scale = $scale; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::INK, $this->scale, $pos)]; + } +} diff --git a/src/world/particle/InstantEnchantParticle.php b/src/world/particle/InstantEnchantParticle.php new file mode 100644 index 0000000000..695a4780ae --- /dev/null +++ b/src/world/particle/InstantEnchantParticle.php @@ -0,0 +1,42 @@ +color = $color; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::MOB_SPELL_INSTANTANEOUS, $this->color->toARGB(), $pos)]; + } +} diff --git a/src/world/particle/ItemBreakParticle.php b/src/world/particle/ItemBreakParticle.php new file mode 100644 index 0000000000..1a4865a6a5 --- /dev/null +++ b/src/world/particle/ItemBreakParticle.php @@ -0,0 +1,42 @@ +item = $item; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::ITEM_BREAK, ($this->item->getId() << 16) | $this->item->getMeta(), $pos)]; + } +} diff --git a/src/world/particle/LavaDripParticle.php b/src/world/particle/LavaDripParticle.php new file mode 100644 index 0000000000..904d5257b1 --- /dev/null +++ b/src/world/particle/LavaDripParticle.php @@ -0,0 +1,35 @@ +x, $pos->y, $pos->z); + public function __construct(int $width = 0, int $height = 0){ + //TODO: bounds checks $this->width = $width; $this->height = $height; } - public function encode(){ - $pk = new LevelEventPacket; - $pk->evid = LevelEventPacket::EVENT_PARTICLE_SPAWN; - $pk->position = $this->asVector3(); - $pk->data = ($this->width & 0xff) + (($this->height & 0xff) << 8); - - return $pk; + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::PARTICLE_SPAWN, ($this->width & 0xff) | (($this->height & 0xff) << 8), $pos)]; } } diff --git a/src/pocketmine/level/particle/EnchantmentTableParticle.php b/src/world/particle/Particle.php similarity index 78% rename from src/pocketmine/level/particle/EnchantmentTableParticle.php rename to src/world/particle/Particle.php index 00cd11dc2d..009bb948dc 100644 --- a/src/pocketmine/level/particle/EnchantmentTableParticle.php +++ b/src/world/particle/Particle.php @@ -21,12 +21,16 @@ declare(strict_types=1); -namespace pocketmine\level\particle; +namespace pocketmine\world\particle; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\ClientboundPacket; + +interface Particle{ + + /** + * @return ClientboundPacket[] + */ + public function encode(Vector3 $pos) : array; -class EnchantmentTableParticle extends GenericParticle{ - public function __construct(Vector3 $pos){ - parent::__construct($pos, Particle::TYPE_ENCHANTMENT_TABLE); - } } diff --git a/src/world/particle/PortalParticle.php b/src/world/particle/PortalParticle.php new file mode 100644 index 0000000000..627ff969ae --- /dev/null +++ b/src/world/particle/PortalParticle.php @@ -0,0 +1,35 @@ +color = $color; + } + + /** + * Returns the default water-bottle splash colour. + * + * TODO: replace this with a standard surrogate object constant (first we need to implement them!) + */ + public static function DEFAULT_COLOR() : Color{ + return new Color(0x38, 0x5d, 0xc6); + } + + public function getColor() : Color{ + return $this->color; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::PARTICLE_SPLASH, $this->color->toARGB(), $pos)]; + } +} diff --git a/src/world/particle/RainSplashParticle.php b/src/world/particle/RainSplashParticle.php new file mode 100644 index 0000000000..8ba67a4ed2 --- /dev/null +++ b/src/world/particle/RainSplashParticle.php @@ -0,0 +1,35 @@ +lifetime = $lifetime; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::REDSTONE, $this->lifetime, $pos)]; + } +} diff --git a/src/world/particle/SmokeParticle.php b/src/world/particle/SmokeParticle.php new file mode 100644 index 0000000000..999f7ffd29 --- /dev/null +++ b/src/world/particle/SmokeParticle.php @@ -0,0 +1,41 @@ +scale = $scale; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::SMOKE, $this->scale, $pos)]; + } +} diff --git a/src/world/particle/SnowballPoofParticle.php b/src/world/particle/SnowballPoofParticle.php new file mode 100644 index 0000000000..1d764238ea --- /dev/null +++ b/src/world/particle/SnowballPoofParticle.php @@ -0,0 +1,35 @@ +x, $pos->y, $pos->z); - $this->id = $id & 0xFFF; - $this->data = $data; + public function __construct(Block $b){ + $this->block = $b; } - public function encode(){ - $pk = new LevelEventPacket; - $pk->evid = LevelEventPacket::EVENT_ADD_PARTICLE_MASK | $this->id; - $pk->position = $this->asVector3(); - $pk->data = $this->data; - - return $pk; + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::standardParticle(ParticleIds::TERRAIN, RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()), $pos)]; } } diff --git a/src/world/particle/WaterDripParticle.php b/src/world/particle/WaterDripParticle.php new file mode 100644 index 0000000000..a1072dc6e2 --- /dev/null +++ b/src/world/particle/WaterDripParticle.php @@ -0,0 +1,35 @@ +block = $block; + } + + 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 new file mode 100644 index 0000000000..831c879a18 --- /dev/null +++ b/src/world/sound/BlockPlaceSound.php @@ -0,0 +1,44 @@ +block = $block; + } + + 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 new file mode 100644 index 0000000000..03d3ded64c --- /dev/null +++ b/src/world/sound/BlockPunchSound.php @@ -0,0 +1,52 @@ +block = $block; + } + + public function encode(Vector3 $pos) : array{ + return [LevelSoundEventPacket::nonActorSound( + LevelSoundEvent::HIT, + $pos, + false, + RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()) + )]; + } +} diff --git a/src/pocketmine/level/particle/RedstoneParticle.php b/src/world/sound/BowShootSound.php similarity index 70% rename from src/pocketmine/level/particle/RedstoneParticle.php rename to src/world/sound/BowShootSound.php index 7fa524115c..2b595972fc 100644 --- a/src/pocketmine/level/particle/RedstoneParticle.php +++ b/src/world/sound/BowShootSound.php @@ -21,12 +21,15 @@ declare(strict_types=1); -namespace pocketmine\level\particle; +namespace pocketmine\world\sound; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelSoundEvent; -class RedstoneParticle extends GenericParticle{ - public function __construct(Vector3 $pos, int $lifetime = 1){ - parent::__construct($pos, Particle::TYPE_REDSTONE, $lifetime); +class BowShootSound implements Sound{ + + 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 new file mode 100644 index 0000000000..e3e991597f --- /dev/null +++ b/src/world/sound/BucketEmptyLavaSound.php @@ -0,0 +1,35 @@ +pitch = $pitch; + } + + public function getPitch() : float{ + return $this->pitch; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::SOUND_CLICK, (int) ($this->pitch * 1000), $pos)]; + } +} diff --git a/src/pocketmine/level/sound/DoorBumpSound.php b/src/world/sound/DoorBumpSound.php similarity index 76% rename from src/pocketmine/level/sound/DoorBumpSound.php rename to src/world/sound/DoorBumpSound.php index 0035b1ef77..86ed0f3398 100644 --- a/src/pocketmine/level/sound/DoorBumpSound.php +++ b/src/world/sound/DoorBumpSound.php @@ -21,13 +21,15 @@ declare(strict_types=1); -namespace pocketmine\level\sound; +namespace pocketmine\world\sound; use pocketmine\math\Vector3; use pocketmine\network\mcpe\protocol\LevelEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelEvent; -class DoorBumpSound extends GenericSound{ - public function __construct(Vector3 $pos, float $pitch = 0){ - parent::__construct($pos, LevelEventPacket::EVENT_SOUND_DOOR_BUMP, $pitch); +class DoorBumpSound implements Sound{ + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::SOUND_DOOR_BUMP, 0, $pos)]; } } diff --git a/src/pocketmine/level/sound/DoorCrashSound.php b/src/world/sound/DoorCrashSound.php similarity index 76% rename from src/pocketmine/level/sound/DoorCrashSound.php rename to src/world/sound/DoorCrashSound.php index e9e817b4b4..3259c4889b 100644 --- a/src/pocketmine/level/sound/DoorCrashSound.php +++ b/src/world/sound/DoorCrashSound.php @@ -21,13 +21,15 @@ declare(strict_types=1); -namespace pocketmine\level\sound; +namespace pocketmine\world\sound; use pocketmine\math\Vector3; use pocketmine\network\mcpe\protocol\LevelEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelEvent; -class DoorCrashSound extends GenericSound{ - public function __construct(Vector3 $pos, float $pitch = 0){ - parent::__construct($pos, LevelEventPacket::EVENT_SOUND_DOOR_CRASH, $pitch); +class DoorCrashSound implements Sound{ + + 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 new file mode 100644 index 0000000000..368136c915 --- /dev/null +++ b/src/world/sound/DoorSound.php @@ -0,0 +1,46 @@ +pitch = $pitch; + } + + public function getPitch() : float{ + return $this->pitch; + } + + 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 new file mode 100644 index 0000000000..3b72698219 --- /dev/null +++ b/src/world/sound/EnderChestCloseSound.php @@ -0,0 +1,35 @@ +entity = $entity; + $this->blockLandedOn = $blockLandedOn; + } + + public function encode(Vector3 $pos) : array{ + return [LevelSoundEventPacket::create( + LevelSoundEvent::LAND, + $pos, + RuntimeBlockMapping::getInstance()->toRuntimeId($this->blockLandedOn->getFullId()), + $this->entity::getNetworkTypeId(), + false, //TODO: does isBaby have any relevance here? + false + )]; + } +} diff --git a/src/pocketmine/event/entity/EntityLevelChangeEvent.php b/src/world/sound/EntityLongFallSound.php similarity index 55% rename from src/pocketmine/event/entity/EntityLevelChangeEvent.php rename to src/world/sound/EntityLongFallSound.php index df688c0eca..c6cd2a33af 100644 --- a/src/pocketmine/event/entity/EntityLevelChangeEvent.php +++ b/src/world/sound/EntityLongFallSound.php @@ -21,32 +21,34 @@ declare(strict_types=1); -namespace pocketmine\event\entity; +namespace pocketmine\world\sound; use pocketmine\entity\Entity; -use pocketmine\event\Cancellable; -use pocketmine\level\Level; +use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelSoundEvent; /** - * @phpstan-extends EntityEvent + * Played when an entity hits ground after falling a long distance (damage). + * This is the bone-breaker "crunch" sound. */ -class EntityLevelChangeEvent extends EntityEvent implements Cancellable{ - /** @var Level */ - private $originLevel; - /** @var Level */ - private $targetLevel; +class EntityLongFallSound implements Sound{ - public function __construct(Entity $entity, Level $originLevel, Level $targetLevel){ + /** @var Entity */ + private $entity; + + public function __construct(Entity $entity){ $this->entity = $entity; - $this->originLevel = $originLevel; - $this->targetLevel = $targetLevel; } - public function getOrigin() : Level{ - return $this->originLevel; - } - - public function getTarget() : Level{ - return $this->targetLevel; + public function encode(Vector3 $pos) : array{ + return [LevelSoundEventPacket::create( + LevelSoundEvent::FALL_BIG, + $pos, + -1, + $this->entity::getNetworkTypeId(), + false, //TODO: is isBaby relevant here? + false + )]; } } diff --git a/src/pocketmine/inventory/EntityInventoryEventProcessor.php b/src/world/sound/EntityShortFallSound.php similarity index 61% rename from src/pocketmine/inventory/EntityInventoryEventProcessor.php rename to src/world/sound/EntityShortFallSound.php index 1b4fbf0eb0..722a3c1a3c 100644 --- a/src/pocketmine/inventory/EntityInventoryEventProcessor.php +++ b/src/world/sound/EntityShortFallSound.php @@ -21,13 +21,18 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\world\sound; use pocketmine\entity\Entity; -use pocketmine\event\entity\EntityInventoryChangeEvent; -use pocketmine\item\Item; +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. + */ +class EntityShortFallSound implements Sound{ -class EntityInventoryEventProcessor implements InventoryEventProcessor{ /** @var Entity */ private $entity; @@ -35,13 +40,14 @@ class EntityInventoryEventProcessor implements InventoryEventProcessor{ $this->entity = $entity; } - public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem, Item $newItem) : ?Item{ - $ev = new EntityInventoryChangeEvent($this->entity, $oldItem, $newItem, $slot); - $ev->call(); - if($ev->isCancelled()){ - return null; - } - - return $ev->getNewItem(); + public function encode(Vector3 $pos) : array{ + return [LevelSoundEventPacket::create( + LevelSoundEvent::FALL_SMALL, + $pos, + -1, + $this->entity::getNetworkTypeId(), + false, //TODO: does isBaby have any relevance here? + false + )]; } } diff --git a/src/pocketmine/level/particle/BlockForceFieldParticle.php b/src/world/sound/ExplodeSound.php similarity index 70% rename from src/pocketmine/level/particle/BlockForceFieldParticle.php rename to src/world/sound/ExplodeSound.php index 4ad56cb77a..b20f5a3592 100644 --- a/src/pocketmine/level/particle/BlockForceFieldParticle.php +++ b/src/world/sound/ExplodeSound.php @@ -21,12 +21,15 @@ declare(strict_types=1); -namespace pocketmine\level\particle; +namespace pocketmine\world\sound; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelSoundEvent; -class BlockForceFieldParticle extends GenericParticle{ - public function __construct(Vector3 $pos, int $data = 0){ - parent::__construct($pos, Particle::TYPE_BLOCK_FORCE_FIELD, $data); //TODO: proper encode/decode of data +class ExplodeSound implements Sound{ + + 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 new file mode 100644 index 0000000000..a4f02fb035 --- /dev/null +++ b/src/world/sound/FireExtinguishSound.php @@ -0,0 +1,35 @@ +pitch = $pitch; + } + + public function getPitch() : float{ + return $this->pitch; + } + + 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 new file mode 100644 index 0000000000..7edd564cf3 --- /dev/null +++ b/src/world/sound/FlintSteelSound.php @@ -0,0 +1,35 @@ +pitch = $pitch; + } + + public function getPitch() : float{ + return $this->pitch; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::SOUND_SHOOT, (int) ($this->pitch * 1000), $pos)]; + } +} diff --git a/src/world/sound/NoteInstrument.php b/src/world/sound/NoteInstrument.php new file mode 100644 index 0000000000..b22bc21492 --- /dev/null +++ b/src/world/sound/NoteInstrument.php @@ -0,0 +1,66 @@ +Enum___construct($name); + $this->magicNumber = $magicNumber; + } + + public function getMagicNumber() : int{ + return $this->magicNumber; + } +} diff --git a/src/world/sound/NoteSound.php b/src/world/sound/NoteSound.php new file mode 100644 index 0000000000..00101b8d77 --- /dev/null +++ b/src/world/sound/NoteSound.php @@ -0,0 +1,48 @@ + 255){ + throw new \InvalidArgumentException("Note $note is outside accepted range"); + } + $this->instrument = $instrument; + $this->note = $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 new file mode 100644 index 0000000000..2363299f39 --- /dev/null +++ b/src/world/sound/PaintingPlaceSound.php @@ -0,0 +1,36 @@ +pitch = $pitch; + } + + public function getPitch() : float{ + return $this->pitch; + } + + public function encode(Vector3 $pos) : array{ + return [LevelEventPacket::create(LevelEvent::SOUND_POP, (int) ($this->pitch * 1000), $pos)]; } } diff --git a/src/world/sound/PotionSplashSound.php b/src/world/sound/PotionSplashSound.php new file mode 100644 index 0000000000..083e8e0210 --- /dev/null +++ b/src/world/sound/PotionSplashSound.php @@ -0,0 +1,35 @@ +recordType = $recordType; + } + + 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 new file mode 100644 index 0000000000..0f2b7dcc9d --- /dev/null +++ b/src/world/sound/RecordStopSound.php @@ -0,0 +1,35 @@ + +namespace pocketmine\world\sound; use pocketmine\math\Vector3; -use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\LevelSoundEventPacket; +use pocketmine\network\mcpe\protocol\types\LevelSoundEvent; +use function intdiv; +use function min; -class SpawnExperienceOrbPacket extends DataPacket{ - public const NETWORK_ID = ProtocolInfo::SPAWN_EXPERIENCE_ORB_PACKET; +class XpLevelUpSound implements Sound{ - /** @var Vector3 */ - public $position; /** @var int */ - public $amount; + private $xpLevel; - protected function decodePayload(){ - $this->position = $this->getVector3(); - $this->amount = $this->getVarInt(); + public function __construct(int $xpLevel){ + $this->xpLevel = $xpLevel; } - protected function encodePayload(){ - $this->putVector3($this->position); - $this->putVarInt($this->amount); + public function getXpLevel() : int{ + return $this->xpLevel; } - public function handle(NetworkSession $session) : bool{ - return $session->handleSpawnExperienceOrb($this); + public function encode(Vector3 $pos) : array{ + //No idea why such odd numbers, but this works... + //TODO: check arbitrary volume + 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 new file mode 100644 index 0000000000..69bdb44e10 --- /dev/null +++ b/src/world/utils/SubChunkExplorer.php @@ -0,0 +1,102 @@ +world = $world; + } + + /** + * @phpstan-return SubChunkExplorerStatus::* + */ + 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){ + $this->currentX = $newChunkX; + $this->currentZ = $newChunkZ; + $this->currentSubChunk = null; + + $this->currentChunk = $this->world->getChunk($this->currentX, $this->currentZ); + if($this->currentChunk === null){ + return SubChunkExplorerStatus::INVALID; + } + } + + $newChunkY = $y >> SubChunk::COORD_BIT_SIZE; + if($this->currentSubChunk === null or $this->currentY !== $newChunkY){ + $this->currentY = $newChunkY; + + if($this->currentY < Chunk::MIN_SUBCHUNK_INDEX or $this->currentY > Chunk::MAX_SUBCHUNK_INDEX){ + $this->currentSubChunk = null; + return SubChunkExplorerStatus::INVALID; + } + + $this->currentSubChunk = $this->currentChunk->getSubChunk($newChunkY); + return SubChunkExplorerStatus::MOVED; + } + + return SubChunkExplorerStatus::OK; + } + + /** + * @phpstan-return SubChunkExplorerStatus::* + */ + public function moveToChunk(int $chunkX, int $chunkY, int $chunkZ) : int{ + //this is a cold path, so we don't care much if it's a bit slower (extra fcall overhead) + return $this->moveTo($chunkX << SubChunk::COORD_BIT_SIZE, $chunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); + } + + /** + * Returns whether we currently have a valid terrain pointer. + */ + public function isValid() : bool{ + return $this->currentSubChunk !== null; + } + + public function invalidate() : void{ + $this->currentChunk = null; + $this->currentSubChunk = null; + } +} diff --git a/src/pocketmine/network/mcpe/protocol/types/ResourcePackType.php b/src/world/utils/SubChunkExplorerStatus.php similarity index 71% rename from src/pocketmine/network/mcpe/protocol/types/ResourcePackType.php rename to src/world/utils/SubChunkExplorerStatus.php index bc4a217cff..43b32f1fa7 100644 --- a/src/pocketmine/network/mcpe/protocol/types/ResourcePackType.php +++ b/src/world/utils/SubChunkExplorerStatus.php @@ -21,21 +21,17 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; - -final class ResourcePackType{ +namespace pocketmine\world\utils; +final class SubChunkExplorerStatus{ private function __construct(){ //NOOP } + /** We encountered terrain not accessible by the current terrain provider */ public const INVALID = 0; - public const ADDON = 1; - public const CACHED = 2; - public const COPY_PROTECTED = 3; - public const BEHAVIORS = 4; - public const PERSONA_PIECE = 5; - public const RESOURCES = 6; - public const SKINS = 7; - public const WORLD_TEMPLATE = 8; + /** We remained inside the same (sub)chunk */ + public const OK = 1; + /** We moved to a different (sub)chunk */ + public const MOVED = 2; } diff --git a/tests/phpstan/bootstrap.php b/tests/phpstan/bootstrap.php index 2969d503c2..1fef2325c8 100644 --- a/tests/phpstan/bootstrap.php +++ b/tests/phpstan/bootstrap.php @@ -25,15 +25,9 @@ if(!defined('LEVELDB_ZLIB_RAW_COMPRESSION')){ //leveldb might not be loaded define('LEVELDB_ZLIB_RAW_COMPRESSION', 4); } - -//TODO: these need to be defined properly or removed -define('pocketmine\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 2) . '/vendor/autoload.php'); -define('pocketmine\DATA', ''); -define('pocketmine\GIT_COMMIT', str_repeat('00', 20)); -define('pocketmine\BUILD_NUMBER', 0); -define('pocketmine\PLUGIN_PATH', ''); -define('pocketmine\START_TIME', microtime(true)); -define('pocketmine\VERSION', '9.9.9'); +if(!extension_loaded('libdeflate')){ + function libdeflate_deflate_compress(string $data, int $level = 6) : string{} +} //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 59203a8d59..52b727319a 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -20,4018 +20,1483 @@ parameters: count: 1 path: ../../../build/server-phar.php - - - message: "#^Cannot access offset \\(float\\|int\\) on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset string on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Instanceof between pocketmine\\\\plugin\\\\PluginManager and pocketmine\\\\plugin\\\\PluginManager will always evaluate to true\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Utils\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 7 - path: ../../../src/pocketmine/MemoryManager.php - - message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/MemoryManager.php + path: ../../../src/MemoryManager.php - message: "#^Parameter \\#1 \\$stream of function fwrite expects resource, resource\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/MemoryManager.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'AnimationExpression' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'Colors' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'Frames' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'Image' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'ImageHeight' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'ImageWidth' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'IsDefault' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'PackId' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'PieceId' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'PieceType' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'ProductId' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access offset 'Type' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access property \\$x on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access property \\$y on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot access property \\$z on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method checkSpawnProtection\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method dropExperience\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method dropItem\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method equals\\(\\) on pocketmine\\\\utils\\\\UUID\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getAllValues\\(\\) on pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 6 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getChunkEntities\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getCollisionBlocks\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getDifficulty\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getEntity\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getFloorX\\(\\) on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getFloorY\\(\\) on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getFloorZ\\(\\) on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getFolderName\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getLevelNonNull\\(\\) on pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getName\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getNearbyEntities\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getSafeSpawn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getTile\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getTileAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method getTime\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method isChunkGenerated\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method isInLoadedTerrain\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method populateChunk\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method registerChunkLoader\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method requestChunk\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method sendBlocks\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method sendDifficulty\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method sendTime\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method setSleepTicks\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method unregisterChunkLoader\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot call method useItemOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Method pocketmine\\\\Player\\:\\:getSpawn\\(\\) should return pocketmine\\\\level\\\\Position but returns pocketmine\\\\level\\\\Position\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Only booleans are allowed in &&, mixed given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the left side\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$height of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinImage constructor expects int, mixed given\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$level of method pocketmine\\\\entity\\\\Human\\:\\:__construct\\(\\) expects pocketmine\\\\level\\\\Level, pocketmine\\\\level\\\\Level\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$pieceId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaSkinPiece constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$pieceType of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaPieceTintColor constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$skinId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$string of function base64_decode expects string, mixed given\\.$#" - count: 7 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\$uuid of method pocketmine\\\\Server\\:\\:updatePlayerListData\\(\\) expects pocketmine\\\\utils\\\\UUID, pocketmine\\\\utils\\\\UUID\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#1 \\.\\.\\.\\$slots of method pocketmine\\\\inventory\\\\BaseInventory\\:\\:addItem\\(\\) expects pocketmine\\\\item\\\\Item, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#10 \\$capeId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#12 \\$armSize of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#13 \\$skinColor of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#17 \\$premium of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects bool, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#18 \\$persona of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects bool, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#19 \\$personaCapeOnClassic of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects bool, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#2 \\$colors of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaPieceTintColor constructor expects array\\, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#2 \\$pieceType of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaSkinPiece constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#2 \\$playFabId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#2 \\$type of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinAnimation constructor expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#2 \\$width of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinImage constructor expects int, mixed given\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#3 \\$data of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinImage constructor expects string, string\\|false given\\.$#" - count: 3 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#3 \\$frames of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinAnimation constructor expects float, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#3 \\$packId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaSkinPiece constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#3 \\$resourcePatch of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#4 \\$expressionType of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinAnimation constructor expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#4 \\$isDefaultPiece of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaSkinPiece constructor expects bool, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#5 \\$productId of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PersonaSkinPiece constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#7 \\$geometryData of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#8 \\$geometryDataEngineVersion of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Parameter \\#9 \\$animationData of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^pocketmine\\\\Player\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\entity\\\\Human\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php + path: ../../../src/MemoryManager.php - message: "#^Binary operation \"\\.\" between array\\\\|string\\|false and '/'\\|'\\\\\\\\' results in an error\\.$#" count: 2 - path: ../../../src/pocketmine/PocketMine.php - - - - message: "#^Cannot access offset 'build' on mixed\\.$#" - count: 3 - path: ../../../src/pocketmine/PocketMine.php - - - - message: "#^Cannot access offset 'git' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/PocketMine.php + path: ../../../src/PocketMine.php - message: "#^Do\\-while loop condition is always false\\.$#" count: 1 - path: ../../../src/pocketmine/PocketMine.php + path: ../../../src/PocketMine.php - message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/PocketMine.php - - - - message: "#^Parameter \\#1 \\$path of function realpath expects string, array\\\\|string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/PocketMine.php + path: ../../../src/PocketMine.php - message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#" count: 2 - path: ../../../src/pocketmine/PocketMine.php + path: ../../../src/PocketMine.php - message: "#^Parameter \\#1 \\$version1 of function version_compare expects string, string\\|false given\\.$#" count: 3 - path: ../../../src/pocketmine/PocketMine.php - - - - message: "#^Cannot access offset 'type' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot call method getFolderName\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot call method getSafeSpawn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot call method wait\\(\\) on Threaded\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot cast array\\\\|string\\|false to string\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot cast mixed to float\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 7 - path: ../../../src/pocketmine/Server.php + path: ../../../src/PocketMine.php - message: "#^Cannot cast mixed to string\\.$#" count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Instanceof between pocketmine\\\\command\\\\CommandReader and pocketmine\\\\command\\\\CommandReader will always evaluate to true\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Instanceof between pocketmine\\\\network\\\\Network and pocketmine\\\\network\\\\Network will always evaluate to true\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Instanceof between pocketmine\\\\plugin\\\\PluginManager and pocketmine\\\\plugin\\\\PluginManager will always evaluate to true\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Instanceof between pocketmine\\\\scheduler\\\\AsyncPool and pocketmine\\\\scheduler\\\\AsyncPool will always evaluate to true\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php + path: ../../../src/Server.php - message: "#^Only numeric types are allowed in \\+, int\\|false given on the left side\\.$#" count: 1 - path: ../../../src/pocketmine/Server.php + path: ../../../src/Server.php - - message: "#^Parameter \\#1 \\$array of function array_filter expects array, array\\\\|false given\\.$#" + message: "#^Parameter \\#1 \\$array of static method pocketmine\\\\plugin\\\\PluginGraylist\\:\\:fromArray\\(\\) expects array, mixed given\\.$#" count: 1 - path: ../../../src/pocketmine/Server.php + path: ../../../src/Server.php - - message: "#^Parameter \\#1 \\$buffer of method pocketmine\\\\nbt\\\\NBTStream\\:\\:readCompressed\\(\\) expects string, string\\|false given\\.$#" + message: "#^Parameter \\#1 \\$input of function yaml_parse expects string, string\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#1 \\$name of static method pocketmine\\\\level\\\\format\\\\io\\\\LevelProviderManager\\:\\:getProviderByName\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#1 \\$uuid of method pocketmine\\\\Server\\:\\:removePlayerListData\\(\\) expects pocketmine\\\\utils\\\\UUID, pocketmine\\\\utils\\\\UUID\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#1 \\$uuid of method pocketmine\\\\Server\\:\\:updatePlayerListData\\(\\) expects pocketmine\\\\utils\\\\UUID, pocketmine\\\\utils\\\\UUID\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#1 \\$uuid of static method pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\PlayerListEntry\\:\\:createAdditionEntry\\(\\) expects pocketmine\\\\utils\\\\UUID, pocketmine\\\\utils\\\\UUID\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#2 \\$defaultValue of method pocketmine\\\\Server\\:\\:getConfigString\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#2 \\$endpoint of class pocketmine\\\\updater\\\\AutoUpdater constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php - - - - message: "#^Parameter \\#2 \\$reason of method pocketmine\\\\Player\\:\\:close\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/Server.php + path: ../../../src/Server.php - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/Server.php + path: ../../../src/Server.php - - message: "#^Method pocketmine\\\\ThreadManager\\:\\:getAll\\(\\) should return array\\ but returns array\\.$#" + message: "#^Cannot cast array\\\\|string\\|false to string\\.$#" count: 1 - path: ../../../src/pocketmine/ThreadManager.php + path: ../../../src/ServerConfigGroup.php - - message: "#^pocketmine\\\\block\\\\Air\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Air.php - - - - message: "#^pocketmine\\\\block\\\\Anvil\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Anvil.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BaseRail.php - - - - message: "#^pocketmine\\\\block\\\\BaseRail\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BaseRail.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the left side\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Bed.php - - - - message: "#^pocketmine\\\\block\\\\Bed\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Bed.php - - - - message: "#^pocketmine\\\\block\\\\Bedrock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Bedrock.php - - - - message: "#^pocketmine\\\\block\\\\Beetroot\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Beetroot.php - - - - message: "#^Cannot call method getBlockMetadata\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/block/Block.php - - - - message: "#^pocketmine\\\\block\\\\Block\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\level\\\\Position\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Block.php - - - - message: "#^Cannot access property \\$level on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^Cannot access property \\$x on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^Cannot access property \\$y on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^Cannot access property \\$z on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^Cannot clone pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^Method pocketmine\\\\block\\\\BlockFactory\\:\\:get\\(\\) should return pocketmine\\\\block\\\\Block but returns pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BlockFactory.php - - - - message: "#^pocketmine\\\\block\\\\BoneBlock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BoneBlock.php - - - - message: "#^pocketmine\\\\block\\\\Bookshelf\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Bookshelf.php - - - - message: "#^pocketmine\\\\block\\\\BrewingStand\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BrewingStand.php - - - - message: "#^pocketmine\\\\block\\\\BrickStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BrickStairs.php - - - - message: "#^pocketmine\\\\block\\\\Bricks\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Bricks.php - - - - message: "#^pocketmine\\\\block\\\\BurningFurnace\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/BurningFurnace.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Button.php - - - - message: "#^pocketmine\\\\block\\\\Button\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Button.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cactus.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cactus.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cactus.php - - - - message: "#^pocketmine\\\\block\\\\Cactus\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cactus.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cake.php - - - - message: "#^pocketmine\\\\block\\\\Cake\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cake.php - - - - message: "#^pocketmine\\\\block\\\\Carpet\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Carpet.php - - - - message: "#^pocketmine\\\\block\\\\Carrot\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Carrot.php - - - - message: "#^pocketmine\\\\block\\\\Chest\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Chest.php - - - - message: "#^pocketmine\\\\block\\\\Clay\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Clay.php - - - - message: "#^pocketmine\\\\block\\\\Coal\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Coal.php - - - - message: "#^pocketmine\\\\block\\\\CoalOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/CoalOre.php - - - - message: "#^pocketmine\\\\block\\\\Cobblestone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cobblestone.php - - - - message: "#^pocketmine\\\\block\\\\CobblestoneStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/CobblestoneStairs.php - - - - message: "#^pocketmine\\\\block\\\\CobblestoneWall\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/CobblestoneWall.php - - - - message: "#^pocketmine\\\\block\\\\Cobweb\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Cobweb.php - - - - message: "#^pocketmine\\\\block\\\\CocoaBlock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/CocoaBlock.php - - - - message: "#^pocketmine\\\\block\\\\Concrete\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Concrete.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/ConcretePowder.php - - - - message: "#^pocketmine\\\\block\\\\ConcretePowder\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/ConcretePowder.php - - - - message: "#^pocketmine\\\\block\\\\CraftingTable\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/CraftingTable.php - - - - message: "#^pocketmine\\\\block\\\\Dandelion\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Dandelion.php - - - - message: "#^pocketmine\\\\block\\\\DaylightSensor\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/DaylightSensor.php - - - - message: "#^pocketmine\\\\block\\\\DeadBush\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/DeadBush.php - - - - message: "#^pocketmine\\\\block\\\\Diamond\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Diamond.php - - - - message: "#^pocketmine\\\\block\\\\DiamondOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/DiamondOre.php - - - - message: "#^pocketmine\\\\block\\\\Dirt\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Dirt.php - - - - message: "#^Cannot call method addSound\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Cannot cast mixed to int\\.$#" count: 2 - path: ../../../src/pocketmine/block/Door.php + path: ../../../src/ServerConfigGroup.php - - message: "#^Cannot call method getDirection\\(\\) on pocketmine\\\\Player\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Door.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Cannot cast mixed to string\\.$#" count: 2 - path: ../../../src/pocketmine/block/Door.php + path: ../../../src/ServerConfigGroup.php - - message: "#^Only numeric types are allowed in \\+, int\\|null given on the left side\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Door.php - - - - message: "#^pocketmine\\\\block\\\\DoublePlant\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/DoublePlant.php - - - - message: "#^pocketmine\\\\block\\\\DoubleSlab\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/DoubleSlab.php - - - - message: "#^pocketmine\\\\block\\\\Emerald\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Emerald.php - - - - message: "#^pocketmine\\\\block\\\\EmeraldOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EmeraldOre.php - - - - message: "#^pocketmine\\\\block\\\\EnchantingTable\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EnchantingTable.php - - - - message: "#^pocketmine\\\\block\\\\EndPortalFrame\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EndPortalFrame.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EndRod.php - - - - message: "#^pocketmine\\\\block\\\\EndRod\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EndRod.php - - - - message: "#^pocketmine\\\\block\\\\EndStone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EndStone.php - - - - message: "#^pocketmine\\\\block\\\\EndStoneBricks\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/EndStoneBricks.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Fallable.php - - - - message: "#^Cannot call method getBlockIdAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Farmland.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/block/Farmland.php - - - - message: "#^pocketmine\\\\block\\\\Farmland\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Farmland.php - - - - message: "#^pocketmine\\\\block\\\\Fence\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Fence.php - - - - message: "#^Cannot call method addSound\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/FenceGate.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the left side\\.$#" + message: "#^Cannot access offset 'git' on mixed\\.$#" count: 2 - path: ../../../src/pocketmine/block/FenceGate.php + path: ../../../src/VersionInfo.php - - message: "#^Cannot call method scheduleDelayedBlockUpdate\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Fire.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Fire.php - - - - message: "#^pocketmine\\\\block\\\\Fire\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Method pocketmine\\\\VersionInfo\\:\\:GIT_HASH\\(\\) should return string but returns mixed\\.$#" count: 1 - path: ../../../src/pocketmine/block/Fire.php + path: ../../../src/VersionInfo.php - - message: "#^pocketmine\\\\block\\\\Flower\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Static property pocketmine\\\\VersionInfo\\:\\:\\$gitHash \\(string\\|null\\) does not accept mixed\\.$#" count: 1 - path: ../../../src/pocketmine/block/Flower.php + path: ../../../src/VersionInfo.php - - message: "#^pocketmine\\\\block\\\\FlowerPot\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Cannot call method setFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\Chunk\\|null\\.$#" count: 1 - path: ../../../src/pocketmine/block/FlowerPot.php + path: ../../../src/block/Block.php - - message: "#^pocketmine\\\\block\\\\Glass\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Glass.php + path: ../../../src/block/Cactus.php - - message: "#^pocketmine\\\\block\\\\GlassPane\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GlassPane.php + path: ../../../src/block/Cactus.php - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the left side\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GlazedTerracotta.php + path: ../../../src/block/Cactus.php - - message: "#^pocketmine\\\\block\\\\GlowingObsidian\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GlowingObsidian.php + path: ../../../src/block/Cactus.php - - message: "#^pocketmine\\\\block\\\\Glowstone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Glowstone.php + path: ../../../src/block/Cactus.php - - message: "#^pocketmine\\\\block\\\\Gold\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Gold.php + path: ../../../src/block/Cactus.php - - message: "#^pocketmine\\\\block\\\\GoldOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GoldOre.php + path: ../../../src/block/DaylightSensor.php - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Grass.php + path: ../../../src/block/DaylightSensor.php - - message: "#^Cannot call method getBlockDataAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getRealBlockSkyLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Grass.php + path: ../../../src/block/DaylightSensor.php - - message: "#^Cannot call method getBlockIdAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Grass.php + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" + count: 1 + path: ../../../src/block/DragonEgg.php - - message: "#^Cannot call method getFullLightAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Grass.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: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Grass.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/pocketmine/block/Grass.php + 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/pocketmine/block/Grass.php + path: ../../../src/block/Grass.php - - message: "#^pocketmine\\\\block\\\\Grass\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Grass.php + path: ../../../src/block/Grass.php - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GrassPath.php + path: ../../../src/block/Grass.php - - message: "#^pocketmine\\\\block\\\\GrassPath\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/GrassPath.php + path: ../../../src/block/Grass.php - - message: "#^pocketmine\\\\block\\\\Gravel\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Gravel.php + path: ../../../src/block/Grass.php - - message: "#^pocketmine\\\\block\\\\HardenedClay\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/HardenedClay.php + path: ../../../src/block/Ice.php - - message: "#^pocketmine\\\\block\\\\HayBale\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/HayBale.php + path: ../../../src/block/Ice.php - - message: "#^Cannot call method getHighestAdjacentBlockLight\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getHighestAdjacentBlockLight\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Ice.php + path: ../../../src/block/Ice.php - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Ice.php + path: ../../../src/block/Leaves.php - - message: "#^pocketmine\\\\block\\\\Ice\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Ice.php + path: ../../../src/block/Leaves.php - - message: "#^pocketmine\\\\block\\\\InvisibleBedrock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\world\\\\World\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/InvisibleBedrock.php + path: ../../../src/block/Leaves.php - - message: "#^pocketmine\\\\block\\\\Iron\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + 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/pocketmine/block/Iron.php + path: ../../../src/block/Liquid.php - - message: "#^pocketmine\\\\block\\\\IronBars\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + 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/pocketmine/block/IronBars.php + path: ../../../src/block/Liquid.php - - message: "#^pocketmine\\\\block\\\\IronDoor\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + 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/pocketmine/block/IronDoor.php - - - - message: "#^pocketmine\\\\block\\\\IronOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/IronOre.php - - - - message: "#^Cannot call method getTile\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/block/ItemFrame.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/ItemFrame.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/ItemFrame.php - - - - message: "#^pocketmine\\\\block\\\\ItemFrame\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/ItemFrame.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Ladder.php - - - - message: "#^pocketmine\\\\block\\\\Ladder\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Ladder.php - - - - message: "#^pocketmine\\\\block\\\\Lapis\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Lapis.php - - - - message: "#^pocketmine\\\\block\\\\LapisOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/LapisOre.php - - - - message: "#^pocketmine\\\\block\\\\Lava\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Lava.php - - - - message: "#^pocketmine\\\\block\\\\Leaves\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Leaves.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Lever.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Lever.php - - - - message: "#^pocketmine\\\\block\\\\Lever\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Lever.php - - - - message: "#^Cannot call method addSound\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 25 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Cannot call method scheduleDelayedBlockUpdate\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#1 \\$blockX of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#2 \\$blockY of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#3 \\$blockZ of method pocketmine\\\\block\\\\Liquid\\:\\:calculateFlowCost\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/block/Liquid.php - - - - message: "#^pocketmine\\\\block\\\\Magma\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Magma.php - - - - message: "#^pocketmine\\\\block\\\\Melon\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Melon.php - - - - message: "#^pocketmine\\\\block\\\\MelonStem\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/MelonStem.php - - - - message: "#^pocketmine\\\\block\\\\MonsterSpawner\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/MonsterSpawner.php + path: ../../../src/block/Liquid.php - message: "#^Parameter \\#1 \\$min of function mt_rand expects int, float\\|int given\\.$#" count: 3 - path: ../../../src/pocketmine/block/Mycelium.php + path: ../../../src/block/Mycelium.php - message: "#^Parameter \\#2 \\$max of function mt_rand expects int, float\\|int given\\.$#" count: 3 - path: ../../../src/pocketmine/block/Mycelium.php + path: ../../../src/block/Mycelium.php - - message: "#^pocketmine\\\\block\\\\Mycelium\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Mycelium.php + path: ../../../src/block/SnowLayer.php - - message: "#^pocketmine\\\\block\\\\NetherBrickStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NetherBrickStairs.php + path: ../../../src/block/SnowLayer.php - - message: "#^pocketmine\\\\block\\\\NetherQuartzOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockLightAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NetherQuartzOre.php + path: ../../../src/block/SnowLayer.php - - message: "#^pocketmine\\\\block\\\\NetherReactor\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NetherReactor.php + path: ../../../src/block/Sugarcane.php - - message: "#^pocketmine\\\\block\\\\NetherWartBlock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NetherWartBlock.php + path: ../../../src/block/Sugarcane.php - - message: "#^pocketmine\\\\block\\\\NetherWartPlant\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NetherWartPlant.php + path: ../../../src/block/Sugarcane.php - - message: "#^pocketmine\\\\block\\\\Netherrack\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Netherrack.php + path: ../../../src/block/Sugarcane.php - - message: "#^pocketmine\\\\block\\\\NoteBlock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/NoteBlock.php + path: ../../../src/block/Sugarcane.php - - message: "#^pocketmine\\\\block\\\\Obsidian\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/block/Obsidian.php - - - - message: "#^pocketmine\\\\block\\\\PackedIce\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/PackedIce.php - - - - message: "#^pocketmine\\\\block\\\\Planks\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Planks.php - - - - message: "#^pocketmine\\\\block\\\\Podzol\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Podzol.php - - - - message: "#^pocketmine\\\\block\\\\Potato\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Potato.php - - - - message: "#^pocketmine\\\\block\\\\Prismarine\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Prismarine.php - - - - message: "#^pocketmine\\\\block\\\\Pumpkin\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Pumpkin.php - - - - message: "#^pocketmine\\\\block\\\\PumpkinStem\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/PumpkinStem.php - - - - message: "#^pocketmine\\\\block\\\\PurpurStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/PurpurStairs.php - - - - message: "#^pocketmine\\\\block\\\\Quartz\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Quartz.php - - - - message: "#^pocketmine\\\\block\\\\QuartzStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/QuartzStairs.php - - - - message: "#^pocketmine\\\\block\\\\RedMushroom\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/RedMushroom.php - - - - message: "#^pocketmine\\\\block\\\\RedMushroomBlock\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/RedMushroomBlock.php - - - - message: "#^pocketmine\\\\block\\\\Redstone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Redstone.php - - - - message: "#^pocketmine\\\\block\\\\RedstoneLamp\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/RedstoneLamp.php - - - - message: "#^pocketmine\\\\block\\\\RedstoneOre\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/RedstoneOre.php - - - - message: "#^pocketmine\\\\block\\\\Sand\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sand.php - - - - message: "#^pocketmine\\\\block\\\\Sandstone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sandstone.php - - - - message: "#^pocketmine\\\\block\\\\SandstoneStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SandstoneStairs.php - - - - message: "#^Cannot call method getFullLightAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sapling.php - - - - message: "#^Parameter \\#2 \\$x of static method pocketmine\\\\level\\\\generator\\\\object\\\\Tree\\:\\:growTree\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sapling.php - - - - message: "#^Parameter \\#3 \\$y of static method pocketmine\\\\level\\\\generator\\\\object\\\\Tree\\:\\:growTree\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sapling.php - - - - message: "#^Parameter \\#4 \\$z of static method pocketmine\\\\level\\\\generator\\\\object\\\\Tree\\:\\:growTree\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sapling.php - - - - message: "#^pocketmine\\\\block\\\\Sapling\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sapling.php - - - - message: "#^pocketmine\\\\block\\\\SeaLantern\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SeaLantern.php - - - - message: "#^pocketmine\\\\block\\\\SignPost\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SignPost.php - - - - message: "#^Cannot call method getTile\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Skull.php - - - - message: "#^pocketmine\\\\block\\\\Skull\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Skull.php - - - - message: "#^pocketmine\\\\block\\\\Slab\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Slab.php - - - - message: "#^pocketmine\\\\block\\\\Snow\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Snow.php - - - - message: "#^Cannot call method getBlockLightAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SnowLayer.php - - - - message: "#^pocketmine\\\\block\\\\SnowLayer\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SnowLayer.php - - - - message: "#^pocketmine\\\\block\\\\SoulSand\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/SoulSand.php - - - - message: "#^pocketmine\\\\block\\\\Sponge\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sponge.php - - - - message: "#^Cannot call method getTile\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/StandingBanner.php - - - - message: "#^pocketmine\\\\block\\\\StandingBanner\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/StandingBanner.php - - - - message: "#^pocketmine\\\\block\\\\Stone\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Stone.php - - - - message: "#^pocketmine\\\\block\\\\StoneBrickStairs\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/StoneBrickStairs.php - - - - message: "#^pocketmine\\\\block\\\\StoneBricks\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/StoneBricks.php - - - - message: "#^pocketmine\\\\block\\\\StonePressurePlate\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/StonePressurePlate.php - - - - message: "#^pocketmine\\\\block\\\\Stonecutter\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Stonecutter.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sugarcane.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sugarcane.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/block/Sugarcane.php - - - - message: "#^pocketmine\\\\block\\\\Sugarcane\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Sugarcane.php - - - - message: "#^pocketmine\\\\block\\\\TNT\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/TNT.php - - - - message: "#^pocketmine\\\\block\\\\TallGrass\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/TallGrass.php - - - - message: "#^pocketmine\\\\block\\\\Torch\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Torch.php - - - - message: "#^Cannot call method addSound\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Trapdoor.php - - - - message: "#^pocketmine\\\\block\\\\Trapdoor\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Trapdoor.php - - - - message: "#^pocketmine\\\\block\\\\Tripwire\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Tripwire.php - - - - message: "#^pocketmine\\\\block\\\\TripwireHook\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/TripwireHook.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Vine.php - - - - message: "#^Cannot call method useBreakOn\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Vine.php - - - - message: "#^pocketmine\\\\block\\\\Vine\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Vine.php - - - - message: "#^pocketmine\\\\block\\\\Water\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Water.php - - - - message: "#^pocketmine\\\\block\\\\WaterLily\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/WaterLily.php - - - - message: "#^pocketmine\\\\block\\\\WeightedPressurePlateLight\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/WeightedPressurePlateLight.php - - - - message: "#^pocketmine\\\\block\\\\Wheat\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Wheat.php - - - - message: "#^pocketmine\\\\block\\\\Wood\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Wood.php - - - - message: "#^pocketmine\\\\block\\\\Wool\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\block\\\\Block\\.$#" - count: 1 - path: ../../../src/pocketmine/block/Wool.php - - - - message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/Command.php - - - - message: "#^Cannot access offset 'mode' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/command/CommandReader.php - - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: ../../../src/pocketmine/command/CommandReader.php - - - - message: "#^Parameter \\#1 \\$stream of method pocketmine\\\\command\\\\CommandReader\\:\\:isPipe\\(\\) expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/CommandReader.php - - - - message: "#^Static property pocketmine\\\\command\\\\CommandReader\\:\\:\\$stdin \\(resource\\) does not accept resource\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/command/CommandReader.php - - - - message: "#^Cannot call method startTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/SimpleCommandMap.php - - - - message: "#^Cannot call method stopTiming\\(\\) on pocketmine\\\\timings\\\\TimingsHandler\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/SimpleCommandMap.php - - - - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/BanIpCommand.php - - - - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/PardonIpCommand.php - - - - message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/ParticleCommand.php - - - - message: "#^Cannot call method getSeed\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/SeedCommand.php - - - - message: "#^Cannot call method setSpawnLocation\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/SetWorldSpawnCommand.php - - - - message: "#^Parameter \\#1 \\$num of function round expects float\\|int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/StatusCommand.php - - - - message: "#^Cannot call method getTime\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimeCommand.php - - - - message: "#^Cannot access offset 0 on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$fp of static method pocketmine\\\\timings\\\\TimingsHandler\\:\\:printTimings\\(\\) expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function fseek expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#1 \\$stream of function stream_get_contents expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#2 \\$host of class class@anonymous/src/pocketmine/command/defaults/TimingsCommand\\.php\\:126 constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Parameter \\#4 \\$data of class class@anonymous/src/pocketmine/command/defaults/TimingsCommand\\.php\\:126 constructor expects array\\, array\\ given\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/TimingsCommand.php - - - - message: "#^Cannot cast mixed to float\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/VanillaCommand.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: ../../../src/pocketmine/command/defaults/VanillaCommand.php - - - - message: "#^Cannot call method getValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/entity/Effect.php - - - - message: "#^Cannot call method setValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/entity/Effect.php - - - - message: "#^Parameter \\#1 \\$id of static method pocketmine\\\\entity\\\\Effect\\:\\:getEffect\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Effect.php - - - - message: "#^Array \\(array\\, string\\>\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot access property \\$updateEntities on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method addEntity\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method addEntity\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method broadcastPacketToViewers\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getAllValues\\(\\) on pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getBlockIdAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 7 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getChunk\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getChunkAtPosition\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getCollisionBlocks\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getCollisionCubes\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 4 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getTickRateTime\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method getViewersForPosition\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method removeEntity\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method setValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Method pocketmine\\\\entity\\\\Entity\\:\\:getNameTag\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Method pocketmine\\\\entity\\\\Entity\\:\\:getScale\\(\\) should return float but returns float\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Only booleans are allowed in a negated boolean, bool\\|null given\\.$#" - count: 6 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Only booleans are allowed in an if condition, bool\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Parameter \\#1 \\$fireTicks of method pocketmine\\\\entity\\\\Entity\\:\\:setFireTicks\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Parameter \\#2 \\$originLevel of class pocketmine\\\\event\\\\entity\\\\EntityLevelChangeEvent constructor expects pocketmine\\\\level\\\\Level, pocketmine\\\\level\\\\Level\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setShort\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method broadcastLevelSoundEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method getDifficulty\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method getMaxValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method getMinValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method getValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 8 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method setValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 6 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Parameter \\#1 \\$attribute of method pocketmine\\\\entity\\\\AttributeMap\\:\\:addAttribute\\(\\) expects pocketmine\\\\entity\\\\Attribute, pocketmine\\\\entity\\\\Attribute\\|null given\\.$#" - count: 5 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 3 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Parameter \\#1 \\$index of method pocketmine\\\\inventory\\\\BaseInventory\\:\\:setItem\\(\\) expects int, int\\|string given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AddPlayerPacket\\:\\:\\$uuid \\(pocketmine\\\\utils\\\\UUID\\) does not accept pocketmine\\\\utils\\\\UUID\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\PlayerSkinPacket\\:\\:\\$uuid \\(pocketmine\\\\utils\\\\UUID\\) does not accept pocketmine\\\\utils\\\\UUID\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php - - - - message: "#^Cannot call method attack\\(\\) on pocketmine\\\\entity\\\\Entity\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method broadcastLevelSoundEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method dropExperience\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method getEffectLevel\\(\\) on pocketmine\\\\entity\\\\EffectInstance\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method getMaxValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method getValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method setMaxValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Cannot call method setValue\\(\\) on pocketmine\\\\entity\\\\Attribute\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Method pocketmine\\\\entity\\\\Living\\:\\:getAirSupplyTicks\\(\\) should return int but returns int\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Method pocketmine\\\\entity\\\\Living\\:\\:getMaxAirSupplyTicks\\(\\) should return int but returns int\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Parameter \\#1 \\$attribute of method pocketmine\\\\entity\\\\AttributeMap\\:\\:addAttribute\\(\\) expects pocketmine\\\\entity\\\\Attribute, pocketmine\\\\entity\\\\Attribute\\|null given\\.$#" - count: 6 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Parameter \\#2 \\$entity of class pocketmine\\\\event\\\\entity\\\\EntityDamageByEntityEvent constructor expects pocketmine\\\\entity\\\\Entity, pocketmine\\\\entity\\\\Entity\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Living.php - - - - message: "#^Method pocketmine\\\\entity\\\\Villager\\:\\:getProfession\\(\\) should return int but returns int\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Villager.php - - - - message: "#^Cannot call method getEntity\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/ExperienceOrb.php - - - - message: "#^Cannot call method getNearestEntity\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/ExperienceOrb.php - - - - message: "#^Cannot call method getBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/FallingBlock.php - - - - message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Cannot call method dropItem\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Method pocketmine\\\\entity\\\\object\\\\Painting\\:\\:getMotive\\(\\) should return pocketmine\\\\entity\\\\object\\\\PaintingMotive but returns pocketmine\\\\entity\\\\object\\\\PaintingMotive\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Parameter \\#1 \\$level of static method pocketmine\\\\entity\\\\object\\\\Painting\\:\\:canFit\\(\\) expects pocketmine\\\\level\\\\Level, pocketmine\\\\level\\\\Level\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/Painting.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/object/PrimedTNT.php - - - - message: "#^Cannot call method broadcastLevelSoundEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Arrow.php - - - - message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Egg.php - - - - message: "#^Cannot call method addSound\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/projectile/EnderPearl.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/EnderPearl.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/ExperienceBottle.php - - - - message: "#^Cannot call method broadcastLevelSoundEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/ExperienceBottle.php - - - - message: "#^Cannot call method dropExperience\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/ExperienceBottle.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/projectile/Projectile.php - - - - message: "#^Cannot call method getCollidingEntities\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Projectile.php - - - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setByte\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Projectile.php - - - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/entity/projectile/Projectile.php - - - - message: "#^Parameter \\#2 \\$value of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setInt\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Projectile.php - - - - message: "#^Cannot call method addParticle\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/Snowball.php - - - - message: "#^Cannot call method broadcastLevelEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/SplashPotion.php - - - - message: "#^Cannot call method broadcastLevelSoundEvent\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/SplashPotion.php - - - - message: "#^Cannot call method getNearbyEntities\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/projectile/SplashPotion.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/entity/projectile/SplashPotion.php - - - - message: "#^Method pocketmine\\\\event\\\\EventPriority\\:\\:fromString\\(\\) should return int but returns mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/event/EventPriority.php - - - - message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" - count: 1 - path: ../../../src/pocketmine/event/HandlerList.php - - - - message: "#^Cannot call method getEffectLevel\\(\\) on pocketmine\\\\entity\\\\EffectInstance\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/event/entity/EntityDamageByEntityEvent.php - - - - message: "#^Property pocketmine\\\\event\\\\entity\\\\EntityShootBowEvent\\:\\:\\$projectile \\(pocketmine\\\\entity\\\\projectile\\\\Projectile\\) does not accept pocketmine\\\\entity\\\\Entity\\.$#" - count: 1 - path: ../../../src/pocketmine/event/entity/EntityShootBowEvent.php - - - - message: "#^PHPDoc type pocketmine\\\\level\\\\Position of property pocketmine\\\\inventory\\\\AnvilInventory\\:\\:\\$holder is not the same as PHPDoc type pocketmine\\\\math\\\\Vector3 of overridden property pocketmine\\\\inventory\\\\ContainerInventory\\:\\:\\$holder\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/AnvilInventory.php - - - - message: "#^PHPDoc type pocketmine\\\\tile\\\\Chest of property pocketmine\\\\inventory\\\\ChestInventory\\:\\:\\$holder is not the same as PHPDoc type pocketmine\\\\math\\\\Vector3 of overridden property pocketmine\\\\inventory\\\\ContainerInventory\\:\\:\\$holder\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/ChestInventory.php - - - - message: "#^Method pocketmine\\\\inventory\\\\CraftingManager\\:\\:getCraftingDataPacket\\(\\) should return pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BatchPacket but returns pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BatchPacket\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/CraftingManager.php - - - - message: "#^Method pocketmine\\\\inventory\\\\CraftingManager\\:\\:hashOutputs\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/CraftingManager.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/CraftingManager.php - - - - message: "#^pocketmine\\\\inventory\\\\DoubleChestInventory\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\inventory\\\\ChestInventory\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/DoubleChestInventory.php - - - - message: "#^PHPDoc type pocketmine\\\\level\\\\Position of property pocketmine\\\\inventory\\\\EnchantInventory\\:\\:\\$holder is not the same as PHPDoc type pocketmine\\\\math\\\\Vector3 of overridden property pocketmine\\\\inventory\\\\ContainerInventory\\:\\:\\$holder\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/EnchantInventory.php - - - - message: "#^PHPDoc type pocketmine\\\\level\\\\Position of property pocketmine\\\\inventory\\\\EnderChestInventory\\:\\:\\$holder is not the same as PHPDoc type pocketmine\\\\tile\\\\Chest of overridden property pocketmine\\\\inventory\\\\ChestInventory\\:\\:\\$holder\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/EnderChestInventory.php - - - - message: "#^pocketmine\\\\inventory\\\\EnderChestInventory\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\inventory\\\\ChestInventory\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/EnderChestInventory.php - - - - message: "#^PHPDoc type pocketmine\\\\tile\\\\Furnace of property pocketmine\\\\inventory\\\\FurnaceInventory\\:\\:\\$holder is not the same as PHPDoc type pocketmine\\\\math\\\\Vector3 of overridden property pocketmine\\\\inventory\\\\ContainerInventory\\:\\:\\$holder\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/FurnaceInventory.php - - - - message: "#^Property pocketmine\\\\inventory\\\\MultiRecipe\\:\\:\\$uuid is never read, only written\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/MultiRecipe.php - - - - message: "#^Parameter \\#2 \\$recipe of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects pocketmine\\\\inventory\\\\CraftingRecipe, pocketmine\\\\inventory\\\\CraftingRecipe\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/transaction/CraftingTransaction.php - - - - message: "#^Parameter \\#3 \\$repetitions of class pocketmine\\\\event\\\\inventory\\\\CraftItemEvent constructor expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/transaction/CraftingTransaction.php - - - - message: "#^Cannot call method count\\(\\) on pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Banner.php - - - - message: "#^Cannot call method isset\\(\\) on pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Banner.php - - - - message: "#^Cannot call method spawnToAll\\(\\) on pocketmine\\\\entity\\\\Entity\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Bow.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 2 - path: ../../../src/pocketmine/item/GoldenApple.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 4 - path: ../../../src/pocketmine/item/GoldenAppleEnchanted.php - - - - message: "#^pocketmine\\\\item\\\\GoldenAppleEnchanted\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\item\\\\GoldenApple\\.$#" - count: 1 - path: ../../../src/pocketmine/item/GoldenAppleEnchanted.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Item.php - - - - message: "#^Method pocketmine\\\\item\\\\Item\\:\\:writeCompoundTag\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Item.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/pocketmine/item/Item.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Item.php - - - - message: "#^Parameter \\#1 \\$object of function get_class expects object, pocketmine\\\\nbt\\\\tag\\\\NamedTag\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/Item.php - - - - message: "#^Parameter \\#1 \\$id of static method pocketmine\\\\item\\\\ItemFactory\\:\\:get\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/ItemFactory.php - - - - message: "#^Parameter \\#1 \\$level of static method pocketmine\\\\entity\\\\object\\\\Painting\\:\\:canFit\\(\\) expects pocketmine\\\\level\\\\Level, pocketmine\\\\level\\\\Level\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/PaintingItem.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/PoisonousPotato.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 3 - path: ../../../src/pocketmine/item/Pufferfish.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/RawChicken.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/RottenFlesh.php - - - - message: "#^Parameter \\#1 \\$effectType of class pocketmine\\\\entity\\\\EffectInstance constructor expects pocketmine\\\\entity\\\\Effect, pocketmine\\\\entity\\\\Effect\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/SpiderEye.php - - - - message: "#^Parameter \\#2 \\$pageText of method pocketmine\\\\item\\\\WritableBook\\:\\:setPageText\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: ../../../src/pocketmine/item/WritableBook.php - - - - message: "#^pocketmine\\\\item\\\\WrittenBook\\:\\:__construct\\(\\) does not call parent constructor from pocketmine\\\\item\\\\WritableBook\\.$#" - count: 1 - path: ../../../src/pocketmine/item/WrittenBook.php - - - - message: "#^Parameter \\#1 \\$id of static method pocketmine\\\\item\\\\enchantment\\\\Enchantment\\:\\:getEnchantment\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/item/enchantment/Enchantment.php - - - - message: "#^Method pocketmine\\\\item\\\\enchantment\\\\EnchantmentList\\:\\:getSlot\\(\\) should return pocketmine\\\\item\\\\enchantment\\\\EnchantmentEntry but returns pocketmine\\\\item\\\\enchantment\\\\EnchantmentEntry\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/item/enchantment/EnchantmentList.php - - - - message: "#^Parameter \\#2 \\$array of function array_map expects array, array\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/lang/BaseLang.php - - - - message: "#^Cannot call method getBlockData\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Cannot call method getBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Only numeric types are allowed in /, float\\|null given on the left side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:setBlockDataAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:setBlockIdAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:setBlockDataAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:setBlockIdAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:setBlockDataAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:setBlockIdAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Explosion.php - - - - message: "#^Cannot access offset 'data' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot access offset 'priority' on array\\{priority\\: int, data\\: pocketmine\\\\math\\\\Vector3\\}\\|int\\|pocketmine\\\\math\\\\Vector3\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot access property \\$level on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot access property \\$x on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot access property \\$y on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot access property \\$z on pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getBlockData\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getBlockLight\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getBlockSkyLight\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getHeightMap\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method getHighestBlockAt\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method isPopulated\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method recalculateHeightMapColumn\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBlockData\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBlockLight\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setBlockSkyLight\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot call method setHeightMap\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 3 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Cannot clone pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Method pocketmine\\\\level\\\\Level\\:\\:getBlockAt\\(\\) should return pocketmine\\\\block\\\\Block but returns pocketmine\\\\block\\\\Block\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Only booleans are allowed in &&, mixed given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Only booleans are allowed in \\|\\|, bool\\|null given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$chunk of method pocketmine\\\\Player\\:\\:onChunkChanged\\(\\) expects pocketmine\\\\level\\\\format\\\\Chunk, pocketmine\\\\level\\\\format\\\\Chunk\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$keys of function array_fill_keys expects array, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getFullBlock\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 4 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:updateBlockLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:updateBlockSkyLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#1 \\$x of static method pocketmine\\\\level\\\\Level\\:\\:chunkBlockHash\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$chunk of class pocketmine\\\\level\\\\generator\\\\PopulationTask constructor expects pocketmine\\\\level\\\\format\\\\Chunk, pocketmine\\\\level\\\\format\\\\Chunk\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getFullBlock\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 4 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:updateBlockLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:updateBlockSkyLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#2 \\$y of static method pocketmine\\\\level\\\\Level\\:\\:chunkBlockHash\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getFullBlock\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getFullLightAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:isInWorld\\(\\) expects int, float\\|int given\\.$#" - count: 4 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:updateBlockLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:updateBlockSkyLight\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\level\\\\Level\\:\\:blockHash\\(\\) expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#3 \\$z of static method pocketmine\\\\level\\\\Level\\:\\:chunkBlockHash\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Parameter \\#4 \\$newLevel of method pocketmine\\\\level\\\\light\\\\LightUpdate\\:\\:setAndUpdateLight\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:\\$x \\(int\\) does not accept float\\|int\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:\\$y \\(int\\) does not accept float\\|int\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UpdateBlockPacket\\:\\:\\$z \\(int\\) does not accept float\\|int\\.$#" - count: 2 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Method pocketmine\\\\level\\\\biome\\\\Biome\\:\\:getBiome\\(\\) should return pocketmine\\\\level\\\\biome\\\\Biome but returns pocketmine\\\\level\\\\biome\\\\Biome\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/biome/Biome.php - - - - message: "#^Cannot call method networkSerialize\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Method pocketmine\\\\level\\\\format\\\\Chunk\\:\\:getHeightMap\\(\\) should return int but returns int\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Method pocketmine\\\\level\\\\format\\\\Chunk\\:\\:getSubChunk\\(\\) should return pocketmine\\\\level\\\\format\\\\SubChunkInterface but returns pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Only booleans are allowed in \\|\\|, bool\\|null given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Property pocketmine\\\\level\\\\format\\\\io\\\\BaseLevelProvider\\:\\:\\$levelData \\(pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\) does not accept pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/BaseLevelProvider.php - - - - message: "#^Parameter \\#1 \\$buffer of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BatchPacket constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/ChunkRequestTask.php - - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Parameter \\#2 \\$value of class pocketmine\\\\nbt\\\\tag\\\\StringTag constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Parameter \\#2 \\$value of method LevelDB\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Cannot call method getByte\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method getByteArray\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method getInt\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method getIntArray\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method getListTag\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method hasTag\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Method pocketmine\\\\level\\\\format\\\\io\\\\region\\\\Anvil\\:\\:nbtSerialize\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Parameter \\#2 \\$list of static method pocketmine\\\\level\\\\format\\\\io\\\\region\\\\McRegion\\:\\:getCompoundList\\(\\) expects pocketmine\\\\nbt\\\\tag\\\\ListTag, pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/Anvil.php - - - - message: "#^Cannot call method getBlockDataColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getBlockIdColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getBlockLightColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getBlockSkyLightColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getByte\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getByteArray\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 6 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getInt\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getIntArray\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method getListTag\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method hasTag\\(\\) on pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\|null\\.$#" - count: 10 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method readChunk\\(\\) on pocketmine\\\\level\\\\format\\\\io\\\\region\\\\RegionLoader\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot call method writeChunk\\(\\) on pocketmine\\\\level\\\\format\\\\io\\\\region\\\\RegionLoader\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Method pocketmine\\\\level\\\\format\\\\io\\\\region\\\\McRegion\\:\\:nbtSerialize\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Parameter \\#1 \\$array of function array_filter expects array, array\\\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Parameter \\#2 \\$list of static method pocketmine\\\\level\\\\format\\\\io\\\\region\\\\McRegion\\:\\:getCompoundList\\(\\) expects pocketmine\\\\nbt\\\\tag\\\\ListTag, pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Parameter \\#2 \\$value of class pocketmine\\\\nbt\\\\tag\\\\StringTag constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/McRegion.php - - - - message: "#^Only numeric types are allowed in %%, int\\<0, max\\>\\|false given on the left side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/RegionLoader.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/RegionLoader.php - - - - message: "#^Parameter \\#2 \\$size of function ftruncate expects int\\<0, max\\>, int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/region/RegionLoader.php - - - - message: "#^Property pocketmine\\\\level\\\\generator\\\\Flat\\:\\:\\$preset \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/Flat.php - - - - message: "#^Cannot call method fastSerialize\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method getAsyncWorkerId\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method getX\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 5 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method getZ\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 5 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method hasChanged\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method isGenerated\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method populateSkyLight\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method recalculateHeightMap\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method setGenerated\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 2 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method setLightPopulated\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Cannot call method setPopulated\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Variable property access on \\$this\\(pocketmine\\\\level\\\\generator\\\\PopulationTask\\)\\.$#" - count: 4 - path: ../../../src/pocketmine/level/generator/PopulationTask.php - - - - message: "#^Method pocketmine\\\\level\\\\generator\\\\biome\\\\BiomeSelector\\:\\:pickBiome\\(\\) should return pocketmine\\\\level\\\\biome\\\\Biome but returns pocketmine\\\\level\\\\biome\\\\Biome\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/biome/BiomeSelector.php - - - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/hell/Nether.php - - - - message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/hell/Nether.php - - - - message: "#^Cannot call method setBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/level/generator/hell/Nether.php - - - - message: "#^Constructor of class pocketmine\\\\level\\\\generator\\\\hell\\\\Nether has an unused parameter \\$options\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/hell/Nether.php - - - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/normal/Normal.php - - - - message: "#^Cannot call method setBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/normal/Normal.php - - - - message: "#^Cannot call method setBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 3 - path: ../../../src/pocketmine/level/generator/normal/Normal.php - - - - message: "#^Constructor of class pocketmine\\\\level\\\\generator\\\\normal\\\\Normal has an unused parameter \\$options\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/normal/Normal.php - - - - message: "#^Offset int does not exist on array\\\\>\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/normal/Normal.php - - - - message: "#^Property pocketmine\\\\level\\\\generator\\\\object\\\\Pond\\:\\:\\$random is never read, only written\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/object/Pond.php - - - - message: "#^Only booleans are allowed in a negated boolean, bool\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/object/SpruceTree.php - - - - message: "#^Parameter \\#1 \\$start of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/generator/object/TallGrass.php - - - - message: "#^Parameter \\#2 \\$end of method pocketmine\\\\utils\\\\Random\\:\\:nextRange\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/generator/object/TallGrass.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\ChunkManager\\:\\:getBlockIdAt\\(\\) expects int, float\\|int given\\.$#" - count: 2 - path: ../../../src/pocketmine/level/generator/object/TallGrass.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\ChunkManager\\:\\:setBlockDataAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/object/TallGrass.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\ChunkManager\\:\\:setBlockIdAt\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/object/TallGrass.php - - - - message: "#^Only booleans are allowed in a negated boolean, bool\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/object/Tree.php - - - - message: "#^Cannot call method getBiomeId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/GroundCover.php - - - - message: "#^Cannot call method getBlockIdColumn\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/GroundCover.php - - - - message: "#^Cannot call method setBlock\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/GroundCover.php - - - - message: "#^Cannot call method setBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\Chunk\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/GroundCover.php - - - - message: "#^Property pocketmine\\\\level\\\\generator\\\\populator\\\\Pond\\:\\:\\$lavaOdd is never read, only written\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/Pond.php - - - - message: "#^Property pocketmine\\\\level\\\\generator\\\\populator\\\\Pond\\:\\:\\$lavaSurfaceOdd is never read, only written\\.$#" - count: 1 - path: ../../../src/pocketmine/level/generator/populator/Pond.php - - - - message: "#^Cannot call method getBlockLight\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/BlockLightUpdate.php - - - - message: "#^Cannot call method setBlockLight\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/BlockLightUpdate.php - - - - message: "#^Cannot call method getBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/LightUpdate.php - - - - message: "#^Only numeric types are allowed in \\-, int\\|null given on the right side\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/LightUpdate.php - - - - message: "#^Cannot call method getBlockSkyLight\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/SkyLightUpdate.php - - - - message: "#^Cannot call method setBlockSkyLight\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/light/SkyLightUpdate.php - - - - message: "#^Method pocketmine\\\\metadata\\\\MetadataStore\\:\\:getMetadataInternal\\(\\) should return array\\ but returns SplObjectStorage\\\\.$#" - count: 1 - path: ../../../src/pocketmine/metadata/MetadataStore.php - - - - message: "#^Parameter \\#1 \\$buffer of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BatchPacket constructor expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/CompressBatchedTask.php - - - - message: "#^Cannot access property \\$x on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Cannot access property \\$y on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Cannot access property \\$z on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:getGameRules\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$data of method pocketmine\\\\nbt\\\\NBTStream\\:\\:write\\(\\) expects array\\\\|pocketmine\\\\nbt\\\\tag\\\\NamedTag, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putString\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putBool\\(\\) expects bool, bool\\|float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putByte\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putLFloat\\(\\) expects float, bool\\|float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putLFloat\\(\\) expects float, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putLShort\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putUnsignedVarInt\\(\\) expects int, bool\\|float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putVarInt\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\utils\\\\BinaryStream\\:\\:putVarLong\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Parameter \\#1 \\$vector of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putVector3Nullable\\(\\) expects pocketmine\\\\math\\\\Vector3\\|null, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/NetworkBinaryStream.php - - - - message: "#^Cannot access offset 'down' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/RakLibInterface.php - - - - message: "#^Cannot access offset 'up' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/RakLibInterface.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/RakLibInterface.php - - - - message: "#^Parameter \\#1 \\$upload of method pocketmine\\\\network\\\\Network\\:\\:addStatistics\\(\\) expects float, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/RakLibInterface.php - - - - message: "#^Parameter \\#2 \\$download of method pocketmine\\\\network\\\\Network\\:\\:addStatistics\\(\\) expects float, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/RakLibInterface.php - - - - message: "#^Cannot access offset 'exp' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Cannot access offset 'identityPublicKey' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Cannot access offset 'nbf' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Cannot access offset 'x5u' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Cannot use array destructuring on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Offset 'chain' does not exist on array\\{chain\\?\\: array\\\\}\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Parameter \\#1 \\$string of function str_split expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Parameter \\#1 \\$string of function wordwrap expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/VerifyLoginTask.php - - - - message: "#^Argument of an invalid type array\\\\|null supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Cannot access offset string on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Method pocketmine\\\\network\\\\mcpe\\\\convert\\\\RuntimeBlockMapping\\:\\:getBedrockKnownStates\\(\\) should return array\\ but returns array\\\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Offset mixed does not exist on array\\\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Parameter \\#1 \\$buffer of class pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Parameter \\#2 \\$legacyId of static method pocketmine\\\\network\\\\mcpe\\\\convert\\\\RuntimeBlockMapping\\:\\:registerMapping\\(\\) expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/convert/RuntimeBlockMapping.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AddActorPacket\\:\\:\\$metadata \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AddActorPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AddItemActorPacket\\:\\:\\$metadata \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AddItemActorPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AddPlayerPacket\\:\\:\\$metadata \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AddPlayerPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AddVolumeEntityPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AvailableActorIdentifiersPacket\\:\\:\\$namedtag \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php - - - - message: "#^Static property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\AvailableActorIdentifiersPacket\\:\\:\\$DEFAULT_NBT_CACHE \\(string\\|null\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/AvailableActorIdentifiersPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BatchPacket\\:\\:\\$payload \\(string\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/BatchPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BiomeDefinitionListPacket\\:\\:\\$namedtag \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php - - - - message: "#^Static property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BiomeDefinitionListPacket\\:\\:\\$DEFAULT_NBT_CACHE \\(string\\|null\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/BiomeDefinitionListPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/ItemComponentPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LevelEventGenericPacket\\:\\:\\$eventData \\(string\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LevelEventGenericPacket.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Cannot access offset 'XUID' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Cannot access offset 'chain' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Cannot access offset 'displayName' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Cannot access offset 'identity' on mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Parameter \\#1 \\$token of static method pocketmine\\\\utils\\\\Utils\\:\\:decodeJWT\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$chainData \\(array\\{chain\\?\\: array\\\\}\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$clientId \\(int\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$clientUUID \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$identityPublicKey \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$locale \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$serverAddress \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$username \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\LoginPacket\\:\\:\\$xuid \\(string\\|null\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/LoginPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/PositionTrackingDBServerBroadcastPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\SetActorDataPacket\\:\\:\\$metadata \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/SetActorDataPacket.php - - - - message: "#^Parameter \\#1 \\$eid of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putEntityUniqueId\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/SetScorePacket.php - - - - message: "#^Parameter \\#1 \\$v of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putString\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/SetScorePacket.php - - - - message: "#^Parameter \\#1 \\$eid of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putEntityUniqueId\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/SetScoreboardIdentityPacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/StartGamePacket.php - - - - message: "#^Parameter \\#1 \\$str of method pocketmine\\\\utils\\\\BinaryStream\\:\\:put\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/SyncActorPropertyPacket.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\UnknownPacket\\:\\:\\$payload \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/UnknownPacket.php - - - - message: "#^Parameter \\#3 \\$resourcePatch of class pocketmine\\\\network\\\\mcpe\\\\protocol\\\\types\\\\SkinData constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/types/LegacySkinAdapter.php - - - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putBlockPosition\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php - - - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putBlockPosition\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php - - - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\network\\\\mcpe\\\\NetworkBinaryStream\\:\\:putBlockPosition\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/types/inventory/UseItemTransactionData.php - - - - message: "#^Cannot call method wakeupSleeper\\(\\) on pocketmine\\\\snooze\\\\SleeperNotifier\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/network/rcon/RCONInstance.php - - - - message: "#^Method pocketmine\\\\permission\\\\DefaultPermissions\\:\\:registerPermission\\(\\) should return pocketmine\\\\permission\\\\Permission but returns pocketmine\\\\permission\\\\Permission\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/permission/DefaultPermissions.php - - - - message: "#^Parameter \\#1 \\$value of static method pocketmine\\\\permission\\\\Permission\\:\\:getByName\\(\\) expects bool\\|string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/permission/Permission.php - - - - message: "#^Parameter \\#2 \\$description of class pocketmine\\\\permission\\\\Permission constructor expects string\\|null, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/permission/Permission.php - - - - message: "#^Variable method call on pocketmine\\\\event\\\\Listener\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/MethodEventExecutor.php - - - - message: "#^Method pocketmine\\\\plugin\\\\PluginBase\\:\\:getConfig\\(\\) should return pocketmine\\\\utils\\\\Config but returns pocketmine\\\\utils\\\\Config\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginBase.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Array \\(array\\\\) does not accept mixed\\.$#" - count: 2 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Cannot cast mixed to string\\.$#" - count: 4 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#1 \\$data of static method pocketmine\\\\permission\\\\Permission\\:\\:loadPermissions\\(\\) expects array\\\\>, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#1 \\$plugin of method pocketmine\\\\plugin\\\\PluginDescription\\:\\:loadMap\\(\\) expects array, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#1 \\$string of function mb_strtoupper expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$depend \\(array\\\\) does not accept array\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$loadBefore \\(array\\\\) does not accept array\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$main \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$name \\(string\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$order \\(int\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Property pocketmine\\\\plugin\\\\PluginDescription\\:\\:\\$softDepend \\(array\\\\) does not accept array\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginDescription.php - - - - message: "#^Parameter \\#1 \\$description of method pocketmine\\\\command\\\\Command\\:\\:setDescription\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginManager.php - - - - message: "#^Parameter \\#1 \\$permissionMessage of method pocketmine\\\\command\\\\Command\\:\\:setPermissionMessage\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginManager.php - - - - message: "#^Parameter \\#1 \\$usage of method pocketmine\\\\command\\\\Command\\:\\:setUsage\\(\\) expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/plugin/PluginManager.php - - - - message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackChunk\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getPackSize\\(\\) should return int but returns int\\<0, max\\>\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Method pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:getSha256\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#" - count: 2 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Parameter \\#2 \\$code of class pocketmine\\\\resourcepacks\\\\ResourcePackException constructor expects int, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int given\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$fileResource \\(resource\\) does not accept resource\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$sha256 \\(string\\|null\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/resourcepacks/ZippedResourcePack.php - - - - message: "#^Cannot call method handleException\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/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/pocketmine/scheduler/AsyncTask.php - - - - message: "#^Parameter \\#1 \\$data of function unserialize expects string, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/AsyncTask.php - - - - message: "#^Property pocketmine\\\\scheduler\\\\AsyncTask\\:\\:\\$result \\(bool\\|float\\|int\\|string\\|null\\) does not accept mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/AsyncTask.php - - - - message: "#^Cannot call method getAsyncWorkerId\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/DumpWorkerMemoryTask.php - - - - message: "#^Cannot call method getLogger\\(\\) on pocketmine\\\\scheduler\\\\AsyncWorker\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/DumpWorkerMemoryTask.php - - - - message: "#^Cannot call method toBinary\\(\\) on pocketmine\\\\utils\\\\UUID\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/SendUsageTask.php - - - - message: "#^Cannot call method getNextRun\\(\\) on array\\\\|int\\|pocketmine\\\\scheduler\\\\TaskHandler\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/TaskScheduler.php - - - - message: "#^Constructor of class pocketmine\\\\scheduler\\\\TaskScheduler has an unused parameter \\$logger\\.$#" - count: 1 - path: ../../../src/pocketmine/scheduler/TaskScheduler.php - - - - message: "#^Parameter \\#1 \\$tag of method pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\:\\:setTag\\(\\) expects pocketmine\\\\nbt\\\\tag\\\\NamedTag, pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null given\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Banner.php - - - - message: "#^Argument of an invalid type pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Chest.php - - - - message: "#^Cannot call method isChunkLoaded\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Chest.php + 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/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#1 \\$x of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" + message: "#^Parameter \\#1 \\$x of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" count: 1 - path: ../../../src/pocketmine/tile/Chest.php + 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/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#2 \\$y of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" + message: "#^Parameter \\#2 \\$y of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, float\\|int given\\.$#" count: 1 - path: ../../../src/pocketmine/tile/Chest.php + 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/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Parameter \\#3 \\$z of method pocketmine\\\\level\\\\Level\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" + message: "#^Parameter \\#3 \\$z of method pocketmine\\\\world\\\\World\\:\\:getTileAt\\(\\) expects int, int\\|null given\\.$#" count: 1 - path: ../../../src/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Property pocketmine\\\\tile\\\\Chest\\:\\:\\$pairX \\(int\\|null\\) does not accept float\\|int\\.$#" + message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairX \\(int\\|null\\) does not accept float\\|int\\.$#" count: 2 - path: ../../../src/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Property pocketmine\\\\tile\\\\Chest\\:\\:\\$pairZ \\(int\\|null\\) does not accept float\\|int\\.$#" + message: "#^Property pocketmine\\\\block\\\\tile\\\\Chest\\:\\:\\$pairZ \\(int\\|null\\) does not accept float\\|int\\.$#" count: 2 - path: ../../../src/pocketmine/tile/Chest.php + path: ../../../src/block/tile/Chest.php - - message: "#^Argument of an invalid type pocketmine\\\\nbt\\\\tag\\\\ListTag\\|null supplied for foreach, only iterables are supported\\.$#" + message: "#^Constant pocketmine\\\\block\\\\tile\\\\Skull\\:\\:TAG_MOUTH_MOVING is unused\\.$#" count: 1 - path: ../../../src/pocketmine/tile/Furnace.php + path: ../../../src/block/tile/Skull.php - - message: "#^Cannot call method broadcastPacketToViewers\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" + message: "#^Constant pocketmine\\\\block\\\\tile\\\\Skull\\:\\:TAG_MOUTH_TICK_COUNT is unused\\.$#" count: 1 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Cannot call method clearChunkCache\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Parameter \\#2 \\$value of class pocketmine\\\\nbt\\\\tag\\\\IntTag constructor expects int, float\\|int given\\.$#" - count: 3 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:\\$x \\(int\\) does not accept float\\|int\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:\\$y \\(int\\) does not accept float\\|int\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\protocol\\\\BlockActorDataPacket\\:\\:\\$z \\(int\\) does not accept float\\|int\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Spawnable.php - - - - message: "#^Array \\(array\\, string\\>\\) does not accept string\\|false\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Tile.php - - - - message: "#^Cannot access property \\$updateTiles on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Tile.php - - - - message: "#^Cannot call method getBlockAt\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Tile.php - - - - message: "#^Cannot call method removeTile\\(\\) on pocketmine\\\\level\\\\Level\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Tile.php + 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/pocketmine/tile/Tile.php + path: ../../../src/block/tile/Spawnable.php - - message: "#^Cannot call method getFullVersion\\(\\) on pocketmine\\\\utils\\\\VersionString\\|null\\.$#" + message: "#^Array \\(array\\, string\\>\\) does not accept string\\|false\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/block/tile/TileFactory.php - - message: "#^Offset 'date' does not exist on array\\\\|null\\.$#" + message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/command/Command.php - - message: "#^Offset 'details_url' does not exist on array\\\\|null\\.$#" + 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/pocketmine/updater/AutoUpdater.php + path: ../../../src/command/defaults/TimingsCommand.php - - message: "#^Offset 'download_url' does not exist on array\\\\|null\\.$#" + message: "#^Parameter \\#1 \\$stream of function fseek expects resource, resource\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/command/defaults/TimingsCommand.php - - message: "#^Parameter \\#1 \\$baseVersion of class pocketmine\\\\utils\\\\VersionString constructor expects string, mixed given\\.$#" + message: "#^Parameter \\#1 \\$stream of function fwrite expects resource, resource\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/command/defaults/TimingsCommand.php - - message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#" + message: "#^Parameter \\#1 \\$stream of function stream_get_contents expects resource, resource\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/command/defaults/TimingsCommand.php - - message: "#^Parameter \\#2 \\$isDevBuild of class pocketmine\\\\utils\\\\VersionString constructor expects bool, mixed given\\.$#" - count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + message: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#" + count: 2 + path: ../../../src/console/ConsoleReader.php - - message: "#^Parameter \\#2 \\$timestamp of function date expects int\\|null, mixed given\\.$#" + message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/crafting/CraftingManagerFromDataHelper.php - - message: "#^Parameter \\#3 \\$buildNumber of class pocketmine\\\\utils\\\\VersionString constructor expects int, mixed given\\.$#" + message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" count: 1 - path: ../../../src/pocketmine/updater/AutoUpdater.php + path: ../../../src/crash/CrashDump.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: "#^Property pocketmine\\\\event\\\\entity\\\\EntityShootBowEvent\\:\\:\\$projectile \\(pocketmine\\\\entity\\\\projectile\\\\Projectile\\) does not accept pocketmine\\\\entity\\\\Entity\\.$#" + count: 1 + path: ../../../src/event/entity/EntityShootBowEvent.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 \\#2 \\$array of function array_map expects array, array\\|false given\\.$#" + count: 1 + path: ../../../src/lang/Language.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: "#^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: "#^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 \\#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\\:\\:\\$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\\:\\: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\\<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 \\#1 \\$string of function strlen expects string, string\\|false given\\.$#" + count: 2 + path: ../../../src/resourcepacks/ZippedResourcePack.php + + - + message: "#^Parameter \\#2 \\$code of class pocketmine\\\\resourcepacks\\\\ResourcePackException constructor expects int, mixed given\\.$#" + 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: "#^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 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/pocketmine/utils/Config.php + path: ../../../src/utils/Config.php - message: "#^Method pocketmine\\\\utils\\\\Config\\:\\:fixYAMLIndexes\\(\\) should return string but returns string\\|null\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Config.php + 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 \\$subject of function preg_match expects string, string\\|false given\\.$#" + count: 1 + path: ../../../src/utils/Filesystem.php - message: "#^Parameter \\#2 \\$offset of function substr expects int, mixed given\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Internet.php + path: ../../../src/utils/Internet.php - message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, mixed given\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Internet.php + path: ../../../src/utils/Internet.php - - message: "#^Method pocketmine\\\\utils\\\\MainLogger\\:\\:getLogger\\(\\) should return pocketmine\\\\utils\\\\MainLogger but returns pocketmine\\\\utils\\\\MainLogger\\|null\\.$#" + message: "#^Parameter \\#1 \\$abbr of function timezone_name_from_abbr expects string, string\\|false given\\.$#" count: 1 - path: ../../../src/pocketmine/utils/MainLogger.php + 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/pocketmine/utils/Timezone.php + 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: "#^Cannot cast mixed to string\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Utils.php + path: ../../../src/utils/Utils.php - message: "#^Method pocketmine\\\\utils\\\\Utils\\:\\:printable\\(\\) should return string but returns string\\|null\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Utils.php + path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Utils\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" + message: "#^Parameter \\#1 \\$path of static method pocketmine\\\\utils\\\\Filesystem\\:\\:cleanPath\\(\\) expects string, mixed given\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Utils.php + 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/pocketmine/utils/Utils.php + path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#1 \\$enchantment of class pocketmine\\\\item\\\\enchantment\\\\EnchantmentInstance constructor expects pocketmine\\\\item\\\\enchantment\\\\Enchantment, pocketmine\\\\item\\\\enchantment\\\\Enchantment\\|null given\\.$#" + message: "#^Part \\$errno \\(mixed\\) of encapsed string cannot be cast to string\\.$#" count: 1 - path: ../../phpunit/item/ItemTest.php + path: ../../../src/utils/Utils.php - - message: "#^Cannot call method cancel\\(\\) on pocketmine\\\\scheduler\\\\TaskHandler\\|null\\.$#" + message: "#^Cannot call method getFullBlock\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" count: 1 - path: ../../plugins/TesterPlugin/src/pmmp/TesterPlugin/CheckTestCompletionTask.php + 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: "#^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 \\$keys of function array_fill_keys expects array, mixed given\\.$#" + 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/gc-hacks.neon b/tests/phpstan/configs/gc-hacks.neon index b4d9cc0cbd..37ac037f18 100644 --- a/tests/phpstan/configs/gc-hacks.neon +++ b/tests/phpstan/configs/gc-hacks.neon @@ -1,77 +1,37 @@ parameters: ignoreErrors: - - message: "#^Property pocketmine\\\\Player\\:\\:\\$craftingGrid \\(pocketmine\\\\inventory\\\\CraftingGrid\\) does not accept null\\.$#" + message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$enderInventory \\(pocketmine\\\\inventory\\\\PlayerEnderInventory\\) does not accept null\\.$#" count: 1 - path: ../../../src/pocketmine/Player.php + path: ../../../src/entity/Human.php - - message: "#^Property pocketmine\\\\Player\\:\\:\\$cursorInventory \\(pocketmine\\\\inventory\\\\PlayerCursorInventory\\) does not accept null\\.$#" + message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$hungerManager \\(pocketmine\\\\entity\\\\HungerManager\\) does not accept null\\.$#" count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Property pocketmine\\\\Player\\:\\:\\$perm \\(pocketmine\\\\permission\\\\PermissibleBase\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Property pocketmine\\\\Player\\:\\:\\$sessionAdapter \\(pocketmine\\\\network\\\\mcpe\\\\PlayerNetworkSessionAdapter\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/Player.php - - - - message: "#^Property pocketmine\\\\entity\\\\Entity\\:\\:\\$namedtag \\(pocketmine\\\\nbt\\\\tag\\\\CompoundTag\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Entity.php - - - - message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$enderChestInventory \\(pocketmine\\\\inventory\\\\EnderChestInventory\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/entity/Human.php + path: ../../../src/entity/Human.php - message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$inventory \\(pocketmine\\\\inventory\\\\PlayerInventory\\) does not accept null\\.$#" count: 1 - path: ../../../src/pocketmine/entity/Human.php + path: ../../../src/entity/Human.php + + - + message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$offHandInventory \\(pocketmine\\\\inventory\\\\PlayerOffHandInventory\\) does not accept null\\.$#" + count: 1 + path: ../../../src/entity/Human.php + + - + message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$xpManager \\(pocketmine\\\\entity\\\\ExperienceManager\\) does not accept null\\.$#" + count: 1 + path: ../../../src/entity/Human.php - message: "#^Property pocketmine\\\\entity\\\\Living\\:\\:\\$armorInventory \\(pocketmine\\\\inventory\\\\ArmorInventory\\) does not accept null\\.$#" count: 1 - path: ../../../src/pocketmine/entity/Living.php + path: ../../../src/entity/Living.php - - message: "#^Property pocketmine\\\\inventory\\\\DoubleChestInventory\\:\\:\\$left \\(pocketmine\\\\inventory\\\\ChestInventory\\) does not accept null\\.$#" + message: "#^Property pocketmine\\\\entity\\\\Living\\:\\:\\$effectManager \\(pocketmine\\\\entity\\\\effect\\\\EffectManager\\) does not accept null\\.$#" count: 1 - path: ../../../src/pocketmine/inventory/DoubleChestInventory.php - - - - message: "#^Property pocketmine\\\\inventory\\\\DoubleChestInventory\\:\\:\\$right \\(pocketmine\\\\inventory\\\\ChestInventory\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/inventory/DoubleChestInventory.php - - - - message: "#^Property pocketmine\\\\level\\\\Level\\:\\:\\$blockMetadata \\(pocketmine\\\\metadata\\\\BlockMetadataStore\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\level\\\\Level\\:\\:\\$provider \\(pocketmine\\\\level\\\\format\\\\io\\\\LevelProvider\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\level\\\\Level\\:\\:\\$temporalPosition \\(pocketmine\\\\level\\\\Position\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/Level.php - - - - message: "#^Property pocketmine\\\\tile\\\\Chest\\:\\:\\$inventory \\(pocketmine\\\\inventory\\\\ChestInventory\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Chest.php - - - - message: "#^Property pocketmine\\\\tile\\\\Furnace\\:\\:\\$inventory \\(pocketmine\\\\inventory\\\\FurnaceInventory\\) does not accept null\\.$#" - count: 1 - path: ../../../src/pocketmine/tile/Furnace.php + path: ../../../src/entity/Living.php diff --git a/tests/phpstan/configs/impossible-generics.neon b/tests/phpstan/configs/impossible-generics.neon new file mode 100644 index 0000000000..0281bcd7f7 --- /dev/null +++ b/tests/phpstan/configs/impossible-generics.neon @@ -0,0 +1,17 @@ +parameters: + ignoreErrors: + - + message: "#^Method pocketmine\\\\event\\\\RegisteredListener\\:\\:__construct\\(\\) has parameter \\$handler with no signature specified for Closure\\.$#" + count: 1 + path: ../../../src/event/RegisteredListener.php + + - + message: "#^Method pocketmine\\\\event\\\\RegisteredListener\\:\\:getHandler\\(\\) return type has no signature specified for Closure\\.$#" + count: 1 + path: ../../../src/event/RegisteredListener.php + + - + message: "#^Property pocketmine\\\\event\\\\RegisteredListener\\:\\:\\$handler type has no signature specified for Closure\\.$#" + count: 1 + path: ../../../src/event/RegisteredListener.php + diff --git a/tests/phpstan/configs/php-bugs.neon b/tests/phpstan/configs/php-bugs.neon index b0a489b342..f4754087b4 100644 --- a/tests/phpstan/configs/php-bugs.neon +++ b/tests/phpstan/configs/php-bugs.neon @@ -1,7 +1,7 @@ parameters: ignoreErrors: - - message: "#^Property pocketmine\\\\network\\\\mcpe\\\\StupidJsonDecodeTest\\:\\:\\$stupidJsonDecodeFunc \\(Closure\\(string, bool\\)\\: mixed\\) does not accept Closure\\|null\\.$#" + message: "#^Property pocketmine\\\\network\\\\mcpe\\\\handler\\\\StupidJsonDecodeTest\\:\\:\\$stupidJsonDecodeFunc \\(Closure\\(string, bool\\)\\: mixed\\) does not accept Closure\\|null\\.$#" count: 1 - path: ../../phpunit/network/mcpe/StupidJsonDecodeTest.php + path: ../../phpunit/network/mcpe/handler/StupidJsonDecodeTest.php diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index cc4842e75e..0d98c36771 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -1,97 +1,47 @@ parameters: ignoreErrors: - - message: "#^Cannot access offset 'base_version' on mixed\\.$#" + message: "#^Instanceof between pocketmine\\\\block\\\\utils\\\\BannerPatternLayer and pocketmine\\\\block\\\\utils\\\\BannerPatternLayer will always evaluate to true\\.$#" count: 1 - path: ../../../src/pocketmine/CrashDump.php + path: ../../../src/block/BaseBanner.php - - message: "#^Cannot access offset 'build' on mixed\\.$#" + message: "#^Method pocketmine\\\\crafting\\\\CraftingManager\\:\\:getDestructorCallbacks\\(\\) should return pocketmine\\\\utils\\\\ObjectSet\\ but returns pocketmine\\\\utils\\\\ObjectSet\\\\|pocketmine\\\\utils\\\\ObjectSet\\\\.$#" count: 1 - path: ../../../src/pocketmine/CrashDump.php + path: ../../../src/crafting/CraftingManager.php - - message: "#^Cannot access offset 'composer_libraries' on mixed\\.$#" + message: "#^Property pocketmine\\\\crafting\\\\CraftingManager\\:\\:\\$destructorCallbacks \\(pocketmine\\\\utils\\\\ObjectSet\\\\|null\\) does not accept pocketmine\\\\utils\\\\ObjectSet\\\\.$#" count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'git' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'is_dev' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'os' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'php' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'php_os' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'protocol' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'uname' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Cannot access offset 'zend' on mixed\\.$#" - count: 1 - path: ../../../src/pocketmine/CrashDump.php - - - - message: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#" - count: 2 - path: ../../../src/pocketmine/command/CommandReader.php + path: ../../../src/crafting/CraftingManager.php - message: "#^Call to function assert\\(\\) with false and 'unknown hit type' will always evaluate to false\\.$#" count: 1 - path: ../../../src/pocketmine/entity/projectile/Projectile.php + path: ../../../src/entity/projectile/Projectile.php - - message: "#^Dead catch \\- ReflectionException is never thrown in the try block\\.$#" + message: "#^Dead catch \\- RuntimeException is never thrown in the try block\\.$#" count: 1 - path: ../../../src/pocketmine/level/format/io/LevelProviderManager.php + path: ../../../src/plugin/PluginManager.php - - message: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#" - count: 2 - path: ../../../src/pocketmine/level/format/io/region/RegionLoader.php - - - - message: "#^Call to function method_exists\\(\\) with pocketmine\\\\network\\\\mcpe\\\\CachedEncapsulatedPacket and '__toString' will always evaluate to true\\.$#" + message: "#^Parameter \\#1 \\$closure of static method pocketmine\\\\utils\\\\Utils\\:\\:getNiceClosureName\\(\\) expects Closure\\(\\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*, \\*NEVER\\*\\)\\: mixed, Closure\\(TEvent of pocketmine\\\\event\\\\Event\\)\\: void given\\.$#" count: 1 - path: ../../../src/pocketmine/network/mcpe/protocol/DataPacket.php + path: ../../../src/plugin/PluginManager.php - message: "#^Parameter \\#1 \\$yamlString of class pocketmine\\\\plugin\\\\PluginDescription constructor expects array\\|string, array\\ given\\.$#" count: 1 - path: ../../../src/pocketmine/plugin/ScriptPluginLoader.php - - - - message: "#^Dead catch \\- ReflectionException is never thrown in the try block\\.$#" - count: 2 - path: ../../../src/pocketmine/utils/Utils.php + path: ../../../src/plugin/ScriptPluginLoader.php - message: "#^Strict comparison using \\=\\=\\= between string and false will always evaluate to false\\.$#" count: 1 - path: ../../../src/pocketmine/utils/Utils.php + path: ../../../src/utils/Utils.php + + - + message: "#^Call to function is_resource\\(\\) with resource will always evaluate to true\\.$#" + count: 2 + path: ../../../src/world/format/io/region/RegionLoader.php diff --git a/tests/phpstan/configs/phpunit-wiring-tests.neon b/tests/phpstan/configs/phpunit-wiring-tests.neon deleted file mode 100644 index 09f9430260..0000000000 --- a/tests/phpstan/configs/phpunit-wiring-tests.neon +++ /dev/null @@ -1,7 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Parameter \\#1 \\$class of static method pocketmine\\\\level\\\\format\\\\io\\\\LevelProviderManager\\:\\:addProvider\\(\\) expects class\\-string\\, string given\\.$#" - count: 2 - path: ../../phpunit/level/format/io/LevelProviderManagerTest.php - diff --git a/tests/phpstan/configs/runtime-type-checks.neon b/tests/phpstan/configs/runtime-type-checks.neon index 88459daaf6..fb1e456b78 100644 --- a/tests/phpstan/configs/runtime-type-checks.neon +++ b/tests/phpstan/configs/runtime-type-checks.neon @@ -3,10 +3,10 @@ parameters: - message: "#^Casting to int something that's already int\\.$#" count: 2 - path: ../../../src/pocketmine/item/Item.php + path: ../../../src/item/Item.php - - message: "#^Call to function is_array\\(\\) with array\\ will always evaluate to true\\.$#" + message: "#^Instanceof between pocketmine\\\\nbt\\\\tag\\\\CompoundTag and pocketmine\\\\nbt\\\\tag\\\\CompoundTag will always evaluate to true\\.$#" count: 1 - path: ../../../src/pocketmine/plugin/PluginManager.php + path: ../../../src/network/mcpe/handler/InGamePacketHandler.php diff --git a/tests/phpstan/configs/spl-fixed-array-sucks.neon b/tests/phpstan/configs/spl-fixed-array-sucks.neon index 39da88bdc9..daa6361dd0 100644 --- a/tests/phpstan/configs/spl-fixed-array-sucks.neon +++ b/tests/phpstan/configs/spl-fixed-array-sucks.neon @@ -1,72 +1,22 @@ parameters: ignoreErrors: - - message: "#^Cannot call method getBlockData\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" + message: "#^Cannot call method collectGarbage\\(\\) on pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\.$#" count: 1 - path: ../../../src/pocketmine/level/Level.php + path: ../../../src/world/format/Chunk.php - - message: "#^Cannot call method getBlockId\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" + message: "#^Method pocketmine\\\\world\\\\format\\\\Chunk\\:\\:getSubChunks\\(\\) should return array\\ but returns array\\\\.$#" count: 1 - path: ../../../src/pocketmine/level/Level.php + path: ../../../src/world/format/Chunk.php - - message: "#^Cannot call method getBlockDataArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(pocketmine\\\\world\\\\format\\\\SubChunk\\|null\\)\\: mixed\\)\\|null, Closure\\(pocketmine\\\\world\\\\format\\\\SubChunk\\)\\: pocketmine\\\\world\\\\format\\\\SubChunk given\\.$#" count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php + path: ../../../src/world/format/Chunk.php - - message: "#^Cannot call method getBlockDataColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" + message: "#^Method pocketmine\\\\world\\\\format\\\\HeightArray\\:\\:getValues\\(\\) should return array\\ but returns array\\\\.$#" count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockIdArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockIdColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockLightArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockLightColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockSkyLightArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockSkyLightColumn\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Method pocketmine\\\\level\\\\format\\\\Chunk\\:\\:getHeightMapArray\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/Chunk.php - - - - message: "#^Cannot call method getBlockDataArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Cannot call method getBlockIdArray\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php - - - - message: "#^Cannot call method isEmpty\\(\\) on pocketmine\\\\level\\\\format\\\\SubChunkInterface\\|null\\.$#" - count: 1 - path: ../../../src/pocketmine/level/format/io/leveldb/LevelDB.php + path: ../../../src/world/format/HeightArray.php diff --git a/tests/phpstan/rules/DisallowEnumComparisonRule.php b/tests/phpstan/rules/DisallowEnumComparisonRule.php new file mode 100644 index 0000000000..726c4cfbfe --- /dev/null +++ b/tests/phpstan/rules/DisallowEnumComparisonRule.php @@ -0,0 +1,90 @@ + + */ +class DisallowEnumComparisonRule implements Rule{ + + public function getNodeType() : string{ + return BinaryOp::class; + } + + public function processNode(Node $node, Scope $scope) : array{ + if(!($node instanceof Identical) and !($node instanceof NotIdentical)){ + return []; + } + + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + $leftEnum = $this->checkForEnumTypes($leftType); + $rightEnum = $this->checkForEnumTypes($rightType); + if($leftEnum && $rightEnum){ + return [RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s involving enum types %s and %s is not reliable.', + $node instanceof Identical ? '===' : '!==', + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()) + ))->build()]; + } + return []; + } + + private function checkForEnumTypes(Type $comparedType) : bool{ + //TODO: what we really want to do here is iterate over the contained types, but there's no universal way to + //do that. This might break with other circumstances. + if($comparedType instanceof ObjectType){ + $types = [$comparedType]; + }elseif($comparedType instanceof UnionType){ + $types = $comparedType->getTypes(); + }else{ + return false; + } + foreach($types as $containedType){ + if(!($containedType instanceof ObjectType)){ + continue; + } + $class = $containedType->getClassReflection(); + if($class !== null and $class->hasTraitUse(EnumTrait::class)){ + return true; + } + } + return false; + } +} diff --git a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php b/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php new file mode 100644 index 0000000000..639a5b469d --- /dev/null +++ b/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php @@ -0,0 +1,90 @@ + + */ +final class UnsafeForeachArrayOfStringRule implements Rule{ + + public function getNodeType() : string{ + return Foreach_::class; + } + + public function processNode(Node $node, Scope $scope) : array{ + /** @var Foreach_ $node */ + if($node->keyVar === null){ + return []; + } + $iterableType = $scope->getType($node->expr); + + if($iterableType->isArray()->no()){ + return []; + } + if($iterableType->isIterableAtLeastOnce()->no()){ + return []; + } + + $hasCastableKeyTypes = false; + $expectsIntKeyTypes = false; + TypeTraverser::map($iterableType->getIterableKeyType(), function(Type $type, callable $traverse) use (&$hasCastableKeyTypes, &$expectsIntKeyTypes) : Type{ + if($type instanceof IntegerType){ + $expectsIntKeyTypes = true; + return $type; + } + if(!$type instanceof StringType){ + return $traverse($type); + } + if($type->isNumericString()->no() || $type instanceof ClassStringType){ + //class-string cannot be numeric, even if PHPStan thinks they can be + return $type; + } + $hasCastableKeyTypes = true; + return $type; + }); + if($hasCastableKeyTypes && !$expectsIntKeyTypes){ + return [ + RuleErrorBuilder::message(sprintf( + "Unsafe foreach on array with key type %s (they might be casted to int).", + $iterableType->getIterableKeyType()->describe(VerbosityLevel::value()) + ))->tip("Use Utils::foreachWithStringKeys() for a safe Generator-based iterator.")->build() + ]; + } + return []; + } + +} \ No newline at end of file diff --git a/tests/phpstan/stubs/JsonMapper.stub b/tests/phpstan/stubs/JsonMapper.stub new file mode 100644 index 0000000000..e597a35ced --- /dev/null +++ b/tests/phpstan/stubs/JsonMapper.stub @@ -0,0 +1,19 @@ + $color_array - * - * @return string - */ - public static function convertBiomeColors($color_array){} -} diff --git a/tests/phpstan/stubs/phpasn1.stub b/tests/phpstan/stubs/phpasn1.stub new file mode 100644 index 0000000000..b459289efb --- /dev/null +++ b/tests/phpstan/stubs/phpasn1.stub @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FG\ASN1\Universal; + +class Integer +{ + /** + * @param int|string $value + */ + public function __construct($value){} + + /** @return int|string */ + public function getContent(){} +} diff --git a/tests/phpunit/block/BlockTest.php b/tests/phpunit/block/BlockTest.php index 417e461774..8e101bc0f5 100644 --- a/tests/phpunit/block/BlockTest.php +++ b/tests/phpunit/block/BlockTest.php @@ -24,29 +24,35 @@ declare(strict_types=1); namespace pocketmine\block; use PHPUnit\Framework\TestCase; +use function file_get_contents; +use function is_array; +use function json_decode; class BlockTest extends TestCase{ + /** @var BlockFactory */ + private $blockFactory; + public function setUp() : void{ - BlockFactory::init(); + $this->blockFactory = new BlockFactory(); } /** * Test registering a block which would overwrite another block, without forcing it */ public function testAccidentalOverrideBlock() : void{ - $block = new MyCustomBlock(); - $this->expectException(\RuntimeException::class); - BlockFactory::registerBlock($block); + $block = new MyCustomBlock(new BlockIdentifier(BlockLegacyIds::COBBLESTONE, 0), "Cobblestone", BlockBreakInfo::instant()); + $this->expectException(\InvalidArgumentException::class); + $this->blockFactory->register($block); } /** * Test registering a block deliberately overwriting another block works as expected */ public function testDeliberateOverrideBlock() : void{ - $block = new MyCustomBlock(); - BlockFactory::registerBlock($block, true); - self::assertInstanceOf(MyCustomBlock::class, BlockFactory::get($block->getId())); + $block = new MyCustomBlock(new BlockIdentifier(BlockLegacyIds::COBBLESTONE, 0), "Cobblestone", BlockBreakInfo::instant()); + $this->blockFactory->register($block, true); + self::assertInstanceOf(MyCustomBlock::class, $this->blockFactory->get($block->getId(), 0)); } /** @@ -54,10 +60,10 @@ class BlockTest extends TestCase{ */ public function testRegisterNewBlock() : void{ for($i = 0; $i < 256; ++$i){ - if(!BlockFactory::isRegistered($i)){ - $b = new StrangeNewBlock($i); - BlockFactory::registerBlock($b); - self::assertInstanceOf(StrangeNewBlock::class, BlockFactory::get($b->getId())); + if(!$this->blockFactory->isRegistered($i)){ + $b = new StrangeNewBlock(new BlockIdentifier($i, 0), "Strange New Block", BlockBreakInfo::instant()); + $this->blockFactory->register($b); + self::assertInstanceOf(StrangeNewBlock::class, $this->blockFactory->get($b->getId(), 0)); return; } } @@ -70,7 +76,7 @@ class BlockTest extends TestCase{ */ public function testRegisterIdTooLarge() : void{ self::expectException(\RuntimeException::class); - BlockFactory::registerBlock(new OutOfBoundsBlock(25555)); + $this->blockFactory->register(new OutOfBoundsBlock(new BlockIdentifier(25555, 0), "Out Of Bounds Block", BlockBreakInfo::instant())); } /** @@ -78,7 +84,7 @@ class BlockTest extends TestCase{ */ public function testRegisterIdTooSmall() : void{ self::expectException(\RuntimeException::class); - BlockFactory::registerBlock(new OutOfBoundsBlock(-1)); + $this->blockFactory->register(new OutOfBoundsBlock(new BlockIdentifier(-1, 0), "Out Of Bounds Block", BlockBreakInfo::instant())); } /** @@ -88,8 +94,8 @@ class BlockTest extends TestCase{ */ public function testBlockFactoryClone() : void{ for($i = 0; $i < 256; ++$i){ - $b1 = BlockFactory::get($i); - $b2 = BlockFactory::get($i); + $b1 = $this->blockFactory->get($i, 0); + $b2 = $this->blockFactory->get($i, 0); self::assertNotSame($b1, $b2); } } @@ -100,11 +106,11 @@ class BlockTest extends TestCase{ */ public function blockGetProvider() : array{ return [ - [Block::STONE, Stone::ANDESITE], - [Block::STONE, 15], - [Block::GOLD_BLOCK, 5], - [Block::WOODEN_PLANKS, Planks::DARK_OAK], - [Block::SAND, 0] + [BlockLegacyIds::STONE, 5], + [BlockLegacyIds::GOLD_BLOCK, 0], + [BlockLegacyIds::WOODEN_PLANKS, 5], + [BlockLegacyIds::SAND, 0], + [BlockLegacyIds::GOLD_BLOCK, 0] ]; } @@ -112,19 +118,16 @@ class BlockTest extends TestCase{ * @dataProvider blockGetProvider */ public function testBlockGet(int $id, int $meta) : void{ - $block = BlockFactory::get($id, $meta); + $block = $this->blockFactory->get($id, $meta); self::assertEquals($id, $block->getId()); - self::assertEquals($meta, $block->getDamage()); + self::assertEquals($meta, $block->getMeta()); } - /** - * Test that all blocks have correctly set names - */ - public function testBlockNames() : void{ - for($id = 0; $id < 256; ++$id){ - $b = BlockFactory::get($id); - self::assertTrue($b instanceof UnknownBlock or $b->getName() !== "Unknown", "Block with ID $id does not have a valid name"); + public function testBlockIds() : void{ + for($i = 0; $i < 256; ++$i){ + $b = $this->blockFactory->get($i, 0); + self::assertContains($i, $b->getIdInfo()->getAllBlockIds()); } } @@ -133,10 +136,38 @@ class BlockTest extends TestCase{ * (like freezes) when doing light population. */ public function testLightFiltersValid() : void{ - foreach(BlockFactory::$lightFilter as $id => $value){ + foreach($this->blockFactory->lightFilter as $id => $value){ self::assertNotNull($value, "Light filter value missing for $id"); self::assertLessThanOrEqual(15, $value, "Light filter value for $id is larger than the expected 15"); self::assertGreaterThan(0, $value, "Light filter value for $id must be larger than 0"); } } + + public function testConsistency() : void{ + $list = json_decode(file_get_contents(__DIR__ . '/block_factory_consistency_check.json'), true); + if(!is_array($list)){ + throw new \pocketmine\utils\AssumptionFailedError("Old table should be array{knownStates: array, remaps: array}"); + } + $knownStates = $list["knownStates"]; + $remaps = $list["remaps"]; + + $states = $this->blockFactory->getAllKnownStates(); + foreach($states as $k => $state){ + if($state->getFullId() !== $k){ + self::assertArrayHasKey($k, $remaps, "New remap of state $k (" . $state->getName() . ") - consistency check may need regenerating"); + self::assertSame($state->getFullId(), $remaps[$k], "Mismatched full IDs of remapped state $k"); + }else{ + self::assertArrayHasKey($k, $knownStates, "New block state $k (" . $state->getName() . ") - consistency check may need regenerating"); + self::assertSame($knownStates[$k], $state->getName()); + } + } + foreach($knownStates as $k => $name){ + self::assertArrayHasKey($k, $states, "Missing previously-known block state $k ($name)"); + self::assertSame($name, $states[$k]->getName()); + } + foreach($remaps as $origin => $destination){ + self::assertArrayHasKey($origin, $states, "Missing previously-remapped state $origin"); + self::assertSame($destination, $states[$origin]->getFullId()); + } + } } diff --git a/tests/phpunit/block/BrewingStandTest.php b/tests/phpunit/block/BrewingStandTest.php new file mode 100644 index 0000000000..9f4a8d5e3d --- /dev/null +++ b/tests/phpunit/block/BrewingStandTest.php @@ -0,0 +1,104 @@ +}, void, void> + */ + public function slotsProvider() : \Generator{ + yield [BrewingStandSlot::getAll()]; + yield [[BrewingStandSlot::EAST()]]; + yield [[BrewingStandSlot::EAST(), BrewingStandSlot::NORTHWEST()]]; + } + + /** + * @dataProvider slotsProvider + * + * @param BrewingStandSlot[] $slots + * @phpstan-param list $slots + */ + public function testHasAndSetSlot(array $slots) : void{ + $block = VanillaBlocks::BREWING_STAND(); + foreach($slots as $slot){ + $block->setSlot($slot, true); + } + foreach($slots as $slot){ + self::assertTrue($block->hasSlot($slot)); + } + + foreach($slots as $slot){ + $block->setSlot($slot, false); + } + foreach($slots as $slot){ + self::assertFalse($block->hasSlot($slot)); + } + } + + /** + * @dataProvider slotsProvider + * + * @param BrewingStandSlot[] $slots + * @phpstan-param list $slots + */ + public function testGetSlots(array $slots) : void{ + $block = VanillaBlocks::BREWING_STAND(); + + foreach($slots as $slot){ + $block->setSlot($slot, true); + } + + self::assertCount(count($slots), $block->getSlots()); + + foreach($slots as $slot){ + $block->setSlot($slot, false); + } + self::assertCount(0, $block->getSlots()); + } + + /** + * @dataProvider slotsProvider + * + * @param BrewingStandSlot[] $slots + * @phpstan-param list $slots + */ + public function testSetSlots(array $slots) : void{ + $block = VanillaBlocks::BREWING_STAND(); + + $block->setSlots($slots); + foreach($slots as $slot){ + self::assertTrue($block->hasSlot($slot)); + } + $block->setSlots([]); + self::assertCount(0, $block->getSlots()); + foreach($slots as $slot){ + self::assertFalse($block->hasSlot($slot)); + } + } +} diff --git a/tests/phpunit/block/MyCustomBlock.php b/tests/phpunit/block/MyCustomBlock.php index 58bb1a6eb7..d0df0261d4 100644 --- a/tests/phpunit/block/MyCustomBlock.php +++ b/tests/phpunit/block/MyCustomBlock.php @@ -23,9 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -class MyCustomBlock extends Cobblestone{ +class MyCustomBlock extends Opaque{ - public function getName() : string{ - return "MyCobblestone"; - } } diff --git a/tests/phpunit/block/StrangeNewBlock.php b/tests/phpunit/block/StrangeNewBlock.php index 1e78a40928..c505184d08 100644 --- a/tests/phpunit/block/StrangeNewBlock.php +++ b/tests/phpunit/block/StrangeNewBlock.php @@ -23,8 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -class StrangeNewBlock extends Solid{ - public function getName() : string{ - return "Strange New Block"; - } +class StrangeNewBlock extends Opaque{ + } diff --git a/tests/phpunit/block/block_factory_consistency_check.json b/tests/phpunit/block/block_factory_consistency_check.json new file mode 100644 index 0000000000..932d526a4c --- /dev/null +++ b/tests/phpunit/block/block_factory_consistency_check.json @@ -0,0 +1 @@ +{"knownStates":{"0":"Air","16":"Stone","17":"Granite","18":"Polished Granite","19":"Diorite","20":"Polished Diorite","21":"Andesite","22":"Polished Andesite","32":"Grass","48":"Dirt","49":"Dirt","64":"Cobblestone","80":"Oak Planks","81":"Spruce Planks","82":"Birch Planks","83":"Jungle Planks","84":"Acacia Planks","85":"Dark Oak Planks","96":"Oak Sapling","97":"Spruce Sapling","98":"Birch Sapling","99":"Jungle Sapling","100":"Acacia Sapling","101":"Dark Oak Sapling","104":"Oak Sapling","105":"Spruce Sapling","106":"Birch Sapling","107":"Jungle Sapling","108":"Acacia Sapling","109":"Dark Oak Sapling","112":"Bedrock","113":"Bedrock","128":"Water","129":"Water","130":"Water","131":"Water","132":"Water","133":"Water","134":"Water","135":"Water","136":"Water","137":"Water","138":"Water","139":"Water","140":"Water","141":"Water","142":"Water","143":"Water","144":"Water","145":"Water","146":"Water","147":"Water","148":"Water","149":"Water","150":"Water","151":"Water","152":"Water","153":"Water","154":"Water","155":"Water","156":"Water","157":"Water","158":"Water","159":"Water","160":"Lava","161":"Lava","162":"Lava","163":"Lava","164":"Lava","165":"Lava","166":"Lava","167":"Lava","168":"Lava","169":"Lava","170":"Lava","171":"Lava","172":"Lava","173":"Lava","174":"Lava","175":"Lava","176":"Lava","177":"Lava","178":"Lava","179":"Lava","180":"Lava","181":"Lava","182":"Lava","183":"Lava","184":"Lava","185":"Lava","186":"Lava","187":"Lava","188":"Lava","189":"Lava","190":"Lava","191":"Lava","192":"Sand","193":"Red Sand","208":"Gravel","224":"Gold Ore","240":"Iron Ore","256":"Coal Ore","272":"Oak Log","273":"Spruce Log","274":"Birch Log","275":"Jungle Log","276":"Oak Log","277":"Spruce Log","278":"Birch Log","279":"Jungle Log","280":"Oak Log","281":"Spruce Log","282":"Birch Log","283":"Jungle Log","288":"Oak Leaves","289":"Spruce Leaves","290":"Birch Leaves","291":"Jungle Leaves","292":"Oak Leaves","293":"Spruce Leaves","294":"Birch Leaves","295":"Jungle Leaves","296":"Oak Leaves","297":"Spruce Leaves","298":"Birch Leaves","299":"Jungle Leaves","300":"Oak Leaves","301":"Spruce Leaves","302":"Birch Leaves","303":"Jungle Leaves","304":"Sponge","305":"Sponge","320":"Glass","336":"Lapis Lazuli Ore","352":"Lapis Lazuli Block","384":"Sandstone","385":"Chiseled Sandstone","386":"Cut Sandstone","387":"Smooth Sandstone","400":"Note Block","416":"Bed Block","417":"Bed Block","418":"Bed Block","419":"Bed Block","420":"Bed Block","421":"Bed Block","422":"Bed Block","423":"Bed Block","424":"Bed Block","425":"Bed Block","426":"Bed Block","427":"Bed Block","428":"Bed Block","429":"Bed Block","430":"Bed Block","431":"Bed Block","432":"Powered Rail","433":"Powered Rail","434":"Powered Rail","435":"Powered Rail","436":"Powered Rail","437":"Powered Rail","440":"Powered Rail","441":"Powered Rail","442":"Powered Rail","443":"Powered Rail","444":"Powered Rail","445":"Powered Rail","448":"Detector Rail","449":"Detector Rail","450":"Detector Rail","451":"Detector Rail","452":"Detector Rail","453":"Detector Rail","456":"Detector Rail","457":"Detector Rail","458":"Detector Rail","459":"Detector Rail","460":"Detector Rail","461":"Detector Rail","480":"Cobweb","497":"Tall Grass","498":"Fern","512":"Dead Bush","560":"Wool","561":"Wool","562":"Wool","563":"Wool","564":"Wool","565":"Wool","566":"Wool","567":"Wool","568":"Wool","569":"Wool","570":"Wool","571":"Wool","572":"Wool","573":"Wool","574":"Wool","575":"Wool","576":"???","592":"Dandelion","608":"Poppy","609":"Blue Orchid","610":"Allium","611":"Azure Bluet","612":"Red Tulip","613":"Orange Tulip","614":"White Tulip","615":"Pink Tulip","616":"Oxeye Daisy","617":"Cornflower","618":"Lily of the Valley","624":"Brown Mushroom","640":"Red Mushroom","656":"Gold Block","672":"Iron Block","688":"Smooth Stone Slab","689":"Sandstone Slab","690":"Fake Wooden Slab","691":"Cobblestone Slab","692":"Brick Slab","693":"Stone Brick Slab","694":"Quartz Slab","695":"Nether Brick Slab","704":"Smooth Stone Slab","705":"Sandstone Slab","706":"Fake Wooden Slab","707":"Cobblestone Slab","708":"Brick Slab","709":"Stone Brick Slab","710":"Quartz Slab","711":"Nether Brick Slab","712":"Smooth Stone Slab","713":"Sandstone Slab","714":"Fake Wooden Slab","715":"Cobblestone Slab","716":"Brick Slab","717":"Stone Brick Slab","718":"Quartz Slab","719":"Nether Brick Slab","720":"Bricks","736":"TNT","737":"TNT","738":"TNT","739":"TNT","752":"Bookshelf","768":"Mossy Cobblestone","784":"Obsidian","801":"Torch","802":"Torch","803":"Torch","804":"Torch","805":"Torch","816":"Fire Block","817":"Fire Block","818":"Fire Block","819":"Fire Block","820":"Fire Block","821":"Fire Block","822":"Fire Block","823":"Fire Block","824":"Fire Block","825":"Fire Block","826":"Fire Block","827":"Fire Block","828":"Fire Block","829":"Fire Block","830":"Fire Block","831":"Fire Block","832":"Monster Spawner","848":"Oak Stairs","849":"Oak Stairs","850":"Oak Stairs","851":"Oak Stairs","852":"Oak Stairs","853":"Oak Stairs","854":"Oak Stairs","855":"Oak Stairs","866":"Chest","867":"Chest","868":"Chest","869":"Chest","880":"Redstone","881":"Redstone","882":"Redstone","883":"Redstone","884":"Redstone","885":"Redstone","886":"Redstone","887":"Redstone","888":"Redstone","889":"Redstone","890":"Redstone","891":"Redstone","892":"Redstone","893":"Redstone","894":"Redstone","895":"Redstone","896":"Diamond Ore","912":"Diamond Block","928":"Crafting Table","944":"Wheat Block","945":"Wheat Block","946":"Wheat Block","947":"Wheat Block","948":"Wheat Block","949":"Wheat Block","950":"Wheat Block","951":"Wheat Block","960":"Farmland","961":"Farmland","962":"Farmland","963":"Farmland","964":"Farmland","965":"Farmland","966":"Farmland","967":"Farmland","978":"Furnace","979":"Furnace","980":"Furnace","981":"Furnace","994":"Furnace","995":"Furnace","996":"Furnace","997":"Furnace","1008":"Oak Sign","1009":"Oak Sign","1010":"Oak Sign","1011":"Oak Sign","1012":"Oak Sign","1013":"Oak Sign","1014":"Oak Sign","1015":"Oak Sign","1016":"Oak Sign","1017":"Oak Sign","1018":"Oak Sign","1019":"Oak Sign","1020":"Oak Sign","1021":"Oak Sign","1022":"Oak Sign","1023":"Oak Sign","1024":"Oak Door","1025":"Oak Door","1026":"Oak Door","1027":"Oak Door","1028":"Oak Door","1029":"Oak Door","1030":"Oak Door","1031":"Oak Door","1032":"Oak Door","1033":"Oak Door","1034":"Oak Door","1035":"Oak Door","1042":"Ladder","1043":"Ladder","1044":"Ladder","1045":"Ladder","1056":"Rail","1057":"Rail","1058":"Rail","1059":"Rail","1060":"Rail","1061":"Rail","1062":"Rail","1063":"Rail","1064":"Rail","1065":"Rail","1072":"Cobblestone Stairs","1073":"Cobblestone Stairs","1074":"Cobblestone Stairs","1075":"Cobblestone Stairs","1076":"Cobblestone Stairs","1077":"Cobblestone Stairs","1078":"Cobblestone Stairs","1079":"Cobblestone Stairs","1090":"Oak Wall Sign","1091":"Oak Wall Sign","1092":"Oak Wall Sign","1093":"Oak Wall Sign","1104":"Lever","1105":"Lever","1106":"Lever","1107":"Lever","1108":"Lever","1109":"Lever","1110":"Lever","1111":"Lever","1112":"Lever","1113":"Lever","1114":"Lever","1115":"Lever","1116":"Lever","1117":"Lever","1118":"Lever","1119":"Lever","1120":"Stone Pressure Plate","1121":"Stone Pressure Plate","1136":"Iron Door","1137":"Iron Door","1138":"Iron Door","1139":"Iron Door","1140":"Iron Door","1141":"Iron Door","1142":"Iron Door","1143":"Iron Door","1144":"Iron Door","1145":"Iron Door","1146":"Iron Door","1147":"Iron Door","1152":"Oak Pressure Plate","1153":"Oak Pressure Plate","1168":"Redstone Ore","1184":"Redstone Ore","1201":"Redstone Torch","1202":"Redstone Torch","1203":"Redstone Torch","1204":"Redstone Torch","1205":"Redstone Torch","1217":"Redstone Torch","1218":"Redstone Torch","1219":"Redstone Torch","1220":"Redstone Torch","1221":"Redstone Torch","1232":"Stone Button","1233":"Stone Button","1234":"Stone Button","1235":"Stone Button","1236":"Stone Button","1237":"Stone Button","1240":"Stone Button","1241":"Stone Button","1242":"Stone Button","1243":"Stone Button","1244":"Stone Button","1245":"Stone Button","1248":"Snow Layer","1249":"Snow Layer","1250":"Snow Layer","1251":"Snow Layer","1252":"Snow Layer","1253":"Snow Layer","1254":"Snow Layer","1255":"Snow Layer","1264":"Ice","1280":"Snow Block","1296":"Cactus","1297":"Cactus","1298":"Cactus","1299":"Cactus","1300":"Cactus","1301":"Cactus","1302":"Cactus","1303":"Cactus","1304":"Cactus","1305":"Cactus","1306":"Cactus","1307":"Cactus","1308":"Cactus","1309":"Cactus","1310":"Cactus","1311":"Cactus","1312":"Clay Block","1328":"Sugarcane","1329":"Sugarcane","1330":"Sugarcane","1331":"Sugarcane","1332":"Sugarcane","1333":"Sugarcane","1334":"Sugarcane","1335":"Sugarcane","1336":"Sugarcane","1337":"Sugarcane","1338":"Sugarcane","1339":"Sugarcane","1340":"Sugarcane","1341":"Sugarcane","1342":"Sugarcane","1343":"Sugarcane","1344":"Jukebox","1360":"Oak Fence","1361":"Spruce Fence","1362":"Birch Fence","1363":"Jungle Fence","1364":"Acacia Fence","1365":"Dark Oak Fence","1376":"Pumpkin","1392":"Netherrack","1408":"Soul Sand","1424":"Glowstone","1441":"Nether Portal","1442":"Nether Portal","1456":"Jack o'Lantern","1457":"Jack o'Lantern","1458":"Jack o'Lantern","1459":"Jack o'Lantern","1472":"Cake","1473":"Cake","1474":"Cake","1475":"Cake","1476":"Cake","1477":"Cake","1478":"Cake","1488":"Redstone Repeater","1489":"Redstone Repeater","1490":"Redstone Repeater","1491":"Redstone Repeater","1492":"Redstone Repeater","1493":"Redstone Repeater","1494":"Redstone Repeater","1495":"Redstone Repeater","1496":"Redstone Repeater","1497":"Redstone Repeater","1498":"Redstone Repeater","1499":"Redstone Repeater","1500":"Redstone Repeater","1501":"Redstone Repeater","1502":"Redstone Repeater","1503":"Redstone Repeater","1504":"Redstone Repeater","1505":"Redstone Repeater","1506":"Redstone Repeater","1507":"Redstone Repeater","1508":"Redstone Repeater","1509":"Redstone Repeater","1510":"Redstone Repeater","1511":"Redstone Repeater","1512":"Redstone Repeater","1513":"Redstone Repeater","1514":"Redstone Repeater","1515":"Redstone Repeater","1516":"Redstone Repeater","1517":"Redstone Repeater","1518":"Redstone Repeater","1519":"Redstone Repeater","1520":"Invisible Bedrock","1536":"Oak Trapdoor","1537":"Oak Trapdoor","1538":"Oak Trapdoor","1539":"Oak Trapdoor","1540":"Oak Trapdoor","1541":"Oak Trapdoor","1542":"Oak Trapdoor","1543":"Oak Trapdoor","1544":"Oak Trapdoor","1545":"Oak Trapdoor","1546":"Oak Trapdoor","1547":"Oak Trapdoor","1548":"Oak Trapdoor","1549":"Oak Trapdoor","1550":"Oak Trapdoor","1551":"Oak Trapdoor","1552":"Infested Stone","1553":"Infested Cobblestone","1554":"Infested Stone Brick","1555":"Infested Mossy Stone Brick","1556":"Infested Cracked Stone Brick","1557":"Infested Chiseled Stone Brick","1568":"Stone Bricks","1569":"Mossy Stone Bricks","1570":"Cracked Stone Bricks","1571":"Chiseled Stone Bricks","1584":"Brown Mushroom Block","1585":"Brown Mushroom Block","1586":"Brown Mushroom Block","1587":"Brown Mushroom Block","1588":"Brown Mushroom Block","1589":"Brown Mushroom Block","1590":"Brown Mushroom Block","1591":"Brown Mushroom Block","1592":"Brown Mushroom Block","1593":"Brown Mushroom Block","1594":"Mushroom Stem","1598":"Brown Mushroom Block","1599":"All Sided Mushroom Stem","1600":"Red Mushroom Block","1601":"Red Mushroom Block","1602":"Red Mushroom Block","1603":"Red Mushroom Block","1604":"Red Mushroom Block","1605":"Red Mushroom Block","1606":"Red Mushroom Block","1607":"Red Mushroom Block","1608":"Red Mushroom Block","1609":"Red Mushroom Block","1614":"Red Mushroom Block","1616":"Iron Bars","1632":"Glass Pane","1648":"Melon Block","1664":"Pumpkin Stem","1665":"Pumpkin Stem","1666":"Pumpkin Stem","1667":"Pumpkin Stem","1668":"Pumpkin Stem","1669":"Pumpkin Stem","1670":"Pumpkin Stem","1671":"Pumpkin Stem","1680":"Melon Stem","1681":"Melon Stem","1682":"Melon Stem","1683":"Melon Stem","1684":"Melon Stem","1685":"Melon Stem","1686":"Melon Stem","1687":"Melon Stem","1696":"Vines","1697":"Vines","1698":"Vines","1699":"Vines","1700":"Vines","1701":"Vines","1702":"Vines","1703":"Vines","1704":"Vines","1705":"Vines","1706":"Vines","1707":"Vines","1708":"Vines","1709":"Vines","1710":"Vines","1711":"Vines","1712":"Oak Fence Gate","1713":"Oak Fence Gate","1714":"Oak Fence Gate","1715":"Oak Fence Gate","1716":"Oak Fence Gate","1717":"Oak Fence Gate","1718":"Oak Fence Gate","1719":"Oak Fence Gate","1720":"Oak Fence Gate","1721":"Oak Fence Gate","1722":"Oak Fence Gate","1723":"Oak Fence Gate","1724":"Oak Fence Gate","1725":"Oak Fence Gate","1726":"Oak Fence Gate","1727":"Oak Fence Gate","1728":"Brick Stairs","1729":"Brick Stairs","1730":"Brick Stairs","1731":"Brick Stairs","1732":"Brick Stairs","1733":"Brick Stairs","1734":"Brick Stairs","1735":"Brick Stairs","1744":"Stone Brick Stairs","1745":"Stone Brick Stairs","1746":"Stone Brick Stairs","1747":"Stone Brick Stairs","1748":"Stone Brick Stairs","1749":"Stone Brick Stairs","1750":"Stone Brick Stairs","1751":"Stone Brick Stairs","1760":"Mycelium","1776":"Lily Pad","1792":"Nether Bricks","1808":"Nether Brick Fence","1824":"Nether Brick Stairs","1825":"Nether Brick Stairs","1826":"Nether Brick Stairs","1827":"Nether Brick Stairs","1828":"Nether Brick Stairs","1829":"Nether Brick Stairs","1830":"Nether Brick Stairs","1831":"Nether Brick Stairs","1840":"Nether Wart","1841":"Nether Wart","1842":"Nether Wart","1843":"Nether Wart","1856":"Enchanting Table","1872":"Brewing Stand","1873":"Brewing Stand","1874":"Brewing Stand","1875":"Brewing Stand","1876":"Brewing Stand","1877":"Brewing Stand","1878":"Brewing Stand","1879":"Brewing Stand","1920":"End Portal Frame","1921":"End Portal Frame","1922":"End Portal Frame","1923":"End Portal Frame","1924":"End Portal Frame","1925":"End Portal Frame","1926":"End Portal Frame","1927":"End Portal Frame","1936":"End Stone","1952":"Dragon Egg","1968":"Redstone Lamp","1984":"Redstone Lamp","2016":"Activator Rail","2017":"Activator Rail","2018":"Activator Rail","2019":"Activator Rail","2020":"Activator Rail","2021":"Activator Rail","2024":"Activator Rail","2025":"Activator Rail","2026":"Activator Rail","2027":"Activator Rail","2028":"Activator Rail","2029":"Activator Rail","2032":"Cocoa Block","2033":"Cocoa Block","2034":"Cocoa Block","2035":"Cocoa Block","2036":"Cocoa Block","2037":"Cocoa Block","2038":"Cocoa Block","2039":"Cocoa Block","2040":"Cocoa Block","2041":"Cocoa Block","2042":"Cocoa Block","2043":"Cocoa Block","2048":"Sandstone Stairs","2049":"Sandstone Stairs","2050":"Sandstone Stairs","2051":"Sandstone Stairs","2052":"Sandstone Stairs","2053":"Sandstone Stairs","2054":"Sandstone Stairs","2055":"Sandstone Stairs","2064":"Emerald Ore","2082":"Ender Chest","2083":"Ender Chest","2084":"Ender Chest","2085":"Ender Chest","2096":"Tripwire Hook","2097":"Tripwire Hook","2098":"Tripwire Hook","2099":"Tripwire Hook","2100":"Tripwire Hook","2101":"Tripwire Hook","2102":"Tripwire Hook","2103":"Tripwire Hook","2104":"Tripwire Hook","2105":"Tripwire Hook","2106":"Tripwire Hook","2107":"Tripwire Hook","2108":"Tripwire Hook","2109":"Tripwire Hook","2110":"Tripwire Hook","2111":"Tripwire Hook","2112":"Tripwire","2113":"Tripwire","2114":"Tripwire","2115":"Tripwire","2116":"Tripwire","2117":"Tripwire","2118":"Tripwire","2119":"Tripwire","2120":"Tripwire","2121":"Tripwire","2122":"Tripwire","2123":"Tripwire","2124":"Tripwire","2125":"Tripwire","2126":"Tripwire","2127":"Tripwire","2128":"Emerald Block","2144":"Spruce Stairs","2145":"Spruce Stairs","2146":"Spruce Stairs","2147":"Spruce Stairs","2148":"Spruce Stairs","2149":"Spruce Stairs","2150":"Spruce Stairs","2151":"Spruce Stairs","2160":"Birch Stairs","2161":"Birch Stairs","2162":"Birch Stairs","2163":"Birch Stairs","2164":"Birch Stairs","2165":"Birch Stairs","2166":"Birch Stairs","2167":"Birch Stairs","2176":"Jungle Stairs","2177":"Jungle Stairs","2178":"Jungle Stairs","2179":"Jungle Stairs","2180":"Jungle Stairs","2181":"Jungle Stairs","2182":"Jungle Stairs","2183":"Jungle Stairs","2208":"Beacon","2224":"Cobblestone Wall","2225":"Mossy Cobblestone Wall","2226":"Granite Wall","2227":"Diorite Wall","2228":"Andesite Wall","2229":"Sandstone Wall","2230":"Brick Wall","2231":"Stone Brick Wall","2232":"Mossy Stone Brick Wall","2233":"Nether Brick Wall","2234":"End Stone Brick Wall","2235":"Prismarine Wall","2236":"Red Sandstone Wall","2237":"Red Nether Brick Wall","2240":"Flower Pot","2256":"Carrot Block","2257":"Carrot Block","2258":"Carrot Block","2259":"Carrot Block","2260":"Carrot Block","2261":"Carrot Block","2262":"Carrot Block","2263":"Carrot Block","2272":"Potato Block","2273":"Potato Block","2274":"Potato Block","2275":"Potato Block","2276":"Potato Block","2277":"Potato Block","2278":"Potato Block","2279":"Potato Block","2288":"Oak Button","2289":"Oak Button","2290":"Oak Button","2291":"Oak Button","2292":"Oak Button","2293":"Oak Button","2296":"Oak Button","2297":"Oak Button","2298":"Oak Button","2299":"Oak Button","2300":"Oak Button","2301":"Oak Button","2305":"Mob Head","2306":"Mob Head","2307":"Mob Head","2308":"Mob Head","2309":"Mob Head","2320":"Anvil","2321":"Anvil","2322":"Anvil","2323":"Anvil","2324":"Anvil","2325":"Anvil","2326":"Anvil","2327":"Anvil","2328":"Anvil","2329":"Anvil","2330":"Anvil","2331":"Anvil","2338":"Trapped Chest","2339":"Trapped Chest","2340":"Trapped Chest","2341":"Trapped Chest","2352":"Weighted Pressure Plate Light","2353":"Weighted Pressure Plate Light","2354":"Weighted Pressure Plate Light","2355":"Weighted Pressure Plate Light","2356":"Weighted Pressure Plate Light","2357":"Weighted Pressure Plate Light","2358":"Weighted Pressure Plate Light","2359":"Weighted Pressure Plate Light","2360":"Weighted Pressure Plate Light","2361":"Weighted Pressure Plate Light","2362":"Weighted Pressure Plate Light","2363":"Weighted Pressure Plate Light","2364":"Weighted Pressure Plate Light","2365":"Weighted Pressure Plate Light","2366":"Weighted Pressure Plate Light","2367":"Weighted Pressure Plate Light","2368":"Weighted Pressure Plate Heavy","2369":"Weighted Pressure Plate Heavy","2370":"Weighted Pressure Plate Heavy","2371":"Weighted Pressure Plate Heavy","2372":"Weighted Pressure Plate Heavy","2373":"Weighted Pressure Plate Heavy","2374":"Weighted Pressure Plate Heavy","2375":"Weighted Pressure Plate Heavy","2376":"Weighted Pressure Plate Heavy","2377":"Weighted Pressure Plate Heavy","2378":"Weighted Pressure Plate Heavy","2379":"Weighted Pressure Plate Heavy","2380":"Weighted Pressure Plate Heavy","2381":"Weighted Pressure Plate Heavy","2382":"Weighted Pressure Plate Heavy","2383":"Weighted Pressure Plate Heavy","2384":"Redstone Comparator","2385":"Redstone Comparator","2386":"Redstone Comparator","2387":"Redstone Comparator","2388":"Redstone Comparator","2389":"Redstone Comparator","2390":"Redstone Comparator","2391":"Redstone Comparator","2408":"Redstone Comparator","2409":"Redstone Comparator","2410":"Redstone Comparator","2411":"Redstone Comparator","2412":"Redstone Comparator","2413":"Redstone Comparator","2414":"Redstone Comparator","2415":"Redstone Comparator","2416":"Daylight Sensor","2417":"Daylight Sensor","2418":"Daylight Sensor","2419":"Daylight Sensor","2420":"Daylight Sensor","2421":"Daylight Sensor","2422":"Daylight Sensor","2423":"Daylight Sensor","2424":"Daylight Sensor","2425":"Daylight Sensor","2426":"Daylight Sensor","2427":"Daylight Sensor","2428":"Daylight Sensor","2429":"Daylight Sensor","2430":"Daylight Sensor","2431":"Daylight Sensor","2432":"Redstone Block","2448":"Nether Quartz Ore","2464":"Hopper","2466":"Hopper","2467":"Hopper","2468":"Hopper","2469":"Hopper","2472":"Hopper","2474":"Hopper","2475":"Hopper","2476":"Hopper","2477":"Hopper","2480":"Quartz Block","2481":"Chiseled Quartz Block","2482":"Quartz Pillar","2483":"Smooth Quartz Block","2485":"Chiseled Quartz Block","2486":"Quartz Pillar","2489":"Chiseled Quartz Block","2490":"Quartz Pillar","2496":"Quartz Stairs","2497":"Quartz Stairs","2498":"Quartz Stairs","2499":"Quartz Stairs","2500":"Quartz Stairs","2501":"Quartz Stairs","2502":"Quartz Stairs","2503":"Quartz Stairs","2512":"Oak Slab","2513":"Spruce Slab","2514":"Birch Slab","2515":"Jungle Slab","2516":"Acacia Slab","2517":"Dark Oak Slab","2528":"Oak Slab","2529":"Spruce Slab","2530":"Birch Slab","2531":"Jungle Slab","2532":"Acacia Slab","2533":"Dark Oak Slab","2536":"Oak Slab","2537":"Spruce Slab","2538":"Birch Slab","2539":"Jungle Slab","2540":"Acacia Slab","2541":"Dark Oak Slab","2544":"Stained Clay","2545":"Stained Clay","2546":"Stained Clay","2547":"Stained Clay","2548":"Stained Clay","2549":"Stained Clay","2550":"Stained Clay","2551":"Stained Clay","2552":"Stained Clay","2553":"Stained Clay","2554":"Stained Clay","2555":"Stained Clay","2556":"Stained Clay","2557":"Stained Clay","2558":"Stained Clay","2559":"Stained Clay","2560":"Stained Glass Pane","2561":"Stained Glass Pane","2562":"Stained Glass Pane","2563":"Stained Glass Pane","2564":"Stained Glass Pane","2565":"Stained Glass Pane","2566":"Stained Glass Pane","2567":"Stained Glass Pane","2568":"Stained Glass Pane","2569":"Stained Glass Pane","2570":"Stained Glass Pane","2571":"Stained Glass Pane","2572":"Stained Glass Pane","2573":"Stained Glass Pane","2574":"Stained Glass Pane","2575":"Stained Glass Pane","2576":"Acacia Leaves","2577":"Dark Oak Leaves","2580":"Acacia Leaves","2581":"Dark Oak Leaves","2584":"Acacia Leaves","2585":"Dark Oak Leaves","2588":"Acacia Leaves","2589":"Dark Oak Leaves","2592":"Acacia Log","2593":"Dark Oak Log","2596":"Acacia Log","2597":"Dark Oak Log","2600":"Acacia Log","2601":"Dark Oak Log","2608":"Acacia Stairs","2609":"Acacia Stairs","2610":"Acacia Stairs","2611":"Acacia Stairs","2612":"Acacia Stairs","2613":"Acacia Stairs","2614":"Acacia Stairs","2615":"Acacia Stairs","2624":"Dark Oak Stairs","2625":"Dark Oak Stairs","2626":"Dark Oak Stairs","2627":"Dark Oak Stairs","2628":"Dark Oak Stairs","2629":"Dark Oak Stairs","2630":"Dark Oak Stairs","2631":"Dark Oak Stairs","2640":"Slime Block","2672":"Iron Trapdoor","2673":"Iron Trapdoor","2674":"Iron Trapdoor","2675":"Iron Trapdoor","2676":"Iron Trapdoor","2677":"Iron Trapdoor","2678":"Iron Trapdoor","2679":"Iron Trapdoor","2680":"Iron Trapdoor","2681":"Iron Trapdoor","2682":"Iron Trapdoor","2683":"Iron Trapdoor","2684":"Iron Trapdoor","2685":"Iron Trapdoor","2686":"Iron Trapdoor","2687":"Iron Trapdoor","2688":"Prismarine","2689":"Dark Prismarine","2690":"Prismarine Bricks","2704":"Sea Lantern","2720":"Hay Bale","2724":"Hay Bale","2728":"Hay Bale","2736":"Carpet","2737":"Carpet","2738":"Carpet","2739":"Carpet","2740":"Carpet","2741":"Carpet","2742":"Carpet","2743":"Carpet","2744":"Carpet","2745":"Carpet","2746":"Carpet","2747":"Carpet","2748":"Carpet","2749":"Carpet","2750":"Carpet","2751":"Carpet","2752":"Hardened Clay","2768":"Coal Block","2784":"Packed Ice","2800":"Sunflower","2801":"Lilac","2802":"Double Tallgrass","2803":"Large Fern","2804":"Rose Bush","2805":"Peony","2808":"Sunflower","2809":"Lilac","2810":"Double Tallgrass","2811":"Large Fern","2812":"Rose Bush","2813":"Peony","2816":"Banner","2817":"Banner","2818":"Banner","2819":"Banner","2820":"Banner","2821":"Banner","2822":"Banner","2823":"Banner","2824":"Banner","2825":"Banner","2826":"Banner","2827":"Banner","2828":"Banner","2829":"Banner","2830":"Banner","2831":"Banner","2834":"Wall Banner","2835":"Wall Banner","2836":"Wall Banner","2837":"Wall Banner","2848":"Daylight Sensor","2849":"Daylight Sensor","2850":"Daylight Sensor","2851":"Daylight Sensor","2852":"Daylight Sensor","2853":"Daylight Sensor","2854":"Daylight Sensor","2855":"Daylight Sensor","2856":"Daylight Sensor","2857":"Daylight Sensor","2858":"Daylight Sensor","2859":"Daylight Sensor","2860":"Daylight Sensor","2861":"Daylight Sensor","2862":"Daylight Sensor","2863":"Daylight Sensor","2864":"Red Sandstone","2865":"Chiseled Red Sandstone","2866":"Cut Red Sandstone","2867":"Smooth Red Sandstone","2880":"Red Sandstone Stairs","2881":"Red Sandstone Stairs","2882":"Red Sandstone Stairs","2883":"Red Sandstone Stairs","2884":"Red Sandstone Stairs","2885":"Red Sandstone Stairs","2886":"Red Sandstone Stairs","2887":"Red Sandstone Stairs","2896":"Red Sandstone Slab","2897":"Purpur Slab","2898":"Prismarine Slab","2899":"Dark Prismarine Slab","2900":"Prismarine Bricks Slab","2901":"Mossy Cobblestone Slab","2902":"Smooth Sandstone Slab","2903":"Red Nether Brick Slab","2912":"Red Sandstone Slab","2913":"Purpur Slab","2914":"Prismarine Slab","2915":"Dark Prismarine Slab","2916":"Prismarine Bricks Slab","2917":"Mossy Cobblestone Slab","2918":"Smooth Sandstone Slab","2919":"Red Nether Brick Slab","2920":"Red Sandstone Slab","2921":"Purpur Slab","2922":"Prismarine Slab","2923":"Dark Prismarine Slab","2924":"Prismarine Bricks Slab","2925":"Mossy Cobblestone Slab","2926":"Smooth Sandstone Slab","2927":"Red Nether Brick Slab","2928":"Spruce Fence Gate","2929":"Spruce Fence Gate","2930":"Spruce Fence Gate","2931":"Spruce Fence Gate","2932":"Spruce Fence Gate","2933":"Spruce Fence Gate","2934":"Spruce Fence Gate","2935":"Spruce Fence Gate","2936":"Spruce Fence Gate","2937":"Spruce Fence Gate","2938":"Spruce Fence Gate","2939":"Spruce Fence Gate","2940":"Spruce Fence Gate","2941":"Spruce Fence Gate","2942":"Spruce Fence Gate","2943":"Spruce Fence Gate","2944":"Birch Fence Gate","2945":"Birch Fence Gate","2946":"Birch Fence Gate","2947":"Birch Fence Gate","2948":"Birch Fence Gate","2949":"Birch Fence Gate","2950":"Birch Fence Gate","2951":"Birch Fence Gate","2952":"Birch Fence Gate","2953":"Birch Fence Gate","2954":"Birch Fence Gate","2955":"Birch Fence Gate","2956":"Birch Fence Gate","2957":"Birch Fence Gate","2958":"Birch Fence Gate","2959":"Birch Fence Gate","2960":"Jungle Fence Gate","2961":"Jungle Fence Gate","2962":"Jungle Fence Gate","2963":"Jungle Fence Gate","2964":"Jungle Fence Gate","2965":"Jungle Fence Gate","2966":"Jungle Fence Gate","2967":"Jungle Fence Gate","2968":"Jungle Fence Gate","2969":"Jungle Fence Gate","2970":"Jungle Fence Gate","2971":"Jungle Fence Gate","2972":"Jungle Fence Gate","2973":"Jungle Fence Gate","2974":"Jungle Fence Gate","2975":"Jungle Fence Gate","2976":"Dark Oak Fence Gate","2977":"Dark Oak Fence Gate","2978":"Dark Oak Fence Gate","2979":"Dark Oak Fence Gate","2980":"Dark Oak Fence Gate","2981":"Dark Oak Fence Gate","2982":"Dark Oak Fence Gate","2983":"Dark Oak Fence Gate","2984":"Dark Oak Fence Gate","2985":"Dark Oak Fence Gate","2986":"Dark Oak Fence Gate","2987":"Dark Oak Fence Gate","2988":"Dark Oak Fence Gate","2989":"Dark Oak Fence Gate","2990":"Dark Oak Fence Gate","2991":"Dark Oak Fence Gate","2992":"Acacia Fence Gate","2993":"Acacia Fence Gate","2994":"Acacia Fence Gate","2995":"Acacia Fence Gate","2996":"Acacia Fence Gate","2997":"Acacia Fence Gate","2998":"Acacia Fence Gate","2999":"Acacia Fence Gate","3000":"Acacia Fence Gate","3001":"Acacia Fence Gate","3002":"Acacia Fence Gate","3003":"Acacia Fence Gate","3004":"Acacia Fence Gate","3005":"Acacia Fence Gate","3006":"Acacia Fence Gate","3007":"Acacia Fence Gate","3040":"Hardened Glass Pane","3056":"Stained Hardened Glass Pane","3057":"Stained Hardened Glass Pane","3058":"Stained Hardened Glass Pane","3059":"Stained Hardened Glass Pane","3060":"Stained Hardened Glass Pane","3061":"Stained Hardened Glass Pane","3062":"Stained Hardened Glass Pane","3063":"Stained Hardened Glass Pane","3064":"Stained Hardened Glass Pane","3065":"Stained Hardened Glass Pane","3066":"Stained Hardened Glass Pane","3067":"Stained Hardened Glass Pane","3068":"Stained Hardened Glass Pane","3069":"Stained Hardened Glass Pane","3070":"Stained Hardened Glass Pane","3071":"Stained Hardened Glass Pane","3072":"Heat Block","3088":"Spruce Door","3089":"Spruce Door","3090":"Spruce Door","3091":"Spruce Door","3092":"Spruce Door","3093":"Spruce Door","3094":"Spruce Door","3095":"Spruce Door","3096":"Spruce Door","3097":"Spruce Door","3098":"Spruce Door","3099":"Spruce Door","3104":"Birch Door","3105":"Birch Door","3106":"Birch Door","3107":"Birch Door","3108":"Birch Door","3109":"Birch Door","3110":"Birch Door","3111":"Birch Door","3112":"Birch Door","3113":"Birch Door","3114":"Birch Door","3115":"Birch Door","3120":"Jungle Door","3121":"Jungle Door","3122":"Jungle Door","3123":"Jungle Door","3124":"Jungle Door","3125":"Jungle Door","3126":"Jungle Door","3127":"Jungle Door","3128":"Jungle Door","3129":"Jungle Door","3130":"Jungle Door","3131":"Jungle Door","3136":"Acacia Door","3137":"Acacia Door","3138":"Acacia Door","3139":"Acacia Door","3140":"Acacia Door","3141":"Acacia Door","3142":"Acacia Door","3143":"Acacia Door","3144":"Acacia Door","3145":"Acacia Door","3146":"Acacia Door","3147":"Acacia Door","3152":"Dark Oak Door","3153":"Dark Oak Door","3154":"Dark Oak Door","3155":"Dark Oak Door","3156":"Dark Oak Door","3157":"Dark Oak Door","3158":"Dark Oak Door","3159":"Dark Oak Door","3160":"Dark Oak Door","3161":"Dark Oak Door","3162":"Dark Oak Door","3163":"Dark Oak Door","3168":"Grass Path","3184":"Item Frame","3185":"Item Frame","3186":"Item Frame","3187":"Item Frame","3188":"Item Frame","3189":"Item Frame","3190":"Item Frame","3191":"Item Frame","3216":"Purpur Block","3218":"Purpur Pillar","3222":"Purpur Pillar","3226":"Purpur Pillar","3233":"Red Torch","3234":"Red Torch","3235":"Red Torch","3236":"Red Torch","3237":"Red Torch","3241":"Green Torch","3242":"Green Torch","3243":"Green Torch","3244":"Green Torch","3245":"Green Torch","3248":"Purpur Stairs","3249":"Purpur Stairs","3250":"Purpur Stairs","3251":"Purpur Stairs","3252":"Purpur Stairs","3253":"Purpur Stairs","3254":"Purpur Stairs","3255":"Purpur Stairs","3265":"Blue Torch","3266":"Blue Torch","3267":"Blue Torch","3268":"Blue Torch","3269":"Blue Torch","3273":"Purple Torch","3274":"Purple Torch","3275":"Purple Torch","3276":"Purple Torch","3277":"Purple Torch","3280":"Shulker Box","3296":"End Stone Bricks","3312":"Frosted Ice","3313":"Frosted Ice","3314":"Frosted Ice","3315":"Frosted Ice","3328":"End Rod","3329":"End Rod","3330":"End Rod","3331":"End Rod","3332":"End Rod","3333":"End Rod","3408":"Magma Block","3424":"Nether Wart Block","3440":"Red Nether Bricks","3456":"Bone Block","3460":"Bone Block","3464":"Bone Block","3488":"Dyed Shulker Box","3489":"Dyed Shulker Box","3490":"Dyed Shulker Box","3491":"Dyed Shulker Box","3492":"Dyed Shulker Box","3493":"Dyed Shulker Box","3494":"Dyed Shulker Box","3495":"Dyed Shulker Box","3496":"Dyed Shulker Box","3497":"Dyed Shulker Box","3498":"Dyed Shulker Box","3499":"Dyed Shulker Box","3500":"Dyed Shulker Box","3501":"Dyed Shulker Box","3502":"Dyed Shulker Box","3503":"Dyed Shulker Box","3506":"Purple Glazed Terracotta","3507":"Purple Glazed Terracotta","3508":"Purple Glazed Terracotta","3509":"Purple Glazed Terracotta","3522":"White Glazed Terracotta","3523":"White Glazed Terracotta","3524":"White Glazed Terracotta","3525":"White Glazed Terracotta","3538":"Orange Glazed Terracotta","3539":"Orange Glazed Terracotta","3540":"Orange Glazed Terracotta","3541":"Orange Glazed Terracotta","3554":"Magenta Glazed Terracotta","3555":"Magenta Glazed Terracotta","3556":"Magenta Glazed Terracotta","3557":"Magenta Glazed Terracotta","3570":"Light Blue Glazed Terracotta","3571":"Light Blue Glazed Terracotta","3572":"Light Blue Glazed Terracotta","3573":"Light Blue Glazed Terracotta","3586":"Yellow Glazed Terracotta","3587":"Yellow Glazed Terracotta","3588":"Yellow Glazed Terracotta","3589":"Yellow Glazed Terracotta","3602":"Lime Glazed Terracotta","3603":"Lime Glazed Terracotta","3604":"Lime Glazed Terracotta","3605":"Lime Glazed Terracotta","3618":"Pink Glazed Terracotta","3619":"Pink Glazed Terracotta","3620":"Pink Glazed Terracotta","3621":"Pink Glazed Terracotta","3634":"Gray Glazed Terracotta","3635":"Gray Glazed Terracotta","3636":"Gray Glazed Terracotta","3637":"Gray Glazed Terracotta","3650":"Light Gray Glazed Terracotta","3651":"Light Gray Glazed Terracotta","3652":"Light Gray Glazed Terracotta","3653":"Light Gray Glazed Terracotta","3666":"Cyan Glazed Terracotta","3667":"Cyan Glazed Terracotta","3668":"Cyan Glazed Terracotta","3669":"Cyan Glazed Terracotta","3698":"Blue Glazed Terracotta","3699":"Blue Glazed Terracotta","3700":"Blue Glazed Terracotta","3701":"Blue Glazed Terracotta","3714":"Brown Glazed Terracotta","3715":"Brown Glazed Terracotta","3716":"Brown Glazed Terracotta","3717":"Brown Glazed Terracotta","3730":"Green Glazed Terracotta","3731":"Green Glazed Terracotta","3732":"Green Glazed Terracotta","3733":"Green Glazed Terracotta","3746":"Red Glazed Terracotta","3747":"Red Glazed Terracotta","3748":"Red Glazed Terracotta","3749":"Red Glazed Terracotta","3762":"Black Glazed Terracotta","3763":"Black Glazed Terracotta","3764":"Black Glazed Terracotta","3765":"Black Glazed Terracotta","3776":"Concrete","3777":"Concrete","3778":"Concrete","3779":"Concrete","3780":"Concrete","3781":"Concrete","3782":"Concrete","3783":"Concrete","3784":"Concrete","3785":"Concrete","3786":"Concrete","3787":"Concrete","3788":"Concrete","3789":"Concrete","3790":"Concrete","3791":"Concrete","3792":"Concrete Powder","3793":"Concrete Powder","3794":"Concrete Powder","3795":"Concrete Powder","3796":"Concrete Powder","3797":"Concrete Powder","3798":"Concrete Powder","3799":"Concrete Powder","3800":"Concrete Powder","3801":"Concrete Powder","3802":"Concrete Powder","3803":"Concrete Powder","3804":"Concrete Powder","3805":"Concrete Powder","3806":"Concrete Powder","3807":"Concrete Powder","3808":"Compound Creator","3809":"Compound Creator","3810":"Compound Creator","3811":"Compound Creator","3812":"Material Reducer","3813":"Material Reducer","3814":"Material Reducer","3815":"Material Reducer","3816":"Element Constructor","3817":"Element Constructor","3818":"Element Constructor","3819":"Element Constructor","3820":"Lab Table","3821":"Lab Table","3822":"Lab Table","3823":"Lab Table","3825":"Underwater Torch","3826":"Underwater Torch","3827":"Underwater Torch","3828":"Underwater Torch","3829":"Underwater Torch","3856":"Stained Glass","3857":"Stained Glass","3858":"Stained Glass","3859":"Stained Glass","3860":"Stained Glass","3861":"Stained Glass","3862":"Stained Glass","3863":"Stained Glass","3864":"Stained Glass","3865":"Stained Glass","3866":"Stained Glass","3867":"Stained Glass","3868":"Stained Glass","3869":"Stained Glass","3870":"Stained Glass","3871":"Stained Glass","3888":"Podzol","3904":"Beetroot Block","3905":"Beetroot Block","3906":"Beetroot Block","3907":"Beetroot Block","3908":"Beetroot Block","3909":"Beetroot Block","3910":"Beetroot Block","3911":"Beetroot Block","3920":"Stonecutter","3936":"Glowing Obsidian","3952":"Nether Reactor Core","3953":"Nether Reactor Core","3954":"Nether Reactor Core","3968":"update!","3984":"ate!upd","4048":"Hardened Glass","4064":"Stained Hardened Glass","4065":"Stained Hardened Glass","4066":"Stained Hardened Glass","4067":"Stained Hardened Glass","4068":"Stained Hardened Glass","4069":"Stained Hardened Glass","4070":"Stained Hardened Glass","4071":"Stained Hardened Glass","4072":"Stained Hardened Glass","4073":"Stained Hardened Glass","4074":"Stained Hardened Glass","4075":"Stained Hardened Glass","4076":"Stained Hardened Glass","4077":"Stained Hardened Glass","4078":"Stained Hardened Glass","4079":"Stained Hardened Glass","4080":"reserved6","4112":"Prismarine Stairs","4113":"Prismarine Stairs","4114":"Prismarine Stairs","4115":"Prismarine Stairs","4116":"Prismarine Stairs","4117":"Prismarine Stairs","4118":"Prismarine Stairs","4119":"Prismarine Stairs","4128":"Dark Prismarine Stairs","4129":"Dark Prismarine Stairs","4130":"Dark Prismarine Stairs","4131":"Dark Prismarine Stairs","4132":"Dark Prismarine Stairs","4133":"Dark Prismarine Stairs","4134":"Dark Prismarine Stairs","4135":"Dark Prismarine Stairs","4144":"Prismarine Bricks Stairs","4145":"Prismarine Bricks Stairs","4146":"Prismarine Bricks Stairs","4147":"Prismarine Bricks Stairs","4148":"Prismarine Bricks Stairs","4149":"Prismarine Bricks Stairs","4150":"Prismarine Bricks Stairs","4151":"Prismarine Bricks Stairs","4160":"Stripped Spruce Log","4161":"Stripped Spruce Log","4162":"Stripped Spruce Log","4176":"Stripped Birch Log","4177":"Stripped Birch Log","4178":"Stripped Birch Log","4192":"Stripped Jungle Log","4193":"Stripped Jungle Log","4194":"Stripped Jungle Log","4208":"Stripped Acacia Log","4209":"Stripped Acacia Log","4210":"Stripped Acacia Log","4224":"Stripped Dark Oak Log","4225":"Stripped Dark Oak Log","4226":"Stripped Dark Oak Log","4240":"Stripped Oak Log","4241":"Stripped Oak Log","4242":"Stripped Oak Log","4256":"Blue Ice","4272":"Hydrogen","4288":"Helium","4304":"Lithium","4320":"Beryllium","4336":"Boron","4352":"Carbon","4368":"Nitrogen","4384":"Oxygen","4400":"Fluorine","4416":"Neon","4432":"Sodium","4448":"Magnesium","4464":"Aluminum","4480":"Silicon","4496":"Phosphorus","4512":"Sulfur","4528":"Chlorine","4544":"Argon","4560":"Potassium","4576":"Calcium","4592":"Scandium","4608":"Titanium","4624":"Vanadium","4640":"Chromium","4656":"Manganese","4672":"Iron","4688":"Cobalt","4704":"Nickel","4720":"Copper","4736":"Zinc","4752":"Gallium","4768":"Germanium","4784":"Arsenic","4800":"Selenium","4816":"Bromine","4832":"Krypton","4848":"Rubidium","4864":"Strontium","4880":"Yttrium","4896":"Zirconium","4912":"Niobium","4928":"Molybdenum","4944":"Technetium","4960":"Ruthenium","4976":"Rhodium","4992":"Palladium","5008":"Silver","5024":"Cadmium","5040":"Indium","5056":"Tin","5072":"Antimony","5088":"Tellurium","5104":"Iodine","5120":"Xenon","5136":"Cesium","5152":"Barium","5168":"Lanthanum","5184":"Cerium","5200":"Praseodymium","5216":"Neodymium","5232":"Promethium","5248":"Samarium","5264":"Europium","5280":"Gadolinium","5296":"Terbium","5312":"Dysprosium","5328":"Holmium","5344":"Erbium","5360":"Thulium","5376":"Ytterbium","5392":"Lutetium","5408":"Hafnium","5424":"Tantalum","5440":"Tungsten","5456":"Rhenium","5472":"Osmium","5488":"Iridium","5504":"Platinum","5520":"Gold","5536":"Mercury","5552":"Thallium","5568":"Lead","5584":"Bismuth","5600":"Polonium","5616":"Astatine","5632":"Radon","5648":"Francium","5664":"Radium","5680":"Actinium","5696":"Thorium","5712":"Protactinium","5728":"Uranium","5744":"Neptunium","5760":"Plutonium","5776":"Americium","5792":"Curium","5808":"Berkelium","5824":"Californium","5840":"Einsteinium","5856":"Fermium","5872":"Mendelevium","5888":"Nobelium","5904":"Lawrencium","5920":"Rutherfordium","5936":"Dubnium","5952":"Seaborgium","5968":"Bohrium","5984":"Hassium","6000":"Meitnerium","6016":"Darmstadtium","6032":"Roentgenium","6048":"Copernicium","6064":"Nihonium","6080":"Flerovium","6096":"Moscovium","6112":"Livermorium","6128":"Tennessine","6144":"Oganesson","6176":"Coral","6177":"Coral","6178":"Coral","6179":"Coral","6180":"Coral","6192":"Coral Block","6193":"Coral Block","6194":"Coral Block","6195":"Coral Block","6196":"Coral Block","6200":"Coral Block","6201":"Coral Block","6202":"Coral Block","6203":"Coral Block","6204":"Coral Block","6208":"Coral Fan","6209":"Coral Fan","6210":"Coral Fan","6211":"Coral Fan","6212":"Coral Fan","6216":"Coral Fan","6217":"Coral Fan","6218":"Coral Fan","6219":"Coral Fan","6220":"Coral Fan","6224":"Coral Fan","6225":"Coral Fan","6226":"Coral Fan","6227":"Coral Fan","6228":"Coral Fan","6232":"Coral Fan","6233":"Coral Fan","6234":"Coral Fan","6235":"Coral Fan","6236":"Coral Fan","6240":"Wall Coral Fan","6241":"Wall Coral Fan","6242":"Wall Coral Fan","6243":"Wall Coral Fan","6244":"Wall Coral Fan","6245":"Wall Coral Fan","6246":"Wall Coral Fan","6247":"Wall Coral Fan","6248":"Wall Coral Fan","6249":"Wall Coral Fan","6250":"Wall Coral Fan","6251":"Wall Coral Fan","6252":"Wall Coral Fan","6253":"Wall Coral Fan","6254":"Wall Coral Fan","6255":"Wall Coral Fan","6256":"Wall Coral Fan","6257":"Wall Coral Fan","6258":"Wall Coral Fan","6259":"Wall Coral Fan","6260":"Wall Coral Fan","6261":"Wall Coral Fan","6262":"Wall Coral Fan","6263":"Wall Coral Fan","6264":"Wall Coral Fan","6265":"Wall Coral Fan","6266":"Wall Coral Fan","6267":"Wall Coral Fan","6268":"Wall Coral Fan","6269":"Wall Coral Fan","6270":"Wall Coral Fan","6271":"Wall Coral Fan","6272":"Wall Coral Fan","6274":"Wall Coral Fan","6276":"Wall Coral Fan","6278":"Wall Coral Fan","6280":"Wall Coral Fan","6282":"Wall Coral Fan","6284":"Wall Coral Fan","6286":"Wall Coral Fan","6304":"Dried Kelp Block","6320":"Acacia Button","6321":"Acacia Button","6322":"Acacia Button","6323":"Acacia Button","6324":"Acacia Button","6325":"Acacia Button","6328":"Acacia Button","6329":"Acacia Button","6330":"Acacia Button","6331":"Acacia Button","6332":"Acacia Button","6333":"Acacia Button","6336":"Birch Button","6337":"Birch Button","6338":"Birch Button","6339":"Birch Button","6340":"Birch Button","6341":"Birch Button","6344":"Birch Button","6345":"Birch Button","6346":"Birch Button","6347":"Birch Button","6348":"Birch Button","6349":"Birch Button","6352":"Dark Oak Button","6353":"Dark Oak Button","6354":"Dark Oak Button","6355":"Dark Oak Button","6356":"Dark Oak Button","6357":"Dark Oak Button","6360":"Dark Oak Button","6361":"Dark Oak Button","6362":"Dark Oak Button","6363":"Dark Oak Button","6364":"Dark Oak Button","6365":"Dark Oak Button","6368":"Jungle Button","6369":"Jungle Button","6370":"Jungle Button","6371":"Jungle Button","6372":"Jungle Button","6373":"Jungle Button","6376":"Jungle Button","6377":"Jungle Button","6378":"Jungle Button","6379":"Jungle Button","6380":"Jungle Button","6381":"Jungle Button","6384":"Spruce Button","6385":"Spruce Button","6386":"Spruce Button","6387":"Spruce Button","6388":"Spruce Button","6389":"Spruce Button","6392":"Spruce Button","6393":"Spruce Button","6394":"Spruce Button","6395":"Spruce Button","6396":"Spruce Button","6397":"Spruce Button","6400":"Acacia Trapdoor","6401":"Acacia Trapdoor","6402":"Acacia Trapdoor","6403":"Acacia Trapdoor","6404":"Acacia Trapdoor","6405":"Acacia Trapdoor","6406":"Acacia Trapdoor","6407":"Acacia Trapdoor","6408":"Acacia Trapdoor","6409":"Acacia Trapdoor","6410":"Acacia Trapdoor","6411":"Acacia Trapdoor","6412":"Acacia Trapdoor","6413":"Acacia Trapdoor","6414":"Acacia Trapdoor","6415":"Acacia Trapdoor","6416":"Birch Trapdoor","6417":"Birch Trapdoor","6418":"Birch Trapdoor","6419":"Birch Trapdoor","6420":"Birch Trapdoor","6421":"Birch Trapdoor","6422":"Birch Trapdoor","6423":"Birch Trapdoor","6424":"Birch Trapdoor","6425":"Birch Trapdoor","6426":"Birch Trapdoor","6427":"Birch Trapdoor","6428":"Birch Trapdoor","6429":"Birch Trapdoor","6430":"Birch Trapdoor","6431":"Birch Trapdoor","6432":"Dark Oak Trapdoor","6433":"Dark Oak Trapdoor","6434":"Dark Oak Trapdoor","6435":"Dark Oak Trapdoor","6436":"Dark Oak Trapdoor","6437":"Dark Oak Trapdoor","6438":"Dark Oak Trapdoor","6439":"Dark Oak Trapdoor","6440":"Dark Oak Trapdoor","6441":"Dark Oak Trapdoor","6442":"Dark Oak Trapdoor","6443":"Dark Oak Trapdoor","6444":"Dark Oak Trapdoor","6445":"Dark Oak Trapdoor","6446":"Dark Oak Trapdoor","6447":"Dark Oak Trapdoor","6448":"Jungle Trapdoor","6449":"Jungle Trapdoor","6450":"Jungle Trapdoor","6451":"Jungle Trapdoor","6452":"Jungle Trapdoor","6453":"Jungle Trapdoor","6454":"Jungle Trapdoor","6455":"Jungle Trapdoor","6456":"Jungle Trapdoor","6457":"Jungle Trapdoor","6458":"Jungle Trapdoor","6459":"Jungle Trapdoor","6460":"Jungle Trapdoor","6461":"Jungle Trapdoor","6462":"Jungle Trapdoor","6463":"Jungle Trapdoor","6464":"Spruce Trapdoor","6465":"Spruce Trapdoor","6466":"Spruce Trapdoor","6467":"Spruce Trapdoor","6468":"Spruce Trapdoor","6469":"Spruce Trapdoor","6470":"Spruce Trapdoor","6471":"Spruce Trapdoor","6472":"Spruce Trapdoor","6473":"Spruce Trapdoor","6474":"Spruce Trapdoor","6475":"Spruce Trapdoor","6476":"Spruce Trapdoor","6477":"Spruce Trapdoor","6478":"Spruce Trapdoor","6479":"Spruce Trapdoor","6480":"Acacia Pressure Plate","6481":"Acacia Pressure Plate","6496":"Birch Pressure Plate","6497":"Birch Pressure Plate","6512":"Dark Oak Pressure Plate","6513":"Dark Oak Pressure Plate","6528":"Jungle Pressure Plate","6529":"Jungle Pressure Plate","6544":"Spruce Pressure Plate","6545":"Spruce Pressure Plate","6560":"Carved Pumpkin","6561":"Carved Pumpkin","6562":"Carved Pumpkin","6563":"Carved Pumpkin","6576":"Sea Pickle","6577":"Sea Pickle","6578":"Sea Pickle","6579":"Sea Pickle","6580":"Sea Pickle","6581":"Sea Pickle","6582":"Sea Pickle","6583":"Sea Pickle","6656":"Barrier","6672":"End Stone Brick Slab","6673":"Smooth Red Sandstone Slab","6674":"Polished Andesite Slab","6675":"Andesite Slab","6676":"Diorite Slab","6677":"Polished Diorite Slab","6678":"Granite Slab","6679":"Polished Granite Slab","6680":"End Stone Brick Slab","6681":"Smooth Red Sandstone Slab","6682":"Polished Andesite Slab","6683":"Andesite Slab","6684":"Diorite Slab","6685":"Polished Diorite Slab","6686":"Granite Slab","6687":"Polished Granite Slab","6688":"Bamboo","6689":"Bamboo","6690":"Bamboo","6691":"Bamboo","6692":"Bamboo","6693":"Bamboo","6696":"Bamboo","6697":"Bamboo","6698":"Bamboo","6699":"Bamboo","6700":"Bamboo","6701":"Bamboo","6704":"Bamboo Sapling","6712":"Bamboo Sapling","6736":"Mossy Stone Brick Slab","6737":"Smooth Quartz Slab","6738":"Stone Slab","6739":"Cut Sandstone Slab","6740":"Cut Red Sandstone Slab","6744":"Mossy Stone Brick Slab","6745":"Smooth Quartz Slab","6746":"Stone Slab","6747":"Cut Sandstone Slab","6748":"Cut Red Sandstone Slab","6752":"End Stone Brick Slab","6753":"Smooth Red Sandstone Slab","6754":"Polished Andesite Slab","6755":"Andesite Slab","6756":"Diorite Slab","6757":"Polished Diorite Slab","6758":"Granite Slab","6759":"Polished Granite Slab","6768":"Mossy Stone Brick Slab","6769":"Smooth Quartz Slab","6770":"Stone Slab","6771":"Cut Sandstone Slab","6772":"Cut Red Sandstone Slab","6784":"Granite Stairs","6785":"Granite Stairs","6786":"Granite Stairs","6787":"Granite Stairs","6788":"Granite Stairs","6789":"Granite Stairs","6790":"Granite Stairs","6791":"Granite Stairs","6800":"Diorite Stairs","6801":"Diorite Stairs","6802":"Diorite Stairs","6803":"Diorite Stairs","6804":"Diorite Stairs","6805":"Diorite Stairs","6806":"Diorite Stairs","6807":"Diorite Stairs","6816":"Andesite Stairs","6817":"Andesite Stairs","6818":"Andesite Stairs","6819":"Andesite Stairs","6820":"Andesite Stairs","6821":"Andesite Stairs","6822":"Andesite Stairs","6823":"Andesite Stairs","6832":"Polished Granite Stairs","6833":"Polished Granite Stairs","6834":"Polished Granite Stairs","6835":"Polished Granite Stairs","6836":"Polished Granite Stairs","6837":"Polished Granite Stairs","6838":"Polished Granite Stairs","6839":"Polished Granite Stairs","6848":"Polished Diorite Stairs","6849":"Polished Diorite Stairs","6850":"Polished Diorite Stairs","6851":"Polished Diorite Stairs","6852":"Polished Diorite Stairs","6853":"Polished Diorite Stairs","6854":"Polished Diorite Stairs","6855":"Polished Diorite Stairs","6864":"Polished Andesite Stairs","6865":"Polished Andesite Stairs","6866":"Polished Andesite Stairs","6867":"Polished Andesite Stairs","6868":"Polished Andesite Stairs","6869":"Polished Andesite Stairs","6870":"Polished Andesite Stairs","6871":"Polished Andesite Stairs","6880":"Mossy Stone Brick Stairs","6881":"Mossy Stone Brick Stairs","6882":"Mossy Stone Brick Stairs","6883":"Mossy Stone Brick Stairs","6884":"Mossy Stone Brick Stairs","6885":"Mossy Stone Brick Stairs","6886":"Mossy Stone Brick Stairs","6887":"Mossy Stone Brick Stairs","6896":"Smooth Red Sandstone Stairs","6897":"Smooth Red Sandstone Stairs","6898":"Smooth Red Sandstone Stairs","6899":"Smooth Red Sandstone Stairs","6900":"Smooth Red Sandstone Stairs","6901":"Smooth Red Sandstone Stairs","6902":"Smooth Red Sandstone Stairs","6903":"Smooth Red Sandstone Stairs","6912":"Smooth Sandstone Stairs","6913":"Smooth Sandstone Stairs","6914":"Smooth Sandstone Stairs","6915":"Smooth Sandstone Stairs","6916":"Smooth Sandstone Stairs","6917":"Smooth Sandstone Stairs","6918":"Smooth Sandstone Stairs","6919":"Smooth Sandstone Stairs","6928":"End Stone Brick Stairs","6929":"End Stone Brick Stairs","6930":"End Stone Brick Stairs","6931":"End Stone Brick Stairs","6932":"End Stone Brick Stairs","6933":"End Stone Brick Stairs","6934":"End Stone Brick Stairs","6935":"End Stone Brick Stairs","6944":"Mossy Cobblestone Stairs","6945":"Mossy Cobblestone Stairs","6946":"Mossy Cobblestone Stairs","6947":"Mossy Cobblestone Stairs","6948":"Mossy Cobblestone Stairs","6949":"Mossy Cobblestone Stairs","6950":"Mossy Cobblestone Stairs","6951":"Mossy Cobblestone Stairs","6960":"Stone Stairs","6961":"Stone Stairs","6962":"Stone Stairs","6963":"Stone Stairs","6964":"Stone Stairs","6965":"Stone Stairs","6966":"Stone Stairs","6967":"Stone Stairs","6976":"Spruce Sign","6977":"Spruce Sign","6978":"Spruce Sign","6979":"Spruce Sign","6980":"Spruce Sign","6981":"Spruce Sign","6982":"Spruce Sign","6983":"Spruce Sign","6984":"Spruce Sign","6985":"Spruce Sign","6986":"Spruce Sign","6987":"Spruce Sign","6988":"Spruce Sign","6989":"Spruce Sign","6990":"Spruce Sign","6991":"Spruce Sign","6994":"Spruce Wall Sign","6995":"Spruce Wall Sign","6996":"Spruce Wall Sign","6997":"Spruce Wall Sign","7008":"Smooth Stone","7024":"Red Nether Brick Stairs","7025":"Red Nether Brick Stairs","7026":"Red Nether Brick Stairs","7027":"Red Nether Brick Stairs","7028":"Red Nether Brick Stairs","7029":"Red Nether Brick Stairs","7030":"Red Nether Brick Stairs","7031":"Red Nether Brick Stairs","7040":"Smooth Quartz Stairs","7041":"Smooth Quartz Stairs","7042":"Smooth Quartz Stairs","7043":"Smooth Quartz Stairs","7044":"Smooth Quartz Stairs","7045":"Smooth Quartz Stairs","7046":"Smooth Quartz Stairs","7047":"Smooth Quartz Stairs","7056":"Birch Sign","7057":"Birch Sign","7058":"Birch Sign","7059":"Birch Sign","7060":"Birch Sign","7061":"Birch Sign","7062":"Birch Sign","7063":"Birch Sign","7064":"Birch Sign","7065":"Birch Sign","7066":"Birch Sign","7067":"Birch Sign","7068":"Birch Sign","7069":"Birch Sign","7070":"Birch Sign","7071":"Birch Sign","7074":"Birch Wall Sign","7075":"Birch Wall Sign","7076":"Birch Wall Sign","7077":"Birch Wall Sign","7088":"Jungle Sign","7089":"Jungle Sign","7090":"Jungle Sign","7091":"Jungle Sign","7092":"Jungle Sign","7093":"Jungle Sign","7094":"Jungle Sign","7095":"Jungle Sign","7096":"Jungle Sign","7097":"Jungle Sign","7098":"Jungle Sign","7099":"Jungle Sign","7100":"Jungle Sign","7101":"Jungle Sign","7102":"Jungle Sign","7103":"Jungle Sign","7106":"Jungle Wall Sign","7107":"Jungle Wall Sign","7108":"Jungle Wall Sign","7109":"Jungle Wall Sign","7120":"Acacia Sign","7121":"Acacia Sign","7122":"Acacia Sign","7123":"Acacia Sign","7124":"Acacia Sign","7125":"Acacia Sign","7126":"Acacia Sign","7127":"Acacia Sign","7128":"Acacia Sign","7129":"Acacia Sign","7130":"Acacia Sign","7131":"Acacia Sign","7132":"Acacia Sign","7133":"Acacia Sign","7134":"Acacia Sign","7135":"Acacia Sign","7138":"Acacia Wall Sign","7139":"Acacia Wall Sign","7140":"Acacia Wall Sign","7141":"Acacia Wall Sign","7152":"Dark Oak Sign","7153":"Dark Oak Sign","7154":"Dark Oak Sign","7155":"Dark Oak Sign","7156":"Dark Oak Sign","7157":"Dark Oak Sign","7158":"Dark Oak Sign","7159":"Dark Oak Sign","7160":"Dark Oak Sign","7161":"Dark Oak Sign","7162":"Dark Oak Sign","7163":"Dark Oak Sign","7164":"Dark Oak Sign","7165":"Dark Oak Sign","7166":"Dark Oak Sign","7167":"Dark Oak Sign","7170":"Dark Oak Wall Sign","7171":"Dark Oak Wall Sign","7172":"Dark Oak Wall Sign","7173":"Dark Oak Wall Sign","7218":"Blast Furnace","7219":"Blast Furnace","7220":"Blast Furnace","7221":"Blast Furnace","7250":"Smoker","7251":"Smoker","7252":"Smoker","7253":"Smoker","7266":"Smoker","7267":"Smoker","7268":"Smoker","7269":"Smoker","7296":"Fletching Table","7328":"Barrel","7329":"Barrel","7330":"Barrel","7331":"Barrel","7332":"Barrel","7333":"Barrel","7336":"Barrel","7337":"Barrel","7338":"Barrel","7339":"Barrel","7340":"Barrel","7341":"Barrel","7344":"Loom","7345":"Loom","7346":"Loom","7347":"Loom","7376":"Bell","7377":"Bell","7378":"Bell","7379":"Bell","7380":"Bell","7381":"Bell","7382":"Bell","7383":"Bell","7384":"Bell","7385":"Bell","7386":"Bell","7387":"Bell","7388":"Bell","7389":"Bell","7390":"Bell","7391":"Bell","7392":"Sweet Berry Bush","7393":"Sweet Berry Bush","7394":"Sweet Berry Bush","7395":"Sweet Berry Bush","7408":"Lantern","7409":"Lantern","7472":"Oak Wood","7473":"Spruce Wood","7474":"Birch Wood","7475":"Jungle Wood","7476":"Acacia Wood","7477":"Dark Oak Wood","7480":"Stripped Oak Wood","7481":"Stripped Spruce Wood","7482":"Stripped Birch Wood","7483":"Stripped Jungle Wood","7484":"Stripped Acacia Wood","7485":"Stripped Dark Oak Wood","7506":"Blast Furnace","7507":"Blast Furnace","7508":"Blast Furnace","7509":"Blast Furnace"},"remaps":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"23":16,"24":16,"25":16,"26":16,"27":16,"28":16,"29":16,"30":16,"31":16,"33":32,"34":32,"35":32,"36":32,"37":32,"38":32,"39":32,"40":32,"41":32,"42":32,"43":32,"44":32,"45":32,"46":32,"47":32,"50":48,"51":48,"52":48,"53":48,"54":48,"55":48,"56":48,"57":48,"58":48,"59":48,"60":48,"61":48,"62":48,"63":48,"65":64,"66":64,"67":64,"68":64,"69":64,"70":64,"71":64,"72":64,"73":64,"74":64,"75":64,"76":64,"77":64,"78":64,"79":64,"86":80,"87":80,"88":80,"89":80,"90":80,"91":80,"92":80,"93":80,"94":80,"95":80,"102":96,"103":96,"110":96,"111":96,"114":112,"115":112,"116":112,"117":112,"118":112,"119":112,"120":112,"121":112,"122":112,"123":112,"124":112,"125":112,"126":112,"127":112,"194":192,"195":192,"196":192,"197":192,"198":192,"199":192,"200":192,"201":192,"202":192,"203":192,"204":192,"205":192,"206":192,"207":192,"209":208,"210":208,"211":208,"212":208,"213":208,"214":208,"215":208,"216":208,"217":208,"218":208,"219":208,"220":208,"221":208,"222":208,"223":208,"225":224,"226":224,"227":224,"228":224,"229":224,"230":224,"231":224,"232":224,"233":224,"234":224,"235":224,"236":224,"237":224,"238":224,"239":224,"241":240,"242":240,"243":240,"244":240,"245":240,"246":240,"247":240,"248":240,"249":240,"250":240,"251":240,"252":240,"253":240,"254":240,"255":240,"257":256,"258":256,"259":256,"260":256,"261":256,"262":256,"263":256,"264":256,"265":256,"266":256,"267":256,"268":256,"269":256,"270":256,"271":256,"284":7472,"285":7473,"286":7474,"287":7475,"306":304,"307":304,"308":304,"309":304,"310":304,"311":304,"312":304,"313":304,"314":304,"315":304,"316":304,"317":304,"318":304,"319":304,"321":320,"322":320,"323":320,"324":320,"325":320,"326":320,"327":320,"328":320,"329":320,"330":320,"331":320,"332":320,"333":320,"334":320,"335":320,"337":336,"338":336,"339":336,"340":336,"341":336,"342":336,"343":336,"344":336,"345":336,"346":336,"347":336,"348":336,"349":336,"350":336,"351":336,"353":352,"354":352,"355":352,"356":352,"357":352,"358":352,"359":352,"360":352,"361":352,"362":352,"363":352,"364":352,"365":352,"366":352,"367":352,"388":384,"389":384,"390":384,"391":384,"392":384,"393":384,"394":384,"395":384,"396":384,"397":384,"398":384,"399":384,"401":400,"402":400,"403":400,"404":400,"405":400,"406":400,"407":400,"408":400,"409":400,"410":400,"411":400,"412":400,"413":400,"414":400,"415":400,"438":432,"439":432,"446":432,"447":432,"454":448,"455":448,"462":448,"463":448,"481":480,"482":480,"483":480,"484":480,"485":480,"486":480,"487":480,"488":480,"489":480,"490":480,"491":480,"492":480,"493":480,"494":480,"495":480,"496":498,"499":498,"500":498,"501":498,"502":498,"503":498,"504":498,"505":498,"506":498,"507":498,"508":498,"509":498,"510":498,"511":498,"513":512,"514":512,"515":512,"516":512,"517":512,"518":512,"519":512,"520":512,"521":512,"522":512,"523":512,"524":512,"525":512,"526":512,"527":512,"577":576,"578":576,"579":576,"580":576,"581":576,"582":576,"583":576,"584":576,"585":576,"586":576,"587":576,"588":576,"589":576,"590":576,"591":576,"593":592,"594":592,"595":592,"596":592,"597":592,"598":592,"599":592,"600":592,"601":592,"602":592,"603":592,"604":592,"605":592,"606":592,"607":592,"619":608,"620":608,"621":608,"622":608,"623":608,"625":624,"626":624,"627":624,"628":624,"629":624,"630":624,"631":624,"632":624,"633":624,"634":624,"635":624,"636":624,"637":624,"638":624,"639":624,"641":640,"642":640,"643":640,"644":640,"645":640,"646":640,"647":640,"648":640,"649":640,"650":640,"651":640,"652":640,"653":640,"654":640,"655":640,"657":656,"658":656,"659":656,"660":656,"661":656,"662":656,"663":656,"664":656,"665":656,"666":656,"667":656,"668":656,"669":656,"670":656,"671":656,"673":672,"674":672,"675":672,"676":672,"677":672,"678":672,"679":672,"680":672,"681":672,"682":672,"683":672,"684":672,"685":672,"686":672,"687":672,"696":688,"697":689,"698":690,"699":691,"700":692,"701":693,"702":694,"703":695,"721":720,"722":720,"723":720,"724":720,"725":720,"726":720,"727":720,"728":720,"729":720,"730":720,"731":720,"732":720,"733":720,"734":720,"735":720,"740":736,"741":736,"742":736,"743":736,"744":736,"745":736,"746":736,"747":736,"748":736,"749":736,"750":736,"751":736,"753":752,"754":752,"755":752,"756":752,"757":752,"758":752,"759":752,"760":752,"761":752,"762":752,"763":752,"764":752,"765":752,"766":752,"767":752,"769":768,"770":768,"771":768,"772":768,"773":768,"774":768,"775":768,"776":768,"777":768,"778":768,"779":768,"780":768,"781":768,"782":768,"783":768,"785":784,"786":784,"787":784,"788":784,"789":784,"790":784,"791":784,"792":784,"793":784,"794":784,"795":784,"796":784,"797":784,"798":784,"799":784,"800":805,"806":805,"807":805,"808":805,"809":805,"810":805,"811":805,"812":805,"813":805,"814":805,"815":805,"833":832,"834":832,"835":832,"836":832,"837":832,"838":832,"839":832,"840":832,"841":832,"842":832,"843":832,"844":832,"845":832,"846":832,"847":832,"856":851,"857":851,"858":851,"859":851,"860":851,"861":851,"862":851,"863":851,"864":866,"865":866,"870":866,"871":866,"872":866,"873":866,"874":866,"875":866,"876":866,"877":866,"878":866,"879":866,"897":896,"898":896,"899":896,"900":896,"901":896,"902":896,"903":896,"904":896,"905":896,"906":896,"907":896,"908":896,"909":896,"910":896,"911":896,"913":912,"914":912,"915":912,"916":912,"917":912,"918":912,"919":912,"920":912,"921":912,"922":912,"923":912,"924":912,"925":912,"926":912,"927":912,"929":928,"930":928,"931":928,"932":928,"933":928,"934":928,"935":928,"936":928,"937":928,"938":928,"939":928,"940":928,"941":928,"942":928,"943":928,"952":944,"953":944,"954":944,"955":944,"956":944,"957":944,"958":944,"959":944,"968":960,"969":960,"970":960,"971":960,"972":960,"973":960,"974":960,"975":960,"976":978,"977":978,"982":978,"983":978,"984":978,"985":978,"986":978,"987":978,"988":978,"989":978,"990":978,"991":978,"992":978,"993":978,"998":978,"999":978,"1000":978,"1001":978,"1002":978,"1003":978,"1004":978,"1005":978,"1006":978,"1007":978,"1036":1027,"1037":1027,"1038":1027,"1039":1027,"1040":1042,"1041":1042,"1046":1042,"1047":1042,"1048":1042,"1049":1042,"1050":1042,"1051":1042,"1052":1042,"1053":1042,"1054":1042,"1055":1042,"1066":1056,"1067":1056,"1068":1056,"1069":1056,"1070":1056,"1071":1056,"1080":1075,"1081":1075,"1082":1075,"1083":1075,"1084":1075,"1085":1075,"1086":1075,"1087":1075,"1088":1090,"1089":1090,"1094":1090,"1095":1090,"1096":1090,"1097":1090,"1098":1090,"1099":1090,"1100":1090,"1101":1090,"1102":1090,"1103":1090,"1122":1120,"1123":1120,"1124":1120,"1125":1120,"1126":1120,"1127":1120,"1128":1120,"1129":1120,"1130":1120,"1131":1120,"1132":1120,"1133":1120,"1134":1120,"1135":1120,"1148":1139,"1149":1139,"1150":1139,"1151":1139,"1154":1152,"1155":1152,"1156":1152,"1157":1152,"1158":1152,"1159":1152,"1160":1152,"1161":1152,"1162":1152,"1163":1152,"1164":1152,"1165":1152,"1166":1152,"1167":1152,"1169":1168,"1170":1168,"1171":1168,"1172":1168,"1173":1168,"1174":1168,"1175":1168,"1176":1168,"1177":1168,"1178":1168,"1179":1168,"1180":1168,"1181":1168,"1182":1168,"1183":1168,"1185":1168,"1186":1168,"1187":1168,"1188":1168,"1189":1168,"1190":1168,"1191":1168,"1192":1168,"1193":1168,"1194":1168,"1195":1168,"1196":1168,"1197":1168,"1198":1168,"1199":1168,"1200":1221,"1206":1221,"1207":1221,"1208":1221,"1209":1221,"1210":1221,"1211":1221,"1212":1221,"1213":1221,"1214":1221,"1215":1221,"1216":1221,"1222":1221,"1223":1221,"1224":1221,"1225":1221,"1226":1221,"1227":1221,"1228":1221,"1229":1221,"1230":1221,"1231":1221,"1238":1232,"1239":1232,"1246":1232,"1247":1232,"1256":1248,"1257":1248,"1258":1248,"1259":1248,"1260":1248,"1261":1248,"1262":1248,"1263":1248,"1265":1264,"1266":1264,"1267":1264,"1268":1264,"1269":1264,"1270":1264,"1271":1264,"1272":1264,"1273":1264,"1274":1264,"1275":1264,"1276":1264,"1277":1264,"1278":1264,"1279":1264,"1281":1280,"1282":1280,"1283":1280,"1284":1280,"1285":1280,"1286":1280,"1287":1280,"1288":1280,"1289":1280,"1290":1280,"1291":1280,"1292":1280,"1293":1280,"1294":1280,"1295":1280,"1313":1312,"1314":1312,"1315":1312,"1316":1312,"1317":1312,"1318":1312,"1319":1312,"1320":1312,"1321":1312,"1322":1312,"1323":1312,"1324":1312,"1325":1312,"1326":1312,"1327":1312,"1345":1344,"1346":1344,"1347":1344,"1348":1344,"1349":1344,"1350":1344,"1351":1344,"1352":1344,"1353":1344,"1354":1344,"1355":1344,"1356":1344,"1357":1344,"1358":1344,"1359":1344,"1366":1360,"1367":1360,"1368":1360,"1369":1360,"1370":1360,"1371":1360,"1372":1360,"1373":1360,"1374":1360,"1375":1360,"1377":1376,"1378":1376,"1379":1376,"1380":1376,"1381":1376,"1382":1376,"1383":1376,"1384":1376,"1385":1376,"1386":1376,"1387":1376,"1388":1376,"1389":1376,"1390":1376,"1391":1376,"1393":1392,"1394":1392,"1395":1392,"1396":1392,"1397":1392,"1398":1392,"1399":1392,"1400":1392,"1401":1392,"1402":1392,"1403":1392,"1404":1392,"1405":1392,"1406":1392,"1407":1392,"1409":1408,"1410":1408,"1411":1408,"1412":1408,"1413":1408,"1414":1408,"1415":1408,"1416":1408,"1417":1408,"1418":1408,"1419":1408,"1420":1408,"1421":1408,"1422":1408,"1423":1408,"1425":1424,"1426":1424,"1427":1424,"1428":1424,"1429":1424,"1430":1424,"1431":1424,"1432":1424,"1433":1424,"1434":1424,"1435":1424,"1436":1424,"1437":1424,"1438":1424,"1439":1424,"1440":1441,"1443":1441,"1444":1441,"1445":1441,"1446":1441,"1447":1441,"1448":1441,"1449":1441,"1450":1441,"1451":1441,"1452":1441,"1453":1441,"1454":1441,"1455":1441,"1460":1458,"1461":1458,"1462":1458,"1463":1458,"1464":1458,"1465":1458,"1466":1458,"1467":1458,"1468":1458,"1469":1458,"1470":1458,"1471":1458,"1479":1472,"1480":1472,"1481":1472,"1482":1472,"1483":1472,"1484":1472,"1485":1472,"1486":1472,"1487":1472,"1521":1520,"1522":1520,"1523":1520,"1524":1520,"1525":1520,"1526":1520,"1527":1520,"1528":1520,"1529":1520,"1530":1520,"1531":1520,"1532":1520,"1533":1520,"1534":1520,"1535":1520,"1558":1552,"1559":1552,"1560":1552,"1561":1552,"1562":1552,"1563":1552,"1564":1552,"1565":1552,"1566":1552,"1567":1552,"1572":1568,"1573":1568,"1574":1568,"1575":1568,"1576":1568,"1577":1568,"1578":1568,"1579":1568,"1580":1568,"1581":1568,"1582":1568,"1583":1568,"1595":1598,"1596":1598,"1597":1598,"1610":1594,"1611":1614,"1612":1614,"1613":1614,"1615":1599,"1617":1616,"1618":1616,"1619":1616,"1620":1616,"1621":1616,"1622":1616,"1623":1616,"1624":1616,"1625":1616,"1626":1616,"1627":1616,"1628":1616,"1629":1616,"1630":1616,"1631":1616,"1633":1632,"1634":1632,"1635":1632,"1636":1632,"1637":1632,"1638":1632,"1639":1632,"1640":1632,"1641":1632,"1642":1632,"1643":1632,"1644":1632,"1645":1632,"1646":1632,"1647":1632,"1649":1648,"1650":1648,"1651":1648,"1652":1648,"1653":1648,"1654":1648,"1655":1648,"1656":1648,"1657":1648,"1658":1648,"1659":1648,"1660":1648,"1661":1648,"1662":1648,"1663":1648,"1672":1664,"1673":1664,"1674":1664,"1675":1664,"1676":1664,"1677":1664,"1678":1664,"1679":1664,"1688":1680,"1689":1680,"1690":1680,"1691":1680,"1692":1680,"1693":1680,"1694":1680,"1695":1680,"1736":1731,"1737":1731,"1738":1731,"1739":1731,"1740":1731,"1741":1731,"1742":1731,"1743":1731,"1752":1747,"1753":1747,"1754":1747,"1755":1747,"1756":1747,"1757":1747,"1758":1747,"1759":1747,"1761":1760,"1762":1760,"1763":1760,"1764":1760,"1765":1760,"1766":1760,"1767":1760,"1768":1760,"1769":1760,"1770":1760,"1771":1760,"1772":1760,"1773":1760,"1774":1760,"1775":1760,"1777":1776,"1778":1776,"1779":1776,"1780":1776,"1781":1776,"1782":1776,"1783":1776,"1784":1776,"1785":1776,"1786":1776,"1787":1776,"1788":1776,"1789":1776,"1790":1776,"1791":1776,"1793":1792,"1794":1792,"1795":1792,"1796":1792,"1797":1792,"1798":1792,"1799":1792,"1800":1792,"1801":1792,"1802":1792,"1803":1792,"1804":1792,"1805":1792,"1806":1792,"1807":1792,"1809":1808,"1810":1808,"1811":1808,"1812":1808,"1813":1808,"1814":1808,"1815":1808,"1816":1808,"1817":1808,"1818":1808,"1819":1808,"1820":1808,"1821":1808,"1822":1808,"1823":1808,"1832":1827,"1833":1827,"1834":1827,"1835":1827,"1836":1827,"1837":1827,"1838":1827,"1839":1827,"1844":1840,"1845":1840,"1846":1840,"1847":1840,"1848":1840,"1849":1840,"1850":1840,"1851":1840,"1852":1840,"1853":1840,"1854":1840,"1855":1840,"1857":1856,"1858":1856,"1859":1856,"1860":1856,"1861":1856,"1862":1856,"1863":1856,"1864":1856,"1865":1856,"1866":1856,"1867":1856,"1868":1856,"1869":1856,"1870":1856,"1871":1856,"1880":1872,"1881":1872,"1882":1872,"1883":1872,"1884":1872,"1885":1872,"1886":1872,"1887":1872,"1928":1922,"1929":1922,"1930":1922,"1931":1922,"1932":1922,"1933":1922,"1934":1922,"1935":1922,"1937":1936,"1938":1936,"1939":1936,"1940":1936,"1941":1936,"1942":1936,"1943":1936,"1944":1936,"1945":1936,"1946":1936,"1947":1936,"1948":1936,"1949":1936,"1950":1936,"1951":1936,"1953":1952,"1954":1952,"1955":1952,"1956":1952,"1957":1952,"1958":1952,"1959":1952,"1960":1952,"1961":1952,"1962":1952,"1963":1952,"1964":1952,"1965":1952,"1966":1952,"1967":1952,"1969":1968,"1970":1968,"1971":1968,"1972":1968,"1973":1968,"1974":1968,"1975":1968,"1976":1968,"1977":1968,"1978":1968,"1979":1968,"1980":1968,"1981":1968,"1982":1968,"1983":1968,"1985":1968,"1986":1968,"1987":1968,"1988":1968,"1989":1968,"1990":1968,"1991":1968,"1992":1968,"1993":1968,"1994":1968,"1995":1968,"1996":1968,"1997":1968,"1998":1968,"1999":1968,"2022":2016,"2023":2016,"2030":2016,"2031":2016,"2044":2032,"2045":2032,"2046":2032,"2047":2032,"2056":2051,"2057":2051,"2058":2051,"2059":2051,"2060":2051,"2061":2051,"2062":2051,"2063":2051,"2065":2064,"2066":2064,"2067":2064,"2068":2064,"2069":2064,"2070":2064,"2071":2064,"2072":2064,"2073":2064,"2074":2064,"2075":2064,"2076":2064,"2077":2064,"2078":2064,"2079":2064,"2080":2082,"2081":2082,"2086":2082,"2087":2082,"2088":2082,"2089":2082,"2090":2082,"2091":2082,"2092":2082,"2093":2082,"2094":2082,"2095":2082,"2129":2128,"2130":2128,"2131":2128,"2132":2128,"2133":2128,"2134":2128,"2135":2128,"2136":2128,"2137":2128,"2138":2128,"2139":2128,"2140":2128,"2141":2128,"2142":2128,"2143":2128,"2152":2147,"2153":2147,"2154":2147,"2155":2147,"2156":2147,"2157":2147,"2158":2147,"2159":2147,"2168":2163,"2169":2163,"2170":2163,"2171":2163,"2172":2163,"2173":2163,"2174":2163,"2175":2163,"2184":2179,"2185":2179,"2186":2179,"2187":2179,"2188":2179,"2189":2179,"2190":2179,"2191":2179,"2209":2208,"2210":2208,"2211":2208,"2212":2208,"2213":2208,"2214":2208,"2215":2208,"2216":2208,"2217":2208,"2218":2208,"2219":2208,"2220":2208,"2221":2208,"2222":2208,"2223":2208,"2238":2224,"2239":2224,"2241":2240,"2242":2240,"2243":2240,"2244":2240,"2245":2240,"2246":2240,"2247":2240,"2248":2240,"2249":2240,"2250":2240,"2251":2240,"2252":2240,"2253":2240,"2254":2240,"2255":2240,"2264":2256,"2265":2256,"2266":2256,"2267":2256,"2268":2256,"2269":2256,"2270":2256,"2271":2256,"2280":2272,"2281":2272,"2282":2272,"2283":2272,"2284":2272,"2285":2272,"2286":2272,"2287":2272,"2294":2288,"2295":2288,"2302":2288,"2303":2288,"2304":2306,"2310":2306,"2311":2306,"2312":2306,"2313":2306,"2314":2306,"2315":2306,"2316":2306,"2317":2306,"2318":2306,"2319":2306,"2332":2322,"2333":2322,"2334":2322,"2335":2322,"2336":2338,"2337":2338,"2342":2338,"2343":2338,"2344":2338,"2345":2338,"2346":2338,"2347":2338,"2348":2338,"2349":2338,"2350":2338,"2351":2338,"2392":2386,"2393":2386,"2394":2386,"2395":2386,"2396":2386,"2397":2386,"2398":2386,"2399":2386,"2400":2386,"2401":2386,"2402":2386,"2403":2386,"2404":2386,"2405":2386,"2406":2386,"2407":2386,"2433":2432,"2434":2432,"2435":2432,"2436":2432,"2437":2432,"2438":2432,"2439":2432,"2440":2432,"2441":2432,"2442":2432,"2443":2432,"2444":2432,"2445":2432,"2446":2432,"2447":2432,"2449":2448,"2450":2448,"2451":2448,"2452":2448,"2453":2448,"2454":2448,"2455":2448,"2456":2448,"2457":2448,"2458":2448,"2459":2448,"2460":2448,"2461":2448,"2462":2448,"2463":2448,"2465":2464,"2470":2464,"2471":2464,"2473":2464,"2478":2464,"2479":2464,"2484":2480,"2487":2480,"2488":2480,"2491":2480,"2492":2480,"2493":2481,"2494":2482,"2495":2480,"2504":2499,"2505":2499,"2506":2499,"2507":2499,"2508":2499,"2509":2499,"2510":2499,"2511":2499,"2520":2512,"2521":2513,"2522":2514,"2523":2515,"2524":2516,"2525":2517,"2578":288,"2579":288,"2582":288,"2583":288,"2586":288,"2587":288,"2590":288,"2591":288,"2604":7476,"2605":7477,"2616":2611,"2617":2611,"2618":2611,"2619":2611,"2620":2611,"2621":2611,"2622":2611,"2623":2611,"2632":2627,"2633":2627,"2634":2627,"2635":2627,"2636":2627,"2637":2627,"2638":2627,"2639":2627,"2641":2640,"2642":2640,"2643":2640,"2644":2640,"2645":2640,"2646":2640,"2647":2640,"2648":2640,"2649":2640,"2650":2640,"2651":2640,"2652":2640,"2653":2640,"2654":2640,"2655":2640,"2691":2688,"2692":2688,"2693":2688,"2694":2688,"2695":2688,"2696":2688,"2697":2688,"2698":2688,"2699":2688,"2700":2688,"2701":2688,"2702":2688,"2703":2688,"2705":2704,"2706":2704,"2707":2704,"2708":2704,"2709":2704,"2710":2704,"2711":2704,"2712":2704,"2713":2704,"2714":2704,"2715":2704,"2716":2704,"2717":2704,"2718":2704,"2719":2704,"2721":2720,"2722":2720,"2723":2720,"2725":2720,"2726":2720,"2727":2720,"2729":2720,"2730":2720,"2731":2720,"2732":2720,"2733":2720,"2734":2720,"2735":2720,"2753":2752,"2754":2752,"2755":2752,"2756":2752,"2757":2752,"2758":2752,"2759":2752,"2760":2752,"2761":2752,"2762":2752,"2763":2752,"2764":2752,"2765":2752,"2766":2752,"2767":2752,"2769":2768,"2770":2768,"2771":2768,"2772":2768,"2773":2768,"2774":2768,"2775":2768,"2776":2768,"2777":2768,"2778":2768,"2779":2768,"2780":2768,"2781":2768,"2782":2768,"2783":2768,"2785":2784,"2786":2784,"2787":2784,"2788":2784,"2789":2784,"2790":2784,"2791":2784,"2792":2784,"2793":2784,"2794":2784,"2795":2784,"2796":2784,"2797":2784,"2798":2784,"2799":2784,"2806":2800,"2807":2800,"2814":2800,"2815":2800,"2832":2834,"2833":2834,"2838":2834,"2839":2834,"2840":2834,"2841":2834,"2842":2834,"2843":2834,"2844":2834,"2845":2834,"2846":2834,"2847":2834,"2868":2864,"2869":2864,"2870":2864,"2871":2864,"2872":2864,"2873":2864,"2874":2864,"2875":2864,"2876":2864,"2877":2864,"2878":2864,"2879":2864,"2888":2883,"2889":2883,"2890":2883,"2891":2883,"2892":2883,"2893":2883,"2894":2883,"2895":2883,"2904":2896,"2905":2897,"2906":2898,"2907":2899,"2908":2900,"2909":2901,"2910":2902,"2911":2903,"3041":3040,"3042":3040,"3043":3040,"3044":3040,"3045":3040,"3046":3040,"3047":3040,"3048":3040,"3049":3040,"3050":3040,"3051":3040,"3052":3040,"3053":3040,"3054":3040,"3055":3040,"3073":3072,"3074":3072,"3075":3072,"3076":3072,"3077":3072,"3078":3072,"3079":3072,"3080":3072,"3081":3072,"3082":3072,"3083":3072,"3084":3072,"3085":3072,"3086":3072,"3087":3072,"3100":3091,"3101":3091,"3102":3091,"3103":3091,"3116":3107,"3117":3107,"3118":3107,"3119":3107,"3132":3123,"3133":3123,"3134":3123,"3135":3123,"3148":3139,"3149":3139,"3150":3139,"3151":3139,"3164":3155,"3165":3155,"3166":3155,"3167":3155,"3169":3168,"3170":3168,"3171":3168,"3172":3168,"3173":3168,"3174":3168,"3175":3168,"3176":3168,"3177":3168,"3178":3168,"3179":3168,"3180":3168,"3181":3168,"3182":3168,"3183":3168,"3192":3187,"3193":3187,"3194":3187,"3195":3187,"3196":3187,"3197":3187,"3198":3187,"3199":3187,"3217":3216,"3219":3216,"3220":3216,"3221":3216,"3223":3216,"3224":3216,"3225":3216,"3227":3216,"3228":3216,"3229":3216,"3230":3218,"3231":3216,"3232":3237,"3238":3237,"3239":3237,"3240":3245,"3246":3245,"3247":3245,"3256":3251,"3257":3251,"3258":3251,"3259":3251,"3260":3251,"3261":3251,"3262":3251,"3263":3251,"3264":3269,"3270":3269,"3271":3269,"3272":3277,"3278":3277,"3279":3277,"3281":3280,"3282":3280,"3283":3280,"3284":3280,"3285":3280,"3286":3280,"3287":3280,"3288":3280,"3289":3280,"3290":3280,"3291":3280,"3292":3280,"3293":3280,"3294":3280,"3295":3280,"3297":3296,"3298":3296,"3299":3296,"3300":3296,"3301":3296,"3302":3296,"3303":3296,"3304":3296,"3305":3296,"3306":3296,"3307":3296,"3308":3296,"3309":3296,"3310":3296,"3311":3296,"3316":3312,"3317":3312,"3318":3312,"3319":3312,"3320":3312,"3321":3312,"3322":3312,"3323":3312,"3324":3312,"3325":3312,"3326":3312,"3327":3312,"3334":3328,"3335":3328,"3336":3328,"3337":3328,"3338":3328,"3339":3328,"3340":3328,"3341":3328,"3342":3328,"3343":3328,"3409":3408,"3410":3408,"3411":3408,"3412":3408,"3413":3408,"3414":3408,"3415":3408,"3416":3408,"3417":3408,"3418":3408,"3419":3408,"3420":3408,"3421":3408,"3422":3408,"3423":3408,"3425":3424,"3426":3424,"3427":3424,"3428":3424,"3429":3424,"3430":3424,"3431":3424,"3432":3424,"3433":3424,"3434":3424,"3435":3424,"3436":3424,"3437":3424,"3438":3424,"3439":3424,"3441":3440,"3442":3440,"3443":3440,"3444":3440,"3445":3440,"3446":3440,"3447":3440,"3448":3440,"3449":3440,"3450":3440,"3451":3440,"3452":3440,"3453":3440,"3454":3440,"3455":3440,"3457":3456,"3458":3456,"3459":3456,"3461":3456,"3462":3456,"3463":3456,"3465":3456,"3466":3456,"3467":3456,"3468":3456,"3469":3456,"3470":3456,"3471":3456,"3504":3506,"3505":3506,"3510":3506,"3511":3506,"3512":3506,"3513":3506,"3514":3506,"3515":3506,"3516":3506,"3517":3506,"3518":3506,"3519":3506,"3520":3522,"3521":3522,"3526":3522,"3527":3522,"3528":3522,"3529":3522,"3530":3522,"3531":3522,"3532":3522,"3533":3522,"3534":3522,"3535":3522,"3536":3538,"3537":3538,"3542":3538,"3543":3538,"3544":3538,"3545":3538,"3546":3538,"3547":3538,"3548":3538,"3549":3538,"3550":3538,"3551":3538,"3552":3554,"3553":3554,"3558":3554,"3559":3554,"3560":3554,"3561":3554,"3562":3554,"3563":3554,"3564":3554,"3565":3554,"3566":3554,"3567":3554,"3568":3570,"3569":3570,"3574":3570,"3575":3570,"3576":3570,"3577":3570,"3578":3570,"3579":3570,"3580":3570,"3581":3570,"3582":3570,"3583":3570,"3584":3586,"3585":3586,"3590":3586,"3591":3586,"3592":3586,"3593":3586,"3594":3586,"3595":3586,"3596":3586,"3597":3586,"3598":3586,"3599":3586,"3600":3602,"3601":3602,"3606":3602,"3607":3602,"3608":3602,"3609":3602,"3610":3602,"3611":3602,"3612":3602,"3613":3602,"3614":3602,"3615":3602,"3616":3618,"3617":3618,"3622":3618,"3623":3618,"3624":3618,"3625":3618,"3626":3618,"3627":3618,"3628":3618,"3629":3618,"3630":3618,"3631":3618,"3632":3634,"3633":3634,"3638":3634,"3639":3634,"3640":3634,"3641":3634,"3642":3634,"3643":3634,"3644":3634,"3645":3634,"3646":3634,"3647":3634,"3648":3650,"3649":3650,"3654":3650,"3655":3650,"3656":3650,"3657":3650,"3658":3650,"3659":3650,"3660":3650,"3661":3650,"3662":3650,"3663":3650,"3664":3666,"3665":3666,"3670":3666,"3671":3666,"3672":3666,"3673":3666,"3674":3666,"3675":3666,"3676":3666,"3677":3666,"3678":3666,"3679":3666,"3696":3698,"3697":3698,"3702":3698,"3703":3698,"3704":3698,"3705":3698,"3706":3698,"3707":3698,"3708":3698,"3709":3698,"3710":3698,"3711":3698,"3712":3714,"3713":3714,"3718":3714,"3719":3714,"3720":3714,"3721":3714,"3722":3714,"3723":3714,"3724":3714,"3725":3714,"3726":3714,"3727":3714,"3728":3730,"3729":3730,"3734":3730,"3735":3730,"3736":3730,"3737":3730,"3738":3730,"3739":3730,"3740":3730,"3741":3730,"3742":3730,"3743":3730,"3744":3746,"3745":3746,"3750":3746,"3751":3746,"3752":3746,"3753":3746,"3754":3746,"3755":3746,"3756":3746,"3757":3746,"3758":3746,"3759":3746,"3760":3762,"3761":3762,"3766":3762,"3767":3762,"3768":3762,"3769":3762,"3770":3762,"3771":3762,"3772":3762,"3773":3762,"3774":3762,"3775":3762,"3824":3829,"3830":3829,"3831":3829,"3832":3829,"3833":3829,"3834":3829,"3835":3829,"3836":3829,"3837":3829,"3838":3829,"3839":3829,"3889":3888,"3890":3888,"3891":3888,"3892":3888,"3893":3888,"3894":3888,"3895":3888,"3896":3888,"3897":3888,"3898":3888,"3899":3888,"3900":3888,"3901":3888,"3902":3888,"3903":3888,"3912":3904,"3913":3904,"3914":3904,"3915":3904,"3916":3904,"3917":3904,"3918":3904,"3919":3904,"3921":3920,"3922":3920,"3923":3920,"3924":3920,"3925":3920,"3926":3920,"3927":3920,"3928":3920,"3929":3920,"3930":3920,"3931":3920,"3932":3920,"3933":3920,"3934":3920,"3935":3920,"3937":3936,"3938":3936,"3939":3936,"3940":3936,"3941":3936,"3942":3936,"3943":3936,"3944":3936,"3945":3936,"3946":3936,"3947":3936,"3948":3936,"3949":3936,"3950":3936,"3951":3936,"3955":3952,"3956":3952,"3957":3952,"3958":3952,"3959":3952,"3960":3952,"3961":3952,"3962":3952,"3963":3952,"3964":3952,"3965":3952,"3966":3952,"3967":3952,"3969":3968,"3970":3968,"3971":3968,"3972":3968,"3973":3968,"3974":3968,"3975":3968,"3976":3968,"3977":3968,"3978":3968,"3979":3968,"3980":3968,"3981":3968,"3982":3968,"3983":3968,"3985":3984,"3986":3984,"3987":3984,"3988":3984,"3989":3984,"3990":3984,"3991":3984,"3992":3984,"3993":3984,"3994":3984,"3995":3984,"3996":3984,"3997":3984,"3998":3984,"3999":3984,"4049":4048,"4050":4048,"4051":4048,"4052":4048,"4053":4048,"4054":4048,"4055":4048,"4056":4048,"4057":4048,"4058":4048,"4059":4048,"4060":4048,"4061":4048,"4062":4048,"4063":4048,"4081":4080,"4082":4080,"4083":4080,"4084":4080,"4085":4080,"4086":4080,"4087":4080,"4088":4080,"4089":4080,"4090":4080,"4091":4080,"4092":4080,"4093":4080,"4094":4080,"4095":4080,"4120":4115,"4121":4115,"4122":4115,"4123":4115,"4124":4115,"4125":4115,"4126":4115,"4127":4115,"4136":4131,"4137":4131,"4138":4131,"4139":4131,"4140":4131,"4141":4131,"4142":4131,"4143":4131,"4152":4147,"4153":4147,"4154":4147,"4155":4147,"4156":4147,"4157":4147,"4158":4147,"4159":4147,"4163":4160,"4164":4160,"4165":4160,"4166":4160,"4167":4160,"4168":4160,"4169":4160,"4170":4160,"4171":4160,"4172":4160,"4173":4160,"4174":4160,"4175":4160,"4179":4176,"4180":4176,"4181":4176,"4182":4176,"4183":4176,"4184":4176,"4185":4176,"4186":4176,"4187":4176,"4188":4176,"4189":4176,"4190":4176,"4191":4176,"4195":4192,"4196":4192,"4197":4192,"4198":4192,"4199":4192,"4200":4192,"4201":4192,"4202":4192,"4203":4192,"4204":4192,"4205":4192,"4206":4192,"4207":4192,"4211":4208,"4212":4208,"4213":4208,"4214":4208,"4215":4208,"4216":4208,"4217":4208,"4218":4208,"4219":4208,"4220":4208,"4221":4208,"4222":4208,"4223":4208,"4227":4224,"4228":4224,"4229":4224,"4230":4224,"4231":4224,"4232":4224,"4233":4224,"4234":4224,"4235":4224,"4236":4224,"4237":4224,"4238":4224,"4239":4224,"4243":4240,"4244":4240,"4245":4240,"4246":4240,"4247":4240,"4248":4240,"4249":4240,"4250":4240,"4251":4240,"4252":4240,"4253":4240,"4254":4240,"4255":4240,"4257":4256,"4258":4256,"4259":4256,"4260":4256,"4261":4256,"4262":4256,"4263":4256,"4264":4256,"4265":4256,"4266":4256,"4267":4256,"4268":4256,"4269":4256,"4270":4256,"4271":4256,"4273":4272,"4274":4272,"4275":4272,"4276":4272,"4277":4272,"4278":4272,"4279":4272,"4280":4272,"4281":4272,"4282":4272,"4283":4272,"4284":4272,"4285":4272,"4286":4272,"4287":4272,"4289":4288,"4290":4288,"4291":4288,"4292":4288,"4293":4288,"4294":4288,"4295":4288,"4296":4288,"4297":4288,"4298":4288,"4299":4288,"4300":4288,"4301":4288,"4302":4288,"4303":4288,"4305":4304,"4306":4304,"4307":4304,"4308":4304,"4309":4304,"4310":4304,"4311":4304,"4312":4304,"4313":4304,"4314":4304,"4315":4304,"4316":4304,"4317":4304,"4318":4304,"4319":4304,"4321":4320,"4322":4320,"4323":4320,"4324":4320,"4325":4320,"4326":4320,"4327":4320,"4328":4320,"4329":4320,"4330":4320,"4331":4320,"4332":4320,"4333":4320,"4334":4320,"4335":4320,"4337":4336,"4338":4336,"4339":4336,"4340":4336,"4341":4336,"4342":4336,"4343":4336,"4344":4336,"4345":4336,"4346":4336,"4347":4336,"4348":4336,"4349":4336,"4350":4336,"4351":4336,"4353":4352,"4354":4352,"4355":4352,"4356":4352,"4357":4352,"4358":4352,"4359":4352,"4360":4352,"4361":4352,"4362":4352,"4363":4352,"4364":4352,"4365":4352,"4366":4352,"4367":4352,"4369":4368,"4370":4368,"4371":4368,"4372":4368,"4373":4368,"4374":4368,"4375":4368,"4376":4368,"4377":4368,"4378":4368,"4379":4368,"4380":4368,"4381":4368,"4382":4368,"4383":4368,"4385":4384,"4386":4384,"4387":4384,"4388":4384,"4389":4384,"4390":4384,"4391":4384,"4392":4384,"4393":4384,"4394":4384,"4395":4384,"4396":4384,"4397":4384,"4398":4384,"4399":4384,"4401":4400,"4402":4400,"4403":4400,"4404":4400,"4405":4400,"4406":4400,"4407":4400,"4408":4400,"4409":4400,"4410":4400,"4411":4400,"4412":4400,"4413":4400,"4414":4400,"4415":4400,"4417":4416,"4418":4416,"4419":4416,"4420":4416,"4421":4416,"4422":4416,"4423":4416,"4424":4416,"4425":4416,"4426":4416,"4427":4416,"4428":4416,"4429":4416,"4430":4416,"4431":4416,"4433":4432,"4434":4432,"4435":4432,"4436":4432,"4437":4432,"4438":4432,"4439":4432,"4440":4432,"4441":4432,"4442":4432,"4443":4432,"4444":4432,"4445":4432,"4446":4432,"4447":4432,"4449":4448,"4450":4448,"4451":4448,"4452":4448,"4453":4448,"4454":4448,"4455":4448,"4456":4448,"4457":4448,"4458":4448,"4459":4448,"4460":4448,"4461":4448,"4462":4448,"4463":4448,"4465":4464,"4466":4464,"4467":4464,"4468":4464,"4469":4464,"4470":4464,"4471":4464,"4472":4464,"4473":4464,"4474":4464,"4475":4464,"4476":4464,"4477":4464,"4478":4464,"4479":4464,"4481":4480,"4482":4480,"4483":4480,"4484":4480,"4485":4480,"4486":4480,"4487":4480,"4488":4480,"4489":4480,"4490":4480,"4491":4480,"4492":4480,"4493":4480,"4494":4480,"4495":4480,"4497":4496,"4498":4496,"4499":4496,"4500":4496,"4501":4496,"4502":4496,"4503":4496,"4504":4496,"4505":4496,"4506":4496,"4507":4496,"4508":4496,"4509":4496,"4510":4496,"4511":4496,"4513":4512,"4514":4512,"4515":4512,"4516":4512,"4517":4512,"4518":4512,"4519":4512,"4520":4512,"4521":4512,"4522":4512,"4523":4512,"4524":4512,"4525":4512,"4526":4512,"4527":4512,"4529":4528,"4530":4528,"4531":4528,"4532":4528,"4533":4528,"4534":4528,"4535":4528,"4536":4528,"4537":4528,"4538":4528,"4539":4528,"4540":4528,"4541":4528,"4542":4528,"4543":4528,"4545":4544,"4546":4544,"4547":4544,"4548":4544,"4549":4544,"4550":4544,"4551":4544,"4552":4544,"4553":4544,"4554":4544,"4555":4544,"4556":4544,"4557":4544,"4558":4544,"4559":4544,"4561":4560,"4562":4560,"4563":4560,"4564":4560,"4565":4560,"4566":4560,"4567":4560,"4568":4560,"4569":4560,"4570":4560,"4571":4560,"4572":4560,"4573":4560,"4574":4560,"4575":4560,"4577":4576,"4578":4576,"4579":4576,"4580":4576,"4581":4576,"4582":4576,"4583":4576,"4584":4576,"4585":4576,"4586":4576,"4587":4576,"4588":4576,"4589":4576,"4590":4576,"4591":4576,"4593":4592,"4594":4592,"4595":4592,"4596":4592,"4597":4592,"4598":4592,"4599":4592,"4600":4592,"4601":4592,"4602":4592,"4603":4592,"4604":4592,"4605":4592,"4606":4592,"4607":4592,"4609":4608,"4610":4608,"4611":4608,"4612":4608,"4613":4608,"4614":4608,"4615":4608,"4616":4608,"4617":4608,"4618":4608,"4619":4608,"4620":4608,"4621":4608,"4622":4608,"4623":4608,"4625":4624,"4626":4624,"4627":4624,"4628":4624,"4629":4624,"4630":4624,"4631":4624,"4632":4624,"4633":4624,"4634":4624,"4635":4624,"4636":4624,"4637":4624,"4638":4624,"4639":4624,"4641":4640,"4642":4640,"4643":4640,"4644":4640,"4645":4640,"4646":4640,"4647":4640,"4648":4640,"4649":4640,"4650":4640,"4651":4640,"4652":4640,"4653":4640,"4654":4640,"4655":4640,"4657":4656,"4658":4656,"4659":4656,"4660":4656,"4661":4656,"4662":4656,"4663":4656,"4664":4656,"4665":4656,"4666":4656,"4667":4656,"4668":4656,"4669":4656,"4670":4656,"4671":4656,"4673":4672,"4674":4672,"4675":4672,"4676":4672,"4677":4672,"4678":4672,"4679":4672,"4680":4672,"4681":4672,"4682":4672,"4683":4672,"4684":4672,"4685":4672,"4686":4672,"4687":4672,"4689":4688,"4690":4688,"4691":4688,"4692":4688,"4693":4688,"4694":4688,"4695":4688,"4696":4688,"4697":4688,"4698":4688,"4699":4688,"4700":4688,"4701":4688,"4702":4688,"4703":4688,"4705":4704,"4706":4704,"4707":4704,"4708":4704,"4709":4704,"4710":4704,"4711":4704,"4712":4704,"4713":4704,"4714":4704,"4715":4704,"4716":4704,"4717":4704,"4718":4704,"4719":4704,"4721":4720,"4722":4720,"4723":4720,"4724":4720,"4725":4720,"4726":4720,"4727":4720,"4728":4720,"4729":4720,"4730":4720,"4731":4720,"4732":4720,"4733":4720,"4734":4720,"4735":4720,"4737":4736,"4738":4736,"4739":4736,"4740":4736,"4741":4736,"4742":4736,"4743":4736,"4744":4736,"4745":4736,"4746":4736,"4747":4736,"4748":4736,"4749":4736,"4750":4736,"4751":4736,"4753":4752,"4754":4752,"4755":4752,"4756":4752,"4757":4752,"4758":4752,"4759":4752,"4760":4752,"4761":4752,"4762":4752,"4763":4752,"4764":4752,"4765":4752,"4766":4752,"4767":4752,"4769":4768,"4770":4768,"4771":4768,"4772":4768,"4773":4768,"4774":4768,"4775":4768,"4776":4768,"4777":4768,"4778":4768,"4779":4768,"4780":4768,"4781":4768,"4782":4768,"4783":4768,"4785":4784,"4786":4784,"4787":4784,"4788":4784,"4789":4784,"4790":4784,"4791":4784,"4792":4784,"4793":4784,"4794":4784,"4795":4784,"4796":4784,"4797":4784,"4798":4784,"4799":4784,"4801":4800,"4802":4800,"4803":4800,"4804":4800,"4805":4800,"4806":4800,"4807":4800,"4808":4800,"4809":4800,"4810":4800,"4811":4800,"4812":4800,"4813":4800,"4814":4800,"4815":4800,"4817":4816,"4818":4816,"4819":4816,"4820":4816,"4821":4816,"4822":4816,"4823":4816,"4824":4816,"4825":4816,"4826":4816,"4827":4816,"4828":4816,"4829":4816,"4830":4816,"4831":4816,"4833":4832,"4834":4832,"4835":4832,"4836":4832,"4837":4832,"4838":4832,"4839":4832,"4840":4832,"4841":4832,"4842":4832,"4843":4832,"4844":4832,"4845":4832,"4846":4832,"4847":4832,"4849":4848,"4850":4848,"4851":4848,"4852":4848,"4853":4848,"4854":4848,"4855":4848,"4856":4848,"4857":4848,"4858":4848,"4859":4848,"4860":4848,"4861":4848,"4862":4848,"4863":4848,"4865":4864,"4866":4864,"4867":4864,"4868":4864,"4869":4864,"4870":4864,"4871":4864,"4872":4864,"4873":4864,"4874":4864,"4875":4864,"4876":4864,"4877":4864,"4878":4864,"4879":4864,"4881":4880,"4882":4880,"4883":4880,"4884":4880,"4885":4880,"4886":4880,"4887":4880,"4888":4880,"4889":4880,"4890":4880,"4891":4880,"4892":4880,"4893":4880,"4894":4880,"4895":4880,"4897":4896,"4898":4896,"4899":4896,"4900":4896,"4901":4896,"4902":4896,"4903":4896,"4904":4896,"4905":4896,"4906":4896,"4907":4896,"4908":4896,"4909":4896,"4910":4896,"4911":4896,"4913":4912,"4914":4912,"4915":4912,"4916":4912,"4917":4912,"4918":4912,"4919":4912,"4920":4912,"4921":4912,"4922":4912,"4923":4912,"4924":4912,"4925":4912,"4926":4912,"4927":4912,"4929":4928,"4930":4928,"4931":4928,"4932":4928,"4933":4928,"4934":4928,"4935":4928,"4936":4928,"4937":4928,"4938":4928,"4939":4928,"4940":4928,"4941":4928,"4942":4928,"4943":4928,"4945":4944,"4946":4944,"4947":4944,"4948":4944,"4949":4944,"4950":4944,"4951":4944,"4952":4944,"4953":4944,"4954":4944,"4955":4944,"4956":4944,"4957":4944,"4958":4944,"4959":4944,"4961":4960,"4962":4960,"4963":4960,"4964":4960,"4965":4960,"4966":4960,"4967":4960,"4968":4960,"4969":4960,"4970":4960,"4971":4960,"4972":4960,"4973":4960,"4974":4960,"4975":4960,"4977":4976,"4978":4976,"4979":4976,"4980":4976,"4981":4976,"4982":4976,"4983":4976,"4984":4976,"4985":4976,"4986":4976,"4987":4976,"4988":4976,"4989":4976,"4990":4976,"4991":4976,"4993":4992,"4994":4992,"4995":4992,"4996":4992,"4997":4992,"4998":4992,"4999":4992,"5000":4992,"5001":4992,"5002":4992,"5003":4992,"5004":4992,"5005":4992,"5006":4992,"5007":4992,"5009":5008,"5010":5008,"5011":5008,"5012":5008,"5013":5008,"5014":5008,"5015":5008,"5016":5008,"5017":5008,"5018":5008,"5019":5008,"5020":5008,"5021":5008,"5022":5008,"5023":5008,"5025":5024,"5026":5024,"5027":5024,"5028":5024,"5029":5024,"5030":5024,"5031":5024,"5032":5024,"5033":5024,"5034":5024,"5035":5024,"5036":5024,"5037":5024,"5038":5024,"5039":5024,"5041":5040,"5042":5040,"5043":5040,"5044":5040,"5045":5040,"5046":5040,"5047":5040,"5048":5040,"5049":5040,"5050":5040,"5051":5040,"5052":5040,"5053":5040,"5054":5040,"5055":5040,"5057":5056,"5058":5056,"5059":5056,"5060":5056,"5061":5056,"5062":5056,"5063":5056,"5064":5056,"5065":5056,"5066":5056,"5067":5056,"5068":5056,"5069":5056,"5070":5056,"5071":5056,"5073":5072,"5074":5072,"5075":5072,"5076":5072,"5077":5072,"5078":5072,"5079":5072,"5080":5072,"5081":5072,"5082":5072,"5083":5072,"5084":5072,"5085":5072,"5086":5072,"5087":5072,"5089":5088,"5090":5088,"5091":5088,"5092":5088,"5093":5088,"5094":5088,"5095":5088,"5096":5088,"5097":5088,"5098":5088,"5099":5088,"5100":5088,"5101":5088,"5102":5088,"5103":5088,"5105":5104,"5106":5104,"5107":5104,"5108":5104,"5109":5104,"5110":5104,"5111":5104,"5112":5104,"5113":5104,"5114":5104,"5115":5104,"5116":5104,"5117":5104,"5118":5104,"5119":5104,"5121":5120,"5122":5120,"5123":5120,"5124":5120,"5125":5120,"5126":5120,"5127":5120,"5128":5120,"5129":5120,"5130":5120,"5131":5120,"5132":5120,"5133":5120,"5134":5120,"5135":5120,"5137":5136,"5138":5136,"5139":5136,"5140":5136,"5141":5136,"5142":5136,"5143":5136,"5144":5136,"5145":5136,"5146":5136,"5147":5136,"5148":5136,"5149":5136,"5150":5136,"5151":5136,"5153":5152,"5154":5152,"5155":5152,"5156":5152,"5157":5152,"5158":5152,"5159":5152,"5160":5152,"5161":5152,"5162":5152,"5163":5152,"5164":5152,"5165":5152,"5166":5152,"5167":5152,"5169":5168,"5170":5168,"5171":5168,"5172":5168,"5173":5168,"5174":5168,"5175":5168,"5176":5168,"5177":5168,"5178":5168,"5179":5168,"5180":5168,"5181":5168,"5182":5168,"5183":5168,"5185":5184,"5186":5184,"5187":5184,"5188":5184,"5189":5184,"5190":5184,"5191":5184,"5192":5184,"5193":5184,"5194":5184,"5195":5184,"5196":5184,"5197":5184,"5198":5184,"5199":5184,"5201":5200,"5202":5200,"5203":5200,"5204":5200,"5205":5200,"5206":5200,"5207":5200,"5208":5200,"5209":5200,"5210":5200,"5211":5200,"5212":5200,"5213":5200,"5214":5200,"5215":5200,"5217":5216,"5218":5216,"5219":5216,"5220":5216,"5221":5216,"5222":5216,"5223":5216,"5224":5216,"5225":5216,"5226":5216,"5227":5216,"5228":5216,"5229":5216,"5230":5216,"5231":5216,"5233":5232,"5234":5232,"5235":5232,"5236":5232,"5237":5232,"5238":5232,"5239":5232,"5240":5232,"5241":5232,"5242":5232,"5243":5232,"5244":5232,"5245":5232,"5246":5232,"5247":5232,"5249":5248,"5250":5248,"5251":5248,"5252":5248,"5253":5248,"5254":5248,"5255":5248,"5256":5248,"5257":5248,"5258":5248,"5259":5248,"5260":5248,"5261":5248,"5262":5248,"5263":5248,"5265":5264,"5266":5264,"5267":5264,"5268":5264,"5269":5264,"5270":5264,"5271":5264,"5272":5264,"5273":5264,"5274":5264,"5275":5264,"5276":5264,"5277":5264,"5278":5264,"5279":5264,"5281":5280,"5282":5280,"5283":5280,"5284":5280,"5285":5280,"5286":5280,"5287":5280,"5288":5280,"5289":5280,"5290":5280,"5291":5280,"5292":5280,"5293":5280,"5294":5280,"5295":5280,"5297":5296,"5298":5296,"5299":5296,"5300":5296,"5301":5296,"5302":5296,"5303":5296,"5304":5296,"5305":5296,"5306":5296,"5307":5296,"5308":5296,"5309":5296,"5310":5296,"5311":5296,"5313":5312,"5314":5312,"5315":5312,"5316":5312,"5317":5312,"5318":5312,"5319":5312,"5320":5312,"5321":5312,"5322":5312,"5323":5312,"5324":5312,"5325":5312,"5326":5312,"5327":5312,"5329":5328,"5330":5328,"5331":5328,"5332":5328,"5333":5328,"5334":5328,"5335":5328,"5336":5328,"5337":5328,"5338":5328,"5339":5328,"5340":5328,"5341":5328,"5342":5328,"5343":5328,"5345":5344,"5346":5344,"5347":5344,"5348":5344,"5349":5344,"5350":5344,"5351":5344,"5352":5344,"5353":5344,"5354":5344,"5355":5344,"5356":5344,"5357":5344,"5358":5344,"5359":5344,"5361":5360,"5362":5360,"5363":5360,"5364":5360,"5365":5360,"5366":5360,"5367":5360,"5368":5360,"5369":5360,"5370":5360,"5371":5360,"5372":5360,"5373":5360,"5374":5360,"5375":5360,"5377":5376,"5378":5376,"5379":5376,"5380":5376,"5381":5376,"5382":5376,"5383":5376,"5384":5376,"5385":5376,"5386":5376,"5387":5376,"5388":5376,"5389":5376,"5390":5376,"5391":5376,"5393":5392,"5394":5392,"5395":5392,"5396":5392,"5397":5392,"5398":5392,"5399":5392,"5400":5392,"5401":5392,"5402":5392,"5403":5392,"5404":5392,"5405":5392,"5406":5392,"5407":5392,"5409":5408,"5410":5408,"5411":5408,"5412":5408,"5413":5408,"5414":5408,"5415":5408,"5416":5408,"5417":5408,"5418":5408,"5419":5408,"5420":5408,"5421":5408,"5422":5408,"5423":5408,"5425":5424,"5426":5424,"5427":5424,"5428":5424,"5429":5424,"5430":5424,"5431":5424,"5432":5424,"5433":5424,"5434":5424,"5435":5424,"5436":5424,"5437":5424,"5438":5424,"5439":5424,"5441":5440,"5442":5440,"5443":5440,"5444":5440,"5445":5440,"5446":5440,"5447":5440,"5448":5440,"5449":5440,"5450":5440,"5451":5440,"5452":5440,"5453":5440,"5454":5440,"5455":5440,"5457":5456,"5458":5456,"5459":5456,"5460":5456,"5461":5456,"5462":5456,"5463":5456,"5464":5456,"5465":5456,"5466":5456,"5467":5456,"5468":5456,"5469":5456,"5470":5456,"5471":5456,"5473":5472,"5474":5472,"5475":5472,"5476":5472,"5477":5472,"5478":5472,"5479":5472,"5480":5472,"5481":5472,"5482":5472,"5483":5472,"5484":5472,"5485":5472,"5486":5472,"5487":5472,"5489":5488,"5490":5488,"5491":5488,"5492":5488,"5493":5488,"5494":5488,"5495":5488,"5496":5488,"5497":5488,"5498":5488,"5499":5488,"5500":5488,"5501":5488,"5502":5488,"5503":5488,"5505":5504,"5506":5504,"5507":5504,"5508":5504,"5509":5504,"5510":5504,"5511":5504,"5512":5504,"5513":5504,"5514":5504,"5515":5504,"5516":5504,"5517":5504,"5518":5504,"5519":5504,"5521":5520,"5522":5520,"5523":5520,"5524":5520,"5525":5520,"5526":5520,"5527":5520,"5528":5520,"5529":5520,"5530":5520,"5531":5520,"5532":5520,"5533":5520,"5534":5520,"5535":5520,"5537":5536,"5538":5536,"5539":5536,"5540":5536,"5541":5536,"5542":5536,"5543":5536,"5544":5536,"5545":5536,"5546":5536,"5547":5536,"5548":5536,"5549":5536,"5550":5536,"5551":5536,"5553":5552,"5554":5552,"5555":5552,"5556":5552,"5557":5552,"5558":5552,"5559":5552,"5560":5552,"5561":5552,"5562":5552,"5563":5552,"5564":5552,"5565":5552,"5566":5552,"5567":5552,"5569":5568,"5570":5568,"5571":5568,"5572":5568,"5573":5568,"5574":5568,"5575":5568,"5576":5568,"5577":5568,"5578":5568,"5579":5568,"5580":5568,"5581":5568,"5582":5568,"5583":5568,"5585":5584,"5586":5584,"5587":5584,"5588":5584,"5589":5584,"5590":5584,"5591":5584,"5592":5584,"5593":5584,"5594":5584,"5595":5584,"5596":5584,"5597":5584,"5598":5584,"5599":5584,"5601":5600,"5602":5600,"5603":5600,"5604":5600,"5605":5600,"5606":5600,"5607":5600,"5608":5600,"5609":5600,"5610":5600,"5611":5600,"5612":5600,"5613":5600,"5614":5600,"5615":5600,"5617":5616,"5618":5616,"5619":5616,"5620":5616,"5621":5616,"5622":5616,"5623":5616,"5624":5616,"5625":5616,"5626":5616,"5627":5616,"5628":5616,"5629":5616,"5630":5616,"5631":5616,"5633":5632,"5634":5632,"5635":5632,"5636":5632,"5637":5632,"5638":5632,"5639":5632,"5640":5632,"5641":5632,"5642":5632,"5643":5632,"5644":5632,"5645":5632,"5646":5632,"5647":5632,"5649":5648,"5650":5648,"5651":5648,"5652":5648,"5653":5648,"5654":5648,"5655":5648,"5656":5648,"5657":5648,"5658":5648,"5659":5648,"5660":5648,"5661":5648,"5662":5648,"5663":5648,"5665":5664,"5666":5664,"5667":5664,"5668":5664,"5669":5664,"5670":5664,"5671":5664,"5672":5664,"5673":5664,"5674":5664,"5675":5664,"5676":5664,"5677":5664,"5678":5664,"5679":5664,"5681":5680,"5682":5680,"5683":5680,"5684":5680,"5685":5680,"5686":5680,"5687":5680,"5688":5680,"5689":5680,"5690":5680,"5691":5680,"5692":5680,"5693":5680,"5694":5680,"5695":5680,"5697":5696,"5698":5696,"5699":5696,"5700":5696,"5701":5696,"5702":5696,"5703":5696,"5704":5696,"5705":5696,"5706":5696,"5707":5696,"5708":5696,"5709":5696,"5710":5696,"5711":5696,"5713":5712,"5714":5712,"5715":5712,"5716":5712,"5717":5712,"5718":5712,"5719":5712,"5720":5712,"5721":5712,"5722":5712,"5723":5712,"5724":5712,"5725":5712,"5726":5712,"5727":5712,"5729":5728,"5730":5728,"5731":5728,"5732":5728,"5733":5728,"5734":5728,"5735":5728,"5736":5728,"5737":5728,"5738":5728,"5739":5728,"5740":5728,"5741":5728,"5742":5728,"5743":5728,"5745":5744,"5746":5744,"5747":5744,"5748":5744,"5749":5744,"5750":5744,"5751":5744,"5752":5744,"5753":5744,"5754":5744,"5755":5744,"5756":5744,"5757":5744,"5758":5744,"5759":5744,"5761":5760,"5762":5760,"5763":5760,"5764":5760,"5765":5760,"5766":5760,"5767":5760,"5768":5760,"5769":5760,"5770":5760,"5771":5760,"5772":5760,"5773":5760,"5774":5760,"5775":5760,"5777":5776,"5778":5776,"5779":5776,"5780":5776,"5781":5776,"5782":5776,"5783":5776,"5784":5776,"5785":5776,"5786":5776,"5787":5776,"5788":5776,"5789":5776,"5790":5776,"5791":5776,"5793":5792,"5794":5792,"5795":5792,"5796":5792,"5797":5792,"5798":5792,"5799":5792,"5800":5792,"5801":5792,"5802":5792,"5803":5792,"5804":5792,"5805":5792,"5806":5792,"5807":5792,"5809":5808,"5810":5808,"5811":5808,"5812":5808,"5813":5808,"5814":5808,"5815":5808,"5816":5808,"5817":5808,"5818":5808,"5819":5808,"5820":5808,"5821":5808,"5822":5808,"5823":5808,"5825":5824,"5826":5824,"5827":5824,"5828":5824,"5829":5824,"5830":5824,"5831":5824,"5832":5824,"5833":5824,"5834":5824,"5835":5824,"5836":5824,"5837":5824,"5838":5824,"5839":5824,"5841":5840,"5842":5840,"5843":5840,"5844":5840,"5845":5840,"5846":5840,"5847":5840,"5848":5840,"5849":5840,"5850":5840,"5851":5840,"5852":5840,"5853":5840,"5854":5840,"5855":5840,"5857":5856,"5858":5856,"5859":5856,"5860":5856,"5861":5856,"5862":5856,"5863":5856,"5864":5856,"5865":5856,"5866":5856,"5867":5856,"5868":5856,"5869":5856,"5870":5856,"5871":5856,"5873":5872,"5874":5872,"5875":5872,"5876":5872,"5877":5872,"5878":5872,"5879":5872,"5880":5872,"5881":5872,"5882":5872,"5883":5872,"5884":5872,"5885":5872,"5886":5872,"5887":5872,"5889":5888,"5890":5888,"5891":5888,"5892":5888,"5893":5888,"5894":5888,"5895":5888,"5896":5888,"5897":5888,"5898":5888,"5899":5888,"5900":5888,"5901":5888,"5902":5888,"5903":5888,"5905":5904,"5906":5904,"5907":5904,"5908":5904,"5909":5904,"5910":5904,"5911":5904,"5912":5904,"5913":5904,"5914":5904,"5915":5904,"5916":5904,"5917":5904,"5918":5904,"5919":5904,"5921":5920,"5922":5920,"5923":5920,"5924":5920,"5925":5920,"5926":5920,"5927":5920,"5928":5920,"5929":5920,"5930":5920,"5931":5920,"5932":5920,"5933":5920,"5934":5920,"5935":5920,"5937":5936,"5938":5936,"5939":5936,"5940":5936,"5941":5936,"5942":5936,"5943":5936,"5944":5936,"5945":5936,"5946":5936,"5947":5936,"5948":5936,"5949":5936,"5950":5936,"5951":5936,"5953":5952,"5954":5952,"5955":5952,"5956":5952,"5957":5952,"5958":5952,"5959":5952,"5960":5952,"5961":5952,"5962":5952,"5963":5952,"5964":5952,"5965":5952,"5966":5952,"5967":5952,"5969":5968,"5970":5968,"5971":5968,"5972":5968,"5973":5968,"5974":5968,"5975":5968,"5976":5968,"5977":5968,"5978":5968,"5979":5968,"5980":5968,"5981":5968,"5982":5968,"5983":5968,"5985":5984,"5986":5984,"5987":5984,"5988":5984,"5989":5984,"5990":5984,"5991":5984,"5992":5984,"5993":5984,"5994":5984,"5995":5984,"5996":5984,"5997":5984,"5998":5984,"5999":5984,"6001":6000,"6002":6000,"6003":6000,"6004":6000,"6005":6000,"6006":6000,"6007":6000,"6008":6000,"6009":6000,"6010":6000,"6011":6000,"6012":6000,"6013":6000,"6014":6000,"6015":6000,"6017":6016,"6018":6016,"6019":6016,"6020":6016,"6021":6016,"6022":6016,"6023":6016,"6024":6016,"6025":6016,"6026":6016,"6027":6016,"6028":6016,"6029":6016,"6030":6016,"6031":6016,"6033":6032,"6034":6032,"6035":6032,"6036":6032,"6037":6032,"6038":6032,"6039":6032,"6040":6032,"6041":6032,"6042":6032,"6043":6032,"6044":6032,"6045":6032,"6046":6032,"6047":6032,"6049":6048,"6050":6048,"6051":6048,"6052":6048,"6053":6048,"6054":6048,"6055":6048,"6056":6048,"6057":6048,"6058":6048,"6059":6048,"6060":6048,"6061":6048,"6062":6048,"6063":6048,"6065":6064,"6066":6064,"6067":6064,"6068":6064,"6069":6064,"6070":6064,"6071":6064,"6072":6064,"6073":6064,"6074":6064,"6075":6064,"6076":6064,"6077":6064,"6078":6064,"6079":6064,"6081":6080,"6082":6080,"6083":6080,"6084":6080,"6085":6080,"6086":6080,"6087":6080,"6088":6080,"6089":6080,"6090":6080,"6091":6080,"6092":6080,"6093":6080,"6094":6080,"6095":6080,"6097":6096,"6098":6096,"6099":6096,"6100":6096,"6101":6096,"6102":6096,"6103":6096,"6104":6096,"6105":6096,"6106":6096,"6107":6096,"6108":6096,"6109":6096,"6110":6096,"6111":6096,"6113":6112,"6114":6112,"6115":6112,"6116":6112,"6117":6112,"6118":6112,"6119":6112,"6120":6112,"6121":6112,"6122":6112,"6123":6112,"6124":6112,"6125":6112,"6126":6112,"6127":6112,"6129":6128,"6130":6128,"6131":6128,"6132":6128,"6133":6128,"6134":6128,"6135":6128,"6136":6128,"6137":6128,"6138":6128,"6139":6128,"6140":6128,"6141":6128,"6142":6128,"6143":6128,"6145":6144,"6146":6144,"6147":6144,"6148":6144,"6149":6144,"6150":6144,"6151":6144,"6152":6144,"6153":6144,"6154":6144,"6155":6144,"6156":6144,"6157":6144,"6158":6144,"6159":6144,"6181":6176,"6182":6176,"6183":6176,"6184":6176,"6185":6176,"6186":6176,"6187":6176,"6188":6176,"6189":6176,"6190":6176,"6191":6176,"6197":6192,"6198":6192,"6199":6192,"6205":6192,"6206":6192,"6207":6192,"6213":6208,"6214":6208,"6215":6208,"6221":6208,"6222":6208,"6223":6208,"6229":6208,"6230":6208,"6231":6208,"6237":6208,"6238":6208,"6239":6208,"6273":6248,"6275":6248,"6277":6248,"6279":6248,"6281":6248,"6283":6248,"6285":6248,"6287":6248,"6305":6304,"6306":6304,"6307":6304,"6308":6304,"6309":6304,"6310":6304,"6311":6304,"6312":6304,"6313":6304,"6314":6304,"6315":6304,"6316":6304,"6317":6304,"6318":6304,"6319":6304,"6326":6320,"6327":6320,"6334":6320,"6335":6320,"6342":6336,"6343":6336,"6350":6336,"6351":6336,"6358":6352,"6359":6352,"6366":6352,"6367":6352,"6374":6368,"6375":6368,"6382":6368,"6383":6368,"6390":6384,"6391":6384,"6398":6384,"6399":6384,"6482":6480,"6483":6480,"6484":6480,"6485":6480,"6486":6480,"6487":6480,"6488":6480,"6489":6480,"6490":6480,"6491":6480,"6492":6480,"6493":6480,"6494":6480,"6495":6480,"6498":6496,"6499":6496,"6500":6496,"6501":6496,"6502":6496,"6503":6496,"6504":6496,"6505":6496,"6506":6496,"6507":6496,"6508":6496,"6509":6496,"6510":6496,"6511":6496,"6514":6512,"6515":6512,"6516":6512,"6517":6512,"6518":6512,"6519":6512,"6520":6512,"6521":6512,"6522":6512,"6523":6512,"6524":6512,"6525":6512,"6526":6512,"6527":6512,"6530":6528,"6531":6528,"6532":6528,"6533":6528,"6534":6528,"6535":6528,"6536":6528,"6537":6528,"6538":6528,"6539":6528,"6540":6528,"6541":6528,"6542":6528,"6543":6528,"6546":6544,"6547":6544,"6548":6544,"6549":6544,"6550":6544,"6551":6544,"6552":6544,"6553":6544,"6554":6544,"6555":6544,"6556":6544,"6557":6544,"6558":6544,"6559":6544,"6564":6562,"6565":6562,"6566":6562,"6567":6562,"6568":6562,"6569":6562,"6570":6562,"6571":6562,"6572":6562,"6573":6562,"6574":6562,"6575":6562,"6584":6580,"6585":6580,"6586":6580,"6587":6580,"6588":6580,"6589":6580,"6590":6580,"6591":6580,"6657":6656,"6658":6656,"6659":6656,"6660":6656,"6661":6656,"6662":6656,"6663":6656,"6664":6656,"6665":6656,"6666":6656,"6667":6656,"6668":6656,"6669":6656,"6670":6656,"6671":6656,"6694":6688,"6695":6688,"6702":6688,"6703":6688,"6705":6704,"6706":6704,"6707":6704,"6708":6704,"6709":6704,"6710":6704,"6711":6704,"6713":6704,"6714":6704,"6715":6704,"6716":6704,"6717":6704,"6718":6704,"6719":6704,"6760":6752,"6761":6753,"6762":6754,"6763":6755,"6764":6756,"6765":6757,"6766":6758,"6767":6759,"6776":6768,"6777":6769,"6778":6770,"6779":6771,"6780":6772,"6792":6787,"6793":6787,"6794":6787,"6795":6787,"6796":6787,"6797":6787,"6798":6787,"6799":6787,"6808":6803,"6809":6803,"6810":6803,"6811":6803,"6812":6803,"6813":6803,"6814":6803,"6815":6803,"6824":6819,"6825":6819,"6826":6819,"6827":6819,"6828":6819,"6829":6819,"6830":6819,"6831":6819,"6840":6835,"6841":6835,"6842":6835,"6843":6835,"6844":6835,"6845":6835,"6846":6835,"6847":6835,"6856":6851,"6857":6851,"6858":6851,"6859":6851,"6860":6851,"6861":6851,"6862":6851,"6863":6851,"6872":6867,"6873":6867,"6874":6867,"6875":6867,"6876":6867,"6877":6867,"6878":6867,"6879":6867,"6888":6883,"6889":6883,"6890":6883,"6891":6883,"6892":6883,"6893":6883,"6894":6883,"6895":6883,"6904":6899,"6905":6899,"6906":6899,"6907":6899,"6908":6899,"6909":6899,"6910":6899,"6911":6899,"6920":6915,"6921":6915,"6922":6915,"6923":6915,"6924":6915,"6925":6915,"6926":6915,"6927":6915,"6936":6931,"6937":6931,"6938":6931,"6939":6931,"6940":6931,"6941":6931,"6942":6931,"6943":6931,"6952":6947,"6953":6947,"6954":6947,"6955":6947,"6956":6947,"6957":6947,"6958":6947,"6959":6947,"6968":6963,"6969":6963,"6970":6963,"6971":6963,"6972":6963,"6973":6963,"6974":6963,"6975":6963,"6992":6994,"6993":6994,"6998":6994,"6999":6994,"7000":6994,"7001":6994,"7002":6994,"7003":6994,"7004":6994,"7005":6994,"7006":6994,"7007":6994,"7009":7008,"7010":7008,"7011":7008,"7012":7008,"7013":7008,"7014":7008,"7015":7008,"7016":7008,"7017":7008,"7018":7008,"7019":7008,"7020":7008,"7021":7008,"7022":7008,"7023":7008,"7032":7027,"7033":7027,"7034":7027,"7035":7027,"7036":7027,"7037":7027,"7038":7027,"7039":7027,"7048":7043,"7049":7043,"7050":7043,"7051":7043,"7052":7043,"7053":7043,"7054":7043,"7055":7043,"7072":7074,"7073":7074,"7078":7074,"7079":7074,"7080":7074,"7081":7074,"7082":7074,"7083":7074,"7084":7074,"7085":7074,"7086":7074,"7087":7074,"7104":7106,"7105":7106,"7110":7106,"7111":7106,"7112":7106,"7113":7106,"7114":7106,"7115":7106,"7116":7106,"7117":7106,"7118":7106,"7119":7106,"7136":7138,"7137":7138,"7142":7138,"7143":7138,"7144":7138,"7145":7138,"7146":7138,"7147":7138,"7148":7138,"7149":7138,"7150":7138,"7151":7138,"7168":7170,"7169":7170,"7174":7170,"7175":7170,"7176":7170,"7177":7170,"7178":7170,"7179":7170,"7180":7170,"7181":7170,"7182":7170,"7183":7170,"7216":7218,"7217":7218,"7222":7218,"7223":7218,"7224":7218,"7225":7218,"7226":7218,"7227":7218,"7228":7218,"7229":7218,"7230":7218,"7231":7218,"7248":7250,"7249":7250,"7254":7250,"7255":7250,"7256":7250,"7257":7250,"7258":7250,"7259":7250,"7260":7250,"7261":7250,"7262":7250,"7263":7250,"7264":7250,"7265":7250,"7270":7250,"7271":7250,"7272":7250,"7273":7250,"7274":7250,"7275":7250,"7276":7250,"7277":7250,"7278":7250,"7279":7250,"7297":7296,"7298":7296,"7299":7296,"7300":7296,"7301":7296,"7302":7296,"7303":7296,"7304":7296,"7305":7296,"7306":7296,"7307":7296,"7308":7296,"7309":7296,"7310":7296,"7311":7296,"7334":7328,"7335":7328,"7342":7328,"7343":7328,"7348":7346,"7349":7346,"7350":7346,"7351":7346,"7352":7346,"7353":7346,"7354":7346,"7355":7346,"7356":7346,"7357":7346,"7358":7346,"7359":7346,"7396":7392,"7397":7392,"7398":7392,"7399":7392,"7400":7392,"7401":7392,"7402":7392,"7403":7392,"7404":7392,"7405":7392,"7406":7392,"7407":7392,"7410":7408,"7411":7408,"7412":7408,"7413":7408,"7414":7408,"7415":7408,"7416":7408,"7417":7408,"7418":7408,"7419":7408,"7420":7408,"7421":7408,"7422":7408,"7423":7408,"7478":7472,"7479":7472,"7486":7472,"7487":7472,"7504":7218,"7505":7218,"7510":7218,"7511":7218,"7512":7218,"7513":7218,"7514":7218,"7515":7218,"7516":7218,"7517":7218,"7518":7218,"7519":7218}} \ No newline at end of file diff --git a/tests/phpunit/block/regenerate_consistency_check.php b/tests/phpunit/block/regenerate_consistency_check.php new file mode 100644 index 0000000000..c8dd68dc50 --- /dev/null +++ b/tests/phpunit/block/regenerate_consistency_check.php @@ -0,0 +1,75 @@ +getAllKnownStates() as $index => $block){ + if($block->getFullId() !== $index){ + $remaps[$index] = $block->getFullId(); + }else{ + $new[$index] = $block->getName(); + } +} +$oldTable = json_decode(file_get_contents(__DIR__ . '/block_factory_consistency_check.json'), true); +if(!is_array($oldTable)){ + throw new \pocketmine\utils\AssumptionFailedError("Old table should be array{knownStates: array, remaps: array}"); +} +$old = $oldTable["knownStates"]; +$oldRemaps = $oldTable["remaps"]; + +foreach($old as $k => $name){ + if(!isset($new[$k])){ + echo "Removed state for $name (" . ($k >> \pocketmine\block\Block::INTERNAL_METADATA_BITS) . ":" . ($k & \pocketmine\block\Block::INTERNAL_METADATA_MASK) . ")\n"; + } +} +foreach($new as $k => $name){ + if(!isset($old[$k])){ + echo "Added state for $name (" . ($k >> \pocketmine\block\Block::INTERNAL_METADATA_BITS) . ":" . ($k & \pocketmine\block\Block::INTERNAL_METADATA_MASK) . ")\n"; + }elseif($old[$k] !== $name){ + echo "Name changed (" . ($k >> \pocketmine\block\Block::INTERNAL_METADATA_BITS) . ":" . ($k & \pocketmine\block\Block::INTERNAL_METADATA_MASK) . "): " . $old[$k] . " -> " . $name . "\n"; + } +} + +foreach($oldRemaps as $index => $mapped){ + if(!isset($remaps[$index])){ + echo "Removed remap of " . ($index >> 4) . ":" . ($index & 0xf) . "\n"; + } +} +foreach($remaps as $index => $mapped){ + if(!isset($oldRemaps[$index])){ + echo "New remap of " . ($index >> 4) . ":" . ($index & 0xf) . " (" . ($mapped >> 4) . ":" . ($mapped & 0xf) . ") (" . $new[$mapped] . ")\n"; + }elseif($oldRemaps[$index] !== $mapped){ + echo "Remap changed for " . ($index >> 4) . ":" . ($index & 0xf) . " (" . ($oldRemaps[$index] >> 4) . ":" . ($oldRemaps[$index] & 0xf) . " (" . $old[$oldRemaps[$index]] . ") -> " . ($mapped >> 4) . ":" . ($mapped & 0xf) . " (" . $new[$mapped] . "))\n"; + } +} +file_put_contents(__DIR__ . '/block_factory_consistency_check.json', json_encode( + [ + "knownStates" => $new, + "remaps" => $remaps + ], +)); diff --git a/tests/phpunit/network/mcpe/protocol/ProtocolInfoTest.php b/tests/phpunit/block/utils/SignTextTest.php similarity index 67% rename from tests/phpunit/network/mcpe/protocol/ProtocolInfoTest.php rename to tests/phpunit/block/utils/SignTextTest.php index e83a919834..3d0c611a12 100644 --- a/tests/phpunit/network/mcpe/protocol/ProtocolInfoTest.php +++ b/tests/phpunit/block/utils/SignTextTest.php @@ -21,17 +21,17 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\block\utils; use PHPUnit\Framework\TestCase; -final class ProtocolInfoTest extends TestCase{ +class SignTextTest extends TestCase{ - public function testMinecraftVersionNetwork() : void{ - self::assertMatchesRegularExpression( - '/^(?:\d+\.)?(?:\d+\.)?(?:\d+\.)?\d+$/', - ProtocolInfo::MINECRAFT_VERSION_NETWORK, - "Network version should only contain 0-9 and \".\", and no more than 4 groups of digits" - ); + public function testConstructorOmitLines() : void{ + $text = new SignText([1 => "test"]); + self::assertSame("", $text->getLine(0)); + self::assertSame("test", $text->getLine(1)); + self::assertSame("", $text->getLine(2)); + self::assertSame("", $text->getLine(3)); } } diff --git a/tests/phpunit/network/mcpe/protocol/DataPacketTest.php b/tests/phpunit/data/bedrock/CoralTypeIdMapTest.php similarity index 66% rename from tests/phpunit/network/mcpe/protocol/DataPacketTest.php rename to tests/phpunit/data/bedrock/CoralTypeIdMapTest.php index 99cd0b4f2d..e0e77297e5 100644 --- a/tests/phpunit/network/mcpe/protocol/DataPacketTest.php +++ b/tests/phpunit/data/bedrock/CoralTypeIdMapTest.php @@ -21,22 +21,18 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol; +namespace pocketmine\data\bedrock; use PHPUnit\Framework\TestCase; +use pocketmine\block\utils\CoralType; -class DataPacketTest extends TestCase{ +class CoralTypeIdMapTest extends TestCase{ - public function testHeaderFidelity() : void{ - $pk = new TestPacket(); - $pk->senderSubId = 3; - $pk->recipientSubId = 2; - $pk->encode(); - - $pk2 = new TestPacket(); - $pk2->setBuffer($pk->getBuffer()); - $pk2->decode(); - self::assertSame($pk2->senderSubId, 3); - self::assertSame($pk2->recipientSubId, 2); + public function testFromIdExhaustiveness() : void{ + foreach(CoralType::getAll() as $type){ + $id = CoralTypeIdMap::getInstance()->toId($type); + $type2 = CoralTypeIdMap::getInstance()->fromId($id); + self::assertTrue($type2 !== null && $type->equals($type2)); + } } } diff --git a/tests/phpunit/data/bedrock/DyeColorIdMapTest.php b/tests/phpunit/data/bedrock/DyeColorIdMapTest.php new file mode 100644 index 0000000000..757cf7921c --- /dev/null +++ b/tests/phpunit/data/bedrock/DyeColorIdMapTest.php @@ -0,0 +1,38 @@ +toId($color); + $color2 = DyeColorIdMap::getInstance()->fromId($id); + self::assertTrue($color2 !== null && $color->equals($color2)); + } + } +} diff --git a/tests/phpunit/data/bedrock/EffectIdMapTest.php b/tests/phpunit/data/bedrock/EffectIdMapTest.php new file mode 100644 index 0000000000..51b5c4d74b --- /dev/null +++ b/tests/phpunit/data/bedrock/EffectIdMapTest.php @@ -0,0 +1,38 @@ +toId($e); + $e2 = EffectIdMap::getInstance()->fromId($id); + self::assertTrue($e === $e2); + } + } +} diff --git a/tests/phpunit/data/bedrock/EnchantmentIdMapTest.php b/tests/phpunit/data/bedrock/EnchantmentIdMapTest.php new file mode 100644 index 0000000000..eb1f35381a --- /dev/null +++ b/tests/phpunit/data/bedrock/EnchantmentIdMapTest.php @@ -0,0 +1,38 @@ +toId($enchantment); + $enchantment2 = EnchantmentIdMap::getInstance()->fromId($id); + self::assertTrue($enchantment === $enchantment2); + } + } +} diff --git a/tests/phpunit/event/HandlerListManagerTest.php b/tests/phpunit/event/HandlerListManagerTest.php new file mode 100644 index 0000000000..7adff6756a --- /dev/null +++ b/tests/phpunit/event/HandlerListManagerTest.php @@ -0,0 +1,94 @@ +) : bool + */ + private $isValidFunc; + /** + * @var \Closure + * @phpstan-var \Closure(\ReflectionClass) : ?\ReflectionClass + */ + private $resolveParentFunc; + + public function setUp() : void{ + /** @see HandlerListManager::isValidClass() */ + $this->isValidFunc = (new \ReflectionMethod(HandlerListManager::class, 'isValidClass'))->getClosure(); + /** @see HandlerListManager::resolveNearestHandleableParent() */ + $this->resolveParentFunc = (new \ReflectionMethod(HandlerListManager::class, 'resolveNearestHandleableParent'))->getClosure(); + } + + /** + * @return \Generator|mixed[][] + * @phpstan-return \Generator, bool, string}, void, void> + */ + public function isValidClassProvider() : \Generator{ + yield [new \ReflectionClass(Event::class), false, "event base should not be handleable"]; + yield [new \ReflectionClass(TestConcreteEvent::class), true, ""]; + yield [new \ReflectionClass(TestAbstractEvent::class), false, "abstract event cannot be handled"]; + yield [new \ReflectionClass(TestAbstractAllowHandleEvent::class), true, "abstract event declaring @allowHandle should be handleable"]; + } + + /** + * @dataProvider isValidClassProvider + * + * @phpstan-param \ReflectionClass $class + */ + public function testIsValidClass(\ReflectionClass $class, bool $isValid, string $reason) : void{ + self::assertSame($isValid, ($this->isValidFunc)($class), $reason); + } + + /** + * @return \Generator|\ReflectionClass[][] + * @phpstan-return \Generator, \ReflectionClass|null}, void, void> + */ + public function resolveParentClassProvider() : \Generator{ + yield [new \ReflectionClass(TestConcreteExtendsAllowHandleEvent::class), new \ReflectionClass(TestAbstractAllowHandleEvent::class)]; + yield [new \ReflectionClass(TestConcreteEvent::class), null]; + yield [new \ReflectionClass(TestConcreteExtendsAbstractEvent::class), null]; + yield [new \ReflectionClass(TestConcreteExtendsConcreteEvent::class), new \ReflectionClass(TestConcreteEvent::class)]; + } + + /** + * @dataProvider resolveParentClassProvider + * + * @phpstan-param \ReflectionClass $class + * @phpstan-param \ReflectionClass|null $expect + */ + public function testResolveParentClass(\ReflectionClass $class, ?\ReflectionClass $expect) : void{ + if($expect === null){ + self::assertNull(($this->resolveParentFunc)($class)); + }else{ + $actualParent = ($this->resolveParentFunc)($class); + self::assertNotNull($actualParent); + self::assertSame($actualParent->getName(), $expect->getName()); + } + } +} diff --git a/tests/phpunit/event/TestAbstractAllowHandleEvent.php b/tests/phpunit/event/TestAbstractAllowHandleEvent.php new file mode 100644 index 0000000000..382660714b --- /dev/null +++ b/tests/phpunit/event/TestAbstractAllowHandleEvent.php @@ -0,0 +1,31 @@ +setCustomName("TEST"); + $inv = new SimpleInventory(1); + $item1 = ItemFactory::getInstance()->get(ItemIds::ARROW, 0, 1); + $item2 = ItemFactory::getInstance()->get(ItemIds::ARROW, 0, 1)->setCustomName("TEST"); $inv->addItem(clone $item1); self::assertFalse($inv->canAddItem($item2), "Item WITHOUT userdata should not stack with item WITH userdata"); @@ -56,4 +47,65 @@ class BaseInventoryTest extends TestCase{ self::assertFalse($inv->canAddItem($item1), "Item WITH userdata should not stack with item WITHOUT userdata"); self::assertNotEmpty($inv->addItem($item1)); } + + /** + * @return Item[] + */ + private function getTestItems() : array{ + return [ + VanillaItems::APPLE()->setCount(16), + VanillaItems::APPLE()->setCount(16), + VanillaItems::APPLE()->setCount(16), + VanillaItems::APPLE()->setCount(16) + ]; + } + + public function testAddMultipleItemsInOneCall() : void{ + $inventory = new SimpleInventory(1); + $leftover = $inventory->addItem(...$this->getTestItems()); + self::assertCount(0, $leftover); + self::assertTrue($inventory->getItem(0)->equalsExact(VanillaItems::APPLE()->setCount(64))); + } + + public function testAddMultipleItemsInOneCallWithLeftover() : void{ + $inventory = new SimpleInventory(1); + $inventory->setItem(0, VanillaItems::APPLE()->setCount(20)); + $leftover = $inventory->addItem(...$this->getTestItems()); + self::assertCount(2, $leftover); //the leftovers are not currently stacked - if they were given separately, they'll be returned separately + self::assertTrue($inventory->getItem(0)->equalsExact(VanillaItems::APPLE()->setCount(64))); + + $leftoverCount = 0; + foreach($leftover as $item){ + self::assertTrue($item->equals(VanillaItems::APPLE())); + $leftoverCount += $item->getCount(); + } + self::assertSame(20, $leftoverCount); + } + + public function testAddItemWithOversizedCount() : void{ + $inventory = new SimpleInventory(10); + $leftover = $inventory->addItem(VanillaItems::APPLE()->setCount(100)); + self::assertCount(0, $leftover); + + $count = 0; + foreach($inventory->getContents() as $item){ + self::assertTrue($item->equals(VanillaItems::APPLE())); + $count += $item->getCount(); + } + self::assertSame(100, $count); + } + + public function testGetAddableItemQuantityStacking() : void{ + $inventory = new SimpleInventory(1); + $inventory->addItem(VanillaItems::APPLE()->setCount(60)); + self::assertSame(2, $inventory->getAddableItemQuantity(VanillaItems::APPLE()->setCount(2))); + self::assertSame(4, $inventory->getAddableItemQuantity(VanillaItems::APPLE()->setCount(6))); + } + + public function testGetAddableItemQuantityEmptyStack() : void{ + $inventory = new SimpleInventory(1); + $item = VanillaItems::APPLE(); + $item->setCount($item->getMaxStackSize()); + self::assertSame($item->getMaxStackSize(), $inventory->getAddableItemQuantity($item)); + } } diff --git a/tests/phpunit/level/format/io/LevelProviderManagerTest.php b/tests/phpunit/item/BannerTest.php similarity index 50% rename from tests/phpunit/level/format/io/LevelProviderManagerTest.php rename to tests/phpunit/item/BannerTest.php index 64b6206225..1aafa82a21 100644 --- a/tests/phpunit/level/format/io/LevelProviderManagerTest.php +++ b/tests/phpunit/item/BannerTest.php @@ -21,33 +21,30 @@ declare(strict_types=1); -namespace pocketmine\level\format\io; +namespace pocketmine\item; use PHPUnit\Framework\TestCase; +use pocketmine\block\utils\BannerPatternLayer; +use pocketmine\block\utils\BannerPatternType; +use pocketmine\block\utils\DyeColor; +use pocketmine\block\VanillaBlocks; +use function assert; -class LevelProviderManagerTest extends TestCase{ +final class BannerTest extends TestCase{ - public function testAddNonClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); + public function testBannerPatternSaveRestore() : void{ + $item = VanillaBlocks::BANNER()->asItem(); + assert($item instanceof Banner); + $item->setPatterns([ + new BannerPatternLayer(BannerPatternType::FLOWER(), DyeColor::RED()) + ]); + $data = $item->nbtSerialize(); - LevelProviderManager::addProvider("lol"); - } - - public function testAddAbstractClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - LevelProviderManager::addProvider(AbstractLevelProvider::class); - } - - public function testAddInterfaceProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - LevelProviderManager::addProvider(InterfaceLevelProvider::class); - } - - public function testAddWrongClassProvider() : void{ - $this->expectException(\InvalidArgumentException::class); - - LevelProviderManager::addProvider(LevelProviderManagerTest::class); + $item2 = Item::nbtDeserialize($data); + self::assertTrue($item->equalsExact($item2)); + self::assertInstanceOf(Banner::class, $item2); + $patterns = $item2->getPatterns(); + self::assertCount(1, $patterns); + self::assertTrue(BannerPatternType::FLOWER()->equals($patterns[0]->getType())); } } diff --git a/tests/phpunit/item/ItemFactoryTest.php b/tests/phpunit/item/ItemFactoryTest.php new file mode 100644 index 0000000000..da8533f669 --- /dev/null +++ b/tests/phpunit/item/ItemFactoryTest.php @@ -0,0 +1,56 @@ +isRegistered($id), ItemFactory::getInstance()->isRegistered($id)); + } + } + + /** + * Test that durable items are correctly created by the item factory + */ + public function testGetDurableItem() : void{ + self::assertInstanceOf(Sword::class, $i1 = ItemFactory::getInstance()->get(ItemIds::WOODEN_SWORD)); + /** @var Sword $i1 */ + self::assertSame(0, $i1->getDamage()); + self::assertInstanceOf(Sword::class, $i2 = ItemFactory::getInstance()->get(ItemIds::WOODEN_SWORD, 1)); + /** @var Sword $i2 */ + self::assertSame(1, $i2->getDamage()); + } + + public function testGetDurableItemWithTooLargeDurability() : void{ + self::assertInstanceOf(Sword::class, ItemFactory::getInstance()->get(ItemIds::WOODEN_SWORD, ToolTier::WOOD()->getMaxDurability())); + ItemFactory::getInstance()->get(ItemIds::WOODEN_SWORD, ToolTier::WOOD()->getMaxDurability() + 1); + } +} diff --git a/tests/phpunit/item/ItemTest.php b/tests/phpunit/item/ItemTest.php index cea63730c4..846b23f5fb 100644 --- a/tests/phpunit/item/ItemTest.php +++ b/tests/phpunit/item/ItemTest.php @@ -24,23 +24,23 @@ declare(strict_types=1); namespace pocketmine\item; use PHPUnit\Framework\TestCase; -use pocketmine\block\BlockFactory; -use pocketmine\item\enchantment\Enchantment; use pocketmine\item\enchantment\EnchantmentInstance; +use pocketmine\item\enchantment\VanillaEnchantments; class ItemTest extends TestCase{ + /** @var Item */ + private $item; + public function setUp() : void{ - BlockFactory::init(); - ItemFactory::init(); - Enchantment::init(); + $this->item = ItemFactory::getInstance()->get(ItemIds::DIAMOND_SWORD); } /** * Test for issue #1145 (items aren't considered equal after NBT serializing and deserializing */ public function testItemEquals() : void{ - $item = ItemFactory::get(Item::STONE)->setCustomName("HI"); + $item = ItemFactory::getInstance()->get(ItemIds::STONE)->setCustomName("HI"); $item2 = Item::nbtDeserialize($item->nbtSerialize()); self::assertTrue($item2->equals($item)); self::assertTrue($item->equals($item2)); @@ -50,53 +50,99 @@ class ItemTest extends TestCase{ * Test that same items without NBT are considered equal */ public function testItemEqualsNoNbt() : void{ - $item1 = ItemFactory::get(Item::DIAMOND_SWORD); + $item1 = ItemFactory::getInstance()->get(ItemIds::DIAMOND_SWORD); $item2 = clone $item1; self::assertTrue($item1->equals($item2)); } /** - * Tests that blocks are considered to be valid registered items + * Tests whether items retain their display properties + * after being deserialized */ - public function testItemBlockRegistered() : void{ - for($id = 0; $id < 256; ++$id){ - self::assertEquals(BlockFactory::isRegistered($id), ItemFactory::isRegistered($id)); + public function testItemPersistsDisplayProperties() : void{ + $lore = ["Line A", "Line B"]; + $name = "HI"; + $item = ItemFactory::getInstance()->get(ItemIds::DIAMOND_SWORD); + $item->setCustomName($name); + $item->setLore($lore); + $item = Item::nbtDeserialize($item->nbtSerialize()); + self::assertTrue($item->getCustomName() === $name); + self::assertTrue($item->getLore() === $lore); + } + + public function testHasEnchantment() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 5)); + self::assertTrue($this->item->hasEnchantment(VanillaEnchantments::EFFICIENCY())); + self::assertTrue($this->item->hasEnchantment(VanillaEnchantments::EFFICIENCY(), 5)); + } + + public function testHasEnchantments() : void{ + self::assertFalse($this->item->hasEnchantments()); + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FIRE_ASPECT())); + self::assertTrue($this->item->hasEnchantments()); + } + + public function testGetEnchantmentLevel() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 5)); + self::assertSame(5, $this->item->getEnchantmentLevel(VanillaEnchantments::EFFICIENCY())); + } + + public function testGetEnchantments() : void{ + /** @var EnchantmentInstance[] $enchantments */ + $enchantments = [ + new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 5), + new EnchantmentInstance(VanillaEnchantments::SHARPNESS(), 1) + ]; + foreach($enchantments as $enchantment){ + $this->item->addEnchantment($enchantment); } + foreach($this->item->getEnchantments() as $enchantment){ + foreach($enchantments as $k => $applied){ + if($enchantment->getType() === $applied->getType() and $enchantment->getLevel() === $applied->getLevel()){ + unset($enchantments[$k]); + continue 2; + } + } + self::fail("Unknown extra enchantment found"); + } + self::assertEmpty($enchantments, "Expected all enchantments to be present"); + } + + public function testOverwriteEnchantment() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS())); + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS(), 5)); + self::assertSame(5, $this->item->getEnchantmentLevel(VanillaEnchantments::SHARPNESS())); + } + + public function testRemoveAllEnchantments() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FIRE_ASPECT())); + self::assertCount(1, $this->item->getEnchantments()); + $this->item->removeEnchantments(); + self::assertEmpty($this->item->getEnchantments()); + } + + public function testRemoveEnchantment() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::KNOCKBACK())); + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS())); + self::assertCount(2, $this->item->getEnchantments()); + $this->item->removeEnchantment(VanillaEnchantments::SHARPNESS()); + self::assertFalse($this->item->hasEnchantment(VanillaEnchantments::SHARPNESS())); + } + + public function testRemoveEnchantmentLevel() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FIRE_ASPECT(), 2)); + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING())); + self::assertCount(2, $this->item->getEnchantments()); + $this->item->removeEnchantment(VanillaEnchantments::FIRE_ASPECT(), 2); + self::assertFalse($this->item->hasEnchantment(VanillaEnchantments::FIRE_ASPECT())); } /** * Tests that when all enchantments are removed from an item, the "ench" tag is removed as well */ - public function testEnchantmentRemoval() : void{ - $item = ItemFactory::get(Item::DIAMOND_SWORD); - $item->addEnchantment(new EnchantmentInstance(Enchantment::getEnchantment(Enchantment::SHARPNESS))); - $item->removeEnchantment(Enchantment::SHARPNESS); - self::assertNull($item->getNamedTag()->getTag(Item::TAG_ENCH)); - } - - /** - * @return mixed[][] - * @phpstan-return list - */ - public function itemFromStringProvider() : array{ - return [ - ["dye:4", ItemIds::DYE, 4], - ["351", ItemIds::DYE, 0], - ["351:4", ItemIds::DYE, 4], - ["stone:3", ItemIds::STONE, 3], - ["minecraft:string", ItemIds::STRING, 0], - ["diamond_pickaxe", ItemIds::DIAMOND_PICKAXE, 0], - ["diamond_pickaxe:5", ItemIds::DIAMOND_PICKAXE, 5] - ]; - } - - /** - * @dataProvider itemFromStringProvider - */ - public function testFromStringSingle(string $string, int $id, int $meta) : void{ - $item = ItemFactory::fromStringSingle($string); - - self::assertEquals($id, $item->getId()); - self::assertEquals($meta, $item->getDamage()); + public function testRemoveAllEnchantmentsNBT() : void{ + $this->item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS(), 1)); + $this->item->removeEnchantment(VanillaEnchantments::SHARPNESS()); + self::assertNull($this->item->getNamedTag()->getTag(Item::TAG_ENCH)); } } diff --git a/tests/phpunit/item/LegacyStringToItemParserTest.php b/tests/phpunit/item/LegacyStringToItemParserTest.php new file mode 100644 index 0000000000..546a383bfd --- /dev/null +++ b/tests/phpunit/item/LegacyStringToItemParserTest.php @@ -0,0 +1,55 @@ + + */ + public function itemFromStringProvider() : array{ + return [ + ["dye:4", ItemIds::DYE, 4], + ["351", ItemIds::DYE, 0], + ["351:4", ItemIds::DYE, 4], + ["stone:3", ItemIds::STONE, 3], + ["minecraft:string", ItemIds::STRING, 0], + ["diamond_pickaxe", ItemIds::DIAMOND_PICKAXE, 0], + ["diamond_pickaxe:5", ItemIds::DIAMOND_PICKAXE, 5] + ]; + } + + /** + * @dataProvider itemFromStringProvider + */ + public function testFromStringSingle(string $string, int $id, int $meta) : void{ + $item = LegacyStringToItemParser::getInstance()->parse($string); + + self::assertEquals($id, $item->getId()); + self::assertEquals($meta, $item->getMeta()); + } +} diff --git a/tests/phpunit/level/format/io/AbstractLevelProvider.php b/tests/phpunit/level/format/io/AbstractLevelProvider.php deleted file mode 100644 index 7f63048e17..0000000000 --- a/tests/phpunit/level/format/io/AbstractLevelProvider.php +++ /dev/null @@ -1,28 +0,0 @@ -stupidJsonDecodeFunc = (new \ReflectionMethod(PlayerNetworkSessionAdapter::class, 'stupid_json_decode'))->getClosure(); + $this->stupidJsonDecodeFunc = (new \ReflectionMethod(InGamePacketHandler::class, 'stupid_json_decode'))->getClosure(); } /** diff --git a/tests/phpunit/network/mcpe/protocol/TestPacket.php b/tests/phpunit/network/mcpe/protocol/TestPacket.php deleted file mode 100644 index b38e04ec4f..0000000000 --- a/tests/phpunit/network/mcpe/protocol/TestPacket.php +++ /dev/null @@ -1,34 +0,0 @@ - + */ + public function compatibleApiProvider() : \Generator{ + yield ["3.0.0", "3.0.0", true]; + yield ["3.1.0", "3.0.0", true]; + yield ["3.0.0", "3.1.0", false]; + yield ["3.1.0", "3.0.1", true]; //old bug where minor wasn't respected when comparing patches + yield ["3.0.0", "4.0.0", false]; + yield ["4.0.0", "3.0.0", false]; + yield ["3.0.0", "3.0.1", false]; //bug fix patch required + yield ["3.0.1", "3.0.0", true]; + yield ["3.0.0-ALPHA1", "3.0.0-ALPHA2", true]; + yield ["3.0.0-ALPHA2", "3.0.0-ALPHA1", true]; //at the time these weren't actually compatible, but these are just test samples. + yield ["3.0.0-ALPHA1", "3.0.0-ALPHA1", true]; + yield ["3.0.0-ALPHA1", "4.0.0-ALPHA1", false]; + } + + /** + * @dataProvider compatibleApiProvider + */ + public function testCompatibleApi(string $myVersion, string $wantVersion, bool $expected) : void{ + self::assertSame($expected, ApiVersion::isCompatible($myVersion, [$wantVersion]), "my version: $myVersion, their version: $wantVersion, expect " . ($expected ? "yes" : "no")); + } + + /** + * @return mixed[][][] + * @phpstan-return \Generator, list}, void, void> + */ + public function ambiguousVersionsProvider() : \Generator{ + yield [["3.0.0"], []]; + yield [["3.0.0", "3.0.1"], ["3.0.0", "3.0.1"]]; + yield [["3.0.0", "3.1.0", "4.0.0"], ["3.0.0", "3.1.0"]]; + yield [["3.0.0", "4.0.0"], []]; + yield [["3.0.0-ALPHA1", "3.0.0-ALPHA2"], []]; + } + + /** + * @dataProvider ambiguousVersionsProvider + * + * @param string[] $input + * @param string[] $expectedOutput + */ + public function testFindAmbiguousVersions(array $input, array $expectedOutput) : void{ + $ambiguous = ApiVersion::checkAmbiguousVersions($input); + + sort($expectedOutput); + sort($ambiguous); + + self::assertSame($expectedOutput, $ambiguous); + } +} diff --git a/tests/phpunit/scheduler/AsyncPoolTest.php b/tests/phpunit/scheduler/AsyncPoolTest.php new file mode 100644 index 0000000000..e2aedfb939 --- /dev/null +++ b/tests/phpunit/scheduler/AsyncPoolTest.php @@ -0,0 +1,72 @@ +mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), false, "Main", new \DateTimeZone('UTC')); + $this->pool = new AsyncPool(2, 1024, new \BaseClassLoader(), $this->mainLogger, new SleeperHandler()); + } + + public function tearDown() : void{ + $this->pool->shutdown(); + $this->mainLogger->shutdownLogWriterThread(); + } + + public function testTaskLeak() : void{ + $start = microtime(true); + $this->pool->submitTask(new LeakTestAsyncTask()); + while(!LeakTestAsyncTask::$destroyed and microtime(true) < $start + 30){ + usleep(50 * 1000); + $this->pool->collectTasks(); + } + self::assertTrue(LeakTestAsyncTask::$destroyed, "Task was not destroyed after 30 seconds"); + } + + public function testPublishProgressRace() : void{ + $task = new PublishProgressRaceAsyncTask(); + $this->pool->submitTask($task); + while($this->pool->collectTasks()){ + usleep(50 * 1000); + } + self::assertTrue(PublishProgressRaceAsyncTask::$success, "Progress was not reported before task completion"); + } +} diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandOutputMessage.php b/tests/phpunit/scheduler/LeakTestAsyncTask.php similarity index 72% rename from src/pocketmine/network/mcpe/protocol/types/CommandOutputMessage.php rename to tests/phpunit/scheduler/LeakTestAsyncTask.php index a2434f9905..942b751d78 100644 --- a/src/pocketmine/network/mcpe/protocol/types/CommandOutputMessage.php +++ b/tests/phpunit/scheduler/LeakTestAsyncTask.php @@ -21,14 +21,19 @@ declare(strict_types=1); -namespace pocketmine\network\mcpe\protocol\types; +namespace pocketmine\scheduler; -class CommandOutputMessage{ +use function usleep; + +class LeakTestAsyncTask extends AsyncTask{ /** @var bool */ - public $isInternal; - /** @var string */ - public $messageId; - /** @var string[] */ - public $parameters = []; + public static $destroyed = false; + public function onRun() : void{ + usleep(50 * 1000); //1 server tick + } + + protected function reallyDestruct() : void{ + self::$destroyed = true; + } } diff --git a/src/pocketmine/block/WallSign.php b/tests/phpunit/scheduler/PublishProgressRaceAsyncTask.php similarity index 68% rename from src/pocketmine/block/WallSign.php rename to tests/phpunit/scheduler/PublishProgressRaceAsyncTask.php index 9714ad5eb0..6c70d26202 100644 --- a/src/pocketmine/block/WallSign.php +++ b/tests/phpunit/scheduler/PublishProgressRaceAsyncTask.php @@ -21,19 +21,20 @@ declare(strict_types=1); -namespace pocketmine\block; +namespace pocketmine\scheduler; -class WallSign extends SignPost{ +class PublishProgressRaceAsyncTask extends AsyncTask{ + /** @var bool */ + public static $success = false; - protected $id = self::WALL_SIGN; - - public function getName() : string{ - return "Wall Sign"; + public function onRun() : void{ + $this->publishProgress("hello"); } - public function onNearbyBlockChange() : void{ - if($this->getSide($this->meta ^ 0x01)->getId() === self::AIR){ - $this->getLevelNonNull()->useBreakOn($this); + public function onProgressUpdate($progress) : void{ + if($progress === "hello"){ + // thread local on main thread + self::$success = true; } } } diff --git a/tests/phpunit/scheduler/TaskSchedulerTest.php b/tests/phpunit/scheduler/TaskSchedulerTest.php new file mode 100644 index 0000000000..bcb1ae97e6 --- /dev/null +++ b/tests/phpunit/scheduler/TaskSchedulerTest.php @@ -0,0 +1,48 @@ +scheduler = new TaskScheduler(); + } + + public function tearDown() : void{ + $this->scheduler->shutdown(); + } + + public function testCancel() : void{ + $task = $this->scheduler->scheduleTask(new ClosureTask(function() : void{ + throw new CancelTaskException(); + })); + $this->scheduler->mainThreadHeartbeat(0); + self::assertTrue($task->isCancelled(), "Task was not cancelled"); + } +} diff --git a/tests/phpunit/utils/CloningRegistryTraitTest.php b/tests/phpunit/utils/CloningRegistryTraitTest.php new file mode 100644 index 0000000000..69ca0a4110 --- /dev/null +++ b/tests/phpunit/utils/CloningRegistryTraitTest.php @@ -0,0 +1,54 @@ + + */ + public function cloningRegistryMembersProvider() : \Generator{ + yield [function() : \stdClass{ return TestCloningRegistry::TEST1(); }]; + yield [function() : \stdClass{ return TestCloningRegistry::TEST2(); }]; + yield [function() : \stdClass{ return TestCloningRegistry::TEST3(); }]; + } + + /** + * @dataProvider cloningRegistryMembersProvider + * @phpstan-param \Closure() : \stdClass $provider + */ + public function testEachMemberClone(\Closure $provider) : void{ + self::assertNotSame($provider(), $provider(), "Cloning registry should never return the same object twice"); + } + + public function testGetAllClone() : void{ + $list1 = TestCloningRegistry::getAll(); + $list2 = TestCloningRegistry::getAll(); + foreach($list1 as $k => $member){ + self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members"); + } + } +} diff --git a/src/pocketmine/Collectable.php b/tests/phpunit/utils/EnumTraitTest.php similarity index 74% rename from src/pocketmine/Collectable.php rename to tests/phpunit/utils/EnumTraitTest.php index 238cfcfe6e..d58c61d08a 100644 --- a/src/pocketmine/Collectable.php +++ b/tests/phpunit/utils/EnumTraitTest.php @@ -21,21 +21,18 @@ declare(strict_types=1); -namespace pocketmine; +namespace pocketmine\utils; -abstract class Collectable extends \Threaded{ +use PHPUnit\Framework\TestCase; - /** @var bool */ - private $isGarbage = false; - - public function isGarbage() : bool{ - return $this->isGarbage; - } +class EnumTraitTest extends TestCase{ /** - * @return void + * @doesNotPerformAssertions */ - public function setGarbage(){ - $this->isGarbage = true; + public function testEnumLazyInit() : void{ + foreach([TestEnum::ONE(), TestEnum::TWO(), TestEnum::THREE()] as $member){ + //NOOP + } } } diff --git a/tests/phpunit/utils/TestCloningRegistry.php b/tests/phpunit/utils/TestCloningRegistry.php new file mode 100644 index 0000000000..edc7a4102b --- /dev/null +++ b/tests/phpunit/utils/TestCloningRegistry.php @@ -0,0 +1,58 @@ + */"); + self::assertArrayHasKey("phpstan-return", $tags); + self::assertEquals("list", $tags["phpstan-return"]); + } + public function testNamespacedNiceClosureName() : void{ //be careful with this test. The closure has to be declared on the same line as the assertion. - self::assertSame('closure@' . Utils::cleanPath(__FILE__) . '#L' . __LINE__, Utils::getNiceClosureName(function() : void{})); + self::assertSame('closure@' . Filesystem::cleanPath(__FILE__) . '#L' . __LINE__, Utils::getNiceClosureName(function() : void{})); + } + + /** + * @return string[][] + * @return list + */ + public function validInstanceProvider() : array{ + return [ + //direct instance / implement / extend + [TestInstantiableClass::class, TestInstantiableClass::class], + [TestInstantiableClass::class, TestAbstractClass::class], + [TestInstantiableClass::class, TestInterface::class], + + //inherited + [TestSubclassOfInstantiableClass::class, TestInstantiableClass::class], + [TestSubclassOfInstantiableClass::class, TestAbstractClass::class], + [TestSubclassOfInstantiableClass::class, TestInterface::class] + ]; + } + + /** + * @dataProvider validInstanceProvider + * @doesNotPerformAssertions + * @phpstan-param class-string $className + * @phpstan-param class-string $baseName + */ + public function testValidInstanceWithValidCombinations(string $className, string $baseName) : void{ + Utils::testValidInstance($className, $baseName); + } + + /** + * @return string[][] + * @return list + */ + public function validInstanceInvalidCombinationsProvider() : array{ + return [ + ["iDontExist abc", TestInstantiableClass::class], + [TestInstantiableClass::class, "iDon'tExist abc"], + ["iDontExist", "iAlsoDontExist"], + [TestInstantiableClass::class, TestTrait::class], + [TestTrait::class, TestTrait::class], + [TestAbstractClass::class, TestAbstractClass::class], + [TestInterface::class, TestInterface::class], + [TestInstantiableClass::class, TestSubclassOfInstantiableClass::class] + ]; + } + + /** + * @dataProvider validInstanceInvalidCombinationsProvider + */ + public function testValidInstanceInvalidParameters(string $className, string $baseName) : void{ + $this->expectException(\InvalidArgumentException::class); + Utils::testValidInstance($className, $baseName); //@phpstan-ignore-line } } diff --git a/tests/phpunit/utils/fixtures/TestAbstractClass.php b/tests/phpunit/utils/fixtures/TestAbstractClass.php new file mode 100644 index 0000000000..212806c4f4 --- /dev/null +++ b/tests/phpunit/utils/fixtures/TestAbstractClass.php @@ -0,0 +1,9 @@ +setFullBlock(0, 0, 0, 1); + $chunk->setBiomeId(0, 0, 1); + $chunk->setHeightMap(0, 0, 1); + + $chunk2 = clone $chunk; + $chunk2->setFullBlock(0, 0, 0, 2); + $chunk2->setBiomeId(0, 0, 2); + $chunk2->setHeightMap(0, 0, 2); + + self::assertNotSame($chunk->getFullBlock(0, 0, 0), $chunk2->getFullBlock(0, 0, 0)); + self::assertNotSame($chunk->getBiomeId(0, 0), $chunk2->getBiomeId(0, 0)); + self::assertNotSame($chunk->getHeightMap(0, 0), $chunk2->getHeightMap(0, 0)); + } +} diff --git a/tests/phpunit/world/format/SubChunkTest.php b/tests/phpunit/world/format/SubChunkTest.php new file mode 100644 index 0000000000..61fd9dee19 --- /dev/null +++ b/tests/phpunit/world/format/SubChunkTest.php @@ -0,0 +1,50 @@ +setFullBlock(0, 0, 0, 1); + $sub1->getBlockLightArray()->set(0, 0, 0, 1); + $sub1->getBlockSkyLightArray()->set(0, 0, 0, 1); + + $sub2 = clone $sub1; + + $sub2->setFullBlock(0, 0, 0, 2); + $sub2->getBlockLightArray()->set(0, 0, 0, 2); + $sub2->getBlockSkyLightArray()->set(0, 0, 0, 2); + + self::assertNotSame($sub1->getFullBlock(0, 0, 0), $sub2->getFullBlock(0, 0, 0)); + self::assertNotSame($sub1->getBlockLightArray()->get(0, 0, 0), $sub2->getBlockLightArray()->get(0, 0, 0)); + self::assertNotSame($sub1->getBlockSkyLightArray()->get(0, 0, 0), $sub2->getBlockSkyLightArray()->get(0, 0, 0)); + } +} diff --git a/tests/phpunit/level/format/io/region/RegionLoaderTest.php b/tests/phpunit/world/format/io/region/RegionLoaderTest.php similarity index 74% rename from tests/phpunit/level/format/io/region/RegionLoaderTest.php rename to tests/phpunit/world/format/io/region/RegionLoaderTest.php index a99ffcb21b..7bb72b5d6d 100644 --- a/tests/phpunit/level/format/io/region/RegionLoaderTest.php +++ b/tests/phpunit/world/format/io/region/RegionLoaderTest.php @@ -21,10 +21,13 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; use PHPUnit\Framework\TestCase; -use pocketmine\level\format\ChunkException; +use pocketmine\world\format\ChunkException; +use Webmozart\PathUtil\Path; +use function bin2hex; +use function clearstatcache; use function file_exists; use function random_bytes; use function str_repeat; @@ -39,12 +42,11 @@ class RegionLoaderTest extends TestCase{ private $region; public function setUp() : void{ - $this->regionPath = sys_get_temp_dir() . '/test.testregion'; + $this->regionPath = Path::join(sys_get_temp_dir(), 'test.testregion'); if(file_exists($this->regionPath)){ unlink($this->regionPath); } - $this->region = new RegionLoader($this->regionPath, 0, 0); - $this->region->open(); + $this->region = RegionLoader::createNew($this->regionPath); } public function tearDown() : void{ @@ -64,8 +66,7 @@ class RegionLoaderTest extends TestCase{ $this->region->writeChunk(0, 0, $data); $this->region->close(); - $r = new RegionLoader($this->regionPath, 0, 0); - $r->open(); + $r = RegionLoader::loadExisting($this->regionPath); self::assertSame($data, $r->readChunk(0, 0)); } @@ -109,7 +110,7 @@ class RegionLoaderTest extends TestCase{ * @dataProvider outOfBoundsCoordsProvider * * @throws \InvalidArgumentException - * @throws \pocketmine\level\format\io\exception\CorruptedChunkException + * @throws \pocketmine\world\format\io\exception\CorruptedChunkException */ public function testReadChunkOutOfBounds(int $x, int $z) : void{ $this->expectException(\InvalidArgumentException::class); @@ -121,13 +122,27 @@ class RegionLoaderTest extends TestCase{ */ public function testRegionHeaderCachedFilesizeRegression() : void{ $this->region->close(); - $region = new RegionLoader($this->regionPath, 0, 0); //now we have a region, so the header will be verified, triggering two filesize() calls - $region->open(); + $region = RegionLoader::loadExisting($this->regionPath); //now we have a region, so the header will be verified, triggering two filesize() calls $data = str_repeat("hello", 2000); $region->writeChunk(0, 0, $data); //add some data to the end of the file, to make the cached filesize invalid $region->close(); - $region = new RegionLoader($this->regionPath, 0, 0); - $region->open(); + $region = RegionLoader::loadExisting($this->regionPath); self::assertSame($data, $region->readChunk(0, 0)); } + + public function testCreateNewWithExistingRegion() : void{ + $this->region->close(); + $this->expectException(\RuntimeException::class); + RegionLoader::createNew($this->regionPath); + } + + public function testLoadExistingWithMissingFile() : void{ + clearstatcache(); + + do{ + $randfile = Path::join(sys_get_temp_dir(), bin2hex(random_bytes(6)) . ".mca"); + }while(file_exists($randfile)); + $this->expectException(\RuntimeException::class); + RegionLoader::loadExisting($randfile); + } } diff --git a/tests/phpunit/level/format/io/region/RegionLocationTableEntryTest.php b/tests/phpunit/world/format/io/region/RegionLocationTableEntryTest.php similarity index 98% rename from tests/phpunit/level/format/io/region/RegionLocationTableEntryTest.php rename to tests/phpunit/world/format/io/region/RegionLocationTableEntryTest.php index c87a537a1e..c3989cbd77 100644 --- a/tests/phpunit/level/format/io/region/RegionLocationTableEntryTest.php +++ b/tests/phpunit/world/format/io/region/RegionLocationTableEntryTest.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace pocketmine\level\format\io\region; +namespace pocketmine\world\format\io\region; use PHPUnit\Framework\TestCase; use function sprintf; diff --git a/tests/plugins/DevTools b/tests/plugins/DevTools index 1606a4307b..6af57741e6 160000 --- a/tests/plugins/DevTools +++ b/tests/plugins/DevTools @@ -1 +1 @@ -Subproject commit 1606a4307bae044c0c4a5e287a4fd958bfa05918 +Subproject commit 6af57741e67a928e2782cebe8dd422044d16dc04 diff --git a/tests/plugins/TesterPlugin/plugin.yml b/tests/plugins/TesterPlugin/plugin.yml index 9314ce5f4a..dc00d97c79 100644 --- a/tests/plugins/TesterPlugin/plugin.yml +++ b/tests/plugins/TesterPlugin/plugin.yml @@ -1,7 +1,8 @@ name: TesterPlugin main: pmmp\TesterPlugin\Main +src-namespace-prefix: pmmp\TesterPlugin version: 0.1.0 -api: [3.2.0] +api: [3.2.0, 4.0.0] load: POSTWORLD author: pmmp description: Plugin used to run tests on PocketMine-MP diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/Main.php b/tests/plugins/TesterPlugin/src/Main.php similarity index 79% rename from tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/Main.php rename to tests/plugins/TesterPlugin/src/Main.php index cbe1408e37..73535da293 100644 --- a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/Main.php +++ b/tests/plugins/TesterPlugin/src/Main.php @@ -26,6 +26,8 @@ namespace pmmp\TesterPlugin; use pocketmine\event\Listener; use pocketmine\event\server\CommandEvent; use pocketmine\plugin\PluginBase; +use pocketmine\scheduler\CancelTaskException; +use pocketmine\scheduler\ClosureTask; use function array_shift; class Main extends PluginBase implements Listener{ @@ -41,13 +43,20 @@ class Main extends PluginBase implements Listener{ public function onEnable() : void{ $this->getServer()->getPluginManager()->registerEvents($this, $this); - $this->getScheduler()->scheduleRepeatingTask(new CheckTestCompletionTask($this), 10); + $this->getScheduler()->scheduleRepeatingTask(new ClosureTask(function() : void{ + if($this->currentTest === null){ + if(!$this->startNextTest()){ + $this->onAllTestsCompleted(); + throw new CancelTaskException(); + } + }elseif($this->currentTest->isFinished() || $this->currentTest->isTimedOut()){ + $this->onTestCompleted($this->currentTest); + }else{ + $this->currentTest->tick(); + } + }), 10); - $this->waitingTests = [ - new tests\AsyncTaskMemoryLeakTest($this), - new tests\AsyncTaskMainLoggerTest($this), - new tests\AsyncTaskPublishProgressRaceTest($this) - ]; + $this->waitingTests = []; } public function onServerCommand(CommandEvent $event) : void{ @@ -56,15 +65,11 @@ class Main extends PluginBase implements Listener{ //be asynchronous tests running. Instead we cancel this and stop the server of our own accord once all tests //have completed. if($event->getCommand() === "stop"){ - $event->setCancelled(); + $event->cancel(); } } - public function getCurrentTest() : ?Test{ - return $this->currentTest; - } - - public function startNextTest() : bool{ + private function startNextTest() : bool{ $this->currentTest = array_shift($this->waitingTests); if($this->currentTest !== null){ $this->getLogger()->notice("Running test #" . (++$this->currentTestNumber) . " (" . $this->currentTest->getName() . ")"); @@ -75,7 +80,7 @@ class Main extends PluginBase implements Listener{ return false; } - public function onTestCompleted(Test $test) : void{ + private function onTestCompleted(Test $test) : void{ $message = "Finished test #" . $this->currentTestNumber . " (" . $test->getName() . "): "; switch($test->getResult()){ case Test::RESULT_OK: @@ -101,7 +106,7 @@ class Main extends PluginBase implements Listener{ $this->currentTest = null; } - public function onAllTestsCompleted() : void{ + private function onAllTestsCompleted() : void{ $this->getLogger()->notice("All tests finished, stopping the server"); $this->getServer()->shutdown(); } diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/Test.php b/tests/plugins/TesterPlugin/src/Test.php similarity index 100% rename from tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/Test.php rename to tests/plugins/TesterPlugin/src/Test.php diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/TestFailedException.php b/tests/plugins/TesterPlugin/src/TestFailedException.php similarity index 100% rename from tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/TestFailedException.php rename to tests/plugins/TesterPlugin/src/TestFailedException.php diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/CheckTestCompletionTask.php b/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/CheckTestCompletionTask.php deleted file mode 100644 index ce73f52cae..0000000000 --- a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/CheckTestCompletionTask.php +++ /dev/null @@ -1,50 +0,0 @@ -plugin = $plugin; - } - - public function onRun(int $currentTick){ - $test = $this->plugin->getCurrentTest(); - if($test === null){ - if(!$this->plugin->startNextTest()){ - $this->getHandler()->cancel(); - $this->plugin->onAllTestsCompleted(); - } - }elseif($test->isFinished() or $test->isTimedOut()){ - $this->plugin->onTestCompleted($test); - }else{ - $test->tick(); - } - } -} diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMainLoggerTest.php b/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMainLoggerTest.php deleted file mode 100644 index dc4b1b122f..0000000000 --- a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMainLoggerTest.php +++ /dev/null @@ -1,75 +0,0 @@ -getPlugin()->getServer()->getAsyncPool()->submitTask(new class($this) extends AsyncTask{ - - /** @var bool */ - protected $success = false; - - public function __construct(AsyncTaskMainLoggerTest $testObject){ - $this->storeLocal($testObject); - } - - public function onRun(){ - ob_start(); - MainLogger::getLogger()->info("Testing"); - $contents = ob_get_contents(); - if($contents === false) throw new AssumptionFailedError("ob_get_contents() should not return false here"); - if(strpos($contents, "Testing") !== false){ - $this->success = true; - } - ob_end_flush(); - } - - public function onCompletion(Server $server){ - /** @var AsyncTaskMainLoggerTest $test */ - $test = $this->fetchLocal(); - $test->setResult($this->success ? Test::RESULT_OK : Test::RESULT_FAILED); - } - }); - } - - public function getName() : string{ - return "MainLogger::getLogger() works in AsyncTasks"; - } - - public function getDescription() : string{ - return "Verifies that the MainLogger is accessible by MainLogger::getLogger() in an AsyncTask"; - } - -} diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMemoryLeakTest.php b/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMemoryLeakTest.php deleted file mode 100644 index b59c08c0ff..0000000000 --- a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskMemoryLeakTest.php +++ /dev/null @@ -1,62 +0,0 @@ -getPlugin()->getServer()->getAsyncPool()->submitTask(new TestAsyncTask()); - } - - public function tick() : void{ - if(TestAsyncTask::$destroyed === true){ - $this->setResult(Test::RESULT_OK); - } - } - - public function getName() : string{ - return "AsyncTask memory leak after completion"; - } - - public function getDescription() : string{ - return "Regression test for AsyncTasks objects not being destroyed after completion"; - } -} - -class TestAsyncTask extends AsyncTask{ - /** @var bool */ - public static $destroyed = false; - - public function onRun(){ - usleep(50 * 1000); //1 server tick - } - - public function __destruct(){ - self::$destroyed = true; - } -} diff --git a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskPublishProgressRaceTest.php b/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskPublishProgressRaceTest.php deleted file mode 100644 index f263c3f357..0000000000 --- a/tests/plugins/TesterPlugin/src/pmmp/TesterPlugin/tests/AsyncTaskPublishProgressRaceTest.php +++ /dev/null @@ -1,69 +0,0 @@ -getPlugin()->getServer()->getAsyncPool()->submitTask(new class($this) extends AsyncTask{ - /** @var bool */ - private static $success = false; - - public function __construct(AsyncTaskPublishProgressRaceTest $t){ - $this->storeLocal($t); - } - - public function onRun() : void{ - $this->publishProgress("hello"); - } - - public function onProgressUpdate(Server $server, $progress) : void{ - if($progress === "hello"){ - // thread local on main thread - self::$success = true; - } - } - - public function onCompletion(Server $server) : void{ - /** @var AsyncTaskPublishProgressRaceTest $t */ - $t = $this->fetchLocal(); - $t->setResult(self::$success ? Test::RESULT_OK : Test::RESULT_FAILED); - } - }); - } -} diff --git a/tests/travis.sh b/tests/travis.sh index e16a283333..094f659053 100755 --- a/tests/travis.sh +++ b/tests/travis.sh @@ -20,7 +20,7 @@ mkdir "$DATA_DIR" mkdir "$PLUGINS_DIR" cd tests/plugins/DevTools -php -dphar.readonly=0 ./src/DevTools/ConsoleScript.php --make ./ --relative ./ --out "$PLUGINS_DIR/DevTools.phar" +php -dphar.readonly=0 ./src/ConsoleScript.php --make ./ --relative ./ --out "$PLUGINS_DIR/DevTools.phar" cd ../../.. composer make-server diff --git a/tools/compact-regions.php b/tools/compact-regions.php new file mode 100644 index 0000000000..62f1d35f46 --- /dev/null +++ b/tools/compact-regions.php @@ -0,0 +1,178 @@ + $files + */ +function find_regions_recursive(string $dir, array &$files) : void{ + $dirFiles = scandir($dir, SCANDIR_SORT_NONE); + if($dirFiles === false){ + return; + } + foreach($dirFiles as $file){ + if($file === "." or $file === ".."){ + continue; + } + $fullPath = $dir . "/" . $file; + if( + in_array(pathinfo($fullPath, PATHINFO_EXTENSION), SUPPORTED_EXTENSIONS, true) and + is_file($fullPath) + ){ + $files[$fullPath] = filesize($fullPath); + }elseif(is_dir($fullPath)){ + find_regions_recursive($fullPath, $files); + } + } +} + +/** + * @param string[] $argv + */ +function main(array $argv) : int{ + if(!isset($argv[1])){ + echo "Usage: " . PHP_BINARY . " " . __FILE__ . " \n"; + return 1; + } + + $logger = \GlobalLogger::get(); + + /** @phpstan-var array $files */ + $files = []; + if(is_file($argv[1])){ + $files[$argv[1]] = filesize($argv[1]); + }elseif(is_dir($argv[1])){ + find_regions_recursive($argv[1], $files); + } + if(count($files) === 0){ + echo "No supported files found\n"; + return 1; + } + + arsort($files, SORT_NUMERIC); + $currentSize = array_sum($files); + $logger->info("Discovered " . count($files) . " files totalling " . number_format($currentSize) . " bytes"); + $logger->warning("Please DO NOT forcibly kill the compactor, or your files may be damaged."); + + $corruptedFiles = []; + $doneCount = 0; + $totalCount = count($files); + foreach($files as $file => $size){ + try{ + $oldRegion = RegionLoader::loadExisting($file); + }catch(CorruptedRegionException $e){ + $logger->error("Damaged region in file $file (" . $e->getMessage() . "), skipping"); + $corruptedFiles[] = $file; + $doneCount++; + continue; + } + + $newFile = $file . ".compacted"; + $newRegion = RegionLoader::createNew($newFile); + + $emptyRegion = true; + $corruption = false; + for($x = 0; $x < 32; $x++){ + for($z = 0; $z < 32; $z++){ + try{ + $data = $oldRegion->readChunk($x, $z); + }catch(CorruptedChunkException $e){ + $logger->error("Damaged chunk $x $z in file $file (" . $e->getMessage() . "), skipping"); + $corruption = true; + continue; + } + if($data !== null){ + $emptyRegion = false; + $newRegion->writeChunk($x, $z, $data); + } + } + } + + $oldRegion->close(); + $newRegion->close(); + if(!$corruption){ + unlink($file); + }else{ + rename($file, $file . ".bak"); + $corruptedFiles[] = $file . ".bak"; + } + if(!$emptyRegion){ + rename($newFile, $file); + }else{ + unlink($newFile); + } + $doneCount++; + $logger->info("Compacted region $file ($doneCount/$totalCount, " . round(($doneCount / $totalCount) * 100, 2) . "%)"); + } + + clearstatcache(); + $newSize = 0; + foreach($files as $file => $oldSize){ + $newSize += file_exists($file) ? filesize($file) : 0; + } + $diff = $currentSize - $newSize; + $logger->info("Finished compaction of " . count($files) . " files. Freed " . number_format($diff) . " bytes of space (" . round(($diff / $currentSize) * 100, 2) . "% reduction)."); + if(count($corruptedFiles) > 0){ + $logger->error("The following backup files were not removed due to corruption detected:"); + foreach($corruptedFiles as $file){ + echo $file . "\n"; + } + return 1; + } + return 0; +} + +if(!defined('pocketmine\_PHPSTAN_ANALYSIS')){ + exit(main($argv)); +} diff --git a/tools/convert-world.php b/tools/convert-world.php new file mode 100644 index 0000000000..7d83d5fd85 --- /dev/null +++ b/tools/convert-world.php @@ -0,0 +1,80 @@ +getAvailableProviders(), fn(WorldProviderManagerEntry $class) => $class instanceof WritableWorldProviderManagerEntry); +$requiredOpts = [ + "world" => "path to the input world for conversion", + "backup" => "path to back up the original files", + "format" => "desired output format (can be one of: " . implode(",", array_keys($writableFormats)) . ")" +]; +$usageMessage = "Options:\n"; +foreach($requiredOpts as $_opt => $_desc){ + $usageMessage .= "\t--$_opt : $_desc\n"; +} +$plainArgs = getopt("", array_map(function(string $str){ return "$str:"; }, array_keys($requiredOpts))); +$args = []; +foreach($requiredOpts as $opt => $desc){ + if(!isset($plainArgs[$opt]) || !is_string($plainArgs[$opt])){ + fwrite(STDERR, $usageMessage); + exit(1); + } + $args[$opt] = $plainArgs[$opt]; +} +if(!array_key_exists($args["format"], $writableFormats)){ + fwrite(STDERR, $usageMessage); + exit(1); +} + +$inputPath = realpath($args["world"]); +if($inputPath === false){ + fwrite(STDERR, "Cannot find input world at location: " . $args["world"] . PHP_EOL); + exit(1); +} +$backupPath = realpath($args["backup"]); +if($backupPath === false || (!@mkdir($backupPath, 0777, true) and !is_dir($backupPath)) or !is_writable($backupPath)){ + fwrite(STDERR, "Backup file path " . $args["backup"] . " is not writable (permission error or doesn't exist), aborting" . PHP_EOL); + exit(1); +} + +$oldProviderClasses = $providerManager->getMatchingProviders($inputPath); +if(count($oldProviderClasses) === 0){ + fwrite(STDERR, "Unknown input world format" . PHP_EOL); + exit(1); +} +if(count($oldProviderClasses) > 1){ + fwrite(STDERR, "Ambiguous input world format: matched " . count($oldProviderClasses) . " (" . implode(array_keys($oldProviderClasses)) . ")" . PHP_EOL); + exit(1); +} +$oldProviderClass = array_shift($oldProviderClasses); +$oldProvider = $oldProviderClass->fromPath($inputPath); + +$converter = new FormatConverter($oldProvider, $writableFormats[$args["format"]], $backupPath, GlobalLogger::get()); +$converter->execute(); diff --git a/tools/generate-permission-doc.php b/tools/generate-permission-doc.php new file mode 100644 index 0000000000..944d2aae30 --- /dev/null +++ b/tools/generate-permission-doc.php @@ -0,0 +1,101 @@ +getPermissions(); +ksort($permissions, SORT_STRING); + +fwrite($doc, "# PocketMine-MP Core Permissions\n"); +fwrite($doc, "Generated from PocketMine-MP " . VersionInfo::VERSION()->getFullVersion() . "\n"); +fwrite($doc, "\n"); +fwrite($doc, "| Name | Description | Implied permissions |\n"); +fwrite($doc, "|:-----|:------------|:-------------------:|\n"); +foreach($permissions as $permission){ + $link = count($permission->getChildren()) === 0 ? "N/A" : "[Jump](#" . markdownify("Permissions implied by `" . $permission->getName() . "`") . ")"; + fwrite($doc, "| `" . $permission->getName() . "` | " . $permission->getDescription() . " | $link |\n"); +} + +fwrite($doc, "\n\n"); +fwrite($doc, "## Implied permissions\n"); +fwrite($doc, "Some permissions automatically grant (or deny) other permissions by default when granted. These are referred to as **implied permissions**.
\n"); +fwrite($doc, "Permissions may imply permissions which in turn imply other permissions (e.g. `pocketmine.group.operator` implies `pocketmine.group.user`, which in turn implies `pocketmine.command.help`).
\n"); +fwrite($doc, "Implied permissions can be overridden by explicit permissions from elsewhere.
\n"); +fwrite($doc, "**Note:** When explicitly denied, implied permissions are inverted. This means that \"granted\" becomes \"denied\" and vice versa.\n"); +fwrite($doc, "\n\n"); +foreach($permissions as $permission){ + if(count($permission->getChildren()) === 0){ + continue; + } + fwrite($doc, "### Permissions implied by `" . $permission->getName() . "`\n"); + fwrite($doc, "Users granted this permission will also be granted/denied the following permissions implicitly:\n\n"); + + fwrite($doc, "| Name | Type |\n"); + fwrite($doc, "|:-----|:----:|\n"); + $children = $permission->getChildren(); + ksort($children, SORT_STRING); + foreach(Utils::stringifyKeys($children) as $childName => $isGranted){ + fwrite($doc, "| `$childName` | " . ($isGranted ? "Granted" : "Denied") . " |\n"); + } + fwrite($doc, "\n"); +} + +fclose($doc); +echo "Done.\n"; diff --git a/tools/simulate-chunk-selector.php b/tools/simulate-chunk-selector.php new file mode 100644 index 0000000000..b30355f7c1 --- /dev/null +++ b/tools/simulate-chunk-selector.php @@ -0,0 +1,172 @@ +selectChunks($radius, $baseX, $baseZ); + + $middleOffsetX = $scale * ($radius + $offsetX); + $middleOffsetZ = $scale * ($radius + $offsetZ); + + $black = imagecolorallocate($image, 0, 0, 0); + $yellow = imagecolorallocate($image, 255, 255, 51); + $red = imagecolorallocate($image, 255, 0, 0); + if($black === false || $yellow === false || $red === false) throw new AssumptionFailedError(); + + $frame = 0; + $seen = []; + while($iterator->valid()){ + $frame++; + + for($i = 0; $i < $chunksPerStep; ++$i){ + $chunkHash = $iterator->current(); + if(!isset($seen[$chunkHash])){ + $color = $chunkColor; + $seen[$chunkHash] = true; + }else{ + $color = $yellow; + } + World::getXZ($chunkHash, $chunkX, $chunkZ); + $imageX = $middleOffsetX + (($chunkX - $baseX) * $scale); + $imageZ = $middleOffsetZ + (($chunkZ - $baseZ) * $scale); + + imagefilledrectangle($image, $imageX, $imageZ, $imageX + $scale, $imageZ + $scale, $color); + imagerectangle($image, $imageX, $imageZ, $imageX + $scale, $imageZ + $scale, $black); + + $iterator->next(); + if(!$iterator->valid()){ + break; + } + } + imagearc($image, $middleOffsetX, $middleOffsetZ, $radius * $scale * 2, $radius * $scale * 2, 0, 360, $red); + + imagepng($image, Path::join($outputFolder, "frame" . str_pad(strval($frame), 5, "0", STR_PAD_LEFT) . ".png")); + echo "\rRendered step $frame"; + } + echo "\n"; +} + +$radius = null; +$baseX = 10000 >> Chunk::COORD_BIT_SIZE; +$baseZ = 10000 >> Chunk::COORD_BIT_SIZE; + +$nChunksPerStep = 32; +$scale = 10; + +if(count(getopt("", ["help"])) !== 0){ + echo "Required parameters:\n"; + echo "--output=path/to/dir: Output folder to put the generated images into (will attempt to create if it doesn't exist)\n"; + echo "--radius=N: Radius of chunks to render (default $radius)\n"; + echo "\n"; + echo "Optional parameters:\n"; + echo "--baseX=N: Base X coordinate to use for simulation (default $baseX\n"; + echo "--baseZ=N: Base Z coordinate to use for simulation (default $baseZ)\n"; + echo "--scale=N: Height/width of square of pixels to use for each chunk (default $scale)\n"; + echo "--chunksPerStep=N: Number of chunks to process in each frame (default $nChunksPerStep)\n"; + exit(0); +} + +foreach(Utils::stringifyKeys(getopt("", ["radius:", "baseX:", "baseZ:", "scale:", "chunksPerStep:"])) as $name => $value){ + if(!is_string($value) || (string) ((int) $value) !== $value){ + fwrite(STDERR, "Value for --$name must be an integer\n"); + exit(1); + } + $value = (int) $value; + match($name){ + "radius" => ($radius = $value), + "baseX" => ($baseX = $value), + "baseZ" => ($baseZ = $value), + "scale" => ($scale = $value), + "chunksPerStep" => ($nChunksPerStep = $value), + default => throw new AssumptionFailedError("getopt() returned unknown option") + }; +} +if($radius === null){ + fwrite(STDERR, "Please specify a radius using --radius\n"); + exit(1); +} + +$outputDirectory = null; +foreach(Utils::stringifyKeys(getopt("", ["output:"])) as $name => $value){ + assert($name === "output"); + if(!is_string($value)){ + fwrite(STDERR, "Value for --$name must be a string\n"); + exit(1); + } + if(!@mkdir($value) && !is_dir($value)){ + fwrite(STDERR, "Output directory $value could not be created\n"); + exit(1); + } + $files = scandir($value, SCANDIR_SORT_NONE); + if($files !== false && count($files) > 2){ //always returns . and .. + fwrite(STDERR, "Output directory $value is not empty\n"); + exit(1); + } + $realPath = realpath($value); + if($realPath === false){ + throw new AssumptionFailedError(); + } + $outputDirectory = $realPath; +} +if($outputDirectory === null){ + fwrite(STDERR, "Please specify an output directory using --output\n"); + exit(1); +} +$image = newImage($scale, $radius); + +$black = imagecolorallocate($image, 0, 0, 0); +$green = imagecolorallocate($image, 0, 220, 0); +if($black === false || $green === false){ + throw new AssumptionFailedError(); +} +render($radius, $baseX, $baseZ, $nChunksPerStep, $scale, $image, $green, 0, 0, $outputDirectory);