├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock └── src ├── Deserializer ├── BedrockEditionNbtDeserializer.php ├── BedrockEditionNetworkNbtDeserializer.php ├── DeserializerFloatReadResult.php ├── DeserializerIntReadResult.php ├── DeserializerReadResult.php ├── DeserializerStringReadResult.php ├── JavaEditionNbtDeserializer.php └── NbtDeserializer.php ├── IO ├── Reader │ ├── AbstractReader.php │ ├── GZipCompressedStringReader.php │ ├── Reader.php │ ├── StringReader.php │ └── ZLibCompressedStringReader.php └── Writer │ ├── AbstractWriter.php │ ├── GZipCompressedStringWriter.php │ ├── StringWriter.php │ ├── Writer.php │ └── ZLibCompressedStringWriter.php ├── MachineByteOrder.php ├── NbtFormat.php ├── Serializer ├── BedrockEditionNbtSerializer.php ├── BedrockEditionNetworkNbtSerializer.php ├── JavaEditionNbtSerializer.php └── NbtSerializer.php ├── String ├── JavaEncoding.php └── StringDataFormatException.php └── Tag ├── ArrayValueTag.php ├── ByteArrayTag.php ├── ByteTag.php ├── CompoundTag.php ├── DoubleTag.php ├── EndTag.php ├── FloatTag.php ├── FloatValueTag.php ├── IntArrayTag.php ├── IntTag.php ├── IntValueTag.php ├── ListTag.php ├── LongArrayTag.php ├── LongTag.php ├── RawTagReadResult.php ├── RawValueTag.php ├── ShortTag.php ├── StringTag.php ├── Tag.php ├── TagOptions.php └── TagType.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.nbt 3 | *.dat 4 | test.php 5 | vendor/ 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2024 Aternos GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-nbt 2 | A full PHP implementation of Minecraft's Named Binary Tag (NBT) format. 3 | 4 | In contrast to other implementations, this library provides full support 5 | for 64-bit types, including the relatively new `TAG_Long_Array`. 6 | 7 | Additionally, all three flavors (Java Edition, Bedrock Edition/little endian, and Bedrock Edition/VarInt) of the NBT format are supported. 8 | 9 | ### Installation 10 | ```shell 11 | composer require aternos/nbt 12 | ``` 13 | 14 | ## Usage 15 | ### Reading NBT data 16 | To read existing NBT data, a Reader object is required. 17 | This library implements different readers to read NBT data from strings. 18 | ```php 19 | //Read uncompressed NBT data 20 | $reader = new \Aternos\Nbt\IO\Reader\StringReader("...nbtData...", \Aternos\Nbt\NbtFormat::BEDROCK_EDITION); 21 | 22 | //Read gzip compressed NBT data 23 | $gzipReader = new \Aternos\Nbt\IO\Reader\GZipCompressedStringReader("...compressedNbtData...", \Aternos\Nbt\NbtFormat::JAVA_EDITION); 24 | 25 | //Read zlib compressed NBT data 26 | $zlibReader = new \Aternos\Nbt\IO\Reader\ZLibCompressedStringReader("...compressedNbtData...", \Aternos\Nbt\NbtFormat::BEDROCK_EDITION_NETWORK); 27 | ``` 28 | Note that the reader object is also used to specify the NBT format flavor. 29 | Available are `\Aternos\Nbt\NbtFormat::JAVA_EDITION`, `\Aternos\Nbt\NbtFormat::BEDROCK_EDITION`, and `\Aternos\Nbt\NbtFormat::BEDROCK_EDITION_NETWORK`. 30 | 31 | More advanced readers can be created by implementing the `\Aternos\Nbt\IO\Reader\Reader` interface or by extending the `\Aternos\Nbt\IO\Reader\AbstractReader` class. 32 | 33 | A reader object can be used to load the NBT tag. 34 | ```php 35 | $reader = new \Aternos\Nbt\IO\Reader\StringReader("...nbtData...", \Aternos\Nbt\NbtFormat::BEDROCK_EDITION); 36 | 37 | $tag = \Aternos\Nbt\Tag\Tag::load($reader); 38 | ``` 39 | In theory, any type of NBT tag could be returned, but in reality all NBT files 40 | will start with either a compound tag or a list tag. 41 | 42 | ### Manipulating NBT structures 43 | Tag values of type `TAG_Byte`, `TAG_Short`, `TAG_Int`, `TAG_Long`, `TAG_Float`, 44 | `TAG_Double`, `TAG_String` can be accessed via their `getValue()` and `setValue()` functions. 45 | ```php 46 | $myInt new \Aternos\Nbt\Tag\IntTag(); 47 | 48 | $myInt->setValue(42); 49 | echo $myInt->getValue(); // 42 50 | ``` 51 | 52 | On String tags, `getValue()` and `setValue()` use the UTF-8 encoding and convert strings based on the selected NBT flavor 53 | when being serialized. 54 | 55 | 56 | Compound tags, list tags, and array tags implement the `ArrayAccess`, `Countable`, 57 | and `Iterator` interfaces and can therefore be accessed as arrays. 58 | ```php 59 | $myCompound = new \Aternos\Nbt\Tag\CompoundTag(); 60 | 61 | $myCompound["myInt"] = (new \Aternos\Nbt\Tag\IntTag())->setValue(42); 62 | $myCompound["myFloat"] = (new \Aternos\Nbt\Tag\IntTag())->setValue(42.42); 63 | echo count($myCompound); // 2 64 | 65 | //Manually setting a list's type is not strictly necessary, 66 | //since it's type will be set automatically when the first element is added 67 | $myList = (new \Aternos\Nbt\Tag\ListTag())->setContentTag(\Aternos\Nbt\Tag\TagType::TAG_String); 68 | 69 | $myList[] = (new \Aternos\Nbt\Tag\StringTag())->setValue("Hello"); 70 | $myList[] = (new \Aternos\Nbt\Tag\StringTag())->setValue("World"); 71 | ``` 72 | 73 | Alternatively, compound tags can be accessed using getter/setter functions. This is especially useful in combination with 74 | the new PHP null safe operator. 75 | ```php 76 | /** @var \Aternos\Nbt\Tag\CompoundTag $playerDat */ 77 | $playerDat = \Aternos\Nbt\Tag\Tag::load($reader); 78 | 79 | $playerDat->set("foo", (new \Aternos\Nbt\Tag\StringTag())->setValue("bar")); //Set a value 80 | $playerDat->delete("foo"); //Delete a value 81 | 82 | $playerName = $playerDat->getCompound("bukkit")?->getString("lastKnownName")?->getValue(); 83 | echo $playerName ?? "Unknown player name"; 84 | ``` 85 | 86 | ### Serializing NBT structures 87 | Similar to the reader object to read NBT data, a writer object is required 88 | to write NBT data. 89 | ```php 90 | //Write uncompressed NBT data 91 | $writer = (new \Aternos\Nbt\IO\Writer\StringWriter())->setFormat(\Aternos\Nbt\NbtFormat::BEDROCK_EDITION); 92 | 93 | //Write gzip compressed NBT data 94 | $gzipWriter = (new \Aternos\Nbt\IO\Writer\GZipCompressedStringWriter())->setFormat(\Aternos\Nbt\NbtFormat::JAVA_EDITION); 95 | 96 | //Write zlib compressed NBT data 97 | $gzipWriter = (new \Aternos\Nbt\IO\Writer\ZLibCompressedStringWriter())->setFormat(\Aternos\Nbt\NbtFormat::BEDROCK_EDITION_NETWORK); 98 | ``` 99 | The NBT flavor used by a writer object can differ from the one used by the 100 | reader object that was originally used to read the NBT structure. 101 | It is therefore possible to use this library to convert NBT structures between the different formats. 102 | 103 | More advanced writers can be created by implementing the `\Aternos\Nbt\IO\Writer\Writer` interface or by extending the `\Aternos\Nbt\IO\Writer\AbstractWriter` class. 104 | 105 | A writer object can be used to write/serialize an NBT structure. 106 | ```php 107 | $writer = (new \Aternos\Nbt\IO\Writer\StringWriter())->setFormat(\Aternos\Nbt\NbtFormat::BEDROCK_EDITION); 108 | 109 | $tag->write($writer); 110 | file_put_contents("data.nbt", $writer->getStringData()); 111 | ``` 112 | 113 | ### Bedrock Edition level.dat 114 | While the Bedrock Edition level.dat file is an uncompressed NBT file, 115 | its NBT data is prepended by two 32-bit little endian integers. 116 | 117 | The first one seems to be the version of the Bedrock Edition Storage Tool, 118 | which is also stored in the `StorageVersion` tag of the NBT structure. 119 | 120 | The second number is the size of the file's NBT structure (not including the two prepending integers). 121 | 122 | A Bedrock Edition level.dat file could be read like this: 123 | ```php 124 | $data = file_get_contents("level.dat"); 125 | 126 | $version = unpack("V", $data)[1]; 127 | $dataLength = unpack("V", $data, 4)[1]; 128 | 129 | if($dataLength !== strlen($data) - 8) { 130 | throw new Exception("Invalid level.dat data length"); 131 | } 132 | $tag = \Aternos\Nbt\Tag\Tag::load(new \Aternos\Nbt\IO\Reader\StringReader(substr($data, 8), \Aternos\Nbt\NbtFormat::BEDROCK_EDITION)); 133 | ``` 134 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aternos/nbt", 3 | "description": "PHP library to parse, modify, and create NBT objects", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Aternos\\Nbt\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Kurt Thiemann", 14 | "email": "kurt@aternos.org" 15 | } 16 | ], 17 | "require": { 18 | "pocketmine/binaryutils": "^0.2.1", 19 | "php": ">=8.1", 20 | "php-64bit": "*", 21 | "ext-zlib": "*", 22 | "ext-json": "*", 23 | "ext-mbstring": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "98a0214009234bb58b601d4cdb87f734", 8 | "packages": [ 9 | { 10 | "name": "pocketmine/binaryutils", 11 | "version": "0.2.4", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/pmmp/BinaryUtils.git", 15 | "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a", 20 | "reference": "5ac7eea91afbad8dc498f5ce34ce6297d5e6ea9a", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.4 || ^8.0", 25 | "php-64bit": "*" 26 | }, 27 | "require-dev": { 28 | "phpstan/extension-installer": "^1.0", 29 | "phpstan/phpstan": "1.3.0", 30 | "phpstan/phpstan-phpunit": "^1.0", 31 | "phpstan/phpstan-strict-rules": "^1.0.0", 32 | "phpunit/phpunit": "^9.5" 33 | }, 34 | "type": "library", 35 | "autoload": { 36 | "psr-4": { 37 | "pocketmine\\utils\\": "src/" 38 | } 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "LGPL-3.0" 43 | ], 44 | "description": "Classes and methods for conveniently handling binary data", 45 | "support": { 46 | "issues": "https://github.com/pmmp/BinaryUtils/issues", 47 | "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.4" 48 | }, 49 | "time": "2022-01-12T18:06:33+00:00" 50 | } 51 | ], 52 | "packages-dev": [], 53 | "aliases": [], 54 | "minimum-stability": "stable", 55 | "stability-flags": [], 56 | "prefer-stable": false, 57 | "prefer-lowest": false, 58 | "platform": { 59 | "php": ">=8.1", 60 | "php-64bit": "*", 61 | "ext-zlib": "*", 62 | "ext-json": "*", 63 | "ext-mbstring": "*" 64 | }, 65 | "platform-dev": [], 66 | "plugin-api-version": "2.6.0" 67 | } 68 | -------------------------------------------------------------------------------- /src/Deserializer/BedrockEditionNbtDeserializer.php: -------------------------------------------------------------------------------- 1 | getReader()->read(4); 26 | return new DeserializerIntReadResult(Binary::signInt(Binary::readLInt($raw)), $raw); 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function readStringLengthPrefix(): DeserializerIntReadResult 33 | { 34 | $raw = $this->getReader()->read(2); 35 | return new DeserializerIntReadResult(Binary::readLShort($raw), $raw); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function readByte(): DeserializerIntReadResult 42 | { 43 | $raw = $this->getReader()->read(1); 44 | return new DeserializerIntReadResult(Binary::readSignedByte($raw), $raw); 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function readShort(): DeserializerIntReadResult 51 | { 52 | $raw = $this->getReader()->read(2); 53 | return new DeserializerIntReadResult(Binary::readSignedLShort($raw), $raw); 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function readInt(): DeserializerIntReadResult 60 | { 61 | $raw = $this->getReader()->read(4); 62 | return new DeserializerIntReadResult(Binary::signInt(Binary::readLInt($raw)), $raw); 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function readLong(): DeserializerIntReadResult 69 | { 70 | $raw = $this->getReader()->read(8); 71 | $value = @unpack("q", MachineByteOrder::isBigEndian() ? strrev($raw) : $raw)[1] ?? 0; 72 | return new DeserializerIntReadResult($value, $raw); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function readFloat(): DeserializerFloatReadResult 79 | { 80 | $raw = $this->getReader()->read(4); 81 | return new DeserializerFloatReadResult(Binary::readLFloat($raw), $raw); 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | public function readDouble(): DeserializerFloatReadResult 88 | { 89 | $raw = $this->getReader()->read(8); 90 | return new DeserializerFloatReadResult(Binary::readLDouble($raw), $raw); 91 | } 92 | 93 | /** 94 | * @inheritDoc 95 | * @throws StringDataFormatException 96 | */ 97 | public function readString(): DeserializerStringReadResult 98 | { 99 | $length = $this->readStringLengthPrefix(); 100 | $val = $this->getReader()->read($length->getValue()); 101 | if(strlen($val) !== $length->getValue()){ 102 | throw new StringDataFormatException("Failed to read string: expected length " . $length->getValue() . ", got " . strlen($val)); 103 | } 104 | return new DeserializerStringReadResult($val, $length->getRawData() . $val); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Deserializer/BedrockEditionNetworkNbtDeserializer.php: -------------------------------------------------------------------------------- 1 | getReader(); 24 | $raw = $reader->read(5); 25 | $offset = 0; 26 | $value = Binary::readVarInt($raw, $offset); 27 | $reader->returnData(substr($raw, $offset)); 28 | return new DeserializerIntReadResult($value, substr($raw, 0, $offset)); 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public function readStringLengthPrefix(): DeserializerIntReadResult 35 | { 36 | $reader = $this->getReader(); 37 | $raw = $reader->read(5); 38 | $offset = 0; 39 | $value = Binary::readUnsignedVarInt($raw, $offset); 40 | $reader->returnData(substr($raw, $offset)); 41 | return new DeserializerIntReadResult($value, substr($raw, 0, $offset)); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function readInt(): DeserializerIntReadResult 48 | { 49 | $reader = $this->getReader(); 50 | $raw = $reader->read(5); 51 | $offset = 0; 52 | $value = Binary::readVarInt($raw, $offset); 53 | $reader->returnData(substr($raw, $offset)); 54 | return new DeserializerIntReadResult($value, substr($raw, 0, $offset)); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function readLong(): DeserializerIntReadResult 61 | { 62 | $reader = $this->getReader(); 63 | $raw = $reader->read(10); 64 | $offset = 0; 65 | $value = Binary::readVarLong($raw, $offset); 66 | $reader->returnData(substr($raw, $offset)); 67 | return new DeserializerIntReadResult($value, substr($raw, 0, $offset)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Deserializer/DeserializerFloatReadResult.php: -------------------------------------------------------------------------------- 1 | value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Deserializer/DeserializerIntReadResult.php: -------------------------------------------------------------------------------- 1 | value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Deserializer/DeserializerReadResult.php: -------------------------------------------------------------------------------- 1 | rawData; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Deserializer/DeserializerStringReadResult.php: -------------------------------------------------------------------------------- 1 | value; 18 | } 19 | 20 | /** 21 | * @return int 22 | */ 23 | public function getRawLength(): int 24 | { 25 | return strlen($this->getRawData()); 26 | } 27 | 28 | /** 29 | * @return int 30 | */ 31 | public function getLength(): int 32 | { 33 | return strlen($this->value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Deserializer/JavaEditionNbtDeserializer.php: -------------------------------------------------------------------------------- 1 | getReader()->read(4); 27 | return new DeserializerIntReadResult(Binary::signInt(Binary::readInt($raw)), $raw); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function readStringLengthPrefix(): DeserializerIntReadResult 34 | { 35 | $raw = $this->getReader()->read(2); 36 | return new DeserializerIntReadResult(Binary::readShort($raw), $raw); 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function readByte(): DeserializerIntReadResult 43 | { 44 | $raw = $this->getReader()->read(1); 45 | return new DeserializerIntReadResult(Binary::readSignedByte($raw), $raw); 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function readShort(): DeserializerIntReadResult 52 | { 53 | $raw = $this->getReader()->read(2); 54 | return new DeserializerIntReadResult(Binary::readSignedShort($raw), $raw); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function readInt(): DeserializerIntReadResult 61 | { 62 | $raw = $this->getReader()->read(4); 63 | return new DeserializerIntReadResult(Binary::signInt(Binary::readInt($raw)), $raw); 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | public function readLong(): DeserializerIntReadResult 70 | { 71 | $raw = $this->getReader()->read(8); 72 | $value = @unpack("q", MachineByteOrder::isLittleEndian() ? strrev($raw) : $raw)[1] ?? 0; 73 | return new DeserializerIntReadResult($value, $raw); 74 | } 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | public function readFloat(): DeserializerFloatReadResult 80 | { 81 | $raw = $this->getReader()->read(4); 82 | return new DeserializerFloatReadResult(Binary::readFloat($raw), $raw); 83 | } 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | public function readDouble(): DeserializerFloatReadResult 89 | { 90 | $raw = $this->getReader()->read(8); 91 | return new DeserializerFloatReadResult(Binary::readDouble($raw), $raw); 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | * @throws StringDataFormatException 97 | */ 98 | public function readString(): DeserializerStringReadResult 99 | { 100 | $length = $this->readStringLengthPrefix(); 101 | $val = $this->getReader()->read($length->getValue()); 102 | if(strlen($val) !== $length->getValue()){ 103 | throw new StringDataFormatException("Failed to read string: expected length " . $length->getValue() . ", got " . strlen($val)); 104 | } 105 | return new DeserializerStringReadResult(JavaEncoding::getInstance()->decode($val), $length->getRawData() . $val); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Deserializer/NbtDeserializer.php: -------------------------------------------------------------------------------- 1 | reader; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/IO/Reader/AbstractReader.php: -------------------------------------------------------------------------------- 1 | deserializer)) { 19 | $this->deserializer = NbtFormat::getDeserializer($this->getFormat(), $this); 20 | } 21 | return $this->deserializer; 22 | } 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getFormat(): int 28 | { 29 | return $this->format; 30 | } 31 | 32 | /** 33 | * @param int $format 34 | * @return $this 35 | */ 36 | public function setFormat(int $format): static 37 | { 38 | $this->format = $format; 39 | $this->deserializer = null; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/IO/Reader/GZipCompressedStringReader.php: -------------------------------------------------------------------------------- 1 | format = $format; 14 | $this->data = $data; 15 | $this->length = strlen($data); 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public function read(int $length): string 22 | { 23 | $length = min($length, $this->length - $this->ptr); 24 | $oldPtr = $this->ptr; 25 | $this->ptr += $length; 26 | return substr($this->data, $oldPtr, $length); 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function eof(): bool 33 | { 34 | return $this->ptr >= $this->length; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function tell(): int 41 | { 42 | return $this->ptr; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function getFormat(): int 49 | { 50 | return $this->format; 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | */ 56 | public function returnData(string $data): void 57 | { 58 | $this->ptr -= strlen($data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/IO/Reader/ZLibCompressedStringReader.php: -------------------------------------------------------------------------------- 1 | serializer)) { 19 | $this->serializer = NbtFormat::getSerializer($this->getFormat(), $this); 20 | } 21 | return $this->serializer; 22 | } 23 | 24 | /** 25 | * @return int 26 | */ 27 | public function getFormat(): int 28 | { 29 | return $this->format; 30 | } 31 | 32 | /** 33 | * @param int $format 34 | * @return $this 35 | */ 36 | public function setFormat(int $format): static 37 | { 38 | $this->format = $format; 39 | $this->serializer = null; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/IO/Writer/GZipCompressedStringWriter.php: -------------------------------------------------------------------------------- 1 | data .= $data; 15 | } 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | public function tell(): int 21 | { 22 | return strlen($this->data); 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getStringData(): string 29 | { 30 | return $this->data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/IO/Writer/Writer.php: -------------------------------------------------------------------------------- 1 | new BedrockEditionNbtDeserializer($reader), 33 | static::BEDROCK_EDITION_NETWORK => new BedrockEditionNetworkNbtDeserializer($reader), 34 | default => new JavaEditionNbtDeserializer($reader), 35 | }; 36 | } 37 | 38 | /** 39 | * Find the appropriate serializer for an NBT format 40 | * 41 | * @param int $type 42 | * @param Writer $writer 43 | * @return NbtSerializer 44 | */ 45 | public static function getSerializer(int $type, Writer $writer): NbtSerializer 46 | { 47 | return match ($type) { 48 | static::BEDROCK_EDITION => new BedrockEditionNbtSerializer($writer), 49 | static::BEDROCK_EDITION_NETWORK => new BedrockEditionNetworkNbtSerializer($writer), 50 | default => new JavaEditionNbtSerializer($writer), 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Serializer/BedrockEditionNbtSerializer.php: -------------------------------------------------------------------------------- 1 | getWriter()->write(Binary::writeLInt(Binary::unsignInt($value))); 25 | return $this; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function writeStringLengthPrefix(int $value): static 32 | { 33 | $this->getWriter()->write(Binary::writeLShort($value)); 34 | return $this; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function writeByte(int $value): static 41 | { 42 | $this->getWriter()->write(Binary::writeByte(Binary::unsignByte($value))); 43 | return $this; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function writeShort(int $value): static 50 | { 51 | $this->getWriter()->write(Binary::writeLShort(Binary::unsignShort($value))); 52 | return $this; 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function writeInt(int $value): static 59 | { 60 | $this->getWriter()->write(Binary::writeLInt(Binary::unsignInt($value))); 61 | return $this; 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function writeLong(int $value): static 68 | { 69 | $packed = pack("q", $value); 70 | $this->getWriter()->write(MachineByteOrder::isBigEndian() ? strrev($packed) : $packed); 71 | return $this; 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public function writeFloat(float $value): static 78 | { 79 | $this->getWriter()->write(Binary::writeLFloat($value)); 80 | return $this; 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function writeDouble(float $value): static 87 | { 88 | $this->getWriter()->write(Binary::writeLDouble($value)); 89 | return $this; 90 | } 91 | 92 | /** 93 | * @inheritDoc 94 | */ 95 | public function writeString(string $value): static 96 | { 97 | $this->writeStringLengthPrefix(strlen($value)); 98 | $this->getWriter()->write($value); 99 | return $this; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Serializer/BedrockEditionNetworkNbtSerializer.php: -------------------------------------------------------------------------------- 1 | getWriter()->write(Binary::writeVarInt($value)); 24 | return $this; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function writeStringLengthPrefix(int $value): static 31 | { 32 | $this->getWriter()->write(Binary::writeUnsignedVarInt($value)); 33 | return $this; 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | public function writeInt(int $value): static 40 | { 41 | $this->getWriter()->write(Binary::writeVarInt($value)); 42 | return $this; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function writeLong(int $value): static 49 | { 50 | $this->getWriter()->write(Binary::writeVarLong($value)); 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Serializer/JavaEditionNbtSerializer.php: -------------------------------------------------------------------------------- 1 | getWriter()->write(Binary::writeInt(Binary::unsignInt($value))); 26 | return $this; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function writeStringLengthPrefix(int $value): static 33 | { 34 | $this->getWriter()->write(Binary::writeShort($value)); 35 | return $this; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function writeByte(int $value): static 42 | { 43 | $this->getWriter()->write(Binary::writeByte(Binary::unsignByte($value))); 44 | return $this; 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function writeShort(int $value): static 51 | { 52 | $this->getWriter()->write(Binary::writeShort(Binary::unsignShort($value))); 53 | return $this; 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function writeInt(int $value): static 60 | { 61 | $this->getWriter()->write(Binary::writeInt(Binary::unsignInt($value))); 62 | return $this; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function writeLong(int $value): static 69 | { 70 | $packed = pack("q", $value); 71 | $this->getWriter()->write(MachineByteOrder::isLittleEndian() ? strrev($packed) : $packed); 72 | return $this; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function writeFloat(float $value): static 79 | { 80 | $this->getWriter()->write(Binary::writeFloat($value)); 81 | return $this; 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | public function writeDouble(float $value): static 88 | { 89 | $this->getWriter()->write(Binary::writeDouble($value)); 90 | return $this; 91 | } 92 | 93 | /** 94 | * @inheritDoc 95 | */ 96 | public function writeString(string $value): static 97 | { 98 | $encoded = JavaEncoding::getInstance()->encode($value); 99 | $this->writeStringLengthPrefix(strlen($encoded)); 100 | $this->getWriter()->write($encoded); 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Serializer/NbtSerializer.php: -------------------------------------------------------------------------------- 1 | writer; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/String/JavaEncoding.php: -------------------------------------------------------------------------------- 1 | > 0x06))); 51 | $result .= chr(0x80 | (0x3F & $c)); 52 | continue; 53 | } 54 | 55 | if($c <= 0xFFFF) { 56 | $result .= chr(0xE0 | (0x0F & ($c >> 0x0C))); 57 | $result .= chr(0x80 | (0x3F & ($c >> 0x06))); 58 | $result .= chr(0x80 | (0x3F & $c)); 59 | continue; 60 | } 61 | 62 | $result .= chr(0xED); 63 | $result .= chr(0xA0 | ((($c >> 0x10) & 0x0F) - 1)); 64 | $result .= chr(0x80 | (($c >> 0x0A) & 0x3f)); 65 | $result .= chr(0xED); 66 | $result .= chr(0xb0 | (($c >> 0x06) & 0x0f)); 67 | $result .= chr(0x80 | ($c & 0x3f)); 68 | } 69 | 70 | return $result; 71 | } 72 | 73 | /** 74 | * @throws StringDataFormatException 75 | */ 76 | public function decode(string $string, string $outputEncoding = "UTF-8"): string 77 | { 78 | $result = ""; 79 | for ($i = 0; $i < strlen($string); $i++) { 80 | $a = ord($string[$i]); 81 | 82 | if ($a === 0) { 83 | throw new StringDataFormatException("Invalid NULL byte in string"); 84 | } 85 | 86 | // Single byte character 87 | if (($a & 0b10000000) === 0b0) { 88 | $result .= mb_chr($a, $outputEncoding); 89 | continue; 90 | } 91 | 92 | $b = ord($string[++$i] ?? "\0"); 93 | 94 | // Two byte character 95 | if (($a & 0b11100000) === 0b11000000) { 96 | if (($b & 0b11000000) !== 0b10000000) { 97 | throw new StringDataFormatException("Invalid \"UTF-8\" sequence"); 98 | } 99 | 100 | $result .= mb_chr((($a & 0x1F) << 6) | ($b & 0x3F), $outputEncoding); 101 | continue; 102 | } 103 | 104 | $c = ord($string[++$i] ?? "\0"); 105 | 106 | // Maybe six byte character 107 | if ($a === 0b11101101 && ($b & 0b11110000) === 0b10100000 && ($c & 0b11000000) === 0b10000000) { 108 | $d = ord($string[$i + 1] ?? "\0"); 109 | $e = ord($string[$i + 2] ?? "\0"); 110 | $f = ord($string[$i + 3] ?? "\0"); 111 | 112 | // Six byte character 113 | if ($d === 0b11101101 && ($e & 0b11110000) === 0b10110000 && ($f & 0b11000000) === 0b10000000) { 114 | $result .= mb_chr(0x10000 | 115 | ($b & 0x0F) << 0x10 | 116 | ($c & 0x3F) << 0x0A | 117 | ($e & 0x0F) << 0x06 | 118 | ($f & 0x3F), $outputEncoding); 119 | 120 | $i += 3; 121 | continue; 122 | } 123 | } 124 | 125 | // Three byte character 126 | if (($a & 0b11110000) === 0b11100000) { 127 | if (($b & 0b11000000) !== 0b10000000 || ($c & 0b11000000) !== 0b10000000) { 128 | throw new StringDataFormatException("Invalid \"UTF-8\" sequence"); 129 | } 130 | 131 | $result .= mb_chr((($a & 0x0F) << 12) | (($b & 0x3F) << 6) | ($c & 0x3F), $outputEncoding); 132 | continue; 133 | } 134 | 135 | throw new StringDataFormatException("Invalid \"UTF-8\" sequence"); 136 | } 137 | return $result; 138 | } 139 | } -------------------------------------------------------------------------------- /src/String/StringDataFormatException.php: -------------------------------------------------------------------------------- 1 | count(); 24 | if ($count > 0x7fffffff) { 25 | throw new Exception("Array exceeds maximum length of " . 0x7fffffff . " entries"); 26 | } 27 | $writer->getSerializer()->writeLengthPrefix($count); 28 | $this->writeValues($writer); 29 | return $this; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | protected function readContent(Reader $reader): static 36 | { 37 | $length = $reader->getDeserializer()->readLengthPrefix()->getValue(); 38 | $this->valueArray = $this->readValues($reader, $length); 39 | return $this; 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 46 | { 47 | $length = $reader->getDeserializer()->readLengthPrefix(); 48 | $valueData = static::readValuesRaw($reader, $length->getValue()); 49 | return $length->getRawData() . $valueData; 50 | } 51 | 52 | /** 53 | * @param Writer $writer 54 | * @return string 55 | */ 56 | abstract protected function writeValues(Writer $writer): string; 57 | 58 | /** 59 | * @param Reader $reader 60 | * @param int $length 61 | * @return array 62 | */ 63 | abstract protected function readValues(Reader $reader, int $length): array; 64 | 65 | /** 66 | * @param Reader $reader 67 | * @param int $length 68 | * @return string 69 | */ 70 | protected static function readValuesRaw(Reader $reader, int $length): string 71 | { 72 | throw new BadMethodCallException("Not implemented"); 73 | } 74 | 75 | /** 76 | * @param $value 77 | * @return bool 78 | */ 79 | abstract protected function checkArrayValue($value): bool; 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | public function current(): mixed 85 | { 86 | return current($this->valueArray); 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function next(): void 93 | { 94 | next($this->valueArray); 95 | } 96 | 97 | /** 98 | * @inheritDoc 99 | */ 100 | public function key(): string|int|null 101 | { 102 | return key($this->valueArray); 103 | } 104 | 105 | /** 106 | * @inheritDoc 107 | */ 108 | public function valid(): bool 109 | { 110 | return current($this->valueArray) !== false; 111 | } 112 | 113 | /** 114 | * @inheritDoc 115 | */ 116 | public function rewind(): void 117 | { 118 | reset($this->valueArray); 119 | } 120 | 121 | /** 122 | * @inheritDoc 123 | */ 124 | public function offsetExists($offset): bool 125 | { 126 | return array_key_exists($offset, $this->valueArray); 127 | } 128 | 129 | /** 130 | * @inheritDoc 131 | */ 132 | public function offsetGet($offset): mixed 133 | { 134 | return $this->valueArray[$offset]; 135 | } 136 | 137 | /** 138 | * @inheritDoc 139 | * @throws Exception 140 | */ 141 | public function offsetSet($offset, $value): void 142 | { 143 | if (!$this->checkArrayValue($value)) { 144 | throw new Exception("Invalid array value"); 145 | } 146 | if (is_null($offset)) { 147 | $this->valueArray[] = $value; 148 | return; 149 | } 150 | if (!$this->checkArrayKey($offset)) { 151 | throw new Exception("Invalid array offset " . $offset); 152 | } 153 | $this->valueArray[$offset] = $value; 154 | } 155 | 156 | /** 157 | * @inheritDoc 158 | */ 159 | public function offsetUnset($offset): void 160 | { 161 | unset($this->valueArray[$offset]); 162 | } 163 | 164 | /** 165 | * @inheritDoc 166 | */ 167 | public function count(): int 168 | { 169 | return count($this->valueArray); 170 | } 171 | 172 | /** 173 | * @param $offset 174 | * @return bool 175 | */ 176 | abstract protected function checkArrayKey($offset): bool; 177 | 178 | /** 179 | * @inheritDoc 180 | */ 181 | protected function getValueString(): string 182 | { 183 | return $this->count() . " entr" . ($this->count() === 1 ? "y" : "ies") . "\n[\n" . 184 | $this->indent(implode(", \n", array_map(strval(...), $this->valueArray))) . 185 | "\n]"; 186 | } 187 | 188 | /** 189 | * @inheritDoc 190 | */ 191 | public function jsonSerialize(): array 192 | { 193 | return $this->valueArray; 194 | } 195 | 196 | /** 197 | * @inheritDoc 198 | */ 199 | public function equals(Tag $tag): bool 200 | { 201 | if ($tag === $this) { 202 | return true; 203 | } 204 | if (!$tag instanceof ArrayValueTag || $this->getType() !== $tag->getType() || count($tag) !== count($this)) { 205 | return false; 206 | } 207 | foreach ($this as $i => $val) { 208 | if ($val !== $tag[$i]) { 209 | return false; 210 | } 211 | } 212 | return true; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Tag/ByteArrayTag.php: -------------------------------------------------------------------------------- 1 | valueArray as $val) { 19 | $writer->getSerializer()->writeByte($val); 20 | } 21 | return $this; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | * @throws Exception 27 | */ 28 | protected function readValues(Reader $reader, int $length): array 29 | { 30 | $values = []; 31 | for ($i = 0; $i < $length; $i++) { 32 | $values[] = $reader->getDeserializer()->readByte()->getValue(); 33 | } 34 | return $values; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | protected static function readValuesRaw(Reader $reader, int $length): string 41 | { 42 | $result = ""; 43 | for ($i = 0; $i < $length; $i++) { 44 | $result .= $reader->getDeserializer()->readByte()->getRawData(); 45 | } 46 | return $result; 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | protected function checkArrayValue($value): bool 53 | { 54 | return is_int($value); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | protected function checkArrayKey($offset): bool 61 | { 62 | return is_int($offset); 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | protected function getValueString(): string 69 | { 70 | $values = array_map(function ($elem) { 71 | return str_pad(dechex($elem), 2, "0"); 72 | }, array_slice($this->valueArray, 0, 32)); 73 | if (count($this->valueArray) > 32) { 74 | $values[] = "..."; 75 | } 76 | return $this->count() . " byte" . ($this->count() === 1 ? "" : "s") . " [" . implode(" ", $values) . "]"; 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | public function toSNBT(): string 83 | { 84 | $values = []; 85 | foreach ($this->valueArray as $val) { 86 | $values[] = $val . "b"; 87 | } 88 | return "[B;" . implode(", ", $values) . "]"; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Tag/ByteTag.php: -------------------------------------------------------------------------------- 1 | getSerializer()->writeByte($this->value); 18 | return $this; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | protected function readContent(Reader $reader): static 25 | { 26 | $this->value = $reader->getDeserializer()->readByte()->getValue(); 27 | return $this; 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 34 | { 35 | return $reader->getDeserializer()->readByte()->getRawData(); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function toSNBT(): string 42 | { 43 | return $this->value . "b"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tag/CompoundTag.php: -------------------------------------------------------------------------------- 1 | rawContent !== null; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | * @throws Exception 34 | */ 35 | public function writeContent(Writer $writer): static 36 | { 37 | if($this->isRaw()) { 38 | if($this->rawContentFormat !== $writer->getFormat()) { 39 | throw new Exception("Cannot change format of raw compound tag"); 40 | } 41 | $writer->write($this->rawContent); 42 | return $this; 43 | } 44 | 45 | $writtenNames = []; 46 | foreach ($this->valueArray as $value) { 47 | if (in_array($value->getName(), $writtenNames)) { 48 | throw new Exception("Duplicate key '" . $value->getName() . "' in compound tag"); 49 | } 50 | $value->writeData($writer); 51 | } 52 | (new EndTag($this->options))->writeData($writer); 53 | return $this; 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | * @throws Exception 59 | */ 60 | protected function readContent(Reader $reader): static 61 | { 62 | if($this->options->shouldBeReadRaw($this)) { 63 | $this->rawContentFormat = $reader->getFormat(); 64 | $this->rawContent = static::readContentRaw($reader, $this->options); 65 | return $this; 66 | } 67 | while (!(($tag = Tag::load($reader, $this->options, $this)) instanceof EndTag)) { 68 | $this->valueArray[] = $tag->setParentTag($this); 69 | } 70 | return $this; 71 | } 72 | 73 | /** 74 | * @inheritDoc 75 | * @throws Exception 76 | */ 77 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 78 | { 79 | $result = ""; 80 | do { 81 | $tag = Tag::loadRaw($reader, $options); 82 | $result .= $tag->getData(); 83 | } while ($tag->getTagType() !== TagType::TAG_End); 84 | return $result; 85 | } 86 | 87 | /** 88 | * @param Tag $value 89 | * @inheritDoc 90 | * @throws Exception 91 | */ 92 | public function offsetSet($offset, $value): void 93 | { 94 | if (!($value instanceof Tag) || $value instanceof EndTag) { 95 | throw new Exception("Invalid CompoundTag value"); 96 | } 97 | if (!is_string($offset) && !is_null($offset)) { 98 | throw new Exception("Invalid CompoundTag key"); 99 | } 100 | if($this->isRaw()) { 101 | throw new Exception("Raw compound tags cannot be modified"); 102 | } 103 | if (is_null($offset) && is_null($value->getName())) { 104 | throw new Exception("Tags inside a CompoundTag must be named."); 105 | } 106 | if (!is_null($offset)) { 107 | $value->setName($offset); 108 | } else { 109 | $offset = $value->getName(); 110 | } 111 | $value->setParentTag($this); 112 | $this->offsetUnset($offset); 113 | $this->valueArray[] = $value; 114 | } 115 | 116 | /** 117 | * @inheritDoc 118 | */ 119 | public function current(): bool|Tag 120 | { 121 | return current($this->valueArray); 122 | } 123 | 124 | /** 125 | * @inheritDoc 126 | */ 127 | public function next(): void 128 | { 129 | next($this->valueArray); 130 | } 131 | 132 | /** 133 | * @inheritDoc 134 | */ 135 | public function key(): ?string 136 | { 137 | return $this->current()->getName(); 138 | } 139 | 140 | /** 141 | * @inheritDoc 142 | */ 143 | public function valid(): bool 144 | { 145 | return current($this->valueArray) !== false; 146 | } 147 | 148 | /** 149 | * @inheritDoc 150 | */ 151 | public function rewind(): void 152 | { 153 | reset($this->valueArray); 154 | } 155 | 156 | /** 157 | * @inheritDoc 158 | */ 159 | public function offsetExists($offset): bool 160 | { 161 | foreach ($this->valueArray as $val) { 162 | if ($val->getName() === $offset) { 163 | return true; 164 | } 165 | } 166 | return false; 167 | } 168 | 169 | /** 170 | * @inheritDoc 171 | */ 172 | public function offsetGet($offset): ?Tag 173 | { 174 | foreach ($this->valueArray as $val) { 175 | if ($val->getName() === $offset) { 176 | return $val; 177 | } 178 | } 179 | return null; 180 | } 181 | 182 | /** 183 | * @inheritDoc 184 | * @throws Exception 185 | */ 186 | public function offsetUnset($offset): void 187 | { 188 | if($this->isRaw()) { 189 | throw new Exception("Raw compound tags cannot be modified"); 190 | } 191 | foreach ($this->valueArray as $i => $val) { 192 | if ($val->getName() === $offset) { 193 | $val->setParentTag(null); 194 | unset($this->valueArray[$i]); 195 | break; 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * @inheritDoc 202 | */ 203 | public function count(): int 204 | { 205 | return count($this->valueArray); 206 | } 207 | 208 | /** 209 | * @inheritDoc 210 | */ 211 | protected function getValueString(): string 212 | { 213 | if($this->isRaw()) { 214 | return strlen($this->rawContent) . " bytes"; 215 | } 216 | return $this->count() . " entr" . ($this->count() === 1 ? "y" : "ies") . "\n{\n" . 217 | $this->indent(implode(", \n", array_map(strval(...), array_values($this->valueArray)))) . 218 | "\n}"; 219 | } 220 | 221 | /** 222 | * @inheritDoc 223 | */ 224 | public function jsonSerialize(): array 225 | { 226 | $data = []; 227 | foreach ($this->valueArray as $value) { 228 | $data[$value->getName()] = $value; 229 | } 230 | return $data; 231 | } 232 | 233 | /** 234 | * Set a child tag by name 235 | * If $name is null, the existing name of the tag object will be used 236 | * 237 | * @param string|null $name 238 | * @param Tag $tag 239 | * @return $this 240 | * @throws Exception 241 | */ 242 | public function set(?string $name, Tag $tag): static 243 | { 244 | $this->offsetSet($name, $tag); 245 | return $this; 246 | } 247 | 248 | /** 249 | * @param string $name 250 | * @return $this 251 | * @throws Exception 252 | */ 253 | public function delete(string $name): static 254 | { 255 | $this->offsetUnset($name); 256 | return $this; 257 | } 258 | 259 | /** 260 | * Get a child tag by name 261 | * 262 | * @param string $name 263 | * @return Tag|null 264 | */ 265 | public function get(string $name): ?Tag 266 | { 267 | return $this->offsetGet($name); 268 | } 269 | 270 | /** 271 | * @param string $name 272 | * @return ByteArrayTag|null 273 | */ 274 | public function getByteArray(string $name): ?ByteArrayTag 275 | { 276 | $tag = $this->get($name); 277 | return $tag instanceof ByteArrayTag ? $tag : null; 278 | } 279 | 280 | /** 281 | * @param string $name 282 | * @return ByteTag|null 283 | */ 284 | public function getByte(string $name): ?ByteTag 285 | { 286 | $tag = $this->get($name); 287 | return $tag instanceof ByteTag ? $tag : null; 288 | } 289 | 290 | /** 291 | * @param string $name 292 | * @return CompoundTag|null 293 | */ 294 | public function getCompound(string $name): ?CompoundTag 295 | { 296 | $tag = $this->get($name); 297 | return $tag instanceof CompoundTag ? $tag : null; 298 | } 299 | 300 | /** 301 | * @param string $name 302 | * @return DoubleTag|null 303 | */ 304 | public function getDouble(string $name): ?DoubleTag 305 | { 306 | $tag = $this->get($name); 307 | return $tag instanceof DoubleTag ? $tag : null; 308 | } 309 | 310 | /** 311 | * @param string $name 312 | * @return FloatTag|null 313 | */ 314 | public function getFloat(string $name): ?FloatTag 315 | { 316 | $tag = $this->get($name); 317 | return $tag instanceof FloatTag ? $tag : null; 318 | } 319 | 320 | /** 321 | * @param string $name 322 | * @return IntArrayTag|null 323 | */ 324 | public function getIntArray(string $name): ?IntArrayTag 325 | { 326 | $tag = $this->get($name); 327 | return $tag instanceof IntArrayTag ? $tag : null; 328 | } 329 | 330 | /** 331 | * @param string $name 332 | * @return IntTag|null 333 | */ 334 | public function getInt(string $name): ?IntTag 335 | { 336 | $tag = $this->get($name); 337 | return $tag instanceof IntTag ? $tag : null; 338 | } 339 | 340 | /** 341 | * @param string $name 342 | * @param int|null $listContentTag - Required content type for the list, if null, any type can be returned 343 | * @return ListTag|null 344 | */ 345 | public function getList(string $name, ?int $listContentTag = null): ?ListTag 346 | { 347 | $tag = $this->get($name); 348 | if (!$tag instanceof ListTag || ($listContentTag !== null && $tag->getContentTag() !== $listContentTag)) { 349 | return null; 350 | } 351 | return $tag; 352 | } 353 | 354 | /** 355 | * @param string $name 356 | * @return LongArrayTag|null 357 | */ 358 | public function getLongArray(string $name): ?LongArrayTag 359 | { 360 | $tag = $this->get($name); 361 | return $tag instanceof LongArrayTag ? $tag : null; 362 | } 363 | 364 | /** 365 | * @param string $name 366 | * @return LongTag|null 367 | */ 368 | public function getLong(string $name): ?LongTag 369 | { 370 | $tag = $this->get($name); 371 | return $tag instanceof LongTag ? $tag : null; 372 | } 373 | 374 | /** 375 | * @param string $name 376 | * @return ShortTag|null 377 | */ 378 | public function getShort(string $name): ?ShortTag 379 | { 380 | $tag = $this->get($name); 381 | return $tag instanceof ShortTag ? $tag : null; 382 | } 383 | 384 | /** 385 | * @param string $name 386 | * @return StringTag|null 387 | */ 388 | public function getString(string $name): ?StringTag 389 | { 390 | $tag = $this->get($name); 391 | return $tag instanceof StringTag ? $tag : null; 392 | } 393 | 394 | /** 395 | * @inheritDoc 396 | */ 397 | function equals(Tag $tag): bool 398 | { 399 | if ($tag === $this) { 400 | return true; 401 | } 402 | if (!$tag instanceof CompoundTag || $this->getType() !== $tag->getType() || count($tag) !== count($this)) { 403 | return false; 404 | } 405 | /** 406 | * @var string $key 407 | * @var Tag $val 408 | */ 409 | foreach ($this as $key => $val) { 410 | if (!isset($tag[$key]) || !$val->equals($tag[$key])) { 411 | return false; 412 | } 413 | } 414 | return true; 415 | } 416 | 417 | /** 418 | * @inheritDoc 419 | */ 420 | public function toSNBT(): string 421 | { 422 | $data = []; 423 | foreach ($this->valueArray as $value) { 424 | $data[] = StringTag::encodeSNBTString($value->getName()) . ": " . $value->toSNBT(); 425 | } 426 | return "{" . implode(", ", $data) . "}"; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/Tag/DoubleTag.php: -------------------------------------------------------------------------------- 1 | rawValueValid($writer->getFormat())) { 20 | $writer->write($this->rawValue); 21 | return $this; 22 | } 23 | $writer->getSerializer()->writeDouble($this->value); 24 | return $this; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected function readContent(Reader $reader): static 31 | { 32 | $result = $reader->getDeserializer()->readDouble(); 33 | $this->setRawDataFromSerializer($result, $reader->getFormat()); 34 | $this->value = $result->getValue(); 35 | return $this; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 42 | { 43 | return $reader->getDeserializer()->readDouble()->getRawData(); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function setValue(float $value): static 50 | { 51 | $this->resetRawValue(); 52 | return parent::setValue($value); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function toSNBT(): string 59 | { 60 | return $this->value . "d"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tag/EndTag.php: -------------------------------------------------------------------------------- 1 | getType() === $this->getType(); 66 | } 67 | 68 | /** 69 | * @inheritDoc 70 | */ 71 | public function toSNBT(): string 72 | { 73 | return ""; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Tag/FloatTag.php: -------------------------------------------------------------------------------- 1 | rawValueValid($writer->getFormat())) { 20 | $writer->write($this->rawValue); 21 | return $this; 22 | } 23 | $writer->getSerializer()->writeFloat($this->value); 24 | return $this; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected function readContent(Reader $reader): static 31 | { 32 | $result = $reader->getDeserializer()->readFloat(); 33 | $this->setRawDataFromSerializer($result, $reader->getFormat()); 34 | $this->value = $result->getValue(); 35 | return $this; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 42 | { 43 | return $reader->getDeserializer()->readFloat()->getRawData(); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function setValue(float $value): static 50 | { 51 | $this->resetRawValue(); 52 | return parent::setValue($value); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function toSNBT(): string 59 | { 60 | return $this->value . "f"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tag/FloatValueTag.php: -------------------------------------------------------------------------------- 1 | value; 15 | } 16 | 17 | /** 18 | * @param float $value 19 | * @return $this 20 | */ 21 | public function setValue(float $value): static 22 | { 23 | $this->value = $value; 24 | return $this; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected function getValueString(): string 31 | { 32 | return strval($this->value); 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function jsonSerialize(): int|float 39 | { 40 | return $this->value; 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function equals(Tag $tag): bool 47 | { 48 | return $tag instanceof FloatValueTag && $this->getType() === $tag->getType() && 49 | $tag->getValue() === $this->getValue(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tag/IntArrayTag.php: -------------------------------------------------------------------------------- 1 | valueArray as $value) { 18 | $writer->getSerializer()->writeInt($value); 19 | } 20 | return $this; 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | protected function readValues(Reader $reader, int $length): array 27 | { 28 | $values = []; 29 | for ($i = 0; $i < $length; $i++) { 30 | $values[] = $reader->getDeserializer()->readInt()->getValue(); 31 | } 32 | return $values; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | protected static function readValuesRaw(Reader $reader, int $length): string 39 | { 40 | $result = ""; 41 | for ($i = 0; $i < $length; $i++) { 42 | $result .= $reader->getDeserializer()->readInt()->getRawData(); 43 | } 44 | return $result; 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | protected function checkArrayValue($value): bool 51 | { 52 | return is_int($value); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | protected function checkArrayKey($offset): bool 59 | { 60 | return is_int($offset); 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function toSNBT(): string 67 | { 68 | $values = []; 69 | foreach ($this->valueArray as $val) { 70 | $values[] = $val; 71 | } 72 | return "[I;" . implode(", ", $values) . "]"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Tag/IntTag.php: -------------------------------------------------------------------------------- 1 | getSerializer()->writeInt($this->value); 18 | return $this; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | protected function readContent(Reader $reader): static 25 | { 26 | $this->value = $reader->getDeserializer()->readInt()->getValue(); 27 | return $this; 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 34 | { 35 | return $reader->getDeserializer()->readInt()->getRawData(); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function toSNBT(): string 42 | { 43 | return strval($this->value); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tag/IntValueTag.php: -------------------------------------------------------------------------------- 1 | value; 15 | } 16 | 17 | /** 18 | * @param int $value 19 | * @return $this 20 | */ 21 | public function setValue(int $value): static 22 | { 23 | $this->value = $value; 24 | return $this; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected function getValueString(): string 31 | { 32 | return strval($this->value); 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function jsonSerialize(): int 39 | { 40 | return $this->value; 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function equals(Tag $tag): bool 47 | { 48 | return $tag instanceof IntValueTag && $this->getType() === $tag->getType() && 49 | $tag->getValue() === $this->getValue(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tag/ListTag.php: -------------------------------------------------------------------------------- 1 | getSerializer()->writeByte($this->contentTagType); 24 | if ($this->isRaw()) { 25 | if($this->rawContentFormat !== $writer->getFormat()) { 26 | throw new Exception("Cannot change format of raw list tag"); 27 | } 28 | 29 | $writer->getSerializer()->writeLengthPrefix($this->rawContentLength); 30 | $writer->write($this->rawContent); 31 | } else { 32 | $writer->getSerializer()->writeLengthPrefix($this->count()); 33 | $this->writeValues($writer); 34 | } 35 | return $this; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | protected function writeValues(Writer $writer): string 42 | { 43 | /** @var Tag $value */ 44 | foreach ($this->valueArray as $value) { 45 | $value->writeContent($writer); 46 | } 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getContentTag(): int 54 | { 55 | return $this->contentTagType; 56 | } 57 | 58 | /** 59 | * @param int $contentTagType 60 | * @return $this 61 | * @throws Exception 62 | */ 63 | public function setContentTag(int $contentTagType): static 64 | { 65 | if ($this->isRaw()) { 66 | throw new Exception("Raw list tags cannot be modified"); 67 | } 68 | 69 | /** @var Tag $value */ 70 | foreach ($this->valueArray as $value) { 71 | if ($value::TYPE !== $contentTagType) { 72 | throw new Exception("New list content type is incompatible with its values"); 73 | } 74 | } 75 | $this->contentTagType = $contentTagType; 76 | return $this; 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | * @throws Exception 82 | */ 83 | protected function readValues(Reader $reader, int $length): array 84 | { 85 | $values = []; 86 | /** @var class-string|null $tagClass */ 87 | $tagClass = Tag::getTagClass($this->contentTagType); 88 | if (is_null($tagClass)) { 89 | throw new Exception("Unknown ListTag content type " . $this->contentTagType); 90 | } 91 | for ($i = 0; $i < $length; $i++) { 92 | $values[] = (new $tagClass($this->options))->setParentTag($this)->read($reader, false); 93 | } 94 | return $values; 95 | } 96 | 97 | /** 98 | * @inheritDoc 99 | */ 100 | protected function checkArrayValue($value): bool 101 | { 102 | if (!($value instanceof Tag)) { 103 | return false; 104 | } 105 | if ($this->count() === 0 && $this->contentTagType === TagType::TAG_End) { 106 | $this->contentTagType = $value::TYPE; 107 | return true; 108 | } 109 | return $value::TYPE === $this->contentTagType; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | * @throws Exception 115 | */ 116 | protected function readContent(Reader $reader): static 117 | { 118 | $this->contentTagType = $reader->getDeserializer()->readByte()->getValue(); 119 | $length = $reader->getDeserializer()->readLengthPrefix()->getValue(); 120 | $maxLength = $this->options->getMaxListTagLength(); 121 | if ($maxLength !== null && $length > $maxLength) { 122 | $this->rawContentFormat = $reader->getFormat(); 123 | $this->rawContentLength = $length; 124 | $this->rawContent = static::readValueTagsRaw($reader, $this->options, $this->contentTagType, $length); 125 | return $this; 126 | } 127 | $this->valueArray = $this->readValues($reader, $length); 128 | return $this; 129 | } 130 | 131 | /** 132 | * @inheritDoc 133 | * @throws Exception 134 | */ 135 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 136 | { 137 | $contentTagType = $reader->getDeserializer()->readByte(); 138 | $length = $reader->getDeserializer()->readLengthPrefix(); 139 | 140 | return $contentTagType->getRawData() . $length->getRawData() . 141 | static::readValueTagsRaw($reader, $options, $contentTagType->getValue(), $length->getValue()); 142 | } 143 | 144 | /** 145 | * @param Reader $reader 146 | * @param TagOptions $options 147 | * @param int $contentType 148 | * @param int $length 149 | * @return string 150 | * @throws Exception 151 | */ 152 | protected static function readValueTagsRaw(Reader $reader, TagOptions $options, int $contentType, int $length): string 153 | { 154 | $valueData = ""; 155 | 156 | /** @var class-string|null $tagClass */ 157 | $tagClass = Tag::getTagClass($contentType); 158 | if (is_null($tagClass)) { 159 | throw new Exception("Unknown ListTag content type " . $contentType); 160 | } 161 | for ($i = 0; $i < $length; $i++) { 162 | $valueData .= $tagClass::readRaw($reader, $options, false); 163 | } 164 | 165 | return $valueData; 166 | } 167 | 168 | /** 169 | * @return bool 170 | */ 171 | public function isRaw(): bool 172 | { 173 | return $this->rawContent !== null; 174 | } 175 | 176 | /** 177 | * @inheritDoc 178 | */ 179 | protected function checkArrayKey($offset): bool 180 | { 181 | return is_int($offset); 182 | } 183 | 184 | /** 185 | * @inheritDoc 186 | */ 187 | protected function getTagTypeString(): string 188 | { 189 | return parent::getTagTypeString() . "<" . TagType::NAMES[$this->contentTagType] . ">"; 190 | } 191 | 192 | /** 193 | * @inheritDoc 194 | */ 195 | public function offsetSet($offset, $value): void 196 | { 197 | if ($this->isRaw()) { 198 | throw new Exception("Raw list tags cannot be modified"); 199 | } 200 | 201 | /** @var Tag|null $previousValue */ 202 | $previousValue = $this->valueArray[$offset] ?? null; 203 | parent::offsetSet($offset, $value); 204 | $value->setParentTag($this); 205 | $previousValue?->setParentTag(null); 206 | } 207 | 208 | /** 209 | * @inheritDoc 210 | * @throws Exception 211 | */ 212 | public function offsetUnset($offset): void 213 | { 214 | if ($this->isRaw()) { 215 | throw new Exception("Raw list tags cannot be modified"); 216 | } 217 | 218 | /** @var Tag|null $previousValue */ 219 | $previousValue = $this->valueArray[$offset] ?? null; 220 | $previousValue?->setParentTag(null); 221 | parent::offsetUnset($offset); 222 | } 223 | 224 | /** 225 | * @inheritDoc 226 | */ 227 | public function equals(Tag $tag): bool 228 | { 229 | if ($tag === $this) { 230 | return true; 231 | } 232 | if (!$tag instanceof ListTag || $this->getType() !== $tag->getType() || 233 | $this->getContentTag() !== $tag->getContentTag() || count($tag) !== count($this)) { 234 | return false; 235 | } 236 | /** 237 | * @var int $i 238 | * @var Tag $val 239 | */ 240 | foreach ($this as $i => $val) { 241 | if (!$val->equals($tag[$i])) { 242 | return false; 243 | } 244 | } 245 | return true; 246 | } 247 | 248 | /** 249 | * @inheritDoc 250 | */ 251 | protected function getValueString(): string 252 | { 253 | if ($this->isRaw()) { 254 | return strlen($this->rawContent) . " bytes"; 255 | } 256 | return parent::getValueString(); 257 | } 258 | 259 | /** 260 | * @inheritDoc 261 | */ 262 | public function toSNBT(): string 263 | { 264 | $values = []; 265 | foreach ($this->valueArray as $value) { 266 | $values[] = $value->toSNBT(); 267 | } 268 | return "[" . implode(", ", $values) . "]"; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Tag/LongArrayTag.php: -------------------------------------------------------------------------------- 1 | rawValueValid($writer->getFormat())) { 20 | $writer->write($this->rawValue); 21 | return $this; 22 | } 23 | foreach ($this->valueArray as $value) { 24 | $writer->getSerializer()->writeLong($value); 25 | } 26 | return $this; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | protected function readValues(Reader $reader, int $length): array 33 | { 34 | $raw = ""; 35 | $values = []; 36 | for ($i = 0; $i < $length; $i++) { 37 | $res = $reader->getDeserializer()->readLong(); 38 | $values[] = $res->getValue(); 39 | $raw .= $res->getRawData(); 40 | } 41 | $this->rawValue = $raw; 42 | $this->rawValueType = $reader->getFormat(); 43 | return $values; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | protected static function readValuesRaw(Reader $reader, int $length): string 50 | { 51 | $raw = ""; 52 | for ($i = 0; $i < $length; $i++) { 53 | $raw = $reader->getDeserializer()->readLong()->getRawData(); 54 | } 55 | return $raw; 56 | } 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | protected function checkArrayValue($value): bool 62 | { 63 | return is_int($value); 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | public function offsetSet($offset, $value): void 70 | { 71 | $this->resetRawValue(); 72 | parent::offsetSet($offset, $value); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function offsetUnset($offset): void 79 | { 80 | $this->resetRawValue(); 81 | parent::offsetUnset($offset); 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | protected function checkArrayKey($offset): bool 88 | { 89 | return is_int($offset); 90 | } 91 | 92 | /** 93 | * @inheritDoc 94 | */ 95 | public function toSNBT(): string 96 | { 97 | $values = []; 98 | foreach ($this->valueArray as $val) { 99 | $values[] = $val . "L"; 100 | } 101 | return "[L;" . implode(", ", $values) . "]"; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Tag/LongTag.php: -------------------------------------------------------------------------------- 1 | rawValueValid($writer->getFormat())) { 22 | $writer->write($this->rawValue); 23 | return $this; 24 | } 25 | $writer->getSerializer()->writeLong($this->value); 26 | return $this; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | protected function readContent(Reader $reader): static 33 | { 34 | $result = $reader->getDeserializer()->readLong(); 35 | $this->setRawDataFromSerializer($result, $reader->getFormat()); 36 | $this->value = $result->getValue(); 37 | return $this; 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 44 | { 45 | return $reader->getDeserializer()->readLong()->getRawData(); 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function setValue(int $value): static 52 | { 53 | $this->resetRawValue(); 54 | return parent::setValue($value); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function toSNBT(): string 61 | { 62 | return $this->value . "L"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Tag/RawTagReadResult.php: -------------------------------------------------------------------------------- 1 | data; 17 | } 18 | 19 | /** 20 | * @return int 21 | */ 22 | public function getTagType(): int 23 | { 24 | return $this->tagType; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tag/RawValueTag.php: -------------------------------------------------------------------------------- 1 | rawValue; 18 | } 19 | 20 | /** 21 | * @param string|null $rawValue 22 | */ 23 | public function setRawValue(?string $rawValue): void 24 | { 25 | $this->rawValueType = null; 26 | $this->rawValue = $rawValue; 27 | } 28 | 29 | protected function resetRawValue(): void 30 | { 31 | $this->rawValue = null; 32 | $this->rawValueType = null; 33 | } 34 | 35 | /** 36 | * @param int $format 37 | * @return bool 38 | */ 39 | protected function rawValueValid(int $format): bool 40 | { 41 | return !is_null($this->rawValue) && (is_null($this->rawValueType) || $this->rawValueType === $format); 42 | } 43 | 44 | /** 45 | * @param DeserializerReadResult $result 46 | * @param int $format 47 | * @return void 48 | */ 49 | protected function setRawDataFromSerializer(DeserializerReadResult $result, int $format): void 50 | { 51 | $this->rawValue = $result->getRawData(); 52 | $this->rawValueType = $format; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Tag/ShortTag.php: -------------------------------------------------------------------------------- 1 | getSerializer()->writeShort($this->value); 18 | return $this; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | protected function readContent(Reader $reader): static 25 | { 26 | $this->value = $reader->getDeserializer()->readShort()->getValue(); 27 | return $this; 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 34 | { 35 | return $reader->getDeserializer()->readShort()->getRawData(); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function toSNBT(): string 42 | { 43 | return $this->value . "s"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tag/StringTag.php: -------------------------------------------------------------------------------- 1 | value; 47 | } 48 | 49 | /** 50 | * @param string $value 51 | * @return $this 52 | */ 53 | public function setValue(string $value): static 54 | { 55 | $this->value = $value; 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public function getLength(): int 63 | { 64 | return strlen($this->value); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | * @throws Exception 70 | */ 71 | public function writeContent(Writer $writer): static 72 | { 73 | $length = strlen($this->value); 74 | if ($length > 0xffff) { 75 | throw new Exception("String exceeds maximum length of " . 0xffff . " characters"); 76 | } 77 | $writer->getSerializer()->writeString($this->value); 78 | return $this; 79 | } 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | protected function readContent(Reader $reader): static 85 | { 86 | $this->value = $reader->getDeserializer()->readString()->getValue(); 87 | return $this; 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 94 | { 95 | $length = $reader->getDeserializer()->readStringLengthPrefix(); 96 | return $length->getRawData() . $reader->read($length->getValue()); 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | protected function getValueString(): string 103 | { 104 | return "'" . str_replace("\n", "\\n", $this->value) . "'"; 105 | } 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | public function jsonSerialize(): string 111 | { 112 | return $this->value; 113 | } 114 | 115 | /** 116 | * @inheritDoc 117 | */ 118 | public function equals(Tag $tag): bool 119 | { 120 | return $tag instanceof StringTag && $this->getType() === $tag->getType() && 121 | $tag->getValue() === $this->getValue(); 122 | } 123 | 124 | /** 125 | * @inheritDoc 126 | */ 127 | public function toSNBT(): string 128 | { 129 | return static::encodeSNBTString($this->value); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Tag/Tag.php: -------------------------------------------------------------------------------- 1 | [] 18 | */ 19 | public const TAGS = [ 20 | TagType::TAG_End => EndTag::class, 21 | TagType::TAG_Byte => ByteTag::class, 22 | TagType::TAG_Short => ShortTag::class, 23 | TagType::TAG_Int => IntTag::class, 24 | TagType::TAG_Long => LongTag::class, 25 | TagType::TAG_Float => FloatTag::class, 26 | TagType::TAG_Double => DoubleTag::class, 27 | TagType::TAG_Byte_Array => ByteArrayTag::class, 28 | TagType::TAG_String => StringTag::class, 29 | TagType::TAG_List => ListTag::class, 30 | TagType::TAG_Compound => CompoundTag::class, 31 | TagType::TAG_Int_Array => IntArrayTag::class, 32 | TagType::TAG_Long_Array => LongArrayTag::class 33 | ]; 34 | 35 | protected ?string $name = null; 36 | protected bool $isBeingSerialized = false; 37 | protected ?Tag $parentTag = null; 38 | protected TagOptions $options; 39 | 40 | /** 41 | * @param TagOptions|null $options 42 | */ 43 | public function __construct(?TagOptions $options = null) 44 | { 45 | $this->options = $options ?: new TagOptions(); 46 | } 47 | 48 | /** 49 | * @param string|null $name 50 | * @return $this 51 | */ 52 | public function setName(?string $name): static 53 | { 54 | $this->name = $name; 55 | return $this; 56 | } 57 | 58 | /** 59 | * @return string|null 60 | */ 61 | public function getName(): ?string 62 | { 63 | return $this->name; 64 | } 65 | 66 | /** 67 | * @param Tag|null $parentTag 68 | * @return $this 69 | */ 70 | public function setParentTag(?Tag $parentTag): static 71 | { 72 | $this->parentTag = $parentTag; 73 | return $this; 74 | } 75 | 76 | /** 77 | * @return Tag|null 78 | */ 79 | public function getParentTag(): ?Tag 80 | { 81 | return $this->parentTag; 82 | } 83 | 84 | /** 85 | * @return array 86 | * @throws Exception 87 | */ 88 | public function getPath(): array 89 | { 90 | if ($this->isBeingSerialized) { 91 | throw new Exception("Failed to resolve path: Circular NBT structure detected"); 92 | } 93 | $this->isBeingSerialized = true; 94 | if($this->parentTag) { 95 | $path = [...$this->parentTag->getPath(), $this->getName()]; 96 | }else { 97 | $path = [$this->getName()]; 98 | } 99 | $this->isBeingSerialized = false; 100 | return $path; 101 | } 102 | 103 | /** 104 | * @return string|null 105 | */ 106 | public function getStringPath(): ?string 107 | { 108 | try { 109 | $path = $this->getPath(); 110 | }catch (Exception) { 111 | return null; 112 | } 113 | return implode("/", $path); 114 | } 115 | 116 | /** 117 | * @return bool 118 | */ 119 | public static function canBeNamed(): bool 120 | { 121 | return true; 122 | } 123 | 124 | /** 125 | * @param Writer $writer 126 | * @return $this 127 | */ 128 | abstract public function writeContent(Writer $writer): static; 129 | 130 | /** 131 | * @param Reader $reader 132 | * @return $this 133 | */ 134 | abstract protected function readContent(Reader $reader): static; 135 | 136 | /** 137 | * @param Reader $reader 138 | * @param TagOptions $options 139 | * @return string 140 | */ 141 | protected static function readContentRaw(Reader $reader, TagOptions $options): string 142 | { 143 | throw new BadMethodCallException("Not implemented"); 144 | } 145 | 146 | /** 147 | * @return int 148 | */ 149 | public function getType(): int 150 | { 151 | return static::TYPE; 152 | } 153 | 154 | /** 155 | * Read tag name and payload 156 | * 157 | * @param Reader $reader 158 | * @param bool $named 159 | * @return $this 160 | * @throws Exception 161 | */ 162 | public function read(Reader $reader, bool $named = true): static 163 | { 164 | if ($named && static::canBeNamed()) { 165 | $name = $reader->getDeserializer()->readString(); 166 | $this->setName($name->getValue()); 167 | } 168 | return $this->readContent($reader); 169 | } 170 | 171 | /** 172 | * @param Reader $reader 173 | * @param TagOptions $options 174 | * @param bool $named 175 | * @return string 176 | * @throws Exception 177 | */ 178 | public static function readRaw(Reader $reader, TagOptions $options, bool $named = true): string 179 | { 180 | $result = ""; 181 | if ($named && static::canBeNamed()) { 182 | $name = $reader->getDeserializer()->readString(); 183 | $result .= $name->getRawData(); 184 | } 185 | $result .= static::readContentRaw($reader, $options); 186 | return $result; 187 | } 188 | 189 | /** 190 | * @param Writer $writer 191 | * @param bool $named 192 | * @return $this 193 | * @throws Exception 194 | */ 195 | public function writeData(Writer $writer, bool $named = true): static 196 | { 197 | if ($this->isBeingSerialized) { 198 | throw new Exception("Failed to serialize: Circular NBT structure detected"); 199 | } 200 | $this->isBeingSerialized = true; 201 | $writer->write(pack("C", static::TYPE & 0xff)); 202 | $serializer = $writer->getSerializer(); 203 | if ($named && static::canBeNamed()) { 204 | $name = $this->getName(); 205 | if (is_null($name)) { 206 | throw new Exception("Cannot write named tag, because tag does not have a name value"); 207 | } 208 | $serializer->writeString($this->getName()); 209 | } 210 | $this->writeContent($writer); 211 | $this->isBeingSerialized = false; 212 | return $this; 213 | } 214 | 215 | /** 216 | * @param Writer $writer 217 | * @return $this 218 | * @throws Exception 219 | */ 220 | public function write(Writer $writer): static 221 | { 222 | if (!($this instanceof CompoundTag) && 223 | !(in_array($writer->getFormat(), [NbtFormat::BEDROCK_EDITION_NETWORK, NbtFormat::BEDROCK_EDITION]) && 224 | $this instanceof ListTag)) { 225 | throw new Exception("NBT files must start with a CompoundTag (or ListTag for Minecraft Bedrock Edition)"); 226 | } 227 | if ($this->getName() === null) { 228 | $this->setName(""); 229 | } 230 | $this->writeData($writer); 231 | return $this; 232 | } 233 | 234 | /** 235 | * @param string $str 236 | * @param int $width 237 | * @return string 238 | */ 239 | protected function indent(string $str, int $width = 2): string 240 | { 241 | return str_repeat(" ", $width) . str_replace("\n", "\n ", $str); 242 | } 243 | 244 | /** 245 | * @return string 246 | */ 247 | protected function getTagTypeString(): string 248 | { 249 | return TagType::NAMES[static::TYPE]; 250 | } 251 | 252 | /** 253 | * @return string 254 | */ 255 | public function __toString(): string 256 | { 257 | return $this->getTagTypeString() . "('" . ($this->getName() ?: "None") . "'): " . $this->getValueString(); 258 | } 259 | 260 | /** 261 | * Convert tag to SNBT 262 | * See https://minecraft.wiki/w/NBT_format#Conversion_to_SNBT 263 | * 264 | * @return string 265 | */ 266 | abstract public function toSNBT(): string; 267 | 268 | /** 269 | * @param Tag $tag 270 | * @return bool 271 | */ 272 | abstract function equals(Tag $tag): bool; 273 | 274 | /** 275 | * @return string 276 | */ 277 | abstract protected function getValueString(): string; 278 | 279 | /** 280 | * @param int $type 281 | * @return class-string|null 282 | */ 283 | public static function getTagClass(int $type): ?string 284 | { 285 | return static::TAGS[$type] ?? null; 286 | } 287 | 288 | /** 289 | * @param Reader $reader 290 | * @param TagOptions|null $options 291 | * @param Tag|null $parent 292 | * @return Tag 293 | * @throws Exception 294 | */ 295 | public static function load(Reader $reader, ?TagOptions $options = null, ?Tag $parent = null): Tag 296 | { 297 | if($options === null) { 298 | $options = new TagOptions(); 299 | } 300 | 301 | $type = $reader->getDeserializer()->readByte()->getValue(); 302 | $class = static::getTagClass($type); 303 | if (is_null($class)) { 304 | throw new Exception("Unknown NBT tag type " . $type); 305 | } 306 | /** @var Tag $tag */ 307 | $tag = new $class($options); 308 | $tag->setParentTag($parent)->read($reader); 309 | return $tag; 310 | } 311 | 312 | /** 313 | * @param Reader $reader 314 | * @param TagOptions|null $options 315 | * @return RawTagReadResult 316 | * @throws Exception 317 | */ 318 | public static function loadRaw(Reader $reader, ?TagOptions $options = null): RawTagReadResult 319 | { 320 | if($options === null) { 321 | $options = new TagOptions(); 322 | } 323 | 324 | $type = $reader->getDeserializer()->readByte(); 325 | $class = static::getTagClass($type->getValue()); 326 | if (is_null($class)) { 327 | throw new Exception("Unknown NBT tag type " . $type->getValue()); 328 | } 329 | /** @var Tag $class */ 330 | $data = $class::readRaw($reader, $options); 331 | return new RawTagReadResult($type->getRawData() . $data, $type->getValue()); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/Tag/TagOptions.php: -------------------------------------------------------------------------------- 1 | rawCompoundPaths = $rawCompoundPaths; 29 | return $this; 30 | } 31 | 32 | /** 33 | * @return string[] 34 | */ 35 | public function getRawCompoundPaths(): array 36 | { 37 | return $this->rawCompoundPaths; 38 | } 39 | 40 | /** 41 | * @param string[]|null $parsedCompoundPaths 42 | * @return $this 43 | */ 44 | public function setParsedCompoundPaths(?array $parsedCompoundPaths): static 45 | { 46 | $this->parsedCompoundPaths = $parsedCompoundPaths; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return string[]|null 52 | */ 53 | public function getParsedCompoundPaths(): ?array 54 | { 55 | return $this->parsedCompoundPaths; 56 | } 57 | 58 | /** 59 | * @param Tag $tag 60 | * @return bool 61 | */ 62 | public function shouldBeReadRaw(Tag $tag): bool 63 | { 64 | $path = $tag->getStringPath(); 65 | if($path === "") { 66 | return false; 67 | } 68 | if(in_array($path, $this->getRawCompoundPaths(), true)) { 69 | return true; 70 | } 71 | $whitelist = $this->getParsedCompoundPaths(); 72 | if($whitelist) { 73 | return !in_array($path, $whitelist, true); 74 | } 75 | return false; 76 | } 77 | 78 | /** 79 | * @param int|null $maxListTagLength 80 | * @return $this 81 | */ 82 | public function setMaxListTagLength(?int $maxListTagLength): static 83 | { 84 | $this->maxListTagLength = $maxListTagLength; 85 | return $this; 86 | } 87 | 88 | /** 89 | * @return int|null 90 | */ 91 | public function getMaxListTagLength(): ?int 92 | { 93 | return $this->maxListTagLength; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Tag/TagType.php: -------------------------------------------------------------------------------- 1 | "TAG_End", 23 | self::TAG_Byte => "TAG_Byte", 24 | self::TAG_Short => "TAG_Short", 25 | self::TAG_Int => "TAG_Int", 26 | self::TAG_Long => "TAG_Long", 27 | self::TAG_Float => "TAG_Float", 28 | self::TAG_Double => "TAG_Double", 29 | self::TAG_Byte_Array => "TAG_Byte_Array", 30 | self::TAG_String => "TAG_String", 31 | self::TAG_List => "TAG_List", 32 | self::TAG_Compound => "TAG_Compound", 33 | self::TAG_Int_Array => "TAG_Int_Array", 34 | self::TAG_Long_Array => "TAG_Long_Array" 35 | ]; 36 | } 37 | --------------------------------------------------------------------------------