Add mod stats funcs

This commit is contained in:
fed 2023-02-15 18:24:35 +01:00
parent c7f6864aea
commit 0225505661
12 changed files with 483 additions and 47 deletions

3
.gitmodules vendored
View File

@ -46,3 +46,6 @@
[submodule "deps/gsc-tool-h2"] [submodule "deps/gsc-tool-h2"]
path = deps/gsc-tool-h2 path = deps/gsc-tool-h2
url = https://github.com/fedddddd/gsc-tool-h2.git url = https://github.com/fedddddd/gsc-tool-h2.git
[submodule "deps/json"]
path = deps/json
url = https://github.com/nlohmann/json.git

View File

@ -39,24 +39,22 @@ LUI.addmenubutton("main_campaign", {
}) })
function getmodname(path) function getmodname(path)
local name = path local modinfo = mods.getinfo(path)
game:addlocalizedstring(name, name)
local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", name)
local infofile = path .. "/info.json"
if (io.fileexists(infofile)) then if (not modinfo.isvalid) then
pcall(function() game:addlocalizedstring(path, path)
local data = json.decode(io.readfile(infofile)) local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", path)
game:addlocalizedstring(data.description, data.description)
game:addlocalizedstring(data.author, data.author) return path, desc
game:addlocalizedstring(data.version, data.version) else
desc = Engine.Localize("@LUA_MENU_MOD_DESC", game:addlocalizedstring(modinfo.name, modinfo.name)
data.description, data.author, data.version) game:addlocalizedstring(modinfo.description, modinfo.description)
name = data.name game:addlocalizedstring(modinfo.author, modinfo.author)
end) 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 end
return name, desc
end end
LUI.MenuBuilder.registerType("mods_menu", function(a1) LUI.MenuBuilder.registerType("mods_menu", function(a1)
@ -69,13 +67,13 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1)
uppercase_title = true 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 if (LUI.MenuBuilder.m_types_build["mods_workshop_menu"]) then
LUI.FlowManager.RequestAddMenu(nil, "mods_workshop_menu") LUI.FlowManager.RequestAddMenu(nil, "mods_workshop_menu")
end end
end, nil, true, nil, { end, nil, true, nil, {
desc_text = Engine.Localize("@LUA_MENU_WORKSHOP_DESC") desc_text = Engine.Localize("@LUA_MENU_WORKSHOP_DESC")
}) })--]]
local modfolder = game:getloadedmod() local modfolder = game:getloadedmod()
if (modfolder ~= "") then if (modfolder ~= "") then
@ -91,21 +89,21 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1)
createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS")) createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS"))
if (io.directoryexists("mods")) then local contentpresent = false
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 local mods = mods.getlist()
game:addlocalizedstring(name, name) for i = 1, #mods do
menu:AddButton(name, function() contentpresent = true
Engine.Exec("loadmod " .. mods[i])
end, nil, true, nil, { local name, desc = getmodname(mods[i])
desc_text = desc
}) if (mods[i] ~= modfolder) then
end game:addlocalizedstring(name, name)
end menu:AddButton(name, function()
Engine.Exec("loadmod " .. mods[i])
end, nil, true, nil, {
desc_text = desc
})
end end
end end
@ -116,7 +114,10 @@ LUI.MenuBuilder.registerType("mods_menu", function(a1)
LUI.Options.InitScrollingList(menu.list, nil) LUI.Options.InitScrollingList(menu.list, nil)
menu:CreateBottomDivider() menu:CreateBottomDivider()
menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu)
if (contentpresent) then
menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu)
end
return menu return menu
end) end)

1
deps/json vendored Submodule

@ -0,0 +1 @@
Subproject commit b2306145e1789368e6f261680e8dc007e91cc986

17
deps/premake/json.lua vendored Normal file
View File

@ -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)

View File

@ -97,6 +97,23 @@ namespace filesystem
static auto current_path = std::filesystem::current_path().string(); static auto current_path = std::filesystem::current_path().string();
return current_path.data(); 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) std::string read_file(const std::string& path)
@ -230,6 +247,50 @@ namespace filesystem
return paths; 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<std::filesystem::path> 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 class component final : public component_interface
{ {
public: public:

View File

@ -14,4 +14,7 @@ namespace filesystem
std::vector<std::string> get_search_paths(); std::vector<std::string> get_search_paths();
std::vector<std::string> get_search_paths_rev(); std::vector<std::string> 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);
} }

View File

@ -16,6 +16,9 @@
#include <utils/io.hpp> #include <utils/io.hpp>
#include <utils/string.hpp> #include <utils/string.hpp>
#define MOD_FOLDER "mods"
#define MOD_STATS_FOLDER "players2/modstats"
namespace mods namespace mods
{ {
namespace namespace
@ -123,6 +126,97 @@ namespace mods
mod_info.zone_info.zones.emplace_back(values[1], alloc_flags); mod_info.zone_info.zones.emplace_back(values[1], alloc_flags);
} }
} }
std::optional<std::string> 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) bool mod_requires_restart(const std::string& path)
@ -139,6 +233,8 @@ namespace mods
filesystem::unregister_path(mod_info.path.value()); filesystem::unregister_path(mod_info.path.value());
} }
write_mod_stats();
initialize_stats();
mod_info.path = path; mod_info.path = path;
filesystem::register_path(path); filesystem::register_path(path);
parse_mod_zones(); parse_mod_zones();
@ -165,14 +261,63 @@ namespace mods
return mod_info.path; return mod_info.path;
} }
std::vector<std::string> get_mod_list()
{
if (!utils::io::directory_exists(MOD_FOLDER))
{
return {};
}
std::vector<std::string> 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<nlohmann::json> 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<std::string, std::string> 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 class component final : public component_interface
{ {
public: public:
void post_unpack() override 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); db_release_xassets_hook.create(0x140416A80, db_release_xassets_stub);

View File

@ -12,4 +12,10 @@ namespace mods
void set_mod(const std::string& path); void set_mod(const std::string& path);
std::optional<std::string> get_mod(); std::optional<std::string> get_mod();
std::vector<mod_zone> get_mod_zones(); std::vector<mod_zone> get_mod_zones();
std::vector<std::string> get_mod_list();
std::optional<nlohmann::json> get_mod_info(const std::string& mod);
nlohmann::json& get_current_stats();
void write_mod_stats();
} }

View File

@ -142,21 +142,106 @@ namespace ui_scripting
} }
} }
template <typename R>
std::function<R(const std::string& str)>
safe_io_func(const std::function<R(const std::string& str)>& 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<bool>();
}
if (json.is_number_integer())
{
return json.get<int>();
}
if (json.is_number_float())
{
return json.get<float>();
}
if (json.is_string())
{
return json.get<std::string>();
}
return {};
}
nlohmann::json lua_to_json(const script_value& value)
{
if (value.is<bool>())
{
return value.as<bool>();
}
if (value.is<int>())
{
return value.as<int>();
}
if (value.is<float>())
{
return value.as<float>();
}
if (value.is<std::string>())
{
return value.as<std::string>();
}
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() void setup_functions()
{ {
const auto lua = get_globals(); const auto lua = get_globals();
lua["io"]["fileexists"] = utils::io::file_exists; lua["io"]["fileexists"] = safe_io_func<bool>(utils::io::file_exists);
lua["io"]["writefile"] = utils::io::write_file; lua["io"]["writefile"] = filesystem::safe_write_file;
lua["io"]["movefile"] = utils::io::move_file; lua["io"]["filesize"] = safe_io_func<size_t>(utils::io::file_size);
lua["io"]["filesize"] = utils::io::file_size; lua["io"]["createdirectory"] = safe_io_func<bool>(utils::io::create_directory);
lua["io"]["createdirectory"] = utils::io::create_directory; lua["io"]["directoryexists"] = safe_io_func<bool>(utils::io::directory_exists);
lua["io"]["directoryexists"] = utils::io::directory_exists; lua["io"]["directoryisempty"] = safe_io_func<bool>(utils::io::directory_is_empty);
lua["io"]["directoryisempty"] = utils::io::directory_is_empty; lua["io"]["listfiles"] = safe_io_func<std::vector<std::string>>(utils::io::list_files);
lua["io"]["listfiles"] = utils::io::list_files; lua["io"]["removefile"] = safe_io_func<bool>(utils::io::remove_file);
lua["io"]["removefile"] = utils::io::remove_file; lua["io"]["removedirectory"] = safe_io_func<bool>(utils::io::remove_directory);
lua["io"]["removedirectory"] = utils::io::remove_directory; lua["io"]["readfile"] = safe_io_func<std::string>(
lua["io"]["readfile"] = static_cast<std::string(*)(const std::string&)>(utils::io::read_file); static_cast<std::string(*)(const std::string&)>(utils::io::read_file));
using game = table; using game = table;
auto game_type = game(); auto game_type = game();
@ -320,6 +405,85 @@ namespace ui_scripting
updater_table["getlasterror"] = updater::get_last_error; updater_table["getlasterror"] = updater::get_last_error;
updater_table["getcurrentfile"] = updater::get_current_file; 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() void start()
@ -490,6 +654,11 @@ namespace ui_scripting
return 0; return 0;
} }
int removed_function_stub(game::hks::lua_State* /*state*/)
{
return 0;
}
} }
template <typename F> template <typename F>

View File

@ -79,6 +79,13 @@ namespace ui_scripting
* Constructors * 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) script_value::script_value(const game::hks::HksObject& value)
: value_(value) : value_(value)
{ {

View File

@ -91,7 +91,7 @@ namespace ui_scripting
class script_value class script_value
{ {
public: public:
script_value() = default; script_value();
script_value(const game::hks::HksObject& value); script_value(const game::hks::HksObject& value);
script_value(int value); script_value(int value);
@ -136,6 +136,19 @@ namespace ui_scripting
{ {
} }
template <typename F>
script_value(const std::optional<F> optional)
{
if (optional.has_value())
{
script_value::script_value(optional.value());
}
else
{
script_value::script_value();
}
}
bool operator==(const script_value& other) const; bool operator==(const script_value& other) const;
arguments operator()() const; arguments operator()() const;
@ -227,6 +240,11 @@ namespace ui_scripting
return args; return args;
} }
operator script_value() const
{
return this->value_;
}
template <typename T> template <typename T>
operator T() const operator T() const
{ {

View File

@ -87,6 +87,11 @@
#include <rapidjson/prettywriter.h> #include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h> #include <rapidjson/stringbuffer.h>
#pragma warning(push)
#pragma warning(disable: 4459)
#include <json.hpp>
#pragma warning(pop)
#include <asmjit/core/jitruntime.h> #include <asmjit/core/jitruntime.h>
#include <asmjit/x86/x86assembler.h> #include <asmjit/x86/x86assembler.h>