diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 28de4d9598..423022fc86 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -611,10 +611,12 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont self.controls.scrollBar = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, {0, 0, 18, 0}, 50, "VERTICAL", true) end) -function ConfigTabClass:Load(xml, fileName) - self.activeConfigSetId = 1 - self.configSets = { } - self.configSetOrderList = { 1 } +function ConfigTabClass:Load(xml, fileName, appendConfigs) + if not appendConfigs then + self.activeConfigSetId = 0 + self.configSets = { } + self.configSetOrderList = { } + end local function setInputAndPlaceholder(node, configSetId) if node.elem == "Input" then @@ -660,24 +662,25 @@ function ConfigTabClass:Load(xml, fileName) if xml.empty then self:NewConfigSet(1, "Default") end - for index, node in ipairs(xml) do + for _, node in ipairs(xml) do if node.elem ~= "ConfigSet" then if not self.configSets[1] then self:NewConfigSet(1, "Default") end setInputAndPlaceholder(node, 1) else - local configSetId = tonumber(node.attrib.id) - self:NewConfigSet(configSetId, node.attrib.title or "Default") - self.configSetOrderList[index] = configSetId + local configSet = self:NewConfigSet(not appendConfigs and tonumber(node.attrib.id) or nil, node.attrib.title or "Default") + t_insert(self.configSetOrderList, configSet.id) for _, child in ipairs(node) do - setInputAndPlaceholder(child, configSetId) + setInputAndPlaceholder(child, configSet.id) end end end - self:SetActiveConfigSet(tonumber(xml.attrib.activeConfigSet) or 1) - self:ResetUndo() + if not appendConfigs then + self:SetActiveConfigSet(tonumber(xml.attrib.activeConfigSet) or 1) + self:ResetUndo() + end end function ConfigTabClass:GetDefaultState(var, varType) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 2a63467109..ab2aa632aa 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -314,6 +314,24 @@ You can get this from your web browser's cookies while logged into the Path of E self.build:Init(self.build.dbFileName, self.build.buildName, self.importCodeXML, false, self.importCodeSite and self.controls.importCodeIn.buf or nil) self.build.viewMode = "TREE" end) + elseif self.controls.importCodeMode.selIndex == 3 then + local controls = { } + t_insert(controls, new("LabelControl", nil, {0, 20, 0, 16}, colorCodes.WARNING.."Warning:^7 Importing many loadouts into the same build")) + t_insert(controls, new("LabelControl", nil, {0, 36, 0, 16}, "may cause performance issues. Use with caution.")) + t_insert(controls, new("LabelControl", nil, {0, 64, 0, 16}, "^7Prefix for imported loadouts:")) + controls.prefix = new("EditControl", nil, {0, 84, 350, 20}, "Imported - ", nil, nil, 50) + controls.import = new("ButtonControl", nil, {-45, 114, 80, 20}, "Import", function() + local prefix = controls.prefix.buf + if prefix == "" then + prefix = "Imported - " + end + main:ClosePopup() + self.build:ImportLoadouts(self.importCodeXML, prefix) + end) + t_insert(controls, new("ButtonControl", nil, {45, 114, 80, 20}, "Cancel", function() + main:ClosePopup() + end)) + main:OpenPopup(380, 144, "Import Loadouts", controls, "import") else self.build:Shutdown() self.build:Init(false, "Imported build", self.importCodeXML, false, self.importCodeSite and self.controls.importCodeIn.buf or nil) @@ -331,7 +349,7 @@ You can get this from your web browser's cookies while logged into the Path of E self.controls.importCodeState.label = function() return self.importCodeDetail or "" end - self.controls.importCodeMode = new("DropDownControl", {"TOPLEFT",self.controls.importCodeIn,"BOTTOMLEFT"}, {0, 4, 160, 20}, { "Import to this build", "Import to a new build" }) + self.controls.importCodeMode = new("DropDownControl", {"TOPLEFT",self.controls.importCodeIn,"BOTTOMLEFT"}, {0, 4, 160, 20}, { "Import to this build", "Import to a new build", "Import loadouts only" }) self.controls.importCodeMode.enabled = function() return self.build.dbFileName and self.importCodeValid end @@ -1214,4 +1232,4 @@ function ImportTabClass:SetPredefinedBuildName() local charData = charSelect.list[charSelect.selIndex].char local charName = charData.name main.predefinedBuildName = accountName.." - "..charName -end \ No newline at end of file +end diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index a32edf66a1..05c0ecba06 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -958,15 +958,23 @@ holding Shift will put it in the second.]]) self.lastSlot = self.slots[baseSlots[#baseSlots]] end) -function ItemsTabClass:Load(xml, dbFileName) - self.activeItemSetId = 0 - self.itemSets = { } - self.itemSetOrderList = { } - self.tradeQuery.statSortSelectionList = { } +function ItemsTabClass:Load(xml, dbFileName, appendItems) + if not appendItems then + self.activeItemSetId = 0 + self.itemSets = { } + self.itemSetOrderList = { } + self.tradeQuery.statSortSelectionList = { } + end + + local itemIdMap = { } for _, node in ipairs(xml) do if node.elem == "Item" then local item = new("Item", "") - item.id = tonumber(node.attrib.id) + local itemId = tonumber(node.attrib.id) + if not appendItems then + item.id = itemId + end + item.variant = tonumber(node.attrib.variant) if node.attrib.variantAlt then item.hasAltVariant = true @@ -1009,8 +1017,13 @@ function ItemsTabClass:Load(xml, dbFileName) end if item.base then item:BuildModList() - self.items[item.id] = item - t_insert(self.itemOrderList, item.id) + if appendItems then + self:AddItem(item, true) + itemIdMap[itemId] = item.id + else + self.items[item.id] = item + t_insert(self.itemOrderList, item.id) + end end -- Below is OBE and left for legacy compatibility (all Slots are part of ItemSets now) elseif node.elem == "Slot" then @@ -1023,14 +1036,18 @@ function ItemsTabClass:Load(xml, dbFileName) end end elseif node.elem == "ItemSet" then - local itemSet = self:NewItemSet(tonumber(node.attrib.id)) + local itemSet = self:NewItemSet(not appendItems and tonumber(node.attrib.id) or nil) itemSet.title = node.attrib.title itemSet.useSecondWeaponSet = node.attrib.useSecondWeaponSet == "true" for _, child in ipairs(node) do if child.elem == "Slot" then local slotName = child.attrib.name or "" if itemSet[slotName] then - itemSet[slotName].selItemId = tonumber(child.attrib.itemId) + local itemId = tonumber(child.attrib.itemId) + if appendItems and itemIdMap[itemId] then + itemId = itemIdMap[itemId] + end + itemSet[slotName].selItemId = itemId itemSet[slotName].active = child.attrib.active == "true" itemSet[slotName].pbURL = child.attrib.itemPbURL or "" end @@ -1051,16 +1068,20 @@ function ItemsTabClass:Load(xml, dbFileName) end end end - if not self.itemSetOrderList[1] then - self.activeItemSet = self:NewItemSet(1) - self.activeItemSet.useSecondWeaponSet = xml.attrib.useSecondWeaponSet == "true" - self.itemSetOrderList[1] = 1 - end - self:SetActiveItemSet(tonumber(xml.attrib.activeItemSet) or 1) - if xml.attrib.showStatDifferences then - self.showStatDifferences = xml.attrib.showStatDifferences == "true" + if not appendItems then + if not self.itemSetOrderList[1] then + self.activeItemSet = self:NewItemSet(1) + self.activeItemSet.useSecondWeaponSet = xml.attrib.useSecondWeaponSet == "true" + self.itemSetOrderList[1] = 1 + end + self:SetActiveItemSet(tonumber(xml.attrib.activeItemSet) or 1) + if xml.attrib.showStatDifferences then + self.showStatDifferences = xml.attrib.showStatDifferences == "true" + end + self:ResetUndo() + else + return itemIdMap end - self:ResetUndo() end function ItemsTabClass:Save(xml) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 65a7e498f6..734cb795d5 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -374,33 +374,36 @@ function SkillsTabClass:LoadSkill(node, skillSetId) t_insert(self.skillSets[skillSetId].socketGroupList, socketGroup) end -function SkillsTabClass:Load(xml, fileName) - self.activeSkillSetId = 0 - self.skillSets = { } - self.skillSetOrderList = { } - -- Handle legacy configuration settings when loading `defaultGemLevel` - if xml.attrib.matchGemLevelToCharacterLevel == "true" then - self.controls.defaultLevel:SelByValue("characterLevel", "gemLevel") - elseif type(xml.attrib.defaultGemLevel) == "string" and tonumber(xml.attrib.defaultGemLevel) == nil then - self.controls.defaultLevel:SelByValue(xml.attrib.defaultGemLevel, "gemLevel") - else - self.controls.defaultLevel:SelByValue("normalMaximum", "gemLevel") - end - self.defaultGemLevel = self.controls.defaultLevel:GetSelValueByKey("gemLevel") - self.defaultGemQuality = m_max(m_min(tonumber(xml.attrib.defaultGemQuality) or 0, 23), 0) - self.controls.defaultQuality:SetText(self.defaultGemQuality or "") - if xml.attrib.sortGemsByDPS then - self.sortGemsByDPS = xml.attrib.sortGemsByDPS == "true" - end - self.controls.sortGemsByDPS.state = self.sortGemsByDPS - if xml.attrib.showAltQualityGems then - self.showAltQualityGems = xml.attrib.showAltQualityGems == "true" - end - self.controls.showAltQualityGems.state = self.showAltQualityGems - self.controls.showSupportGemTypes:SelByValue(xml.attrib.showSupportGemTypes or "ALL", "show") - self.controls.sortGemsByDPSFieldControl:SelByValue(xml.attrib.sortGemsByDPSField or "CombinedDPS", "type") - self.showSupportGemTypes = self.controls.showSupportGemTypes:GetSelValueByKey("show") - self.sortGemsByDPSField = self.controls.sortGemsByDPSFieldControl:GetSelValueByKey("type") +function SkillsTabClass:Load(xml, fileName, appendSkills) + if not appendSkills then + self.activeSkillSetId = 0 + self.skillSets = { } + self.skillSetOrderList = { } + -- Handle legacy configuration settings when loading `defaultGemLevel` + if xml.attrib.matchGemLevelToCharacterLevel == "true" then + self.controls.defaultLevel:SelByValue("characterLevel", "gemLevel") + elseif type(xml.attrib.defaultGemLevel) == "string" and tonumber(xml.attrib.defaultGemLevel) == nil then + self.controls.defaultLevel:SelByValue(xml.attrib.defaultGemLevel, "gemLevel") + else + self.controls.defaultLevel:SelByValue("normalMaximum", "gemLevel") + end + self.defaultGemLevel = self.controls.defaultLevel:GetSelValueByKey("gemLevel") + self.defaultGemQuality = m_max(m_min(tonumber(xml.attrib.defaultGemQuality) or 0, 23), 0) + self.controls.defaultQuality:SetText(self.defaultGemQuality or "") + if xml.attrib.sortGemsByDPS then + self.sortGemsByDPS = xml.attrib.sortGemsByDPS == "true" + end + self.controls.sortGemsByDPS.state = self.sortGemsByDPS + if xml.attrib.showAltQualityGems then + self.showAltQualityGems = xml.attrib.showAltQualityGems == "true" + end + self.controls.showAltQualityGems.state = self.showAltQualityGems + self.controls.showSupportGemTypes:SelByValue(xml.attrib.showSupportGemTypes or "ALL", "show") + self.controls.sortGemsByDPSFieldControl:SelByValue(xml.attrib.sortGemsByDPSField or "CombinedDPS", "type") + self.showSupportGemTypes = self.controls.showSupportGemTypes:GetSelValueByKey("show") + self.sortGemsByDPSField = self.controls.sortGemsByDPSFieldControl:GetSelValueByKey("type") + end + for _, node in ipairs(xml) do if node.elem == "Skill" then -- Old format, initialize skill sets if needed @@ -412,7 +415,7 @@ function SkillsTabClass:Load(xml, fileName) end if node.elem == "SkillSet" then - local skillSet = self:NewSkillSet(tonumber(node.attrib.id)) + local skillSet = self:NewSkillSet(not appendSkills and tonumber(node.attrib.id) or nil) skillSet.title = node.attrib.title t_insert(self.skillSetOrderList, skillSet.id) for _, subNode in ipairs(node) do @@ -420,8 +423,10 @@ function SkillsTabClass:Load(xml, fileName) end end end - self:SetActiveSkillSet(tonumber(xml.attrib.activeSkillSet) or 1) - self:ResetUndo() + if not appendSkills then + self:SetActiveSkillSet(tonumber(xml.attrib.activeSkillSet) or 1) + self:ResetUndo() + end end function SkillsTabClass:Save(xml) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index abae20fe26..9f7cf0c8e5 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -444,8 +444,11 @@ function TreeTabClass:GetSpecList() return newSpecList end -function TreeTabClass:Load(xml, dbFileName) - self.specList = { } +function TreeTabClass:Load(xml, dbFileName, appendSpecs, itemIdMap) + if not appendSpecs then + self.specList = { } + end + if xml.elem == "Spec" then -- Import single spec from old build self.specList[1] = new("PassiveSpec", self.build, defaultTreeVersion) @@ -461,16 +464,35 @@ function TreeTabClass:Load(xml, dbFileName) main:OpenMessagePopup("Unknown Passive Tree Version", "The build you are trying to load uses an unrecognised version of the passive skill tree.\nYou may need to update the program before loading this build.") return true end + + -- Remap jewel socket item IDs if appending + if appendSpecs and itemIdMap then + for _, child in ipairs(node) do + if child.elem == "Sockets" then + for _, socket in ipairs(child) do + if socket.elem == "Socket" then + local oldItemId = tonumber(socket.attrib.itemId) + if oldItemId and itemIdMap[oldItemId] then + socket.attrib.itemId = tostring(itemIdMap[oldItemId]) + end + end + end + end + end + end + local newSpec = new("PassiveSpec", self.build, node.attrib.treeVersion or defaultTreeVersion) newSpec:Load(node, dbFileName) t_insert(self.specList, newSpec) end end end - if not self.specList[1] then - self.specList[1] = new("PassiveSpec", self.build, latestTreeVersion) + if not appendSpecs then + if not self.specList[1] then + self.specList[1] = new("PassiveSpec", self.build, latestTreeVersion) + end + self:SetActiveSpec(tonumber(xml.attrib.activeSpec) or 1) end - self:SetActiveSpec(tonumber(xml.attrib.activeSpec) or 1) end function TreeTabClass:PostLoad() diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 700f0d8574..96f4af2060 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -1817,17 +1817,26 @@ do end end -function buildMode:LoadDB(xmlText, fileName) +function buildMode:ParseXML(xmlText, fileName) -- Parse the XML local dbXML, errMsg = common.xml.ParseXML(xmlText) if not dbXML then launch:ShowErrMsg("^1Error loading '%s': %s", fileName, errMsg) - return true + return nil elseif #dbXML == 0 then main:OpenMessagePopup("Error", "Build file is empty, or error parsing xml.\n\n"..fileName) - return true + return nil elseif dbXML[1].elem ~= "PathOfBuilding" then launch:ShowErrMsg("^1Error parsing '%s': 'PathOfBuilding' root element missing", fileName) + return nil + end + return dbXML +end + +function buildMode:LoadDB(xmlText, fileName) + -- Parse the XML + local dbXML = self:ParseXML(xmlText, fileName) + if not dbXML then return true end @@ -1872,6 +1881,142 @@ function buildMode:LoadDBFile() return self:LoadDB(xmlText, self.dbFileName) end +function buildMode:ImportLoadouts(xmlText, prefix) + -- Parse the XML + local dbXML = self:ParseXML(xmlText, self.dbFileName) + if not dbXML then + return true + end + + -- Ensure unique names for imported entries + local usedNames = {} + local function makeUniqueName(name) + local baseName = name + local suffix = 2 + while usedNames[name] do + name = baseName .. " (" .. suffix .. ")" + suffix = suffix + 1 + end + usedNames[name] = true + return name + end + local function markName(name) + usedNames[name or "Default"] = true + end + + -- Collect existing names from current build + for _, itemSet in pairs(self.itemsTab.itemSets) do + markName(itemSet.title) + end + for _, skillSet in pairs(self.skillsTab.skillSets) do + markName(skillSet.title) + end + for _, spec in ipairs(self.treeTab.specList) do + markName(spec.title) + end + for _, configSet in pairs(self.configTab.configSets) do + markName(configSet.title) + end + + -- Pre-process the XML to add prefix and ensure unique names + local function addPrefixAndMakeUnique(node) + local titleAttr = "title" + if node.attrib and node.attrib[titleAttr] then + local title = prefix .. (node.attrib[titleAttr] or "") + node.attrib[titleAttr] = makeUniqueName(title) + end + end + + -- Extract relevant sections from imported XML + local itemsSection = nil + local skillsSection = nil + local treeSection = nil + local configSection = nil + local itemSetCount = 0 + local skillSetCount = 0 + local treeSpecCount = 0 + local configSetCount = 0 + + for _, node in ipairs(dbXML[1]) do + if type(node) == "table" then + if node.elem == "Items" then + itemsSection = node + for _, n in ipairs(itemsSection) do + if n.elem == "ItemSet" then + addPrefixAndMakeUnique(n) + itemSetCount = itemSetCount + 1 + end + end + elseif node.elem == "Skills" then + skillsSection = node + for _, n in ipairs(skillsSection) do + if n.elem == "SkillSet" then + addPrefixAndMakeUnique(n) + skillSetCount = skillSetCount + 1 + end + end + elseif node.elem == "Tree" then + treeSection = node + for _, n in ipairs(treeSection) do + if n.elem == "Spec" then + addPrefixAndMakeUnique(n) + treeSpecCount = treeSpecCount + 1 + end + end + elseif node.elem == "Config" then + configSection = node + for _, n in ipairs(configSection) do + if n.elem == "ConfigSet" then + addPrefixAndMakeUnique(n) + configSetCount = configSetCount + 1 + end + end + end + end + end + + if itemSetCount == 0 and skillSetCount == 0 and treeSpecCount == 0 and configSetCount == 0 then + main:OpenMessagePopup("Import Loadouts", "No loadouts (item sets, skill sets, tree specs, or config sets) found in the imported build.") + return + end + + local itemIdMap = { } + if itemsSection then + itemIdMap = self.itemsTab:Load(itemsSection, self.dbFileName, true) or { } + end + if skillsSection then + self.skillsTab:Load(skillsSection, self.dbFileName, true) + end + if treeSection then + self.treeTab:Load(treeSection, self.dbFileName, true, nil, itemIdMap) + self.treeTab:PostLoad() + end + if configSection then + self.configTab:Load(configSection, self.dbFileName, true) + end + + -- Mark build as modified + self.modFlag = true + self.buildFlag = true + self:SyncLoadouts() + + -- Show success message + local message = "Successfully imported:\n" + if itemSetCount > 0 then + message = message .. "- " .. itemSetCount .. " item set(s)\n" + end + if skillSetCount > 0 then + message = message .. "- " .. skillSetCount .. " skill set(s)\n" + end + if treeSpecCount > 0 then + message = message .. "- " .. treeSpecCount .. " tree spec(s)\n" + end + if configSetCount > 0 then + message = message .. "- " .. configSetCount .. " config set(s)\n" + end + main:OpenMessagePopup("Import Complete", message) +end + function buildMode:SaveDB(fileName) local dbXML = { elem = "PathOfBuilding" }