mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-09 11:16:57 +00:00
Compare commits
13 Commits
dependabot
...
ext-encodi
Author | SHA1 | Date | |
---|---|---|---|
1541cfa90e | |||
2f2c53067b | |||
1bc7cf340d | |||
26cd5c471c | |||
3999a1f9f4 | |||
925b34e5c6 | |||
fa5cc3301c | |||
b2d0be5b75 | |||
d3c36e6287 | |||
e569cc3275 | |||
c931437a30 | |||
dca9d3a010 | |||
9310c46ea1 |
Submodule build/php updated: 1d9cda6688...b839e5227b
@ -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
17
composer.lock
generated
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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()){
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user