Compare commits

...

21 Commits

Author SHA1 Message Date
084feaef53 more work on ItemCombine 2025-03-23 17:14:20 +01:00
eaab4b35d2 add unit testing on MaterialRepairRecipe 2025-03-22 23:00:15 +01:00
f346799920 fill in CraftingManager for MaterialRepair 2025-03-22 21:47:20 +01:00
11135c2fd8 remove dead code 2025-03-22 21:22:08 +01:00
b4cb09fe5e continue refactoring, not finished, not tested
This is a bump from my workspace
2025-03-22 20:21:56 +01:00
a1695a52d5 Merge remote-tracking branch 'upstream/feat/anvil' into feat/anvil 2025-03-22 19:24:16 +01:00
51d45bedce Merge branch 'minor-next' into feat/anvil 2025-03-09 01:58:16 +00:00
b3f0ed23ca Merge remote-tracking branch 'upstream/minor-next' into feat/anvil 2024-12-14 15:16:39 +01:00
5b9dc2c275 rewrote the system with CraftingManager 2024-12-14 15:15:15 +01:00
947c8a0621 remove phpstan docs 2024-11-18 14:51:53 +01:00
c77a72f15a some work on anvil 2024-11-18 14:31:08 +01:00
b9df798796 made AnvilAction constructor final 2024-08-19 22:30:39 +02:00
7cfb6eea51 first look at anvil actions 2024-08-19 22:27:45 +02:00
1cc809c332 Merge branch 'minor-next' into feat/anvil 2024-08-19 19:04:21 +01:00
b1a773ceb8 Merge branch 'minor-next' into feat/anvil 2024-08-11 00:06:05 +02:00
654b44447e add sound and anvil damage 2024-08-10 23:51:38 +02:00
804731d87f fix PHPstan 2024-08-10 23:33:14 +02:00
726e2cba23 Add anvil event 2024-08-10 23:26:07 +02:00
54f746fc11 finalize anvil transaction 2024-08-10 23:16:54 +02:00
44c3e03598 fix PHPstan 2024-08-10 19:10:10 +02:00
e4f979dadf first look at anvil 2024-08-10 19:04:17 +02:00
19 changed files with 1194 additions and 3 deletions

View File

@ -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);
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\crafting\AnvilCraftResult;
use pocketmine\item\Item;
use pocketmine\Server;
final class AnvilHelper{
private const COST_LIMIT = 39;
/**
* Attempts to calculate the result of an anvil operation.
*
* Returns null if the operation can't do anything.
*/
public static function calculateResult(Item $base, Item $material, ?string $customName, bool $isCreative) : ?AnvilCraftResult{
$recipe = Server::getInstance()->getCraftingManager()->matchAnvilRecipe($base, $material);
if($recipe === null){
return null;
}
$result = $recipe->getResultFor($base, $material);
if($result !== null){
$resultItem = $result->getOutput();
$xpCost = $result->getXpCost();
if(($customName === null || $customName === "") && $resultItem->hasCustomName()){
$xpCost++;
$resultItem->clearCustomName();
}elseif($customName !== null && $resultItem->getCustomName() !== $customName){
$xpCost++;
$resultItem->setCustomName($customName);
}
$result = new AnvilCraftResult($xpCost, $resultItem, $result->getSacrificeResult());
}
if($result === null || $result->getXpCost() <= 0 || ($result->getXpCost() > self::COST_LIMIT && !$isCreative)){
return null;
}
return $result;
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
/**
* This class is here to hold the result of an anvil crafting process.
*/
final class AnvilCraftResult{
/**
* @param Item|null $sacrificeResult If the given item is considered as null (count <= 0), the value will be set to null.
*/
public function __construct(
private int $xpCost,
private Item $output,
private ?Item $sacrificeResult
){
if($this->sacrificeResult !== null && $this->sacrificeResult->isNull()){
$this->sacrificeResult = null;
}
}
/**
* Represent the amount of experience points required to craft the output item.
*/
public function getXpCost() : int{
return $this->xpCost;
}
/**
* Represent the item given as output of the crafting process.
*/
public function getOutput() : Item{
return $this->output;
}
/**
* This result has to be null if the sacrifice slot need to be emptied.
* If not null, it represent the item that will be left in the sacrifice slot after the crafting process.
*/
public function getSacrificeResult() : ?Item{
return $this->sacrificeResult;
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Durable;
use pocketmine\item\ToolTier;
use pocketmine\item\VanillaArmorMaterials;
use pocketmine\item\VanillaItems;
use pocketmine\world\format\io\GlobalItemDataHandlers;
final class AnvilCraftingManagerDataFiller{
public static function fillData(CraftingManager $manager) : CraftingManager{
foreach([
[
VanillaItems::DIAMOND(),
[VanillaArmorMaterials::DIAMOND(), ToolTier::DIAMOND]
], [
VanillaItems::GOLD_INGOT(),
[VanillaArmorMaterials::GOLD(), ToolTier::GOLD]
], [
VanillaItems::IRON_INGOT(),
[VanillaArmorMaterials::IRON(), ToolTier::IRON]
], [
VanillaItems::NETHERITE_INGOT(),
[VanillaArmorMaterials::NETHERITE(), ToolTier::NETHERITE]
], [
VanillaItems::SCUTE(),
[VanillaArmorMaterials::TURTLE(), null]
], [
VanillaItems::LEATHER(),
[VanillaArmorMaterials::LEATHER(), null]
]
] as [$item, [$armorMaterial, $toolTier]]){
$manager->registerAnvilRecipe(new MaterialRepairRecipe(
new ArmorRecipeIngredient($armorMaterial),
new ExactRecipeIngredient($item)
));
if($toolTier !== null){
$manager->registerAnvilRecipe(new MaterialRepairRecipe(
new TieredToolRecipeIngredient($toolTier),
new ExactRecipeIngredient($item)
));
}
}
foreach(VanillaItems::getAll() as $item){
if($item instanceof Durable){
$itemId = GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName();
$manager->registerAnvilRecipe(new ItemSelfCombineRecipe(
new MetaWildcardRecipeIngredient($itemId)
));
}
}
return $manager;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
interface AnvilRecipe{
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult;
}

View File

@ -0,0 +1,50 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Armor;
use pocketmine\item\ArmorMaterial;
use pocketmine\item\Item;
use function spl_object_id;
class ArmorRecipeIngredient implements RecipeIngredient{
public function __construct(
private ArmorMaterial $material
){
}
public function getMaterial() : ArmorMaterial{ return $this->material; }
public function accepts(Item $item) : bool{
if($item->getCount() < 1){
return false;
}
return $item instanceof Armor && $item->getMaterial() === $this->material;
}
public function __toString() : string{
return "ArmorRecipeIngredient(ArmorMaterial@" . spl_object_id($this->material) . ")";
}
}

View File

@ -80,6 +80,18 @@ class CraftingManager{
*/
private array $brewingRecipeCache = [];
/**
* @var AnvilRecipe[]
* @phpstan-var list<AnvilRecipe>
*/
private array $anvilRecipes = [];
/**
* @var AnvilRecipe[][]
* @phpstan-var array<int, array<int, AnvilRecipe>>
*/
private array $anvilRecipeCache = [];
/** @phpstan-var ObjectSet<\Closure() : void> */
private ObjectSet $recipeRegisteredCallbacks;
@ -187,6 +199,14 @@ class CraftingManager{
return $this->potionContainerChangeRecipes;
}
/**
* @return AnvilRecipe[][]
* @phpstan-return list<AnvilRecipe>
*/
public function getAnvilRecipes() : array{
return $this->anvilRecipes;
}
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
$this->craftingRecipeIndex[] = $recipe;
@ -221,6 +241,14 @@ class CraftingManager{
}
}
public function registerAnvilRecipe(AnvilRecipe $recipe) : void{
$this->anvilRecipes[] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){
$callback();
}
}
/**
* @param Item[] $outputs
*/
@ -294,4 +322,21 @@ class CraftingManager{
return null;
}
public function matchAnvilRecipe(Item $input, Item $material) : ?AnvilRecipe{
$inputHash = $input->getStateId();
$materialHash = $material->getStateId();
$cached = $this->anvilRecipeCache[$inputHash][$materialHash] ?? null;
if($cached !== null){
return $cached;
}
foreach($this->anvilRecipes as $recipe){
if($recipe->getResultFor($input, $material) !== null){
return $this->anvilRecipeCache[$inputHash][$materialHash] = $recipe;
}
}
return null;
}
}

View File

@ -185,6 +185,7 @@ final class CraftingManagerFromDataHelper{
/**
* @param mixed[] $data
*
* @return object[]
*
* @phpstan-template TRecipeData of object
@ -210,7 +211,7 @@ final class CraftingManagerFromDataHelper{
$result = new CraftingManager();
foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shapeless_crafting.json'), ShapelessRecipeData::class) as $recipe){
$recipeType = match($recipe->block){
$recipeType = match ($recipe->block) {
"crafting_table" => ShapelessRecipeType::CRAFTING,
"stonecutter" => ShapelessRecipeType::STONECUTTER,
"smithing_table" => ShapelessRecipeType::SMITHING,
@ -271,7 +272,7 @@ final class CraftingManagerFromDataHelper{
));
}
foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'smelting.json'), FurnaceRecipeData::class) as $recipe){
$furnaceType = match ($recipe->block){
$furnaceType = match ($recipe->block) {
"furnace" => FurnaceType::FURNACE,
"blast_furnace" => FurnaceType::BLAST_FURNACE,
"smoker" => FurnaceType::SMOKER,
@ -333,6 +334,8 @@ final class CraftingManagerFromDataHelper{
));
}
$result = AnvilCraftingManagerDataFiller::fillData($result);
//TODO: smithing
return $result;

View File

@ -0,0 +1,123 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Durable;
use pocketmine\item\EnchantedBook;
use pocketmine\item\enchantment\AvailableEnchantmentRegistry;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\Rarity;
use pocketmine\item\Item;
use function floor;
use function max;
use function min;
abstract class ItemCombineRecipe implements AnvilRecipe{
abstract protected function validate(Item $input, Item $material) : bool;
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult{
if($this->validate($input, $material)){
$result = (clone $input);
$xpCost = 0;
if($result instanceof Durable && $material instanceof Durable && $this->repair($result, $material)){
// The two items are compatible for repair
$xpCost = 2;
}
// combining enchantments
foreach($material->getEnchantments() as $instance){
$enchantment = $instance->getType();
$level = $instance->getLevel();
if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $input)){
continue;
}
if(($targetEnchantment = $input->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($input->getEnchantments() as $testedInstance){
$testedEnchantment = $testedInstance->getType();
if(!$testedEnchantment->isCompatibleWith($enchantment)){
$xpCost++;
//TODO: XP COST
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($material instanceof EnchantedBook){
// Enchanted books are half as expensive to combine
$costAddition = max(1, $costAddition / 2);
}
$levelDifference = $instance->getLevel() - $input->getEnchantmentLevel($instance->getType());
$xpCost += (int) floor($costAddition * $levelDifference);
$result->addEnchantment($instance);
$xpCost += 2 ** $input->getAnvilRepairCost() - 1;
$xpCost += 2 ** $material->getAnvilRepairCost() - 1;
$result->setAnvilRepairCost(
max($result->getAnvilRepairCost(), $material->getAnvilRepairCost()) + 1
);
}
if($xpCost !== 0){
return new AnvilCraftResult($xpCost, $result, null);
}
}
return null;
}
private function repair(Durable $result, Durable $material) : bool{
$damage = $result->getDamage();
if($damage === 0){
return false;
}
$baseMaxDurability = $result->getMaxDurability();
$baseDurability = $baseMaxDurability - $damage;
$materialDurability = $material->getMaxDurability() - $material->getDamage();
$addDurability = (int) ($baseMaxDurability * 12 / 100);
$result->setDamage($baseMaxDurability - min(
$baseMaxDurability,
$baseDurability + $materialDurability + $addDurability
));
return true;
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class ItemDifferentCombineRecipe extends ItemCombineRecipe{
public function __construct(
private RecipeIngredient $base,
private RecipeIngredient $material
){
}
protected function validate(Item $input, Item $material) : bool{
return $this->base->accepts($input) && $this->material->accepts($material);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class ItemSelfCombineRecipe extends ItemCombineRecipe{
/**
* @param RecipeIngredient $target The item that will be concerned by the combinaison.
* The input and material have to be accepted by this ingredient to be able to combine them.
*/
public function __construct(
private RecipeIngredient $target
){
}
protected function validate(Item $input, Item $material) : bool{
return $this->target->accepts($input) && $this->target->accepts($material);
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use function ceil;
use function floor;
use function max;
use function min;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class MaterialRepairRecipe implements AnvilRecipe{
public function __construct(
private RecipeIngredient $input,
private RecipeIngredient $material
){
}
public function getInput() : RecipeIngredient{
return $this->input;
}
public function getMaterial() : RecipeIngredient{
return $this->material;
}
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult{
if($this->input->accepts($input) && $this->material->accepts($material) && $input instanceof Durable){
$damage = $input->getDamage();
if($damage !== 0){
$quarter = min($damage, (int) floor($input->getMaxDurability() / 4));
$numberRepair = min($material->getCount(), (int) ceil($damage / $quarter));
$damage -= $quarter * $numberRepair;
return new AnvilCraftResult(
$numberRepair,
(clone $input)->setDamage(max(0, $damage)),
(clone $material)->setCount($material->getCount() - $numberRepair)
);
}
}
return null;
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
use pocketmine\item\TieredTool;
use pocketmine\item\ToolTier;
class TieredToolRecipeIngredient implements RecipeIngredient{
public function __construct(
private ToolTier $tier
){
}
public function getTier() : ToolTier{ return $this->tier; }
public function accepts(Item $item) : bool{
if($item->getCount() < 1){
return false;
}
return $item instanceof TieredTool && $item->getTier() === $this->tier;
}
public function __toString() : string{
return "TieredToolRecipeIngredient(" . $this->tier->name . ")";
}
}

View File

@ -0,0 +1,86 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\event\player;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\item\Item;
use pocketmine\player\Player;
/**
* Called when a player uses an anvil (renaming, repairing, combining items).
* This event is called once per action even if multiple tasks are performed at once.
*/
class PlayerUseAnvilEvent extends PlayerEvent implements Cancellable{
use CancellableTrait;
public function __construct(
Player $player,
private Item $baseItem,
private ?Item $materialItem,
private Item $resultItem,
private ?string $customName,
private int $xpCost
){
$this->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;
}
}

View File

@ -0,0 +1,155 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory\transaction;
use pocketmine\block\Anvil;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\utils\AnvilHelper;
use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\AnvilCraftResult;
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;
private ?Item $materialItem = null;
public function __construct(
Player $source,
private readonly AnvilCraftResult $expectedResult,
private readonly ?string $customName
){
parent::__construct($source);
}
private function validateFiniteResources(int $xpSpent) : void{
$expectedXpCost = $this->expectedResult->getXpCost();
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) : ?int{
$calculAttempt = AnvilHelper::calculateResult($base, $material, $this->customName, $this->source->isCreative());
if($calculAttempt === null){
return null;
}
$result = $calculAttempt->getOutput();
if(!$result->equalsExact($expectedOutput)){
return null;
}
$this->baseItem = $base;
$this->materialItem = $material;
return $calculAttempt->getXpCost();
}
public function validate() : void{
if(count($this->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);
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){
$xpCost = $this->validateInputs($inputs[0], VanillaItems::AIR(), $outputItem) ??
throw new TransactionValidationException("Inputs do not match expected result");
}else{
$xpCost = $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($xpCost);
}
}
public function execute() : void{
parent::execute();
if($this->source->hasFiniteResources()){
$this->source->getXpManager()->subtractXpLevels($this->expectedResult->getXpCost());
}
$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{
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->getOutput(), $this->customName, $this->expectedResult->getXpCost());
$ev->call();
return !$ev->isCancelled();
}
}

View File

@ -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 $anvilRepairCost = 0;
/** TODO: this needs to die in a fire */
protected ?CompoundTag $blockEntityTag = null;
@ -282,6 +284,30 @@ class Item implements \JsonSerializable{
return $this;
}
/**
* 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 getAnvilRepairCost() : int{
return $this->anvilRepairCost;
}
/**
* 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 setAnvilRepairCost(int $cost) : self{
$this->anvilRepairCost = $cost;
return $this;
}
/**
* @throws NbtException
*/
@ -338,6 +364,7 @@ class Item implements \JsonSerializable{
}
$this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0;
$this->anvilRepairCost = $tag->getInt(self::TAG_REPAIR_COST, 0);
}
protected function serializeCompoundTag(CompoundTag $tag) : void{
@ -406,6 +433,12 @@ class Item implements \JsonSerializable{
}else{
$tag->removeTag(self::TAG_KEEP_ON_DEATH);
}
if($this->anvilRepairCost > 0){
$tag->setInt(self::TAG_REPAIR_COST, $this->anvilRepairCost);
}else{
$tag->removeTag(self::TAG_REPAIR_COST);
}
}
public function getCount() : int{

View File

@ -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;
@ -290,7 +294,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{
@ -348,6 +352,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($window->getInput(), $window->getMaterial(), $this->request->getFilterStrings()[0] ?? null, $this->player->isCreative());
if($result !== null){
$this->specialTransaction = new AnvilTransaction($this->player, $result, $this->request->getFilterStrings()[0] ?? null);
$this->setNextCreatedItem($result->getOutput());
}
}
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
$this->assertDoingCrafting();
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance

View File

@ -0,0 +1,194 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use Generator;
use PHPUnit\Framework\TestCase;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use function floor;
class AnvilCraftTest extends TestCase{
public static function materialRepairRecipeProvider() : Generator{
yield "No repair available" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND(),
null
];
yield "Repair one damage" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(1),
VanillaItems::DIAMOND(),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair one damage with more materials than expected" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(1),
VanillaItems::DIAMOND()->setCount(2),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), VanillaItems::DIAMOND())
];
$diamondPickaxeQuarter = (int) floor(VanillaItems::DIAMOND_PICKAXE()->getMaxDurability() / 4);
yield "Repair one quarter" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter),
VanillaItems::DIAMOND()->setCount(1),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair one quarter plus 1" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter + 1),
VanillaItems::DIAMOND()->setCount(1),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE()->setDamage(1), null)
];
yield "Repair more than one quarter" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter * 2),
VanillaItems::DIAMOND()->setCount(2),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair more than one quarter with more materials than expected" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter * 2),
VanillaItems::DIAMOND()->setCount(3),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE(), VanillaItems::DIAMOND()->setCount(1))
];
}
/**
* @dataProvider materialRepairRecipeProvider
*/
public function testMaterialRepairRecipe(Item $base, Item $material, ?AnvilCraftResult $expected) : void{
$recipe = new MaterialRepairRecipe(
new WildcardRecipeIngredient(),
new WildcardRecipeIngredient()
);
self::assertAnvilCraftResultEquals($expected, $recipe->getResultFor($base, $material));
}
public static function itemSelfCombineRecipeProvider() : Generator{
yield "Combine two identical items without damage and enchants" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE(),
null
];
yield "Enchant on base item and no enchants on material with no damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
VanillaItems::DIAMOND_PICKAXE(),
null
];
yield "No enchant on base item and one enchant on material" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)), null)
];
yield "Combine two identical items with damage" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(10),
VanillaItems::DIAMOND_PICKAXE(),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE()->setDamage(0), null)
];
yield "Combine two identical items with damage for material" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE()->setDamage(10),
null
];
yield "Combine two identical items with different enchantments" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with different enchantments with damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))->setDamage(10),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(4, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with different enchantments with damage for material" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1))->setDamage(10),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with same enchantment level" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
new AnvilCraftResult(8, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2)), null)
];
yield "Combine two identical items with same enchantment level and damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1))->setDamage(10),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
new AnvilCraftResult(10, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2))->setDamage(0), null)
];
yield "Combine two identical items with same enchantment level and damage for material" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1))->setDamage(10),
new AnvilCraftResult(8, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2)), null)
];
}
/**
* @dataProvider itemSelfCombineRecipeProvider
*/
public function testItemSelfCombineRecipe(Item $base, Item $combined, ?AnvilCraftResult $expected) : void{
$recipe = new ItemSelfCombineRecipe(new WildcardRecipeIngredient());
self::assertAnvilCraftResultEquals($expected, $recipe->getResultFor($base, $combined));
}
private static function assertAnvilCraftResultEquals(?AnvilCraftResult $expected, ?AnvilCraftResult $actual) : void{
if($expected === null){
self::assertNull($actual, "Recipe did not match expected result");
return;
}else{
self::assertNotNull($actual, "Recipe did not match expected result");
}
self::assertEquals($expected->getXpCost(), $actual->getXpCost(), "XP cost did not match expected result");
self::assertTrue($expected->getOutput()->equalsExact($actual->getOutput()), "Recipe output did not match expected result");
$sacrificeResult = $expected->getSacrificeResult();
if($sacrificeResult !== null){
$resultExpected = $actual->getSacrificeResult();
self::assertNotNull($resultExpected, "Recipe sacrifice result did not match expected result");
self::assertTrue($sacrificeResult->equalsExact($resultExpected), "Recipe sacrifice result did not match expected result");
}else{
self::assertNull($actual->getSacrificeResult(), "Recipe sacrifice result did not match expected result");
}
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
final class WildcardRecipeIngredient implements RecipeIngredient{
public function __toString() : string{
return "WildcardRecipeIngredient()";
}
public function accepts(Item $item) : bool{
return true;
}
}