From 0746540f40638f032a813ee25d669c37cef7a2c2 Mon Sep 17 00:00:00 2001 From: Rasmus Kromann-Larsen Date: Wed, 9 Sep 2026 13:15:11 +0200 Subject: [PATCH 1/2] Save builds with a stable element order The XML save path walked several maps with pairs(): calcs/config inputs and placeholders, item slots, saver sections, weapon-set node lists, and the node-id-keyed allocNodes, masterySelections, jewels, attribute nodes and hashOverrides in PassiveSpec. Their emission order was hash order, so the same build could save differently between runs. Sort each walk (names ascending, node ids ascending, slots in creation order, weapon sets by number) so a build always serialises the same way. EncodeURL also walks allocNodes ascending, so the nodeCount < 255 cap drops the same nodes every time. Loaders read all of these back as maps, so this is a pure reordering. --- src/Classes/CalcsTab.lua | 2 +- src/Classes/ConfigTab.lua | 4 +-- src/Classes/ItemsTab.lua | 3 ++- src/Classes/PassiveSpec.lua | 50 +++++++++++++++++++++++++++++++------ src/Modules/Build.lua | 2 +- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index 4ed3fe035d..6c1775afa8 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -237,7 +237,7 @@ function CalcsTabClass:Load(xml, dbFileName) end function CalcsTabClass:Save(xml) - for k, v in pairs(self.input) do + for k, v in pairsSortByKey(self.input) do local child = { elem = "Input", attrib = {name = k} } if type(v) == "number" then child.attrib.number = tostring(v) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index bbb71497b2..6c57a9c330 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -1006,7 +1006,7 @@ function ConfigTabClass:Save(xml) local child = { elem = "ConfigSet", attrib = { id = tostring(configSetId), title = configSet.title } } t_insert(xml, child) - for k, v in pairs(configSet.input) do + for k, v in pairsSortByKey(configSet.input) do if v ~= self:GetDefaultState(k, type(v)) then local node = { elem = "Input", attrib = { name = k } } if type(v) == "number" then @@ -1019,7 +1019,7 @@ function ConfigTabClass:Save(xml) t_insert(child, node) end end - for k, v in pairs(configSet.placeholder) do + for k, v in pairsSortByKey(configSet.placeholder) do local node = { elem = "Placeholder", attrib = { name = k } } if type(v) == "number" then node.attrib.number = tostring(v) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index d6fe6a271f..222855bced 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1371,7 +1371,8 @@ function ItemsTabClass:Save(xml) for _, itemSetId in ipairs(self.itemSetOrderList) do local itemSet = self.itemSets[itemSetId] local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet) } } - for slotName, slot in pairs(self.slots) do + for _, slot in ipairs(self.orderedSlots) do + local slotName = slot.slotName if not slot.parentSlot or itemSet[slotName].selItemId ~= 0 then if not slot.nodeId then t_insert(child, { elem = "Slot", attrib = { name = slotName, itemId = tostring(itemSet[slotName].selItemId), itemPbURL = itemSet[slotName].pbURL or "", active = itemSet[slotName].active and "true", note = itemSet[slotName].note }}) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 636204787e..30a76b185c 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -252,7 +252,13 @@ end function PassiveSpecClass:Save(xml) local allocNodeIdList = { } local weaponSets = {} - for nodeId, node in pairs(self.allocNodes) do + local sortedAllocNodeIds = { } + for nodeId in pairs(self.allocNodes) do + t_insert(sortedAllocNodeIds, nodeId) + end + table.sort(sortedAllocNodeIds) + for _, nodeId in ipairs(sortedAllocNodeIds) do + local node = self.allocNodes[nodeId] if not (node.isGrantedPassive and node.isFreeAllocate) then t_insert(allocNodeIdList, nodeId) end @@ -264,9 +270,14 @@ function PassiveSpecClass:Save(xml) t_insert(weaponSets[weaponSet], nodeId) end end + local masteryNodeIdList = { } + for mastery in pairs(self.masterySelections) do + t_insert(masteryNodeIdList, mastery) + end + table.sort(masteryNodeIdList) local masterySelections = { } - for mastery, effect in pairs(self.masterySelections) do - t_insert(masterySelections, "{"..mastery..","..effect.."}") + for _, mastery in ipairs(masteryNodeIdList) do + t_insert(masterySelections, "{"..mastery..","..self.masterySelections[mastery].."}") end local classInternalId = self.tree.classes[self.curClassId].integerId @@ -296,10 +307,15 @@ function PassiveSpecClass:Save(xml) }) if #weaponSets > 0 then - for weaponSet, nodes in pairs(weaponSets) do + local weaponSetNumbers = { } + for weaponSet in pairs(weaponSets) do + t_insert(weaponSetNumbers, weaponSet) + end + table.sort(weaponSetNumbers) + for _, weaponSet in ipairs(weaponSetNumbers) do t_insert(xml, { elem = "WeaponSet"..weaponSet, - attrib = { nodes = table.concat(nodes, ",") } + attrib = { nodes = table.concat(weaponSets[weaponSet], ",") } }) end end @@ -307,7 +323,13 @@ function PassiveSpecClass:Save(xml) local sockets = { elem = "Sockets" } - for nodeId, itemId in pairs(self.jewels) do + local socketNodeIdList = { } + for nodeId in pairs(self.jewels) do + t_insert(socketNodeIdList, nodeId) + end + table.sort(socketNodeIdList) + for _, nodeId in ipairs(socketNodeIdList) do + local itemId = self.jewels[nodeId] -- jewel socket contents should not be saved unless they contain a valid jewel if itemId > 0 then local socket = { elem = "Socket", attrib = { nodeId = tostring(nodeId), itemId = tostring(itemId) }} @@ -321,7 +343,13 @@ function PassiveSpecClass:Save(xml) } if self.hashOverrides then local strList, dexList, intList = { }, { }, { } - for nodeId, node in pairs(self.hashOverrides) do + local overrideNodeIdList = { } + for nodeId in pairs(self.hashOverrides) do + t_insert(overrideNodeIdList, nodeId) + end + table.sort(overrideNodeIdList) + for _, nodeId in ipairs(overrideNodeIdList) do + local node = self.hashOverrides[nodeId] if node.isAttribute then if node.dn == "Strength" then t_insert(strList, nodeId) @@ -616,7 +644,13 @@ function PassiveSpecClass:EncodeURL(prefix) local clusterNodeIds = {} local masteryNodeIds = {} - for id, node in pairs(self.allocNodes) do + local encodeNodeIdList = { } + for id in pairs(self.allocNodes) do + t_insert(encodeNodeIdList, id) + end + table.sort(encodeNodeIdList) + for _, id in ipairs(encodeNodeIdList) do + local node = self.allocNodes[id] if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" and id < 65536 and nodeCount < 255 then t_insert(a, m_floor(id / 256)) t_insert(a, id % 256) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index ef83aed47d..c6ba6674c6 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -2754,7 +2754,7 @@ function buildMode:SaveDB(fileName) end -- Call on all savers to save their data in their respective sections - for elem, saver in pairs(self.savers) do + for elem, saver in pairsSortByKey(self.savers) do local node = { elem = elem } saver:Save(node) t_insert(dbXML, node) From 76e91e99a09cad1c8306d256494dae1dfdd85e6a Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Thu, 10 Sep 2026 12:22:42 +1000 Subject: [PATCH 2/2] Iterate runes with ipairs --- src/Classes/ItemsTab.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 222855bced..4628337d26 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1383,7 +1383,7 @@ function ItemsTabClass:Save(xml) end end end - for slotName, _ in pairs(self.runeSlots) do + for _, slotName in ipairs(self.runeSlotOrder) do local runeName = (itemSet[slotName] and itemSet[slotName].runeName) or "None" local node = { elem = "RuneSlot", attrib = { slotName = slotName, runeName = runeName } } t_insert(child, node)