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/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/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/items/lore/ItemLoreRenderer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/lore/ItemLoreRenderer.java index 9318c6fd..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(); @@ -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; 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/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..c8f4d87b --- /dev/null +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/recipes/RecipeIngredient.java @@ -0,0 +1,103 @@ +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) { + 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; + this.id = item.getItemId(); + } + + 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; + 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/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; 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) { 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/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() { 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()); + } +}