diff --git a/src/client/component/assets/weapons.cpp b/src/client/component/assets/weapons.cpp new file mode 100644 index 0000000..23c42ab --- /dev/null +++ b/src/client/component/assets/weapons.cpp @@ -0,0 +1,61 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "component/console.hpp" + +#include + +namespace weapons +{ + namespace + { + void g_setup_level_weapon_def_stub() + { + game::G_SetupLevelWeaponDef(); + + // The count on this game seems pretty high + std::array weapons{}; + const auto count = game::DB_GetAllXAssetOfType_FastFile(game::ASSET_TYPE_WEAPON, (void**)weapons.data(), static_cast(weapons.max_size())); + + std::sort(weapons.begin(), weapons.begin() + count, [](game::WeaponCompleteDef* weapon1, game::WeaponCompleteDef* weapon2) + { + assert(weapon1->szInternalName); + assert(weapon2->szInternalName); + + return std::strcmp(weapon1->szInternalName, weapon2->szInternalName) < 0; + }); + +#ifdef _DEBUG + console::info("Found %i weapons to precache\n", count); +#endif + + for (auto i = 0; i < count; ++i) + { +#ifdef _DEBUG + console::info("Precaching weapon \"%s\"\n", weapons[i]->szInternalName); +#endif + (void)game::G_GetWeaponForName(weapons[i]->szInternalName); + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) return; + + // Kill Scr_PrecacheItem (We are going to do this from code) + utils::hook::nop(0x1403101D0, 4); + utils::hook::set(0x1403101D0, 0xC3); + + // Load weapons from the DB + utils::hook::call(0x1402F6EF4, g_setup_level_weapon_def_stub); + utils::hook::call(0x140307401, g_setup_level_weapon_def_stub); + } + }; +} + +REGISTER_COMPONENT(weapons::component) diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp index 1e972b0..07aa9fd 100644 --- a/src/client/component/dvar_cheats.cpp +++ b/src/client/component/dvar_cheats.cpp @@ -11,7 +11,7 @@ namespace dvar_cheats { - void apply_sv_cheats(const game::dvar_t* dvar, const game::DvarSetSource source, game::dvar_value* value) + void apply_sv_cheats(const game::dvar_t* dvar, const game::DvarSetSource source, game::DvarValue* value) { if (dvar && dvar->name == "sv_cheats"s) { diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index 9d02af4..5b7dfd7 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -137,21 +137,6 @@ namespace fastfiles db_find_x_asset_header_hook.create(game::DB_FindXAssetHeader, db_find_x_asset_header_stub); dvars::g_dump_scripts = game::Dvar_RegisterBool("g_dumpScripts", false, game::DVAR_FLAG_NONE); - command::add("loadzone", [](const command::params& params) - { - if (params.size() < 2) - { - console::info("usage: loadzone \n"); - return; - } - - game::XZoneInfo info{}; - info.name = params.get(1); - info.allocFlags = 1; - info.freeFlags = 0; - game::DB_LoadXAssets(&info, 1u, game::DBSyncMode::DB_LOAD_SYNC); - }); - command::add("g_poolSizes", []() { for (auto i = 0; i < game::ASSET_TYPE_COUNT; i++) diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp new file mode 100644 index 0000000..9f32a78 --- /dev/null +++ b/src/client/component/mods.cpp @@ -0,0 +1,233 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "mods.hpp" + +#include +#include + +namespace mods +{ + namespace + { + utils::hook::detour sys_create_file_hook; + + void db_build_os_path_from_source(const char* zone_name, game::FF_DIR source, int size, char* filename) + { + char user_map[MAX_PATH]{}; + + switch (source) + { + case game::FFD_DEFAULT: + (void)game::Com_sprintf(filename, size, "%s\\%s.ff", std::filesystem::current_path().string().c_str(), zone_name); + break; + case game::FFD_MOD_DIR: + assert(mods::is_using_mods()); + + (void)game::Com_sprintf(filename, size, "%s\\%s\\%s.ff", std::filesystem::current_path().string().c_str(), (*dvars::fs_gameDirVar)->current.string, zone_name); + break; + case game::FFD_USER_MAP: + game::I_strncpyz(user_map, zone_name, sizeof(user_map)); + + (void)game::Com_sprintf(filename, size, "%s\\%s\\%s\\%s.ff", std::filesystem::current_path().string().c_str(), "usermaps", user_map, zone_name); + break; + default: + assert(false && "inconceivable"); + break; + } + } + + bool fs_game_dir_domain_func(game::dvar_t* dvar, game::DvarValue new_value) + { + if (*new_value.string == '\0') + { + return true; + } + + if (game::I_strnicmp(new_value.string, "mods", 4) != 0) + { + game::LiveStorage_StatsWriteNotNeeded(game::CONTROLLER_INDEX_0); + console::error("ERROR: Invalid server value '%s' for '%s'\n", new_value.string, dvar->name); + return false; + } + + if (5 < std::strlen(new_value.string) && (new_value.string[4] == '\\' || new_value.string[4] == '/')) + { + const auto* s1 = std::strstr(new_value.string, ".."); + const auto* s2 = std::strstr(new_value.string, "::"); + if (s1 == nullptr && s2 == nullptr) + { + return true; + } + + game::LiveStorage_StatsWriteNotNeeded(game::CONTROLLER_INDEX_0); + console::error("ERROR: Invalid server value '%s' for '%s'\n", new_value.string, dvar->name); + return false; + } + + // Invalid path specified + game::LiveStorage_StatsWriteNotNeeded(game::CONTROLLER_INDEX_0); + console::error("ERROR: Invalid server value '%s' for '%s'\n", new_value.string, dvar->name); + return false; + } + + const auto skip_extra_zones_stub = utils::hook::assemble([](utils::hook::assembler& a) -> void + { + 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, 0x140809D40); + a.mov(rcx, rbp); + a.call(0x1406FE120); + a.jmp(0x140271B63); + + a.bind(skip); + a.popad64(); + a.mov(r13d, game::DB_ZONE_CUSTOM); + a.not_(r13d); + a.and_(esi, r13d); + a.jmp(0x140271D02); + }); + + game::Sys_File sys_create_file_stub(game::Sys_Folder folder, const char* base_filename) + { + auto result = sys_create_file_hook.invoke(folder, base_filename); + + if (result.handle != INVALID_HANDLE_VALUE) + { + return result; + } + + if (!is_using_mods()) + { + return result; + } + + // .ff extension was added previously + if (!std::strcmp(base_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; + } + + void db_load_x_assets_stub(game::XZoneInfo* zone_info, unsigned int zone_count, game::DBSyncMode sync_mode) + { + std::vector zones(zone_info, zone_info + zone_count); + + if (db_mod_file_exists()) + { + zones.emplace_back("mod", game::DB_ZONE_COMMON | game::DB_ZONE_CUSTOM, 0); + } + + game::DB_LoadXAssets(zones.data(), static_cast(zones.size()), sync_mode); + } + } + + bool is_using_mods() + { + return (*dvars::fs_gameDirVar) && *(*dvars::fs_gameDirVar)->current.string; + } + + bool db_mod_file_exists() + { + if (!*(*dvars::fs_gameDirVar)->current.string) + { + return false; + } + + char filename[MAX_PATH]{}; + db_build_os_path_from_source("mod", game::FFD_MOD_DIR, sizeof(filename), filename); + + if (auto zone_file = game::Sys_OpenFileReliable(filename); zone_file != INVALID_HANDLE_VALUE) + { + ::CloseHandle(zone_file); + return true; + } + + return false; + } + + class component final : public component_interface + { + public: + static_assert(sizeof(game::Sys_File) == 8); + + void post_unpack() override + { + dvars::fs_gameDirVar = reinterpret_cast(SELECT_VALUE(0x14A6A7D98, 0x14B20EB48)); + + // Remove DVAR_INIT from fs_game + utils::hook::set(SELECT_VALUE(0x14036137F + 2, 0x1404AE4CB + 2), SELECT_VALUE(game::DVAR_FLAG_NONE, game::DVAR_FLAG_SERVERINFO)); + + utils::hook::inject(SELECT_VALUE(0x140361391 + 3, 0x1404AE4D6 + 3), &fs_game_dir_domain_func); + + if (game::environment::is_sp()) + { + return; + } + + utils::hook::nop(0x140271B54, 15); + utils::hook::jump(0x140271B54, skip_extra_zones_stub, true); + + // Add custom zone paths + sys_create_file_hook.create(game::Sys_CreateFile, sys_create_file_stub); + + // Load mod.ff + utils::hook::call(0x1405A562A, db_load_x_assets_stub); // R_LoadGraphicsAssets According to myself but I don't remember where I got it from + + command::add("loadmod", [](const command::params& params) -> void + { + if (params.size() != 2) + { + console::info("USAGE: %s \"mods/\"", params.get(0)); + return; + } + + std::string mod_name = utils::string::to_lower(params.get(1)); + + if (!mod_name.empty() && !mod_name.starts_with("mods/")) + { + mod_name = "mods/" + mod_name; + } + + // change fs_game if needed + if (mod_name != (*dvars::fs_gameDirVar)->current.string) + { + game::Dvar_SetString((*dvars::fs_gameDirVar), mod_name.c_str()); + command::execute("vid_restart\n"); + } + }); + + command::add("unloadmod", []([[maybe_unused]] const command::params& params) -> void + { + if (*dvars::fs_gameDirVar == nullptr || *(*dvars::fs_gameDirVar)->current.string == '\0') + { + return; + } + + game::Dvar_SetString(*dvars::fs_gameDirVar, ""); + command::execute("vid_restart\n"); + }); + + // TODO: without a way to monitor all the ways fs_game can be changed there is no way to detect when we + // should unregister the path from the internal filesystem we use + // HINT: It could be done in fs_game_dir_domain_func, but I haven't tested if that's the best place to monitor for changes and register/unregister the mods folder + } + }; +} + +REGISTER_COMPONENT(mods::component) diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp new file mode 100644 index 0000000..66ac5a1 --- /dev/null +++ b/src/client/component/mods.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace mods +{ + bool is_using_mods(); + bool db_mod_file_exists(); +} diff --git a/src/client/game/dvars.cpp b/src/client/game/dvars.cpp index 6eb82d2..86c27a8 100644 --- a/src/client/game/dvars.cpp +++ b/src/client/game/dvars.cpp @@ -36,13 +36,13 @@ namespace dvars game::dvar_t* r_fullbright = nullptr; - game::dvar_t* cg_legacyCrashHandling = nullptr; - game::dvar_t* sv_cheats = nullptr; game::dvar_t* com_developer = nullptr; game::dvar_t* com_developer_script = nullptr; + game::dvar_t** fs_gameDirVar = nullptr; + std::string get_dvar_string(const std::string& dvar) { const auto* dvar_value = game::Dvar_FindVar(dvar.data()); diff --git a/src/client/game/dvars.hpp b/src/client/game/dvars.hpp index 2ba7ad9..814deb5 100644 --- a/src/client/game/dvars.hpp +++ b/src/client/game/dvars.hpp @@ -35,13 +35,13 @@ namespace dvars extern game::dvar_t* r_fullbright; - extern game::dvar_t* cg_legacyCrashHandling; - extern game::dvar_t* sv_cheats; extern game::dvar_t* com_developer; extern game::dvar_t* com_developer_script; + extern game::dvar_t** fs_gameDirVar; + std::string get_dvar_string(const std::string& dvar); bool get_dvar_bool(const std::string& dvar); diff --git a/src/client/game/game.cpp b/src/client/game/game.cpp index dcad5aa..79c8fd1 100644 --- a/src/client/game/game.cpp +++ b/src/client/game/game.cpp @@ -30,6 +30,13 @@ namespace game return !game::environment::is_sp() && *mp::virtualLobby_loaded == 1; } + HANDLE Sys_OpenFileReliable(const char* filename) + { + return ::CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); + } + namespace environment { launcher::mode mode = launcher::mode::none; diff --git a/src/client/game/game.hpp b/src/client/game/game.hpp index 385a527..08c6be0 100644 --- a/src/client/game/game.hpp +++ b/src/client/game/game.hpp @@ -66,6 +66,8 @@ namespace game [[nodiscard]] bool VirtualLobby_Loaded(); + [[nodiscard]] HANDLE Sys_OpenFileReliable(const char* filename); + [[nodiscard]] bool is_headless(); void show_error(const std::string& text, const std::string& title = "Error"); } diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index b7e7afa..e12f4de 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -25,6 +25,14 @@ namespace game typedef void(*BuiltinMethod)(scr_entref_t); typedef void(*BuiltinFunction)(); + enum ControllerIndex_t + { + INVALID_CONTROLLER_PORT = -1, + CONTROLLER_INDEX_0 = 0x0, + CONTROLLER_INDEX_FIRST = 0x0, + CONTROLLER_INDEX_COUNT = 0x1, + }; + enum { VAR_UNDEFINED = 0x0, @@ -905,6 +913,7 @@ namespace game DVAR_FLAG_LATCHED = 0x2, DVAR_FLAG_CHEAT = 0x4, DVAR_FLAG_REPLICATED = 0x8, + DVAR_FLAG_SERVERINFO = 0x400, DVAR_FLAG_WRITE = 0x800, DVAR_FLAG_READ = 0x2000, }; @@ -923,7 +932,7 @@ namespace game rgb = 9 // Color without alpha }; - union dvar_value + union DvarValue { bool enabled; int integer; @@ -966,9 +975,9 @@ namespace game unsigned int flags; dvar_type type; bool modified; - dvar_value current; - dvar_value latched; - dvar_value reset; + DvarValue current; + DvarValue latched; + DvarValue reset; dvar_limits domain; }; @@ -1319,6 +1328,15 @@ namespace game const char *name; }; + struct WeaponDef + {}; + + struct WeaponCompleteDef + { + const char* szInternalName; + WeaponDef* weapDef; + }; // Incomplete + union XAssetHeader { void* data; @@ -1443,6 +1461,31 @@ namespace game netProfileStream_t recieve; }; + enum + { + 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 = 0x80, + }; + + enum FF_DIR + { + FFD_DEFAULT = 0x0, + FFD_MOD_DIR = 0x1, + FFD_USER_MAP = 0x2, + }; + + struct Sys_File + { + HANDLE handle; + }; + namespace mp { enum diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 256f103..5058f15 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -60,6 +60,7 @@ namespace game WEAK symbol DB_GetRawFileLen{0x14017E890, 0x14026FCC0}; WEAK symbol DB_GetRawBuffer{0x14017E750, 0x14026FB90}; WEAK symbol DB_ReadRawFile{0x140180E30, 0x140273080}; + WEAK symbol DB_GetAllXAssetOfType_FastFile{0x0, 0x14026F970}; WEAK symbol Dvar_FindVar{0x140370860, 0x1404BF8B0}; WEAK symbol Dvar_ClearModified{0x140370700, 0x1404BF690}; @@ -70,7 +71,7 @@ namespace game WEAK symbol Dvar_SetString{0x140373DE0, 0x1404C3610}; WEAK symbol Dvar_SetBool{0x0, 0x1404C1F30}; WEAK symbol Dvar_SetFromStringByNameFromSource{0x1403737D0, 0x1404C2E40}; - WEAK symbol Dvar_ValueToString{0x140374E10, 0x1404C47B0}; + WEAK symbol Dvar_ValueToString{0x140374E10, 0x1404C47B0}; WEAK symbol Dvar_RegisterBool{0x140371850, 0x1404C0BE0}; WEAK symbol Dvar_RegisterEnum{0x140371B30, 0x1404C0EC0}; @@ -98,13 +99,13 @@ namespace game WEAK symbol GetVariable{0x0, 0x1403F3730}; WEAK symbol G_Glass_Update{0x14021D540, 0x1402EDEE0}; - WEAK symbol G_GetClientScore{0, 0x1402F6AB0}; WEAK symbol G_GetWeaponForName{0x140274590, 0x14033FF60}; WEAK symbol G_GivePlayerWeapon{0x1402749B0, 0x140340470}; WEAK symbol G_InitializeAmmo{0x1402217F0, 0x1402F22B0}; WEAK symbol G_SelectWeapon{0x140275380, 0x140340D50}; WEAK symbol G_TakePlayerWeapon{0x1402754E0, 0x1403411D0}; + WEAK symbol G_SetupLevelWeaponDef{0x0, 0x140340DE0}; WEAK symbol I_CleanStr{0x140379010, 0x1404C99A0}; @@ -113,6 +114,7 @@ namespace game WEAK symbol Key_KeynumToString{0x14013F380, 0x140207C50}; WEAK symbol Live_SyncOnlineDataFlags{0x1404459A0, 0x140562830}; + WEAK symbol LiveStorage_StatsWriteNotNeeded{0x1402F51F0, 0x1403C3CD0}; WEAK symbol LUI_OpenMenu{0, 0x14048E450}; WEAK symbol LUI_EnterCriticalSection{0, 0x1400D2B10}; @@ -226,6 +228,8 @@ namespace game WEAK symbol MSG_WriteReliableCommandToBuffer{0x0, 0x1403E1090}; + WEAK symbol I_strnicmp{0x1403793E0, 0x1404C9E90}; + WEAK symbol longjmp{0x14059C5C0, 0x1406FD930}; WEAK symbol _setjmp{0x14059CD00, 0x1406FE070};