From 5cd9ffe898c3da512b230727fe79c98afb5bdb21 Mon Sep 17 00:00:00 2001 From: NaySurGithub Date: Wed, 19 Aug 2026 00:39:51 +0200 Subject: [PATCH 1/4] Implemented smithing table --- generated/item/VanillaItems.php | 14 +++ src/crafting/CraftingManager.php | 13 +++ .../CraftingManagerFromDataHelper.php | 58 ++++++++- src/crafting/SmithingTrimRecipe.php | 67 +++++++++++ .../ItemSerializerDeserializerRegistrar.php | 2 + .../transaction/SmithingTrimTransaction.php | 110 ++++++++++++++++++ src/item/Armor.php | 28 +++++ src/item/ArmorTrim.php | 50 ++++++++ src/item/ArmorTrimRegistry.php | 83 +++++++++++++ src/item/ItemTypeIds.php | 4 +- src/item/VanillaItemsInputs.php | 2 + src/network/mcpe/cache/CraftingDataCache.php | 37 +++++- src/network/mcpe/cache/TrimDataCache.php | 76 ++++++++++++ .../mcpe/handler/ItemStackRequestExecutor.php | 61 +++++++++- .../mcpe/handler/PreSpawnPacketHandler.php | 4 + 15 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 src/crafting/SmithingTrimRecipe.php create mode 100644 src/inventory/transaction/SmithingTrimTransaction.php create mode 100644 src/item/ArmorTrim.php create mode 100644 src/item/ArmorTrimRegistry.php create mode 100644 src/network/mcpe/cache/TrimDataCache.php diff --git a/generated/item/VanillaItems.php b/generated/item/VanillaItems.php index 64ae5d9a6..331444534 100644 --- a/generated/item/VanillaItems.php +++ b/generated/item/VanillaItems.php @@ -63,6 +63,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; @@ -184,6 +185,7 @@ final class VanillaItems{ private static FishingRod $_mFISHING_ROD; private static Item $_mFLINT; private static FlintSteel $_mFLINT_AND_STEEL; + private static Item $_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE; private static Item $_mGHAST_TEAR; private static GlassBottle $_mGLASS_BOTTLE; private static Item $_mGLISTERING_MELON; @@ -437,6 +439,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, @@ -558,6 +561,7 @@ private static function getInitAssigners() : array{ "fishing_rod" => fn(FishingRod $v) => self::$_mFISHING_ROD = $v, "flint" => fn(Item $v) => self::$_mFLINT = $v, "flint_and_steel" => fn(FlintSteel $v) => self::$_mFLINT_AND_STEEL = $v, + "flow_armor_trim_smithing_template" => fn(Item $v) => self::$_mFLOW_ARMOR_TRIM_SMITHING_TEMPLATE = $v, "ghast_tear" => fn(Item $v) => self::$_mGHAST_TEAR = $v, "glass_bottle" => fn(GlassBottle $v) => self::$_mGLASS_BOTTLE = $v, "glistering_melon" => fn(Item $v) => self::$_mGLISTERING_MELON = $v, @@ -907,6 +911,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; @@ -1512,6 +1521,11 @@ public static function FLINT_AND_STEEL() : FlintSteel{ return clone self::$_mFLINT_AND_STEEL; } + 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 GHAST_TEAR() : Item{ if(!isset(self::$_mGHAST_TEAR)){ self::init(); } return clone self::$_mGHAST_TEAR; 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/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/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index eee0be0bf..3b2dc9d4c 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -193,6 +193,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()); @@ -275,6 +276,7 @@ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::FISHING_ROD, Items::FISHING_ROD()); $this->map1to1Item(Ids::FLINT, Items::FLINT()); $this->map1to1Item(Ids::FLINT_AND_STEEL, Items::FLINT_AND_STEEL()); + $this->map1to1Item(Ids::FLOW_ARMOR_TRIM_SMITHING_TEMPLATE, Items::FLOW_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::GHAST_TEAR, Items::GHAST_TEAR()); $this->map1to1Item(Ids::GLASS_BOTTLE, Items::GLASS_BOTTLE()); $this->map1to1Item(Ids::GLISTERING_MELON_SLICE, Items::GLISTERING_MELON()); diff --git a/src/inventory/transaction/SmithingTrimTransaction.php b/src/inventory/transaction/SmithingTrimTransaction.php new file mode 100644 index 000000000..68e691e99 --- /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 = ArmorTrimRegistry::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 true; + } +} 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/ArmorTrimRegistry.php b/src/item/ArmorTrimRegistry.php new file mode 100644 index 000000000..9b16487ca --- /dev/null +++ b/src/item/ArmorTrimRegistry.php @@ -0,0 +1,83 @@ + 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/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index d9ba31d8e..932a8a24e 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -380,8 +380,10 @@ private function __construct(){ public const STONE_SPEAR = 20339; public const WOODEN_SPEAR = 20340; public const MACE = 20341; + public const BOLT_ARMOR_TRIM_SMITHING_TEMPLATE = 20342; + public const FLOW_ARMOR_TRIM_SMITHING_TEMPLATE = 20343; - public const FIRST_UNUSED_ITEM_ID = 20342; + public const FIRST_UNUSED_ITEM_ID = 20344; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/VanillaItemsInputs.php b/src/item/VanillaItemsInputs.php index d59dcbbe2..257171e47 100644 --- a/src/item/VanillaItemsInputs.php +++ b/src/item/VanillaItemsInputs.php @@ -441,6 +441,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..5322ffb23 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -26,7 +26,9 @@ namespace pocketmine\network\mcpe\handler; use pocketmine\block\inventory\EnchantInventory; +use pocketmine\block\inventory\SmithingTableInventory; use pocketmine\crafting\CraftingResultTransfer; +use pocketmine\crafting\SmithingTrimRecipe; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; @@ -34,8 +36,12 @@ 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\ArmorTrimRegistry; use pocketmine\item\Durable; use pocketmine\item\Item; use pocketmine\network\mcpe\cache\CraftingDataCache; @@ -248,6 +254,13 @@ 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 @@ -263,6 +276,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 = ArmorTrimRegistry::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 +351,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 2d29fef7b..9ab032850 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -28,6 +28,7 @@ use pocketmine\nbt\tag\CompoundTag; 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; @@ -162,6 +163,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{ From cf12af94c2566dfe86867bc1fe55db172b68a9ea Mon Sep 17 00:00:00 2001 From: NaySurGithub Date: Wed, 19 Aug 2026 00:52:02 +0200 Subject: [PATCH 2/4] Moved ArmorTrimRegistry to data/bedrock/item --- src/{ => data/bedrock}/item/ArmorTrimRegistry.php | 3 ++- src/inventory/transaction/SmithingTrimTransaction.php | 2 +- src/network/mcpe/handler/ItemStackRequestExecutor.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename src/{ => data/bedrock}/item/ArmorTrimRegistry.php (97%) diff --git a/src/item/ArmorTrimRegistry.php b/src/data/bedrock/item/ArmorTrimRegistry.php similarity index 97% rename from src/item/ArmorTrimRegistry.php rename to src/data/bedrock/item/ArmorTrimRegistry.php index 9b16487ca..89e701073 100644 --- a/src/item/ArmorTrimRegistry.php +++ b/src/data/bedrock/item/ArmorTrimRegistry.php @@ -23,11 +23,12 @@ declare(strict_types=1); -namespace pocketmine\item; +namespace pocketmine\data\bedrock\item; use pocketmine\crafting\CraftingManagerFromDataHelper; use pocketmine\data\bedrock\BedrockDataFiles; use pocketmine\data\SavedDataLoadingException; +use pocketmine\item\Item; use pocketmine\utils\Filesystem; use pocketmine\utils\SingletonTrait; use function is_array; diff --git a/src/inventory/transaction/SmithingTrimTransaction.php b/src/inventory/transaction/SmithingTrimTransaction.php index 68e691e99..bfa3c8e49 100644 --- a/src/inventory/transaction/SmithingTrimTransaction.php +++ b/src/inventory/transaction/SmithingTrimTransaction.php @@ -25,9 +25,9 @@ namespace pocketmine\inventory\transaction; +use pocketmine\data\bedrock\item\ArmorTrimRegistry; use pocketmine\item\Armor; use pocketmine\item\ArmorTrim; -use pocketmine\item\ArmorTrimRegistry; use pocketmine\item\Item; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 5322ffb23..645fa799c 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -29,6 +29,7 @@ use pocketmine\block\inventory\SmithingTableInventory; use pocketmine\crafting\CraftingResultTransfer; use pocketmine\crafting\SmithingTrimRecipe; +use pocketmine\data\bedrock\item\ArmorTrimRegistry; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; @@ -41,7 +42,6 @@ use pocketmine\inventory\transaction\TransactionBuilderInventory; use pocketmine\item\Armor; use pocketmine\item\ArmorTrim; -use pocketmine\item\ArmorTrimRegistry; use pocketmine\item\Durable; use pocketmine\item\Item; use pocketmine\network\mcpe\cache\CraftingDataCache; From 5f47e002c2d49d86d6ba3d2229de35336430666e Mon Sep 17 00:00:00 2001 From: NaySurGithub Date: Wed, 19 Aug 2026 13:02:41 +0200 Subject: [PATCH 3/4] Renamed ArmorTrimRegistry to ArmorTrimIdMap --- .../bedrock/item/{ArmorTrimRegistry.php => ArmorTrimIdMap.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/data/bedrock/item/{ArmorTrimRegistry.php => ArmorTrimIdMap.php} (100%) diff --git a/src/data/bedrock/item/ArmorTrimRegistry.php b/src/data/bedrock/item/ArmorTrimIdMap.php similarity index 100% rename from src/data/bedrock/item/ArmorTrimRegistry.php rename to src/data/bedrock/item/ArmorTrimIdMap.php From 577ad7225cd0c0bd91354c540dfcb5c91bcfb805 Mon Sep 17 00:00:00 2001 From: NaySurGithub Date: Wed, 19 Aug 2026 13:03:11 +0200 Subject: [PATCH 4/4] Renamed ArmorTrimRegistry references to ArmorTrimIdMap --- src/data/bedrock/item/ArmorTrimIdMap.php | 2 +- src/inventory/transaction/SmithingTrimTransaction.php | 4 ++-- src/network/mcpe/handler/ItemStackRequestExecutor.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/bedrock/item/ArmorTrimIdMap.php b/src/data/bedrock/item/ArmorTrimIdMap.php index 89e701073..b092140b1 100644 --- a/src/data/bedrock/item/ArmorTrimIdMap.php +++ b/src/data/bedrock/item/ArmorTrimIdMap.php @@ -39,7 +39,7 @@ * Maps smithing templates to trim pattern IDs, and trim-eligible ingredients (ingots, crystals, etc.) to trim material * IDs, as used by the smithing table's armor trim recipe. */ -final class ArmorTrimRegistry{ +final class ArmorTrimIdMap{ use SingletonTrait; /** @var array typeId => patternId */ diff --git a/src/inventory/transaction/SmithingTrimTransaction.php b/src/inventory/transaction/SmithingTrimTransaction.php index bfa3c8e49..484b9ce19 100644 --- a/src/inventory/transaction/SmithingTrimTransaction.php +++ b/src/inventory/transaction/SmithingTrimTransaction.php @@ -25,7 +25,7 @@ namespace pocketmine\inventory\transaction; -use pocketmine\data\bedrock\item\ArmorTrimRegistry; +use pocketmine\data\bedrock\item\ArmorTrimIdMap; use pocketmine\item\Armor; use pocketmine\item\ArmorTrim; use pocketmine\item\Item; @@ -62,7 +62,7 @@ public function validate() : void{ throw new TransactionValidationException("Expected exactly 3 input items (equipment, material and template), got " . count($inputs)); } - $registry = ArmorTrimRegistry::getInstance(); + $registry = ArmorTrimIdMap::getInstance(); $patternId = null; $materialId = null; foreach($inputs as $input){ diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 645fa799c..59afa8995 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -29,7 +29,7 @@ use pocketmine\block\inventory\SmithingTableInventory; use pocketmine\crafting\CraftingResultTransfer; use pocketmine\crafting\SmithingTrimRecipe; -use pocketmine\data\bedrock\item\ArmorTrimRegistry; +use pocketmine\data\bedrock\item\ArmorTrimIdMap; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; @@ -285,7 +285,7 @@ private function beginSmithingTrim() : void{ throw new ItemStackRequestProcessException("The armor trim recipe requires an open smithing table"); } - $registry = ArmorTrimRegistry::getInstance(); + $registry = ArmorTrimIdMap::getInstance(); $equipment = null; $patternId = null; $materialId = null;