@@ -84,10 +84,15 @@ function PassiveSpecClass:Init(treeVersion, convert)
8484
8585 -- Cached highlight path for Split Personality jewels
8686 self .splitPersonalityPath = { }
87+
88+ -- Cluster hash format version used by saved builds; 2 is current.
89+ self .clusterHashFormatVersion = 2
8790end
8891
8992function PassiveSpecClass :Load (xml , dbFileName )
9093 self .title = xml .attrib .title
94+ -- Specs without this attribute predate the hash-fix migration and are treated as legacy.
95+ self .clusterHashFormatVersion = tonumber (xml .attrib .clusterHashFormatVersion ) or (xml .attrib .nodes and 1 or 2 )
9196 local url
9297 for _ , node in pairs (xml ) do
9398 if type (node ) == " table" then
@@ -190,6 +195,7 @@ function PassiveSpecClass:Save(xml)
190195 xml .attrib = {
191196 title = self .title ,
192197 treeVersion = self .treeVersion ,
198+ clusterHashFormatVersion = tostring (self .clusterHashFormatVersion or 2 ),
193199 -- New format
194200 classId = tostring (self .curClassId ),
195201 ascendClassId = tostring (self .curAscendClassId ),
@@ -868,6 +874,17 @@ function PassiveSpecClass:GetJewel(itemId)
868874 return item
869875end
870876
877+ function PassiveSpecClass :GetSocketedJewel (nodeId )
878+ local itemId = self .jewels [nodeId ]
879+ if (not itemId or itemId == 0 ) and self .legacyClusterNodeMapReverse then
880+ local legacyNodeId = self .legacyClusterNodeMapReverse [nodeId ]
881+ if legacyNodeId then
882+ itemId = self .jewels [legacyNodeId ]
883+ end
884+ end
885+ return self :GetJewel (itemId )
886+ end
887+
871888-- Perform a breadth-first search of the tree, starting from this node, and determine if it is the closest node to any other nodes
872889function PassiveSpecClass :BuildPathFromNode (root )
873890 root .pathDist = 0
@@ -1574,6 +1591,10 @@ function PassiveSpecClass:ReconnectNodeToClassStart(node)
15741591end
15751592
15761593function PassiveSpecClass :BuildClusterJewelGraphs ()
1594+ local needsLegacyClusterHashConversion = (self .clusterHashFormatVersion or 2 ) < 2
1595+ self .legacyClusterNodeMap = needsLegacyClusterHashConversion and { } or nil
1596+ self .legacyClusterNodeMapReverse = needsLegacyClusterHashConversion and { } or nil
1597+
15771598 -- Remove old subgraphs
15781599 for id , subGraph in pairs (self .subGraphs ) do
15791600 for _ , node in ipairs (subGraph .nodes ) do
@@ -1611,13 +1632,52 @@ function PassiveSpecClass:BuildClusterJewelGraphs()
16111632 end
16121633 for nodeId in pairs (self .tree .sockets ) do
16131634 local node = self .tree .nodes [nodeId ]
1614- local jewel = self :GetJewel ( self . jewels [ nodeId ] )
1635+ local jewel = self :GetSocketedJewel ( nodeId )
16151636 if node and node .expansionJewel and node .expansionJewel .size == 2 and jewel and jewel .jewelData .clusterJewelValid then
16161637 -- This is a Large Jewel Socket, and it has a cluster jewel in it
16171638 self :BuildSubgraph (jewel , self .nodes [nodeId ], nil , nil , importedNodes , importedGroups )
16181639 end
16191640 end
16201641
1642+ if needsLegacyClusterHashConversion and self .legacyClusterNodeMap then
1643+ local convertedNodeIds = { }
1644+ local seenNodeIds = { }
1645+ for _ , nodeId in ipairs (self .allocSubgraphNodes ) do
1646+ local convertedNodeId = self .legacyClusterNodeMap [nodeId ]
1647+ if convertedNodeId and self .nodes [convertedNodeId ] then
1648+ nodeId = convertedNodeId
1649+ end
1650+ if not seenNodeIds [nodeId ] then
1651+ seenNodeIds [nodeId ] = true
1652+ t_insert (convertedNodeIds , nodeId )
1653+ end
1654+ end
1655+ self .allocSubgraphNodes = convertedNodeIds
1656+
1657+ -- Legacy cluster socket IDs can be normal tree node IDs (< 65536), so they bypass allocSubgraphNodes.
1658+ -- Move any such allocations onto their mapped current cluster node IDs.
1659+ for legacyNodeId , currentNodeId in pairs (self .legacyClusterNodeMap ) do
1660+ if legacyNodeId ~= currentNodeId and self .allocNodes [legacyNodeId ] and self .nodes [currentNodeId ] then
1661+ self .allocNodes [legacyNodeId ].alloc = false
1662+ self .allocNodes [legacyNodeId ] = nil
1663+ if not seenNodeIds [currentNodeId ] then
1664+ seenNodeIds [currentNodeId ] = true
1665+ t_insert (self .allocSubgraphNodes , currentNodeId )
1666+ end
1667+ end
1668+ end
1669+
1670+ local convertedJewels = { }
1671+ for nodeId , itemId in pairs (self .jewels ) do
1672+ local convertedNodeId = self .legacyClusterNodeMap [nodeId ]
1673+ if convertedNodeId and self .nodes [convertedNodeId ] then
1674+ nodeId = convertedNodeId
1675+ end
1676+ convertedJewels [nodeId ] = itemId
1677+ end
1678+ self .jewels = convertedJewels
1679+ end
1680+
16211681 -- (Re-)allocate subgraph nodes
16221682 for _ , nodeId in ipairs (self .allocSubgraphNodes ) do
16231683 local node = self .nodes [nodeId ]
@@ -1638,6 +1698,9 @@ function PassiveSpecClass:BuildClusterJewelGraphs()
16381698
16391699 -- Rebuild node search cache because the tree might have changed
16401700 self .build .treeTab .viewer .searchStrCached = " "
1701+ self .clusterHashFormatVersion = 2
1702+ self .legacyClusterNodeMap = nil
1703+ self .legacyClusterNodeMapReverse = nil
16411704end
16421705
16431706function PassiveSpecClass :BuildSubgraph (jewel , parentSocket , id , upSize , importedNodes , importedGroups )
@@ -1781,6 +1844,28 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
17811844 end
17821845 end
17831846
1847+ local legacyProxyGroup
1848+ if self .legacyClusterNodeMap then
1849+ -- Legacy builds used proxy-group downsizing; reproduce it so old socket IDs can be mapped.
1850+ local legacyGroup = proxyGroup
1851+ local groupSize = expansionJewel .size
1852+ local guard = 0
1853+ while clusterJewel .sizeIndex < groupSize and guard < 4 do
1854+ local socket = findSocket (legacyGroup , 1 ) or findSocket (legacyGroup , 0 )
1855+ if not socket then
1856+ break
1857+ end
1858+ local legacyProxyNode = self .tree .nodes [tonumber (socket .expansionJewel .proxy )]
1859+ if not legacyProxyNode or not legacyProxyNode .group then
1860+ break
1861+ end
1862+ legacyGroup = legacyProxyNode .group
1863+ groupSize = socket .expansionJewel .size
1864+ guard = guard + 1
1865+ end
1866+ legacyProxyGroup = legacyGroup
1867+ end
1868+
17841869 -- Initialise orbit flags
17851870 local nodeOrbit = clusterJewel .sizeIndex + 1
17861871 subGraph .group .oo [nodeOrbit ] = true
@@ -1847,6 +1932,16 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
18471932 }
18481933 t_insert (subGraph .nodes , node )
18491934 indicies [nodeIndex ] = node
1935+
1936+ if legacyProxyGroup and self .legacyClusterNodeMap then
1937+ local legacySocket = findSocket (legacyProxyGroup , jewelIndex )
1938+ if legacySocket then
1939+ self .legacyClusterNodeMap [legacySocket .id ] = node .id
1940+ if self .legacyClusterNodeMapReverse then
1941+ self .legacyClusterNodeMapReverse [node .id ] = legacySocket .id
1942+ end
1943+ end
1944+ end
18501945 end
18511946
18521947 -- First pass: sockets
@@ -1985,6 +2080,10 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
19852080 return ({[0 ] = 0 , 1 , 3 , 4 , 5 , 7 , 8 , 9 , 11 , 12 , 13 , 15 })[srcOidx ]
19862081 elseif srcNodesPerOrbit == 16 and destNodesPerOrbit == 12 then
19872082 return ({[0 ] = 0 , 1 , 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 , 7 , 8 , 9 , 10 , 10 , 11 })[srcOidx ]
2083+ elseif srcNodesPerOrbit == 6 and destNodesPerOrbit == 16 then
2084+ return ({[0 ] = 0 , 3 , 5 , 8 , 11 , 13 })[srcOidx ]
2085+ elseif srcNodesPerOrbit == 16 and destNodesPerOrbit == 6 then
2086+ return ({[0 ] = 0 , 0 , 0 , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 5 , 5 , 5 })[srcOidx ]
19882087 else
19892088 -- there is no known case where this should happen...
19902089 launch :ShowErrMsg (" ^1Error: unexpected cluster jewel node counts %d -> %d" , srcNodesPerOrbit , destNodesPerOrbit )
@@ -1997,12 +2096,33 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
19972096 local startOidx = data .clusterJewels .orbitOffsets [proxyNode .id ][clusterJewel .sizeIndex ]
19982097 -- Translate oidx positioning to TreeData-relative values
19992098 for _ , node in pairs (indicies ) do
2000- local startOidxRelativeToClusterIndicies = translateOidx (startOidx , skillsPerOrbit , clusterJewel .totalIndicies )
20012099 local correctedNodeOidxRelativeToClusterIndicies = (node .oidx + startOidx ) % clusterJewel .totalIndicies
20022100 local correctedNodeOidxRelativeToTreeSkillsPerOrbit = translateOidx (correctedNodeOidxRelativeToClusterIndicies , clusterJewel .totalIndicies , skillsPerOrbit )
20032101 node .oidx = correctedNodeOidxRelativeToTreeSkillsPerOrbit
20042102 end
20052103
2104+ if self .legacyClusterNodeMap then
2105+ local legacySkillsPerOrbit = self .tree .skillsPerOrbit [proxyNode .o + 1 ]
2106+ local legacyProxyNodeOidxRelativeToClusterIndicies = translateOidx (proxyNode .oidx , legacySkillsPerOrbit , clusterJewel .totalIndicies )
2107+ local legacyNodeIdsByOidx = { }
2108+ local currentNodeIdsByOidx = { }
2109+ for nodeIndex , node in pairs (indicies ) do
2110+ local legacyNodeOidxRelativeToClusterIndicies = (nodeIndex + legacyProxyNodeOidxRelativeToClusterIndicies ) % clusterJewel .totalIndicies
2111+ local legacyNodeOidx = translateOidx (legacyNodeOidxRelativeToClusterIndicies , clusterJewel .totalIndicies , legacySkillsPerOrbit )
2112+ legacyNodeIdsByOidx [legacyNodeOidx ] = node .id
2113+ local currentNodeOidxRelativeToClusterIndicies = translateOidx (node .oidx , skillsPerOrbit , clusterJewel .totalIndicies )
2114+ local currentNodeOidxInLegacySkillsPerOrbit = translateOidx (currentNodeOidxRelativeToClusterIndicies , clusterJewel .totalIndicies , legacySkillsPerOrbit )
2115+ currentNodeIdsByOidx [currentNodeOidxInLegacySkillsPerOrbit ] = node .id
2116+ end
2117+ for oidx , legacyNodeId in pairs (legacyNodeIdsByOidx ) do
2118+ local currentNodeId = currentNodeIdsByOidx [oidx ]
2119+ if currentNodeId and legacyNodeId ~= currentNodeId then
2120+ self .legacyClusterNodeMap [legacyNodeId ] = currentNodeId
2121+ self .legacyClusterNodeMapReverse [currentNodeId ] = legacyNodeId
2122+ end
2123+ end
2124+ end
2125+
20062126 -- Perform processing on nodes to calculate positions, parse mods, and other goodies
20072127 for _ , node in ipairs (subGraph .nodes ) do
20082128 node .linked = { }
@@ -2041,7 +2161,7 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
20412161 end
20422162 if node .type == " Socket" then
20432163 -- Recurse to smaller jewels
2044- local jewel = self :GetJewel ( self . jewels [ node .id ] )
2164+ local jewel = self :GetSocketedJewel ( node .id )
20452165 if jewel and jewel .jewelData .clusterJewelValid then
20462166 self :BuildSubgraph (jewel , node , id , upSize , importedNodes , importedGroups )
20472167 end
0 commit comments