use pocketmine\item\ItemIds; use pocketmine\math\Vector3; use pocketmine\nbt\NbtDataException; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\TreeRoot; use pocketmine\network\mcpe\protocol\PacketDecodeException; use pocketmine\network\mcpe\protocol\types\command\CommandOriginData; use pocketmine\network\mcpe\protocol\types\entity\Attribute; use pocketmine\network\mcpe\protocol\types\entity\BlockPosMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\ByteMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\CompoundTagMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\EntityLink; use pocketmine\network\mcpe\protocol\types\entity\FloatMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\IntMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\LongMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\MetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\ShortMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\StringMetadataProperty; use pocketmine\network\mcpe\protocol\types\entity\Vec3MetadataProperty; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\PersonaPieceTintColor; use pocketmine\network\mcpe\protocol\types\PersonaSkinPiece; use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient; use pocketmine\network\mcpe\protocol\types\SkinAnimation; use pocketmine\network\mcpe\protocol\types\SkinData; use pocketmine\network\mcpe\protocol\types\SkinImage; use pocketmine\network\mcpe\protocol\types\StructureEditorData; use pocketmine\network\mcpe\protocol\types\StructureSettings; use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryStream; use pocketmine\utils\UUID; use function count; use function strlen; class NetworkBinaryStream extends BinaryStream{ /** * @throws BinaryDataException */ public function getString() : string{ return $this->get($this->getUnsignedVarInt()); } public function putString(string $v) : void{ $this->putUnsignedVarInt(strlen($v)); $this->put($v); } /** * @throws BinaryDataException */ public function getUUID() : UUID{ //This is actually two little-endian longs: UUID Most followed by UUID Least $part1 = $this->getLInt(); $part0 = $this->getLInt(); $part3 = $this->getLInt(); $part2 = $this->getLInt(); return new UUID($part0, $part1, $part2, $part3); } public function putUUID(UUID $uuid) : void{ $this->putLInt($uuid->getPart(1)); $this->putLInt($uuid->getPart(0)); $this->putLInt($uuid->getPart(3)); $this->putLInt($uuid->getPart(2)); } public function getSkin() : SkinData{ $skinId = $this->getString(); $skinResourcePatch = $this->getString(); $skinData = $this->getSkinImage(); $animationCount = $this->getLInt(); $animations = []; for($i = 0; $i < $animationCount; ++$i){ $animations[] = new SkinAnimation( $skinImage = $this->getSkinImage(), $animationType = $this->getLInt(), $animationFrames = $this->getLFloat() ); } $capeData = $this->getSkinImage(); $geometryData = $this->getString(); $animationData = $this->getString(); $premium = $this->getBool(); $persona = $this->getBool(); $capeOnClassic = $this->getBool(); $capeId = $this->getString(); $fullSkinId = $this->getString(); $armSize = $this->getString(); $skinColor = $this->getString(); $personaPieceCount = $this->getLInt(); $personaPieces = []; for($i = 0; $i < $personaPieceCount; ++$i){ $personaPieces[] = new PersonaSkinPiece( $pieceId = $this->getString(), $pieceType = $this->getString(), $packId = $this->getString(), $isDefaultPiece = $this->getBool(), $productId = $this->getString() ); } $pieceTintColorCount = $this->getLInt(); $pieceTintColors = []; for($i = 0; $i < $pieceTintColorCount; ++$i){ $pieceType = $this->getString(); $colorCount = $this->getLInt(); $colors = []; for($j = 0; $j < $colorCount; ++$j){ $colors[] = $this->getString(); } $pieceTintColors[] = new PersonaPieceTintColor( $pieceType, $colors ); } return new SkinData($skinId, $skinResourcePatch, $skinData, $animations, $capeData, $geometryData, $animationData, $premium, $persona, $capeOnClassic, $capeId, $fullSkinId, $armSize, $skinColor, $personaPieces, $pieceTintColors); } public function putSkin(SkinData $skin): void{ $this->putString($skin->getSkinId()); $this->putString($skin->getResourcePatch()); $this->putSkinImage($skin->getSkinImage()); $this->putLInt(count($skin->getAnimations())); foreach($skin->getAnimations() as $animation){ $this->putSkinImage($animation->getImage()); $this->putLInt($animation->getType()); $this->putLFloat($animation->getFrames()); } $this->putSkinImage($skin->getCapeImage()); $this->putString($skin->getGeometryData()); $this->putString($skin->getAnimationData()); $this->putBool($skin->isPremium()); $this->putBool($skin->isPersona()); $this->putBool($skin->isPersonaCapeOnClassic()); $this->putString($skin->getCapeId()); $this->putString($skin->getFullSkinId()); $this->putString($skin->getArmSize()); $this->putString($skin->getSkinColor()); $this->putLInt(count($skin->getPersonaPieces())); foreach($skin->getPersonaPieces() as $piece){ $this->putString($piece->getPieceId()); $this->putString($piece->getPieceType()); $this->putString($piece->getPackId()); $this->putBool($piece->isDefaultPiece()); $this->putString($piece->getProductId()); } $this->putLInt(count($skin->getPieceTintColors())); foreach($skin->getPieceTintColors() as $tint){ $this->putString($tint->getPieceType()); $this->putLInt(count($tint->getColors())); foreach($tint->getColors() as $color){ $this->putString($color); } } } private function getSkinImage() : SkinImage{ $width = $this->getLInt(); $height = $this->getLInt(); $data = $this->getString(); try{ return new SkinImage($height, $width, $data); }catch(\InvalidArgumentException $e){ throw new PacketDecodeException($e->getMessage(), 0, $e); } } private function putSkinImage(SkinImage $image) : void{ $this->putLInt($image->getWidth()); $this->putLInt($image->getHeight()); $this->putString($image->getData()); } /** * @throws PacketDecodeException * @throws BinaryDataException */ public function getSlot() : ItemStack{ $id = $this->getVarInt(); if($id === 0){ return ItemStack::null(); } $auxValue = $this->getVarInt(); $meta = $auxValue >> 8; $count = $auxValue & 0xff; $nbtLen = $this->getLShort(); /** @var CompoundTag|null $compound */ $compound = null; if($nbtLen === 0xffff){ $c = $this->getByte(); if($c !== 1){ throw new PacketDecodeException("Unexpected NBT count $c"); } try{ $compound = (new NetworkNbtSerializer())->read($this->buffer, $this->offset, 512)->mustGetCompoundTag(); }catch(NbtDataException $e){ throw new PacketDecodeException($e->getMessage(), 0, $e); } }elseif($nbtLen !== 0){ throw new PacketDecodeException("Unexpected fake NBT length $nbtLen"); } $canPlaceOn = []; for($i = 0, $canPlaceOnCount = $this->getVarInt(); $i < $canPlaceOnCount; ++$i){ $canPlaceOn[] = $this->getString(); } $canDestroy = []; for($i = 0, $canDestroyCount = $this->getVarInt(); $i < $canDestroyCount; ++$i){ $canDestroy[] = $this->getString(); } $shieldBlockingTick = null; if($id === ItemIds::SHIELD){ $shieldBlockingTick = $this->getVarLong(); } return new ItemStack($id, $meta, $count, $compound, $canPlaceOn, $canDestroy, $shieldBlockingTick); } public function putSlot(ItemStack $item) : void{ if($item->getId() === 0){ $this->putVarInt(0); return; } $this->putVarInt($item->getId()); $auxValue = (($item->getMeta() & 0x7fff) << 8) | $item->getCount(); $this->putVarInt($auxValue); $nbt = $item->getNbt(); if($nbt !== null){ $this->putLShort(0xffff); $this->putByte(1); //TODO: some kind of count field? always 1 as of 1.9.0 $this->put((new NetworkNbtSerializer())->write(new TreeRoot($nbt))); }else{ $this->putLShort(0); } $this->putVarInt(count($item->getCanPlaceOn())); foreach($item->getCanPlaceOn() as $entry){ $this->putString($entry); } $this->putVarInt(count($item->getCanDestroy())); foreach($item->getCanDestroy() as $entry){ $this->putString($entry); } $blockingTick = $item->getShieldBlockingTick(); if($blockingTick !== null){ $this->putVarLong($blockingTick); } } public function getRecipeIngredient() : RecipeIngredient{ $id = $this->getVarInt(); if($id === 0){ return new RecipeIngredient(0, 0, 0); } $meta = $this->getVarInt(); $count = $this->getVarInt(); return new RecipeIngredient($id, $meta, $count); } public function putRecipeIngredient(RecipeIngredient $ingredient) : void{ if($ingredient->getId() === 0){ $this->putVarInt(0); }else{ $this->putVarInt($ingredient->getId()); $this->putVarInt($ingredient->getMeta()); $this->putVarInt($ingredient->getCount()); } } /** * Decodes entity metadata from the stream. * * @return MetadataProperty[] * @phpstan-return array * * @throws PacketDecodeException * @throws BinaryDataException */ public function getEntityMetadata() : array{ $count = $this->getUnsignedVarInt(); $data = []; for($i = 0; $i < $count; ++$i){ $key = $this->getUnsignedVarInt(); $type = $this->getUnsignedVarInt(); $data[$key] = $this->readMetadataProperty($type); } return $data; } private function readMetadataProperty(int $type) : MetadataProperty{ switch($type){ case ByteMetadataProperty::id(): return ByteMetadataProperty::read($this); case ShortMetadataProperty::id(): return ShortMetadataProperty::read($this); case IntMetadataProperty::id(): return IntMetadataProperty::read($this); case FloatMetadataProperty::id(): return FloatMetadataProperty::read($this); case StringMetadataProperty::id(): return StringMetadataProperty::read($this); case CompoundTagMetadataProperty::id(): return CompoundTagMetadataProperty::read($this); case BlockPosMetadataProperty::id(): return BlockPosMetadataProperty::read($this); case LongMetadataProperty::id(): return LongMetadataProperty::read($this); case Vec3MetadataProperty::id(): return Vec3MetadataProperty::read($this); default: throw new PacketDecodeException("Unknown entity metadata type " . $type); } } /** * Writes entity metadata to the packet buffer. * * @param MetadataProperty[] $metadata * @phpstan-param array $metadata */ public function putEntityMetadata(array $metadata) : void{ $this->putUnsignedVarInt(count($metadata)); foreach($metadata as $key => $d){ $this->putUnsignedVarInt($key); $this->putUnsignedVarInt($d::id()); $d->write($this); } } /** * Reads a list of Attributes from the stream. * @return Attribute[] * * @throws PacketDecodeException if reading an attribute with an unrecognized name * @throws BinaryDataException */ public function getAttributeList() : array{ $list = []; $count = $this->getUnsignedVarInt(); for($i = 0; $i < $count; ++$i){ $min = $this->getLFloat(); $max = $this->getLFloat(); $current = $this->getLFloat(); $default = $this->getLFloat(); $id = $this->getString(); $list[] = new Attribute($id, $min, $max, $current, $default); } return $list; } /** * Writes a list of Attributes to the packet buffer using the standard format. * * @param Attribute ...$attributes */ public function putAttributeList(Attribute ...$attributes) : void{ $this->putUnsignedVarInt(count($attributes)); foreach($attributes as $attribute){ $this->putLFloat($attribute->getMin()); $this->putLFloat($attribute->getMax()); $this->putLFloat($attribute->getCurrent()); $this->putLFloat($attribute->getDefault()); $this->putString($attribute->getId()); } } /** * Reads and returns an EntityUniqueID * * @throws BinaryDataException */ public function getEntityUniqueId() : int{ return $this->getVarLong(); } /** * Writes an EntityUniqueID */ public function putEntityUniqueId(int $eid) : void{ $this->putVarLong($eid); } /** * Reads and returns an EntityRuntimeID * * @throws BinaryDataException */ public function getEntityRuntimeId() : int{ return $this->getUnsignedVarLong(); } /** * Writes an EntityRuntimeID */ public function putEntityRuntimeId(int $eid) : void{ $this->putUnsignedVarLong($eid); } /** * Reads an block position with unsigned Y coordinate. * * @param int $x reference parameter * @param int $y reference parameter * @param int $z reference parameter * * @throws BinaryDataException */ public function getBlockPosition(&$x, &$y, &$z) : void{ $x = $this->getVarInt(); $y = $this->getUnsignedVarInt(); $z = $this->getVarInt(); } /** * Writes a block position with unsigned Y coordinate. */ public function putBlockPosition(int $x, int $y, int $z) : void{ $this->putVarInt($x); $this->putUnsignedVarInt($y); $this->putVarInt($z); } /** * Reads a block position with a signed Y coordinate. * * @param int $x reference parameter * @param int $y reference parameter * @param int $z reference parameter * * @throws BinaryDataException */ public function getSignedBlockPosition(&$x, &$y, &$z) : void{ $x = $this->getVarInt(); $y = $this->getVarInt(); $z = $this->getVarInt(); } /** * Writes a block position with a signed Y coordinate. */ public function putSignedBlockPosition(int $x, int $y, int $z) : void{ $this->putVarInt($x); $this->putVarInt($y); $this->putVarInt($z); } /** * Reads a floating-point Vector3 object with coordinates rounded to 4 decimal places. * * @throws BinaryDataException */ public function getVector3() : Vector3{ return new Vector3( $this->getRoundedLFloat(4), $this->getRoundedLFloat(4), $this->getRoundedLFloat(4) ); } /** * Writes a floating-point Vector3 object, or 3x zero if null is given. * * Note: ONLY use this where it is reasonable to allow not specifying the vector. * For all other purposes, use the non-nullable version. * * @see NetworkBinaryStream::putVector3() */ public function putVector3Nullable(?Vector3 $vector) : void{ if($vector !== null){ $this->putVector3($vector); }else{ $this->putLFloat(0.0); $this->putLFloat(0.0); $this->putLFloat(0.0); } } /** * Writes a floating-point Vector3 object */ public function putVector3(Vector3 $vector) : void{ $this->putLFloat($vector->x); $this->putLFloat($vector->y); $this->putLFloat($vector->z); } /** * @throws BinaryDataException */ public function getByteRotation() : float{ return ($this->getByte() * (360 / 256)); } public function putByteRotation(float $rotation) : void{ $this->putByte((int) ($rotation / (360 / 256))); } /** * Reads gamerules * TODO: implement this properly * * @return mixed[][], members are in the structure [name => [type, value]] * @phpstan-return array * * @throws PacketDecodeException * @throws BinaryDataException */ public function getGameRules() : array{ $count = $this->getUnsignedVarInt(); $rules = []; for($i = 0; $i < $count; ++$i){ $name = $this->getString(); $type = $this->getUnsignedVarInt(); $value = null; switch($type){ case 1: $value = $this->getBool(); break; case 2: $value = $this->getUnsignedVarInt(); break; case 3: $value = $this->getLFloat(); break; default: throw new PacketDecodeException("Unknown gamerule type $type"); } $rules[$name] = [$type, $value]; } return $rules; } /** * Writes a gamerule array, members should be in the structure [name => [type, value]] * TODO: implement this properly * * @param mixed[][] $rules * @phpstan-param array $rules */ public function putGameRules(array $rules) : void{ $this->putUnsignedVarInt(count($rules)); foreach($rules as $name => $rule){ $this->putString($name); $this->putUnsignedVarInt($rule[0]); switch($rule[0]){ case 1: $this->putBool($rule[1]); break; case 2: $this->putUnsignedVarInt($rule[1]); break; case 3: $this->putLFloat($rule[1]); break; default: throw new \InvalidArgumentException("Invalid gamerule type " . $rule[0]); } } } /** * @throws BinaryDataException */ public function getEntityLink() : EntityLink{ $link = new EntityLink(); $link->fromEntityUniqueId = $this->getEntityUniqueId(); $link->toEntityUniqueId = $this->getEntityUniqueId(); $link->type = $this->getByte(); $link->immediate = $this->getBool(); return $link; } public function putEntityLink(EntityLink $link) : void{ $this->putEntityUniqueId($link->fromEntityUniqueId); $this->putEntityUniqueId($link->toEntityUniqueId); $this->putByte($link->type); $this->putBool($link->immediate); } /** * @throws BinaryDataException */ public function getCommandOriginData() : CommandOriginData{ $result = new CommandOriginData(); $result->type = $this->getUnsignedVarInt(); $result->uuid = $this->getUUID(); $result->requestId = $this->getString(); if($result->type === CommandOriginData::ORIGIN_DEV_CONSOLE or $result->type === CommandOriginData::ORIGIN_TEST){ $result->varlong1 = $this->getVarLong(); } return $result; } public function putCommandOriginData(CommandOriginData $data) : void{ $this->putUnsignedVarInt($data->type); $this->putUUID($data->uuid); $this->putString($data->requestId); if($data->type === CommandOriginData::ORIGIN_DEV_CONSOLE or $data->type === CommandOriginData::ORIGIN_TEST){ $this->putVarLong($data->varlong1); } } public function getStructureSettings() : StructureSettings{ $result = new StructureSettings(); $result->paletteName = $this->getString(); $result->ignoreEntities = $this->getBool(); $result->ignoreBlocks = $this->getBool(); $this->getBlockPosition($result->structureSizeX, $result->structureSizeY, $result->structureSizeZ); $this->getBlockPosition($result->structureOffsetX, $result->structureOffsetY, $result->structureOffsetZ); $result->lastTouchedByPlayerID = $this->getEntityUniqueId(); $result->rotation = $this->getByte(); $result->mirror = $this->getByte(); $result->integrityValue = $this->getFloat(); $result->integritySeed = $this->getInt(); $result->pivot = $this->getVector3(); return $result; } public function putStructureSettings(StructureSettings $structureSettings) : void{ $this->putString($structureSettings->paletteName); $this->putBool($structureSettings->ignoreEntities); $this->putBool($structureSettings->ignoreBlocks); $this->putBlockPosition($structureSettings->structureSizeX, $structureSettings->structureSizeY, $structureSettings->structureSizeZ); $this->putBlockPosition($structureSettings->structureOffsetX, $structureSettings->structureOffsetY, $structureSettings->structureOffsetZ); $this->putEntityUniqueId($structureSettings->lastTouchedByPlayerID); $this->putByte($structureSettings->rotation); $this->putByte($structureSettings->mirror); $this->putFloat($structureSettings->integrityValue); $this->putInt($structureSettings->integritySeed); $this->putVector3($structureSettings->pivot); } public function getStructureEditorData() : StructureEditorData{ $result = new StructureEditorData(); $result->structureName = $this->getString(); $result->structureDataField = $this->getString(); $result->includePlayers = $this->getBool(); $result->showBoundingBox = $this->getBool(); $result->structureBlockType = $this->getVarInt(); $result->structureSettings = $this->getStructureSettings(); $result->structureRedstoneSaveMove = $this->getVarInt(); return $result; } public function putStructureEditorData(StructureEditorData $structureEditorData) : void{ $this->putString($structureEditorData->structureName); $this->putString($structureEditorData->structureDataField); $this->putBool($structureEditorData->includePlayers); $this->putBool($structureEditorData->showBoundingBox); $this->putVarInt($structureEditorData->structureBlockType); $this->putStructureSettings($structureEditorData->structureSettings); $this->putVarInt($structureEditorData->structureRedstoneSaveMove); } }