From 42608b5dfea26a6432f8f87b1588071e1eef0f9f Mon Sep 17 00:00:00 2001 From: Rasmus Kromann-Larsen Date: Wed, 9 Sep 2026 11:59:31 +0200 Subject: [PATCH] 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, and the node-id-keyed allocNodes, masterySelections, jewels 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) 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 | 34 +++++++++++++++++++++++++++++----- src/Modules/Build.lua | 2 +- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index 7d79e865d98..95386e5f20f 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -201,7 +201,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 3d1f007cd63..7a6bf0b95c3 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -967,7 +967,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 @@ -980,7 +980,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 0b3852aba58..3915e60b505 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1384,7 +1384,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.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" }}) else diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 3e645124071..4df48c41ebf 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -199,9 +199,15 @@ function PassiveSpecClass:Save(xml) for nodeId in pairs(self.allocNodes) do t_insert(allocNodeIdList, nodeId) end + table.sort(allocNodeIdList) + 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 xml.attrib = { title = self.title, @@ -223,7 +229,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) }} @@ -236,7 +248,13 @@ function PassiveSpecClass:Save(xml) elem = "Overrides" } if self.hashOverrides then - 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] local override = { elem = "Override", attrib = { nodeId = tostring(nodeId), icon = tostring(node.icon), activeEffectImage = tostring(node.activeEffectImage), dn = tostring(node.dn) } } for _, modLine in ipairs(node.sd) do t_insert(override, modLine) @@ -534,7 +552,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 3ad9d8eafff..1f4d810ad1a 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -2285,7 +2285,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)