From 0225505661f2aebf7355acb61dffb1e2e9d5deb3 Mon Sep 17 00:00:00 2001 From: fed <58637860+fedddddd@users.noreply.github.com> Date: Wed, 15 Feb 2023 18:24:35 +0100 Subject: [PATCH] Add mod stats funcs --- .gitmodules | 3 + data/cdata/ui_scripts/mods/loading.lua | 67 +++--- deps/json | 1 + deps/premake/json.lua | 17 ++ src/client/component/filesystem.cpp | 61 ++++++ src/client/component/filesystem.hpp | 3 + src/client/component/mods.cpp | 149 +++++++++++++- src/client/component/mods.hpp | 6 + src/client/component/ui_scripting.cpp | 191 +++++++++++++++++- src/client/game/ui_scripting/script_value.cpp | 7 + src/client/game/ui_scripting/script_value.hpp | 20 +- src/client/std_include.hpp | 5 + 12 files changed, 483 insertions(+), 47 deletions(-) create mode 160000 deps/json create mode 100644 deps/premake/json.lua diff --git a/.gitmodules b/.gitmodules index c93e05aa..1c83538a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,6 @@ [submodule "deps/gsc-tool-h2"] path = deps/gsc-tool-h2 url = https://github.com/fedddddd/gsc-tool-h2.git +[submodule "deps/json"] + path = deps/json + url = https://github.com/nlohmann/json.git diff --git a/data/cdata/ui_scripts/mods/loading.lua b/data/cdata/ui_scripts/mods/loading.lua index 67a89df7..bf5c0bee 100644 --- a/data/cdata/ui_scripts/mods/loading.lua +++ b/data/cdata/ui_scripts/mods/loading.lua @@ -39,24 +39,22 @@ LUI.addmenubutton("main_campaign", { }) function getmodname(path) - local name = path - game:addlocalizedstring(name, name) - local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", name) - local infofile = path .. "/info.json" + local modinfo = mods.getinfo(path) - if (io.fileexists(infofile)) then - pcall(function() - local data = json.decode(io.readfile(infofile)) - game:addlocalizedstring(data.description, data.description) - game:addlocalizedstring(data.author, data.author) - game:addlocalizedstring(data.version, data.version) - desc = Engine.Localize("@LUA_MENU_MOD_DESC", - data.description, data.author, data.version) - name = data.name - end) + if (not modinfo.isvalid) then + game:addlocalizedstring(path, path) + local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", path) + + return path, desc + else + game:addlocalizedstring(modinfo.name, modinfo.name) + game:addlocalizedstring(modinfo.description, modinfo.description) + game:addlocalizedstring(modinfo.author, modinfo.author) + game:addlocalizedstring(modinfo.version, modinfo.version) + local desc = Engine.Localize("@LUA_MENU_MOD_DESC", + modinfo.description, modinfo.author, modinfo.version) + return modinfo.name, desc end - - return name, desc end LUI.MenuBuilder.registerType("mods_menu", function(a1) @@ -69,13 +67,13 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1) uppercase_title = true }) - menu:AddButton("@LUA_MENU_WORKSHOP", function() + --[[menu:AddButton("@LUA_MENU_WORKSHOP", function() if (LUI.MenuBuilder.m_types_build["mods_workshop_menu"]) then LUI.FlowManager.RequestAddMenu(nil, "mods_workshop_menu") end end, nil, true, nil, { desc_text = Engine.Localize("@LUA_MENU_WORKSHOP_DESC") - }) + })--]] local modfolder = game:getloadedmod() if (modfolder ~= "") then @@ -91,21 +89,21 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1) 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]) + local contentpresent = false - 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 + local mods = mods.getlist() + for i = 1, #mods do + contentpresent = true + + 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 @@ -116,7 +114,10 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1) LUI.Options.InitScrollingList(menu.list, nil) menu:CreateBottomDivider() - menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + + if (contentpresent) then + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + end return menu end) diff --git a/deps/json b/deps/json new file mode 160000 index 00000000..b2306145 --- /dev/null +++ b/deps/json @@ -0,0 +1 @@ +Subproject commit b2306145e1789368e6f261680e8dc007e91cc986 diff --git a/deps/premake/json.lua b/deps/premake/json.lua new file mode 100644 index 00000000..c060e3a0 --- /dev/null +++ b/deps/premake/json.lua @@ -0,0 +1,17 @@ +json = { + source = path.join(dependencies.basePath, "json") +} + +function json.import() + json.includes() +end + +function json.includes() + includedirs {path.join(json.source, "single_include/*")} +end + +function json.project() + +end + +table.insert(dependencies, json) diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 5aad4bf7..f640120f 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -97,6 +97,23 @@ namespace filesystem static auto current_path = std::filesystem::current_path().string(); return current_path.data(); } + + bool is_parent_path(const std::filesystem::path& parent, const std::filesystem::path& child) + { + std::filesystem::path iter = child; + + while (iter != iter.parent_path()) + { + if (iter == parent) + { + return true; + } + + iter = iter.parent_path(); + } + + return false; + } } std::string read_file(const std::string& path) @@ -230,6 +247,50 @@ namespace filesystem return paths; } + void check_path(const std::filesystem::path& path) + { + if (path.generic_string().find("..") != std::string::npos) + { + throw std::runtime_error("directory traversal is not allowed"); + } + } + + std::string get_safe_path(const std::filesystem::path& path) + { + check_path(path); + const auto absolute = std::filesystem::weakly_canonical(path); + + static std::vector allowed_directories = + { + {std::filesystem::weakly_canonical("mods")}, + {std::filesystem::weakly_canonical("h2-mod")}, + {std::filesystem::weakly_canonical("players2/default")}, + }; + + auto is_allowed = false; + for (const auto& dir : allowed_directories) + { + if (is_parent_path(dir, absolute)) + { + is_allowed = true; + break; + } + } + + if (!is_allowed) + { + throw std::runtime_error(std::format("Disallowed access to directory \"{}\"", path.generic_string())); + } + + return path.generic_string(); + } + + bool safe_write_file(const std::string& file, const std::string& data, bool append) + { + const auto path = filesystem::get_safe_path(file); + return utils::io::write_file(path, data, append); + } + class component final : public component_interface { public: diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index 79d3fca8..978258f7 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -14,4 +14,7 @@ namespace filesystem std::vector get_search_paths(); std::vector get_search_paths_rev(); + + std::string get_safe_path(const std::filesystem::path& path); + bool safe_write_file(const std::string& file, const std::string& data, bool append = false); } diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index bf895ade..7056e6be 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -16,6 +16,9 @@ #include #include +#define MOD_FOLDER "mods" +#define MOD_STATS_FOLDER "players2/modstats" + namespace mods { namespace @@ -123,6 +126,97 @@ namespace mods mod_info.zone_info.zones.emplace_back(values[1], alloc_flags); } } + + std::optional get_mod_basename() + { + const auto mod = get_mod(); + if (!mod.has_value()) + { + return {}; + } + + const auto& value = mod.value(); + const auto last_index = value.find_last_of('/'); + const auto basename = value.substr(last_index + 1); + return {basename}; + } + + nlohmann::json default_mod_stats() + { + nlohmann::json json; + json["maps"] = {}; + return json; + } + + nlohmann::json verify_mod_stats(nlohmann::json& json) + { + if (!json.is_object()) + { + json = {}; + } + + if (!json.contains("maps") || !json["maps"].is_object()) + { + json["maps"] = {}; + } + + return json; + } + + nlohmann::json parse_mod_stats() + { + const auto name = get_mod_basename(); + if (!name.has_value()) + { + return default_mod_stats(); + } + + const auto& name_value = name.value(); + const auto stat_file = MOD_STATS_FOLDER "/" + name_value + ".json"; + if (!utils::io::file_exists(stat_file)) + { + return default_mod_stats(); + } + + const auto data = utils::io::read_file(stat_file); + try + { + auto json = nlohmann::json::parse(data); + return verify_mod_stats(json); + } + catch (const std::exception& e) + { + console::error("Failed to parse json mod stat file \"%s.json\": %s", + name_value.data(), e.what()); + } + + return default_mod_stats(); + } + + void initialize_stats() + { + get_current_stats() = parse_mod_stats(); + } + } + + nlohmann::json& get_current_stats() + { + static nlohmann::json stats; + stats = verify_mod_stats(stats); + return stats; + } + + void write_mod_stats() + { + const auto name = get_mod_basename(); + if (!name.has_value()) + { + return; + } + + const auto& name_value = name.value(); + const auto stat_file = MOD_STATS_FOLDER "/" + name_value + ".json"; + utils::io::write_file(stat_file, get_current_stats().dump(), false); } bool mod_requires_restart(const std::string& path) @@ -139,6 +233,8 @@ namespace mods filesystem::unregister_path(mod_info.path.value()); } + write_mod_stats(); + initialize_stats(); mod_info.path = path; filesystem::register_path(path); parse_mod_zones(); @@ -165,14 +261,63 @@ namespace mods return mod_info.path; } + std::vector get_mod_list() + { + if (!utils::io::directory_exists(MOD_FOLDER)) + { + return {}; + } + + std::vector mod_list; + + const auto files = utils::io::list_files(MOD_FOLDER); + for (const auto& file : files) + { + if (!utils::io::directory_exists(file) || utils::io::directory_is_empty(file)) + { + continue; + } + + mod_list.push_back(file); + } + + return mod_list; + } + + std::optional get_mod_info(const std::string& name) + { + const auto info_file = name + "/info.json"; + if (!utils::io::directory_exists(name) || !utils::io::file_exists(info_file)) + { + return {}; + } + + std::unordered_map info; + const auto data = utils::io::read_file(info_file); + try + { + return {nlohmann::json::parse(data)}; + } + catch (const std::exception&) + { + } + + return {}; + } + class component final : public component_interface { public: void post_unpack() override { - if (!utils::io::directory_exists("mods")) + if (!utils::io::directory_exists(MOD_FOLDER)) { - utils::io::create_directory("mods"); + utils::io::create_directory(MOD_FOLDER); + } + + if (!utils::io::directory_exists(MOD_STATS_FOLDER)) + { + utils::io::create_directory(MOD_STATS_FOLDER); } db_release_xassets_hook.create(0x140416A80, db_release_xassets_stub); diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp index 7f3f69f0..bc2a523d 100644 --- a/src/client/component/mods.hpp +++ b/src/client/component/mods.hpp @@ -12,4 +12,10 @@ namespace mods void set_mod(const std::string& path); std::optional get_mod(); std::vector get_mod_zones(); + + std::vector get_mod_list(); + std::optional get_mod_info(const std::string& mod); + + nlohmann::json& get_current_stats(); + void write_mod_stats(); } \ No newline at end of file diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index e8d2320e..2f2fa360 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -142,21 +142,106 @@ namespace ui_scripting } } + template + std::function + safe_io_func(const std::function& func) + { + return [func](const std::string& path) + { + const auto safe_path = filesystem::get_safe_path(path); + return func(safe_path); + }; + } + + script_value json_to_lua(const nlohmann::json& json) + { + if (json.is_object()) + { + table object; + for (const auto& [key, value] : json.items()) + { + object[key] = json_to_lua(value); + } + } + + if (json.is_array()) + { + table array; + auto index = 1; + for (const auto& value : json.array()) + { + array[index++] = json_to_lua(value); + } + } + + if (json.is_boolean()) + { + return json.get(); + } + + if (json.is_number_integer()) + { + return json.get(); + } + + if (json.is_number_float()) + { + return json.get(); + } + + if (json.is_string()) + { + return json.get(); + } + + return {}; + } + + nlohmann::json lua_to_json(const script_value& value) + { + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.get_raw().t == game::hks::TNIL) + { + return {}; + } + + throw std::runtime_error("lua value must be of primitive type (boolean, integer, float, string)"); + } + void setup_functions() { const auto lua = get_globals(); - lua["io"]["fileexists"] = utils::io::file_exists; - lua["io"]["writefile"] = utils::io::write_file; - lua["io"]["movefile"] = utils::io::move_file; - lua["io"]["filesize"] = utils::io::file_size; - lua["io"]["createdirectory"] = utils::io::create_directory; - lua["io"]["directoryexists"] = utils::io::directory_exists; - lua["io"]["directoryisempty"] = utils::io::directory_is_empty; - lua["io"]["listfiles"] = utils::io::list_files; - lua["io"]["removefile"] = utils::io::remove_file; - lua["io"]["removedirectory"] = utils::io::remove_directory; - lua["io"]["readfile"] = static_cast(utils::io::read_file); + lua["io"]["fileexists"] = safe_io_func(utils::io::file_exists); + lua["io"]["writefile"] = filesystem::safe_write_file; + lua["io"]["filesize"] = safe_io_func(utils::io::file_size); + lua["io"]["createdirectory"] = safe_io_func(utils::io::create_directory); + lua["io"]["directoryexists"] = safe_io_func(utils::io::directory_exists); + lua["io"]["directoryisempty"] = safe_io_func(utils::io::directory_is_empty); + lua["io"]["listfiles"] = safe_io_func>(utils::io::list_files); + lua["io"]["removefile"] = safe_io_func(utils::io::remove_file); + lua["io"]["removedirectory"] = safe_io_func(utils::io::remove_directory); + lua["io"]["readfile"] = safe_io_func( + static_cast(utils::io::read_file)); using game = table; auto game_type = game(); @@ -320,6 +405,85 @@ namespace ui_scripting updater_table["getlasterror"] = updater::get_last_error; updater_table["getcurrentfile"] = updater::get_current_file; + + auto mods_table = table(); + lua["mods"] = mods_table; + + mods_table["getloaded"] = []() -> script_value + { + const auto& mod = mods::get_mod(); + if (mod.has_value()) + { + return mod.value(); + } + + return {}; + }; + + mods_table["getlist"] = mods::get_mod_list; + mods_table["getinfo"] = [](const std::string& mod) + { + table info_table{}; + const auto info = mods::get_mod_info(mod); + const auto has_value = info.has_value(); + info_table["isvalid"] = has_value; + + if (!has_value) + { + return info_table; + } + + const auto& map = info.value(); + for (const auto& [key, value] : map.items()) + { + info_table[key] = json_to_lua(value); + } + + return info_table; + }; + + auto mods_stats_table = table(); + mods_table["stats"] = mods_stats_table; + + mods_stats_table["set"] = [](const std::string& key, const script_value& value) + { + const auto json_value = lua_to_json(value); + mods::get_current_stats()[key] = json_value; + mods::write_mod_stats(); + }; + + mods_stats_table["get"] = [](const std::string& key) + { + return json_to_lua(mods::get_current_stats()); + }; + + mods_stats_table["mapset"] = [](const std::string& mapname, + const std::string& key, const script_value& value) + { + const auto json_value = lua_to_json(value); + auto& stats = mods::get_current_stats(); + stats["maps"][mapname][key] = json_value; + mods::write_mod_stats(); + }; + + mods_stats_table["mapget"] = [](const std::string& mapname, + const std::string& key) + { + auto& stats = mods::get_current_stats(); + return json_to_lua(stats["maps"][mapname][key]); + }; + + mods_stats_table["save"] = mods::write_mod_stats; + mods_stats_table["getall"] = []() + { + return json_to_lua(mods::get_current_stats()); + }; + + mods_stats_table["setfromjson"] = [](const std::string& data) + { + const auto json = nlohmann::json::parse(data); + mods::get_current_stats() = json; + }; } void start() @@ -490,6 +654,11 @@ namespace ui_scripting return 0; } + + int removed_function_stub(game::hks::lua_State* /*state*/) + { + return 0; + } } template diff --git a/src/client/game/ui_scripting/script_value.cpp b/src/client/game/ui_scripting/script_value.cpp index a6285cef..bfabc0f9 100644 --- a/src/client/game/ui_scripting/script_value.cpp +++ b/src/client/game/ui_scripting/script_value.cpp @@ -79,6 +79,13 @@ namespace ui_scripting * Constructors **************************************************************/ + script_value::script_value() + { + game::hks::HksObject nil{}; + nil.t = game::hks::TNIL; + this->value_ = nil; + } + script_value::script_value(const game::hks::HksObject& value) : value_(value) { diff --git a/src/client/game/ui_scripting/script_value.hpp b/src/client/game/ui_scripting/script_value.hpp index 03d0211e..d669ccfd 100644 --- a/src/client/game/ui_scripting/script_value.hpp +++ b/src/client/game/ui_scripting/script_value.hpp @@ -91,7 +91,7 @@ namespace ui_scripting class script_value { public: - script_value() = default; + script_value(); script_value(const game::hks::HksObject& value); script_value(int value); @@ -136,6 +136,19 @@ namespace ui_scripting { } + template + script_value(const std::optional optional) + { + if (optional.has_value()) + { + script_value::script_value(optional.value()); + } + else + { + script_value::script_value(); + } + } + bool operator==(const script_value& other) const; arguments operator()() const; @@ -227,6 +240,11 @@ namespace ui_scripting return args; } + operator script_value() const + { + return this->value_; + } + template operator T() const { diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index e76e0d8d..6fe72a49 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -87,6 +87,11 @@ #include #include +#pragma warning(push) +#pragma warning(disable: 4459) +#include +#pragma warning(pop) + #include #include