diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index cf17542a..5698bbb7 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -6,10 +6,12 @@ #include "command.hpp" #include "console.hpp" #include "localized_strings.hpp" +#include "mods.hpp" #include #include #include +#include namespace fastfiles { @@ -113,7 +115,7 @@ namespace fastfiles a.jmp(0x140415E29); } - bool try_load_zone(std::string name, bool localized, bool game = false) + bool try_load_zone(const std::string& name, bool localized, bool game = false) { if (localized) { @@ -134,6 +136,19 @@ namespace fastfiles return true; } + void load_mod_zones() + { + try_load_zone("mod", true); + const auto mod_zones = mods::get_mod_zones(); + for (const auto& zone : mod_zones) + { + if (zone.alloc_flags & game::DB_ZONE_COMMON) + { + try_load_zone(zone.name, true); + } + } + } + void load_post_gfx_and_ui_and_common_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) { @@ -146,7 +161,7 @@ namespace fastfiles game::DB_LoadXAssets(zoneInfo, zoneCount, syncMode); - try_load_zone("mod", true); + load_mod_zones(); } constexpr unsigned int get_asset_type_size(const game::XAssetType type) @@ -419,6 +434,37 @@ namespace fastfiles add_custom_level_load_zone(load, name, size_est); } + void add_mod_zones(game::LevelLoad* load) + { + const auto mod_zones = mods::get_mod_zones(); + for (const auto& zone : mod_zones) + { + if (zone.alloc_flags & game::DB_ZONE_GAME) + { + add_custom_level_load_zone(load, zone.name.data(), 0x40000); + } + } + } + + void db_decide_level_load_stub(utils::hook::assembler& a) + { + const auto loc_140412859 = a.newLabel(); + + a.pushad64(); + a.mov(rcx, rbx); + a.call_aligned(add_mod_zones); + a.popad64(); + + a.mov(rcx, rdi); + a.call_aligned(0x140609650); + a.test(al, al); + a.jz(loc_140412859); + a.jmp(0x140412817); + + a.bind(loc_140412859); + a.jmp(0x140412859); + } + void db_load_level_add_map_zone_stub(game::LevelLoad* load, const char* name, const unsigned int alloc_flags, const size_t size_est) { @@ -554,10 +600,13 @@ namespace fastfiles // only load extra zones with addon maps & common_specialops & common_survival & custom maps if they exist utils::hook::call(0x1404128B0, db_load_level_add_map_zone_stub); - utils::hook::call(0x140412854, db_load_level_add_custom_zone_stub); utils::hook::call(0x14041282D, db_load_level_add_custom_zone_stub); + utils::hook::call(0x140412854, db_load_level_add_custom_zone_stub); utils::hook::call(0x14041287C, db_load_level_add_custom_zone_stub); + // Load custom mod zones with DB_ZONE_GAME alloc flag + utils::hook::jump(0x14041280B, utils::hook::assemble(db_decide_level_load_stub), true); + // Load assets from 2nd phase (common_specialops, addon map) with DB_LOAD_SYNC utils::hook::call(0x140414EA1, db_load_xassets_stub); diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 46dcea4b..5aad4bf7 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -52,8 +52,7 @@ namespace filesystem const auto mod_path = utils::flags::get_flag("mod"); if (mod_path.has_value()) { - filesystem::register_path(mod_path.value()); - mods::mod_path = mod_path.value(); + mods::set_mod(mod_path.value()); } localized_strings::clear(); diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index e1bc36f6..bf895ade 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -14,13 +14,41 @@ #include #include +#include namespace mods { - std::string mod_path{}; - namespace { + struct mod_zone_info + { + bool has_common_zones; + std::vector zones; + }; + + struct + { + std::optional path; + mod_zone_info zone_info; + } mod_info; + + std::unordered_map alloc_flags_map = + { + {"common", game::DB_ZONE_COMMON}, + {"game", game::DB_ZONE_GAME}, + }; + + unsigned int get_alloc_flag(const std::string& name) + { + const auto lower = utils::string::to_lower(name); + if (alloc_flags_map.find(lower) != alloc_flags_map.end()) + { + return alloc_flags_map[lower]; + } + + return game::DB_ZONE_COMMON; + } + utils::hook::detour db_release_xassets_hook; bool release_assets = false; @@ -56,11 +84,85 @@ namespace mods utils::nt::relaunch_self(" -singleplayer "s.append(arg), true); utils::nt::terminate(); } + + void clear_mod_zones() + { + mod_info.zone_info = {}; + } + + void parse_mod_zones() + { + clear_mod_zones(); + if (!mod_info.path.has_value()) + { + return; + } + + const auto path = mod_info.path.value() + "/zones.csv"; + std::string data{}; + if (!utils::io::read_file(path, &data)) + { + return; + } + + const auto lines = utils::string::split_lines(data); + for (const auto& line : lines) + { + const auto values = utils::string::split(line, ','); + if (values.size() < 2) + { + continue; + } + + const auto alloc_flags = get_alloc_flag(values[0]) | game::DB_ZONE_CUSTOM; + if (alloc_flags & game::DB_ZONE_COMMON) + { + mod_info.zone_info.has_common_zones = true; + } + + mod_info.zone_info.zones.emplace_back(values[1], alloc_flags); + } + } } bool mod_requires_restart(const std::string& path) { - return utils::io::file_exists(path + "/mod.ff") || utils::io::file_exists(path + "/zone/mod.ff"); + return mod_info.zone_info.has_common_zones || + utils::io::file_exists(path + "/mod.ff") || + utils::io::file_exists(path + "/zone/mod.ff"); + } + + void set_mod(const std::string& path) + { + if (mod_info.path.has_value()) + { + filesystem::unregister_path(mod_info.path.value()); + } + + mod_info.path = path; + filesystem::register_path(path); + parse_mod_zones(); + } + + void clear_mod() + { + if (mod_info.path.has_value()) + { + filesystem::unregister_path(mod_info.path.value()); + } + + mod_info.path.reset(); + clear_mod_zones(); + } + + std::vector get_mod_zones() + { + return mod_info.zone_info.zones; + } + + std::optional get_mod() + { + return mod_info.path; } class component final : public component_interface @@ -98,8 +200,10 @@ namespace mods } console::info("Loading mod %s\n", path); + set_mod(path); - if (mod_requires_restart(mod_path) || mod_requires_restart(path)) + if ((mod_info.path.has_value() && mod_requires_restart(mod_info.path.value())) || + mod_requires_restart(path)) { // vid_restart is still broken :( console::info("Restarting...\n"); @@ -107,16 +211,13 @@ namespace mods } else { - filesystem::unregister_path(mod_path); - filesystem::register_path(path); - mod_path = path; restart(); } }); command::add("unloadmod", [](const command::params& params) { - if (mod_path.empty()) + if (!mod_info.path.has_value()) { console::info("No mod loaded\n"); return; @@ -129,17 +230,16 @@ namespace mods return; } - console::info("Unloading mod %s\n", mod_path.data()); + console::info("Unloading mod %s\n", mod_info.path.value().data()); - if (mod_requires_restart(mod_path)) + if (mod_requires_restart(mod_info.path.value())) { console::info("Restarting...\n"); full_restart(""); } else { - filesystem::unregister_path(mod_path); - mod_path.clear(); + clear_mod(); restart(); } }); diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp index 7b7655f6..7f3f69f0 100644 --- a/src/client/component/mods.hpp +++ b/src/client/component/mods.hpp @@ -2,7 +2,14 @@ namespace mods { - extern std::string mod_path; + struct mod_zone + { + std::string name; + unsigned int alloc_flags; + }; bool mod_requires_restart(const std::string& path); + void set_mod(const std::string& path); + std::optional get_mod(); + std::vector get_mod_zones(); } \ No newline at end of file diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index 1d364858..2e2970f8 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -231,7 +231,8 @@ namespace ui_scripting game_type["getloadedmod"] = [](const game&) { - return mods::mod_path; + const auto& path = mods::get_mod(); + return path.value_or(""); }; game_type["addlocalizedstring"] = [](const game&, const std::string& string, diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index 016eaf0c..fba42a88 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -761,7 +761,8 @@ namespace scripting::lua game_type["getloadedmod"] = [](const game&) { - return mods::mod_path; + const auto& mod = mods::get_mod(); + return mod.value_or(""); }; game_type["addlocalizedstring"] = [](const game&, const std::string& string, diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index b25ef09a..401ff88f 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -34,6 +34,25 @@ namespace utils::string return elems; } + std::vector split_lines(const std::string& s) + { + std::stringstream ss(s); + std::string item; + std::vector elems; + + while (std::getline(ss, item, '\n')) + { + if (item.ends_with('\r')) + { + item.pop_back(); + } + + elems.push_back(item); // elems.push_back(std::move(item)); // if C++11 (based on comment from @mchiasson) + } + + return elems; + } + std::string to_lower(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index 970e1617..447a6960 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -81,6 +81,7 @@ namespace utils::string const char* va(const char* fmt, ...); std::vector split(const std::string& s, char delim); + std::vector split_lines(const std::string& s); std::string to_lower(std::string text); std::string to_upper(std::string text);