Skip to content

Commit c0404dc

Browse files
committed
Merge remote-tracking branch 'TheClassified/feature/poe2-build-export' into feature/poe2-build-export-improved
# Conflicts: # src/Classes/GemSelectControl.lua # src/Classes/ItemSetListControl.lua # src/Classes/SkillSetListControl.lua # src/Classes/SkillsTab.lua # src/Modules/Build.lua
2 parents 3c6e7e2 + 587114e commit c0404dc

15 files changed

Lines changed: 10376 additions & 5082 deletions

src/Classes/ImportTab.lua

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,24 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(
345345
end
346346
end
347347

348+
-- Path of Exile 2 BuildPlanner export
349+
local BuildExportPoE2 = require("Modules/BuildExportPoE2")
350+
self.controls.sectionPoE2Export = new("SectionControl", {"TOPLEFT",self.controls.sectionBuild,"BOTTOMLEFT",true}, {0, 18, 650, 112}, "Export to Path of Exile 2 BuildPlanner")
351+
self.controls.poe2ExportDesc = new("LabelControl", {"TOPLEFT",self.controls.sectionPoE2Export,"TOPLEFT"}, {6, 14, 0, 16}, "^7Save this build as a .build file the in-game BuildPlanner can load.")
352+
self.controls.poe2ExportDesc2 = new("LabelControl", {"TOPLEFT",self.controls.poe2ExportDesc,"BOTTOMLEFT"}, {0, 2, 0, 14}, "^xAAAAAATree specs, item sets and skill sets are exported as level-bracketed loadouts.")
353+
self.controls.poe2ExportDesc3 = new("LabelControl", {"TOPLEFT",self.controls.poe2ExportDesc2,"BOTTOMLEFT"}, {0, 2, 0, 14}, "^xAAAAAAEdit each set's level range in its Manage popup.")
354+
self.poe2ExportStatus = ""
355+
self.controls.poe2ExportPath = new("EditControl", {"TOPLEFT",self.controls.poe2ExportDesc3,"BOTTOMLEFT"}, {0, 8, 560, 20}, BuildExportPoE2.DefaultPath(self.build), "Path", nil, 260)
356+
self.controls.poe2ExportSave = new("ButtonControl", {"LEFT",self.controls.poe2ExportPath,"RIGHT"}, {8, 0, 80, 20}, "Save", function()
357+
self:DoPoE2Export(BuildExportPoE2, self.controls.poe2ExportPath.buf)
358+
end)
359+
self.controls.poe2ExportSave.enabled = function()
360+
return self.controls.poe2ExportPath.buf and self.controls.poe2ExportPath.buf ~= ""
361+
end
362+
self.controls.poe2ExportStatusLabel = new("LabelControl", {"TOPLEFT",self.controls.poe2ExportPath,"BOTTOMLEFT"}, {0, 4, 0, 14}, function()
363+
return self.poe2ExportStatus or ""
364+
end)
365+
348366
-- validate the status of the api the first time
349367
self:RefreshAuthStatus()
350368
end)
@@ -373,6 +391,26 @@ function ImportTabClass:SaveApiSettings()
373391
main:SaveSettings()
374392
end
375393

394+
function ImportTabClass:DoPoE2Export(Exporter, path)
395+
local function doWrite()
396+
local ok, err = Exporter.WriteFile(self.build, path)
397+
if ok then
398+
self.poe2ExportStatus = colorCodes.POSITIVE .. "Saved to " .. path
399+
else
400+
self.poe2ExportStatus = colorCodes.NEGATIVE .. (err or "Export failed")
401+
main:OpenMessagePopup("Export Failed", err or "Unknown error")
402+
end
403+
end
404+
-- Confirm overwrite if the file already exists.
405+
local existing = io.open(path, "r")
406+
if existing then
407+
existing:close()
408+
main:OpenConfirmPopup("Overwrite?", "A file already exists at:\n" .. path .. "\n\nOverwrite it?", "Overwrite", doWrite)
409+
else
410+
doWrite()
411+
end
412+
end
413+
376414
function ImportTabClass:Load(xml, fileName)
377415
self.lastRealm = xml.attrib.lastRealm
378416
self.controls.accountRealm:SelByValue(self.lastRealm or main.lastRealm or "PC", "id")

src/Classes/ItemSetListControl.lua

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
-- Item set list control.
55
--
66
local t_insert = table.insert
7+
local t_remove = table.remove
8+
local m_max = math.max
9+
local s_format = string.format
710

811
local ItemSetListClass = newClass("ItemSetListControl", "ListControl", function(self, anchor, rect, itemsTab)
912
self.ListControl(anchor, rect, 16, "VERTICAL", true, itemsTab.itemSetOrderList)
1013
self.itemsTab = itemsTab
11-
self.itemSetService = new("ItemSetService", itemsTab)
1214
self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function()
13-
self:CopyItemSet(self.selValue)
15+
local newSet = copyTable(itemsTab.itemSets[self.selValue])
16+
newSet.id = 1
17+
while itemsTab.itemSets[newSet.id] do
18+
newSet.id = newSet.id + 1
19+
end
20+
itemsTab.itemSets[newSet.id] = newSet
21+
self:RenameSet(newSet, true)
1422
end)
1523
self.controls.copy.enabled = function()
1624
return self.selValue ~= nil
@@ -22,68 +30,48 @@ local ItemSetListClass = newClass("ItemSetListControl", "ListControl", function(
2230
return self.selValue ~= nil and #self.list > 1
2331
end
2432
self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function()
25-
self:RenameItemSet(self.selValue)
33+
self:RenameSet(itemsTab.itemSets[self.selValue])
2634
end)
2735
self.controls.rename.enabled = function()
2836
return self.selValue ~= nil
2937
end
3038
self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function()
31-
self:CreateItemSet()
39+
local existing = { }
40+
for _, id in ipairs(itemsTab.itemSetOrderList) do
41+
t_insert(existing, itemsTab.itemSets[id])
42+
end
43+
local newSet = itemsTab:NewItemSet()
44+
require("Modules/BuildExportPoE2").PresetNextLevels(existing, newSet)
45+
self:RenameSet(newSet, true)
3246
end)
3347
end)
3448

35-
function ItemSetListClass:CreateItemSet()
36-
local controls = {}
37-
controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for new item set:")
38-
controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Item Set", nil, nil, 100, function(buf)
39-
controls.save.enabled = buf:match("%S")
40-
end)
41-
controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function()
42-
self.itemSetService:NewItemSet(controls.edit.buf)
43-
main:ClosePopup()
44-
end)
45-
controls.save.enabled = false
46-
controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function()
47-
main:ClosePopup()
48-
end)
49-
main:OpenPopup(370, 100, "Create Item Set", controls, "save", "edit", "cancel")
50-
end
51-
52-
function ItemSetListClass:CopyItemSet(selValue)
53-
local itemSet = self.itemsTab.itemSets[selValue]
54-
local controls = {}
55-
controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this item set:")
56-
controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, itemSet.title or "Default", nil, nil, 100, function(buf)
57-
controls.save.enabled = buf:match("%S")
58-
end)
59-
controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function()
60-
self.itemSetService:CopyItemSet(selValue, controls.edit.buf)
61-
main:ClosePopup()
62-
end)
63-
controls.save.enabled = false
64-
controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function()
65-
main:ClosePopup()
66-
end)
67-
main:OpenPopup(370, 100, "Copy Item Set", controls, "save", "edit", "cancel")
68-
end
69-
70-
function ItemSetListClass:RenameItemSet(selValue)
71-
local itemSet = self.itemsTab.itemSets[selValue]
72-
local controls = {}
73-
local setName = itemSet.title or "Default"
74-
controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this item set:")
75-
controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, setName, nil, nil, 100, function(buf)
49+
function ItemSetListClass:RenameSet(itemSet, addOnName)
50+
local controls = { }
51+
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this item set:")
52+
controls.edit = new("EditControl", nil, {0, 40, 350, 20}, itemSet.title, nil, nil, 100, function(buf)
7653
controls.save.enabled = buf:match("%S")
7754
end)
78-
controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function()
79-
self.itemSetService:RenameItemSet(selValue, controls.edit.buf)
55+
controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function()
56+
itemSet.title = controls.edit.buf
57+
self.itemsTab.modFlag = true
58+
if addOnName then
59+
t_insert(self.list, itemSet.id)
60+
self.selIndex = #self.list
61+
self.selValue = itemSet.id
62+
end
63+
self.itemsTab:AddUndoState()
64+
self.itemsTab.build:SyncLoadouts()
8065
main:ClosePopup()
8166
end)
8267
controls.save.enabled = false
83-
controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function()
68+
controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function()
69+
if addOnName then
70+
self.itemsTab.itemSets[itemSet.id] = nil
71+
end
8472
main:ClosePopup()
8573
end)
86-
main:OpenPopup(370, 100, setName and "Rename Item Set" or "Set Name", controls, "save", "edit", "cancel")
74+
main:OpenPopup(370, 100, itemSet.title and "Rename" or "Set Name", controls, "save", "edit", "cancel")
8775
end
8876

8977
function ItemSetListClass:GetRowValue(column, index, itemSetId)
@@ -109,7 +97,7 @@ end
10997

11098
function ItemSetListClass:ReceiveDrag(type, value, source)
11199
if type == "SharedItemList" then
112-
local itemSet = self.itemsTab:CreateItemSet()
100+
local itemSet = self.itemsTab:NewItemSet()
113101
itemSet.title = value.title
114102
for slotName, item in pairs(value.slots) do
115103
local newItem = new("Item", item.raw)
@@ -136,18 +124,23 @@ end
136124
function ItemSetListClass:OnSelDelete(index, itemSetId)
137125
local itemSet = self.itemsTab.itemSets[itemSetId]
138126
if #self.list > 1 then
139-
main:OpenConfirmPopup("Delete Item Set",
140-
"Are you sure you want to delete '" ..
141-
(itemSet.title or "Default") .. "'?\nThis will not delete any items used by the set.", "Delete", function()
142-
self.itemSetService:DeleteItemSet(itemSetId, index)
143-
self.selIndex = nil
144-
self.selValue = nil
145-
end)
127+
main:OpenConfirmPopup("Delete Item Set", "Are you sure you want to delete '"..(itemSet.title or "Default").."'?\nThis will not delete any items used by the set.", "Delete", function()
128+
t_remove(self.list, index)
129+
self.itemsTab.itemSets[itemSetId] = nil
130+
self.selIndex = nil
131+
self.selValue = nil
132+
if itemSetId == self.itemsTab.activeItemSetId then
133+
self.itemsTab:SetActiveItemSet(self.list[m_max(1, index - 1)])
134+
end
135+
self.itemsTab:AddUndoState()
136+
self.itemsTab.build:SyncLoadouts()
137+
end)
146138
end
147139
end
148140

149141
function ItemSetListClass:OnSelKeyDown(index, itemSetId, key)
142+
local itemSet = self.itemsTab.itemSets[itemSetId]
150143
if key == "F2" then
151-
self:RenameItemSet(itemSetId)
144+
self:RenameSet(itemSet)
152145
end
153146
end

src/Classes/ItemsTab.lua

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,8 @@ function ItemsTabClass:Load(xml, dbFileName)
11201120
elseif node.elem == "ItemSet" then
11211121
local itemSet = self:CreateItemSet(tonumber(node.attrib.id), node.attrib.title or "Default")
11221122
itemSet.useSecondWeaponSet = node.attrib.useSecondWeaponSet == "true"
1123+
itemSet.levelMin = tonumber(node.attrib.levelMin)
1124+
itemSet.levelMax = tonumber(node.attrib.levelMax)
11231125
for _, child in ipairs(node) do
11241126
if child.elem == "Slot" then
11251127
local slotName = child.attrib.name or ""
@@ -1208,7 +1210,7 @@ function ItemsTabClass:Save(xml)
12081210
end
12091211
for _, itemSetId in ipairs(self.itemSetOrderList) do
12101212
local itemSet = self.itemSets[itemSetId]
1211-
local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet) } }
1213+
local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet), levelMin = itemSet.levelMin and tostring(itemSet.levelMin) or nil, levelMax = itemSet.levelMax and tostring(itemSet.levelMax) or nil } }
12121214
for slotName, slot in pairs(self.slots) do
12131215
if not slot.parentSlot or itemSet[slotName].selItemId ~= 0 then
12141216
if not slot.nodeId then
@@ -2268,7 +2270,49 @@ function ItemsTabClass:OpenItemSetManagePopup()
22682270
controls.sharedList = new("SharedItemSetListControl", nil, {155, 50, 300, 200}, self)
22692271
controls.setList.dragTargetList = { controls.sharedList }
22702272
controls.sharedList.dragTargetList = { controls.setList }
2271-
controls.close = new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function()
2273+
2274+
-- Level bracket inputs for the .build (PoE2 BuildPlanner) export.
2275+
-- Bound to the row currently selected in the local item-set list.
2276+
local function clampLvl(buf)
2277+
local n = tonumber(buf)
2278+
if not n then return nil end
2279+
n = m_floor(n)
2280+
if n < 0 then n = 0 end
2281+
if n > 100 then n = 100 end
2282+
return n
2283+
end
2284+
local function selectedSet()
2285+
return self.itemSets[controls.setList.selValue]
2286+
end
2287+
controls.lvlLabel = new("LabelControl", nil, {-225, 260, 0, 16}, "^7.build export level range:")
2288+
controls.lvlMin = new("EditControl", nil, {-95, 260, 60, 20}, nil, nil, "%D", 3, function(buf)
2289+
local set = selectedSet()
2290+
if set then
2291+
set.levelMin = clampLvl(buf)
2292+
self.modFlag = true
2293+
end
2294+
end)
2295+
controls.lvlMin.tooltipText = "Lowest character level this item set applies to in the exported .build (1-100). Leave blank to auto-split across item sets."
2296+
controls.lvlDash = new("LabelControl", nil, {-30, 260, 0, 16}, "^7-")
2297+
controls.lvlMax = new("EditControl", nil, {30, 260, 60, 20}, nil, nil, "%D", 3, function(buf)
2298+
local set = selectedSet()
2299+
if set then
2300+
set.levelMax = clampLvl(buf)
2301+
self.modFlag = true
2302+
end
2303+
end)
2304+
controls.lvlMax.tooltipText = "Highest character level this item set applies to in the exported .build (1-100). Leave blank to auto-split across item sets."
2305+
local function refreshLvl()
2306+
local set = selectedSet()
2307+
controls.lvlMin:SetText(set and set.levelMin and tostring(set.levelMin) or "")
2308+
controls.lvlMax:SetText(set and set.levelMax and tostring(set.levelMax) or "")
2309+
end
2310+
controls.setList.OnSelClick = function(listSelf, index, value, doubleClick)
2311+
refreshLvl()
2312+
end
2313+
refreshLvl()
2314+
2315+
controls.close = new("ButtonControl", nil, {180, 260, 90, 20}, "Done", function()
22722316
main:ClosePopup()
22732317
end)
22742318
main:OpenPopup(630, 290, "Manage Item Sets", controls)

src/Classes/PassiveSpec.lua

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,17 @@ function PassiveSpecClass:Init(treeVersion, convert)
9797

9898
-- Keys are node IDs, values are the replacement node
9999
self.hashOverrides = { }
100+
101+
-- Author notes attached to allocated nodes (Shift+Right-Click on a node to
102+
-- set one). Keyed by node id; emitted into the PoE2 .build export as the
103+
-- node's additional_text.
104+
self.nodeNotes = { }
100105
end
101106

102107
function PassiveSpecClass:Load(xml, dbFileName)
103108
self.title = xml.attrib.title
109+
self.levelMin = tonumber(xml.attrib.levelMin)
110+
self.levelMax = tonumber(xml.attrib.levelMax)
104111
local weaponSets = {}
105112
local url
106113
for _, node in pairs(xml) do
@@ -143,6 +150,17 @@ function PassiveSpecClass:Load(xml, dbFileName)
143150
for nodeId in node.attrib.nodes:gmatch("%d+") do
144151
weaponSets[tonumber(nodeId)] = weaponSet
145152
end
153+
elseif node.elem == "Notes" then
154+
for _, child in ipairs(node) do
155+
if child.elem == "Note" and child.attrib.nodeId then
156+
local nid = tonumber(child.attrib.nodeId)
157+
-- Note text lives in the element body (preserves newlines, no XML attribute escaping headaches).
158+
local text = type(child[1]) == "string" and child[1] or child.attrib.text
159+
if nid and text and text ~= "" then
160+
self.nodeNotes[nid] = text
161+
end
162+
end
163+
end
146164
end
147165
end
148166
end
@@ -259,7 +277,9 @@ function PassiveSpecClass:Save(xml)
259277
ascendancyInternalId = tostring(ascendancyInternalId),
260278
secondaryAscendClassId = tostring(self.curSecondaryAscendClassId),
261279
nodes = table.concat(allocNodeIdList, ","),
262-
masteryEffects = table.concat(masterySelections, ",")
280+
masteryEffects = table.concat(masterySelections, ","),
281+
levelMin = self.levelMin and tostring(self.levelMin) or nil,
282+
levelMax = self.levelMax and tostring(self.levelMax) or nil,
263283
}
264284
t_insert(xml, {
265285
-- Legacy format
@@ -309,6 +329,17 @@ function PassiveSpecClass:Save(xml)
309329
end
310330
t_insert(xml, overrides)
311331

332+
-- Per-node author notes (Shift+Right-Click on a node). Stored as element
333+
-- body text so multi-line notes survive without XML attribute escaping.
334+
local notesElem = { elem = "Notes" }
335+
local hasNotes = false
336+
for nodeId, note in pairs(self.nodeNotes) do
337+
if note and note ~= "" then
338+
hasNotes = true
339+
t_insert(notesElem, { elem = "Note", attrib = { nodeId = tostring(nodeId) }, [1] = note })
340+
end
341+
end
342+
if hasNotes then t_insert(xml, notesElem) end
312343
end
313344

314345
function PassiveSpecClass:PostLoad()

src/Classes/PassiveSpecListControl.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ local PassiveSpecListClass = newClass("PassiveSpecListControl", "ListControl", f
3939
newSpec:SelectClass(treeTab.build.spec.curClassId)
4040
newSpec:SelectAscendClass(treeTab.build.spec.curAscendClassId)
4141
newSpec:SelectSecondaryAscendClass(treeTab.build.spec.curSecondaryAscendClassId)
42+
require("Modules/BuildExportPoE2").PresetNextLevels(treeTab.specList, newSpec)
4243
self:RenameSpec(newSpec, "New Tree", true)
4344
end)
4445
self:UpdateItemsTabPassiveTreeDropdown()

src/Classes/PassiveTreeView.lua

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,16 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents)
539539
elseif treeClick == "RIGHT" then
540540
-- User right-clicked on a node
541541
if hoverNode then
542-
if hoverNode.alloc and (hoverNode.type == "Socket" or hoverNode.containJewelSocket) then
542+
if IsKeyDown("SHIFT") then
543+
-- Shift+Right-Click: open a popup to edit the per-node author note
544+
-- (consumed by the PoE2 .build export as the node's additional_text).
545+
local nodeId = hoverNode.id
546+
local title = "Note: " .. (hoverNode.dn or hoverNode.name or "Passive")
547+
main:OpenNoteEditPopup(title, spec.nodeNotes[nodeId], function(text)
548+
spec.nodeNotes[nodeId] = text
549+
build.modFlag = true
550+
end)
551+
elseif hoverNode.alloc and (hoverNode.type == "Socket" or hoverNode.containJewelSocket) then
543552
local slot = build.itemsTab.sockets[hoverNode.id]
544553
if slot:IsEnabled() then
545554
-- User right-clicked a jewel socket, jump to the item page and focus the corresponding item slot control
@@ -1986,6 +1995,15 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi
19861995
tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Ctrl to hide this tooltip.")
19871996
tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+C to copy this node's text.")
19881997
end
1998+
-- Per-node author note (Shift+Right-Click to set/edit) emitted into the PoE2 .build export.
1999+
if node.id and build.spec and build.spec.nodeNotes then
2000+
local existing = build.spec.nodeNotes[node.id]
2001+
tooltip:AddSeparator(10)
2002+
tooltip:AddLine(14, colorCodes.TIP.."Shift + Right-Click to add a build note (PoE2 .build export)")
2003+
if existing and existing ~= "" then
2004+
tooltip:AddLine(14, "^7Note: "..existing)
2005+
end
2006+
end
19892007
end
19902008

19912009
-- Helper function to check if a node is connected to weapon set nodes

0 commit comments

Comments
 (0)