From 5d5ad592178b3eddd3fee2f1924bf13875f37513 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Wed, 12 Aug 2026 00:18:37 +0200 Subject: [PATCH] Preserve main skill selection across character reimports Keep exact state matching for unchanged groups. When a group's gems change, relocate it by source skill and preferred slot, then restore the selected active effect after its skill list is rebuilt. Use the existing group guess only when the source skill disappears. Addresses issue 10199. --- spec/System/TestImportReimport_spec.lua | 62 +++++++++++++++++++++++++ src/Classes/ImportTab.lua | 36 ++++++++++++++ src/Modules/CalcSetup.lua | 10 ++++ 3 files changed, 108 insertions(+) diff --git a/spec/System/TestImportReimport_spec.lua b/spec/System/TestImportReimport_spec.lua index 9ae82d037c0..999e7b97f07 100644 --- a/spec/System/TestImportReimport_spec.lua +++ b/spec/System/TestImportReimport_spec.lua @@ -55,6 +55,22 @@ describe("TestImportReimport", function() } end + local function pasteSocketGroups(mainSocketGroup, ...) + for _, lines in ipairs({ ... }) do + build.skillsTab:PasteSocketGroup(table.concat(lines, "\n") .. "\n") + end + build.mainSocketGroup = mainSocketGroup + runCallback("OnFrame") + end + + local function makeImportItemWithGems(itemTypeLine, inventoryId, ...) + local socketedItems = { } + for index, gemName in ipairs({ ... }) do + socketedItems[index] = makeSocketedGemEntry(index - 1, gemName:match(" Support$") ~= nil, gemName, 20) + end + return makeImportItem(itemTypeLine, inventoryId, socketedItems, "test-import-item-" .. inventoryId) + end + local function reimportSocketedItemsWithOptions(itemTypeLine, inventoryId, socketedItems, clearItems) build.importTab:ImportItemsAndSkills(buildImportPayload({ makeImportItem(itemTypeLine, inventoryId, socketedItems), @@ -228,6 +244,52 @@ Blight 20/0 1 assert.is_false(groupsBySlot.Gloves.enabled) end) + it("preserves the main socket group when supports change", function() + pasteSocketGroups(1, + { "Slot: Body Armour", "Cleave 20/0 1", "Added Fire Damage 20/0 DISABLED 1" } + ) + + build.importTab:ImportItemsAndSkills(buildImportPayload({ + makeImportItemWithGems("Rawhide Gloves", "Gloves", "Cleave"), + makeImportItemWithGems("Plate Vest", "BodyArmour", "Cleave", "Faster Attacks Support"), + }), false, true, true) + runCallback("OnFrame") + + local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup] + assert.are.equal("Body Armour", mainSocketGroup.slot) + assert.is_true(mainSocketGroup.gemList[2].enabled) + end) + + it("preserves a support-granted main skill when supports change", function() + pasteSocketGroups(1, + { "Slot: Body Armour", "Cleave 20/0 1", "Prismatic Burst 20/0 1", "Added Fire Damage 20/0 1" } + ) + build.skillsTab.socketGroupList[1].mainActiveSkill = 2 + + build.importTab:ImportItemsAndSkills(buildImportPayload({ + makeImportItemWithGems("Plate Vest", "BodyArmour", "Cleave", "Prismatic Burst Support", "Faster Attacks Support"), + }), false, true, true) + runCallback("OnFrame") + + local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup] + assert.are.equal("PrismaticBurst", mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.grantedEffect.id) + end) + + it("guesses a new main socket group when the previous main skill is gone", function() + pasteSocketGroups(1, + { "Slot: Body Armour", "Cleave 20/0 1" }, + { "Slot: Boots", "Frostblink 20/0 1" } + ) + + build.importTab:ImportItemsAndSkills(buildImportPayload({ + makeImportItemWithGems("Iron Greaves", "Boots", "Frostblink"), + makeImportItemWithGems("Rawhide Gloves", "Gloves", "Fireball", "Added Fire Damage Support"), + }), false, true, true) + runCallback("OnFrame") + + assert.are.equal("Gloves", build.skillsTab.socketGroupList[build.mainSocketGroup].slot) + end) + it("preserves skill part selection when reimporting items and skills", function() assertReimportPreservesSkillSubstate("Helmet", "Iron Hat", "Helm", "Blight", "skillPart", 2) end) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 12f99be6b2a..4d7ffc0b38d 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -1267,6 +1267,22 @@ local function getSocketGroupReimportKey(socketGroup) }, SOCKET_GROUP_REIMPORT_KEY_SEPARATOR) end +local function findSocketGroupBySkillId(socketGroupList, skillId, preferredSlot) + local fallbackIndex + for index, socketGroup in ipairs(socketGroupList) do + for _, gem in ipairs(socketGroup.gemList) do + if gem.skillId == skillId then + if socketGroup.slot == preferredSlot then + return index + end + fallbackIndex = fallbackIndex or index + break + end + end + end + return fallbackIndex +end + local function snapshotSocketGroupReimportState(socketGroup, isMainGroup) local gemStates = { } for gemIndex, gem in ipairs(socketGroup.gemList) do @@ -1390,7 +1406,18 @@ function ImportTabClass:ImportItemsAndSkills(charData, clearItems, clearSkills, local mainSkillEmpty = #self.build.skillsTab.socketGroupList == 0 local skillOrder local preservedSocketGroupStateByKey + local preservedMainSkillId + local preservedMainGroupSlot + local preservedMainGrantedEffectId if clearSkills then + local mainSocketGroup = self.build.skillsTab.socketGroupList[self.build.mainSocketGroup] + local mainActiveSkill = mainSocketGroup and mainSocketGroup.displaySkillList + and mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill or 1] + local mainActiveEffect = mainActiveSkill and mainActiveSkill.activeEffect + local srcInstance = mainActiveEffect and mainActiveEffect.srcInstance + preservedMainSkillId = srcInstance and srcInstance.skillId + preservedMainGroupSlot = mainSocketGroup and mainSocketGroup.slot + preservedMainGrantedEffectId = mainActiveEffect and mainActiveEffect.grantedEffect and mainActiveEffect.grantedEffect.id skillOrder = { } preservedSocketGroupStateByKey = { } for _, socketGroup in ipairs(self.build.skillsTab.socketGroupList) do @@ -1472,6 +1499,15 @@ function ImportTabClass:ImportItemsAndSkills(charData, clearItems, clearSkills, end if restoredMainSocketGroup then self.build.mainSocketGroup = restoredMainSocketGroup + elseif not mainSkillEmpty then + local matchingMainSocketGroup = preservedMainSkillId + and findSocketGroupBySkillId(self.build.skillsTab.socketGroupList, preservedMainSkillId, preservedMainGroupSlot) + if matchingMainSocketGroup then + self.build.mainSocketGroup = matchingMainSocketGroup + self.build.skillsTab.socketGroupList[matchingMainSocketGroup].pendingMainGrantedEffectId = preservedMainGrantedEffectId + else + self.build.mainSocketGroup = self:GuessMainSocketGroup() + end end end if mainSkillEmpty then diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 5b2d481a3cb..fea21be093f 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1773,6 +1773,15 @@ function calcs.initEnv(build, mode, override, specEnv) env.player.modDB:AddMod(modLib.setSource(value.value, groupCfg.slotName or "")) end + if index == env.mainSocketGroup and env.mode == "MAIN" and group.pendingMainGrantedEffectId then + for activeSkillIndex, activeSkill in ipairs(socketGroupSkillList) do + if activeSkill.activeEffect.grantedEffect.id == group.pendingMainGrantedEffectId then + group.mainActiveSkill = activeSkillIndex + break + end + end + end + if index == env.mainSocketGroup and #socketGroupSkillList > 0 then -- Select the main skill from this socket group local activeSkillIndex @@ -1790,6 +1799,7 @@ function calcs.initEnv(build, mode, override, specEnv) end if env.mode == "MAIN" then + group.pendingMainGrantedEffectId = nil -- Create display label for the socket group if the user didn't specify one if group.label and group.label:match("%S") then group.displayLabel = group.label