From e4f979dadf7d6440df88c14e0f7132e1f7c27cba Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 19:04:17 +0200 Subject: [PATCH] first look at anvil --- src/block/inventory/AnvilInventory.php | 9 + src/block/utils/AnvilHelper.php | 187 ++++++++++++++++++ src/block/utils/AnvilResult.php | 41 ++++ .../transaction/AnvilTransaction.php | 60 ++++++ src/item/Armor.php | 5 + src/item/ArmorMaterial.php | 13 +- src/item/Durable.php | 4 + src/item/Item.php | 26 +++ src/item/TieredTool.php | 6 + src/item/ToolTier.php | 42 +++- src/item/TurtleHelmet.php | 4 + src/item/VanillaArmorMaterials.php | 12 +- .../mcpe/handler/ItemStackRequestExecutor.php | 15 +- 13 files changed, 407 insertions(+), 17 deletions(-) create mode 100644 src/block/utils/AnvilHelper.php create mode 100644 src/block/utils/AnvilResult.php create mode 100644 src/inventory/transaction/AnvilTransaction.php diff --git a/src/block/inventory/AnvilInventory.php b/src/block/inventory/AnvilInventory.php index 7d906a632..1b6ee210a 100644 --- a/src/block/inventory/AnvilInventory.php +++ b/src/block/inventory/AnvilInventory.php @@ -25,6 +25,7 @@ namespace pocketmine\block\inventory; use pocketmine\inventory\SimpleInventory; use pocketmine\inventory\TemporaryInventory; +use pocketmine\item\Item; use pocketmine\world\Position; class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ @@ -37,4 +38,12 @@ class AnvilInventory extends SimpleInventory implements BlockInventory, Temporar $this->holder = $holder; parent::__construct(2); } + + public function getInput() : Item { + return $this->getItem(self::SLOT_INPUT); + } + + public function getMaterial() : Item { + return $this->getItem(self::SLOT_MATERIAL); + } } diff --git a/src/block/utils/AnvilHelper.php b/src/block/utils/AnvilHelper.php new file mode 100644 index 000000000..816d86d37 --- /dev/null +++ b/src/block/utils/AnvilHelper.php @@ -0,0 +1,187 @@ +isValidRepairMaterial($material) && $resultItem->getDamage() > 0){ + $resultCost += self::repairWithMaterial($resultItem, $material); + }else{ + if($resultItem->getTypeId() === $material->getTypeId() && $resultItem instanceof Durable && $material instanceof Durable){ + $resultCost += self::repairWithSacrifice($resultItem, $material); + } + if($material->hasEnchantments()){ + $resultCost += self::combineEnchantments($resultItem, $material); + } + } + + // Repair cost increment if the item has been processed, the rename is free of penalty + $additionnalRepairCost = $resultCost > 0 ? 1 : 0; + $resultCost += self::renameItem($resultItem, $customName); + + $resultCost += 2 ** $resultItem->getRepairCost() - 1; + $resultCost += 2 ** $material->getRepairCost() - 1; + $resultItem->setRepairCost( + max($resultItem->getRepairCost(), $material->getRepairCost()) + $additionnalRepairCost + ); + + if($resultCost <= 0 || ($resultCost > self::COST_LIMIT && !$player->isCreative())){ + return null; + } + + return new AnvilResult($resultCost, $resultItem); + } + + /** + * @return int The XP cost of repairing the item + */ + private static function repairWithMaterial(Durable $result, Item $material) : int { + $damage = $result->getDamage(); + $quarter = min($damage, (int) floor($result->getMaxDurability() / 4)); + $numberRepair = min($material->getCount(), (int) ceil($damage / $quarter)); + if($numberRepair > 0){ + $material->pop($numberRepair); + $damage -= $quarter * $numberRepair; + } + $result->setDamage(max(0, $damage)); + + return $numberRepair * self::COST_REPAIR_MATERIAL; + } + + /** + * @return int The XP cost of repairing the item + */ + private static function repairWithSacrifice(Durable $result, Durable $sacrifice) : int{ + if($result->getDamage() === 0){ + return 0; + } + $baseDurability = $result->getMaxDurability() - $result->getDamage(); + $materialDurability = $sacrifice->getMaxDurability() - $sacrifice->getDamage(); + $addDurability = (int) ($result->getMaxDurability() * 12 / 100); + + $newDurability = min($result->getMaxDurability(), $baseDurability + $materialDurability + $addDurability); + + $result->setDamage($result->getMaxDurability() - $newDurability); + + return self::COST_REPAIR_SACRIFICE; + } + + /** + * @return int The XP cost of combining the enchantments + */ + private static function combineEnchantments(Item $base, Item $sacrifice) : int{ + $cost = 0; + foreach($sacrifice->getEnchantments() as $instance){ + $enchantment = $instance->getType(); + $level = $instance->getLevel(); + if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $base)){ + continue; + } + if(($targetEnchantment = $base->getEnchantment($enchantment)) !== null){ + // Enchant already present on the target item + $targetLevel = $targetEnchantment->getLevel(); + $newLevel = ($targetLevel === $level ? $targetLevel + 1 : max($targetLevel, $level)); + $level = min($newLevel, $enchantment->getMaxLevel()); + $instance = new EnchantmentInstance($enchantment, $level); + }else{ + // Check if the enchantment is compatible with the existing enchantments + foreach($base->getEnchantments() as $testedInstance){ + $testedEnchantment = $testedInstance->getType(); + if(!$testedEnchantment->isCompatibleWith($enchantment)){ + $cost++; + continue 2; + } + } + } + + $costAddition = self::getCostAddition($enchantment); + + if($sacrifice instanceof EnchantedBook){ + // Enchanted books are half as expensive to combine + $costAddition = max(1, $costAddition / 2); + } + $levelDifference = $instance->getLevel() - $base->getEnchantmentLevel($instance->getType()); + $cost += $costAddition * $levelDifference; + $base->addEnchantment($instance); + } + + return (int) $cost; + } + + /** + * @return int The XP cost of renaming the item + */ + private static function renameItem(Item $item, ?string $customName) : int{ + $resultCost = 0; + if($customName === null || strlen($customName) === 0){ + if($item->hasCustomName()){ + $resultCost += self::COST_RENAME; + $item->clearCustomName(); + } + }else{ + $resultCost += self::COST_RENAME; + $item->setCustomName($customName); + } + + return $resultCost; + } + + private static function getCostAddition(Enchantment $enchantment) : int { + return match($enchantment->getRarity()){ + Rarity::COMMON => 1, + Rarity::UNCOMMON => 2, + Rarity::RARE => 4, + Rarity::MYTHIC => 8, + default => throw new TransactionValidationException("Invalid rarity " . $enchantment->getRarity() . " found") + }; + } +} diff --git a/src/block/utils/AnvilResult.php b/src/block/utils/AnvilResult.php new file mode 100644 index 000000000..3dc6d011f --- /dev/null +++ b/src/block/utils/AnvilResult.php @@ -0,0 +1,41 @@ +repairCost; + } + + public function getResult() : ?Item{ + return $this->result; + } +} diff --git a/src/inventory/transaction/AnvilTransaction.php b/src/inventory/transaction/AnvilTransaction.php new file mode 100644 index 000000000..fd4512ebf --- /dev/null +++ b/src/inventory/transaction/AnvilTransaction.php @@ -0,0 +1,60 @@ +actions) < 1){ + throw new TransactionValidationException("Transaction must have at least one action to be executable"); + } + + /** @var Item[] $inputs */ + $inputs = []; + /** @var Item[] $outputs */ + $outputs = []; + $this->matchItems($outputs, $inputs); + + //TODO + } + + public function execute() : void{ + parent::execute(); + + if($this->source->hasFiniteResources()){ + $this->source->getXpManager()->subtractXpLevels($this->anvilResult->getRepairCost()); + } + } +} diff --git a/src/item/Armor.php b/src/item/Armor.php index 417c57f75..1f9148ec2 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -33,6 +33,7 @@ use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntTag; use pocketmine\player\Player; use pocketmine\utils\Binary; +use function in_array; use function lcg_value; use function mt_rand; @@ -172,4 +173,8 @@ class Armor extends Durable{ $tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->customColor->toARGB())) : $tag->removeTag(self::TAG_CUSTOM_COLOR); } + + public function isValidRepairMaterial(Item $material) : bool{ + return in_array($material->getTypeId(), $this->armorInfo->getMaterial()->getRepairMaterials(), true); + } } diff --git a/src/item/ArmorMaterial.php b/src/item/ArmorMaterial.php index d0ea33feb..2465acc79 100644 --- a/src/item/ArmorMaterial.php +++ b/src/item/ArmorMaterial.php @@ -27,9 +27,13 @@ use pocketmine\world\sound\Sound; class ArmorMaterial{ + /** + * @param int[] $repairMaterials + */ public function __construct( private readonly int $enchantability, - private readonly ?Sound $equipSound = null + private readonly ?Sound $equipSound = null, + private readonly array $repairMaterials = [] ){ } @@ -49,4 +53,11 @@ class ArmorMaterial{ public function getEquipSound() : ?Sound{ return $this->equipSound; } + + /** + * Returns the items that can be used to repair the armor + */ + public function getRepairMaterials() : array{ + return $this->repairMaterials; + } } diff --git a/src/item/Durable.php b/src/item/Durable.php index f110f6ea5..164e7a273 100644 --- a/src/item/Durable.php +++ b/src/item/Durable.php @@ -118,6 +118,10 @@ abstract class Durable extends Item{ return $this->damage >= $this->getMaxDurability() || $this->isNull(); } + public function isValidRepairMaterial(Item $material) : bool { + return false; + } + protected function deserializeCompoundTag(CompoundTag $tag) : void{ parent::deserializeCompoundTag($tag); $this->unbreakable = $tag->getByte("Unbreakable", 0) !== 0; diff --git a/src/item/Item.php b/src/item/Item.php index 1a74345b5..ea7e6ef71 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -69,6 +69,7 @@ class Item implements \JsonSerializable{ public const TAG_DISPLAY_NAME = "Name"; public const TAG_DISPLAY_LORE = "Lore"; + public const TAG_REPAIR_COST = "RepairCost"; public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death"; @@ -84,6 +85,7 @@ class Item implements \JsonSerializable{ protected string $customName = ""; /** @var string[] */ protected array $lore = []; + protected int $repairCost = 0; /** TODO: this needs to die in a fire */ protected ?CompoundTag $blockEntityTag = null; @@ -282,6 +284,23 @@ class Item implements \JsonSerializable{ return $this; } + /** + * Returns the repair cost of the item. + */ + public function getRepairCost() : int{ + return $this->repairCost; + } + + /** + * Sets the repair cost of the item. + * + * @return $this + */ + public function setRepairCost(int $cost) : self{ + $this->repairCost = $cost; + return $this; + } + /** * @throws NbtException */ @@ -338,6 +357,7 @@ class Item implements \JsonSerializable{ } $this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0; + $this->repairCost = $tag->getInt(self::TAG_REPAIR_COST, 0); } protected function serializeCompoundTag(CompoundTag $tag) : void{ @@ -406,6 +426,12 @@ class Item implements \JsonSerializable{ }else{ $tag->removeTag(self::TAG_KEEP_ON_DEATH); } + + if($this->repairCost){ + $tag->setInt(self::TAG_REPAIR_COST, $this->repairCost); + }else{ + $tag->removeTag(self::TAG_REPAIR_COST); + } } public function getCount() : int{ diff --git a/src/item/TieredTool.php b/src/item/TieredTool.php index 20b40bbcb..6c2a96e4e 100644 --- a/src/item/TieredTool.php +++ b/src/item/TieredTool.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace pocketmine\item; +use function in_array; + abstract class TieredTool extends Tool{ protected ToolTier $tier; @@ -61,4 +63,8 @@ abstract class TieredTool extends Tool{ public function isFireProof() : bool{ return $this->tier === ToolTier::NETHERITE; } + + public function isValidRepairMaterial(Item $material) : bool{ + return in_array($material->getTypeId(), $this->tier->getRepairMaterials(), true); + } } diff --git a/src/item/ToolTier.php b/src/item/ToolTier.php index 8469bc7e5..dd8862e73 100644 --- a/src/item/ToolTier.php +++ b/src/item/ToolTier.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\item; +use pocketmine\block\BlockTypeIds; use pocketmine\utils\LegacyEnumShimTrait; /** @@ -36,7 +37,7 @@ use pocketmine\utils\LegacyEnumShimTrait; * @method static ToolTier STONE() * @method static ToolTier WOOD() * - * @phpstan-type TMetadata array{0: int, 1: int, 2: int, 3: int, 4: int} + * @phpstan-type TMetadata array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int[]} */ enum ToolTier{ use LegacyEnumShimTrait; @@ -52,8 +53,8 @@ enum ToolTier{ * This function exists only to permit the use of named arguments and to make the code easier to read in PhpStorm. * @phpstan-return TMetadata */ - private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability) : array{ - return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability]; + private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability, array $repairMaterials = []) : array{ + return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability, $repairMaterials]; } /** @@ -61,12 +62,26 @@ enum ToolTier{ */ private function getMetadata() : array{ return match($this){ - self::WOOD => self::meta(1, 60, 5, 2, 15), - self::GOLD => self::meta(2, 33, 5, 12, 22), - self::STONE => self::meta(3, 132, 6, 4, 5), - self::IRON => self::meta(4, 251, 7, 6, 14), - self::DIAMOND => self::meta(5, 1562, 8, 8, 10), - self::NETHERITE => self::meta(6, 2032, 9, 9, 15) + self::WOOD => self::meta(1, 60, 5, 2, 15, [ + ItemTypeIds::fromBlockTypeId(BlockTypeIds::OAK_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::SPRUCE_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::BIRCH_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::JUNGLE_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::ACACIA_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::DARK_OAK_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::CRIMSON_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::WARPED_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::CHERRY_PLANKS), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::MANGROVE_PLANKS) + ]), + self::GOLD => self::meta(2, 33, 5, 12, 22, [ItemTypeIds::GOLD_INGOT]), + self::STONE => self::meta(3, 132, 6, 4, 5, [ + ItemTypeIds::fromBlockTypeId(BlockTypeIds::COBBLESTONE), + ItemTypeIds::fromBlockTypeId(BlockTypeIds::COBBLED_DEEPSLATE) + ]), + self::IRON => self::meta(4, 251, 7, 6, 14, [ItemTypeIds::IRON_INGOT]), + self::DIAMOND => self::meta(5, 1562, 8, 8, 10, [ItemTypeIds::DIAMOND]), + self::NETHERITE => self::meta(6, 2032, 9, 9, 15, [ItemTypeIds::NETHERITE_INGOT]) }; } @@ -95,4 +110,13 @@ enum ToolTier{ public function getEnchantability() : int{ return $this->getMetadata()[4]; } + + /** + * Returns the list of items that can be used to repair this tool. + * + * @return int[] + */ + public function getRepairMaterials() : array{ + return $this->getMetadata()[5]; + } } diff --git a/src/item/TurtleHelmet.php b/src/item/TurtleHelmet.php index 2ee1d74d2..172c1cb3c 100644 --- a/src/item/TurtleHelmet.php +++ b/src/item/TurtleHelmet.php @@ -38,4 +38,8 @@ class TurtleHelmet extends Armor{ return false; } + + public function isValidRepairMaterial(Item $material) : bool{ + return $material->getTypeId() === ItemTypeIds::SCUTE; + } } diff --git a/src/item/VanillaArmorMaterials.php b/src/item/VanillaArmorMaterials.php index 818273d20..d803f9f82 100644 --- a/src/item/VanillaArmorMaterials.php +++ b/src/item/VanillaArmorMaterials.php @@ -69,12 +69,12 @@ final class VanillaArmorMaterials{ } protected static function setup() : void{ - self::register("leather", new ArmorMaterial(15, new ArmorEquipLeatherSound())); + self::register("leather", new ArmorMaterial(15, new ArmorEquipLeatherSound(), [ItemTypeIds::LEATHER])); self::register("chainmail", new ArmorMaterial(12, new ArmorEquipChainSound())); - self::register("iron", new ArmorMaterial(9, new ArmorEquipIronSound())); - self::register("turtle", new ArmorMaterial(9, new ArmorEquipGenericSound())); - self::register("gold", new ArmorMaterial(25, new ArmorEquipGoldSound())); - self::register("diamond", new ArmorMaterial(10, new ArmorEquipDiamondSound())); - self::register("netherite", new ArmorMaterial(15, new ArmorEquipNetheriteSound())); + self::register("iron", new ArmorMaterial(9, new ArmorEquipIronSound(), [ItemTypeIds::IRON_INGOT])); + self::register("turtle", new ArmorMaterial(9, new ArmorEquipGenericSound(), [ItemTypeIds::SCUTE])); + self::register("gold", new ArmorMaterial(25, new ArmorEquipGoldSound(), [ItemTypeIds::GOLD_INGOT])); + self::register("diamond", new ArmorMaterial(10, new ArmorEquipDiamondSound(), [ItemTypeIds::DIAMOND])); + self::register("netherite", new ArmorMaterial(15, new ArmorEquipNetheriteSound(), [ItemTypeIds::NETHERITE_INGOT])); } } diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index a36ae9f40..4cd76ec72 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -23,11 +23,14 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\handler; +use pocketmine\block\inventory\AnvilInventory; use pocketmine\block\inventory\EnchantInventory; +use pocketmine\block\utils\AnvilHelper; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DropItemAction; +use pocketmine\inventory\transaction\AnvilTransaction; use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\inventory\transaction\EnchantingTransaction; use pocketmine\inventory\transaction\InventoryTransaction; @@ -39,6 +42,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingCreateSpecificResultStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction; +use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeOptionalStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CreativeCreateStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction; @@ -289,7 +293,7 @@ class ItemStackRequestExecutor{ * @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 AnvilTransaction){ if($this->specialTransaction === null){ throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action"); }else{ @@ -347,6 +351,15 @@ class ItemStackRequestExecutor{ } }elseif($action instanceof CraftRecipeAutoStackRequestAction){ $this->beginCrafting($action->getRecipeId(), $action->getRepetitions()); + }elseif($action instanceof CraftRecipeOptionalStackRequestAction){ + $window = $this->player->getCurrentWindow(); + if($window instanceof AnvilInventory){ + $result = AnvilHelper::calculateResult($this->player, $window->getInput(), $window->getMaterial(), $this->request->getFilterStrings()[0] ?? null); + if($result !== null){ + $this->specialTransaction = new AnvilTransaction($this->player, $result); + $this->setNextCreatedItem($result->getResult()); + } + } }elseif($action instanceof CraftingConsumeInputStackRequestAction){ $this->assertDoingCrafting(); $this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance