diff --git a/generated/item/VanillaItems.php b/generated/item/VanillaItems.php index 4e2b05e57..aca3d0331 100644 --- a/generated/item/VanillaItems.php +++ b/generated/item/VanillaItems.php @@ -64,6 +64,7 @@ final class VanillaItems{ private static Item $_mBLAZE_POWDER; private static BlazeRod $_mBLAZE_ROD; private static Item $_mBLEACH; + private static Item $_mBOLT_ARMOR_TRIM_SMITHING_TEMPLATE; private static Item $_mBONE; private static Fertilizer $_mBONE_MEAL; private static Book $_mBOOK; @@ -192,6 +193,7 @@ final class VanillaItems{ private static Item $_mFLINT; private static FlintSteel $_mFLINT_AND_STEEL; private static BannerPattern $_mFLOWER_BANNER_PATTERN; + private static Item $_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE; private static BannerPattern $_mFLOW_BANNER_PATTERN; private static Item $_mGHAST_TEAR; private static GlassBottle $_mGLASS_BOTTLE; @@ -458,6 +460,7 @@ private static function getInitAssigners() : array{ "blaze_powder" => fn(Item $v) => self::$_mBLAZE_POWDER = $v, "blaze_rod" => fn(BlazeRod $v) => self::$_mBLAZE_ROD = $v, "bleach" => fn(Item $v) => self::$_mBLEACH = $v, + "bolt_armor_trim_smithing_template" => fn(Item $v) => self::$_mBOLT_ARMOR_TRIM_SMITHING_TEMPLATE = $v, "bone" => fn(Item $v) => self::$_mBONE = $v, "bone_meal" => fn(Fertilizer $v) => self::$_mBONE_MEAL = $v, "book" => fn(Book $v) => self::$_mBOOK = $v, @@ -586,6 +589,7 @@ private static function getInitAssigners() : array{ "flint" => fn(Item $v) => self::$_mFLINT = $v, "flint_and_steel" => fn(FlintSteel $v) => self::$_mFLINT_AND_STEEL = $v, "flower_banner_pattern" => fn(BannerPattern $v) => self::$_mFLOWER_BANNER_PATTERN = $v, + "flow_armor_trim_smithing_template" => fn(Item $v) => self::$_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE = $v, "flow_banner_pattern" => fn(BannerPattern $v) => self::$_mFLOW_BANNER_PATTERN = $v, "ghast_tear" => fn(Item $v) => self::$_mGHAST_TEAR = $v, "glass_bottle" => fn(GlassBottle $v) => self::$_mGLASS_BOTTLE = $v, @@ -952,6 +956,11 @@ public static function BLEACH() : Item{ return clone self::$_mBLEACH; } + public static function BOLT_ARMOR_TRIM_SMITHING_TEMPLATE() : Item{ + if(!isset(self::$_mBOLT_ARMOR_TRIM_SMITHING_TEMPLATE)){ self::init(); } + return clone self::$_mBOLT_ARMOR_TRIM_SMITHING_TEMPLATE; + } + public static function BONE() : Item{ if(!isset(self::$_mBONE)){ self::init(); } return clone self::$_mBONE; @@ -1592,6 +1601,11 @@ public static function FLOWER_BANNER_PATTERN() : BannerPattern{ return clone self::$_mFLOWER_BANNER_PATTERN; } + public static function FLOW_ARMOR_TRIM_SMITHING_TEMPLATE() : Item{ + if(!isset(self::$_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE)){ self::init(); } + return clone self::$_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE; + } + public static function FLOW_BANNER_PATTERN() : BannerPattern{ if(!isset(self::$_mFLOW_BANNER_PATTERN)){ self::init(); } return clone self::$_mFLOW_BANNER_PATTERN; diff --git a/src/crafting/CraftingManager.php b/src/crafting/CraftingManager.php index 61c027acb..376284976 100644 --- a/src/crafting/CraftingManager.php +++ b/src/crafting/CraftingManager.php @@ -210,6 +210,19 @@ public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{ } } + /** + * Registers a smithing trim recipe. Unlike shaped/shapeless recipes, this isn't indexed by its results, since + * the actual output depends on which specific pattern and material items are provided - it's not something that + * can be matched generically, so it's handled as a special case in {@link SmithingTrimRecipe}'s consumer. + */ + public function registerSmithingTrimRecipe(SmithingTrimRecipe $recipe) : void{ + $this->craftingRecipeIndex[] = $recipe; + + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback(); + } + } + public function registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void{ $this->potionTypeRecipes[] = $recipe; diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php index 078a4b3be..060d9de25 100644 --- a/src/crafting/CraftingManagerFromDataHelper.php +++ b/src/crafting/CraftingManagerFromDataHelper.php @@ -318,6 +318,58 @@ private static function loadShapelessRecipe(CraftingManager $manager, array $rec } } + /** + * @param mixed[] $recipe + */ + private static function loadSmithingTransformRecipe(CraftingManager $manager, array $recipe) : void{ + if(!isset($recipe["base"], $recipe["addition"], $recipe["template"], $recipe["result"]) || + !is_array($recipe["base"]) || !is_array($recipe["addition"]) || !is_array($recipe["template"]) || !is_array($recipe["result"]) + ){ + throw new SavedDataLoadingException("Smithing transform recipe should have base, addition, template and result objects"); + } + + $base = self::deserializeNetworkIngredient($recipe["base"]); + $addition = self::deserializeNetworkIngredient($recipe["addition"]); + $template = self::deserializeNetworkIngredient($recipe["template"]); + if($base === null || $addition === null || $template === null){ + //unknown ingredient item + return; + } + + $result = self::deserializeNetworkItemStack($recipe["result"]); + if($result === null){ + //unknown result item + return; + } + + $manager->registerShapelessRecipe(new ShapelessRecipe( + [$base, $addition, $template], + [$result], + ShapelessRecipeType::SMITHING + )); + } + + /** + * @param mixed[] $recipe + */ + private static function loadSmithingTrimRecipe(CraftingManager $manager, array $recipe) : void{ + if(!isset($recipe["base"], $recipe["addition"], $recipe["template"]) || + !is_array($recipe["base"]) || !is_array($recipe["addition"]) || !is_array($recipe["template"]) + ){ + throw new SavedDataLoadingException("Smithing trim recipe should have base, addition and template objects"); + } + + $base = self::deserializeNetworkIngredient($recipe["base"]); + $addition = self::deserializeNetworkIngredient($recipe["addition"]); + $template = self::deserializeNetworkIngredient($recipe["template"]); + if($base === null || $addition === null || $template === null){ + //unknown ingredient item + return; + } + + $manager->registerSmithingTrimRecipe(new SmithingTrimRecipe($base, $addition, $template)); + } + /** * @param mixed[] $recipe */ @@ -425,9 +477,13 @@ public static function make(string $filePath) : CraftingManager{ case self::NETWORK_RECIPE_TYPE_SHAPED: self::loadShapedRecipe($result, $recipe); break; - case self::NETWORK_RECIPE_TYPE_MULTI: case self::NETWORK_RECIPE_TYPE_SMITHING_TRANSFORM: + self::loadSmithingTransformRecipe($result, $recipe); + break; case self::NETWORK_RECIPE_TYPE_SMITHING_TRIM: + self::loadSmithingTrimRecipe($result, $recipe); + break; + case self::NETWORK_RECIPE_TYPE_MULTI: //TODO: not supported by the crafting system yet break; } diff --git a/src/crafting/CraftingResultTransfer.php b/src/crafting/CraftingResultTransfer.php index 78304b8cd..0e3395aa6 100644 --- a/src/crafting/CraftingResultTransfer.php +++ b/src/crafting/CraftingResultTransfer.php @@ -81,4 +81,32 @@ public static function transferContainerNamedTag(array $inputs, array $results) return; } } + + /** + * Smithing transforms (e.g. netherite upgrades) keep everything from the base item (enchantments, custom name, + * damage, trim...) and only change its type. The base is the input accepted by the recipe's first ingredient. + * + * @param Item[] $inputs + * @param Item[] $results + * @phpstan-param array $inputs + * @phpstan-param array $results + */ + public static function transferSmithingBaseNamedTag(ShapelessRecipe $recipe, array $inputs, array $results) : void{ + if($recipe->getType() !== ShapelessRecipeType::SMITHING){ + return; + } + $baseIngredient = $recipe->getIngredientList()[0] ?? null; + if($baseIngredient === null){ + return; + } + foreach($inputs as $input){ + if(!$baseIngredient->accepts($input)){ + continue; + } + foreach($results as $result){ + $result->setNamedTag(clone $input->getNamedTag()); + } + return; + } + } } diff --git a/src/crafting/SmithingTrimRecipe.php b/src/crafting/SmithingTrimRecipe.php new file mode 100644 index 000000000..c9950953d --- /dev/null +++ b/src/crafting/SmithingTrimRecipe.php @@ -0,0 +1,67 @@ +base; + } + + public function getAddition() : RecipeIngredient{ + return $this->addition; + } + + public function getTemplate() : RecipeIngredient{ + return $this->template; + } + + public function getIngredientList() : array{ + return [$this->base, $this->addition, $this->template]; + } + + public function getResultsFor(CraftingGrid $grid) : array{ + throw new AssumptionFailedError("Armor trim results are computed by SmithingTrimTransaction, not through the generic recipe system"); + } + + public function matchesCraftingGrid(CraftingGrid $grid) : bool{ + return false; + } +} diff --git a/src/data/bedrock/item/ArmorTrimIdMap.php b/src/data/bedrock/item/ArmorTrimIdMap.php new file mode 100644 index 000000000..b092140b1 --- /dev/null +++ b/src/data/bedrock/item/ArmorTrimIdMap.php @@ -0,0 +1,84 @@ + typeId => patternId */ + private array $patterns = []; + /** @var array typeId => materialId */ + private array $materials = []; + + private function __construct(){ + $data = json_decode(Filesystem::fileGetContents(BedrockDataFiles::TRIM_DATA_JSON), true); + if(!is_array($data) || !isset($data["patterns"], $data["materials"]) || !is_array($data["patterns"]) || !is_array($data["materials"])){ + throw new SavedDataLoadingException(BedrockDataFiles::TRIM_DATA_JSON . " should contain patterns and materials lists"); + } + + foreach($data["patterns"] as $pattern){ + if(!is_array($pattern) || !isset($pattern["itemName"], $pattern["patternId"]) || !is_string($pattern["itemName"]) || !is_string($pattern["patternId"])){ + throw new SavedDataLoadingException("Invalid trim pattern entry"); + } + $item = CraftingManagerFromDataHelper::deserializeItemStackFromFields($pattern["itemName"], null, 1, null, null); + if($item !== null){ + $this->patterns[$item->getTypeId()] = $pattern["patternId"]; + } + } + + foreach($data["materials"] as $material){ + if(!is_array($material) || !isset($material["itemName"], $material["materialId"]) || !is_string($material["itemName"]) || !is_string($material["materialId"])){ + throw new SavedDataLoadingException("Invalid trim material entry"); + } + $item = CraftingManagerFromDataHelper::deserializeItemStackFromFields($material["itemName"], null, 1, null, null); + if($item !== null){ + $this->materials[$item->getTypeId()] = $material["materialId"]; + } + } + } + + public function getPatternId(Item $template) : ?string{ + return $this->patterns[$template->getTypeId()] ?? null; + } + + public function getMaterialId(Item $ingredient) : ?string{ + return $this->materials[$ingredient->getTypeId()] ?? null; + } +} diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 760bcc402..9fbfc46d9 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -201,6 +201,7 @@ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::BLAZE_POWDER, Items::BLAZE_POWDER()); $this->map1to1Item(Ids::BLAZE_ROD, Items::BLAZE_ROD()); $this->map1to1Item(Ids::BLEACH, Items::BLEACH()); + $this->map1to1Item(Ids::BOLT_ARMOR_TRIM_SMITHING_TEMPLATE, Items::BOLT_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::BONE, Items::BONE()); $this->map1to1Item(Ids::BONE_MEAL, Items::BONE_MEAL()); $this->map1to1Item(Ids::BOOK, Items::BOOK()); @@ -289,6 +290,7 @@ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::FLINT, Items::FLINT()); $this->map1to1Item(Ids::FLINT_AND_STEEL, Items::FLINT_AND_STEEL()); $this->map1to1Item(Ids::FLOWER_BANNER_PATTERN, Items::FLOWER_BANNER_PATTERN()); + $this->map1to1Item(Ids::FLOW_ARMOR_TRIM_SMITHING_TEMPLATE, Items::FLOW_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::FLOW_BANNER_PATTERN, Items::FLOW_BANNER_PATTERN()); $this->map1to1Item(Ids::GHAST_TEAR, Items::GHAST_TEAR()); $this->map1to1Item(Ids::GLASS_BOTTLE, Items::GLASS_BOTTLE()); diff --git a/src/inventory/transaction/CraftingTransaction.php b/src/inventory/transaction/CraftingTransaction.php index b09963f8e..4914f7c05 100644 --- a/src/inventory/transaction/CraftingTransaction.php +++ b/src/inventory/transaction/CraftingTransaction.php @@ -29,6 +29,7 @@ use pocketmine\crafting\CraftingRecipe; use pocketmine\crafting\CraftingResultTransfer; use pocketmine\crafting\RecipeIngredient; +use pocketmine\crafting\ShapelessRecipe; use pocketmine\event\inventory\CraftItemEvent; use pocketmine\item\Item; use pocketmine\player\Player; @@ -253,6 +254,9 @@ private function getExpectedResultsFor(CraftingRecipe $recipe) : array{ //grid may already be empty by validate() time - use transaction inputs as a fallback $results = $recipe->getResultsFor($this->source->getCraftingGrid()); CraftingResultTransfer::transferContainerNamedTag($this->inputs, $results); + if($recipe instanceof ShapelessRecipe){ + CraftingResultTransfer::transferSmithingBaseNamedTag($recipe, $this->inputs, $results); + } return $results; } diff --git a/src/inventory/transaction/SmithingTrimTransaction.php b/src/inventory/transaction/SmithingTrimTransaction.php new file mode 100644 index 000000000..ffada8bab --- /dev/null +++ b/src/inventory/transaction/SmithingTrimTransaction.php @@ -0,0 +1,110 @@ +actions) < 1){ + throw new TransactionValidationException("Transaction must have at least one action to be executable"); + } + + /** @var Item[] $outputs */ + $outputs = []; + /** @var Item[] $inputs */ + $inputs = []; + $this->matchItems($outputs, $inputs); + + if(count($inputs) !== 3){ + throw new TransactionValidationException("Expected exactly 3 input items (equipment, material and template), got " . count($inputs)); + } + + $registry = ArmorTrimIdMap::getInstance(); + $patternId = null; + $materialId = null; + foreach($inputs as $input){ + if($input instanceof Armor){ + if($this->equipment !== null){ + throw new TransactionValidationException("Received more than 1 item to apply a trim to"); + } + $this->equipment = $input; + continue; + } + if(($foundPattern = $registry->getPatternId($input)) !== null){ + $patternId = $foundPattern; + continue; + } + if(($foundMaterial = $registry->getMaterialId($input)) !== null){ + $materialId = $foundMaterial; + continue; + } + throw new TransactionValidationException("Item $input is not a valid trim equipment, template or material"); + } + + if($this->equipment === null || $patternId === null || $materialId === null){ + throw new TransactionValidationException("Missing equipment, template or material for armor trim"); + } + + if(($outputCount = count($outputs)) !== 1){ + throw new TransactionValidationException("Expected 1 output item, but received $outputCount"); + } + + $expected = clone $this->equipment; + $expected->setTrim(new ArmorTrim($patternId, $materialId)); + if(!$outputs[0]->equalsExact($expected)){ + throw new TransactionValidationException("Invalid output item"); + } + $this->output = $outputs[0]; + } + + protected function callExecuteEvent() : bool{ + if($this->equipment === null || $this->output === null){ + throw new AssumptionFailedError("Expected that equipment and output are not null before executing the event"); + } + + return parent::callExecuteEvent(); + } +} diff --git a/src/item/Armor.php b/src/item/Armor.php index f7ee20e1e..ab70985fd 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -40,7 +40,12 @@ class Armor extends Durable implements DyeableItem{ public const TAG_CUSTOM_COLOR = DyeableItem::TAG_CUSTOM_COLOR; // TODO: remove this, this is here for BC compatibility + public const TAG_TRIM = "Trim"; //TAG_Compound + public const TAG_TRIM_PATTERN = "Pattern"; //TAG_String + public const TAG_TRIM_MATERIAL = "Material"; //TAG_String + private ArmorTypeInfo $armorInfo; + private ?ArmorTrim $trim = null; /** * @param string[] $enchantmentTags @@ -77,6 +82,16 @@ public function getMaterial() : ArmorMaterial{ return $this->armorInfo->getMaterial(); } + public function getTrim() : ?ArmorTrim{ + return $this->trim; + } + + /** @return $this */ + public function setTrim(?ArmorTrim $trim) : self{ + $this->trim = $trim; + return $this; + } + public function getEnchantability() : int{ return $this->armorInfo->getMaterial()->getEnchantability(); } @@ -135,10 +150,23 @@ public function onClickAir(Player $player, Vector3 $directionVector, array &$ret protected function deserializeCompoundTag(CompoundTag $tag) : void{ parent::deserializeCompoundTag($tag); $this->deserializeCustomColor($tag); + + $trimTag = $tag->getCompoundTag(self::TAG_TRIM); + $this->trim = $trimTag !== null ? + new ArmorTrim($trimTag->getString(self::TAG_TRIM_PATTERN, ""), $trimTag->getString(self::TAG_TRIM_MATERIAL, "")) : + null; } protected function serializeCompoundTag(CompoundTag $tag) : void{ parent::serializeCompoundTag($tag); $this->serializeCustomColor($tag); + + if($this->trim !== null){ + $tag->setTag(self::TAG_TRIM, CompoundTag::create() + ->setString(self::TAG_TRIM_PATTERN, $this->trim->getPatternId()) + ->setString(self::TAG_TRIM_MATERIAL, $this->trim->getMaterialId())); + }else{ + $tag->removeTag(self::TAG_TRIM); + } } } diff --git a/src/item/ArmorTrim.php b/src/item/ArmorTrim.php new file mode 100644 index 000000000..858deb74e --- /dev/null +++ b/src/item/ArmorTrim.php @@ -0,0 +1,50 @@ +patternId; + } + + public function getMaterialId() : string{ + return $this->materialId; + } + + public function equals(ArmorTrim $other) : bool{ + return $this->patternId === $other->patternId && $this->materialId === $other->materialId; + } +} diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index e6221d206..6dc7e7f83 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -399,8 +399,10 @@ private function __construct(){ public const POPLAR_HANGING_SIGN = 20358; public const POPLAR_BOAT = 20359; public const CUSHION = 20360; + public const BOLT_ARMOR_TRIM_SMITHING_TEMPLATE = 20361; + public const FLOW_ARMOR_TRIM_SMITHING_TEMPLATE = 20362; - public const FIRST_UNUSED_ITEM_ID = 20361; + public const FIRST_UNUSED_ITEM_ID = 20363; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/VanillaItemsInputs.php b/src/item/VanillaItemsInputs.php index 7523573df..64c6efba6 100644 --- a/src/item/VanillaItemsInputs.php +++ b/src/item/VanillaItemsInputs.php @@ -460,6 +460,8 @@ private function registerSmithingTemplates() : void{ self::register("ward_armor_trim_smithing_template", fn(IID $id) => new Item($id, "Ward Armor Trim Smithing Template")); self::register("wayfinder_armor_trim_smithing_template", fn(IID $id) => new Item($id, "Wayfinder Armor Trim Smithing Template")); self::register("wild_armor_trim_smithing_template", fn(IID $id) => new Item($id, "Wild Armor Trim Smithing Template")); + self::register("bolt_armor_trim_smithing_template", fn(IID $id) => new Item($id, "Bolt Armor Trim Smithing Template")); + self::register("flow_armor_trim_smithing_template", fn(IID $id) => new Item($id, "Flow Armor Trim Smithing Template")); } } diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php index 1d973baf6..d158773ec 100644 --- a/src/network/mcpe/cache/CraftingDataCache.php +++ b/src/network/mcpe/cache/CraftingDataCache.php @@ -31,6 +31,7 @@ use pocketmine\crafting\ShapedRecipe; use pocketmine\crafting\ShapelessRecipe; use pocketmine\crafting\ShapelessRecipeType; +use pocketmine\crafting\SmithingTrimRecipe; use pocketmine\network\mcpe\convert\TypeConverter; use pocketmine\network\mcpe\protocol\CraftingDataPacket; use pocketmine\network\mcpe\protocol\types\recipe\CraftingRecipeBlockName; @@ -41,11 +42,14 @@ use pocketmine\network\mcpe\protocol\types\recipe\RecipeUnlockingRequirement; use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe; use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe; +use pocketmine\network\mcpe\protocol\types\recipe\SmithingTransformRecipe as ProtocolSmithingTransformRecipe; +use pocketmine\network\mcpe\protocol\types\recipe\SmithingTrimRecipe as ProtocolSmithingTrimRecipe; use pocketmine\timings\Timings; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use Ramsey\Uuid\Uuid; use function array_map; +use function count; use function spl_object_id; final class CraftingDataCache{ @@ -92,12 +96,41 @@ private function buildCraftingDataCache(CraftingManager $manager) : CraftingData foreach($manager->getCraftingRecipeIndex() as $index => $recipe){ //the client doesn't like recipes with an ID of 0, so we need to offset them $recipeNetId = $index + self::RECIPE_ID_OFFSET; - if($recipe instanceof ShapelessRecipe){ + if($recipe instanceof ShapelessRecipe && $recipe->getType() === ShapelessRecipeType::SMITHING){ + //smithing transform recipes (e.g. netherite upgrade) use a dedicated network entry, not the generic + //shapeless one - the client's smithing table UI won't recognize them otherwise + $ingredients = $recipe->getIngredientList(); + $results = $recipe->getResults(); + if(count($ingredients) !== 3 || count($results) !== 1){ + continue; + } + [$base, $addition, $template] = $ingredients; + $recipesWithTypeIds[] = new ProtocolSmithingTransformRecipe( + CraftingDataPacket::ENTRY_SMITHING_TRANSFORM, + "smithing_transform_$recipeNetId", + $converter->coreRecipeIngredientToNet($template), + $converter->coreRecipeIngredientToNet($base), + $converter->coreRecipeIngredientToNet($addition), + $converter->coreItemStackToNet($results[0]), + CraftingRecipeBlockName::SMITHING_TABLE, + $recipeNetId + ); + }elseif($recipe instanceof SmithingTrimRecipe){ + $recipesWithTypeIds[] = new ProtocolSmithingTrimRecipe( + CraftingDataPacket::ENTRY_SMITHING_TRIM, + "smithing_trim_$recipeNetId", + $converter->coreRecipeIngredientToNet($recipe->getTemplate()), + $converter->coreRecipeIngredientToNet($recipe->getBase()), + $converter->coreRecipeIngredientToNet($recipe->getAddition()), + CraftingRecipeBlockName::SMITHING_TABLE, + $recipeNetId + ); + }elseif($recipe instanceof ShapelessRecipe){ $typeTag = match($recipe->getType()){ ShapelessRecipeType::CRAFTING => CraftingRecipeBlockName::CRAFTING_TABLE, ShapelessRecipeType::STONECUTTER => CraftingRecipeBlockName::STONECUTTER, ShapelessRecipeType::CARTOGRAPHY => CraftingRecipeBlockName::CARTOGRAPHY_TABLE, - ShapelessRecipeType::SMITHING => CraftingRecipeBlockName::SMITHING_TABLE, + ShapelessRecipeType::SMITHING => throw new AssumptionFailedError("Smithing transform recipes are handled in the branch above"), }; $recipesWithTypeIds[] = new ProtocolShapelessRecipe( CraftingDataPacket::ENTRY_SHAPELESS, diff --git a/src/network/mcpe/cache/TrimDataCache.php b/src/network/mcpe/cache/TrimDataCache.php new file mode 100644 index 000000000..dc2251e59 --- /dev/null +++ b/src/network/mcpe/cache/TrimDataCache.php @@ -0,0 +1,76 @@ +cache ??= $this->buildPacket(); + } + + private function buildPacket() : TrimDataPacket{ + $data = json_decode(Filesystem::fileGetContents(BedrockDataFiles::TRIM_DATA_JSON), true); + if(!is_array($data) || !isset($data["patterns"], $data["materials"]) || !is_array($data["patterns"]) || !is_array($data["materials"])){ + throw new SavedDataLoadingException(BedrockDataFiles::TRIM_DATA_JSON . " should contain patterns and materials lists"); + } + + $patterns = []; + foreach($data["patterns"] as $pattern){ + if(!is_array($pattern) || !isset($pattern["itemName"], $pattern["patternId"]) || !is_string($pattern["itemName"]) || !is_string($pattern["patternId"])){ + throw new SavedDataLoadingException("Invalid trim pattern entry"); + } + $patterns[] = new TrimPattern($pattern["itemName"], $pattern["patternId"]); + } + + $materials = []; + foreach($data["materials"] as $material){ + if( + !is_array($material) || + !isset($material["itemName"], $material["materialId"], $material["color"]) || + !is_string($material["itemName"]) || !is_string($material["materialId"]) || !is_string($material["color"]) + ){ + throw new SavedDataLoadingException("Invalid trim material entry"); + } + $materials[] = new TrimMaterial($material["materialId"], $material["color"], $material["itemName"]); + } + + return TrimDataPacket::create($patterns, $materials); + } +} diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 021617b69..2afa7f8e6 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -26,7 +26,11 @@ namespace pocketmine\network\mcpe\handler; use pocketmine\block\inventory\EnchantInventory; +use pocketmine\block\inventory\SmithingTableInventory; use pocketmine\crafting\CraftingResultTransfer; +use pocketmine\crafting\ShapelessRecipe; +use pocketmine\crafting\SmithingTrimRecipe; +use pocketmine\data\bedrock\item\ArmorTrimIdMap; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; @@ -34,8 +38,11 @@ use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\inventory\transaction\EnchantingTransaction; use pocketmine\inventory\transaction\InventoryTransaction; +use pocketmine\inventory\transaction\SmithingTrimTransaction; use pocketmine\inventory\transaction\TransactionBuilder; use pocketmine\inventory\transaction\TransactionBuilderInventory; +use pocketmine\item\Armor; +use pocketmine\item\ArmorTrim; use pocketmine\item\Durable; use pocketmine\item\Item; use pocketmine\network\mcpe\cache\CraftingDataCache; @@ -248,11 +255,22 @@ protected function beginCrafting(int $recipeId, int $repetitions) : void{ throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeIndex"); } + if($recipe instanceof SmithingTrimRecipe){ + //the result depends on the specific pattern/material used, which can't be expressed through the generic + //recipe matching system - see SmithingTrimTransaction + $this->beginSmithingTrim(); + return; + } + $this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions); //CraftRecipeAuto may leave the crafting grid empty; container NBT is copied when //CraftingConsumeInput is handled below (needed for shulker box dyeing etc.) $craftingResults = $recipe->getResultsFor($this->player->getCraftingGrid()); + $window = $this->player->getCurrentWindow(); + if($recipe instanceof ShapelessRecipe && $window instanceof SmithingTableInventory){ + CraftingResultTransfer::transferSmithingBaseNamedTag($recipe, $window->getContents(), $craftingResults); + } foreach($craftingResults as $k => $craftingResult){ $craftingResult->setCount($craftingResult->getCount() * $repetitions); $this->craftingResults[$k] = $craftingResult; @@ -263,6 +281,48 @@ protected function beginCrafting(int $recipeId, int $repetitions) : void{ } } + /** + * @throws ItemStackRequestProcessException + */ + private function beginSmithingTrim() : void{ + $window = $this->player->getCurrentWindow(); + if(!$window instanceof SmithingTableInventory){ + throw new ItemStackRequestProcessException("The armor trim recipe requires an open smithing table"); + } + + $registry = ArmorTrimIdMap::getInstance(); + $equipment = null; + $patternId = null; + $materialId = null; + foreach($window->getContents() as $item){ + if($item instanceof Armor){ + if($equipment !== null){ + throw new ItemStackRequestProcessException("More than 1 item to apply a trim to"); + } + $equipment = $item; + continue; + } + if(($foundPattern = $registry->getPatternId($item)) !== null){ + $patternId = $foundPattern; + continue; + } + if(($foundMaterial = $registry->getMaterialId($item)) !== null){ + $materialId = $foundMaterial; + } + } + + if($equipment === null || $patternId === null || $materialId === null){ + throw new ItemStackRequestProcessException("Missing equipment, template or material for armor trim"); + } + + $this->specialTransaction = new SmithingTrimTransaction($this->player); + + $result = clone $equipment; + $result->setTrim(new ArmorTrim($patternId, $materialId)); + $this->craftingResults = [$result]; + $this->setNextCreatedItem($result); + } + /** * @throws ItemStackRequestProcessException */ @@ -296,7 +356,11 @@ protected function takeCreatedItem(int $count) : Item{ * @throws ItemStackRequestProcessException */ private function assertDoingCrafting() : void{ - if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction){ + if( + !$this->specialTransaction instanceof CraftingTransaction && + !$this->specialTransaction instanceof EnchantingTransaction && + !$this->specialTransaction instanceof SmithingTrimTransaction + ){ if($this->specialTransaction === null){ throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action"); }else{ diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php index b33480285..61a4e8f11 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -30,6 +30,7 @@ use pocketmine\nbt\tag\ListTag; use pocketmine\network\mcpe\cache\CraftingDataCache; use pocketmine\network\mcpe\cache\StaticPacketCache; +use pocketmine\network\mcpe\cache\TrimDataCache; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\ItemRegistryPacket; @@ -175,6 +176,9 @@ public function setUp() : void{ $this->session->getLogger()->debug("Sending crafting data"); $this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager())); + $this->session->getLogger()->debug("Sending trim data"); + $this->session->sendDataPacket(TrimDataCache::getInstance()->getPacket()); + $this->session->getLogger()->debug("Sending player list"); $this->session->syncPlayerList($this->server->getOnlinePlayers()); }finally{