diff --git a/data/ui_scripts/mods/__init__.lua b/data/ui_scripts/mods/__init__.lua new file mode 100644 index 00000000..1ca1f8d2 --- /dev/null +++ b/data/ui_scripts/mods/__init__.lua @@ -0,0 +1,3 @@ +if (game:issingleplayer()) then + require("loading") +end diff --git a/data/ui_scripts/mods/loading.lua b/data/ui_scripts/mods/loading.lua new file mode 100644 index 00000000..16cc3084 --- /dev/null +++ b/data/ui_scripts/mods/loading.lua @@ -0,0 +1,116 @@ +game:addlocalizedstring("MENU_MODS", "MODS") +game:addlocalizedstring("MENU_MODS_DESC", "Load installed mods.") +game:addlocalizedstring("LUA_MENU_MOD_DESC_DEFAULT", "Load &&1.") +game:addlocalizedstring("LUA_MENU_MOD_DESC", "&&1\nAuthor: &&2\nVersion: &&3") +game:addlocalizedstring("LUA_MENU_OPEN_STORE", "Open store") +game:addlocalizedstring("LUA_MENU_OPEN_STORE_DESC", "Download and install mods.") +game:addlocalizedstring("LUA_MENU_LOADED_MOD", "Loaded mod: ^2&&1") +game:addlocalizedstring("LUA_MENU_AVAILABLE_MODS", "Available mods") +game:addlocalizedstring("LUA_MENU_UNLOAD", "Unload") +game:addlocalizedstring("LUA_MENU_UNLOAD_DESC", "Unload the currently loaded mod.") + +function createdivider(menu, text) + local element = LUI.UIElement.new( { + leftAnchor = true, + rightAnchor = true, + left = 0, + right = 0, + topAnchor = true, + bottomAnchor = false, + top = 0, + bottom = 33.33 + }) + + element.scrollingToNext = true + element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { + title_bar_text = Engine.ToUpperCase(text) + })) + + menu.list:addElement(element) +end + +function string:truncate(length) + if (#self <= length) then + return self + end + + return self:sub(1, length - 3) .. "..." +end + +LUI.addmenubutton("main_campaign", { + index = 6, + text = "@MENU_MODS", + description = Engine.Localize("@MENU_MODS_DESC"), + callback = function() + LUI.FlowManager.RequestAddMenu(nil, "mods_menu") + end +}) + +function getmodname(path) + local name = path + local desc = Engine.Localize("@LUA_MENU_MOD_DESC_DEFAULT", name) + local infofile = path .. "/info.json" + + if (io.fileexists(infofile)) then + pcall(function() + local data = json.decode(io.readfile(infofile)) + desc = Engine.Localize("@LUA_MENU_MOD_DESC", + data.description, data.author, data.version) + name = data.name + end) + end + + return name, desc +end + +LUI.MenuBuilder.m_types_build["mods_menu"] = function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = "@MENU_MODS", + exclusiveController = 0, + menu_width = 400, + menu_top_indent = LUI.MenuTemplate.spMenuOffset, + showTopRightSmallBar = true + }) + + local modfolder = game:getloadedmod() + if (modfolder ~= "") then + createdivider(menu, Engine.Localize("@LUA_MENU_LOADED_MOD", getmodname(modfolder):truncate(24))) + + menu:AddButton("@LUA_MENU_UNLOAD", function() + Engine.Exec("unloadmod") + end, nil, true, nil, { + desc_text = Engine.Localize("@LUA_MENU_UNLOAD_DESC") + }) + end + + createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS")) + + if (io.directoryexists("mods")) then + local mods = io.listfiles("mods/") + for i = 1, #mods do + if (io.directoryexists(mods[i]) and not io.directoryisempty(mods[i])) then + local name, desc = getmodname(mods[i]) + + if (mods[i] ~= modfolder) then + game:addlocalizedstring(name, name) + menu:AddButton(name, function() + Engine.Exec("loadmod " .. mods[i]) + end, nil, true, nil, { + desc_text = desc + }) + end + end + end + end + + menu:AddBackButton(function(a1) + Engine.PlaySound(CoD.SFX.MenuBack) + LUI.FlowManager.RequestLeaveMenu(a1) + end) + + LUI.Options.InitScrollingList(menu.list, nil) + menu:CreateBottomDivider() + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + + return menu +end diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index da11d2e4..d1827936 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace filesystem { @@ -70,6 +71,40 @@ namespace filesystem return this->name_; } + std::unordered_set& get_search_paths() + { + static std::unordered_set search_paths{}; + return search_paths; + } + + std::string read_file(const std::string& path) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::file_exists(path_)) + { + return utils::io::read_file(path_); + } + } + + return {}; + } + + bool read_file(const std::string& path, std::string* data) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::read_file(path_, data)) + { + return true; + } + } + + return false; + } + class component final : public component_interface { public: @@ -87,6 +122,10 @@ namespace filesystem utils::hook::call(SELECT_VALUE(0x1403B8D31, 0x1404EE3D0), register_custom_path_stub); utils::hook::call(SELECT_VALUE(0x1403B8D51, 0x1404EE3F0), register_custom_path_stub); utils::hook::call(SELECT_VALUE(0x1403B8D90, 0x1404EE42F), register_custom_path_stub); + + get_search_paths().insert("."); + get_search_paths().insert("h1-mod"); + get_search_paths().insert("data"); } }; } diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index 6cec8c87..c3d36dc6 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -16,4 +16,8 @@ namespace filesystem std::string name_; std::string buffer_; }; + + std::unordered_set& get_search_paths(); + std::string read_file(const std::string& path); + bool read_file(const std::string& path, std::string* data); } \ No newline at end of file diff --git a/src/client/component/fonts.cpp b/src/client/component/fonts.cpp index ae8f37fe..32f55399 100644 --- a/src/client/component/fonts.cpp +++ b/src/client/component/fonts.cpp @@ -3,6 +3,7 @@ #include "fonts.hpp" #include "console.hpp" +#include "filesystem.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -36,6 +37,13 @@ namespace fonts return font; } + void free_font(game::TTF* font) + { + utils::memory::get_allocator()->free(font->buffer); + utils::memory::get_allocator()->free(font->name); + utils::memory::get_allocator()->free(font); + } + game::TTF* load_font(const std::string& name) { return font_data.access([&](font_data_t& data_) -> game::TTF* @@ -51,9 +59,7 @@ namespace fonts data = i->second; } - if (data.empty() - && !utils::io::read_file(utils::string::va("h1-mod/%s", name.data()), &data) - && !utils::io::read_file(utils::string::va("data/%s", name.data()), &data)) + if (data.empty() && !filesystem::read_file(name, &data)) { return nullptr; } @@ -98,6 +104,20 @@ namespace fonts }); } + void clear() + { + font_data.access([&](font_data_t& data_) + { + for (auto& font : data_.fonts) + { + free_font(font.second); + } + + data_.fonts.clear(); + utils::hook::set(SELECT_VALUE(0x14F09DBB8, 0x14FD61EE8), 0); // reset registered font count + }); + } + class component final : public component_interface { public: diff --git a/src/client/component/fonts.hpp b/src/client/component/fonts.hpp index 15749bcf..c2d22869 100644 --- a/src/client/component/fonts.hpp +++ b/src/client/component/fonts.hpp @@ -3,4 +3,5 @@ namespace fonts { void add(const std::string& name, const std::string& data); + void clear(); } diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp index 23b63212..26d492ce 100644 --- a/src/client/component/game_console.cpp +++ b/src/client/component/game_console.cpp @@ -571,7 +571,7 @@ namespace game_console { if (key == game::keyNum_t::K_F10) { - if (!game::Com_InFrontEnd()) + if (!game::Com_InFrontend()) { return false; } diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index c1b39b9d..7801bcbd 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -15,13 +15,21 @@ namespace input utils::hook::detour cl_char_event_hook; utils::hook::detour cl_key_event_hook; + bool lui_running() + { + return *game::hks::lua_state != nullptr; + } + void cl_char_event_stub(const int local_client_num, const int key) { - ui_scripting::notify("keypress", + if (lui_running()) { - {"keynum", key}, - {"key", game::Key_KeynumToString(key, 0, 1)}, - }); + ui_scripting::notify("keypress", + { + {"keynum", key}, + {"key", game::Key_KeynumToString(key, 0, 1)}, + }); + } if (!game_console::console_char_event(local_client_num, key)) { @@ -33,11 +41,14 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { - ui_scripting::notify(down ? "keydown" : "keyup", + if (lui_running()) { - {"keynum", key}, - {"key", game::Key_KeynumToString(key, 0, 1)}, - }); + ui_scripting::notify(down ? "keydown" : "keyup", + { + {"keynum", key}, + {"key", game::Key_KeynumToString(key, 0, 1)}, + }); + } if (!game_console::console_key_event(local_client_num, key, down)) { diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp index 99b22a45..4fbb3ed8 100644 --- a/src/client/component/materials.cpp +++ b/src/client/component/materials.cpp @@ -3,6 +3,7 @@ #include "materials.hpp" #include "console.hpp" +#include "filesystem.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -20,6 +21,7 @@ namespace materials { utils::hook::detour db_material_streaming_fail_hook; utils::hook::detour material_register_handle_hook; + utils::hook::detour db_get_material_index_hook; struct material_data_t { @@ -68,6 +70,16 @@ namespace materials return material; } + void free_material(game::Material* material) + { + material->textureTable->u.image->textures.___u0.map->Release(); + material->textureTable->u.image->textures.shaderView->Release(); + utils::memory::get_allocator()->free(material->textureTable->u.image); + utils::memory::get_allocator()->free(material->textureTable); + utils::memory::get_allocator()->free(material->name); + utils::memory::get_allocator()->free(material); + } + game::Material* load_material(const std::string& name) { return material_data.access([&](material_data_t& data_) -> game::Material* @@ -83,9 +95,7 @@ namespace materials data = i->second; } - if (data.empty() - && !utils::io::read_file(utils::string::va("h1-mod/materials/%s.png", name.data()), &data) - && !utils::io::read_file(utils::string::va("data/materials/%s.png", name.data()), &data)) + if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) { data_.materials[name] = nullptr; return nullptr; @@ -136,6 +146,16 @@ namespace materials return db_material_streaming_fail_hook.invoke(material); } + + unsigned int db_get_material_index_stub(game::Material* material) + { + if (material->constantTable == &constant_table) + { + return 0; + } + + return db_get_material_index_hook.invoke(material); + } } void add(const std::string& name, const std::string& data) @@ -146,6 +166,24 @@ namespace materials }); } + void clear() + { + material_data.access([&](material_data_t& data_) + { + for (auto& material : data_.materials) + { + if (material.second == nullptr) + { + continue; + } + + free_material(material.second); + } + + data_.materials.clear(); + }); + } + class component final : public component_interface { public: @@ -158,6 +196,7 @@ namespace materials material_register_handle_hook.create(game::Material_RegisterHandle, material_register_handle_stub); db_material_streaming_fail_hook.create(SELECT_VALUE(0x1401D3180, 0x1402C6260), db_material_streaming_fail_stub); + db_get_material_index_hook.create(SELECT_VALUE(0x1401CAD00, 0x1402BBB20), db_get_material_index_stub); } }; } diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp index ac58b511..3a548548 100644 --- a/src/client/component/materials.hpp +++ b/src/client/component/materials.hpp @@ -3,4 +3,5 @@ namespace materials { void add(const std::string& name, const std::string& data); + void clear(); } diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp new file mode 100644 index 00000000..ea6a9027 --- /dev/null +++ b/src/client/component/mods.cpp @@ -0,0 +1,119 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "filesystem.hpp" +#include "materials.hpp" +#include "fonts.hpp" +#include "mods.hpp" + +#include +#include + +namespace mods +{ + std::string mod_path{}; + + namespace + { + utils::hook::detour db_release_xassets_hook; + bool release_assets = false; + + void db_release_xassets_stub() + { + if (release_assets) + { + materials::clear(); + fonts::clear(); + } + + db_release_xassets_hook.invoke(); + } + + void restart() + { + scheduler::once([]() + { + release_assets = true; + game::Com_Shutdown(""); + release_assets = false; + }, scheduler::pipeline::main); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_sp()) + { + return; + } + + if (!utils::io::directory_exists("mods")) + { + utils::io::create_directory("mods"); + } + + db_release_xassets_hook.create(SELECT_VALUE(0x1401CD560, 0x1402BF160), db_release_xassets_stub); + + command::add("loadmod", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("Usage: loadmod mods/"); + return; + } + + if (!game::Com_InFrontend()) + { + console::info("Cannot load mod while in-game!\n"); + game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); + return; + } + + const auto path = params.get(1); + if (!utils::io::directory_exists(path)) + { + console::info("Mod %s not found!\n", path); + return; + } + + console::info("Loading mod %s\n", path); + filesystem::get_search_paths().erase(mod_path); + filesystem::get_search_paths().insert(path); + mod_path = path; + restart(); + }); + + command::add("unloadmod", [](const command::params& params) + { + if (mod_path.empty()) + { + console::info("No mod loaded\n"); + return; + } + + if (!game::Com_InFrontend()) + { + console::info("Cannot unload mod while in-game!\n"); + game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); + return; + } + + console::info("Unloading mod %s\n", mod_path.data()); + filesystem::get_search_paths().erase(mod_path); + mod_path.clear(); + restart(); + }); + } + }; +} + +REGISTER_COMPONENT(mods::component) diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp new file mode 100644 index 00000000..364a2f11 --- /dev/null +++ b/src/client/component/mods.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace mods +{ + extern std::string mod_path; +} \ No newline at end of file diff --git a/src/client/game/scripting/lua/engine.cpp b/src/client/game/scripting/lua/engine.cpp index 390a5995..936c8e1f 100644 --- a/src/client/game/scripting/lua/engine.cpp +++ b/src/client/game/scripting/lua/engine.cpp @@ -4,6 +4,7 @@ #include "../execution.hpp" #include "../../../component/logfile.hpp" +#include "../../../component/filesystem.hpp" #include @@ -49,18 +50,17 @@ namespace scripting::lua::engine { stop(); - load_scripts("h1-mod/scripts/"); - load_scripts("data/scripts/"); - - if (game::environment::is_sp()) + for (const auto& path : filesystem::get_search_paths()) { - load_scripts("h1-mod/scripts/sp/"); - load_scripts("data/scripts/sp/"); - } - else - { - load_scripts("h1-mod/scripts/mp/"); - load_scripts("data/scripts/mp/"); + load_scripts(path + "/scripts/"); + if (game::environment::is_sp()) + { + load_scripts(path + "/scripts/sp/"); + } + else + { + load_scripts(path + "/scripts/mp/"); + } } running = true; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 6653b555..58058533 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -33,10 +33,12 @@ namespace game WEAK symbol Com_Frame_Try_Block_Function{0x1401CE8D0, 0x1400D8310}; WEAK symbol Com_GetCurrentCoDPlayMode{0, 0x1405039A0}; - WEAK symbol Com_InFrontEnd{0x1400E4B30, 0x140176A30}; + WEAK symbol Com_InFrontend{0x1400E4B30, 0x140176A30}; WEAK symbol Com_SetSlowMotion{0, 0x1400DB790}; WEAK symbol Com_Error{0x1403509C0, 0x1400D78A0}; WEAK symbol Com_Quit_f{0x140352BE0, 0x1400DA830}; + WEAK symbol Com_Shutdown{0x140353B70, 0x1400DB8A0}; + WEAK symbol Quit{0x140352D90, 0x1400DA830}; WEAK symbol CG_GameMessage{0x1401389A0, 0x140220CC0}; @@ -47,6 +49,7 @@ namespace game bool isAlternate, char* outputBuffer, int bufferLen)> CG_GetWeaponDisplayName{0x14016EC30, 0x1400B5840}; WEAK symbol CL_IsCgameInitialized{0x14017EE30, 0x140245650}; + WEAK symbol CL_VirtualLobbyShutdown{0, 0x140256D40}; WEAK symbol Dvar_SetCommand{0x1403C72B0, 0x1404FD0A0}; WEAK symbol Dvar_FindVar{0x1403C5D50, 0x1404FBB00}; diff --git a/src/client/game/ui_scripting/lua/context.cpp b/src/client/game/ui_scripting/lua/context.cpp index 1db94910..d9a930f9 100644 --- a/src/client/game/ui_scripting/lua/context.cpp +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -13,6 +13,7 @@ #include "../../../component/localized_strings.hpp" #include "../../../component/fastfiles.hpp" #include "../../../component/scripting.hpp" +#include "../../../component/mods.hpp" #include "component/game_console.hpp" #include "component/scheduler.hpp" @@ -25,6 +26,31 @@ namespace ui_scripting::lua { namespace { + const auto json_script = utils::nt::load_resource(LUA_JSON); + + void setup_json(sol::state& state) + { + const auto json = state.safe_script(json_script, &sol::script_pass_on_error); + handle_error(json); + state["json"] = json; + } + + void setup_io(sol::state& state) + { + state["io"]["fileexists"] = utils::io::file_exists; + state["io"]["writefile"] = utils::io::write_file; + state["io"]["movefile"] = utils::io::move_file; + state["io"]["filesize"] = utils::io::file_size; + state["io"]["createdirectory"] = utils::io::create_directory; + state["io"]["directoryexists"] = utils::io::directory_exists; + state["io"]["directoryisempty"] = utils::io::directory_is_empty; + state["io"]["listfiles"] = utils::io::list_files; + state["io"]["copyfolder"] = utils::io::copy_folder; + state["io"]["removefile"] = utils::io::remove_file; + state["io"]["removedirectory"] = utils::io::remove_directory; + state["io"]["readfile"] = static_cast(utils::io::read_file); + } + void setup_types(sol::state& state, scheduler& scheduler) { struct game @@ -138,6 +164,11 @@ namespace ui_scripting::lua return std::string(buffer); }; + game_type["getloadedmod"] = [](const game&) + { + return mods::mod_path; + }; + auto userdata_type = state.new_usertype("userdata_"); userdata_type["new"] = sol::property( @@ -322,6 +353,8 @@ namespace ui_scripting::lua sol::lib::math, sol::lib::table); + setup_json(this->state_); + setup_io(this->state_); setup_types(this->state_, this->scheduler_); if (type == script_type::file) diff --git a/src/client/game/ui_scripting/lua/engine.cpp b/src/client/game/ui_scripting/lua/engine.cpp index cc3d3d33..a52b1b9e 100644 --- a/src/client/game/ui_scripting/lua/engine.cpp +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -3,6 +3,7 @@ #include "context.hpp" #include "../../../component/ui_scripting.hpp" +#include "../../../component/filesystem.hpp" #include #include @@ -52,18 +53,17 @@ namespace ui_scripting::lua::engine load_code(lui_common); load_code(lui_updater); - load_scripts("h1-mod/ui_scripts/"); - load_scripts("data/ui_scripts/"); - - if (game::environment::is_sp()) + for (const auto& path : filesystem::get_search_paths()) { - load_scripts("h1-mod/ui_scripts/sp/"); - load_scripts("data/ui_scripts/sp/"); - } - else - { - load_scripts("h1-mod/ui_scripts/mp/"); - load_scripts("data/ui_scripts/mp/"); + load_scripts(path + "/ui_scripts/"); + if (game::environment::is_sp()) + { + load_scripts(path + "/ui_scripts/sp/"); + } + else + { + load_scripts(path + "/ui_scripts/mp/"); + } } } diff --git a/src/client/resource.hpp b/src/client/resource.hpp index adc92d9c..ceb43a33 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -18,3 +18,5 @@ #define LUI_COMMON 309 #define LUI_UPDATER 310 + +#define LUA_JSON 311 diff --git a/src/client/resource.rc b/src/client/resource.rc index c53c106a..2996abfa 100644 --- a/src/client/resource.rc +++ b/src/client/resource.rc @@ -121,6 +121,8 @@ ICON_IMAGE RCDATA "resources/icon.png" LUI_COMMON RCDATA "resources/ui_scripts/common.lua" LUI_UPDATER RCDATA "resources/ui_scripts/updater.lua" +LUA_JSON RCDATA "resources/json.lua" + #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/src/client/resources/json.lua b/src/client/resources/json.lua new file mode 100644 index 00000000..711ef786 --- /dev/null +++ b/src/client/resources/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- 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 json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +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 json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +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 + error( 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 + error( 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 + 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 decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + 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 + + 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 + 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 + 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 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 + 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 + 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 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 + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/src/common/utils/io.cpp b/src/common/utils/io.cpp index 4968f449..9b161d39 100644 --- a/src/common/utils/io.cpp +++ b/src/common/utils/io.cpp @@ -104,6 +104,11 @@ namespace utils::io return std::filesystem::is_empty(directory); } + bool remove_directory(const std::string& directory) + { + return std::filesystem::remove_all(directory); + } + std::vector list_files(const std::string& directory) { std::vector files; diff --git a/src/common/utils/io.hpp b/src/common/utils/io.hpp index ab4ebaa4..38344987 100644 --- a/src/common/utils/io.hpp +++ b/src/common/utils/io.hpp @@ -16,6 +16,7 @@ namespace utils::io bool create_directory(const std::string& directory); bool directory_exists(const std::string& directory); bool directory_is_empty(const std::string& directory); + bool remove_directory(const std::string& directory); std::vector list_files(const std::string& directory); void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); }