[amount])'
Invalid-Perms: '&cYou are not allowed to use this command!'
diff --git a/resources/plugin.yml b/resources/plugin.yml
index b86af59..65ccdfe 100644
--- a/resources/plugin.yml
+++ b/resources/plugin.yml
@@ -9,12 +9,11 @@ commands:
crecipe:
description: Displays the custom recipe help page.
aliases: [crecipes, customrecipe, customrecipes, customr, cr]
- permissions:
- crecipe.*:
- default: op
edititem:
description: NBT Editor for items!
aliases: [ei, edit]
- permissions:
- crecipe.*:
- default: op
\ No newline at end of file
+ permission: crecipe.edititem
+ recipes:
+ description: Opens the recipe booklet!
+ aliases: [recipe]
+ permission: crecipe.book
\ No newline at end of file
diff --git a/resources/recipes/CursedPick.yml b/resources/recipes/CursedPick.yml
index f0aafae..9436160 100644
--- a/resources/recipes/CursedPick.yml
+++ b/resources/recipes/CursedPick.yml
@@ -7,22 +7,17 @@ CursedPick:
# Set to true for shapeless
Shapeless: false
+ # Defines a group, valid only for crafting recipes
+ # Empty string denotes no group
+ # Ommit or set to none if not needed
+ Group: ''
+
# Defines a cooldown for crafting this item.
# Define amount of time in seconds before the player can craft
# this recipe again
# Set to -1 or Omit if not needed
Cooldown: 60
- # Item ENUM for the crafted item
- Item: 'DIAMOND_PICKAXE'
-
- # Specific item type (e.g. "19/00" or "13:4").
- # Ommit or set to none if not needed
- Item-Damage: 'none'
-
- # The quantity received upon crafting
- Amount: 1
-
# Can this item be placed as a block?
Placeable: true
@@ -35,10 +30,6 @@ CursedPick:
# Improves performance
Custom-Tagged: true
- # 1-100 durability.
- # Omit or set to 100 if not applicable
- Durability: '100'
-
# Unique identifier for the recipe
# Also known as the NamespacedKey
Identifier: 'CURSED_PICKAXE'
@@ -60,27 +51,48 @@ CursedPick:
# Category recipe is to be shown in the recipe book
Book-Category: 'MISC'
- # Display name with color codes.
- # Omit or set to none if not needed
- Name: '&4&lCursed Pick'
-
- # Lore for the item, can be multiple lines.
- # Use [] if not needed
- Lore:
- - '&c&lThe Cursed Pickaxe'
+ Result:
+ # Item ENUM for the crafted item
+ Item: 'DIAMOND_PICKAXE'
+ # Specific item type (e.g. "19/00" or "13:4").
+ # Ommit or set to none if not needed
+ Item-Damage: 'none'
+ # 1-100 durability.
+ # Omit or set to 100 if not applicable
+ Durability: '100'
+ # The quantity received upon crafting
+ Amount: 1
+ # Display name with color codes.
+ # Omit or set to none if not needed
+ Name: '&4&lCursed Pick'
+ # Lore for the item, can be multiple lines.
+ # Use [] if not needed
+ Lore:
+ - '&c&lThe Cursed Pickaxe'
+ # List of enchantments.
+ # Use [] if not needed
+ Enchantments:
+ - 'DURABILITY:255'
+ # Should enchantments be hidden?
+ Hide-Enchants: true
+ # For texture packs, available in MC 1.14+
+ # Omit or set to none if not needed
+ Custom-Model-Data: none
+ # Add item flags, e.g., to hide attributes
+ # Use [] if not needed
+ Item-Flags: []
+ # Specify attributes to add
+ # Format: ATTRIBUTE:AMOUNT:SLOT
+ # Use [] if not needed
+ Attribute: []
+ # Add specific NBT tags if needed
+ # Use [] if not needed
+ Custom-Tags: []
# Potion effects format: "EFFECT:DURATION:AMPLIFIER"
# Use [] if not needed
Effects:
- - 'POISON:20:2'
-
- # Should enchantments be hidden?
- Hide-Enchants: true
-
- # List of enchantments.
- # Use [] if not needed
- Enchantments:
- - 'DURABILITY:255'
+ - 'POISON:20:2'
# List of commands to run
# Should the output item still be granted?
@@ -153,23 +165,6 @@ CursedPick:
- 'minecraft:adventure/adventuring_time'
moon_phase: [0, 4] # 0 = full moon, 4 = new moon [0-7]
- # Add specific NBT tags if needed
- # Use [] if not needed
- Custom-Tags: []
-
- # Add item flags, e.g., to hide attributes
- # Use [] if not needed
- Item-Flags: []
-
- # Specify attributes to add
- # Format: ATTRIBUTE:AMOUNT:SLOT
- # Use [] if not needed
- Attribute: []
-
- # For texture packs, available in MC 1.14+
- # Omit or set to none if not needed
- Custom-Model-Data: none
-
# Disable crafting in specific worlds
# Use [] if not needed
Disabled-Worlds: []
\ No newline at end of file
diff --git a/resources/recipes/CursedSword.yml b/resources/recipes/CursedSword.yml
index d054007..6e659dd 100644
--- a/resources/recipes/CursedSword.yml
+++ b/resources/recipes/CursedSword.yml
@@ -1,10 +1,8 @@
CursedSword:
Enabled: true
Shapeless: true
+ Group: ''
Cooldown: -1
- Item: 'DIAMOND_SWORD'
- Item-Damage: 'none'
- Amount: 1
Placeable: true
Exact-Choice: false
Custom-Tagged: true
@@ -13,14 +11,23 @@ CursedSword:
Permission: 'crecipe.recipe.cursedsword'
Auto-Discover-Recipe: true
Book-Category: 'MISC'
- Name: '&eCursed Sword'
- Lore:
- - '&fThe Cursed Sword'
+ Result:
+ Item: 'DIAMOND_SWORD'
+ Item-Damage: 'none'
+ Durability: '100'
+ Amount: 1
+ Name: '&eCursed Sword'
+ Lore:
+ - '&fThe Cursed Sword'
+ Enchantments:
+ - 'DURABILITY:255'
+ Hide-Enchants: true
+ Custom-Model-Data: none
+ Item-Flags: []
+ Attribute: []
+ Custom-Tags: []
Effects:
- 'POISON:20:2'
- Hide-Enchants: true
- Enchantments:
- - 'DURABILITY:255'
Commands:
Give-Item: true
Run-Commands: []
@@ -50,9 +57,5 @@ CursedSword:
Ignore-Name: false
Use-Conditions: false
Conditions_All: []
- Custom-Tags: []
- Item-Flags: []
- Attribute: []
- Custom-Model-Data: none
Disabled-Worlds: []
\ No newline at end of file
diff --git a/resources/recipes/WheatSand.yml b/resources/recipes/WheatSand.yml
index f9a8ab6..667a8dd 100644
--- a/resources/recipes/WheatSand.yml
+++ b/resources/recipes/WheatSand.yml
@@ -2,13 +2,14 @@ WheatSand:
Enabled: true
Exact-Choice: true
Auto-Discover-Recipe: true
- Book-Category: 'MISC'
- Item: SAND
- Amount: 1
Identifier: WHEATSAND
Converter: FURNACE
+ Book-Category: 'MISC'
Cooktime: 20 # In ticks - 20 ticks per second
Experience: 1
+ Result:
+ Item: SAND
+ Amount: 1
ItemCrafting:
- WXX
- XXX
@@ -18,7 +19,4 @@ WheatSand:
Material: WHEAT_SEEDS
Identifier: none
Amount: 1
- Item-Flags: []
- Attribute: []
- Disabled-Worlds: []
- Custom-Tags: []
\ No newline at end of file
+ Disabled-Worlds: []
\ No newline at end of file
diff --git a/src/me/mehboss/anvil/AnvilManager.java b/src/me/mehboss/anvil/AnvilManager.java
index 1f2c670..33c435e 100644
--- a/src/me/mehboss/anvil/AnvilManager.java
+++ b/src/me/mehboss/anvil/AnvilManager.java
@@ -13,25 +13,46 @@
import org.bukkit.inventory.ItemStack;
import me.mehboss.recipe.Main;
+import me.mehboss.utils.CompatibilityUtil;
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Ingredient;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
-
+import me.mehboss.utils.data.WorkstationRecipeData;
+
+/**
+ * Handles custom anvil crafting logic for recipes registered via RecipeUtil.
+ *
+ * This class listens for {@link PrepareAnvilEvent} and attempts to match the
+ * anvil's current input items to any custom anvil-type recipe. If a match is
+ * found and the player has the required permissions, the output result and
+ * repair cost are applied to the anvil.
+ */
public class AnvilManager implements Listener {
public HashMap runTimes = new HashMap();
+ /**
+ * Handles anvil preparation events and checks if the items inside the anvil
+ * inputs match any custom ANVIL-type recipe.
+ *
+ * @param e The PrepareAnvilEvent fired by Bukkit.
+ */
@EventHandler
- public void onPlace(PrepareAnvilEvent e) {
+ void onPlace(PrepareAnvilEvent e) {
+
+ if (Main.getInstance().serverVersionLessThan(1, 9))
+ return;
+
AnvilInventory inv = e.getInventory();
Recipe matchedRecipe = null;
- Player p = (Player) e.getView().getPlayer();
+ Player p = (Player) CompatibilityUtil.getPlayerFromView(CompatibilityUtil.getInventoryView(e));
- if (getRecipeUtil().getAllRecipes() == null)
+ HashMap anvilRecipes = getRecipeUtil().getRecipesFromType(RecipeType.ANVIL);
+ if (anvilRecipes == null || anvilRecipes.isEmpty())
return;
- for (Recipe recipe : getRecipeUtil().getAllRecipes().values()) {
+ for (Recipe recipe : anvilRecipes.values()) {
Boolean matchedToRecipe = true;
if (recipe.getType() != RecipeType.ANVIL) {
@@ -65,13 +86,24 @@ public void onPlace(PrepareAnvilEvent e) {
return;
}
- logDebug(matchedRecipe.getName() + ": Requirements met.. Successfully set anvil result slot");
- e.setResult(matchedRecipe.getResult());
- inv.setRepairCost(matchedRecipe.getRepairCost());
+ WorkstationRecipeData anvil = (WorkstationRecipeData) matchedRecipe;
+
+ logDebug(anvil.getName() + ": Requirements met.. Successfully set anvil result slot");
+ e.setResult(anvil.getResult());
+ inv.setRepairCost(anvil.getRepairCost());
p.updateInventory();
}
}
+ /**
+ * Checks whether the provided item satisfies the ingredient's required amount.
+ *
+ * @param recipeName Name of the recipe (used for debugging messages).
+ * @param item ItemStack inside the anvil.
+ * @param ingredient Ingredient definition being compared to.
+ * @return true if the item meets or exceeds the amount required; false
+ * otherwise.
+ */
Boolean amountsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
if (item == null || ingredient == null) {
logDebug(recipeName + ": Item or Ingredient is null");
@@ -91,9 +123,18 @@ Boolean amountsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
}
RecipeUtil getRecipeUtil() {
- return Main.getInstance().recipeUtil;
+ return Main.getInstance().recipeUtil;
}
-
+
+ /**
+ * Checks if the given ItemStack matches the ingredient's item type and
+ * metadata.
+ *
+ * @param recipeName Name of the recipe (used for debug logging).
+ * @param item ItemStack from the anvil input.
+ * @param ingredient Ingredient to compare against.
+ * @return true if items match according to MetaChecks; false otherwise.
+ */
boolean itemsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
return Main.getInstance().metaChecks.itemsMatch(recipeName, item, ingredient);
}
diff --git a/src/me/mehboss/anvil/AnvilManager_1_8.java b/src/me/mehboss/anvil/AnvilManager_1_8.java
new file mode 100644
index 0000000..6eb73f7
--- /dev/null
+++ b/src/me/mehboss/anvil/AnvilManager_1_8.java
@@ -0,0 +1,145 @@
+package me.mehboss.anvil;
+
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.AnvilInventory;
+import org.bukkit.inventory.ItemStack;
+
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.RecipeUtil;
+import me.mehboss.utils.RecipeUtil.Ingredient;
+import me.mehboss.utils.RecipeUtil.Recipe;
+import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.WorkstationRecipeData;
+
+public class AnvilManager_1_8 implements Listener {
+
+ @EventHandler
+ void onPlace(InventoryClickEvent e) {
+
+ // Only run in an anvil
+ if (!(e.getInventory() instanceof AnvilInventory))
+ return;
+
+ if (Main.getInstance().serverVersionAtLeast(1, 9))
+ return;
+
+ // Only respond to clicks inside the anvil input/result slots
+ if (e.getRawSlot() > 2)
+ return;
+
+ Player p = (Player) e.getWhoClicked();
+ AnvilInventory inv = (AnvilInventory) e.getInventory();
+
+ // Delay by 1 tick so the inventory updates like PrepareAnvilEvent would
+ Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
+
+ Recipe matchedRecipe = null;
+ HashMap anvilRecipes = getRecipeUtil().getRecipesFromType(RecipeType.ANVIL);
+
+ if (anvilRecipes == null || anvilRecipes.isEmpty())
+ return;
+
+ for (Recipe recipe : anvilRecipes.values()) {
+ Boolean matchedToRecipe = true;
+
+ int slot = 0;
+ for (Ingredient ingredient : recipe.getIngredients()) {
+ if (!itemsMatch(recipe.getName(), inv.getItem(slot), ingredient)
+ || !amountsMatch(recipe.getName(), inv.getItem(slot), ingredient)) {
+ matchedToRecipe = false;
+ break;
+ }
+ slot++;
+ }
+
+ if (matchedToRecipe) {
+ matchedRecipe = recipe;
+ break;
+ }
+
+ logDebug(recipe.getName() + ": Requirements not met.. Skipping to next recipe");
+ }
+
+ if (matchedRecipe != null) {
+ if (!matchedRecipe.isActive()
+ || (matchedRecipe.getPerm() != null && !p.hasPermission(matchedRecipe.getPerm()))
+ || matchedRecipe.getDisabledWorlds().contains(p.getWorld().getName())) {
+ sendNoPermsMessage(p, matchedRecipe.getName());
+ inv.setItem(2, null);
+ return;
+ }
+
+ WorkstationRecipeData anvil = (WorkstationRecipeData) matchedRecipe;
+
+ logDebug(anvil.getName() + ": Requirements met.. Successfully set anvil result slot");
+
+ inv.setItem(2, anvil.getResult()); // prepareAnvilEvent#setResult equivalent
+ inv.setRepairCost(anvil.getRepairCost());
+ p.updateInventory();
+ }
+ });
+ }
+
+ /**
+ * Checks whether the provided item satisfies the ingredient's required amount.
+ *
+ * @param recipeName Name of the recipe (used for debugging messages).
+ * @param item ItemStack inside the anvil.
+ * @param ingredient Ingredient definition being compared to.
+ * @return true if the item meets or exceeds the amount required; false
+ * otherwise.
+ */
+ Boolean amountsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
+ if (item == null || ingredient == null) {
+ logDebug(recipeName + ": Item or Ingredient is null");
+ logDebug(recipeName + ": Item - " + item);
+ logDebug(recipeName + ": Ingredient - " + ingredient);
+ return false;
+ }
+
+ if (item.getAmount() < ingredient.getAmount()) {
+ logDebug(recipeName + ": Amount requirements not met");
+ logDebug(recipeName + ": Slot amount - " + item.getAmount());
+ logDebug(recipeName + ": Ingredient amount - " + ingredient.getAmount());
+ return false;
+ }
+
+ return true;
+ }
+
+ RecipeUtil getRecipeUtil() {
+ return Main.getInstance().recipeUtil;
+ }
+
+ /**
+ * Checks if the given ItemStack matches the ingredient's item type and
+ * metadata.
+ *
+ * @param recipeName Name of the recipe (used for debug logging).
+ * @param item ItemStack from the anvil input.
+ * @param ingredient Ingredient to compare against.
+ * @return true if items match according to MetaChecks; false otherwise.
+ */
+ boolean itemsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
+ return Main.getInstance().metaChecks.itemsMatch(recipeName, item, ingredient);
+ }
+
+ void logDebug(String st) {
+ if (Main.getInstance().debug)
+ Logger.getLogger("Minecraft").log(Level.WARNING, "[DEBUG][" + Main.getInstance().getName() + "] " + st);
+ }
+
+ void sendNoPermsMessage(Player p, String recipe) {
+ logDebug("[sendNoPermsMessage] Player " + p.getName()
+ + " does not have required recipe crafting permissions for recipe " + recipe);
+ Main.getInstance().sendnoPerms(p);
+ }
+}
diff --git a/src/me/mehboss/anvil/GrindstoneManager.java b/src/me/mehboss/anvil/GrindstoneManager.java
index 4af115a..296188c 100644
--- a/src/me/mehboss/anvil/GrindstoneManager.java
+++ b/src/me/mehboss/anvil/GrindstoneManager.java
@@ -33,7 +33,16 @@
import me.mehboss.utils.RecipeUtil.Ingredient;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
-
+import me.mehboss.utils.data.WorkstationRecipeData;
+
+/**
+ * Handles all custom logic for grindstone-based workstation recipes.
+ *
+ * This listener monitors grindstone input changes, result collection actions,
+ * and drag events to allow players to craft custom grindstone recipes registered
+ * via {@link RecipeUtil}. The class also handles permission checks, XP rewards,
+ * ingredient consumption, and result previewing.
+ */
public class GrindstoneManager implements Listener {
private static final Map matchedByPlayer = new HashMap<>();
@@ -41,8 +50,21 @@ RecipeUtil getRecipeUtil() {
return Main.getInstance().recipeUtil;
}
+ /**
+ * Handles all result-collection actions inside a grindstone. This includes:
+ *
+ * - Detecting whether the click is a valid take action
+ * - Ensuring custom output is only given when a recipe was matched
+ * - Handling cursor stacking and shift-clicking
+ * - Playing grindstone sound effects
+ * - Giving experience (if configured)
+ * - Consuming recipe ingredients
+ *
+ *
+ * @param event The InventoryClickEvent triggered by a player.
+ */
@EventHandler(priority = EventPriority.HIGHEST)
- public void onCollectResult(InventoryClickEvent event) {
+ void onCollectResult(InventoryClickEvent event) {
if (event.getClickedInventory() == null || event.getAction() == InventoryAction.NOTHING
|| event.getClickedInventory().getType() != InventoryType.GRINDSTONE) {
return;
@@ -137,19 +159,38 @@ public void onCollectResult(InventoryClickEvent event) {
Bukkit.getScheduler().runTask(Main.getInstance(), player::updateInventory);
}
+
+ /**
+ * Prevents dragging into grindstone slots, ensuring only controlled click
+ * interactions can place or remove items.
+ *
+ * @param event The InventoryDragEvent triggered by a drag action.
+ */
@EventHandler(ignoreCancelled = true)
- public void onDrag(InventoryDragEvent event) {
+ void onDrag(InventoryDragEvent event) {
if (event.getInventory().getType() != InventoryType.GRINDSTONE || event.getInventorySlots().isEmpty())
return;
InventoryView view = event.getView();
- // If any dragged raw slot is the grindstone, cancel (matches your original)
if (event.getRawSlots().stream().anyMatch(i -> view.getInventory(i) instanceof GrindstoneInventory)) {
event.setCancelled(true);
}
}
- private void processGrindstone(Inventory inv, Player player, InventoryInteractEvent event) {
+ /**
+ * Core logic to determine whether current grindstone inputs match a custom recipe.
+ * Handles:
+ *
+ * - Clearing the result slot when no longer matching
+ * - Displaying the custom output if matched
+ * - Performing permission & world-disabled checks
+ *
+ *
+ * @param inv Grindstone inventory being processed.
+ * @param player Player using the grindstone.
+ * @param event Inventory event that triggered this update.
+ */
+ void processGrindstone(Inventory inv, Player player, InventoryInteractEvent event) {
if (!(inv instanceof GrindstoneInventory))
return;
@@ -195,7 +236,16 @@ private void processGrindstone(Inventory inv, Player player, InventoryInteractEv
}
}
- private Optional findMatch(ItemStack top, ItemStack bottom, Player p, InventoryView view) {
+ /**
+ * Attempts to match two grindstone input items with a registered custom recipe.
+ *
+ * @param top Item in slot 0.
+ * @param bottom Item in slot 1.
+ * @param p Player interacting with the grindstone.
+ * @param view InventoryView representing the UI state.
+ * @return Optional containing a matching Recipe or empty.
+ */
+ Optional findMatch(ItemStack top, ItemStack bottom, Player p, InventoryView view) {
if (getRecipeUtil().getAllRecipes() == null)
return Optional.empty();
@@ -241,15 +291,24 @@ else if (bottomIng == null)
return Optional.empty();
}
+ /** Returns a safe result ItemStack for recipe output (never null). */
private ItemStack safeResultOf(Recipe recipe) {
ItemStack res = recipe.getResult();
return res == null ? new ItemStack(Material.AIR) : res.clone();
}
+ /** Clears output slot (slot 2) of a grindstone. */
private void clearResultSlot(GrindstoneInventory inv) {
inv.setItem(2, new ItemStack(Material.AIR));
}
+ /**
+ * Consumes ingredients from the grindstone input slots according to recipe needs.
+ *
+ * @param inv Grindstone inventory used.
+ * @param recipe Recipe whose ingredients will be consumed.
+ * @param p Player performing the craft.
+ */
private void consumeIngredients(GrindstoneInventory inv, Recipe recipe, Player p) {
Ingredient topIng = null;
@@ -271,6 +330,7 @@ else if (bottomIng == null)
shrinkSlot(inv, 1, bottomIng, p);
}
+ /** Reduces item count in a grindstone input slot or clears it if empty. */
private void shrinkSlot(GrindstoneInventory inv, int slot, Ingredient ing, Player p) {
ItemStack stack = inv.getItem(slot);
if (stack == null)
@@ -287,18 +347,20 @@ private void shrinkSlot(GrindstoneInventory inv, int slot, Ingredient ing, Playe
}
}
- private void tryGiveXp(Player p, Recipe recipe) {
-
- if (recipe.getExperience() <= 0)
+ /** Gives the player experience based on the recipe configuration. */
+ private void tryGiveXp(Player p, Recipe rawRecipe) {
+ WorkstationRecipeData matched = (WorkstationRecipeData) rawRecipe;
+ if (matched.getExperience() <= 0)
return;
- int xp = (int) recipe.getExperience();
+ int xp = (int) matched.getExperience();
if (xp > 0) {
ExperienceOrb orb = p.getWorld().spawn(p.getLocation(), ExperienceOrb.class);
orb.setExperience(xp);
}
}
+ /** Checks ingredient amount requirements. */
private boolean amountsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
if (item == null || ingredient == null) {
logDebug("Item or Ingredient is null", recipeName);
@@ -315,6 +377,7 @@ private boolean amountsMatch(String recipeName, ItemStack item, Ingredient ingre
return true;
}
+ /** Checks item similarity using metadata comparison handlers. */
private boolean itemsMatch(String recipeName, ItemStack item, Ingredient ingredient) {
return Main.getInstance().metaChecks.itemsMatch(recipeName, item, ingredient);
}
diff --git a/src/me/mehboss/anvil/package-info.java b/src/me/mehboss/anvil/package-info.java
new file mode 100644
index 0000000..63b463a
--- /dev/null
+++ b/src/me/mehboss/anvil/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Contains all logic related to custom anvil and workstation crafting.
+ *
+ * Includes event listeners, recipe matching, and output logic used to
+ * override or extend vanilla anvil behavior.
+ */
+package me.mehboss.anvil;
\ No newline at end of file
diff --git a/src/me/mehboss/brewing/BrewAction.java b/src/me/mehboss/brewing/BrewAction.java
new file mode 100644
index 0000000..1f49dcf
--- /dev/null
+++ b/src/me/mehboss/brewing/BrewAction.java
@@ -0,0 +1,20 @@
+package me.mehboss.brewing;
+
+import org.bukkit.inventory.BrewerInventory;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Represents a brewing action to perform when brewing completes.
+ * Extend this class to define custom behavior for each brewing recipe.
+ */
+public abstract class BrewAction {
+
+ /**
+ * Called when brewing completes.
+ *
+ * @param inventory the brewing stand inventory
+ * @param item the potion/output slot item
+ * @param ingredient the ingredient used for brewing
+ */
+ public abstract void brew(BrewerInventory inventory, ItemStack item, ItemStack ingredient);
+}
\ No newline at end of file
diff --git a/src/me/mehboss/brewing/BrewEvent.java b/src/me/mehboss/brewing/BrewEvent.java
new file mode 100644
index 0000000..873e757
--- /dev/null
+++ b/src/me/mehboss/brewing/BrewEvent.java
@@ -0,0 +1,157 @@
+package me.mehboss.brewing;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.BrewerInventory;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.data.BrewingRecipeData;
+
+/**
+ * Handles custom brewing logic: - Allows non-standard items in brewing stands -
+ * Prevents item disappearance with shift-clicks - Automatically starts brewing
+ * when valid custom recipes are present - Supports both empty-slot and
+ * bottle-required brewing
+ */
+public class BrewEvent implements Listener {
+
+ @EventHandler
+ void onCustomPotionClick(InventoryClickEvent event) {
+ Inventory inv = event.getClickedInventory();
+ if (inv == null || inv.getType() != InventoryType.BREWING)
+ return;
+ if (!(inv instanceof BrewerInventory))
+ return;
+
+ BrewerInventory brewer = (BrewerInventory) inv;
+ Player player = (Player) event.getWhoClicked();
+ int slot = event.getRawSlot();
+
+ if (event.getClick() == ClickType.SHIFT_LEFT || event.getClick() == ClickType.SHIFT_RIGHT) {
+ Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> {
+ if (brewer.getHolder().getBrewingTime() > 0)
+ return;
+
+ BrewingRecipeData recipe = BrewingRecipe.getRecipe(brewer);
+ if (recipe == null)
+ return;
+
+ if (!canBrewIntoResultSlots(brewer, recipe))
+ return;
+
+ BrewingRecipe.startBrewing(brewer, recipe);
+ }, 2L);
+ return;
+ }
+
+ // --- Regular left/right click handling ---
+ if (!(event.getClick() == ClickType.LEFT || event.getClick() == ClickType.RIGHT))
+ return;
+
+ event.setCancelled(true); // override vanilla behavior safely
+
+ ItemStack clicked = event.getCurrentItem();
+ ItemStack cursor = event.getCursor();
+
+ if (clicked == null)
+ clicked = new ItemStack(Material.AIR);
+ if (cursor == null)
+ cursor = new ItemStack(Material.AIR);
+
+ boolean compare = clicked.isSimilar(cursor);
+ ClickType clickType = event.getClick();
+ int firstAmount = clicked.getAmount();
+ int secondAmount = cursor.getAmount();
+ int stackMax = clicked.getMaxStackSize();
+
+ // --- Manual placement logic ---
+ if (clickType == ClickType.LEFT) {
+ if (clicked.getType() == Material.AIR) {
+ inv.setItem(slot, cursor.clone());
+ player.setItemOnCursor(null);
+ } else if (compare) {
+ int canAdd = stackMax - firstAmount;
+ if (secondAmount <= canAdd) {
+ clicked.setAmount(firstAmount + secondAmount);
+ player.setItemOnCursor(null);
+ } else {
+ cursor.setAmount(secondAmount - canAdd);
+ clicked.setAmount(stackMax);
+ player.setItemOnCursor(cursor);
+ }
+ } else {
+ inv.setItem(slot, cursor.clone());
+ player.setItemOnCursor(clicked.clone());
+ }
+ } else if (clickType == ClickType.RIGHT) {
+ if (clicked.getType() == Material.AIR) {
+ ItemStack one = cursor.clone();
+ one.setAmount(1);
+ inv.setItem(slot, one);
+ cursor.setAmount(secondAmount - 1);
+ if (cursor.getAmount() <= 0)
+ player.setItemOnCursor(null);
+ else
+ player.setItemOnCursor(cursor);
+ } else if (compare) {
+ if (firstAmount + 1 <= stackMax) {
+ clicked.setAmount(firstAmount + 1);
+ cursor.setAmount(secondAmount - 1);
+ if (cursor.getAmount() <= 0)
+ player.setItemOnCursor(null);
+ }
+ } else {
+ inv.setItem(slot, cursor.clone());
+ player.setItemOnCursor(clicked.clone());
+ }
+ }
+
+ // --- Run brew check after inventory updates (next tick) ---
+ Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> {
+
+ // If stand is already brewing, don't restart it
+ if (brewer.getHolder().getBrewingTime() > 0)
+ return;
+
+ // Lookup recipe (now that slots are updated)
+ BrewingRecipeData recipe = BrewingRecipe.getRecipe(brewer);
+ if (recipe == null)
+ return;
+
+ // Validate bottle/output slots before brewing
+ if (!canBrewIntoResultSlots(brewer, recipe))
+ return;
+
+ BrewingRecipe.startBrewing(brewer, recipe);
+ }, 2L);
+ }
+
+ /**
+ * Validates that there’s at least one valid bottle slot or empty slot depending
+ * on whether the recipe requires bottles.
+ */
+ private boolean canBrewIntoResultSlots(BrewerInventory inventory, BrewingRecipeData recipe) {
+ boolean requiresBottles = recipe.requiresBottles();
+ Material requiredBottle = recipe.getRequiredBottleType();
+
+ for (int i = 0; i < 3; i++) {
+ ItemStack item = inventory.getItem(i);
+ if (!requiresBottles) {
+ if (item == null || item.getType() == Material.AIR)
+ return true;
+ } else {
+ if (item != null && item.getType() == requiredBottle)
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/me/mehboss/brewing/Brewery.java b/src/me/mehboss/brewing/Brewery.java
new file mode 100644
index 0000000..b5dbb3c
--- /dev/null
+++ b/src/me/mehboss/brewing/Brewery.java
@@ -0,0 +1,74 @@
+package me.mehboss.brewing;
+
+import org.bukkit.Bukkit;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.BrewerInventory;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import me.mehboss.utils.data.BrewingRecipeData;
+
+/**
+ * Handles the registration and execution of custom brewing recipes.
+ *
+ * This class acts as the core manager for custom brewing logic by:
+ *
+ * - Maintaining a static singleton reference for recipe access
+ * - Listening for brewing stand inventory interactions
+ * - Triggering custom brewing behavior when recipe inputs match
+ *
+ * Custom brewing recipes are retrieved through {@link BrewingRecipe#getRecipe(BrewerInventory)}
+ * and executed using {@link BrewingRecipe#startBrewing(BrewerInventory, BrewingRecipeData)}.
+ */
+public class Brewery implements Listener {
+
+ private static Brewery instance;
+ private final JavaPlugin plugin;
+
+ /**
+ * Creates a new Brewery manager and registers it as an event listener.
+ *
+ * @param plugin The plugin instance that owns this brewing system.
+ */
+ public Brewery(JavaPlugin plugin) {
+ this.plugin = plugin;
+ instance = this;
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+ }
+
+ public static Brewery getInstance() {
+ return instance;
+ }
+
+ public JavaPlugin getPlugin() {
+ return plugin;
+ }
+
+ /**
+ * Intercepts brewing stand clicks and evaluates whether custom brewing should begin.
+ *
+ * This event handler:
+ *
+ * - Ensures interaction occurred inside a brewing stand inventory
+ * - Schedules a short delayed task (2 ticks) to allow item placement to finalize
+ * - Attempts to match inputs to a {@link BrewingRecipeData}
+ * - If matched, starts custom brewing via {@link BrewingRecipe#startBrewing}
+ *
+ *
+ * @param event The InventoryClickEvent triggered when a player interacts with an inventory.
+ */
+ @EventHandler
+ public void onBrewClick(InventoryClickEvent event) {
+ if (event.getClickedInventory() == null || event.getClickedInventory().getType() != InventoryType.BREWING)
+ return;
+
+ BrewerInventory inv = (BrewerInventory) event.getClickedInventory();
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ BrewingRecipeData recipe = BrewingRecipe.getRecipe(inv);
+ if (recipe != null)
+ BrewingRecipe.startBrewing(inv, recipe);
+ }, 2L);
+ }
+}
\ No newline at end of file
diff --git a/src/me/mehboss/brewing/BrewingRecipe.java b/src/me/mehboss/brewing/BrewingRecipe.java
new file mode 100644
index 0000000..e450f74
--- /dev/null
+++ b/src/me/mehboss/brewing/BrewingRecipe.java
@@ -0,0 +1,331 @@
+package me.mehboss.brewing;
+
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.BrewingStand;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.BrewerInventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import com.cryptomorin.xseries.XSound;
+
+import de.tr7zw.changeme.nbtapi.NBTTileEntity;
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.RecipeUtil;
+import me.mehboss.utils.RecipeUtil.Recipe;
+import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.BrewingRecipeData;
+
+/**
+ * Handles runtime logic for custom brewing recipes.
+ *
+ * This class:
+ *
+ * - Locates matching brewing recipes based on inventory contents
+ * - Starts and manages brewing timers
+ * - Consumes fuel and ingredients
+ * - Controls progress via NBT BrewTime
+ * - Applies custom brewing results
+ *
+ *
+ * Note: This class does not store recipe definitions. All
+ * recipe data is stored within {@link BrewingRecipeData} via RecipeUtil.
+ */
+public class BrewingRecipe {
+
+
+ /**
+ * Attempts to locate a brewing recipe that matches the given brewer inventory.
+ *
+ * Matching criteria:
+ *
+ * - Ingredient slot item matches recipe ingredient
+ * - Fuel slot item matches recipe fuel
+ * - Recipe is active
+ * - Player has permission (if required)
+ * - World is not disabled
+ *
+ *
+ * @param inventory The brewing stand inventory to match.
+ * @return A matching {@link BrewingRecipeData}, or {@code null} if none match.
+ */
+ public static BrewingRecipeData getRecipe(BrewerInventory inventory) {
+ RecipeUtil recipeUtil = Main.getInstance().getRecipeUtil();
+ Map recipes = recipeUtil.getRecipesFromType(RecipeType.BREWING_STAND);
+ if (recipes == null || recipes.isEmpty()) return null;
+
+ for (Recipe base : recipes.values()) {
+ if (!(base instanceof BrewingRecipeData)) continue;
+ BrewingRecipeData recipe = (BrewingRecipeData) base;
+
+ ItemStack ingredient = inventory.getItem(recipe.getBrewIngredientSlot());
+ ItemStack fuel = inventory.getItem(recipe.getBrewFuelSlot());
+
+ boolean ingredientMatch = recipe.isBrewPerfect()
+ ? (ingredient != null && ingredient.isSimilar(recipe.getBrewIngredient()))
+ : (ingredient != null && ingredient.getType() == recipe.getBrewIngredient().getType());
+
+ boolean fuelMatch = recipe.isBrewPerfect()
+ ? (fuel != null && fuel.isSimilar(recipe.getBrewFuel()))
+ : (fuel != null && fuel.getType() == recipe.getBrewFuel().getType());
+
+ Player p = null;
+ World w = inventory.getLocation().getWorld();
+
+ if (inventory.getHolder() != null && inventory.getHolder() instanceof Player)
+ p = (Player) inventory.getHolder();
+
+ boolean hasPerms = p == null || !recipe.hasPerm() || p.hasPermission(recipe.getPerm());
+ boolean allowWorld = w == null || !recipe.getDisabledWorlds().contains(w.getName());
+
+ if (ingredientMatch && fuelMatch && recipe.isActive() && hasPerms && allowWorld)
+ return recipe;
+ }
+
+ return null;
+ }
+
+ /**
+ * Begins a brewing cycle using the provided recipe.
+ *
+ * The default brewing time is 400 ticks (20 seconds), unless overridden in
+ * {@link BrewClock}.
+ *
+ *
This method:
+ *
+ * - Consumes fuel if internal fuel tank is empty
+ * - Initializes a {@link BrewClock} to track progress
+ *
+ *
+ * @param inventory Brewer inventory involved in brewing.
+ * @param recipe The recipe to brew.
+ */
+ public static void startBrewing(BrewerInventory inventory, BrewingRecipeData recipe) {
+ BrewingStand stand = inventory.getHolder();
+ if (stand == null) return;
+
+ NBTTileEntity nbt = new NBTTileEntity(stand);
+ int currentFuel = nbt.getInteger("Fuel");
+
+ // Only consume fuel item if internal fuel tank is empty
+ if (currentFuel <= 0) {
+ ItemStack fuelItem = inventory.getItem(recipe.getBrewFuelSlot());
+ if (fuelItem != null && fuelItem.getType() != Material.AIR && fuelItem.getAmount() > 0) {
+ // Add internal fuel and consume one fuel item
+ nbt.setInteger("Fuel", recipe.getBrewFuelSet());
+
+ int newAmount = fuelItem.getAmount() - 1;
+ if (newAmount <= 0)
+ inventory.setItem(recipe.getBrewFuelSlot(), new ItemStack(Material.AIR));
+ else {
+ fuelItem.setAmount(newAmount);
+ inventory.setItem(recipe.getBrewFuelSlot(), fuelItem);
+ }
+ } else {
+ return; // No fuel available
+ }
+ }
+
+ // Begin the brew process
+ new BrewClock(inventory, recipe, 400);
+ }
+
+
+ /**
+ * Handles the ticking and completion of a brewing cycle.
+ *
+ * This class:
+ *
+ * - Counts down BrewTime via NBT
+ * - Checks for missing ingredients/fuel mid-brew
+ * - Consumes internal tank fuel
+ * - Consumes recipe ingredients
+ * - Applies recipe results into potion bottle slots
+ * - Automatically triggers the next brewing cycle if conditions allow
+ *
+ */
+ private static class BrewClock extends BukkitRunnable {
+ private final BrewerInventory inventory;
+ private final BrewingRecipeData recipe;
+ private int ticksRemaining;
+
+ /**
+ * Creates and starts a brewing timer.
+ *
+ * @param inventory The brewing stand inventory.
+ * @param recipe The recipe being brewed.
+ * @param brewTimeTicks Total ticks for the brewing cycle (typically 400).
+ */
+ public BrewClock(BrewerInventory inventory, BrewingRecipeData recipe, int brewTimeTicks) {
+ this.inventory = inventory;
+ this.recipe = recipe;
+ this.ticksRemaining = brewTimeTicks;
+ runTaskTimer(Main.getInstance(), 0L, 1L);
+ }
+
+ @Override
+ public void run() {
+ BrewingStand stand = inventory.getHolder();
+ if (stand == null || stand.getBlock().getType() != Material.BREWING_STAND) {
+ cancel();
+ return;
+ }
+
+ if (ingredientOrFuelGone()) {
+ setBrewTimeNBT(stand, 0);
+ cancel();
+ return;
+ }
+
+ if (ticksRemaining <= 0) {
+ cancel();
+ finishOneBrew(stand);
+ return;
+ }
+
+ setBrewTimeNBT(stand, ticksRemaining);
+ ticksRemaining--;
+ }
+
+ /**
+ * Completes a single brewing cycle.
+ * Handles:
+ *
+ * - Fuel drain / consumption
+ * - Ingredient consumption
+ * - Result placement
+ * - Sound effects
+ * - Automatic continuation
+ *
+ *
+ * @param stand The brewing stand completing its brew.
+ */
+ private void finishOneBrew(BrewingStand stand) {
+ ItemStack ingredient = inventory.getItem(recipe.getBrewIngredientSlot());
+ if (ingredient == null || ingredient.getType() == Material.AIR)
+ return;
+
+ // --- Fuel Management ---
+ NBTTileEntity standNBT = new NBTTileEntity(stand);
+ int currentFuel = standNBT.getInteger("Fuel");
+ int requiredCharge = recipe.getBrewFuelCharge();
+
+ // If not enough internal fuel left, consume another fuel item
+ if (currentFuel < requiredCharge) {
+ ItemStack fuel = inventory.getItem(recipe.getBrewFuelSlot());
+ if (fuel != null && fuel.getType() != Material.AIR && fuel.getAmount() > 0) {
+ int newAmount = fuel.getAmount() - 1;
+ if (newAmount <= 0)
+ inventory.setItem(recipe.getBrewFuelSlot(), new ItemStack(Material.AIR));
+ else {
+ fuel.setAmount(newAmount);
+ inventory.setItem(recipe.getBrewFuelSlot(), fuel);
+ }
+ currentFuel += recipe.getBrewFuelSet();
+ } else {
+ standNBT.setInteger("Fuel", 0);
+ return;
+ }
+ }
+
+ // Drain internal fuel
+ int newFuel = Math.max(0, currentFuel - requiredCharge);
+ standNBT.setInteger("Fuel", newFuel);
+
+ // --- Ingredient consumption ---
+ int newIngAmount = ingredient.getAmount() - 1;
+ if (newIngAmount <= 0)
+ inventory.setItem(recipe.getBrewIngredientSlot(), new ItemStack(Material.AIR));
+ else {
+ ingredient.setAmount(newIngAmount);
+ inventory.setItem(recipe.getBrewIngredientSlot(), ingredient);
+ }
+
+ // --- Apply results ---
+ ItemStack result = recipe.getResult();
+ if (result != null && result.getType() != Material.AIR) {
+ boolean requiresBottles = recipe.requiresBottles();
+ Material requiredBottle = recipe.getRequiredBottleType();
+
+ for (int i = 0; i < 3; i++) {
+ ItemStack base = inventory.getItem(i);
+ if (!requiresBottles) {
+ if (base == null || base.getType() == Material.AIR)
+ inventory.setItem(i, result.clone());
+ } else if (base != null && base.getType() == requiredBottle) {
+ inventory.setItem(i, result.clone());
+ }
+ }
+ }
+
+ // --- Visual / Sound ---
+ Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> {
+ setBrewTimeNBT(stand, 0);
+ stand.getWorld().playSound(stand.getLocation(), XSound.BLOCK_BREWING_STAND_BREW.parseSound(), 1f, 1f);
+ }, 1L);
+
+ // --- Continue automatically ---
+ if (canContinue())
+ startBrewing(inventory, recipe);
+ }
+
+ /** @return true if brewing can continue into another automatic cycle. */
+ private boolean canContinue() {
+ ItemStack ing = inventory.getItem(recipe.getBrewIngredientSlot());
+ ItemStack fuel = inventory.getItem(recipe.getBrewFuelSlot());
+ return ing != null && ing.getType() != Material.AIR &&
+ fuel != null && fuel.getType() != Material.AIR &&
+ hasValidResultSlot();
+ }
+
+ /**
+ * Checks whether there is at least one available or valid bottle slot.
+ */
+ private boolean hasValidResultSlot() {
+ boolean requiresBottles = recipe.requiresBottles();
+ Material requiredBottle = recipe.getRequiredBottleType();
+ for (int i = 0; i < 3; i++) {
+ ItemStack item = inventory.getItem(i);
+ if (!requiresBottles) {
+ if (item == null || item.getType() == Material.AIR)
+ return true;
+ } else if (item != null && item.getType() == requiredBottle) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @return true if either ingredient or fuel is missing. */
+ private boolean ingredientOrFuelGone() {
+ ItemStack ing = inventory.getItem(recipe.getBrewIngredientSlot());
+ ItemStack fuel = inventory.getItem(recipe.getBrewFuelSlot());
+ return ing == null || ing.getType() == Material.AIR ||
+ fuel == null || fuel.getType() == Material.AIR;
+ }
+ }
+
+ /**
+ * Updates the brewing stand's NBT BrewTime value.
+ *
+ * @param stand The brewing stand whose BrewTime is being updated.
+ * @param ticks The number of ticks remaining in the brewing cycle.
+ */
+ private static void setBrewTimeNBT(BrewingStand stand, int ticks) {
+ try {
+ new NBTTileEntity(stand).setInteger("BrewTime", ticks);
+ } catch (Exception e) {
+ logError("Failed to update BrewTime for stand at " + stand.getLocation());
+ }
+ }
+
+ private static void logError(String msg) {
+ Logger.getLogger("Minecraft").log(Level.WARNING, "[DEBUG][" + Main.getInstance().getName() + "] " + msg);
+ }
+}
\ No newline at end of file
diff --git a/src/me/mehboss/brewing/package-info.java b/src/me/mehboss/brewing/package-info.java
new file mode 100644
index 0000000..6c4967e
--- /dev/null
+++ b/src/me/mehboss/brewing/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Handles custom brewing stand recipes and brewing mechanics.
+ *
+ * Includes recipe matching, fuel handling, brewing timers, and result
+ * processing through the BrewingRecipe and BrewClock systems.
+ */
+package me.mehboss.brewing;
\ No newline at end of file
diff --git a/src/me/mehboss/commands/CommandEditItem.java b/src/me/mehboss/commands/CommandEditItem.java
index 39deb71..0d62a85 100644
--- a/src/me/mehboss/commands/CommandEditItem.java
+++ b/src/me/mehboss/commands/CommandEditItem.java
@@ -9,8 +9,9 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
-import io.github.bananapuncher714.nbteditor.NBTEditor;
-import me.mehboss.gui.RecipeGUI;
+import de.tr7zw.changeme.nbtapi.NBT;
+import de.tr7zw.changeme.nbtapi.NBTItem;
+import me.mehboss.gui.framework.RecipeGUI;
import me.mehboss.recipe.Main;
public class CommandEditItem {
@@ -70,7 +71,9 @@ public static boolean Run(CRCommand command) {
if (command.args.length == 3 && command.args[1].equalsIgnoreCase("key")) {
ItemStack item = p.getItemInHand();
- item = NBTEditor.set(item, command.args[2], "CUSTOM_ITEM_IDENTIFIER");
+ NBT.modify(item, (nbt) -> {
+ nbt.setString("CUSTOM_ITEM_IDENTIFIER", command.args[2]);
+ });
p.setItemInHand(item);
p.sendMessage(ChatColor.GREEN + "Successfully updated item identifier!");
@@ -161,8 +164,6 @@ public static boolean Run(CRCommand command) {
}
if (command.args.length == 2 && command.args[1].equalsIgnoreCase("lore")) {
- RecipeGUI.getInstance().sendLoremsg(p);
- RecipeGUI.getInstance().editmeta.put(p.getUniqueId(), "Lore");
return false;
}
diff --git a/src/me/mehboss/commands/CommandGive.java b/src/me/mehboss/commands/CommandGive.java
index 1179ec2..8089d8d 100644
--- a/src/me/mehboss/commands/CommandGive.java
+++ b/src/me/mehboss/commands/CommandGive.java
@@ -19,7 +19,7 @@ static FileConfiguration getConfig() {
public static boolean Run(CRCommand command) {
- Recipe recipe = Main.getInstance().getRecipeUtil().getRecipe(command.args[2]);
+ Recipe recipe = Main.getInstance().getRecipeUtil().getRecipeFromKey(command.args[2]);
CommandSender p = command.sender;
int amount = 1;
@@ -56,7 +56,7 @@ public static boolean Run(CRCommand command) {
// recipe id
if (recipe != null)
- item = new ItemStack(Main.getInstance().getRecipeUtil().getRecipe(command.args[2]).getResult());
+ item = new ItemStack(recipe.getResult());
// custom item id
else
item = new ItemStack(ItemBuilder.get(command.args[2]));
diff --git a/src/me/mehboss/commands/CommandRecipe.java b/src/me/mehboss/commands/CommandRecipe.java
index 40135c5..fbf69a1 100644
--- a/src/me/mehboss/commands/CommandRecipe.java
+++ b/src/me/mehboss/commands/CommandRecipe.java
@@ -8,11 +8,15 @@
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.BrewingRecipeData;
+import me.mehboss.utils.data.CookingRecipeData;
+import me.mehboss.utils.data.CraftingRecipeData;
+import me.mehboss.utils.data.WorkstationRecipeData;
public class CommandRecipe {
static RecipeUtil getRecipeUtil() {
- return Main.getInstance().recipeUtil;
+ return Main.getInstance().recipeUtil;
}
// Keep the method header as requested
@@ -32,11 +36,11 @@ public static boolean Run(CRCommand command) {
String typeRaw = args[1];
RecipeType type;
try {
- type = RecipeType.valueOf(typeRaw.toUpperCase());
+ type = RecipeType.fromString(typeRaw.toUpperCase());
} catch (IllegalArgumentException ex) {
sender.sendMessage(ChatColor.RED + "[CustomRecipes] Invalid recipe type: " + typeRaw);
sender.sendMessage(ChatColor.GRAY
- + "Valid types: SHAPELESS, SHAPED, STONECUTTER, FURNACE, ANVIL, BLASTFURNACE, SMOKER, CAMPFIRE, GRINDSTONE, BREWING_STAND");
+ + "Valid types: SHAPELESS, SHAPED, STONECUTTER, FURNACE, ANVIL, BLASTFURNACE, SMOKER, CAMPFIRE, GRINDSTONE, BREWING");
return true;
}
@@ -61,11 +65,43 @@ public static boolean Run(CRCommand command) {
// Optional permission
String permission = (args.length >= 4) ? args[3] : "none";
+ Recipe recipe;
+ switch (type) {
+
+ case SHAPED:
+ case SHAPELESS:
+ recipe = new CraftingRecipeData(id);
+ break;
+
+ case FURNACE:
+ case BLASTFURNACE:
+ case SMOKER:
+ case CAMPFIRE:
+ recipe = new CookingRecipeData(id);
+ break;
+
+ case STONECUTTER:
+ case GRINDSTONE:
+ case ANVIL:
+ recipe = new WorkstationRecipeData(id);
+ break;
+
+ case BREWING_STAND:
+ recipe = new BrewingRecipeData(id);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unsupported recipe type: " + type);
+ }
+
+ recipe.setType(type);
+ recipe.setPerm(permission);
+
sender.sendMessage(
ChatColor.translateAlternateColorCodes('&', "&c[CustomRecipes] &fPreparing to create recipe:&r "));
sender.sendMessage(ChatColor.translateAlternateColorCodes('&',
"&fType= &a" + type.toString() + " &fID= &a" + id + " &fPermission= &a" + permission));
- Main.getInstance().recipes.showCreationMenu(null, null, (Player) sender, id, permission, true, false, type);
+ Main.getInstance().recipes.showCreationMenu((Player) sender, recipe, true, false);
return true;
}
}
\ No newline at end of file
diff --git a/src/me/mehboss/commands/CommandRecipes.java b/src/me/mehboss/commands/CommandRecipes.java
new file mode 100644
index 0000000..5aa092a
--- /dev/null
+++ b/src/me/mehboss/commands/CommandRecipes.java
@@ -0,0 +1,35 @@
+package me.mehboss.commands;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Player;
+
+import me.mehboss.recipe.Main;
+
+public class CommandRecipes implements CommandExecutor {
+
+ FileConfiguration getConfig() {
+ return Main.getInstance().getConfig();
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ final CRCommand cmd = new CRCommand(sender, command, label, args);
+
+ if (!(sender instanceof Player)) {
+ sender.sendMessage("You must be a player in order to use this command!");
+ return true;
+ }
+
+ if (!sender.hasPermission("crecipe.book")) {
+ sender.sendMessage(
+ ChatColor.translateAlternateColorCodes('&', getConfig().getString("Messages.Invalid-Perms")));
+ return true;
+ }
+
+ return CommandBook.Run(cmd);
+ }
+}
diff --git a/src/me/mehboss/commands/CommandRemove.java b/src/me/mehboss/commands/CommandRemove.java
new file mode 100644
index 0000000..0c5143d
--- /dev/null
+++ b/src/me/mehboss/commands/CommandRemove.java
@@ -0,0 +1,49 @@
+package me.mehboss.commands;
+
+import java.io.File;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.RecipeUtil;
+import me.mehboss.utils.RecipeUtil.Recipe;
+
+public class CommandRemove {
+
+ static RecipeUtil getRecipeUtil() {
+ return Main.getInstance().recipeUtil;
+ }
+
+ public static boolean Run(CRCommand command) {
+ CommandSender sender = command.sender;
+ String[] args = command.args;
+
+ if (args.length < 2) {
+ sender.sendMessage(ChatColor.RED + "[CustomRecipes] Usage: /crecipe remove ");
+ return true;
+ }
+
+ String id = args[1].toLowerCase();
+ Recipe existing = getRecipeUtil().getRecipeFromKey(id);
+ if (existing == null) {
+ sender.sendMessage(ChatColor.RED + "[CustomRecipes] A recipe with the ID '" + id + "' could not be found.");
+ return true;
+ }
+
+ File recipeFile = new File(Main.getInstance().getDataFolder(), "recipes/" + existing.getName() + ".yml");
+ if (recipeFile.exists()) {
+ if (recipeFile.delete()) {
+ removeRecipe(existing.getName());
+ sender.sendMessage(ChatColor.GREEN + "[CustomRecipes] Recipe '" + id + "' was successfully deleted.");
+ } else {
+ sender.sendMessage(ChatColor.RED + "[CustomRecipes] Failed to delete recipe '" + id + "'.");
+ }
+ }
+ return true;
+ }
+
+ public static void removeRecipe(String name) {
+ getRecipeUtil().removeRecipe(name);
+ }
+
+}
\ No newline at end of file
diff --git a/src/me/mehboss/commands/CommandShow.java b/src/me/mehboss/commands/CommandShow.java
index 81e1c1a..6f1f1d1 100644
--- a/src/me/mehboss/commands/CommandShow.java
+++ b/src/me/mehboss/commands/CommandShow.java
@@ -11,7 +11,7 @@
public class CommandShow {
static RecipeUtil getRecipeUtil() {
- return Main.getInstance().recipeUtil;
+ return Main.getInstance().recipeUtil;
}
public static boolean Run(CRCommand command) {
@@ -22,7 +22,7 @@ public static boolean Run(CRCommand command) {
sender.sendMessage(ChatColor.RED + "[CustomRecipes] Usage: /crecipe show ");
return true;
}
-
+
String id = args[1].toLowerCase();
// Check if recipe already exists
Recipe existing = getRecipeUtil().getRecipeFromKey(id);
@@ -33,8 +33,7 @@ public static boolean Run(CRCommand command) {
Player p = (Player) sender;
sender.sendMessage(ChatColor.RED + "[CustomRecipes] Showing recipe with ID '" + id + "'..");
- Main.getInstance().recipes.showCreationMenu(null, existing.getResult(), p, existing.getName(), null, false,
- false, null);
+ Main.getInstance().recipes.showCreationMenu(p, existing, false, false);
return true;
}
}
\ No newline at end of file
diff --git a/src/me/mehboss/commands/TabCompletion.java b/src/me/mehboss/commands/TabCompletion.java
index fe8f347..00b825a 100644
--- a/src/me/mehboss/commands/TabCompletion.java
+++ b/src/me/mehboss/commands/TabCompletion.java
@@ -2,7 +2,9 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@@ -12,6 +14,8 @@
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
+import me.mehboss.utils.ItemBuilder;
+
import me.mehboss.recipe.Main;
public class TabCompletion implements TabCompleter {
@@ -30,6 +34,8 @@ public List onTabComplete(CommandSender sender, Command command, String
}
if (sender.hasPermission("crecipe.list"))
subCommands.add("list");
+ if (sender.hasPermission("crecipe.items"))
+ subCommands.add("items");
if (sender.hasPermission("crecipe.give"))
subCommands.add("give");
if (sender.hasPermission("crecipe.reload"))
@@ -57,7 +63,7 @@ public List onTabComplete(CommandSender sender, Command command, String
} else if (args[0].equalsIgnoreCase("give") && args.length == 3 && sender.hasPermission("crecipe.give")) {
// /customrecipes give
- StringUtil.copyPartialMatches(args[2], getRecipes(), completions);
+ StringUtil.copyPartialMatches(args[2], getAllKeys(), completions);
} else if (args[0].equalsIgnoreCase("edititem") && sender.hasPermission("crecipe.edititem")) {
// /customrecipes edititem ...
@@ -149,6 +155,7 @@ private List getRecipeTypes() {
types.add("CAMPFIRE");
types.add("ANVIL");
types.add("GRINDSTONE");
+ types.add("BREWING");
return types;
}
@@ -160,7 +167,14 @@ private List getOnlinePlayerNames() {
return playerNames;
}
+ private Collection getAllKeys() {
+ Set combined = new HashSet<>();
+ combined.addAll(ItemBuilder.getAllItems());
+ combined.addAll(Main.getInstance().recipeUtil.getAllKeys());
+ return combined;
+ }
+
private Collection getRecipes() {
- return Main.getInstance().recipeUtil.getAllRecipes().keySet();
+ return Main.getInstance().recipeUtil.getAllKeys();
}
}
diff --git a/src/me/mehboss/commands/package-info.java b/src/me/mehboss/commands/package-info.java
new file mode 100644
index 0000000..9e481b2
--- /dev/null
+++ b/src/me/mehboss/commands/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Provides all command handlers for the plugin, including administrative
+ * commands, player utilities, debug tools, and recipe management commands.
+ *
+ * Each command class implements a specific action such as listing recipes,
+ * reloading configuration, or giving custom items.
+ */
+package me.mehboss.commands;
\ No newline at end of file
diff --git a/src/me/mehboss/cooking/CookingManager.java b/src/me/mehboss/cooking/CookingManager.java
index e3cec59..1a858f3 100644
--- a/src/me/mehboss/cooking/CookingManager.java
+++ b/src/me/mehboss/cooking/CookingManager.java
@@ -24,15 +24,39 @@
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Recipe;
+/**
+ * Handles custom furnace, smoker, and blast furnace recipe behaviors.
+ *
+ * Responsible for:
+ *
+ * - Tracking which player is interacting with which furnace
+ * - Validating permissions before smelting custom results
+ * - Preventing invalid or unauthorized smelting attempts
+ * - Forcing furnace recipe refreshes to enable custom results
+ *
+ *
+ * This system relies on {@link RecipeUtil} to determine whether a smelted output
+ * corresponds to a custom recipe, and applies appropriate permission/world checks.
+ */
public class CookingManager implements Listener {
public HashMap cooking = new HashMap();
- /*
- * Furnace start event
- */
+ /**
+ * Fired when a custom item is being smelted and the server is about to
+ * output a result item.
+ *
+ * This event performs:
+ *
+ * - Permission validation
+ * - Disabled-world checks
+ * - Cancellation of illegal smelts
+ *
+ *
+ * @param e The smelt event containing the result and furnace context.
+ */
@EventHandler
- public void onFurnaceSmelt(FurnaceSmeltEvent e) {
+ void onFurnaceSmelt(FurnaceSmeltEvent e) {
ItemStack item = e.getResult();
Furnace f = (Furnace) e.getBlock().getState();
FurnaceInventory inv = f.getInventory();
@@ -64,8 +88,20 @@ public void onFurnaceSmelt(FurnaceSmeltEvent e) {
logDebug("[FurnaceSmelt] Attempt to smelt " + recipe.getName() + " has been detected, handling override..");
}
+ /**
+ * Ensures correct result-matching for custom furnace recipes.
+ *
+ * When a furnace starts smelting:
+ *
+ * - Detects if a custom recipe should apply
+ * - Prevents vanilla smelting from overriding custom smelts
+ * - Forces a recipe refresh by inserting a dummy stone item
+ *
+ *
+ * @param e FurnaceStartSmeltEvent triggered when the furnace begins smelting.
+ */
@EventHandler
- public void onStart(FurnaceStartSmeltEvent e) {
+ void onStart(FurnaceStartSmeltEvent e) {
if (Main.getInstance().serverVersionLessThan(1, 16))
return;
@@ -91,8 +127,15 @@ public void onStart(FurnaceStartSmeltEvent e) {
furnace.update(true, true);
}
+ /**
+ * Tracks which player last interacted with a furnace/smoker/blast furnace.
+ *
+ * This allows permission checking when custom smelting begins.
+ *
+ * @param e InventoryClickEvent fired when a player clicks an inventory.
+ */
@EventHandler
- public void onFurnaceClick(InventoryClickEvent e) {
+ void onFurnaceClick(InventoryClickEvent e) {
if (e.getInventory().getType() == null)
return;
@@ -107,8 +150,14 @@ public void onFurnaceClick(InventoryClickEvent e) {
}
}
+
+ /**
+ * Removes furnace tracking when the player closes the inventory.
+ *
+ * @param e InventoryCloseEvent triggered when inventory is closed.
+ */
@EventHandler
- public void onFurnaceClose(InventoryCloseEvent e) {
+ void onFurnaceClose(InventoryCloseEvent e) {
if (cooking.containsKey(e.getInventory()))
cooking.remove(e.getInventory());
}
diff --git a/src/me/mehboss/cooking/package-info.java b/src/me/mehboss/cooking/package-info.java
new file mode 100644
index 0000000..b725286
--- /dev/null
+++ b/src/me/mehboss/cooking/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Contains custom furnace, smoker, and blast furnace processing logic.
+ *
+ * Handles custom smelting recipes, permission checks, furnace interaction
+ * tracking, and result overrides for cooking-based workstations.
+ */
+package me.mehboss.cooking;
\ No newline at end of file
diff --git a/src/me/mehboss/crafting/AmountManager.java b/src/me/mehboss/crafting/AmountManager.java
index c464cf9..fde803d 100644
--- a/src/me/mehboss/crafting/AmountManager.java
+++ b/src/me/mehboss/crafting/AmountManager.java
@@ -4,17 +4,23 @@
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
+import org.bukkit.event.Event.Result;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.inventory.InventoryAction;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryType.SlotType;
+import org.bukkit.event.inventory.PrepareItemCraftEvent;
import org.bukkit.inventory.CraftingInventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapelessRecipe;
@@ -23,10 +29,13 @@
import io.github.bananapuncher714.nbteditor.NBTEditor;
import me.mehboss.recipe.Main;
+import me.mehboss.utils.CooldownManager.Cooldown;
+import me.mehboss.utils.CooldownManager;
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Ingredient;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.CraftingRecipeData;
public class AmountManager implements Listener {
@@ -41,10 +50,18 @@ void logDebug(String st) {
Logger.getLogger("Minecraft").log(Level.WARNING, "[DEBUG][" + Main.getInstance().getName() + "]" + st);
}
+ CooldownManager getCooldownManager() {
+ return Main.getInstance().cooldownManager;
+ }
+
RecipeUtil getRecipeUtil() {
return Main.getInstance().recipeUtil;
}
+ ShapedChecks shapedChecks() {
+ return Main.getInstance().shapedChecks;
+ }
+
private boolean hasAllIngredients(CraftingInventory inv, String recipes, List recipeIngredients,
UUID id) {
return craftManager.hasAllIngredients(inv, recipes, recipeIngredients, id);
@@ -56,8 +73,9 @@ private boolean hasMatchingDisplayName(String recipeName, ItemStack item, String
}
// Helper method for the handleShiftClick method
- void handlesItemRemoval(CraftItemEvent e, CraftingInventory inv, Recipe recipe, ItemStack item,
- RecipeUtil.Ingredient ingredient, int slot, int itemsToRemove, int itemsToAdd, int requiredAmount) {
+ void handlesItemRemoval(CraftItemEvent e, CraftingInventory inv, CraftingRecipeData recipe, ItemStack item,
+ RecipeUtil.Ingredient ingredient, int slot, int itemsToRemove, int itemsToAdd, int requiredAmount,
+ AtomicBoolean containerCraft) {
String recipeName = recipe.getName();
logDebug("[handleShiftClicks] Checking slot " + slot + " for the recipe " + recipeName);
@@ -90,12 +108,36 @@ void handlesItemRemoval(CraftItemEvent e, CraftingInventory inv, Recipe recipe,
}
if (e.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) {
- if ((item.getAmount() + 1) - requiredAmount == 0)
- inv.setItem(slot, null);
- else
+ if (item.getAmount() == 1 && !containerCraft.get())
+ return;
+
+ if (((item.getAmount()) - requiredAmount == 0)) {
+ if (containerCraft.get() && !recipe.isLeftover(id)) {
+ Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
+ inv.setItem(slot, null);
+ });
+ return;
+ }
+ // set to 1, not 0, since we don't cancel the event. Vanilla will handle the
+ // leftover.
+ item.setAmount(1);
+ } else {
+ if (containerCraft.get()) {
+ // remove exact, since containers are cancelled, normal vanilla deduction does
+ // not happen.
+ item.setAmount((item.getAmount()) - requiredAmount);
+ return;
+ }
item.setAmount((item.getAmount() + 1) - requiredAmount);
+ }
} else {
if ((item.getAmount() - itemsToRemove) <= 0) {
+ if (containerCraft.get() && !recipe.isLeftover(id)) {
+ Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
+ inv.setItem(slot, null);
+ });
+ return;
+ }
inv.setItem(slot, null);
return;
}
@@ -112,11 +154,10 @@ boolean matchesIngredient(ItemStack item, String recipeName, RecipeUtil.Ingredie
ItemStack customItem = ingredient.hasIdentifier() ? getRecipeUtil().getResultFromKey(ingredient.getIdentifier())
: null;
- return (customItem != null && item.isSimilar(customItem))
- || (craftManager.tagsMatch(ingredient, item)
- || (ingredient.hasIdentifier() && exactMatch != null && item.isSimilar(exactMatch.getResult()))
- || (item.getType() == material && hasMatchingDisplayName(recipeName, item, displayName,
- ingredient.getIdentifier(), hasIdentifier, false)));
+ return (customItem != null && item.isSimilar(customItem)) || (craftManager.tagsMatch(ingredient, item)
+ || (ingredient.hasIdentifier() && exactMatch != null && item.isSimilar(exactMatch.getResult()))
+ || (item.getType() == material && hasMatchingDisplayName(recipeName, item, displayName,
+ ingredient.getIdentifier(), hasIdentifier, false)));
}
boolean isCraftingRecipe(RecipeType type) {
@@ -133,222 +174,261 @@ void handleShiftClicks(CraftItemEvent e) {
if (inv.getResult() == null || inv.getResult().getType() == Material.AIR)
return;
- // added debounce mechanism, which will automatically remove after 75ms to
- // greatly improve server performance on large amounts of crafts (such as
- // concrete power)
+ // debounce
+ logDebug("[handleShiftClicks] Fired #1");
+
+ Recipe matched = getRecipeUtil().getRecipeFromResult(inv.getResult());
Main.getInstance().debounceMap.put(id, System.currentTimeMillis());
- if (getRecipeUtil().getRecipeFromResult(inv.getResult()) == null)
+ if (matched == null)
return;
- logDebug("[handleShiftClicks] Fired amount checking mechanics..");
+ logDebug("[handleShiftClicks] Fired #2");
if (e.getCurrentItem() == null || e.getCurrentItem().getType() == Material.AIR)
return;
- String findName = getRecipeUtil().getRecipeFromResult(inv.getResult()) != null
- ? getRecipeUtil().getRecipeFromResult(inv.getResult()).getName()
- : null;
+ ItemStack cursor = e.getCursor();
+ ItemStack result = inv.getResult();
+ String findName = matched.getName();
+ boolean isShapeless = e.getRecipe() instanceof ShapelessRecipe;
- final ItemStack result = inv.getResult();
- boolean isShapeless = e.getRecipe() instanceof ShapelessRecipe ? true : false;
HashMap types = isShapeless ? getRecipeUtil().getRecipesFromType(RecipeType.SHAPELESS)
: getRecipeUtil().getRecipesFromType(RecipeType.SHAPED);
- logDebug("[handleShiftClicks] Initial recipe found recipe '" + findName + "' to handle..");
-
if (NBTEditor.contains(inv.getResult(), NBTEditor.CUSTOM_DATA, "CUSTOM_ITEM_IDENTIFIER")) {
String foundID = NBTEditor.getString(inv.getResult(), NBTEditor.CUSTOM_DATA, "CUSTOM_ITEM_IDENTIFIER");
-
- if (getRecipeUtil().getRecipeFromKey(foundID) != null) {
- findName = getRecipeUtil().getRecipeFromKey(foundID).getName();
- }
+ Recipe keyedRecipe = getRecipeUtil().getRecipeFromKey(foundID);
+ if (keyedRecipe != null)
+ findName = keyedRecipe.getName();
}
- // safe guard check to prevent matches to other recipes types
- if (types != null && !types.isEmpty())
- for (Recipe recipe : types.values()) {
- ItemStack item = recipe.getResult();
- if (hasAllIngredients(inv, recipe.getName(), recipe.getIngredients(), id)
- && (result.equals(item) || result.isSimilar(item))) {
- findName = recipe.getName();
+ // safeguard: match correct custom recipe result
+ if (types != null)
+ for (Recipe r : types.values()) {
+ ItemStack itm = r.getResult();
+ if ((result.equals(itm) || result.isSimilar(itm))
+ && hasAllIngredients(inv, r.getName(), r.getIngredients(), id)) {
+ findName = r.getName();
break;
}
}
- logDebug("[handleShiftClicks] Actual found recipe '" + findName + "' to handle..");
+ logDebug("[handleShiftClicks] Final recipe = " + findName);
- if (findName == null)
+ CraftingRecipeData recipe = (CraftingRecipeData) getRecipeUtil().getRecipe(findName);
+ if (recipe == null)
return;
- logDebug("[handleShiftClicks] Paired it to a custom recipe. Running crafting amount calculations..");
+ if (!recipe.isGrantItem()) {
+ if (e.getAction() == InventoryAction.DROP_ONE_SLOT || e.getAction() == InventoryAction.DROP_ALL_SLOT) {
+ logDebug("[handleShiftClicks] Denying craft due to drop request on an ungrantable item..");
+ e.setCancelled(true);
+ return;
+ }
+ }
- final String recipeName = findName;
- if (e.isCancelled()) {
- logDebug("Couldn't complete craftItemEvent for recipe " + recipeName
- + ", the event was unexpectedly cancelled.");
- logDebug("Please seek support or open a ticket https://github.com/mehboss/CustomRecipes/issues");
- return;
+ // prevents "ghost crafts" where the item isn't supposed to craft, but
+ // CraftItemEvent still fires.
+ if (cursor != null && cursor.getType() != Material.AIR) {
+ boolean canStackWithResult = cursor.isSimilar(result)
+ && cursor.getAmount() + result.getAmount() <= cursor.getMaxStackSize();
+
+ if (!canStackWithResult) {
+ e.setCancelled(true);
+ return;
+ }
}
- Recipe recipe = getRecipeUtil().getRecipe(recipeName);
- ArrayList handledIngredients = new ArrayList();
+ RecipeType type = recipe.getType();
+ ArrayList handledIngredients = new ArrayList<>();
- // =========================
- // PASS 1: compute itemsToAdd
- // bottleneck across ingredients based on required amounts
- // =========================
+ // ============================================================
+ // PASS 1 — ITEMS TO ADD
+ // ============================================================
int itemsToAdd = Integer.MAX_VALUE;
int itemsToRemove = 0;
- for (RecipeUtil.Ingredient ingredient : recipe.getIngredients()) {
- if (ingredient.isEmpty())
- continue;
+ ShapedChecks.AlignedResult aligned = null;
+
+ if (type == RecipeType.SHAPED) {
+
+ aligned = shapedChecks().getAlignedGrid(inv, recipe.getIngredients());
+ if (aligned == null) {
+ // cannot craft at all
+ itemsToAdd = 0;
+ } else {
+
+ for (int i = 0; i < recipe.getIngredients().size(); i++) {
+
+ Ingredient ing = recipe.getIngredients().get(i);
+ if (ing.isEmpty())
+ continue;
+
+ ItemStack stack = aligned.getMatrix[i];
+ if (stack == null || stack.getType() == Material.AIR) {
+ itemsToAdd = 0;
+ break;
+ }
- final int requiredAmount = Math.max(1, ingredient.getAmount());
- int totalAvailable = 0;
+ int requiredAmount = Math.max(1, ing.getAmount());
+ int possible = stack.getAmount() / requiredAmount;
+ itemsToAdd = Math.min(itemsToAdd, possible);
+ }
+ }
+
+ } else {
+ // shapeless
+ for (Ingredient ing : recipe.getIngredients()) {
+ if (ing.isEmpty())
+ continue;
+ int req = Math.max(1, ing.getAmount());
- if (recipe.getType() == RecipeType.SHAPELESS) {
- // Sum across ALL crafting grid slots (1..9) that match THIS ingredient
for (int i = 1; i < 10; i++) {
ItemStack slot = inv.getItem(i);
if (slot == null || slot.getType() == Material.AIR)
continue;
- if (matchesIngredient(slot, recipeName, ingredient, ingredient.getMaterial(),
- ingredient.getDisplayName(), ingredient.hasIdentifier())) {
- totalAvailable += slot.getAmount();
- }
- }
- } else {
- // SHAPED / fixed-slot types: only the ingredient's slot counts
- int fixed = ingredient.getSlot();
- ItemStack slot = inv.getItem(fixed);
- if (slot != null && slot.getType() != Material.AIR && matchesIngredient(slot, recipeName, ingredient,
- ingredient.getMaterial(), ingredient.getDisplayName(), ingredient.hasIdentifier())) {
- totalAvailable = slot.getAmount();
+ if (!matchesIngredient(slot, findName, ing, ing.getMaterial(), ing.getDisplayName(),
+ ing.hasIdentifier()))
+ continue;
+
+ int possible = slot.getAmount() / req;
+ itemsToAdd = Math.min(itemsToAdd, possible);
}
}
-
- int possibleForThisIngredient = totalAvailable / requiredAmount; // floor
- itemsToAdd = Math.min(itemsToAdd, possibleForThisIngredient);
}
- // If nothing craftable, bail before removal logic
if (itemsToAdd <= 0 || itemsToAdd == Integer.MAX_VALUE) {
- logDebug("[handleShiftClicks][" + findName
- + "] An issue has been detected whilest calculating amount deductions..");
- logDebug("Please reach out for support to report this.. this shouldn't happen.");
+ logDebug("[handleShiftClicks][" + findName + "] No possible crafts.");
e.setCancelled(true);
return;
}
- // =========================
- // PASS 2:
- // =========================
- for (RecipeUtil.Ingredient ingredient : recipe.getIngredients()) {
- if (ingredient.isEmpty())
- continue;
+ boolean recipeHasContainer = recipe.getAllIngredientTypes().stream()
+ .anyMatch(mat -> mat == XMaterial.DRAGON_BREATH.get() || mat == XMaterial.POTION.get()
+ || mat == XMaterial.LINGERING_POTION.get() || mat.toString().contains("_BUCKET"));
+ AtomicBoolean containerCraft = new AtomicBoolean(recipeHasContainer);
- final Material material = ingredient.getMaterial();
- final String displayName = ingredient.getDisplayName();
- final int requiredAmount = Math.max(1, ingredient.getAmount());
- final boolean hasIdentifier = ingredient.hasIdentifier();
+ // ============================================================
+ // PASS 2 — REMOVE ITEMS
+ // ============================================================
+ if (type == RecipeType.SHAPED) {
- if (recipe.getType() == RecipeType.SHAPELESS) {
- logDebug("[handleShiftClicks] Found shapeless recipe to handle..");
+ for (int i = 0; i < recipe.getIngredients().size(); i++) {
- for (int i = 1; i < 10; i++) {
- ItemStack item = inv.getItem(i);
- int slot = i;
+ Ingredient ing = recipe.getIngredients().get(i);
+ if (ing.isEmpty())
+ continue;
- if (item == null || item.getType() == Material.AIR)
- continue;
+ int realInvSlot = aligned.invSlotMap[i];
+ ItemStack stack = inv.getItem(realInvSlot);
+ if (stack == null || stack.getType() == Material.AIR)
+ continue;
- if (!matchesIngredient(item, recipeName, ingredient, material, displayName, hasIdentifier))
- continue;
+ int requiredAmount = Math.max(1, ing.getAmount());
- if (!handledIngredients.contains(ingredient.getAbbreviation())) {
- handlesItemRemoval(e, inv, recipe, item, ingredient, slot, itemsToRemove, itemsToAdd,
- requiredAmount);
- }
- }
+ handlesItemRemoval(e, inv, recipe, stack, ing, realInvSlot, itemsToRemove, itemsToAdd, requiredAmount,
+ containerCraft);
+ }
- if (!handledIngredients.contains(ingredient.getAbbreviation()))
- handledIngredients.add(ingredient.getAbbreviation());
+ } else {
+ // shapeless
+ for (Ingredient ing : recipe.getIngredients()) {
+ if (ing.isEmpty())
+ continue;
- } else {
- logDebug("[handleShiftClicks] Found other recipe type to handle..");
+ final Material mat = ing.getMaterial();
+ final String name = ing.getDisplayName();
+ final boolean hasID = ing.hasIdentifier();
+ int req = Math.max(1, ing.getAmount());
- ItemStack item = inv.getItem(ingredient.getSlot());
- int slot = ingredient.getSlot();
+ for (int i = 1; i < 10; i++) {
+ ItemStack stack = inv.getItem(i);
+ if (stack == null || stack.getType() == Material.AIR)
+ continue;
- if (item == null || item.getType() == Material.AIR)
- continue;
+ if (!matchesIngredient(stack, findName, ing, mat, name, hasID))
+ continue;
- handlesItemRemoval(e, inv, recipe, item, ingredient, slot, itemsToRemove, itemsToAdd, requiredAmount);
+ if (!handledIngredients.contains(ing.getAbbreviation())) {
+ handlesItemRemoval(e, inv, recipe, stack, ing, i, itemsToRemove, itemsToAdd, req,
+ containerCraft);
+ }
+ }
+
+ handledIngredients.add(ing.getAbbreviation());
}
}
+ // ============================================================
+ // COMMANDS + OUTPUT
+ // ============================================================
Player player = (Player) e.getWhoClicked();
- Main.getInstance().cooldownManager.setCooldown(player.getUniqueId(), recipe.getKey(),
- System.currentTimeMillis());
+ Cooldown cooldown = new Cooldown(recipe.getKey(), recipe.getCooldown());
+ getCooldownManager().addCooldown(player.getUniqueId(), cooldown);
- // Delayed task to prevent debug spam
+ // debug suppression window
Main.getInstance().inInventory.add(id);
- Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> {
- Main.getInstance().inInventory.remove(id);
- }, 2L);
+ Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> Main.getInstance().inInventory.remove(id), 2L);
- // Handles the commands portion
if (recipe.hasCommands()) {
- logDebug("[ExecuteCMD] Found commands to run for recipe " + recipeName);
-
if (e.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY)
itemsToAdd = 1;
- for (int n = 0; n < itemsToAdd; n++) {
- for (String command : recipe.getCommand()) {
- String parsed = command.replace("%crafter%", player.getName());
- Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed);
- }
- }
+ for (int n = 0; n < itemsToAdd; n++)
+ for (String cmd : recipe.getCommand())
+ Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%crafter%", player.getName()));
if (!recipe.isGrantItem()) {
- logDebug("[ExecuteCMD] Cancelling craft of " + recipeName
- + " because a command to perform was found instead.");
- Bukkit.getScheduler().runTaskLater(Main.getInstance(), new Runnable() {
- @Override
- public void run() {
- e.getWhoClicked().setItemOnCursor(null);
- }
- }, 2L);
-
+ Bukkit.getScheduler().runTaskLater(Main.getInstance(), () -> e.getWhoClicked().setItemOnCursor(null),
+ 2L);
return;
}
}
- // Add the result items to the player's inventory
if (e.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) {
- logDebug("[handleShiftClicks] Didn't detect shift click from inventory.. Ignoring..");
-
- } else {
- e.setCancelled(true);
+ logDebug("[handleShiftClicks] Normal click, no mass-add");
- if (recipe.hasCommands() && !recipe.isGrantItem())
- return;
+ if (containerCraft.get()) {
+ e.setCancelled(true);
- inv.setResult(new ItemStack(Material.AIR));
+ logDebug("[handleShiftClicks] Manual handle of container, since vanilla mechanics replace.");
+ if (cursor == null || cursor.getType() == Material.AIR)
+ player.setItemOnCursor(result.clone());
+ else if (cursor.isSimilar(result))
+ cursor.setAmount(cursor.getAmount() + result.getAmount());
+ else
+ return;
- for (int i = 0; i < itemsToAdd; i++) {
- if (player.getInventory().firstEmpty() == -1) {
- player.getLocation().getWorld().dropItem(player.getLocation(), result);
- continue;
- }
- player.getInventory().addItem(result);
+ // since we cancel, we have to retrigger an evaluation manually
+ Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
+ PrepareItemCraftEvent event = new PrepareItemCraftEvent(inv, e.getView(), false);
+ Bukkit.getPluginManager().callEvent(event);
+ });
}
- logDebug("[handleShiftClicks] Shift click detected. Adding " + itemsToAdd + " to inventory.");
- logDebug("[handleShiftClicks] Added " + itemsToAdd + " items and removed items from table.");
+ if (itemsToAdd == 1)
+ Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
+ inv.setResult(new ItemStack(Material.AIR));
+ });
+ return;
+ }
+
+ // SHIFT CLICK OUTPUT
+ e.setCancelled(true);
+
+ if (recipe.hasCommands() && !recipe.isGrantItem())
+ return;
+
+ inv.setResult(new ItemStack(Material.AIR));
+
+ for (int i = 0; i < itemsToAdd; i++) {
+ if (player.getInventory().firstEmpty() == -1)
+ player.getWorld().dropItem(player.getLocation(), result.clone());
+ else
+ player.getInventory().addItem(result.clone());
}
+
+ logDebug("[handleShiftClicks] Added x" + itemsToAdd);
}
}
diff --git a/src/me/mehboss/crafting/CooldownManager.java b/src/me/mehboss/crafting/CooldownManager.java
deleted file mode 100644
index 1b1ae74..0000000
--- a/src/me/mehboss/crafting/CooldownManager.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package me.mehboss.crafting;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-
-import me.mehboss.recipe.Main;
-
-public class CooldownManager {
- private Map> playerCooldowns = new HashMap<>(); // Keyed by player UUID, then by recipe ID
-
- // Sets the cooldown for a player and a specific recipe by recipeID
- public void setCooldown(UUID player, String recipeID, long time) {
- if (time < 1) {
- removeCooldown(player, recipeID); // Remove cooldown if time is less than 1
- } else {
- playerCooldowns
- .computeIfAbsent(player, k -> new HashMap<>()) // Ensure the player's cooldown map exists
- .put(recipeID, time); // Set the cooldown for the specific recipe
- }
- }
-
- // Removes the cooldown for a specific recipe for a player by recipeID
- public void removeCooldown(UUID player, String recipeID) {
- if (playerCooldowns.containsKey(player)) {
- playerCooldowns.get(player).remove(recipeID);
- }
- }
-
- // Get the time left for the recipe cooldown by recipeID
- public Long getTimeLeft(UUID player, String recipeID) {
- if (recipeID == null || !hasCooldown(player, recipeID)) {
- return 0L;
- }
-
- long timeLeft = getCooldownTime(recipeID)
- - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() -
- playerCooldowns.getOrDefault(player, new HashMap<>()).getOrDefault(recipeID, 0L));
-
- return timeLeft;
- }
-
- // Checks if the cooldown for a recipe by recipeID has expired for the player
- public boolean cooldownExpired(UUID player, String recipeID) {
- if (recipeID == null || !hasCooldown(player, recipeID)) {
- return true; // If recipe doesn't have a cooldown, treat it as expired
- }
-
- long timeLeft = TimeUnit.MILLISECONDS
- .toSeconds(System.currentTimeMillis() -
- playerCooldowns.getOrDefault(player, new HashMap<>()).getOrDefault(recipeID, 0L));
-
- // Assuming getCooldownTime() returns the cooldown period for a given recipeID
- if (timeLeft >= getCooldownTime(recipeID)) {
- removeCooldown(player, recipeID); // Remove the expired cooldown
- return true;
- }
-
- return false;
- }
-
- // Returns true if the player has a cooldown for the recipe by recipeID
- public boolean hasCooldown(UUID player, String recipeID) {
- return playerCooldowns.containsKey(player) && playerCooldowns.get(player).containsKey(recipeID);
- }
-
- // Assuming you have a method to get cooldown time for a given recipeID (you can customize this)
- private long getCooldownTime(String recipeID) {
- // Placeholder: You would need to replace this with actual logic to fetch the cooldown for a given recipe
- // This could be from a configuration, a database, etc.
- return Main.getInstance().recipeUtil.getRecipeFromKey(recipeID).getCooldown(); // Example: Default cooldown time for all recipes is 60 seconds
- }
-
- // Returns the player's cooldown map for debugging or other purposes
- public Map> getCooldowns() {
- return playerCooldowns;
- }
-
- // Sets the entire cooldown map, in case you need to load it from a file/database
- public void setCooldownMap(Map> map) {
- try {
- playerCooldowns = map;
- } catch (Exception e) {
- Main.getInstance().getLogger().log(Level.SEVERE,
- "An error has occurred while attempting to fetch recipe cooldowns");
- e.printStackTrace();
- }
- }
-}
\ No newline at end of file
diff --git a/src/me/mehboss/crafting/CraftManager.java b/src/me/mehboss/crafting/CraftManager.java
index 3a4d378..9d0c94b 100644
--- a/src/me/mehboss/crafting/CraftManager.java
+++ b/src/me/mehboss/crafting/CraftManager.java
@@ -15,17 +15,17 @@
*/
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
import org.bukkit.Material;
-import org.bukkit.attribute.Attribute;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -36,25 +36,26 @@
import org.bukkit.inventory.CraftingInventory;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.ShapelessRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import com.cryptomorin.xseries.XMaterial;
-import com.ssomar.score.api.executableitems.ExecutableItemsAPI;
-import com.ssomar.score.api.executableitems.config.ExecutableItemInterface;
-
-import dev.lone.itemsadder.api.CustomStack;
import io.github.bananapuncher714.nbteditor.NBTEditor;
import me.clip.placeholderapi.PlaceholderAPI;
+import me.mehboss.crafting.ShapedChecks.AlignedResult;
import me.mehboss.recipe.Main;
import me.mehboss.utils.CompatibilityUtil;
+import me.mehboss.utils.CooldownManager;
import me.mehboss.utils.RecipeConditions.ConditionSet;
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
-import net.md_5.bungee.api.ChatColor;
+import me.mehboss.utils.data.CraftingRecipeData;
public class CraftManager implements Listener {
+ FileConfiguration customConfig() {
+ return Main.getInstance().customConfig;
+ }
+
ArrayList disabledrecipe() {
return Main.getInstance().disabledrecipe;
}
@@ -63,14 +64,22 @@ Logger getLogger() {
return Main.getInstance().getLogger();
}
- FileConfiguration customConfig() {
- return Main.getInstance().customConfig;
+ CooldownManager getCooldownManager() {
+ return Main.getInstance().cooldownManager;
}
RecipeUtil getRecipeUtil() {
return Main.getInstance().recipeUtil;
}
+ ShapedChecks shapedChecks() {
+ return Main.getInstance().shapedChecks;
+ }
+
+ ShapelessChecks shapelessChecks() {
+ return Main.getInstance().shapelessChecks;
+ }
+
boolean isInt(String s) {
try {
Integer.parseInt(s);
@@ -103,7 +112,6 @@ void sendNoPermsMessage(Player p, String recipe) {
}
public boolean matchedRecipe(CraftingInventory inv, UUID id) {
-// if (result == null || result == new ItemStack(Material.AIR)) {
if (Main.getInstance().serverVersionAtLeast(1, 13)) {
if (inv.isEmpty()) {
logDebug("[matchedRecipe] Could not find a recipe to match with!", "", id);
@@ -127,25 +135,44 @@ public boolean matchedRecipe(CraftingInventory inv, UUID id) {
boolean validateItem(ItemStack item, RecipeUtil.Ingredient ingredient, String recipeName, int slot, boolean debug,
boolean returnType) {
- ItemStack recipe = getRecipeUtil().getRecipeFromResult(item) != null
- ? getRecipeUtil().getRecipeFromResult(item).getResult()
- : null;
- ItemStack exactMatch = ingredient.hasIdentifier() ? getRecipeUtil().getResultFromKey(ingredient.getIdentifier())
- : recipe;
+ if (item == null)
+ return returnType;
+
+ ItemStack recipeResult = null;
+ Recipe foundRecipe = getRecipeUtil().getRecipeFromResult(item);
+ if (foundRecipe != null) {
+ recipeResult = foundRecipe.getResult();
+ }
- if (item == null || (ingredient.hasIdentifier() && exactMatch == null))
+ ItemStack exactMatch = null;
+ boolean matchesIdentifier = false;
+ boolean matchesTags = tagsMatch(ingredient, item);
+
+ // since getResultFromKey is so resource heavy, try to check tags first for
+ // custom items.
+ if (!matchesTags) {
+ exactMatch = ingredient.hasIdentifier() ? getRecipeUtil().getResultFromKey(ingredient.getIdentifier())
+ : recipeResult;
+ matchesIdentifier = ingredient.hasIdentifier() && item.isSimilar(exactMatch);
+ }
+
+ if (ingredient.hasIdentifier() && !matchesTags && exactMatch == null)
return returnType;
- // Attempt to match the ingredient to a recipe using the identifier
- // Attempt to match the ingredient to an itemsadder item
- // If all else fail, match the itemstack to the regular ingredient requirements
- if ((ingredient.hasIdentifier() && item.isSimilar(exactMatch)) || (tagsMatch(ingredient, exactMatch))
- || (item.getType() == ingredient.getMaterial() && hasMatchingDisplayName(recipeName, item,
- ingredient.getDisplayName(), ingredient.getIdentifier(), ingredient.hasIdentifier(), false))) {
+ boolean matchesMaterial = item.getType() == ingredient.getMaterial();
+ boolean matchesMaterialChoice = ingredient.hasMaterialChoices()
+ && ingredient.getMaterialChoices().contains(item.getType());
+ boolean matchesSingleMaterialWithName = matchesMaterial && hasMatchingDisplayName(recipeName, item,
+ ingredient.getDisplayName(), ingredient.getIdentifier(), ingredient.hasIdentifier(), false);
+
+ if (matchesIdentifier || matchesTags || matchesMaterialChoice || matchesSingleMaterialWithName) {
if (debug)
logDebug("[amountsMatch] Checking slot " + slot + " for required amounts.. ", recipeName);
+ if (ingredient.isEmpty())
+ return true;
+
if (item.getAmount() < ingredient.getAmount()) {
if (debug) {
@@ -161,45 +188,66 @@ boolean validateItem(ItemStack item, RecipeUtil.Ingredient ingredient, String re
return true;
}
+
return returnType;
}
- boolean amountsMatch(CraftingInventory inv, String recipeName, List recipeIngredients,
- boolean debug, UUID id) {
+ boolean amountsMatch(AlignedResult alignedGrid, Inventory inv, String recipeName,
+ List recipeIngredients, boolean debug, UUID id) {
logDebug("[amountsMatch] Checking recipe amounts..", recipeName, id);
+
+ boolean isCraftingInventory = inv.getType() == InventoryType.WORKBENCH
+ || inv.getType() == InventoryType.CRAFTING;
+ RecipeType type = getRecipeUtil().getRecipe(recipeName).getType();
+ ItemStack[] matrix = inv.getContents();
+
+ boolean[] usedSlots = new boolean[matrix.length];
for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- if (ingredient.isEmpty())
- continue;
- int slot = ingredient.getSlot();
- if (getRecipeUtil().getRecipe(recipeName).getType() == RecipeType.SHAPED) {
- ItemStack invSlot = inv.getMatrix()[slot - 1];
+ if (type == RecipeType.SHAPED) {
+
+ if (alignedGrid == null)
+ return false;
+
+ int slot = ingredient.getSlot();
+ ItemStack invSlot = alignedGrid.getMatrix[slot - 1];
+
+ if (ingredient.isEmpty()) {
+ continue;
+ }
if (!validateItem(invSlot, ingredient, recipeName, slot, debug, false))
return false;
- }
- if (getRecipeUtil().getRecipe(recipeName).getType() == RecipeType.SHAPELESS) {
- slot = 0;
- for (ItemStack item : inv.getMatrix()) {
+ continue;
+
+ } else {
+ if (ingredient.isEmpty())
+ continue;
+ int slot = isCraftingInventory ? 0 : -1;
+
+ for (ItemStack item : matrix) {
+ if (isCraftingInventory && slot == 0) {
+ slot++;
+ continue;
+ }
+
+ if (usedSlots[slot])
+ continue;
+ if (item == null || item.getType() == Material.AIR)
+ continue;
+ if (validateItem(item, ingredient, recipeName, slot, debug, false)) {
+ usedSlots[slot] = true;
+ break;
+ }
slot++;
- if (!validateItem(item, ingredient, recipeName, slot, debug, true))
- return false;
}
}
}
return true;
}
- boolean hasCooldown(Player p, Recipe recipe) {
- if (recipe.hasCooldown())
- if (!(Main.getInstance().cooldownManager.cooldownExpired(p.getUniqueId(), recipe.getKey())))
- return true;
-
- return false;
- }
-
@SuppressWarnings("deprecation")
boolean isBlacklisted(ItemStack result, Player p) {
if (customConfig().getBoolean("blacklist-recipes")) {
@@ -288,92 +336,6 @@ public boolean hasMatchingDisplayName(String recipeName, ItemStack item, String
}
}
- // Checks if inventory slots and recipe materials match
- public boolean hasShapedIngredients(Inventory inv, String recipeName,
- List recipeIngredients) {
- ArrayList invMaterials = new ArrayList<>();
-
- boolean isCraftingInventory = inv.getType() == InventoryType.WORKBENCH
- || inv.getType() == InventoryType.CRAFTING;
- int slot = 0;
- for (ItemStack contents : inv.getContents()) {
- if (isCraftingInventory && slot == 0) {
- slot++;
- continue;
- }
- if (invMaterials.size() == 9)
- break;
-
- if (contents == null || contents.getType() == Material.AIR) {
- invMaterials.add("null");
- continue;
- }
-
- invMaterials.add(contents.getType().toString());
- }
-
- slot = 0;
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
-
- if (ingredient.isEmpty() && !invMaterials.get(slot).equals("null")) {
- logDebug("[hasShapedIngredients] Required ingredient not found in slot " + slot, recipeName);
- return false;
- }
-
- if (!ingredient.isEmpty()) {
- if (ingredient.hasIdentifier()) {
- String ingMat = getRecipeUtil().getResultFromKey(ingredient.getIdentifier()).getType().toString();
- if (!invMaterials.get(slot).equals(ingMat)) {
- logDebug("[hasShapedIngredients] Recipe ingredient requirement for slot " + slot
- + " does not match the ID ingredient", recipeName);
- logDebug("[hasShapedIngredients] Required ingredient: " + ingMat + " - Found: "
- + invMaterials.get(slot), recipeName);
- return false;
- }
- slot++;
- continue;
- }
-
- if (!invMaterials.get(slot).equals(ingredient.getMaterial().toString())) {
- logDebug("[hasShapedIngredients] Recipe ingredient requirement for slot " + slot
- + " does not match the ingredient", recipeName);
- logDebug("[hasShapedIngredients] Required ingredient: " + ingredient.getMaterial().toString()
- + " - Found: " + invMaterials.get(slot), recipeName);
- return false;
- }
- }
-
- slot++;
- }
-
- if (invMaterials.size() != 9) {
- logDebug(
- "[hasShapedIngredients] An internal error has occurred.. Please contact mehboss on spigot! Found size "
- + invMaterials.size() + "but should be 9",
- recipeName);
-
- return false;
- }
- return true;
- }
-
- // Method for specifically shapeless recipes only (checks all inv materials)
- public ArrayList getIngredients(String recipeName,
- List allIngredients) {
- ArrayList newIngredients = new ArrayList<>();
-
- for (RecipeUtil.Ingredient ingredient : allIngredients) {
- // Ignore the slot field and only save material, amount, and slot
- if (!ingredient.isEmpty() && ingredient.getMaterial() != null) {
- if (!newIngredients.contains(ingredient)) {
- newIngredients.add(ingredient);
- }
- }
- }
-
- return newIngredients;
- }
-
// Checks all inv materials for recipe (shaped or shapeless)
public boolean hasAllIngredients(Inventory inv, String recipeName, List recipeIngredients,
UUID id) {
@@ -383,6 +345,12 @@ public boolean hasAllIngredients(Inventory inv, String recipeName, List leftOvers = new ArrayList<>();
for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
if (!ingredient.isEmpty()) {
@@ -408,8 +377,20 @@ public boolean hasAllIngredients(Inventory inv, String recipeName, List types = Arrays.asList("WORKBENCH", "CRAFTER");
+ if (types.contains(inv.getType().toString())) {
- return false;
+ if (invMaterials.size() != 9 || ingMaterials.size() != 9) {
+ logDebug("[hasAllIngredients] Ingredients size is " + ingMaterials.size() + ", Inventory size is "
+ + invMaterials.size(), recipeName, id);
+ return false;
+ }
+
+ Map invCount = new HashMap<>();
+ Map ingCount = new HashMap<>();
+
+ for (String s : invMaterials)
+ invCount.put(s, invCount.getOrDefault(s, 0) + 1);
+
+ for (String s : ingMaterials)
+ ingCount.put(s, ingCount.getOrDefault(s, 0) + 1);
+
+ for (Map.Entry e : ingCount.entrySet()) {
+ if (invCount.getOrDefault(e.getKey(), 0) < e.getValue()) {
+ logDebug("[hasAllIngredients] Missing required: " + e.getKey(), recipeName, id);
+ return false;
+ }
+ }
}
- // handles 4x4 slot
- if (inv.getType() == InventoryType.CRAFTING
- && (!invMaterials.containsAll(ingMaterials) || !ingMaterials.containsAll(invMaterials))) {
- logDebug("[hasAllIngredients] Recipe ingredient requirements not met..", recipeName, id);
- return false;
+ // handles 2x2 slot
+ if (inv.getType() == InventoryType.CRAFTING) {
+ Map invCount = new HashMap<>();
+ Map ingCount = new HashMap<>();
+
+ for (String s : invMaterials)
+ invCount.put(s, invCount.getOrDefault(s, 0) + 1);
+
+ for (String s : ingMaterials)
+ ingCount.put(s, ingCount.getOrDefault(s, 0) + 1);
+
+ for (Map.Entry e : ingCount.entrySet()) {
+ if (invCount.getOrDefault(e.getKey(), 0) < e.getValue()) {
+ logDebug("[hasAllIngredients] Recipe ingredient requirements not met..", recipeName, id);
+ return false;
+ }
+ }
}
return true;
@@ -463,398 +475,29 @@ public boolean hasVanillaIngredients(Inventory inv, ItemStack result) {
if (item == null || item.getType() == Material.AIR)
continue;
- if (item.hasItemMeta() && (item.getItemMeta().hasDisplayName() || item.getItemMeta().hasLore()))
+ if (item.hasItemMeta()
+ && (CompatibilityUtil.hasDisplayname(item.getItemMeta()) || item.getItemMeta().hasLore()))
return false;
}
+ logDebug("[hasVanillaIngredients] Skipping checks.. vanilla recipe detected.", "");
return true;
}
boolean tagsMatch(RecipeUtil.Ingredient ingredient, ItemStack item) {
- String invID = getRecipeUtil().getKeyFromResult(item);
+ String key = getRecipeUtil().getKeyFromResult(item);
+ String invID = key != null ? key.toLowerCase() : "none";
String ingID = ingredient.hasIdentifier() ? ingredient.getIdentifier().toLowerCase() : "none";
- logDebug("[tagsMatch] invID === " + invID, "");
- logDebug("[tagsMatch] ingID === " + ingID, "");
- if (invID == null && ingID.equals("none"))
+ logDebug("[tagsMatch] Inventory Tag: " + invID, "");
+ logDebug("[tagsMatch] Ingredient Tag: " + ingID, "");
+ if (invID.equals("none") && ingID.equals("none"))
return true;
- if (invID != null && invID.toLowerCase().equals(ingID))
+ if (!invID.equals("none") && invID.equals(ingID))
return true;
return false;
}
-// Will be removed in a later version, after testing.
-//
-//
-// boolean checkCustomItems(ItemStack exactMatch, ItemStack item, String identifier, Boolean logDebug) {
-// ItemStack ingredientItem = exactMatch != null ? exactMatch.clone() : null;
-// ItemMeta ingredientMeta = ingredientItem != null ? ingredientItem.getItemMeta() : null;
-// ItemStack inventoryItem = item.clone();
-// ItemMeta inventoryMeta = item.getItemMeta();
-//
-// String customItem = getRecipeUtil().getCustomItemPlugin(identifier);
-// if (customItem != null
-// && (customItem.toLowerCase().equals("mythicmobs") || customItem.toLowerCase().equals("itemsadder"))) {
-// for (Attribute am : Attribute.values()) {
-// inventoryMeta.removeAttributeModifier(am);
-// if (ingredientItem != null)
-// ingredientMeta.removeAttributeModifier(am);
-// }
-//
-// ingredientItem.setItemMeta(ingredientMeta);
-// inventoryItem.setItemMeta(inventoryMeta);
-// }
-//
-// if (ingredientItem == null || inventoryItem == null)
-// return false;
-// if (ingredientItem.isSimilar(inventoryItem))
-// return true;
-//
-// if (customItem != null) {
-// if (customItem.toLowerCase().equals("itemsadder")) {
-// if (!Main.getInstance().hasCustomPlugin("itemsadder"))
-// return false;
-//
-// CustomStack stack = CustomStack.byItemStack(inventoryItem);
-// if (stack == null) {
-// logDebug("[checkCustomItems] ItemsAdder stack came back null, by error.", "", null);
-// logDebug("Item in question: " + identifier, "");
-// return false;
-// }
-//
-// String iaID = identifier.split(":")[1].toLowerCase();
-// String invID = stack.getNamespacedID().split(":")[1].toLowerCase();
-//
-// if (invID.equals(iaID))
-// return true;
-// }
-//
-// if (customItem.toLowerCase().equals("executableitems")) {
-// if (Main.getInstance().hasCustomPlugin("executableitems"))
-// return false;
-//
-// Optional ei = ExecutableItemsAPI.getExecutableItemsManager()
-// .getExecutableItem(inventoryItem);
-// if (!ei.isPresent()) {
-// logDebug("[checkCustomItems] EI stack came back null, by error.", "");
-// logDebug("Item in question: " + identifier, "");
-// return false;
-// }
-//
-// String iaID = identifier.split(":")[1].toLowerCase();
-// String invID = ei.get().getId().toLowerCase();
-//
-// if (invID.equals(iaID))
-// return true;
-// }
-// }
-//
-// return false;
-// }
-
- boolean handleShapelessRecipe(CraftingInventory inv, Recipe recipe, List recipeIngredients,
- UUID id) {
- logDebug("[handleShapeless] Handling shapeless checks..!", recipe.getName(), id);
-
- ArrayList slotNames = new ArrayList<>();
- ArrayList recipeNames = new ArrayList<>();
-
- ArrayList recipeMD = new ArrayList<>();
- ArrayList inventoryMD = new ArrayList<>();
-
- ArrayList slotIDs = new ArrayList<>();
- ArrayList recipeIDs = new ArrayList<>();
-
- Map recipeCount = new HashMap<>();
- Map inventoryCount = new HashMap<>();
-
- // Add 4 empties to the 4x4 slot to makeup for the missing slots
- if (inv.getType() == InventoryType.CRAFTING) {
- inventoryCount.put(Material.AIR, 5);
- Collections.addAll(slotNames, "null", "null", "null", "null", "null");
- Collections.addAll(slotIDs, "null", "null", "null", "null", "null");
- Collections.addAll(inventoryMD, -1, -1, -1, -1, -1);
-
- }
-
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- if (ingredient.isEmpty()) {
- recipeCount.put(Material.AIR, recipeCount.getOrDefault(Material.AIR, 0) + 1);
- recipeNames.add("null");
- continue;
- }
-
- if (ingredient.hasIdentifier() && getRecipeUtil().getResultFromKey(ingredient.getIdentifier()) != null) {
- ItemStack exactMatch = getRecipeUtil().getResultFromKey(ingredient.getIdentifier());
- recipeCount.put(exactMatch.getType(), recipeCount.getOrDefault(exactMatch.getType(), 0) + 1);
- recipeIDs.add(ingredient.getIdentifier());
-
- // recipe model data
- if (Main.getInstance().serverVersionAtLeast(1, 14)) {
- if (exactMatch.hasItemMeta() && exactMatch.getItemMeta().hasCustomModelData()) {
- recipeMD.add(exactMatch.getItemMeta().getCustomModelData());
- } else {
- recipeMD.add(-1);
- }
- }
-
- // recipe names
- if (exactMatch.getItemMeta().hasDisplayName()) {
- recipeNames.add(exactMatch.getItemMeta().getDisplayName());
- } else {
- recipeNames.add("false");
- }
- continue;
- }
-
- recipeCount.put(ingredient.getMaterial(), recipeCount.getOrDefault(ingredient.getMaterial(), 0) + 1);
- recipeIDs.add("null");
-
- if (Main.getInstance().serverVersionAtLeast(1, 14))
- recipeMD.add(ingredient.getCustomModelData());
-
- if (ingredient.hasDisplayName()) {
- recipeNames.add(ingredient.getDisplayName());
- } else {
- recipeNames.add("false");
- }
- }
-
- ItemStack[] matrix = inv.getMatrix(); // len 4 or 9
-
- for (int i = 0; i < matrix.length; i++) {
- ItemStack it = matrix[i];
-
- if (it == null || it.getType() == Material.AIR || it.getAmount() <= 0) {
- slotNames.add("null");
- inventoryCount.put(Material.AIR, inventoryCount.getOrDefault(Material.AIR, 0) + 1);
- continue;
- }
-
- // check ids
- String key = getRecipeUtil().getKeyFromResult(it);
- if (key == null)
- key = "null";
- slotIDs.add(key);
-
- // inv model data
- if (Main.getInstance().serverVersionAtLeast(1, 14)) {
- if (it.hasItemMeta() && it.getItemMeta().hasCustomModelData()) {
- inventoryMD.add(it.getItemMeta().getCustomModelData());
- } else {
- inventoryMD.add(-1);
- }
- }
-
- inventoryCount.put(it.getType(), inventoryCount.getOrDefault(it.getType(), 0) + 1);
-
- if (!it.hasItemMeta() || !it.getItemMeta().hasDisplayName()) {
- slotNames.add("false");
- continue;
- }
-
- slotNames.add(it.getItemMeta().getDisplayName());
- }
-
- boolean hasIngredients = true;
- for (Map.Entry entry : recipeCount.entrySet()) {
- Material material = entry.getKey();
- int requiredCount = entry.getValue();
- int invCount = inventoryCount.getOrDefault(material, 0);
-
- if (invCount < requiredCount) {
- hasIngredients = false;
- break;
- }
- }
-
- if (!hasIngredients) {
- logDebug("[handleShapeless] Missing required ingredients! Skipping recipe..", recipe.getName(), id);
- return false;
- }
- logDebug("[handleShapeless] All required ingredients found..", recipe.getName(), id);
-
- // checks hardcoded nbt tags
- if (!recipeIDs.equals(slotIDs) || !slotIDs.equals(recipeIDs)) {
- logDebug("[handleShapeless] NBT tags mismatch: recipe vs inventory", recipe.getName(), id);
- return false;
- }
-
- // ignore data
- if (recipe.getIgnoreData())
- return true;
-
- // ignore model data
- if (!recipe.getIgnoreModelData()) {
- Map recipeModelCount = new HashMap<>();
- Map inventoryModelCount = new HashMap<>();
-
- for (int model : recipeMD) {
- recipeModelCount.put(model, recipeModelCount.getOrDefault(model, 0) + 1);
- }
- for (int model : inventoryMD) {
- inventoryModelCount.put(model, inventoryModelCount.getOrDefault(model, 0) + 1);
- }
-
- if (!recipeMD.containsAll(inventoryMD) || !inventoryMD.containsAll(recipeMD)) {
- logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName(), id);
- return false;
- }
-
- if (!recipeModelCount.equals(inventoryModelCount)) {
- logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName(), id);
- logDebug(" RM: " + recipeModelCount, recipe.getName(), id);
- logDebug(" IM: " + inventoryModelCount, recipe.getName(), id);
- return false;
- }
- }
-
- // ignore names
- if (!recipe.getIgnoreNames()) {
- Map recipeNameCount = new HashMap<>();
- Map slotNameCount = new HashMap<>();
-
- for (String name : recipeNames) {
- recipeNameCount.put(name, recipeNameCount.getOrDefault(name, 0) + 1);
- }
- for (String name : slotNames) {
- slotNameCount.put(name, slotNameCount.getOrDefault(name, 0) + 1);
- }
-
- if (!recipeNames.containsAll(slotNames) || !slotNames.containsAll(recipeNames)) {
- logDebug("[handleShapeless] Display name mismatch: recipe vs inventory", recipe.getName(), id);
- logDebug(slotNames.toString(), recipe.getName(), id);
- logDebug(recipeNames.toString(), recipe.getName(), id);
- return false;
- }
-
- if (!recipeNameCount.equals(slotNameCount)) {
- logDebug("[handleShapeless] Display name count mismatch: recipe vs inventory", recipe.getName(), id);
- logDebug(" RN: " + recipeNameCount, recipe.getName(), id);
- logDebug(" IN: " + slotNameCount, recipe.getName(), id);
- return false;
- }
- }
- return true;
- }
-
- boolean handleShapedRecipe(CraftingInventory inv, Recipe recipe, List recipeIngredients) {
- int i = 0;
-
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- i++;
-
- if (inv.getItem(i) != null && inv.getItem(i).getType() != Material.AIR) {
- ItemMeta meta = inv.getItem(i).getItemMeta();
-
- // checks hard coded nbt tags
- if (ingredient.hasIdentifier()) {
- String invID = getRecipeUtil().getKeyFromResult(inv.getItem(i));
-
- if (invID == null || !invID.equals(ingredient.getIdentifier())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] IDs do not match", recipe.getName());
- logDebug("[handleShaped] IngID = ", ingredient.getIdentifier());
- logDebug("[handleShaped] InvID = ", invID);
- return false;
- }
-
- logDebug("[handleShaped] Passed all checks for the ingredient in slot " + i, recipe.getName());
- continue;
-
- }
-
- logDebug("[handleShaped] Ingredient slot " + i + " does not have an identifier..", recipe.getName());
- if (recipe.getIgnoreData()) {
- logDebug("[handleShaped] Skipping data checks.. Ignore-Data is enabled..", recipe.getName());
- continue;
- }
-
- // only if ignore names is false
- if (!recipe.getIgnoreNames()) {
- // checks if displayname is null
- if ((!meta.hasDisplayName() && ingredient.hasDisplayName())
- || (meta.hasDisplayName() && !ingredient.hasDisplayName())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug(
- "[handleShaped] The recipe ingredient displayname and the inventory slot displayname do not match",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug(
- "[handleShaped] Does the ingredient have a displayname? " + ingredient.hasDisplayName(),
- recipe.getName());
- logDebug("[handleShaped] Does the inventory have a displayname? " + meta.hasDisplayName(),
- recipe.getName());
- return false;
- }
-
- if (ingredient.hasDisplayName() && meta.hasDisplayName()
- && !(ingredient.getDisplayName().equals(meta.getDisplayName()))) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The ingredient name for the recipe and inventory do not match",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] The ingredient displayname: " + ingredient.getDisplayName(),
- recipe.getName());
- logDebug("[handleShaped] The inventory displayname: " + meta.getDisplayName(),
- recipe.getName());
- return false;
- }
- logDebug("[handleShaped] Inventory and recipe ingredient displayname matched for slot " + i,
- recipe.getName());
- }
-
- if (!Main.getInstance().serverVersionAtLeast(1, 14)) {
- logDebug("[handleShaped] Skipping CMD checks.. Version is less than 1.14..", recipe.getName());
- continue;
- }
-
- // only if ignore model data is false
- if (!recipe.getIgnoreModelData()) {
- if ((!meta.hasCustomModelData() && ingredient.hasCustomModelData())
- || (meta.hasCustomModelData() && !ingredient.hasCustomModelData())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The recipe ingredient CMD and the inventory slot CMD do not match..",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] Does the ingredient have CMD? " + ingredient.hasCustomModelData(),
- recipe.getName());
- logDebug("[handleShaped] Does the inventory have CMD? " + meta.hasCustomModelData(),
- recipe.getName());
- return false;
- }
-
- if (ingredient.hasCustomModelData() && meta.hasCustomModelData()
- && (ingredient.getCustomModelData() != meta.getCustomModelData())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The ingredient CMD for the recipe and inventory do not match..",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] The ingredient CMD: " + ingredient.getCustomModelData(),
- recipe.getName());
- logDebug("[handleShaped] The inventory CMD: " + meta.getCustomModelData(), recipe.getName());
- return false;
- }
- logDebug("[handleShaped] Inventory and recipe ingredient CMD matched for slot " + i,
- recipe.getName());
- }
- }
- }
- return true;
- }
-
@EventHandler(priority = EventPriority.LOWEST)
void handleCrafting(PrepareItemCraftEvent e) {
@@ -889,33 +532,34 @@ void handleCrafting(PrepareItemCraftEvent e) {
if (hasVanillaIngredients(inv, inv.getResult()))
return;
- if (inv.getType() == InventoryType.CRAFTING) {
- if (!(inv.getRecipe() instanceof ShapelessRecipe)) {
- if (getRecipeUtil().getRecipeFromResult(inv.getResult()) != null)
- inv.setResult(new ItemStack(Material.AIR));
- return;
- }
- }
+// if (inv.getType() == InventoryType.CRAFTING) {
+// if (!(inv.getRecipe() instanceof ShapelessRecipe)) {
+// logDebug("[handleCrafting] Skipping 2x2 checks.. recipe isn't shapeless..", "");
+//
+// if (getRecipeUtil().getRecipeFromResult(inv.getResult()) != null)
+// inv.setResult(new ItemStack(Material.AIR));
+// return;
+// }
+// }
logDebug("[handleCrafting] Fired craft event, beginning checks..", "", p.getUniqueId());
handleCraftingChecks(inv, p);
}
public void handleCraftingChecks(CraftingInventory inv, Player p) {
- Recipe finalRecipe = null;
+ CraftingRecipeData recipe = null;
Boolean passedCheck = true;
Boolean found = false;
UUID id = p.getUniqueId();
+ AlignedResult alignedGrid = null;
- for (Recipe recipe : getRecipeUtil().getAllRecipesSortedByResult(inv.getResult())) {
- finalRecipe = recipe;
-
- List recipeIngredients = recipe.getIngredients();
-
- if (recipe.getType() != RecipeType.SHAPELESS && recipe.getType() != RecipeType.SHAPED)
+ for (Recipe data : getRecipeUtil().getAllRecipesSortedByResult(inv.getResult())) {
+ if (data.getType() != RecipeType.SHAPELESS && data.getType() != RecipeType.SHAPED)
continue;
+ recipe = (CraftingRecipeData) data;
+ List recipeIngredients = recipe.getIngredients();
if (!hasAllIngredients(inv, recipe.getName(), recipeIngredients, id)) {
logDebug("[handleCrafting] Skipping to the next recipe! Ingredients did not match..", recipe.getName(),
id);
@@ -924,79 +568,90 @@ public void handleCraftingChecks(CraftingInventory inv, Player p) {
}
logDebug("[handleCrafting] Inventory contained all of the ingredients. Continuing checks.. RecipeType: "
- + recipe.getType(), recipe.getName());
+ + recipe.getType(), recipe.getName(), id);
passedCheck = true;
found = true;
+ // -------------------------
+ // SHAPELESS
+ // -------------------------
if (recipe.getType() == RecipeType.SHAPELESS) {
- if (!handleShapelessRecipe(inv, recipe, recipeIngredients, id)) {
+
+ if (!shapelessChecks().handleShapelessRecipe(inv, recipe, recipeIngredients, id)) {
passedCheck = false;
continue;
}
- } else {
+ }
- if (!hasShapedIngredients(inv, recipe.getName(), recipeIngredients)) {
+ // -------------------------
+ // SHAPED
+ // -------------------------
+ else {
+
+ alignedGrid = shapedChecks().getAlignedGrid(inv, recipeIngredients);
+ if (alignedGrid == null) {
logDebug("[handleCrafting] Skipping to the next recipe! Ingredients did not have exact match..",
- recipe.getName());
+ recipe.getName(), id);
passedCheck = false;
continue;
}
- logDebug("[handleCrafting] Ingredients matched. Continuing checks..", recipe.getName());
-
- if (recipe.getIgnoreData() == true)
- continue;
-
- if (!handleShapedRecipe(inv, recipe, recipeIngredients)) {
- passedCheck = false;
- continue;
+ logDebug("[handleCrafting] Ingredients matched. Continuing checks..", recipe.getName(), id);
- }
+ // strict checks (NBT / CMD / name)
+ if (!recipe.getIgnoreData())
+ if (!shapedChecks().handleShapedRecipe(inv.getType(), alignedGrid, recipe, recipeIngredients)) {
+ passedCheck = false;
+ continue;
+ }
}
- if (!(amountsMatch(inv, recipe.getName(), recipeIngredients, true, id))) {
+ // -------------------------
+ // AMOUNT CHECK
+ // -------------------------
+ if (!amountsMatch(alignedGrid, inv, recipe.getName(), recipeIngredients, true, id)) {
logDebug(
"[handleCrafting] Skipping recipe.. The amount check indicates that the requirements have not been met..",
- recipe.getName());
+ recipe.getName(), id);
passedCheck = false;
continue;
}
- if (passedCheck && found) {
+ if (passedCheck && found)
break;
-
- }
}
if (!found)
return;
logDebug("[handleCrafting] Final crafting results: (passedChecks: " + passedCheck + ")(foundRecipe: " + found
- + ")", finalRecipe.getName(), id);
+ + ")", recipe.getName(), id);
if ((!passedCheck) || (passedCheck && !found)) {
inv.setResult(new ItemStack(Material.AIR));
return;
}
- // checks if recipe is active
- if (!finalRecipe.isActive()) {
+ // -------------------------
+ // DISABLED CHECKS
+ // -------------------------
+ if (!recipe.isActive()) {
inv.setResult(new ItemStack(Material.AIR));
- logDebug(" Attempt to craft disabled recipe detected..", finalRecipe.getName());
+ logDebug(" Attempt to craft disabled recipe detected..", recipe.getName(), id);
return;
}
- // checks for required perms
- if (finalRecipe.hasPerm() && !p.hasPermission(finalRecipe.getPerm())) {
+ // PERMISSIONS
+ if (recipe.hasPerm() && !p.hasPermission(recipe.getPerm())) {
inv.setResult(new ItemStack(Material.AIR));
- sendNoPermsMessage(p, finalRecipe.getName());
+ sendNoPermsMessage(p, recipe.getName());
return;
}
- // checks for disabled world
- if (!(finalRecipe.getDisabledWorlds().isEmpty())) {
- for (String string : finalRecipe.getDisabledWorlds()) {
+ // WORLD DISABLED
+ if (!(recipe.getDisabledWorlds().isEmpty())) {
+ for (String string : recipe.getDisabledWorlds()) {
if (p.getWorld().getName().equalsIgnoreCase(string)) {
inv.setResult(new ItemStack(Material.AIR));
sendMessages(p, "none", 0);
@@ -1005,21 +660,22 @@ public void handleCraftingChecks(CraftingInventory inv, Player p) {
}
}
- // checks for cool down
- if ((finalRecipe.hasPerm() && !(p.hasPermission(finalRecipe.getPerm() + ".bypass")))
- && hasCooldown(p, finalRecipe)) {
- Long timeLeft = Main.getInstance().cooldownManager.getTimeLeft(p.getUniqueId(), finalRecipe.getKey());
+ // COOLDOWN
+ if ((recipe.hasPerm() && !(p.hasPermission(recipe.getPerm() + ".bypass")))
+ && getCooldownManager().hasCooldown(p.getUniqueId(), recipe.getKey())) {
+ Long timeLeft = Main.getInstance().cooldownManager.getTimeLeft(p.getUniqueId(), recipe.getKey());
sendMessages(p, "crafting-limit", timeLeft);
inv.setResult(new ItemStack(Material.AIR));
return;
}
- // checks recipe conditions
- ConditionSet cs = finalRecipe.getConditionSet();
+ // CONDITIONS
+ ConditionSet cs = recipe.getConditionSet();
if (cs != null && !cs.isEmpty()) {
List reasons = new ArrayList();
if (!cs.test(p.getLocation(), p, reasons)) {
+
if (!reasons.isEmpty()) {
p.sendMessage(ChatColor.RED + "You cannot craft this recipe until conditions are met: "
+ ChatColor.GRAY + String.join(", ", reasons));
@@ -1027,20 +683,25 @@ && hasCooldown(p, finalRecipe)) {
Boolean closeInventory = Main.getInstance().customConfig.getBoolean("conditions-failed.close-inventory",
true);
+
if (closeInventory)
p.closeInventory();
- logDebug(" Preventing craft due to failing required recipe conditions!", finalRecipe.getName());
+ logDebug(" Preventing craft due to failing required recipe conditions!", recipe.getName(), id);
inv.setResult(new ItemStack(Material.AIR));
return;
}
}
+ // -------------------------
+ // SUCCESSFUL CRAFT
+ // -------------------------
if (passedCheck && found) {
- ItemStack item = new ItemStack(finalRecipe.getResult());
+ ItemStack item = new ItemStack(recipe.getResult());
+
+ // PlaceholderAPI support
+ if (!recipe.isCustomItem()) {
- // only parse placeholders for items that are NOT IA, MM, ETC.
- if (!finalRecipe.isCustomItem()) {
List withPlaceholders = null;
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null)
@@ -1048,9 +709,8 @@ && hasCooldown(p, finalRecipe)) {
? PlaceholderAPI.setPlaceholders(p, item.getItemMeta().getLore())
: null;
- ItemMeta itemMeta = item.getItemMeta();
-
if (withPlaceholders != null) {
+ ItemMeta itemMeta = item.getItemMeta();
itemMeta.setLore(withPlaceholders);
item.setItemMeta(itemMeta);
}
diff --git a/src/me/mehboss/crafting/CrafterManager.java b/src/me/mehboss/crafting/CrafterManager.java
index 9a52605..48f5d32 100644
--- a/src/me/mehboss/crafting/CrafterManager.java
+++ b/src/me/mehboss/crafting/CrafterManager.java
@@ -1,54 +1,60 @@
package me.mehboss.crafting;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
+import org.bukkit.Location;
import org.bukkit.Material;
-import org.bukkit.block.Container;
+import org.bukkit.block.Block;
+import org.bukkit.block.Crafter;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.CrafterCraftEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
import com.cryptomorin.xseries.XMaterial;
+
+import me.mehboss.crafting.ShapedChecks.AlignedResult;
import me.mehboss.recipe.Main;
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeConditions.ConditionSet;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.CraftingRecipeData;
public class CrafterManager implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void onCollectResult(CrafterCraftEvent e) {
- Container block = (Container) e.getBlock().getState();
- Inventory inv = block.getInventory();
- e.setResult(handleCraftingChecks(e, inv, e.getResult()));
+ Block block = e.getBlock();
+
+ if (!(block.getState() instanceof Crafter))
+ return;
+
+ Crafter crafter = (Crafter) block.getState();
+ Inventory inv = crafter.getInventory();
+ e.setResult(handleCraftingChecks(crafter, inv, e.getResult()));
}
- ItemStack handleCraftingChecks(CrafterCraftEvent e, Inventory inv, ItemStack result) {
- Recipe finalRecipe = null;
+ ItemStack handleCraftingChecks(Crafter crafter, Inventory inv, ItemStack result) {
+ CraftingRecipeData recipe = null;
Boolean passedCheck = true;
Boolean found = false;
+ AlignedResult grid = null;
if (CraftManager().hasVanillaIngredients(inv, result))
return result;
- for (Recipe recipe : getRecipeUtil().getAllRecipesSortedByResult(result)) {
-
- finalRecipe = recipe;
-
- List recipeIngredients = recipe.getIngredients();
+ for (Recipe data : getRecipeUtil().getAllRecipesSortedByResult(result)) {
- if (recipe.getType() != RecipeType.SHAPELESS && recipe.getType() != RecipeType.SHAPED)
+ if (data.getType() != RecipeType.SHAPELESS && data.getType() != RecipeType.SHAPED)
continue;
+ recipe = (CraftingRecipeData) data;
+ List recipeIngredients = recipe.getIngredients();
if (!CraftManager().hasAllIngredients(inv, recipe.getName(), recipeIngredients, null)) {
logDebug("[handleCrafting] Skipping to the next recipe! Ingredients did not match..", recipe.getName());
passedCheck = false;
@@ -61,14 +67,24 @@ ItemStack handleCraftingChecks(CrafterCraftEvent e, Inventory inv, ItemStack res
passedCheck = true;
found = true;
+ // -------------------------
+ // SHAPELESS RECIPE CHECKS
+ // -------------------------
if (recipe.getType() == RecipeType.SHAPELESS) {
- if (!handleShapelessRecipe(inv, result, recipe, recipeIngredients)) {
+
+ if (!shapelessChecks().handleShapelessRecipe(inv, recipe, recipeIngredients, null)) {
passedCheck = false;
continue;
}
- } else {
- if (!CraftManager().hasShapedIngredients(inv, recipe.getName(), recipeIngredients)) {
+ }
+ // ----------------------
+ // SHAPED RECIPE CHECKS
+ // ----------------------
+ else {
+
+ grid = shapedChecks().getAlignedGrid(inv, recipeIngredients);
+ if (grid == null) {
logDebug("[handleCrafting] Skipping to the next recipe! Ingredients did not have exact match..",
recipe.getName());
passedCheck = false;
@@ -77,17 +93,18 @@ ItemStack handleCraftingChecks(CrafterCraftEvent e, Inventory inv, ItemStack res
logDebug("[handleCrafting] Ingredients matched. Continuing checks..", recipe.getName());
- if (recipe.getIgnoreData() == true)
- continue;
-
- if (!handleShapedRecipe(inv, recipe, recipeIngredients)) {
- passedCheck = false;
- continue;
-
- }
+ // NEW: run strict NBT-name-CMD checks on aligned grid
+ if (!recipe.getIgnoreData())
+ if (!shapedChecks().handleShapedRecipe(inv.getType(), grid, recipe, recipeIngredients)) {
+ passedCheck = false;
+ continue;
+ }
}
- if (!(amountsMatch(inv, recipe.getName(), recipeIngredients, true))) {
+ // -----------------------
+ // AMOUNT CHECK
+ // -----------------------
+ if (!(CraftManager().amountsMatch(grid, inv, recipe.getName(), recipeIngredients, true, null))) {
logDebug(
"[handleCrafting] Skipping recipe.. The amount check indicates that the requirements have not been met..",
recipe.getName());
@@ -97,12 +114,11 @@ ItemStack handleCraftingChecks(CrafterCraftEvent e, Inventory inv, ItemStack res
if (passedCheck && found) {
break;
-
}
}
logDebug("[handleCrafting] Final crafting results: (passedChecks: " + passedCheck + ")(foundRecipe: " + found
- + ")", finalRecipe.getName());
+ + ")", recipe.getName());
if (!found)
return result;
@@ -111,455 +127,146 @@ ItemStack handleCraftingChecks(CrafterCraftEvent e, Inventory inv, ItemStack res
return new ItemStack(Material.AIR);
}
- if (!finalRecipe.isActive() || finalRecipe.getDisabledWorlds().contains(e.getBlock().getWorld().getName())
- || Main.getInstance().disabledrecipe.contains(finalRecipe.getKey())) {
- logDebug(" Attempt to craft recipe was detected, but recipe is disabled!", finalRecipe.getName());
+ Location location = crafter.getBlock().getLocation();
+ if (!recipe.isActive() || recipe.getDisabledWorlds().contains(location.getWorld().getName())
+ || Main.getInstance().disabledrecipe.contains(recipe.getKey()) || recipe.hasCommands()) {
+
+ logDebug(" Attempt to craft recipe was detected, but recipe is disabled!", recipe.getName());
return new ItemStack(Material.AIR);
}
- // checks recipe conditions
- ConditionSet cs = finalRecipe.getConditionSet();
+ // CONDITIONS
+ ConditionSet cs = recipe.getConditionSet();
if (cs != null && !cs.isEmpty()) {
- if (!cs.test(e.getBlock().getLocation(), null, null)) {
- logDebug("Preventing craft of due to failing required recipe conditions!", finalRecipe.getName());
+ if (!cs.test(location, null, null)) {
+ logDebug("Preventing craft due to failing required recipe conditions!", recipe.getName());
return new ItemStack(Material.AIR);
}
}
if (passedCheck && found) {
- ItemStack item = new ItemStack(finalRecipe.getResult());
- handleAmountDeductions(e, finalRecipe.getName(), inv);
+ ItemStack item = new ItemStack(recipe.getResult());
+ handleAmountDeductions(grid, crafter, recipe.getName(), inv);
return item;
}
return result;
}
- boolean amountsMatch(Inventory inv, String recipeName, List recipeIngredients,
- boolean debug) {
-
- logDebug("[amountsMatch] Checking recipe amounts..", recipeName);
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- if (ingredient.isEmpty())
- continue;
-
- int slot = ingredient.getSlot();
- if (getRecipeUtil().getRecipe(recipeName).getType() == RecipeType.SHAPED) {
- ItemStack invSlot = inv.getContents()[slot - 1];
-
- if (!CraftManager().validateItem(invSlot, ingredient, recipeName, slot, debug, false))
- return false;
- }
-
- if (getRecipeUtil().getRecipe(recipeName).getType() == RecipeType.SHAPELESS) {
- slot = 0;
- for (ItemStack item : inv.getContents()) {
- if (!CraftManager().validateItem(item, ingredient, recipeName, slot, debug, true))
- return false;
- slot++;
- }
- }
- }
- return true;
- }
-
- boolean handleShapelessRecipe(Inventory inv, ItemStack result, Recipe recipe,
- List recipeIngredients) {
- // runs checks if recipe is shapeless
-
- logDebug("[handleShapeless] Handling shapeless checks..!", recipe.getName());
- if (getRecipeUtil().getRecipeFromResult(result) != null)
- recipe = getRecipeUtil().getRecipeFromResult(result);
-
- ArrayList slotNames = new ArrayList<>();
- ArrayList recipeNames = new ArrayList<>();
-
- ArrayList recipeMD = new ArrayList<>();
- ArrayList inventoryMD = new ArrayList<>();
-
- ArrayList slotIDs = new ArrayList<>();
- ArrayList recipeIDs = new ArrayList<>();
-
- Map recipeCount = new HashMap<>();
- Map inventoryCount = new HashMap<>();
-
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- if (ingredient.isEmpty()) {
- recipeCount.put(Material.AIR, recipeCount.getOrDefault(Material.AIR, 0) + 1);
- recipeNames.add("null");
- continue;
- }
-
- if (ingredient.hasIdentifier() && getRecipeUtil().getResultFromKey(ingredient.getIdentifier()) != null) {
- ItemStack exactMatch = getRecipeUtil().getResultFromKey(ingredient.getIdentifier());
- recipeCount.put(exactMatch.getType(), recipeCount.getOrDefault(exactMatch.getType(), 0) + 1);
- recipeIDs.add(ingredient.getIdentifier());
-
- // recipe model data
- if (Main.getInstance().serverVersionAtLeast(1, 14)) {
- if (exactMatch.hasItemMeta() && exactMatch.getItemMeta().hasCustomModelData()) {
- recipeMD.add(exactMatch.getItemMeta().getCustomModelData());
- } else {
- recipeMD.add(-1);
- }
- }
-
- // recipe names
- if (exactMatch.getItemMeta().hasDisplayName()) {
- recipeNames.add(exactMatch.getItemMeta().getDisplayName());
- } else {
- recipeNames.add("false");
- }
- continue;
- }
-
- recipeCount.put(ingredient.getMaterial(), recipeCount.getOrDefault(ingredient.getMaterial(), 0) + 1);
- recipeIDs.add("null");
-
- if (Main.getInstance().serverVersionAtLeast(1, 14))
- recipeMD.add(ingredient.getCustomModelData());
-
- if (ingredient.hasDisplayName()) {
- recipeNames.add(ingredient.getDisplayName());
- } else {
- recipeNames.add("false");
- }
- }
-
- for (int i = 0; i < 9; i++) {
- ItemStack it = inv.getItem(i);
-
- if (it == null || it.getType() == Material.AIR || it.getAmount() <= 0) {
- slotNames.add("null");
- inventoryCount.put(Material.AIR, inventoryCount.getOrDefault(Material.AIR, 0) + 1);
- continue;
- }
-
- // check ids
- String key = getRecipeUtil().getKeyFromResult(it);
- if (key == null)
- key = "null";
- slotIDs.add(key);
-
- // inv model data
- if (Main.getInstance().serverVersionAtLeast(1, 14)) {
- if (it.hasItemMeta() && it.getItemMeta().hasCustomModelData()) {
- inventoryMD.add(it.getItemMeta().getCustomModelData());
- } else {
- inventoryMD.add(-1);
- }
- }
-
- inventoryCount.put(it.getType(), inventoryCount.getOrDefault(it.getType(), 0) + 1);
-
- if (!it.hasItemMeta() || !it.getItemMeta().hasDisplayName()) {
- slotNames.add("false");
- continue;
- }
-
- slotNames.add(it.getItemMeta().getDisplayName());
- }
-
- boolean hasIngredients = true;
- for (Map.Entry entry : recipeCount.entrySet()) {
- Material material = entry.getKey();
- int requiredCount = entry.getValue();
- int invCount = inventoryCount.getOrDefault(material, 0);
-
- if (invCount < requiredCount) {
- hasIngredients = false;
- break;
- }
- }
-
- if (!hasIngredients) {
- logDebug("[handleShapeless] Missing required ingredients! Skipping recipe..", recipe.getName());
- return false;
- }
-
- logDebug("[handleShapeless] All required ingredients found..", recipe.getName());
- // checks hardcoded nbt tags
- if (!recipeIDs.equals(slotIDs) || !slotIDs.equals(recipeIDs)) {
- logDebug("[handleShapeless] NBT tags mismatch: recipe vs inventory", recipe.getName());
- return false;
- }
-
- // ignore data
- if (recipe.getIgnoreData())
- return true;
-
- // ignore model data
- if (!recipe.getIgnoreModelData()) {
- Map recipeModelCount = new HashMap<>();
- Map inventoryModelCount = new HashMap<>();
-
- for (int model : recipeMD) {
- recipeModelCount.put(model, recipeModelCount.getOrDefault(model, 0) + 1);
- }
- for (int model : inventoryMD) {
- inventoryModelCount.put(model, inventoryModelCount.getOrDefault(model, 0) + 1);
- }
-
- if (!recipeMD.containsAll(inventoryMD) || !inventoryMD.containsAll(recipeMD)) {
- logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName());
- return false;
- }
-
- if (!recipeModelCount.equals(inventoryModelCount)) {
- logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName());
- logDebug(" Recipe Model Data Map: " + recipeModelCount, recipe.getName());
- logDebug(" Inventory Model Data Map: " + inventoryModelCount, recipe.getName());
- return false;
- }
- }
-
- // ignore names
- if (!recipe.getIgnoreNames()) {
- Map recipeNameCount = new HashMap<>();
- Map slotNameCount = new HashMap<>();
-
- for (String name : recipeNames) {
- recipeNameCount.put(name, recipeNameCount.getOrDefault(name, 0) + 1);
- }
- for (String name : slotNames) {
- slotNameCount.put(name, slotNameCount.getOrDefault(name, 0) + 1);
- }
-
- if (!recipeNames.containsAll(slotNames) || !slotNames.containsAll(recipeNames)) {
- logDebug("[handleShapeless] Display name mismatch: recipe vs inventory", recipe.getName());
- logDebug(slotNames.toString(), recipe.getName());
- logDebug(recipeNames.toString(), recipe.getName());
- return false;
- }
-
- if (!recipeNameCount.equals(slotNameCount)) {
- logDebug("[handleShapeless] Display name count mismatch: recipe vs inventory", recipe.getName());
- logDebug(" RN: " + recipeNameCount, recipe.getName());
- logDebug(" IN: " + slotNameCount, recipe.getName());
- return false;
- }
- }
- return true;
- }
-
- boolean handleShapedRecipe(Inventory inv, Recipe recipe, List recipeIngredients) {
- int i = -1;
+ void handleAmountDeductions(AlignedResult aligned, Crafter crafter, String recipeName, Inventory inv) {
+ CraftingRecipeData recipe = (CraftingRecipeData) getRecipeUtil().getRecipe(recipeName);
+ if (recipe == null)
+ return;
- for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
- i++;
+ ArrayList handledIngredients = new ArrayList<>();
+ boolean[] handledSlots = new boolean[9];
- if (inv.getItem(i) != null && inv.getItem(i).getType() != Material.AIR) {
- ItemMeta meta = inv.getItem(i).getItemMeta();
-
- // checks hard coded nbt tags
- if (ingredient.hasIdentifier()) {
- String invID = getRecipeUtil().getKeyFromResult(inv.getItem(i));
-
- if (invID == null || !invID.equals(ingredient.getIdentifier())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] IDs do not match", recipe.getName());
- logDebug("[handleShaped] IngID = ", ingredient.getIdentifier());
- logDebug("[handleShaped] InvID = ", invID);
- return false;
- }
+ final int itemsToAdd = 1;
+ int itemsToRemove = 0;
- logDebug("[handleShaped] Passed all checks for the ingredient in slot " + i, recipe.getName());
- continue;
+ RecipeType type = recipe.getType();
- }
+ if (type == RecipeType.SHAPED) {
+ if (aligned == null)
+ return;
- logDebug("[handleShaped] Ingredient slot " + i + " does not have an identifier..", recipe.getName());
- if (recipe.getIgnoreData()) {
- logDebug("[handleShaped] Skipping data checks.. Ignore-Data is enabled..", recipe.getName());
+ for (int i = 0; i < recipe.getIngredients().size(); i++) {
+ RecipeUtil.Ingredient ing = recipe.getIngredients().get(i);
+ if (ing.isEmpty())
continue;
- }
-
- // only if ignore names is false
- if (!recipe.getIgnoreNames()) {
- // checks if displayname is null
- if ((!meta.hasDisplayName() && ingredient.hasDisplayName())
- || (meta.hasDisplayName() && !ingredient.hasDisplayName())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug(
- "[handleShaped] The recipe ingredient displayname and the inventory slot displayname do not match",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug(
- "[handleShaped] Does the ingredient have a displayname? " + ingredient.hasDisplayName(),
- recipe.getName());
- logDebug("[handleShaped] Does the inventory have a displayname? " + meta.hasDisplayName(),
- recipe.getName());
- return false;
- }
- if (ingredient.hasDisplayName() && meta.hasDisplayName()
- && !(ingredient.getDisplayName().equals(meta.getDisplayName()))) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The ingredient name for the recipe and inventory do not match",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] The ingredient displayname: " + ingredient.getDisplayName(),
- recipe.getName());
- logDebug("[handleShaped] The inventory displayname: " + meta.getDisplayName(),
- recipe.getName());
- return false;
- }
- logDebug("[handleShaped] Inventory and recipe ingredient displayname matched for slot " + i,
- recipe.getName());
- }
-
- if (!Main.getInstance().serverVersionAtLeast(1, 14)) {
- logDebug("[handleShaped] Skipping CMD checks.. Version is less than 1.14..", recipe.getName());
+ int slot = aligned.invSlotMap[i];
+ ItemStack item = inv.getItem(slot);
+ if (item == null || item.getType() == Material.AIR)
continue;
- }
-
- // only if ignore model data is false
- if (!recipe.getIgnoreModelData()) {
- if ((!meta.hasCustomModelData() && ingredient.hasCustomModelData())
- || (meta.hasCustomModelData() && !ingredient.hasCustomModelData())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The recipe ingredient CMD and the inventory slot CMD do not match..",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] Does the ingredient have CMD? " + ingredient.hasCustomModelData(),
- recipe.getName());
- logDebug("[handleShaped] Does the inventory have CMD? " + meta.hasCustomModelData(),
- recipe.getName());
- return false;
- }
- if (ingredient.hasCustomModelData() && meta.hasCustomModelData()
- && (ingredient.getCustomModelData() != meta.getCustomModelData())) {
- logDebug("[handleShaped] Skipping recipe..", recipe.getName());
- logDebug("[handleShaped] The ingredient CMD for the recipe and inventory do not match..",
- recipe.getName());
- logDebug(
- "[handleShaped] The inventory slot in question: " + i
- + ". The ingredient slot in question: " + ingredient.getSlot(),
- recipe.getName());
- logDebug("[handleShaped] The ingredient CMD: " + ingredient.getCustomModelData(),
- recipe.getName());
- logDebug("[handleShaped] The inventory CMD: " + meta.getCustomModelData(), recipe.getName());
- return false;
- }
- logDebug("[handleShaped] Inventory and recipe ingredient CMD matched for slot " + i,
- recipe.getName());
- }
+ int requiredAmount = Math.max(1, ing.getAmount());
+ handlesItemRemoval(inv, recipe, item, ing, slot, itemsToRemove, itemsToAdd, requiredAmount);
}
- }
- return true;
- }
- void handleAmountDeductions(CrafterCraftEvent e, String recipeName, Inventory inv) {
- Recipe recipe = getRecipeUtil().getRecipe(recipeName);
- ArrayList handledIngredients = new ArrayList();
-
- int itemsToAdd = 1;
- int itemsToRemove = 0;
- for (RecipeUtil.Ingredient ingredient : recipe.getIngredients()) {
- if (ingredient.isEmpty())
- continue;
+ } else {
+ // Shapeless
+ for (RecipeUtil.Ingredient ing : recipe.getIngredients()) {
+ if (ing.isEmpty())
+ continue;
- final Material material = ingredient.getMaterial();
- final String displayName = ingredient.getDisplayName();
- final int requiredAmount = Math.max(1, ingredient.getAmount());
- final boolean hasIdentifier = ingredient.hasIdentifier();
-
- // === Your existing per-type logic ===
- if (recipe.getType() == RecipeType.SHAPELESS) {
- logDebug("[handleShiftClicks] Found shapeless recipe to handle..", recipeName);
+ final Material mat = ing.getMaterial();
+ final String name = ing.getDisplayName();
+ final boolean hasID = ing.hasIdentifier();
+ int requiredAmount = Math.max(1, ing.getAmount());
for (int i = 0; i < 9; i++) {
- ItemStack item = inv.getItem(i);
- int slot = i;
+ if (handledSlots[i])
+ continue;
- if (item == null || item.getType() == Material.AIR)
+ ItemStack item = inv.getItem(i);
+ if (item == null || item.getType() == Material.AIR || crafter.isSlotDisabled(i))
continue;
- if (!AmountManager().matchesIngredient(item, recipeName, ingredient, material, displayName,
- hasIdentifier))
+ if (!AmountManager().matchesIngredient(item, recipeName, ing, mat, name, hasID))
continue;
- // If your original removal didn’t require an explicit matchesIngredient()
- // here, leave it as-is. (You earlier said only Pass 1 needs matching.)
- if (!handledIngredients.contains(ingredient.getAbbreviation())) {
- handlesItemRemoval(inv, recipe, item, ingredient, slot, itemsToRemove, itemsToAdd,
- requiredAmount);
- }
+ handlesItemRemoval(inv, recipe, item, ing, i, itemsToRemove, itemsToAdd, requiredAmount);
+ handledSlots[i] = true;
}
- if (!handledIngredients.contains(ingredient.getAbbreviation()))
- handledIngredients.add(ingredient.getAbbreviation());
-
- } else {
- logDebug("[handleShiftClicks] Found other recipe type to handle..", recipeName);
- int slot = ingredient.getSlot() - 1;
- ItemStack item = inv.getItem(slot);
-
- if (item == null || item.getType() == Material.AIR)
- continue;
-
- // Same note: keep your original behavior.
- handlesItemRemoval(inv, recipe, item, ingredient, slot, itemsToRemove, itemsToAdd, requiredAmount);
+ handledIngredients.add(ing.getAbbreviation());
}
}
}
- // Helper method for the handleShiftClick method
- void handlesItemRemoval(Inventory inv, Recipe recipe, ItemStack item, RecipeUtil.Ingredient ingredient, int slot,
- int itemsToRemove, int itemsToAdd, int requiredAmount) {
+ void handlesItemRemoval(Inventory inv, CraftingRecipeData recipe, ItemStack item, RecipeUtil.Ingredient ingredient,
+ int slot, int itemsToRemove, int itemsToAdd, int requiredAmount) {
String recipeName = recipe.getName();
- logDebug("[handleShiftClicks] Checking slot " + slot + " for the recipe ", recipeName);
-
- if (AmountManager().matchesIngredient(item, recipeName, ingredient, ingredient.getMaterial(),
- ingredient.getDisplayName(), ingredient.hasIdentifier())) {
-
- itemsToRemove = (itemsToAdd * requiredAmount) - 1;
-
- int availableItems = item.getAmount();
-
- logDebug("[handleShiftClicks] Handling recipe " + recipeName, "");
- logDebug("[handleShiftClicks] ItemsToRemove: " + itemsToRemove + " - ItemsToAdd: " + itemsToAdd, "");
- logDebug("[handleShiftClicks] ItemAmount: " + availableItems, "");
- logDebug("[handleShiftClicks] RequiredAmount: " + requiredAmount, "");
- logDebug("[handleShiftClicks] Identifier: " + ingredient.getIdentifier() + " - HasIdentifier: "
- + ingredient.hasIdentifier(), "");
- logDebug("[handleShiftClicks] Material: " + ingredient.getMaterial().toString(), "");
- logDebug("[handleShiftClicks] Displayname: " + ingredient.getDisplayName(), "");
-
- if (availableItems < requiredAmount)
- return;
-
- String id = ingredient.hasIdentifier() ? ingredient.getIdentifier() : item.getType().toString();
- if (recipe.isLeftover(id)) {
- if (item.getType().toString().contains("_BUCKET"))
- item.setType(XMaterial.BUCKET.parseMaterial());
- return;
- }
- if ((item.getAmount() - itemsToRemove) <= 0) {
- inv.setItem(slot, null);
- return;
+ if (!AmountManager().matchesIngredient(item, recipeName, ingredient, ingredient.getMaterial(),
+ ingredient.getDisplayName(), ingredient.hasIdentifier()))
+ return;
+
+ itemsToRemove = (itemsToAdd * requiredAmount) - 1; // remove exact amount for the crafter
+
+ int availableItems = item.getAmount();
+ if (availableItems < requiredAmount)
+ return;
+
+ logDebug("[handleShiftClicks] Handling recipe " + recipeName, "");
+ logDebug("[handleShiftClicks] ItemsToRemove: " + (itemsToRemove + 1) + " - ItemsToAdd: " + itemsToAdd, "");
+ logDebug("[handleShiftClicks] ItemAmount: " + availableItems, "");
+ logDebug("[handleShiftClicks] RequiredAmount: " + requiredAmount, "");
+ logDebug("[handleShiftClicks] Identifier: " + ingredient.getIdentifier() + " - HasIdentifier: "
+ + ingredient.hasIdentifier(), "");
+ logDebug("[handleShiftClicks] Material: " + ingredient.getMaterial().toString(), "");
+ logDebug("[handleShiftClicks] Displayname: " + ingredient.getDisplayName(), "");
+
+ String id = ingredient.hasIdentifier() ? ingredient.getIdentifier() : item.getType().toString();
+ if (recipe.isLeftover(id)) {
+ if (item.getType().toString().contains("_BUCKET")) {
+ item.setType(XMaterial.BUCKET.parseMaterial());
}
+ return;
+ }
- item.setAmount(item.getAmount() - itemsToRemove);
+ if ((item.getAmount() - itemsToRemove) <= 0) {
+ inv.setItem(slot, null);
+ return;
}
+
+ item.setAmount(item.getAmount() - itemsToRemove);
}
RecipeUtil getRecipeUtil() {
return Main.getInstance().recipeUtil;
}
+ ShapedChecks shapedChecks() {
+ return Main.getInstance().shapedChecks;
+ }
+
+ ShapelessChecks shapelessChecks() {
+ return Main.getInstance().shapelessChecks;
+ }
+
CraftManager CraftManager() {
return Main.getInstance().craftManager;
}
diff --git a/src/me/mehboss/crafting/ShapedChecks.java b/src/me/mehboss/crafting/ShapedChecks.java
new file mode 100644
index 0000000..40e137f
--- /dev/null
+++ b/src/me/mehboss/crafting/ShapedChecks.java
@@ -0,0 +1,273 @@
+package me.mehboss.crafting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Material;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.CompatibilityUtil;
+import me.mehboss.utils.RecipeUtil;
+import me.mehboss.utils.RecipeUtil.Ingredient;
+import me.mehboss.utils.RecipeUtil.Recipe;
+
+public class ShapedChecks {
+
+ public class AlignedResult {
+ public final ItemStack[] getMatrix;
+ public final int[] invSlotMap; // maps aligned index → real inventory slot
+
+ public AlignedResult(ItemStack[] alignedGrid, int[] invSlotMap) {
+ this.getMatrix = alignedGrid;
+ this.invSlotMap = invSlotMap;
+ }
+ }
+
+ public AlignedResult getAlignedGrid(Inventory inv, List recipeIngredients) {
+ boolean isCraftingInventory = inv.getType() == InventoryType.WORKBENCH
+ || inv.getType() == InventoryType.CRAFTING;
+
+ ItemStack[] invGrid = new ItemStack[9];
+ int[] invSlotMap = new int[9]; // map each index to real inventory slot
+
+ if (inv.getType() == InventoryType.CRAFTING) {
+ // 2×2 player crafting grid
+ int[] map = { 1, 2, -1, 3, 4, -1, -1, -1, -1 };
+
+ for (int i = 0; i < 9; i++) {
+ int real = map[i];
+ if (real != -1) {
+ ItemStack item = inv.getItem(real);
+ invGrid[i] = (item == null ? new ItemStack(Material.AIR) : item);
+ invSlotMap[i] = real;
+ }
+ }
+ } else {
+ int idx = 0;
+ for (int slot = 0; slot < inv.getContents().length && idx < 9; slot++) {
+ if (isCraftingInventory && slot == 0)
+ continue;
+
+ ItemStack item = inv.getItem(slot);
+ invGrid[idx] = (item == null ? new ItemStack(Material.AIR) : item);
+ invSlotMap[idx] = slot; // << real inventory slot
+ idx++;
+ }
+ }
+
+ // Build 3×3 recipe grid
+ ItemStack[] recipeGrid = new ItemStack[9];
+ for (int i = 0; i < 9; i++) {
+ Ingredient ing = recipeIngredients.get(i);
+ if (ing.isEmpty())
+ recipeGrid[i] = new ItemStack(Material.AIR);
+ else if (ing.hasIdentifier())
+ recipeGrid[i] = getRecipeUtil().getResultFromKey(ing.getIdentifier());
+ else
+ recipeGrid[i] = new ItemStack(ing.getMaterialChoices().get(0));
+ }
+
+ // Trim recipe bounding box
+ int rTop = 3, rBottom = -1, rLeft = 3, rRight = -1;
+ for (int i = 0; i < 9; i++) {
+ if (recipeGrid[i].getType() != Material.AIR) {
+ int r = i / 3;
+ int c = i % 3;
+ rTop = Math.min(rTop, r);
+ rBottom = Math.max(rBottom, r);
+ rLeft = Math.min(rLeft, c);
+ rRight = Math.max(rRight, c);
+ }
+ }
+
+ int recipeHeight = rBottom - rTop + 1;
+ int recipeWidth = rRight - rLeft + 1;
+
+ // Try all shifts
+ for (int shiftRow = 0; shiftRow <= 3 - recipeHeight; shiftRow++) {
+ for (int shiftCol = 0; shiftCol <= 3 - recipeWidth; shiftCol++) {
+
+ boolean match = true;
+
+ for (int rr = 0; rr < recipeHeight && match; rr++) {
+ for (int rc = 0; rc < recipeWidth && match; rc++) {
+
+ int invIndex = (shiftRow + rr) * 3 + (shiftCol + rc);
+ int recIndex = (rTop + rr) * 3 + (rLeft + rc);
+
+ ItemStack invItem = invGrid[invIndex];
+ Ingredient ing = recipeIngredients.get(recIndex);
+
+ if (ing.isEmpty())
+ continue;
+
+ if (invItem == null || !ing.getMaterialChoices().contains(invItem.getType()))
+ match = false;
+ }
+ }
+
+ if (match) {
+ ItemStack[] aligned = new ItemStack[9];
+ int[] map = new int[9];
+
+ Arrays.fill(aligned, new ItemStack(Material.AIR));
+ Arrays.fill(map, -1);
+
+ // Copy matched region
+ for (int rr = 0; rr < recipeHeight; rr++) {
+ for (int rc = 0; rc < recipeWidth; rc++) {
+
+ int invIndex = (shiftRow + rr) * 3 + (shiftCol + rc);
+ int recIndex = (rTop + rr) * 3 + (rLeft + rc);
+
+ aligned[recIndex] = invGrid[invIndex];
+ map[recIndex] = invSlotMap[invIndex]; // <<< real inventory slot
+ }
+ }
+
+ return new AlignedResult(aligned, map);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public boolean handleShapedRecipe(InventoryType type, AlignedResult alignedGrid, Recipe recipe,
+ List recipeIngredients) {
+ int i = -1;
+ for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
+
+ i++;
+ ItemStack stack = alignedGrid.getMatrix[i];
+ if (ingredient.isEmpty()) {
+ if (stack.getType() != Material.AIR)
+ return false;
+
+ continue;
+ }
+
+ ItemMeta meta = stack.getItemMeta();
+ // IDENTIFIER CHECK
+ if (ingredient.hasIdentifier()) {
+ String invID = getRecipeUtil().getKeyFromResult(stack);
+
+ if (invID == null || !invID.equals(ingredient.getIdentifier())) {
+ logDebug("[handleShaped] Skipping recipe..", recipe.getName());
+ logDebug("[handleShaped] IDs do not match", recipe.getName());
+ logDebug("[handleShaped] IngID = " + ingredient.getIdentifier(), recipe.getName());
+ logDebug("[handleShaped] InvID = " + invID, recipe.getName());
+ return false;
+ }
+
+ logDebug("[handleShaped] Passed all checks for the ingredient in slot " + i, recipe.getName());
+ continue;
+ }
+
+ logDebug("[handleShaped] Ingredient slot " + i + " does not have an identifier..", recipe.getName());
+
+ if (recipe.getIgnoreData()) {
+ logDebug("[handleShaped] Skipping data checks.. Ignore-Data is enabled..", recipe.getName());
+ continue;
+ }
+
+ // NAME CHECKS
+ if (!recipe.getIgnoreNames()) {
+
+ if ((!CompatibilityUtil.hasDisplayname(meta) && ingredient.hasDisplayName())
+ || (CompatibilityUtil.hasDisplayname(meta) && !ingredient.hasDisplayName())) {
+ logDebug("[handleShaped] Skipping recipe..", recipe.getName());
+ logDebug(
+ "[handleShaped] The recipe ingredient displayname and the inventory slot displayname do not match",
+ recipe.getName());
+ logDebug("[handleShaped] The inventory slot in question: " + i
+ + ". The ingredient slot in question: " + ingredient.getSlot(), recipe.getName());
+ logDebug("[handleShaped] Does the ingredient have a displayname? " + ingredient.hasDisplayName(),
+ recipe.getName());
+ logDebug("[handleShaped] Does the inventory have a displayname? " + meta.hasDisplayName(),
+ recipe.getName());
+ return false;
+ }
+
+ if (ingredient.hasDisplayName() && CompatibilityUtil.hasDisplayname(meta)
+ && !(ingredient.getDisplayName().equals(CompatibilityUtil.getDisplayname(meta)))) {
+ logDebug("[handleShaped] Skipping recipe..", recipe.getName());
+ logDebug("[handleShaped] The ingredient name for the recipe and inventory do not match",
+ recipe.getName());
+ logDebug("[handleShaped] The inventory slot in question: " + i
+ + ". The ingredient slot in question: " + ingredient.getSlot(), recipe.getName());
+ logDebug("[handleShaped] The ingredient displayname: " + ingredient.getDisplayName(),
+ recipe.getName());
+ logDebug("[handleShaped] The inventory displayname: " + meta.getDisplayName(), recipe.getName());
+ return false;
+ }
+
+ logDebug("[handleShaped] Inventory and recipe ingredient displayname matched for slot " + i,
+ recipe.getName());
+ }
+
+ // MODEL DATA CHECKS
+ if (!Main.getInstance().serverVersionAtLeast(1, 14)) {
+ logDebug("[handleShaped] Skipping CMD checks.. Version is less than 1.14..", recipe.getName());
+ continue;
+ }
+
+ if (!recipe.getIgnoreModelData()) {
+
+ if ((!meta.hasCustomModelData() && ingredient.hasCustomModelData())
+ || (meta.hasCustomModelData() && !ingredient.hasCustomModelData())) {
+ logDebug("[handleShaped] Skipping recipe..", recipe.getName());
+ logDebug("[handleShaped] The recipe ingredient CMD and the inventory slot CMD do not match..",
+ recipe.getName());
+ logDebug("[handleShaped] The inventory slot in question: " + i
+ + ". The ingredient slot in question: " + ingredient.getSlot(), recipe.getName());
+ logDebug("[handleShaped] Does the ingredient have CMD? " + ingredient.hasCustomModelData(),
+ recipe.getName());
+ logDebug("[handleShaped] Does the inventory have CMD? " + meta.hasCustomModelData(),
+ recipe.getName());
+ return false;
+ }
+
+ if (ingredient.hasCustomModelData() && meta.hasCustomModelData()
+ && (ingredient.getCustomModelData() != meta.getCustomModelData())) {
+ logDebug("[handleShaped] Skipping recipe..", recipe.getName());
+ logDebug("[handleShaped] The ingredient CMD for the recipe and inventory do not match..",
+ recipe.getName());
+ logDebug("[handleShaped] The inventory slot in question: " + i
+ + ". The ingredient slot in question: " + ingredient.getSlot(), recipe.getName());
+ logDebug("[handleShaped] The ingredient CMD: " + ingredient.getCustomModelData(), recipe.getName());
+ logDebug("[handleShaped] The inventory CMD: " + meta.getCustomModelData(), recipe.getName());
+ return false;
+ }
+
+ logDebug("[handleShaped] Inventory and recipe ingredient CMD matched for slot " + i, recipe.getName());
+ }
+ }
+
+ return true;
+ }
+
+ RecipeUtil getRecipeUtil() {
+ return Main.getInstance().recipeUtil;
+ }
+
+ void logDebug(String st, String recipeName, UUID id) {
+ if (Main.getInstance().debug && (id == null || (!Main.getInstance().inInventory.contains(id))))
+ Logger.getLogger("Minecraft").log(Level.WARNING,
+ "[DEBUG][" + Main.getInstance().getName() + "][Crafting][" + recipeName + "]" + st);
+ }
+
+ void logDebug(String st, String recipeName) {
+ if (Main.getInstance().debug)
+ Logger.getLogger("Minecraft").log(Level.WARNING,
+ "[DEBUG][" + Main.getInstance().getName() + "][Crafting][" + recipeName + "]" + st);
+ }
+
+}
diff --git a/src/me/mehboss/crafting/ShapelessChecks.java b/src/me/mehboss/crafting/ShapelessChecks.java
new file mode 100644
index 0000000..13e00ae
--- /dev/null
+++ b/src/me/mehboss/crafting/ShapelessChecks.java
@@ -0,0 +1,255 @@
+package me.mehboss.crafting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Material;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+
+import me.mehboss.recipe.Main;
+import me.mehboss.utils.CompatibilityUtil;
+import me.mehboss.utils.RecipeUtil;
+import me.mehboss.utils.RecipeUtil.Recipe;
+
+public class ShapelessChecks {
+
+ public boolean handleShapelessRecipe(Inventory inv, Recipe recipe, List recipeIngredients,
+ UUID id) {
+
+ logDebug("[handleShapeless] Handling shapeless checks..!", recipe.getName(), id);
+
+ ArrayList slotNames = new ArrayList<>();
+ ArrayList recipeNames = new ArrayList<>();
+
+ ArrayList recipeMD = new ArrayList<>();
+ ArrayList inventoryMD = new ArrayList<>();
+
+ ArrayList slotIDs = new ArrayList<>();
+ ArrayList recipeIDs = new ArrayList<>();
+
+ Map recipeCount = new HashMap<>();
+ Map inventoryCount = new HashMap<>();
+
+ if (inv.getType() == InventoryType.CRAFTING) {
+ inventoryCount.put(Material.AIR, 5);
+ Collections.addAll(slotNames, "null", "null", "null", "null", "null");
+ Collections.addAll(slotIDs, "null", "null", "null", "null", "null");
+ Collections.addAll(inventoryMD, -1, -1, -1, -1, -1);
+ }
+
+ Boolean isCrafting = inv.getType() == InventoryType.CRAFTING || inv.getType() == InventoryType.WORKBENCH;
+ ItemStack[] matrix = inv.getContents();
+
+ for (int i = 0; i < matrix.length; i++) {
+ if (i == 0 && isCrafting)
+ continue;
+
+ ItemStack it = matrix[i];
+
+ if (it == null || it.getType() == Material.AIR || it.getAmount() <= 0) {
+ slotNames.add("null");
+ slotIDs.add("null");
+ inventoryMD.add(-1);
+ inventoryCount.put(Material.AIR, inventoryCount.getOrDefault(Material.AIR, 0) + 1);
+ continue;
+ }
+
+ String key = getRecipeUtil().getKeyFromResult(it);
+ if (key == null)
+ key = "null";
+ slotIDs.add(key);
+
+ if (Main.getInstance().serverVersionAtLeast(1, 14)) {
+ if (it.hasItemMeta() && it.getItemMeta().hasCustomModelData()) {
+ inventoryMD.add(it.getItemMeta().getCustomModelData());
+ } else {
+ inventoryMD.add(-1);
+ }
+ }
+
+ inventoryCount.put(it.getType(), inventoryCount.getOrDefault(it.getType(), 0) + 1);
+
+ if (!it.hasItemMeta() || !CompatibilityUtil.hasDisplayname(it.getItemMeta())) {
+ slotNames.add("false");
+ continue;
+ }
+
+ slotNames.add(CompatibilityUtil.getDisplayname(it.getItemMeta()));
+ }
+
+ // working copy used for consumption while matching
+ Map availableCount = new HashMap<>(inventoryCount);
+
+ for (RecipeUtil.Ingredient ingredient : recipeIngredients) {
+ if (ingredient.isEmpty()) {
+ recipeCount.put(Material.AIR, recipeCount.getOrDefault(Material.AIR, 0) + 1);
+ recipeNames.add("null");
+ recipeMD.add(-1);
+ recipeIDs.add("null");
+ continue;
+ }
+
+ if (ingredient.hasIdentifier() && getRecipeUtil().getResultFromKey(ingredient.getIdentifier()) != null) {
+ ItemStack exactMatch = getRecipeUtil().getResultFromKey(ingredient.getIdentifier());
+ Material type = exactMatch.getType();
+
+ recipeCount.put(type, recipeCount.getOrDefault(type, 0) + 1);
+ availableCount.put(type, availableCount.getOrDefault(type, 0) - 1);
+
+ recipeIDs.add(ingredient.getIdentifier());
+
+ if (Main.getInstance().serverVersionAtLeast(1, 14)) {
+ if (exactMatch.hasItemMeta() && exactMatch.getItemMeta().hasCustomModelData()) {
+ recipeMD.add(exactMatch.getItemMeta().getCustomModelData());
+ } else {
+ recipeMD.add(-1);
+ }
+ }
+
+ if (CompatibilityUtil.hasDisplayname(exactMatch.getItemMeta())) {
+ recipeNames.add(CompatibilityUtil.getDisplayname(exactMatch.getItemMeta()));
+ } else {
+ recipeNames.add("false");
+ }
+ continue;
+ }
+
+ if (ingredient.hasMaterialChoices()) {
+ boolean matched = false;
+ for (Material choice : ingredient.getMaterialChoices()) {
+ int avail = availableCount.getOrDefault(choice, 0);
+ if (avail > 0) {
+ availableCount.put(choice, avail - 1);
+ recipeCount.put(choice, recipeCount.getOrDefault(choice, 0) + 1);
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ return false;
+ }
+ } else {
+ Material mat = ingredient.getMaterial();
+ recipeCount.put(mat, recipeCount.getOrDefault(mat, 0) + 1);
+ availableCount.put(mat, availableCount.getOrDefault(mat, 0) - 1);
+ }
+
+ recipeIDs.add("null");
+ recipeMD.add(ingredient.getCustomModelData());
+
+ if (ingredient.hasDisplayName()) {
+ recipeNames.add(ingredient.getDisplayName());
+ } else {
+ recipeNames.add("false");
+ }
+ }
+
+ boolean hasIngredients = true;
+ for (Map.Entry entry : recipeCount.entrySet()) {
+ Material material = entry.getKey();
+ int requiredCount = entry.getValue();
+ int invCount = inventoryCount.getOrDefault(material, 0);
+
+ if (invCount < requiredCount) {
+ hasIngredients = false;
+ break;
+ }
+ }
+
+ if (!hasIngredients) {
+ logDebug("[handleShapeless] Missing required ingredients! Skipping recipe..", recipe.getName(), id);
+ return false;
+ }
+ logDebug("[handleShapeless] All required ingredients found..", recipe.getName(), id);
+
+ if (!recipeIDs.equals(slotIDs) || !slotIDs.equals(recipeIDs)) {
+ logDebug("[handleShapeless] NBT tags mismatch: recipe vs inventory", recipe.getName(), id);
+ logDebug("[handleShapeless] recipeIDS: " + recipeIDs, recipe.getName(), id);
+ logDebug("[handleShapeless] slotIDS: " + slotIDs, recipe.getName(), id);
+ return false;
+ }
+
+ if (recipe.getIgnoreData())
+ return true;
+
+ if (!recipe.getIgnoreModelData()) {
+ Map recipeModelCount = new HashMap<>();
+ Map inventoryModelCount = new HashMap<>();
+
+ if (!Main.getInstance().serverVersionAtLeast(1, 14)) {
+ recipeMD.clear();
+ inventoryMD.clear();
+ }
+ for (int model : recipeMD) {
+ recipeModelCount.put(model, recipeModelCount.getOrDefault(model, 0) + 1);
+ }
+ for (int model : inventoryMD) {
+ inventoryModelCount.put(model, inventoryModelCount.getOrDefault(model, 0) + 1);
+ }
+
+ if (!recipeMD.containsAll(inventoryMD) || !inventoryMD.containsAll(recipeMD)) {
+ logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName(), id);
+ return false;
+ }
+
+ if (!recipeModelCount.equals(inventoryModelCount)) {
+ logDebug("[handleShapeless] Model data mismatch: recipe vs inventory", recipe.getName(), id);
+ logDebug(" RM: " + recipeModelCount, recipe.getName(), id);
+ logDebug(" IM: " + inventoryModelCount, recipe.getName(), id);
+ return false;
+ }
+ }
+
+ if (!recipe.getIgnoreNames()) {
+ Map recipeNameCount = new HashMap<>();
+ Map slotNameCount = new HashMap<>();
+
+ for (String name : recipeNames) {
+ recipeNameCount.put(name, recipeNameCount.getOrDefault(name, 0) + 1);
+ }
+ for (String name : slotNames) {
+ slotNameCount.put(name, slotNameCount.getOrDefault(name, 0) + 1);
+ }
+
+ if (!recipeNames.containsAll(slotNames) || !slotNames.containsAll(recipeNames)) {
+ logDebug("[handleShapeless] Display name mismatch: recipe vs inventory", recipe.getName(), id);
+ logDebug(slotNames.toString(), recipe.getName(), id);
+ logDebug(recipeNames.toString(), recipe.getName(), id);
+ return false;
+ }
+
+ if (!recipeNameCount.equals(slotNameCount)) {
+ logDebug("[handleShapeless] Display name count mismatch: recipe vs inventory", recipe.getName(), id);
+ logDebug(" RN: " + recipeNameCount, recipe.getName(), id);
+ logDebug(" IN: " + slotNameCount, recipe.getName(), id);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ RecipeUtil getRecipeUtil() {
+ return Main.getInstance().recipeUtil;
+ }
+
+ void logDebug(String st, String recipeName, UUID id) {
+ if (Main.getInstance().debug && (id == null || (!Main.getInstance().inInventory.contains(id))))
+ Logger.getLogger("Minecraft").log(Level.WARNING,
+ "[DEBUG][" + Main.getInstance().getName() + "][Crafting][" + recipeName + "]" + st);
+ }
+
+ void logDebug(String st, String recipeName) {
+ if (Main.getInstance().debug)
+ Logger.getLogger("Minecraft").log(Level.WARNING,
+ "[DEBUG][" + Main.getInstance().getName() + "][Crafting][" + recipeName + "]" + st);
+ }
+}
diff --git a/src/me/mehboss/crafting/package-info.java b/src/me/mehboss/crafting/package-info.java
new file mode 100644
index 0000000..585d129
--- /dev/null
+++ b/src/me/mehboss/crafting/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Houses all logic used for custom crafting table recipes.
+ *
+ * Includes shaped and shapeless recipe handling, leftover items,
+ * crafting conditions, and integration hooks for recipe registration.
+ */
+package me.mehboss.crafting;
\ No newline at end of file
diff --git a/src/me/mehboss/gui/BookGUI.java b/src/me/mehboss/gui/BookGUI.java
index f876649..1e28c02 100644
--- a/src/me/mehboss/gui/BookGUI.java
+++ b/src/me/mehboss/gui/BookGUI.java
@@ -12,6 +12,10 @@
import me.mehboss.utils.RecipeUtil;
import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
+import me.mehboss.utils.data.BrewingRecipeData;
+import me.mehboss.utils.data.CookingRecipeData;
+import me.mehboss.utils.data.CraftingRecipeData;
+import me.mehboss.utils.data.WorkstationRecipeData;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
@@ -30,6 +34,9 @@
import org.bukkit.plugin.Plugin;
import com.cryptomorin.xseries.XMaterial;
+
+import me.mehboss.gui.framework.RecipeGUI;
+import me.mehboss.gui.framework.GuiView.GuiRegistry;
import me.mehboss.recipe.Main;
import me.mehboss.utils.CompatibilityUtil;
@@ -53,7 +60,7 @@ public BookGUI(Plugin p) {
getConfig().getString("gui.Displayname").replace("%page%", "1")));
}
- private List buildRecipesFor(Player p, RecipeType type) {
+ public List buildRecipesFor(Player p, RecipeType type) {
HashMap recipesMap = (type != null)
? Main.getInstance().recipeUtil.getRecipesFromType(type)
: Main.getInstance().recipeUtil.getAllRecipes();
@@ -61,7 +68,7 @@ private List buildRecipesFor(Player p, RecipeType type) {
if (recipesMap == null || recipesMap.isEmpty())
return new ArrayList<>();
- return recipesMap.values().stream().filter(r -> !r.hasPerm() || p.hasPermission(r.getPerm()))
+ return recipesMap.values().stream().filter(r -> r.isActive() && (!r.hasPerm() || p.hasPermission(r.getPerm())))
.collect(Collectors.toList());
}
@@ -131,26 +138,53 @@ private RecipeType parseTypeFromHeader(ItemStack header) {
return null;
String name = ChatColor.stripColor(m.getDisplayName()).toUpperCase().split(" ")[0];
- return RecipeType.valueOf(name);
+ return RecipeType.fromString(name);
}
- public void showCreationMenu(Inventory inv, ItemStack item, Player p, String recipeName, String perm,
- Boolean creating, Boolean viewing, RecipeType type) {
- Inventory cInv = null;
+ public void showCreationMenu(Player player, Recipe rawRecipe, boolean creating, boolean viewing) {
- if (!(Main.getInstance().recipeBook.contains(p.getUniqueId()))) {
- cInv = Bukkit.getServer().createInventory(null, 54,
- ChatColor.translateAlternateColorCodes('&', "&cEDITING: " + recipeName));
- } else {
- cInv = Bukkit.getServer().createInventory(null, 54,
- ChatColor.translateAlternateColorCodes('&', "&cVIEWING: " + recipeName));
+ RecipeGUI gui = Main.getInstance().editItem;
+ Recipe recipe = rawRecipe;
+
+ if (creating) {
+ RecipeType type = rawRecipe.getType();
+ String id = rawRecipe.getName();
+ switch (type) {
+
+ case SHAPED:
+ case SHAPELESS:
+ recipe = new CraftingRecipeData(id);
+ break;
+
+ case FURNACE:
+ case BLASTFURNACE:
+ case SMOKER:
+ case CAMPFIRE:
+ recipe = new CookingRecipeData(id);
+ break;
+
+ case STONECUTTER:
+ case GRINDSTONE:
+ case ANVIL:
+ recipe = new WorkstationRecipeData(id);
+ break;
+
+ case BREWING_STAND:
+ recipe = new BrewingRecipeData(id);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unsupported recipe type: " + type);
+ }
}
- RecipeGUI.getInstance().setItems(creating, viewing, cInv, recipeName, perm, item, p, type);
- p.openInventory(cInv);
+ recipe.setType(rawRecipe.getType());
- if (inv != null)
- Main.getInstance().saveInventory.put(p.getUniqueId(), inv);
+ if (viewing) {
+ gui.openViewing(player, recipe);
+ } else {
+ gui.openEditing(player, recipe);
+ }
}
@EventHandler
@@ -178,29 +212,25 @@ void onClick(InventoryClickEvent e) {
if (e.getCurrentItem() == null || e.getCurrentItem().getType() == Material.AIR)
return;
- // --- Create button
- if (e.getCurrentItem().getType() != XMaterial.BLACK_STAINED_GLASS_PANE.parseMaterial()
+ ItemStack stained = createItem("stainedG", XMaterial.BLACK_STAINED_GLASS_PANE, " ");
+ if (!e.getCurrentItem().equals(stained)
&& (e.getRawSlot() == 3 || e.getRawSlot() == 5)) {
- showCreationMenu(e.getInventory(), null, p, "", null, true, false, type);
+ Recipe recipe = new Recipe("New Recipe");
+ recipe.setType(type);
+ showCreationMenu(p, recipe, true, false);
return;
}
- // --- Main menu button
if (e.getRawSlot() == 49) {
- if (Main.getInstance().saveInventory.containsKey(p.getUniqueId())) {
- p.openInventory(Main.getInstance().saveInventory.get(p.getUniqueId()));
- Main.getInstance().saveInventory.remove(p.getUniqueId());
- } else {
- Main.getInstance().typeGUI.open(p);
- }
+ Main.getInstance().typeGUI.open(p);
return;
}
- // --- Paging (Prev 48 / Next 50) ---
if (e.getRawSlot() == 48 || e.getRawSlot() == 50) {
- String original = CompatibilityUtil.getTitle(e);
+
+ String original = title;
String[] cp = original.split("Page ");
- int currentpage = Integer.valueOf(cp[1]);
+ int currentpage = Integer.parseInt(cp[1]);
int newpage = currentpage;
List recipes = buildRecipesFor(p, type);
@@ -222,6 +252,7 @@ void onClick(InventoryClickEvent e) {
original.replace(String.valueOf(currentpage), String.valueOf(newpage)));
Inventory newp = Bukkit.getServer().createInventory(null, 54, newname);
+
ItemStack header = (type == null) ? createTypeHeader(null) : createTypeHeader(type);
items(p, newp, newpage - 1, recipes, header);
@@ -241,18 +272,13 @@ void onClick(InventoryClickEvent e) {
}
}
- logDebug("[RecipeBooklet][" + p.getName() + "] Triggered open recipe matrix.. " + recipeName);
-
if (recipeName != null) {
- Boolean viewing = false;
- if (Main.getInstance().recipeBook.contains(p.getUniqueId()))
- viewing = true;
+ boolean viewing = Main.getInstance().recipeBook.contains(p.getUniqueId());
+ Recipe recipe = Main.getInstance().recipeUtil.getRecipe(recipeName);
- logDebug("[RecipeBooklet][" + p.getName() + "] Opening recipe matrix.. " + recipeName);
- showCreationMenu(e.getInventory(), e.getCurrentItem(), p, recipeName, null, false, viewing, null);
+ showCreationMenu(p, recipe, false, viewing);
}
}
- return;
}
}
@@ -360,8 +386,20 @@ ItemStack createItem(String id, XMaterial material, String name, String... lore)
public void openType(Player p, RecipeType type) {
List recipes = buildRecipesFor(p, type);
- if (recipes.isEmpty())
+ boolean viewing = Main.getInstance().recipeBook.contains(p.getUniqueId());
+
+ if (recipes.isEmpty()) {
+ if (!viewing) {
+ Recipe recipe = new Recipe("New Recipe");
+ recipe.setType(type);
+ showCreationMenu(p, recipe, true, false);
+ }
return;
+ }
+
+ boolean hasRoot = GuiRegistry.hasRootView(p.getUniqueId());
+ if (hasRoot)
+ GuiRegistry.clearRootView(p.getUniqueId());
String title = ChatColor.translateAlternateColorCodes('&',
Main.getInstance().getConfig().getString("gui.Displayname").replace("%page%", "1"));
diff --git a/src/me/mehboss/gui/RecipeGUI.java b/src/me/mehboss/gui/RecipeGUI.java
deleted file mode 100644
index 4ec59c5..0000000
--- a/src/me/mehboss/gui/RecipeGUI.java
+++ /dev/null
@@ -1,921 +0,0 @@
-package me.mehboss.gui;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.UUID;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
-import org.bukkit.Material;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.bukkit.event.inventory.InventoryClickEvent;
-import org.bukkit.event.inventory.InventoryType;
-import org.bukkit.event.player.AsyncPlayerChatEvent;
-import org.bukkit.event.player.PlayerQuitEvent;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-import org.bukkit.plugin.Plugin;
-
-import com.cryptomorin.xseries.XMaterial;
-
-import me.clip.placeholderapi.PlaceholderAPI;
-import me.mehboss.recipe.Main;
-import me.mehboss.utils.CompatibilityUtil;
-import me.mehboss.utils.RecipeUtil;
-import me.mehboss.utils.RecipeUtil.Ingredient;
-import me.mehboss.utils.RecipeUtil.Recipe;
-import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
-
-public class RecipeGUI implements Listener {
-
- // *** this menu is the GUI for adding a new recipe
- public HashMap editmeta = new HashMap<>();
- HashMap inventoryinstance = new HashMap<>();
- HashMap getnewid = new HashMap<>();
-
- List lore = new ArrayList<>();
-
- private static RecipeGUI instance;
-
- Plugin config = Bukkit.getPluginManager().getPlugin("CustomRecipes");
- RecipeSaver saveChanges;
-
- public RecipeGUI(Plugin p, String item) {
- instance = this;
- saveChanges = new RecipeSaver();
- }
-
- public static RecipeGUI getInstance() {
- return instance;
- }
-
- RecipeUtil getRecipeUtil() {
- return Main.getInstance().recipeUtil;
- }
-
- FileConfiguration getConfig(String recipeName) {
- File dataFolder = Main.getInstance().getDataFolder();
- File recipesFolder = new File(dataFolder, "recipes");
- File recipeFile = new File(recipesFolder, recipeName + ".yml");
- return YamlConfiguration.loadConfiguration(recipeFile);
- }
-
- private static final class RecipeLayout {
- final int[] ingredientSlots; // where ingredients go (order matters for SHAPED)
- final Integer fuelSlot; // null when no fuel slot is used
- final int resultSlot; // output slot
-
- RecipeLayout(int[] ingredientSlots, Integer fuelSlot, int resultSlot) {
- this.ingredientSlots = ingredientSlots;
- this.fuelSlot = fuelSlot;
- this.resultSlot = resultSlot;
- }
-
- boolean usesFuel() {
- return fuelSlot != null;
- }
- }
-
- private static final HashMap LAYOUTS = new HashMap<>();
- static {
- int[] CRAFTING_GRID = { 11, 12, 13, 20, 21, 22, 29, 30, 31 };
-
- // Crafting
- LAYOUTS.put(RecipeType.SHAPED, new RecipeLayout(CRAFTING_GRID, null, 24));
- LAYOUTS.put(RecipeType.SHAPELESS, new RecipeLayout(CRAFTING_GRID, null, 24));
-
- // Single-input machines (no fuel)
- LAYOUTS.put(RecipeType.STONECUTTER, new RecipeLayout(new int[] { 20 }, null, 24));
- LAYOUTS.put(RecipeType.CAMPFIRE, new RecipeLayout(new int[] { 20 }, null, 24));
-
- // Furnace-like (input at 11, fuel 29, result 24)
- LAYOUTS.put(RecipeType.FURNACE, new RecipeLayout(new int[] { 11 }, 29, 24));
- LAYOUTS.put(RecipeType.BLASTFURNACE, new RecipeLayout(new int[] { 11 }, 29, 24));
- LAYOUTS.put(RecipeType.SMOKER, new RecipeLayout(new int[] { 11 }, 29, 24));
-
- // Two-input utilities with a center gap (20 [gap 21] 22)
- LAYOUTS.put(RecipeType.ANVIL, new RecipeLayout(new int[] { 20, 22 }, null, 24));
- LAYOUTS.put(RecipeType.GRINDSTONE, new RecipeLayout(new int[] { 20, 22 }, null, 24));
- }
-
- public void setItems(Boolean creating, Boolean viewing, Inventory i, String invname, String perm, ItemStack item,
- OfflinePlayer p, RecipeType rType) {
-
- String configname = invname;
-
- // Get recipe and decide layout from its type
- Recipe r = Main.getInstance().getRecipeUtil().getRecipe(configname);
- RecipeType type = (r != null && r.getType() != null) ? r.getType() : rType;
- RecipeLayout layout = LAYOUTS.getOrDefault(type, LAYOUTS.get(RecipeType.SHAPED));
-
- List loreList = new ArrayList<>();
-
- final int[] controlSlots = { 0, 7, 8, 9, 26, 35, 44, 45, 48, 49, 50, 52, 53 };
-
- boolean[] paneAt = new boolean[54];
- for (int s = 0; s < 54; s++)
- paneAt[s] = true;
- paneAt[layout.resultSlot] = false;
- for (int s : layout.ingredientSlots)
- paneAt[s] = false;
- if (layout.usesFuel())
- paneAt[layout.fuelSlot] = false;
-
- // gap between two-input utilities (center slot 21)
- boolean isTwoInputUtility = (type == RecipeType.ANVIL || type == RecipeType.GRINDSTONE);
- if (isTwoInputUtility)
- paneAt[21] = true;
-
- // In EDITING mode, keep control slots empty (they'll be populated by widgets);
- // in VIEWING mode, we want them to be panes except the Back buttons.
- if (!viewing) {
- for (int s : controlSlots)
- paneAt[s] = false;
- }
-
- ItemStack stained = XMaterial.BLACK_STAINED_GLASS_PANE.parseItem();
- ItemMeta stainedm = stained.getItemMeta();
- stainedm.setDisplayName(" ");
- stained.setItemMeta(stainedm);
- for (int s = 0; s < 54; s++)
- if (paneAt[s])
- i.setItem(s, stained);
-
- // not creating new recipe, fill in slots
- if (!creating) {
- int ingIndex = 0;
- Recipe recipe = Main.getInstance().getRecipeUtil().getRecipe(configname);
-
- for (Ingredient ingredient : recipe.getIngredients()) {
- if (recipe.getType() != RecipeType.SHAPED && (ingredient == null || ingredient.isEmpty())) {
- continue; // skip empties, don't advance ingIndex
- }
-
- if (ingIndex >= layout.ingredientSlots.length)
- break;
-
- boolean foundIdentifier = itemIngredients(ingredient, configname, ingIndex + 1, p) != null;
- ItemStack mat = foundIdentifier ? itemIngredients(ingredient, configname, ingIndex + 1, p)
- : XMaterial.matchXMaterial(ingredient.getMaterial().toString()).get().parseItem();
-
- // for legacy versions for short values
- if (!foundIdentifier && ingredient.hasMaterialData())
- mat = ingredient.getMaterialData().toItemStack();
-
- mat.setAmount(ingredient.getAmount());
-
- if (!foundIdentifier) {
- ItemMeta matm = mat.getItemMeta();
- if (ingredient.hasDisplayName()) {
- matm.setDisplayName(ingredient.getDisplayName());
- }
- mat.setItemMeta(matm);
- }
-
- i.setItem(layout.ingredientSlots[ingIndex], mat);
- ingIndex++;
- }
-
- ItemStack result = new ItemStack(Main.getInstance().recipeUtil.getRecipe(configname).getResult());
- ItemMeta resultM = result.getItemMeta();
-
- if (result.hasItemMeta() && resultM.hasLore() && hasPlaceholder()) {
- resultM.setLore(PlaceholderAPI.setPlaceholders(p, resultM.getLore()));
- result.setItemMeta(resultM);
- }
-
- i.setItem(layout.resultSlot, result);
- }
-
- // VIEWING mode: turn all control slots into panes, then add Back buttons
- if (viewing) {
- for (int s : controlSlots) {
- if (s == 48 || s == 49 || s == 50)
- continue; // back buttons set below
- i.setItem(s, stained);
- }
-
- // Three "Back" buttons centered bottom
- ItemStack back = XMaterial.RED_STAINED_GLASS_PANE.parseItem();
- ItemMeta backm = back.getItemMeta();
- backm.setDisplayName(ChatColor.DARK_RED + "Back");
- back.setItemMeta(backm);
- i.setItem(48, back);
- i.setItem(49, back);
- i.setItem(50, back);
-
- return;
- }
-
- /* ====== below here are the editor widgets (only when not viewing) ====== */
-
- ItemStack identifier = XMaterial.PAPER.parseItem();
- ItemMeta identifierm = identifier.getItemMeta();
- identifierm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fIdentifier:&7 "));
- loreList.add(ChatColor.GRAY + getConfig(configname).getString(configname + ".Identifier"));
-
- if (creating && !invname.isEmpty() && !invname.equals("")) {
- loreList.clear();
- loreList.add(ChatColor.GRAY + invname);
- }
-
- identifierm.setLore(loreList);
- identifier.setItemMeta(identifierm);
- loreList.clear();
-
- ItemStack lore = XMaterial.BOOK.parseItem();
- ItemMeta lorem = lore.getItemMeta();
- lorem.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fLore"));
-
- if (item != null && item.hasItemMeta() && item.getItemMeta().hasLore()) {
- lorem.setLore(item.getItemMeta().getLore());
- if (hasPlaceholder())
- lorem.setLore(PlaceholderAPI.setPlaceholders(p, item.getItemMeta().getLore()));
- }
- lore.setItemMeta(lorem);
-
- ItemStack effects = XMaterial.GHAST_TEAR.parseItem();
- ItemMeta effectsm = effects.getItemMeta();
- effectsm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fEffects"));
- if (getConfig(configname).getString(configname + ".Effects") != null) {
- for (String effectslore : getConfig(configname).getStringList(configname + ".Effects")) {
- loreList.add(ChatColor.GRAY + effectslore);
- }
- effectsm.setLore(loreList);
- }
- effects.setItemMeta(effectsm);
- loreList.clear();
-
- ItemStack converter = XMaterial.FURNACE.parseItem();
- ItemMeta converterm = effects.getItemMeta(); // keep original behavior
- converterm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fConverter"));
- loreList.add(ChatColor.GRAY + type.toString().toUpperCase());
- converterm.setLore(loreList);
- converter.setItemMeta(converterm);
- loreList.clear();
-
- ItemStack permission = XMaterial.ENDER_PEARL.parseItem();
- ItemMeta permissionm = permission.getItemMeta();
- permissionm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fPermission: &7"));
-
- if (getConfig(configname).isSet(configname + ".Permission")
- && !getConfig(configname).getString(configname + ".Permission").equals("none"))
- loreList.add(ChatColor.GRAY + getConfig(configname).getString(configname + ".Permission"));
-
- if (perm != null)
- loreList.add(ChatColor.GRAY + perm);
-
- permissionm.setLore(loreList);
- permission.setItemMeta(permissionm);
- loreList.clear();
-
- ItemStack hideenchants = XMaterial.DIRT.parseItem();
- ItemMeta hideenchantsm = hideenchants.getItemMeta();
- Boolean hc = getConfig(configname).getBoolean(configname + ".Placeable");
- String ht = "&cfalse";
- if (hc == true)
- ht = "&atrue";
- hideenchantsm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fPlaceable: &f" + ht));
- hideenchants.setItemMeta(hideenchantsm);
-
- // Shapeless toggle only for crafting types
- boolean showShapeless = (type == RecipeType.SHAPED || type == RecipeType.SHAPELESS);
- ItemStack shapeless = XMaterial.CRAFTING_TABLE.parseItem();
- if (showShapeless) {
- shapeless = XMaterial.CRAFTING_TABLE.parseItem();
- ItemMeta shapelessm = shapeless.getItemMeta();
- Boolean isShapeless = creating ? rType.equals(RecipeType.SHAPELESS) : true;
- Boolean sc = creating ? isShapeless : getConfig(configname).getBoolean(configname + ".Shapeless");
- String toggle = sc ? "&atrue" : "&cfalse";
- shapelessm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fShapeless: &f" + toggle));
- shapeless.setItemMeta(shapelessm);
- }
-
- // Exact choice
- ItemStack ec = XMaterial.LEVER.parseItem();
- ItemMeta ecm = ec.getItemMeta();
- Boolean exact = getConfig(configname).getBoolean(configname + ".Exact-Choice", true);
- String toggle = exact ? "&atrue" : "&cfalse";
- ecm.setDisplayName(ChatColor.translateAlternateColorCodes('&', "&fExact-Choice: &f" + toggle));
- ec.setItemMeta(ecm);
-
- ItemStack name = XMaterial.WRITABLE_BOOK.parseItem();
- ItemMeta namem = name.getItemMeta();
- namem.setDisplayName(ChatColor.WHITE + "Recipe Name: ");
- if (getConfig(configname).isConfigurationSection(configname))
- loreList.add(ChatColor.GRAY + configname);
-
- if (creating && !invname.isEmpty() && !invname.equals("")) {
- loreList.clear();
- loreList.add(ChatColor.GRAY + invname);
- }
-
- namem.setLore(loreList);
- name.setItemMeta(namem);
- loreList.clear();
-
- ItemStack amount = XMaterial.FEATHER.parseItem();
- ItemMeta amountm = amount.getItemMeta();
- amountm.setDisplayName(ChatColor.translateAlternateColorCodes('&',
- "&fAmount: &7" + getConfig(configname).getString(configname + ".Amount")));
- amount.setItemMeta(amountm);
-
- i.setItem(9, identifier);
- i.setItem(8, hideenchants);
- i.setItem(7, effects);
- i.setItem(17, converter);
- i.setItem(26, permission);
- i.setItem(0, name);
- i.setItem(35, amount);
- i.setItem(44, lore);
- i.setItem(18, ec);
-
- // Slot 53 – shapeless only shown for crafting; otherwise fill with pane
- if (showShapeless) {
- i.setItem(53, shapeless);
- } else {
- i.setItem(53, stained);
- }
-
- ItemStack enabled = XMaterial.SLIME_BALL.parseItem();
- ItemMeta enabledm = enabled.getItemMeta();
- enabledm.setDisplayName(ChatColor.GREEN + "Enabled");
- enabled.setItemMeta(enabledm);
-
- Boolean toggled = getConfig(configname).isBoolean(configname + ".Enabled")
- ? getConfig(configname).getBoolean(configname + ".Enabled")
- : null;
- if (toggled != null && !toggled) {
- enabled = XMaterial.SNOWBALL.parseItem();
- enabledm.setDisplayName(ChatColor.RED + "Disabled");
- enabled.setItemMeta(enabledm);
- }
- i.setItem(52, enabled);
-
- ItemStack cancel = XMaterial.RED_STAINED_GLASS_PANE.parseItem();
- ItemMeta cancelm = cancel.getItemMeta();
- cancelm.setDisplayName(ChatColor.DARK_RED + "Cancel");
- cancel.setItemMeta(cancelm);
- i.setItem(48, cancel);
-
- ItemStack mainmenu = XMaterial.WHITE_STAINED_GLASS_PANE.parseItem();
- ItemMeta mainmenum = mainmenu.getItemMeta();
- mainmenum.setDisplayName("Main Menu");
- mainmenu.setItemMeta(mainmenum);
- i.setItem(49, mainmenu);
-
- ItemStack update = XMaterial.GREEN_STAINED_GLASS_PANE.parseItem();
- ItemMeta updatem = update.getItemMeta();
- updatem.setDisplayName(ChatColor.DARK_GREEN + "Update");
- update.setItemMeta(updatem);
- i.setItem(50, update);
-
- ItemStack delete = XMaterial.BARRIER.parseItem();
- ItemMeta deletem = delete.getItemMeta();
- deletem.setDisplayName(ChatColor.DARK_RED + "Delete Recipe");
- delete.setItemMeta(deletem);
- i.setItem(45, delete);
- }
-
- String chatColor(String st) {
- if (st == null)
- return null;
- if (st.equalsIgnoreCase("false"))
- return st;
- return ChatColor.translateAlternateColorCodes('&', st);
- }
-
- ItemStack itemIngredients(Ingredient ingredient, String recipename, int slot, OfflinePlayer p) {
- if (ingredient.hasIdentifier()) {
- Boolean isCustomItem = getRecipeUtil().getRecipeFromKey(ingredient.getIdentifier()) == null ? true : false;
- ItemStack item = isCustomItem ? getRecipeUtil().getResultFromKey(ingredient.getIdentifier())
- : getRecipeUtil().getRecipeFromKey(ingredient.getIdentifier()).getResult();
-
- if (item == null) {
- logError("Failed to set item ingredients! Could not find itemstack for ingredient slot "
- + ingredient.getSlot() + ". isCustomItem: " + isCustomItem, recipename);
- return null;
- }
-
- ItemStack finalItem = new ItemStack(item);
- ItemMeta itemM = finalItem.getItemMeta();
- if (!isCustomItem && finalItem.hasItemMeta() && itemM.hasLore() && hasPlaceholder()) {
- itemM.setLore(PlaceholderAPI.setPlaceholders(p, itemM.getLore()));
- finalItem.setItemMeta(itemM);
- }
- return finalItem;
- }
- return null;
- }
-
- @EventHandler
- public void onClick(InventoryClickEvent e) {
-
- if (!(e.getWhoClicked() instanceof Player) || e.getClickedInventory() == null
- || e.getClickedInventory().getType() == InventoryType.PLAYER)
- return;
-
- Player p = (Player) e.getWhoClicked();
- String inventoryTitle = CompatibilityUtil.getTitle(e);
-
- if (e.getInventory() != null && inventoryTitle != null && inventoryTitle.contains("VIEWING: ")) {
- e.setCancelled(true);
-
- if (e.getCurrentItem() == null || e.getCurrentItem().getType() == Material.AIR || e.getRawSlot() == 24)
- return;
-
- if (getRecipeUtil().getRecipeFromResult(e.getCurrentItem()) != null) {
- String name = getRecipeUtil().getRecipeFromResult(e.getCurrentItem()).getName();
- Inventory edit = Bukkit.getServer().createInventory(null, 54,
- ChatColor.translateAlternateColorCodes('&', "&cVIEWING: " + name));
- setItems(false, true, edit, name, null, e.getCurrentItem(), p, null);
- Main.getInstance().saveInventory.put(p.getUniqueId(), e.getInventory());
- p.openInventory(edit);
- }
-
- if (e.getRawSlot() == 48 || e.getRawSlot() == 49 || e.getRawSlot() == 50) {
- if (Main.getInstance().saveInventory.containsKey(p.getUniqueId())) {
- p.openInventory(Main.getInstance().saveInventory.get(p.getUniqueId()));
- Main.getInstance().saveInventory.remove(p.getUniqueId());
- } else {
- Main.getInstance().typeGUI.open(p);
- }
- }
- return;
- }
-
- if (e.getInventory() != null && inventoryTitle != null && inventoryTitle.contains("EDITING: ")) {
-
- String[] split = inventoryTitle.split("EDITING: ");
- String recipeName = split.length == 2 ? split[1] : null;
-
- // Type-aware click allowance
- Recipe r = Main.getInstance().getRecipeUtil().getRecipe(recipeName);
-
- ItemMeta converterItem = e.getInventory().getItem(17).getItemMeta();
- String converterLore = converterItem.hasLore() ? converterItem.getLore().get(0) : "SHAPED";
-
- RecipeType creationType = RecipeType.valueOf(ChatColor.stripColor(converterLore));
- RecipeType type = (r != null && r.getType() != null) ? r.getType() : creationType;
- boolean showShapeless = (type == RecipeType.SHAPED || type == RecipeType.SHAPELESS);
- boolean isCooking = (type == RecipeType.FURNACE || type == RecipeType.BLASTFURNACE
- || type == RecipeType.SMOKER || type == RecipeType.CAMPFIRE);
-
- // Control buttons (always handled below)
- HashSet controls = new HashSet<>();
- int[] craftingSlots = { 11, 12, 13, 20, 21, 22, 29, 30, 31, 24 };
- int[] cookingSlots = { 11, 29, 24 };
- int[] otherSlots = { 20, 22, 24 };
-
- int[] controlSlots = craftingSlots;
-
- if (!showShapeless) {
- controlSlots = otherSlots;
- if (isCooking)
- controlSlots = cookingSlots;
- }
-
- for (int s : controlSlots)
- controls.add(s);
-
- if (!controls.contains(e.getRawSlot())) {
- e.setCancelled(true);
- }
-
- if (e.getRawSlot() == 7)
- return; // effects button
- if (e.getRawSlot() == 17)
- return; // enchants button
-
- if (recipeName == null || recipeName.isEmpty() || recipeName.equals(" ") || recipeName.equals("null")) {
- ItemMeta nameItem = e.getInventory().getItem(0).getItemMeta();
- String newTitle = nameItem.hasLore() ? nameItem.getLore().get(0) : null;
- if (recipeName == null || !recipeName.equals(newTitle))
- e.getView().setTitle(ChatColor.GREEN + "EDITING: " + newTitle);
- inventoryinstance.put(p.getUniqueId(), p.getOpenInventory().getTopInventory());
- }
- if (e.getRawSlot() == 35) {
- // amount button
- handleStringMessage(p, "Amount", e);
- }
-
- ItemStack toggle = e.getCurrentItem();
- if (toggle == null || toggle.getItemMeta() == null)
- return;
- ItemMeta togglem = toggle.getItemMeta();
-
- if (e.getRawSlot() == 44) {
- // item lore
- ItemStack item = e.getInventory().getItem(44);
- if (item.hasItemMeta() && item.getItemMeta().hasLore()) {
- lore = item.getItemMeta().getLore();
- }
- inventoryinstance.put(p.getUniqueId(), p.getOpenInventory().getTopInventory());
- editmeta.put(p.getUniqueId(), "Lore");
- p.closeInventory();
- sendLoremsg(p);
- }
-
- if (e.getRawSlot() == 48) {
- // cancel button
- p.sendMessage(ChatColor.RED + "You have successfully cancelled any changes to the recipe '" + recipeName
- + "'");
- p.closeInventory();
- }
-
- if (e.getRawSlot() == 49) {
- // main menu button
- Main.getInstance().typeGUI.open(p);
- }
-
- if (e.getRawSlot() == 50) {
- // update button
- saveChanges.saveRecipe(e.getClickedInventory(), p, recipeName);
- }
-
- if (e.getRawSlot() == 52) {
- // recipe toggle enable/disable
- if (togglem.getDisplayName().contains("Enabled")) {
- togglem.setDisplayName(ChatColor.RED + "Disabled");
- toggle.setItemMeta(togglem);
- p.getOpenInventory().getTopInventory().setItem(52, toggle);
- e.getCurrentItem().setType(XMaterial.SNOWBALL.parseMaterial());
- return;
- }
- if (togglem.getDisplayName().contains("Disabled")) {
- togglem.setDisplayName(ChatColor.GREEN + "Enabled");
- toggle.setItemMeta(togglem);
- p.getOpenInventory().getTopInventory().setItem(52, toggle);
- e.getCurrentItem().setType(XMaterial.SLIME_BALL.parseMaterial());
- return;
- }
- }
-
- if (e.getRawSlot() == 8) {
- toggleBooleanDisplay(p, e, toggle, 8, "Placeable: ");
- return;
- }
-
- if (e.getRawSlot() == 53 && showShapeless) {
- toggleBooleanDisplay(p, e, toggle, 53, "Shapeless: ");
- return;
- }
-
- if (e.getRawSlot() == 18) {
- toggleBooleanDisplay(p, e, toggle, 18, "Exact-Choice: ");
- return;
- }
-
- if (e.getRawSlot() == 9)
- handleStringMessage(p, "Identifier", e);
- if (e.getRawSlot() == 26)
- handleStringMessage(p, "Permission", e);
- if (e.getRawSlot() == 0)
- handleStringMessage(p, "Name", e);
-
- if (e.getRawSlot() == 45) {
- if (recipeName == null || recipeName.isEmpty() || recipeName.equals(" ") || recipeName.equals("null"))
- return;
-
- if (ChatColor.stripColor(e.getClickedInventory().getItem(45).getItemMeta().getDisplayName())
- .equals("Confirm Recipe Deletion")) {
-
- File recipeFile = new File(Main.getInstance().getDataFolder(), "recipes/" + recipeName + ".yml");
-
- if (recipeFile.exists()) {
- if (recipeFile.delete()) {
- p.sendMessage(ChatColor.GREEN + "Recipe '" + recipeName + "' was successfully deleted.");
- } else {
- p.sendMessage(ChatColor.RED + "Failed to delete recipe '" + recipeName + "'.");
- }
- }
-
- Main.getInstance().reload();
- Main.getInstance().typeGUI.open(p);
- return;
- }
-
- ItemStack confirm = e.getClickedInventory().getItem(45);
- ItemMeta confirmm = e.getClickedInventory().getItem(45).getItemMeta();
- confirmm.setDisplayName(ChatColor.DARK_RED + "Confirm Recipe Deletion");
- confirm.setItemMeta(confirmm);
- }
- }
- }
-
- void toggleBooleanDisplay(Player p, InventoryClickEvent e, ItemStack toggle, int slot, String labelPrefix) {
-
- ItemMeta meta = toggle.getItemMeta();
- String name = ChatColor.stripColor(meta.getDisplayName());
-
- if (name.contains("true")) {
- meta.setDisplayName(labelPrefix + ChatColor.RED + "false");
- toggle.setItemMeta(meta);
- p.getOpenInventory().getTopInventory().setItem(slot, toggle);
- } else if (name.contains("false")) {
- meta.setDisplayName(labelPrefix + ChatColor.GREEN + "true");
- toggle.setItemMeta(meta);
- p.getOpenInventory().getTopInventory().setItem(slot, toggle);
- }
- }
-
- public void sendLoremsg(Player p) {
-
- int num = 1;
-
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
- p.sendMessage("Please type your new recipe lore");
- p.sendMessage("Current Lore: ");
- p.sendMessage(" ");
-
- if (lore != null) {
- for (String line : lore) {
- p.sendMessage(num + ": " + line);
- num++;
- }
- } else {
- p.sendMessage("NO LORE");
- }
-
- p.sendMessage(" ");
- p.sendMessage("Type 'LINE#: string' to edit/add a line to your lore.");
- p.sendMessage("Type 'LINE#: remove' to remove a line.");
- p.sendMessage("Type ONLY the line number for an empty space.");
- p.sendMessage("Type " + ChatColor.RED + "CANCEL " + ChatColor.WHITE + "to go back.");
- p.sendMessage("Type " + ChatColor.GREEN + "DONE " + ChatColor.WHITE + "when finished.");
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
- }
-
- boolean hasPlaceholder() {
- return Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null;
- }
-
- public void handleLore(int slot, String string, Player p, AsyncPlayerChatEvent e) {
-
- Inventory inv = inventoryinstance.get(p.getUniqueId());
- ItemStack item = inv.getItem(44);
- ItemStack result = inv.getItem(24); // result slot updated
-
- if (!editmeta.containsKey(p.getUniqueId()))
- return;
-
- if (e.getMessage().equalsIgnoreCase("done")) {
-
- if (!lore.isEmpty() && result != null && item.hasItemMeta()) {
- ItemMeta itemm = item.getItemMeta();
- itemm.setLore(lore);
-
- if (hasPlaceholder())
- itemm.setLore(PlaceholderAPI.setPlaceholders(p, lore));
-
- item.setItemMeta(itemm);
-
- ItemMeta resultm = result.getItemMeta();
- resultm.setLore(lore);
-
- if (hasPlaceholder())
- resultm.setLore(PlaceholderAPI.setPlaceholders(p, lore));
-
- result.setItemMeta(resultm);
- }
-
- lore.clear();
-
- Bukkit.getScheduler().runTask(Bukkit.getPluginManager().getPlugin("CustomRecipes"), () -> {
- p.openInventory(inv);
- });
-
- editmeta.remove(p.getUniqueId());
- inventoryinstance.remove(p.getUniqueId());
- return;
- }
-
- if (e.getMessage().equalsIgnoreCase("cancel")) {
- lore.clear();
-
- Bukkit.getScheduler().runTask(Bukkit.getPluginManager().getPlugin("CustomRecipes"), () -> {
- p.openInventory(inv);
- });
-
- editmeta.remove(p.getUniqueId());
- inventoryinstance.remove(p.getUniqueId());
- return;
- }
-
- String[] spl = e.getMessage().split(": ");
-
- if (spl == null || !isInt(spl[0]) || Integer.parseInt(spl[0]) <= 0) {
- p.sendMessage(ChatColor.RED + "Wrong format! You must first define the line number!");
- return;
- }
-
- if (lore.isEmpty() || lore.size() < Integer.parseInt(spl[0])) {
-
- if (spl.length < 2) {
- lore.add(" ");
- sendLoremsg(p);
- return;
- }
-
- lore.add(ChatColor.translateAlternateColorCodes('&', spl[1]));
- sendLoremsg(p);
- return;
- }
-
- if (spl.length < 2) {
- lore.set(Integer.parseInt(spl[0]) - 1, " ");
- sendLoremsg(p);
-
- } else if (spl[1].equals("remove")) {
- lore.remove(Integer.parseInt(spl[0]) - 1);
- sendLoremsg(p);
-
- } else {
- lore.set(Integer.parseInt(spl[0]) - 1, ChatColor.translateAlternateColorCodes('&', spl[1]));
- sendLoremsg(p);
- }
- }
-
- @EventHandler
- public void onClose(PlayerQuitEvent e) {
- UUID uuid = e.getPlayer().getUniqueId();
-
- if (Main.getInstance().recipeBook.contains(uuid))
- Main.getInstance().recipeBook.remove(uuid);
-
- if (Main.getInstance().saveInventory.containsKey(uuid))
- Main.getInstance().saveInventory.remove(uuid);
-
- if (editmeta.containsKey(uuid))
- editmeta.remove(uuid);
-
- if (inventoryinstance.containsKey(uuid))
- inventoryinstance.remove(uuid);
-
- if (getnewid.containsKey(uuid))
- getnewid.remove(uuid);
- }
-
- @EventHandler
- public void onChat(AsyncPlayerChatEvent e) {
-
- Player p = e.getPlayer();
-
- if (!editmeta.containsKey(p.getUniqueId()) || editmeta.get(p.getUniqueId()) == null)
- return;
-
- e.setCancelled(true);
-
- if (editmeta.get(p.getUniqueId()).equals("Lore")) {
- handleLore(44, "Lore", p, e);
- return;
- }
-
- if (editmeta.get(p.getUniqueId()).equals("identifier")) {
- handleString(9, "Identifier", p, e);
- return;
- }
-
- if (editmeta.get(p.getUniqueId()).equals("permission")) {
- handleString(26, "Permission", p, e);
- return;
- }
-
- if (editmeta.get(p.getUniqueId()).equals("name")) {
- handleString(0, "Name", p, e);
- return;
- }
-
- if (editmeta.get(p.getUniqueId()).equals("amount")) {
- handleString(35, "Amount", p, e);
- return;
- }
- }
-
- public void handleStringMessage(Player p, String s, InventoryClickEvent e) {
- inventoryinstance.put(p.getUniqueId(), p.getOpenInventory().getTopInventory());
- editmeta.put(p.getUniqueId(), s.toLowerCase());
- p.closeInventory();
-
- List getid = null;
- String identifier = null;
- if (e.getCurrentItem().getItemMeta().hasLore()) {
- getid = e.getCurrentItem().getItemMeta().getLore();
- identifier = getid.get(0);
- } else {
- String[] split = e.getCurrentItem().getItemMeta().getDisplayName().split(": ");
- String name = split.length > 1 ? split[1] : "null";
- identifier = name;
- }
-
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
- p.sendMessage("Please type your new recipe " + s.toLowerCase());
- p.sendMessage("Current " + s + ": " + ChatColor.translateAlternateColorCodes('&', identifier));
- p.sendMessage("Type " + ChatColor.RED + "CANCEL " + ChatColor.WHITE + "to go back.");
- p.sendMessage("Type " + ChatColor.GREEN + "DONE " + ChatColor.WHITE + "when finished.");
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
- }
-
- public boolean isInt(String text) {
- try {
- Integer.parseInt(text);
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
- }
-
- private void handleString(int slot, String string, Player p, AsyncPlayerChatEvent e) {
-
- Inventory inv = inventoryinstance.get(p.getUniqueId());
- ItemStack id = inv.getItem(slot);
- ItemMeta idm = id.getItemMeta();
-
- if (!e.getMessage().equalsIgnoreCase("done") && !e.getMessage().equalsIgnoreCase("cancel")) {
- p.sendMessage(" ");
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
- p.sendMessage(ChatColor.GRAY + ChatColor.translateAlternateColorCodes('&', e.getMessage()) + ChatColor.WHITE
- + " has been selected as the new " + string.toLowerCase() + ".");
- p.sendMessage("Type " + ChatColor.GREEN + "DONE " + ChatColor.WHITE + "to confirm your selection!");
- p.sendMessage("Type " + ChatColor.RED + "CANCEL " + ChatColor.WHITE + "to cancel your selection!");
- p.sendMessage(ChatColor.GRAY + "Or retype selection for a different " + string.toLowerCase());
- p.sendMessage(ChatColor.DARK_GRAY + "---------------------------------------------");
-
- getnewid.put(p.getUniqueId(), e.getMessage());
- }
-
- if (getnewid.containsKey(p.getUniqueId()) && e.getMessage().equalsIgnoreCase("done")) {
-
- String newidf = getnewid.get(p.getUniqueId());
- List loreList = new ArrayList<>();
-
- if (!string.equals("Amount")) {
- loreList.add(ChatColor.GRAY + ChatColor.translateAlternateColorCodes('&', newidf));
- idm.setLore(loreList);
- id.setItemMeta(idm);
- inv.setItem(slot, id);
- }
-
- if (string.equals("Amount")) {
- if (!isInt(newidf)) {
- p.sendMessage(" ");
- p.sendMessage(ChatColor.RED + "Value must be in number format! Please try again.");
- return;
- }
- ItemStack item = inv.getItem(35);
- ItemMeta itemm = item.getItemMeta();
- itemm.setDisplayName(ChatColor.WHITE + "Amount: " + ChatColor.GRAY + newidf);
- item.setItemMeta(itemm);
- inv.setItem(35, item);
-
- if (inv.getItem(24) != null)
- inv.getItem(24).setAmount(Integer.valueOf(newidf)); // result slot updated
- }
-
- Bukkit.getScheduler().runTask(Bukkit.getPluginManager().getPlugin("CustomRecipes"), () -> {
- p.openInventory(inv);
- });
-
- inventoryinstance.remove(p.getUniqueId());
- editmeta.remove(p.getUniqueId());
- getnewid.remove(p.getUniqueId());
- return;
- }
-
- if (!getnewid.containsKey(p.getUniqueId()) || e.getMessage().equalsIgnoreCase("cancel")) {
- editmeta.remove(p.getUniqueId());
- Bukkit.getScheduler().runTask(Bukkit.getPluginManager().getPlugin("CustomRecipes"), () -> {
- p.openInventory(inv);
- });
-
- inventoryinstance.remove(p.getUniqueId());
- }
- }
-
- // Legacy helper left as-is for compatibility with other code paths
- public boolean hasSomething(Inventory i) {
- // updated crafting grid + result slot
- if ((i.getItem(11) != null || i.getItem(12) != null || i.getItem(13) != null || i.getItem(20) != null
- || i.getItem(21) != null || i.getItem(22) != null || i.getItem(29) != null || i.getItem(30) != null
- || i.getItem(31) != null) && i.getItem(24) != null) {
- return true;
- }
- return false;
- }
-
- private void logError(String st, String recipe) {
- Logger.getLogger("Minecraft").log(Level.WARNING,
- "[DEBUG][" + Main.getInstance().getName() + "][" + recipe + "] " + st);
- }
-}
\ No newline at end of file
diff --git a/src/me/mehboss/gui/RecipeSaver.java b/src/me/mehboss/gui/RecipeSaver.java
index 21ec199..c2b18be 100644
--- a/src/me/mehboss/gui/RecipeSaver.java
+++ b/src/me/mehboss/gui/RecipeSaver.java
@@ -10,10 +10,11 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
import org.bukkit.ChatColor;
+import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -22,189 +23,491 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.potion.PotionData;
+import org.bukkit.potion.PotionEffect;
+
+import com.cryptomorin.xseries.XMaterial;
+import com.cryptomorin.xseries.XPotion;
import io.github.bananapuncher714.nbteditor.NBTEditor;
+import me.mehboss.gui.framework.GuiButton;
+import me.mehboss.gui.framework.GuiButton.GuiStringButton;
+import me.mehboss.gui.framework.GuiButton.GuiToggleButton;
+import me.mehboss.gui.framework.GuiView;
+import me.mehboss.gui.framework.RecipeLayout;
import me.mehboss.recipe.Main;
+import me.mehboss.utils.CompatibilityUtil;
+import me.mehboss.utils.RecipeUtil.Recipe;
import me.mehboss.utils.RecipeUtil.Recipe.RecipeType;
public class RecipeSaver {
- // GUI slot constants
- private static final int SLOT_IDENTIFIER = 9;
- private static final int SLOT_PLACEABLE_TOGGLE = 8;
- private static final int SLOT_RESULT = 24;
- private static final int SLOT_PERMISSION = 26;
- private static final int SLOT_ENABLED_TOGGLE = 52;
- private static final int SLOT_SHAPELESS_TOGGLE = 53;
- private static final int SLOT_EXACTCHOICE_TOGGLE = 18;
- private static final int SLOT_CONVERTER = 17;
-
- // Crafting grid slots (keep the same as before)
- private static final int[] CRAFT_SLOTS = { 11, 12, 13, 20, 21, 22, 29, 30, 31 };
- private static final int[] COOKING_SLOTS = { 11, 29 };
- private static final int[] OTHER_SLOTS = { 20, 22 };
- private static int[] CONTROL_SLOTS = CRAFT_SLOTS;
-
- // Save the recipe map to the configuration file
- public void saveRecipe(Inventory inv, Player p, String coloredName) {
-
- String recipeName = ChatColor.stripColor(coloredName);
- File recipesFolder = new File(Main.getInstance().getDataFolder(), "recipes");
- if (!recipesFolder.exists()) {
- recipesFolder.mkdirs();
- }
+ /**
+ * Saves the contents of a recipe GUI to its YAML file.
+ *
+ * @param view The active GuiView (editing view)
+ * @param player The player who clicked "Update"
+ * @param recipe The logical recipe being edited/created
+ */
+ public void saveRecipe(GuiView view, Player player, Recipe recipe) {
+
+ Inventory inv = view.getInventory();
+ RecipeType type = recipe.getType();
- if (recipeName == null || recipeName.isEmpty() || recipeName.equals("null") || recipeName.equals(" "))
+ if (type == null) {
+ player.sendMessage(ChatColor.RED + "Recipe has no type set; cannot save.");
return;
+ }
- String rawConverter = readLoreTag(inv);
- RecipeType type = rawConverter.equals("none") ? RecipeType.SHAPED : RecipeType.valueOf(rawConverter);
- boolean showShapeless = (type == RecipeType.SHAPED || type == RecipeType.SHAPELESS);
- boolean isCooking = (type == RecipeType.FURNACE || type == RecipeType.BLASTFURNACE || type == RecipeType.SMOKER
- || type == RecipeType.CAMPFIRE);
+ RecipeLayout layout = RecipeLayout.forType(type);
+ int[] ingredientSlots = layout.getIngredientSlots();
+ int resultSlot = layout.getResultSlot();
- if (!showShapeless) {
- CONTROL_SLOTS = OTHER_SLOTS;
- if (isCooking)
- CONTROL_SLOTS = COOKING_SLOTS;
+ String recipeName = recipe.getName();
+ if (recipeName == null || recipeName.trim().isEmpty()) {
+ player.sendMessage(ChatColor.RED + "Recipe name is empty; cannot save.");
+ return;
}
+ // Ensure there is at least one ingredient and a result
boolean matrixEmpty = true;
- boolean slotEmpty = inv.getItem(SLOT_RESULT) == null || inv.getItem(SLOT_RESULT).getType() == Material.AIR;
- for (int slot : CONTROL_SLOTS) {
- if (inv.getItem(slot) != null && inv.getItem(slot).getType() != Material.AIR) {
+ for (int slot : ingredientSlots) {
+ if (!isEmpty(inv, slot)) {
matrixEmpty = false;
break;
}
}
- if (matrixEmpty || slotEmpty) {
+ if (matrixEmpty || isEmpty(inv, resultSlot)) {
+ player.sendMessage(ChatColor.RED + "Recipe matrix or result is empty; nothing to save.");
return;
}
+ // Prepare recipes folder
+ File recipesFolder = new File(Main.getInstance().getDataFolder(), "recipes");
+ if (!recipesFolder.exists()) {
+ recipesFolder.mkdirs();
+ }
+
File recipeFile = new File(recipesFolder, recipeName + ".yml");
FileConfiguration recipeConfig = YamlConfiguration.loadConfiguration(recipeFile);
- Map recipeData = convertToRecipeConfig(inv, recipeName);
+ Map recipeData = convertToRecipeConfig(view, inv, recipe, type, layout);
recipeConfig.set(recipeName, recipeData);
try {
recipeConfig.save(recipeFile);
- p.sendMessage(ChatColor.GREEN + "Recipe saved successfully to " + recipeFile.getName() + "!");
+ player.sendMessage(ChatColor.GREEN + "Recipe saved successfully to " + recipeFile.getName() + "!");
} catch (IOException e) {
e.printStackTrace();
- p.sendMessage(ChatColor.RED + "Failed to save the recipe to " + recipeFile.getName() + ".");
+ player.sendMessage(ChatColor.RED + "Failed to save the recipe to " + recipeFile.getName() + ".");
}
- Main.getInstance().reload();
- p.closeInventory();
+ Main.getInstance().recipeManager.addRecipes(recipeName);
+ player.closeInventory();
}
- public Map convertToRecipeConfig(Inventory inventory, String recipeName) {
- // Use LinkedHashMap so YAML preserves our field order (matching your example)
+ private Map convertToRecipeConfig(GuiView view, Inventory inventory, Recipe recipe, RecipeType type,
+ RecipeLayout layout) {
+
LinkedHashMap cfg = new LinkedHashMap<>();
+ int[] ingredientSlots = layout.getIngredientSlots();
+ int resultSlot = layout.getResultSlot();
+
// ====== Result item ======
- ItemStack resultItem = safeGet(inventory, SLOT_RESULT);
+ ItemStack resultItem = safeGet(inventory, resultSlot);
int resultAmount = (resultItem != null && resultItem.getAmount() > 0) ? resultItem.getAmount() : 1;
- // ====== Read GUI toggles and text ======
- AtomicBoolean resultHasID = new AtomicBoolean(false);
- boolean enabled = readEnabledToggle(inventory);
- boolean shapeless = readBooleanToggle(inventory, SLOT_SHAPELESS_TOGGLE);
- boolean exactchoice = readBooleanToggle(inventory, SLOT_EXACTCHOICE_TOGGLE);
- boolean placeable = readBooleanToggle(inventory, SLOT_PLACEABLE_TOGGLE);
- String identifier = readIdentifier(inventory, resultItem, recipeName);
- String permission = readPermission(inventory, identifier);
- String displayNameColored = readNameColored(inventory);
-
- String rawConverterType = readLoreTag(inventory);
- String converterType = rawConverterType.equals("SHAPED") || rawConverterType.equals("SHAPELESS") ? "none"
- : rawConverterType;
+ // ====== High-level flags ======
+ boolean enabled = readEnabledToggle(view);
+ boolean shapeless = readBooleanToggle(view, "Shapeless", false);
+ boolean exactChoice = readBooleanToggle(view, "Exact-Choice", true);
+ boolean placeable = readBooleanToggle(view, "Placeable", false);
+
+ // ====== Textual fields ======
+ String identifier = readIdentifier(view, recipe.getName());
+ String permission = readStringField(view, "Permission", "none");
+ String group = readStringField(view, "Group", "none");
+ if ("none".equalsIgnoreCase(group))
+ group = "";
+
+ String displayNameColored = readNameColored(resultItem);
+ // ====== Effects (currently unused, keep as empty list) ======
+ List effects = new ArrayList<>(); // or from button "Effects" later
+
+ // ====== Enchantments & lore ======
List enchantList = readEnchantList(resultItem);
- Map letters = setItemLetters(inventory);
- List itemCrafting = generateItemCrafting(letters, inventory);
+ AtomicBoolean resultHasID = new AtomicBoolean(false);
+ String material = getItemValue(resultItem, identifier, resultHasID);
List lore = getItemLoreList(resultItem);
if (lore == null)
lore = new ArrayList<>();
- LinkedHashMap ingredients = buildIngredientsFromLetters(inventory, letters);
+ // ====== Ingredient mapping / pattern ======
+ Map letters = setItemLetters(inventory, ingredientSlots);
+ List itemCrafting = generateItemCrafting(letters, inventory, ingredientSlots);
+ LinkedHashMap ingredients = buildIngredientsFromLetters(inventory, letters, ingredientSlots);
+
+ String converterType = (type == RecipeType.SHAPED || type == RecipeType.SHAPELESS) ? "none" : type.toString();
+
+ boolean isCooking = type == RecipeType.FURNACE || type == RecipeType.BLASTFURNACE || type == RecipeType.SMOKER
+ || type == RecipeType.CAMPFIRE;
+ boolean hasRepairCost = type == RecipeType.GRINDSTONE || type == RecipeType.ANVIL;
+ boolean isBrewing = type == RecipeType.BREWING_STAND;
+ boolean isCrafting = type == RecipeType.SHAPED || type == RecipeType.SHAPELESS;
+ // ====== Base fields (mostly unchanged from original) ======
cfg.put("Enabled", enabled);
- cfg.put("Shapeless", shapeless);
- cfg.put("Cooldown", 60);
- cfg.put("Item", getItemValue(resultItem, identifier, resultHasID));
- cfg.put("Item-Damage", "none");
- cfg.put("Amount", resultAmount);
+ if (isCrafting)
+ cfg.put("Shapeless", shapeless);
+ if (isCrafting || type == RecipeType.STONECUTTER)
+ cfg.put("Group", group);
+ cfg.put("Cooldown", -1);
cfg.put("Placeable", placeable);
- cfg.put("Ignore-Data", false);
- cfg.put("Ignore-Model-Data", false);
- cfg.put("Multi-Resulted", false);
- cfg.put("Exact-Choice", exactchoice);
- cfg.put("Custom-Tagged", false);
- cfg.put("Durability", resultItem.getDurability());
-
+ cfg.put("Exact-Choice", exactChoice);
cfg.put("Identifier", identifier);
+
+ // Converter: use recipe type; crafting types use "none" like before
cfg.put("Converter", converterType);
- cfg.put("Permission", permission);
+ // ---- Cooking: Cook-Time & Experience ----
+ if (isCooking) {
+ Integer cookTime = readIntField(view, "Cook-Time");
+ if (cookTime != null) {
+ cfg.put("Cook-Time", cookTime);
+ }
+ }
+
+ if (isCooking || type == RecipeType.GRINDSTONE) {
+ Double exp = readDoubleField(view, "Experience");
+ if (exp != null) {
+ cfg.put("Experience", exp);
+ }
+ }
+
+ // ---- Workstation: Repair-Cost ----
+ if (hasRepairCost) {
+ Integer repairCost = readIntField(view, "Repair-Cost");
+ if (repairCost != null) {
+ cfg.put("Repair-Cost", repairCost);
+ }
+ }
+
+ // ---- Brewing: Required-Items, Fuel-Set, Fuel-Charge ----
+ if (isBrewing) {
+ Integer fuelSet = readIntField(view, "Fuel-Set");
+ if (fuelSet != null) {
+ cfg.put("FuelSet", fuelSet);
+ }
+
+ Integer fuelCharge = readIntField(view, "Fuel-Charge");
+ if (fuelCharge != null) {
+ cfg.put("FuelCharge", fuelCharge);
+ }
+
+ boolean requiresItems = readBooleanToggle(view, "Required-Items", false);
+ cfg.put("Required-Items", requiresItems);
+ }
+
+ cfg.put("Permission", permission);
cfg.put("Auto-Discover-Recipe", true);
cfg.put("Book-Category", "MISC");
- if (resultHasID.get()) {
- cfg.put("Name", "none");
- cfg.put("Lore", null);
-
- } else {
- if (displayNameColored != null && !displayNameColored.isEmpty()) {
- cfg.put("Name", displayNameColored);
+ boolean isPotion = material == XMaterial.POTION.get().toString()
+ || material == XMaterial.SPLASH_POTION.get().toString()
+ || material == XMaterial.LINGERING_POTION.get().toString();
+
+ // handles "result" configuration section
+ Map result = new LinkedHashMap<>();
+ if (!isPotion && !resultHasID.get()) {
+ result.put("Item", material);
+ result.put("Item-Damage", "none");
+ result.put("Durability", (resultItem != null) ? resultItem.getDurability() : 0);
+ result.put("Amount", resultAmount);
+
+ if (resultHasID.get()) {
+ result.put("Name", "none");
+ result.put("Lore", new ArrayList<>());
} else {
- cfg.put("Name", "none");
+ result.put("Name",
+ (displayNameColored != null && !displayNameColored.isEmpty()) ? displayNameColored : "none");
+ result.put("Lore", lore);
}
- cfg.put("Lore", lore);
+ result.put("Hide-Enchants", true);
+ result.put("Enchantments", enchantList);
+ result.put("Custom-Tagged", false);
+ result.put("Custom-Model-Data", "none");
+ result.put("Item-Flags", new ArrayList<>());
+ result.put("Attribute", new ArrayList<>());
+ result.put("Custom-Tags", new ArrayList<>());
+ } else if (isPotion) {
+ savePotionToConfig(resultItem, result);
}
+ // adds the section back to parent
+ cfg.put("Result", result);
+
+ if (!isBrewing) {
+ cfg.put("Effects", effects);
- cfg.put("Effects", new ArrayList<>());
- cfg.put("Hide-Enchants", true);
- cfg.put("Enchantments", enchantList);
+ Map commands = new LinkedHashMap<>();
+ commands.put("Give-Item", true);
+ commands.put("Run-Commands", new ArrayList<>());
+ cfg.put("Commands", commands);
+ }
cfg.put("ItemCrafting", itemCrafting);
cfg.put("Ingredients", ingredients);
- cfg.put("Custom-Tags", new ArrayList<>());
- cfg.put("Item-Flags", new ArrayList<>());
- cfg.put("Attribute", new ArrayList<>());
- cfg.put("Custom-Model-Data", "none");
+ cfg.put("ItemsLeftover", new ArrayList<>());
+
+ Map flags = new LinkedHashMap<>();
+ flags.put("Ignore-Data", false);
+ flags.put("Ignore-Model-Data", false);
+ flags.put("Ignore-Name", false);
+ cfg.put("Flags", flags);
+
+ cfg.put("Use-Conditions", false);
+ cfg.put("Conditions_All", new ArrayList<>());
cfg.put("Disabled-Worlds", new ArrayList<>());
return cfg;
}
- private String getItemValue(ItemStack item, String id, AtomicBoolean hasID) {
- if (item == null || item.getType() == Material.AIR)
- return "AIR"; // fallback for empty slots
+ public void savePotionToConfig(ItemStack item, Map configValues) {
+ Map cfg = configValues;
- // Try custom key from RecipeUtil (for plugin-based custom items)
- String key = Main.getInstance().getRecipeUtil().getKeyFromResult(item);
- if (key != null && !key.isEmpty() && !key.equals(id)) {
- hasID.set(true);
- return key;
+ // --- Material ---
+ cfg.put("Item", item.getType().name());
+
+ if (!(item.getItemMeta() instanceof PotionMeta)) {
+ return;
}
- // Fallback to Material name
- return item.getType().toString();
+ PotionMeta meta = (PotionMeta) item.getItemMeta();
+
+ // --- Name ---
+ if (meta.hasDisplayName())
+ cfg.put("Name", meta.getDisplayName());
+
+ // --- Color ---
+ if (meta.hasColor()) {
+ Color c = meta.getColor();
+ cfg.put("Color", c.getRed() + "," + c.getGreen() + "," + c.getBlue());
+ } // Try to detect vanilla vs custom
+ boolean vanilla = meta.hasCustomEffects() && meta.getBasePotionData() != null;
+
+ // =====================================================
+ // Case 1: VANILLA POTION (BasePotionData)
+ // =====================================================
+ if (meta.getBasePotionData() != null && vanilla) {
+ PotionData pd = meta.getBasePotionData();
+
+ // XPotion reverse lookup
+ XPotion xp = XPotion.matchXPotion(pd.getType());
+
+ cfg.put("PotionType", xp.getPotionType().name());
+ cfg.put("Amplifier", pd.isUpgraded() ? 1 : 0);
+ cfg.put("Duration", pd.isExtended() ? 3600 : 1800); // approx
+
+ return;
+ }
+
+ // =====================================================
+ // Case 2: CUSTOM EFFECTS
+ // =====================================================
+ if (meta.getCustomEffects().isEmpty()) {
+ return;
+ }
+
+ List
RecipeChoiceor +MaterialAPIs instead. MaterialData was + removed after Minecraft 1.13.