From 179fd4f83489e70a3e54175abbc9fcf9aedfdd18 Mon Sep 17 00:00:00 2001 From: KAMKEEL <28842281+KAMKEEL@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:54:14 -0400 Subject: [PATCH] Make ID reassignment safe, and apply it to quests too The free-ID search only consulted the global map, so it could pick an id the same category still held further down the iteration - and renameTo silently overwrites on POSIX, so the rename then destroyed that entry's file. It now skips ids held by the category and any file already on disk. Quests had the same discard-on-collision behaviour and now get the same reassignment, including re-registering shared quests under the new id. --- .../npcs/controllers/DialogController.java | 18 +++++--- .../npcs/controllers/QuestController.java | 44 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/DialogController.java b/src/main/java/noppes/npcs/controllers/DialogController.java index b9a416762..b65e1ef5e 100644 --- a/src/main/java/noppes/npcs/controllers/DialogController.java +++ b/src/main/java/noppes/npcs/controllers/DialogController.java @@ -84,14 +84,22 @@ public class DialogController implements IDialogHandler { lastUsedDialogID = id; Dialog dialog = category.dialogs.get(id); if (dialogs.containsKey(id)) { - // Find the next free ID, skipping any already claimed slots. - int newId = lastUsedDialogID; - do { newId++; } while (dialogs.containsKey(newId)); - lastUsedDialogID = newId; - // Dialog IDs live in the filename, not inside the JSON body, // so a simple rename permanently resolves the conflict. File catDir = new File(getDir(), category.title); + + // Find the next free ID. It has to clear three things: the ids already + // registered globally, the ids this category still holds but has not + // reached yet, and any file already on disk - renameTo overwrites + // silently on POSIX, so landing on an existing file would destroy it. + int newId = lastUsedDialogID; + do { + newId++; + } while (dialogs.containsKey(newId) + || category.dialogs.containsKey(newId) + || new File(catDir, newId + ".json").exists()); + lastUsedDialogID = newId; + File oldFile = new File(catDir, id + ".json"); File newFile = new File(catDir, newId + ".json"); boolean renamed = oldFile.renameTo(newFile); diff --git a/src/main/java/noppes/npcs/controllers/QuestController.java b/src/main/java/noppes/npcs/controllers/QuestController.java index 69bbe88d0..3d9b707b3 100644 --- a/src/main/java/noppes/npcs/controllers/QuestController.java +++ b/src/main/java/noppes/npcs/controllers/QuestController.java @@ -70,6 +70,7 @@ public class QuestController implements IQuestHandler { if (!file.isDirectory()) continue; QuestCategory category = loadCategoryDir(file); + List reassigned = new ArrayList(); Iterator ite = category.quests.keySet().iterator(); while (ite.hasNext()) { int id = ite.next(); @@ -77,8 +78,39 @@ public class QuestController implements IQuestHandler { lastUsedQuestID = id; Quest quest = category.quests.get(id); if (quests.containsKey(id)) { - LogWriter.error("Duplicate id " + quest.id + " from category " + category.title); + // Quest IDs live in the filename, not inside the JSON body, + // so a simple rename permanently resolves the conflict. + File catDir = new File(getDir(), category.title); + + // Find the next free ID. It has to clear three things: the ids already + // registered globally, the ids this category still holds but has not + // reached yet, and any file already on disk - renameTo overwrites + // silently on POSIX, so landing on an existing file would destroy it. + int newId = lastUsedQuestID; + do { + newId++; + } while (quests.containsKey(newId) + || category.quests.containsKey(newId) + || new File(catDir, newId + ".json").exists()); + lastUsedQuestID = newId; + + File oldFile = new File(catDir, id + ".json"); + File newFile = new File(catDir, newId + ".json"); + boolean renamed = oldFile.renameTo(newFile); + + quest.id = newId; ite.remove(); + reassigned.add(quest); + + if (renamed) { + LogWriter.info("Quest ID conflict: reassigned \"" + quest.title + + "\" from id " + id + " to " + newId + + " in category \"" + category.title + "\""); + } else { + LogWriter.error("Quest ID conflict: could not rename file for \"" + + quest.title + "\" (id " + id + + " in category \"" + category.title + "\") to " + newId); + } } else { quests.put(id, quest); if (quest.profileOptions.enableOptions @@ -88,6 +120,16 @@ public class QuestController implements IQuestHandler { } } } + // Re-register reassigned quests under their new IDs. + for (Quest quest : reassigned) { + quests.put(quest.id, quest); + category.quests.put(quest.id, quest); + if (quest.profileOptions.enableOptions + && (quest.profileOptions.completeControl == EnumProfileSync.Shared + || quest.profileOptions.cooldownControl == EnumProfileSync.Shared)) { + sharedQuests.put(quest.id, quest); + } + } lastUsedCatID++; category.id = lastUsedCatID; categories.put(category.id, category);