From dd65494ca295b70959e3219a94060eb3ec07080c Mon Sep 17 00:00:00 2001 From: m Date: Tue, 16 Aug 2022 15:23:36 -0700 Subject: [PATCH] allow loading of custom zones (basic mod support) (#172) * allow loading of custom zones * remove game:: * remove unnecessary hook * mod support - fs_game support - precache all weapon files loaded in database - parse startup variables correctly Co-authored-by: quaK <38787176+Joelrau@users.noreply.github.com> --- src/client/component/command.cpp | 42 ++++++- src/client/component/dvars.cpp | 24 ++++ src/client/component/dvars.hpp | 2 + src/client/component/fastfiles.cpp | 187 ++++++++++++++++++++++++++++ src/client/component/fastfiles.hpp | 2 + src/client/component/filesystem.cpp | 3 + src/client/component/party.cpp | 18 ++- src/client/component/party.hpp | 2 +- src/client/component/weapon.cpp | 124 ++++++++++++++++++ src/client/game/structs.hpp | 20 +++ src/client/game/symbols.hpp | 5 + 11 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 src/client/component/weapon.cpp diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp index c26eea7e..8eda387a 100644 --- a/src/client/component/command.cpp +++ b/src/client/component/command.cpp @@ -11,6 +11,7 @@ #include "fastfiles.hpp" #include "scheduler.hpp" #include "logfile.hpp" +#include "dvars.hpp" #include #include @@ -22,7 +23,6 @@ namespace command namespace { utils::hook::detour client_command_hook; - utils::hook::detour parse_commandline_hook; std::unordered_map> handlers; std::unordered_map> handlers_sv; @@ -105,10 +105,44 @@ namespace command parsed = true; } - void parse_commandline_stub() + void parse_startup_variables() { + auto& com_num_console_lines = *reinterpret_cast(0x35634B8_b); + auto* com_console_lines = reinterpret_cast(0x35634C0_b); + + for (int i = 0; i < com_num_console_lines; i++) + { + game::Cmd_TokenizeString(com_console_lines[i]); + + // only +set dvar value + if (game::Cmd_Argc() >= 3 && game::Cmd_Argv(0) == "set"s) + { + const std::string& dvar_name = game::Cmd_Argv(1); + const std::string& value = game::Cmd_Argv(2); + + const auto* dvar = game::Dvar_FindVar(dvar_name.data()); + if (dvar) + { + game::Dvar_SetCommand(dvar->hash, "", value.data()); + } + else + { + dvars::on_register(dvar_name, [dvar_name, value]() + { + game::Dvar_SetCommand(game::generateHashValue(dvar_name.data()), "", value.data()); + }); + } + } + + game::Cmd_EndTokenizeString(); + } + } + + void parse_commandline_stub(char* commandline) + { + //utils::hook::invoke(0x17CB60_b, commandline); // Com_ParseCommandLine parse_command_line(); - parse_commandline_hook.invoke(); + parse_startup_variables(); } game::dvar_t* dvar_command_stub() @@ -531,7 +565,7 @@ namespace command } else { - parse_commandline_hook.create(0x157D50_b, parse_commandline_stub); + utils::hook::call(0x15C44B_b, parse_commandline_stub); add_commands_mp(); } diff --git a/src/client/component/dvars.cpp b/src/client/component/dvars.cpp index 3b285f20..416aaace 100644 --- a/src/client/component/dvars.cpp +++ b/src/client/component/dvars.cpp @@ -263,6 +263,8 @@ namespace dvars utils::hook::detour dvar_register_vector3_hook; utils::hook::detour dvar_register_enum_hook; + utils::hook::detour dvar_register_new_hook; + utils::hook::detour dvar_set_bool_hook; utils::hook::detour dvar_set_float_hook; utils::hook::detour dvar_set_int_hook; @@ -407,6 +409,26 @@ namespace dvars return dvar_register_enum_hook.invoke(hash, name, value_list, default_index, flags); } + std::unordered_map> dvar_on_register_function_map; + void on_register(const std::string& name, const std::function& callback) + { + dvar_on_register_function_map[game::generateHashValue(name.data())] = callback; + } + + game::dvar_t* dvar_register_new(const int hash, const char* name, game::dvar_type type, unsigned int flags, + game::dvar_value* value, game::dvar_limits* domain, const char* description) + { + auto* dvar = dvar_register_new_hook.invoke(hash, name, type, flags, value, domain, description); + + if (dvar && dvar_on_register_function_map.find(hash) != dvar_on_register_function_map.end()) + { + dvar_on_register_function_map[hash](); + dvar_on_register_function_map.erase(hash); + } + + return dvar; + } + void dvar_set_bool(game::dvar_t* dvar, bool boolean) { const auto disabled = find_dvar(disable::set_bool_disables, dvar->hash); @@ -505,6 +527,8 @@ namespace dvars dvar_register_vector3_hook.create(SELECT_VALUE(0x419A00_b, 0x182DB0_b), &dvar_register_vector3); dvar_register_enum_hook.create(SELECT_VALUE(0x419500_b, 0x182700_b), &dvar_register_enum); + dvar_register_new_hook.create(SELECT_VALUE(0x41B1D0_b, 0x184DF0_b), &dvar_register_new); + if (!game::environment::is_sp()) { dvar_register_bool_hashed_hook.create(SELECT_VALUE(0x0, 0x182420_b), &dvar_register_bool_hashed); diff --git a/src/client/component/dvars.hpp b/src/client/component/dvars.hpp index 419530e2..c00eafa6 100644 --- a/src/client/component/dvars.hpp +++ b/src/client/component/dvars.hpp @@ -26,4 +26,6 @@ namespace dvars void set_string(const std::string& name, const std::string& string); void set_from_string(const std::string& name, const std::string& value); } + + void on_register(const std::string& name, const std::function& callback); } diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index 1ff9a8ae..00ad4293 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -82,6 +82,131 @@ namespace fastfiles return result; } + + utils::hook::detour db_read_stream_file_hook; + void db_read_stream_file_stub(int a1, int a2) + { + // always use lz4 compressor type when reading stream files + *game::g_compressor = 4; + return db_read_stream_file_hook.invoke(a1, a2); + } + + void skip_extra_zones_stub(utils::hook::assembler& a) + { + const auto skip = a.newLabel(); + const auto original = a.newLabel(); + + a.pushad64(); + a.test(esi, game::DB_ZONE_CUSTOM); // allocFlags + a.jnz(skip); + + a.bind(original); + a.popad64(); + a.mov(rdx, 0x8E2F80_b); + a.mov(rcx, rbp); + a.call(0x840A20_b); + a.jmp(0x398070_b); + + a.bind(skip); + a.popad64(); + a.mov(r14d, game::DB_ZONE_CUSTOM); + a.not_(r14d); + a.and_(esi, r14d); + a.jmp(0x39814F_b); + } + + 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); + } + + 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 load_pre_gfx_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // code_pre_gfx_mp + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + + void load_post_gfx_and_ui_and_common_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // code_post_gfx_mp + // ui_mp + // common_mp + + 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_ui_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // ui_mp + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + } + + bool exists(const std::string& zone) + { + auto is_localized = game::DB_IsLocalized(zone.data()); + auto handle = game::Sys_CreateFile((is_localized ? game::SF_ZONE_LOC : game::SF_ZONE), utils::string::va("%s.ff", zone.data())); + if (handle != (HANDLE)-1) + { + CloseHandle(handle); + return true; + } + return false; } std::string get_current_fastfile() @@ -113,6 +238,68 @@ namespace fastfiles db_find_xasset_header_hook.create(game::DB_FindXAssetHeader, db_find_xasset_header_stub); g_dump_scripts = dvars::register_bool("g_dumpScripts", false, game::DVAR_FLAG_NONE, "Dump GSC scripts"); + + // Allow loading of unsigned fastfiles + if (!game::environment::is_sp()) + { + utils::hook::nop(0x368153_b, 2); // DB_InflateInit + } + + // Allow loading of mixed compressor types + utils::hook::nop(SELECT_VALUE(0x1C4BE7_b, 0x3687A7_b), 2); + + // Fix compressor type on streamed file load + db_read_stream_file_hook.create(SELECT_VALUE(0x1FB9D0_b, 0x3A1BF0_b), db_read_stream_file_stub); + + // Don't load extra zones with loadzone + if (!game::environment::is_sp()) + { + // TODO: SP? + utils::hook::nop(0x398061_b, 15); + utils::hook::jump(0x398061_b, utils::hook::assemble(skip_extra_zones_stub), true); + } + + if (!game::environment::is_sp()) + { + // Add custom zone paths + sys_createfile_hook.create(game::Sys_CreateFile, sys_create_file_stub); + + // load our custom pre_gfx zones + utils::hook::call(0x15C3FD_b, load_pre_gfx_zones); + utils::hook::call(0x15C75D_b, load_pre_gfx_zones); + + // load our custom ui and common zones + utils::hook::call(0x686421_b, load_post_gfx_and_ui_and_common_zones); + + // load our custom ui zones + utils::hook::call(0x17C6D2_b, load_ui_zones); + } + + command::add("loadzone", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: loadzone \n"); + return; + } + + const char* name = params.get(1); + + if (!fastfiles::exists(name)) + { + console::warn("loadzone: zone \"%s\" could not be found!\n", name); + return; + } + + game::XZoneInfo info; + info.name = name; + info.allocFlags = game::DB_ZONE_GAME; + info.freeFlags = 0; + + info.allocFlags |= game::DB_ZONE_CUSTOM; // skip extra zones with this flag! + + game::DB_LoadXAssets(&info, 1, game::DBSyncMode::DB_LOAD_ASYNC); + }); } }; } diff --git a/src/client/component/fastfiles.hpp b/src/client/component/fastfiles.hpp index 4f4108a0..dbda02c7 100644 --- a/src/client/component/fastfiles.hpp +++ b/src/client/component/fastfiles.hpp @@ -4,6 +4,8 @@ namespace fastfiles { + bool exists(const std::string& zone); + std::string get_current_fastfile(); void enum_assets(const game::XAssetType type, diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 15d11826..17364883 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -83,6 +83,9 @@ namespace filesystem get_search_paths().insert("."); get_search_paths().insert("h1-mod"); get_search_paths().insert("data"); + + // fs_game flags + utils::hook::set(0x189275_b, 0); } }; } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index da192a27..e8be9910 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -247,13 +247,13 @@ namespace party return connect_state.challenge; } - void start_map(const std::string& mapname) + void start_map(const std::string& mapname, bool dev) { if (game::Live_SyncOnlineDataFlags(0) > 32) { scheduler::once([=]() { - command::execute("map " + mapname, false); + start_map(mapname, dev); }, scheduler::pipeline::main, 1s); } else @@ -299,6 +299,8 @@ namespace party command::execute(utils::string::va("party_maxplayers %i", maxclients->current.integer), true); }*/ + command::execute((dev ? "sv_cheats 1" : "sv_cheats 0"), true); + const auto* args = "StartServer"; game::UI_RunMenuScript(0, &args); } @@ -349,7 +351,17 @@ namespace party return; } - start_map(argument[1]); + start_map(argument[1], false); + }); + + command::add("devmap", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + party::start_map(argument[1], true); }); command::add("map_restart", []() diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 13990aea..3efa7b79 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -6,7 +6,7 @@ namespace party void reset_connect_state(); void connect(const game::netadr_s& target); - void start_map(const std::string& mapname); + void start_map(const std::string& mapname, bool dev = false); void clear_sv_motd(); game::netadr_s get_state_host(); diff --git a/src/client/component/weapon.cpp b/src/client/component/weapon.cpp new file mode 100644 index 00000000..5d62251b --- /dev/null +++ b/src/client/component/weapon.cpp @@ -0,0 +1,124 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "console.hpp" +#include "command.hpp" +#include "fastfiles.hpp" + +#include "utils/hook.hpp" + +namespace weapon +{ + namespace + { + utils::hook::detour g_setup_level_weapon_def_hook; + void g_setup_level_weapon_def_stub() + { + // precache level weapons first + g_setup_level_weapon_def_hook.invoke(); + + std::vector weapons; + + // find all weapons in asset pools + fastfiles::enum_assets(game::ASSET_TYPE_WEAPON, [&weapons](game::XAssetHeader header) + { + weapons.push_back(header.weapon); + }, false); + + // sort weapons + std::sort(weapons.begin(), weapons.end(), [](game::WeaponDef* weapon1, game::WeaponDef* weapon2) + { + return std::string_view(weapon1->name) < + std::string_view(weapon2->name); + }); + + // precache items + for (std::size_t i = 0; i < weapons.size(); i++) + { + console::debug("precaching weapon \"%s\"\n", weapons[i]->name); + game::G_GetWeaponForName(weapons[i]->name); + } + } + + template + void set_weapon_field(const std::string& weapon_name, unsigned int field, T value) + { + auto weapon = game::DB_FindXAssetHeader(game::ASSET_TYPE_WEAPON, weapon_name.data(), false).data; + if (weapon) + { + if (field && field < (0xE20 + sizeof(T))) + { + *reinterpret_cast(reinterpret_cast(weapon) + field) = value; + } + else + { + console::warn("weapon field: %d is higher than the size of weapon struct!\n", field); + } + } + else + { + console::warn("weapon %s not found!\n", weapon_name.data()); + } + } + + void set_weapon_field_float(const std::string& weapon_name, unsigned int field, float value) + { + set_weapon_field(weapon_name, field, value); + } + + void set_weapon_field_int(const std::string& weapon_name, unsigned int field, int value) + { + set_weapon_field(weapon_name, field, value); + } + + void set_weapon_field_bool(const std::string& weapon_name, unsigned int field, bool value) + { + set_weapon_field(weapon_name, field, value); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + g_setup_level_weapon_def_hook.create(0x462630_b, g_setup_level_weapon_def_stub); + +#ifdef DEBUG + command::add("setWeaponFieldFloat", [](const command::params& params) + { + if (params.size() <= 3) + { + console::info("usage: setWeaponFieldInt \n"); + return; + } + set_weapon_field_float(params.get(1), atoi(params.get(2)), static_cast(atof(params.get(3)))); + }); + + command::add("setWeaponFieldInt", [](const command::params& params) + { + if (params.size() <= 3) + { + console::info("usage: setWeaponFieldInt \n"); + return; + } + set_weapon_field_int(params.get(1), atoi(params.get(2)), static_cast(atoi(params.get(3)))); + }); + + command::add("setWeaponFieldBool", [](const command::params& params) + { + if (params.size() <= 3) + { + console::info("usage: setWeaponFieldBool \n"); + return; + } + set_weapon_field_bool(params.get(1), atoi(params.get(2)), static_cast(atoi(params.get(3)))); + }); +#endif + } + }; +} + +REGISTER_COMPONENT(weapon::component) \ No newline at end of file diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index a75ed125..fd6db443 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1010,6 +1010,20 @@ namespace game DB_LOAD_SYNC_SKIP_ALWAYS_LOADED = 0x5, }; + enum DBAllocFlags : std::int32_t + { + DB_ZONE_NONE = 0x0, + DB_ZONE_COMMON = 0x1, + DB_ZONE_UI = 0x2, + DB_ZONE_GAME = 0x4, + DB_ZONE_LOAD = 0x8, + DB_ZONE_DEV = 0x10, + DB_ZONE_BASEMAP = 0x20, + DB_ZONE_TRANSIENT_POOL = 0x40, + DB_ZONE_TRANSIENT_MASK = 0x40, + DB_ZONE_CUSTOM = 0x200 // added for custom zone loading + }; + struct XZoneInfo { const char* name; @@ -1397,6 +1411,11 @@ namespace game const char* name; }; + struct WeaponDef + { + const char* name; + }; + union XAssetHeader { void* data; @@ -1408,6 +1427,7 @@ namespace game LuaFile* luaFile; GfxImage* image; TTF* ttf; + WeaponDef* weapon; }; struct XAsset diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 375bb3d4..76b9baef 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -164,6 +164,9 @@ namespace game WEAK symbol DB_GetXAssetTypeSize{0x0, 0x0}; WEAK symbol DB_FindXAssetHeader{0x1F1120, 0x3950C0}; + WEAK symbol DB_FileExists{0x1F0D50, 0x394DC0}; + WEAK symbol DB_LoadXAssets{0x1F31E0, 0x397500}; + WEAK symbol DB_IsLocalized{0x0, 0x396790}; WEAK symbol LUI_OpenMenu{0x3F20A0, 0x1E1210}; @@ -207,6 +210,7 @@ namespace game WEAK symbol Sys_IsDatabaseReady2{0x3AB100, 0x4F79C0}; WEAK symbol Sys_SendPacket{0x0, 0x5BDA90}; WEAK symbol Sys_FileExists{0x0, 0x0}; + WEAK symbol Sys_CreateFile{0x0, 0x5B2860}; WEAK symbol UI_GetMapDisplayName{0x0, 0x4DDEE0}; WEAK symbol UI_GetGameTypeDisplayName{0x0, 0x4DD8C0}; @@ -238,6 +242,7 @@ namespace game WEAK symbol connectionState{0x0, 0x2EC82C8}; WEAK symbol g_poolSize{0x0, 0x0}; + WEAK symbol g_compressor{0x2574804, 0x3962804}; WEAK symbol scr_VarGlob{0xBD80E00, 0xB138180}; WEAK symbol scr_VmPub{0xC3F4E20, 0xB7AE3C0};