From 363556e9b6b8bad73828c0d38866a341ce847c4b Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 30 Nov 2019 12:31:31 +0000 Subject: [PATCH 1/4] AvailableCommandsPacket: encode & decode for enum value constraints This is a peculiarly overengineered system that is used for restricting access to enum members under certain conditions, e.g. to disallow changing specific gamerules in survival. --- .../mcpe/protocol/AvailableCommandsPacket.php | 60 ++++++++++++++++- .../protocol/types/CommandEnumConstraint.php | 67 +++++++++++++++++++ .../mcpe/protocol/types/CommandParameter.php | 3 + 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/pocketmine/network/mcpe/protocol/types/CommandEnumConstraint.php diff --git a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php b/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php index 5a32a277c..ba3fa2abf 100644 --- a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php +++ b/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php @@ -28,6 +28,7 @@ namespace pocketmine\network\mcpe\protocol; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\types\CommandData; use pocketmine\network\mcpe\protocol\types\CommandEnum; +use pocketmine\network\mcpe\protocol\types\CommandEnumConstraint; use pocketmine\network\mcpe\protocol\types\CommandParameter; use pocketmine\utils\BinaryDataException; use function count; @@ -92,6 +93,12 @@ class AvailableCommandsPacket extends DataPacket{ */ public $softEnums = []; + /** + * @var CommandEnumConstraint[] + * List of constraints for enum members. Used to constrain gamerules that can bechanged in nocheats mode and more. + */ + public $enumConstraints = []; + protected function decodePayload(){ /** @var string[] $enumValues */ $enumValues = []; @@ -118,6 +125,10 @@ class AvailableCommandsPacket extends DataPacket{ for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ $this->softEnums[] = $this->getSoftEnum(); } + + for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ + $this->enumConstraints[] = $this->getEnumConstraint($enums, $enumValues); + } } /** @@ -210,6 +221,50 @@ class AvailableCommandsPacket extends DataPacket{ } } + /** + * @param CommandEnum[] $enums + * @param string[] $enumValues + * + * @return CommandEnumConstraint + */ + protected function getEnumConstraint(array $enums, array $enumValues) : CommandEnumConstraint{ + //wtf, what was wrong with an offset inside the enum? :( + $valueIndex = $this->getLInt(); + if(!isset($enumValues[$valueIndex])){ + throw new \UnexpectedValueException("Enum constraint refers to unknown enum value index $valueIndex"); + } + $enumIndex = $this->getLInt(); + if(!isset($enums[$enumIndex])){ + throw new \UnexpectedValueException("Enum constraint refers to unknown enum index $enumIndex"); + } + $enum = $enums[$enumIndex]; + $valueOffset = array_search($enumValues[$valueIndex], $enum->enumValues, true); + if($valueOffset === false){ + throw new \UnexpectedValueException("Value \"" . $enumValues[$valueIndex] . "\" does not belong to enum \"$enum->enumName\""); + } + + $constraintIds = []; + for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ + $constraintIds[] = $this->getByte(); + } + + return new CommandEnumConstraint($enum, $valueOffset, $constraintIds); + } + + /** + * @param CommandEnumConstraint $value + * @param int[] $enumIndexes string enum name -> int index + * @param int[] $enumValueIndexes string value -> int index + */ + protected function putEnumConstraint(CommandEnumConstraint $constraint, array $enumIndexes, array $enumValueIndexes) : void{ + $this->putLInt($enumValueIndexes[$constraint->getAffectedValue()]); + $this->putLInt($enumIndexes[$constraint->getEnum()->enumName]); + $this->putUnsignedVarInt(count($constraint->getConstraints())); + foreach($constraint->getConstraints() as $v){ + $this->putByte($v); + } + } + /** * @param CommandEnum[] $enums * @param string[] $postfixes @@ -407,7 +462,10 @@ class AvailableCommandsPacket extends DataPacket{ $this->putSoftEnum($enum); } - $this->putUnsignedVarInt(0); //TODO + $this->putUnsignedVarInt(count($this->enumConstraints)); + foreach($this->enumConstraints as $constraint){ + $this->putEnumConstraint($constraint, $enumIndexes, $enumValueIndexes); + } } public function handle(NetworkSession $session) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandEnumConstraint.php b/src/pocketmine/network/mcpe/protocol/types/CommandEnumConstraint.php new file mode 100644 index 000000000..a97bb9cda --- /dev/null +++ b/src/pocketmine/network/mcpe/protocol/types/CommandEnumConstraint.php @@ -0,0 +1,67 @@ +enumValues[$valueOffset])){ + throw new \InvalidArgumentException("Invalid enum value offset $valueOffset"); + } + $this->enum = $enum; + $this->valueOffset = $valueOffset; + $this->constraints = $constraints; + } + + public function getEnum() : CommandEnum{ + return $this->enum; + } + + public function getValueOffset() : int{ + return $this->valueOffset; + } + + public function getAffectedValue() : string{ + return $this->enum->enumValues[$this->valueOffset]; + } + + /** + * @return int[] + */ + public function getConstraints() : array{ + return $this->constraints; + } +} diff --git a/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php b/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php index b1a00137b..a2f1a26ed 100644 --- a/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php +++ b/src/pocketmine/network/mcpe/protocol/types/CommandParameter.php @@ -24,6 +24,9 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\protocol\types; class CommandParameter{ + public const FLAG_FORCE_COLLAPSE_ENUM = 0x1; + public const FLAG_HAS_ENUM_CONSTRAINT = 0x2; + /** @var string */ public $paramName; /** @var int */ From 76bd0f452cf0584f5919b6265084884f88fd0dd4 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 30 Nov 2019 12:41:44 +0000 Subject: [PATCH 2/4] AvailableCommandsPacket: add special handling for enums which aren't referenced by any command directly the CommandName enum is a magic enum used by the argtype. TODO: It's possible that not sending the CommandName enum might be causing client sided crashes. Investigate. --- .../mcpe/protocol/AvailableCommandsPacket.php | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php b/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php index ba3fa2abf..719e36dfd 100644 --- a/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php +++ b/src/pocketmine/network/mcpe/protocol/AvailableCommandsPacket.php @@ -75,6 +75,10 @@ class AvailableCommandsPacket extends DataPacket{ */ public const ARG_FLAG_ENUM = 0x200000; + public const HARDCODED_ENUM_NAMES = [ + "CommandName" => true + ]; + /** * This is used for /xp L. It can only be applied to integer parameters. */ @@ -86,6 +90,13 @@ class AvailableCommandsPacket extends DataPacket{ */ public $commandData = []; + /** + * @var CommandEnum[] + * List of enums which aren't directly referenced by any vanilla command. + * This is used for the `CommandName` enum, which is a magic enum used by the `command` argument type. + */ + public $hardcodedEnums = []; + /** * @var CommandEnum[] * List of dynamic command enums, also referred to as "soft" enums. These can by dynamically updated mid-game @@ -115,7 +126,10 @@ class AvailableCommandsPacket extends DataPacket{ /** @var CommandEnum[] $enums */ $enums = []; for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ - $enums[] = $this->getEnum($enumValues); + $enums[] = $enum = $this->getEnum($enumValues); + if(isset(self::HARDCODED_ENUM_NAMES[$enum->enumName])){ + $this->hardcodedEnums[] = $enum; + } } for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ @@ -404,15 +418,21 @@ class AvailableCommandsPacket extends DataPacket{ $enumIndexes = []; /** @var CommandEnum[] $enums */ $enums = []; + + $addEnumFn = static function(CommandEnum $enum) use (&$enums, &$enumIndexes, &$enumValueIndexes){ + if(!isset($enumIndexes[$enum->enumName])){ + $enums[$enumIndexes[$enum->enumName] = count($enumIndexes)] = $enum; + } + foreach($enum->enumValues as $str){ + $enumValueIndexes[$str] = $enumValueIndexes[$str] ?? count($enumValueIndexes); + } + }; + foreach($this->hardcodedEnums as $enum){ + $addEnumFn($enum); + } foreach($this->commandData as $commandData){ if($commandData->aliases !== null){ - if(!isset($enumIndexes[$commandData->aliases->enumName])){ - $enums[$enumIndexes[$commandData->aliases->enumName] = count($enumIndexes)] = $commandData->aliases; - } - - foreach($commandData->aliases->enumValues as $str){ - $enumValueIndexes[$str] = $enumValueIndexes[$str] ?? count($enumValueIndexes); //latest index - } + $addEnumFn($commandData->aliases); } foreach($commandData->overloads as $overload){ @@ -422,12 +442,7 @@ class AvailableCommandsPacket extends DataPacket{ */ foreach($overload as $parameter){ if($parameter->enum !== null){ - if(!isset($enumIndexes[$parameter->enum->enumName])){ - $enums[$enumIndexes[$parameter->enum->enumName] = count($enumIndexes)] = $parameter->enum; - } - foreach($parameter->enum->enumValues as $str){ - $enumValueIndexes[$str] = $enumValueIndexes[$str] ?? count($enumValueIndexes); - } + $addEnumFn($parameter->enum); } if($parameter->postfix !== null){ From b7a5a53c9df3d735fa9c054d22eac8e35fda08a3 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 30 Nov 2019 12:56:16 +0000 Subject: [PATCH 3/4] MoveActorDeltaPacket: flags is now a short --- src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php b/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php index 21ee91309..c92265abd 100644 --- a/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php +++ b/src/pocketmine/network/mcpe/protocol/MoveActorDeltaPacket.php @@ -70,7 +70,7 @@ class MoveActorDeltaPacket extends DataPacket{ protected function decodePayload(){ $this->entityRuntimeId = $this->getEntityRuntimeId(); - $this->flags = $this->getByte(); + $this->flags = $this->getLShort(); $this->xDiff = $this->maybeReadCoord(self::FLAG_HAS_X); $this->yDiff = $this->maybeReadCoord(self::FLAG_HAS_Y); $this->zDiff = $this->maybeReadCoord(self::FLAG_HAS_Z); @@ -93,7 +93,7 @@ class MoveActorDeltaPacket extends DataPacket{ protected function encodePayload(){ $this->putEntityRuntimeId($this->entityRuntimeId); - $this->putByte($this->flags); + $this->putLShort($this->flags); $this->maybeWriteCoord(self::FLAG_HAS_X, $this->xDiff); $this->maybeWriteCoord(self::FLAG_HAS_Y, $this->yDiff); $this->maybeWriteCoord(self::FLAG_HAS_Z, $this->zDiff); From d8188b807a3d3e708c32483f463342da12d8f3a1 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 30 Nov 2019 21:18:54 +0000 Subject: [PATCH 4/4] CraftingDataPacket: read & write potion recipes --- .../mcpe/protocol/CraftingDataPacket.php | 30 +++++++++++ .../types/PotionContainerChangeRecipe.php | 51 +++++++++++++++++++ .../mcpe/protocol/types/PotionTypeRecipe.php | 51 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/pocketmine/network/mcpe/protocol/types/PotionContainerChangeRecipe.php create mode 100644 src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php diff --git a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php b/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php index bb5f7cd8f..a05d2bc27 100644 --- a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php +++ b/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php @@ -33,6 +33,8 @@ use pocketmine\item\Item; use pocketmine\item\ItemFactory; use pocketmine\network\mcpe\NetworkBinaryStream; use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\types\PotionContainerChangeRecipe; +use pocketmine\network\mcpe\protocol\types\PotionTypeRecipe; #ifndef COMPILE use pocketmine\utils\Binary; #endif @@ -53,6 +55,10 @@ class CraftingDataPacket extends DataPacket{ /** @var object[] */ public $entries = []; + /** @var PotionTypeRecipe[] */ + public $potionTypeRecipes = []; + /** @var PotionContainerChangeRecipe[] */ + public $potionContainerRecipes = []; /** @var bool */ public $cleanRecipes = false; @@ -140,6 +146,18 @@ class CraftingDataPacket extends DataPacket{ } $this->decodedEntries[] = $entry; } + for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ + $input = $this->getVarInt(); + $ingredient = $this->getVarInt(); + $output = $this->getVarInt(); + $this->potionTypeRecipes[] = new PotionTypeRecipe($input, $ingredient, $output); + } + for($i = 0, $count = $this->getUnsignedVarInt(); $i < $count; ++$i){ + $input = $this->getVarInt(); + $ingredient = $this->getVarInt(); + $output = $this->getVarInt(); + $this->potionContainerRecipes[] = new PotionContainerChangeRecipe($input, $ingredient, $output); + } $this->cleanRecipes = $this->getBool(); } @@ -240,6 +258,18 @@ class CraftingDataPacket extends DataPacket{ $writer->reset(); } + $this->putUnsignedVarInt(count($this->potionTypeRecipes)); + foreach($this->potionTypeRecipes as $recipe){ + $this->putVarInt($recipe->getInputPotionType()); + $this->putVarInt($recipe->getIngredientItemId()); + $this->putVarInt($recipe->getOutputPotionType()); + } + $this->putUnsignedVarInt(count($this->potionContainerRecipes)); + foreach($this->potionContainerRecipes as $recipe){ + $this->putVarInt($recipe->getInputItemId()); + $this->putVarInt($recipe->getIngredientItemId()); + $this->putVarInt($recipe->getOutputItemId()); + } $this->putBool($this->cleanRecipes); } diff --git a/src/pocketmine/network/mcpe/protocol/types/PotionContainerChangeRecipe.php b/src/pocketmine/network/mcpe/protocol/types/PotionContainerChangeRecipe.php new file mode 100644 index 000000000..8d3ad196e --- /dev/null +++ b/src/pocketmine/network/mcpe/protocol/types/PotionContainerChangeRecipe.php @@ -0,0 +1,51 @@ +inputItemId = $inputItemId; + $this->ingredientItemId = $ingredientItemId; + $this->outputItemId = $outputItemId; + } + + public function getInputItemId() : int{ + return $this->inputItemId; + } + + public function getIngredientItemId() : int{ + return $this->ingredientItemId; + } + + public function getOutputItemId() : int{ + return $this->outputItemId; + } +} \ No newline at end of file diff --git a/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php b/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php new file mode 100644 index 000000000..faacb6bf1 --- /dev/null +++ b/src/pocketmine/network/mcpe/protocol/types/PotionTypeRecipe.php @@ -0,0 +1,51 @@ +inputPotionType = $inputPotionType; + $this->ingredientItemId = $ingredientItemId; + $this->outputPotionType = $outputPotionType; + } + + public function getInputPotionType() : int{ + return $this->inputPotionType; + } + + public function getIngredientItemId() : int{ + return $this->ingredientItemId; + } + + public function getOutputPotionType() : int{ + return $this->outputPotionType; + } +} \ No newline at end of file