diff --git a/.github/workflows/discord-release-notify.yml b/.github/workflows/discord-release-notify.yml index 76780d633..204a0faa3 100644 --- a/.github/workflows/discord-release-notify.yml +++ b/.github/workflows/discord-release-notify.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.35.4 + uses: shivammathur/setup-php@2.35.5 with: php-version: 8.2 diff --git a/.github/workflows/draft-release-pr-check.yml b/.github/workflows/draft-release-pr-check.yml index 9905d843d..819b46fef 100644 --- a/.github/workflows/draft-release-pr-check.yml +++ b/.github/workflows/draft-release-pr-check.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup PHP - uses: shivammathur/setup-php@2.35.4 + uses: shivammathur/setup-php@2.35.5 with: php-version: 8.2 diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml index 6edf05dfb..bdde3b68f 100644 --- a/.github/workflows/draft-release.yml +++ b/.github/workflows/draft-release.yml @@ -87,7 +87,7 @@ jobs: submodules: true - name: Setup PHP - uses: shivammathur/setup-php@2.35.4 + uses: shivammathur/setup-php@2.35.5 with: php-version: ${{ env.PHP_VERSION }} @@ -165,7 +165,7 @@ jobs: ${{ github.workspace }}/core-permissions.rst - name: Create draft release - uses: ncipollo/release-action@v1.18.0 + uses: ncipollo/release-action@v1.20.0 id: create-draft with: artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a7d6eb0fc..504a3f64a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v5 - name: Setup PHP and tools - uses: shivammathur/setup-php@2.35.4 + uses: shivammathur/setup-php@2.35.5 with: php-version: 8.3 tools: php-cs-fixer:3.75 diff --git a/.github/workflows/pr-remove-waiting-label.yml b/.github/workflows/pr-remove-waiting-label.yml index b7cd85acd..d628317fa 100644 --- a/.github/workflows/pr-remove-waiting-label.yml +++ b/.github/workflows/pr-remove-waiting-label.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Remove label - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ github.token }} script: | diff --git a/.github/workflows/pr-stale.yml b/.github/workflows/pr-stale.yml index 23518b2cf..ab9db69de 100644 --- a/.github/workflows/pr-stale.yml +++ b/.github/workflows/pr-stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: days-before-issue-stale: -1 days-before-issue-close: -1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 799c9d99c..f34245e8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,6 +85,13 @@ Note that there might be a long wait time before a reviewer looks at your PR. Depending on the changes, maintainers might ask you to make changes to the PR to fix problems or to improve the code. **Do not delete your fork** while your pull request remains open, otherwise you won't be able to make any requested changes and the PR will end up being declined. +> [!TIP] +> Don't worry about getting a PR perfect on the first try. +> In fact, it's quite unusual for a PR to be perfect when it's first submitted, and most PRs will get changes requested by reviewers, even when the PR is made by one of our team members. +> +> Mistakes are normal, and PMMP team members will review your code and suggest changes to your code as needed. +> Just make sure to stick with it so you can communicate with reviewers and/or make changes. + ### Requirements The following are required as a minimum for pull requests. PRs that don't meet these requirements will be declined unless updated to meet them. diff --git a/build/php b/build/php index 8fe187335..271403457 160000 --- a/build/php +++ b/build/php @@ -1 +1 @@ -Subproject commit 8fe187335f666b8fb251927940f66ef372bfd3a6 +Subproject commit 2714034579b9b614df56da8f44cb5cbe067aa608 diff --git a/changelogs/5.34.md b/changelogs/5.34.md new file mode 100644 index 000000000..eb2d728c1 --- /dev/null +++ b/changelogs/5.34.md @@ -0,0 +1,111 @@ +# 5.34.0 +Released 26th September 2025. + +This is a minor feature release containing performance improvements, new gameplay features, new API additions and network changes. + +**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace. +Do not update plugin minimum API versions unless you need new features added in this release. + +**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.** +Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. + +## General +- PocketMine-MP now requires and uses [`pmmp/ext-encoding`](https://github.com/pmmp/ext-encoding) version `1.0.0`, a PHP extension designed to significantly improve performance of data encoding and decoding (@dktapps). + - This first pass only implements support in low-risk areas, such as network protocol and read-only data. Further integration will follow in future minor versions with additional performance improvements. + - While ext-encoding has been heavily tested, we can't be sure there won't be major issues once it reaches production. Please report any bugs you find on the GitHub issue tracker. +- New Bedrock OpenID authentication support has been implemented (@dries-c, @dktapps). + - This system fetches keys from an API provided by Microsoft. Therefore, your server must now have internet access to authenticate players. +- `/timings paste` now creates private reports by default on `timings.pmmp.io` (@dktapps). + - Private reports require an access token to view, so your timings reports can no longer be viewed by others just by guessing the ID. + - If you're using a custom timings host, be sure to update it to get support for this feature. + - The command will generate a warning in the console if the target timings host doesn't support private reports. + +## Performance +- Significantly improved performance of packet encoding and decoding using `ext-encoding` (@dktapps). +- Unnecessary NBT is now stripped from items before sending them over the network. This significantly improves performance when working with writable books, shulker boxes, etc. (@dktapps). +- Improved performance of item saving in `ItemSerializer` by avoiding slow `hasNamedTag()` call followed by `getNamedTag()` (both will rebuild the NBT) (@dktapps). + +## Gameplay +- Implemented basic Trident functionality (@IvanCraft623). +- Implemented Firework and Firework Star (@IvanCraft623). +- Editing the rear side of signs is now supported (@dktapps). +- Sneaking hitbox height has been adjusted to match vanilla (@Dasciam). + +## API +### General +- `pocketmine/nbt` version `1.2.0` is now used ([changelog](https://github.com/pmmp/NBT/releases/tag/1.2.0)). +- `pmmp/ext-encoding` version `1.0.0` is now required and used. + - This can be used as a faster alternative to `BinaryStream` and `Binary` in most use cases. However, please note that its API is very different, and it hasn't been battle-tested yet. + - A recent JetBrains IDE stub can be found in our [custom stubs repository](https://github.com/pmmp/phpstorm-stubs/blob/fork/encoding/encoding.php). + +### `pocketmine\block` +- The following API methods have been added: + - `public BaseSign->getFaceText(bool $frontFace) : SignText` + - `public BaseSign->setFaceText(bool $frontFace, SignText $text) : $this` + - `public BaseSign->updateFaceText(Player $author, bool $frontFace, SignText $text) : bool` - called by the network system when a player edits a sign, triggers `SignChangeEvent` etc. + - `protected BaseSign->getHitboxCenter() : Vector3` - returns the center of the sign's hitbox, used to decide which face of the sign the player is editing + - `protected BaseSign->getFacingDegrees() : float` (to become abstract in PM6) - returns the horizontal facing of the sign in degrees, used to decide which face of the sign the player is editing +- The following API methods have been deprecated: + - `public BaseSign->getText() : SignText` - use `getFaceText()` instead + - `public BaseSign->setText(SignText $text) : $this` - use `setFaceText()` instead + - `public BaseSign->updateText(Player $author, SignText $text) : bool` - use `updateFaceText()` instead + +### `pocketmine\entity` +- The following API classes have been added: + - `NeverSavedWithChunkEntity` - implement this instead of overriding `canSaveWithChunk()` if your entity will never need a save ID + - Used currently by `Player` and `FireworkRocket`. + - `animation\FireworkParticlesAnimation` + - `object\FireworkRocket` + - `projectile\Trident` +- The following API methods have been added: + - `public Living->getSneakOffset() : float` - returns how much the entity's hitbox is shortened and eye height lowered when sneaking (0 by default) + - `protected Projectile->despawnsOnEntityHit() : bool` - returns `true` by default, overridden by tridents (to be removed in a future major version in favour of cleaner BC-breaking methods) + +### `pocketmine\event\block` +- The following API methods have been added: + - `public SignChangeEvent->isFrontFace() : bool` - returns `true` if the front face of the sign is being edited, `false` for the rear face + +### `pocketmine\inventory\transaction` +- `InventoryTransaction` no longer shuffles actions before executing a transaction. + - This was intended to prevent dependency on weird client behaviour, but it is no longer necessary, as the order is now consistent since the introduction of the `ItemStackRequest` system. + +### `pocketmine\item` +- The following API classes have been added: + - `FireworkRocket` + - `FireworkRocketExplosion` + - `FireworkRocketType` (enum) + - `FireworkStar` + - `Trident` +- The following API methods have been added: + - `VanillaItems::FIREWORK_ROCKET() : FireworkRocket` + - `VanillaItems::FIREWORK_STAR() : FireworkStar` + - `VanillaItems::TRIDENT() : Trident` + +### `pocketmine\player` +- The following API methods have signature changes: + - `Player->openSignEditor()` now accepts an optional `bool $frontFace = true` parameter + +### `pocketmine\world\sound` +- The following API classes have been added: + - `FireworkCrackleSound` + - `FireworkExplosionSound` + - `FireworkLargeExplosionSound` + - `FireworkLaunchSound` + - `TridentHitEntitySound` + - `TridentHitBlockSound` + - `TridentThrowSound` + +## Internals +- Many low-risk data handling areas have been switched to use `ext-encoding`, including: + - Bedrock packets + - Bedrock chunk serialization + - `FastChunkSerializer` (used for transmitting chunks between threads) + - GS4 Query + - Auxiliary read-only data loading in the `pocketmine\data\bedrock` package + +# 5.34.1 +Released 26th September 2025. + +## Fixes +- Player login JSON processing no longer bails out on unexpected extra properties. A warning will now be logged instead (@dktapps). +- Fixed container drop issues when an ender crystal explosion causes another ender crystal nearby to explode (@dktapps, @kostamax27). diff --git a/changelogs/5.35.md b/changelogs/5.35.md new file mode 100644 index 000000000..0b5a20203 --- /dev/null +++ b/changelogs/5.35.md @@ -0,0 +1,21 @@ +# 5.35.0 +Released 3rd October 2025. + +This is a support release for Minecraft: Bedrock Edition 1.21.111. + +**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace. +Do not update plugin minimum API versions unless you need new features added in this release. + +**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.** +Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. + +## Interim releases +If you're upgrading from 5.32.x directly to 5.35.0, please also read the following changelogs, as the interim releases contain important changes: +- [5.33.0](https://github.com/pmmp/PocketMine-MP/blob/5.35.0/changelogs/5.33.md) - internals improvements, API improvements and new gameplay features +- [5.34.0](https://github.com/pmmp/PocketMine-MP/blob/5.35.0/changelogs/5.34.md) - significant performance improvements, new gameplay features, new API additions and network changes + +Please also note that an updated PHP binary is required due to changes introduced in 5.34.0 (initial `ext-encoding` integration). + +## General +- Added support for Minecraft: Bedrock Edition 1.21.111 (@dries-c). +- Removed support for earlier versions. diff --git a/composer.json b/composer.json index 9451fd6eb..87dddf5e8 100644 --- a/composer.json +++ b/composer.json @@ -34,15 +34,15 @@ "composer-runtime-api": "^2.0", "adhocore/json-comment": "~1.2.0", "netresearch/jsonmapper": "~v5.0.0", - "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", - "pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100", - "pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100", - "pocketmine/bedrock-protocol": "~50.0.0+bedrock-1.21.100", + "pocketmine/bedrock-block-upgrade-schema": "~5.2.0+bedrock-1.21.110", + "pocketmine/bedrock-data": "~6.1.0+bedrock-1.21.111", + "pocketmine/bedrock-item-upgrade-schema": "~1.16.0+bedrock-1.21.110", + "pocketmine/bedrock-protocol": "~51.1.0+bedrock-1.21.111", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.7.0", - "pocketmine/locale-data": "~2.25.0", + "pocketmine/locale-data": "~2.26.0", "pocketmine/log": "^0.4.0", "pocketmine/math": "~1.0.0", "pocketmine/nbt": "~1.2.0", diff --git a/composer.lock b/composer.lock index ce923690e..d4cbdb82f 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": "0d71d3fba23ba8c4734cac59b9e10129", + "content-hash": "ad9a8e8069598ae5ec679f069461623f", "packages": [ { "name": "adhocore/json-comment", @@ -178,16 +178,16 @@ }, { "name": "pocketmine/bedrock-block-upgrade-schema", - "version": "5.1.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", - "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a" + "reference": "5d7889c9a1cdf9e3cd814d2a104ad69b75116ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", - "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/5d7889c9a1cdf9e3cd814d2a104ad69b75116ec7", + "reference": "5d7889c9a1cdf9e3cd814d2a104ad69b75116ec7", "shasum": "" }, "type": "library", @@ -198,22 +198,22 @@ "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", "support": { "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.1.0" + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.2.0" }, - "time": "2025-02-11T17:41:44+00:00" + "time": "2025-10-02T13:22:10+00:00" }, { "name": "pocketmine/bedrock-data", - "version": "6.0.0+bedrock-1.21.100", + "version": "6.1.0+bedrock-1.21.111", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "edc0d829175e5e1e57c87001acfd03526c63fd34" + "reference": "7484fe3c3d7949fd48cc520add4f7eeebc4ba4af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/edc0d829175e5e1e57c87001acfd03526c63fd34", - "reference": "edc0d829175e5e1e57c87001acfd03526c63fd34", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/7484fe3c3d7949fd48cc520add4f7eeebc4ba4af", + "reference": "7484fe3c3d7949fd48cc520add4f7eeebc4ba4af", "shasum": "" }, "type": "library", @@ -224,22 +224,22 @@ "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/BedrockData/issues", - "source": "https://github.com/pmmp/BedrockData/tree/6.0.0+bedrock-1.21.100" + "source": "https://github.com/pmmp/BedrockData/tree/6.1.0+bedrock-1.21.111" }, - "time": "2025-08-30T17:25:42+00:00" + "time": "2025-10-02T15:28:18+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git", - "reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b" + "reference": "8c48ceaa72d390e89c4dbff9542aa4dfa734057d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/09e0dbe9743f21a76b1fe04b2b4136785775f52b", - "reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b", + "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/8c48ceaa72d390e89c4dbff9542aa4dfa734057d", + "reference": "8c48ceaa72d390e89c4dbff9542aa4dfa734057d", "shasum": "" }, "type": "library", @@ -250,22 +250,22 @@ "description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves", "support": { "issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.15.0" + "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.16.0" }, - "time": "2025-08-06T15:08:48+00:00" + "time": "2025-10-02T13:22:32+00:00" }, { "name": "pocketmine/bedrock-protocol", - "version": "50.0.0+bedrock-1.21.100", + "version": "51.1.0+bedrock-1.21.111", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a" + "reference": "e380be227766ea58a874eb7d93de76f21c8ec04b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2d7aa27a5537ae593fb1c39158648ea462fef72a", - "reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/e380be227766ea58a874eb7d93de76f21c8ec04b", + "reference": "e380be227766ea58a874eb7d93de76f21c8ec04b", "shasum": "" }, "require": { @@ -297,9 +297,9 @@ "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "support": { "issues": "https://github.com/pmmp/BedrockProtocol/issues", - "source": "https://github.com/pmmp/BedrockProtocol/tree/50.0.0+bedrock-1.21.100" + "source": "https://github.com/pmmp/BedrockProtocol/tree/51.1.0+bedrock-1.21.111" }, - "time": "2025-09-20T23:09:19+00:00" + "time": "2025-10-03T14:12:27+00:00" }, { "name": "pocketmine/binaryutils", @@ -472,16 +472,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.25.1", + "version": "2.26.0", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd" + "reference": "f791369ae082fc5cdf0f2b0bd683e611ff7f90f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd", - "reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd", + "url": "https://api.github.com/repos/pmmp/Language/zipball/f791369ae082fc5cdf0f2b0bd683e611ff7f90f6", + "reference": "f791369ae082fc5cdf0f2b0bd683e611ff7f90f6", "shasum": "" }, "type": "library", @@ -489,9 +489,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.25.1" + "source": "https://github.com/pmmp/Language/tree/2.26.0" }, - "time": "2025-04-16T11:15:32+00:00" + "time": "2025-10-07T17:26:32+00:00" }, { "name": "pocketmine/log", diff --git a/src/Server.php b/src/Server.php index af9cbeda7..9f12dec07 100644 --- a/src/Server.php +++ b/src/Server.php @@ -872,7 +872,7 @@ class Server{ $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(Yml::SETTINGS_ENABLE_DEV_BUILDS))); - $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5("https://github.com/pmmp/PocketMine-MP/releases"))); + $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5(VersionInfo::GITHUB_URL . "/releases"))); $this->forceShutdownExit(); return; @@ -1093,7 +1093,23 @@ class Server{ $this->configGroup->save(); $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName()))); - $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET))); + $highlight = TextFormat::AQUA; + $reset = TextFormat::RESET; + $github = VersionInfo::GITHUB_URL; + $splash = "\n\n"; + foreach([ + KnownTranslationFactory::pocketmine_server_url_discord("{$highlight}https://discord.pmmp.io{$reset}"), + KnownTranslationFactory::pocketmine_server_url_docs("{$highlight}https://doc.pmmp.io{$reset}"), + KnownTranslationFactory::pocketmine_server_url_sourceCode("{$highlight}{$github}{$reset}"), + KnownTranslationFactory::pocketmine_server_url_freePlugins("{$highlight}https://poggit.pmmp.io/plugins{$reset}"), + KnownTranslationFactory::pocketmine_server_url_donations("{$highlight}https://patreon.com/pocketminemp{$reset}"), + KnownTranslationFactory::pocketmine_server_url_translations("{$highlight}https://translate.pocketmine.net{$reset}"), + KnownTranslationFactory::pocketmine_server_url_bugReporting("{$highlight}{$github}/issues{$reset}") + ] as $link){ + $splash .= "- " . $this->language->translate($link) . "\n"; + } + $this->logger->info($splash); + $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3))))); $forwarder = new BroadcastLoggerForwarder($this, $this->logger, $this->language); diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 944d6d84f..8c68b4e13 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,9 +31,10 @@ use function str_repeat; final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.33.3"; + public const BASE_VERSION = "5.35.1"; public const IS_DEVELOPMENT_BUILD = true; public const BUILD_CHANNEL = "stable"; + public const GITHUB_URL = "https://github.com/pmmp/PocketMine-MP"; /** * PocketMine-MP-specific version ID for world data. Used to determine what fixes need to be applied to old world diff --git a/src/block/Cauldron.php b/src/block/Cauldron.php index 772583a5a..1d275e137 100644 --- a/src/block/Cauldron.php +++ b/src/block/Cauldron.php @@ -82,7 +82,7 @@ final class Cauldron extends Transparent{ $this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::LAVA_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems); }elseif($item->getTypeId() === ItemTypeIds::POWDER_SNOW_BUCKET){ //TODO: powder snow cauldron - }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + }elseif($item instanceof Potion || $item instanceof SplashPotion){ if($item->getType() === PotionType::WATER){ $this->fill(WaterCauldron::WATER_BOTTLE_FILL_AMOUNT, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); }else{ diff --git a/src/block/WaterCauldron.php b/src/block/WaterCauldron.php index 8129f2960..97db78e55 100644 --- a/src/block/WaterCauldron.php +++ b/src/block/WaterCauldron.php @@ -123,7 +123,7 @@ final class WaterCauldron extends FillableCauldron{ $world->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronAddDyeSound()); $item->pop(); - }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + }elseif($item instanceof Potion || $item instanceof SplashPotion){ if($item->getType() === PotionType::WATER){ $this->setCustomWaterColor(null)->addFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); }else{ diff --git a/src/block/tile/Campfire.php b/src/block/tile/Campfire.php index ad4a193d7..0c806c5d2 100644 --- a/src/block/tile/Campfire.php +++ b/src/block/tile/Campfire.php @@ -93,6 +93,7 @@ class Campfire extends Spawnable implements Container{ $listeners = $this->inventory->getListeners()->toArray(); $this->inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization + $baseErrorContext = "Campfire ($this->position)"; foreach([ [0, self::TAG_FIRST_INPUT_ITEM, self::TAG_FIRST_COOKING_TIME], [1, self::TAG_SECOND_INPUT_ITEM, self::TAG_SECOND_COOKING_TIME], @@ -100,7 +101,7 @@ class Campfire extends Spawnable implements Container{ [3, self::TAG_FOURTH_INPUT_ITEM, self::TAG_FOURTH_COOKING_TIME], ] as [$slot, $itemTag, $cookingTimeTag]){ if(($tag = $nbt->getTag($itemTag)) instanceof CompoundTag){ - $items[$slot] = Item::nbtDeserialize($tag); + $items[$slot] = Item::safeNbtDeserialize($tag, "$baseErrorContext slot $slot"); } if(($tag = $nbt->getTag($cookingTimeTag)) instanceof IntTag){ $this->cookingTimes[$slot] = $tag->getValue(); diff --git a/src/block/tile/Cauldron.php b/src/block/tile/Cauldron.php index d10f97e14..53d2ae389 100644 --- a/src/block/tile/Cauldron.php +++ b/src/block/tile/Cauldron.php @@ -76,7 +76,6 @@ final class Cauldron extends Spawnable{ default => throw new AssumptionFailedError("Unexpected potion item type") }); - //TODO: lingering potion $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); @@ -96,7 +95,7 @@ final class Cauldron extends Spawnable{ $this->potionItem = match($containerType){ self::POTION_CONTAINER_TYPE_NORMAL => VanillaItems::POTION()->setType($potionType), self::POTION_CONTAINER_TYPE_SPLASH => VanillaItems::SPLASH_POTION()->setType($potionType), - self::POTION_CONTAINER_TYPE_LINGERING => throw new SavedDataLoadingException("Not implemented"), + self::POTION_CONTAINER_TYPE_LINGERING => VanillaItems::LINGERING_POTION()->setType($potionType), default => throw new SavedDataLoadingException("Invalid potion container type ID $containerType") }; }else{ @@ -115,7 +114,6 @@ final class Cauldron extends Spawnable{ default => throw new AssumptionFailedError("Unexpected potion item type") }); - //TODO: lingering potion $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); diff --git a/src/block/tile/ChiseledBookshelf.php b/src/block/tile/ChiseledBookshelf.php index 90bf8f29b..0fea0767d 100644 --- a/src/block/tile/ChiseledBookshelf.php +++ b/src/block/tile/ChiseledBookshelf.php @@ -26,7 +26,6 @@ namespace pocketmine\block\tile; use pocketmine\block\utils\ChiseledBookshelfSlot; use pocketmine\data\bedrock\item\SavedItemData; use pocketmine\data\bedrock\item\SavedItemStackData; -use pocketmine\data\SavedDataLoadingException; use pocketmine\inventory\SimpleInventory; use pocketmine\item\Item; use pocketmine\math\Vector3; @@ -99,18 +98,13 @@ class ChiseledBookshelf extends Tile implements Container{ $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $newContents = []; + $errorLogContext = "ChiseledBookshelf ($this->position)"; foreach($inventoryTag as $slot => $itemNBT){ - try{ - $count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT); - if($count === 0){ - continue; - } - $newContents[$slot] = Item::nbtDeserialize($itemNBT); - }catch(SavedDataLoadingException $e){ - //TODO: not the best solution - \GlobalLogger::get()->logException($e); + $count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT); + if($count === 0){ continue; } + $newContents[$slot] = Item::safeNbtDeserialize($itemNBT, "$errorLogContext slot $slot"); } $inventory->setContents($newContents); diff --git a/src/block/tile/ContainerTrait.php b/src/block/tile/ContainerTrait.php index 6b9158d7a..72a0dc2e6 100644 --- a/src/block/tile/ContainerTrait.php +++ b/src/block/tile/ContainerTrait.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace pocketmine\block\tile; use pocketmine\data\bedrock\item\SavedItemStackData; -use pocketmine\data\SavedDataLoadingException; use pocketmine\inventory\Inventory; use pocketmine\item\Item; use pocketmine\nbt\NBT; @@ -56,14 +55,10 @@ trait ContainerTrait{ $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $newContents = []; + $errorLogContext = "Container (" . $this->getPosition() . ")"; foreach($inventoryTag as $itemNBT){ - try{ - $newContents[$itemNBT->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($itemNBT); - }catch(SavedDataLoadingException $e){ - //TODO: not the best solution - \GlobalLogger::get()->logException($e); - continue; - } + $slotId = $itemNBT->getByte(SavedItemStackData::TAG_SLOT); + $newContents[$slotId] = Item::safeNbtDeserialize($itemNBT, "$errorLogContext slot $slotId"); } $inventory->setContents($newContents); diff --git a/src/block/tile/ItemFrame.php b/src/block/tile/ItemFrame.php index 7d003770e..d69f533e3 100644 --- a/src/block/tile/ItemFrame.php +++ b/src/block/tile/ItemFrame.php @@ -51,7 +51,7 @@ class ItemFrame extends Spawnable{ public function readSaveData(CompoundTag $nbt) : void{ if(($itemTag = $nbt->getCompoundTag(self::TAG_ITEM)) !== null){ - $this->item = Item::nbtDeserialize($itemTag); + $this->item = Item::safeNbtDeserialize($itemTag, "ItemFrame ($this->position) framed item"); } if($nbt->getTag(self::TAG_ITEM_ROTATION) instanceof FloatTag){ $this->itemRotation = (int) ($nbt->getFloat(self::TAG_ITEM_ROTATION, $this->itemRotation * 45) / 45); diff --git a/src/block/tile/Jukebox.php b/src/block/tile/Jukebox.php index 54acd60ee..cc4448c32 100644 --- a/src/block/tile/Jukebox.php +++ b/src/block/tile/Jukebox.php @@ -44,7 +44,7 @@ class Jukebox extends Spawnable{ public function readSaveData(CompoundTag $nbt) : void{ if(($tag = $nbt->getCompoundTag(self::TAG_RECORD)) !== null){ - $record = Item::nbtDeserialize($tag); + $record = Item::safeNbtDeserialize($tag, "Jukebox ($this->position) record"); if($record instanceof Record){ $this->record = $record; } diff --git a/src/block/tile/Lectern.php b/src/block/tile/Lectern.php index 37e79b10e..2ce586252 100644 --- a/src/block/tile/Lectern.php +++ b/src/block/tile/Lectern.php @@ -45,7 +45,7 @@ class Lectern extends Spawnable{ public function readSaveData(CompoundTag $nbt) : void{ $this->viewedPage = $nbt->getInt(self::TAG_PAGE, 0); if(($itemTag = $nbt->getCompoundTag(self::TAG_BOOK)) !== null){ - $book = Item::nbtDeserialize($itemTag); + $book = Item::safeNbtDeserialize($itemTag, "Lectern ($this->position) book"); if($book instanceof WritableBookBase && !$book->isNull()){ $this->book = $book; } diff --git a/src/data/bedrock/block/BlockStateNames.php b/src/data/bedrock/block/BlockStateNames.php index 704798d1d..d24edab72 100644 --- a/src/data/bedrock/block/BlockStateNames.php +++ b/src/data/bedrock/block/BlockStateNames.php @@ -109,6 +109,7 @@ final class BlockStateNames{ public const PILLAR_AXIS = "pillar_axis"; public const PORTAL_AXIS = "portal_axis"; public const POWERED_BIT = "powered_bit"; + public const POWERED_SHELF_TYPE = "powered_shelf_type"; public const PROPAGULE_STAGE = "propagule_stage"; public const RAIL_DATA_BIT = "rail_data_bit"; public const RAIL_DIRECTION = "rail_direction"; diff --git a/src/data/bedrock/block/BlockTypeNames.php b/src/data/bedrock/block/BlockTypeNames.php index 71402c74a..32c91571b 100644 --- a/src/data/bedrock/block/BlockTypeNames.php +++ b/src/data/bedrock/block/BlockTypeNames.php @@ -42,6 +42,7 @@ final class BlockTypeNames{ public const ACACIA_PLANKS = "minecraft:acacia_planks"; public const ACACIA_PRESSURE_PLATE = "minecraft:acacia_pressure_plate"; public const ACACIA_SAPLING = "minecraft:acacia_sapling"; + public const ACACIA_SHELF = "minecraft:acacia_shelf"; public const ACACIA_SLAB = "minecraft:acacia_slab"; public const ACACIA_STAIRS = "minecraft:acacia_stairs"; public const ACACIA_STANDING_SIGN = "minecraft:acacia_standing_sign"; @@ -80,6 +81,7 @@ final class BlockTypeNames{ public const BAMBOO_PLANKS = "minecraft:bamboo_planks"; public const BAMBOO_PRESSURE_PLATE = "minecraft:bamboo_pressure_plate"; public const BAMBOO_SAPLING = "minecraft:bamboo_sapling"; + public const BAMBOO_SHELF = "minecraft:bamboo_shelf"; public const BAMBOO_SLAB = "minecraft:bamboo_slab"; public const BAMBOO_STAIRS = "minecraft:bamboo_stairs"; public const BAMBOO_STANDING_SIGN = "minecraft:bamboo_standing_sign"; @@ -107,6 +109,7 @@ final class BlockTypeNames{ public const BIRCH_PLANKS = "minecraft:birch_planks"; public const BIRCH_PRESSURE_PLATE = "minecraft:birch_pressure_plate"; public const BIRCH_SAPLING = "minecraft:birch_sapling"; + public const BIRCH_SHELF = "minecraft:birch_shelf"; public const BIRCH_SLAB = "minecraft:birch_slab"; public const BIRCH_STAIRS = "minecraft:birch_stairs"; public const BIRCH_STANDING_SIGN = "minecraft:birch_standing_sign"; @@ -192,7 +195,6 @@ final class BlockTypeNames{ public const CAVE_VINES = "minecraft:cave_vines"; public const CAVE_VINES_BODY_WITH_BERRIES = "minecraft:cave_vines_body_with_berries"; public const CAVE_VINES_HEAD_WITH_BERRIES = "minecraft:cave_vines_head_with_berries"; - public const CHAIN = "minecraft:chain"; public const CHAIN_COMMAND_BLOCK = "minecraft:chain_command_block"; public const CHALKBOARD = "minecraft:chalkboard"; public const CHEMICAL_HEAT = "minecraft:chemical_heat"; @@ -207,6 +209,7 @@ final class BlockTypeNames{ public const CHERRY_PLANKS = "minecraft:cherry_planks"; public const CHERRY_PRESSURE_PLATE = "minecraft:cherry_pressure_plate"; public const CHERRY_SAPLING = "minecraft:cherry_sapling"; + public const CHERRY_SHELF = "minecraft:cherry_shelf"; public const CHERRY_SLAB = "minecraft:cherry_slab"; public const CHERRY_STAIRS = "minecraft:cherry_stairs"; public const CHERRY_STANDING_SIGN = "minecraft:cherry_standing_sign"; @@ -253,12 +256,17 @@ final class BlockTypeNames{ public const COMPOSTER = "minecraft:composter"; public const COMPOUND_CREATOR = "minecraft:compound_creator"; public const CONDUIT = "minecraft:conduit"; + public const COPPER_BARS = "minecraft:copper_bars"; public const COPPER_BLOCK = "minecraft:copper_block"; public const COPPER_BULB = "minecraft:copper_bulb"; + public const COPPER_CHAIN = "minecraft:copper_chain"; public const COPPER_CHEST = "minecraft:copper_chest"; public const COPPER_DOOR = "minecraft:copper_door"; + public const COPPER_GOLEM_STATUE = "minecraft:copper_golem_statue"; public const COPPER_GRATE = "minecraft:copper_grate"; + public const COPPER_LANTERN = "minecraft:copper_lantern"; public const COPPER_ORE = "minecraft:copper_ore"; + public const COPPER_TORCH = "minecraft:copper_torch"; public const COPPER_TRAPDOOR = "minecraft:copper_trapdoor"; public const CORNFLOWER = "minecraft:cornflower"; public const CRACKED_DEEPSLATE_BRICKS = "minecraft:cracked_deepslate_bricks"; @@ -282,6 +290,7 @@ final class BlockTypeNames{ public const CRIMSON_PLANKS = "minecraft:crimson_planks"; public const CRIMSON_PRESSURE_PLATE = "minecraft:crimson_pressure_plate"; public const CRIMSON_ROOTS = "minecraft:crimson_roots"; + public const CRIMSON_SHELF = "minecraft:crimson_shelf"; public const CRIMSON_SLAB = "minecraft:crimson_slab"; public const CRIMSON_STAIRS = "minecraft:crimson_stairs"; public const CRIMSON_STANDING_SIGN = "minecraft:crimson_standing_sign"; @@ -322,6 +331,7 @@ final class BlockTypeNames{ public const DARK_OAK_PLANKS = "minecraft:dark_oak_planks"; public const DARK_OAK_PRESSURE_PLATE = "minecraft:dark_oak_pressure_plate"; public const DARK_OAK_SAPLING = "minecraft:dark_oak_sapling"; + public const DARK_OAK_SHELF = "minecraft:dark_oak_shelf"; public const DARK_OAK_SLAB = "minecraft:dark_oak_slab"; public const DARK_OAK_STAIRS = "minecraft:dark_oak_stairs"; public const DARK_OAK_TRAPDOOR = "minecraft:dark_oak_trapdoor"; @@ -533,15 +543,20 @@ final class BlockTypeNames{ public const ENDER_CHEST = "minecraft:ender_chest"; public const EXPOSED_CHISELED_COPPER = "minecraft:exposed_chiseled_copper"; public const EXPOSED_COPPER = "minecraft:exposed_copper"; + public const EXPOSED_COPPER_BARS = "minecraft:exposed_copper_bars"; public const EXPOSED_COPPER_BULB = "minecraft:exposed_copper_bulb"; + public const EXPOSED_COPPER_CHAIN = "minecraft:exposed_copper_chain"; public const EXPOSED_COPPER_CHEST = "minecraft:exposed_copper_chest"; public const EXPOSED_COPPER_DOOR = "minecraft:exposed_copper_door"; + public const EXPOSED_COPPER_GOLEM_STATUE = "minecraft:exposed_copper_golem_statue"; public const EXPOSED_COPPER_GRATE = "minecraft:exposed_copper_grate"; + public const EXPOSED_COPPER_LANTERN = "minecraft:exposed_copper_lantern"; public const EXPOSED_COPPER_TRAPDOOR = "minecraft:exposed_copper_trapdoor"; public const EXPOSED_CUT_COPPER = "minecraft:exposed_cut_copper"; public const EXPOSED_CUT_COPPER_SLAB = "minecraft:exposed_cut_copper_slab"; public const EXPOSED_CUT_COPPER_STAIRS = "minecraft:exposed_cut_copper_stairs"; public const EXPOSED_DOUBLE_CUT_COPPER_SLAB = "minecraft:exposed_double_cut_copper_slab"; + public const EXPOSED_LIGHTNING_ROD = "minecraft:exposed_lightning_rod"; public const FARMLAND = "minecraft:farmland"; public const FENCE_GATE = "minecraft:fence_gate"; public const FERN = "minecraft:fern"; @@ -660,6 +675,7 @@ final class BlockTypeNames{ public const INVISIBLE_BEDROCK = "minecraft:invisible_bedrock"; public const IRON_BARS = "minecraft:iron_bars"; public const IRON_BLOCK = "minecraft:iron_block"; + public const IRON_CHAIN = "minecraft:iron_chain"; public const IRON_DOOR = "minecraft:iron_door"; public const IRON_ORE = "minecraft:iron_ore"; public const IRON_TRAPDOOR = "minecraft:iron_trapdoor"; @@ -676,6 +692,7 @@ final class BlockTypeNames{ public const JUNGLE_PLANKS = "minecraft:jungle_planks"; public const JUNGLE_PRESSURE_PLATE = "minecraft:jungle_pressure_plate"; public const JUNGLE_SAPLING = "minecraft:jungle_sapling"; + public const JUNGLE_SHELF = "minecraft:jungle_shelf"; public const JUNGLE_SLAB = "minecraft:jungle_slab"; public const JUNGLE_STAIRS = "minecraft:jungle_stairs"; public const JUNGLE_STANDING_SIGN = "minecraft:jungle_standing_sign"; @@ -779,6 +796,7 @@ final class BlockTypeNames{ public const MANGROVE_PRESSURE_PLATE = "minecraft:mangrove_pressure_plate"; public const MANGROVE_PROPAGULE = "minecraft:mangrove_propagule"; public const MANGROVE_ROOTS = "minecraft:mangrove_roots"; + public const MANGROVE_SHELF = "minecraft:mangrove_shelf"; public const MANGROVE_SLAB = "minecraft:mangrove_slab"; public const MANGROVE_STAIRS = "minecraft:mangrove_stairs"; public const MANGROVE_STANDING_SIGN = "minecraft:mangrove_standing_sign"; @@ -836,6 +854,7 @@ final class BlockTypeNames{ public const OAK_LOG = "minecraft:oak_log"; public const OAK_PLANKS = "minecraft:oak_planks"; public const OAK_SAPLING = "minecraft:oak_sapling"; + public const OAK_SHELF = "minecraft:oak_shelf"; public const OAK_SLAB = "minecraft:oak_slab"; public const OAK_STAIRS = "minecraft:oak_stairs"; public const OAK_WOOD = "minecraft:oak_wood"; @@ -858,15 +877,20 @@ final class BlockTypeNames{ public const OXEYE_DAISY = "minecraft:oxeye_daisy"; public const OXIDIZED_CHISELED_COPPER = "minecraft:oxidized_chiseled_copper"; public const OXIDIZED_COPPER = "minecraft:oxidized_copper"; + public const OXIDIZED_COPPER_BARS = "minecraft:oxidized_copper_bars"; public const OXIDIZED_COPPER_BULB = "minecraft:oxidized_copper_bulb"; + public const OXIDIZED_COPPER_CHAIN = "minecraft:oxidized_copper_chain"; public const OXIDIZED_COPPER_CHEST = "minecraft:oxidized_copper_chest"; public const OXIDIZED_COPPER_DOOR = "minecraft:oxidized_copper_door"; + public const OXIDIZED_COPPER_GOLEM_STATUE = "minecraft:oxidized_copper_golem_statue"; public const OXIDIZED_COPPER_GRATE = "minecraft:oxidized_copper_grate"; + public const OXIDIZED_COPPER_LANTERN = "minecraft:oxidized_copper_lantern"; public const OXIDIZED_COPPER_TRAPDOOR = "minecraft:oxidized_copper_trapdoor"; public const OXIDIZED_CUT_COPPER = "minecraft:oxidized_cut_copper"; public const OXIDIZED_CUT_COPPER_SLAB = "minecraft:oxidized_cut_copper_slab"; public const OXIDIZED_CUT_COPPER_STAIRS = "minecraft:oxidized_cut_copper_stairs"; public const OXIDIZED_DOUBLE_CUT_COPPER_SLAB = "minecraft:oxidized_double_cut_copper_slab"; + public const OXIDIZED_LIGHTNING_ROD = "minecraft:oxidized_lightning_rod"; public const PACKED_ICE = "minecraft:packed_ice"; public const PACKED_MUD = "minecraft:packed_mud"; public const PALE_HANGING_MOSS = "minecraft:pale_hanging_moss"; @@ -883,6 +907,7 @@ final class BlockTypeNames{ public const PALE_OAK_PLANKS = "minecraft:pale_oak_planks"; public const PALE_OAK_PRESSURE_PLATE = "minecraft:pale_oak_pressure_plate"; public const PALE_OAK_SAPLING = "minecraft:pale_oak_sapling"; + public const PALE_OAK_SHELF = "minecraft:pale_oak_shelf"; public const PALE_OAK_SLAB = "minecraft:pale_oak_slab"; public const PALE_OAK_STAIRS = "minecraft:pale_oak_stairs"; public const PALE_OAK_STANDING_SIGN = "minecraft:pale_oak_standing_sign"; @@ -1099,6 +1124,7 @@ final class BlockTypeNames{ public const SPRUCE_PLANKS = "minecraft:spruce_planks"; public const SPRUCE_PRESSURE_PLATE = "minecraft:spruce_pressure_plate"; public const SPRUCE_SAPLING = "minecraft:spruce_sapling"; + public const SPRUCE_SHELF = "minecraft:spruce_shelf"; public const SPRUCE_SLAB = "minecraft:spruce_slab"; public const SPRUCE_STAIRS = "minecraft:spruce_stairs"; public const SPRUCE_STANDING_SIGN = "minecraft:spruce_standing_sign"; @@ -1202,6 +1228,7 @@ final class BlockTypeNames{ public const WARPED_PLANKS = "minecraft:warped_planks"; public const WARPED_PRESSURE_PLATE = "minecraft:warped_pressure_plate"; public const WARPED_ROOTS = "minecraft:warped_roots"; + public const WARPED_SHELF = "minecraft:warped_shelf"; public const WARPED_SLAB = "minecraft:warped_slab"; public const WARPED_STAIRS = "minecraft:warped_stairs"; public const WARPED_STANDING_SIGN = "minecraft:warped_standing_sign"; @@ -1213,10 +1240,14 @@ final class BlockTypeNames{ public const WATERLILY = "minecraft:waterlily"; public const WAXED_CHISELED_COPPER = "minecraft:waxed_chiseled_copper"; public const WAXED_COPPER = "minecraft:waxed_copper"; + public const WAXED_COPPER_BARS = "minecraft:waxed_copper_bars"; public const WAXED_COPPER_BULB = "minecraft:waxed_copper_bulb"; + public const WAXED_COPPER_CHAIN = "minecraft:waxed_copper_chain"; public const WAXED_COPPER_CHEST = "minecraft:waxed_copper_chest"; public const WAXED_COPPER_DOOR = "minecraft:waxed_copper_door"; + public const WAXED_COPPER_GOLEM_STATUE = "minecraft:waxed_copper_golem_statue"; public const WAXED_COPPER_GRATE = "minecraft:waxed_copper_grate"; + public const WAXED_COPPER_LANTERN = "minecraft:waxed_copper_lantern"; public const WAXED_COPPER_TRAPDOOR = "minecraft:waxed_copper_trapdoor"; public const WAXED_CUT_COPPER = "minecraft:waxed_cut_copper"; public const WAXED_CUT_COPPER_SLAB = "minecraft:waxed_cut_copper_slab"; @@ -1224,48 +1255,69 @@ final class BlockTypeNames{ public const WAXED_DOUBLE_CUT_COPPER_SLAB = "minecraft:waxed_double_cut_copper_slab"; public const WAXED_EXPOSED_CHISELED_COPPER = "minecraft:waxed_exposed_chiseled_copper"; public const WAXED_EXPOSED_COPPER = "minecraft:waxed_exposed_copper"; + public const WAXED_EXPOSED_COPPER_BARS = "minecraft:waxed_exposed_copper_bars"; public const WAXED_EXPOSED_COPPER_BULB = "minecraft:waxed_exposed_copper_bulb"; + public const WAXED_EXPOSED_COPPER_CHAIN = "minecraft:waxed_exposed_copper_chain"; public const WAXED_EXPOSED_COPPER_CHEST = "minecraft:waxed_exposed_copper_chest"; public const WAXED_EXPOSED_COPPER_DOOR = "minecraft:waxed_exposed_copper_door"; + public const WAXED_EXPOSED_COPPER_GOLEM_STATUE = "minecraft:waxed_exposed_copper_golem_statue"; public const WAXED_EXPOSED_COPPER_GRATE = "minecraft:waxed_exposed_copper_grate"; + public const WAXED_EXPOSED_COPPER_LANTERN = "minecraft:waxed_exposed_copper_lantern"; public const WAXED_EXPOSED_COPPER_TRAPDOOR = "minecraft:waxed_exposed_copper_trapdoor"; public const WAXED_EXPOSED_CUT_COPPER = "minecraft:waxed_exposed_cut_copper"; public const WAXED_EXPOSED_CUT_COPPER_SLAB = "minecraft:waxed_exposed_cut_copper_slab"; public const WAXED_EXPOSED_CUT_COPPER_STAIRS = "minecraft:waxed_exposed_cut_copper_stairs"; public const WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB = "minecraft:waxed_exposed_double_cut_copper_slab"; + public const WAXED_EXPOSED_LIGHTNING_ROD = "minecraft:waxed_exposed_lightning_rod"; + public const WAXED_LIGHTNING_ROD = "minecraft:waxed_lightning_rod"; public const WAXED_OXIDIZED_CHISELED_COPPER = "minecraft:waxed_oxidized_chiseled_copper"; public const WAXED_OXIDIZED_COPPER = "minecraft:waxed_oxidized_copper"; + public const WAXED_OXIDIZED_COPPER_BARS = "minecraft:waxed_oxidized_copper_bars"; public const WAXED_OXIDIZED_COPPER_BULB = "minecraft:waxed_oxidized_copper_bulb"; + public const WAXED_OXIDIZED_COPPER_CHAIN = "minecraft:waxed_oxidized_copper_chain"; public const WAXED_OXIDIZED_COPPER_CHEST = "minecraft:waxed_oxidized_copper_chest"; public const WAXED_OXIDIZED_COPPER_DOOR = "minecraft:waxed_oxidized_copper_door"; + public const WAXED_OXIDIZED_COPPER_GOLEM_STATUE = "minecraft:waxed_oxidized_copper_golem_statue"; public const WAXED_OXIDIZED_COPPER_GRATE = "minecraft:waxed_oxidized_copper_grate"; + public const WAXED_OXIDIZED_COPPER_LANTERN = "minecraft:waxed_oxidized_copper_lantern"; public const WAXED_OXIDIZED_COPPER_TRAPDOOR = "minecraft:waxed_oxidized_copper_trapdoor"; public const WAXED_OXIDIZED_CUT_COPPER = "minecraft:waxed_oxidized_cut_copper"; public const WAXED_OXIDIZED_CUT_COPPER_SLAB = "minecraft:waxed_oxidized_cut_copper_slab"; public const WAXED_OXIDIZED_CUT_COPPER_STAIRS = "minecraft:waxed_oxidized_cut_copper_stairs"; public const WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB = "minecraft:waxed_oxidized_double_cut_copper_slab"; + public const WAXED_OXIDIZED_LIGHTNING_ROD = "minecraft:waxed_oxidized_lightning_rod"; public const WAXED_WEATHERED_CHISELED_COPPER = "minecraft:waxed_weathered_chiseled_copper"; public const WAXED_WEATHERED_COPPER = "minecraft:waxed_weathered_copper"; + public const WAXED_WEATHERED_COPPER_BARS = "minecraft:waxed_weathered_copper_bars"; public const WAXED_WEATHERED_COPPER_BULB = "minecraft:waxed_weathered_copper_bulb"; + public const WAXED_WEATHERED_COPPER_CHAIN = "minecraft:waxed_weathered_copper_chain"; public const WAXED_WEATHERED_COPPER_CHEST = "minecraft:waxed_weathered_copper_chest"; public const WAXED_WEATHERED_COPPER_DOOR = "minecraft:waxed_weathered_copper_door"; + public const WAXED_WEATHERED_COPPER_GOLEM_STATUE = "minecraft:waxed_weathered_copper_golem_statue"; public const WAXED_WEATHERED_COPPER_GRATE = "minecraft:waxed_weathered_copper_grate"; + public const WAXED_WEATHERED_COPPER_LANTERN = "minecraft:waxed_weathered_copper_lantern"; public const WAXED_WEATHERED_COPPER_TRAPDOOR = "minecraft:waxed_weathered_copper_trapdoor"; public const WAXED_WEATHERED_CUT_COPPER = "minecraft:waxed_weathered_cut_copper"; public const WAXED_WEATHERED_CUT_COPPER_SLAB = "minecraft:waxed_weathered_cut_copper_slab"; public const WAXED_WEATHERED_CUT_COPPER_STAIRS = "minecraft:waxed_weathered_cut_copper_stairs"; public const WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB = "minecraft:waxed_weathered_double_cut_copper_slab"; + public const WAXED_WEATHERED_LIGHTNING_ROD = "minecraft:waxed_weathered_lightning_rod"; public const WEATHERED_CHISELED_COPPER = "minecraft:weathered_chiseled_copper"; public const WEATHERED_COPPER = "minecraft:weathered_copper"; + public const WEATHERED_COPPER_BARS = "minecraft:weathered_copper_bars"; public const WEATHERED_COPPER_BULB = "minecraft:weathered_copper_bulb"; + public const WEATHERED_COPPER_CHAIN = "minecraft:weathered_copper_chain"; public const WEATHERED_COPPER_CHEST = "minecraft:weathered_copper_chest"; public const WEATHERED_COPPER_DOOR = "minecraft:weathered_copper_door"; + public const WEATHERED_COPPER_GOLEM_STATUE = "minecraft:weathered_copper_golem_statue"; public const WEATHERED_COPPER_GRATE = "minecraft:weathered_copper_grate"; + public const WEATHERED_COPPER_LANTERN = "minecraft:weathered_copper_lantern"; public const WEATHERED_COPPER_TRAPDOOR = "minecraft:weathered_copper_trapdoor"; public const WEATHERED_CUT_COPPER = "minecraft:weathered_cut_copper"; public const WEATHERED_CUT_COPPER_SLAB = "minecraft:weathered_cut_copper_slab"; public const WEATHERED_CUT_COPPER_STAIRS = "minecraft:weathered_cut_copper_stairs"; public const WEATHERED_DOUBLE_CUT_COPPER_SLAB = "minecraft:weathered_double_cut_copper_slab"; + public const WEATHERED_LIGHTNING_ROD = "minecraft:weathered_lightning_rod"; public const WEB = "minecraft:web"; public const WEEPING_VINES = "minecraft:weeping_vines"; public const WET_SPONGE = "minecraft:wet_sponge"; diff --git a/src/data/bedrock/block/convert/VanillaBlockMappings.php b/src/data/bedrock/block/convert/VanillaBlockMappings.php index f339ce3ef..e1be1b037 100644 --- a/src/data/bedrock/block/convert/VanillaBlockMappings.php +++ b/src/data/bedrock/block/convert/VanillaBlockMappings.php @@ -1264,7 +1264,7 @@ final class VanillaBlockMappings{ $reg->mapModel(Model::create(Blocks::CARVED_PUMPKIN(), Ids::CARVED_PUMPKIN)->properties([ $commonProperties->horizontalFacingCardinal ])); - $reg->mapModel(Model::create(Blocks::CHAIN(), Ids::CHAIN)->properties([$commonProperties->pillarAxis])); + $reg->mapModel(Model::create(Blocks::CHAIN(), Ids::IRON_CHAIN)->properties([$commonProperties->pillarAxis])); $reg->mapModel(Model::create(Blocks::CHISELED_BOOKSHELF(), Ids::CHISELED_BOOKSHELF)->properties([ $commonProperties->horizontalFacingSWNE, new ValueSetFromIntProperty( @@ -1355,7 +1355,10 @@ final class VanillaBlockMappings{ new ValueFromStringProperty(StateNames::LEVER_DIRECTION, ValueMappings::getInstance()->leverFacing, fn(Lever $b) => $b->getFacing(), fn(Lever $b, LeverFacing $v) => $b->setFacing($v)), new BoolProperty(StateNames::OPEN_BIT, fn(Lever $b) => $b->isActivated(), fn(Lever $b, bool $v) => $b->setActivated($v)), ])); - $reg->mapModel(Model::create(Blocks::LIGHTNING_ROD(), Ids::LIGHTNING_ROD)->properties([$commonProperties->anyFacingClassic])); + $reg->mapModel(Model::create(Blocks::LIGHTNING_ROD(), Ids::LIGHTNING_ROD)->properties([ + $commonProperties->anyFacingClassic, + new DummyProperty(StateNames::POWERED_BIT, false) //TODO + ])); $reg->mapModel(Model::create(Blocks::LIT_PUMPKIN(), Ids::LIT_PUMPKIN)->properties([$commonProperties->horizontalFacingCardinal])); $reg->mapModel(Model::create(Blocks::LOOM(), Ids::LOOM)->properties([$commonProperties->horizontalFacingSWNE])); diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index bb4c61509..36edb6078 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -141,7 +141,6 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Block(Ids::CAKE, Blocks::CAKE()); $this->map1to1Block(Ids::CAMPFIRE, Blocks::CAMPFIRE()); $this->map1to1Block(Ids::CAULDRON, Blocks::CAULDRON()); - $this->map1to1Block(Ids::CHAIN, Blocks::CHAIN()); $this->map1to1Block(Ids::CHERRY_DOOR, Blocks::CHERRY_DOOR()); $this->map1to1Block(Ids::COMPARATOR, Blocks::REDSTONE_COMPARATOR()); $this->map1to1Block(Ids::CRIMSON_DOOR, Blocks::CRIMSON_DOOR()); @@ -519,6 +518,14 @@ final class ItemSerializerDeserializerRegistrar{ }, fn(GoatHorn $item) => GoatHornTypeIdMap::getInstance()->toId($item->getHornType()) ); + $this->map1to1ItemWithMeta( + Ids::LINGERING_POTION, + Items::LINGERING_POTION(), + function(SplashPotion $item, int $meta) : void{ + $item->setType(PotionTypeIdMap::getInstance()->fromId($meta) ?? throw new ItemTypeDeserializeException("Unknown potion type ID $meta")); + }, + fn(SplashPotion $item) => PotionTypeIdMap::getInstance()->toId($item->getType()) + ); $this->map1to1ItemWithMeta( Ids::MEDICINE, Items::MEDICINE(), diff --git a/src/data/bedrock/item/ItemTypeNames.php b/src/data/bedrock/item/ItemTypeNames.php index 5c648ff38..0153f6901 100644 --- a/src/data/bedrock/item/ItemTypeNames.php +++ b/src/data/bedrock/item/ItemTypeNames.php @@ -112,7 +112,6 @@ final class ItemTypeNames{ public const CAT_SPAWN_EGG = "minecraft:cat_spawn_egg"; public const CAULDRON = "minecraft:cauldron"; public const CAVE_SPIDER_SPAWN_EGG = "minecraft:cave_spider_spawn_egg"; - public const CHAIN = "minecraft:chain"; public const CHAINMAIL_BOOTS = "minecraft:chainmail_boots"; public const CHAINMAIL_CHESTPLATE = "minecraft:chainmail_chestplate"; public const CHAINMAIL_HELMET = "minecraft:chainmail_helmet"; @@ -160,6 +159,7 @@ final class ItemTypeNames{ public const COPPER_GOLEM_SPAWN_EGG = "minecraft:copper_golem_spawn_egg"; public const COPPER_HELMET = "minecraft:copper_helmet"; public const COPPER_HOE = "minecraft:copper_hoe"; + public const COPPER_HORSE_ARMOR = "minecraft:copper_horse_armor"; public const COPPER_INGOT = "minecraft:copper_ingot"; public const COPPER_LEGGINGS = "minecraft:copper_leggings"; public const COPPER_NUGGET = "minecraft:copper_nugget"; diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php index 94639cd3f..091654c4a 100644 --- a/src/entity/EntityFactory.php +++ b/src/entity/EntityFactory.php @@ -32,6 +32,7 @@ use pocketmine\data\bedrock\PotionTypeIdMap; use pocketmine\data\bedrock\PotionTypeIds; use pocketmine\data\SavedDataLoadingException; use pocketmine\entity\EntityDataHelper as Helper; +use pocketmine\entity\object\AreaEffectCloud; use pocketmine\entity\object\EndCrystal; use pocketmine\entity\object\ExperienceOrb; use pocketmine\entity\object\FallingBlock; @@ -87,6 +88,10 @@ final class EntityFactory{ //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(AreaEffectCloud::class, function(World $world, CompoundTag $nbt) : AreaEffectCloud{ + return new AreaEffectCloud(Helper::parseLocation($nbt, $world), $nbt); + }, ['AreaEffectCloud', 'minecraft:area_effect_cloud']); + $this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{ return new Arrow(Helper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt); }, ['Arrow', 'minecraft:arrow']); diff --git a/src/entity/Human.php b/src/entity/Human.php index 97ebdefca..132f971ee 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -309,9 +309,11 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ if($slot >= 0 && $slot < 9){ //Hotbar //Old hotbar saving stuff, ignore it }elseif($slot >= 100 && $slot < 104){ //Armor - $armorInventoryItems[$slot - 100] = Item::nbtDeserialize($item); + $armorSlot = $slot - 100; + $armorInventoryItems[$armorSlot] = Item::safeNbtDeserialize($item, "Human armor slot $armorSlot"); }elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){ - $inventoryItems[$slot - 9] = Item::nbtDeserialize($item); + $inventorySlot = $slot - 9; + $inventoryItems[$inventorySlot] = Item::safeNbtDeserialize($item, "Human inventory slot $inventorySlot"); } } @@ -320,7 +322,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ } $offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM); if($offHand !== null){ - $this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand)); + $this->offHandInventory->setItem(0, Item::safeNbtDeserialize($offHand, "Human off-hand item")); } $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent( $this->getViewers(), @@ -331,8 +333,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ if($enderChestInventoryTag !== null){ $enderChestInventoryItems = []; - foreach($enderChestInventoryTag as $i => $item){ - $enderChestInventoryItems[$item->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($item); + foreach($enderChestInventoryTag as $item){ + $slot = $item->getByte(SavedItemStackData::TAG_SLOT); + $enderChestInventoryItems[$slot] = Item::safeNbtDeserialize($item, "Human ender chest slot $slot"); } self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems); } diff --git a/src/entity/effect/EffectCollection.php b/src/entity/effect/EffectCollection.php new file mode 100644 index 000000000..ed1abf65b --- /dev/null +++ b/src/entity/effect/EffectCollection.php @@ -0,0 +1,223 @@ + + */ + protected ObjectSet $effectAddHooks; + + /** + * @var \Closure[]|ObjectSet + * @phpstan-var ObjectSet<\Closure(EffectInstance) : void> + */ + protected ObjectSet $effectRemoveHooks; + + protected Color $bubbleColor; + + protected bool $onlyAmbientEffects = false; + + /** + * Validates whether an effect will be used for bubbles color calculation. + * + * @phpstan-var \Closure(EffectInstance) : bool + */ + protected \Closure $effectFilterForBubbles; + + public function __construct(){ + $this->bubbleColor = new Color(0, 0, 0, 0); + $this->effectAddHooks = new ObjectSet(); + $this->effectRemoveHooks = new ObjectSet(); + + $this->setEffectFilterForBubbles(static fn(EffectInstance $e) : bool => $e->isVisible() && $e->getType()->hasBubbles()); + } + + /** + * Returns all the effects in the collection, indexed by spl_object_id of the effect type. + * @return EffectInstance[] + */ + public function all() : array{ + return $this->effects; + } + + /** + * Removes all effects. + */ + public function clear() : void{ + foreach($this->effects as $effect){ + $this->remove($effect->getType()); + } + } + + /** + * Removes the effect with the specified ID. + */ + public function remove(Effect $effectType) : void{ + $index = spl_object_id($effectType); + if(isset($this->effects[$index])){ + $effect = $this->effects[$index]; + + unset($this->effects[$index]); + foreach($this->effectRemoveHooks as $hook){ + $hook($effect); + } + + $this->recalculateEffectColor(); + } + } + + /** + * Returns the effect instance active with the specified ID, or null if 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. + */ + public function has(Effect $effect) : bool{ + return isset($this->effects[spl_object_id($effect)]); + } + + /** + * In the following cases it will return true: + * - if the effect type is not already applied + * - if an existing effect of the same type can be replaced (due to shorter duration or lower level) + */ + public function canAdd(EffectInstance $effect) : bool{ + $index = spl_object_id($effect->getType()); + if(isset($this->effects[$index])){ + $oldEffect = $this->effects[$index]; + if( + abs($effect->getAmplifier()) < $oldEffect->getAmplifier() + || (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration()) + ){ + return false; + } + } + return true; + } + + /** + * Adds an effect to the collection. + * Existing effects of the same type will be replaced if {@see self::canAdd()} returns true. + * + * @return bool whether the effect has been successfully applied. + */ + public function add(EffectInstance $effect) : bool{ + if($this->canAdd($effect)){ + $index = spl_object_id($effect->getType()); + $replacesOldEffect = isset($this->effects[$index]); + + $this->effects[$index] = $effect; + foreach($this->effectAddHooks as $hook){ + $hook($effect, $replacesOldEffect); + } + + $this->recalculateEffectColor(); + return true; + } + + return false; + } + + /** + * Sets the filter that determines which effects will be displayed in the bubbles. + * + * @phpstan-param \Closure(EffectInstance) : bool $filter + */ + public function setEffectFilterForBubbles(\Closure $filter) : void{ + Utils::validateCallableSignature(fn(EffectInstance $e) : bool => false, $filter); + $this->effectFilterForBubbles = $filter; + } + + /** + * Recalculates the potion bubbles colour based on the active effects. + */ + protected function recalculateEffectColor() : void{ + /** @var Color[] $colors */ + $colors = []; + $ambient = true; + foreach($this->effects as $effect){ + if(($this->effectFilterForBubbles)($effect)){ + $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; + } + + /** + * @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/EffectManager.php b/src/entity/effect/EffectManager.php index 747662dc7..448681ede 100644 --- a/src/entity/effect/EffectManager.php +++ b/src/entity/effect/EffectManager.php @@ -23,56 +23,18 @@ declare(strict_types=1); namespace pocketmine\entity\effect; -use pocketmine\color\Color; use pocketmine\entity\Living; use pocketmine\event\entity\EntityEffectAddEvent; use pocketmine\event\entity\EntityEffectRemoveEvent; -use pocketmine\utils\ObjectSet; -use function abs; use function count; use function spl_object_id; -class EffectManager{ - /** @var EffectInstance[] */ - protected array $effects = []; - - protected Color $bubbleColor; - protected bool $onlyAmbientEffects = false; - - /** - * @var \Closure[]|ObjectSet - * @phpstan-var ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void> - */ - protected ObjectSet $effectAddHooks; - /** - * @var \Closure[]|ObjectSet - * @phpstan-var ObjectSet<\Closure(EffectInstance) : void> - */ - protected ObjectSet $effectRemoveHooks; +class EffectManager extends EffectCollection{ public function __construct( private Living $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()); - } + parent::__construct(); } /** @@ -91,55 +53,17 @@ class EffectManager{ return; } - unset($this->effects[$index]); $effect->getType()->remove($this->entity, $effect); - foreach($this->effectRemoveHooks as $hook){ - $hook($effect); - } - - $this->recalculateEffectColor(); + parent::remove($effectType); } } - /** - * 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() - || (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration()) - ){ - $cancelled = true; - } - } + $oldEffect = $this->effects[$index] ?? null; $ev = new EntityEffectAddEvent($this->entity, $effect, $oldEffect); - if($cancelled){ + if(!$this->canAdd($effect)){ $ev->cancel(); } @@ -153,53 +77,8 @@ class EffectManager{ } $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() && $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; + return parent::add($effect); } public function tick(int $tickDiff = 1) : bool{ @@ -216,20 +95,4 @@ class EffectManager{ 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/object/AreaEffectCloud.php b/src/entity/object/AreaEffectCloud.php new file mode 100644 index 000000000..3f7d1fff9 --- /dev/null +++ b/src/entity/object/AreaEffectCloud.php @@ -0,0 +1,427 @@ + entity ID => expiration */ + protected array $victims = []; + + protected int $maxAge = self::DEFAULT_DURATION; + protected int $maxAgeChangeOnUse = self::DEFAULT_DURATION_CHANGE_ON_USE; + + protected int $reapplicationDelay = self::REAPPLICATION_DELAY; + + protected int $pickupCount = 0; + protected float $radiusChangeOnPickup = self::DEFAULT_RADIUS_CHANGE_ON_PICKUP; + + protected float $initialRadius = self::DEFAULT_RADIUS; + protected float $radius = self::DEFAULT_RADIUS; + protected float $radiusChangeOnUse = self::DEFAULT_RADIUS_CHANGE_ON_USE; + protected float $radiusChangePerTick = self::DEFAULT_RADIUS_CHANGE_PER_TICK; + + public function __construct( + Location $location, + ?CompoundTag $nbt = null + ){ + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.5, $this->radius * 2); } + + protected function getInitialDragMultiplier() : float{ return 0.0; } + + protected function getInitialGravity() : float{ return 0.0; } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->effectCollection = new EffectCollection(); + $this->effectCollection->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); + $this->effectCollection->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); + $this->effectCollection->setEffectFilterForBubbles(static fn(EffectInstance $e) : bool => $e->isVisible()); + + $worldTime = $this->getWorld()->getTime(); + $this->age = max($worldTime - $nbt->getLong(self::TAG_SPAWN_TICK, $worldTime), 0); + $this->maxAge = $nbt->getInt(self::TAG_DURATION, self::DEFAULT_DURATION); + $this->maxAgeChangeOnUse = $nbt->getInt(self::TAG_DURATION_ON_USE, self::DEFAULT_DURATION_CHANGE_ON_USE); + $this->pickupCount = $nbt->getInt(self::TAG_PICKUP_COUNT, 0); + $this->reapplicationDelay = $nbt->getInt(self::TAG_REAPPLICATION_DELAY, self::REAPPLICATION_DELAY); + + $this->initialRadius = $nbt->getFloat(self::TAG_INITIAL_RADIUS, self::DEFAULT_RADIUS); + $this->setRadius($nbt->getFloat(self::TAG_RADIUS, $this->initialRadius)); + $this->radiusChangeOnPickup = $nbt->getFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, self::DEFAULT_RADIUS_CHANGE_ON_PICKUP); + $this->radiusChangeOnUse = $nbt->getFloat(self::TAG_RADIUS_ON_USE, self::DEFAULT_RADIUS_CHANGE_ON_USE); + $this->radiusChangePerTick = $nbt->getFloat(self::TAG_RADIUS_PER_TICK, self::DEFAULT_RADIUS_CHANGE_PER_TICK); + + $effectsTag = $nbt->getListTag(self::TAG_EFFECTS, CompoundTag::class); + if($effectsTag !== null){ + foreach($effectsTag as $e){ + $effect = EffectIdMap::getInstance()->fromId($e->getByte("Id")); + if($effect === null){ + continue; + } + + $this->effectCollection->add(new EffectInstance( + $effect, + $e->getInt("Duration"), + Binary::unsignByte($e->getByte("Amplifier")), + $e->getByte("ShowParticles", 1) !== 0, + $e->getByte("Ambient", 0) !== 0 + )); + } + } + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + + $nbt->setLong(self::TAG_SPAWN_TICK, $this->getWorld()->getTime() - $this->age); + $nbt->setShort(self::TAG_POTION_ID, PotionTypeIds::WATER); //not used, mobEffects is used exclusively in Bedrock + $nbt->setInt(self::TAG_DURATION, $this->maxAge); + $nbt->setInt(self::TAG_DURATION_ON_USE, $this->maxAgeChangeOnUse); + $nbt->setInt(self::TAG_PICKUP_COUNT, $this->pickupCount); + $nbt->setInt(self::TAG_REAPPLICATION_DELAY, $this->reapplicationDelay); + $nbt->setFloat(self::TAG_INITIAL_RADIUS, $this->initialRadius); + $nbt->setFloat(self::TAG_RADIUS, $this->radius); + $nbt->setFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, $this->radiusChangeOnPickup); + $nbt->setFloat(self::TAG_RADIUS_ON_USE, $this->radiusChangeOnUse); + $nbt->setFloat(self::TAG_RADIUS_PER_TICK, $this->radiusChangePerTick); + + if(count($this->effectCollection->all()) > 0){ + $effects = []; + foreach($this->effectCollection->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); + } + $nbt->setTag(self::TAG_EFFECTS, new ListTag($effects)); + } + + return $nbt; + } + + public function isFireProof() : bool{ + return true; + } + + public function canBeCollidedWith() : bool{ + return false; + } + + /** + * Returns the current age of the cloud (in ticks). + */ + public function getAge() : int{ + return $this->age; + } + + public function getEffects() : EffectCollection{ + return $this->effectCollection; + } + + /** + * Returns the initial radius (in blocks). + */ + public function getInitialRadius() : float{ + return $this->initialRadius; + } + + /** + * Returns the current radius (in blocks). + */ + public function getRadius() : float{ + return $this->radius; + } + + /** + * Sets the current radius (in blocks). + */ + protected function setRadius(float $radius) : void{ + $this->radius = $radius; + $this->setSize($this->getInitialSizeInfo()); + $this->networkPropertiesDirty = true; + } + + /** + * Returns the amount that the radius of this cloud will add by when it is + * picked up (in blocks). Usually negative resulting in a radius reduction. + * + * Applied when getting dragon breath bottle. + */ + public function getRadiusChangeOnPickup() : float{ + return $this->radiusChangeOnPickup; + } + + /** + * Sets the amount that the radius of this cloud will add by when it is + * picked up (in blocks). Usually negative resulting in a radius reduction. + * + * Applied when getting dragon breath bottle. + */ + public function setRadiusChangeOnPickup(float $radiusChangeOnPickup) : void{ + $this->radiusChangeOnPickup = $radiusChangeOnPickup; + } + + /** + * Returns the amount that the radius of this cloud will add by when it + * applies an effect to an entity (in blocks). Usually negative resulting in a radius reduction. + */ + public function getRadiusChangeOnUse() : float{ + return $this->radiusChangeOnUse; + } + + /** + * Sets the amount that the radius of this cloud will add by when it + * applies an effect to an entity (in blocks). + */ + public function setRadiusChangeOnUse(float $radiusChangeOnUse) : void{ + $this->radiusChangeOnUse = $radiusChangeOnUse; + } + + /** + * Returns the amount that the radius of this cloud will add by when an update + * is performed (in blocks). Usually negative resulting in a radius reduction. + */ + public function getRadiusChangePerTick() : float{ + return $this->radiusChangePerTick; + } + + /** + * Sets the amount that the radius of this cloud will add by when an update is performed (in blocks). + */ + public function setRadiusChangePerTick(float $radiusChangePerTick) : void{ + $this->radiusChangePerTick = $radiusChangePerTick; + } + + /** + * Returns the age at which the cloud will despawn. + */ + public function getMaxAge() : int{ + return $this->maxAge; + } + + /** + * Sets the age at which the cloud will despawn. + */ + public function setMaxAge(int $maxAge) : void{ + $this->maxAge = $maxAge; + } + + /** + * Returns the amount that the max age of this cloud will change by when it + * applies an effect to an entity (in ticks). + */ + public function getMaxAgeChangeOnUse() : int{ + return $this->maxAgeChangeOnUse; + } + + /** + * Sets the amount that the max age of this cloud will change by when it + * applies an effect to an entity (in ticks). + */ + public function setMaxAgeChangeOnUse(int $maxAgeChangeOnUse) : void{ + $this->maxAgeChangeOnUse = $maxAgeChangeOnUse; + } + + /** + * Returns the time that an entity will be immune from subsequent exposure (in ticks). + */ + public function getReapplicationDelay() : int{ + return $this->reapplicationDelay; + } + + /** + * Sets the time that an entity will be immune from subsequent exposure (in ticks). + */ + public function setReapplicationDelay(int $delay) : void{ + $this->reapplicationDelay = $delay; + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + $this->age += $tickDiff; + $radius = $this->radius + ($this->radiusChangePerTick * $tickDiff); + if($radius < 0.5){ + $this->flagForDespawn(); + return true; + } + $this->setRadius($radius); + if($this->age >= self::UPDATE_DELAY && ($this->age % self::UPDATE_DELAY) === 0){ + if($this->age > $this->maxAge){ + $this->flagForDespawn(); + return true; + } + + foreach($this->victims as $entityId => $expiration){ + if($this->age >= $expiration){ + unset($this->victims[$entityId]); + } + } + + $entities = []; + $radiusChange = 0.0; + $maxAgeChange = 0; + foreach($this->getWorld()->getCollidingEntities($this->getBoundingBox(), $this) as $entity){ + if(!$entity instanceof Living || isset($this->victims[$entity->getId()])){ + continue; + } + $entityPosition = $entity->getPosition(); + $xDiff = $entityPosition->getX() - $this->location->getX(); + $zDiff = $entityPosition->getZ() - $this->location->getZ(); + if(($xDiff ** 2 + $zDiff ** 2) > $this->radius ** 2){ + continue; + } + $entities[] = $entity; + if($this->radiusChangeOnUse !== 0.0){ + $radiusChange += $this->radiusChangeOnUse; + if($this->radius + $radiusChange <= 0){ + break; + } + } + if($this->maxAgeChangeOnUse !== 0){ + $maxAgeChange += $this->maxAgeChangeOnUse; + if($this->maxAge + $maxAgeChange <= 0){ + break; + } + } + } + if(count($entities) === 0){ + return $hasUpdate; + } + + $ev = new AreaEffectCloudApplyEvent($this, $entities); + $ev->call(); + if($ev->isCancelled()){ + return $hasUpdate; + } + + foreach($ev->getAffectedEntities() as $entity){ + foreach($this->effectCollection->all() as $effect){ + $effect = clone $effect; //avoid accidental modification + if($effect->getType() instanceof InstantEffect){ + $effect->getType()->applyEffect($entity, $effect, 0.5, $this); + }else{ + $entity->getEffects()->add($effect->setDuration((int) round($effect->getDuration() / 4))); + } + } + if($this->reapplicationDelay !== 0){ + $this->victims[$entity->getId()] = $this->age + $this->reapplicationDelay; + } + } + + $radius = $this->radius + $radiusChange; + $maxAge = $this->maxAge + $maxAgeChange; + if($radius <= 0 || $maxAge <= 0){ + $this->flagForDespawn(); + return true; + } + $this->setRadius($radius); + $this->setMaxAge($maxAge); + $hasUpdate = true; + } + + return $hasUpdate; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + //visual properties + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS, $this->radius); + $properties->setInt(EntityMetadataProperties::POTION_COLOR, Binary::signInt(( + count($this->effectCollection->all()) === 0 ? PotionSplashParticle::DEFAULT_COLOR() : $this->effectCollection->getBubbleColor() + )->toARGB())); + + //these are properties the client expects, and are used for client-sided logic, which we don't want + $properties->setByte(EntityMetadataProperties::POTION_AMBIENT, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_DURATION, -1); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_CHANGE_ON_PICKUP, 0); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_PER_TICK, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_SPAWN_TIME, 0); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_PICKUP_COUNT, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_WAITING, 0); + } + + protected function destroyCycles() : void{ + //wipe out callback refs + $this->effectCollection = new EffectCollection(); + parent::destroyCycles(); + } +} diff --git a/src/entity/object/EndCrystal.php b/src/entity/object/EndCrystal.php index 74c7664bf..2b79773dc 100644 --- a/src/entity/object/EndCrystal.php +++ b/src/entity/object/EndCrystal.php @@ -52,6 +52,8 @@ class EndCrystal extends Entity implements Explosive{ protected bool $showBase = false; protected ?Vector3 $beamTarget = null; + private bool $primed = false; + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(2.0, 2.0); } protected function getInitialDragMultiplier() : float{ return 1.0; } @@ -88,11 +90,9 @@ class EndCrystal extends Entity implements Explosive{ parent::attack($source); if( $source->getCause() !== EntityDamageEvent::CAUSE_VOID && - !$this->isFlaggedForDespawn() && !$source->isCancelled() ){ - $this->flagForDespawn(); - $this->explode(); + $this->primed = true; } } @@ -125,6 +125,13 @@ class EndCrystal extends Entity implements Explosive{ return $nbt; } + protected function onDeathUpdate(int $tickDiff) : bool{ + if($this->primed){ + $this->explode(); + } + return true; + } + public function explode() : void{ $ev = new EntityPreExplodeEvent($this, 6); $ev->call(); diff --git a/src/entity/projectile/SplashPotion.php b/src/entity/projectile/SplashPotion.php index f4635cad7..3fda33dd8 100644 --- a/src/entity/projectile/SplashPotion.php +++ b/src/entity/projectile/SplashPotion.php @@ -32,10 +32,10 @@ use pocketmine\entity\effect\InstantEffect; use pocketmine\entity\Entity; use pocketmine\entity\Living; use pocketmine\entity\Location; +use pocketmine\entity\object\AreaEffectCloud; use pocketmine\event\entity\ProjectileHitBlockEvent; use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEvent; -use pocketmine\item\Potion; use pocketmine\item\PotionType; use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\protocol\types\entity\EntityIds; @@ -96,8 +96,8 @@ class SplashPotion extends Throwable{ $this->getWorld()->addParticle($this->location, $particle); $this->broadcastSound(new PotionSplashSound()); - if($hasEffects){ - if(!$this->willLinger()){ + if(!$this->willLinger()){ + if($hasEffects){ foreach($this->getWorld()->getCollidingEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){ if($entity instanceof Living){ $distanceSquared = $entity->getEyePos()->distanceSquared($this->location); @@ -126,10 +126,18 @@ class SplashPotion extends Throwable{ } } } - }else{ - //TODO: lingering potions } - }elseif($event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){ + }else{ + $entity = new AreaEffectCloud(Location::fromObject($this->location->floor()->add(0.5, 0.5, 0.5), $this->getWorld())); + foreach($this->potionType->getEffects() as $effect){ + $entity->getEffects()->add($effect); + } + if(($owner = $this->getOwningEntity()) !== null && !$owner->isClosed()){ + $entity->setOwningEntity($owner); + } + $entity->spawnToAll(); + } + if(!$hasEffects && $event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){ $blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace()); if($blockIn->hasTypeTag(BlockTypeTags::FIRE)){ diff --git a/src/event/entity/AreaEffectCloudApplyEvent.php b/src/event/entity/AreaEffectCloudApplyEvent.php new file mode 100644 index 000000000..c6246a339 --- /dev/null +++ b/src/event/entity/AreaEffectCloudApplyEvent.php @@ -0,0 +1,64 @@ + + */ +class AreaEffectCloudApplyEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + + /** + * @param Living[] $affectedEntities + */ + public function __construct( + AreaEffectCloud $entity, + protected array $affectedEntities + ){ + $this->entity = $entity; + } + + /** + * @return AreaEffectCloud + */ + public function getEntity(){ + return $this->entity; + } + + /** + * Returns the affected entities. + * + * @return Living[] + */ + public function getAffectedEntities() : array{ + return $this->affectedEntities; + } +} diff --git a/src/item/Item.php b/src/item/Item.php index e7c86e167..6f7e010c9 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -775,6 +775,24 @@ class Item implements \JsonSerializable{ } } + /** + * Same as nbtDeserialize(), but purposely suppresses data errors and returns AIR if deserialization fails. + * An error will be logged to the global logger if this happens. + * + * @param string $errorLogContext Used in log messages if deserialization fails to aid debugging (e.g. inventory owner, slot number, etc.) + */ + public static function safeNbtDeserialize(CompoundTag $tag, string $errorLogContext, ?\Logger $logger = null) : Item{ + try{ + return self::nbtDeserialize($tag); + }catch(SavedDataLoadingException $e){ + //TODO: what if the intention was to suppress logging? + $logger ??= \GlobalLogger::get(); + $logger->error("$errorLogContext: Error deserializing item (item will be replaced by AIR): " . $e->getMessage()); + //no trace here, otherwise things could get very noisy + return VanillaItems::AIR(); + } + } + public function __clone(){ $this->nbt = clone $this->nbt; if($this->blockEntityTag !== null){ diff --git a/src/item/SplashPotion.php b/src/item/SplashPotion.php index e1c9167ac..947b1b4be 100644 --- a/src/item/SplashPotion.php +++ b/src/item/SplashPotion.php @@ -33,6 +33,16 @@ class SplashPotion extends ProjectileItem{ private PotionType $potionType = PotionType::WATER; + public function __construct( + ItemIdentifier $identifier, + string $name = "Splash Potion", + array $enchantmentTags = [], + private bool $linger = false + ){ + //TODO: remove unnecessary default parameters in PM6, they remain because backward compatibility + parent::__construct($identifier, $name, $enchantmentTags); + } + protected function describeState(RuntimeDataDescriber $w) : void{ $w->enum($this->potionType); } @@ -52,10 +62,19 @@ class SplashPotion extends ProjectileItem{ } protected function createEntity(Location $location, Player $thrower) : Throwable{ - return new SplashPotionEntity($location, $thrower, $this->potionType); + $projectile = new SplashPotionEntity($location, $thrower, $this->potionType); + $projectile->setLinger($this->linger); + return $projectile; } public function getThrowForce() : float{ return 0.5; } + + /** + * Returns whether this splash potion will create an area-effect cloud on impact. + */ + public function willLinger() : bool{ + return $this->linger; + } } diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 63fa88538..77676ea01 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1226,6 +1226,7 @@ final class StringToItemParser extends StringToTParser{ $result->register($prefix("potion"), fn() => Items::POTION()->setType($potionType)); $result->register($prefix("splash_potion"), fn() => Items::SPLASH_POTION()->setType($potionType)); + $result->register($prefix("lingering_potion"), fn() => Items::LINGERING_POTION()->setType($potionType)); } } diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index 31a62d1aa..c9b3f4917 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -222,6 +222,7 @@ use function strtolower; * @method static Armor LEATHER_CAP() * @method static Armor LEATHER_PANTS() * @method static Armor LEATHER_TUNIC() + * @method static SplashPotion LINGERING_POTION() * @method static Item MAGMA_CREAM() * @method static Boat MANGROVE_BOAT() * @method static HangingSign MANGROVE_HANGING_SIGN() @@ -543,6 +544,7 @@ final class VanillaItems{ self::register("lapis_lazuli", fn(IID $id) => new Item($id, "Lapis Lazuli")); self::register("lava_bucket", fn(IID $id) => new LiquidBucket($id, "Lava Bucket", Blocks::LAVA())); self::register("leather", fn(IID $id) => new Item($id, "Leather")); + self::register("lingering_potion", fn(IID $id) => new SplashPotion($id, "Lingering Potion", linger: true)); self::register("magma_cream", fn(IID $id) => new Item($id, "Magma Cream")); self::register("mangrove_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::MANGROVE_SIGN(), Blocks::MANGROVE_WALL_SIGN())); self::register("mangrove_hanging_sign", fn(IID $id) => new HangingSign($id, "Mangrove Hanging Sign", Blocks::MANGROVE_CEILING_CENTER_HANGING_SIGN(), Blocks::MANGROVE_CEILING_EDGES_HANGING_SIGN(), Blocks::MANGROVE_WALL_HANGING_SIGN())); diff --git a/src/lang/KnownTranslationFactory.php b/src/lang/KnownTranslationFactory.php index eadd74f32..cfd8d378e 100644 --- a/src/lang/KnownTranslationFactory.php +++ b/src/lang/KnownTranslationFactory.php @@ -2863,6 +2863,48 @@ final class KnownTranslationFactory{ return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_TICKOVERLOAD, []); } + public static function pocketmine_server_url_bugReporting(Translatable|string $bugReportingUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_BUGREPORTING, [ + "bugReportingUrl" => $bugReportingUrl, + ]); + } + + public static function pocketmine_server_url_discord(Translatable|string $discordUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_DISCORD, [ + "discordUrl" => $discordUrl, + ]); + } + + public static function pocketmine_server_url_docs(Translatable|string $docsUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_DOCS, [ + "docsUrl" => $docsUrl, + ]); + } + + public static function pocketmine_server_url_donations(Translatable|string $donationsUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_DONATIONS, [ + "donationsUrl" => $donationsUrl, + ]); + } + + public static function pocketmine_server_url_freePlugins(Translatable|string $pluginsUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_FREEPLUGINS, [ + "pluginsUrl" => $pluginsUrl, + ]); + } + + public static function pocketmine_server_url_sourceCode(Translatable|string $sourceUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_SOURCECODE, [ + "sourceUrl" => $sourceUrl, + ]); + } + + public static function pocketmine_server_url_translations(Translatable|string $translationsUrl) : Translatable{ + return new Translatable(KnownTranslationKeys::POCKETMINE_SERVER_URL_TRANSLATIONS, [ + "translationsUrl" => $translationsUrl, + ]); + } + public static function pocketmine_plugins() : Translatable{ return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGINS, []); } diff --git a/src/lang/KnownTranslationKeys.php b/src/lang/KnownTranslationKeys.php index 44a64c489..d0e2c1a68 100644 --- a/src/lang/KnownTranslationKeys.php +++ b/src/lang/KnownTranslationKeys.php @@ -616,6 +616,13 @@ final class KnownTranslationKeys{ public const POCKETMINE_SERVER_START = "pocketmine.server.start"; public const POCKETMINE_SERVER_STARTFINISHED = "pocketmine.server.startFinished"; public const POCKETMINE_SERVER_TICKOVERLOAD = "pocketmine.server.tickOverload"; + public const POCKETMINE_SERVER_URL_BUGREPORTING = "pocketmine.server.url.bugReporting"; + public const POCKETMINE_SERVER_URL_DISCORD = "pocketmine.server.url.discord"; + public const POCKETMINE_SERVER_URL_DOCS = "pocketmine.server.url.docs"; + public const POCKETMINE_SERVER_URL_DONATIONS = "pocketmine.server.url.donations"; + public const POCKETMINE_SERVER_URL_FREEPLUGINS = "pocketmine.server.url.freePlugins"; + public const POCKETMINE_SERVER_URL_SOURCECODE = "pocketmine.server.url.sourceCode"; + public const POCKETMINE_SERVER_URL_TRANSLATIONS = "pocketmine.server.url.translations"; public const POCKETMINE_PLUGINS = "pocketmine_plugins"; public const POCKETMINE_WILL_START = "pocketmine_will_start"; public const PORT_WARNING = "port_warning"; diff --git a/src/network/mcpe/cache/StaticPacketCache.php b/src/network/mcpe/cache/StaticPacketCache.php index 861881437..d7a899c64 100644 --- a/src/network/mcpe/cache/StaticPacketCache.php +++ b/src/network/mcpe/cache/StaticPacketCache.php @@ -79,10 +79,7 @@ class StaticPacketCache{ $biomeDefinition->id, $biomeDefinition->temperature, $biomeDefinition->downfall, - $biomeDefinition->redSporeDensity, - $biomeDefinition->blueSporeDensity, - $biomeDefinition->ashDensity, - $biomeDefinition->whiteAshDensity, + $biomeDefinition->foliageSnow, $biomeDefinition->depth, $biomeDefinition->scale, new Color( diff --git a/src/network/mcpe/encryption/PrepareEncryptionTask.php b/src/network/mcpe/encryption/PrepareEncryptionTask.php index 5c982bad0..5167b7aea 100644 --- a/src/network/mcpe/encryption/PrepareEncryptionTask.php +++ b/src/network/mcpe/encryption/PrepareEncryptionTask.php @@ -51,6 +51,9 @@ class PrepareEncryptionTask extends AsyncTask{ private string $clientPub, \Closure $onCompletion ){ + //make sure the key is valid before we break the stack trace + //TODO: maybe in the future we should require OpenSSLAsymmetricKey here instead of string + JwtUtils::parseDerPublicKey($this->clientPub); if(self::$SERVER_PRIVATE_KEY === null){ $serverPrivateKey = openssl_pkey_new(["ec" => ["curve_name" => "secp384r1"]]); if($serverPrivateKey === false){ diff --git a/src/network/mcpe/handler/LoginPacketHandler.php b/src/network/mcpe/handler/LoginPacketHandler.php index aa7c1da7a..0ab39420c 100644 --- a/src/network/mcpe/handler/LoginPacketHandler.php +++ b/src/network/mcpe/handler/LoginPacketHandler.php @@ -56,6 +56,7 @@ use function is_object; use function json_decode; use function md5; use function ord; +use function var_export; use const JSON_THROW_ON_ERROR; /** @@ -114,7 +115,7 @@ class LoginPacketHandler extends PacketHandler{ throw new PacketHandlingException("Unexpected type for self-signed certificate chain: " . gettype($chainData) . ", expected object"); } try{ - $chain = $this->defaultJsonMapper()->map($chainData, new LegacyAuthChain()); + $chain = $this->defaultJsonMapper("Self-signed auth chain JSON")->map($chainData, new LegacyAuthChain()); }catch(\JsonMapper_Exception $e){ throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate chain"); } @@ -132,7 +133,7 @@ class LoginPacketHandler extends PacketHandler{ } try{ - $claims = $this->defaultJsonMapper()->map($claimsArray["extraData"], new LegacyAuthIdentityData()); + $claims = $this->defaultJsonMapper("Self-signed auth JWT 'extraData'")->map($claimsArray["extraData"], new LegacyAuthIdentityData()); }catch(\JsonMapper_Exception $e){ throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate extraData"); } @@ -244,7 +245,7 @@ class LoginPacketHandler extends PacketHandler{ throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object"); } - $mapper = $this->defaultJsonMapper(); + $mapper = $this->defaultJsonMapper("Root authentication info JSON"); try{ $clientData = $mapper->map($authInfoJson, new AuthenticationInfo()); }catch(\JsonMapper_Exception $e){ @@ -258,7 +259,7 @@ class LoginPacketHandler extends PacketHandler{ * @throws PacketHandlingException */ protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{ - $mapper = $this->defaultJsonMapper(); + $mapper = $this->defaultJsonMapper("OpenID JWT header"); try{ $header = $mapper->map($headerArray, new XboxAuthJwtHeader()); }catch(\JsonMapper_Exception $e){ @@ -272,7 +273,7 @@ class LoginPacketHandler extends PacketHandler{ * @throws PacketHandlingException */ protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{ - $mapper = $this->defaultJsonMapper(); + $mapper = $this->defaultJsonMapper("OpenID JWT body"); try{ $header = $mapper->map($bodyArray, new XboxAuthJwtBody()); }catch(\JsonMapper_Exception $e){ @@ -291,7 +292,7 @@ class LoginPacketHandler extends PacketHandler{ throw PacketHandlingException::wrap($e); } - $mapper = $this->defaultJsonMapper(); + $mapper = $this->defaultJsonMapper("ClientData JWT body"); try{ $clientData = $mapper->map($clientDataClaims, new ClientData()); }catch(\JsonMapper_Exception $e){ @@ -329,12 +330,21 @@ class LoginPacketHandler extends PacketHandler{ $this->server->getAsyncPool()->submitTask(new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: null, authRequired: $authRequired, onCompletion: $this->authCallback)); } - private function defaultJsonMapper() : \JsonMapper{ + private function defaultJsonMapper(string $logContext) : \JsonMapper{ $mapper = new \JsonMapper(); $mapper->bExceptionOnMissingData = true; - $mapper->bExceptionOnUndefinedProperty = true; + $mapper->undefinedPropertyHandler = $this->warnUndefinedJsonPropertyHandler($logContext); $mapper->bStrictObjectTypeChecking = true; $mapper->bEnforceMapType = false; return $mapper; } + + /** + * @phpstan-return \Closure(object, string, mixed) : void + */ + private function warnUndefinedJsonPropertyHandler(string $context) : \Closure{ + return fn(object $object, string $name, mixed $value) => $this->session->getLogger()->warning( + "$context: Unexpected JSON property for " . (new \ReflectionClass($object))->getShortName() . ": " . $name . " = " . var_export($value, return: true) + ); + } } diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php index d98d8e9ad..c7f5f99cf 100644 --- a/src/network/mcpe/handler/ResourcePacksPacketHandler.php +++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php @@ -62,6 +62,20 @@ class ResourcePacksPacketHandler extends PacketHandler{ */ private const MAX_CONCURRENT_CHUNK_REQUESTS = 1; + /** + * All data/resource_packs/chemistry* packs need to be listed here to get chemistry blocks to render + * correctly, unfortunately there doesn't seem to be a better way to do this + */ + private const CHEMISTRY_RESOURCE_PACKS = [ + ["b41c2785-c512-4a49-af56-3a87afd47c57", "1.21.30"], + ["a4df0cb3-17be-4163-88d7-fcf7002b935d", "1.21.20"], + ["d19adffe-a2e1-4b02-8436-ca4583368c89", "1.21.10"], + ["85d5603d-2824-4b21-8044-34f441f4fce1", "1.21.0"], + ["e977cd13-0a11-4618-96fb-03dfe9c43608", "1.20.60"], + ["0674721c-a0aa-41a1-9ba8-1ed33ea3e7ed", "1.20.50"], + ["0fba4063-dba1-4281-9b89-ff9390653530", "1.0.0"], + ]; + /** * @var ResourcePack[] * @phpstan-var array @@ -200,8 +214,10 @@ class ResourcePacksPacketHandler extends PacketHandler{ return new ResourcePackStackEntry($pack->getPackId(), $pack->getPackVersion(), ""); //TODO: subpacks }, $this->resourcePackStack); - //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 support chemistry blocks by default, the client should already have these installed + foreach(self::CHEMISTRY_RESOURCE_PACKS as [$uuid, $version]){ + $stack[] = new ResourcePackStackEntry($uuid, $version, ""); + } //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 diff --git a/src/utils/Timezone.php b/src/utils/Timezone.php index 6723b12eb..849daadc5 100644 --- a/src/utils/Timezone.php +++ b/src/utils/Timezone.php @@ -26,9 +26,11 @@ namespace pocketmine\utils; use function abs; use function date_default_timezone_set; use function date_parse; +use function escapeshellarg; use function exec; use function file_get_contents; -use function implode; +use function floor; +use function hexdec; use function ini_get; use function ini_set; use function is_array; @@ -37,6 +39,7 @@ use function json_decode; use function parse_ini_file; use function preg_match; use function readlink; +use function sprintf; use function str_contains; use function str_replace; use function str_starts_with; @@ -105,40 +108,67 @@ abstract class Timezone{ public static function detectSystemTimezone() : string|false{ switch(Utils::getOS()){ case Utils::OS_WINDOWS: - $regex = '/(UTC)(\+*\-*\d*\d*\:*\d*\d*)/'; + $keyPath = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation'; /* - * wmic timezone get Caption - * Get the timezone offset + * Get the timezone offset through the registry * * Sample Output var_dump - * array(3) { - * [0] => - * string(7) "Caption" - * [1] => - * string(20) "(UTC+09:30) Adelaide" - * [2] => - * string(0) "" - * } + * array(13) { + * [0]=> + * string(0) "" + * [1]=> + * string(71) "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + * [2]=> + * string(35) " Bias REG_DWORD 0xfffffe20" + * [3]=> + * string(43) " DaylightBias REG_DWORD 0xffffffc4" + * [4]=> + * string(45) " DaylightName REG_SZ @tzres.dll,-571" + * [5]=> + * string(67) " DaylightStart REG_BINARY 00000000000000000000000000000000" + * [6]=> + * string(36) " StandardBias REG_DWORD 0x0" + * [7]=> + * string(45) " StandardName REG_SZ @tzres.dll,-572" + * [8]=> + * string(67) " StandardStart REG_BINARY 00000000000000000000000000000000" + * [9]=> + * string(52) " TimeZoneKeyName REG_SZ China Standard Time" + * [10]=> + * string(51) " DynamicDaylightTimeDisabled REG_DWORD 0x0" + * [11]=> + * string(45) " ActiveTimeBias REG_DWORD 0xfffffe20" + * [12]=> + * string(0) "" + * } */ - exec("wmic timezone get Caption", $output); + exec("reg query " . escapeshellarg($keyPath), $output); - $string = trim(implode("\n", $output)); + foreach($output as $line){ + if(preg_match('/ActiveTimeBias\s+REG_DWORD\s+0x([0-9a-fA-F]+)/', $line, $matches) > 0){ + $offsetMinutes = Binary::signInt((int) hexdec(trim($matches[1]))); - //Detect the Time Zone string - preg_match($regex, $string, $matches); + if($offsetMinutes === 0){ + return "UTC"; + } - if(!isset($matches[2])){ - return false; + $sign = $offsetMinutes <= 0 ? '+' : '-'; //windows timezone + and - are opposite + $absMinutes = abs($offsetMinutes); + $hours = floor($absMinutes / 60); + $minutes = $absMinutes % 60; + + $offset = sprintf( + "%s%02d:%02d", + $sign, + $hours, + $minutes + ); + + return self::parseOffset($offset); + } } - - $offset = $matches[2]; - - if($offset === ""){ - return "UTC"; - } - - return self::parseOffset($offset); + return false; case Utils::OS_LINUX: // Ubuntu / Debian. $data = @file_get_contents('/etc/timezone'); diff --git a/src/world/Explosion.php b/src/world/Explosion.php index 9e83d06be..88968a77e 100644 --- a/src/world/Explosion.php +++ b/src/world/Explosion.php @@ -38,6 +38,7 @@ use pocketmine\item\VanillaItems; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; +use pocketmine\math\VoxelRayTrace; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; use pocketmine\world\format\SubChunk; @@ -218,8 +219,9 @@ class Explosion{ if($distance <= 1){ $motion = $entityPos->subtractVector($this->source)->normalize(); + $exposure = $this->getExposure($this->source, $entity); - $impact = (1 - $distance) * ($exposure = 1); + $impact = (1 - $distance) * $exposure; $damage = (int) ((($impact * $impact + $impact) / 2) * 8 * $explosionSize + 1); @@ -269,6 +271,51 @@ class Explosion{ return true; } + /** + * Returns the explosion exposure of an entity, used to calculate explosion impact. + */ + private function getExposure(Vector3 $origin, Entity $entity) : float{ + $bb = $entity->getBoundingBox(); + + $diff = (new Vector3($bb->getXLength(), $bb->getYLength(), $bb->getZLength()))->multiply(2)->add(1, 1, 1); + $step = new Vector3(1.0 / $diff->x, 1.0 / $diff->y, 1.0 / $diff->z); + + $xOffset = (1.0 - (floor($diff->x) / $diff->x)) / 2.0; + $zOffset = (1.0 - (floor($diff->z) / $diff->z)) / 2.0; + + $checks = 0.0; + $hits = 0.0; + + for($x = 0.0; $x <= 1.0; $x += $step->x){ + for($y = 0.0; $y <= 1.0; $y += $step->y){ + for($z = 0.0; $z <= 1.0; $z += $step->z){ + $point = new Vector3( + self::lerp($x, $bb->minX, $bb->maxX) + $xOffset, + self::lerp($y, $bb->minY, $bb->maxY), + self::lerp($z, $bb->minZ, $bb->maxZ) + $zOffset + ); + + $intercepted = false; + + foreach(VoxelRayTrace::betweenPoints($origin, $point) as $pos){ + $block = $this->world->getBlock($pos); + if($block->calculateIntercept($origin, $point) !== null){ + $intercepted = true; + break; + } + } + + if(!$intercepted){ + $hits++; + } + $checks++; + } + } + } + + return $checks > 0.0 ? $hits / $checks : 0.0; + } + /** * Sets a chance between 0 and 1 of creating a fire. * For example, if the chance is 1/3, then that amount of affected blocks will be ignited. @@ -282,4 +329,8 @@ class Explosion{ } $this->fireChance = $fireChance; } + + private static function lerp(float $scale, float $a, float $b) : float{ + return $a + $scale * ($b - $a); + } } diff --git a/src/world/biome/model/BiomeDefinitionEntryData.php b/src/world/biome/model/BiomeDefinitionEntryData.php index bb63b36e1..2ec4d0d28 100644 --- a/src/world/biome/model/BiomeDefinitionEntryData.php +++ b/src/world/biome/model/BiomeDefinitionEntryData.php @@ -37,16 +37,7 @@ final class BiomeDefinitionEntryData{ public float $downfall; /** @required */ - public float $redSporeDensity; - - /** @required */ - public float $blueSporeDensity; - - /** @required */ - public float $ashDensity; - - /** @required */ - public float $whiteAshDensity; + public float $foliageSnow; /** @required */ public float $depth; diff --git a/tests/phpstan/stubs/JsonMapper.stub b/tests/phpstan/stubs/JsonMapper.stub index e597a35ce..f129a3104 100644 --- a/tests/phpstan/stubs/JsonMapper.stub +++ b/tests/phpstan/stubs/JsonMapper.stub @@ -5,6 +5,9 @@ class JsonMapper_Exception extends \Exception{} class JsonMapper{ + /** @var ?\Closure(object, string, mixed) : void */ + public $undefinedPropertyHandler = null; + /** * @template TModel of object * diff --git a/tools/generate-bedrock-data-from-packets.php b/tools/generate-bedrock-data-from-packets.php index 47411701a..904384618 100644 --- a/tools/generate-bedrock-data-from-packets.php +++ b/tools/generate-bedrock-data-from-packets.php @@ -589,10 +589,7 @@ class ParserPacketHandler extends PacketHandler{ $data->id = $entry->getId(); $data->temperature = round($entry->getTemperature(), 3); $data->downfall = round($entry->getDownfall(), 3); - $data->redSporeDensity = round($entry->getRedSporeDensity(), 3); - $data->blueSporeDensity = round($entry->getBlueSporeDensity(), 3); - $data->ashDensity = round($entry->getAshDensity(), 3); - $data->whiteAshDensity = round($entry->getWhiteAshDensity(), 3); + $data->foliageSnow = round($entry->getFoliageSnow(), 3); $data->depth = round($entry->getDepth(), 3); $data->scale = round($entry->getScale(), 3); $data->mapWaterColour = $mapWaterColor;