From f04c5db493558f24421036f68e71db708d5bea55 Mon Sep 17 00:00:00 2001 From: Parnic Date: Fri, 2 Sep 2022 21:44:12 -0500 Subject: [PATCH] Re-enable dev-only profile export/import Exporting works pretty well, but importing is rough (mostly because error messaging isn't quite in place, and it's not obvious that you have to close the window to execute the import). I found a generic json serializer, decided to adapt it to WoW, and it seems to work. But anyway, I saw this code sitting around and figured it wouldn't take too much work to get it in working order. I was mostly right. --- IceHUD.lua | 39 ++++- IceHUD.toc | 3 + IceHUD_Options/IceHUD_Options.toc | 3 + IceHUD_Options/Json.lua | 1 + IceHUD_Options/JsonDecode.lua | 241 ++++++++++++++++++++++++++++++ IceHUD_Options/JsonEncode.lua | 100 +++++++++++++ IceHUD_Options/Options.lua | 30 ++-- IceHUD_Options/TablePrint.lua | 37 +++++ 8 files changed, 439 insertions(+), 15 deletions(-) create mode 100644 IceHUD_Options/Json.lua create mode 100644 IceHUD_Options/JsonDecode.lua create mode 100644 IceHUD_Options/JsonEncode.lua create mode 100644 IceHUD_Options/TablePrint.lua diff --git a/IceHUD.lua b/IceHUD.lua index ecd5ff0..3498619 100644 --- a/IceHUD.lua +++ b/IceHUD.lua @@ -7,8 +7,6 @@ local SML = LibStub("LibSharedMedia-3.0") local ACR = LibStub("AceConfigRegistry-3.0") local ConfigDialog = LibStub("AceConfigDialog-3.0") local icon = LibStub("LibDBIcon-1.0", true) -local AceGUI = LibStub("AceGUI-3.0") -local AceSerializer = LibStub("AceSerializer-3.0", 1) local pendingModuleLoads = {} local bReadyToRegisterModules = false @@ -92,6 +90,43 @@ end IceHUD.deepcopy = deepcopy +function IceHUD:removeDefaults(db, defaults, blocker) + -- remove all metatables from the db, so we don't accidentally create new sub-tables through them + setmetatable(db, nil) + -- loop through the defaults and remove their content + for k,v in pairs(defaults) do + if type(v) == "table" and type(db[k]) == "table" then + -- if a blocker was set, dive into it, to allow multi-level defaults + self:removeDefaults(db[k], v, blocker and blocker[k]) + if next(db[k]) == nil then + db[k] = nil + end + else + -- check if the current value matches the default, and that its not blocked by another defaults table + if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then + db[k] = nil + end + end + end +end + +function IceHUD:populateDefaults(db, defaults, blocker) + -- remove all metatables from the db, so we don't accidentally create new sub-tables through them + setmetatable(db, nil) + -- loop through the defaults and add their content + for k,v in pairs(defaults) do + if type(v) == "table" and type(db[k]) == "table" then + -- if a blocker was set, dive into it, to allow multi-level defaults + self:populateDefaults(db[k], v, blocker and blocker[k]) + else + -- check if the current value matches the default, and that its not blocked by another defaults table + if db[k] == nil then + db[k] = defaults[k] + end + end + end +end + IceHUD.Location = "Interface\\AddOns\\IceHUD" StaticPopupDialogs["ICEHUD_CUSTOM_BAR_CREATED"] = diff --git a/IceHUD.toc b/IceHUD.toc index 3331599..c4223eb 100644 --- a/IceHUD.toc +++ b/IceHUD.toc @@ -100,5 +100,8 @@ modules\ArcaneCharges.lua modules\RollTheBones.lua #@do-not-package@ +IceHUD_Options\Json.lua +IceHUD_Options\JsonDecode.lua +IceHUD_Options\JsonEncode.lua IceHUD_Options\Options.lua #@end-do-not-package@ diff --git a/IceHUD_Options/IceHUD_Options.toc b/IceHUD_Options/IceHUD_Options.toc index 0831299..93a5a49 100644 --- a/IceHUD_Options/IceHUD_Options.toc +++ b/IceHUD_Options/IceHUD_Options.toc @@ -11,4 +11,7 @@ ## Dependencies: IceHUD ## LoadOnDemand: 1 +Json.lua +JsonDecode.lua +JsonEncode.lua Options.lua diff --git a/IceHUD_Options/Json.lua b/IceHUD_Options/Json.lua new file mode 100644 index 0000000..26e59e3 --- /dev/null +++ b/IceHUD_Options/Json.lua @@ -0,0 +1 @@ +IceHUD.json = {} diff --git a/IceHUD_Options/JsonDecode.lua b/IceHUD_Options/JsonDecode.lua new file mode 100644 index 0000000..8be6a16 --- /dev/null +++ b/IceHUD_Options/JsonDecode.lua @@ -0,0 +1,241 @@ +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[select(i, ...)] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + ["true"] = true, + ["false"] = false, + ["null"] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + + return string.format("%s at line %d col %d", msg, line_count, col_count) +end + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + return "", string.format("invalid unicode codepoint '%x'", n) +end + +local function parse_unicode_escape(s) + local n1 = tonumber(s:sub(1, 4), 16) + local n2 = tonumber(s:sub(7, 10), 16) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + return str, j, decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or false + if hex == false then + return str, j-1, decode_error(str, j - 1, "invalid unicode escape in string") + end + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + return str, j-1, decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + return str, i, decode_error(str, i, "expected closing quote for string") +end + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + return -1, -1, decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + return false, -1, decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then return nil, -1, decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + return nil, -1, decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + return nil, -1, decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then return nil, -1, decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + +local char_func_map = { + ['"'] = parse_string, + ["0"] = parse_number, + ["1"] = parse_number, + ["2"] = parse_number, + ["3"] = parse_number, + ["4"] = parse_number, + ["5"] = parse_number, + ["6"] = parse_number, + ["7"] = parse_number, + ["8"] = parse_number, + ["9"] = parse_number, + ["-"] = parse_number, + ["t"] = parse_literal, + ["f"] = parse_literal, + ["n"] = parse_literal, + ["["] = parse_array, + ["{"] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + return false, -1, decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function IceHUD.json.decode(str) + if type(str) ~= "string" then + return nil, "expected argument of type string, got " .. type(str) + end + local res, idx, err = parse(str, next_char(str, 1, space_chars, true)) + if err ~= nil then + return nil, err + end + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + return nil, decode_error(str, idx, "trailing garbage") + end + return res, nil +end diff --git a/IceHUD_Options/JsonEncode.lua b/IceHUD_Options/JsonEncode.lua new file mode 100644 index 0000000..0ebe412 --- /dev/null +++ b/IceHUD_Options/JsonEncode.lua @@ -0,0 +1,100 @@ +local encode + +local escape_char_map = { + ["\\"] = "\\", + ["\""] = "\"", + ["\b"] = "b", + ["\f"] = "f", + ["\n"] = "n", + ["\r"] = "r", + ["\t"] = "t", +} + +local escape_char_map_inv = { ["/"] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + +local function encode_nil(val) + return "null" +end + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + +local type_func_map = { + ["nil"] = encode_nil, + ["table"] = encode_table, + ["string"] = encode_string, + ["number"] = encode_number, + ["boolean"] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + +function IceHUD.json.encode(val) + return (encode(val)) +end diff --git a/IceHUD_Options/Options.lua b/IceHUD_Options/Options.lua index 5b089fe..823be6c 100644 --- a/IceHUD_Options/Options.lua +++ b/IceHUD_Options/Options.lua @@ -1,6 +1,8 @@ local LibDualSpec = LibStub('LibDualSpec-1.0', true) local L = LibStub("AceLocale-3.0"):GetLocale("IceHUD", false) local icon = LibStub("LibDBIcon-1.0", true) +local AceGUI = LibStub("AceGUI-3.0") +local AceSerializer = LibStub("AceSerializer-3.0", 1) local lastCustomModule = "Bar" IceHUD_Options = {} @@ -757,6 +759,9 @@ function IceHUD_Options:OnLoad() self:GenerateModuleOptions(true) self.options.args.colors.args = IceHUD.IceCore:GetColorOptions() self.options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(IceHUD.db) + --@debug@ + IceHUD_Options:SetupProfileImportButtons() + --@end-debug@ -- Add dual-spec support if IceHUD.db ~= nil and LibDualSpec then @@ -787,14 +792,14 @@ function IceHUD_Options:SetupProfileImportButtons() editbox:SetLabel("Profile") editbox:SetFullWidth(true) editbox:SetFullHeight(true) - local profileTable = deepcopy(IceHUD.db.profile) + local profileTable = IceHUD.deepcopy(IceHUD.db.profile) IceHUD:removeDefaults(profileTable, IceHUD.IceCore.defaults.profile) - editbox:SetText(IceHUD:Serialize(profileTable)) + editbox:SetText(IceHUD.json.encode(profileTable)) editbox:DisableButton(true) frame:AddChild(editbox) end, hidden = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! exporting works well enough, but importing is very rough, so enable this at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -803,7 +808,7 @@ function IceHUD_Options:SetupProfileImportButtons() --@end-debug@ , disabled = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! exporting works well enough, but importing is very rough, so enable this at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -824,11 +829,14 @@ function IceHUD_Options:SetupProfileImportButtons() frame:SetStatusText("Exported profile details") frame:SetLayout("Flow") frame:SetCallback("OnClose", function(widget) - local success, newTable = IceHUD:Deserialize(widget.children[1]:GetText()) - if success then + local newTable, err = IceHUD.json.decode(widget.children[1]:GetText()) + if err ~= nil then + print("failed to import profile: "..err) + else + print("importing profile") IceHUD:PreProfileChanged() IceHUD:populateDefaults(newTable, IceHUD.IceCore.defaults.profile) - IceHUD.db.profile = deepcopy(newTable) + IceHUD.db.profile = IceHUD.deepcopy(newTable) IceHUD:PostProfileChanged() end AceGUI:Release(widget) @@ -841,7 +849,7 @@ function IceHUD_Options:SetupProfileImportButtons() frame:AddChild(editbox) end, hidden = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! this feature is really rough, so enable it at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -850,7 +858,7 @@ function IceHUD_Options:SetupProfileImportButtons() --@end-debug@ , disabled = - -- hello, snooper! this feature doesn't actually work yet, so enabling it won't help you much :) + -- hello, snooper! this feature is really rough, so enable it at your own peril --[===[@non-debug@ true --@end-non-debug@]===] @@ -862,7 +870,3 @@ function IceHUD_Options:SetupProfileImportButtons() } end end - ---@debug@ -IceHUD_Options:SetupProfileImportButtons() ---@end-debug@ diff --git a/IceHUD_Options/TablePrint.lua b/IceHUD_Options/TablePrint.lua new file mode 100644 index 0000000..f79eadc --- /dev/null +++ b/IceHUD_Options/TablePrint.lua @@ -0,0 +1,37 @@ +local function table_print(tt, indent, done) + done = done or {} + indent = indent or 0 + if type(tt) == "table" then + local sb = {} + for key, value in pairs(tt) do + table.insert(sb, string.rep(" ", indent)) -- indent it + if type(value) == "table" and not done[value] then + done[value] = true + table.insert(sb, key .. " = {\n"); + table.insert(sb, table_print(value, indent + 2, done)) + table.insert(sb, string.rep(" ", indent)) -- indent it + table.insert(sb, "}\n"); + elseif "number" == type(key) then + table.insert(sb, string.format("\"%s\"\n", tostring(value))) + else + table.insert(sb, string.format( + "%s = \"%s\"\n", tostring(key), tostring(value))) + end + end + return table.concat(sb) + else + return tt .. "\n" + end +end + +local function to_string(tbl) + if "nil" == type(tbl) then + return tostring(nil) + elseif "table" == type(tbl) then + return table_print(tbl) + elseif "string" == type(tbl) then + return tbl + else + return tostring(tbl) + end +end