From 5b134cd5b5479900058384544d77824e4fea3575 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:20:22 +0200 Subject: [PATCH 1/6] refactor(recipes): replace CraftingObject with RecipeIngredient and encapsulate RecipeRegistry Introduce RecipeIngredient to model custom and vanilla ingredients with O(1) ID resolution, on-demand ItemStack generation, and safe String ID factory overloads. Encapsulate recipe storage within RecipeRegistry with deterministic matching, and migrate all recipe builders, preview menus, and ritual offerings. --- .../content/recipes/ForgeRecipes.java | 9 +- .../entities/rituals/AncientRitual.java | 18 ++-- .../rituals/menu/AncientRitualsMenu.java | 4 +- .../rituals/menu/RitualAltarMenu.java | 18 ++-- .../engine/recipes/CraftingObject.java | 84 --------------- .../engine/recipes/RecipeIngredient.java | 101 ++++++++++++++++++ .../engine/recipes/RecipePreviewManager.java | 15 +-- .../engine/recipes/ShapeEncoder.java | 34 +++--- .../recipes/crafting/BlightedRecipe.java | 61 +++-------- .../crafting/BlightedShapedRecipe.java | 23 ++-- .../crafting/BlightedShapelessRecipe.java | 22 ++-- .../crafting/builder/ShapedRecipeBuilder.java | 8 +- .../builder/ShapelessRecipeBuilder.java | 22 ++-- .../menu/CraftingRecipePreviewMenu.java | 56 ++++------ .../crafting/menu/CraftingTableMenu.java | 16 +-- .../recipes/crafting/menu/RecipeBookMenu.java | 3 +- .../crafting/registry/RecipeRegistry.java | 73 +++++++++++-- .../engine/recipes/forging/ForgeRecipe.java | 30 +++--- .../recipes/forging/menu/ForgeMenu.java | 18 ++-- .../forging/menu/ForgeRecipePreviewMenu.java | 30 +++--- .../forging/menu/ForgeRecipesMenu.java | 4 +- .../moussax/blightedSMP/utils/Utilities.java | 12 +-- .../engine/recipes/RecipeIngredientTest.java | 50 +++++++++ 23 files changed, 379 insertions(+), 332 deletions(-) delete mode 100644 blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/CraftingObject.java create mode 100644 blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java create mode 100644 blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredientTest.java diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/recipes/ForgeRecipes.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/recipes/ForgeRecipes.java index 1a8e392e..47b2442a 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/recipes/ForgeRecipes.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/recipes/ForgeRecipes.java @@ -1,8 +1,7 @@ package fr.moussax.blightedSMP.content.recipes; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeRecipe; -import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; import fr.moussax.blightedSMP.registry.RegistryModule; import java.util.function.Consumer; import org.bukkit.Material; @@ -13,9 +12,9 @@ public void register(Consumer registry) { var plasmaBucket = ForgeRecipe.Builder.of("PLASMA_BUCKET", 1) .fuelCost(10000) .ingredients( - new CraftingObject(ItemRegistry.get("MAGMA_BUCKET"), 1), - new CraftingObject(ItemRegistry.get("MAGMA_BUCKET"), 1), - new CraftingObject(Material.NETHER_STAR, 1) + RecipeIngredient.of("MAGMA_BUCKET", 1), + RecipeIngredient.of("MAGMA_BUCKET", 1), + RecipeIngredient.of(Material.NETHER_STAR, 1) ) .build(); diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/AncientRitual.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/AncientRitual.java index b33b717b..9e6075dd 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/AncientRitual.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/AncientRitual.java @@ -2,7 +2,7 @@ import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; import lombok.Getter; import org.bukkit.Material; @@ -23,14 +23,14 @@ public final class AncientRitual { private final AncientCreature summonedCreature; - private final List offerings; + private final List offerings; private final ItemStack displayedItem; private final int gemsCost; private final int levelCost; private AncientRitual( AncientCreature summonedCreature, - List offerings, + List offerings, ItemStack displayedItem, int gemsCost, int levelCost @@ -51,7 +51,7 @@ private AncientRitual( public static final class Builder { private final AncientCreature summonedCreature; - private final List offerings = new ArrayList<>(); + private final List offerings = new ArrayList<>(); private ItemStack displayedItem; private int gemsCost; private int levelCost; @@ -96,7 +96,7 @@ public Builder displayedItem( * @param offering required offering * @return this builder */ - public Builder addOffering(CraftingObject offering) { + public Builder addOffering(RecipeIngredient offering) { this.offerings.add(offering); return this; } @@ -109,7 +109,7 @@ public Builder addOffering(CraftingObject offering) { * @return this builder */ public Builder addOffering(Material material, int amount) { - return addOffering(new CraftingObject(material, amount)); + return addOffering(RecipeIngredient.of(material, amount)); } /** @@ -120,7 +120,7 @@ public Builder addOffering(Material material, int amount) { * @return this builder */ public Builder addOffering(BlightedItem item, int amount) { - return addOffering(new CraftingObject(item, amount)); + return addOffering(RecipeIngredient.of(item, amount)); } /** @@ -131,7 +131,7 @@ public Builder addOffering(BlightedItem item, int amount) { * @return this builder */ public Builder addOffering(String itemId, int amount) { - return addOffering(ItemRegistry.get(itemId), amount); + return addOffering(RecipeIngredient.of(itemId, amount)); } /** @@ -140,7 +140,7 @@ public Builder addOffering(String itemId, int amount) { * @param offerings offerings required by the ritual * @return this builder */ - public Builder offerings(CraftingObject... offerings) { + public Builder offerings(RecipeIngredient... offerings) { this.offerings.addAll(List.of(offerings)); return this; } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/AncientRitualsMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/AncientRitualsMenu.java index e4e999af..16df2f54 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/AncientRitualsMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/AncientRitualsMenu.java @@ -6,7 +6,7 @@ import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedSMP.engine.entities.rituals.AncientRitual; import fr.moussax.blightedSMP.engine.entities.rituals.registry.RitualRegistry; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.utils.Utilities; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -88,7 +88,7 @@ private ItemStack buildRiteDisplayItem(AncientRitual ritual) { } builder.addLore(" §7Offerings required: "); - for (CraftingObject offering : ritual.getOfferings()) { + for (RecipeIngredient offering : ritual.getOfferings()) { builder.addLore(" §8‣ " + Utilities.extractIngredientName(offering) + " §8x" + offering.getAmount()); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/RitualAltarMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/RitualAltarMenu.java index 805dba00..26c15fd0 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/RitualAltarMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/entities/rituals/menu/RitualAltarMenu.java @@ -11,7 +11,7 @@ import fr.moussax.blightedSMP.engine.entities.rituals.AncientCreature; import fr.moussax.blightedSMP.engine.entities.rituals.AncientRitual; import fr.moussax.blightedSMP.engine.entities.rituals.RitualAnimations; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.player.BlightedPlayer; import fr.moussax.blightedSMP.utils.Utilities; import org.bukkit.Bukkit; @@ -156,7 +156,7 @@ private Map aggregateRequirements() { Map counts = new HashMap<>(); if (ritual == null) return counts; - for (CraftingObject ingredient : ritual.getOfferings()) { + for (RecipeIngredient ingredient : ritual.getOfferings()) { counts.merge(ingredient.getId(), ingredient.getAmount(), Integer::sum); } return counts; @@ -164,17 +164,13 @@ private Map aggregateRequirements() { private void displayRequiredIngredients() { for (int i = 0; i < ritual.getOfferings().size() && i < GRID_SLOTS.length; i++) { - CraftingObject ingredient = ritual.getOfferings().get(i); + RecipeIngredient ingredient = ritual.getOfferings().get(i); setItem(GRID_SLOTS[i], createDisplayItem(ingredient)); } } - private ItemStack createDisplayItem(CraftingObject ingredient) { - ItemStack item = ingredient.isCustom() - ? Objects.requireNonNull(ingredient.getManager()).toItemStack().clone() - : Objects.requireNonNull(ingredient.getVanillaItem()).clone(); - item.setAmount(ingredient.getAmount()); - return item; + private ItemStack createDisplayItem(RecipeIngredient ingredient) { + return ingredient.toItemStack(); } private void setupStatusPanes() { @@ -223,7 +219,7 @@ private void setupInvokeButton() { builder.setDisplayName("§5Invoke " + creatureName) .addLore("", " §7Offerings required:"); - for (CraftingObject ingredient : ritual.getOfferings()) { + for (RecipeIngredient ingredient : ritual.getOfferings()) { builder.addLore(" §8‣ " + Utilities.extractIngredientName(ingredient) + " §8x" + ingredient.getAmount()); } @@ -277,7 +273,7 @@ private void setupNavigationButtons() { } private void consumeIngredients(Player player) { - for (CraftingObject ingredient : ritual.getOfferings()) { + for (RecipeIngredient ingredient : ritual.getOfferings()) { Utilities.consumeItemsFromInventory(player, ingredient); } BlightedPlayer.get(player).removeGems(ritual.getGemsCost()); diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/CraftingObject.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/CraftingObject.java deleted file mode 100644 index 5a5174e8..00000000 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/CraftingObject.java +++ /dev/null @@ -1,84 +0,0 @@ -package fr.moussax.blightedSMP.engine.recipes; - -import fr.moussax.blightedSMP.engine.items.BlightedItem; -import lombok.Getter; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -/** - * Represents an ingredient used in a crafting recipe, which can be either - * a custom item managed by {@link BlightedItem} or a vanilla {@link Material}. - */ -public final class CraftingObject { - - @Getter - private final BlightedItem manager; - private final ItemStack vanillaItem; - @Getter - private final int amount; - private final String itemId; - - /** - * Creates a crafting object representing a custom item. - * - * @param manager the {@link BlightedItem} managing the custom item - * @param amount the required quantity - */ - public CraftingObject(BlightedItem manager, int amount) { - this.manager = manager; - this.vanillaItem = null; - this.amount = amount; - this.itemId = manager != null ? manager.getItemId() : ""; - } - - /** - * Creates a crafting object representing a vanilla item. - * - * @param material the {@link Material} type - * @param amount the required quantity - */ - public CraftingObject(Material material, int amount) { - this.manager = null; - this.vanillaItem = new ItemStack(material, amount); - this.amount = amount; - this.itemId = "vanilla:" + material.name(); - } - - /** - * Returns a clone of the vanilla item stack. - * - * @return a cloned {@link ItemStack} or {@code null} if this is a custom item - */ - public ItemStack getVanillaItem() { - return vanillaItem == null ? null : vanillaItem.clone(); - } - - /** - * Checks whether this crafting object represents a custom item. - * - * @return {@code true} if custom, {@code false} otherwise - */ - public boolean isCustom() { - return manager != null; - } - - - /** - * Checks whether this crafting object represents a vanilla item. - * - * @return {@code true} if vanilla, {@code false} otherwise - */ - public boolean isVanilla() { - return vanillaItem != null; - } - - /** - * Returns the unique identifier of this crafting object. - * Custom items use their manager's ID; vanilla items use the format {@code vanilla:MATERIAL_NAME}. - * - * @return the item identifier - */ - public String getId() { - return itemId; - } -} diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java new file mode 100644 index 00000000..e67eae69 --- /dev/null +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java @@ -0,0 +1,101 @@ +package fr.moussax.blightedSMP.engine.recipes; + +import fr.moussax.blightedSMP.engine.items.BlightedItem; +import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; +import lombok.Getter; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +/** + * Represents an ingredient used in a crafting or forging recipe, which can be either + * a custom item managed by {@link BlightedItem} or a vanilla {@link Material}. + */ +@Getter +public final class RecipeIngredient { + + @Nullable + private final BlightedItem item; + + @Nullable + private final Material material; + + private final int amount; + + private final String id; + + public RecipeIngredient(@NonNull BlightedItem item, int amount) { + this.item = Objects.requireNonNull(item, "item cannot be null"); + this.material = null; + this.amount = amount; + this.id = item.getItemId(); + } + + public RecipeIngredient(@NonNull Material material, int amount) { + this.item = null; + this.material = Objects.requireNonNull(material, "material cannot be null"); + this.amount = amount; + this.id = "vanilla:" + material.name(); + } + + public static RecipeIngredient of(@NonNull BlightedItem item, int amount) { + return new RecipeIngredient(item, amount); + } + + public static RecipeIngredient of(@NonNull Material material, int amount) { + return new RecipeIngredient(material, amount); + } + + public static RecipeIngredient of(@NonNull BlightedItem item) { + return new RecipeIngredient(item, 1); + } + + public static RecipeIngredient of(@NonNull Material material) { + return new RecipeIngredient(material, 1); + } + + public static RecipeIngredient of(@NonNull String itemId, int amount) { + return new RecipeIngredient(ItemRegistry.getOrThrow(itemId), amount); + } + + public static RecipeIngredient of(@NonNull String itemId) { + return of(itemId, 1); + } + + public boolean isCustom() { + return item != null; + } + + public boolean isVanilla() { + return material != null; + } + + /** + * Creates an ItemStack representing this ingredient for display or inventory operations. + * + * @return constructed item stack + */ + public ItemStack toItemStack() { + if (item != null) { + ItemStack stack = item.toItemStack(); + stack.setAmount(amount); + return stack; + } + return new ItemStack(Objects.requireNonNull(material), amount); + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof RecipeIngredient other)) return false; + return amount == other.amount && Objects.equals(id, other.id); + } + + @Override + public int hashCode() { + return Objects.hash(id, amount); + } +} diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipePreviewManager.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipePreviewManager.java index 6ddd7bbf..191de171 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipePreviewManager.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipePreviewManager.java @@ -5,6 +5,7 @@ import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapedRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapelessRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.menu.CraftingRecipePreviewMenu; +import fr.moussax.blightedSMP.engine.recipes.crafting.registry.RecipeRegistry; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeRecipe; import fr.moussax.blightedSMP.engine.recipes.forging.menu.ForgeRecipePreviewMenu; import fr.moussax.blightedSMP.engine.recipes.forging.registry.ForgeRegistry; @@ -32,7 +33,7 @@ private RecipePreviewManager() { public static List getAllRecipesForItem(@NonNull BlightedItem targetItem) { List recipes = new ArrayList<>(); - for (BlightedRecipe blightedRecipe : BlightedRecipe.REGISTERED_RECIPES) { + for (BlightedRecipe blightedRecipe : RecipeRegistry.getAll()) { if (blightedRecipe != null && blightedRecipe.getResult() != null && blightedRecipe.getResult().equals(targetItem)) { recipes.add(blightedRecipe); } @@ -44,7 +45,7 @@ public static List getAllRecipesForItem(@NonNull BlightedItem targetItem } } - for (BlightedRecipe blightedRecipe : BlightedRecipe.REGISTERED_RECIPES) { + for (BlightedRecipe blightedRecipe : RecipeRegistry.getAll()) { if (isIngredientInCraftingRecipe(targetItem, blightedRecipe) && !recipes.contains(blightedRecipe)) { recipes.add(blightedRecipe); } @@ -60,7 +61,7 @@ public static List getAllRecipesForItem(@NonNull BlightedItem targetItem } private static boolean isIngredientInCraftingRecipe(BlightedItem targetItem, BlightedRecipe blightedRecipe) { - List ingredients; + List ingredients; if (blightedRecipe instanceof BlightedShapedRecipe shapedRecipe) { ingredients = shapedRecipe.getRecipe(); } else if (blightedRecipe instanceof BlightedShapelessRecipe shapelessRecipe) { @@ -69,8 +70,8 @@ private static boolean isIngredientInCraftingRecipe(BlightedItem targetItem, Bli return false; } - for (CraftingObject ingredient : ingredients) { - if (ingredient != null && ingredient.isCustom() && targetItem.equals(ingredient.getManager())) { + for (RecipeIngredient ingredient : ingredients) { + if (ingredient != null && ingredient.isCustom() && targetItem.equals(ingredient.getItem())) { return true; } } @@ -79,8 +80,8 @@ private static boolean isIngredientInCraftingRecipe(BlightedItem targetItem, Bli private static boolean isIngredientInForgeRecipe(BlightedItem targetItem, ForgeRecipe forgeRecipe) { if (forgeRecipe.getIngredients() == null) return false; - for (CraftingObject ingredient : forgeRecipe.getIngredients()) { - if (ingredient != null && ingredient.isCustom() && targetItem.equals(ingredient.getManager())) { + for (RecipeIngredient ingredient : forgeRecipe.getIngredients()) { + if (ingredient != null && ingredient.isCustom() && targetItem.equals(ingredient.getItem())) { return true; } } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/ShapeEncoder.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/ShapeEncoder.java index 835f39e2..5c0062d6 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/ShapeEncoder.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/ShapeEncoder.java @@ -7,10 +7,10 @@ import java.util.HashMap; /** - * Encodes a 3x3 shaped crafting recipe into a list of {@link CraftingObject}s. + * Encodes a 3x3 shaped crafting recipe into a list of {@link RecipeIngredient}s. *

* Each recipe shape is defined by three lines of three characters each, - * where each character represents a crafting ingredient bound to a {@link CraftingObject}. + * where each character represents a crafting ingredient bound to a {@link RecipeIngredient}. * Spaces or unbound characters are treated as empty slots. */ public final class ShapeEncoder { @@ -18,7 +18,7 @@ public final class ShapeEncoder { private final String line2; private final String line3; - private final HashMap keys = new HashMap<>(); + private final HashMap keys = new HashMap<>(); /** * Constructs a shape encoder for a 3x3 crafting recipe. @@ -38,24 +38,24 @@ public ShapeEncoder(String line1, String line2, String line3) { } /** - * Binds a shape character to a {@link CraftingObject}. + * Binds a shape character to a {@link RecipeIngredient}. * - * @param key the character representing the ingredient in the recipe shape - * @param object the crafting object to associate with this key + * @param key the character representing the ingredient in the recipe shape + * @param ingredient the recipe ingredient to associate with this key */ - public void bindKey(char key, CraftingObject object) { - keys.put(key, object); + public void bindKey(char key, RecipeIngredient ingredient) { + keys.put(key, ingredient); } /** * Binds a shape character to a custom item managed by {@link BlightedItem}. * - * @param key the character representing the ingredient - * @param manager the item manager for the custom item - * @param amount the quantity required + * @param key the character representing the ingredient + * @param item the custom item + * @param amount the quantity required */ - public void bindKey(char key, BlightedItem manager, int amount) { - keys.put(key, new CraftingObject(manager, amount)); + public void bindKey(char key, BlightedItem item, int amount) { + keys.put(key, new RecipeIngredient(item, amount)); } /** @@ -66,11 +66,11 @@ public void bindKey(char key, BlightedItem manager, int amount) { * @param amount the quantity required */ public void bindKey(char key, Material material, int amount) { - keys.put(key, new CraftingObject(material, amount)); + keys.put(key, new RecipeIngredient(material, amount)); } /** - * Converts the 3x3 shape and bound keys into a list of 9 crafting objects, + * Converts the 3x3 shape and bound keys into a list of 9 recipe ingredients, * corresponding to the recipe's slots in row-major order. *

* Empty or unbound characters produce {@code null} entries. @@ -78,8 +78,8 @@ public void bindKey(char key, Material material, int amount) { * @return a list of 9 elements representing the recipe * @throws RuntimeException if the encoded list does not contain exactly 9 elements */ - public ArrayList encodeCraftingRecipe() { - ArrayList objects = new ArrayList<>(); + public ArrayList encodeCraftingRecipe() { + ArrayList objects = new ArrayList<>(); String[] lines = {line1, line2, line3}; for (String line : lines) { diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedRecipe.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedRecipe.java index 74914cf6..be9cac16 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedRecipe.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedRecipe.java @@ -1,7 +1,8 @@ package fr.moussax.blightedSMP.engine.recipes.crafting; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; +import fr.moussax.blightedSMP.engine.recipes.crafting.registry.RecipeRegistry; import fr.moussax.blightedSMP.utils.Utilities; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -19,11 +20,6 @@ */ public sealed abstract class BlightedRecipe permits BlightedShapedRecipe, BlightedShapelessRecipe { - /** - * Global registry of all registered Blighted recipes. - */ - public static final Set REGISTERED_RECIPES = new HashSet<>(); - /** * @return the logical result definition of this recipe */ @@ -44,10 +40,10 @@ public sealed abstract class BlightedRecipe permits BlightedShapedRecipe, Blight public abstract ItemStack assemble(List craftingGrid); /** - * Registers this recipe in the global registry. + * Registers this recipe in the central {@link RecipeRegistry}. */ public void addRecipe() { - REGISTERED_RECIPES.add(this); + RecipeRegistry.register(this); } /** @@ -57,33 +53,13 @@ public void addRecipe() { * @return matching recipes, or empty if none match */ public static Set findMatchingRecipes(List craftingGrid) { - boolean isEmpty = craftingGrid.stream() - .allMatch(item -> item == null || item.getType() == Material.AIR); - if (isEmpty) return Collections.emptySet(); - - List craftingGridItemIds = resolveItemIdsFromGrid(craftingGrid); - Set matchingRecipes = new HashSet<>(); - - for (BlightedRecipe recipe : REGISTERED_RECIPES) { - if (recipe.getResult() == null) continue; - - boolean isMatch = switch (recipe) { - case BlightedShapedRecipe shapedRecipe -> - matchesShapedRecipe(shapedRecipe, craftingGrid, craftingGridItemIds); - case BlightedShapelessRecipe shapelessRecipe -> - matchesShapelessRecipe(shapelessRecipe, craftingGrid, craftingGridItemIds); - }; - - if (isMatch) matchingRecipes.add(recipe); - } - - return matchingRecipes; + return RecipeRegistry.findMatchingRecipes(craftingGrid); } /** * Resolves custom or vanilla item IDs for each grid slot. */ - private static List resolveItemIdsFromGrid(List craftingGrid) { + public static List resolveItemIdsFromGrid(List craftingGrid) { List itemIdsInGrid = new ArrayList<>(craftingGrid.size()); for (ItemStack stack : craftingGrid) { @@ -99,32 +75,23 @@ private static List resolveItemIdsFromGrid(List craftingGrid) /** * Checks whether a shaped recipe matches the crafting grid exactly. */ - private static boolean matchesShapedRecipe(BlightedShapedRecipe recipe, - List craftingGrid, - List craftingGridItemIds) { + public static boolean matchesShapedRecipe(BlightedShapedRecipe recipe, + List craftingGrid, + List craftingGridItemIds) { - List expectedPattern = recipe.getRecipe(); + List expectedPattern = recipe.getRecipe(); if (expectedPattern.size() != craftingGrid.size()) return false; for (int slotIndex = 0; slotIndex < expectedPattern.size(); slotIndex++) { - CraftingObject expectedSlot = expectedPattern.get(slotIndex); + RecipeIngredient expectedSlot = expectedPattern.get(slotIndex); String currentItemId = craftingGridItemIds.get(slotIndex); - if (expectedSlot == null || (expectedSlot.getManager() == null && !expectedSlot.isVanilla())) { + if (expectedSlot == null) { if (!currentItemId.isEmpty()) return false; continue; } - String expectedItemId; - if (expectedSlot.isCustom()) { - expectedItemId = Objects.requireNonNull(expectedSlot.getManager()).getItemId(); - } else if (expectedSlot.isVanilla()) { - expectedItemId = "vanilla:" + Objects.requireNonNull(expectedSlot.getVanillaItem()).getType().name(); - } else { - return false; - } - - if (!currentItemId.equals(expectedItemId)) return false; + if (!currentItemId.equals(expectedSlot.getId())) return false; ItemStack currentStack = craftingGrid.get(slotIndex); if (currentStack == null || currentStack.getAmount() < expectedSlot.getAmount()) return false; @@ -136,7 +103,7 @@ private static boolean matchesShapedRecipe(BlightedShapedRecipe recipe, /** * Checks whether a shapeless recipe matches the crafting grid. */ - private static boolean matchesShapelessRecipe(BlightedShapelessRecipe recipe, List craftingGrid, List craftingGridItemIds) { + public static boolean matchesShapelessRecipe(BlightedShapelessRecipe recipe, List craftingGrid, List craftingGridItemIds) { Map remainingRequiredCounts = new HashMap<>(recipe.getIngredientCountMap()); for (int slotIndex = 0; slotIndex < craftingGrid.size(); slotIndex++) { diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapedRecipe.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapedRecipe.java index a89be4f6..5eae2154 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapedRecipe.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapedRecipe.java @@ -1,8 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.crafting; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; -import fr.moussax.blightedSMP.utils.Utilities; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import org.bukkit.inventory.ItemStack; import java.util.*; @@ -16,7 +15,7 @@ public final class BlightedShapedRecipe extends BlightedRecipe { private final BlightedItem resultBlightedItem; private final int resultAmount; - private final List recipePattern = new ArrayList<>(); + private final List recipePattern = new ArrayList<>(); /** * Index of the slot used as the attribute source. @@ -60,18 +59,18 @@ public ItemStack assemble(List craftingGrid) { } /** @return immutable view of the recipe pattern */ - public List getRecipe() { + public List getRecipe() { return Collections.unmodifiableList(recipePattern); } /** Replaces the entire recipe pattern. */ - public void setRecipe(List recipePattern) { + public void setRecipe(List recipePattern) { this.recipePattern.clear(); this.recipePattern.addAll(recipePattern); } /** Appends a single ingredient to the recipe pattern. */ - public void addIngredient(CraftingObject ingredient) { + public void addIngredient(RecipeIngredient ingredient) { this.recipePattern.add(ingredient); } @@ -94,17 +93,9 @@ public void setAttributeSourceSlot(int slotIndex) { */ public Map getIngredientCountMap() { var ingredientCountMap = new HashMap(); - for (CraftingObject ingredient : recipePattern) { + for (RecipeIngredient ingredient : recipePattern) { if (ingredient == null) continue; - String itemId; - if (ingredient.isCustom()) { - itemId = Objects.requireNonNull(ingredient.getManager()).getItemId(); - } else if (ingredient.isVanilla()) { - itemId = Utilities.resolveItemId(Objects.requireNonNull(ingredient.getVanillaItem()), "vanilla:"); - } else { - throw new IllegalArgumentException("Ingredient must be custom or vanilla"); - } - ingredientCountMap.merge(itemId, ingredient.getAmount(), Integer::sum); + ingredientCountMap.merge(ingredient.getId(), ingredient.getAmount(), Integer::sum); } return Collections.unmodifiableMap(ingredientCountMap); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapelessRecipe.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapelessRecipe.java index 576d9926..f3e90f14 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapelessRecipe.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/BlightedShapelessRecipe.java @@ -1,7 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.crafting; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.utils.Utilities; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -15,7 +15,7 @@ * a designated ingredient instance found in the crafting grid. */ public final class BlightedShapelessRecipe extends BlightedRecipe { - private final List ingredientList = new ArrayList<>(); + private final List ingredientList = new ArrayList<>(); private final Map ingredientCountMap = new HashMap<>(); private final BlightedItem resultBlightedItem; private final int resultAmount; @@ -24,7 +24,7 @@ public final class BlightedShapelessRecipe extends BlightedRecipe { * Ingredient acting as the attribute source. * {@code null} disables attribute transfer. */ - private CraftingObject attributeSourceIngredient = null; + private RecipeIngredient attributeSourceIngredient = null; public BlightedShapelessRecipe(BlightedItem resultBlightedItem) { this(resultBlightedItem, 1); @@ -78,17 +78,9 @@ public ItemStack assemble(List craftingGrid) { * * @param ingredient recipe ingredient */ - public void addIngredient(CraftingObject ingredient) { + public void addIngredient(RecipeIngredient ingredient) { ingredientList.add(ingredient); - String itemId; - if (ingredient.isCustom()) { - itemId = Objects.requireNonNull(ingredient.getManager()).getItemId(); - } else if (ingredient.isVanilla()) { - itemId = Utilities.resolveItemId(Objects.requireNonNull(ingredient.getVanillaItem()), "vanilla:"); - } else { - throw new IllegalArgumentException("Ingredient must be custom or vanilla"); - } - ingredientCountMap.merge(itemId, ingredient.getAmount(), Integer::sum); + ingredientCountMap.merge(ingredient.getId(), ingredient.getAmount(), Integer::sum); } /** @@ -96,7 +88,7 @@ public void addIngredient(CraftingObject ingredient) { * * @param ingredient registered ingredient */ - public void setAttributeSource(CraftingObject ingredient) { + public void setAttributeSource(RecipeIngredient ingredient) { if (!ingredientList.contains(ingredient)) { throw new IllegalArgumentException("Attribute source must be a registered ingredient of this recipe"); } @@ -104,7 +96,7 @@ public void setAttributeSource(CraftingObject ingredient) { } /** @return immutable list of registered ingredients */ - public List getIngredients() { + public List getIngredients() { return Collections.unmodifiableList(ingredientList); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapedRecipeBuilder.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapedRecipeBuilder.java index 71a84e7a..6d961192 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapedRecipeBuilder.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapedRecipeBuilder.java @@ -1,7 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.crafting.builder; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapedRecipe; import fr.moussax.blightedSMP.engine.recipes.ShapeEncoder; import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; @@ -24,7 +24,7 @@ public final class ShapedRecipeBuilder { private String line1; private String line2; private String line3; - private final Map bindings = new HashMap<>(); + private final Map bindings = new HashMap<>(); private int attributeSourceSlot = -1; private ShapedRecipeBuilder(BlightedItem result, int amount) { @@ -96,7 +96,7 @@ public ShapedRecipeBuilder bind(char key, Material material) { * @return this builder */ public ShapedRecipeBuilder bind(char key, Material material, int amount) { - bindings.put(key, new CraftingObject(material, amount)); + bindings.put(key, new RecipeIngredient(material, amount)); return this; } @@ -121,7 +121,7 @@ public ShapedRecipeBuilder bind(char key, BlightedItem item) { * @return this builder */ public ShapedRecipeBuilder bind(char key, BlightedItem item, int amount) { - bindings.put(key, new CraftingObject(item, amount)); + bindings.put(key, new RecipeIngredient(item, amount)); return this; } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapelessRecipeBuilder.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapelessRecipeBuilder.java index 53155a4b..d1f3f03e 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapelessRecipeBuilder.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/builder/ShapelessRecipeBuilder.java @@ -2,7 +2,7 @@ import fr.moussax.blightedSMP.engine.items.BlightedItem; import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapelessRecipe; import org.bukkit.Material; @@ -21,8 +21,8 @@ public final class ShapelessRecipeBuilder { private final BlightedItem result; private final int amount; - private final List ingredients = new ArrayList<>(); - private CraftingObject attributeSource = null; + private final List ingredients = new ArrayList<>(); + private RecipeIngredient attributeSource = null; private ShapelessRecipeBuilder(BlightedItem result, int amount) { this.result = Objects.requireNonNull(result, "result"); @@ -115,9 +115,9 @@ public ShapelessRecipeBuilder addIngredient( int amount, boolean isAttributeSource ) { - CraftingObject craftingObject = new CraftingObject(material, amount); - ingredients.add(craftingObject); - if (isAttributeSource) this.attributeSource = craftingObject; + RecipeIngredient ingredient = RecipeIngredient.of(material, amount); + ingredients.add(ingredient); + if (isAttributeSource) this.attributeSource = ingredient; return this; } @@ -158,9 +158,9 @@ public ShapelessRecipeBuilder addIngredient( int amount, boolean isAttributeSource ) { - CraftingObject craftingObject = new CraftingObject(item, amount); - ingredients.add(craftingObject); - if (isAttributeSource) this.attributeSource = craftingObject; + RecipeIngredient ingredient = RecipeIngredient.of(item, amount); + ingredients.add(ingredient); + if (isAttributeSource) this.attributeSource = ingredient; return this; } @@ -203,7 +203,7 @@ public ShapelessRecipeBuilder addIngredient( int amount, boolean isAttributeSource ) { - return addIngredient(ItemRegistry.get(itemId), amount, isAttributeSource); + return addIngredient(ItemRegistry.getOrThrow(itemId), amount, isAttributeSource); } /** @@ -216,7 +216,7 @@ public ShapelessRecipeBuilder addIngredient( */ public BlightedShapelessRecipe build() { BlightedShapelessRecipe recipe = new BlightedShapelessRecipe(result, amount); - for (CraftingObject ingredient : ingredients) { + for (RecipeIngredient ingredient : ingredients) { recipe.addIngredient(ingredient); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingRecipePreviewMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingRecipePreviewMenu.java index 43461dea..c4865b51 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingRecipePreviewMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingRecipePreviewMenu.java @@ -1,7 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.crafting.menu; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.RecipePreviewManager; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapedRecipe; @@ -99,35 +99,35 @@ private void setupRecipeVisualization(Player player) { } private void setupShapedRecipeGrid(BlightedShapedRecipe shapedRecipe) { - List pattern = shapedRecipe.getRecipe(); + List pattern = shapedRecipe.getRecipe(); for (int i = 0; i < pattern.size() && i < CRAFTING_GRID_SLOTS.length; i++) { - CraftingObject craftingObject = pattern.get(i); + RecipeIngredient ingredient = pattern.get(i); - if (craftingObject == null) { + if (ingredient == null) { setItem(CRAFTING_GRID_SLOTS[i], new ItemStack(Material.AIR)); continue; } - ItemStack ingredientItem = createIngredientDisplay(craftingObject); + ItemStack ingredientItem = createIngredientDisplay(ingredient); setItem(CRAFTING_GRID_SLOTS[i], ingredientItem, (clickingPlayer, _) -> { - if (!craftingObject.isCustom() || craftingObject.getManager() == null) return; - RecipePreviewManager.openPreview(clickingPlayer, craftingObject.getManager(), this); + if (!ingredient.isCustom() || ingredient.getItem() == null) return; + RecipePreviewManager.openPreview(clickingPlayer, ingredient.getItem(), this); }); } } private void setupShapelessRecipeGrid(BlightedShapelessRecipe shapelessRecipe) { - List ingredients = shapelessRecipe.getIngredients(); + List ingredients = shapelessRecipe.getIngredients(); for (int i = 0; i < CRAFTING_GRID_SLOTS.length; i++) { if (i < ingredients.size()) { - CraftingObject ingredient = ingredients.get(i); + RecipeIngredient ingredient = ingredients.get(i); ItemStack ingredientItem = createIngredientDisplay(ingredient); setItem(CRAFTING_GRID_SLOTS[i], ingredientItem, (clickingPlayer, _) -> { - if (!ingredient.isCustom() || ingredient.getManager() == null) return; - RecipePreviewManager.openPreview(clickingPlayer, ingredient.getManager(), this); + if (!ingredient.isCustom() || ingredient.getItem() == null) return; + RecipePreviewManager.openPreview(clickingPlayer, ingredient.getItem(), this); }); } else { setItem(CRAFTING_GRID_SLOTS[i], new ItemStack(Material.AIR)); @@ -192,9 +192,9 @@ private void executeQuickcraft(Player player, Map requir } for (IngredientInfo info : requirements.values()) { - CraftingObject consumeObject = info.ingredient.isCustom() - ? new CraftingObject(Objects.requireNonNull(info.ingredient.getManager()), info.amount) - : new CraftingObject(Objects.requireNonNull(info.ingredient.getVanillaItem()).getType(), info.amount); + RecipeIngredient consumeObject = info.ingredient.isCustom() + ? RecipeIngredient.of(Objects.requireNonNull(info.ingredient.getItem()), info.amount) + : RecipeIngredient.of(Objects.requireNonNull(info.ingredient.getMaterial()), info.amount); Utilities.consumeItemsFromInventory(player, consumeObject); } @@ -216,12 +216,12 @@ private void executeQuickcraft(Player player, Map requir private List createVirtualCraftingGrid() { if (recipe instanceof BlightedShapedRecipe shapedRecipe) { return shapedRecipe.getRecipe().stream() - .map(object -> object != null ? getCraftingObjectItem(object) : null) + .map(ingredient -> ingredient != null ? ingredient.toItemStack() : null) .toList(); } if (recipe instanceof BlightedShapelessRecipe shapelessRecipe) { return shapelessRecipe.getIngredients().stream() - .map(object -> object != null ? getCraftingObjectItem(object) : null) + .map(ingredient -> ingredient != null ? ingredient.toItemStack() : null) .toList(); } return List.of(); @@ -229,7 +229,7 @@ private List createVirtualCraftingGrid() { private Map aggregateRecipeIngredients(BlightedRecipe recipe) { Map map = new LinkedHashMap<>(); - List rawList; + List rawList; if (recipe instanceof BlightedShapedRecipe shapedRecipe) { rawList = shapedRecipe.getRecipe().stream().filter(Objects::nonNull).toList(); } else if (recipe instanceof BlightedShapelessRecipe shapelessRecipe) { @@ -238,7 +238,7 @@ private Map aggregateRecipeIngredients(BlightedRecipe re rawList = List.of(); } - for (CraftingObject ingredient : rawList) { + for (RecipeIngredient ingredient : rawList) { String ingredientId = ingredient.getId(); if (ingredientId.isEmpty()) continue; if (!map.containsKey(ingredientId)) { @@ -252,31 +252,21 @@ private Map aggregateRecipeIngredients(BlightedRecipe re } private static class IngredientInfo { - final CraftingObject ingredient; + final RecipeIngredient ingredient; int amount; - IngredientInfo(CraftingObject ingredient, int amount) { + IngredientInfo(RecipeIngredient ingredient, int amount) { this.ingredient = ingredient; this.amount = amount; } } - private ItemStack createIngredientDisplay(CraftingObject craftingObject) { - ItemStack ingredientItem = getCraftingObjectItem(craftingObject); - ingredientItem.setAmount(Math.max(1, craftingObject.getAmount())); + private ItemStack createIngredientDisplay(RecipeIngredient ingredient) { + ItemStack ingredientItem = ingredient.toItemStack(); + ingredientItem.setAmount(Math.max(1, ingredient.getAmount())); return ingredientItem; } - private ItemStack getCraftingObjectItem(CraftingObject craftingObject) { - if (craftingObject.isCustom() && craftingObject.getManager() != null) { - return craftingObject.getManager().toItemStack().clone(); - } - if (craftingObject.isVanilla() && craftingObject.getVanillaItem() != null) { - return craftingObject.getVanillaItem().clone(); - } - return new ItemStack(Material.AIR); - } - private void setupNavigation() { RecipePreviewManager.setupNavigation(this, recipe, targetItem, previousMenu); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingTableMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingTableMenu.java index dae4d398..72bf77fe 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingTableMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/CraftingTableMenu.java @@ -3,7 +3,8 @@ import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapedRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapelessRecipe; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; +import fr.moussax.blightedSMP.engine.recipes.crafting.registry.RecipeRegistry; import fr.moussax.bedrock.ui.menu.types.InteractiveMenu; import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; import fr.moussax.bedrock.utils.ItemBuilder; @@ -117,10 +118,10 @@ private void consumeIngredients(BlightedRecipe recipe, int times) { } private void consumeShaped(BlightedShapedRecipe recipe, int times) { - List pattern = recipe.getRecipe(); + List pattern = recipe.getRecipe(); for (int i = 0; i < INPUT_SLOTS.length; i++) { if (i >= pattern.size()) break; - CraftingObject requiredIngredient = pattern.get(i); + RecipeIngredient requiredIngredient = pattern.get(i); if (requiredIngredient == null) continue; ItemStack stack = inventory.getItem(INPUT_SLOTS[i]); @@ -171,10 +172,10 @@ private void consumeShapeless(BlightedShapelessRecipe recipe, int times) { private int getMaxCraftCount(BlightedRecipe recipe) { if (recipe instanceof BlightedShapedRecipe shaped) { int max = Integer.MAX_VALUE; - List pattern = shaped.getRecipe(); + List pattern = shaped.getRecipe(); for (int i = 0; i < INPUT_SLOTS.length; i++) { - if (i >= pattern.size()) continue; - CraftingObject requiredIngredient = pattern.get(i); + if (i >= pattern.size()) break; + RecipeIngredient requiredIngredient = pattern.get(i); if (requiredIngredient == null) continue; ItemStack stack = inventory.getItem(INPUT_SLOTS[i]); @@ -220,8 +221,7 @@ private List getInputGrid() { } private BlightedRecipe getMatchingRecipe() { - Set matches = BlightedRecipe.findMatchingRecipes(getInputGrid()); - return matches.isEmpty() ? null : matches.iterator().next(); + return RecipeRegistry.findFirstMatching(getInputGrid()).orElse(null); } private void updateOutputSlot(BlightedRecipe recipe) { diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/RecipeBookMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/RecipeBookMenu.java index 4840c54a..d34058f8 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/RecipeBookMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/menu/RecipeBookMenu.java @@ -1,6 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.crafting.menu; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedRecipe; +import fr.moussax.blightedSMP.engine.recipes.crafting.registry.RecipeRegistry; import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; import org.bukkit.Material; @@ -18,7 +19,7 @@ public final class RecipeBookMenu extends PaginatedMenu { public RecipeBookMenu(@org.jspecify.annotations.Nullable Menu previousMenu) { super("Recipes", 54, previousMenu != null ? previousMenu : new CraftingTableMenu()); - this.cachedRecipes = new ArrayList<>(BlightedRecipe.REGISTERED_RECIPES); + this.cachedRecipes = new ArrayList<>(RecipeRegistry.getAll()); this.cachedRecipes.sort((firstRecipe, secondRecipe) -> { String firstName = firstRecipe.getResult().getDisplayName(); String secondName = secondRecipe.getResult().getDisplayName(); diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/registry/RecipeRegistry.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/registry/RecipeRegistry.java index 5d5f6982..55aba017 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/registry/RecipeRegistry.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/crafting/registry/RecipeRegistry.java @@ -1,25 +1,29 @@ package fr.moussax.blightedSMP.engine.recipes.crafting.registry; import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedRecipe; +import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapedRecipe; +import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedShapelessRecipe; import fr.moussax.blightedSMP.engine.recipes.crafting.builder.ShapedRecipeBuilder; import fr.moussax.blightedSMP.engine.recipes.crafting.builder.ShapelessRecipeBuilder; import fr.moussax.blightedSMP.registry.RegistryModule; import fr.moussax.bedrock.utils.debug.Log; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; -import java.util.List; +import java.util.*; import java.util.function.Consumer; /** * Central registry for custom {@link BlightedRecipe} definitions. * *

Recipes are provided by registered {@link RegistryModule} implementations - * and added to the global recipe collection when {@link #initialize(List)} is - * called. The registry also provides factory methods for creating shaped and - * shapeless recipe builders.

+ * and added to the recipe collection when {@link #initialize(List)} is called.

*/ public final class RecipeRegistry { + private static final Set RECIPES = new LinkedHashSet<>(); + private RecipeRegistry() { } @@ -33,8 +37,10 @@ private RecipeRegistry() { */ public static void initialize(List>> modules) { clear(); - modules.forEach(module -> module.register(RecipeRegistry::register)); - Log.success("RecipesRegistry", "Registered " + BlightedRecipe.REGISTERED_RECIPES.size() + " custom recipes."); + if (modules != null) { + modules.forEach(module -> module.register(RecipeRegistry::register)); + } + Log.success("RecipeRegistry", "Registered " + RECIPES.size() + " custom recipes."); } /** @@ -43,7 +49,7 @@ public static void initialize(List>> mod * @param recipe the recipe to register */ public static void register(@NonNull BlightedRecipe recipe) { - recipe.addRecipe(); + RECIPES.add(Objects.requireNonNull(recipe, "recipe cannot be null")); } /** @@ -57,6 +63,57 @@ public static void register(BlightedRecipe... recipes) { } } + /** + * Returns an unmodifiable collection of all registered recipes. + * + * @return unmodifiable collection of recipes + */ + public static Collection getAll() { + return Collections.unmodifiableCollection(RECIPES); + } + + /** + * Resolves all recipes matching the given crafting grid. + * + * @param craftingGrid 3×3 grid in row-major order + * @return set of matching recipes + */ + public static Set findMatchingRecipes(List craftingGrid) { + if (craftingGrid == null || craftingGrid.isEmpty()) return Collections.emptySet(); + + boolean isEmpty = craftingGrid.stream() + .allMatch(item -> item == null || item.getType() == Material.AIR); + if (isEmpty) return Collections.emptySet(); + + List craftingGridItemIds = BlightedRecipe.resolveItemIdsFromGrid(craftingGrid); + Set matchingRecipes = new LinkedHashSet<>(); + + for (BlightedRecipe recipe : RECIPES) { + if (recipe.getResult() == null) continue; + + boolean isMatch = switch (recipe) { + case BlightedShapedRecipe shapedRecipe -> + BlightedRecipe.matchesShapedRecipe(shapedRecipe, craftingGrid, craftingGridItemIds); + case BlightedShapelessRecipe shapelessRecipe -> + BlightedRecipe.matchesShapelessRecipe(shapelessRecipe, craftingGrid, craftingGridItemIds); + }; + + if (isMatch) matchingRecipes.add(recipe); + } + + return matchingRecipes; + } + + /** + * Resolves the first recipe matching the given crafting grid. + * + * @param craftingGrid 3×3 grid in row-major order + * @return matching recipe, or empty if none match + */ + public static Optional findFirstMatching(List craftingGrid) { + return findMatchingRecipes(craftingGrid).stream().findFirst(); + } + /** * Creates a builder for a shaped custom recipe. * @@ -93,6 +150,6 @@ public static ShapelessRecipeBuilder shapelessRecipe(String resultId, int amount * Removes all recipes currently registered in the recipe registry. */ public static void clear() { - BlightedRecipe.REGISTERED_RECIPES.clear(); + RECIPES.clear(); } } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/ForgeRecipe.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/ForgeRecipe.java index 2fc0a1dc..f1852a6e 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/ForgeRecipe.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/ForgeRecipe.java @@ -1,7 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.forging; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; import lombok.Getter; import org.bukkit.Material; @@ -22,16 +22,14 @@ public final class ForgeRecipe { private final BlightedItem forgedItem; private final int forgedAmount; - private final List ingredients; + private final List ingredients; private final int fuelCost; private ForgeRecipe( BlightedItem forgedItem, int forgedAmount, - List ingredients, - int fuelCost, - int gemsCost, - int levelCost + List ingredients, + int fuelCost ) { this.forgedItem = forgedItem; this.forgedAmount = forgedAmount; @@ -43,16 +41,14 @@ private ForgeRecipe( * Fluent builder for constructing a {@link ForgeRecipe}. * *

The builder supports vanilla materials, registered custom items, - * and complete {@link CraftingObject} instances as ingredients.

+ * and complete {@link RecipeIngredient} instances as ingredients.

*/ public static final class Builder { private final BlightedItem forgedItem; private final int forgedAmount; - private final List ingredients = new ArrayList<>(); + private final List ingredients = new ArrayList<>(); private int fuelCost; - private int gemsCost; - private int levelCost; private Builder(BlightedItem forgedItem, int forgedAmount) { if (forgedItem == null) { @@ -94,7 +90,7 @@ public static Builder of(String itemId, int amount) { * @param ingredient required ingredient * @return this builder */ - public Builder addIngredient(CraftingObject ingredient) { + public Builder addIngredient(RecipeIngredient ingredient) { this.ingredients.add(ingredient); return this; } @@ -107,7 +103,7 @@ public Builder addIngredient(CraftingObject ingredient) { * @return this builder */ public Builder addIngredient(Material material, int amount) { - return addIngredient(new CraftingObject(material, amount)); + return addIngredient(RecipeIngredient.of(material, amount)); } /** @@ -118,7 +114,7 @@ public Builder addIngredient(Material material, int amount) { * @return this builder */ public Builder addIngredient(BlightedItem item, int amount) { - return addIngredient(new CraftingObject(item, amount)); + return addIngredient(RecipeIngredient.of(item, amount)); } /** @@ -129,7 +125,7 @@ public Builder addIngredient(BlightedItem item, int amount) { * @return this builder */ public Builder addIngredient(String itemId, int amount) { - return addIngredient(ItemRegistry.get(itemId), amount); + return addIngredient(RecipeIngredient.of(itemId, amount)); } /** @@ -138,7 +134,7 @@ public Builder addIngredient(String itemId, int amount) { * @param ingredients ingredients required by the recipe * @return this builder */ - public Builder ingredients(CraftingObject... ingredients) { + public Builder ingredients(RecipeIngredient... ingredients) { this.ingredients.addAll(List.of(ingredients)); return this; } @@ -168,9 +164,7 @@ public ForgeRecipe build() { forgedItem, forgedAmount, ingredients, - fuelCost, - gemsCost, - levelCost + fuelCost ); } } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeMenu.java index 151a1807..9a18e11f 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeMenu.java @@ -1,7 +1,7 @@ package fr.moussax.blightedSMP.engine.recipes.forging.menu; import fr.moussax.blightedSMP.content.sound.BlightedSounds; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeFuel; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeRecipe; import fr.moussax.bedrock.ui.menu.Menu; @@ -140,7 +140,7 @@ private void checkRequirements(Player player) { private Map aggregateRequirements() { Map counts = new HashMap<>(); - for (CraftingObject ingredient : recipe.getIngredients()) { + for (RecipeIngredient ingredient : recipe.getIngredients()) { String id = ingredient.getId(); counts.put(id, counts.getOrDefault(id, 0) + ingredient.getAmount()); } @@ -149,18 +149,14 @@ private Map aggregateRequirements() { private void displayRequiredIngredients() { for (int i = 0; i < recipe.getIngredients().size() && i < GRID_SLOTS.length; i++) { - CraftingObject ingredient = recipe.getIngredients().get(i); + RecipeIngredient ingredient = recipe.getIngredients().get(i); ItemStack displayItem = createDisplayItem(ingredient); setItem(GRID_SLOTS[i], displayItem); } } - private ItemStack createDisplayItem(CraftingObject ingredient) { - ItemStack displayItem = ingredient.isCustom() - ? Objects.requireNonNull(ingredient.getManager()).toItemStack().clone() - : Objects.requireNonNull(ingredient.getVanillaItem()).clone(); - displayItem.setAmount(ingredient.getAmount()); - return displayItem; + private ItemStack createDisplayItem(RecipeIngredient ingredient) { + return ingredient.toItemStack(); } private void setupStatusPanes() { @@ -202,7 +198,7 @@ private void setupForgeButton() { builder.setDisplayName("§6Forging..."); } else { builder.setDisplayName("§aConfirm process").addLore("", " §7Items required: "); - for (CraftingObject ingredient : recipe.getIngredients()) { + for (RecipeIngredient ingredient : recipe.getIngredients()) { builder.addLore(" §8‣ " + Utilities.extractIngredientName(ingredient) + " §8x" + ingredient.getAmount()); } @@ -464,7 +460,7 @@ private void setStatusSlot(int slot, Material material) { } private void consumeIngredients(Player player) { - for (CraftingObject ingredient : recipe.getIngredients()) { + for (RecipeIngredient ingredient : recipe.getIngredients()) { Utilities.consumeItemsFromInventory(player, ingredient); } } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipePreviewMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipePreviewMenu.java index 79af6c00..8c399222 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipePreviewMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipePreviewMenu.java @@ -2,7 +2,7 @@ import fr.moussax.blightedSMP.content.sound.BlightedSounds; import fr.moussax.blightedSMP.engine.items.BlightedItem; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.RecipePreviewManager; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeRecipe; import fr.moussax.blightedSMP.engine.player.BlightedPlayer; @@ -130,24 +130,20 @@ private void setupIngredientGrid() { .addLore("§7This slot isn't used for", "§7the selected recipe."); fillSlots(GRID_SLOTS, emptySlotBuilder.toItemStack()); - List ingredients = recipe.getIngredients(); + List ingredients = recipe.getIngredients(); for (int i = 0; i < ingredients.size() && i < GRID_SLOTS.length; i++) { - CraftingObject ingredient = ingredients.get(i); + RecipeIngredient ingredient = ingredients.get(i); ItemStack displayItem = createIngredientDisplay(ingredient); setItem(GRID_SLOTS[i], displayItem, (clickingPlayer, _) -> { - if (!ingredient.isCustom() || ingredient.getManager() == null) return; - RecipePreviewManager.openPreview(clickingPlayer, ingredient.getManager(), this); + if (!ingredient.isCustom() || ingredient.getItem() == null) return; + RecipePreviewManager.openPreview(clickingPlayer, ingredient.getItem(), this); }); } } - private ItemStack createIngredientDisplay(CraftingObject ingredient) { - ItemStack displayItem = ingredient.isCustom() - ? Objects.requireNonNull(ingredient.getManager()).toItemStack().clone() - : Objects.requireNonNull(ingredient.getVanillaItem()).clone(); - displayItem.setAmount(Math.max(1, ingredient.getAmount())); - return displayItem; + private ItemStack createIngredientDisplay(RecipeIngredient ingredient) { + return ingredient.toItemStack(); } private void setupResultDisplay() { @@ -241,9 +237,9 @@ private void executeHyperforge(Player player, BlightedPlayer blightedPlayer, Map } for (IngredientInfo info : requirements.values()) { - CraftingObject consumeObject = info.ingredient.isCustom() - ? new CraftingObject(Objects.requireNonNull(info.ingredient.getManager()), info.amount) - : new CraftingObject(Objects.requireNonNull(info.ingredient.getVanillaItem()).getType(), info.amount); + RecipeIngredient consumeObject = info.ingredient.isCustom() + ? RecipeIngredient.of(Objects.requireNonNull(info.ingredient.getItem()), info.amount) + : RecipeIngredient.of(Objects.requireNonNull(info.ingredient.getMaterial()), info.amount); Utilities.consumeItemsFromInventory(player, consumeObject); } @@ -265,7 +261,7 @@ private void executeHyperforge(Player player, BlightedPlayer blightedPlayer, Map private Map aggregateForgeIngredients(ForgeRecipe recipe) { Map recipeIngredients = new LinkedHashMap<>(); - for (CraftingObject ingredient : recipe.getIngredients()) { + for (RecipeIngredient ingredient : recipe.getIngredients()) { if (ingredient == null) continue; String ingredientId = ingredient.getId(); if (ingredientId.isEmpty()) continue; @@ -280,10 +276,10 @@ private Map aggregateForgeIngredients(ForgeRecipe recipe } private static class IngredientInfo { - final CraftingObject ingredient; + final RecipeIngredient ingredient; int amount; - IngredientInfo(CraftingObject ingredient, int amount) { + IngredientInfo(RecipeIngredient ingredient, int amount) { this.ingredient = ingredient; this.amount = amount; } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipesMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipesMenu.java index 78681649..fe791dc5 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipesMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/forging/menu/ForgeRecipesMenu.java @@ -1,6 +1,6 @@ package fr.moussax.blightedSMP.engine.recipes.forging.menu; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.blightedSMP.engine.recipes.forging.ForgeRecipe; import fr.moussax.blightedSMP.engine.recipes.forging.registry.ForgeRegistry; import fr.moussax.bedrock.text.Formatter; @@ -73,7 +73,7 @@ private ItemStack buildRecipeDisplayItem(ForgeRecipe recipe) { builder.setAmount(recipe.getForgedAmount()); builder.addLore("", " §7Items required:"); - for (CraftingObject ingredient : recipe.getIngredients()) { + for (RecipeIngredient ingredient : recipe.getIngredients()) { builder.addLore(" §8‣ " + Utilities.extractIngredientName(ingredient) + " §8x" + ingredient.getAmount()); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/utils/Utilities.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/utils/Utilities.java index fc01ca67..671e16ed 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/utils/Utilities.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/utils/Utilities.java @@ -1,6 +1,6 @@ package fr.moussax.blightedSMP.utils; -import fr.moussax.blightedSMP.engine.recipes.CraftingObject; +import fr.moussax.blightedSMP.engine.recipes.RecipeIngredient; import fr.moussax.bedrock.text.Formatter; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -44,11 +44,11 @@ public static String resolveItemId(@NonNull ItemStack item, @NonNull String reso * @param ingredient the ingredient to get the name from * @return display the name of the ingredient */ - public static String extractIngredientName(CraftingObject ingredient) { - if (ingredient.isCustom()) { - return Objects.requireNonNull(ingredient.getManager().toItemStack().getItemMeta()).getDisplayName(); + public static String extractIngredientName(RecipeIngredient ingredient) { + if (ingredient.isCustom() && ingredient.getItem() != null) { + return Objects.requireNonNull(ingredient.getItem().toItemStack().getItemMeta()).getDisplayName(); } - return "§f" + Formatter.formatEnumName(Objects.requireNonNull(ingredient.getVanillaItem()).getType().name()); + return "§f" + Formatter.formatEnumName(Objects.requireNonNull(ingredient.getMaterial()).name()); } /** @@ -57,7 +57,7 @@ public static String extractIngredientName(CraftingObject ingredient) { * @param player the player whose inventory to modify * @param ingredient the ingredient and amount to consume */ - public static void consumeItemsFromInventory(Player player, CraftingObject ingredient) { + public static void consumeItemsFromInventory(Player player, RecipeIngredient ingredient) { String requiredId = ingredient.getId(); int remainingToRemove = ingredient.getAmount(); ItemStack[] contents = player.getInventory().getContents(); diff --git a/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredientTest.java b/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredientTest.java new file mode 100644 index 00000000..cfc36e90 --- /dev/null +++ b/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredientTest.java @@ -0,0 +1,50 @@ +package fr.moussax.blightedSMP.engine.recipes; + +import fr.moussax.blightedSMP.engine.recipes.crafting.BlightedRecipe; +import fr.moussax.blightedSMP.engine.recipes.crafting.registry.RecipeRegistry; +import org.bukkit.Material; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RecipeIngredientTest { + + @Test + @DisplayName("Vanilla ingredient stores material, amount, and formatted id correctly") + void testVanillaIngredient() { + RecipeIngredient ingredient = RecipeIngredient.of(Material.NETHERITE_INGOT, 4); + + assertTrue(ingredient.isVanilla()); + assertFalse(ingredient.isCustom()); + assertEquals(Material.NETHERITE_INGOT, ingredient.getMaterial()); + assertEquals(4, ingredient.getAmount()); + assertEquals("vanilla:NETHERITE_INGOT", ingredient.getId()); + } + + @Test + @DisplayName("Value equality and hashCode depend on ingredient id and amount") + void testEqualityAndHashCode() { + RecipeIngredient first = RecipeIngredient.of(Material.DIAMOND, 2); + RecipeIngredient second = RecipeIngredient.of(Material.DIAMOND, 2); + RecipeIngredient differentAmount = RecipeIngredient.of(Material.DIAMOND, 3); + RecipeIngredient differentMaterial = RecipeIngredient.of(Material.EMERALD, 2); + + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + + assertNotEquals(first, differentAmount); + assertNotEquals(first, differentMaterial); + assertNotEquals(first, null); + } + + @Test + @DisplayName("RecipeRegistry empty grid returns empty matching recipes") + void testRecipeRegistryEmptyGrid() { + assertTrue(RecipeRegistry.findMatchingRecipes(null).isEmpty()); + assertTrue(RecipeRegistry.findMatchingRecipes(List.of()).isEmpty()); + assertTrue(RecipeRegistry.findFirstMatching(List.of()).isEmpty()); + } +} From d1dad49142d40b0260992629ee976cdfed1f732a Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:20:39 +0200 Subject: [PATCH 2/6] refactor(player): decouple PlayerDataHandler and optimize BlightedPlayer concurrency Introduce immutable PlayerData record and eliminate duplicated state fields in PlayerDataHandler. Modernize BlightedPlayer cooldown storage to a ConcurrentHashMap with CooldownKey, add UUID lookups, and safely handle context cleanup across reconnections. --- .../engine/player/BlightedPlayer.java | 99 ++++++++++++++----- .../server/database/PlayerDataHandler.java | 51 +++++----- 2 files changed, 102 insertions(+), 48 deletions(-) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java index dd7ac89b..5ac4b34c 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java @@ -15,6 +15,7 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.IllegalPluginAccessException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -39,6 +40,8 @@ public final class BlightedPlayer { private final UUID playerId; private final PlayerDataHandler dataHandler; + public record CooldownKey(String key, AbilityTrigger trigger) {} + @Getter private int gems; @Getter @@ -50,7 +53,7 @@ public final class BlightedPlayer { private double manaRegenerationRate; private final List activeFullSetBonuses = new ArrayList<>(); - private final List cooldowns = new ArrayList<>(); + private final Map cooldowns = new ConcurrentHashMap<>(); private final EnumMap armorPieces = new EnumMap<>(ItemType.class); private ItemStack[] lastKnownArmor = new ItemStack[4]; @@ -70,12 +73,16 @@ private BlightedPlayer(Player player) { this.player = player; this.playerId = player.getUniqueId(); this.dataHandler = new PlayerDataHandler(playerId, player.getName()); - this.gems = dataHandler.getSavedGems(); + PlayerDataHandler.PlayerData data = dataHandler.load(); + this.gems = data.gems(); this.maxMana = DEFAULT_MAX_MANA; - this.manaRegenerationRate = Optional.ofNullable(BlightedSMP.getInstance()).map(BlightedSMP::getSettings).map(PluginSettings::getDefaultManaRegenerationRate).orElse(DEFAULT_MANA_REGEN_RATE); - setCurrentMana(dataHandler.getSavedMana()); - this.forgeFuel = dataHandler.getSavedForgeFuel(); + this.manaRegenerationRate = Optional.ofNullable(BlightedSMP.getInstance()) + .map(BlightedSMP::getSettings) + .map(PluginSettings::getDefaultManaRegenerationRate) + .orElse(DEFAULT_MANA_REGEN_RATE); + setCurrentMana(data.mana()); + this.forgeFuel = data.forgeFuel(); ArmorSetManager.updatePlayerArmor(this); } @@ -95,7 +102,26 @@ public void tick() { */ public static BlightedPlayer get(Player player) { if (player == null || !player.isOnline()) return null; - return players.computeIfAbsent(player.getUniqueId(), _ -> new BlightedPlayer(player)); + return players.compute(player.getUniqueId(), (_, existing) -> { + if (existing == null || existing.getPlayer() != player) { + if (existing != null) { + existing.cleanup(); + } + return new BlightedPlayer(player); + } + return existing; + }); + } + + /** + * Retrieves the active player context associated with a player UUID. + * + * @param uuid unique identifier of the player + * @return registered player context, or {@code null} if not found + */ + public static BlightedPlayer get(UUID uuid) { + if (uuid == null) return null; + return players.get(uuid); } /** @@ -130,10 +156,12 @@ private void cleanup() { /** * Returns an unmodifiable list of active ability cooldown entries. * - * @return unmodifiable list of cooldown entries + * @return unmodifiable list of active cooldown entries */ public List getCooldowns() { - return Collections.unmodifiableList(cooldowns); + return cooldowns.values().stream() + .filter(entry -> !entry.isExpired()) + .toList(); } /** @@ -142,7 +170,7 @@ public List getCooldowns() { * @param entry cooldown entry to add */ public void addCooldown(CooldownEntry entry) { - cooldowns.add(entry); + cooldowns.put(new CooldownKey(entry.key(), entry.trigger()), entry); } /** @@ -151,7 +179,7 @@ public void addCooldown(CooldownEntry entry) { * @param entry cooldown entry to remove */ public void removeCooldown(CooldownEntry entry) { - cooldowns.remove(entry); + cooldowns.remove(new CooldownKey(entry.key(), entry.trigger())); } /** @@ -163,8 +191,7 @@ public void removeCooldown(CooldownEntry entry) { */ public void setCooldown(String key, AbilityTrigger trigger, int seconds) { long expire = System.currentTimeMillis() + (seconds * 1000L); - cooldowns.removeIf(currentCooldown -> currentCooldown.key().equals(key) && currentCooldown.trigger() == trigger); - cooldowns.add(new CooldownEntry(key, trigger, expire)); + cooldowns.put(new CooldownKey(key, trigger), new CooldownEntry(key, trigger, expire)); } /** @@ -182,28 +209,26 @@ public void setCooldown(Class abilityClass, AbilityTrigge /** * Returns the remaining cooldown duration in seconds for a string key and ability trigger. * - *

Expired cooldown entries are removed before performing the lookup.

- * * @param key cooldown key or ability name * @param trigger ability trigger associated with the cooldown * @return remaining cooldown in seconds, or {@code 0} if no active cooldown exists */ public double getRemainingCooldown(String key, AbilityTrigger trigger) { - cooldowns.removeIf(CooldownEntry::isExpired); - - for (CooldownEntry entry : cooldowns) { - if (entry.key().equals(key) && entry.trigger() == trigger) { - return entry.getRemainingCooldownTimeInSeconds(); - } + CooldownKey cooldownKey = new CooldownKey(key, trigger); + CooldownEntry entry = cooldowns.get(cooldownKey); + if (entry == null) { + return 0; + } + if (entry.isExpired()) { + cooldowns.remove(cooldownKey, entry); + return 0; } - return 0; + return entry.getRemainingCooldownTimeInSeconds(); } /** * Returns the remaining cooldown duration in seconds for an ability class and ability trigger. * - *

Expired cooldown entries are removed before performing the lookup.

- * * @param abilityClass ability class associated with the cooldown * @param trigger ability trigger associated with the cooldown * @return remaining cooldown in seconds, or {@code 0} if no active cooldown exists @@ -451,12 +476,38 @@ public void addItemToInventory(ItemStack item) { /** * Asynchronously persists resources and forge fuel to database storage. + * If the plugin is already disabled or shutting down, saves synchronously. */ public void saveData() { + BlightedSMP instance = BlightedSMP.getInstance(); + if (instance == null || !instance.isEnabled()) { + saveSync(); + return; + } int gemsToSave = this.gems; double manaToSave = this.currentMana; int forgeFuelToSave = this.forgeFuel; - Bukkit.getScheduler().runTaskAsynchronously(BlightedSMP.getInstance(), () -> dataHandler.save(gemsToSave, manaToSave, forgeFuelToSave)); + try { + Bukkit.getScheduler().runTaskAsynchronously(instance, () -> dataHandler.save(gemsToSave, manaToSave, forgeFuelToSave)); + } catch (IllegalPluginAccessException _) { + saveSync(); + } + } + + /** + * Synchronously persists resources and forge fuel to database storage. + */ + public void saveSync() { + dataHandler.save(this.gems, this.currentMana, this.forgeFuel); + } + + /** + * Synchronously persists all active players to database storage. + */ + public static void saveAllSync() { + for (BlightedPlayer player : players.values()) { + player.saveSync(); + } } /** diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/database/PlayerDataHandler.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/database/PlayerDataHandler.java index c757c8d7..7c9fcffb 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/database/PlayerDataHandler.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/database/PlayerDataHandler.java @@ -1,7 +1,6 @@ package fr.moussax.blightedSMP.server.database; import fr.moussax.blightedSMP.BlightedSMP; -import lombok.Getter; import java.sql.Connection; import java.sql.PreparedStatement; @@ -13,19 +12,24 @@ * Manages SQLite database persistence for player resources and forge fuel state. */ public final class PlayerDataHandler { + + /** + * Immutable data carrier representing loaded player state from the database. + * + * @param gems current gem balance + * @param mana current mana pool + * @param forgeFuel current thermal forge fuel in millibuckets + */ + public record PlayerData(int gems, double mana, int forgeFuel) { + public static final PlayerData DEFAULT = new PlayerData(0, 100.0, 0); + } + private final UUID playerId; private final String playerName; private final Connection connection; - @Getter - private int savedGems; - @Getter - private double savedMana; - @Getter - private int savedForgeFuel; - /** - * Creates a player data handler for the specified player, loading initial state from storage. + * Creates a player data handler for the specified player. * * @param playerId unique identifier of the player * @param playerName current username of the player @@ -34,8 +38,6 @@ public PlayerDataHandler(UUID playerId, String playerName) { this.playerId = playerId; this.playerName = playerName; this.connection = BlightedSMP.getInstance().getDatabase().getConnection(); - - load(); } /** @@ -46,10 +48,6 @@ public PlayerDataHandler(UUID playerId, String playerName) { * @param forgeFuel current forge fuel amount to save */ public void save(int gems, double mana, int forgeFuel) { - this.savedGems = gems; - this.savedMana = mana; - this.savedForgeFuel = forgeFuel; - String query = """ INSERT INTO players (uuid, name, gems, mana, forge_fuel) VALUES (?, ?, ?, ?, ?) @@ -74,7 +72,13 @@ ON CONFLICT(uuid) DO UPDATE SET } } - private void load() { + /** + * Loads the player's saved state from the database. + * If no existing record exists, inserts and returns the default state. + * + * @return loaded player data + */ + public PlayerData load() { String query = "SELECT name, gems, mana, forge_fuel FROM players WHERE uuid = ?"; synchronized (connection) { @@ -83,19 +87,18 @@ private void load() { try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { - this.savedGems = resultSet.getInt("gems"); - this.savedMana = resultSet.getDouble("mana"); - this.savedForgeFuel = resultSet.getInt("forge_fuel"); + int gems = resultSet.getInt("gems"); + double mana = resultSet.getDouble("mana"); + int forgeFuel = resultSet.getInt("forge_fuel"); String storedName = resultSet.getString("name"); if (!storedName.equals(playerName)) { - save(savedGems, savedMana, savedForgeFuel); + save(gems, mana, forgeFuel); } + return new PlayerData(gems, mana, forgeFuel); } else { - this.savedGems = 0; - this.savedMana = 100.0; - this.savedForgeFuel = 0; - save(savedGems, savedMana, savedForgeFuel); + save(PlayerData.DEFAULT.gems(), PlayerData.DEFAULT.mana(), PlayerData.DEFAULT.forgeFuel()); + return PlayerData.DEFAULT; } } } catch (SQLException exception) { From e470f3ddc331d3d9a5c43917877b683036eab48c Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:20:55 +0200 Subject: [PATCH 3/6] fix(server): guarantee synchronous data persistence on server shutdown Save active player states synchronously during BlightedSMP.onDisable prior to database disconnect. Add scheduler rejection fallback in saveData and clarify mana regeneration rate documentation. --- .../src/main/java/fr/moussax/blightedSMP/BlightedSMP.java | 1 + .../main/java/fr/moussax/blightedSMP/server/PluginSettings.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java index 08f138b1..70ea259b 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java @@ -70,6 +70,7 @@ public void onEnable() { @Override public void onDisable() { CustomFishingHook.cleanupAll(); + BlightedPlayer.saveAllSync(); if (database != null) { database.closeConnection(); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/PluginSettings.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/PluginSettings.java index 6a9d1864..4322fb5c 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/PluginSettings.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/server/PluginSettings.java @@ -11,7 +11,7 @@ public final class PluginSettings { /** - * Default passive mana regeneration rate per tick. + * Default passive mana regeneration amount per second (applied every 20 server ticks). */ @Getter private double defaultManaRegenerationRate; From 3a644c443b2ceabca55958221b45be1f6ea5a4fa Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:21:10 +0200 Subject: [PATCH 4/6] fix(items): remove redundant spacing above canonical rarity lore footer --- .../moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java index 9318c6fd..e60467ed 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java @@ -76,7 +76,6 @@ public static List render(BlightedItem item) { ItemType type = item.getItemType(); String typeSuffix = (type != null && type != ItemType.UNCATEGORIZED) ? " " + type.name() : ""; - lore.add(""); lore.add(rarity.getName() + typeSuffix); return lore; From e32869c631ca154f2a2b1eddd1f6d6d753451bab Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:39:03 +0200 Subject: [PATCH 5/6] fix(recipe): validate and enforce positive amount --- .../fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java index e67eae69..c8f4d87b 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java @@ -28,6 +28,7 @@ public final class RecipeIngredient { private final String id; public RecipeIngredient(@NonNull BlightedItem item, int amount) { + if (amount <= 0) throw new IllegalArgumentException("amount must be positive"); this.item = Objects.requireNonNull(item, "item cannot be null"); this.material = null; this.amount = amount; @@ -35,6 +36,7 @@ public RecipeIngredient(@NonNull BlightedItem item, int amount) { } public RecipeIngredient(@NonNull Material material, int amount) { + if (amount <= 0) throw new IllegalArgumentException("amount must be positive"); this.item = null; this.material = Objects.requireNonNull(material, "material cannot be null"); this.amount = amount; From 9795fbaa07868cca63e9ba97a5aecb99856d26d6 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sun, 4 Oct 2026 11:55:50 +0200 Subject: [PATCH 6/6] fix(items): restore flush subtitle layout for thermal fuel lore Use addLore for thermal fuels to avoid automatic description spacing injecting an unwanted blank line above the subtitle header, and guard ItemLoreRenderer against prefixing empty lines with color codes. --- .../content/items/ThermalFuels.java | 44 ++++++++++--------- .../engine/items/lore/ItemLoreRenderer.java | 4 +- .../blightedSMP/engine/items/ItemTest.java | 17 +++++++ 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/items/ThermalFuels.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/items/ThermalFuels.java index 1e7d5051..5fec0b0c 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/items/ThermalFuels.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/content/items/ThermalFuels.java @@ -14,51 +14,55 @@ public class ThermalFuels implements RegistryModule> { public void register(Consumer registry) { BlightedItem enchantedCoal = new BlightedItem("ENCHANTED_COAL", ItemType.MATERIAL, ItemRarity.UNCOMMON, Material.COAL); enchantedCoal.setDisplayName("Enchanted Coal"); - enchantedCoal.description( + enchantedCoal.addLore( "§8Thermal Fuel", "", - "Ultra-dense carbon radiating", - "with intense heat that adds", - "§6\uD83E\uDEA3 3,000mB §7to a refuelable", - "machine." + " §7Ultra-dense carbon radiating", + " §7with intense heat that adds", + " §6\uD83E\uDEA3 3,000mB §7to a refuelable", + " §7machine.", + "" ); enchantedCoal.glow(); BlightedItem enchantedLavaBucket = new BlightedItem("ENCHANTED_LAVA_BUCKET", ItemType.UNCATEGORIZED, ItemRarity.RARE, Material.LAVA_BUCKET); enchantedLavaBucket.setDisplayName("Enchanted Lava Bucket"); - enchantedLavaBucket.description( + enchantedLavaBucket.addLore( "§8Thermal Fuel", "", - "Enriched lava capable of", - "prolonged burning that adds", - "§6\uD83E\uDEA3 10,000mB §7to a refuelable", - "machine." + " §7Enriched lava capable of", + " §7prolonged burning that adds", + " §6\uD83E\uDEA3 10,000mB §7to a refuelable", + " §7machine.", + "" ); enchantedLavaBucket.glow(); enchantedLavaBucket.preventBucketInteractions(); BlightedItem magmaBucket = new BlightedItem("MAGMA_BUCKET", ItemType.UNCATEGORIZED, ItemRarity.EPIC, Material.LAVA_BUCKET); magmaBucket.setDisplayName("Magma Bucket"); - magmaBucket.description( + magmaBucket.addLore( "§8Thermal Fuel", "", - "A superheated amalgam of", - "compressed magma that adds", - "§6\uD83E\uDEA3 20,000mB §7to a refuelable", - "machine." + " §7A superheated amalgam of", + " §7compressed magma that adds", + " §6\uD83E\uDEA3 20,000mB §7to a refuelable", + " §7machine.", + "" ); magmaBucket.glow(); magmaBucket.preventBucketInteractions(); BlightedItem plasmaBucket = new BlightedItem("PLASMA_BUCKET", ItemType.UNCATEGORIZED, ItemRarity.LEGENDARY, Material.LAVA_BUCKET); plasmaBucket.setDisplayName("Plasma Bucket"); - plasmaBucket.description( + plasmaBucket.addLore( "§8Thermal Fuel", "", - "Stable ionized matter containing", - "stellar-grade heat that adds", - "§6\uD83E\uDEA3 50,000mB §7to a refuelable", - "machine." + " §7Stable ionized matter containing", + " §7stellar-grade heat that adds", + " §6\uD83E\uDEA3 50,000mB §7to a refuelable", + " §7machine.", + "" ); plasmaBucket.glow(); plasmaBucket.preventBucketInteractions(); diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java index e60467ed..bebd3419 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java @@ -34,7 +34,7 @@ public static List render(BlightedItem item) { if (!description.isEmpty()) { lore.add(""); for (String line : description) { - lore.add("§7 " + line); + lore.add(line.isEmpty() ? "" : "§7 " + line); } } @@ -44,7 +44,7 @@ public static List render(BlightedItem item) { lore.add("§5 Ability: " + ability.getName() + " " + ability.getTrigger().getDisplayName()); for (String line : ability.getDescription()) { - lore.add("§7 " + line); + lore.add(line.isEmpty() ? "" : "§7 " + line); } int mana = ability.getManaCost(); diff --git a/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/items/ItemTest.java b/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/items/ItemTest.java index ada36144..c71e0e56 100644 --- a/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/items/ItemTest.java +++ b/blighted-smp/src/test/java/fr/moussax/blightedSMP/engine/items/ItemTest.java @@ -129,6 +129,23 @@ public boolean triggerAbility(PlayerInteractEvent event) { assertEquals(1, rarityLineCount, "Item lore should contain exactly one canonical rarity footer"); } + @Test + @DisplayName("Thermal Fuel items render flush header without leading blank line") + void testThermalFuelLoreRendering() { + ItemRegistry.clear(); + new fr.moussax.blightedSMP.content.items.ThermalFuels().register(ItemRegistry::register); + + BlightedItem enchantedCoal = ItemRegistry.getOrThrow("ENCHANTED_COAL"); + List lore = ItemLoreRenderer.render(enchantedCoal); + + assertNotNull(lore); + assertFalse(lore.isEmpty()); + assertEquals("§8Thermal Fuel", lore.getFirst(), "First lore line must be the flush subtitle header without empty line above"); + assertEquals("", lore.get(1), "Second lore line should separate subtitle from body description"); + assertTrue(lore.getLast().contains(ItemRarity.UNCOMMON.getName()), "Last line must be canonical rarity footer"); + ItemRegistry.clear(); + } + @Test @DisplayName("ItemRegistry safe get and getOrThrow contract") void testItemRegistryLookup() {