Merge branch 'minor-next' into gameplay-permissions

This commit is contained in:
Dylan K. Taylor
2025-10-07 18:41:07 +01:00
51 changed files with 1286 additions and 292 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

111
changelogs/5.34.md Normal file
View File

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

21
changelogs/5.35.md Normal file
View File

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

View File

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

62
composer.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
continue;
}
$newContents[$slot] = Item::safeNbtDeserialize($itemNBT, "$errorLogContext slot $slot");
}
$inventory->setContents($newContents);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

@@ -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']);

View File

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

View File

@@ -0,0 +1,223 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\effect;
use pocketmine\color\Color;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\Utils;
use function abs;
use function count;
use function spl_object_id;
class EffectCollection{
/** @var EffectInstance[] */
protected array $effects = [];
/**
* @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;
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;
}
}

View File

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

View File

@@ -0,0 +1,427 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\object;
use pocketmine\data\bedrock\EffectIdMap;
use pocketmine\data\bedrock\PotionTypeIds;
use pocketmine\entity\effect\EffectCollection;
use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\InstantEffect;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Living;
use pocketmine\entity\Location;
use pocketmine\event\entity\AreaEffectCloudApplyEvent;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\utils\Binary;
use pocketmine\world\particle\PotionSplashParticle;
use function count;
use function max;
use function round;
class AreaEffectCloud extends Entity{
public const DEFAULT_DURATION = 600; // in ticks
public const DEFAULT_DURATION_CHANGE_ON_USE = 0; // in ticks
public const UPDATE_DELAY = 10; // in ticks
public const REAPPLICATION_DELAY = 40; // in ticks
public const DEFAULT_RADIUS = 3.0; // in blocks
public const DEFAULT_RADIUS_CHANGE_ON_PICKUP = -0.5; // in blocks
public const DEFAULT_RADIUS_CHANGE_ON_USE = -0.5; // in blocks
public const DEFAULT_RADIUS_CHANGE_PER_TICK = -(self::DEFAULT_RADIUS / self::DEFAULT_DURATION); // in blocks
protected const TAG_POTION_ID = "PotionId"; //TAG_Short
protected const TAG_SPAWN_TICK = "SpawnTick"; //TAG_Long
protected const TAG_DURATION = "Duration"; //TAG_Int
protected const TAG_PICKUP_COUNT = "PickupCount"; //TAG_Int
protected const TAG_DURATION_ON_USE = "DurationOnUse"; //TAG_Int
protected const TAG_REAPPLICATION_DELAY = "ReapplicationDelay"; //TAG_Int
protected const TAG_INITIAL_RADIUS = "InitialRadius"; //TAG_Float
protected const TAG_RADIUS = "Radius"; //TAG_Float
protected const TAG_RADIUS_CHANGE_ON_PICKUP = "RadiusChangeOnPickup"; //TAG_Float
protected const TAG_RADIUS_ON_USE = "RadiusOnUse"; //TAG_Float
protected const TAG_RADIUS_PER_TICK = "RadiusPerTick"; //TAG_Float
protected const TAG_EFFECTS = "mobEffects"; //TAG_List
public static function getNetworkTypeId() : string{ return EntityIds::AREA_EFFECT_CLOUD; }
protected int $age = 0;
protected EffectCollection $effectCollection;
/** @var array<int, int> 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();
}
}

View File

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

View File

@@ -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($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)){

View File

@@ -0,0 +1,64 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\event\entity;
use pocketmine\entity\Living;
use pocketmine\entity\object\AreaEffectCloud;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
/**
* Called when an area effect cloud applies effects to entities.
*
* @phpstan-extends EntityEvent<AreaEffectCloud>
*/
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, []);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, ResourcePack>
@@ -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

View File

@@ -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) {
* array(13) {
* [0]=>
* string(7) "Caption"
* string(0) ""
* [1]=>
* string(20) "(UTC+09:30) Adelaide"
* 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(!isset($matches[2])){
return false;
}
$offset = $matches[2];
if($offset === ""){
if($offsetMinutes === 0){
return "UTC";
}
$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);
}
}
return false;
case Utils::OS_LINUX:
// Ubuntu / Debian.
$data = @file_get_contents('/etc/timezone');

View File

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

View File

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

View File

@@ -5,6 +5,9 @@ class JsonMapper_Exception extends \Exception{}
class JsonMapper{
/** @var ?\Closure(object, string, mixed) : void */
public $undefinedPropertyHandler = null;
/**
* @template TModel of object
*

View File

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