Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e4f979d
first look at anvil
ShockedPlot7560 Aug 10, 2024
44c3e03
fix PHPstan
ShockedPlot7560 Aug 10, 2024
54f746f
finalize anvil transaction
ShockedPlot7560 Aug 10, 2024
726e2cb
Add anvil event
ShockedPlot7560 Aug 10, 2024
804731d
fix PHPstan
ShockedPlot7560 Aug 10, 2024
654b444
add sound and anvil damage
ShockedPlot7560 Aug 10, 2024
b1a773c
Merge branch 'minor-next' into feat/anvil
ShockedPlot7560 Aug 10, 2024
1cc809c
Merge branch 'minor-next' into feat/anvil
dktapps Aug 19, 2024
7cfb6ee
first look at anvil actions
ShockedPlot7560 Aug 19, 2024
b9df798
made AnvilAction constructor final
ShockedPlot7560 Aug 19, 2024
c77a72f
some work on anvil
ShockedPlot7560 Nov 18, 2024
947c8a0
remove phpstan docs
ShockedPlot7560 Nov 18, 2024
5b9dc2c
rewrote the system with CraftingManager
ShockedPlot7560 Dec 14, 2024
b3f0ed2
Merge remote-tracking branch 'upstream/minor-next' into feat/anvil
ShockedPlot7560 Dec 14, 2024
51d45be
Merge branch 'minor-next' into feat/anvil
dktapps Mar 9, 2025
a1695a5
Merge remote-tracking branch 'upstream/feat/anvil' into feat/anvil
ShockedPlot7560 Mar 22, 2025
b4cb09f
continue refactoring, not finished, not tested
ShockedPlot7560 Mar 22, 2025
11135c2
remove dead code
ShockedPlot7560 Mar 22, 2025
f346799
fill in CraftingManager for MaterialRepair
ShockedPlot7560 Mar 22, 2025
eaab4b3
add unit testing on MaterialRepairRecipe
ShockedPlot7560 Mar 22, 2025
084feae
more work on ItemCombine
ShockedPlot7560 Mar 23, 2025
2c7043d
Add validation for negative XP cost in AnvilCraftResult
ShockedPlot7560 Nov 5, 2025
373d2d6
Merge branch 'minor-next' into feat/anvil
ShockedPlot7560 Nov 5, 2025
072e4ae
Add AnvilUseEvent
ShockedPlot7560 Nov 7, 2025
b402ce6
Introduce enchanted books
ShockedPlot7560 Nov 7, 2025
a9bd3fb
fix CS
ShockedPlot7560 Nov 7, 2025
996f3a5
Use MetaWildcard instead for enchanted book
ShockedPlot7560 Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/block/inventory/AnvilInventory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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);
}
}
65 changes: 65 additions & 0 deletions src/block/utils/AnvilHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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;
}
}
71 changes: 71 additions & 0 deletions src/crafting/AnvilCraftResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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 int $xpCost The experience points cost required to craft the output item. (positive integer, 0 means no cost)
* @param Item $output The item given as output of the crafting process.
* @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->xpCost < 0){
throw new \InvalidArgumentException("XP cost cannot be negative");
}
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;
}
}
87 changes: 87 additions & 0 deletions src/crafting/AnvilCraftingManagerDataFiller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?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\data\bedrock\item\ItemTypeNames;
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)
));
}
}

$manager->registerAnvilRecipe(new ItemSelfCombineRecipe(
new MetaWildcardRecipeIngredient(ItemTypeNames::ENCHANTED_BOOK)
));

foreach(VanillaItems::getAll() as $item){
if($item instanceof Durable){
$itemId = GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName();
$manager->registerAnvilRecipe(new ItemSelfCombineRecipe(
new MetaWildcardRecipeIngredient($itemId)
));
$manager->registerAnvilRecipe(new ItemDifferentCombineRecipe(
new MetaWildcardRecipeIngredient($itemId),
new MetaWildcardRecipeIngredient(ItemTypeNames::ENCHANTED_BOOK)
));
}
}

return $manager;
}
}
30 changes: 30 additions & 0 deletions src/crafting/AnvilRecipe.php
Original file line number Diff line number Diff line change
@@ -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;
}
50 changes: 50 additions & 0 deletions src/crafting/ArmorRecipeIngredient.php
Original file line number Diff line number Diff line change
@@ -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) . ")";
}
}
45 changes: 45 additions & 0 deletions src/crafting/CraftingManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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;

Expand Down Expand Up @@ -190,6 +202,14 @@ public function getPotionContainerChangeRecipes() : array{
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;
Expand Down Expand Up @@ -224,6 +244,14 @@ public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe
}
}

public function registerAnvilRecipe(AnvilRecipe $recipe) : void{
$this->anvilRecipes[] = $recipe;

foreach($this->recipeRegisteredCallbacks as $callback){
$callback();
}
}

/**
* @param Item[] $outputs
*/
Expand Down Expand Up @@ -297,4 +325,21 @@ public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingReci

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;
}
}
Loading
Loading