From e243afacb63aaf84c6b2ca9a19f1c371707f3468 Mon Sep 17 00:00:00 2001 From: HighTechRedNeck Date: Mon, 20 Jan 2025 14:43:24 -0500 Subject: [PATCH] Added Loadmod/UnloadMod commands and Lua functions directoryexists, listfiles, directoryisempty, and fileexists for mod loading menu. Cleaned up mods.cpp and fastfile.cpp. Added Load_LevelZone to reload mods after maps have been loaded to allow overriding map gsc's and other assets. Added Com_Shutdown to Symbols.hpp. --- src/client/component/fastfiles.cpp | 120 +++++++++++++++++ src/client/component/mods.cpp | 180 ++++++++++++++++++-------- src/client/component/ui_scripting.cpp | 21 +++ src/client/game/symbols.hpp | 2 + 4 files changed, 271 insertions(+), 52 deletions(-) diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index f5bb265..a012d3a 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -10,6 +10,7 @@ #include #include +#include #include namespace fastfiles @@ -19,6 +20,25 @@ namespace fastfiles utils::hook::detour db_try_load_x_file_internal_hook; utils::hook::detour db_find_x_asset_header_hook; + template inline void merge(std::vector* target, T* source, size_t length) + { + if (source) + { + for (size_t i = 0; i < length; ++i) + { + target->push_back(source[i]); + } + } + } + + template inline void merge(std::vector* target, std::vector source) + { + for (auto& entry : source) + { + target->push_back(entry); + } + } + void db_try_load_x_file_internal(const char* zone_name, const int zone_flags, const int is_base_map) { console::info("Loading fastfile %s\n", zone_name); @@ -90,6 +110,42 @@ namespace fastfiles console::info("Unloaded fastfile %s\n", name); game::PMem_Free(name, alloc_dir); } + + const auto skip_extra_zones_stub = utils::hook::assemble([](utils::hook::assembler& a) + { + const auto skip = a.newLabel(); + const auto original = a.newLabel(); + + a.pushad64(); + a.test(ebp, game::DB_ZONE_CUSTOM); // allocFlags + a.jnz(skip); + + a.bind(original); + a.popad64(); + a.mov(rdx, 0x140835F28); + a.mov(rcx, rsi); + a.call_aligned(strcmp); + a.jmp(0x1403217C0); + + a.bind(skip); + a.popad64(); + a.mov(r15d, 0x80); + a.not_(r15d); + a.and_(ebp, r15d); + a.jmp(0x1403217F6); + }); + } + + bool exists(const std::string& zone_name) + { + auto is_localized = game::DB_IsLocalized(zone_name.data()); + auto handle = game::Sys_CreateFile(game::Sys_Folder(is_localized), utils::string::va("%s.ff", zone_name.data())); + if (handle != (HANDLE)-1) + { + CloseHandle(handle); + return true; + } + return false; } void enum_assets(const game::XAssetType type, const std::function& callback, const bool include_override) @@ -101,6 +157,57 @@ namespace fastfiles }), &callback, include_override); } + void Load_CommonZones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + if (fastfiles::exists("mod")) + { + data.push_back({ "mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0 }); + } + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + + void Load_LevelZones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + if (fastfiles::exists("mod")) + { + data.push_back({ "mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0 }); + } + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + + utils::hook::detour sys_createfile_hook; + HANDLE sys_create_file_stub(game::Sys_Folder folder, const char* base_filename) + { + auto* fs_basepath = game::Dvar_FindVar("fs_basepath"); + auto* fs_game = game::Dvar_FindVar("fs_game"); + + std::string dir = fs_basepath ? fs_basepath->current.string : ""; + std::string mod_dir = fs_game ? fs_game->current.string : ""; + + if (base_filename == "mod.ff"s) + { + if (!mod_dir.empty()) + { + auto path = utils::string::va("%s\\%s\\%s", dir.data(), mod_dir.data(), base_filename); + if (utils::io::file_exists(path)) + { + return CreateFileA(path, 0x80000000, 1u, 0, 3u, 0x60000000u, 0); + } + } + return (HANDLE)-1; + } + + return sys_createfile_hook.invoke(folder, base_filename); + } + class component final : public component_interface { public: @@ -118,6 +225,19 @@ namespace fastfiles utils::hook::set(0x1402FBF23, 0xEB); // DB_LoadXFile utils::hook::nop(0x1402FC445, 2); // DB_SetFileLoadCompressor + // Don't load eng_ + patch_ with loadzone + utils::hook::nop(0x1403217B1, 15); + utils::hook::jump(0x1403217B1, skip_extra_zones_stub, true); + + // Add custom zone paths + sys_createfile_hook.create(game::Sys_CreateFile, sys_create_file_stub); + + // Load our custom fastfiles (Mod) + utils::hook::call(0x1405E7113, Load_CommonZones); + + // Reload mod after level is loaded to allow overriding map stuff + utils::hook::call(0x140320ED1, Load_LevelZones); + command::add("materiallist", [](const command::params& params) { game::DB_EnumXAssets_FastFile(game::ASSET_TYPE_MATERIAL, [](const game::XAssetHeader header, void*) diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index fd588b4..7936bfe 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -3,16 +3,27 @@ #include "game/game.hpp" #include "game/dvars.hpp" +#include "command.hpp" +#include "console.hpp" +#include "filesystem.hpp" +#include "scheduler.hpp" + #include "mods.hpp" #include +#include namespace mods { + + std::optional mod_path; + namespace { utils::hook::detour sys_create_file_hook; + bool release_assets = false; + void db_build_os_path_from_source(const char* zone_name, game::FF_DIR source, unsigned int size, char* filename) { char user_map[MAX_PATH]{}; @@ -38,66 +49,74 @@ namespace mods } } - game::Sys_File sys_create_file_stub(const char* dir, const char* filename) + void restart() { - auto result = sys_create_file_hook.invoke(dir, filename); - - if (result.handle != INVALID_HANDLE_VALUE) + scheduler::once([]() { - return result; - } + release_assets = true; + const auto _0 = gsl::finally([]() + { + release_assets = false; + }); - if (!is_using_mods()) - { - return result; - } - - // .ff extension was added previously - if (!std::strcmp(filename, "mod.ff") && mods::db_mod_file_exists()) - { - char file_path[MAX_PATH]{}; - db_build_os_path_from_source("mod", game::FFD_MOD_DIR, sizeof(file_path), file_path); - result.handle = game::Sys_OpenFileReliable(file_path); - } - - return result; + game::Com_Shutdown(""); + }, scheduler::pipeline::main); } - void db_load_x_assets_stub(game::XZoneInfo* zone_info, unsigned int zone_count, game::DBSyncMode sync_mode) + void full_restart(const std::string& arg) { - std::vector zones(zone_info, zone_info + zone_count); - - if (db_mod_file_exists()) + if (game::environment::is_mp()) { - zones.emplace_back("mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0); + command::execute("vid_restart"); + scheduler::once([] + { + //mods::read_stats(); + }, scheduler::main); + return; } - game::DB_LoadXAssets(zones.data(), static_cast(zones.size()), sync_mode); + auto mode = game::environment::is_mp() ? " -multiplayer "s : " -singleplayer "s; + + utils::nt::relaunch_self(); + utils::nt::terminate(); } - const auto skip_extra_zones_stub = utils::hook::assemble([](utils::hook::assembler& a) + bool mod_requires_restart(const std::string& path) { - const auto skip = a.newLabel(); - const auto original = a.newLabel(); + return utils::io::file_exists(path + "/mod.ff") || utils::io::file_exists(path + "/zone/mod.ff"); + } - a.pushad64(); - a.test(ebp, game::DB_ZONE_CUSTOM); // allocFlags - a.jnz(skip); + void set_filesystem_data(const std::string& path, bool change_fs_game) + { + if (mod_path.has_value()) + { + filesystem::unregister_path(mod_path.value()); + } - a.bind(original); - a.popad64(); - a.mov(rdx, 0x140835F28); - a.mov(rcx, rsi); - a.call_aligned(strcmp); - a.jmp(0x1403217C0); + if (change_fs_game) + { + game::Dvar_SetFromStringByNameFromSource("fs_game", path.data(), game::DVAR_SOURCE_INTERNAL); + } - a.bind(skip); - a.popad64(); - a.mov(r15d, 0x80); - a.not_(r15d); - a.and_(ebp, r15d); - a.jmp(0x1403217F6); - }); + if (path != "") + { + filesystem::register_path(path); + } + } + } + + void set_mod(const std::string& path, bool change_fs_game) + { + set_filesystem_data(path, change_fs_game); + + if (path != "") + { + mod_path = path; + } + else + { + mod_path.reset(); + } } bool is_using_mods() @@ -141,15 +160,72 @@ namespace mods return; } - // Don't load eng_ + patch_ with loadzone - utils::hook::nop(0x1403217B1, 15); - utils::hook::jump(0x1403217B1, skip_extra_zones_stub, true); + command::add("loadmod", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("Usage: loadmod mods/"); + return; + } - // Add custom zone paths - sys_create_file_hook.create(game::Sys_CreateFile, sys_create_file_stub); + /*if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) + { + console::info("Cannot load mod while in-game!\n"); + game::CG_GameMessage(0, "^1Cannot load mod while in-game!"); + return; + }*/ - // Load mod.ff - utils::hook::call(0x1405E7113, db_load_x_assets_stub); // R_LoadGraphicsAssets According to myself but I don't remember where I got it from + 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); + set_mod(path, true); + + if ((mod_path.has_value() && mod_requires_restart(mod_path.value())) || + mod_requires_restart(path)) + { + console::info("Restarting...\n"); + full_restart("-mod \""s + path + "\""); + } + else + { + restart(); + } + }); + + command::add("unloadmod", [](const command::params& params) + { + if (!mod_path.has_value()) + { + console::info("No mod loaded\n"); + return; + } + + /*if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) + { + 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.value().data()); + + if (mod_requires_restart(mod_path.value())) + { + console::info("Restarting...\n"); + set_mod("", true); + full_restart(""); + } + else + { + set_mod("", true); + restart(); + } + }); } }; } diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index 5a1f7cc..ac1e879 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -195,6 +195,27 @@ namespace ui_scripting setup_functions(); lua["print"] = function(reinterpret_cast(0x14017B120)); // hks::base_print + + lua["directoryexists"] = [](const std::string& string) + { + return utils::io::directory_exists(string); + }; + + lua["listfiles"] = [](const std::string& string) + { + return utils::io::list_files(string); + }; + + lua["directoryisempty"] = [](const std::string& string) + { + return utils::io::directory_is_empty(string); + }; + + lua["fileexists"] = [](const std::string& string) + { + return utils::io::file_exists(string); + }; + lua["table"]["unpack"] = lua["unpack"]; lua["luiglobals"] = lua; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index a96c362..8087ca5 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -8,6 +8,8 @@ namespace game * Functions **************************************************************/ + WEAK symbol Com_Shutdown{ 0x0, 0x140415B30 }; + WEAK symbol AddRefToObject{0x1403D7A10, 0x1404326D0}; WEAK symbol AddRefToValue{0x1403D7740, 0x1404326E0}; WEAK symbol AllocThread{0, 0x1404329B0};