@@ -1590,10 +1590,82 @@ function PassiveSpecClass:ReconnectNodeToClassStart(node)
15901590 end
15911591end
15921592
1593- function PassiveSpecClass :BuildClusterJewelGraphs ()
1593+ -- Initializes temporary lookup tables used when loading legacy (v1) cluster hashes.
1594+ -- Returns true when legacy conversion is active for this graph rebuild.
1595+ function PassiveSpecClass :BeginLegacyClusterHashConversion ()
15941596 local needsLegacyClusterHashConversion = (self .clusterHashFormatVersion or 2 ) < 2
15951597 self .legacyClusterNodeMap = needsLegacyClusterHashConversion and { } or nil
15961598 self .legacyClusterNodeMapReverse = needsLegacyClusterHashConversion and { } or nil
1599+ return needsLegacyClusterHashConversion
1600+ end
1601+
1602+ -- Legacy conversion updates node IDs while rebuilding cluster subgraphs.
1603+ -- This helper keeps forward and reverse mappings in sync.
1604+ function PassiveSpecClass :RegisterLegacyClusterNodeMap (legacyNodeId , currentNodeId )
1605+ if not self .legacyClusterNodeMap or not legacyNodeId or not currentNodeId then
1606+ return
1607+ end
1608+ self .legacyClusterNodeMap [legacyNodeId ] = currentNodeId
1609+ if self .legacyClusterNodeMapReverse then
1610+ self .legacyClusterNodeMapReverse [currentNodeId ] = legacyNodeId
1611+ end
1612+ end
1613+
1614+ -- Returns the remapped node ID when a valid legacy -> current cluster mapping exists.
1615+ function PassiveSpecClass :GetMappedClusterNodeId (nodeId )
1616+ local mappedNodeId = self .legacyClusterNodeMap and self .legacyClusterNodeMap [nodeId ]
1617+ if mappedNodeId and self .nodes [mappedNodeId ] then
1618+ return mappedNodeId
1619+ end
1620+ return nodeId
1621+ end
1622+
1623+ -- Applies legacy -> current remapping to both cluster allocations and socketed jewel ownership.
1624+ function PassiveSpecClass :ApplyLegacyClusterNodeRemap ()
1625+ if not self .legacyClusterNodeMap then
1626+ return
1627+ end
1628+
1629+ local convertedNodeIds = { }
1630+ local seenNodeIds = { }
1631+ for _ , nodeId in ipairs (self .allocSubgraphNodes ) do
1632+ nodeId = self :GetMappedClusterNodeId (nodeId )
1633+ if not seenNodeIds [nodeId ] then
1634+ seenNodeIds [nodeId ] = true
1635+ t_insert (convertedNodeIds , nodeId )
1636+ end
1637+ end
1638+ self .allocSubgraphNodes = convertedNodeIds
1639+
1640+ -- Legacy cluster socket IDs can be normal tree node IDs (< 65536), so they bypass allocSubgraphNodes.
1641+ -- Move any such allocations onto their mapped current cluster node IDs.
1642+ for legacyNodeId , currentNodeId in pairs (self .legacyClusterNodeMap ) do
1643+ if legacyNodeId ~= currentNodeId and self .allocNodes [legacyNodeId ] and self .nodes [currentNodeId ] then
1644+ self .allocNodes [legacyNodeId ].alloc = false
1645+ self .allocNodes [legacyNodeId ] = nil
1646+ if not seenNodeIds [currentNodeId ] then
1647+ seenNodeIds [currentNodeId ] = true
1648+ t_insert (self .allocSubgraphNodes , currentNodeId )
1649+ end
1650+ end
1651+ end
1652+
1653+ local convertedJewels = { }
1654+ for nodeId , itemId in pairs (self .jewels ) do
1655+ convertedJewels [self :GetMappedClusterNodeId (nodeId )] = itemId
1656+ end
1657+ self .jewels = convertedJewels
1658+ end
1659+
1660+ -- Finalizes cluster hash conversion state after each graph rebuild.
1661+ function PassiveSpecClass :EndLegacyClusterHashConversion ()
1662+ self .clusterHashFormatVersion = 2
1663+ self .legacyClusterNodeMap = nil
1664+ self .legacyClusterNodeMapReverse = nil
1665+ end
1666+
1667+ function PassiveSpecClass :BuildClusterJewelGraphs ()
1668+ local needsLegacyClusterHashConversion = self :BeginLegacyClusterHashConversion ()
15971669
15981670 -- Remove old subgraphs
15991671 for id , subGraph in pairs (self .subGraphs ) do
@@ -1639,43 +1711,8 @@ function PassiveSpecClass:BuildClusterJewelGraphs()
16391711 end
16401712 end
16411713
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
1714+ if needsLegacyClusterHashConversion then
1715+ self :ApplyLegacyClusterNodeRemap ()
16791716 end
16801717
16811718 -- (Re-)allocate subgraph nodes
@@ -1698,9 +1735,95 @@ function PassiveSpecClass:BuildClusterJewelGraphs()
16981735
16991736 -- Rebuild node search cache because the tree might have changed
17001737 self .build .treeTab .viewer .searchStrCached = " "
1701- self .clusterHashFormatVersion = 2
1702- self .legacyClusterNodeMap = nil
1703- self .legacyClusterNodeMapReverse = nil
1738+ self :EndLegacyClusterHashConversion ()
1739+ end
1740+
1741+ -- Finds a specific expansion socket entry within a passive-tree group.
1742+ function PassiveSpecClass :FindClusterSocket (group , index )
1743+ for _ , nodeId in ipairs (group .n ) do
1744+ local node = self .tree .nodes [tonumber (nodeId )]
1745+ if node .expansionJewel and node .expansionJewel .index == index then
1746+ return node
1747+ end
1748+ end
1749+ end
1750+
1751+ -- Legacy parser behavior downsized the proxy group while descending into nested clusters.
1752+ -- Reproducing that traversal lets us recover legacy socket IDs for migration.
1753+ function PassiveSpecClass :BuildLegacyProxyGroup (proxyGroup , expansionJewelSize , clusterSizeIndex )
1754+ local legacyGroup = proxyGroup
1755+ local groupSize = expansionJewelSize
1756+ local guard = 0
1757+ while clusterSizeIndex < groupSize and guard < 4 do
1758+ local socket = self :FindClusterSocket (legacyGroup , 1 ) or self :FindClusterSocket (legacyGroup , 0 )
1759+ if not socket then
1760+ break
1761+ end
1762+ local legacyProxyNode = self .tree .nodes [tonumber (socket .expansionJewel .proxy )]
1763+ if not legacyProxyNode or not legacyProxyNode .group then
1764+ break
1765+ end
1766+ legacyGroup = legacyProxyNode .group
1767+ groupSize = socket .expansionJewel .size
1768+ guard = guard + 1
1769+ end
1770+ return legacyGroup
1771+ end
1772+
1773+ -- Converts cluster orbit indices between different node-count spaces.
1774+ -- 12<->16 mappings reflect the 3.17 cluster export change; 6<->16 supports legacy nested mapping.
1775+ function PassiveSpecClass :TranslateClusterOrbitIndex (srcOidx , srcNodesPerOrbit , destNodesPerOrbit )
1776+ if srcNodesPerOrbit == destNodesPerOrbit then
1777+ return srcOidx
1778+ elseif srcNodesPerOrbit == 12 and destNodesPerOrbit == 16 then
1779+ return ({[0 ] = 0 , 1 , 3 , 4 , 5 , 7 , 8 , 9 , 11 , 12 , 13 , 15 })[srcOidx ]
1780+ elseif srcNodesPerOrbit == 16 and destNodesPerOrbit == 12 then
1781+ return ({[0 ] = 0 , 1 , 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 , 7 , 8 , 9 , 10 , 10 , 11 })[srcOidx ]
1782+ elseif srcNodesPerOrbit == 6 and destNodesPerOrbit == 16 then
1783+ return ({[0 ] = 0 , 3 , 5 , 8 , 11 , 13 })[srcOidx ]
1784+ elseif srcNodesPerOrbit == 16 and destNodesPerOrbit == 6 then
1785+ return ({[0 ] = 0 , 0 , 0 , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 5 , 5 , 5 })[srcOidx ]
1786+ else
1787+ -- there is no known case where this should happen...
1788+ launch :ShowErrMsg (" ^1Error: unexpected cluster jewel node counts %d -> %d" , srcNodesPerOrbit , destNodesPerOrbit )
1789+ -- ...but if a future patch adds one, this should end up only a little krangled, close enough for initial skill data imports:
1790+ return m_floor (srcOidx * destNodesPerOrbit / srcNodesPerOrbit )
1791+ end
1792+ end
1793+
1794+ -- Applies proxy orbit offsets and converts from cluster template indices into tree orbit-space indices.
1795+ function PassiveSpecClass :ApplyClusterOrbitIndexAdjustment (indicies , startOidx , clusterTotalIndicies , skillsPerOrbit )
1796+ for _ , node in pairs (indicies ) do
1797+ local correctedNodeOidxRelativeToClusterIndicies = (node .oidx + startOidx ) % clusterTotalIndicies
1798+ node .oidx = self :TranslateClusterOrbitIndex (correctedNodeOidxRelativeToClusterIndicies , clusterTotalIndicies , skillsPerOrbit )
1799+ end
1800+ end
1801+
1802+ -- Builds additional legacy node mappings by matching equivalent nodes in legacy and current orbit spaces.
1803+ function PassiveSpecClass :BuildLegacyClusterOrbitMappings (indicies , proxyNode , clusterTotalIndicies , skillsPerOrbit )
1804+ if not self .legacyClusterNodeMap then
1805+ return
1806+ end
1807+
1808+ local legacySkillsPerOrbit = self .tree .skillsPerOrbit [proxyNode .o + 1 ]
1809+ local legacyProxyNodeOidxRelativeToClusterIndicies = self :TranslateClusterOrbitIndex (proxyNode .oidx , legacySkillsPerOrbit , clusterTotalIndicies )
1810+ local legacyNodeIdsByOidx = { }
1811+ local currentNodeIdsByOidx = { }
1812+ for nodeIndex , node in pairs (indicies ) do
1813+ local legacyNodeOidxRelativeToClusterIndicies = (nodeIndex + legacyProxyNodeOidxRelativeToClusterIndicies ) % clusterTotalIndicies
1814+ local legacyNodeOidx = self :TranslateClusterOrbitIndex (legacyNodeOidxRelativeToClusterIndicies , clusterTotalIndicies , legacySkillsPerOrbit )
1815+ legacyNodeIdsByOidx [legacyNodeOidx ] = node .id
1816+
1817+ local currentNodeOidxRelativeToClusterIndicies = self :TranslateClusterOrbitIndex (node .oidx , skillsPerOrbit , clusterTotalIndicies )
1818+ local currentNodeOidxInLegacySkillsPerOrbit = self :TranslateClusterOrbitIndex (currentNodeOidxRelativeToClusterIndicies , clusterTotalIndicies , legacySkillsPerOrbit )
1819+ currentNodeIdsByOidx [currentNodeOidxInLegacySkillsPerOrbit ] = node .id
1820+ end
1821+ for oidx , legacyNodeId in pairs (legacyNodeIdsByOidx ) do
1822+ local currentNodeId = currentNodeIdsByOidx [oidx ]
1823+ if currentNodeId and legacyNodeId ~= currentNodeId then
1824+ self :RegisterLegacyClusterNodeMap (legacyNodeId , currentNodeId )
1825+ end
1826+ end
17041827end
17051828
17061829function PassiveSpecClass :BuildSubgraph (jewel , parentSocket , id , upSize , importedNodes , importedGroups )
@@ -1834,36 +1957,9 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
18341957 return
18351958 end
18361959
1837- local function findSocket (group , index )
1838- -- Find the given socket index in the group
1839- for _ , nodeId in ipairs (group .n ) do
1840- local node = self .tree .nodes [tonumber (nodeId )]
1841- if node .expansionJewel and node .expansionJewel .index == index then
1842- return node
1843- end
1844- end
1845- end
1846-
18471960 local legacyProxyGroup
18481961 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
1962+ legacyProxyGroup = self :BuildLegacyProxyGroup (proxyGroup , expansionJewel .size , clusterJewel .sizeIndex )
18671963 end
18681964
18691965 -- Initialise orbit flags
@@ -1915,7 +2011,7 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
19152011
19162012 local function makeJewel (nodeIndex , jewelIndex )
19172013 -- Look for the socket
1918- local socket = findSocket (proxyGroup , jewelIndex )
2014+ local socket = self : FindClusterSocket (proxyGroup , jewelIndex )
19192015 assert (socket , " Socket not found (ran out of sockets nani?)" )
19202016
19212017 -- Construct the new node
@@ -1934,12 +2030,9 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
19342030 indicies [nodeIndex ] = node
19352031
19362032 if legacyProxyGroup and self .legacyClusterNodeMap then
1937- local legacySocket = findSocket (legacyProxyGroup , jewelIndex )
2033+ local legacySocket = self : FindClusterSocket (legacyProxyGroup , jewelIndex )
19382034 if legacySocket then
1939- self .legacyClusterNodeMap [legacySocket .id ] = node .id
1940- if self .legacyClusterNodeMapReverse then
1941- self .legacyClusterNodeMapReverse [node .id ] = legacySocket .id
1942- end
2035+ self :RegisterLegacyClusterNodeMap (legacySocket .id , node .id )
19432036 end
19442037 end
19452038 end
@@ -2067,61 +2160,11 @@ function PassiveSpecClass:BuildSubgraph(jewel, parentSocket, id, upSize, importe
20672160 assert (indicies [0 ], " No entrance to subgraph" )
20682161 subGraph .entranceNode = indicies [0 ]
20692162
2070- -- The nodes' oidx values we just calculated are all relative to the totalIndicies properties of Data/ClusterJewels,
2071- -- but the PassiveTree rendering logic treats node.oidx as relative to the tree.skillsPerOrbit constants. Those used
2072- -- to be the same, but as of 3.17 they can differ, so we need to translate the ClusterJewels-relative indices into
2073- -- tree.skillsPerOrbit-relative indices before we invoke tree:ProcessNode or do math against proxyNode.oidx.
2074- --
2075- -- The specific 12<->16 mappings are derived from https://github.com/grindinggear/skilltree-export/blob/3.17.0/README.md
2076- local function translateOidx (srcOidx , srcNodesPerOrbit , destNodesPerOrbit )
2077- if srcNodesPerOrbit == destNodesPerOrbit then
2078- return srcOidx
2079- elseif srcNodesPerOrbit == 12 and destNodesPerOrbit == 16 then
2080- return ({[0 ] = 0 , 1 , 3 , 4 , 5 , 7 , 8 , 9 , 11 , 12 , 13 , 15 })[srcOidx ]
2081- elseif srcNodesPerOrbit == 16 and destNodesPerOrbit == 12 then
2082- 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 ]
2087- else
2088- -- there is no known case where this should happen...
2089- launch :ShowErrMsg (" ^1Error: unexpected cluster jewel node counts %d -> %d" , srcNodesPerOrbit , destNodesPerOrbit )
2090- -- ...but if a future patch adds one, this should end up only a little krangled, close enough for initial skill data imports:
2091- return m_floor (srcOidx * destNodesPerOrbit / srcNodesPerOrbit )
2092- end
2093- end
2094-
2163+ -- Convert from cluster-template index space into the tree's orbit index space.
20952164 local skillsPerOrbit = self .tree .skillsPerOrbit [clusterJewel .sizeIndex + 2 ]
20962165 local startOidx = data .clusterJewels .orbitOffsets [proxyNode .id ][clusterJewel .sizeIndex ]
2097- -- Translate oidx positioning to TreeData-relative values
2098- for _ , node in pairs (indicies ) do
2099- local correctedNodeOidxRelativeToClusterIndicies = (node .oidx + startOidx ) % clusterJewel .totalIndicies
2100- local correctedNodeOidxRelativeToTreeSkillsPerOrbit = translateOidx (correctedNodeOidxRelativeToClusterIndicies , clusterJewel .totalIndicies , skillsPerOrbit )
2101- node .oidx = correctedNodeOidxRelativeToTreeSkillsPerOrbit
2102- end
2103-
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
2166+ self :ApplyClusterOrbitIndexAdjustment (indicies , startOidx , clusterJewel .totalIndicies , skillsPerOrbit )
2167+ self :BuildLegacyClusterOrbitMappings (indicies , proxyNode , clusterJewel .totalIndicies , skillsPerOrbit )
21252168
21262169 -- Perform processing on nodes to calculate positions, parse mods, and other goodies
21272170 for _ , node in ipairs (subGraph .nodes ) do
0 commit comments