From 1a5e97b8d1fc137e3774d25d7fa95ba83acf26e4 Mon Sep 17 00:00:00 2001 From: Alkorith <101947793+Alkorith@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:17:59 -0500 Subject: [PATCH] feat(filterpreset): Implement loading/saving filter presets commit 3f6636a2fb6f60c18769148680e1e60f186663a3 Author: Alkorith <101947793+Alkorith@users.noreply.github.com> Date: Mon Sep 11 11:25:05 2023 -0500 feat(filterpreset): Implement loading/saving filter presets commit 98725542c8dbc223d963987e2c0668badb5fe5e9 Author: Alkorith <101947793+Alkorith@users.noreply.github.com> Date: Mon Sep 11 11:30:53 2023 -0500 chore(filterpresets): Add lunajson dependency --- .../playerInfoFrame/searchfilter.lua | 63 ++ Themes/Rebirth/Languages/en.ini | 7 +- .../ScreenSelectMusic decorations/filters.lua | 87 ++- Themes/Til Death/Languages/en.ini | 7 + Themes/_fallback/lib/lua/5.1/filterpreset.lua | 7 + .../lib/lua/5.1/filterpreset/load_preset.lua | 80 +++ .../lib/lua/5.1/filterpreset/save_preset.lua | 44 ++ lib/lua/5.1/lunajson.lua | 8 + lib/lua/5.1/lunajson/decoder.lua | 538 ++++++++++++++++++ lib/lua/5.1/lunajson/encoder.lua | 208 +++++++ src/Etterna/Singletons/FilterManager.cpp | 21 + src/Etterna/Singletons/ThemeManager.cpp | 7 + 12 files changed, 1075 insertions(+), 2 deletions(-) create mode 100644 Themes/_fallback/lib/lua/5.1/filterpreset.lua create mode 100644 Themes/_fallback/lib/lua/5.1/filterpreset/load_preset.lua create mode 100644 Themes/_fallback/lib/lua/5.1/filterpreset/save_preset.lua create mode 100644 lib/lua/5.1/lunajson.lua create mode 100644 lib/lua/5.1/lunajson/decoder.lua create mode 100644 lib/lua/5.1/lunajson/encoder.lua diff --git a/Themes/Rebirth/BGAnimations/playerInfoFrame/searchfilter.lua b/Themes/Rebirth/BGAnimations/playerInfoFrame/searchfilter.lua index ab1bf13c84..3429b11b3d 100644 --- a/Themes/Rebirth/BGAnimations/playerInfoFrame/searchfilter.lua +++ b/Themes/Rebirth/BGAnimations/playerInfoFrame/searchfilter.lua @@ -1,3 +1,5 @@ +local filterpreset = require('filterpreset') + local ratios = { Width = 782 / 1920, Height = 971 / 1080, @@ -55,6 +57,9 @@ local translations = { Results = THEME:GetString("SearchFilter", "Results"), Reset = THEME:GetString("SearchFilter", "Reset"), Apply = THEME:GetString("SearchFilter", "Apply"), + SaveToDefaultPreset = THEME:GetString("FilterPreset", "SaveToDefaultPreset"), + ExportPresetToFile = THEME:GetString("FilterPreset", "ExportPresetToFile"), + SaveFilterPresetPrompt = THEME:GetString("FilterPreset", "SaveFilterPresetPrompt"), } local visibleframeX = SCREEN_WIDTH - actuals.Width @@ -1170,6 +1175,60 @@ local function lowerSection() end } + t[#t+1] = filterMiscLine(9) .. { + Name = "SaveLine", + InitCommand = function(self) + self:settext(translations["SaveToDefaultPreset"]) + end, + MouseOverCommand = onHover, + MouseOutCommand = onUnHover, + MouseDownCommand = function(self) + filterpreset.save_preset("default", PLAYER_1) + end + } + + + t[#t+1] = filterMiscLine(10) .. { + Name = "ExportLine", + InitCommand = function(self) + self:settext(translations["ExportPresetToFile"]) + end, + MouseOverCommand = onHover, + MouseOutCommand = onUnHover, + MouseDownCommand = function(self) + local redir = SCREENMAN:get_input_redirected(PLAYER_1) + local function off() + if redir then + SCREENMAN:set_input_redirected(PLAYER_1, false) + end + end + local function on() + if redir then + SCREENMAN:set_input_redirected(PLAYER_1, true) + end + end + off() + + askForInputStringWithFunction( + translations["SaveFilterPresetPrompt"], + 32, + false, + function(answer) + -- success if the answer isnt blank + if answer:gsub("^%s*(.-)%s*$", "%1") ~= "" then + filterpreset.save_preset(answer) + else + on() + end + end, + function() return true, "" end, + function() + on() + end + ) + end + } + return t end @@ -1218,4 +1277,8 @@ t[#t+1] = Def.Quad { t[#t+1] = upperSection() t[#t+1] = lowerSection() +-- Load default preset if it exists. We should only be setting the values once +-- at startup. Subsequent calls should not occur. +filterpreset.load_preset("default", false, PLAYER_1) + return t diff --git a/Themes/Rebirth/Languages/en.ini b/Themes/Rebirth/Languages/en.ini index 77ad14c004..3bf175bf23 100644 --- a/Themes/Rebirth/Languages/en.ini +++ b/Themes/Rebirth/Languages/en.ini @@ -627,6 +627,11 @@ Results=Matches Reset=Reset Apply=Apply +[FilterPreset] +ExportPresetToFile=Export +SaveFilterPresetPrompt=Enter filter preset file name: +SaveToDefaultPreset=Save + [Settings] NothingBound=none CurrentlyBinding=Currently Binding @@ -1021,4 +1026,4 @@ Bm_Double7=14K+2 Maniax_Single=4K Maniax_Double=8K Pnm_Five=5K -Pnm_Nine=9K \ No newline at end of file +Pnm_Nine=9K diff --git a/Themes/Til Death/BGAnimations/ScreenSelectMusic decorations/filters.lua b/Themes/Til Death/BGAnimations/ScreenSelectMusic decorations/filters.lua index c8ad8f8e60..9563da4f15 100644 --- a/Themes/Til Death/BGAnimations/ScreenSelectMusic decorations/filters.lua +++ b/Themes/Til Death/BGAnimations/ScreenSelectMusic decorations/filters.lua @@ -1,3 +1,5 @@ +local filterpreset = require('filterpreset') + local numbershers = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"} local frameX = 10 local frameY = 45 @@ -99,6 +101,9 @@ local translated_info = { MaxRate = THEME:GetString("TabFilter", "MaxRate"), Title = THEME:GetString("TabFilter", "Title"), MinRate = THEME:GetString("TabFilter", "MinRate"), + ExportFilterToFile = THEME:GetString("FilterPreset", "ExportFilterToFile"), + SaveFilterPresetPrompt = THEME:GetString("FilterPreset", "SaveFilterPresetPrompt"), + SaveToDefaultFilterPreset = THEME:GetString("FilterPreset", "SaveToDefaultFilterPreset"), } local f = Def.ActorFrame { @@ -433,6 +438,81 @@ local f = Def.ActorFrame { ResetFilterMessageCommand = function(self) self:queuecommand("Set") end + }, + --[[ + -- FIXME: Hot Reloading does not work (yet). You would need to leave the + -- song select menu, then go back in for the filter to apply. + UIElements.TextToolTip(1, 1, "Common Large") ..{ + InitCommand = function(self) + self:xy(10, 175 + spacingY * -2) + self:zoom(textzoom) + self:halign(0) + self:diffuse(getMainColor("positive")) + self:settext(translated_info["LoadFilterPreset"]) + end, + MouseOverCommand = function(self) + self:diffusealpha(hoverAlpha) + end, + MouseOutCommand = function(self) + self:diffusealpha(1) + end, + MouseDownCommand = function(self, params) + if params.event == "DeviceButton_left mouse button" and active then + easyInputStringWithParams( + translated_info["LoadFilterPresetPrompt"], + 32, + false, + filterpreset.load_preset, + true + ) + end + end + }, + --]] + UIElements.TextToolTip(1, 1, "Common Large") ..{ + InitCommand = function(self) + self:xy(frameX + frameWidth / 2 + 100, 175 + spacingY * 7) + self:zoom(textzoom) + self:halign(0) + self:settext(translated_info["SaveToDefaultFilterPreset"]) + self:diffuse(getMainColor("positive")) + end, + MouseOverCommand = function(self) + self:diffusealpha(hoverAlpha) + end, + MouseOutCommand = function(self) + self:diffusealpha(1) + end, + MouseDownCommand = function(self, params) + if params.event == "DeviceButton_left mouse button" and active then + filterpreset.save_preset("default", PLAYER_1) + end + end + }, + UIElements.TextToolTip(1, 1, "Common Large") ..{ + InitCommand = function(self) + self:xy(frameX + frameWidth / 2 + 100, 175 + spacingY * 8) + self:zoom(textzoom) + self:halign(0) + self:diffuse(getMainColor("positive")) + self:settext("Export") + end, + MouseOverCommand = function(self) + self:diffusealpha(hoverAlpha) + end, + MouseOutCommand = function(self) + self:diffusealpha(1) + end, + MouseDownCommand = function(self, params) + if params.event == "DeviceButton_left mouse button" and active then + easyInputStringWithFunction( + THEME:GetString("FilterPreset", "SaveFilterPresetPrompt"), + 32, + false, + filterpreset.save_preset + ) + end + end } } @@ -648,9 +728,14 @@ f[#f + 1] = UIElements.TextButton(1, 1, "Common Large") .. { end end, } -]] +--]] for i = 1, (#ms.SkillSets + 2) do f[#f + 1] = CreateFilterInputBox(i) end + +-- Load default preset if it exists. We should only be setting the values once +-- at startup. Subsequent calls should not occur. +filterpreset.load_preset("default", false, PLAYER_1) + return f diff --git a/Themes/Til Death/Languages/en.ini b/Themes/Til Death/Languages/en.ini index 02b9136fe4..195446a516 100644 --- a/Themes/Til Death/Languages/en.ini +++ b/Themes/Til Death/Languages/en.ini @@ -572,6 +572,13 @@ Apply=Apply AND=ALL OR=ANY +[FilterPreset] +ExportFilterToFile=Export +LoadFilterPreset=Load +LoadFilterPresetPrompt=Enter filter preset file name: +SaveFilterPresetPrompt=Enter filter preset file name: +SaveToDefaultFilterPreset=Save + [TabGoals] PriorityLong=Priority PriorityShort=P diff --git a/Themes/_fallback/lib/lua/5.1/filterpreset.lua b/Themes/_fallback/lib/lua/5.1/filterpreset.lua new file mode 100644 index 0000000000..e27534aff9 --- /dev/null +++ b/Themes/_fallback/lib/lua/5.1/filterpreset.lua @@ -0,0 +1,7 @@ +local load_preset = require("filterpreset.load_preset") +local save_preset = require("filterpreset.save_preset") + +return { + load_preset = load_preset, + save_preset = save_preset +} diff --git a/Themes/_fallback/lib/lua/5.1/filterpreset/load_preset.lua b/Themes/_fallback/lib/lua/5.1/filterpreset/load_preset.lua new file mode 100644 index 0000000000..2c73c3b42f --- /dev/null +++ b/Themes/_fallback/lib/lua/5.1/filterpreset/load_preset.lua @@ -0,0 +1,80 @@ +local json = require('lunajson') + +local first_time_call_guard = true + +-- Use default skillset names +local json_filter_categories = DeepCopy(ms.SkillSets) +table.insert(json_filter_categories, "Length") +table.insert(json_filter_categories, "ClearPercent") + +local RateKey = "Rate" +local ExclusiveFilterKey = "ExclusiveFilter" +local HighestSkillsetOnlyKey = "HighestSkillsetOnly" +local HighestDifficultyOnlyKey = "HighestDifficultyOnly" + +local function load_preset(filename, explicit_call, pn) + explicit_call = explicit_call or false + + if not (explicit_call or first_time_call_guard) then return end + if first_time_call_guard then first_time_call_guard = false end + + pn = pn or PLAYER_1 + filename = filename or "default.json" + + local full_path = ( + PROFILEMAN:GetProfileDir(pn_to_profile_slot(pn)) + .. '/filter_presets/' + .. filename + .. '.json' + ) + + local file_output = File.Read(full_path) + if not file_output then return end + + + local data = json.decode(file_output) + + for i = 1, #json_filter_categories do + if data[json_filter_categories[i]] then + local setting = data[json_filter_categories[i]] + + if setting.min then + FILTERMAN:SetSSFilter(setting.min, i, 0) + end + + if setting.max then + FILTERMAN:SetSSFilter(setting.max, i, 1) + end + end + end + + if data[RateKey] then + local setting = data[RateKey] + + if setting.min then + FILTERMAN:SetMinFilterRate(setting.min) + end + + if setting.max then + FILTERMAN:SetMaxFilterRate(setting.max) + end + end + + if data[ExclusiveFilterKey] then + FILTERMAN:SetFilterMode(data[ExclusiveFilterKey]) + end + + if data[HighestSkillsetOnlyKey] then + FILTERMAN:SetHighestSkillsetsOnly(data[HighestSkillsetOnlyKey]) + end + + if data[HighestDifficultyOnlyKey] then + FILTERMAN:HighestDifficultyOnlyKey(data[HighestDifficultyOnlyKey]) + end + + if explicit_call then + ms.ok(string.format("Loaded filter preset: %s", full_path)) + end +end + +return load_preset diff --git a/Themes/_fallback/lib/lua/5.1/filterpreset/save_preset.lua b/Themes/_fallback/lib/lua/5.1/filterpreset/save_preset.lua new file mode 100644 index 0000000000..bce68a5d89 --- /dev/null +++ b/Themes/_fallback/lib/lua/5.1/filterpreset/save_preset.lua @@ -0,0 +1,44 @@ +local json = require('lunajson') + +-- Use default skillset names +local json_filter_categories = DeepCopy(ms.SkillSets) +table.insert(json_filter_categories, "Length") +table.insert(json_filter_categories, "ClearPercent") + +local RateKey = "Rate" +local ExclusiveFilterKey = "ExclusiveFilter" +local HighestSkillsetOnlyKey = "HighestSkillsetOnly" +local HighestDifficultyOnlyKey = "HighestDifficultyOnly" + +local function save_preset(filename, pn) + filename = filename or "default" + pn = pn or PLAYER_1 + + local filter_preset = { + Rate = { + min = math.round(FILTERMAN:GetMinFilterRate(), 1), + max = math.round(FILTERMAN:GetMaxFilterRate(), 1) + }, + ExclusiveFilter = FILTERMAN:GetFilterMode(), + HighestSkillsetOnly = FILTERMAN:GetHighestSkillsetsOnly(), + HighestDifficultyOnly = FILTERMAN:GetHighestDifficultyOnly() + } + + for i = 1, #json_filter_categories do + filter_preset[json_filter_categories[i]] = { + min = FILTERMAN:GetSSFilter(i, 0), + max = FILTERMAN:GetSSFilter(i, 1) + } + end + + local full_path = ( + PROFILEMAN:GetProfileDir(pn_to_profile_slot(pn)) + .. '/filter_presets/' + .. filename + .. '.json' + ) + File.Write(full_path, json.encode(filter_preset)) + ms.ok(string.format("Saved file to: %s", full_path)) +end + +return save_preset diff --git a/lib/lua/5.1/lunajson.lua b/lib/lua/5.1/lunajson.lua new file mode 100644 index 0000000000..2ed9700287 --- /dev/null +++ b/lib/lua/5.1/lunajson.lua @@ -0,0 +1,8 @@ +local newdecoder = require 'lunajson.decoder' +local newencoder = require 'lunajson.encoder' +-- If you need multiple contexts of decoder and/or encoder, +-- you can require lunajson.decoder and/or lunajson.encoder directly. +return { + decode = newdecoder(), + encode = newencoder(), +} diff --git a/lib/lua/5.1/lunajson/decoder.lua b/lib/lua/5.1/lunajson/decoder.lua new file mode 100644 index 0000000000..e61f2ce1be --- /dev/null +++ b/lib/lua/5.1/lunajson/decoder.lua @@ -0,0 +1,538 @@ +--[=====[ +The MIT License (MIT) + +Copyright (c) 2015-2017 Shunsuke Shimizu (grafi) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--]=====] +local setmetatable, tonumber, tostring = + setmetatable, tonumber, tostring +local floor, inf = + math.floor, math.huge +local mininteger, tointeger = + math.mininteger or nil, math.tointeger or nil +local byte, char, find, gsub, match, sub = + string.byte, string.char, string.find, string.gsub, string.match, string.sub + +local function _decode_error(pos, errmsg) + error("parse error at " .. pos .. ": " .. errmsg, 2) +end + +local f_str_ctrl_pat +if _VERSION == "Lua 5.1" then + -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly + f_str_ctrl_pat = '[^\32-\255]' +else + f_str_ctrl_pat = '[\0-\31]' +end + +local _ENV = nil + + +local function newdecoder() + local json, pos, nullv, arraylen, rec_depth + + -- `f` is the temporary for dispatcher[c] and + -- the dummy for the first return value of `find` + local dispatcher, f + + --[[ + Helper + --]] + local function decode_error(errmsg) + return _decode_error(pos, errmsg) + end + + --[[ + Invalid + --]] + local function f_err() + decode_error('invalid value') + end + + --[[ + Constants + --]] + -- null + local function f_nul() + if sub(json, pos, pos+2) == 'ull' then + pos = pos+3 + return nullv + end + decode_error('invalid value') + end + + -- false + local function f_fls() + if sub(json, pos, pos+3) == 'alse' then + pos = pos+4 + return false + end + decode_error('invalid value') + end + + -- true + local function f_tru() + if sub(json, pos, pos+2) == 'rue' then + pos = pos+3 + return true + end + decode_error('invalid value') + end + + --[[ + Numbers + Conceptually, the longest prefix that matches to `[-+.0-9A-Za-z]+` (in regexp) + is captured as a number and its conformance to the JSON spec is checked. + --]] + -- deal with non-standard locales + local radixmark = match(tostring(0.5), '[^0-9]') + local fixedtonumber = tonumber + if radixmark ~= '.' then + if find(radixmark, '%W') then + radixmark = '%' .. radixmark + end + fixedtonumber = function(s) + return tonumber(gsub(s, '.', radixmark)) + end + end + + local function number_error() + return decode_error('invalid number') + end + + -- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?` + local function f_zro(mns) + local num, c = match(json, '^(%.?[0-9]*)([-+.A-Za-z]?)', pos) -- skipping 0 + + if num == '' then + if c == '' then + if mns then + return -0.0 + end + return 0 + end + + if c == 'e' or c == 'E' then + num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos) + if c == '' then + pos = pos + #num + if mns then + return -0.0 + end + return 0.0 + end + end + number_error() + end + + if byte(num) ~= 0x2E or byte(num, -1) == 0x2E then + number_error() + end + + if c ~= '' then + if c == 'e' or c == 'E' then + num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos) + end + if c ~= '' then + number_error() + end + end + + pos = pos + #num + c = fixedtonumber(num) + + if mns then + c = -c + end + return c + end + + -- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?` + local function f_num(mns) + pos = pos-1 + local num, c = match(json, '^([0-9]+%.?[0-9]*)([-+.A-Za-z]?)', pos) + if byte(num, -1) == 0x2E then -- error if ended with period + number_error() + end + + if c ~= '' then + if c ~= 'e' and c ~= 'E' then + number_error() + end + num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos) + if not num or c ~= '' then + number_error() + end + end + + pos = pos + #num + c = fixedtonumber(num) + + if mns then + c = -c + if c == mininteger and not find(num, '[^0-9]') then + c = mininteger + end + end + return c + end + + -- skip minus sign + local function f_mns() + local c = byte(json, pos) + if c then + pos = pos+1 + if c > 0x30 then + if c < 0x3A then + return f_num(true) + end + else + if c > 0x2F then + return f_zro(true) + end + end + end + decode_error('invalid number') + end + + --[[ + Strings + --]] + local f_str_hextbl = { + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, inf, inf, inf, inf, inf, inf, + inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, inf, + inf, inf, inf, inf, inf, inf, inf, inf, + inf, inf, inf, inf, inf, inf, inf, inf, + inf, inf, inf, inf, inf, inf, inf, inf, + inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, + __index = function() + return inf + end + } + setmetatable(f_str_hextbl, f_str_hextbl) + + local f_str_escapetbl = { + ['"'] = '"', + ['\\'] = '\\', + ['/'] = '/', + ['b'] = '\b', + ['f'] = '\f', + ['n'] = '\n', + ['r'] = '\r', + ['t'] = '\t', + __index = function() + decode_error("invalid escape sequence") + end + } + setmetatable(f_str_escapetbl, f_str_escapetbl) + + local function surrogate_first_error() + return decode_error("1st surrogate pair byte not continued by 2nd") + end + + local f_str_surrogate_prev = 0 + local function f_str_subst(ch, ucode) + if ch == 'u' then + local c1, c2, c3, c4, rest = byte(ucode, 1, 5) + ucode = f_str_hextbl[c1-47] * 0x1000 + + f_str_hextbl[c2-47] * 0x100 + + f_str_hextbl[c3-47] * 0x10 + + f_str_hextbl[c4-47] + if ucode ~= inf then + if ucode < 0x80 then -- 1byte + if rest then + return char(ucode, rest) + end + return char(ucode) + elseif ucode < 0x800 then -- 2bytes + c1 = floor(ucode / 0x40) + c2 = ucode - c1 * 0x40 + c1 = c1 + 0xC0 + c2 = c2 + 0x80 + if rest then + return char(c1, c2, rest) + end + return char(c1, c2) + elseif ucode < 0xD800 or 0xE000 <= ucode then -- 3bytes + c1 = floor(ucode / 0x1000) + ucode = ucode - c1 * 0x1000 + c2 = floor(ucode / 0x40) + c3 = ucode - c2 * 0x40 + c1 = c1 + 0xE0 + c2 = c2 + 0x80 + c3 = c3 + 0x80 + if rest then + return char(c1, c2, c3, rest) + end + return char(c1, c2, c3) + elseif 0xD800 <= ucode and ucode < 0xDC00 then -- surrogate pair 1st + if f_str_surrogate_prev == 0 then + f_str_surrogate_prev = ucode + if not rest then + return '' + end + surrogate_first_error() + end + f_str_surrogate_prev = 0 + surrogate_first_error() + else -- surrogate pair 2nd + if f_str_surrogate_prev ~= 0 then + ucode = 0x10000 + + (f_str_surrogate_prev - 0xD800) * 0x400 + + (ucode - 0xDC00) + f_str_surrogate_prev = 0 + c1 = floor(ucode / 0x40000) + ucode = ucode - c1 * 0x40000 + c2 = floor(ucode / 0x1000) + ucode = ucode - c2 * 0x1000 + c3 = floor(ucode / 0x40) + c4 = ucode - c3 * 0x40 + c1 = c1 + 0xF0 + c2 = c2 + 0x80 + c3 = c3 + 0x80 + c4 = c4 + 0x80 + if rest then + return char(c1, c2, c3, c4, rest) + end + return char(c1, c2, c3, c4) + end + decode_error("2nd surrogate pair byte appeared without 1st") + end + end + decode_error("invalid unicode codepoint literal") + end + if f_str_surrogate_prev ~= 0 then + f_str_surrogate_prev = 0 + surrogate_first_error() + end + return f_str_escapetbl[ch] .. ucode + end + + -- caching interpreted keys for speed + local f_str_keycache = setmetatable({}, {__mode="v"}) + + local function f_str(iskey) + local newpos = pos + local tmppos, c1, c2 + repeat + newpos = find(json, '"', newpos, true) -- search '"' + if not newpos then + decode_error("unterminated string") + end + tmppos = newpos-1 + newpos = newpos+1 + c1, c2 = byte(json, tmppos-1, tmppos) + if c2 == 0x5C and c1 == 0x5C then -- skip preceding '\\'s + repeat + tmppos = tmppos-2 + c1, c2 = byte(json, tmppos-1, tmppos) + until c2 ~= 0x5C or c1 ~= 0x5C + tmppos = newpos-2 + end + until c2 ~= 0x5C -- leave if '"' is not preceded by '\' + + local str = sub(json, pos, tmppos) + pos = newpos + + if iskey then -- check key cache + tmppos = f_str_keycache[str] -- reuse tmppos for cache key/val + if tmppos then + return tmppos + end + tmppos = str + end + + if find(str, f_str_ctrl_pat) then + decode_error("unescaped control string") + end + if find(str, '\\', 1, true) then -- check whether a backslash exists + -- We need to grab 4 characters after the escape char, + -- for encoding unicode codepoint to UTF-8. + -- As we need to ensure that every first surrogate pair byte is + -- immediately followed by second one, we grab upto 5 characters and + -- check the last for this purpose. + str = gsub(str, '\\(.)([^\\]?[^\\]?[^\\]?[^\\]?[^\\]?)', f_str_subst) + if f_str_surrogate_prev ~= 0 then + f_str_surrogate_prev = 0 + decode_error("1st surrogate pair byte not continued by 2nd") + end + end + if iskey then -- commit key cache + f_str_keycache[tmppos] = str + end + return str + end + + --[[ + Arrays, Objects + --]] + -- array + local function f_ary() + rec_depth = rec_depth + 1 + if rec_depth > 1000 then + decode_error('too deeply nested json (> 1000)') + end + local ary = {} + + pos = match(json, '^[ \n\r\t]*()', pos) + + local i = 0 + if byte(json, pos) == 0x5D then -- check closing bracket ']' which means the array empty + pos = pos+1 + else + local newpos = pos + repeat + i = i+1 + f = dispatcher[byte(json,newpos)] -- parse value + pos = newpos+1 + ary[i] = f() + newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos) -- check comma + until not newpos + + newpos = match(json, '^[ \n\r\t]*%]()', pos) -- check closing bracket + if not newpos then + decode_error("no closing bracket of an array") + end + pos = newpos + end + + if arraylen then -- commit the length of the array if `arraylen` is set + ary[0] = i + end + rec_depth = rec_depth - 1 + return ary + end + + -- objects + local function f_obj() + rec_depth = rec_depth + 1 + if rec_depth > 1000 then + decode_error('too deeply nested json (> 1000)') + end + local obj = {} + + pos = match(json, '^[ \n\r\t]*()', pos) + if byte(json, pos) == 0x7D then -- check closing bracket '}' which means the object empty + pos = pos+1 + else + local newpos = pos + + repeat + if byte(json, newpos) ~= 0x22 then -- check '"' + decode_error("not key") + end + pos = newpos+1 + local key = f_str(true) -- parse key + + -- optimized for compact json + -- c1, c2 == ':', or + -- c1, c2, c3 == ':', ' ', + f = f_err + local c1, c2, c3 = byte(json, pos, pos+3) + if c1 == 0x3A then + if c2 ~= 0x20 then + f = dispatcher[c2] + newpos = pos+2 + else + f = dispatcher[c3] + newpos = pos+3 + end + end + if f == f_err then -- read a colon and arbitrary number of spaces + newpos = match(json, '^[ \n\r\t]*:[ \n\r\t]*()', pos) + if not newpos then + decode_error("no colon after a key") + end + f = dispatcher[byte(json, newpos)] + newpos = newpos+1 + end + pos = newpos + obj[key] = f() -- parse value + newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos) + until not newpos + + newpos = match(json, '^[ \n\r\t]*}()', pos) + if not newpos then + decode_error("no closing bracket of an object") + end + pos = newpos + end + + rec_depth = rec_depth - 1 + return obj + end + + --[[ + The jump table to dispatch a parser for a value, + indexed by the code of the value's first char. + Nil key means the end of json. + --]] + dispatcher = { [0] = + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err, + f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num, + f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err, + f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err, + f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err, + f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err, + __index = function() + decode_error("unexpected termination") + end + } + setmetatable(dispatcher, dispatcher) + + --[[ + run decoder + --]] + local function decode(json_, pos_, nullv_, arraylen_) + json, pos, nullv, arraylen = json_, pos_, nullv_, arraylen_ + rec_depth = 0 + + pos = match(json, '^[ \n\r\t]*()', pos) + + f = dispatcher[byte(json, pos)] + pos = pos+1 + local v = f() + + if pos_ then + return v, pos + else + f, pos = find(json, '^[ \n\r\t]*', pos) + if pos ~= #json then + decode_error('json ended') + end + return v + end + end + + return decode +end + +return newdecoder diff --git a/lib/lua/5.1/lunajson/encoder.lua b/lib/lua/5.1/lunajson/encoder.lua new file mode 100644 index 0000000000..5dea5ad73e --- /dev/null +++ b/lib/lua/5.1/lunajson/encoder.lua @@ -0,0 +1,208 @@ +--[=====[ +The MIT License (MIT) + +Copyright (c) 2015-2017 Shunsuke Shimizu (grafi) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--]=====] +local error = error +local byte, find, format, gsub, match = string.byte, string.find, string.format, string.gsub, string.match +local concat = table.concat +local tostring = tostring +local pairs, type = pairs, type +local setmetatable = setmetatable +local huge, tiny = 1/0, -1/0 + +local f_string_esc_pat +if _VERSION == "Lua 5.1" then + -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly + f_string_esc_pat = '[^ -!#-[%]^-\255]' +else + f_string_esc_pat = '[\0-\31"\\]' +end + +local _ENV = nil + + +local function newencoder() + local v, nullv + local i, builder, visited + + local function f_tostring(v) + builder[i] = tostring(v) + i = i+1 + end + + local radixmark = match(tostring(0.5), '[^0-9]') + local delimmark = match(tostring(12345.12345), '[^0-9' .. radixmark .. ']') + if radixmark == '.' then + radixmark = nil + end + + local radixordelim + if radixmark or delimmark then + radixordelim = true + if radixmark and find(radixmark, '%W') then + radixmark = '%' .. radixmark + end + if delimmark and find(delimmark, '%W') then + delimmark = '%' .. delimmark + end + end + + local f_number = function(n) + if tiny < n and n < huge then + local s = format("%.17g", n) + if radixordelim then + if delimmark then + s = gsub(s, delimmark, '') + end + if radixmark then + s = gsub(s, radixmark, '.') + end + end + builder[i] = s + i = i+1 + return + end + error('invalid number') + end + + local doencode + + local f_string_subst = { + ['"'] = '\\"', + ['\\'] = '\\\\', + ['\b'] = '\\b', + ['\f'] = '\\f', + ['\n'] = '\\n', + ['\r'] = '\\r', + ['\t'] = '\\t', + __index = function(_, c) + return format('\\u00%02X', byte(c)) + end + } + setmetatable(f_string_subst, f_string_subst) + + local function f_string(s) + builder[i] = '"' + if find(s, f_string_esc_pat) then + s = gsub(s, f_string_esc_pat, f_string_subst) + end + builder[i+1] = s + builder[i+2] = '"' + i = i+3 + end + + local function f_table(o) + if visited[o] then + error("loop detected") + end + visited[o] = true + + local tmp = o[0] + if type(tmp) == 'number' then -- arraylen available + builder[i] = '[' + i = i+1 + for j = 1, tmp do + doencode(o[j]) + builder[i] = ',' + i = i+1 + end + if tmp > 0 then + i = i-1 + end + builder[i] = ']' + + else + tmp = o[1] + if tmp ~= nil then -- detected as array + builder[i] = '[' + i = i+1 + local j = 2 + repeat + doencode(tmp) + tmp = o[j] + if tmp == nil then + break + end + j = j+1 + builder[i] = ',' + i = i+1 + until false + builder[i] = ']' + + else -- detected as object + builder[i] = '{' + i = i+1 + local tmp = i + for k, v in pairs(o) do + if type(k) ~= 'string' then + error("non-string key") + end + f_string(k) + builder[i] = ':' + i = i+1 + doencode(v) + builder[i] = ',' + i = i+1 + end + if i > tmp then + i = i-1 + end + builder[i] = '}' + end + end + + i = i+1 + visited[o] = nil + end + + local dispatcher = { + boolean = f_tostring, + number = f_number, + string = f_string, + table = f_table, + __index = function() + error("invalid type value") + end + } + setmetatable(dispatcher, dispatcher) + + function doencode(v) + if v == nullv then + builder[i] = 'null' + i = i+1 + return + end + return dispatcher[type(v)](v) + end + + local function encode(v_, nullv_) + v, nullv = v_, nullv_ + i, builder, visited = 1, {}, {} + + doencode(v) + return concat(builder) + end + + return encode +end + +return newencoder diff --git a/src/Etterna/Singletons/FilterManager.cpp b/src/Etterna/Singletons/FilterManager.cpp index 5536801cbb..de90b7f4ed 100644 --- a/src/Etterna/Singletons/FilterManager.cpp +++ b/src/Etterna/Singletons/FilterManager.cpp @@ -167,6 +167,12 @@ class LunaFilterManager : public Luna p->ExclusiveFilter = !p->ExclusiveFilter; return 0; } + static int SetFilterMode(T* p, lua_State* L) + { + bool ExclusiveFilter = BArg(1); + p->ExclusiveFilter = ExclusiveFilter; + return 0; + } static int GetFilterMode(T* p, lua_State* L) { lua_pushboolean(L, p->ExclusiveFilter); @@ -177,6 +183,12 @@ class LunaFilterManager : public Luna p->HighestSkillsetsOnly = !p->HighestSkillsetsOnly; return 0; } + static int SetHighestSkillsetsOnly(T* p, lua_State* L) + { + bool HighestSkillsetsOnly = BArg(1); + p->HighestSkillsetsOnly = HighestSkillsetsOnly; + return 0; + } static int GetHighestSkillsetsOnly(T* p, lua_State* L) { lua_pushboolean(L, p->HighestSkillsetsOnly); @@ -187,6 +199,12 @@ class LunaFilterManager : public Luna p->HighestDifficultyOnly = !p->HighestDifficultyOnly; return 0; } + static int SetHighestDifficultyOnly(T* p, lua_State* L) + { + bool HighestDifficultyOnly = BArg(1); + p->HighestDifficultyOnly = HighestDifficultyOnly; + return 0; + } static int GetHighestDifficultyOnly(T* p, lua_State* L) { lua_pushboolean(L, p->HighestDifficultyOnly); @@ -242,10 +260,13 @@ class LunaFilterManager : public Luna ADD_METHOD(SetMinFilterRate); ADD_METHOD(GetMinFilterRate); ADD_METHOD(ToggleFilterMode); + ADD_METHOD(SetFilterMode); ADD_METHOD(GetFilterMode); ADD_METHOD(ToggleHighestSkillsetsOnly); + ADD_METHOD(SetHighestSkillsetsOnly); ADD_METHOD(GetHighestSkillsetsOnly); ADD_METHOD(ToggleHighestDifficultyOnly); + ADD_METHOD(SetHighestDifficultyOnly); ADD_METHOD(GetHighestDifficultyOnly); ADD_METHOD(HelpImTrappedInAChineseFortuneCodingFactory); ADD_METHOD(oopsimlazylol); diff --git a/src/Etterna/Singletons/ThemeManager.cpp b/src/Etterna/Singletons/ThemeManager.cpp index ee1bed1caf..0b46f38d24 100644 --- a/src/Etterna/Singletons/ThemeManager.cpp +++ b/src/Etterna/Singletons/ThemeManager.cpp @@ -627,8 +627,15 @@ ThemeManager::UpdateLuaGlobals() // explicitly refresh cached metrics that we use. ScreenDimensions::ReloadScreenDimensions(); + // Include global lua libraries + AppendToLuaPackagePath("./lib/lua/5.1/?.lua"); + // run global scripts RunLuaScripts("*.lua"); + + // Include fallback theme lua libraries + AppendToLuaPackagePath("./Themes/_fallback/lib/lua/5.1/?.lua"); + // run theme scripts RunLuaScripts("*.lua", true); #endif