From e4f979dadf7d6440df88c14e0f7132e1f7c27cba Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 19:04:17 +0200 Subject: [PATCH 01/10] 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 7d906a6326e..1b6ee210ab1 100644 --- a/src/block/inventory/AnvilInventory.php +++ b/src/block/inventory/AnvilInventory.php @@ -25,6 +25,7 @@ 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 @@ public function __construct(Position $holder){ $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 00000000000..816d86d37ce --- /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 00000000000..3dc6d011fea --- /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 00000000000..fd4512ebf21 --- /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 417c57f75ca..1f9148ec2b5 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -33,6 +33,7 @@ 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 @@ protected function serializeCompoundTag(CompoundTag $tag) : void{ $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 d0ea33feb4e..2465acc797d 100644 --- a/src/item/ArmorMaterial.php +++ b/src/item/ArmorMaterial.php @@ -27,9 +27,13 @@ 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 @@ public function getEnchantability() : int{ 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 f110f6ea517..164e7a2730b 100644 --- a/src/item/Durable.php +++ b/src/item/Durable.php @@ -118,6 +118,10 @@ public function isBroken() : bool{ 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 1a74345b559..ea7e6ef7159 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 @@ public function clearNamedTag() : Item{ 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 @@ protected function deserializeCompoundTag(CompoundTag $tag) : void{ } $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 @@ protected function serializeCompoundTag(CompoundTag $tag) : void{ }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 20b40bbcb40..6c2a96e4e99 100644 --- a/src/item/TieredTool.php +++ b/src/item/TieredTool.php @@ -23,6 +23,8 @@ namespace pocketmine\item; +use function in_array; + abstract class TieredTool extends Tool{ protected ToolTier $tier; @@ -61,4 +63,8 @@ public function getFuelTime() : int{ 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 8469bc7e5f4..dd8862e73b0 100644 --- a/src/item/ToolTier.php +++ b/src/item/ToolTier.php @@ -23,6 +23,7 @@ namespace pocketmine\item; +use pocketmine\block\BlockTypeIds; use pocketmine\utils\LegacyEnumShimTrait; /** @@ -36,7 +37,7 @@ * @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 @@ private static function meta(int $harvestLevel, int $maxDurability, int $baseAtt */ 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 @@ public function getBaseEfficiency() : int{ 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 2ee1d74d2fc..172c1cb3cfd 100644 --- a/src/item/TurtleHelmet.php +++ b/src/item/TurtleHelmet.php @@ -38,4 +38,8 @@ public function onTickWorn(Living $entity) : bool{ 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 818273d2082..d803f9f8259 100644 --- a/src/item/VanillaArmorMaterials.php +++ b/src/item/VanillaArmorMaterials.php @@ -69,12 +69,12 @@ public static function getAll() : array{ } 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 a36ae9f4051..4cd76ec72e4 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -23,11 +23,14 @@ 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\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 @@ 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 AnvilTransaction){ if($this->specialTransaction === null){ throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action"); }else{ @@ -347,6 +351,15 @@ protected function processItemStackRequestAction(ItemStackRequestAction $action) } }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 From 44c3e035983948de075202442fa02629f3c123c9 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 19:10:10 +0200 Subject: [PATCH 02/10] fix PHPstan --- src/item/ArmorMaterial.php | 2 ++ src/item/Item.php | 2 +- src/item/ToolTier.php | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/item/ArmorMaterial.php b/src/item/ArmorMaterial.php index 2465acc797d..dc0ddcbb89f 100644 --- a/src/item/ArmorMaterial.php +++ b/src/item/ArmorMaterial.php @@ -56,6 +56,8 @@ public function getEquipSound() : ?Sound{ /** * Returns the items that can be used to repair the armor + * + * @return int[] */ public function getRepairMaterials() : array{ return $this->repairMaterials; diff --git a/src/item/Item.php b/src/item/Item.php index ea7e6ef7159..ceeeecb2082 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -427,7 +427,7 @@ protected function serializeCompoundTag(CompoundTag $tag) : void{ $tag->removeTag(self::TAG_KEEP_ON_DEATH); } - if($this->repairCost){ + if($this->repairCost > 0){ $tag->setInt(self::TAG_REPAIR_COST, $this->repairCost); }else{ $tag->removeTag(self::TAG_REPAIR_COST); diff --git a/src/item/ToolTier.php b/src/item/ToolTier.php index dd8862e73b0..e0c7f48a387 100644 --- a/src/item/ToolTier.php +++ b/src/item/ToolTier.php @@ -51,6 +51,7 @@ enum ToolTier{ /** * This function exists only to permit the use of named arguments and to make the code easier to read in PhpStorm. + * @param int[] $repairMaterials The typeId of the items that can be used to repair this tool in the anvil. * @phpstan-return TMetadata */ private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability, array $repairMaterials = []) : array{ @@ -112,7 +113,7 @@ public function getEnchantability() : int{ } /** - * Returns the list of items that can be used to repair this tool. + * Returns the typeId of items that can be used to repair this tool in the anvil. * * @return int[] */ From 54f746fc111bbdb9b2eff422f5734bf245175bab Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 23:16:54 +0200 Subject: [PATCH 03/10] finalize anvil transaction --- src/block/utils/AnvilHelper.php | 6 ++- .../transaction/AnvilTransaction.php | 53 +++++++++++++++++-- .../mcpe/handler/ItemStackRequestExecutor.php | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/block/utils/AnvilHelper.php b/src/block/utils/AnvilHelper.php index 816d86d37ce..ed8a1bfd5f6 100644 --- a/src/block/utils/AnvilHelper.php +++ b/src/block/utils/AnvilHelper.php @@ -168,8 +168,10 @@ private static function renameItem(Item $item, ?string $customName) : int{ $item->clearCustomName(); } }else{ - $resultCost += self::COST_RENAME; - $item->setCustomName($customName); + if($item->getCustomName() !== $customName){ + $resultCost += self::COST_RENAME; + $item->setCustomName($customName); + } } return $resultCost; diff --git a/src/inventory/transaction/AnvilTransaction.php b/src/inventory/transaction/AnvilTransaction.php index fd4512ebf21..7301df982d9 100644 --- a/src/inventory/transaction/AnvilTransaction.php +++ b/src/inventory/transaction/AnvilTransaction.php @@ -23,19 +23,43 @@ namespace pocketmine\inventory\transaction; +use pocketmine\block\utils\AnvilHelper; use pocketmine\block\utils\AnvilResult; use pocketmine\item\Item; +use pocketmine\item\VanillaItems; use pocketmine\player\Player; use function count; class AnvilTransaction extends InventoryTransaction{ public function __construct( Player $source, - private AnvilResult $anvilResult + private readonly AnvilResult $expectedResult, + private readonly ?string $customName ) { parent::__construct($source); } + private function validateFiniteResources(int $xpSpent) : void{ + $expectedXpCost = $this->expectedResult->getRepairCost(); + if($xpSpent !== $expectedXpCost){ + throw new TransactionValidationException("Expected the amount of xp spent to be $expectedXpCost, but received $xpSpent"); + } + + $xpLevel = $this->source->getXpManager()->getXpLevel(); + if($xpLevel < $expectedXpCost){ + throw new TransactionValidationException("Player's XP level $xpLevel is less than the required XP level $expectedXpCost"); + } + } + + private function validateInputs(Item $base, Item $material, Item $expectedOutput) : ?AnvilResult { + $calculAttempt = AnvilHelper::calculateResult($this->source, $base, $material, $this->customName); + if($calculAttempt->getResult() === null || !$calculAttempt->getResult()->equalsExact($expectedOutput)){ + return null; + } + + return $calculAttempt; + } + public function validate() : void{ if(count($this->actions) < 1){ throw new TransactionValidationException("Transaction must have at least one action to be executable"); @@ -47,14 +71,37 @@ public function validate() : void{ $outputs = []; $this->matchItems($outputs, $inputs); - //TODO + if(($outputCount = count($outputs)) !== 1){ + throw new TransactionValidationException("Expected 1 output item, but received $outputCount"); + } + $outputItem = $outputs[0]; + + if(($inputCount = count($inputs)) < 1){ + throw new TransactionValidationException("Expected at least 1 input item, but received $inputCount"); + } + if($inputCount > 2){ + throw new TransactionValidationException("Expected at most 2 input items, but received $inputCount"); + } + + if(count($inputs) < 2){ + $attempt = $this->validateInputs($inputs[0], VanillaItems::AIR(), $outputItem) ?? + throw new TransactionValidationException("Inputs do not match expected result"); + }else{ + $attempt = $this->validateInputs($inputs[0], $inputs[1], $outputItem) ?? + $this->validateInputs($inputs[1], $inputs[0], $outputItem) ?? + throw new TransactionValidationException("Inputs do not match expected result"); + } + + if($this->source->hasFiniteResources()){ + $this->validateFiniteResources($attempt->getRepairCost()); + } } public function execute() : void{ parent::execute(); if($this->source->hasFiniteResources()){ - $this->source->getXpManager()->subtractXpLevels($this->anvilResult->getRepairCost()); + $this->source->getXpManager()->subtractXpLevels($this->expectedResult->getRepairCost()); } } } diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 4cd76ec72e4..ccecf3b4a96 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -356,7 +356,7 @@ protected function processItemStackRequestAction(ItemStackRequestAction $action) 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->specialTransaction = new AnvilTransaction($this->player, $result, $this->request->getFilterStrings()[0] ?? null); $this->setNextCreatedItem($result->getResult()); } } From 726e2cba23043b0d2640c09638d6e1f696094bf1 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 23:26:07 +0200 Subject: [PATCH 04/10] Add anvil event --- src/event/player/PlayerUseAnvilEvent.php | 86 +++++++++++++++++++ .../transaction/AnvilTransaction.php | 20 +++++ 2 files changed, 106 insertions(+) create mode 100644 src/event/player/PlayerUseAnvilEvent.php diff --git a/src/event/player/PlayerUseAnvilEvent.php b/src/event/player/PlayerUseAnvilEvent.php new file mode 100644 index 00000000000..4a7c4ca8dfc --- /dev/null +++ b/src/event/player/PlayerUseAnvilEvent.php @@ -0,0 +1,86 @@ +player = $player; + } + + /** + * Returns the item that the player is using as the base item (left slot). + */ + public function getBaseItem() : Item{ + return $this->baseItem; + } + + /** + * Returns the item that the player is using as the material item (right slot), or null if there is no material item + * (e.g. when renaming an item). + */ + public function getMaterialItem() : ?Item{ + return $this->materialItem; + } + + /** + * Returns the item that the player will receive as a result of the anvil operation. + */ + public function getResultItem() : Item{ + return $this->resultItem; + } + + /** + * Returns the custom name that the player is setting on the item, or null if the player is not renaming the item. + * + * This value is defined when the base item is already renamed. + */ + public function getCustomName() : ?string{ + return $this->customName; + } + + /** + * Returns the amount of XP levels that the player will spend on this anvil operation. + */ + public function getXpCost() : int{ + return $this->xpCost; + } +} diff --git a/src/inventory/transaction/AnvilTransaction.php b/src/inventory/transaction/AnvilTransaction.php index 7301df982d9..849d8707708 100644 --- a/src/inventory/transaction/AnvilTransaction.php +++ b/src/inventory/transaction/AnvilTransaction.php @@ -25,12 +25,18 @@ use pocketmine\block\utils\AnvilHelper; use pocketmine\block\utils\AnvilResult; +use pocketmine\event\player\PlayerUseAnvilEvent; use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\player\Player; +use pocketmine\utils\AssumptionFailedError; use function count; class AnvilTransaction extends InventoryTransaction{ + private ?Item $baseItem = null; + private ?Item $materialItem = null; + private ?Item $resultItem = null; + public function __construct( Player $source, private readonly AnvilResult $expectedResult, @@ -57,6 +63,10 @@ private function validateInputs(Item $base, Item $material, Item $expectedOutput return null; } + $this->baseItem = $base; + $this->materialItem = $material; + $this->resultItem = $expectedOutput; + return $calculAttempt; } @@ -104,4 +114,14 @@ public function execute() : void{ $this->source->getXpManager()->subtractXpLevels($this->expectedResult->getRepairCost()); } } + + protected function callExecuteEvent() : bool{ + if($this->baseItem === null){ + throw new AssumptionFailedError("Expected that baseItem are not null before executing the event"); + } + + $ev = new PlayerUseAnvilEvent($this->source, $this->baseItem, $this->materialItem, $this->expectedResult->getResult(), $this->customName, $this->expectedResult->getRepairCost()); + $ev->call(); + return !$ev->isCancelled(); + } } From 804731d87fb384173020b7a8d5564af1125ab024 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 23:33:14 +0200 Subject: [PATCH 05/10] fix PHPstan --- src/inventory/transaction/AnvilTransaction.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/inventory/transaction/AnvilTransaction.php b/src/inventory/transaction/AnvilTransaction.php index 849d8707708..be3112aa6c3 100644 --- a/src/inventory/transaction/AnvilTransaction.php +++ b/src/inventory/transaction/AnvilTransaction.php @@ -35,7 +35,6 @@ class AnvilTransaction extends InventoryTransaction{ private ?Item $baseItem = null; private ?Item $materialItem = null; - private ?Item $resultItem = null; public function __construct( Player $source, @@ -59,13 +58,16 @@ private function validateFiniteResources(int $xpSpent) : void{ private function validateInputs(Item $base, Item $material, Item $expectedOutput) : ?AnvilResult { $calculAttempt = AnvilHelper::calculateResult($this->source, $base, $material, $this->customName); - if($calculAttempt->getResult() === null || !$calculAttempt->getResult()->equalsExact($expectedOutput)){ + if($calculAttempt === null){ + return null; + } + $result = $calculAttempt->getResult(); + if($result === null || !$result->equalsExact($expectedOutput)){ return null; } $this->baseItem = $base; $this->materialItem = $material; - $this->resultItem = $expectedOutput; return $calculAttempt; } @@ -120,7 +122,9 @@ protected function callExecuteEvent() : bool{ throw new AssumptionFailedError("Expected that baseItem are not null before executing the event"); } - $ev = new PlayerUseAnvilEvent($this->source, $this->baseItem, $this->materialItem, $this->expectedResult->getResult(), $this->customName, $this->expectedResult->getRepairCost()); + $ev = new PlayerUseAnvilEvent($this->source, $this->baseItem, $this->materialItem, $this->expectedResult->getResult() ?? throw new \AssertionError( + "Expected that the expected result is not null" + ), $this->customName, $this->expectedResult->getRepairCost()); $ev->call(); return !$ev->isCancelled(); } From 654b44447ea9eb780476f68a5eee5ecc1e0f02d3 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Sat, 10 Aug 2024 23:51:38 +0200 Subject: [PATCH 06/10] add sound and anvil damage --- .../transaction/AnvilTransaction.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/inventory/transaction/AnvilTransaction.php b/src/inventory/transaction/AnvilTransaction.php index be3112aa6c3..47245865d8f 100644 --- a/src/inventory/transaction/AnvilTransaction.php +++ b/src/inventory/transaction/AnvilTransaction.php @@ -23,14 +23,20 @@ namespace pocketmine\inventory\transaction; +use pocketmine\block\Anvil; +use pocketmine\block\inventory\AnvilInventory; use pocketmine\block\utils\AnvilHelper; use pocketmine\block\utils\AnvilResult; +use pocketmine\block\VanillaBlocks; use pocketmine\event\player\PlayerUseAnvilEvent; use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; +use pocketmine\world\sound\AnvilBreakSound; +use pocketmine\world\sound\AnvilUseSound; use function count; +use function mt_rand; class AnvilTransaction extends InventoryTransaction{ private ?Item $baseItem = null; @@ -115,6 +121,26 @@ public function execute() : void{ if($this->source->hasFiniteResources()){ $this->source->getXpManager()->subtractXpLevels($this->expectedResult->getRepairCost()); } + + $inventory = $this->source->getCurrentWindow(); + if($inventory instanceof AnvilInventory){ + $world = $inventory->getHolder()->getWorld(); + if(mt_rand(0, 12) === 0){ + $anvilBlock = $world->getBlock($inventory->getHolder()); + if($anvilBlock instanceof Anvil){ + $newDamage = $anvilBlock->getDamage() + 1; + if($newDamage > Anvil::VERY_DAMAGED){ + $newBlock = VanillaBlocks::AIR(); + $world->addSound($inventory->getHolder(), new AnvilBreakSound()); + }else{ + $newBlock = $anvilBlock->setDamage($newDamage); + } + $world->setBlock($inventory->getHolder(), $newBlock); + } + + } + $world->addSound($inventory->getHolder(), new AnvilUseSound()); + } } protected function callExecuteEvent() : bool{ From 7cfb6eea5164231d034eaf49bddd05dc100cf075 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Mon, 19 Aug 2024 22:27:45 +0200 Subject: [PATCH 07/10] first look at anvil actions --- src/block/anvil/AnvilAction.php | 52 ++++++ src/block/anvil/AnvilActionsFactory.php | 71 ++++++++ src/block/anvil/CombineEnchantmentsAction.php | 81 ++++++++++ src/block/anvil/RenameItemAction.php | 49 ++++++ src/block/anvil/RepairWithMaterialAction.php | 58 +++++++ src/block/anvil/RepairWithSacrificeAction.php | 58 +++++++ src/block/utils/AnvilHelper.php | 153 ++---------------- 7 files changed, 384 insertions(+), 138 deletions(-) create mode 100644 src/block/anvil/AnvilAction.php create mode 100644 src/block/anvil/AnvilActionsFactory.php create mode 100644 src/block/anvil/CombineEnchantmentsAction.php create mode 100644 src/block/anvil/RenameItemAction.php create mode 100644 src/block/anvil/RepairWithMaterialAction.php create mode 100644 src/block/anvil/RepairWithSacrificeAction.php diff --git a/src/block/anvil/AnvilAction.php b/src/block/anvil/AnvilAction.php new file mode 100644 index 00000000000..a925195df73 --- /dev/null +++ b/src/block/anvil/AnvilAction.php @@ -0,0 +1,52 @@ +xpCost; + } + + /** + * If only actions marked as free of repair cost is applied, the result item + * will not have any repair cost increase. + */ + public function isFreeOfRepairCost() : bool { + return false; + } + + abstract public function process(Item $resultItem) : void; + + abstract public function canBeApplied() : bool; +} diff --git a/src/block/anvil/AnvilActionsFactory.php b/src/block/anvil/AnvilActionsFactory.php new file mode 100644 index 00000000000..c53e3794cfe --- /dev/null +++ b/src/block/anvil/AnvilActionsFactory.php @@ -0,0 +1,71 @@ +, true> */ + private array $actions = []; + + private function __construct(){ + $this->register(RenameItemAction::class); + $this->register(CombineEnchantmentsAction::class); + $this->register(RepairWithSacrificeAction::class); + $this->register(RepairWithMaterialAction::class); + } + + /** + * @param class-string $class + */ + public function register(string $class) : void{ + if(!is_subclass_of($class, AnvilAction::class, true)){ + throw new \InvalidArgumentException("Class $class is not an AnvilAction"); + } + if(isset($this->actions[$class])){ + throw new \InvalidArgumentException("Class $class is already registered"); + } + $this->actions[$class] = true; + } + + /** + * Return all available actions for the given items. + * + * @return AnvilAction[] + */ + public function getActions(Item $base, Item $material, ?string $customName) : array{ + $actions = []; + foreach($this->actions as $class => $_){ + $action = new $class($base, $material, $customName); + if($action->canBeApplied()){ + $actions[] = $action; + } + } + return $actions; + } +} diff --git a/src/block/anvil/CombineEnchantmentsAction.php b/src/block/anvil/CombineEnchantmentsAction.php new file mode 100644 index 00000000000..6419c48c06a --- /dev/null +++ b/src/block/anvil/CombineEnchantmentsAction.php @@ -0,0 +1,81 @@ +material->hasEnchantments(); + } + + public function process(Item $resultItem) : void{ + foreach($this->material->getEnchantments() as $instance){ + $enchantment = $instance->getType(); + $level = $instance->getLevel(); + if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $this->base)){ + continue; + } + if(($targetEnchantment = $this->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($this->base->getEnchantments() as $testedInstance){ + $testedEnchantment = $testedInstance->getType(); + if(!$testedEnchantment->isCompatibleWith($enchantment)){ + $this->xpCost++; + continue 2; + } + } + } + + $costAddition = match($enchantment->getRarity()){ + Rarity::COMMON => 1, + Rarity::UNCOMMON => 2, + Rarity::RARE => 4, + Rarity::MYTHIC => 8, + default => throw new TransactionValidationException("Invalid rarity " . $enchantment->getRarity() . " found") + }; + + if($this->material instanceof EnchantedBook){ + // Enchanted books are half as expensive to combine + $costAddition = max(1, $costAddition / 2); + } + $levelDifference = $instance->getLevel() - $this->base->getEnchantmentLevel($instance->getType()); + $this->xpCost += $costAddition * $levelDifference; + $resultItem->addEnchantment($instance); + } + } +} diff --git a/src/block/anvil/RenameItemAction.php b/src/block/anvil/RenameItemAction.php new file mode 100644 index 00000000000..ac8aae72f45 --- /dev/null +++ b/src/block/anvil/RenameItemAction.php @@ -0,0 +1,49 @@ +customName === null || strlen($this->customName) === 0){ + if($this->base->hasCustomName()){ + $this->xpCost += self::COST; + $resultItem->clearCustomName(); + } + }else{ + if($this->base->getCustomName() !== $this->customName){ + $this->xpCost += self::COST; + $resultItem->setCustomName($this->customName); + } + } + } +} diff --git a/src/block/anvil/RepairWithMaterialAction.php b/src/block/anvil/RepairWithMaterialAction.php new file mode 100644 index 00000000000..9d9b12b15cb --- /dev/null +++ b/src/block/anvil/RepairWithMaterialAction.php @@ -0,0 +1,58 @@ +base instanceof Durable && + $this->base->isValidRepairMaterial($this->material) && + $this->base->getDamage() > 0; + } + + public function process(Item $resultItem) : void{ + assert($resultItem instanceof Durable, "Result item must be durable"); + assert($this->base instanceof Durable, "Base item must be durable"); + + $damage = $this->base->getDamage(); + $quarter = min($damage, (int) floor($this->base->getMaxDurability() / 4)); + $numberRepair = min($this->material->getCount(), (int) ceil($damage / $quarter)); + if($numberRepair > 0){ + $this->material->pop($numberRepair); + $damage -= $quarter * $numberRepair; + } + $resultItem->setDamage(max(0, $damage)); + + $this->xpCost = $numberRepair * self::COST; + } +} diff --git a/src/block/anvil/RepairWithSacrificeAction.php b/src/block/anvil/RepairWithSacrificeAction.php new file mode 100644 index 00000000000..7575f7c8f3b --- /dev/null +++ b/src/block/anvil/RepairWithSacrificeAction.php @@ -0,0 +1,58 @@ +base instanceof Durable && + $this->material instanceof Durable && + $this->base->getTypeId() === $this->material->getTypeId(); + } + + public function process(Item $resultItem) : void{ + assert($resultItem instanceof Durable, "Result item must be durable"); + assert($this->base instanceof Durable, "Base item must be durable"); + assert($this->material instanceof Durable, "Material item must be durable"); + + if($this->base->getDamage() !== 0){ + $baseMaxDurability = $this->base->getMaxDurability(); + $baseDurability = $baseMaxDurability - $this->base->getDamage(); + $materialDurability = $this->material->getMaxDurability() - $this->material->getDamage(); + $addDurability = (int) ($baseMaxDurability * 12 / 100); + + $newDurability = min($baseMaxDurability, $baseDurability + $materialDurability + $addDurability); + + $resultItem->setDamage($baseMaxDurability - $newDurability); + + $this->xpCost = self::COST; + } + } +} diff --git a/src/block/utils/AnvilHelper.php b/src/block/utils/AnvilHelper.php index ed8a1bfd5f6..86542318ca6 100644 --- a/src/block/utils/AnvilHelper.php +++ b/src/block/utils/AnvilHelper.php @@ -23,25 +23,12 @@ namespace pocketmine\block\utils; -use pocketmine\inventory\transaction\TransactionValidationException; -use pocketmine\item\Durable; -use pocketmine\item\EnchantedBook; -use pocketmine\item\enchantment\AvailableEnchantmentRegistry; -use pocketmine\item\enchantment\Enchantment; -use pocketmine\item\enchantment\EnchantmentInstance; -use pocketmine\item\enchantment\Rarity; +use pocketmine\block\anvil\AnvilActionsFactory; use pocketmine\item\Item; use pocketmine\player\Player; -use function ceil; -use function floor; use function max; -use function min; -use function strlen; -class AnvilHelper{ - private const COST_REPAIR_MATERIAL = 1; - private const COST_REPAIR_SACRIFICE = 2; - private const COST_RENAME = 1; +final class AnvilHelper{ private const COST_LIMIT = 39; /** @@ -50,140 +37,30 @@ class AnvilHelper{ * Returns null if the operation can't do anything. */ public static function calculateResult(Player $player, Item $base, Item $material, ?string $customName = null) : ?AnvilResult { - $resultCost = 0; + $xpCost = 0; $resultItem = clone $base; - if($resultItem instanceof Durable && $resultItem->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); + $additionnalRepairCost = 0; + foreach(AnvilActionsFactory::getInstance()->getActions($base, $material, $customName) as $action){ + $action->process($resultItem); + if(!$action->isFreeOfRepairCost() && $action->getXpCost() > 0){ + // Repair cost increment if the item has been processed + // and any of the action is not free of repair cost + $additionnalRepairCost = 1; } + $xpCost += $action->getXpCost(); } - // 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; + $xpCost += 2 ** $resultItem->getRepairCost() - 1; + $xpCost += 2 ** $material->getRepairCost() - 1; $resultItem->setRepairCost( max($resultItem->getRepairCost(), $material->getRepairCost()) + $additionnalRepairCost ); - if($resultCost <= 0 || ($resultCost > self::COST_LIMIT && !$player->isCreative())){ + if($xpCost <= 0 || ($xpCost > 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{ - if($item->getCustomName() !== $customName){ - $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") - }; + return new AnvilResult($xpCost, $resultItem); } } From b9df7987965477c046c85581153b48b03191fac0 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Mon, 19 Aug 2024 22:30:39 +0200 Subject: [PATCH 08/10] made AnvilAction constructor final --- src/block/anvil/AnvilAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/block/anvil/AnvilAction.php b/src/block/anvil/AnvilAction.php index a925195df73..35fb31c3b19 100644 --- a/src/block/anvil/AnvilAction.php +++ b/src/block/anvil/AnvilAction.php @@ -28,7 +28,7 @@ abstract class AnvilAction{ protected int $xpCost = 0; - public function __construct( + final public function __construct( protected Item $base, protected Item $material, protected ?string $customName From c77a72f15a8fd68594ff04d57484a5c9ef8b5714 Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Mon, 18 Nov 2024 14:31:08 +0100 Subject: [PATCH 09/10] some work on anvil --- src/block/anvil/AnvilAction.php | 14 ++++++++++ src/block/anvil/CombineEnchantmentsAction.php | 3 ++- src/block/anvil/RepairWithMaterialAction.php | 2 +- src/block/utils/AnvilHelper.php | 8 +++--- src/item/Item.php | 27 ++++++++++++------- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/block/anvil/AnvilAction.php b/src/block/anvil/AnvilAction.php index 35fb31c3b19..459763c29f3 100644 --- a/src/block/anvil/AnvilAction.php +++ b/src/block/anvil/AnvilAction.php @@ -26,6 +26,7 @@ use pocketmine\item\Item; abstract class AnvilAction{ + /** @phpstan-var int<0, max> */ protected int $xpCost = 0; final public function __construct( @@ -34,6 +35,12 @@ final public function __construct( protected ?string $customName ){ } + /** + * Returns the XP cost requested for this action. + * This XP cost will be summed up to the total XP cost of the anvil operation. + * + * @phpstan-return int<0, max> + */ final public function getXpCost() : int{ return $this->xpCost; } @@ -46,7 +53,14 @@ public function isFreeOfRepairCost() : bool { return false; } + /** + * Processing an action means applying the changes to the result item + * and updating the XP cost property of the action. + */ abstract public function process(Item $resultItem) : void; + /** + * Returns whether this action is valid and can be applied. + */ abstract public function canBeApplied() : bool; } diff --git a/src/block/anvil/CombineEnchantmentsAction.php b/src/block/anvil/CombineEnchantmentsAction.php index 6419c48c06a..585caf659d5 100644 --- a/src/block/anvil/CombineEnchantmentsAction.php +++ b/src/block/anvil/CombineEnchantmentsAction.php @@ -29,6 +29,7 @@ use pocketmine\item\enchantment\EnchantmentInstance; use pocketmine\item\enchantment\Rarity; use pocketmine\item\Item; +use function floor; use function max; use function min; @@ -74,7 +75,7 @@ public function process(Item $resultItem) : void{ $costAddition = max(1, $costAddition / 2); } $levelDifference = $instance->getLevel() - $this->base->getEnchantmentLevel($instance->getType()); - $this->xpCost += $costAddition * $levelDifference; + $this->xpCost += (int) floor($costAddition * $levelDifference); $resultItem->addEnchantment($instance); } } diff --git a/src/block/anvil/RepairWithMaterialAction.php b/src/block/anvil/RepairWithMaterialAction.php index 9d9b12b15cb..817513d08bb 100644 --- a/src/block/anvil/RepairWithMaterialAction.php +++ b/src/block/anvil/RepairWithMaterialAction.php @@ -53,6 +53,6 @@ public function process(Item $resultItem) : void{ } $resultItem->setDamage(max(0, $damage)); - $this->xpCost = $numberRepair * self::COST; + $this->xpCost = (int) floor($numberRepair * self::COST); } } diff --git a/src/block/utils/AnvilHelper.php b/src/block/utils/AnvilHelper.php index 86542318ca6..6066c0be9db 100644 --- a/src/block/utils/AnvilHelper.php +++ b/src/block/utils/AnvilHelper.php @@ -51,10 +51,10 @@ public static function calculateResult(Player $player, Item $base, Item $materia $xpCost += $action->getXpCost(); } - $xpCost += 2 ** $resultItem->getRepairCost() - 1; - $xpCost += 2 ** $material->getRepairCost() - 1; - $resultItem->setRepairCost( - max($resultItem->getRepairCost(), $material->getRepairCost()) + $additionnalRepairCost + $xpCost += 2 ** $resultItem->getAnvilRepairCost() - 1; + $xpCost += 2 ** $material->getAnvilRepairCost() - 1; + $resultItem->setAnvilRepairCost( + max($resultItem->getAnvilRepairCost(), $material->getAnvilRepairCost()) + $additionnalRepairCost ); if($xpCost <= 0 || ($xpCost > self::COST_LIMIT && !$player->isCreative())){ diff --git a/src/item/Item.php b/src/item/Item.php index ceeeecb2082..f21a2000ba9 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -85,7 +85,7 @@ class Item implements \JsonSerializable{ protected string $customName = ""; /** @var string[] */ protected array $lore = []; - protected int $repairCost = 0; + protected int $anvilRepairCost = 0; /** TODO: this needs to die in a fire */ protected ?CompoundTag $blockEntityTag = null; @@ -285,19 +285,26 @@ public function clearNamedTag() : Item{ } /** - * Returns the repair cost of the item. + * Returns the anvil repair cost of the item. + * This value is used in anvil to determine the XP cost of repairing the item. + * + * In vanilla, this value is stored in the "RepairCost" tag. */ - public function getRepairCost() : int{ - return $this->repairCost; + public function getAnvilRepairCost() : int{ + return $this->anvilRepairCost; } /** - * Sets the repair cost of the item. + * Sets the anvil repair cost value of the item. + * This value is used in anvil to determine the XP cost of repairing the item. + * Higher cost means more XP is required to repair the item. + * + * In vanilla, this value is stored in the "RepairCost" tag. * * @return $this */ - public function setRepairCost(int $cost) : self{ - $this->repairCost = $cost; + public function setAnvilRepairCost(int $cost) : self{ + $this->anvilRepairCost = $cost; return $this; } @@ -357,7 +364,7 @@ protected function deserializeCompoundTag(CompoundTag $tag) : void{ } $this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0; - $this->repairCost = $tag->getInt(self::TAG_REPAIR_COST, 0); + $this->anvilRepairCost = $tag->getInt(self::TAG_REPAIR_COST, 0); } protected function serializeCompoundTag(CompoundTag $tag) : void{ @@ -427,8 +434,8 @@ protected function serializeCompoundTag(CompoundTag $tag) : void{ $tag->removeTag(self::TAG_KEEP_ON_DEATH); } - if($this->repairCost > 0){ - $tag->setInt(self::TAG_REPAIR_COST, $this->repairCost); + if($this->anvilRepairCost > 0){ + $tag->setInt(self::TAG_REPAIR_COST, $this->anvilRepairCost); }else{ $tag->removeTag(self::TAG_REPAIR_COST); } From 947c8a062159cee0516861a127246824cbfb1c8b Mon Sep 17 00:00:00 2001 From: ShockedPlot7560 Date: Mon, 18 Nov 2024 14:51:53 +0100 Subject: [PATCH 10/10] remove phpstan docs --- src/block/anvil/AnvilAction.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/block/anvil/AnvilAction.php b/src/block/anvil/AnvilAction.php index 459763c29f3..b2f038b9ba4 100644 --- a/src/block/anvil/AnvilAction.php +++ b/src/block/anvil/AnvilAction.php @@ -26,7 +26,6 @@ use pocketmine\item\Item; abstract class AnvilAction{ - /** @phpstan-var int<0, max> */ protected int $xpCost = 0; final public function __construct( @@ -38,8 +37,6 @@ final public function __construct( /** * Returns the XP cost requested for this action. * This XP cost will be summed up to the total XP cost of the anvil operation. - * - * @phpstan-return int<0, max> */ final public function getXpCost() : int{ return $this->xpCost;