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.
This commit is contained in:
KAMKEEL committed 2026-08-11 05:54:14 -04:00
1 parent 1d0b640c61
commit 179fd4f834
2 files changed
+56 -6

No files matched your search

@@ -70,6 +70,7 @@ public class QuestController implements IQuestHandler {
if (!file.isDirectory())
continue;
QuestCategory category = loadCategoryDir(file);
List<Quest> reassigned = new ArrayList<Quest>();
Iterator<Integer> 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);