Skip to content
Open
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
177 changes: 177 additions & 0 deletions spec/System/TestConfigSetCodec_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
describe("TestConfigSetCodec", function()
local savedDeflate, savedInflate

before_each(function()
newBuild()
-- Headless stubs for Deflate/Inflate return "",
-- which would break encode/decode.
-- Replaced them with identity functions,
-- so that the base64 + XML layer is tested in isolation.
savedDeflate = Deflate
savedInflate = Inflate
_G.Deflate = function(data) return data end
_G.Inflate = function(data) return data end
end)

after_each(function()
_G.Deflate = savedDeflate
_G.Inflate = savedInflate
end)

-- Mirrors the serialisation logic in ConfigTabClass:OpenExportConfigSetPopup.
local function encodeConfigSet(configSet, configTab)
local xmlNode = { elem = "ConfigSet", attrib = { title = configSet.title } }
for k, v in pairs(configSet.input) do
if v ~= configTab:GetDefaultState(k, type(v)) then
local node = { elem = "Input", attrib = { name = k } }
if type(v) == "number" then
node.attrib.number = tostring(v)
elseif type(v) == "boolean" then
node.attrib.boolean = tostring(v)
else
node.attrib.string = tostring(v)
end
table.insert(xmlNode, node)
end
end
for k, v in pairs(configSet.placeholder) do
local node = { elem = "Placeholder", attrib = { name = k } }
if type(v) == "number" then
node.attrib.number = tostring(v)
else
node.attrib.string = tostring(v)
end
table.insert(xmlNode, node)
end
local xmlText = common.xml.ComposeXML(xmlNode)
return common.base64.encode(Deflate(xmlText)):gsub("+", "-"):gsub("/", "_")
end

-- Mirrors the deserialisation logic in ConfigTabClass:OpenImportConfigSetPopup.
local function decodeConfigSet(code, configTab, name)
local xmlText = Inflate(common.base64.decode(code:gsub("-", "+"):gsub("_", "/")))
if not xmlText or #xmlText == 0 then
return nil, "decode failed"
end
local parsedXML, errMsg = common.xml.ParseXML(xmlText)
if errMsg or not parsedXML or not parsedXML[1] or parsedXML[1].elem ~= "ConfigSet" then
return nil, errMsg or "invalid config set code"
end
local xmlConfigSet = parsedXML[1]
local newConfigSet = configTab:NewConfigSet(nil, name or xmlConfigSet.attrib.title or "Imported")
for _, child in ipairs(xmlConfigSet) do
if child.elem == "Input" and child.attrib.name then
if child.attrib.number then
newConfigSet.input[child.attrib.name] = tonumber(child.attrib.number)
elseif child.attrib.boolean then
newConfigSet.input[child.attrib.name] = child.attrib.boolean == "true"
elseif child.attrib.string then
newConfigSet.input[child.attrib.name] = child.attrib.string
end
elseif child.elem == "Placeholder" and child.attrib.name then
if child.attrib.number then
newConfigSet.placeholder[child.attrib.name] = tonumber(child.attrib.number)
elseif child.attrib.string then
newConfigSet.placeholder[child.attrib.name] = child.attrib.string
end
end
end
return newConfigSet, nil
end

it("export produces a non-empty base64 code", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
configSet.input["usePowerCharges"] = true
local code = encodeConfigSet(configSet, configTab)
assert.is_not_nil(code)
assert.is_true(#code > 0)
end)

it("roundtrip preserves boolean inputs", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
configSet.input["usePowerCharges"] = true
local code = encodeConfigSet(configSet, configTab)
local imported, err = decodeConfigSet(code, configTab, "Test")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal(true, imported.input["usePowerCharges"])
end)

it("roundtrip preserves numeric inputs", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
-- Use 75 to avoid matching any boss-level placeholder (83/84/85),
-- which would cause GetDefaultState skip export.
configSet.input["enemyLevel"] = 75
local code = encodeConfigSet(configSet, configTab)
local imported, err = decodeConfigSet(code, configTab, "Test")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal(75, imported.input["enemyLevel"])
end)

it("roundtrip preserves string inputs", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
-- "Uber" is non-default (default is "Pinnacle" from defaultIndex=3)
configSet.input["enemyIsBoss"] = "Uber"
local code = encodeConfigSet(configSet, configTab)
local imported, err = decodeConfigSet(code, configTab, "Test")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal("Uber", imported.input["enemyIsBoss"])
end)

it("roundtrip preserves multiple values simultaneously", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
configSet.input["usePowerCharges"] = true
configSet.input["enemyLevel"] = 75
configSet.input["enemyIsBoss"] = "Uber"
local code = encodeConfigSet(configSet, configTab)
local imported, err = decodeConfigSet(code, configTab, "Multi")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal(true, imported.input["usePowerCharges"])
assert.are.equal(75, imported.input["enemyLevel"])
assert.are.equal("Uber", imported.input["enemyIsBoss"])
end)

it("import uses the provided name, not the exported title", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
configSet.title = "Original Title"
local code = encodeConfigSet(configSet, configTab)
local imported, err = decodeConfigSet(code, configTab, "Custom Name")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal("Custom Name", imported.title)
end)

it("export and re-import of an unmodified config set succeeds", function()
local configTab = build.configTab
local configSet = configTab.configSets[configTab.activeConfigSetId]
local code = encodeConfigSet(configSet, configTab)
assert.is_true(#code > 0)
local imported, err = decodeConfigSet(code, configTab, "Imported")
assert.is_nil(err)
assert.is_not_nil(imported)
assert.are.equal("Imported", imported.title)
end)

it("returns error for garbage input", function()
local configTab = build.configTab
local _, err = decodeConfigSet("!!!not_valid!!!", configTab, "Test")
assert.is_not_nil(err)
end)

it("returns error when decoded XML root is not a ConfigSet", function()
local configTab = build.configTab
local xmlText = "<PathOfBuilding></PathOfBuilding>"
local code = common.base64.encode(Deflate(xmlText)):gsub("+", "-"):gsub("/", "_")
local _, err = decodeConfigSet(code, configTab, "Test")
assert.is_not_nil(err)
end)
end)
114 changes: 112 additions & 2 deletions src/Classes/ConfigTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -946,14 +946,124 @@ function ConfigTabClass:RestoreUndoState(state)
end

function ConfigTabClass:OpenConfigSetManagePopup()
local listControl = new("ConfigSetListControl", nil, {0, 50, 350, 200}, self)
local importConfig = new("ButtonControl", nil, {-99, 259, 90, 20}, "Import Config", function()
self:OpenImportConfigSetPopup()
end)
local exportConfig = new("ButtonControl", {"LEFT", importConfig, "RIGHT"}, {8, 0, 90, 20}, "Export Config", function()
if listControl.selValue then
self:OpenExportConfigSetPopup(self.configSets[listControl.selValue])
end
end)
exportConfig.enabled = function()
return listControl.selValue ~= nil
end
main:OpenPopup(370, 290, "Manage Config Sets", {
new("ConfigSetListControl", nil, {0, 50, 350, 200}, self),
new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function()
listControl,
importConfig,
exportConfig,
new("ButtonControl", {"LEFT", exportConfig, "RIGHT"}, {8, 0, 90, 20}, "Done", function()
main:ClosePopup()
end),
})
end

function ConfigTabClass:OpenExportConfigSetPopup(configSet)
local xmlNode = { elem = "ConfigSet", attrib = { title = configSet.title } }
for k, v in pairs(configSet.input) do
if v ~= self:GetDefaultState(k, type(v)) then
local node = { elem = "Input", attrib = { name = k } }
if type(v) == "number" then
node.attrib.number = tostring(v)
elseif type(v) == "boolean" then
node.attrib.boolean = tostring(v)
else
node.attrib.string = tostring(v)
end
t_insert(xmlNode, node)
end
end
for k, v in pairs(configSet.placeholder) do
local node = { elem = "Placeholder", attrib = { name = k } }
if type(v) == "number" then
node.attrib.number = tostring(v)
else
node.attrib.string = tostring(v)
end
t_insert(xmlNode, node)
end
local xmlText = common.xml.ComposeXML(xmlNode)
local code = common.base64.encode(Deflate(xmlText)):gsub("+", "-"):gsub("/", "_")
local controls = { }
controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "Config set code:")
controls.edit = new("EditControl", nil, {0, 40, 350, 18}, code, nil, "%Z")
controls.copy = new("ButtonControl", nil, {-45, 70, 80, 20}, "Copy", function()
Copy(code)
end)
controls.done = new("ButtonControl", nil, {45, 70, 80, 20}, "Done", function()
main:ClosePopup()
end)
main:OpenPopup(380, 100, "Export Config Set", controls, "done", "edit")
end

function ConfigTabClass:OpenImportConfigSetPopup()
local controls = { }
controls.nameLabel = new("LabelControl", nil, {-175, 20, 0, 16}, "Enter name for this config set:")
controls.name = new("EditControl", nil, {100, 20, 350, 18}, "", nil, nil, nil, function(buf)
controls.msg.label = ""
controls.import.enabled = buf:match("%S") and controls.edit.buf:match("%S")
end)
controls.editLabel = new("LabelControl", nil, {-150, 45, 0, 16}, "Enter config set code:")
controls.edit = new("EditControl", nil, {100, 45, 350, 18}, "", nil, nil, nil, function(buf)
controls.msg.label = ""
controls.import.enabled = buf:match("%S") and controls.name.buf:match("%S")
end)
controls.msg = new("LabelControl", nil, {0, 65, 0, 16}, "")
controls.import = new("ButtonControl", nil, {-45, 85, 80, 20}, "Import", function()
local buf = controls.edit.buf
if #buf == 0 then return end
local xmlText = Inflate(common.base64.decode(buf:gsub("-", "+"):gsub("_", "/")))
if not xmlText then
controls.msg.label = "^1Invalid code"
return
end
local parsedXML, errMsg = common.xml.ParseXML(xmlText)
if errMsg or not parsedXML or not parsedXML[1] or parsedXML[1].elem ~= "ConfigSet" then
controls.msg.label = "^1Invalid config set code"
return
end
local xmlConfigSet = parsedXML[1]
local newConfigSet = self:NewConfigSet(nil, controls.name.buf)
for _, child in ipairs(xmlConfigSet) do
if child.elem == "Input" and child.attrib.name then
if child.attrib.number then
newConfigSet.input[child.attrib.name] = tonumber(child.attrib.number)
elseif child.attrib.boolean then
newConfigSet.input[child.attrib.name] = child.attrib.boolean == "true"
elseif child.attrib.string then
newConfigSet.input[child.attrib.name] = child.attrib.string
end
elseif child.elem == "Placeholder" and child.attrib.name then
if child.attrib.number then
newConfigSet.placeholder[child.attrib.name] = tonumber(child.attrib.number)
elseif child.attrib.string then
newConfigSet.placeholder[child.attrib.name] = child.attrib.string
end
end
end
t_insert(self.configSetOrderList, newConfigSet.id)
self.modFlag = true
self:AddUndoState()
self.build:SyncLoadouts()
main:ClosePopup()
end)
controls.import.enabled = false
controls.cancel = new("ButtonControl", nil, {45, 85, 80, 20}, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(580, 115, "Import Config Set", controls, "import", "name")
end

-- Creates a new config set
function ConfigTabClass:NewConfigSet(configSetId, title)
local configSet = { id = configSetId, title = title, input = { }, placeholder = { } }
Expand Down