Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified src/Assets/monster-categories_36_36_BC7.dds.zst
Binary file not shown.
12 changes: 12 additions & 0 deletions src/Classes/GemSelectControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,18 @@ function GemSelectClass:OnKeyDown(key, doubleClick)
self:ScrollSelIntoView()
end
end
elseif key == "RIGHTBUTTON" and IsKeyDown("SHIFT") then
-- Shift+Right-Click: edit the per-gem author note for the PoE2 .build export.
local gemList = self.skillsTab.displayGroup and self.skillsTab.displayGroup.gemList
local gemInstance = gemList and gemList[self.index]
if gemInstance then
local title = "Note: " .. ((gemInstance.nameSpec and gemInstance.nameSpec ~= "") and gemInstance.nameSpec or "Gem")
main:OpenNoteEditPopup(title, gemInstance.note, function(text)
gemInstance.note = text
self.skillsTab.build.modFlag = true
end)
end
return self
elseif key == "RETURN" or key == "RIGHTBUTTON" then
self.dropped = true
self:UpdateSortCache()
Expand Down
6 changes: 6 additions & 0 deletions src/Classes/GemTooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,12 @@ function GemTooltip.AddGemTooltip(tooltip, build, gemInstance, options)
tooltip:AddLine(fontSizeBig, colorCodes.UNIQUE .. line, "FONTIN SC ITALIC")
end
end
-- Author note (Shift+Right-Click to set/edit) emitted into the PoE2 .build export.
tooltip:AddSeparator(10)
tooltip:AddLine(14, colorCodes.TIP .. "Shift + Right-Click to add a build note (PoE2 .build export)")
if gemInstance.note and gemInstance.note ~= "" then
tooltip:AddLine(14, "^7Note: " .. gemInstance.note)
end
end

return GemTooltip
127 changes: 125 additions & 2 deletions src/Classes/ImportTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
local ipairs = ipairs
local t_insert = table.insert
local t_remove = table.remove
local b_rshift = bit.rshift
local band = bit.band
local t_concat = table.concat
local s_format = string.format
local m_max = math.max
local dkjson = require "dkjson"

Expand Down Expand Up @@ -348,10 +348,133 @@
end
end

-- Path of Exile 2 BuildPlanner export
local BuildExportPoE2 = require("Modules.BuildExportPoE2")
self.controls.sectionPoE2Export = new("SectionControl", { "TOPLEFT", self.controls.sectionBuild, "BOTTOMLEFT", true }, { 0, 18, 650, 282 }, "Export to in-game build planner")
self.controls.poe2ExportDesc = new("LabelControl", {"TOPLEFT",self.controls.sectionPoE2Export,"TOPLEFT"}, {6, 14, 0, 16}, "^7Save this build as a .build file the in-game build planner can load.")
self.controls.poe2ExportDesc2 = new("LabelControl", { "TOPLEFT", self.controls.poe2ExportDesc, "BOTTOMLEFT" }, { 0, 2, 0, 14 }, "^8Each file holds one passive tree, one skill set and one item set.")

self.controls.buildPlannerBuildName = new("EditControl", { "TOPLEFT", self.controls.poe2ExportDesc2, "BOTTOMLEFT" }, { 0, 8, 200, 20 }, self.build.buildName or "New Build", "Build name", nil, nil)
self.controls.buildPlannerAuthorName = new("EditControl", {"LEFT",self.controls.buildPlannerBuildName,"RIGHT"}, {8, 0, 200, 20}, "Author", "Author name", nil, nil)
self.controls.buildPlannerSetsLabel = new("LabelControl", { "TOPLEFT", self.controls.buildPlannerBuildName, "BOTTOMLEFT" }, { 0, 8, 0, 16 }, "^7Sets to export:")
self.controls.buildPlannerSpec = new("DropDownControl", { "TOPLEFT", self.controls.buildPlannerSetsLabel, "BOTTOMLEFT" }, { 0, 2, 180, 20 }, {}, function(index, value)
self.exportSpecIndex = value.key
end, "^7Which passive tree to export")
self.controls.buildPlannerSkillSet = new("DropDownControl", { "LEFT", self.controls.buildPlannerSpec, "RIGHT" }, { 8, 0, 180, 20 }, {}, function(index, value)
self.exportSkillSetId = value.key
end, "^7Which skill set to export")
self.controls.buildPlannerItemSet = new("DropDownControl", { "LEFT", self.controls.buildPlannerSkillSet, "RIGHT" }, { 8, 0, 180, 20 }, {}, function(index, value)
self.exportItemSetId = value.key
end, "^7Which item set to export")
self:RefreshBuildPlannerSets()

self.controls.buildPlannerDescLabel = new("LabelControl", { "TOPLEFT", self.controls.buildPlannerSpec, "BOTTOMLEFT" }, { 0, 8, 0, 16 }, "^7Description:")
self.controls.buildPlannerDescription = new("EditControl", {"TOPLEFT",self.controls.buildPlannerDescLabel,"BOTTOMLEFT"}, {0, 8, 560, 64}, "", nil, "^%C\t\n", nil, nil, 16)
self.controls.poe2ExportPath = new("EditControl", { "TOPLEFT", self.controls.buildPlannerDescription, "BOTTOMLEFT" }, { 0, 8, 560, 20 }, BuildExportPoE2.DefaultPath(self.build), "Path", nil, 260)
self.controls.poe2ExportSave = new("ButtonControl", { "TOPLEFT", self.controls.poe2ExportPath, "BOTTOMLEFT" }, { 0, 8, 140, 20 }, "Export Selected Sets", function()
local path = self.controls.poe2ExportPath.buf
local function doWrite()
local ok, err = BuildExportPoE2.WriteFile(self.build, path, self:GetBuildPlannerMetadata(), {
specIndex = self.exportSpecIndex,
skillSetId = self.exportSkillSetId,
itemSetId = self.exportItemSetId,
})
if not ok then
main:OpenMessagePopup("Error", "Couldn't save the build file:\n"..err.."\nMake sure the save folder exists and is writable.")
else
main:OpenMessagePopup("Success", string.format("Build file exported succesfully to:\n%s", path))

Check warning on line 385 in src/Classes/ImportTab.lua

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (succesfully)
end
end
-- Confirm overwrite if the file already exists.
local existing = io.open(path, "r")
if existing then
existing:close()
main:OpenConfirmPopup("Overwrite?", "A file already exists at:\n" .. path .. "\n\nOverwrite it?", "Overwrite", doWrite)
else
doWrite()
end
end)
self.controls.poe2ExportSave.enabled = function()
return self.controls.poe2ExportPath.buf and self.controls.poe2ExportPath.buf ~= ""
end
self.controls.poe2ExportSave.tooltipText = "^7Writes a single file containing the three sets picked above."
self.controls.poe2ExportSaveAll = new("ButtonControl", { "LEFT", self.controls.poe2ExportSave, "RIGHT" }, { 8, 0, 140, 20 }, "Export All Loadouts", function()
local path = self.controls.poe2ExportPath.buf
local loadouts = BuildExportPoE2.GetLoadouts(self.build)
local function doWrite()
local written, errors = BuildExportPoE2.WriteAllLoadouts(self.build, path, self:GetBuildPlannerMetadata(), loadouts)
local msg = s_format("Wrote %d file(s) to:\n%s", #written, path:match("^(.*[/\\])") or ".")
msg ..= s_format("\n%s", t_concat(written, "\n"))
if #errors > 0 then
msg = msg .. s_format("\n\n%d failed:\n%s", #errors, t_concat(errors, "\n"))
end
main:OpenMessagePopup("Loadout export", msg)
end
local existingFiles = {}
for _, loadout in ipairs(loadouts) do
local loadoutPath = BuildExportPoE2.LoadoutPath(path, loadout.name)
local existing = io.open(loadoutPath, "r")
if existing then
existing:close()
t_insert(existingFiles, loadoutPath:match("([^/\\]+)$") or loadoutPath)
end
end
if #existingFiles > 0 then
main:OpenConfirmPopup("Overwrite?",
s_format("%d file(s) already exist:\n%s\n\nOverwrite them?", #existingFiles, t_concat(existingFiles, "\n")),
"Overwrite", doWrite)
else
doWrite()
end
end)
self.controls.poe2ExportSaveAll.enabled = function()
return self.controls.poe2ExportPath.buf and self.controls.poe2ExportPath.buf ~= ""
end
self.controls.poe2ExportSaveAll.tooltipText = "^7Exports one file per loadout. The loadout name is appended to each filename."

-- validate the status of the api the first time
self:RefreshAuthStatus()
end)

-- Metadata shared by both export buttons.
function ImportTabClass:GetBuildPlannerMetadata()
return {
name = self.controls.buildPlannerBuildName.buf,
author = self.controls.buildPlannerAuthorName.buf,
description = self.controls.buildPlannerDescription.buf,
}
end

-- Rebuild the three set dropdowns
function ImportTabClass:RefreshBuildPlannerSets()
local build = self.build
if not (build and build.treeTab and build.skillsTab and build.itemsTab) then return end

local treeList = {}
for index, spec in ipairs(build.treeTab.specList) do
t_insert(treeList, { label = spec.title or "Default", key = index })
end

local skillList = {}
for _, setId in ipairs(build.skillsTab.skillSetOrderList) do
local skillSet = build.skillsTab.skillSets[setId]
if skillSet then
t_insert(skillList, { label = skillSet.title or "Default", key = setId })
end
end

local itemList = {}
for _, setId in ipairs(build.itemsTab.itemSetOrderList) do
local itemSet = build.itemsTab.itemSets[setId]
if itemSet then
t_insert(itemList, { label = itemSet.title or "Default", key = setId })
end
end

self.controls.buildPlannerSpec:SetList(treeList)
self.controls.buildPlannerSkillSet:SetList(skillList)
self.controls.buildPlannerItemSet:SetList(itemList)
end
function ImportTabClass:RefreshAuthStatus()
main.api:ValidateAuth(function(valid, updateSettings)
if valid then
Expand Down
27 changes: 25 additions & 2 deletions src/Classes/ItemSlotControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl", function(se
self.selItemId = 0
self.slotName = slotName
self.slotNum = tonumber(slotName:match("%d+$") or slotName:match("%d+"))
if data.buildFileInventorySlotMap[slotName] then
self.controls.noteButton = new("ButtonControl", {"LEFT",self,"RIGHT"}, {2, 0, 20, 20}, "~", function()
main:OpenNoteEditPopup(self.slotName, self.note or "", function(note)
self.note = note
itemsTab:PopulateSlots()
itemsTab:AddUndoState()
itemsTab.build.buildFlag = true
end)
end)
self.controls.noteButton.tooltipText = function() return self.note or "Add a note for this item slot" end
end
if slotName:match("Flask") then
self.controls.activate = new("CheckBoxControl", {"RIGHT",self,"LEFT"}, {-2, 0, 20}, nil, function(state)
self.active = state
Expand Down Expand Up @@ -61,7 +72,9 @@ local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl", function(se
self.tooltipFunc = function(tooltip, mode, index, itemId)
local item = itemsTab.items[self.items[index]]
-- not selControl.ListControl allows hover when All Items or Unique/Rare DB Sections are in focus
if main.popups[1] or mode == "OUT" or not item or (not self.dropped and itemsTab.selControl and itemsTab.selControl ~= self.controls.activate and not itemsTab.selControl.ListControl) then
if main.popups[1] or mode == "OUT" or not item
or self:GetMouseOverControl() == self.controls.noteButton -- Note button has its own tooltip
or (not self.dropped and itemsTab.selControl and itemsTab.selControl ~= self.controls.activate and not itemsTab.selControl.ListControl) then
tooltip:Clear(true)
elseif tooltip:CheckForUpdate(item, launch.devModeAlt, itemsTab.build.outputRevision, IsKeyDown("SHIFT")) then
itemsTab:AddItemTooltip(tooltip, item, self)
Expand Down Expand Up @@ -113,6 +126,9 @@ function ItemSlotClass:Populate()
for i, jewelSocket in ipairs(self.jewelSocketList) do
jewelSocket.inactive = i > jewelSocketCount
end
if not self.nodeId then
self.itemsTab.activeItemSet[self.slotName].note = self.note
end
end

function ItemSlotClass:CanReceiveDrag(type, value)
Expand Down Expand Up @@ -153,10 +169,17 @@ function ItemSlotClass:Draw(viewPort)
end

function ItemSlotClass:OnKeyDown(key)
if not self:IsShown() or not self:IsEnabled() then
if not self:IsShown() then
return
end
local mOverControl = self:GetMouseOverControl()
-- Notes don't care if the item slot is enabled or not
if mOverControl and mOverControl == self.controls.noteButton then
return mOverControl:OnKeyDown(key)
end
if not self:IsEnabled() then
return
end
if mOverControl and mOverControl == self.controls.activate then
return mOverControl:OnKeyDown(key)
end
Expand Down
11 changes: 7 additions & 4 deletions src/Classes/ItemsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Contro
if main.portraitMode then
self.controls.itemList = new("ItemListControl", {"TOPRIGHT",self.lastSlot,"BOTTOMRIGHT"}, {0, 0, 360, 308}, self, true)
else
self.controls.itemList = new("ItemListControl", {"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {20, 20, 360, 308}, self, true)
self.controls.itemList = new("ItemListControl", {"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {40, 20, 360, 308}, self, true)
end

-- Database selector
Expand Down Expand Up @@ -1158,6 +1158,7 @@ function ItemsTabClass:Load(xml, dbFileName)
itemSet[slotName].selItemId = tonumber(child.attrib.itemId)
itemSet[slotName].active = child.attrib.active == "true"
itemSet[slotName].pbURL = child.attrib.itemPbURL or ""
itemSet[slotName].note = child.attrib.note
end
elseif child.elem == "SocketIdURL" then
local id = tonumber(child.attrib.nodeId)
Expand Down Expand Up @@ -1251,7 +1252,7 @@ function ItemsTabClass:Save(xml)
for slotName, slot in pairs(self.slots) do
if not slot.parentSlot or itemSet[slotName].selItemId ~= 0 then
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" }})
t_insert(child, { elem = "Slot", attrib = { name = slotName, itemId = tostring(itemSet[slotName].selItemId), itemPbURL = itemSet[slotName].pbURL or "", active = itemSet[slotName].active and "true", note = itemSet[slotName].note }})
else
if self.build.spec.allocNodes[slot.nodeId] then
t_insert(child, { elem = "SocketIdURL", attrib = { name = slotName, nodeId = tostring(slot.nodeId), itemPbURL = itemSet[slot.nodeId] and itemSet[slot.nodeId].pbURL or ""}})
Expand Down Expand Up @@ -1402,7 +1403,7 @@ function ItemsTabClass:Draw(viewPort, inputEvents)
if main.portraitMode then
self.controls.itemList:SetAnchor("TOPRIGHT", self.lastSlot, "BOTTOMRIGHT", 0, 40)
else
self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 20, 20)
self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 40, 20)
end
self.controls.craftDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0 or -20)
self.anchorDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0)
Expand All @@ -1429,7 +1430,7 @@ function ItemsTabClass:CreateItemSet(itemSetId, name)
end
for slotName, slot in pairs(self.slots) do
if not slot.nodeId then
itemSet[slotName] = { selItemId = 0 }
itemSet[slotName] = { selItemId = 0, note = slot.note }
end
end
self.itemSets[itemSet.id] = itemSet
Expand Down Expand Up @@ -1490,10 +1491,12 @@ function ItemsTabClass:SetActiveItemSet(itemSetId, deferSync)
-- Update the previous set
prevSet[slotName].selItemId = slot.selItemId
prevSet[slotName].active = slot.active
prevSet[slotName].note = slot.note
end
-- Equip the incoming set's item
slot.selItemId = curSet[slotName].selItemId
slot.active = curSet[slotName].active
slot.note = curSet[slotName].note
if slot.controls.activate then
slot.controls.activate.state = slot.active
end
Expand Down
29 changes: 28 additions & 1 deletion src/Classes/PassiveSpec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ function PassiveSpecClass:Init(treeVersion, convert)

-- Keys are node IDs, values are the replacement node
self.hashOverrides = { }

-- Author notes attached to allocated nodes (Shift+Right-Click on a node to
-- set one). Keyed by node id; emitted into the PoE2 .build export as the
-- node's additional_text.
self.nodeNotes = { }
end

function PassiveSpecClass:Load(xml, dbFileName)
Expand Down Expand Up @@ -143,6 +148,17 @@ function PassiveSpecClass:Load(xml, dbFileName)
for nodeId in node.attrib.nodes:gmatch("%d+") do
weaponSets[tonumber(nodeId)] = weaponSet
end
elseif node.elem == "Notes" then
for _, child in ipairs(node) do
if child.elem == "Note" and child.attrib.nodeId then
local nid = tonumber(child.attrib.nodeId)
-- Note text lives in the element body (preserves newlines, no XML attribute escaping headaches).
local text = type(child[1]) == "string" and child[1] or child.attrib.text
if nid and text and text ~= "" then
self.nodeNotes[nid] = text
end
end
end
end
end
end
Expand Down Expand Up @@ -261,7 +277,7 @@ function PassiveSpecClass:Save(xml)
ascendancyInternalId = tostring(ascendancyInternalId),
secondaryAscendClassId = tostring(self.curSecondaryAscendClassId),
nodes = table.concat(allocNodeIdList, ","),
masteryEffects = table.concat(masterySelections, ",")
masteryEffects = table.concat(masterySelections, ","),
}
t_insert(xml, {
-- Legacy format
Expand Down Expand Up @@ -311,6 +327,17 @@ function PassiveSpecClass:Save(xml)
end
t_insert(xml, overrides)

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

function PassiveSpecClass:PostLoad()
Expand Down
1 change: 1 addition & 0 deletions src/Classes/PassiveTree.lua
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ local PassiveTreeClass = newClass("PassiveTree", function(self, treeVersion)
local nodeMap = { }
for _, node in pairs(self.nodes) do
node.id = node.skill
node.iname = node.stringId
node.g = node.group
node.o = node.orbit
node.oidx = node.orbitIndex
Expand Down
22 changes: 20 additions & 2 deletions src/Classes/PassiveTreeView.lua
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,16 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents)
elseif treeClick == "RIGHT" then
-- User right-clicked on a node
if hoverNode then
if hoverNode.alloc and (hoverNode.type == "Socket" or hoverNode.containJewelSocket) then
if IsKeyDown("SHIFT") then
-- Shift+Right-Click: open a popup to edit the per-node author note
-- (consumed by the PoE2 .build export as the node's additional_text).
local nodeId = hoverNode.id
local title = "Note: " .. (hoverNode.dn or hoverNode.name or "Passive")
main:OpenNoteEditPopup(title, spec.nodeNotes[nodeId], function(text)
spec.nodeNotes[nodeId] = text
build.modFlag = true
end)
elseif hoverNode.alloc and (hoverNode.type == "Socket" or hoverNode.containJewelSocket) then
local slot = build.itemsTab.sockets[hoverNode.id]
if slot:IsEnabled() then
-- User right-clicked a jewel socket, jump to the item page and focus the corresponding item slot control
Expand Down Expand Up @@ -1557,7 +1566,7 @@ function PassiveTreeViewClass:AddNodeName(tooltip, node, build)
nodeName = "^xF8E6CA" .. node.dn
end
tooltip.center = true
tooltip:AddLine(24, nodeName..(launch.devModeAlt and " ["..node.id.."]" or ""), "FONTIN")
tooltip:AddLine(24, launch.devModeAlt and (node.iname .. " ["..node.id.."]") or nodeName, "FONTIN")
tooltip.center = false
if launch.devModeAlt and node.id > 65535 then
-- Decompose cluster node Id
Expand Down Expand Up @@ -1991,6 +2000,15 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi
tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Ctrl to hide this tooltip.")
tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+C to copy this node's text.")
end
-- Per-node author note (Shift+Right-Click to set/edit) emitted into the PoE2 .build export.
if node.id and build.spec and build.spec.nodeNotes then
local existing = build.spec.nodeNotes[node.id]
tooltip:AddSeparator(10)
tooltip:AddLine(14, colorCodes.TIP.."Shift + Right-Click to add a build note (PoE2 .build export)")
if existing and existing ~= "" then
tooltip:AddLine(14, "^7Note: "..existing)
end
end
end

-- Helper function to check if a node is connected to weapon set nodes
Expand Down
Loading
Loading