");
} else if (event.isRightClick()) {
if (recipe.getSettings().getFlags().isEmpty()) {
return;
}
// Remove the last flag from the set
- ItemFlag lastFlag = new ArrayList<>(recipe.getSettings().getFlags()).get(recipe.getSettings().getFlags().size() - 1);
+ ItemFlag lastFlag = new ArrayList<>(recipe.getSettings().getFlags()).get(
+ recipe.getSettings().getFlags().size() - 1);
recipe.getSettings().getFlags().remove(lastFlag);
hasChanges = true;
}
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 d8c0aa7..ff9de7e 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/IngredientFingerprint.java
@@ -8,24 +8,24 @@
/**
* 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)
+ * - Material
+ * - customModelData (if present)
+ * - displayName (if present)
+ * - lore lines (if present)
+ * - all enchantments (if present)
+ * - unbreakable flag
+ * - durability (if Damageable)
*/
public class IngredientFingerprint {
- private final Material type;
- private final int customModelData;
- private final String displayName;
- private final List lore;
+ private final Material type;
+ private final int customModelData;
+ private final String displayName;
+ private final List lore;
private final Map enchantments;
- private final boolean unbreakable;
- private final int durability;
+ private final boolean unbreakable;
+ private final int durability;
public IngredientFingerprint(Material type,
int customModelData,
@@ -43,17 +43,19 @@ public IngredientFingerprint(Material type,
this.durability = durability;
}
- /** Build an IngredientFingerprint by examining a live ItemStack. */
+ /**
+ * Build an IngredientFingerprint by examining a live ItemStack.
+ */
public static IngredientFingerprint of(ItemStack is) {
- Material mat = is.getType();
+ Material mat = is.getType();
ItemMeta meta = is.getItemMeta();
- int cmd = 0;
- String name = "";
- List loreList = Collections.emptyList();
+ int cmd = 0;
+ String name = "";
+ List loreList = Collections.emptyList();
Map enchantsMap = Collections.emptyMap();
- boolean unbreak = false;
- int dmg = 0;
+ boolean unbreak = false;
+ int dmg = 0;
if (meta != null) {
if (meta.hasCustomModelData()) {
@@ -65,7 +67,7 @@ public static IngredientFingerprint of(ItemStack is) {
if (meta.hasLore()) {
loreList = new ArrayList<>(Objects.requireNonNull(meta.getLore()));
}
- Map raw = meta.getEnchants();
+ Map raw = meta.getEnchants();
if (!raw.isEmpty()) {
enchantsMap = new HashMap<>(raw);
}
diff --git a/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java b/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
index d2b0688..1418129 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
@@ -13,15 +13,15 @@
/**
* Helper to build a small MD5 fingerprint of an entire PlayerInventory.
* We incorporate:
- * - Material ordinal
- * - amount
- * - customModelData
- * - displayName
- * - lore lines
- * - enchantments
- * - unbreakable
- * - durability if Damageable
- *
+ * - Material ordinal
+ * - amount
+ * - customModelData
+ * - displayName
+ * - lore lines
+ * - enchantments
+ * - unbreakable
+ * - durability if Damageable
+ *
* If MD5 is not available, we fall back to a simple int‐hash of slot hashCodes.
*/
public class InventoryFingerprint {
diff --git a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeCacheKey.java b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeCacheKey.java
index 154d1b9..49fc7c1 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeCacheKey.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeCacheKey.java
@@ -9,16 +9,16 @@
public class RecipeCacheKey {
private final String recipeId;
private final byte[] inventoryHash;
- private final int playerLevel;
+ private final int playerLevel;
private final double playerMoney;
// TODO add:
/*
- * - Vanilla Exp
- * - Conditions.McMMO Map
- * - Conditions.Fabled Map
- * - Conditions.Aura Map
- * - Conditions.ProfessionLevels Map
+ * - Vanilla Exp
+ * - Conditions.McMMO Map
+ * - Conditions.Fabled Map
+ * - Conditions.Aura Map
+ * - Conditions.ProfessionLevels Map
*/
public RecipeCacheKey(String recipeId, byte[] inventoryHash, int playerLevel, double playerMoney) {
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 2f9435f..ef9204a 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
@@ -29,7 +29,7 @@ public class RecipeGuiEventRouter implements Listener {
* We fetch that player’s FusionPlayer via PlayerLoader.getPlayer(Player).
*/
private RecipeGui findGuiFor(Player player, Inventory inv) {
- if(!ProfessionGuiRegistry.getLatestRecipeGui().containsKey(player.getUniqueId()))
+ if (!ProfessionGuiRegistry.getLatestRecipeGui().containsKey(player.getUniqueId()))
return null;
RecipeGui gui = ProfessionGuiRegistry.getLatestRecipeGui().get(player.getUniqueId());
if (gui.getInventory().equals(inv)) {
@@ -80,7 +80,7 @@ public void onInventoryClose(InventoryCloseEvent event) {
@EventHandler(ignoreCancelled = true)
public void onPlayerDrop(PlayerDropItemEvent event) {
- Player p = event.getPlayer();
+ Player p = event.getPlayer();
RecipeGui gui = findGuiFor(p, p.getOpenInventory().getTopInventory());
if (gui == null) return;
@@ -100,13 +100,13 @@ public void onItemPickup(EntityPickupItemEvent event) {
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
- Player p = event.getPlayer();
+ Player p = event.getPlayer();
FusionPlayer fp = PlayerLoader.getPlayer(p);
if (fp == null) return;
// On quit, close and remove *all* open RecipeGuis for that player
RecipeGui gui = ProfessionGuiRegistry.getLatestRecipeGui().get(p.getUniqueId());
- if(gui == null) return;
+ if (gui == null) return;
gui.close(p, gui.getInventory());
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/gui/slot/Slot.java b/src/main/java/studio/magemonkey/fusion/gui/slot/Slot.java
index e220850..8503e36 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/slot/Slot.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/slot/Slot.java
@@ -93,7 +93,7 @@ public ItemStack canHoldItem(ItemStack item) {
/**
* -- GETTER --
- * Returns base slot type.
+ * Returns base slot type.
*
*/
protected final SlotType slotType;
diff --git a/src/test/java/studio/magemonkey/fusion/commands/ForceCommandsTest.java b/src/test/java/studio/magemonkey/fusion/commands/ForceCommandsTest.java
index 8d586c1..0503766 100644
--- a/src/test/java/studio/magemonkey/fusion/commands/ForceCommandsTest.java
+++ b/src/test/java/studio/magemonkey/fusion/commands/ForceCommandsTest.java
@@ -2,7 +2,8 @@
import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* Simple test to verify force command methods exist and have the expected signatures.
@@ -16,31 +17,31 @@ public void testForceCommandMethodsExist() {
// Verify that all force command methods exist as public static methods
try {
// Check forceJoinProfession method exists
- CommandMechanics.class.getDeclaredMethod("forceJoinProfession",
- org.bukkit.command.CommandSender.class, String[].class);
-
+ CommandMechanics.class.getDeclaredMethod("forceJoinProfession",
+ org.bukkit.command.CommandSender.class, String[].class);
+
// Check forceLeaveProfession method exists
- CommandMechanics.class.getDeclaredMethod("forceLeaveProfession",
- org.bukkit.command.CommandSender.class, String[].class);
-
+ CommandMechanics.class.getDeclaredMethod("forceLeaveProfession",
+ org.bukkit.command.CommandSender.class, String[].class);
+
// Check forceStats method exists
- CommandMechanics.class.getDeclaredMethod("forceStats",
- org.bukkit.command.CommandSender.class, String[].class);
-
+ CommandMechanics.class.getDeclaredMethod("forceStats",
+ org.bukkit.command.CommandSender.class, String[].class);
+
// Check forceMaster method exists
- CommandMechanics.class.getDeclaredMethod("forceMaster",
- org.bukkit.command.CommandSender.class, String[].class);
-
+ CommandMechanics.class.getDeclaredMethod("forceMaster",
+ org.bukkit.command.CommandSender.class, String[].class);
+
// Check forceShow method exists
- CommandMechanics.class.getDeclaredMethod("forceShow",
- org.bukkit.command.CommandSender.class, String[].class);
-
+ CommandMechanics.class.getDeclaredMethod("forceShow",
+ org.bukkit.command.CommandSender.class, String[].class);
+
} catch (NoSuchMethodException e) {
fail("Force command method not found: " + e.getMessage());
}
}
- @Test
+ @Test
public void testCommandArgumentValidation() {
// This test documents expected argument patterns for force commands
// forcejoin = 3 args
@@ -48,7 +49,7 @@ public void testCommandArgumentValidation() {
// forcestats = 2 args
// forcemaster = 3 args
// forceshow = 2 args
-
+
// Test would verify argument length checking but requires mocking the full environment
assertTrue(true, "Force commands require 2-3 arguments as documented");
}
From b418ce3774da9ef0750e0c69c556696fcd8aa20c Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 14 Sep 2025 18:54:02 +0200
Subject: [PATCH 10/34] fixed categories not being openable when registered in
the gui
---
.../java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java | 1 -
.../java/studio/magemonkey/fusion/commands/Commands.java | 3 +--
.../studio/magemonkey/fusion/data/player/FusionPlayer.java | 1 +
.../fusion/data/professions/ProfessionSettings.java | 2 +-
src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java | 5 ++++-
5 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java b/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
index e91be64..d1f70ee 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
@@ -121,7 +121,6 @@ private static void loadProfessions(File root) {
cfgs.put(ct.getName(), cfg);
files.put(ct.getName(), file);
injectProfessionLevelConfig(ct, file);
-
} catch (Exception e) {
e.printStackTrace();
Fusion.getInstance().getLogger().warning("Can't load crafting table: " + e.getMessage());
diff --git a/src/main/java/studio/magemonkey/fusion/commands/Commands.java b/src/main/java/studio/magemonkey/fusion/commands/Commands.java
index 50c5dc1..9af8c83 100644
--- a/src/main/java/studio/magemonkey/fusion/commands/Commands.java
+++ b/src/main/java/studio/magemonkey/fusion/commands/Commands.java
@@ -125,8 +125,7 @@ public List onTabComplete(@NotNull CommandSender sender,
return List.of();
List entries = new ArrayList<>();
- List professions = new ArrayList<>();
- professions = new ArrayList<>(PlayerLoader.getPlayer((player).getUniqueId()).getProfessions());
+ List professions = new ArrayList<>(PlayerLoader.getPlayer((player).getUniqueId()).getProfessions());
if (args.length == 1) {
if (sender.hasPermission("fusion.browse")
&& "browse".startsWith(args[0])) entries.add("browse");
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index bd8b9cd..465418a 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -3,6 +3,7 @@
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
diff --git a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
index dc14c68..7a4cba6 100644
--- a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
+++ b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
@@ -274,7 +274,7 @@ private void generateIcon(String namespace) {
meta = potionMeta;
}
builder = builder.data(meta);
- this.recipeItem = new RecipeCustomItem(builder, iconReference.getAmount(), false);
+ this.recipeItem = new RecipeCustomItem(builder, 1, false);
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java b/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
index 1f1a423..55924e8 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
@@ -36,6 +36,7 @@ public class CategoryGui implements Listener {
private final CraftingTable table;
private Inventory inventory;
+ private final Map allCategoriesMap = new HashMap<>();
private final Map categories = new HashMap<>();
private int page = 0;
private int nextPage = -1;
@@ -103,6 +104,8 @@ public void reloadCategories() {
: CodexEngine.get().getVault().getBalance(player))
});
+ allCategories.forEach((category) -> allCategoriesMap.putIfAbsent(category.getName(), new RecipeGui(player, table, category)));
+
for (int k = (page * pageSize), e = Math.min(slots.length, allCategoryArray.length);
(k < allCategoryArray.length) && (i < e);
k++, i++) {
@@ -255,7 +258,7 @@ private void nextPage() {
public void open(Player player, Category category) {
if (category == null) open(player);
else {
- for (RecipeGui gui : categories.values()) {
+ for (RecipeGui gui : allCategoriesMap.values()) {
if (gui.getCategory().equals(category)) {
gui.open(player);
return;
From d18c7967d261b7c01b72f9a2ac31927944c03034 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Tue, 16 Sep 2025 22:38:57 +0200
Subject: [PATCH 11/34] remove latest gui on leave
---
.../magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java | 1 +
1 file changed, 1 insertion(+)
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 ef9204a..c5de114 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
@@ -108,5 +108,6 @@ public void onPlayerQuit(PlayerQuitEvent event) {
RecipeGui gui = ProfessionGuiRegistry.getLatestRecipeGui().get(p.getUniqueId());
if (gui == null) return;
gui.close(p, gui.getInventory());
+ ProfessionGuiRegistry.getLatestRecipeGui().remove(p.getUniqueId());
}
}
From 02e07a61d946b865faefd21e534da9b72f718771 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Tue, 16 Sep 2025 22:39:28 +0200
Subject: [PATCH 12/34] caching money to reduce lags through db-connections
---
.../magemonkey/fusion/gui/RecipeGui.java | 6 +-
.../magemonkey/fusion/hook/VaultHook.java | 67 +++++++++++++++++++
2 files changed, 69 insertions(+), 4 deletions(-)
create mode 100644 src/main/java/studio/magemonkey/fusion/hook/VaultHook.java
diff --git a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
index 02dfcda..81d107c 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
@@ -47,12 +47,12 @@
import studio.magemonkey.fusion.gui.recipe.InventoryFingerprint;
import studio.magemonkey.fusion.gui.recipe.RecipeCacheKey;
import studio.magemonkey.fusion.gui.slot.Slot;
+import studio.magemonkey.fusion.hook.VaultHook;
import studio.magemonkey.fusion.util.ChatUT;
import studio.magemonkey.fusion.util.ExperienceManager;
import studio.magemonkey.fusion.util.PlayerUtil;
import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
@Getter
public class RecipeGui implements Listener {
@@ -264,9 +264,7 @@ public void reloadRecipes() {
//
byte[] newHash = InventoryFingerprint.fingerprint(player);
int newLevel = table.getLevelFunction().getLevel(player);
- double newMoney = (CodexEngine.get().getVault() == null)
- ? 0.0
- : CodexEngine.get().getVault().getBalance(player);
+ double newMoney = VaultHook.getBalance(player);
boolean invChanged = !Arrays.equals(newHash, lastInventoryHash);
boolean levelChanged = (newLevel != lastSeenLevel);
diff --git a/src/main/java/studio/magemonkey/fusion/hook/VaultHook.java b/src/main/java/studio/magemonkey/fusion/hook/VaultHook.java
new file mode 100644
index 0000000..029c31c
--- /dev/null
+++ b/src/main/java/studio/magemonkey/fusion/hook/VaultHook.java
@@ -0,0 +1,67 @@
+package studio.magemonkey.fusion.hook;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+import studio.magemonkey.codex.CodexEngine;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class VaultHook {
+
+ private static BukkitTask task;
+ private static Map> storedBalances = new HashMap<>();
+ private static int thresholdSeconds = 10;
+
+ public static double getBalance(Player player) {
+ if (storedBalances.containsKey(player.getUniqueId())) {
+ Pair balanceData = storedBalances.get(player.getUniqueId());
+ double balance = balanceData.getLeft();
+ LocalDateTime timestamp = balanceData.getRight();
+ if (timestamp.plusSeconds(thresholdSeconds).isBefore(LocalDateTime.now())) {
+ double updatedBalance = CodexEngine.get().getVault() != null ? CodexEngine.get().getVault().getBalance(player) : 0.0;
+ storedBalances.put(player.getUniqueId(), Pair.of(updatedBalance, LocalDateTime.now()));
+ return updatedBalance;
+ } else {
+ return balance;
+ }
+ } else {
+ double balance = CodexEngine.get().getVault() != null ? CodexEngine.get().getVault().getBalance(player) : 0.0;
+ storedBalances.put(player.getUniqueId(), Pair.of(balance, LocalDateTime.now()));
+ return balance;
+ }
+ }
+
+ public static void startMoneyUpdateTask() {
+ // Schedule a repeating task to update all stored balances every thresholdSeconds
+ task = Bukkit.getScheduler().runTaskTimerAsynchronously(CodexEngine.get(), () -> {
+ for (UUID uuid : storedBalances.keySet()) {
+ Player player = Bukkit.getPlayer(uuid);
+ if (player != null && player.isOnline()) {
+ getBalanceAsync(player, balance -> {
+ storedBalances.put(uuid, Pair.of(balance, LocalDateTime.now()));
+ });
+ }
+ }
+ }, thresholdSeconds * 20L, thresholdSeconds * 20L);
+ }
+
+ public static void cancelMoneyUpdateTask() {
+ if (task != null) {
+ task.cancel();
+ task = null;
+ }
+ }
+
+ private static void getBalanceAsync(Player player, Consumer moneyConsumer) {
+ Bukkit.getScheduler().runTaskAsynchronously(CodexEngine.get(), () -> {
+ double balance = CodexEngine.get().getVault() != null ? CodexEngine.get().getVault().getBalance(player) : 0.0;
+ moneyConsumer.accept(balance);
+ });
+ }
+}
From b9c341ce9e2b324948f4a7f3b5da8ea8365ae30d Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 17 Sep 2025 11:20:43 +0200
Subject: [PATCH 13/34] added debug for issue handling with queue data loss on
rejoining
---
.../studio/magemonkey/fusion/data/queue/CraftingQueue.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
index a7d1c53..68117a3 100644
--- a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
+++ b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
@@ -12,6 +12,7 @@
import studio.magemonkey.fusion.cfg.Cfg;
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
+import studio.magemonkey.fusion.data.player.FusionPlayer;
import studio.magemonkey.fusion.data.player.PlayerLoader;
import studio.magemonkey.fusion.data.professions.pattern.Category;
import studio.magemonkey.fusion.data.recipes.Recipe;
@@ -50,6 +51,10 @@ public CraftingQueue(Player player, String profession, Category category) {
* queue sequentially. All items are saved with the same timestamp when
* saved, so use the first item's timestamp to calculate the offline duration.
*/
+ if(!queue.isEmpty()) {
+ Fusion.getInstance().getLogger().warning("[Debug] Loaded " + queue.size() + " items for " + player.getName() + " in " + profession + " - " + category.getName() + ": ItemPaths:" + queue.stream().map(item -> item.getRecipe().getRecipePath()).toList());
+ }
+
if (Cfg.updateQueueOffline && !queue.isEmpty()) {
long now = System.currentTimeMillis();
// find the first unfinished item
From 9aadc3e2814c8ba9981a513d39b8a94f107be620 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 17 Sep 2025 23:38:07 +0200
Subject: [PATCH 14/34] fixed queue data loss with properly configured primary
keys
---
.../java/studio/magemonkey/fusion/Fusion.java | 4 +--
.../cfg/sql/tables/FusionQueuesSQL.java | 30 +++++--------------
2 files changed, 9 insertions(+), 25 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/Fusion.java b/src/main/java/studio/magemonkey/fusion/Fusion.java
index 69b0198..59eb78d 100644
--- a/src/main/java/studio/magemonkey/fusion/Fusion.java
+++ b/src/main/java/studio/magemonkey/fusion/Fusion.java
@@ -127,8 +127,8 @@ public void onEnable() {
LevelFunction.generate(200);
this.getCommand("craft").setExecutor(new Commands());
this.getCommand("fusion-editor").setExecutor(new FusionEditorCommand());
- getServer().getPluginManager().registerEvents(this, this);
- Bukkit.getPluginManager().registerEvents(new RecipeGuiEventRouter(), this);
+ registerListener(this);
+ registerListener(new RecipeGuiEventRouter());
runQueueTask();
if (hookManager.isHooked(HookType.PlaceholderAPI)) {
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
index 9af7774..8c2050b 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
@@ -22,7 +22,7 @@ public class FusionQueuesSQL {
public FusionQueuesSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id long,"
+ + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
+ "UUID varchar(36), "
+ "RecipePath varchar(100),"
+ "CraftingTime numeric,"
@@ -37,33 +37,17 @@ public FusionQueuesSQL() {
}
}
- public long getNextId() {
- try (PreparedStatement select = SQLManager.connection().prepareStatement("SELECT Count(Id) FROM " + Table)) {
- ResultSet result = select.executeQuery();
- if (result.next()) {
- return result.getLong(1);
- }
- } catch (SQLException e) {
- Fusion.getInstance()
- .getLogger()
- .warning("[SQL:FusionQueuesSQL:getNextId] Something went wrong with the sql-connection: "
- + e.getMessage());
- }
- return 0;
- }
-
public boolean setQueueItem(UUID uuid, QueueItem item) {
if (item == null) return false;
if (item.getId() == -1) {
try (PreparedStatement insert = SQLManager.connection()
.prepareStatement("INSERT INTO " + Table
- + "(Id, UUID, RecipePath, Timestamp, CraftingTime, SavedSeconds) VALUES (?,?,?,?,?,?)")) {
- insert.setLong(1, getNextId());
- insert.setString(2, uuid.toString());
- insert.setString(3, item.getRecipePath());
- insert.setLong(4, item.getTimestamp());
- insert.setLong(5, item.getRecipe().getCraftingTime());
- insert.setLong(6, item.getSavedSeconds());
+ + "(UUID, RecipePath, Timestamp, CraftingTime, SavedSeconds) VALUES (?,?,?,?,?)")) {
+ insert.setString(1, uuid.toString());
+ insert.setString(2, item.getRecipePath());
+ insert.setLong(3, item.getTimestamp());
+ insert.setLong(4, item.getRecipe().getCraftingTime());
+ insert.setLong(5, item.getSavedSeconds());
insert.execute();
return true;
} catch (SQLException e) {
From 362ff2a5074532c3d477b8c8276a565a564383c0 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 17 Sep 2025 23:38:32 +0200
Subject: [PATCH 15/34] removed debug
---
.../studio/magemonkey/fusion/data/queue/CraftingQueue.java | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
index 68117a3..def10b6 100644
--- a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
+++ b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
@@ -51,10 +51,6 @@ public CraftingQueue(Player player, String profession, Category category) {
* queue sequentially. All items are saved with the same timestamp when
* saved, so use the first item's timestamp to calculate the offline duration.
*/
- if(!queue.isEmpty()) {
- Fusion.getInstance().getLogger().warning("[Debug] Loaded " + queue.size() + " items for " + player.getName() + " in " + profession + " - " + category.getName() + ": ItemPaths:" + queue.stream().map(item -> item.getRecipe().getRecipePath()).toList());
- }
-
if (Cfg.updateQueueOffline && !queue.isEmpty()) {
long now = System.currentTimeMillis();
// find the first unfinished item
From 2f6666a9c071c0fdcb88c6766f38272133e53b9e Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Thu, 18 Sep 2025 23:04:16 +0200
Subject: [PATCH 16/34] fixed skull meta on queued items
---
.../magemonkey/fusion/cfg/ProfessionsCfg.java | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java b/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
index d1f70ee..ec69b80 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/ProfessionsCfg.java
@@ -1,13 +1,16 @@
package studio.magemonkey.fusion.cfg;
import lombok.Getter;
+import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import studio.magemonkey.codex.legacy.item.ItemBuilder;
+import studio.magemonkey.codex.legacy.item.SkullBuilder;
import studio.magemonkey.fusion.Fusion;
import studio.magemonkey.fusion.cfg.migrations.ProfessionMigration;
import studio.magemonkey.fusion.data.professions.pattern.Category;
@@ -15,6 +18,7 @@
import studio.magemonkey.fusion.data.recipes.CraftingTable;
import studio.magemonkey.fusion.gui.ProfessionGuiRegistry;
import studio.magemonkey.fusion.hook.NexoHook;
+import studio.magemonkey.fusion.util.ChatUT;
import studio.magemonkey.fusion.util.Utils;
import java.io.File;
@@ -464,10 +468,17 @@ public static ItemStack getQueueItem(String key, QueueItem item) {
.warning("Profession '" + key + "' has an unknown material: " + materialString);
return new ItemStack(Material.AIR);
}
+ if(material != result.getType()) result.setType(material);
+ ItemMeta meta = result.getItemMeta();
+ if(meta != null) {
+ List lore = cfg.getStringList(path + ".lore");
+ lore.replaceAll(s -> ChatUT.hexString(s.replace("%time%", Utils.getFormattedTime(item.getVisualRemainingItemTime()))));
+ meta.setLore(lore);
+ result.setItemMeta(meta);
+ }
+
- List lore = cfg.getStringList(path + ".lore");
- lore.replaceAll(s -> s.replace("%time%", Utils.getFormattedTime(item.getVisualRemainingItemTime())));
- return ItemBuilder.newItem(result).material(material).lore(lore).build();
+ return result;
}
public static void closeAll() {
From a9a4900bf8aaf964f7eca1153ceb6ab0b70b2843 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 17 Aug 2025 09:55:31 +0200
Subject: [PATCH 17/34] added * for itemflag hiding in settings item
---
pom.xml | 2 +-
.../data/professions/ProfessionSettings.java | 16 ++++++++++++----
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/pom.xml b/pom.xml
index acc692b..d24977c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
16
- 1.1.1-R0.3-SNAPSHOT
+ 1.1.1-R0.7-SNAPSHOT
diff --git a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
index d190baa..dc14c68 100644
--- a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
+++ b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
@@ -92,8 +92,12 @@ public ProfessionSettings(String profession, ConfigurationSection config) {
List flagsList = config.getStringList("settings.icon.optionals.flags");
if(!flagsList.isEmpty()) {
flags = new HashSet<>();
- for (String flag : flagsList) {
- flags.add(ItemFlag.valueOf(flag));
+ if(flagsList.contains("*")) {
+ flags.addAll(Arrays.asList(ItemFlag.values()));
+ } else {
+ for (String flag : flagsList) {
+ flags.add(ItemFlag.valueOf(flag));
+ }
}
}
color = config.getString("settings.icon.optionals.color");
@@ -147,8 +151,12 @@ public ProfessionSettings(String profession, DeserializationWorker dw) {
if (optionalIconSettings.get("flags") != null) {
flags = new HashSet<>();
List flagsList = (List) optionalIconSettings.get("flags");
- for (String flag : flagsList) {
- flags.add(ItemFlag.valueOf(flag));
+ if(flagsList.contains("*")) {
+ flags.addAll(Arrays.asList(ItemFlag.values()));
+ } else {
+ for (String flag : flagsList) {
+ flags.add(ItemFlag.valueOf(flag));
+ }
}
}
if (optionalIconSettings.get("color") != null)
From 411fec38182de8c68ccd3aef3eff0d40d7278d90 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 24 Sep 2025 21:57:52 +0200
Subject: [PATCH 18/34] fixed recipe gui reacting on player inventory instead
of top inv
---
src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java b/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
index 55924e8..1649e0b 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/CategoryGui.java
@@ -325,7 +325,7 @@ public void click(InventoryClickEvent event) {
@EventHandler
public void onClick(InventoryClickEvent event) {
- if (event.getInventory() != getInventory()) return;
+ if (event.getClickedInventory() != getInventory()) return;
event.setCancelled(true);
event.setResult(Event.Result.DENY);
From b51de93dee3045a21f9f3e71dbb5c38c09fb28cd Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sat, 27 Sep 2025 22:05:00 +0200
Subject: [PATCH 19/34] improved queue updating for performance
---
.../magemonkey/fusion/gui/RecipeGui.java | 53 ++++++++++++++++++-
1 file changed, 52 insertions(+), 1 deletion(-)
diff --git a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
index 81d107c..a7f32bb 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
@@ -474,7 +474,7 @@ public void reloadRecipes() {
}
}
if (requiresUpdate) {
- Bukkit.getScheduler().runTaskLater(Fusion.getInstance(), this::reloadRecipes, 20L);
+ Bukkit.getScheduler().runTaskLater(Fusion.getInstance(), this::updateQueuedSlots, 20L);
}
this.isLoaded = true;
}
@@ -485,6 +485,57 @@ public void reloadRecipesTask() {
Bukkit.getScheduler().runTaskLater(Fusion.getInstance(), this::reloadRecipes, 1L);
}
+ // Updates only the queued-slot icons/progress without rebuilding the whole GUI.
+ private void updateQueuedSlots() {
+ if (!player.isOnline()) return;
+ if (!Cfg.craftingQueue || queue == null || queuedSlots.isEmpty()) return;
+
+ // Run the actual inventory updates on the main server thread
+ Bukkit.getScheduler().runTask(Fusion.getInstance(), () -> {
+ List allQueuedItems = new ArrayList<>(queue.getQueue());
+ int queueSize = allQueuedItems.size();
+ int queuePageSize = queuedSlots.size();
+
+ Integer[] queuedIndices = queuedSlots.toArray(new Integer[0]);
+
+ // Reset all queued slots to the empty queue icon
+ for (int qIndex : queuedIndices) {
+ inventory.setItem(qIndex, ProfessionsCfg.getQueueSlot(table.getName()));
+ }
+
+ // Clear the internal mapping and repopulate for current page only
+ this.queue.getQueuedItems().clear();
+
+ if (!allQueuedItems.isEmpty() && queuePageSize > 0) {
+ int j = 0;
+ int qStart = queuePage * queuePageSize;
+ int qEnd = Math.min(qStart + queuePageSize, queueSize);
+ QueueItem[] allQueueItemsArray = allQueuedItems.toArray(new QueueItem[0]);
+
+ for (int q = qStart; q < qEnd && j < queuedIndices.length; q++, j++) {
+ QueueItem qi = allQueueItemsArray[q];
+ int slot = queuedIndices[j];
+ this.queue.getQueuedItems().put(slot, qi);
+ qi.updateIcon();
+ inventory.setItem(slot, qi.getIcon().clone());
+ }
+ }
+
+ // Decide whether we need another update next second (any unfinished item)
+ boolean requiresUpdate = false;
+ for (QueueItem qi : allQueuedItems) {
+ if (!qi.isDone()) {
+ requiresUpdate = true;
+ break;
+ }
+ }
+
+ if (requiresUpdate) {
+ Bukkit.getScheduler().runTaskLater(Fusion.getInstance(), this::updateQueuedSlots, 20L);
+ }
+ });
+ }
+
private boolean validatePageCount() {
if (this.page <= 0) {
this.reloadRecipesTask();
From 7e238132805d9d9a836e3c914cb5998ed3f7c9a6 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Tue, 7 Oct 2025 22:35:57 +0200
Subject: [PATCH 20/34] sync fixes and inv checks
---
.../studio/magemonkey/fusion/gui/RecipeGui.java | 5 +++--
.../gui/editors/pattern/PatternItemEditor.java | 2 +-
.../professions/recipes/RecipeItemEditor.java | 2 +-
.../fusion/gui/recipe/RecipeGuiEventRouter.java | 14 +++++++++++---
4 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
index a7f32bb..8a53608 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
@@ -864,7 +864,7 @@ private boolean craft(int slot, boolean addToCursor) {
// Restart the crafting sequence if auto-crafting is enabled
if (PlayerLoader.getPlayer(player).isAutoCrafting() && !this.recipes.isEmpty()) {
reloadRecipesTask();
- boolean success = craft(slot, addToCursor); //Call this method again recursively
+ boolean success = craft(slot, addToCursor); // Call this method again recursively
if (!success)
CodexEngine.get().getMessageUtil().sendMessage("fusion.autoCancelled", player);
}
@@ -1056,7 +1056,7 @@ public void close(Player p, Inventory inv) {
return;
}
Inventory pInventory = p.getInventory();
- if (inv.equals(this.inventory)) {
+ if (inv.equals(this.inventory) && !Cfg.craftingQueue) {
for (int i = 0; i < this.slots.length; i++) {
if (this.slots[i].equals(Slot.BLOCKED_SLOT) ||
this.slots[i].equals(Slot.BASE_RESULT_SLOT) ||
@@ -1075,6 +1075,7 @@ public void close(Player p, Inventory inv) {
cancel(true);
inv.clear();
}
+ ProfessionGuiRegistry.getLatestRecipeGui().remove(p.getUniqueId());
}
/*
diff --git a/src/main/java/studio/magemonkey/fusion/gui/editors/pattern/PatternItemEditor.java b/src/main/java/studio/magemonkey/fusion/gui/editors/pattern/PatternItemEditor.java
index 3ae835c..c372d10 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/editors/pattern/PatternItemEditor.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/editors/pattern/PatternItemEditor.java
@@ -91,7 +91,7 @@ private void initialize() {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
- if (event.getInventory() != getInventory()) return;
+ if (event.getClickedInventory() != getInventory()) return;
event.setCancelled(true);
Player player = (Player) event.getWhoClicked();
boolean hasChanges = false;
diff --git a/src/main/java/studio/magemonkey/fusion/gui/editors/professions/recipes/RecipeItemEditor.java b/src/main/java/studio/magemonkey/fusion/gui/editors/professions/recipes/RecipeItemEditor.java
index ab52500..7731441 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/editors/professions/recipes/RecipeItemEditor.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/editors/professions/recipes/RecipeItemEditor.java
@@ -63,7 +63,7 @@ public void initialize() {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
- if (event.getInventory() != getInventory()) return;
+ if (event.getClickedInventory() != getInventory()) return;
event.setCancelled(true);
Player player = (Player) event.getWhoClicked();
boolean hasChanges = false;
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 c5de114..11baae1 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/RecipeGuiEventRouter.java
@@ -8,6 +8,7 @@
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
@@ -41,9 +42,9 @@ private RecipeGui findGuiFor(Player player, Inventory inv) {
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player p)) return;
- Inventory inv = event.getInventory();
+ Inventory inv = event.getClickedInventory();
RecipeGui gui = findGuiFor(p, inv);
- if (gui == null) return;
+ if (gui == null || inv == null) return;
// Only forward if the clicked inventory is *exactly* the GUI’s inventory
if (!inv.equals(gui.getInventory())) return;
@@ -108,6 +109,13 @@ public void onPlayerQuit(PlayerQuitEvent event) {
RecipeGui gui = ProfessionGuiRegistry.getLatestRecipeGui().get(p.getUniqueId());
if (gui == null) return;
gui.close(p, gui.getInventory());
- ProfessionGuiRegistry.getLatestRecipeGui().remove(p.getUniqueId());
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
+ Player p = event.getPlayer();
+ RecipeGui gui = ProfessionGuiRegistry.getLatestRecipeGui().get(p.getUniqueId());
+ if (gui == null) return;
+ gui.close(p, gui.getInventory());
}
}
From dbdd99216462221d3a1af14a62610cf5ea627d5e Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 8 Oct 2025 17:45:35 +0200
Subject: [PATCH 21/34] implemented lock mechanism to prevent the plugin to
load crafting queues before the other server node got saved async
---
pom.xml | 1 +
.../cfg/sql/tables/FusionPlayersSQL.java | 64 ++++++++++++++++++-
.../fusion/data/player/FusionPlayer.java | 28 +++++---
.../fusion/data/player/PlayerLoader.java | 2 +
4 files changed, 84 insertions(+), 11 deletions(-)
diff --git a/pom.xml b/pom.xml
index d24977c..e128aee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
16
1.1.1-R0.7-SNAPSHOT
+ 1.0.4-R0.63-SNAPSHOT
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
index d1edc64..32ad38e 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
@@ -17,8 +17,17 @@ public FusionPlayersSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
+ "UUID varchar(36), "
- + "AutoCrafting boolean)")) {
+ + "AutoCrafting boolean, "
+ + "Locked boolean)")) {
create.execute();
+
+ boolean lockedColumnAdded = alterIfLockedNotExistent();
+ if (lockedColumnAdded) {
+ Fusion.getInstance()
+ .getLogger()
+ .info("[SQL:FusionPlayersSQL:FusionPlayersSQL] Added 'Locked' column to 'fusion_players' table.");
+ }
+
} catch (SQLException e) {
Fusion.getInstance()
.getLogger()
@@ -42,6 +51,37 @@ public void setAutoCrafting(UUID uuid, boolean autoCrafting) {
}
}
+ public void setLocked(UUID uuid, boolean locked) {
+ addPlayer(uuid);
+ try (PreparedStatement update = SQLManager.connection()
+ .prepareStatement("UPDATE " + Table + " SET Locked=? WHERE UUID=?")) {
+ update.setBoolean(1, locked);
+ update.setString(2, uuid.toString());
+ update.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance()
+ .getLogger()
+ .warning("[SQL:FusionPlayersSQL:setLocked] Something went wrong with the sql-connection: "
+ + e.getMessage());
+ }
+ }
+
+ public boolean isLocked(UUID uuid) {
+ try (PreparedStatement select = SQLManager.connection()
+ .prepareStatement("SELECT Locked FROM " + Table + " WHERE UUID=?")) {
+ select.setString(1, uuid.toString());
+ ResultSet result = select.executeQuery();
+ if (result.next())
+ return result.getBoolean("Locked");
+ } catch (SQLException e) {
+ Fusion.getInstance()
+ .getLogger()
+ .warning("[SQL:FusionPlayersSQL:isLocked] Something went wrong with the sql-connection: "
+ + e.getMessage());
+ }
+ return false;
+ }
+
public void addPlayer(UUID uuid) {
if (hasPlayer(uuid))
return;
@@ -89,4 +129,26 @@ public boolean isAutoCrafting(UUID uuid) {
}
return false;
}
+
+ public boolean alterIfLockedNotExistent() {
+ try (PreparedStatement select = SQLManager.connection()
+ .prepareStatement("SELECT Locked FROM " + Table + " LIMIT 1")) {
+ ResultSet result = select.executeQuery();
+ if (result.next())
+ return false;
+ } catch (SQLException e) {
+ // Column does not exist, we need to add it
+ try (PreparedStatement alter = SQLManager.connection()
+ .prepareStatement("ALTER TABLE " + Table + " ADD COLUMN Locked boolean DEFAULT false")) {
+ alter.execute();
+ return true;
+ } catch (SQLException ex) {
+ Fusion.getInstance()
+ .getLogger()
+ .warning("[SQL:FusionPlayersSQL:alterIfLockedNotExistent] Something went wrong with the sql-connection: "
+ + ex.getMessage());
+ }
+ }
+ return false;
+ }
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index 465418a..a3ccaea 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -6,6 +6,7 @@
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
+import studio.magemonkey.fusion.Fusion;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
import studio.magemonkey.fusion.data.professions.Profession;
import studio.magemonkey.fusion.data.professions.pattern.Category;
@@ -18,6 +19,7 @@
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
@@ -344,15 +346,21 @@ public int getFinishedSize() {
}
public void save() {
- SQLManager.players().setAutoCrafting(uuid, autoCrafting);
- for (Profession profession : professions.values()) {
- SQLManager.professions().setProfession(uuid, profession);
- }
- for (CraftingQueue queue : cachedQueues.values()) {
- SQLManager.queues().saveCraftingQueue(queue);
- }
- SQLManager.recipeLimits().saveRecipeLimits(uuid, cachedRecipeLimits);
- cachedQueues.clear();
- cachedRecipeLimits.clear();
+ SQLManager.players().setLocked(uuid, true);
+
+ Bukkit.getScheduler().runTaskAsynchronously(Fusion.getInstance(), () -> {
+ SQLManager.players().setAutoCrafting(uuid, autoCrafting);
+ for (Profession profession : professions.values()) {
+ SQLManager.professions().setProfession(uuid, profession);
+ }
+ for (CraftingQueue queue : cachedQueues.values()) {
+ SQLManager.queues().saveCraftingQueue(queue);
+ }
+ SQLManager.recipeLimits().saveRecipeLimits(uuid, cachedRecipeLimits);
+ cachedQueues.clear();
+ cachedRecipeLimits.clear();
+
+ SQLManager.players().setLocked(uuid, false);
+ });
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
index 44422af..d6d96a1 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
@@ -1,6 +1,7 @@
package studio.magemonkey.fusion.data.player;
import org.bukkit.entity.Player;
+import studio.magemonkey.fusion.cfg.sql.SQLManager;
import java.util.Map;
import java.util.TreeMap;
@@ -12,6 +13,7 @@ public class PlayerLoader {
public static FusionPlayer getPlayer(UUID uuid) {
if (!cachedPlayers.containsKey(uuid)) {
+ if(!SQLManager.players().isLocked(uuid)) return null;
cachedPlayers.put(uuid, new FusionPlayer(uuid));
}
return cachedPlayers.get(uuid);
From 86f481a9a555e2cda1860679ac74118cefe0e084 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:06:56 +0200
Subject: [PATCH 22/34] changing some mistakes
---
.../fusion/cfg/sql/tables/FusionPlayersSQL.java | 3 ++-
.../magemonkey/fusion/data/player/FusionPlayer.java | 8 ++++++++
.../magemonkey/fusion/data/player/PlayerLoader.java | 2 +-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
index 32ad38e..2400de1 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
@@ -86,9 +86,10 @@ public void addPlayer(UUID uuid) {
if (hasPlayer(uuid))
return;
try (PreparedStatement insert = SQLManager.connection()
- .prepareStatement("INSERT INTO " + Table + "(UUID, AutoCrafting) VALUES(?,?)")) {
+ .prepareStatement("INSERT INTO " + Table + "(UUID, AutoCrafting, Locked) VALUES(?,?,?)")) {
insert.setString(1, uuid.toString());
insert.setBoolean(2, false);
+ insert.setBoolean(3, false);
insert.execute();
} catch (SQLException e) {
Fusion.getInstance()
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index a3ccaea..21921eb 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -360,6 +360,14 @@ public void save() {
cachedQueues.clear();
cachedRecipeLimits.clear();
+ /*
+ In case of race conditions we wait a bit before unlocking the player. Not required but just to be safe.
+ try {
+ Thread.sleep(250);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ */
SQLManager.players().setLocked(uuid, false);
});
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
index d6d96a1..a3d9d98 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
@@ -13,7 +13,7 @@ public class PlayerLoader {
public static FusionPlayer getPlayer(UUID uuid) {
if (!cachedPlayers.containsKey(uuid)) {
- if(!SQLManager.players().isLocked(uuid)) return null;
+ if(SQLManager.players().isLocked(uuid)) return null;
cachedPlayers.put(uuid, new FusionPlayer(uuid));
}
return cachedPlayers.get(uuid);
From 0f354969b287d40ec0a3040bfd216974c54941eb Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 12 Oct 2025 18:08:16 +0200
Subject: [PATCH 23/34] fixed some issues related to limits
---
.../cfg/sql/tables/FusionRecipeLimitsSQL.java | 93 +++++++++++++------
.../fusion/data/queue/CraftingQueue.java | 29 ------
2 files changed, 67 insertions(+), 55 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
index 584720c..62388ee 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
@@ -71,35 +71,76 @@ public Map getRecipeLimits(UUID uuid) {
}
public void saveRecipeLimits(UUID uuid, Map recipeLimits) {
- try (PreparedStatement delete = SQLManager.connection()
- .prepareStatement("DELETE FROM " + Table + " WHERE UUID = ?")) {
- delete.setString(1, uuid.toString());
- delete.execute();
- } catch (SQLException e) {
- Fusion.getInstance()
- .getLogger()
- .warning(
- "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Something went wrong with the sql-connection: "
- + e.getMessage());
+ // Get all current limits from DB
+ Map currentLimits = getRecipeLimits(uuid);
+
+ // Remove limits that are no longer present in the provided map
+ for (String recipePath : currentLimits.keySet()) {
+ if (!recipeLimits.containsKey(recipePath)) {
+ PlayerRecipeLimit limit = currentLimits.get(recipePath);
+ // Limits mit Timestamp = -1 nicht löschen, außer explizit entfernt
+ if (limit.getCooldownTimestamp() != -1) {
+ try (PreparedStatement delete = SQLManager.connection().prepareStatement(
+ "DELETE FROM " + Table + " WHERE UUID = ? AND RecipePath = ?")) {
+ delete.setString(1, uuid.toString());
+ delete.setString(2, recipePath);
+ delete.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance().getLogger().warning(
+ "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Error at deletion: " + e.getMessage());
+ }
+ }
+ }
}
- try (PreparedStatement insert = SQLManager.connection()
- .prepareStatement(
+
+ // Update or Insert limits
+ for (Map.Entry entry : recipeLimits.entrySet()) {
+ String recipePath = entry.getKey();
+ PlayerRecipeLimit limit = entry.getValue();
+ if (limit.getLimit() <= 0) {
+ // Falls Limit <= 0, löschen (außer Timestamp = -1)
+ if (limit.getCooldownTimestamp() != -1) {
+ try (PreparedStatement delete = SQLManager.connection().prepareStatement(
+ "DELETE FROM " + Table + " WHERE UUID = ? AND RecipePath = ?")) {
+ delete.setString(1, uuid.toString());
+ delete.setString(2, recipePath);
+ delete.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance().getLogger().warning(
+ "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Error at deletion: " + e.getMessage());
+ }
+ }
+ continue;
+ }
+ // Check if the limit already exists
+ if (currentLimits.containsKey(recipePath)) {
+ // Update
+ try (PreparedStatement update = SQLManager.connection().prepareStatement(
+ "UPDATE " + Table + " SET Amount = ?, Timestamp = ? WHERE UUID = ? AND RecipePath = ?")) {
+ update.setInt(1, limit.getLimit());
+ update.setLong(2, limit.getCooldownTimestamp());
+ update.setString(3, uuid.toString());
+ update.setString(4, recipePath);
+ update.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance().getLogger().warning(
+ "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Error at update: " + e.getMessage());
+ }
+ } else {
+ // Insert
+ try (PreparedStatement insert = SQLManager.connection().prepareStatement(
"INSERT INTO " + Table + "(Id, UUID, RecipePath, Amount, Timestamp) VALUES(?,?,?,?,?)")) {
- for (Map.Entry entry : recipeLimits.entrySet()) {
- if (entry.getValue().getLimit() <= 0) continue;
- insert.setLong(1, getNextId());
- insert.setString(2, uuid.toString());
- insert.setString(3, entry.getKey());
- insert.setInt(4, entry.getValue().getLimit());
- insert.setLong(5, entry.getValue().getCooldownTimestamp());
- insert.execute();
+ insert.setLong(1, getNextId());
+ insert.setString(2, uuid.toString());
+ insert.setString(3, recipePath);
+ insert.setInt(4, limit.getLimit());
+ insert.setLong(5, limit.getCooldownTimestamp());
+ insert.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance().getLogger().warning(
+ "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Error at insert: " + e.getMessage());
+ }
}
- } catch (SQLException e) {
- Fusion.getInstance()
- .getLogger()
- .warning(
- "[SQL:FusionRecipeLimitsSQL:saveRecipeLimits] Something went wrong with the sql-connection: "
- + e.getMessage());
}
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
index def10b6..79e8132 100644
--- a/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
+++ b/src/main/java/studio/magemonkey/fusion/data/queue/CraftingQueue.java
@@ -12,12 +12,9 @@
import studio.magemonkey.fusion.cfg.Cfg;
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
-import studio.magemonkey.fusion.data.player.FusionPlayer;
-import studio.magemonkey.fusion.data.player.PlayerLoader;
import studio.magemonkey.fusion.data.professions.pattern.Category;
import studio.magemonkey.fusion.data.recipes.Recipe;
import studio.magemonkey.fusion.data.recipes.RecipeItem;
-import studio.magemonkey.fusion.util.PlayerUtil;
import java.util.ArrayList;
import java.util.HashMap;
@@ -104,32 +101,6 @@ public void run() {
}
public void addRecipe(Recipe recipe) {
- int[] limits = PlayerLoader.getPlayer(player.getUniqueId()).getQueueSizes(profession, category);
- int categoryLimit =
- PlayerUtil.getPermOption(player, "fusion.queue." + profession + "." + category.getName() + ".limit");
- int professionLimit = PlayerUtil.getPermOption(player, "fusion.queue." + profession + ".limit");
- int limit = PlayerUtil.getPermOption(player, "fusion.queue.limit");
-
- if (categoryLimit > 0 && limits[0] >= categoryLimit) {
- CodexEngine.get().getMessageUtil().sendMessage("fusion.queue.fullCategory",
- player,
- new MessageData("limit", categoryLimit),
- new MessageData("category", category.getName()),
- new MessageData("profession", profession));
- return;
- } else if (professionLimit > 0 && limits[1] >= professionLimit) {
- CodexEngine.get().getMessageUtil().sendMessage("fusion.queue.fullProfession",
- player,
- new MessageData("limit", professionLimit),
- new MessageData("profession", profession));
- return;
- } else if (limit > 0 && limits[2] >= limit) {
- CodexEngine.get()
- .getMessageUtil()
- .sendMessage("fusion.queue.fullGlobal", player, new MessageData("limit", limit));
- return;
- }
-
QueueItem item = new QueueItem(-1, profession, category, recipe, System.currentTimeMillis(), 0);
FusionAPI.getEventServices()
.getQueueService()
From da2b2ae7e9fcf574fcf651fcce0e4d4c79b0c5b4 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 12 Oct 2025 18:08:29 +0200
Subject: [PATCH 24/34] preparations for future enhancements
---
.../magemonkey/fusion/gui/RecipeGui.java | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
index 8a53608..91f91bd 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java
@@ -53,6 +53,7 @@
import studio.magemonkey.fusion.util.PlayerUtil;
import java.util.*;
+import java.util.stream.Collectors;
@Getter
public class RecipeGui implements Listener {
@@ -447,6 +448,9 @@ public void reloadRecipes() {
new MessageData("category", category),
new MessageData("gui", getName()),
new MessageData("player", player.getName()),
+ new MessageData("queue_done", queue != null ? queue.getQueue().stream().filter(QueueItem::isDone).toList().size() : 0),
+ new MessageData("queue_size", queue != null ? queue.getQueue().size() : 0),
+ new MessageData("queue_time", queue != null ? queue.getVisualRemainingTotalTime() : 0),
new MessageData("bal",
CodexEngine.get().getVault() == null
? 0
@@ -679,6 +683,34 @@ private boolean canCraft(CalculatedRecipe calculatedRecipe, int slot) {
.sendMessage("fusion.error.noFunds", player, new MessageData("recipe", recipe));
return false;
}
+
+ // Check queue limits
+ int[] limits = PlayerLoader.getPlayer(player.getUniqueId()).getQueueSizes(table.getName(), category);
+ int categoryLimit =
+ PlayerUtil.getPermOption(player, "fusion.queue." + table.getName() + "." + category.getName() + ".limit");
+ int professionLimit = PlayerUtil.getPermOption(player, "fusion.queue." + table.getName() + ".limit");
+ int limit = PlayerUtil.getPermOption(player, "fusion.queue.limit");
+
+ if (categoryLimit > 0 && limits[0] >= categoryLimit) {
+ CodexEngine.get().getMessageUtil().sendMessage("fusion.queue.fullCategory",
+ player,
+ new MessageData("limit", categoryLimit),
+ new MessageData("category", category.getName()),
+ new MessageData("profession", table.getName()));
+ return false;
+ } else if (professionLimit > 0 && limits[1] >= professionLimit) {
+ CodexEngine.get().getMessageUtil().sendMessage("fusion.queue.fullProfession",
+ player,
+ new MessageData("limit", professionLimit),
+ new MessageData("profession", table.getName()));
+ return false;
+ } else if (limit > 0 && limits[2] >= limit) {
+ CodexEngine.get()
+ .getMessageUtil()
+ .sendMessage("fusion.queue.fullGlobal", player, new MessageData("limit", limit));
+ return false;
+ }
+
return true;
}
From 3b7f68ba6e3f48e4a6acd426300c13470f37fdd6 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:54:02 +0200
Subject: [PATCH 25/34] fixed hashes not updating on changes of itemstack
amount
---
.../magemonkey/fusion/gui/recipe/InventoryFingerprint.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java b/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
index 1418129..8c67870 100644
--- a/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
+++ b/src/main/java/studio/magemonkey/fusion/gui/recipe/InventoryFingerprint.java
@@ -37,6 +37,7 @@ public static byte[] fingerprint(Player p) {
// Material
md.update((byte) is.getType().ordinal());
// Amount (4 bytes)
+ md.update(ByteBuffer.allocate(4).putInt(is.getAmount()).array());
// displayName
if (im != null && im.hasDisplayName()) {
byte[] nameBytes = im.getDisplayName().getBytes(java.nio.charset.StandardCharsets.UTF_8);
From 6ba8401deae069e4ef24f8cbacfdedc9f40c8c33 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 19 Oct 2025 16:17:03 +0200
Subject: [PATCH 26/34] further sql fixes
---
.../cfg/sql/tables/FusionProfessionsSQL.java | 30 +++++--------------
.../cfg/sql/tables/FusionRecipeLimitsSQL.java | 28 ++++-------------
2 files changed, 13 insertions(+), 45 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
index 6c1c0a3..e182e92 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
@@ -18,7 +18,7 @@ public class FusionProfessionsSQL {
public FusionProfessionsSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id numeric, "
+ + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, "
+ "UUID varchar(36), "
+ "Profession varchar(100),"
+ "Experience numeric,"
@@ -34,21 +34,6 @@ public FusionProfessionsSQL() {
}
}
- public long getNextId() {
- try (PreparedStatement select = SQLManager.connection().prepareStatement("SELECT COUNT(*) FROM " + Table)) {
- ResultSet result = select.executeQuery();
- if (result.next()) {
- return result.getLong(1);
- }
- } catch (SQLException e) {
- Fusion.getInstance()
- .getLogger()
- .warning("[SQL:FusionProfessionsSQL:getNextId] Something went wrong with the sql-connection: "
- + e.getMessage());
- }
- return 0;
- }
-
public void setProfession(UUID uuid, Profession profession) {
if (hasProfession(uuid, profession.getName())) {
updateProfession(profession);
@@ -60,13 +45,12 @@ public void setProfession(UUID uuid, Profession profession) {
public void addProfession(Profession profession) {
try (PreparedStatement insert = SQLManager.connection()
.prepareStatement("INSERT INTO " + Table
- + "(Id, UUID, Profession, Experience, Mastered, Joined) VALUES(?,?,?,?,?,?)")) {
- insert.setLong(1, getNextId());
- insert.setString(2, profession.getUuid().toString());
- insert.setString(3, profession.getName());
- insert.setDouble(4, profession.getExp());
- insert.setBoolean(5, profession.isMastered());
- insert.setBoolean(6, profession.isJoined());
+ + "(UUID, Profession, Experience, Mastered, Joined) VALUES(?,?,?,?,?)")) {
+ insert.setString(1, profession.getUuid().toString());
+ insert.setString(2, profession.getName());
+ insert.setDouble(3, profession.getExp());
+ insert.setBoolean(4, profession.isMastered());
+ insert.setBoolean(5, profession.isJoined());
insert.execute();
} catch (SQLException e) {
Fusion.getInstance()
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
index 62388ee..98bfef7 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
@@ -18,7 +18,7 @@ public class FusionRecipeLimitsSQL {
public FusionRecipeLimitsSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id long,"
+ + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
+ "UUID varchar(36), "
+ "RecipePath varchar(100),"
+ "Amount numeric,"
@@ -33,21 +33,6 @@ public FusionRecipeLimitsSQL() {
}
}
- public long getNextId() {
- try (PreparedStatement select = SQLManager.connection().prepareStatement("SELECT Count(Id) FROM " + Table)) {
- ResultSet result = select.executeQuery();
- if (result.next()) {
- return result.getLong(1);
- }
- } catch (SQLException e) {
- Fusion.getInstance()
- .getLogger()
- .warning("[SQL:FusionRecipeLimitsSQL:getNextId] Something went wrong with the sql-connection: "
- + e.getMessage());
- }
- return 0;
- }
-
public Map getRecipeLimits(UUID uuid) {
Map limits = new HashMap<>();
try (PreparedStatement select = SQLManager.connection()
@@ -129,12 +114,11 @@ public void saveRecipeLimits(UUID uuid, Map recipeLim
} else {
// Insert
try (PreparedStatement insert = SQLManager.connection().prepareStatement(
- "INSERT INTO " + Table + "(Id, UUID, RecipePath, Amount, Timestamp) VALUES(?,?,?,?,?)")) {
- insert.setLong(1, getNextId());
- insert.setString(2, uuid.toString());
- insert.setString(3, recipePath);
- insert.setInt(4, limit.getLimit());
- insert.setLong(5, limit.getCooldownTimestamp());
+ "INSERT INTO " + Table + "(UUID, RecipePath, Amount, Timestamp) VALUES(?,?,?,?)")) {
+ insert.setString(1, uuid.toString());
+ insert.setString(2, recipePath);
+ insert.setInt(3, limit.getLimit());
+ insert.setLong(4, limit.getCooldownTimestamp());
insert.execute();
} catch (SQLException e) {
Fusion.getInstance().getLogger().warning(
From 3c0954b7301f9e57978b4eddac27cb8fd8e2ad61 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Thu, 20 Nov 2025 21:17:23 +0100
Subject: [PATCH 27/34] re-implemented a `includeOriginalLore` setting
---
.../data/professions/ProfessionSettings.java | 20 ++++++++++++++++++-
.../fusion/data/recipes/CalculatedRecipe.java | 8 +++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
index 7a4cba6..6936448 100644
--- a/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
+++ b/src/main/java/studio/magemonkey/fusion/data/professions/ProfessionSettings.java
@@ -32,6 +32,7 @@ public class ProfessionSettings implements ConfigurationSerializable {
// Icon related fields
private RecipeItem recipeItem;
private String iconNamespace;
+ private boolean includeOriginalLore;
// Optional item fields
private String name;
@@ -50,12 +51,13 @@ public class ProfessionSettings implements ConfigurationSerializable {
private Boolean hideRecipeLimitReached;
public ProfessionSettings(String profession, Boolean hideNoPermission,
- Boolean hideRecipeLimitReached, String iconNamespace, String name, int customModelData, List lore, boolean unbreakable, Map enchantments, Set flags, String color, boolean cancelDrop, List commandsOnClick) {
+ Boolean hideRecipeLimitReached, String iconNamespace, boolean includeOriginalLore, String name, int customModelData, List lore, boolean unbreakable, Map enchantments, Set flags, String color, boolean cancelDrop, List commandsOnClick) {
this.profession = profession;
this.hideNoPermission = hideNoPermission;
this.hideRecipeLimitReached = hideRecipeLimitReached;
this.iconNamespace = iconNamespace;
+ this.includeOriginalLore = includeOriginalLore;
this.name = name;
this.customModelData = customModelData;
this.lore = lore;
@@ -77,6 +79,8 @@ public ProfessionSettings(String profession, ConfigurationSection config) {
// Setup of the icon
String iconNamespace = config.getString("settings.icon.item");
+ includeOriginalLore = config.getBoolean("settings.icon.includeOriginalLore", true);
+
if (config.isSet("settings.icon.optionals") && !config.getConfigurationSection("settings.icon.optionals").getKeys(false).isEmpty()) {
name = config.getString("settings.icon.optionals.name");
customModelData = config.getInt("settings.icon.optionals.customModelData", -1);
@@ -129,6 +133,7 @@ public ProfessionSettings(String profession, DeserializationWorker dw) {
return;
}
String iconNamespace = (String) iconSettings.get("item");
+ includeOriginalLore = iconSettings.get("includeOriginalLore") != null && (boolean) iconSettings.get("includeOriginalLore");
Map optionalIconSettings = (Map) iconSettings.get("optionals");
if (optionalIconSettings != null && !optionalIconSettings.isEmpty()) {
if(optionalIconSettings.get("name") != null)
@@ -188,6 +193,7 @@ public ProfessionSettings(String profession, DeserializationWorker dw) {
Map iconSettings = new HashMap<>(3);
iconSettings.put("item", iconNamespace);
+ iconSettings.put("includeOriginalLore", includeOriginalLore);
Map optionalIconSettings = new HashMap<>(10);
if (name != null) optionalIconSettings.put("name", name);
if (customModelData >= 0) optionalIconSettings.put("customModelData", customModelData);
@@ -223,6 +229,7 @@ public static ProfessionSettings copy(ProfessionSettings results) {
results.hideNoPermission,
results.hideRecipeLimitReached,
results.iconNamespace,
+ results.includeOriginalLore,
results.name,
results.customModelData,
results.lore,
@@ -248,6 +255,17 @@ private void generateIcon(String namespace) {
builder.name(name);
if (customModelData >= 0)
meta.setCustomModelData(customModelData);
+
+ if(includeOriginalLore) {
+ List existingLore = meta.getLore();
+ if (existingLore != null) {
+ if (lore == null) {
+ lore = new ArrayList<>();
+ }
+ lore.addAll(0, existingLore);
+ }
+ }
+
if (lore != null)
builder = builder.lore(lore);
if (enchantments != null)
diff --git a/src/main/java/studio/magemonkey/fusion/data/recipes/CalculatedRecipe.java b/src/main/java/studio/magemonkey/fusion/data/recipes/CalculatedRecipe.java
index c63dda9..f32f5b2 100644
--- a/src/main/java/studio/magemonkey/fusion/data/recipes/CalculatedRecipe.java
+++ b/src/main/java/studio/magemonkey/fusion/data/recipes/CalculatedRecipe.java
@@ -64,7 +64,13 @@ public static CalculatedRecipe create(Recipe recipe,
ItemMeta baseMeta = iconResult.getItemMeta();
List resultLore = (baseMeta == null) ? Collections.emptyList() : baseMeta.getLore();
- // (Optional custom lore logic omitted)
+ // Append resultLore if exists
+ if (resultLore != null && !resultLore.isEmpty()) {
+ for (String line : resultLore) {
+ lore.append(line).append('\n');
+ }
+ lore.append(" ").append('\n');
+ }
// 1) “Requirement” header
String requirementLine = CraftingRequirementsCfg.getCraftingRequirementLine("recipes");
From fe00e98f698bb84fdcadadd0cd78114f86ef8c00 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 25 Jan 2026 22:44:18 +0100
Subject: [PATCH 28/34] copilot-instruction
---
.github/copilot-instructions.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 .github/copilot-instructions.md
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..e69de29
From d9d33fedc32c4e0edda7b5c02f3e7c0d2d18de92 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 25 Jan 2026 23:25:02 +0100
Subject: [PATCH 29/34] fixed craft show on empty set
---
.../magemonkey/fusion/commands/CommandMechanics.java | 7 +++++++
src/main/resources/lang/lang_en.yml | 2 ++
2 files changed, 9 insertions(+)
diff --git a/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java b/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
index b1413b5..b2a851f 100644
--- a/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
+++ b/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
@@ -467,6 +467,13 @@ private static void showIngredientUsage(Player player) {
}
}
+ if(recipeUsage.isEmpty()) {
+ CodexEngine.get().getMessageUtil().sendMessage("fusion.show.noUsage",
+ player,
+ new MessageData("item", item),
+ new MessageData("sender", player));
+ return;
+ }
new ShowRecipesGui(player, recipeUsage).open(player);
}
diff --git a/src/main/resources/lang/lang_en.yml b/src/main/resources/lang/lang_en.yml
index c7e4916..dcf3716 100644
--- a/src/main/resources/lang/lang_en.yml
+++ b/src/main/resources/lang/lang_en.yml
@@ -48,6 +48,8 @@ fusion:
fullCategory: "&cYou have reached the category maximum of your queue size. ($ of profession $)"
finished: "&aYou have crafting items ready for pickup! ($)"
cancelled: "&cYour crafting queue has been cancelled."
+ show:
+ noUsage: "&cThere is no usage for this ingredient."
help: |2-
You typed: &7$&r
Valid syntax:
From 3ab88720d3194f77758b4fdefb95809fd72b1aed Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 15 Feb 2026 14:50:22 +0100
Subject: [PATCH 30/34] changed typo mistake in sql swapping
---
.../studio/magemonkey/fusion/commands/CommandMechanics.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java b/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
index b2a851f..886576e 100644
--- a/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
+++ b/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java
@@ -208,7 +208,7 @@ public static void setStorage(CommandSender sender, String[] args) {
String storage = args[1];
DatabaseType type =
DatabaseType.valueOf(Objects.requireNonNull(Cfg.getConfig())
- .getString("storage.type", "LOCALE")
+ .getString("storage.type", "LOCAL")
.toUpperCase());
switch (storage.toLowerCase()) {
case "local":
From 33a84bdcecb11c4d0d5cd5d211873509c6d8d612 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:05:25 +0100
Subject: [PATCH 31/34] initializing auto increment per DatabaseType
---
.../magemonkey/fusion/cfg/sql/SQLManager.java | 46 +++++++++++++++----
.../cfg/sql/tables/FusionProfessionsSQL.java | 3 +-
.../cfg/sql/tables/FusionQueuesSQL.java | 3 +-
.../cfg/sql/tables/FusionRecipeLimitsSQL.java | 3 +-
4 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
index 3e3bb7b..55b2357 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
@@ -29,6 +29,9 @@ public class SQLManager {
private static String user;
private static String password;
+ // Track the currently selected database type so we can produce dialect-specific SQL when needed
+ private static DatabaseType currentType;
+
public static void init() {
FileConfiguration cfg = Cfg.getConfig();
DatabaseType type = DatabaseType.valueOf(cfg.getString("storage.type", "LOCAL").toUpperCase());
@@ -38,6 +41,9 @@ public static void init() {
user = cfg.getString("storage.user", "root");
password = cfg.getString("storage.password", "password");
+ // store the selected type
+ currentType = type;
+
Fusion.getInstance().getLogger().info("Initializing SQLManager with type: " + type);
switch (type) {
@@ -155,10 +161,9 @@ public static void swapToLocal() {
statement.execute("DROP TABLE IF EXISTS fusion_professions");
statement.execute("DROP TABLE IF EXISTS fusion_queues");
statement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36), AutoCrafting boolean)");
- statement.execute(
- "CREATE TABLE IF NOT EXISTS fusion_professions(Id long, UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
- statement.execute(
- "CREATE TABLE IF NOT EXISTS fusion_queues(Id long, UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
+ // Use SQLite-compatible id column definition
+ statement.execute("CREATE TABLE IF NOT EXISTS fusion_professions(" + getIdColumn(DatabaseType.LOCAL) + " UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
+ statement.execute("CREATE TABLE IF NOT EXISTS fusion_queues(" + getIdColumn(DatabaseType.LOCAL) + " UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
} catch (SQLException e) {
Fusion.getInstance().getLogger().severe("Error while dropping tables: " + e.getMessage());
@@ -207,7 +212,7 @@ public static void swapToLocal() {
try (Connection sqliteConnection = getSQLiteConnection();
PreparedStatement insertStatement = sqliteConnection.prepareStatement(
- "INSERT INTO fusion_queues (Id, UUID, RecipePath, Timestamp, CraftingTime, SavedSeconds) VALUES (?, ?, ?, ?, ?, ?)")) {
+ "INSERT INTO fusion_queues (Id, UUID, RecipePath, Timestamp, CraftingTime, SavedSeconds) VALUES (?, ?, ?, ?, ?, ?)") ) {
insertQueue(resultQueues, insertStatement);
} catch (SQLException e) {
Fusion.getInstance()
@@ -236,10 +241,9 @@ public static void swapToSql() {
// Delete all content of the current database and recreate tables
sqlStatement.execute("DROP TABLE IF EXISTS fusion_players, fusion_professions, fusion_queues");
sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36), AutoCrafting boolean)");
- sqlStatement.execute(
- "CREATE TABLE IF NOT EXISTS fusion_professions(Id long, UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
- sqlStatement.execute(
- "CREATE TABLE IF NOT EXISTS fusion_queues(Id long, UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
+ // Use MySQL-compatible id column definition
+ sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_professions(" + getIdColumn(DatabaseType.MYSQL) + " UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
+ sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_queues(" + getIdColumn(DatabaseType.MYSQL) + " UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
// Get all data from the local database
try (Connection sqliteConnection = getSQLiteConnection();
@@ -327,4 +331,28 @@ private static void insertProfession(ResultSet resultProfessions, PreparedStatem
insertStatement.executeUpdate();
}
}
+
+ /**
+ * Returns a dialect-specific id column definition including the trailing comma.
+ * For SQLITE (LOCAL) this returns: "Id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ * For MYSQL/MARIADB this returns: "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
+ */
+ public static String getIdColumn(DatabaseType type) {
+ if (type == DatabaseType.LOCAL) {
+ return "Id INTEGER PRIMARY KEY AUTOINCREMENT,";
+ }
+ // default to MySQL/MariaDB style
+ return "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,";
+ }
+
+ /**
+ * Returns the id column definition for the currently configured database type.
+ */
+ public static String getIdColumn() {
+ return getIdColumn(currentType == null ? DatabaseType.MYSQL : currentType);
+ }
+
+ public static DatabaseType getDatabaseType() {
+ return currentType;
+ }
}
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
index e182e92..d5e8dc7 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java
@@ -18,8 +18,7 @@ public class FusionProfessionsSQL {
public FusionProfessionsSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, "
- + "UUID varchar(36), "
+ + SQLManager.getIdColumn() + " UUID varchar(36), "
+ "Profession varchar(100),"
+ "Experience numeric,"
+ "Mastered boolean,"
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
index 8c2050b..a92e615 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionQueuesSQL.java
@@ -22,8 +22,7 @@ public class FusionQueuesSQL {
public FusionQueuesSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
- + "UUID varchar(36), "
+ + SQLManager.getIdColumn() + " UUID varchar(36), "
+ "RecipePath varchar(100),"
+ "CraftingTime numeric,"
+ "SavedSeconds numeric,"
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
index 98bfef7..426c9a7 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionRecipeLimitsSQL.java
@@ -18,8 +18,7 @@ public class FusionRecipeLimitsSQL {
public FusionRecipeLimitsSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
- + "UUID varchar(36), "
+ + SQLManager.getIdColumn() + " UUID varchar(36), "
+ "RecipePath varchar(100),"
+ "Amount numeric,"
+ "Timestamp BIGINT)")) {
From f2b47121b6d009410286d48e2606bc311f8e4417 Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:34:49 +0100
Subject: [PATCH 32/34] adressed npe potential from locking
---
.../magemonkey/fusion/cfg/sql/SQLManager.java | 37 ++++++++++---
.../cfg/sql/tables/FusionPlayersSQL.java | 32 +++++++----
.../fusion/data/player/FusionPlayer.java | 12 ++++-
.../fusion/data/player/PlayerLoader.java | 54 ++++++++++++++++++-
4 files changed, 116 insertions(+), 19 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
index 55b2357..6ea29b8 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
@@ -160,7 +160,7 @@ public static void swapToLocal() {
statement.execute("DROP TABLE IF EXISTS fusion_players");
statement.execute("DROP TABLE IF EXISTS fusion_professions");
statement.execute("DROP TABLE IF EXISTS fusion_queues");
- statement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36), AutoCrafting boolean)");
+ statement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36) PRIMARY KEY, AutoCrafting boolean DEFAULT false, Locked boolean DEFAULT false)");
// Use SQLite-compatible id column definition
statement.execute("CREATE TABLE IF NOT EXISTS fusion_professions(" + getIdColumn(DatabaseType.LOCAL) + " UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
statement.execute("CREATE TABLE IF NOT EXISTS fusion_queues(" + getIdColumn(DatabaseType.LOCAL) + " UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
@@ -178,10 +178,16 @@ public static void swapToLocal() {
try (Connection sqliteConnection = getSQLiteConnection();
PreparedStatement insertStatement = sqliteConnection.prepareStatement(
- "INSERT INTO fusion_players (UUID, AutoCrafting) VALUES (?, ?)")) {
+ "INSERT INTO fusion_players (UUID, AutoCrafting, Locked) VALUES (?, ?, ?)") ) {
while (resultPlayers.next()) {
insertStatement.setString(1, resultPlayers.getString("UUID"));
insertStatement.setBoolean(2, resultPlayers.getBoolean("AutoCrafting"));
+ // If source DB doesn't have Locked column, getBoolean will return false; that's acceptable
+ try {
+ insertStatement.setBoolean(3, resultPlayers.getBoolean("Locked"));
+ } catch (SQLException ignored) {
+ insertStatement.setBoolean(3, false);
+ }
insertStatement.executeUpdate();
}
} catch (SQLException e) {
@@ -197,7 +203,7 @@ public static void swapToLocal() {
try (Connection sqliteConnection = getSQLiteConnection();
PreparedStatement insertStatement = sqliteConnection.prepareStatement(
- "INSERT INTO fusion_professions (Id, UUID, Profession, Experience, Mastered, Joined) VALUES (?, ?, ?, ?, ?, ?)")) {
+ "INSERT INTO fusion_professions (Id, UUID, Profession, Experience, Mastered, Joined) VALUES (?, ?, ?, ?, ?, ?)") ) {
insertProfession(resultProfessions, insertStatement);
} catch (SQLException e) {
Fusion.getInstance()
@@ -240,7 +246,7 @@ public static void swapToSql() {
// Delete all content of the current database and recreate tables
sqlStatement.execute("DROP TABLE IF EXISTS fusion_players, fusion_professions, fusion_queues");
- sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36), AutoCrafting boolean)");
+ sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_players(UUID varchar(36) PRIMARY KEY, AutoCrafting boolean DEFAULT false, Locked boolean DEFAULT false)");
// Use MySQL-compatible id column definition
sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_professions(" + getIdColumn(DatabaseType.MYSQL) + " UUID varchar(36), Profession varchar(100), Experience numeric, Mastered boolean, Joined boolean)");
sqlStatement.execute("CREATE TABLE IF NOT EXISTS fusion_queues(" + getIdColumn(DatabaseType.MYSQL) + " UUID varchar(36), RecipePath varchar(100), Timestamp BIGINT, CraftingTime numeric, SavedSeconds numeric)");
@@ -251,7 +257,20 @@ public static void swapToSql() {
// Retrieve data from local database
ResultSet resultPlayers = localStatement.executeQuery("SELECT * FROM fusion_players");
- insertPlayers(sqlConnection, resultPlayers);
+ // Ensure we transfer Locked value if present
+ try (PreparedStatement insert = sqlConnection.prepareStatement(
+ "INSERT INTO fusion_players (UUID, AutoCrafting, Locked) VALUES (?, ?, ?)") ) {
+ while (resultPlayers.next()) {
+ insert.setString(1, resultPlayers.getString("UUID"));
+ insert.setBoolean(2, resultPlayers.getBoolean("AutoCrafting"));
+ try {
+ insert.setBoolean(3, resultPlayers.getBoolean("Locked"));
+ } catch (SQLException ignored) {
+ insert.setBoolean(3, false);
+ }
+ insert.executeUpdate();
+ }
+ }
ResultSet resultProfessions = localStatement.executeQuery("SELECT * FROM fusion_professions");
insertProfessions(sqlConnection, resultProfessions);
@@ -281,11 +300,16 @@ public static void swapToSql() {
}
private static void insertPlayers(Connection connection, ResultSet resultSet) throws SQLException {
- String insertQuery = "INSERT INTO fusion_players (UUID, AutoCrafting) VALUES (?, ?)";
+ String insertQuery = "INSERT INTO fusion_players (UUID, AutoCrafting, Locked) VALUES (?, ?, ?)";
try (PreparedStatement preparedStatement = connection.prepareStatement(insertQuery)) {
while (resultSet.next()) {
preparedStatement.setString(1, resultSet.getString("UUID"));
preparedStatement.setBoolean(2, resultSet.getBoolean("AutoCrafting"));
+ try {
+ preparedStatement.setBoolean(3, resultSet.getBoolean("Locked"));
+ } catch (SQLException ignored) {
+ preparedStatement.setBoolean(3, false);
+ }
preparedStatement.executeUpdate();
}
}
@@ -356,3 +380,4 @@ public static DatabaseType getDatabaseType() {
return currentType;
}
}
+
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
index 2400de1..7c8508b 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
@@ -2,6 +2,7 @@
import studio.magemonkey.fusion.Fusion;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
+import studio.magemonkey.fusion.cfg.sql.DatabaseType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -16,9 +17,9 @@ public class FusionPlayersSQL {
public FusionPlayersSQL() {
try (PreparedStatement create = SQLManager.connection()
.prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "("
- + "UUID varchar(36), "
- + "AutoCrafting boolean, "
- + "Locked boolean)")) {
+ + "UUID varchar(36) PRIMARY KEY, "
+ + "AutoCrafting boolean DEFAULT false, "
+ + "Locked boolean DEFAULT false)")) {
create.execute();
boolean lockedColumnAdded = alterIfLockedNotExistent();
@@ -27,7 +28,7 @@ public FusionPlayersSQL() {
.getLogger()
.info("[SQL:FusionPlayersSQL:FusionPlayersSQL] Added 'Locked' column to 'fusion_players' table.");
}
-
+
} catch (SQLException e) {
Fusion.getInstance()
.getLogger()
@@ -83,19 +84,30 @@ public boolean isLocked(UUID uuid) {
}
public void addPlayer(UUID uuid) {
- if (hasPlayer(uuid))
- return;
- try (PreparedStatement insert = SQLManager.connection()
- .prepareStatement("INSERT INTO " + Table + "(UUID, AutoCrafting, Locked) VALUES(?,?,?)")) {
+ // Use a dialect-aware insert that ignores duplicates to avoid race conditions across nodes
+ DatabaseType dbType = SQLManager.getDatabaseType();
+ String sql;
+ if (dbType == DatabaseType.LOCAL) {
+ // SQLite: INSERT OR IGNORE
+ sql = "INSERT OR IGNORE INTO " + Table + "(UUID, AutoCrafting, Locked) VALUES(?,?,?)";
+ } else {
+ // MySQL/MariaDB: use ON DUPLICATE KEY UPDATE as a no-op
+ sql = "INSERT INTO " + Table + "(UUID, AutoCrafting, Locked) VALUES(?,?,?) ON DUPLICATE KEY UPDATE UUID=UUID";
+ }
+
+ try (PreparedStatement insert = SQLManager.connection().prepareStatement(sql)) {
insert.setString(1, uuid.toString());
insert.setBoolean(2, false);
insert.setBoolean(3, false);
insert.execute();
} catch (SQLException e) {
+ // If we still hit a duplicate key exception, ignore it safely
+ if (e.getSQLState() != null && (e.getSQLState().startsWith("23") || e.getMessage().toLowerCase().contains("duplicate"))) {
+ return;
+ }
Fusion.getInstance()
.getLogger()
- .warning("[SQL:FusionPlayersSQL:addPlayer] Something went wrong with the sql-connection: "
- + e.getMessage());
+ .warning("[SQL:FusionPlayersSQL:addPlayer] Something went wrong with the sql-connection: " + e.getMessage());
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index 21921eb..3874147 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -3,7 +3,6 @@
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import studio.magemonkey.fusion.Fusion;
@@ -19,7 +18,6 @@
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
@@ -35,8 +33,15 @@ public class FusionPlayer {
@Setter
private boolean autoCrafting;
+ // Track whether this player is currently locked for saving (in-memory mirror of DB lock)
+ @Getter
+ @Setter
+ private volatile boolean locked;
+
public FusionPlayer(UUID uuid) {
this.uuid = uuid;
+ // initialize locked state from DB to reflect current status
+ this.locked = SQLManager.players().isLocked(uuid);
autoCrafting = SQLManager.players().isAutoCrafting(uuid);
for (Profession profession : SQLManager.professions().getProfessions(uuid)) {
professions.put(profession.getName(), profession);
@@ -346,7 +351,9 @@ public int getFinishedSize() {
}
public void save() {
+ // set DB lock and in-memory lock
SQLManager.players().setLocked(uuid, true);
+ this.locked = true;
Bukkit.getScheduler().runTaskAsynchronously(Fusion.getInstance(), () -> {
SQLManager.players().setAutoCrafting(uuid, autoCrafting);
@@ -369,6 +376,7 @@ public void save() {
}
*/
SQLManager.players().setLocked(uuid, false);
+ this.locked = false;
});
}
}
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
index a3d9d98..7940dc1 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
@@ -1,6 +1,7 @@
package studio.magemonkey.fusion.data.player;
import org.bukkit.entity.Player;
+import studio.magemonkey.fusion.Fusion;
import studio.magemonkey.fusion.cfg.sql.SQLManager;
import java.util.Map;
@@ -12,9 +13,21 @@ public class PlayerLoader {
private static final Map cachedPlayers = new TreeMap<>();
public static FusionPlayer getPlayer(UUID uuid) {
+ // Always return a FusionPlayer instance. Previously this returned null when the player was marked as
+ // locked in the database (during async save), which caused NullPointerExceptions at many call sites.
+ // Returning a FusionPlayer ensures call sites remain stable; the locking is handled at the persistence layer.
if (!cachedPlayers.containsKey(uuid)) {
- if(SQLManager.players().isLocked(uuid)) return null;
+ boolean locked = SQLManager.players().isLocked(uuid);
+ if (locked) {
+ Fusion.getInstance().getLogger().info("Player " + uuid + " is currently locked for saving; returning FusionPlayer instance.");
+ }
cachedPlayers.put(uuid, new FusionPlayer(uuid));
+ } else {
+ // If we already have a cached player, refresh its in-memory locked state from the DB so
+ // that cross-node changes to the lock are reflected when other nodes fetch the same player.
+ FusionPlayer fp = cachedPlayers.get(uuid);
+ boolean locked = SQLManager.players().isLocked(uuid);
+ fp.setLocked(locked);
}
return cachedPlayers.get(uuid);
}
@@ -23,6 +36,45 @@ public static FusionPlayer getPlayer(Player player) {
return getPlayer(player.getUniqueId());
}
+ /**
+ * Returns the FusionPlayer if it is not currently locked for saving, otherwise returns null.
+ * This provides an explicit, safe way for call sites that cannot proceed during a save window.
+ */
+ public static FusionPlayer getPlayerIfReady(UUID uuid) {
+ FusionPlayer fp = getPlayer(uuid);
+ if (fp == null) return null; // defensive, but getPlayer never returns null
+ if (fp.isLocked()) return null;
+ return fp;
+ }
+
+ public static FusionPlayer getPlayerIfReady(Player player) {
+ return getPlayerIfReady(player.getUniqueId());
+ }
+
+ /**
+ * Blocks (polling) until the player's DB lock is cleared or until timeoutMs is reached.
+ * Returns the FusionPlayer if the lock cleared within the timeout, otherwise returns null.
+ * Note: Blocking the main server thread is dangerous; call this from an async thread or keep timeout small.
+ */
+ public static FusionPlayer getPlayerBlocking(UUID uuid, long timeoutMs) {
+ long start = System.currentTimeMillis();
+ while (System.currentTimeMillis() - start < timeoutMs) {
+ FusionPlayer fp = getPlayer(uuid);
+ if (fp != null && !fp.isLocked()) return fp;
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ return null;
+ }
+
+ public static FusionPlayer getPlayerBlocking(Player player, long timeoutMs) {
+ return getPlayerBlocking(player.getUniqueId(), timeoutMs);
+ }
+
public static void loadPlayer(Player player) {
cachedPlayers.put(player.getUniqueId(), new FusionPlayer(player.getUniqueId()));
}
From 4377c021248cd7d851651c3a40056b2daa2697ce Mon Sep 17 00:00:00 2001
From: MaksyKun <77341370+MaksyKun@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:38:19 +0100
Subject: [PATCH 33/34] adressed async cache clearing improvement
---
.../magemonkey/fusion/data/player/FusionPlayer.java | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index 3874147..3835d46 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -355,17 +355,20 @@ public void save() {
SQLManager.players().setLocked(uuid, true);
this.locked = true;
+ Map queuesToSave = new TreeMap<>(cachedQueues);
+ Map recipeLimitsToSave = new TreeMap<>(cachedRecipeLimits);
+ cachedQueues.clear();
+ cachedRecipeLimits.clear();
+
Bukkit.getScheduler().runTaskAsynchronously(Fusion.getInstance(), () -> {
SQLManager.players().setAutoCrafting(uuid, autoCrafting);
for (Profession profession : professions.values()) {
SQLManager.professions().setProfession(uuid, profession);
}
- for (CraftingQueue queue : cachedQueues.values()) {
+ for (CraftingQueue queue : queuesToSave.values()) {
SQLManager.queues().saveCraftingQueue(queue);
}
- SQLManager.recipeLimits().saveRecipeLimits(uuid, cachedRecipeLimits);
- cachedQueues.clear();
- cachedRecipeLimits.clear();
+ SQLManager.recipeLimits().saveRecipeLimits(uuid, recipeLimitsToSave);
/*
In case of race conditions we wait a bit before unlocking the player. Not required but just to be safe.
From 041c16ed42963ac3ba1b93b571f6c0a7cdb1a185 Mon Sep 17 00:00:00 2001
From: Trav
Date: Mon, 16 Feb 2026 20:20:53 -0700
Subject: [PATCH 34/34] fix: Enhance saving lock mechanism and prevent data
loss
- Clear all player locks on startup in SQLManager to prevent permanent locks after a crash.
- Modify FusionPlayer.save() to only clear caches when explicitly requested (e.g., during unload), preventing data loss during periodic saves.
- Fix race conditions in Fusion.onPlayerJoin by waiting for pending save locks asynchronously before loading player data.
- Optimize PlayerLoader.getPlayer by removing frequent database queries for lock status, relying on initial load instead.
- Fix redundant save and reload logic in PlayerManager.savePlayer to ensure consistency.
---
.../java/studio/magemonkey/fusion/Fusion.java | 19 +++++++++++-------
.../magemonkey/fusion/api/PlayerManager.java | 4 +---
.../magemonkey/fusion/cfg/sql/SQLManager.java | 1 +
.../cfg/sql/tables/FusionPlayersSQL.java | 13 ++++++++++++
.../fusion/data/player/FusionPlayer.java | 11 ++++++++--
.../fusion/data/player/PlayerLoader.java | 20 ++++++-------------
6 files changed, 42 insertions(+), 26 deletions(-)
diff --git a/src/main/java/studio/magemonkey/fusion/Fusion.java b/src/main/java/studio/magemonkey/fusion/Fusion.java
index 59eb78d..d948e3b 100644
--- a/src/main/java/studio/magemonkey/fusion/Fusion.java
+++ b/src/main/java/studio/magemonkey/fusion/Fusion.java
@@ -189,13 +189,18 @@ private void runQueueTask() {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
- PlayerLoader.loadPlayer(event.getPlayer());
- if(!Cfg.autoJoinProfessions.isEmpty()) {
- Cfg.autoJoinProfessions(event.getPlayer());
- }
- if (Cfg.craftingQueue) {
- notifyForQueue(event.getPlayer());
- }
+ Player player = event.getPlayer();
+ Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
+ PlayerLoader.getPlayerBlocking(player, 5000); // Wait up to 5s for any pending saves to finish
+ Bukkit.getScheduler().runTask(this, () -> {
+ if(!Cfg.autoJoinProfessions.isEmpty()) {
+ Cfg.autoJoinProfessions(player);
+ }
+ if (Cfg.craftingQueue) {
+ notifyForQueue(player);
+ }
+ });
+ });
}
@EventHandler
diff --git a/src/main/java/studio/magemonkey/fusion/api/PlayerManager.java b/src/main/java/studio/magemonkey/fusion/api/PlayerManager.java
index 6d2c1d9..f9df2d0 100644
--- a/src/main/java/studio/magemonkey/fusion/api/PlayerManager.java
+++ b/src/main/java/studio/magemonkey/fusion/api/PlayerManager.java
@@ -43,7 +43,7 @@ public FusionPlayer getPlayer(UUID uuid) {
/**
* Save the player data of a player.
- * This will save the player data to the database and reload the player.
+ * This will save the player data to the database.
*
* @param player The Player object of the player.
*/
@@ -51,8 +51,6 @@ public void savePlayer(Player player) {
FusionPlayer fusionPlayer = getPlayer(player);
if (fusionPlayer != null) {
fusionPlayer.save();
- PlayerLoader.unloadPlayer(player);
- PlayerLoader.loadPlayer(player);
} else {
FusionAPI.getInstance()
.getLogger()
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
index 6ea29b8..47c26f6 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/SQLManager.java
@@ -62,6 +62,7 @@ public static void init() {
Fusion.getInstance().getLogger().severe("Failed to initialize the Connection.");
} else {
fusionPlayersSQL = new FusionPlayersSQL();
+ fusionPlayersSQL.clearAllLocks();
fusionProfessionsSQL = new FusionProfessionsSQL();
fusionQueuesSQL = new FusionQueuesSQL();
fusionRecipeLimitsSQL = new FusionRecipeLimitsSQL();
diff --git a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
index 7c8508b..69539b2 100644
--- a/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
+++ b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java
@@ -67,6 +67,19 @@ public void setLocked(UUID uuid, boolean locked) {
}
}
+ public void clearAllLocks() {
+ try (PreparedStatement update = SQLManager.connection()
+ .prepareStatement("UPDATE " + Table + " SET Locked=?")) {
+ update.setBoolean(1, false);
+ update.execute();
+ } catch (SQLException e) {
+ Fusion.getInstance()
+ .getLogger()
+ .warning("[SQL:FusionPlayersSQL:clearAllLocks] Something went wrong with the sql-connection: "
+ + e.getMessage());
+ }
+ }
+
public boolean isLocked(UUID uuid) {
try (PreparedStatement select = SQLManager.connection()
.prepareStatement("SELECT Locked FROM " + Table + " WHERE UUID=?")) {
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
index 3835d46..0d9ca4c 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java
@@ -351,14 +351,21 @@ public int getFinishedSize() {
}
public void save() {
+ save(false);
+ }
+
+ public void save(boolean clearCaches) {
// set DB lock and in-memory lock
SQLManager.players().setLocked(uuid, true);
this.locked = true;
Map queuesToSave = new TreeMap<>(cachedQueues);
Map recipeLimitsToSave = new TreeMap<>(cachedRecipeLimits);
- cachedQueues.clear();
- cachedRecipeLimits.clear();
+
+ if (clearCaches) {
+ cachedQueues.clear();
+ cachedRecipeLimits.clear();
+ }
Bukkit.getScheduler().runTaskAsynchronously(Fusion.getInstance(), () -> {
SQLManager.players().setAutoCrafting(uuid, autoCrafting);
diff --git a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
index 7940dc1..8c21d38 100644
--- a/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
+++ b/src/main/java/studio/magemonkey/fusion/data/player/PlayerLoader.java
@@ -17,17 +17,7 @@ public static FusionPlayer getPlayer(UUID uuid) {
// locked in the database (during async save), which caused NullPointerExceptions at many call sites.
// Returning a FusionPlayer ensures call sites remain stable; the locking is handled at the persistence layer.
if (!cachedPlayers.containsKey(uuid)) {
- boolean locked = SQLManager.players().isLocked(uuid);
- if (locked) {
- Fusion.getInstance().getLogger().info("Player " + uuid + " is currently locked for saving; returning FusionPlayer instance.");
- }
cachedPlayers.put(uuid, new FusionPlayer(uuid));
- } else {
- // If we already have a cached player, refresh its in-memory locked state from the DB so
- // that cross-node changes to the lock are reflected when other nodes fetch the same player.
- FusionPlayer fp = cachedPlayers.get(uuid);
- boolean locked = SQLManager.players().isLocked(uuid);
- fp.setLocked(locked);
}
return cachedPlayers.get(uuid);
}
@@ -59,8 +49,10 @@ public static FusionPlayer getPlayerIfReady(Player player) {
public static FusionPlayer getPlayerBlocking(UUID uuid, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
- FusionPlayer fp = getPlayer(uuid);
- if (fp != null && !fp.isLocked()) return fp;
+ boolean locked = SQLManager.players().isLocked(uuid);
+ if (!locked) {
+ return getPlayer(uuid);
+ }
try {
Thread.sleep(50);
} catch (InterruptedException e) {
@@ -82,14 +74,14 @@ public static void loadPlayer(Player player) {
public static void unloadPlayer(Player player) {
if (cachedPlayers.containsKey(player.getUniqueId())) {
FusionPlayer fusionPlayer = cachedPlayers.get(player.getUniqueId());
- fusionPlayer.save();
+ fusionPlayer.save(true);
cachedPlayers.remove(player.getUniqueId());
}
}
public static void clearCache() {
for (FusionPlayer fusionPlayer : cachedPlayers.values())
- fusionPlayer.save();
+ fusionPlayer.save(true);
cachedPlayers.clear();
}
}