Compare commits

..

13 Commits

Author SHA1 Message Date
1541cfa90e First pass integrating ext-encoding where possible 2025-09-08 21:44:32 +01:00
2f2c53067b Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/17507094833
2025-09-06 00:02:53 +00:00
1bc7cf340d Merge branch 'stable' into minor-next 2025-09-04 23:30:09 +01:00
26cd5c471c Merge branch 'stable' into minor-next 2025-09-04 22:03:22 +01:00
3999a1f9f4 Fix sneaking hitbox height (#6771) 2025-09-03 08:53:32 +02:00
925b34e5c6 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/17418990148
2025-09-03 00:02:53 +00:00
fa5cc3301c Strip unnecessary NBT from network items (#6790)
TypeConverter: Strip unnecessary NBT from clientbound items
Item containers like shulker boxes, or chests with block entity data obtained with ctrl+middle click, will often have extremely large NBT payloads.
This problem gets exponentially worse in cases where it's possible to nest inventories, as in #4665.

We can't easily address this at the core level, because tiles are not able to exist within items (due to position requirement)
so we don't have a good way to avoid this useless NBT in the first place. However, we can strip it before the item is sent to
the client, which dramatically reduces the network costs of such items, as well as removing any reason the client could have
to send such enormous items to the server.

A fully loaded shulker box with written books now takes only 5-6 KB on the wire instead of ~1 MB, which is a substantial improvement.

Dealing with this in a less hacky way is currently blocked on #6147.
2025-09-02 18:36:00 +01:00
b2d0be5b75 Support editing the back side of signs (#6774)
* Deprecate BaseSign get/set/updateText(), add get/set/updateFaceText() which accepts true/false for front/back
* add isFrontFace() to SignChangeEvent
* add optional frontFace to Player::openSignEditor()
* add BaseSign::getFacingDegrees() and getHitboxCenter() which need to be implemented by subclasses
2025-09-01 17:15:29 +01:00
d3c36e6287 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/17364003235
2025-09-01 00:03:50 +00:00
e569cc3275 stfu 2025-08-31 21:47:49 +01:00
c931437a30 InventoryTransaction: Remove action shuffling
we used to have this to prevent dependence on client ordering, and make ordering consistently not work.
However, since the introduction of the ItemStackRequest protocol, we don't expect to see client actions
in the wrong order anymore, so this shouldn't be needed anymore.

closes #6701
2025-08-31 21:45:55 +01:00
dca9d3a010 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/17346805858
2025-08-30 17:49:04 +00:00
9310c46ea1 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/17336647491
2025-08-30 00:02:47 +00:00
25 changed files with 498 additions and 284 deletions

View File

@ -12,6 +12,7 @@
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-encoding": "~0.5.0",
"ext-gmp": "*",
"ext-hash": "*",
"ext-igbinary": "^3.0.1",

17
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": "27fee330bdcb6ea2373c57cdfb3bc22f",
"content-hash": "847557938e401ac309306c1d7945316d",
"packages": [
{
"name": "adhocore/json-comment",
@ -818,20 +818,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.1",
"version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@ -890,9 +890,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.0"
},
"time": "2025-09-04T20:59:21+00:00"
"time": "2025-06-25T14:20:11+00:00"
},
{
"name": "symfony/filesystem",
@ -2784,6 +2784,7 @@
"ext-ctype": "*",
"ext-curl": "*",
"ext-date": "*",
"ext-encoding": "~0.5.0",
"ext-gmp": "*",
"ext-hash": "*",
"ext-igbinary": "^3.0.1",

View File

@ -41,14 +41,19 @@ use pocketmine\utils\TextFormat;
use pocketmine\world\BlockTransaction;
use pocketmine\world\sound\DyeUseSound;
use pocketmine\world\sound\InkSacUseSound;
use function abs;
use function array_map;
use function assert;
use function atan2;
use function fmod;
use function rad2deg;
use function strlen;
abstract class BaseSign extends Transparent implements WoodMaterial{
use WoodTypeTrait;
protected SignText $text;
protected SignText $text; //TODO: rename this (BC break)
protected SignText $backText;
private bool $waxed = false;
protected ?int $editorEntityRuntimeId = null;
@ -63,6 +68,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$this->woodType = $woodType;
parent::__construct($idInfo, $name, $typeInfo);
$this->text = new SignText();
$this->backText = new SignText();
$this->asItemCallback = $asItemCallback;
}
@ -71,6 +77,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$tile = $this->position->getWorld()->getTile($this->position);
if($tile instanceof TileSign){
$this->text = $tile->getText();
$this->backText = $tile->getBackText();
$this->waxed = $tile->isWaxed();
$this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId();
}
@ -83,6 +90,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
$tile = $this->position->getWorld()->getTile($this->position);
assert($tile instanceof TileSign);
$tile->setText($this->text);
$tile->setBackText($this->backText);
$tile->setWaxed($this->waxed);
$tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId);
}
@ -127,11 +135,11 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
}
}
private function doSignChange(SignText $newText, Player $player, Item $item) : bool{
$ev = new SignChangeEvent($this, $player, $newText);
private function doSignChange(SignText $newText, Player $player, Item $item, bool $frontFace) : bool{
$ev = new SignChangeEvent($this, $player, $newText, $frontFace);
$ev->call();
if(!$ev->isCancelled()){
$this->text = $ev->getNewText();
$this->setFaceText($frontFace, $ev->getNewText());
$this->position->getWorld()->setBlock($this->position, $this);
$item->pop();
return true;
@ -140,8 +148,9 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
return false;
}
private function changeSignGlowingState(bool $glowing, Player $player, Item $item) : bool{
if($this->text->isGlowing() !== $glowing && $this->doSignChange(new SignText($this->text->getLines(), $this->text->getBaseColor(), $glowing), $player, $item)){
private function changeSignGlowingState(bool $glowing, Player $player, Item $item, bool $frontFace) : bool{
$text = $this->getFaceText($frontFace);
if($text->isGlowing() !== $glowing && $this->doSignChange(new SignText($text->getLines(), $text->getBaseColor(), $glowing), $player, $item, $frontFace)){
$this->position->getWorld()->addSound($this->position, new InkSacUseSound());
return true;
}
@ -168,6 +177,8 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
return true;
}
$frontFace = $this->interactsFront($this->getHitboxCenter(), $player->getPosition(), $this->getFacingDegrees());
$dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){
ItemTypeIds::BONE_MEAL => DyeColor::WHITE,
ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE,
@ -176,40 +187,82 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
};
if($dyeColor !== null){
$color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue();
$text = $this->getFaceText($frontFace);
if(
$color->toARGB() !== $this->text->getBaseColor()->toARGB() &&
$this->doSignChange(new SignText($this->text->getLines(), $color, $this->text->isGlowing()), $player, $item)
$color->toARGB() !== $text->getBaseColor()->toARGB() &&
$this->doSignChange(new SignText($text->getLines(), $color, $text->isGlowing()), $player, $item, $frontFace)
){
$this->position->getWorld()->addSound($this->position, new DyeUseSound());
return true;
}
}elseif(match($item->getTypeId()){
ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item),
ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item),
ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item, $frontFace),
ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item, $frontFace),
ItemTypeIds::HONEYCOMB => $this->wax($player, $item),
default => false
}){
return true;
}
$player->openSignEditor($this->position);
$player->openSignEditor($this->position, $frontFace);
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.
* @deprecated
* @see self::getFaceText()
*/
public function getText() : SignText{
return $this->text;
}
/** @return $this */
/**
* @deprecated
* @see self::setFaceText()
* @return $this
*/
public function setText(SignText $text) : self{
$this->text = $text;
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.
*/
@ -234,13 +287,21 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
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.
*
* @return bool if the sign update was successful.
* @throws \UnexpectedValueException if the text payload is too large
*/
public function updateText(Player $author, SignText $text) : bool{
public function updateFaceText(Player $author, bool $frontFace, SignText $text) : bool{
$size = 0;
foreach($text->getLines() as $line){
$size += strlen($line);
@ -248,15 +309,16 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
if($size > 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{
return TextFormat::clean($line, false);
}, $text->getLines()), $this->text->getBaseColor(), $this->text->isGlowing()));
}, $text->getLines()), $oldText->getBaseColor(), $oldText->isGlowing()), $frontFace);
if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){
$ev->cancel();
}
$ev->call();
if(!$ev->isCancelled()){
$this->setText($ev->getNewText());
$this->setFaceText($frontFace, $ev->getNewText());
$this->setEditorEntityRuntimeId(null);
$this->position->getWorld()->setBlock($this->position, $this);
return true;

View File

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

View File

@ -30,6 +30,7 @@ use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction;
final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing{
@ -65,4 +66,14 @@ final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing
$supportBlock->getSupportType(Facing::DOWN) === SupportType::FULL ||
(($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,4 +48,8 @@ final class FloorSign extends BaseSign implements SignLikeRotation{
}
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
protected function getFacingDegrees() : float{
return $this->rotation * 22.5;
}
}

View File

@ -32,6 +32,7 @@ use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction;
final class WallHangingSign extends BaseSign implements HorizontalFacing{
@ -78,4 +79,14 @@ final class WallHangingSign extends BaseSign implements HorizontalFacing{
($block instanceof WallHangingSign && Facing::axis(Facing::rotateY($block->getFacing(), clockwise: true)) === Facing::axis($face)) ||
$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,6 +30,7 @@ use pocketmine\math\Axis;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\BlockTransaction;
final class WallSign extends BaseSign implements HorizontalFacing{
@ -46,4 +47,25 @@ final class WallSign extends BaseSign implements HorizontalFacing{
$this->facing = $face;
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,16 +70,18 @@ class Sign extends Spawnable{
}
protected SignText $text;
protected SignText $backText;
private bool $waxed = false;
protected ?int $editorEntityRuntimeId = null;
public function __construct(World $world, Vector3 $pos){
$this->text = new SignText();
$this->backText = new SignText();
parent::__construct($world, $pos);
}
private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : void{
private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : SignText{
$baseColor = new Color(0, 0, 0);
$glowingText = false;
if(($baseColorTag = $nbt->getTag(self::TAG_TEXT_COLOR)) instanceof IntTag){
@ -90,19 +92,27 @@ class Sign extends Spawnable{
//see https://bugs.mojang.com/browse/MCPE-117835
$glowingText = $glowingTextTag->getValue() !== 0;
}
$this->text = SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText);
return 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{
$frontTextTag = $nbt->getTag(self::TAG_FRONT_TEXT);
if($frontTextTag instanceof CompoundTag){
$this->readTextTag($frontTextTag, true);
$this->text = $this->readTextTag($frontTextTag, true);
}elseif($nbt->getTag(self::TAG_TEXT_BLOB) instanceof StringTag){ //MCPE 1.2 save format
$lightingBugResolved = false;
if(($lightingBugResolvedTag = $nbt->getTag(self::TAG_LEGACY_BUG_RESOLVE)) instanceof ByteTag){
$lightingBugResolved = $lightingBugResolvedTag->getValue() !== 0;
}
$this->readTextTag($nbt, $lightingBugResolved);
$this->text = $this->readTextTag($nbt, $lightingBugResolved);
}else{
$text = [];
for($i = 0; $i < SignText::LINE_COUNT; ++$i){
@ -113,22 +123,14 @@ class Sign extends Spawnable{
}
$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;
}
protected function writeSaveData(CompoundTag $nbt) : void{
$nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
->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->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text));
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText));
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
}
@ -141,6 +143,10 @@ class Sign extends Spawnable{
$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 setWaxed(bool $waxed) : void{ $this->waxed = $waxed; }
@ -162,19 +168,8 @@ class Sign extends Spawnable{
}
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
$nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
->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->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text));
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText));
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
$nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1);
}

View File

@ -23,10 +23,11 @@ declare(strict_types=1);
namespace pocketmine\crafting;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\VarInt;
use pocketmine\item\Item;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\DestructorCallbackTrait;
use pocketmine\utils\ObjectSet;
use function array_shift;
@ -114,11 +115,13 @@ class CraftingManager{
}
private static function hashOutput(Item $output) : string{
$write = new BinaryStream();
$write->putVarInt($output->getStateId());
$write->put((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
$write = new ByteBufferWriter();
VarInt::writeSignedInt($write, $output->getStateId());
//TODO: the NBT serializer allocates its own ByteBufferWriter, we should change the API in the future to
//allow passing our own to avoid this extra allocation
$write->writeByteArray((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
return $write->getBuffer();
return $write->getData();
}
/**

View File

@ -23,11 +23,13 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock\block\upgrade;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\VarInt;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use function strlen;
/**
* Handles translating legacy 1.12 block ID/meta into modern blockstates.
@ -84,24 +86,24 @@ final class BlockIdMetaUpgrader{
public static function loadFromString(string $data, LegacyBlockIdToStringIdMap $idMap, BlockStateUpgrader $blockStateUpgrader) : self{
$mappingTable = [];
$legacyStateMapReader = new BinaryStream($data);
$legacyStateMapReader = new ByteBufferReader($data);
$nbtReader = new LittleEndianNbtSerializer();
$idCount = $legacyStateMapReader->getUnsignedVarInt();
$idCount = VarInt::readUnsignedInt($legacyStateMapReader);
for($idIndex = 0; $idIndex < $idCount; $idIndex++){
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt());
$id = $legacyStateMapReader->readByteArray(VarInt::readUnsignedInt($legacyStateMapReader));
$metaCount = $legacyStateMapReader->getUnsignedVarInt();
$metaCount = VarInt::readUnsignedInt($legacyStateMapReader);
for($metaIndex = 0; $metaIndex < $metaCount; $metaIndex++){
$meta = $legacyStateMapReader->getUnsignedVarInt();
$meta = VarInt::readUnsignedInt($legacyStateMapReader);
$offset = $legacyStateMapReader->getOffset();
$state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag();
$state = $nbtReader->read($legacyStateMapReader->getData(), $offset)->mustGetCompoundTag();
$legacyStateMapReader->setOffset($offset);
$mappingTable[$id][$meta] = $blockStateUpgrader->upgrade(BlockStateData::fromNbt($state));
}
}
if(!$legacyStateMapReader->feof()){
if($legacyStateMapReader->getOffset() < strlen($legacyStateMapReader->getData())){
throw new BinaryDataException("Unexpected trailing data in legacy state map data");
}

View File

@ -247,6 +247,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
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.
*/

View File

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

View File

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

View File

@ -30,13 +30,11 @@ use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
use function array_keys;
use function array_values;
use function assert;
use function count;
use function get_class;
use function min;
use function shuffle;
use function spl_object_hash;
use function spl_object_id;
@ -95,10 +93,13 @@ class InventoryTransaction{
}
/**
* Returns an **unordered** set of actions involved in this transaction.
* Returns a set of actions involved in this transaction.
*
* WARNING: This system is **explicitly designed NOT to care about ordering**. Any order seen in this set has NO
* significance and should not be relied on.
* Note: This system is designed to care only about item balances. While you can usually assume that the actions
* are provided in the correct order, it will still successfully complete transactions whose actions are provided in
* 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[]
* @phpstan-return array<int, InventoryAction>
@ -119,19 +120,6 @@ 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[] $haveItems
@ -308,8 +296,6 @@ class InventoryTransaction{
throw new TransactionValidationException("Transaction has already been executed");
}
$this->shuffleActions();
$this->validate();
if(!$this->callExecuteEvent()){

View File

@ -23,8 +23,9 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function base64_decode;
use function base64_encode;
@ -130,17 +131,17 @@ final class JwtUtils{
return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence;
}
private static function signaturePartFromAsn1(BinaryStream $stream) : string{
$prefix = $stream->get(1);
private static function signaturePartFromAsn1(ByteBufferReader $stream) : string{
$prefix = $stream->readByteArray(1);
if($prefix !== self::ASN1_INTEGER_TAG){
throw new \InvalidArgumentException("Expected an ASN.1 INTEGER tag, got " . bin2hex($prefix));
}
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
$length = $stream->getByte();
$length = Byte::readUnsigned($stream);
if($length > self::SIGNATURE_PART_LENGTH + 1){ //each part may have an extra leading 0 byte to prevent it being interpreted as a negative number
throw new \InvalidArgumentException("Expected at most 49 bytes for signature R or S, got $length");
}
$part = $stream->get($length);
$part = $stream->readByteArray($length);
return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT);
}
@ -156,11 +157,11 @@ final class JwtUtils{
throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts));
}
$stream = new BinaryStream($parts);
$stream = new ByteBufferReader($parts);
$rRaw = self::signaturePartFromAsn1($stream);
$sRaw = self::signaturePartFromAsn1($stream);
if(!$stream->feof()){
if($stream->getOffset() < strlen($stream->getData())){
throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data");
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\block\tile\Container;
use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient;
@ -31,10 +32,16 @@ use pocketmine\crafting\TagWildcardRecipeIngredient;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\data\bedrock\item\ItemTypeNames;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtException;
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\PacketSerializer;
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
@ -52,11 +59,13 @@ use pocketmine\utils\SingletonTrait;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use function get_class;
use function hash;
class TypeConverter{
use SingletonTrait;
private const PM_ID_TAG = "___Id___";
private const PM_FULL_NBT_HASH_TAG = "___FullNbtHash___";
private const RECIPE_INPUT_WILDCARD_META = 0x7fff;
@ -197,6 +206,85 @@ class TypeConverter{
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{
if($itemStack->isNull()){
return ItemStack::null();
@ -205,7 +293,7 @@ class TypeConverter{
if($nbt->count() === 0){
$nbt = null;
}else{
$nbt = clone $nbt;
$nbt = $this->cleanupUnnecessaryItemNBT($nbt);
}
$idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack);

View File

@ -756,6 +756,43 @@ class InGamePacketHandler extends PacketHandler{
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{
$pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
if($pos->distanceSquared($this->player->getLocation()) > 10000){
@ -767,29 +804,9 @@ class InGamePacketHandler extends PacketHandler{
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
if($block instanceof BaseSign){
$frontTextTag = $nbt->getTag(Sign::TAG_FRONT_TEXT);
if(!$frontTextTag instanceof CompoundTag){
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);
if(!$this->updateSignText($nbt, Sign::TAG_FRONT_TEXT, true, $block, $pos)){
//only one side can be updated at a time
$this->updateSignText($nbt, Sign::TAG_BACK_TEXT, false, $block, $pos);
}
return true;

View File

@ -23,16 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\serializer;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\VarInt;
use pocketmine\block\tile\Spawnable;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\convert\BlockTranslator;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
@ -84,7 +84,7 @@ final class ChunkSerializer{
* @phpstan-param DimensionIds::* $dimensionId
*/
public static function serializeFullChunk(Chunk $chunk, int $dimensionId, BlockTranslator $blockTranslator, ?string $tiles = null) : string{
$stream = PacketSerializer::encoder();
$stream = new ByteBufferWriter();
$subChunkCount = self::getSubChunkCount($chunk, $dimensionId);
$writtenCount = 0;
@ -100,37 +100,34 @@ final class ChunkSerializer{
self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream);
}
$stream->putByte(0); //border block array count
Byte::writeUnsigned($stream, 0); //border block array count
//Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client.
if($tiles !== null){
$stream->put($tiles);
$stream->writeByteArray($tiles);
}else{
$stream->put(self::serializeTiles($chunk));
$stream->writeByteArray(self::serializeTiles($chunk));
}
return $stream->getBuffer();
return $stream->getData();
}
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, ByteBufferWriter $stream, bool $persistentBlockStates) : void{
$layers = $subChunk->getBlockLayers();
$stream->putByte(8); //version
Byte::writeUnsigned($stream, 8); //version
$stream->putByte(count($layers));
Byte::writeUnsigned($stream, count($layers));
$blockStateDictionary = $blockTranslator->getBlockStateDictionary();
foreach($layers as $blocks){
$bitsPerBlock = $blocks->getBitsPerBlock();
$words = $blocks->getWordArray();
$stream->putByte(($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1));
$stream->put($words);
Byte::writeUnsigned($stream, ($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1));
$stream->writeByteArray($words);
$palette = $blocks->getPalette();
if($bitsPerBlock !== 0){
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$stream->putUnsignedVarInt(count($palette) << 1); //yes, this is intentionally zigzag
VarInt::writeSignedInt($stream, count($palette)); //yes, this is intentionally zigzag
}
if($persistentBlockStates){
$nbtSerializer = new NetworkNbtSerializer();
@ -141,46 +138,44 @@ final class ChunkSerializer{
$state = $blockTranslator->getFallbackStateData();
}
$stream->put($nbtSerializer->write(new TreeRoot($state->toNbt())));
$stream->writeByteArray($nbtSerializer->write(new TreeRoot($state->toNbt())));
}
}else{
$networkPalette = [];
foreach($palette as $p){
$stream->put(Binary::writeUnsignedVarInt($blockTranslator->internalIdToNetworkId($p) << 1));
$networkPalette[] = $blockTranslator->internalIdToNetworkId($p);
}
VarInt::writeSignedIntArray($stream, $networkPalette);
}
}
}
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $stream) : void{
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, ByteBufferWriter $stream) : void{
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
$stream->putByte(($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
$stream->put($biomePalette->getWordArray());
Byte::writeUnsigned($stream, ($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
$stream->writeByteArray($biomePalette->getWordArray());
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
//zigzag and just shift directly.
$biomePaletteArray = $biomePalette->getPalette();
if($biomePaletteBitsPerBlock !== 0){
$stream->putUnsignedVarInt(count($biomePaletteArray) << 1);
VarInt::writeSignedInt($stream, count($biomePaletteArray));
}
foreach($biomePaletteArray as $p){
foreach($biomePaletteArray as $k => $p){
if($biomeIdMap->legacyToString($p) === null){
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
$p = BiomeIds::OCEAN;
$biomePaletteArray[$k] = BiomeIds::OCEAN;
}
$stream->put(Binary::writeUnsignedVarInt($p << 1));
}
VarInt::writeSignedIntArray($stream, $biomePaletteArray);
}
public static function serializeTiles(Chunk $chunk) : string{
$stream = new BinaryStream();
$stream = new ByteBufferWriter();
foreach($chunk->getTiles() as $tile){
if($tile instanceof Spawnable){
$stream->put($tile->getSerializedSpawnCompound()->getEncodedNbt());
$stream->writeByteArray($tile->getSerializedSpawnCompound()->getEncodedNbt());
}
}
return $stream->getBuffer();
return $stream->getData();
}
}

View File

@ -27,13 +27,15 @@ declare(strict_types=1);
*/
namespace pocketmine\network\query;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\RawPacketHandler;
use pocketmine\Server;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use function chr;
use function hash;
use function random_bytes;
use function strlen;
@ -85,40 +87,42 @@ class QueryHandler implements RawPacketHandler{
public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{
try{
$stream = new BinaryStream($packet);
$header = $stream->get(2);
$stream = new ByteBufferReader($packet);
$header = $stream->readByteArray(2);
if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above
return false;
}
$packetType = $stream->getByte();
$sessionID = $stream->getInt();
$packetType = Byte::readUnsigned($stream);
$sessionID = BE::readUnsignedInt($stream);
switch($packetType){
case self::HANDSHAKE: //Handshake
$reply = chr(self::HANDSHAKE);
$reply .= Binary::writeInt($sessionID);
$reply .= self::getTokenString($this->token, $address) . "\x00";
$writer = new ByteBufferWriter();
Byte::writeUnsigned($writer, self::HANDSHAKE);
BE::writeUnsignedInt($writer, $sessionID);
$writer->writeByteArray(self::getTokenString($this->token, $address) . "\x00");
$interface->sendRawPacket($address, $port, $reply);
$interface->sendRawPacket($address, $port, $writer->getData());
return true;
case self::STATISTICS: //Stat
$token = $stream->getInt();
$token = BE::readUnsignedInt($stream);
if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){
$this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2");
return true;
}
$reply = chr(self::STATISTICS);
$reply .= Binary::writeInt($sessionID);
$writer = new ByteBufferWriter();
Byte::writeUnsigned($writer, self::STATISTICS);
BE::writeUnsignedInt($writer, $sessionID);
$remaining = $stream->getRemaining();
if(strlen($remaining) === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01
$reply .= $this->server->getQueryInformation()->getLongQuery();
$remaining = strlen($stream->getData()) - $stream->getOffset();
if($remaining === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01
$writer->writeByteArray($this->server->getQueryInformation()->getLongQuery());
}else{
$reply .= $this->server->getQueryInformation()->getShortQuery();
$writer->writeByteArray($this->server->getQueryInformation()->getShortQuery());
}
$interface->sendRawPacket($address, $port, $reply);
$interface->sendRawPacket($address, $port, $writer->getData());
return true;
default:

View File

@ -2838,13 +2838,12 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/**
* 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) : void{
public function openSignEditor(Vector3 $position, bool $frontFace = true) : void{
$block = $this->getWorld()->getBlock($position);
if($block instanceof BaseSign){
$this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId()));
$this->getNetworkSession()->onOpenSignEditor($position, true);
$this->getNetworkSession()->onOpenSignEditor($position, $frontFace);
}else{
throw new \InvalidArgumentException("Block at this position is not a sign");
}

View File

@ -23,16 +23,14 @@ declare(strict_types=1);
namespace pocketmine\world\format\io;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function array_values;
use function count;
use function pack;
use function strlen;
use function unpack;
/**
* This class provides a serializer used for transmitting chunks between threads.
@ -45,15 +43,14 @@ final class FastChunkSerializer{
//NOOP
}
private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array) : void{
private static function serializePalettedArray(ByteBufferWriter $stream, PalettedBlockArray $array) : void{
$wordArray = $array->getWordArray();
$palette = $array->getPalette();
$stream->putByte($array->getBitsPerBlock());
$stream->put($wordArray);
$serialPalette = pack("L*", ...$palette);
$stream->putInt(strlen($serialPalette));
$stream->put($serialPalette);
Byte::writeUnsigned($stream, $array->getBitsPerBlock());
$stream->writeByteArray($wordArray);
BE::writeUnsignedInt($stream, count($palette));
BE::writeUnsignedIntArray($stream, $palette);
}
/**
@ -61,21 +58,20 @@ final class FastChunkSerializer{
* TODO: tiles and entities
*/
public static function serializeTerrain(Chunk $chunk) : string{
$stream = new BinaryStream();
$stream->putByte(
($chunk->isPopulated() ? self::FLAG_POPULATED : 0)
);
$stream = new ByteBufferWriter();
Byte::writeUnsigned($stream, ($chunk->isPopulated() ? self::FLAG_POPULATED : 0));
//subchunks
$subChunks = $chunk->getSubChunks();
$count = count($subChunks);
$stream->putByte($count);
Byte::writeUnsigned($stream, $count);
foreach($subChunks as $y => $subChunk){
$stream->putByte($y);
$stream->putInt($subChunk->getEmptyBlockId());
Byte::writeSigned($stream, $y);
BE::writeUnsignedInt($stream, $subChunk->getEmptyBlockId());
$layers = $subChunk->getBlockLayers();
$stream->putByte(count($layers));
Byte::writeUnsigned($stream, count($layers));
foreach($layers as $blocks){
self::serializePalettedArray($stream, $blocks);
}
@ -83,15 +79,14 @@ final class FastChunkSerializer{
}
return $stream->getBuffer();
return $stream->getData();
}
private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte();
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
/** @var int[] $unpackedPalette */
$unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here
$palette = array_values($unpackedPalette);
private static function deserializePalettedArray(ByteBufferReader $stream) : PalettedBlockArray{
$bitsPerBlock = Byte::readUnsigned($stream);
$words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
$paletteSize = BE::readUnsignedInt($stream);
$palette = BE::readUnsignedIntArray($stream, $paletteSize);
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
@ -100,20 +95,21 @@ final class FastChunkSerializer{
* Deserializes a fast-serialized chunk
*/
public static function deserializeTerrain(string $data) : Chunk{
$stream = new BinaryStream($data);
$stream = new ByteBufferReader($data);
$flags = $stream->getByte();
$flags = Byte::readUnsigned($stream);
$terrainPopulated = (bool) ($flags & self::FLAG_POPULATED);
$subChunks = [];
$count = $stream->getByte();
$count = Byte::readUnsigned($stream);
for($subCount = 0; $subCount < $count; ++$subCount){
$y = Binary::signByte($stream->getByte());
$airBlockId = $stream->getInt();
$y = Byte::readSigned($stream);
//TODO: why the heck are we using big-endian here?
$airBlockId = BE::readUnsignedInt($stream);
$layers = [];
for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){
for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){
$layers[] = self::deserializePalettedArray($stream);
}
$biomeArray = self::deserializePalettedArray($stream);

View File

@ -23,6 +23,11 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\leveldb;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pmmp\encoding\ByteBufferWriter;
use pmmp\encoding\LE;
use pocketmine\block\Block;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
@ -35,7 +40,6 @@ use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use pocketmine\world\format\Chunk;
@ -55,7 +59,6 @@ use pocketmine\world\format\SubChunk;
use pocketmine\world\WorldCreationOptions;
use Symfony\Component\Filesystem\Path;
use function array_map;
use function array_values;
use function chr;
use function count;
use function defined;
@ -69,7 +72,6 @@ use function str_repeat;
use function strlen;
use function substr;
use function trim;
use function unpack;
use const LEVELDB_ZLIB_RAW_COMPRESSION;
class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
@ -149,11 +151,11 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/**
* @throws CorruptedChunkException
*/
protected function deserializeBlockPalette(BinaryStream $stream, \Logger $logger) : PalettedBlockArray{
$bitsPerBlock = $stream->getByte() >> 1;
protected function deserializeBlockPalette(ByteBufferReader $stream, \Logger $logger) : PalettedBlockArray{
$bitsPerBlock = Byte::readUnsigned($stream) >> 1;
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
$words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted storage: " . $e->getMessage(), 0, $e);
}
@ -174,18 +176,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
* - https://github.com/pmmp/PocketMine-MP/issues/5911
*/
$offset = $stream->getOffset();
$byte1 = $stream->getByte();
$byte1 = Byte::readUnsigned($stream);
$stream->setOffset($offset); //reset offset
if($byte1 !== NBT::TAG_Compound){ //normally the first byte would be the NBT of the blockstate
$susLength = $stream->getLInt();
$susLength = LE::readUnsignedInt($stream);
if($susLength !== 1){ //make sure the data isn't complete garbage
throw new CorruptedChunkException("CustomItemAPI borked 0 bpb palette should always have a length of 1");
}
$logger->error("Unexpected palette size for 0 bpb palette");
}
}else{
$paletteSize = $stream->getLInt();
$paletteSize = LE::readUnsignedInt($stream);
}
$blockDecodeErrors = [];
@ -193,7 +195,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
for($i = 0; $i < $paletteSize; ++$i){
try{
$offset = $stream->getOffset();
$blockStateNbt = $nbt->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$blockStateNbt = $nbt->read($stream->getData(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
}catch(NbtDataException $e){
//NBT borked, unrecoverable
@ -234,20 +236,20 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private function serializeBlockPalette(BinaryStream $stream, PalettedBlockArray $blocks) : void{
$stream->putByte($blocks->getBitsPerBlock() << 1);
$stream->put($blocks->getWordArray());
private function serializeBlockPalette(ByteBufferWriter $stream, PalettedBlockArray $blocks) : void{
Byte::writeUnsigned($stream, $blocks->getBitsPerBlock() << 1);
$stream->writeByteArray($blocks->getWordArray());
$palette = $blocks->getPalette();
if($blocks->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
LE::writeUnsignedInt($stream, count($palette));
}
$tags = [];
foreach($palette as $p){
$tags[] = new TreeRoot($this->blockStateSerializer->serialize($p)->toNbt());
}
$stream->put((new LittleEndianNbtSerializer())->writeMultiple($tags));
$stream->writeByteArray((new LittleEndianNbtSerializer())->writeMultiple($tags));
}
/**
@ -267,34 +269,28 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/**
* @throws CorruptedChunkException
*/
private static function deserializeBiomePalette(BinaryStream $stream, int $bitsPerBlock) : PalettedBlockArray{
private static function deserializeBiomePalette(ByteBufferReader $stream, int $bitsPerBlock) : PalettedBlockArray{
try{
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
$words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
}catch(\InvalidArgumentException $e){
throw new CorruptedChunkException("Failed to deserialize paletted biomes: " . $e->getMessage(), 0, $e);
}
$palette = [];
$paletteSize = $bitsPerBlock === 0 ? 1 : $stream->getLInt();
for($i = 0; $i < $paletteSize; ++$i){
$palette[] = $stream->getLInt();
}
$paletteSize = $bitsPerBlock === 0 ? 1 : LE::readUnsignedInt($stream);
$palette = LE::readUnsignedIntArray($stream, $paletteSize);
//TODO: exceptions
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
}
private static function serializeBiomePalette(BinaryStream $stream, PalettedBlockArray $biomes) : void{
$stream->putByte($biomes->getBitsPerBlock() << 1);
$stream->put($biomes->getWordArray());
private static function serializeBiomePalette(ByteBufferWriter $stream, PalettedBlockArray $biomes) : void{
Byte::writeUnsigned($stream, $biomes->getBitsPerBlock() << 1);
$stream->writeByteArray($biomes->getWordArray());
$palette = $biomes->getPalette();
if($biomes->getBitsPerBlock() !== 0){
$stream->putLInt(count($palette));
}
foreach($palette as $p){
$stream->putLInt($p);
LE::writeUnsignedInt($stream, count($palette));
}
LE::writeUnsignedIntArray($stream, $palette);
}
/**
@ -302,7 +298,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
* @return PalettedBlockArray[]
* @phpstan-return array<int, PalettedBlockArray>
*/
private static function deserialize3dBiomes(BinaryStream $stream, int $chunkVersion, \Logger $logger) : array{
private static function deserialize3dBiomes(ByteBufferReader $stream, int $chunkVersion, \Logger $logger) : array{
$previous = null;
$result = [];
$nextIndex = Chunk::MIN_SUBCHUNK_INDEX;
@ -310,7 +306,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$expectedCount = self::getExpected3dBiomesCount($chunkVersion);
for($i = 0; $i < $expectedCount; ++$i){
try{
$bitsPerBlock = $stream->getByte() >> 1;
$bitsPerBlock = Byte::readUnsigned($stream) >> 1;
if($bitsPerBlock === 127){
if($previous === null){
throw new CorruptedChunkException("Serialized biome palette $i has no previous palette to copy from");
@ -322,7 +318,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$previous = $decoded;
if($nextIndex <= Chunk::MAX_SUBCHUNK_INDEX){ //older versions wrote additional superfluous biome palettes
$result[$nextIndex++] = $decoded;
}elseif($stream->feof()){
}elseif($stream->getOffset() >= strlen($stream->getData())){
//not enough padding biome arrays for the given version - this is non-critical since we discard the excess anyway, but this should be logged
$logger->error("Wrong number of 3D biome palettes for this chunk version: expected $expectedCount, but got " . ($i + 1) . " - this is not a problem, but may indicate a corrupted chunk");
break;
@ -331,7 +327,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
throw new CorruptedChunkException("Failed to deserialize biome palette $i: " . $e->getMessage(), 0, $e);
}
}
if(!$stream->feof()){
if($stream->getOffset() < strlen($stream->getData())){
//maybe bad output produced by a third-party conversion tool like Chunker
$logger->error("Unexpected trailing data after 3D biomes data");
}
@ -342,7 +338,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/**
* @param SubChunk[] $subChunks
*/
private static function serialize3dBiomes(BinaryStream $stream, array $subChunks) : void{
private static function serialize3dBiomes(ByteBufferWriter $stream, array $subChunks) : void{
//TODO: the server-side min/max may not coincide with the world storage min/max - we may need additional logic to handle this
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; $y++){
//TODO: is it worth trying to use the previous palette if it's the same as the current one? vanilla supports
@ -378,12 +374,12 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/** @var PalettedBlockArray[] $extraDataLayers */
$extraDataLayers = [];
$binaryStream = new BinaryStream($extraRawData);
$count = $binaryStream->getLInt();
$binaryStream = new ByteBufferReader($extraRawData);
$count = LE::readUnsignedInt($binaryStream);
for($i = 0; $i < $count; ++$i){
$key = $binaryStream->getLInt();
$value = $binaryStream->getLShort();
$key = LE::readUnsignedInt($binaryStream);
$value = LE::readUnsignedShort($binaryStream);
self::deserializeExtraDataKey($chunkVersion, $key, $x, $fullY, $z);
@ -439,24 +435,24 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
if($legacyTerrain === false){
throw new CorruptedChunkException("Missing expected LEGACY_TERRAIN tag for format version $chunkVersion");
}
$binaryStream = new BinaryStream($legacyTerrain);
$binaryStream = new ByteBufferReader($legacyTerrain);
try{
$fullIds = $binaryStream->get(32768);
$fullData = $binaryStream->get(16384);
$binaryStream->get(32768); //legacy light info, discard it
$fullIds = $binaryStream->readByteArray(32768);
$fullData = $binaryStream->readByteArray(16384);
$binaryStream->readByteArray(32768); //legacy light info, discard it
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
try{
$binaryStream->get(256); //heightmap, discard it
/** @var int[] $unpackedBiomeArray */
$unpackedBiomeArray = unpack("N*", $binaryStream->get(1024)); //unpack() will never fail here
$biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors(array_values($unpackedBiomeArray))); //never throws
$binaryStream->readByteArray(256); //heightmap, discard it
//TODO: big endian here doesn't seem correct, but I have no way to verify
$biomeColors = BE::readUnsignedIntArray($binaryStream, 256);
$biomes3d = ChunkUtils::extrapolate3DBiomes(ChunkUtils::convertBiomeColors($biomeColors)); //never throws
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if(!$binaryStream->feof()){
if($binaryStream->getOffset() < strlen($binaryStream->getData())){
$logger->error("Unexpected trailing data in legacy terrain data");
}
@ -482,18 +478,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
/**
* Deserializes a subchunk stored in the legacy non-paletted format used from 1.0 until 1.2.13.
*/
private function deserializeNonPalettedSubChunkData(BinaryStream $binaryStream, int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
private function deserializeNonPalettedSubChunkData(ByteBufferReader $binaryStream, int $chunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
try{
$blocks = $binaryStream->get(4096);
$blockData = $binaryStream->get(2048);
$blocks = $binaryStream->readByteArray(4096);
$blockData = $binaryStream->readByteArray(2048);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
}
if($chunkVersion < ChunkVersion::v1_1_0){
try{
$binaryStream->get(4096); //legacy light info, discard it
if(!$binaryStream->feof()){
$binaryStream->readByteArray(4096); //legacy light info, discard it
if($binaryStream->getOffset() < strlen($binaryStream->getData())){
$logger->error("Unexpected trailing data in legacy subchunk data");
}
}catch(BinaryDataException $e){
@ -515,7 +511,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
* @see ChunkDataKey::SUBCHUNK
* @throws CorruptedChunkException
*/
private function deserializeSubChunkData(BinaryStream $binaryStream, int $chunkVersion, int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
private function deserializeSubChunkData(ByteBufferReader $binaryStream, int $chunkVersion, int $subChunkVersion, ?PalettedBlockArray $convertedLegacyExtraData, PalettedBlockArray $biomePalette, \Logger $logger) : SubChunk{
switch($subChunkVersion){
case SubChunkVersion::CLASSIC:
case SubChunkVersion::CLASSIC_BUG_2: //these are all identical to version 0, but vanilla respects these so we should also
@ -535,10 +531,10 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8
$storageCount = $binaryStream->getByte();
$storageCount = Byte::readUnsigned($binaryStream);
if($subChunkVersion >= SubChunkVersion::PALETTED_MULTI_WITH_OFFSET){
//height ignored; this seems pointless since this is already in the key anyway
$binaryStream->getByte();
Byte::readSigned($binaryStream);
}
$storages = [];
@ -579,11 +575,11 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
continue;
}
$binaryStream = new BinaryStream($data);
if($binaryStream->feof()){
if($data === ""){
throw new CorruptedChunkException("Unexpected empty data for subchunk $y");
}
$subChunkVersion = $binaryStream->getByte();
$binaryStream = new ByteBufferReader($data);
$subChunkVersion = Byte::readUnsigned($binaryStream);;
if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
$hasBeenUpgraded = true;
}
@ -610,12 +606,12 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
private function deserializeBiomeData(string $index, int $chunkVersion, \Logger $logger) : array{
$biomeArrays = [];
if(($maps2d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps2d);
$binaryStream = new ByteBufferReader($maps2d);
try{
$binaryStream->get(512); //heightmap, discard it
$biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->get(256)); //never throws
if(!$binaryStream->feof()){
$binaryStream->readByteArray(512); //heightmap, discard it
$biomes3d = ChunkUtils::extrapolate3DBiomes($binaryStream->readByteArray(256)); //never throws
if($binaryStream->getOffset() < strlen($binaryStream->getData())){
$logger->error("Unexpected trailing data after 2D biome data");
}
}catch(BinaryDataException $e){
@ -625,10 +621,10 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$biomeArrays[$i] = clone $biomes3d;
}
}elseif(($maps3d = $this->db->get($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES)) !== false){
$binaryStream = new BinaryStream($maps3d);
$binaryStream = new ByteBufferReader($maps3d);
try{
$binaryStream->get(512);
$binaryStream->readByteArray(512);
$biomeArrays = self::deserialize3dBiomes($binaryStream, $chunkVersion, $logger);
}catch(BinaryDataException $e){
throw new CorruptedChunkException($e->getMessage(), 0, $e);
@ -771,26 +767,26 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
if($subChunk->isEmptyAuthoritative()){
$write->delete($key);
}else{
$subStream = new BinaryStream();
$subStream->putByte(self::CURRENT_LEVEL_SUBCHUNK_VERSION);
$subStream = new ByteBufferWriter();
Byte::writeUnsigned($subStream, self::CURRENT_LEVEL_SUBCHUNK_VERSION);
$layers = $subChunk->getBlockLayers();
$subStream->putByte(count($layers));
Byte::writeUnsigned($subStream, count($layers));
foreach($layers as $blocks){
$this->serializeBlockPalette($subStream, $blocks);
}
$write->put($key, $subStream->getBuffer());
$write->put($key, $subStream->getData());
}
}
}
if(($dirtyFlags & Chunk::DIRTY_FLAG_BIOMES) !== 0){
$write->delete($index . ChunkDataKey::HEIGHTMAP_AND_2D_BIOMES);
$stream = new BinaryStream();
$stream->put(str_repeat("\x00", 512)); //fake heightmap
$stream = new ByteBufferWriter();
$stream->writeByteArray(str_repeat("\x00", 512)); //fake heightmap
self::serialize3dBiomes($stream, $subChunks);
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getBuffer());
$write->put($index . ChunkDataKey::HEIGHTMAP_AND_3D_BIOMES, $stream->getData());
}
//TODO: use this properly

View File

@ -23,10 +23,12 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\region;
use pmmp\encoding\BE;
use pmmp\encoding\Byte;
use pmmp\encoding\ByteBufferReader;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\ChunkException;
use pocketmine\world\format\io\exception\CorruptedChunkException;
use function assert;
@ -156,20 +158,20 @@ class RegionLoader{
if($payload === false || strlen($payload) !== $bytesToRead){
throw new CorruptedChunkException("Corrupted chunk detected (unexpected EOF, truncated or non-padded chunk found)");
}
$stream = new BinaryStream($payload);
$stream = new ByteBufferReader($payload);
try{
$length = $stream->getInt();
$length = BE::readUnsignedInt($stream);
if($length <= 0){ //TODO: if we reached here, the locationTable probably needs updating
return null;
}
$compression = $stream->getByte();
$compression = Byte::readUnsigned($stream);
if($compression !== self::COMPRESSION_ZLIB && $compression !== self::COMPRESSION_GZIP){
throw new CorruptedChunkException("Invalid compression type (got $compression, expected " . self::COMPRESSION_ZLIB . " or " . self::COMPRESSION_GZIP . ")");
}
return $stream->get($length - 1); //length prefix includes the compression byte
return $stream->readByteArray($length - 1); //length prefix includes the compression byte
}catch(BinaryDataException $e){
throw new CorruptedChunkException("Corrupted chunk detected: " . $e->getMessage(), 0, $e);
}