Guest User

PassiveSpec.lua

a guest
Feb 3rd, 2020
691
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.43 KB | None | 0 0
  1. -- Path of Building
  2. --
  3. -- Class: Passive Spec
  4. -- Passive tree spec class.
  5. -- Manages node allocation and pathing for a given passive spec
  6. --
  7. local pairs = pairs
  8. local ipairs = ipairs
  9. local t_insert = table.insert
  10. local m_min = math.min
  11. local m_max = math.max
  12. local m_floor = math.floor
  13.  
  14. local nodeMigrate32_33 = {
  15. [17788] = 38129,
  16. [38807] = 63639,
  17. [5607] = 62069,
  18. [61547] = 31583,
  19. [29619] = 1600,
  20. }
  21.  
  22. local PassiveSpecClass = newClass("PassiveSpec", "UndoHandler", function(self, build, treeVersion)
  23. self.UndoHandler()
  24.  
  25. self.build = build
  26. self.treeVersion = treeVersion
  27. self.tree = main.tree[treeVersion]
  28.  
  29. -- Make a local copy of the passive tree that we can modify
  30. self.nodes = { }
  31. for _, treeNode in pairs(self.tree.nodes) do
  32. self.nodes[treeNode.id] = setmetatable({
  33. linked = { },
  34. power = { }
  35. }, treeNode.meta)
  36. end
  37. for id, node in pairs(self.nodes) do
  38. for _, otherId in ipairs(node.linkedId) do
  39. t_insert(node.linked, self.nodes[otherId])
  40. end
  41. end
  42.  
  43. -- List of currently allocated nodes
  44. -- Keys are node IDs, values are nodes
  45. self.allocNodes = { }
  46.  
  47. -- Table of jewels equipped in this tree
  48. -- Keys are node IDs, values are items
  49. self.jewels = { }
  50.  
  51. self:SelectClass(0)
  52. end)
  53.  
  54. function PassiveSpecClass:Load(xml, dbFileName)
  55. local url
  56. self.title = xml.attrib.title
  57. for _, node in pairs(xml) do
  58. if type(node) == "table" then
  59. if node.elem == "URL" then
  60. if type(node[1]) ~= "string" then
  61. launch:ShowErrMsg("^1Error parsing '%s': 'URL' element missing content", dbFileName)
  62. return true
  63. end
  64. url = node[1]
  65. elseif node.elem == "Sockets" then
  66. for _, child in ipairs(node) do
  67. if child.elem == "Socket" then
  68. if not child.attrib.nodeId then
  69. launch:ShowErrMsg("^1Error parsing '%s': 'Socket' element missing 'nodeId' attribute", dbFileName)
  70. return true
  71. end
  72. if not child.attrib.itemId then
  73. launch:ShowErrMsg("^1Error parsing '%s': 'Socket' element missing 'itemId' attribute", dbFileName)
  74. return true
  75. end
  76. self.jewels[tonumber(child.attrib.nodeId)] = tonumber(child.attrib.itemId)
  77. end
  78. end
  79. end
  80. end
  81. end
  82. if url then
  83. self:DecodeURL(url)
  84. end
  85. self:ResetUndo()
  86. end
  87.  
  88. function PassiveSpecClass:Save(xml)
  89. xml.attrib = {
  90. title = self.title,
  91. treeVersion = self.treeVersion,
  92. }
  93. t_insert(xml, {
  94. elem = "URL",
  95. [1] = self:EncodeURL("https://www.pathofexile.com/passive-skill-tree/")
  96. })
  97. local sockets = {
  98. elem = "Sockets"
  99. }
  100. for nodeId, itemId in pairs(self.jewels) do
  101. t_insert(sockets, { elem = "Socket", attrib = { nodeId = tostring(nodeId), itemId = tostring(itemId) } })
  102. end
  103. t_insert(xml, sockets)
  104. self.modFlag = false
  105. end
  106.  
  107. function PassiveSpecClass:MigrateNodeId(nodeId)
  108. if self.build.targetVersion == "3_0" then
  109. -- Migration for 3.2 -> 3.3
  110. return nodeMigrate32_33[nodeId] or nodeId
  111. end
  112. return nodeId
  113. end
  114.  
  115. -- Import passive spec from the provided class IDs and node hash list
  116. function PassiveSpecClass:ImportFromNodeList(classId, ascendClassId, hashList)
  117. self:ResetNodes()
  118. self:SelectClass(classId)
  119. for _, id in pairs(hashList) do
  120. id = self:MigrateNodeId(id)
  121. local node = self.nodes[id]
  122. if node then
  123. node.alloc = true
  124. self.allocNodes[id] = node
  125. end
  126. end
  127. self:SelectAscendClass(ascendClassId)
  128. end
  129.  
  130. -- Decode the given passive tree URL
  131. -- Supports both the official skill tree links as well as PoE Planner links
  132. function PassiveSpecClass:DecodeURL(url)
  133. local b = common.base64.decode(url:gsub("^.+/",""):gsub("-","+"):gsub("_","/"))
  134. if not b or #b < 6 then
  135. return "Invalid tree link (unrecognised format)"
  136. end
  137. local classId, ascendClassId, bandits, nodes
  138. if b:byte(1) == 0 and b:byte(2) == 2 then
  139. -- Hold on to your headgear, it looks like a PoE Planner link
  140. -- Let's grab a scalpel and start peeling back the 50 layers of base 64 encoding
  141. local treeLinkLen = b:byte(4) * 256 + b:byte(5)
  142. local treeLink = b:sub(6, 6 + treeLinkLen - 1)
  143. b = common.base64.decode(treeLink:gsub("^.+/",""):gsub("-","+"):gsub("_","/"))
  144. classId = b:byte(3)
  145. ascendClassId = b:byte(4)
  146. bandits = b:byte(5)
  147. nodes = b:sub(8, -1)
  148. elseif b:byte(1) == 0 and b:byte(2) == 4 then
  149. -- PoE Planner version 4
  150. -- Now with 50% fewer layers of base 64 encoding
  151. classId = b:byte(6) % 16
  152. ascendClassId = m_floor(b:byte(6) / 16)
  153. bandits = b:byte(7)
  154. local numNodes = b:byte(8) * 256 + b:byte(9)
  155. nodes = b:sub(10, 10 + numNodes * 2 - 1)
  156. else
  157. local ver = b:byte(1) * 16777216 + b:byte(2) * 65536 + b:byte(3) * 256 + b:byte(4)
  158. if ver > 4 then
  159. return "Invalid tree link (unknown version number '"..ver.."')"
  160. end
  161. classId = b:byte(5)
  162. ascendClassId = 0--(ver >= 4) and b:byte(6) or 0 -- This value would be reliable if the developer of a certain online skill tree planner *cough* PoE Planner *cough* hadn't bollocked up
  163. -- the generation of the official tree URL. The user would most likely import the PoE Planner URL instead but that can't be relied upon.
  164. nodes = b:sub(ver >= 4 and 8 or 7, -1)
  165. end
  166. if not self.tree.classes[classId] then
  167. return "Invalid tree link (bad class ID '"..classId.."')"
  168. end
  169. self:ResetNodes()
  170. self:SelectClass(classId)
  171. for i = 1, #nodes - 1, 2 do
  172. local id = self:MigrateNodeId(nodes:byte(i) * 256 + nodes:byte(i + 1))
  173. local node = self.nodes[id]
  174. if node then
  175. node.alloc = true
  176. self.allocNodes[id] = node
  177. if ascendClassId == 0 and node.ascendancyName then
  178. -- Just guess the ascendancy class based on the allocated nodes
  179. ascendClassId = self.tree.ascendNameMap[node.ascendancyName].ascendClassId
  180. end
  181. end
  182. end
  183. self:SelectAscendClass(ascendClassId)
  184. if bandits then
  185. -- Decode bandits from PoEPlanner
  186. local lookup = { [0] = "None", "Alira", "Kraityn", "Oak" }
  187. self.build.banditNormal = lookup[bandits % 4]
  188. self.build.banditCruel = lookup[m_floor(bandits / 4) % 4]
  189. self.build.banditMerciless = lookup[m_floor(bandits / 16) % 4]
  190. end
  191. end
  192.  
  193. -- Encodes the current spec into a URL, using the official skill tree's format
  194. -- Prepends the URL with an optional prefix
  195. function PassiveSpecClass:EncodeURL(prefix)
  196. local a = { 0, 0, 0, 4, self.curClassId, self.curAscendClassId, 0 }
  197. for id, node in pairs(self.allocNodes) do
  198. if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then
  199. t_insert(a, m_floor(id / 256))
  200. t_insert(a, id % 256)
  201. end
  202. end
  203. return (prefix or "")..common.base64.encode(string.char(unpack(a))):gsub("+","-"):gsub("/","_")
  204. end
  205.  
  206. -- Change the current class, preserving currently allocated nodes if they connect to the new class's starting node
  207. function PassiveSpecClass:SelectClass(classId)
  208. if self.curClassId then
  209. -- Deallocate the current class's starting node
  210. local oldStartNodeId = self.curClass.startNodeId
  211. self.nodes[oldStartNodeId].alloc = false
  212. self.allocNodes[oldStartNodeId] = nil
  213. end
  214.  
  215. self.curClassId = classId
  216. local class = self.tree.classes[classId]
  217. self.curClass = class
  218. self.curClassName = class.name
  219.  
  220. -- Allocate the new class's starting node
  221. local startNode = self.nodes[class.startNodeId]
  222. startNode.alloc = true
  223. self.allocNodes[startNode.id] = startNode
  224.  
  225. -- Reset the ascendancy class
  226. -- This will also rebuild the node paths and dependancies
  227. self:SelectAscendClass(0)
  228. end
  229.  
  230. function PassiveSpecClass:SelectAscendClass(ascendClassId)
  231. self.curAscendClassId = ascendClassId
  232. local ascendClass = self.curClass.classes[ascendClassId] or self.curClass.classes[0]
  233. self.curAscendClass = ascendClass
  234. self.curAscendClassName = ascendClass.name
  235.  
  236. -- Deallocate any allocated ascendancy nodes that don't belong to the new ascendancy class
  237. for id, node in pairs(self.allocNodes) do
  238. if node.ascendancyName and node.ascendancyName ~= ascendClass.name then
  239. node.alloc = false
  240. self.allocNodes[id] = nil
  241. end
  242. end
  243.  
  244. if ascendClass.startNodeId then
  245. -- Allocate the new ascendancy class's start node
  246. local startNode = self.nodes[ascendClass.startNodeId]
  247. startNode.alloc = true
  248. self.allocNodes[startNode.id] = startNode
  249. end
  250.  
  251. -- Rebuild all the node paths and dependancies
  252. self:BuildAllDependsAndPaths()
  253. end
  254.  
  255. -- Determines if the given class's start node is connected to the current class's start node
  256. -- Attempts to find a path between the nodes which doesn't pass through any ascendancy nodes (i.e Ascendant)
  257. function PassiveSpecClass:IsClassConnected(classId)
  258. for _, other in ipairs(self.nodes[self.tree.classes[classId].startNodeId].linked) do
  259. -- For each of the nodes to which the given class's start node connects...
  260. if other.alloc then
  261. -- If the node is allocated, try to find a path back to the current class's starting node
  262. other.visited = true
  263. local visited = { }
  264. local found = self:FindStartFromNode(other, visited, true)
  265. for i, n in ipairs(visited) do
  266. n.visited = false
  267. end
  268. other.visited = false
  269. if found then
  270. -- Found a path, so the given class's start node is definately connected to the current class's start node
  271. -- There might still be nodes which are connected to the current tree by an entirely different path though
  272. -- E.g via Ascendant or by connecting to another "first passive node"
  273. return true
  274. end
  275. end
  276. end
  277. return false
  278. end
  279.  
  280. -- Clear the allocated status of all non-class-start nodes
  281. function PassiveSpecClass:ResetNodes()
  282. for id, node in pairs(self.nodes) do
  283. if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then
  284. node.alloc = false
  285. self.allocNodes[id] = nil
  286. end
  287. end
  288. end
  289.  
  290. -- Allocate the given node, if possible, and all nodes along the path to the node
  291. -- An alternate path to the node may be provided, otherwise the default path will be used
  292. -- The path must always contain the given node, as will be the case for the default path
  293. function PassiveSpecClass:AllocNode(node, altPath)
  294. if not node.path then
  295. -- Node cannot be connected to the tree as there is no possible path
  296. return
  297. end
  298.  
  299. -- Allocate all nodes along the path
  300. if node.dependsOnIntuitiveLeap then
  301. node.alloc = true
  302. self.allocNodes[node.id] = node
  303. else
  304. for _, pathNode in ipairs(altPath or node.path) do
  305. pathNode.alloc = true
  306. self.allocNodes[pathNode.id] = pathNode
  307. end
  308. end
  309.  
  310. if node.isMultipleChoiceOption then
  311. -- For multiple choice passives, make sure no other choices are allocated
  312. local parent = node.linked[1]
  313. for _, optNode in ipairs(parent.linked) do
  314. if optNode.isMultipleChoiceOption and optNode.alloc and optNode ~= node then
  315. optNode.alloc = false
  316. self.allocNodes[optNode.id] = nil
  317. end
  318. end
  319. end
  320.  
  321. -- Rebuild all dependancies and paths for all allocated nodes
  322. self:BuildAllDependsAndPaths()
  323. end
  324.  
  325. function PassiveSpecClass:AllocSingleNode(node)
  326. node.alloc = true
  327. node.dependsOnIntuitiveLeap = true
  328. self.allocNodes[node.id] = node
  329.  
  330. -- Rebuild all dependancies and paths for all allocated nodes
  331. self:BuildAllDependsAndPaths()
  332. end
  333.  
  334. -- Deallocate the given node, and all nodes which depend on it (i.e which are only connected to the tree through this node)
  335. function PassiveSpecClass:DeallocNode(node)
  336. for _, depNode in ipairs(node.depends) do
  337. depNode.alloc = false
  338. self.allocNodes[depNode.id] = nil
  339. end
  340.  
  341. -- Rebuild all paths and dependancies for all allocated nodes
  342. self:BuildAllDependsAndPaths()
  343. end
  344.  
  345. -- Count the number of allocated nodes and allocated ascendancy nodes
  346. function PassiveSpecClass:CountAllocNodes()
  347. local used, ascUsed, sockets = 0, 0, 0
  348. for _, node in pairs(self.allocNodes) do
  349. if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then
  350. if node.ascendancyName then
  351. if not node.isMultipleChoiceOption then
  352. ascUsed = ascUsed + 1
  353. end
  354. else
  355. used = used + 1
  356. end
  357. if node.type == "Socket" then
  358. sockets = sockets + 1
  359. end
  360. end
  361. end
  362. return used, ascUsed, sockets
  363. end
  364.  
  365. -- Attempt to find a class start node starting from the given node
  366. -- Unless noAscent == true it will also look for an ascendancy class start node
  367. function PassiveSpecClass:FindStartFromNode(node, visited, noAscend)
  368. -- Mark the current node as visited so we don't go around in circles
  369. node.visited = true
  370. t_insert(visited, node)
  371.  
  372. -- For each node which is connected to this one, check if...
  373. for _, other in ipairs(node.linked) do
  374. -- Either:
  375. -- - the other node is a start node, or
  376. -- - there is a path to a start node through the other node which didn't pass through any nodes which have already been visited
  377. local startIndex = #visited + 1
  378. if other.alloc and
  379. (other.type == "ClassStart" or other.type == "AscendClassStart" or
  380. (not other.visited and self:FindStartFromNode(other, visited, noAscend))
  381. ) then
  382. if node.ascendancyName and not other.ascendancyName then
  383. -- Pathing out of Ascendant, un-visit the outside nodes
  384. for i = startIndex, #visited do
  385. visited[i].visited = false
  386. visited[i] = nil
  387. end
  388. elseif not noAscend or other.type ~= "AscendClassStart" then
  389. return true
  390. end
  391. end
  392. end
  393. end
  394.  
  395. -- Perform a breadth-first search of the tree, starting from this node, and determine if it is the closest node to any other nodes
  396. function PassiveSpecClass:BuildPathFromNode(root)
  397. root.pathDist = 0
  398. root.path = { }
  399. local queue = { root }
  400. local o, i = 1, 2 -- Out, in
  401. while o < i do
  402. -- Nodes are processed in a queue, until there are no nodes left
  403. -- All nodes that are 1 node away from the root will be processed first, then all nodes that are 2 nodes away, etc
  404. local node = queue[o]
  405. o = o + 1
  406. local curDist = node.pathDist + 1
  407. -- Iterate through all nodes that are connected to this one
  408. for _, other in ipairs(node.linked) do
  409. -- Paths must obey two rules:
  410. -- 1. They must not pass through class or ascendancy class start nodes (but they can start from such nodes)
  411. -- 2. They cannot pass between different ascendancy classes or between an ascendancy class and the main tree
  412. -- The one exception to that rule is that a path may start from an ascendancy node and pass into the main tree
  413. -- This permits pathing from the Ascendant 'Path of the X' nodes into the respective class start areas
  414. if other.type ~= "ClassStart" and other.type ~= "AscendClassStart" and other.pathDist > curDist and (node.ascendancyName == other.ascendancyName or (curDist == 1 and not other.ascendancyName)) then
  415. -- The shortest path to the other node is through the current node
  416. other.pathDist = curDist
  417. other.path = wipeTable(other.path)
  418. other.path[1] = other
  419. for i, n in ipairs(node.path) do
  420. other.path[i+1] = n
  421. end
  422. -- Add the other node to the end of the queue
  423. queue[i] = other
  424. i = i + 1
  425. end
  426. end
  427. end
  428. end
  429.  
  430. -- Rebuilds dependancies and paths for all nodes
  431. function PassiveSpecClass:BuildAllDependsAndPaths()
  432. -- This table will keep track of which nodes have been visited during each path-finding attempt
  433. local visited = { }
  434.  
  435. -- Check all nodes for other nodes which depend on them (i.e are only connected to the tree through that node)
  436. for id, node in pairs(self.nodes) do
  437. node.depends = wipeTable(node.depends)
  438. --node.dependsOnIntuitiveLeap = false
  439.  
  440. if node.type ~= "ClassStart" then
  441. for nodeId, itemId in pairs(self.jewels) do
  442. if self.allocNodes[nodeId] and self.nodes[nodeId].nodesInRadius[1][node.id] then
  443. if itemId ~= 0 and self.build.itemsTab.items[itemId] and self.build.itemsTab.items[itemId].jewelData and self.build.itemsTab.items[itemId].jewelData.intuitiveLeap then
  444. -- This node depends on Intuitive Leap
  445. -- This flag:
  446. -- 1. Prevents generation of paths from this node
  447. -- 2. Prevents this node from being deallocted via dependancy
  448. -- 3. Prevents allocation of path nodes when this node is being allocated
  449. node.dependsOnIntuitiveLeap = true
  450. break
  451. end
  452. end
  453. end
  454. end
  455. if node.alloc then
  456. node.depends[1] = node -- All nodes depend on themselves
  457. end
  458. end
  459. for id, node in pairs(self.allocNodes) do
  460. node.visited = true
  461.  
  462. local anyStartFound = (node.type == "ClassStart" or node.type == "AscendClassStart" or node.dependsOnIntuitiveLeap == true)
  463. for _, other in ipairs(node.linked) do
  464. if other.alloc and not isValueInArray(node.depends, other) then
  465. -- The other node is allocated and isn't already dependant on this node, so try and find a path to a start node through it
  466. if other.type == "ClassStart" or other.type == "AscendClassStart" then
  467. -- Well that was easy!
  468. anyStartFound = true
  469. elseif self:FindStartFromNode(other, visited) then
  470. -- We found a path through the other node, therefore the other node cannot be dependant on this node
  471. anyStartFound = true
  472. for i, n in ipairs(visited) do
  473. n.visited = false
  474. visited[i] = nil
  475. end
  476. else
  477. -- No path was found, so all the nodes visited while trying to find the path must be dependant on this node
  478. for i, n in ipairs(visited) do
  479. if not n.dependsOnIntuitiveLeap then
  480. t_insert(node.depends, n)
  481. end
  482. n.visited = false
  483. visited[i] = nil
  484. end
  485. end
  486. end
  487. end
  488. node.visited = false
  489. if not anyStartFound then
  490. -- No start nodes were found through ANY nodes
  491. -- Therefore this node and all nodes depending on it are orphans and should be pruned
  492. for _, depNode in ipairs(node.depends) do
  493. local prune = true
  494. for nodeId, itemId in pairs(self.jewels) do
  495. if self.allocNodes[nodeId] and self.nodes[nodeId].nodesInRadius[1][depNode.id] then
  496. if itemId ~= 0 and (not self.build.itemsTab.items[itemId] or (self.build.itemsTab.items[itemId].jewelData and self.build.itemsTab.items[itemId].jewelData.intuitiveLeap)) or depNode.dependsOnIntuitiveLeap then
  497. -- Hold off on the pruning; this node is within the radius of a jewel that is or could be Intuitive Leap
  498. prune = false
  499. t_insert(self.nodes[nodeId].depends, depNode)
  500. break
  501. end
  502. end
  503. end
  504. if prune then
  505. depNode.alloc = false
  506. self.allocNodes[depNode.id] = nil
  507. end
  508. end
  509. end
  510. end
  511.  
  512. -- Reset and rebuild all node paths
  513. for id, node in pairs(self.nodes) do
  514. node.pathDist = (node.alloc and not node.dependsOnIntuitiveLeap) and 0 or 1000
  515. node.path = nil
  516. end
  517. for id, node in pairs(self.allocNodes) do
  518. if not node.dependsOnIntuitiveLeap then
  519. self:BuildPathFromNode(node)
  520. end
  521. end
  522. end
  523.  
  524. function PassiveSpecClass:CreateUndoState()
  525. return self:EncodeURL()
  526. end
  527.  
  528. function PassiveSpecClass:RestoreUndoState(state)
  529. self:DecodeURL(state)
  530. end
Add Comment
Please, Sign In to add comment