diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e69de29 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 3e3bb7b..47c26f6 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) { @@ -56,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(); @@ -154,11 +161,10 @@ 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_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)"); + 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)"); } catch (SQLException e) { Fusion.getInstance().getLogger().severe("Error while dropping tables: " + e.getMessage()); @@ -173,10 +179,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) { @@ -192,7 +204,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() @@ -207,7 +219,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() @@ -235,11 +247,10 @@ 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)"); + 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)"); // Get all data from the local database try (Connection sqliteConnection = getSQLiteConnection(); @@ -247,7 +258,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); @@ -277,11 +301,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(); } } @@ -327,4 +356,29 @@ 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/FusionPlayersSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionPlayersSQL.java index d1edc64..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 @@ -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,18 @@ public class FusionPlayersSQL { public FusionPlayersSQL() { try (PreparedStatement create = SQLManager.connection() .prepareStatement("CREATE TABLE IF NOT EXISTS " + Table + "(" - + "UUID varchar(36), " - + "AutoCrafting boolean)")) { + + "UUID varchar(36) PRIMARY KEY, " + + "AutoCrafting boolean DEFAULT false, " + + "Locked boolean DEFAULT false)")) { 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,19 +52,75 @@ 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 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=?")) { + 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; - try (PreparedStatement insert = SQLManager.connection() - .prepareStatement("INSERT INTO " + Table + "(UUID, AutoCrafting) 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()); } } @@ -89,4 +155,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/cfg/sql/tables/FusionProfessionsSQL.java b/src/main/java/studio/magemonkey/fusion/cfg/sql/tables/FusionProfessionsSQL.java index 6c1c0a3..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 numeric, " - + "UUID varchar(36), " + + SQLManager.getIdColumn() + " UUID varchar(36), " + "Profession varchar(100)," + "Experience numeric," + "Mastered boolean," @@ -34,21 +33,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 +44,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/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 584720c..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 long," - + "UUID varchar(36), " + + SQLManager.getIdColumn() + " UUID varchar(36), " + "RecipePath varchar(100)," + "Amount numeric," + "Timestamp BIGINT)")) { @@ -33,21 +32,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() @@ -71,35 +55,75 @@ 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( - "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(); + + // 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 + "(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( + "[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/commands/CommandMechanics.java b/src/main/java/studio/magemonkey/fusion/commands/CommandMechanics.java index b1413b5..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": @@ -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/java/studio/magemonkey/fusion/data/player/FusionPlayer.java b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java index 465418a..0d9ca4c 100644 --- a/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java +++ b/src/main/java/studio/magemonkey/fusion/data/player/FusionPlayer.java @@ -3,9 +3,9 @@ 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; import studio.magemonkey.fusion.cfg.sql.SQLManager; import studio.magemonkey.fusion.data.professions.Profession; import studio.magemonkey.fusion.data.professions.pattern.Category; @@ -33,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); @@ -344,15 +351,42 @@ 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); + 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); + + if (clearCaches) { + cachedQueues.clear(); + cachedRecipeLimits.clear(); } - SQLManager.recipeLimits().saveRecipeLimits(uuid, 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 : queuesToSave.values()) { + SQLManager.queues().saveCraftingQueue(queue); + } + 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. + try { + Thread.sleep(250); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + */ + 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 44422af..8c21d38 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,8 @@ 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; import java.util.TreeMap; @@ -11,6 +13,9 @@ 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)) { cachedPlayers.put(uuid, new FusionPlayer(uuid)); } @@ -21,6 +26,47 @@ 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) { + boolean locked = SQLManager.players().isLocked(uuid); + if (!locked) { + return getPlayer(uuid); + } + 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())); } @@ -28,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(); } } 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/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() 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"); 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); diff --git a/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java b/src/main/java/studio/magemonkey/fusion/gui/RecipeGui.java index 81d107c..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 @@ -474,7 +478,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 +489,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(); @@ -628,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; } @@ -813,7 +896,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); } @@ -1005,7 +1088,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) || @@ -1024,6 +1107,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/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); 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()); } } 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: