Compare commits

..

5 Commits

Author SHA1 Message Date
1e797b9897 Prepare 5.33.2 release (#6804) 2025-09-15 23:53:05 +01:00
a056af1617 Update composer dev dependencies 2025-09-15 22:41:02 +01:00
b237cacfc9 InGamePacketHandler: don't resync blocks if the client predicted an interact fail
if the prediction was a fail, we can assume that the client didn't do anything visual on its end,
which avoids the need to resend blocks.
This fixes block lag when towering as discussed in #6803.

The more general issue in #6803 remains unresolved (that the server's block resyncing may overwrite
additional client side predictions if it places a block before the server's resync for the initial
placement arrives), but that's more complicated to fix and I'm not convinced on the correct method
to resolve it yet.

In any case, this change nets a decent improvement for everyone, regardless.
2025-09-15 19:36:29 +01:00
e7ad3c25fa InGamePacketHandler: ignore CREATIVE_PLAYER_DESTROY_BLOCK
fixes #6757

we get PREDICT_DESTROY_BLOCK anyway, so it's not needed
2025-09-15 18:02:10 +01:00
c6a28d8df0 InventoryManager: fixed window sending getting stuck on client rejecting window opening
closes #6778

honestly, we could just stop checking the window ID entirely, considering the need for delaying and waiting
for window close acks, it seems useless at this point...
2025-09-15 17:34:35 +01:00
30 changed files with 225 additions and 382 deletions

View File

@ -29,6 +29,9 @@ use pocketmine\data\bedrock\block\BlockStateStringValues;
use pocketmine\data\bedrock\block\BlockTypeNames; use pocketmine\data\bedrock\block\BlockTypeNames;
use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\nbt\NbtException; use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\BlockStateDictionary; use pocketmine\network\mcpe\convert\BlockStateDictionary;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
@ -78,6 +81,9 @@ function generateBlockPaletteReport(array $states) : BlockPaletteReport{
$name = $stateData->getName(); $name = $stateData->getName();
$result->seenTypes[$name] = $name; $result->seenTypes[$name] = $name;
foreach(Utils::stringifyKeys($stateData->getStates()) as $k => $v){ foreach(Utils::stringifyKeys($stateData->getStates()) as $k => $v){
if(!$v instanceof ByteTag && !$v instanceof IntTag && !$v instanceof StringTag){
throw new AssumptionFailedError("Assumed all state tags should be TAG_Byte, TAG_Int or TAG_String, but found $k ($v) on block $name");
}
$result->seenStateValues[$k][$v->getValue()] = $v->getValue(); $result->seenStateValues[$k][$v->getValue()] = $v->getValue();
asort($result->seenStateValues[$k]); asort($result->seenStateValues[$k]);
} }

View File

@ -133,3 +133,15 @@ Released 31st August 2025.
## Fixes ## Fixes
- Fixed banners placed in prior versions getting their tiles deleted (due to missing `Type` tags). - Fixed banners placed in prior versions getting their tiles deleted (due to missing `Type` tags).
# 5.33.2
Released 16th September 2025.
## General
- PHP 8.4 has now been added to the test matrix.
## Fixes
- Fixed PHP 8.4 deprecation notice in `AsyncGeneratorExecutor`.
- Fixed inventory windows breaking if a window is sent while the player has the chat window open (e.g. quickly pressing E followed by / could trigger this).
- Fixed `BlockBreakEvent` being called twice in creative if cancelled.
- Reduced block lag when towering and other situations where the placed block might intersect with the player's AABB. Block lag may still appear on higher latency clients - this is still being worked on.

View File

@ -36,7 +36,7 @@
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100", "pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100",
"pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100", "pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~41.0.0+bedrock-1.21.100", "pocketmine/bedrock-protocol": "~40.0.0+bedrock-1.21.100",
"pocketmine/binaryutils": "^0.2.1", "pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2", "pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0", "pocketmine/color": "^0.3.0",
@ -52,7 +52,7 @@
"symfony/filesystem": "~6.4.0" "symfony/filesystem": "~6.4.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "2.1.17", "phpstan/phpstan": "2.1.25",
"phpstan/phpstan-phpunit": "^2.0.0", "phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0", "phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.24" "phpunit/phpunit": "^10.5.24"

52
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "7bf7cd54642c2d65ecdfdcb28f3a64a8", "content-hash": "008c888b5812dda09a0ec6e425453153",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -256,16 +256,16 @@
}, },
{ {
"name": "pocketmine/bedrock-protocol", "name": "pocketmine/bedrock-protocol",
"version": "41.0.0+bedrock-1.21.100", "version": "40.0.0+bedrock-1.21.100",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git", "url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf" "reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/920ac291fe1b0143b2ebc90b3374ddab0b8531bf", "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf", "reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -296,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": { "support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues", "issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/41.0.0+bedrock-1.21.100" "source": "https://github.com/pmmp/BedrockProtocol/tree/40.0.0+bedrock-1.21.100"
}, },
"time": "2025-09-09T20:52:18+00:00" "time": "2025-08-06T15:13:45+00:00"
}, },
{ {
"name": "pocketmine/binaryutils", "name": "pocketmine/binaryutils",
@ -1204,16 +1204,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "2.1.17", "version": "2.1.25",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" "reference": "4087d28bd252895874e174d65e26b2c202ed893a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4087d28bd252895874e174d65e26b2c202ed893a",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "reference": "4087d28bd252895874e174d65e26b2c202ed893a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1258,25 +1258,25 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-05-21T20:55:28+00:00" "time": "2025-09-12T14:26:42+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",
"version": "2.0.6", "version": "2.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git", "url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260" "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6b92469f8a7995e626da3aa487099617b8dfa260", "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc",
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260", "reference": "9a9b161baee88a5f5c58d816943cff354ff233dc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.0.4" "phpstan/phpstan": "^2.1.18"
}, },
"conflict": { "conflict": {
"phpunit/phpunit": "<7.0" "phpunit/phpunit": "<7.0"
@ -1309,9 +1309,9 @@
"description": "PHPUnit extensions and rules for PHPStan", "description": "PHPUnit extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues", "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.6" "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7"
}, },
"time": "2025-03-26T12:47:06+00:00" "time": "2025-07-13T11:31:46+00:00"
}, },
{ {
"name": "phpstan/phpstan-strict-rules", "name": "phpstan/phpstan-strict-rules",
@ -1684,16 +1684,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "10.5.54", "version": "10.5.55",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "b1dbbaaf96106b76d500b9d3db51f9b01f6a3589" "reference": "4b2d546b336876bd9562f24641b08a25335b06b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b1dbbaaf96106b76d500b9d3db51f9b01f6a3589", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b2d546b336876bd9562f24641b08a25335b06b6",
"reference": "b1dbbaaf96106b76d500b9d3db51f9b01f6a3589", "reference": "4b2d546b336876bd9562f24641b08a25335b06b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1714,7 +1714,7 @@
"phpunit/php-timer": "^6.0.0", "phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1", "sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0", "sebastian/code-unit": "^2.0.0",
"sebastian/comparator": "^5.0.3", "sebastian/comparator": "^5.0.4",
"sebastian/diff": "^5.1.1", "sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0", "sebastian/environment": "^6.1.0",
"sebastian/exporter": "^5.1.2", "sebastian/exporter": "^5.1.2",
@ -1765,7 +1765,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.54" "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.55"
}, },
"funding": [ "funding": [
{ {
@ -1789,7 +1789,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-11T06:19:38+00:00" "time": "2025-09-14T06:19:20+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View File

@ -32,7 +32,7 @@ use function str_repeat;
final class VersionInfo{ final class VersionInfo{
public const NAME = "PocketMine-MP"; public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.33.2"; public const BASE_VERSION = "5.33.2";
public const IS_DEVELOPMENT_BUILD = true; public const IS_DEVELOPMENT_BUILD = false;
public const BUILD_CHANNEL = "stable"; public const BUILD_CHANNEL = "stable";
/** /**

View File

@ -41,19 +41,14 @@ use pocketmine\utils\TextFormat;
use pocketmine\world\BlockTransaction; use pocketmine\world\BlockTransaction;
use pocketmine\world\sound\DyeUseSound; use pocketmine\world\sound\DyeUseSound;
use pocketmine\world\sound\InkSacUseSound; use pocketmine\world\sound\InkSacUseSound;
use function abs;
use function array_map; use function array_map;
use function assert; use function assert;
use function atan2;
use function fmod;
use function rad2deg;
use function strlen; use function strlen;
abstract class BaseSign extends Transparent implements WoodMaterial{ abstract class BaseSign extends Transparent implements WoodMaterial{
use WoodTypeTrait; use WoodTypeTrait;
protected SignText $text; //TODO: rename this (BC break) protected SignText $text;
protected SignText $backText;
private bool $waxed = false; private bool $waxed = false;
protected ?int $editorEntityRuntimeId = null; protected ?int $editorEntityRuntimeId = null;
@ -68,7 +63,6 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$this->woodType = $woodType; $this->woodType = $woodType;
parent::__construct($idInfo, $name, $typeInfo); parent::__construct($idInfo, $name, $typeInfo);
$this->text = new SignText(); $this->text = new SignText();
$this->backText = new SignText();
$this->asItemCallback = $asItemCallback; $this->asItemCallback = $asItemCallback;
} }
@ -77,7 +71,6 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$tile = $this->position->getWorld()->getTile($this->position); $tile = $this->position->getWorld()->getTile($this->position);
if($tile instanceof TileSign){ if($tile instanceof TileSign){
$this->text = $tile->getText(); $this->text = $tile->getText();
$this->backText = $tile->getBackText();
$this->waxed = $tile->isWaxed(); $this->waxed = $tile->isWaxed();
$this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId(); $this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId();
} }
@ -90,7 +83,6 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$tile = $this->position->getWorld()->getTile($this->position); $tile = $this->position->getWorld()->getTile($this->position);
assert($tile instanceof TileSign); assert($tile instanceof TileSign);
$tile->setText($this->text); $tile->setText($this->text);
$tile->setBackText($this->backText);
$tile->setWaxed($this->waxed); $tile->setWaxed($this->waxed);
$tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId); $tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId);
} }
@ -135,11 +127,11 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
} }
} }
private function doSignChange(SignText $newText, Player $player, Item $item, bool $frontFace) : bool{ private function doSignChange(SignText $newText, Player $player, Item $item) : bool{
$ev = new SignChangeEvent($this, $player, $newText, $frontFace); $ev = new SignChangeEvent($this, $player, $newText);
$ev->call(); $ev->call();
if(!$ev->isCancelled()){ if(!$ev->isCancelled()){
$this->setFaceText($frontFace, $ev->getNewText()); $this->text = $ev->getNewText();
$this->position->getWorld()->setBlock($this->position, $this); $this->position->getWorld()->setBlock($this->position, $this);
$item->pop(); $item->pop();
return true; return true;
@ -148,9 +140,8 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
return false; return false;
} }
private function changeSignGlowingState(bool $glowing, Player $player, Item $item, bool $frontFace) : bool{ private function changeSignGlowingState(bool $glowing, Player $player, Item $item) : bool{
$text = $this->getFaceText($frontFace); if($this->text->isGlowing() !== $glowing && $this->doSignChange(new SignText($this->text->getLines(), $this->text->getBaseColor(), $glowing), $player, $item)){
if($text->isGlowing() !== $glowing && $this->doSignChange(new SignText($text->getLines(), $text->getBaseColor(), $glowing), $player, $item, $frontFace)){
$this->position->getWorld()->addSound($this->position, new InkSacUseSound()); $this->position->getWorld()->addSound($this->position, new InkSacUseSound());
return true; return true;
} }
@ -177,8 +168,6 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
return true; return true;
} }
$frontFace = $this->interactsFront($this->getHitboxCenter(), $player->getPosition(), $this->getFacingDegrees());
$dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){ $dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){
ItemTypeIds::BONE_MEAL => DyeColor::WHITE, ItemTypeIds::BONE_MEAL => DyeColor::WHITE,
ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE, ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE,
@ -187,82 +176,40 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
}; };
if($dyeColor !== null){ if($dyeColor !== null){
$color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue(); $color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue();
$text = $this->getFaceText($frontFace);
if( if(
$color->toARGB() !== $text->getBaseColor()->toARGB() && $color->toARGB() !== $this->text->getBaseColor()->toARGB() &&
$this->doSignChange(new SignText($text->getLines(), $color, $text->isGlowing()), $player, $item, $frontFace) $this->doSignChange(new SignText($this->text->getLines(), $color, $this->text->isGlowing()), $player, $item)
){ ){
$this->position->getWorld()->addSound($this->position, new DyeUseSound()); $this->position->getWorld()->addSound($this->position, new DyeUseSound());
return true; return true;
} }
}elseif(match($item->getTypeId()){ }elseif(match($item->getTypeId()){
ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item, $frontFace), ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item),
ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item, $frontFace), ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item),
ItemTypeIds::HONEYCOMB => $this->wax($player, $item), ItemTypeIds::HONEYCOMB => $this->wax($player, $item),
default => false default => false
}){ }){
return true; return true;
} }
$player->openSignEditor($this->position, $frontFace); $player->openSignEditor($this->position);
return true; return true;
} }
private function interactsFront(Vector3 $hitboxCenter, Vector3 $playerPosition, float $signFacingDegrees) : bool{
$playerCenterDiffX = $playerPosition->x - $hitboxCenter->x;
$playerCenterDiffZ = $playerPosition->z - $hitboxCenter->z;
$f1 = rad2deg(atan2($playerCenterDiffZ, $playerCenterDiffX)) - 90.0;
$rotationDiff = $signFacingDegrees - $f1;
$rotation = fmod($rotationDiff + 180.0, 360.0) - 180.0; // Normalize to [-180, 180]
return abs($rotation) <= 90.0;
}
/**
* Returns the center of the sign's hitbox. Used to decide which face of the sign to open when a player interacts.
*/
protected function getHitboxCenter() : Vector3{
return $this->position->add(0.5, 0.5, 0.5);
}
/**
* TODO: make this abstract (BC break)
*/
protected function getFacingDegrees() : float{
return 0;
}
/** /**
* Returns an object containing information about the sign text. * Returns an object containing information about the sign text.
* @deprecated
* @see self::getFaceText()
*/ */
public function getText() : SignText{ public function getText() : SignText{
return $this->text; return $this->text;
} }
/** /** @return $this */
* @deprecated
* @see self::setFaceText()
* @return $this
*/
public function setText(SignText $text) : self{ public function setText(SignText $text) : self{
$this->text = $text; $this->text = $text;
return $this; return $this;
} }
public function getFaceText(bool $frontFace) : SignText{
return $frontFace ? $this->text : $this->backText;
}
/** @return $this */
public function setFaceText(bool $frontFace, SignText $text) : self{
$frontFace ? $this->text = $text : $this->backText = $text;
return $this;
}
/** /**
* Returns whether the sign has been waxed using a honeycomb. If true, the sign cannot be edited by a player. * Returns whether the sign has been waxed using a honeycomb. If true, the sign cannot be edited by a player.
*/ */
@ -287,21 +234,13 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
return $this; return $this;
} }
/**
* @deprecated
* @see self::updateFaceText()
*/
public function updateText(Player $author, SignText $text) : bool{
return $this->updateFaceText($author, true, $text);
}
/** /**
* Called by the player controller (network session) to update the sign text, firing events as appropriate. * Called by the player controller (network session) to update the sign text, firing events as appropriate.
* *
* @return bool if the sign update was successful. * @return bool if the sign update was successful.
* @throws \UnexpectedValueException if the text payload is too large * @throws \UnexpectedValueException if the text payload is too large
*/ */
public function updateFaceText(Player $author, bool $frontFace, SignText $text) : bool{ public function updateText(Player $author, SignText $text) : bool{
$size = 0; $size = 0;
foreach($text->getLines() as $line){ foreach($text->getLines() as $line){
$size += strlen($line); $size += strlen($line);
@ -309,16 +248,15 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
if($size > 1000){ if($size > 1000){
throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)"); throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)");
} }
$oldText = $this->getFaceText($frontFace);
$ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{ $ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{
return TextFormat::clean($line, false); return TextFormat::clean($line, false);
}, $text->getLines()), $oldText->getBaseColor(), $oldText->isGlowing()), $frontFace); }, $text->getLines()), $this->text->getBaseColor(), $this->text->isGlowing()));
if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){ if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){
$ev->cancel(); $ev->cancel();
} }
$ev->call(); $ev->call();
if(!$ev->isCancelled()){ if(!$ev->isCancelled()){
$this->setFaceText($frontFace, $ev->getNewText()); $this->setText($ev->getNewText());
$this->setEditorEntityRuntimeId(null); $this->setEditorEntityRuntimeId(null);
$this->position->getWorld()->setBlock($this->position, $this); $this->position->getWorld()->setBlock($this->position, $this);
return true; return true;

View File

@ -58,8 +58,4 @@ final class CeilingCenterHangingSign extends BaseSign implements SignLikeRotatio
$supportBlock->getSupportType(Facing::DOWN)->hasCenterSupport() || $supportBlock->getSupportType(Facing::DOWN)->hasCenterSupport() ||
$supportBlock->hasTypeTag(BlockTypeTags::HANGING_SIGN); $supportBlock->hasTypeTag(BlockTypeTags::HANGING_SIGN);
} }
protected function getFacingDegrees() : float{
return $this->rotation * 22.5;
}
} }

View File

@ -30,7 +30,6 @@ use pocketmine\item\Item;
use pocketmine\math\Facing; use pocketmine\math\Facing;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction; use pocketmine\world\BlockTransaction;
final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing{ final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing{
@ -66,14 +65,4 @@ final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing
$supportBlock->getSupportType(Facing::DOWN) === SupportType::FULL || $supportBlock->getSupportType(Facing::DOWN) === SupportType::FULL ||
(($supportBlock instanceof WallHangingSign || $supportBlock instanceof CeilingEdgesHangingSign) && Facing::axis($supportBlock->getFacing()) === Facing::axis($this->facing)); (($supportBlock instanceof WallHangingSign || $supportBlock instanceof CeilingEdgesHangingSign) && Facing::axis($supportBlock->getFacing()) === Facing::axis($this->facing));
} }
protected function getFacingDegrees() : float{
return match($this->facing){
Facing::SOUTH => 0,
Facing::WEST => 90,
Facing::NORTH => 180,
Facing::EAST => 270,
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
};
}
} }

View File

@ -48,8 +48,4 @@ final class FloorSign extends BaseSign implements SignLikeRotation{
} }
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
} }
protected function getFacingDegrees() : float{
return $this->rotation * 22.5;
}
} }

View File

@ -32,7 +32,6 @@ use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing; use pocketmine\math\Facing;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction; use pocketmine\world\BlockTransaction;
final class WallHangingSign extends BaseSign implements HorizontalFacing{ final class WallHangingSign extends BaseSign implements HorizontalFacing{
@ -79,14 +78,4 @@ final class WallHangingSign extends BaseSign implements HorizontalFacing{
($block instanceof WallHangingSign && Facing::axis(Facing::rotateY($block->getFacing(), clockwise: true)) === Facing::axis($face)) || ($block instanceof WallHangingSign && Facing::axis(Facing::rotateY($block->getFacing(), clockwise: true)) === Facing::axis($face)) ||
$block->getSupportType(Facing::opposite($face)) === SupportType::FULL; $block->getSupportType(Facing::opposite($face)) === SupportType::FULL;
} }
protected function getFacingDegrees() : float{
return match($this->facing){
Facing::SOUTH => 0,
Facing::WEST => 90,
Facing::NORTH => 180,
Facing::EAST => 270,
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
};
}
} }

View File

@ -30,7 +30,6 @@ use pocketmine\math\Axis;
use pocketmine\math\Facing; use pocketmine\math\Facing;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction; use pocketmine\world\BlockTransaction;
final class WallSign extends BaseSign implements HorizontalFacing{ final class WallSign extends BaseSign implements HorizontalFacing{
@ -47,25 +46,4 @@ final class WallSign extends BaseSign implements HorizontalFacing{
$this->facing = $face; $this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
} }
protected function getHitboxCenter() : Vector3{
[$xOffset, $zOffset] = match($this->facing){
Facing::NORTH => [0, 15 / 16],
Facing::SOUTH => [0, 1 / 16],
Facing::WEST => [15 / 16, 0],
Facing::EAST => [1 / 16, 0],
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
};
return $this->position->add($xOffset, 0.5, $zOffset);
}
protected function getFacingDegrees() : float{
return match($this->facing){
Facing::SOUTH => 0,
Facing::WEST => 90,
Facing::NORTH => 180,
Facing::EAST => 270,
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
};
}
} }

View File

@ -70,18 +70,16 @@ class Sign extends Spawnable{
} }
protected SignText $text; protected SignText $text;
protected SignText $backText;
private bool $waxed = false; private bool $waxed = false;
protected ?int $editorEntityRuntimeId = null; protected ?int $editorEntityRuntimeId = null;
public function __construct(World $world, Vector3 $pos){ public function __construct(World $world, Vector3 $pos){
$this->text = new SignText(); $this->text = new SignText();
$this->backText = new SignText();
parent::__construct($world, $pos); parent::__construct($world, $pos);
} }
private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : SignText{ private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : void{
$baseColor = new Color(0, 0, 0); $baseColor = new Color(0, 0, 0);
$glowingText = false; $glowingText = false;
if(($baseColorTag = $nbt->getTag(self::TAG_TEXT_COLOR)) instanceof IntTag){ if(($baseColorTag = $nbt->getTag(self::TAG_TEXT_COLOR)) instanceof IntTag){
@ -92,27 +90,19 @@ class Sign extends Spawnable{
//see https://bugs.mojang.com/browse/MCPE-117835 //see https://bugs.mojang.com/browse/MCPE-117835
$glowingText = $glowingTextTag->getValue() !== 0; $glowingText = $glowingTextTag->getValue() !== 0;
} }
return SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText); $this->text = SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText);
}
private function writeTextTag(SignText $text) : CompoundTag{
return CompoundTag::create()
->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $text->getLines()), "\n"))
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($text->getBaseColor()->toARGB()))
->setByte(self::TAG_GLOWING_TEXT, $text->isGlowing() ? 1 : 0)
->setByte(self::TAG_PERSIST_FORMATTING, 1);
} }
public function readSaveData(CompoundTag $nbt) : void{ public function readSaveData(CompoundTag $nbt) : void{
$frontTextTag = $nbt->getTag(self::TAG_FRONT_TEXT); $frontTextTag = $nbt->getTag(self::TAG_FRONT_TEXT);
if($frontTextTag instanceof CompoundTag){ if($frontTextTag instanceof CompoundTag){
$this->text = $this->readTextTag($frontTextTag, true); $this->readTextTag($frontTextTag, true);
}elseif($nbt->getTag(self::TAG_TEXT_BLOB) instanceof StringTag){ //MCPE 1.2 save format }elseif($nbt->getTag(self::TAG_TEXT_BLOB) instanceof StringTag){ //MCPE 1.2 save format
$lightingBugResolved = false; $lightingBugResolved = false;
if(($lightingBugResolvedTag = $nbt->getTag(self::TAG_LEGACY_BUG_RESOLVE)) instanceof ByteTag){ if(($lightingBugResolvedTag = $nbt->getTag(self::TAG_LEGACY_BUG_RESOLVE)) instanceof ByteTag){
$lightingBugResolved = $lightingBugResolvedTag->getValue() !== 0; $lightingBugResolved = $lightingBugResolvedTag->getValue() !== 0;
} }
$this->text = $this->readTextTag($nbt, $lightingBugResolved); $this->readTextTag($nbt, $lightingBugResolved);
}else{ }else{
$text = []; $text = [];
for($i = 0; $i < SignText::LINE_COUNT; ++$i){ for($i = 0; $i < SignText::LINE_COUNT; ++$i){
@ -123,14 +113,22 @@ class Sign extends Spawnable{
} }
$this->text = new SignText($text); $this->text = new SignText($text);
} }
$backTextTag = $nbt->getTag(self::TAG_BACK_TEXT);
$this->backText = $backTextTag instanceof CompoundTag ? $this->readTextTag($backTextTag, true) : new SignText();
$this->waxed = $nbt->getByte(self::TAG_WAXED, 0) !== 0; $this->waxed = $nbt->getByte(self::TAG_WAXED, 0) !== 0;
} }
protected function writeSaveData(CompoundTag $nbt) : void{ protected function writeSaveData(CompoundTag $nbt) : void{
$nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text)); $nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText)); ->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $this->text->getLines()), "\n"))
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB()))
->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0)
->setByte(self::TAG_PERSIST_FORMATTING, 1)
);
$nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create()
->setString(self::TAG_TEXT_BLOB, "")
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
->setByte(self::TAG_GLOWING_TEXT, 0)
->setByte(self::TAG_PERSIST_FORMATTING, 1)
);
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0); $nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
} }
@ -143,10 +141,6 @@ class Sign extends Spawnable{
$this->text = $text; $this->text = $text;
} }
public function getBackText() : SignText{ return $this->backText; }
public function setBackText(SignText $backText) : void{ $this->backText = $backText; }
public function isWaxed() : bool{ return $this->waxed; } public function isWaxed() : bool{ return $this->waxed; }
public function setWaxed(bool $waxed) : void{ $this->waxed = $waxed; } public function setWaxed(bool $waxed) : void{ $this->waxed = $waxed; }
@ -168,8 +162,19 @@ class Sign extends Spawnable{
} }
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
$nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text)); $nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText)); ->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $this->text->getLines()), "\n"))
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB()))
->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0)
->setByte(self::TAG_PERSIST_FORMATTING, 1) //TODO: not sure what this is used for
);
//TODO: this is not yet used by the server, but needed to rollback any client-side changes to the back text
$nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create()
->setString(self::TAG_TEXT_BLOB, "")
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
->setByte(self::TAG_GLOWING_TEXT, 0)
->setByte(self::TAG_PERSIST_FORMATTING, 1)
);
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0); $nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
$nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1); $nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1);
} }

View File

@ -148,8 +148,7 @@ class TimingsCommand extends VanillaCommand{
private function uploadReport(array $lines, CommandSender $sender) : void{ private function uploadReport(array $lines, CommandSender $sender) : void{
$data = [ $data = [
"browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(), "browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(),
"data" => implode("\n", $lines), "data" => implode("\n", $lines)
"private" => "true"
]; ];
$host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io"); $host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io");
@ -182,13 +181,8 @@ class TimingsCommand extends VanillaCommand{
} }
$response = json_decode($result->getBody(), true); $response = json_decode($result->getBody(), true);
if(is_array($response) && isset($response["id"]) && (is_int($response["id"]) || is_string($response["id"]))){ if(is_array($response) && isset($response["id"]) && (is_int($response["id"]) || is_string($response["id"]))){
$url = "https://" . $host . "/?id=" . $response["id"]; Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
if(isset($response["access_token"]) && is_string($response["access_token"])){ "https://" . $host . "/?id=" . $response["id"]));
$url .= "&access_token=" . $response["access_token"];
}else{
$sender->getServer()->getLogger()->warning("Your chosen timings host does not support private reports. Anyone will be able to see your report if they guess the ID.");
}
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead($url));
}else{ }else{
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody()); $sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError()); Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());

View File

@ -278,6 +278,10 @@ final class BlockStateUpgrader{
private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{ private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{
$flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null; $flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null;
$expectedType = $flattenInfo->flattenedPropertyType; $expectedType = $flattenInfo->flattenedPropertyType;
if($expectedType === null){
//TODO: we can't make this non-nullable in a patch release
throw new AssumptionFailedError("We never give this null");
}
if(!$flattenedValue instanceof $expectedType){ if(!$flattenedValue instanceof $expectedType){
//flattened property is not of the expected type, so this transformation is not applicable //flattened property is not of the expected type, so this transformation is not applicable
return [$oldName, $states]; return [$oldName, $states];

View File

@ -117,8 +117,8 @@ final class ItemSerializer{
$data = $serializer($item); $data = $serializer($item);
} }
$resultTag = $item->getNamedTag(); if($item->hasNamedTag()){
if($resultTag->count() > 0){ $resultTag = $item->getNamedTag();
$extraTag = $data->getTag(); $extraTag = $data->getTag();
if($extraTag !== null){ if($extraTag !== null){
$resultTag = $resultTag->merge($extraTag); $resultTag = $resultTag->merge($extraTag);

View File

@ -247,10 +247,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
return $this->enderInventory; return $this->enderInventory;
} }
public function getSneakOffset() : float{
return 0.31;
}
/** /**
* For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT.
*/ */

View File

@ -242,10 +242,6 @@ abstract class Living extends Entity{
$this->absorptionAttr->setValue($absorption); $this->absorptionAttr->setValue($absorption);
} }
public function getSneakOffset() : float{
return 0.0;
}
public function isSneaking() : bool{ public function isSneaking() : bool{
return $this->sneaking; return $this->sneaking;
} }
@ -296,7 +292,7 @@ abstract class Living extends Entity{
$width = $size->getWidth(); $width = $size->getWidth();
$this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale())); $this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale()));
}elseif($this->isSneaking()){ }elseif($this->isSneaking()){
$this->setSize((new EntitySizeInfo($size->getHeight() - $this->getSneakOffset(), $size->getWidth(), $size->getEyeHeight() - $this->getSneakOffset()))->scale($this->getScale())); $this->setSize((new EntitySizeInfo(3 / 4 * $size->getHeight(), $size->getWidth(), 3 / 4 * $size->getEyeHeight()))->scale($this->getScale()));
}else{ }else{
$this->setSize($size->scale($this->getScale())); $this->setSize($size->scale($this->getScale()));
} }

View File

@ -35,15 +35,11 @@ use pocketmine\player\Player;
class SignChangeEvent extends BlockEvent implements Cancellable{ class SignChangeEvent extends BlockEvent implements Cancellable{
use CancellableTrait; use CancellableTrait;
private SignText $oldText;
public function __construct( public function __construct(
private BaseSign $sign, private BaseSign $sign,
private Player $player, private Player $player,
private SignText $text, private SignText $text
private bool $frontFace = true
){ ){
$this->oldText = $this->sign->getFaceText($this->frontFace);
parent::__construct($sign); parent::__construct($sign);
} }
@ -59,7 +55,7 @@ class SignChangeEvent extends BlockEvent implements Cancellable{
* Returns the text currently on the sign. * Returns the text currently on the sign.
*/ */
public function getOldText() : SignText{ public function getOldText() : SignText{
return $this->oldText; return $this->sign->getText();
} }
/** /**
@ -75,6 +71,4 @@ class SignChangeEvent extends BlockEvent implements Cancellable{
public function setNewText(SignText $text) : void{ public function setNewText(SignText $text) : void{
$this->text = $text; $this->text = $text;
} }
public function isFrontFace() : bool{ return $this->frontFace; }
} }

View File

@ -30,11 +30,13 @@ use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function array_keys;
use function array_values; use function array_values;
use function assert; use function assert;
use function count; use function count;
use function get_class; use function get_class;
use function min; use function min;
use function shuffle;
use function spl_object_hash; use function spl_object_hash;
use function spl_object_id; use function spl_object_id;
@ -93,13 +95,10 @@ class InventoryTransaction{
} }
/** /**
* Returns a set of actions involved in this transaction. * Returns an **unordered** set of actions involved in this transaction.
* *
* Note: This system is designed to care only about item balances. While you can usually assume that the actions * WARNING: This system is **explicitly designed NOT to care about ordering**. Any order seen in this set has NO
* are provided in the correct order, it will still successfully complete transactions whose actions are provided in * significance and should not be relied on.
* the "wrong" order, as long as the transaction balances.
* For example, you may see that an action setting a slot to a particular item may appear before the action that
* removes that item from its original slot. While unintuitive, this is still valid.
* *
* @return InventoryAction[] * @return InventoryAction[]
* @phpstan-return array<int, InventoryAction> * @phpstan-return array<int, InventoryAction>
@ -120,6 +119,19 @@ class InventoryTransaction{
} }
} }
/**
* Shuffles actions in the transaction to prevent external things relying on any implicit ordering.
*/
private function shuffleActions() : void{
$keys = array_keys($this->actions);
shuffle($keys);
$actions = [];
foreach($keys as $key){
$actions[$key] = $this->actions[$key];
}
$this->actions = $actions;
}
/** /**
* @param Item[] $needItems * @param Item[] $needItems
* @param Item[] $haveItems * @param Item[] $haveItems
@ -296,6 +308,8 @@ class InventoryTransaction{
throw new TransactionValidationException("Transaction has already been executed"); throw new TransactionValidationException("Transaction has already been executed");
} }
$this->shuffleActions();
$this->validate(); $this->validate();
if(!$this->callExecuteEvent()){ if(!$this->callExecuteEvent()){

View File

@ -64,6 +64,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\network\PacketHandlingException; use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\ObjectSet; use pocketmine\utils\ObjectSet;
use function array_fill_keys; use function array_fill_keys;
use function array_keys; use function array_keys;
@ -419,6 +420,15 @@ class InventoryManager{
} }
public function onClientRemoveWindow(int $id) : void{ public function onClientRemoveWindow(int $id) : void{
if(Binary::signByte($id) === ContainerIds::NONE){ //TODO: REMOVE signByte() once BedrockProtocol + ext-encoding are implemented
//TODO: HACK! Since 1.21.100 (and probably earlier), the client will send -1 to close windows that it can't
//view for some reason, e.g. if the chat window was already open. This is pretty awkward, since it means
//that we can only assume it refers to the most recently sent window, and if we don't handle it,
//InventoryManager will never get the green light to send subsequent windows, which breaks inventory UIs.
//Fortunately, we already wait for close acks anyway, so the window ID is technically useless...?
$this->session->getLogger()->debug("Client rejected opening of a window, assuming it was $this->lastInventoryNetworkId");
$id = $this->lastInventoryNetworkId;
}
if($id === $this->lastInventoryNetworkId){ if($id === $this->lastInventoryNetworkId){
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){ if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
$this->remove($id); $this->remove($id);

View File

@ -27,8 +27,8 @@ use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable; use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\JwtException; use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthJwtBody; use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody;
use pocketmine\network\mcpe\protocol\types\login\SelfSignedJwtHeader; use pocketmine\network\mcpe\protocol\types\login\JwtHeader;
use pocketmine\scheduler\AsyncTask; use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue; use pocketmine\thread\NonThreadSafeValue;
use function base64_decode; use function base64_decode;
@ -128,8 +128,8 @@ class ProcessLoginTask extends AsyncTask{
$mapper->bEnforceMapType = false; $mapper->bEnforceMapType = false;
try{ try{
/** @var SelfSignedJwtHeader $headers */ /** @var JwtHeader $headers */
$headers = $mapper->map($headersArray, new SelfSignedJwtHeader()); $headers = $mapper->map($headersArray, new JwtHeader());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e); throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
} }
@ -172,8 +172,8 @@ class ProcessLoginTask extends AsyncTask{
$mapper->bEnforceMapType = false; $mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true; $mapper->bRemoveUndefinedAttributes = true;
try{ try{
/** @var LegacyAuthJwtBody $claims */ /** @var JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new LegacyAuthJwtBody()); $claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e); throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
} }

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert; namespace pocketmine\network\mcpe\convert;
use pocketmine\block\tile\Container;
use pocketmine\block\VanillaBlocks; use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient; use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient; use pocketmine\crafting\MetaWildcardRecipeIngredient;
@ -32,16 +31,10 @@ use pocketmine\crafting\TagWildcardRecipeIngredient;
use pocketmine\data\bedrock\BedrockDataFiles; use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\data\bedrock\item\ItemTypeNames; use pocketmine\data\bedrock\item\ItemTypeNames;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\item\VanillaItems; use pocketmine\item\VanillaItems;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtException; use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\nbt\tag\Tag;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode; use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
@ -59,13 +52,11 @@ use pocketmine\utils\SingletonTrait;
use pocketmine\world\format\io\GlobalBlockStateHandlers; use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers; use pocketmine\world\format\io\GlobalItemDataHandlers;
use function get_class; use function get_class;
use function hash;
class TypeConverter{ class TypeConverter{
use SingletonTrait; use SingletonTrait;
private const PM_ID_TAG = "___Id___"; private const PM_ID_TAG = "___Id___";
private const PM_FULL_NBT_HASH_TAG = "___FullNbtHash___";
private const RECIPE_INPUT_WILDCARD_META = 0x7fff; private const RECIPE_INPUT_WILDCARD_META = 0x7fff;
@ -206,85 +197,6 @@ class TypeConverter{
return new ExactRecipeIngredient($result); return new ExactRecipeIngredient($result);
} }
/**
* Strips unnecessary block actor NBT from items that have it.
* This tag can potentially be extremely large, and is not read by the client anyway.
*/
protected function stripBlockEntityNBT(CompoundTag $tag) : bool{
if(($tag->getTag(Item::TAG_BLOCK_ENTITY_TAG)) !== null){
//client doesn't use this tag, so it's fine to delete completely
$tag->removeTag(Item::TAG_BLOCK_ENTITY_TAG);
return true;
}
return false;
}
/**
* Strips non-viewable data from shulker boxes and similar blocks
* The lore for shulker boxes only requires knowing the type & count of items and possibly custom name
* We don't need to, and should not allow, sending nested inventories across the network.
*/
protected function stripContainedItemNonVisualNBT(CompoundTag $tag) : bool{
if(
($blockEntityInventoryTag = $tag->getTag(Container::TAG_ITEMS)) !== null &&
$blockEntityInventoryTag instanceof ListTag &&
$blockEntityInventoryTag->getTagType() === NBT::TAG_Compound &&
$blockEntityInventoryTag->count() > 0
){
$stripped = new ListTag();
/** @var CompoundTag $itemTag */
foreach($blockEntityInventoryTag as $itemTag){
try{
$containedItem = Item::nbtDeserialize($itemTag);
$customName = $containedItem->getCustomName();
$containedItem->clearNamedTag();
$containedItem->setCustomName($customName);
$stripped->push($containedItem->nbtSerialize());
}catch(SavedDataLoadingException){
continue;
}
}
$tag->setTag(Container::TAG_ITEMS, $stripped);
return true;
}
return false;
}
/**
* Computes a hash of an item's server-side NBT.
* This is baked into an item's network NBT to make sure the client doesn't try to stack items with the same network
* NBT but different server-side NBT.
*/
protected function hashNBT(Tag $tag) : string{
$encoded = (new LittleEndianNbtSerializer())->write(new TreeRoot($tag));
return hash('sha256', $encoded, binary: true);
}
/**
* TODO: HACK!
* Creates a copy of an item's NBT with non-viewable data stripped.
* This is a pretty yucky hack that's mainly needed because of inventories inside blockitems containing blockentity
* data. There isn't really a good way to deal with this due to the way tiles currently require a position,
* otherwise we could just keep a copy of the tile context and ask it for persistent vs network NBT as needed.
* Unfortunately, making this nice will require significant BC breaks, so this will have to do for now.
*/
protected function cleanupUnnecessaryItemNBT(CompoundTag $original) : CompoundTag{
$tag = clone $original;
$anythingStripped = false;
foreach([
$this->stripContainedItemNonVisualNBT($tag),
$this->stripBlockEntityNBT($tag)
] as $stripped){
$anythingStripped = $anythingStripped || $stripped;
}
if($anythingStripped){
$tag->setByteArray(self::PM_FULL_NBT_HASH_TAG, $this->hashNBT($original));
}
return $tag;
}
public function coreItemStackToNet(Item $itemStack) : ItemStack{ public function coreItemStackToNet(Item $itemStack) : ItemStack{
if($itemStack->isNull()){ if($itemStack->isNull()){
return ItemStack::null(); return ItemStack::null();
@ -293,7 +205,7 @@ class TypeConverter{
if($nbt->count() === 0){ if($nbt->count() === 0){
$nbt = null; $nbt = null;
}else{ }else{
$nbt = $this->cleanupUnnecessaryItemNBT($nbt); $nbt = clone $nbt;
} }
$idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack); $idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack);

View File

@ -89,6 +89,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData; use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction; use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData; use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\PredictedResult;
use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData; use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse; use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
@ -498,11 +499,13 @@ class InGamePacketHandler extends PacketHandler{
$blockPos = $data->getBlockPosition(); $blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos); $this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos);
//always sync this in case plugins caused a different result than the client expected if($data->getClientInteractPrediction() === PredictedResult::SUCCESS){
//we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating //always sync this in case plugins caused a different result than the client expected
//more information up the stack. For now I think this is good enough. //we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating
//if only the client would tell us what blocks it thinks changed... //more information up the stack. For now I think this is good enough.
$this->syncBlocksNearby($vBlockPos, $data->getFace()); //if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
}
return true; return true;
case UseItemTransactionData::ACTION_CLICK_AIR: case UseItemTransactionData::ACTION_CLICK_AIR:
if($this->player->isUsingItem()){ if($this->player->isUsingItem()){
@ -717,7 +720,8 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now) case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now)
break; break;
case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK: case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//TODO: do we need to handle this? //in server auth block breaking, we get PREDICT_DESTROY_BLOCK anyway, so this action is redundant
break;
case PlayerAction::PREDICT_DESTROY_BLOCK: case PlayerAction::PREDICT_DESTROY_BLOCK:
self::validateFacing($face); self::validateFacing($face);
if(!$this->player->breakBlock($pos)){ if(!$this->player->breakBlock($pos)){
@ -756,43 +760,6 @@ class InGamePacketHandler extends PacketHandler{
return true; //this packet is useless return true; //this packet is useless
} }
/**
* @throws PacketHandlingException
*/
private function updateSignText(CompoundTag $nbt, string $tagName, bool $frontFace, BaseSign $block, Vector3 $pos) : bool{
$textTag = $nbt->getTag($tagName);
if(!$textTag instanceof CompoundTag){
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textTag) . " for tag \"$tagName\" in sign update data");
}
$textBlobTag = $textTag->getTag(Sign::TAG_TEXT_BLOB);
if(!$textBlobTag instanceof StringTag){
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data");
}
try{
$text = SignText::fromBlob($textBlobTag->getValue());
}catch(\InvalidArgumentException $e){
throw PacketHandlingException::wrap($e, "Invalid sign text update");
}
$oldText = $block->getFaceText($frontFace);
if($text->getLines() === $oldText->getLines()){
return false;
}
try{
if(!$block->updateFaceText($this->player, $frontFace, $text)){
foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){
$this->session->sendDataPacket($updatePacket);
}
return false;
}
return true;
}catch(\UnexpectedValueException $e){
throw PacketHandlingException::wrap($e);
}
}
public function handleBlockActorData(BlockActorDataPacket $packet) : bool{ public function handleBlockActorData(BlockActorDataPacket $packet) : bool{
$pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ()); $pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
if($pos->distanceSquared($this->player->getLocation()) > 10000){ if($pos->distanceSquared($this->player->getLocation()) > 10000){
@ -804,9 +771,29 @@ class InGamePacketHandler extends PacketHandler{
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
if($block instanceof BaseSign){ if($block instanceof BaseSign){
if(!$this->updateSignText($nbt, Sign::TAG_FRONT_TEXT, true, $block, $pos)){ $frontTextTag = $nbt->getTag(Sign::TAG_FRONT_TEXT);
//only one side can be updated at a time if(!$frontTextTag instanceof CompoundTag){
$this->updateSignText($nbt, Sign::TAG_BACK_TEXT, false, $block, $pos); throw new PacketHandlingException("Invalid tag type " . get_debug_type($frontTextTag) . " for tag \"" . Sign::TAG_FRONT_TEXT . "\" in sign update data");
}
$textBlobTag = $frontTextTag->getTag(Sign::TAG_TEXT_BLOB);
if(!$textBlobTag instanceof StringTag){
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data");
}
try{
$text = SignText::fromBlob($textBlobTag->getValue());
}catch(\InvalidArgumentException $e){
throw PacketHandlingException::wrap($e, "Invalid sign text update");
}
try{
if(!$block->updateText($this->player, $text)){
foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){
$this->session->sendDataPacket($updatePacket);
}
}
}catch(\UnexpectedValueException $e){
throw PacketHandlingException::wrap($e);
} }
return true; return true;

View File

@ -32,12 +32,12 @@ use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo; use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType; use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientData; use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper; use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain; use pocketmine\network\mcpe\protocol\types\login\JwtChain;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData;
use pocketmine\network\PacketHandlingException; use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\player\PlayerInfo; use pocketmine\player\PlayerInfo;
@ -180,7 +180,7 @@ class LoginPacketHandler extends PacketHandler{
/** /**
* @throws PacketHandlingException * @throws PacketHandlingException
*/ */
protected function parseJwtChain(string $chainDataJwt) : LegacyAuthChain{ protected function parseJwtChain(string $chainDataJwt) : JwtChain{
try{ try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR); $jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){ }catch(\JsonException $e){
@ -195,7 +195,7 @@ class LoginPacketHandler extends PacketHandler{
$mapper->bExceptionOnUndefinedProperty = true; $mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true; $mapper->bStrictObjectTypeChecking = true;
try{ try{
$clientData = $mapper->map($jwtChainJson, new LegacyAuthChain()); $clientData = $mapper->map($jwtChainJson, new JwtChain());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }
@ -205,8 +205,8 @@ class LoginPacketHandler extends PacketHandler{
/** /**
* @throws PacketHandlingException * @throws PacketHandlingException
*/ */
protected function fetchAuthData(LegacyAuthChain $chain) : LegacyAuthIdentityData{ protected function fetchAuthData(JwtChain $chain) : AuthenticationData{
/** @var LegacyAuthIdentityData|null $extraData */ /** @var AuthenticationData|null $extraData */
$extraData = null; $extraData = null;
foreach($chain->chain as $jwt){ foreach($chain->chain as $jwt){
//validate every chain element //validate every chain element
@ -229,8 +229,8 @@ class LoginPacketHandler extends PacketHandler{
$mapper->bExceptionOnUndefinedProperty = true; $mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true; $mapper->bStrictObjectTypeChecking = true;
try{ try{
/** @var LegacyAuthIdentityData $extraData */ /** @var AuthenticationData $extraData */
$extraData = $mapper->map($claims["extraData"], new LegacyAuthIdentityData()); $extraData = $mapper->map($claims["extraData"], new AuthenticationData());
}catch(\JsonMapper_Exception $e){ }catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e); throw PacketHandlingException::wrap($e);
} }

View File

@ -2838,12 +2838,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/** /**
* Opens the player's sign editor GUI for the sign at the given position. * Opens the player's sign editor GUI for the sign at the given position.
* TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations)
*/ */
public function openSignEditor(Vector3 $position, bool $frontFace = true) : void{ public function openSignEditor(Vector3 $position) : void{
$block = $this->getWorld()->getBlock($position); $block = $this->getWorld()->getBlock($position);
if($block instanceof BaseSign){ if($block instanceof BaseSign){
$this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId())); $this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId()));
$this->getNetworkSession()->onOpenSignEditor($position, $frontFace); $this->getNetworkSession()->onOpenSignEditor($position, true);
}else{ }else{
throw new \InvalidArgumentException("Block at this position is not a sign"); throw new \InvalidArgumentException("Block at this position is not a sign");
} }

View File

@ -948,6 +948,12 @@ parameters:
count: 1 count: 1
path: ../../../src/plugin/PluginDescription.php path: ../../../src/plugin/PluginDescription.php
-
message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$srcNamespacePrefix \(string\) does not accept mixed\.$#'
identifier: assign.propertyType
count: 1
path: ../../../src/plugin/PluginDescription.php
- -
message: '#^Cannot call method addChild\(\) on pocketmine\\permission\\Permission\|null\.$#' message: '#^Cannot call method addChild\(\) on pocketmine\\permission\\Permission\|null\.$#'
identifier: method.nonObject identifier: method.nonObject
@ -990,6 +996,12 @@ parameters:
count: 1 count: 1
path: ../../../src/scheduler/TaskScheduler.php path: ../../../src/scheduler/TaskScheduler.php
-
message: '#^Possibly invalid array key type mixed\.$#'
identifier: offsetAccess.invalidOffset
count: 1
path: ../../../src/thread/ThreadManager.php
- -
message: '#^Cannot access offset string on mixed\.$#' message: '#^Cannot access offset string on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible identifier: offsetAccess.nonOffsetAccessible

View File

@ -192,18 +192,6 @@ parameters:
count: 1 count: 1
path: ../../../src/network/mcpe/cache/CraftingDataCache.php path: ../../../src/network/mcpe/cache/CraftingDataCache.php
-
message: '#^Method pocketmine\\network\\mcpe\\compression\\ZlibCompressor\:\:getNetworkId\(\) never returns 1 so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: ../../../src/network/mcpe/compression/ZlibCompressor.php
-
message: '#^Method pocketmine\\network\\mcpe\\compression\\ZlibCompressor\:\:getNetworkId\(\) never returns 255 so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: ../../../src/network/mcpe/compression/ZlibCompressor.php
- -
message: '#^Parameter \#1 \$states of class pocketmine\\network\\mcpe\\convert\\BlockStateDictionary constructor expects list\<pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\>, array\<int\<0, max\>, pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\> given\.$#' message: '#^Parameter \#1 \$states of class pocketmine\\network\\mcpe\\convert\\BlockStateDictionary constructor expects list\<pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\>, array\<int\<0, max\>, pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\> given\.$#'
identifier: argument.type identifier: argument.type

View File

@ -30,3 +30,26 @@ parameters:
count: 4 count: 4
path: ../../../src/world/generator/noise/Noise.php path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q00 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q01 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q10 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q11 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php

View File

@ -47,7 +47,7 @@ final class CloningRegistryTraitTest extends TestCase{
public function testGetAllClone() : void{ public function testGetAllClone() : void{
$list1 = TestCloningRegistry::getAll(); $list1 = TestCloningRegistry::getAll();
$list2 = TestCloningRegistry::getAll(); $list2 = TestCloningRegistry::getAll();
foreach(Utils::promoteKeys($list1) as $k => $member){ foreach(Utils::stringifyKeys($list1) as $k => $member){
self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members"); self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members");
} }
} }

View File

@ -354,6 +354,9 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra
* @param string[] $strings * @param string[] $strings
*/ */
function findCommonPrefix(array $strings) : string{ function findCommonPrefix(array $strings) : string{
if(count($strings) === 0){
return "";
}
sort($strings, SORT_STRING); sort($strings, SORT_STRING);
$first = $strings[array_key_first($strings)]; $first = $strings[array_key_first($strings)];