From a9ead5567b85a94979f71542a780e88d080a5329 Mon Sep 17 00:00:00 2001 From: Shoghi Cervantes Date: Sat, 8 Aug 2015 22:39:43 +0200 Subject: [PATCH] Improved NBT json parsing, attribute base --- src/pocketmine/Server.php | 2 + .../command/defaults/GiveCommand.php | 24 +- src/pocketmine/entity/Attribute.php | 165 ++++++++++ src/pocketmine/inventory/CraftingManager.php | 7 +- src/pocketmine/item/Item.php | 8 +- src/pocketmine/nbt/NBT.php | 288 +++++++++++++++++- .../protocol/UpdateAttributesPacket.php | 56 ++++ 7 files changed, 514 insertions(+), 36 deletions(-) create mode 100644 src/pocketmine/entity/Attribute.php create mode 100644 src/pocketmine/network/protocol/UpdateAttributesPacket.php diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index b8ac46e98..e38119b28 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -32,6 +32,7 @@ use pocketmine\command\ConsoleCommandSender; use pocketmine\command\PluginIdentifiableCommand; use pocketmine\command\SimpleCommandMap; use pocketmine\entity\Arrow; +use pocketmine\entity\Attribute; use pocketmine\entity\Effect; use pocketmine\entity\Entity; use pocketmine\entity\FallingSand; @@ -1609,6 +1610,7 @@ class Server{ Biome::init(); Effect::init(); Enchantment::init(); + Attribute::init(); /** TODO: @deprecated */ TextWrapper::init(); $this->craftingManager = new CraftingManager(); diff --git a/src/pocketmine/command/defaults/GiveCommand.php b/src/pocketmine/command/defaults/GiveCommand.php index 665965576..b8cd20e39 100644 --- a/src/pocketmine/command/defaults/GiveCommand.php +++ b/src/pocketmine/command/defaults/GiveCommand.php @@ -62,24 +62,20 @@ class GiveCommand extends VanillaCommand{ } if(isset($args[3])){ + $tags = $exception = null; $data = implode(" ", array_slice($args, 3)); - $data = preg_replace("/([A-Za-z0-9_]+[ ]*):/", '"$1":', $data); - $tags = @json_decode($data, true); - if(!is_array($tags)){ - $sender->sendMessage(new TranslationContainer("commands.give.tagError", [json_last_error_msg()])); + try{ + $tags = NBT::parseJSON($data); + }catch (\Exception $ex){ + $exception = $ex; + } + + if(!($tags instanceof Compound) or $exception !== null){ + $sender->sendMessage(new TranslationContainer("commands.give.tagError", [$exception !== null ? $exception->getMessage() : "Invalid tag conversion"])); return true; } - $nbt = new NBT(); - $nbt->setArray($tags); - $tag = $nbt->getData(); - - if(!($tag instanceof Compound)){ - $sender->sendMessage(new TranslationContainer("commands.give.tagError", ["Invalid tag conversion"])); - return true; - } - - $item->setNamedTag($tag); + $item->setNamedTag($tags); } if($player instanceof Player){ diff --git a/src/pocketmine/entity/Attribute.php b/src/pocketmine/entity/Attribute.php new file mode 100644 index 000000000..c3409cf9a --- /dev/null +++ b/src/pocketmine/entity/Attribute.php @@ -0,0 +1,165 @@ + $maxValue or $defaultValue > $maxValue or $defaultValue < $minValue){ + throw new \InvalidArgumentException("Invalid ranges: min value: $minValue, max value: $maxValue, $defaultValue: $defaultValue"); + } + + return self::$attributes[(int) $id] = new Attribute($id, $name, $minValue, $maxValue, $defaultValue, $shouldSend); + } + + /** + * @param $id + * @return null|Attribute + */ + public static function getAttribute($id){ + return isset(self::$attributes[$id]) ? clone self::$attributes[$id] : null; + } + + /** + * @param $name + * @return null|Attribute + */ + public static function getAttributeByName($name){ + foreach(self::$attributes as $a){ + if($a->getName() === $name){ + return clone $a; + } + } + + return null; + } + + private function __construct($id, $name, $minValue, $maxValue, $defaultValue, $shouldSend = false){ + $this->id = (int) $id; + $this->name = (string) $name; + $this->minValue = (float) $minValue; + $this->maxValue = (float) $maxValue; + $this->defaultValue = (float) $defaultValue; + $this->shouldSend = (float) $shouldSend; + + $this->currentValue = $this->defaultValue; + } + + public function getMinValue(){ + return $this->minValue; + } + + public function setMinValue($minValue){ + if($minValue > $this->getMaxValue()){ + throw new \InvalidArgumentException("Value $minValue is bigger than the maxValue!"); + } + + $this->minValue = $minValue; + return $this; + } + + public function getMaxValue(){ + return $this->maxValue; + } + + public function setMaxValue($maxValue){ + if($maxValue < $this->getMinValue()){ + throw new \InvalidArgumentException("Value $maxValue is bigger than the minValue!"); + } + + $this->maxValue = $maxValue; + return $this; + } + + public function getDefaultValue(){ + return $this->defaultValue; + } + + public function setDefaultValue($defaultValue){ + if($defaultValue > $this->getMaxValue() or $defaultValue < $this->getMinValue()){ + throw new \InvalidArgumentException("Value $defaultValue exceeds the range!"); + } + + $this->defaultValue = $defaultValue; + return $this; + } + + public function getValue(){ + return $this->currentValue; + } + + public function setValue($value){ + if($value > $this->getMaxValue() or $value < $this->getMinValue()){ + throw new \InvalidArgumentException("Value $value exceeds the range!"); + } + + $this->currentValue = $value; + + return $this; + } + + public function getName(){ + return $this->name; + } + + public function getId(){ + return $this->id; + } + + public function isSyncable(){ + return $this->shouldSend; + } + +} diff --git a/src/pocketmine/inventory/CraftingManager.php b/src/pocketmine/inventory/CraftingManager.php index 2d0fc6c7f..381cbaeee 100644 --- a/src/pocketmine/inventory/CraftingManager.php +++ b/src/pocketmine/inventory/CraftingManager.php @@ -63,7 +63,12 @@ class CraftingManager{ $this->registerRecipe((new ShapelessRecipe(Item::get(Item::GLOWSTONE_BLOCK, 0, 1)))->addIngredient(Item::get(Item::GLOWSTONE_DUST, 0, 4))); $this->registerRecipe((new ShapelessRecipe(Item::get(Item::LIT_PUMPKIN, 0, 1)))->addIngredient(Item::get(Item::PUMPKIN, 0, 1))->addIngredient(Item::get(Item::TORCH, 0, 1))); - $this->registerRecipe((new ShapelessRecipe(Item::get(Item::SNOW_BLOCK, 0, 1)))->addIngredient(Item::get(Item::SNOWBALL, 0, 4))); + + $this->registerRecipe((new ShapedRecipe(Item::get(Item::SNOW_BLOCK, 0, 1), + "XX", + "XX" + ))->setIngredient("X", Item::get(Item::SNOWBALL))); + $this->registerRecipe((new ShapelessRecipe(Item::get(Item::SNOW_LAYER, 0, 6)))->addIngredient(Item::get(Item::SNOW_BLOCK, 0, 3))); $this->registerRecipe((new ShapedRecipe(Item::get(Item::STICK, 0, 4), diff --git a/src/pocketmine/item/Item.php b/src/pocketmine/item/Item.php index ed4f4877a..e58fbd533 100644 --- a/src/pocketmine/item/Item.php +++ b/src/pocketmine/item/Item.php @@ -1188,13 +1188,9 @@ class Item{ if($tag->display->getCount() === 0){ unset($tag->display); } - }else{ - $tag->display = new Compound("display", [ - "Name" => new String("Name", $name) - ]); - } - $this->setNamedTag($tag); + $this->setNamedTag($tag); + } return $this; } diff --git a/src/pocketmine/nbt/NBT.php b/src/pocketmine/nbt/NBT.php index 16773c936..3b8285d81 100644 --- a/src/pocketmine/nbt/NBT.php +++ b/src/pocketmine/nbt/NBT.php @@ -181,6 +181,257 @@ class NBT{ return true; } + public static function parseJSON($data, &$offset = 0){ + $len = strlen($data); + for(; $offset < $len; ++$offset){ + $c = $data{$offset}; + if($c === "{"){ + ++$offset; + $data = self::parseCompound($data, $offset); + return new Compound("", $data); + }elseif($c !== " " and $c !== "\r" and $c !== "\n" and $c !== "\t"){ + throw new \Exception("Syntax error: unexpected '$c' at offset $offset"); + } + } + + return null; + } + + private static function parseList($str, &$offset = 0){ + $len = strlen($str); + + + $key = 0; + $value = null; + + $data = []; + + for(; $offset < $len; ++$offset){ + if($str{$offset - 1} === "]"){ + break; + }elseif($str{$offset} === "]"){ + ++$offset; + break; + } + + $value = self::readValue($str, $offset, $type); + + switch($type){ + case NBT::TAG_Byte: + $data[$key] = new Byte($key, $value); + break; + case NBT::TAG_Short: + $data[$key] = new Short($key, $value); + break; + case NBT::TAG_Int: + $data[$key] = new Int($key, $value); + break; + case NBT::TAG_Long: + $data[$key] = new Long($key, $value); + break; + case NBT::TAG_Float: + $data[$key] = new Float($key, $value); + break; + case NBT::TAG_Double: + $data[$key] = new Double($key, $value); + break; + case NBT::TAG_ByteArray: + $data[$key] = new ByteArray($key, $value); + break; + case NBT::TAG_String: + $data[$key] = new Byte($key, $value); + break; + case NBT::TAG_Enum: + $data[$key] = new Enum($key, $value); + break; + case NBT::TAG_Compound: + $data[$key] = new Compound($key, $value); + break; + case NBT::TAG_IntArray: + $data[$key] = new IntArray($key, $value); + break; + } + + $key++; + } + + return $data; + } + + private static function parseCompound($str, &$offset = 0){ + $len = strlen($str); + + $data = []; + + for(; $offset < $len; ++$offset){ + if($str{$offset - 1} === "}"){ + break; + }elseif($str{$offset} === "}"){ + ++$offset; + break; + } + + $key = self::readKey($str, $offset); + $value = self::readValue($str, $offset, $type); + + switch($type){ + case NBT::TAG_Byte: + $data[$key] = new Byte($key, $value); + break; + case NBT::TAG_Short: + $data[$key] = new Short($key, $value); + break; + case NBT::TAG_Int: + $data[$key] = new Int($key, $value); + break; + case NBT::TAG_Long: + $data[$key] = new Long($key, $value); + break; + case NBT::TAG_Float: + $data[$key] = new Float($key, $value); + break; + case NBT::TAG_Double: + $data[$key] = new Double($key, $value); + break; + case NBT::TAG_ByteArray: + $data[$key] = new ByteArray($key, $value); + break; + case NBT::TAG_String: + $data[$key] = new Byte($key, $value); + break; + case NBT::TAG_Enum: + $data[$key] = new Enum($key, $value); + break; + case NBT::TAG_Compound: + $data[$key] = new Compound($key, $value); + break; + case NBT::TAG_IntArray: + $data[$key] = new IntArray($key, $value); + break; + } + } + + return $data; + } + + private static function readValue($data, &$offset, &$type = null){ + $value = ""; + $type = null; + $inQuotes = false; + + $len = strlen($data); + for(; $offset < $len; ++$offset){ + $c = $data{$offset}; + + if(!$inQuotes and ($c === " " or $c === "\r" or $c === "\n" or $c === "\t" or $c === "," or $c === "}" or $c === "]")){ + if($c === "," or $c === "}" or $c === "]"){ + break; + } + }elseif($c === '"'){ + $inQuotes = !$inQuotes; + if($type === null){ + $type = self::TAG_String; + }elseif($inQuotes){ + throw new \Exception("Syntax error: invalid quote at offset $offset"); + } + }elseif($c === "\\"){ + $value .= "\\" . isset($data{$offset + 1}) ? $data{$offset + 1} : ""; + ++$offset; + }elseif($c === "{" and !$inQuotes){ + if($value !== ""){ + throw new \Exception("Syntax error: invalid compound start at offset $offset"); + } + ++$offset; + $value = self::parseCompound($data, $offset); + $type = self::TAG_Compound; + break; + }elseif($c === "[" and !$inQuotes){ + if($value !== ""){ + throw new \Exception("Syntax error: invalid list start at offset $offset"); + } + ++$offset; + $value = self::parseList($data, $offset); + $type = self::TAG_Enum; + break; + }else{ + $value .= $c; + } + } + + if($value === ""){ + throw new \Exception("Syntax error: invalid empty value at offset $offset"); + } + + if($type === null and strlen($value) > 0){ + $value = trim($value); + $last = strtolower(substr($value, -1)); + $part = substr($value, 0, -1); + + if($last !== "b" and $last !== "s" and $last !== "l" and $last !== "f" and $last !== "d"){ + $part = $value; + $last = null; + } + + if($last !== "f" and $last !== "d" and ((string) ((int) $part)) === $part){ + if($last === "b"){ + $type = self::TAG_Byte; + }elseif($last === "s"){ + $type = self::TAG_Short; + }elseif($last === "l"){ + $type = self::TAG_Long; + }else{ + $type = self::TAG_Int; + } + $value = (int) $part; + }elseif(is_numeric($part)){ + if($last === "f" or $last === "d" or strpos($part, ".") !== false){ + if($last === "f"){ + $type = self::TAG_Float; + }elseif($last === "d"){ + $type = self::TAG_Double; + }else{ + $type = self::TAG_Float; + } + $value = (float) $part; + }else{ + if($last === "l"){ + $type = self::TAG_Long; + }else{ + $type = self::TAG_Int; + } + + $value = $part; + } + }else{ + $type = self::TAG_String; + } + } + + return $value; + } + + private static function readKey($data, &$offset){ + $key = ""; + + $len = strlen($data); + for(; $offset < $len; ++$offset){ + $c = $data{$offset}; + + if($c === ":"){ + ++$offset; + break; + }elseif($c !== " " and $c !== "\r" and $c !== "\n" and $c !== "\t"){ + $key .= $c; + } + } + + if($key === ""){ + throw new \Exception("Syntax error: invalid empty key at offset $offset"); + } + + return $key; + } + public function get($len){ if($len < 0){ $this->offset = strlen($this->buffer) - 1; @@ -388,7 +639,21 @@ class NBT{ } } - private static function fromArray(Tag $tag, array $data, $byteArray = false){ + private static function fromArrayGuesser($key, $value){ + if(is_int($value)){ + return new Int($key, $value); + }elseif(is_float($value)){ + return new Float($key, $value); + }elseif(is_string($value)){ + return new String($key, $value); + }elseif(is_bool($value)){ + return new Byte($key, $value ? 1 : 0); + } + + return null; + } + + private static function fromArray(Tag $tag, array $data, callable $guesser){ foreach($data as $key => $value){ if(is_array($value)){ $isNumeric = true; @@ -402,26 +667,19 @@ class NBT{ } } $tag{$key} = $isNumeric ? ($isIntArray ? new IntArray($key, []) : new Enum($key, [])) : new Compound($key, []); - self::fromArray($tag->{$key}, $value); - }elseif(is_int($value)){ - $tag{$key} = new Int($key, $value); - }elseif(is_float($value)){ - $tag{$key} = new Float($key, $value); - }elseif(is_string($value)){ - if($byteArray and Utils::printable($value) !== $value){ - $tag{$key} = new ByteArray($key, $value); - }else{ - $tag{$key} = new String($key, $value); + self::fromArray($tag->{$key}, $value, $guesser); + }else{ + $v = call_user_func($guesser, $key, $value); + if($v instanceof Tag){ + $tag{$key} = $v; } - }elseif(is_bool($value)){ - $tag{$key} = new Byte($key, $value ? 1 : 0); } } } - public function setArray(array $data, $byteArray = false){ + public function setArray(array $data, callable $guesser = null){ $this->data = new Compound("", []); - self::fromArray($this->data, $data, $byteArray); + self::fromArray($this->data, $data, $guesser === null ? [self::class, "fromArrayGuesser"] : $guesser); } /** diff --git a/src/pocketmine/network/protocol/UpdateAttributesPacket.php b/src/pocketmine/network/protocol/UpdateAttributesPacket.php new file mode 100644 index 000000000..46bc3cf6e --- /dev/null +++ b/src/pocketmine/network/protocol/UpdateAttributesPacket.php @@ -0,0 +1,56 @@ + + + +use pocketmine\entity\Attribute; + +class UpdateAttributesPacket extends DataPacket{ + const NETWORK_ID = Info::UPDATE_ATTRIBUTES_PACKET; + + + public $entityId; + /** @var Attribute[] */ + public $entries = []; + + public function decode(){ + + } + + public function encode(){ + $this->reset(); + + $this->putLong($this->entityId); + + $this->putShort(count($this->entries)); + + foreach($this->entries as $entry){ + $this->putFloat($entry->getMinValue()); + $this->putFloat($entry->getMaxValue()); + $this->putFloat($entry->getValue()); + $this->putString($entry->getName()); + } + } + +}