diff --git a/spec/System/TestConfigPersistence_spec.lua b/spec/System/TestConfigPersistence_spec.lua new file mode 100644 index 00000000000..7b22bc46d7b --- /dev/null +++ b/spec/System/TestConfigPersistence_spec.lua @@ -0,0 +1,97 @@ +describe("Configuration persistence", function() + before_each(function() + newBuild() + end) + + it("omits untouched defaults while retaining existing non-default settings", function() + local configTab = build.configTab + local function savedInputs() + local xml = { } + configTab:Save(xml) + local inputs = { } + for _, node in ipairs(xml[1]) do + if node.elem == "Input" then + inputs[node.attrib.name] = node.attrib + end + end + return inputs + end + local defaults = { + "resistancePenalty", "GamblesprintMovementSpeed", "bloodsoakedBannerStages", + "conditionCorruptingCryStages", "touchedDebuffsCount", "maniaDebuffsCount", + } + local inputs = savedInputs() + for _, var in ipairs(defaults) do + assert.is_not_nil(configTab.input[var]) + assert.is_nil(inputs[var]) + end + assert.is_nil(inputs.overridePowerCharges) + -- Selecting a numeric dropdown's default must not create a saved override. + configTab.input.EHPUnluckyWorstOf = 1 + assert.is_nil(savedInputs().EHPUnluckyWorstOf) + + configTab.input.resistancePenalty = 0 + configTab.input.EHPUnluckyWorstOf = 2 + configTab.varControls.maniaDebuffsCount:SetText("2", true) + inputs = savedInputs() + assert.are.equal("0", inputs.resistancePenalty.number) + assert.are.equal("2", inputs.EHPUnluckyWorstOf.number) + assert.are.equal("2", inputs.maniaDebuffsCount.number) + loadBuildFromXML(build:SaveDB("code")) + assert.are.equal(0, build.configTab.input.resistancePenalty) + assert.are.equal(2, build.configTab.input.EHPUnluckyWorstOf) + assert.are.equal(2, build.configTab.input.maniaDebuffsCount) + end) + + it("preserves explicit numeric inputs separately from blank fields in every config set", function() + local configTab = build.configTab + local vars = { "overrideEnduranceCharges", "overrideCrabBarriers", "conditionStationary" } + for id = 1, 3 do + if id > 1 then + configTab:NewConfigSet(id, "Config " .. id) + table.insert(configTab.configSetOrderList, id) + end + configTab:SetActiveConfigSet(id) + for _, var in ipairs(vars) do + configTab.varControls[var]:SetText(id == 1 and "0" or id == 2 and "2" or "", true) + configTab.placeholder[var] = 0 + end + end + + loadBuildFromXML(build:SaveDB("code")) + + configTab = build.configTab + for id = 1, 3 do + configTab:SetActiveConfigSet(id) + for _, var in ipairs(vars) do + assert.are.equal(id == 1 and "0" or id == 2 and "2" or "", configTab.varControls[var].buf) + if id == 3 then + assert.is_nil(configTab.input[var]) + else + assert.are.equal(id == 1 and 0 or 2, configTab.input[var]) + end + assert.are.equal(0, configTab.placeholder[var]) + end + end + end) + + for _, minion in ipairs({ false, true }) do + it("calculates saved charge overrides for " .. (minion and "minions" or "the player"), function() + build.skillsTab:PasteSocketGroup("Raise Zombie 20/0 1") + for _, text in ipairs({ "0", "2", "" }) do + local configTab = build.configTab + for _, charge in ipairs({ "Power", "Frenzy", "Endurance" }) do + configTab.input[(minion and "minionsUse" or "use") .. charge .. "Charges"] = true + configTab.varControls[(minion and "minionsOverride" or "override") .. charge .. "Charges"]:SetText(text, true) + end + loadBuildFromXML(build:SaveDB("code")) + runCallback("OnFrame") + local output = minion and build.calcsTab.mainEnv.minion.output or build.calcsTab.mainOutput + for _, charge in ipairs({ "Power", "Frenzy", "Endurance" }) do + assert.are.equal(3, output[charge .. "ChargesMax"]) + assert.are.equal(tonumber(text) or 3, output[charge .. "Charges"]) + end + end + end) + end +end) diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 3d1f007cd63..071433532bc 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -13,6 +13,13 @@ local varList = require("Modules.ConfigOptions") local configVisibility = require("Modules.ConfigVisibility") local configModBrowser = require("Modules.ConfigModBrowser") +local numericOptions = { } +for _, varData in ipairs(varList) do + if varData.type == "count" or varData.type == "integer" or varData.type == "countAllowZero" or varData.type == "float" then + numericOptions[varData.var] = varData + end +end + ---@class CustomModBlockControl: ControlHost, Control local CustomModBlockClass = newClass("CustomModBlockControl", "ControlHost", "Control") @@ -968,7 +975,9 @@ function ConfigTabClass:Save(xml) t_insert(xml, child) for k, v in pairs(configSet.input) do - if v ~= self:GetDefaultState(k, type(v)) then + local varData = numericOptions[k] + -- Preserve numeric entries that would not be restored by the field's declared default. + if (type(v) == "number" and varData and v ~= varData.defaultState) or v ~= self:GetDefaultState(k, type(v)) then local node = { elem = "Input", attrib = { name = k } } if type(v) == "number" then node.attrib.number = tostring(v) diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 8ff3e6f8c79..0ebf9c35d3e 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -900,19 +900,19 @@ Huge sets the radius to 11. { var = "usePowerCharges", type = "check", label = "Do you use Power Charges?", apply = function(val, modList, enemyModList) modList:NewMod("UsePowerCharges", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) end }, - { var = "overridePowerCharges", type = "count", label = "# of Power Charges (if not maximum):", ifOption = "usePowerCharges", apply = function(val, modList, enemyModList) + { var = "overridePowerCharges", type = "countAllowZero", label = "# of Power Charges (if not maximum):", ifOption = "usePowerCharges", apply = function(val, modList, enemyModList) modList:NewMod("PowerCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) end }, { var = "useFrenzyCharges", type = "check", label = "Do you use Frenzy Charges?", apply = function(val, modList, enemyModList) modList:NewMod("UseFrenzyCharges", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) end }, - { var = "overrideFrenzyCharges", type = "count", label = "# of Frenzy Charges (if not maximum):", ifOption = "useFrenzyCharges", apply = function(val, modList, enemyModList) + { var = "overrideFrenzyCharges", type = "countAllowZero", label = "# of Frenzy Charges (if not maximum):", ifOption = "useFrenzyCharges", apply = function(val, modList, enemyModList) modList:NewMod("FrenzyCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) end }, { var = "useEnduranceCharges", type = "check", label = "Do you use Endurance Charges?", apply = function(val, modList, enemyModList) modList:NewMod("UseEnduranceCharges", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) end }, - { var = "overrideEnduranceCharges", type = "count", label = "# of Endurance Charges (if not maximum):", ifOption = "useEnduranceCharges", apply = function(val, modList, enemyModList) + { var = "overrideEnduranceCharges", type = "countAllowZero", label = "# of Endurance Charges (if not maximum):", ifOption = "useEnduranceCharges", apply = function(val, modList, enemyModList) modList:NewMod("EnduranceCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) end }, { var = "useSiphoningCharges", type = "check", label = "Do you use Siphoning Charges?", ifMult = "SiphoningCharge", apply = function(val, modList, enemyModList) @@ -982,13 +982,13 @@ Huge sets the radius to 11. { var = "minionsUseEnduranceCharges", type = "check", label = "Do your Minions use Endur. Charges?", ifFlag = "haveMinion", apply = function(val, modList, enemyModList) modList:NewMod("MinionModifier", "LIST", { mod = modLib.createMod("UseEnduranceCharges", "FLAG", true, "Config", { type = "Condition", var = "Combat" }) }, "Config") end }, - { var = "minionsOverridePowerCharges", type = "count", label = "# of Power Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUsePowerCharges", apply = function(val, modList, enemyModList) + { var = "minionsOverridePowerCharges", type = "countAllowZero", label = "# of Power Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUsePowerCharges", apply = function(val, modList, enemyModList) modList:NewMod("MinionModifier", "LIST", { mod = modLib.createMod("PowerCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) }, "Config") end }, - { var = "minionsOverrideFrenzyCharges", type = "count", label = "# of Frenzy Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUseFrenzyCharges", apply = function(val, modList, enemyModList) + { var = "minionsOverrideFrenzyCharges", type = "countAllowZero", label = "# of Frenzy Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUseFrenzyCharges", apply = function(val, modList, enemyModList) modList:NewMod("MinionModifier", "LIST", { mod = modLib.createMod("FrenzyCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) }, "Config") end }, - { var = "minionsOverrideEnduranceCharges", type = "count", label = "# of Endurance Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUseEnduranceCharges", apply = function(val, modList, enemyModList) + { var = "minionsOverrideEnduranceCharges", type = "countAllowZero", label = "# of Endurance Charges (if not maximum):", ifFlag = "haveMinion", ifOption = "minionsUseEnduranceCharges", apply = function(val, modList, enemyModList) modList:NewMod("MinionModifier", "LIST", { mod = modLib.createMod("EnduranceCharges", "OVERRIDE", val, "Config", { type = "Condition", var = "Combat" }) }, "Config") end }, { var = "multiplierRampage", type = "count", label = "# of Rampage Kills:", ifFlag = "Condition:Rampage", tooltip = "Rampage grants the following, up to 1000 stacks:\n\t1% increased Movement Speed per 20 Rampage\n\t2% increased Damage per 20 Rampage\nYou lose Rampage if you do not get a Kill within 5 seconds.", apply = function(val, modList, enemyModList)