From ec30a4a7558d9fe6745cbb88d555d4c6249dc3f6 Mon Sep 17 00:00:00 2001 From: John Costanzo Date: Mon, 5 Oct 2026 21:12:04 -0400 Subject: [PATCH] Add option to set the build planner export path Add a "Build planner path" option so the in-game BuildPlanner export folder can be changed, e.g. when Documents is redirected by OneDrive. When left empty, exports use the default Documents/My Games/Path of Exile 2/BuildPlanner folder as before. Closes #2558 --- spec/System/TestBuildExportPoE2_spec.lua | 28 ++++++++++++++++++++++++ src/Classes/ImportTab.lua | 8 +++++++ src/Modules/BuildExportPoE2.lua | 20 +++++++++++++++-- src/Modules/Main.lua | 22 ++++++++++++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/spec/System/TestBuildExportPoE2_spec.lua b/spec/System/TestBuildExportPoE2_spec.lua index b904b75b2f..142785fab2 100644 --- a/spec/System/TestBuildExportPoE2_spec.lua +++ b/spec/System/TestBuildExportPoE2_spec.lua @@ -35,6 +35,34 @@ Implicits: 0 after_each(function() BuildExportPoE2.WriteFile = originalWriteFile BuildExportPoE2.WriteAllLoadouts = originalWriteAllLoadouts + main.buildPlannerPath = nil + end) + + it("uses the standard BuildPlanner folder when no path is configured", function() + main.buildPlannerPath = nil + assert.are.equal(BuildExportPoE2.StandardDir(), BuildExportPoE2.DefaultDir()) + main.buildPlannerPath = " " + assert.are.equal(BuildExportPoE2.StandardDir(), BuildExportPoE2.DefaultDir()) + end) + + it("uses the configured build planner path", function() + main.buildPlannerPath = "C:\\Users\\Me\\OneDrive\\Documents\\My Games\\Path of Exile 2\\BuildPlanner" + assert.are.equal(main.buildPlannerPath .. "\\", BuildExportPoE2.DefaultDir()) + assert.are.equal(main.buildPlannerPath .. "\\My Build.build", BuildExportPoE2.BuildPath("My Build")) + assert.are.equal("...\\Path of Exile 2\\BuildPlanner\\My Build.build", BuildExportPoE2.DisplayPath(BuildExportPoE2.BuildPath("My Build"))) + + main.buildPlannerPath = "/home/me/planner/" + assert.are.equal("/home/me/planner/", BuildExportPoE2.DefaultDir()) + assert.are.equal(".../me/planner/My Build.build", BuildExportPoE2.DisplayPath(BuildExportPoE2.BuildPath("My Build"))) + end) + + it("resets the import tab export path to the configured folder", function() + local importTab = build.importTab + importTab.controls.buildPlannerBuildName:SetText("My Build", true) + main.buildPlannerPath = "/home/me/planner/" + importTab:ResetBuildPlannerPath() + local treeVersion = build.treeTab.specList[importTab.exportSpecIndex].treeVersion:gsub("_", ".") + assert.are.equal("/home/me/planner/My Build [" .. treeVersion .. "].build", importTab.controls.poe2ExportPath.buf) end) it("uses game ids for active and support gems", function() diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 01bcff14cb..fd5bfcd9ab 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -480,6 +480,14 @@ function ImportTabClass:ImportTab(build) return self end +-- Point the export path at the current default folder, e.g. after the "Build planner path" option changes. +function ImportTabClass:ResetBuildPlannerPath() + local BuildExportPoE2 = require("Modules.BuildExportPoE2") + local buildName = self.controls.buildPlannerBuildName.buf ~= "" and self.controls.buildPlannerBuildName.buf or self.controls.buildPlannerBuildName.placeholder + local spec = self.build.treeTab and self.build.treeTab.specList[self.exportSpecIndex] + self.controls.poe2ExportPath:SetText(BuildExportPoE2.BuildPath(buildName, spec and spec.treeVersion)) +end + -- Metadata shared by both export buttons. function ImportTabClass:GetBuildPlannerMetadata() return { diff --git a/src/Modules/BuildExportPoE2.lua b/src/Modules/BuildExportPoE2.lua index 4294e5420a..4bdf80ace9 100644 --- a/src/Modules/BuildExportPoE2.lua +++ b/src/Modules/BuildExportPoE2.lua @@ -28,13 +28,26 @@ local function treeVersionSuffix(treeVersion) return treeVersion and " [" .. tostring(treeVersion):gsub("_", ".") .. "]" or "" end -function M.DefaultDir() +--- The game's own BuildPlanner folder, ignoring any user override. +function M.StandardDir() local home = os.getenv("USERPROFILE") or (GetScriptPath() .. "/../") or "" local sep = home:find("\\") and "\\" or "/" return home .. sep .. "Documents" .. sep .. "My Games" .. sep .. "Path of Exile 2" .. sep .. "BuildPlanner" .. sep end +--- The export folder: the "Build planner path" option if set, otherwise the standard folder. +function M.DefaultDir() + local custom = main and main.buildPlannerPath + if custom and custom:match("%S") then + if not custom:match("[/\\]$") then + custom = custom .. (custom:find("\\") and "\\" or "/") + end + return custom + end + return M.StandardDir() +end + function M.BuildPath(buildName, treeVersion, path) local dir = path and (path:match("^(.*[/\\])") or "") or M.DefaultDir() return dir .. safeFilename(buildName) .. treeVersionSuffix(treeVersion) .. ".build" @@ -45,7 +58,10 @@ function M.DisplayPath(path) local sep = defaultDir:find("\\") and "\\" or "/" if path:sub(1, #defaultDir):lower() == defaultDir:lower() then local fileName = path:sub(#defaultDir + 1) - return "..." .. sep .. "Path of Exile 2" .. sep .. "BuildPlanner" .. (fileName ~= "" and sep .. fileName or "") + -- Show the last two folders of the export directory, e.g. "...\Path of Exile 2\BuildPlanner\My Build.build" + local parent, leaf = defaultDir:gsub("[/\\]+$", ""):match("([^/\\]*)[/\\]+([^/\\]+)$") + local shownDir = parent and (parent ~= "" and parent .. sep or "") .. leaf or defaultDir:gsub("[/\\]+$", "") + return "..." .. sep .. shownDir .. (fileName ~= "" and sep .. fileName or "") end return "..." .. sep .. (path:gsub("[/\\]+$", ""):match("([^/\\]+)$") or "") end diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index c02db950ac..92f729fe05 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -609,6 +609,9 @@ function main:LoadSettings(ignoreBuild) if node.attrib.buildPath then self.buildPath = node.attrib.buildPath end + if node.attrib.buildPlannerPath then + self.buildPlannerPath = node.attrib.buildPlannerPath + end if node.attrib.nodePowerTheme then self.nodePowerTheme = node.attrib.nodePowerTheme end @@ -808,6 +811,7 @@ function main:SaveSettings() connectionProtocol = tostring(launch.connectionProtocol), proxyURL = launch.proxyURL, buildPath = (self.buildPath ~= self.defaultBuildPath and self.buildPath or nil), + buildPlannerPath = self.buildPlannerPath, nodePowerTheme = self.nodePowerTheme, colorPositive = self.colorPositive, colorNegative = self.colorNegative, @@ -923,7 +927,7 @@ function main:OpenOptionsPopup(savedState) } -- NOTE: Height needs to be adjusted if more menu options are added - local oneColumnHeightReq = 850 -- Min height required to not split menu into two columns + local oneColumnHeightReq = 876 -- Min height required to not split menu into two columns local columnWidth = 600 local startingY = 20 @@ -1016,6 +1020,14 @@ function main:OpenOptionsPopup(savedState) end controls.buildPath.tooltipText = "Overrides the default save location for builds.\nThe default location is: '"..self.defaultBuildPath.."'" + nextRow() + controls.buildPlannerPath = new("EditControl"):EditControl({ "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 290, 18 }) + controls.buildPlannerPathLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.buildPlannerPath, "LEFT" }, { defaultLabelSpacingPx, 0, 0, 16 }, "^7Build planner path:") + if self.buildPlannerPath then + controls.buildPlannerPath:SetText(self.buildPlannerPath) + end + controls.buildPlannerPath.tooltipText = "Overrides the folder used when exporting to the in-game build planner.\nUseful if your Documents folder is redirected (e.g. by OneDrive).\nThe default location is: '"..require("Modules.BuildExportPoE2").StandardDir().."'" + nextRow() controls.nodePowerTheme = new("DropDownControl"):DropDownControl({ "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 100, 18 }, { { label = "Red & Blue", theme = "RED/BLUE" }, @@ -1237,6 +1249,14 @@ function main:OpenOptionsPopup(savedState) else self.buildPath = self.defaultBuildPath end + local buildPlannerPath = controls.buildPlannerPath.buf:match("%S") and controls.buildPlannerPath.buf or nil + if buildPlannerPath ~= self.buildPlannerPath then + self.buildPlannerPath = buildPlannerPath + local importTab = self.modes.BUILD and self.modes.BUILD.importTab + if self.mode == "BUILD" and importTab then + importTab:ResetBuildPlannerPath() + end + end if self.mode == "LIST" then self.modes.LIST:BuildList() end