categories = ((RecipeEditor) getParentEditor()).getTable().getCategoryList();
String currentCategory = recipe.getCategory();
@@ -289,6 +319,7 @@ else if (event.isRightClick()) {
}
case 53 -> {
reload(false);
+ suppressCloseNav = true;
((RecipeEditor) getParentEditor()).reload(true);
return;
}
@@ -296,14 +327,28 @@ else if (event.isRightClick()) {
if (hasChanges) {
reload(true);
+ Editor root = getRootEditor();
+ if (root instanceof ProfessionEditor) ((ProfessionEditor) root).autoSave();
}
}
+ @EventHandler
+ public void onInventoryClose(InventoryCloseEvent event) {
+ if (event.getInventory() != getInventory()) return;
+ if (suppressCloseNav) {
+ suppressCloseNav = false;
+ return;
+ }
+ Bukkit.getScheduler().runTaskLater(Fusion.getInstance(), () -> openParent(player), 1);
+ }
+
public void reload(boolean open) {
setIcons(EditorRegistry.getRecipeEditorCfg().getSubIcons(recipe));
initialize();
- if (open)
+ if (open) {
+ suppressCloseNav = true;
open(player);
+ }
}
public String getRecipeName() {
diff --git a/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java b/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java
index ff9de7e..33f60f5 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java
@@ -1,22 +1,25 @@
package studio.magemonkey.fusion.gui.recipe;
import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import studio.magemonkey.codex.util.DataUT;
+import studio.magemonkey.fusion.cfg.hooks.divinity.DivinityModuleItemType;
+import studio.magemonkey.fusion.data.recipes.RecipeCustomItem;
+import studio.magemonkey.fusion.data.recipes.RecipeItem;
import java.util.*;
/**
* Immutable fingerprint for an ItemStack that matches CalculatedRecipe.isSimilar(...) logic.
*
- * We compare:
- * - Material
- * - customModelData (if present)
- * - displayName (if present)
- * - lore lines (if present)
- * - all enchantments (if present)
- * - unbreakable flag
- * - durability (if Damageable)
+ * Special handling for Divinity items (item_generator, gems, essences, runes): only the
+ * item_id and level are compared, ignoring variable stats like lore or enchantments.
+ * A divinityItemLevel of -1 acts as a wildcard ("any level") in matching — used for
+ * recipe ingredients that do not specify a level constraint.
*/
public class IngredientFingerprint {
private final Material type;
@@ -26,6 +29,20 @@ public class IngredientFingerprint {
private final Map enchantments;
private final boolean unbreakable;
private final int durability;
+ private final boolean hasSocketFill;
+
+ // --- Divinity-specific fields ---
+ private final String divinityItemId; // null if not a Divinity item
+ private final int divinityItemLevel; // -1 means "any level" (wildcard) or not a Divinity item
+
+ private static final NamespacedKey DIVINITY_MODULE_KEY = new NamespacedKey("divinity", "item_module");
+ private static final NamespacedKey DIVINITY_ITEM_ID_KEY = new NamespacedKey("divinity", "item_id");
+ private static final NamespacedKey DIVINITY_ITEM_LEVEL_KEY = new NamespacedKey("divinity", "item_level");
+ private static final String DIVINITY_MODULE_ITEMGEN = "item_generator";
+ private static final String DIVINITY_MODULE_GEMS = "gems";
+ private static final String DIVINITY_MODULE_ESSENCES = "essences";
+ private static final String DIVINITY_MODULE_RUNES = "runes";
+ private static final String DIVINITY_MODULE_EXTRACTOR = "extractor";
public IngredientFingerprint(Material type,
int customModelData,
@@ -33,7 +50,10 @@ public IngredientFingerprint(Material type,
List lore,
Map enchantments,
boolean unbreakable,
- int durability) {
+ int durability,
+ boolean hasSocketFill,
+ String divinityItemId,
+ int divinityItemLevel) {
this.type = type;
this.customModelData = customModelData;
this.displayName = (displayName == null ? "" : displayName);
@@ -41,10 +61,43 @@ public IngredientFingerprint(Material type,
this.enchantments = (enchantments == null ? Collections.emptyMap() : new HashMap<>(enchantments));
this.unbreakable = unbreakable;
this.durability = durability;
+ this.hasSocketFill = hasSocketFill;
+ this.divinityItemId = divinityItemId;
+ this.divinityItemLevel = divinityItemLevel;
+ }
+
+ public static boolean hasSocketFill(ItemStack item) {
+ if (item == null) return false;
+ return hasSocketFill(item.getItemMeta());
+ }
+
+ private static boolean hasSocketFill(ItemMeta meta) {
+ if (meta == null) return false;
+ PersistentDataContainer pdc = meta.getPersistentDataContainer();
+ for (NamespacedKey key : pdc.getKeys()) {
+ if (!key.getNamespace().equals("divinity")) continue;
+ String k = key.getKey().toLowerCase();
+ if (!k.startsWith("item_socket_gem_")
+ && !k.startsWith("item_socket_rune_")
+ && !k.startsWith("item_socket_essence_")) continue;
+ String[] value = pdc.get(key, DataUT.STRING_ARRAY);
+ if (value != null && value.length == 2 && !value[0].isEmpty() && !value[1].isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isDivinityModule(String module) {
+ return DIVINITY_MODULE_ITEMGEN.equalsIgnoreCase(module)
+ || DIVINITY_MODULE_GEMS.equalsIgnoreCase(module)
+ || DIVINITY_MODULE_ESSENCES.equalsIgnoreCase(module)
+ || DIVINITY_MODULE_RUNES.equalsIgnoreCase(module)
+ || DIVINITY_MODULE_EXTRACTOR.equalsIgnoreCase(module);
}
/**
- * Build an IngredientFingerprint by examining a live ItemStack.
+ * Build an IngredientFingerprint by examining a live ItemStack from a player's inventory.
*/
public static IngredientFingerprint of(ItemStack is) {
Material mat = is.getType();
@@ -57,6 +110,9 @@ public static IngredientFingerprint of(ItemStack is) {
boolean unbreak = false;
int dmg = 0;
+ String divId = null;
+ int divLevel = -1;
+
if (meta != null) {
if (meta.hasCustomModelData()) {
cmd = meta.getCustomModelData();
@@ -75,15 +131,123 @@ public static IngredientFingerprint of(ItemStack is) {
if (meta instanceof org.bukkit.inventory.meta.Damageable dmeta) {
dmg = dmeta.getDamage();
}
+
+ // Detect Divinity items (item_generator, gems, essences, runes)
+ PersistentDataContainer pdc = meta.getPersistentDataContainer();
+ String module = pdc.get(DIVINITY_MODULE_KEY, PersistentDataType.STRING);
+ if (isDivinityModule(module)) {
+ String itemId = pdc.get(DIVINITY_ITEM_ID_KEY, PersistentDataType.STRING);
+ if (itemId != null && !itemId.isEmpty()) {
+ divId = itemId.toLowerCase();
+ }
+ Integer level = pdc.get(DIVINITY_ITEM_LEVEL_KEY, PersistentDataType.INTEGER);
+ if (level != null) {
+ divLevel = level;
+ }
+ }
}
- return new IngredientFingerprint(mat, cmd, name, loreList, enchantsMap, unbreak, dmg);
+ return new IngredientFingerprint(mat,
+ cmd,
+ name,
+ loreList,
+ enchantsMap,
+ unbreak,
+ dmg,
+ divId != null && hasSocketFill(meta), // non-Divinity items can't have Divinity sockets
+ divId,
+ divLevel);
+ }
+
+ /**
+ * Build a fingerprint for a recipe ingredient requirement — avoids calling getItemStack()
+ * on Divinity items (which would generate a random-level item when level=-1).
+ * The resulting fingerprint uses level=-1 as a wildcard matching any actual item level.
+ */
+ public static IngredientFingerprint forRequired(RecipeItem required) {
+ if (required instanceof RecipeCustomItem rci
+ && rci.getItemType() instanceof DivinityModuleItemType dmt
+ && dmt.getModuleItem() != null) {
+ return new IngredientFingerprint(
+ Material.AIR, 0, "", Collections.emptyList(),
+ Collections.emptyMap(), false, 0, false,
+ dmt.getModuleItem().getId().toLowerCase(),
+ dmt.getLevel());
+ }
+ ItemStack single = required.getItemStack().clone();
+ single.setAmount(1);
+ return of(single);
+ }
+
+ /**
+ * Sum all values in the map whose keys match this fingerprint.
+ * Required when this fingerprint has divinityItemLevel=-1 (any level), since multiple
+ * inventory entries with different specific levels all map to different HashMap buckets
+ * under the old hashCode, but must all be counted.
+ */
+ public int sumMatchingEntries(Map map) {
+ if (divinityItemId != null && divinityItemLevel == -1) {
+ int total = 0;
+ for (Map.Entry e : map.entrySet()) {
+ IngredientFingerprint k = e.getKey();
+ if (k.divinityItemId != null && k.divinityItemId.equals(this.divinityItemId)) {
+ total += e.getValue();
+ }
+ }
+ return total;
+ }
+ return map.getOrDefault(this, 0);
+ }
+
+ /**
+ * Drain `need` units from the map for entries matching this fingerprint.
+ * Handles the wildcard (level=-1) case by draining across all matching specific-level entries.
+ */
+ public void drainFromMap(Map map, int need) {
+ if (divinityItemId != null && divinityItemLevel == -1) {
+ for (Iterator> it = map.entrySet().iterator();
+ it.hasNext() && need > 0; ) {
+ Map.Entry e = it.next();
+ IngredientFingerprint k = e.getKey();
+ if (k.divinityItemId != null && k.divinityItemId.equals(this.divinityItemId)) {
+ int drain = Math.min(e.getValue(), need);
+ need -= drain;
+ if (e.getValue() - drain <= 0) {
+ it.remove();
+ } else {
+ e.setValue(e.getValue() - drain);
+ }
+ }
+ }
+ return;
+ }
+ int cur = map.getOrDefault(this, 0);
+ if (cur - need <= 0) {
+ map.remove(this);
+ } else {
+ map.put(this, cur - need);
+ }
+ }
+
+ public Material getType() {
+ return type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IngredientFingerprint that)) return false;
+
+ // Specialized comparison for Divinity items
+ if (this.divinityItemId != null && that.divinityItemId != null) {
+ if (!this.divinityItemId.equals(that.divinityItemId)) return false;
+ // -1 on either side = wildcard (no level constraint in recipe)
+ if (this.divinityItemLevel == -1 || that.divinityItemLevel == -1) return true;
+ return this.divinityItemLevel == that.divinityItemLevel;
+ }
+
+ // Fallback to original logic for non-Divinity items
+ if (this.hasSocketFill || that.hasSocketFill) return false;
return customModelData == that.customModelData &&
unbreakable == that.unbreakable &&
durability == that.durability &&
@@ -95,6 +259,11 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
+ if (divinityItemId != null) {
+ // Exclude level so that wildcard (-1) and specific levels hash to the same bucket.
+ // equals() handles the level comparison correctly.
+ return Objects.hash(divinityItemId);
+ }
return Objects.hash(type, customModelData, displayName, lore, enchantments, unbreakable, durability);
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
index 11baae1..e2e8527 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
@@ -1,5 +1,6 @@
package studio.magemonkey.fusion.gui.recipe;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@@ -12,8 +13,11 @@
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
+import studio.magemonkey.fusion.Fusion;
+import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.data.player.FusionPlayer;
import studio.magemonkey.fusion.data.player.PlayerLoader;
+import studio.magemonkey.fusion.data.recipes.CraftingTable;
import studio.magemonkey.fusion.gui.ProfessionGuiRegistry;
import studio.magemonkey.fusion.gui.RecipeGui;
@@ -75,8 +79,15 @@ public void onInventoryClose(InventoryCloseEvent event) {
RecipeGui gui = findGuiFor(p, inv);
if (gui == null) return;
- // If the player closes this GUI, perform cleanup
gui.close(p, inv);
+
+ if (isPlayerClose(event)) {
+ CraftingTable table = gui.getTable();
+ if (table.getUseCategories() && !table.getCategories().isEmpty()) {
+ Bukkit.getScheduler().runTaskLater(Fusion.getInstance(),
+ () -> ProfessionsCfg.getGUI(gui.getName()).open(p), 1L);
+ }
+ }
}
@EventHandler(ignoreCancelled = true)
@@ -118,4 +129,15 @@ public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
if (gui == null) return;
gui.close(p, gui.getInventory());
}
+
+ /** Paper-safe check: returns true if the close was initiated by the player (not plugin-triggered).
+ * Falls back to true on Spigot where getReason() doesn't exist. */
+ private static boolean isPlayerClose(InventoryCloseEvent event) {
+ try {
+ Object reason = event.getClass().getMethod("getReason").invoke(event);
+ return reason != null && "PLAYER".equals(reason.toString());
+ } catch (Exception ignored) {
+ return true; // Spigot: no getReason(), treat all closes as player-initiated
+ }
+ }
}
diff --git a/src/main/java/studio/magemonkey/fusion/gui/show/ShowRecipesGui.java b/src/main/java/studio/magemonkey/fusion/gui/show/ShowRecipesGui.java
index 11e9cef..2f1369e 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/show/ShowRecipesGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/show/ShowRecipesGui.java
@@ -19,9 +19,11 @@
import studio.magemonkey.fusion.Fusion;
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.cfg.ShowRecipesCfg;
+import studio.magemonkey.fusion.data.professions.pattern.Category;
import studio.magemonkey.fusion.data.recipes.CraftingTable;
import studio.magemonkey.fusion.data.recipes.Recipe;
import studio.magemonkey.fusion.data.recipes.RecipeItem;
+import studio.magemonkey.fusion.gui.ProfessionGuiRegistry;
import studio.magemonkey.fusion.gui.slot.Slot;
import java.util.ArrayList;
@@ -287,9 +289,16 @@ public void onClick(InventoryClickEvent event) {
}
if (recipeSlots.containsKey(slot)) {
- Recipe recipe = recipeSlots.get(slot);
- CraftingTable table = recipe.getTable();
- ProfessionsCfg.getGuiMap().get(table.getName()).open(player, table.getCategory(recipe.getCategory()));
+ Recipe recipe = recipeSlots.get(slot);
+ CraftingTable table = recipe.getTable();
+ ProfessionGuiRegistry gui = ProfessionsCfg.getGuiMap().get(table.getName());
+ if (gui == null) return;
+ Category category = table.getCategory(recipe.getCategory());
+ if (category != null) {
+ gui.open(player, category);
+ } else {
+ gui.open(player);
+ }
}
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/util/StationChecker.java b/src/main/java/studio/magemonkey/fusion/util/StationChecker.java
new file mode 100644
index 0000000..b9131db
--- /dev/null
+++ b/src/main/java/studio/magemonkey/fusion/util/StationChecker.java
@@ -0,0 +1,90 @@
+package studio.magemonkey.fusion.util;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Sprawdza, czy gracz posiada wymaganą "stację" (Divinity lub vanilla) gdziekolwiek w swoim ekwipunku.
+ */
+public class StationChecker {
+
+ private static final NamespacedKey DIVINITY_ITEM_ID = new NamespacedKey("divinity", "item_id");
+ private static final String DIVINITY_PREFIX = "divinity:";
+
+ /**
+ * Główna metoda – zwraca true, jeśli gracz ma stację określoną przez parametr "station".
+ *
+ * Format parametru:
+ *
+ * - {@code "divinity:twoje_id"} – sprawdza Divinity item z danym item_id
+ * - {@code "CRAFTING_TABLE"} – sprawdza dowolny przedmiot o tym materiale (vanilla)
+ *
+ *
+ */
+ public static boolean hasStation(Player player, String station) {
+ if (station == null || station.isEmpty()) return true; // brak wymagania
+
+ // Rozróżnienie typu stacji po prefiksie
+ if (station.toLowerCase().startsWith(DIVINITY_PREFIX)) {
+ String divinityId = station.substring(DIVINITY_PREFIX.length());
+ return hasDivinityStation(player, divinityId);
+ } else {
+ // Vanilla – oczekujemy nazwy materiału (np. CRAFTING_TABLE)
+ Material material = Material.getMaterial(station.toUpperCase());
+ if (material == null) {
+ // Nieznany materiał – logujemy ostrzeżenie i uznajemy, że gracz nie ma stacji
+ player.getServer().getLogger().warning("[Fusion] Nieznany materiał stacji: " + station);
+ return false;
+ }
+ return hasVanillaStation(player, material);
+ }
+ }
+
+ /**
+ * Sprawdza, czy gracz ma gdziekolwiek w ekwipunku przedmiot z podanym divinity:item_id.
+ */
+ private static boolean hasDivinityStation(Player player, String itemId) {
+ return anyItemMatches(player, item -> {
+ ItemMeta meta = item.getItemMeta();
+ if (meta == null) return false;
+ String id = meta.getPersistentDataContainer().get(DIVINITY_ITEM_ID, PersistentDataType.STRING);
+ return itemId.equalsIgnoreCase(id);
+ });
+ }
+
+ /**
+ * Sprawdza, czy gracz ma gdziekolwiek w ekwipunku przedmiot o podanym materiale.
+ */
+ private static boolean hasVanillaStation(Player player, Material material) {
+ return anyItemMatches(player, item -> item.getType() == material);
+ }
+
+ /**
+ * Pomocnicza – sprawdza wszystkie przedmioty gracza (cały Inventory + main hand)
+ * pod kątem warunku zdefiniowanego przez "predicate".
+ */
+ private static boolean anyItemMatches(Player player, java.util.function.Predicate predicate) {
+ PlayerInventory inv = player.getInventory();
+
+ // Sprawdź wszystkie sloty Inventory (plecak, pancerz, offhand)
+ if (Arrays.stream(inv.getContents()).filter(Objects::nonNull).anyMatch(predicate)) {
+ return true;
+ }
+
+ // Sprawdź przedmiot w głównej ręce (nie jest częścią getContents())
+ ItemStack mainHand = inv.getItemInMainHand();
+ if (mainHand != null && !mainHand.getType().isAir() && predicate.test(mainHand)) {
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/studio/magemonkey/fusion/util/TabCacher.java b/src/main/java/studio/magemonkey/fusion/util/TabCacher.java
index 57c3a85..194a86c 100644
--- a/src/main/java/studio/magemonkey/fusion/util/TabCacher.java
+++ b/src/main/java/studio/magemonkey/fusion/util/TabCacher.java
@@ -12,9 +12,13 @@
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
+import studio.magemonkey.divinity.Divinity;
import studio.magemonkey.divinity.api.DivinityAPI;
+import studio.magemonkey.divinity.modules.api.QModuleDrop;
import studio.magemonkey.fabled.Fabled;
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
+import studio.magemonkey.fusion.cfg.hooks.HookType;
+import studio.magemonkey.fusion.Fusion;
import java.util.*;
@@ -46,10 +50,29 @@ public static void clearTabs(UUID uuid, String key) {
PlayerTabs.get(uuid).CachedTabs.remove(key);
}
+ public static void clearAllCaches(String key) {
+ PlayerTabs.values().forEach(cacher -> cacher.CachedTabs.remove(key));
+ }
+
public static List getTabs(UUID uuid, String key, String arg) {
List entries = new ArrayList<>();
if (isNotCached(uuid, key)) {
switch (key) {
+ case "station":
+ for (Material material : Material.values()) {
+ if (material.isAir()) continue;
+ entries.add(material.toString().toLowerCase());
+ }
+ if (Bukkit.getPluginManager().isPluginEnabled("Divinity")) {
+ DivinityAPI.getModuleManager()
+ .getCustomItemsManager()
+ .getItems()
+ .forEach((k) -> {
+ entries.add("DIVINITY_" + k.getId().toLowerCase());
+ entries.add("DIV_CUSTOM_" + k.getId().toLowerCase());
+ });
+ }
+ break;
case "items":
for (Material material : Material.values()) {
if (material.isAir()) continue;
@@ -62,6 +85,62 @@ public static List getTabs(UUID uuid, String key, String arg) {
.getItems()
.forEach((k) -> entries.add("DIVINITY_" + k.getId().toLowerCase()));
}
+ if (Fusion.getInstance().getHookManager().isHooked(HookType.Divinity)) {
+ String random = QModuleDrop.RANDOM_ID;
+ var mc = Divinity.getInstance().getModuleCache();
+ for (String id : mc.getTierManager().getItemIds()) {
+ if (id.equals(random)) continue;
+ entries.add("DIV_ITEMGEN_" + id);
+ entries.add("DIV_ITEMGEN_" + id + "_NOENCH");
+ }
+ for (String id : mc.getGemManager().getItemIds()) {
+ if (!id.equals(random)) entries.add("DIV_GEM_" + id);
+ }
+ for (String id : mc.getEssenceManager().getItemIds()) {
+ if (!id.equals(random)) entries.add("DIV_ESSENCE_" + id);
+ }
+ for (String id : mc.getRuneManager().getItemIds()) {
+ if (!id.equals(random)) entries.add("DIV_RUNE_" + id);
+ }
+ if (mc.getArrowManager() != null)
+ mc.getArrowManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_ARROW_" + id));
+ if (mc.getConsumablesManager() != null)
+ mc.getConsumablesManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_CONSUMABLE_" + id));
+ mc.getCustomItemsManager().getItems()
+ .forEach(k -> entries.add("DIV_CUSTOM_" + k.getId().toLowerCase()));
+ if (mc.getFortifyManager() != null)
+ mc.getFortifyManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_FORTIFY_" + id));
+ if (mc.getIdentifyManager() != null)
+ mc.getIdentifyManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_IDENTIFY_" + id));
+ if (mc.getMagicDustManager() != null)
+ mc.getMagicDustManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_DUST_" + id));
+ if (mc.getRepairManager() != null)
+ mc.getRepairManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_REPAIR_" + id));
+ if (mc.getResolveManager() != null)
+ mc.getResolveManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_DISMANTLE_" + id));
+ if (mc.getRefineManager() != null)
+ mc.getRefineManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_REFINE_" + id));
+ if (mc.getExtractManager() != null)
+ mc.getExtractManager().getItemIds().stream()
+ .filter(id -> !id.equals(random))
+ .forEach(id -> entries.add("DIV_EXTRACTOR_" + id));
+ }
break;
case "professions":
entries.addAll(ProfessionsCfg.getMap().keySet());
@@ -231,4 +310,40 @@ public static List getFlagsTab(String[] args) {
}
return entries;
}
+
+ /** Level hints for DIV_ item tab completion (single value or range). */
+ public static List getLevelHints(String partial) {
+ List hints = new ArrayList<>();
+ hints.add("");
+ hints.add("1");
+ hints.add("5");
+ hints.add("10");
+ hints.add("20");
+ hints.add("1:5");
+ hints.add("1:10");
+ List result = new ArrayList<>();
+ for (String h : hints) {
+ if (h.toLowerCase().startsWith(partial.toLowerCase())) result.add(h);
+ }
+ return result;
+ }
+
+ /** Material type suggestions for a given DIV_ITEMGEN item ID. */
+ public static List getItemGenMaterialTypes(String itemId, String partial) {
+ List entries = new ArrayList<>();
+ if (!Fusion.getInstance().getHookManager().isHooked(HookType.Divinity)) return entries;
+ var tierMgr = Divinity.getInstance().getModuleCache().getTierManager();
+ if (tierMgr == null) return entries;
+ String cleanId = itemId.toUpperCase().endsWith("_NOENCH")
+ ? itemId.substring(0, itemId.length() - 7)
+ : itemId;
+ var genItem = tierMgr.getItemById(cleanId.toLowerCase());
+ if (genItem == null) return entries;
+ entries.add("");
+ for (studio.magemonkey.codex.api.items.ItemType mat : genItem.getMaterialsList()) {
+ String id = mat.getID().toLowerCase();
+ if (id.toLowerCase().startsWith(partial.toLowerCase())) entries.add(id);
+ }
+ return entries;
+ }
}
diff --git a/src/main/resources/Editors/professions/RecipeEditor.yml b/src/main/resources/Editors/professions/RecipeEditor.yml
index f0c5604..2804b0b 100644
--- a/src/main/resources/Editors/professions/RecipeEditor.yml
+++ b/src/main/resources/Editors/professions/RecipeEditor.yml
@@ -382,6 +382,47 @@ subEditor:
auraManaAbility: '&3Aura Mana Ability &2$&7: &a$'
auraSkill: '&3Aura Skill &2$&7: &a$'
auraStats: '&3Aura Stat &2$&7: &a$'
+ station:
+ material: ANVIL
+ amount: 1
+ durability: 0
+ unbreakable: false
+ name: '&6Station'
+ lore:
+ - '&7Current: &a$'
+ - '&7The station (Divinity item ID) required'
+ - '&7to craft this recipe.'
+ - '&7Leave empty for no station requirement.'
+ - '&8--------------------'
+ - '&aLeft click &7to set the station ID.'
+ - '&aRight click &7to clear the station.'
+ flags: [ ]
+ enchants: { }
+ fuelCost:
+ material: BLAZE_POWDER
+ amount: 1
+ durability: 0
+ unbreakable: false
+ name: '&6Fuel Cost'
+ lore:
+ - '&7Current: &a$'
+ - '&7The amount of fuel consumed when'
+ - '&7crafting this recipe.'
+ - '&8--------------------'
+ - '&aLeft click &7to increase the fuel cost.'
+ - '&aRight click &7to decrease the fuel cost.'
+ - '&aShift click &7each to modify by 10.'
+ flags: [ ]
+ enchants: { }
+ highlight:
+ material: ORANGE_STAINED_GLASS_PANE
+ amount: 1
+ durability: 0
+ unbreakable: false
+ name: ' '
+ lore: [ ]
+ flags: [ ]
+ enchants: { }
back:
material: BARRIER
amount: 1
diff --git a/src/main/resources/lang/lang_en.yml b/src/main/resources/lang/lang_en.yml
index dcf3716..8428a24 100644
--- a/src/main/resources/lang/lang_en.yml
+++ b/src/main/resources/lang/lang_en.yml
@@ -85,6 +85,8 @@ editor:
recipeRenamed: "&aYou renamed the recipe &3$ &ato &3$"
recipeAdded: "&aYou created the recipe &3$ &awith result &3$"
recipePermissionUpdated: "&aYou updated the recipes permission &ato &3$"
+ recipeStationUpdated: "&aYou updated the recipe's station &ato &3$"
+ recipeFuelCostUpdated: "&aYou updated the recipe's fuel cost &ato &3$"
resultEdited: "&aYou edited the recipe &3$ &awith result &3$"
invalidConditionKey: "&cYou tried to parse an invalid condition key: &3$"
invalidConditionValue: "&cYou tried to parse an invalid condition value: &3$"
diff --git a/src/main/resources/professions/armor_smithing.yml b/src/main/resources/professions/armor_smithing.yml
index f0d8977..a19574f 100644
--- a/src/main/resources/professions/armor_smithing.yml
+++ b/src/main/resources/professions/armor_smithing.yml
@@ -202,7 +202,17 @@ pattern:
# Since here are no patterns, we can leave this empty too in theory
categoryPattern: null
# The categories that are used for the profession.
-# Since we disabled categories, we can leave this empty
+# Since we disabled categories, we can leave this empty.
+# Example of the new category format with optional display name, lore, and slot:
+#categories:
+# - name: light_armor
+# display:
+# name: "&bLight Armor"
+# lore:
+# - "&7Leather and chainmail pieces"
+# icon: LEATHER_CHESTPLATE
+# order: 1
+# # slot: 0 # pins this category to result-slot index 0 (optional)
categories: []
# The recipes that are shown in this profession
@@ -318,3 +328,29 @@ recipes:
enableLore: true
lore:
- 'These are chain boots'
+
+ # ── Divinity item example ──────────────────────────────────────────────────
+ # Requires Divinity plugin. Replace "my_helmet" with an actual item ID from
+ # your Divinity ItemGenerator configuration.
+ #
+ # Ingredient format: DIV_ITEMGEN_:;
+ # DIV_ESSENCE_: | DIV_RUNE_:
+ # Result format: DIV_ITEMGEN_~level::
+ #
+ # - name: DivinityHelmet
+ # craftingTime: 30
+ # results:
+ # vanillaExp: 0
+ # item: DIV_ITEMGEN_my_helmet~level:10:1 # result: 1x my_helmet at level 10
+ # professionExp: 100
+ # commands: []
+ # costs:
+ # money: 500.0
+ # exp: 0
+ # items:
+ # - DIV_ITEMGEN_my_helmet:2;5 # ingredient: 2x my_helmet, minimum level 5
+ # - DIV_ESSENCE_fire:1 # ingredient: 1x fire essence
+ # conditions:
+ # professionLevel: 20
+ # mastery: false
+ # ──────────────────────────────────────────────────────────────────────────
diff --git a/src/main/resources/professions/weapon_smithing.yml b/src/main/resources/professions/weapon_smithing.yml
index 8e171de..4e445fb 100644
--- a/src/main/resources/professions/weapon_smithing.yml
+++ b/src/main/resources/professions/weapon_smithing.yml
@@ -203,14 +203,27 @@ categoryPattern: null
categories:
# Category for wooden weapons with first slot in order
- name: wooden_weapons
+ # Optional display block: use 'display.name' to set a custom name shown in the GUI
+ # (falls back to 'name' if omitted). 'display.lore' adds lines below the icon.
+ display:
+ name: "&6Wooden Weapons"
+ lore:
+ - "&7Basic wooden armaments"
icon: WOODEN_SWORD
order: 1
+ # Optional: 'slot' pins this category to a specific result-slot index (0-based).
+ # Omit or set to -1 for automatic sequential placement.
+ # slot: 0
# Category for stone weapons with second slot in order
- name: stone_weapons
+ display:
+ name: "&7Stone Weapons"
icon: STONE_SWORD
order: 2
# Category for iron weapons with third slot in order
- name: iron_weapons
+ display:
+ name: "&fIron Weapons"
icon: IRON_SWORD
order: 3
# This pattern is null now. You can however structure it 1:1 like the `pattern`-section
@@ -328,3 +341,31 @@ recipes:
professionLevel: 5
mastery: false
+ # ── Divinity item example ──────────────────────────────────────────────────
+ # Requires Divinity plugin. Replace "my_sword" with an actual item ID from
+ # your Divinity ItemGenerator configuration.
+ #
+ # Ingredient format: DIV_ITEMGEN_:;
+ # DIV_GEM_: | DIV_ESSENCE_: | DIV_RUNE_:
+ # Result format: DIV_ITEMGEN_~level::
+ # Optional suffix _NOENCH prevents enchantments on match.
+ #
+ # - name: DivinityBlade
+ # craftingTime: 30
+ # category: iron_weapons
+ # results:
+ # vanillaExp: 0
+ # item: DIV_ITEMGEN_my_sword~level:10:1 # result: 1x my_sword at level 10
+ # professionExp: 100
+ # commands: []
+ # costs:
+ # money: 500.0
+ # exp: 0
+ # items:
+ # - DIV_ITEMGEN_my_sword:2;5 # ingredient: 2x my_sword, minimum level 5
+ # - DIV_GEM_ruby:1 # ingredient: 1x ruby gem
+ # conditions:
+ # professionLevel: 20
+ # mastery: false
+ # ──────────────────────────────────────────────────────────────────────────
+