From d0003da606fbf8a8ec0581caa04cccb28e81f701 Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:05:06 +0100 Subject: [PATCH 01/14] Changed all game struct suffixes from _s to _t. --- src/client/component/auth.cpp | 4 +- src/client/component/bots.cpp | 6 +- src/client/component/command.cpp | 4 +- src/client/component/dedicated.cpp | 2 +- src/client/component/discord.cpp | 2 +- src/client/component/dvar_cheats.cpp | 2 +- src/client/component/game_console.cpp | 2 +- src/client/component/gameplay.cpp | 24 ++++---- src/client/component/network.cpp | 20 +++---- src/client/component/network.hpp | 22 +++---- src/client/component/party.cpp | 16 ++--- src/client/component/party.hpp | 4 +- src/client/component/patches.cpp | 4 +- src/client/component/rcon.cpp | 8 +-- src/client/component/server_list.cpp | 14 ++--- src/client/component/server_list.hpp | 4 +- src/client/game/structs.hpp | 84 +++++++++++++-------------- src/client/game/symbols.hpp | 48 +++++++-------- 18 files changed, 135 insertions(+), 135 deletions(-) diff --git a/src/client/component/auth.cpp b/src/client/component/auth.cpp index 74115b2..54f0ac1 100644 --- a/src/client/component/auth.cpp +++ b/src/client/component/auth.cpp @@ -82,7 +82,7 @@ namespace auth return key; } - int send_connect_data_stub(game::netsrc_t sock, game::netadr_s* adr, const char* format, const int len) + int send_connect_data_stub(game::netsrc_t sock, game::netadr_t* adr, const char* format, const int len) { std::string connect_string(format, len); game::SV_Cmd_TokenizeString(connect_string.data()); @@ -116,7 +116,7 @@ namespace auth return true; } - void direct_connect(game::netadr_s* from, game::msg_t* msg) + void direct_connect(game::netadr_t* from, game::msg_t* msg) { const auto offset = sizeof("connect") + 4; diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp index 21ba876..dd4f2ec 100644 --- a/src/client/component/bots.cpp +++ b/src/client/component/bots.cpp @@ -144,7 +144,7 @@ namespace bots { bot_names_received = false; - game::netadr_s master{}; + game::netadr_t master{}; if (server_list::get_master_server(master)) { console::info("Getting bots...\n"); @@ -198,9 +198,9 @@ namespace bots parse_bot_names_from_file(); } - network::on("getbotsResponse", [](const game::netadr_s& target, const std::string& data) + network::on("getbotsResponse", [](const game::netadr_t& target, const std::string& data) { - game::netadr_s master{}; + game::netadr_t master{}; if (server_list::get_master_server(master) && !bot_names_received && target == master) { bot_names = utils::string::split(data, '\n'); diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp index 32127b6..4ecd32f 100644 --- a/src/client/component/command.cpp +++ b/src/client/component/command.cpp @@ -194,7 +194,7 @@ namespace command void add_raw(const char* name, void (*callback)()) { - game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate()); + game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate()); } void add(const char* name, const std::function& callback) @@ -318,7 +318,7 @@ namespace command } console::info("================================ COMMAND DUMP =====================================\n"); - game::cmd_function_s* cmd = *game::cmd_functions; + game::cmd_function_t* cmd = *game::cmd_functions; auto i = 0; while (cmd) { diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp index 03b951b..8734002 100644 --- a/src/client/component/dedicated.cpp +++ b/src/client/component/dedicated.cpp @@ -110,7 +110,7 @@ namespace dedicated return; } - game::netadr_s target{}; + game::netadr_t target{}; if (server_list::get_master_server(target)) { network::send(target, "heartbeat", "IW6"); diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 97960c1..32bd91f 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -71,7 +71,7 @@ namespace discord discord_presence.state = host_name; discord_presence.partyMax = party::server_client_count(); - std::hash hash_fn; + std::hash hash_fn; static const auto nonce = utils::cryptography::random::get_integer(); const auto& address = party::get_target(); diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp index 19450f2..bda4b71 100644 --- a/src/client/component/dvar_cheats.cpp +++ b/src/client/component/dvar_cheats.cpp @@ -110,7 +110,7 @@ namespace dvar_cheats a.jmp(0x1404F0D74); }); - void cg_set_client_dvar_from_server(const int local_client_num, game::mp::cg_s* cg, const char* dvar_id, const char* value) + void cg_set_client_dvar_from_server(const int local_client_num, game::mp::cg_t* cg, const char* dvar_id, const char* value) { if (dvar_id == "cg_fov"s || dvar_id == "com_maxfps"s) { diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp index 5b8fe91..be9f95c 100644 --- a/src/client/component/game_console.cpp +++ b/src/client/component/game_console.cpp @@ -674,7 +674,7 @@ namespace game_console } } - game::cmd_function_s* cmd = (*game::cmd_functions); + game::cmd_function_t* cmd = (*game::cmd_functions); while (cmd) { if (cmd->name) diff --git a/src/client/component/gameplay.cpp b/src/client/component/gameplay.cpp index db4c8da..73422e3 100644 --- a/src/client/component/gameplay.cpp +++ b/src/client/component/gameplay.cpp @@ -133,7 +133,7 @@ namespace gameplay return jump_height; } - void jump_apply_slowdown_stub(game::mp::playerState_s* ps) + void jump_apply_slowdown_stub(game::mp::playerState_t* ps) { assert(ps->pm_flags & game::PMF_JUMPING); @@ -163,7 +163,7 @@ namespace gameplay } } - float jump_get_slowdown_friction(game::mp::playerState_s* ps) + float jump_get_slowdown_friction(game::mp::playerState_t* ps) { assert(ps->pm_flags & game::PMF_JUMPING); assert(ps->pm_time <= game::JUMP_LAND_SLOWDOWN_TIME); @@ -181,7 +181,7 @@ namespace gameplay return 2.5f; } - float jump_reduce_friction_stub(game::mp::playerState_s* ps) + float jump_reduce_friction_stub(game::mp::playerState_t* ps) { float control; @@ -199,7 +199,7 @@ namespace gameplay return control; } - float jump_get_land_factor(game::mp::playerState_s* ps) + float jump_get_land_factor(game::mp::playerState_t* ps) { assert(ps->pm_flags & game::PMF_JUMPING); assert(ps->pm_time <= game::JUMP_LAND_SLOWDOWN_TIME); @@ -219,9 +219,9 @@ namespace gameplay void jump_start_stub(game::pmove_t* pm, game::pml_t* pml, float height) { - static_assert(offsetof(game::mp::playerState_s, groundEntityNum) == 0x70); - static_assert(offsetof(game::mp::playerState_s, pm_time) == 0x8); - static_assert(offsetof(game::mp::playerState_s, sprintState.sprintButtonUpRequired) == 0x240); + static_assert(offsetof(game::mp::playerState_t, groundEntityNum) == 0x70); + static_assert(offsetof(game::mp::playerState_t, pm_time) == 0x8); + static_assert(offsetof(game::mp::playerState_t, sprintState.sprintButtonUpRequired) == 0x240); static_assert(offsetof(game::pml_t, frametime) == 0x24); static_assert(offsetof(game::pml_t, walking) == 0x2C); static_assert(offsetof(game::pml_t, groundPlane) == 0x30); @@ -229,9 +229,9 @@ namespace gameplay float factor; float velocity_sqrd; - game::mp::playerState_s* ps; + game::mp::playerState_t* ps; - ps = static_cast(pm->ps); + ps = static_cast(pm->ps); assert(ps); @@ -295,7 +295,7 @@ namespace gameplay } } - void pm_weapon_use_ammo_stub(game::playerState_s* ps, game::Weapon weapon, + void pm_weapon_use_ammo_stub(game::playerState_t* ps, game::Weapon weapon, bool is_alternate, int amount, game::PlayerHandIndex hand) { if (!dvars::player_sustainAmmo->current.enabled) @@ -304,10 +304,10 @@ namespace gameplay } } - game::mp::gentity_s* weapon_rocket_launcher_fire_stub(game::mp::gentity_s* ent, game::Weapon weapon, float spread, game::weaponParms* wp, + game::mp::gentity_t* weapon_rocket_launcher_fire_stub(game::mp::gentity_t* ent, game::Weapon weapon, float spread, game::weaponParms* wp, const float* gun_vel, game::mp::missileFireParms* fire_parms, bool magic_bullet) { - auto* result = utils::hook::invoke(0x1403DB8A0, ent, weapon, spread, wp, gun_vel, fire_parms, magic_bullet); + auto* result = utils::hook::invoke(0x1403DB8A0, ent, weapon, spread, wp, gun_vel, fire_parms, magic_bullet); if (ent->client != nullptr && wp->weapDef->inventoryType != game::WEAPINVENTORY_EXCLUSIVE) { diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 0222a44..2ccf6a9 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -20,7 +20,7 @@ namespace network return callbacks; } - bool handle_command(game::netadr_s* address, const char* command, game::msg_t* message) + bool handle_command(game::netadr_t* address, const char* command, game::msg_t* message) { const auto cmd_string = utils::string::to_lower(command); auto& callbacks = get_callbacks(); @@ -61,7 +61,7 @@ namespace network a.jmp(0x1402C64EE); } - int net_compare_base_address(const game::netadr_s* a1, const game::netadr_s* a2) + int net_compare_base_address(const game::netadr_t* a1, const game::netadr_t* a2) { if (a1->type == a2->type) { @@ -83,12 +83,12 @@ namespace network return false; } - int net_compare_address(const game::netadr_s* a1, const game::netadr_s* a2) + int net_compare_address(const game::netadr_t* a1, const game::netadr_t* a2) { return net_compare_base_address(a1, a2) && a1->port == a2->port; } - void reconnect_migrated_client(game::mp::client_t*, game::netadr_s* from, const int, const int, const char*, const char*, bool) + void reconnect_migrated_client(game::mp::client_t*, game::netadr_t* from, const int, const int, const char*, const char*, bool) { // This happens when a client tries to rejoin after being recently disconnected, OR by a duplicated guid // We don't want this to do anything. It decides to crash seemingly randomly @@ -107,14 +107,14 @@ namespace network get_callbacks()[utils::string::to_lower(command)] = callback; } - int dw_send_to_stub(const int size, const char* src, game::netadr_s* addr) + int dw_send_to_stub(const int size, const char* src, game::netadr_t* addr) { sockaddr s = {}; game::NetadrToSockadr(addr, &s); return sendto(*game::query_socket, src, size, 0, &s, 16) >= 0; } - void send(const game::netadr_s& address, const std::string& command, const std::string& data, const char separator) + void send(const game::netadr_t& address, const std::string& command, const std::string& data, const char separator) { std::string packet = "\xFF\xFF\xFF\xFF"; packet.append(command); @@ -124,7 +124,7 @@ namespace network send_data(address, packet); } - void send_data(const game::netadr_s& address, const std::string& data) + void send_data(const game::netadr_t& address, const std::string& data) { if (address.type == game::NA_LOOPBACK) { @@ -136,12 +136,12 @@ namespace network } } - bool are_addresses_equal(const game::netadr_s& a, const game::netadr_s& b) + bool are_addresses_equal(const game::netadr_t& a, const game::netadr_t& b) { return net_compare_address(&a, &b); } - const char* net_adr_to_string(const game::netadr_s& a) + const char* net_adr_to_string(const game::netadr_t& a) { if (a.type == game::NA_LOOPBACK) { @@ -286,7 +286,7 @@ namespace network if (!game::environment::is_dedi()) { // we need this on the client for RCon - on("print", [](const game::netadr_s& address, const std::string& message) + on("print", [](const game::netadr_t& address, const std::string& message) { if (address != party::get_target()) { diff --git a/src/client/component/network.hpp b/src/client/component/network.hpp index 4e6ae49..03cc87f 100644 --- a/src/client/component/network.hpp +++ b/src/client/component/network.hpp @@ -2,23 +2,23 @@ namespace network { - using callback = std::function; + using callback = std::function; void on(const std::string& command, const callback& callback); - void send(const game::netadr_s& address, const std::string& command, const std::string& data = {}, char separator = ' '); - void send_data(const game::netadr_s& address, const std::string& data); + void send(const game::netadr_t& address, const std::string& command, const std::string& data = {}, char separator = ' '); + void send_data(const game::netadr_t& address, const std::string& data); - bool are_addresses_equal(const game::netadr_s& a, const game::netadr_s& b); + bool are_addresses_equal(const game::netadr_t& a, const game::netadr_t& b); - const char* net_adr_to_string(const game::netadr_s& a); + const char* net_adr_to_string(const game::netadr_t& a); } -inline bool operator==(const game::netadr_s& a, const game::netadr_s& b) +inline bool operator==(const game::netadr_t& a, const game::netadr_t& b) { return network::are_addresses_equal(a, b); // } -inline bool operator!=(const game::netadr_s& a, const game::netadr_s& b) +inline bool operator!=(const game::netadr_t& a, const game::netadr_t& b) { return !(a == b); // } @@ -26,20 +26,20 @@ inline bool operator!=(const game::netadr_s& a, const game::netadr_s& b) namespace std { template <> - struct equal_to + struct equal_to { using result_type = bool; - bool operator()(const game::netadr_s& lhs, const game::netadr_s& rhs) const + bool operator()(const game::netadr_t& lhs, const game::netadr_t& rhs) const { return network::are_addresses_equal(lhs, rhs); } }; template <> - struct hash + struct hash { - size_t operator()(const game::netadr_s& x) const noexcept + size_t operator()(const game::netadr_t& x) const noexcept { return hash()(*reinterpret_cast(&x.ip[0])) ^ hash()(x.port); } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index bc073e6..5266941 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -26,7 +26,7 @@ namespace party { struct { - game::netadr_s host{}; + game::netadr_t host{}; std::string challenge{}; bool hostDefined{false}; } connect_state; @@ -37,7 +37,7 @@ namespace party int sv_maxclients; - void connect_to_party(const game::netadr_s& target, const std::string& mapname, const std::string& gametype) + void connect_to_party(const game::netadr_t& target, const std::string& mapname, const std::string& gametype) { if (game::environment::is_sp()) { @@ -58,7 +58,7 @@ namespace party // CL_ConnectFromParty char session_info[0x100] = {}; - reinterpret_cast(0x1402C5700)( + reinterpret_cast(0x1402C5700)( 0, session_info, &target, mapname.data(), gametype.data()); } @@ -146,7 +146,7 @@ namespace party return count; } - game::netadr_s& get_target() + game::netadr_t& get_target() { return connect_state.host; } @@ -175,7 +175,7 @@ namespace party return -1; } - void connect(const game::netadr_s& target) + void connect(const game::netadr_t& target) { if (game::environment::is_sp()) { @@ -252,7 +252,7 @@ namespace party return; } - game::netadr_s target{}; + game::netadr_t target{}; if (game::NET_StringToAdr(params[1], &target)) { connect(target); @@ -405,7 +405,7 @@ namespace party console::info("%s\n", message.data()); }); - network::on("getInfo", [](const game::netadr_s& target, const std::string& data) + network::on("getInfo", [](const game::netadr_t& target, const std::string& data) { utils::info_string info; info.set("challenge", data); @@ -430,7 +430,7 @@ namespace party return; } - network::on("infoResponse", [](const game::netadr_s& target, const std::string& data) + network::on("infoResponse", [](const game::netadr_t& target, const std::string& data) { const utils::info_string info(data); server_list::handle_info_response(target, info); diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index fd32ce5..85d1a82 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -7,7 +7,7 @@ namespace party void reset_connect_state(); - void connect(const game::netadr_s& target); + void connect(const game::netadr_t& target); void map_restart(); [[nodiscard]] int server_client_count(); @@ -15,5 +15,5 @@ namespace party [[nodiscard]] int get_client_count(); [[nodiscard]] int get_bot_count(); - [[nodiscard]] game::netadr_s& get_target(); + [[nodiscard]] game::netadr_t& get_target(); } diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 59afda4..3afc21a 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -150,7 +150,7 @@ namespace patches return 0; } - game::Font_s* get_chat_font_handle() + game::Font_t* get_chat_font_handle() { return game::R_RegisterFont("fonts/bigFont"); } @@ -187,7 +187,7 @@ namespace patches } utils::hook::detour cmd_lui_notify_server_hook; - void cmd_lui_notify_server_stub(game::mp::gentity_s* ent) + void cmd_lui_notify_server_stub(game::mp::gentity_t* ent) { command::params_sv params{}; const auto menu_id = atoi(params.get(1)); diff --git a/src/client/component/rcon.cpp b/src/client/component/rcon.cpp index 52dff19..0371a68 100644 --- a/src/client/component/rcon.cpp +++ b/src/client/component/rcon.cpp @@ -14,11 +14,11 @@ namespace rcon namespace { bool is_redirecting_ = false; - game::netadr_s redirect_target_ = {}; + game::netadr_t redirect_target_ = {}; std::string redirect_buffer = {}; std::recursive_mutex redirect_lock; - void setup_redirect(const game::netadr_s& target) + void setup_redirect(const game::netadr_t& target) { std::lock_guard $(redirect_lock); @@ -92,7 +92,7 @@ namespace rcon if (*reinterpret_cast(0x1419E1AE0) >= 5) //clientUIActive.connectionState >= CA_CONNECTED { - const auto target = *reinterpret_cast(0x141CB535C); + const auto target = *reinterpret_cast(0x141CB535C); const auto buffer = password + " " + data; network::send(target, "rcon", buffer); } @@ -170,7 +170,7 @@ namespace rcon } else { - network::on("rcon", [](const game::netadr_s& addr, const std::string& data) + network::on("rcon", [](const game::netadr_t& addr, const std::string& data) { const auto pos = data.find_first_of(" "); if (pos == std::string::npos) diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp index b8abc61..34ea29f 100644 --- a/src/client/component/server_list.cpp +++ b/src/client/component/server_list.cpp @@ -33,14 +33,14 @@ namespace server_list std::string map_name; std::string game_type; char in_game; - game::netadr_s address; + game::netadr_t address; }; struct { - game::netadr_s address{}; + game::netadr_t address{}; volatile bool requesting = false; - std::unordered_map queued_servers{}; + std::unordered_map queued_servers{}; } master_state; volatile bool update_server_list = false; @@ -307,12 +307,12 @@ namespace server_list } } - bool get_master_server(game::netadr_s& address) + bool get_master_server(game::netadr_t& address) { return game::NET_StringToAdr("server.alterware.dev:20810", &address); } - void handle_info_response(const game::netadr_s& address, const utils::info_string& info) + void handle_info_response(const game::netadr_t& address, const utils::info_string& info) { int start_time{}; const auto now = game::Sys_Milliseconds(); @@ -436,7 +436,7 @@ namespace server_list scheduler::loop(do_frame_work, scheduler::pipeline::main); - network::on("getServersResponse", [](const game::netadr_s& target, const std::string& data) + network::on("getServersResponse", [](const game::netadr_t& target, const std::string& data) { { std::lock_guard _(mutex); @@ -469,7 +469,7 @@ namespace server_list break; } - game::netadr_s address{}; + game::netadr_t address{}; address.type = game::NA_IP; address.localNetID = game::NS_CLIENT1; std::memcpy(&address.ip[0], data.data() + i + 0, 4); diff --git a/src/client/component/server_list.hpp b/src/client/component/server_list.hpp index 09f62b2..76e260a 100644 --- a/src/client/component/server_list.hpp +++ b/src/client/component/server_list.hpp @@ -3,8 +3,8 @@ namespace server_list { - bool get_master_server(game::netadr_s& address); - void handle_info_response(const game::netadr_s& address, const utils::info_string& info); + bool get_master_server(game::netadr_t& address); + void handle_info_response(const game::netadr_t& address, const utils::info_string& info); bool sl_key_event(int key, int down); } diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 87e6847..d097247 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -181,7 +181,7 @@ namespace game static_assert(sizeof(db_internal_state) == 0x20); - struct db_z_stream_s + struct db_z_stream_t { unsigned char* next_in; unsigned int avail_in; @@ -216,7 +216,7 @@ namespace game unsigned char* start_in; _OVERLAPPED overlapped; unsigned int readError; - db_z_stream_s stream; + db_z_stream_t stream; int readingResident; }; @@ -1064,7 +1064,7 @@ namespace game float t1; }; - struct Font_s + struct Font_t { const char* fontName; int pixelHeight; @@ -1182,9 +1182,9 @@ namespace game const char** argv[8]; }; - struct cmd_function_s + struct cmd_function_t { - cmd_function_s* next; + cmd_function_t* next; const char* name; void (__cdecl* function)(); }; @@ -1235,7 +1235,7 @@ namespace game WEAPON_HAND_DEFAULT = 0x0, }; - struct usercmd_s + struct usercmd_t { int serverTime; unsigned int buttons; @@ -1311,7 +1311,7 @@ namespace game bool hybridScope; }; - struct compressedAnimData_s + struct compressedAnimData_t { int flags; int animRate; @@ -1330,7 +1330,7 @@ namespace game int flags; int startTime; float startPosition[3]; - compressedAnimData_s compressedAnimData; + compressedAnimData_t compressedAnimData; }; struct SlideState @@ -1339,7 +1339,7 @@ namespace game int noFricTime; }; - struct SprintState_s + struct SprintState_t { int sprintButtonUpRequired; int sprintDelay; @@ -1398,7 +1398,7 @@ namespace game NS_INVALID_NETSRC = 0x4, }; - struct netadr_s + struct netadr_t { netadrtype_t type; unsigned char ip[4]; @@ -1463,7 +1463,7 @@ namespace game unsigned int rgba; }; - struct hudelem_s + struct hudelem_t { he_type_t type; float x; @@ -1509,9 +1509,9 @@ namespace game int flags; }; - struct game_hudelem_s + struct game_hudelem_t { - hudelem_s elem; + hudelem_t elem; int clientNum; int team; int archived; @@ -1939,7 +1939,7 @@ namespace game FxWorld *fxWorld; GfxWorld *gfxWorld; GfxLightDef *lightDef;*/ - Font_s* font; + Font_t* font; /*MenuList *menuList; menuDef_t *menu; AnimationClass *animClass; @@ -2018,21 +2018,21 @@ namespace game namespace sp { // very shit structures for the moment cuz i cba mapping the whole thing out right now... - struct gclient_s + struct gclient_t { char __0x00[0xB6DC]; int flags; }; - struct gentity_s + struct gentity_t { char __0x00[0x110]; - gclient_s* client; + gclient_t* client; char __0x118[0x4C]; int flags; }; - struct playerState_s + struct playerState_t { }; @@ -2049,7 +2049,7 @@ namespace game namespace mp { #pragma pack(push, 4) - struct playerState_s + struct playerState_t { int commandTime; int pm_type; @@ -2101,7 +2101,7 @@ namespace game vec3_t viewangles; char _0x190[0xAC]; int locationSelectionInfo; - SprintState_s sprintState; + SprintState_t sprintState; float holdBreathScale; int holdBreathTimer; float moveSpeedScaleMultiplier; @@ -2179,7 +2179,7 @@ namespace game TEAM_NUM_TEAMS = 0x4, }; - struct clientState_s + struct clientState_t { int clientIndex; team_t team; @@ -2203,21 +2203,21 @@ namespace game int killCamEntity; int isInKillcam; char __0x332C[0x14]; - usercmd_s cmd; - usercmd_s oldcmd; + usercmd_t cmd; + usercmd_t oldcmd; int localClient; char __0x33C4[0x24]; float moveSpeedScaleMultiplier; int viewmodelIndex; int noSpectate; - clientState_s cs; + clientState_t cs; ScoreInfo scores; char __pad[0x1B0]; }; - struct gclient_s + struct gclient_t { - playerState_s ps; + playerState_t ps; clientSession_t sess; int flags; int spectatorClient; @@ -2266,11 +2266,11 @@ namespace game char _0x14C[0xC]; }; - struct gentity_s + struct gentity_t { EntityState s; EntityShared r; - gclient_s* client; + gclient_t* client; char _0x170[0x50]; int flags; char _0x1C4[0x18]; @@ -2279,7 +2279,7 @@ namespace game char _0x1E4[0x10C]; }; - struct snapshot_s + struct snapshot_t { char _0x0[0x3C78]; int ping; @@ -2288,12 +2288,12 @@ namespace game int numClients; }; - struct cg_s + struct cg_t { - playerState_s ps; + playerState_t ps; char _0x3324[0x64]; - snapshot_s* snap; - snapshot_s* nextSnap; // theres alot after this cba rn + snapshot_t* snap; + snapshot_t* nextSnap; // theres alot after this cba rn }; struct netProfilePacket_t @@ -2328,7 +2328,7 @@ namespace game netsrc_t sock; int dropped; int incomingSequence; - netadr_s remoteAddress; + netadr_t remoteAddress; int fragmentSequence; int fragmentLength; char* fragmentBuffer; @@ -2378,7 +2378,7 @@ namespace game int reliableSent; int messageAcknowledge; char _0xC30[0x41238]; - gentity_s* gentity; + gentity_t* gentity; char name[16]; int lastPacketTime; int lastConnectTime; @@ -2411,7 +2411,7 @@ namespace game struct missileFireParms { - gentity_s* target; + gentity_t* target; float targetPosOrOffset[3]; float autoDetonateTime; bool lockon; @@ -2423,10 +2423,10 @@ namespace game static_assert(sizeof(mp::client_t) == 0x83570); - union playerState_s + union playerState_t { - sp::playerState_s* sp; - mp::playerState_s* mp; + sp::playerState_t* sp; + mp::playerState_t* mp; }; enum GfxDrawSceneMethod @@ -2457,7 +2457,7 @@ namespace game TECHNIQUE_NONE = 0x137, }; - struct GfxDrawMethod_s + struct GfxDrawMethod_t { int drawScene; int baseTechType; @@ -2509,8 +2509,8 @@ namespace game struct pmove_t { void* ps; - usercmd_s cmd; - usercmd_s oldcmd; + usercmd_t cmd; + usercmd_t oldcmd; int tracemask; int numtouch; int touchents[32]; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 69b3007..770cb0b 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -39,9 +39,9 @@ namespace game WEAK symbol CL_IsCgameInitialized{0x140234DA0, 0x1402B9A70}; WEAK symbol CG_GameMessage{0x1401F2E20, 0x140271320}; - WEAK symbol CG_SetClientDvarFromServer{0x0, 0x14028A2C0}; + WEAK symbol CG_SetClientDvarFromServer{0x0, 0x14028A2C0}; - WEAK symbol Cmd_AddCommandInternal{0x1403B3570, 0x1403F7070}; + WEAK symbol Cmd_AddCommandInternal{0x1403B3570, 0x1403F7070}; WEAK symbol Cmd_ExecuteSingleCommand{0x1403B3B10, 0x1403F7680}; WEAK symbol DB_EnumXAssets_FastFile{0x140271F50, 0x14031EF90}; @@ -94,17 +94,17 @@ namespace game WEAK symbol FS_AddLocalizedGameDirectory{0x14041A2F0, 0x1404DC760}; WEAK symbol G_FindItem{0x140462490, 0x14021B7E0}; - WEAK symbol G_GivePlayerWeapon{0x140359E20, 0x1403DA5E0}; + WEAK symbol G_GivePlayerWeapon{0x140359E20, 0x1403DA5E0}; WEAK symbol G_GetWeaponForName{0x140359890, 0x1403DA060}; WEAK symbol G_Glass_Update{0x14030E680, 0x140397450}; - WEAK symbol G_InitializeAmmo{0x140311F00, 0x14039AEA0}; + WEAK symbol G_InitializeAmmo{0x140311F00, 0x14039AEA0}; WEAK symbol G_SelectWeapon{0x14035A200, 0x1403DA880}; - WEAK symbol G_TakePlayerWeapon{0x14035A350, 0x1403DA9C0}; + WEAK symbol G_TakePlayerWeapon{0x14035A350, 0x1403DA9C0}; WEAK symbol G_FindConfigstringIndex{0x0, 0x140161F90}; WEAK symbol G_RunFrame{0x0, 0x1403A05E0}; - WEAK symbol HudElem_Alloc{0x0, 0x1403997E0}; + WEAK symbol HudElem_Alloc{0x0, 0x1403997E0}; WEAK symbol I_CleanStr{0x140432460, 0x1404F63C0}; @@ -144,18 +144,18 @@ namespace game WEAK symbol Material_RegisterHandle{0x140523D90, 0x1405F0E20}; - WEAK symbol NET_OutOfBandPrint{0, 0x14041D5C0}; - WEAK symbol NET_SendLoopPacket{0, 0x14041D780}; - WEAK symbol NET_StringToAdr{0, 0x14041D870}; - WEAK symbol NetadrToSockadr{0, 0x1404E53D0}; + WEAK symbol NET_OutOfBandPrint{0, 0x14041D5C0}; + WEAK symbol NET_SendLoopPacket{0, 0x14041D780}; + WEAK symbol NET_StringToAdr{0, 0x14041D870}; + WEAK symbol NetadrToSockadr{0, 0x1404E53D0}; WEAK symbol R_AddCmdDrawStretchPic{0x140234460, 0x140600BE0}; - WEAK symbol R_AddCmdDrawText{0x140533E40, 0x140601070}; - WEAK symbol R_AddCmdDrawTextWithCursor{0x140534170, 0x1406013A0}; - WEAK symbol R_RegisterFont{0x1405130B0, 0x1405DFAC0}; + WEAK symbol R_AddCmdDrawText{0x140533E40, 0x140601070}; + WEAK symbol R_AddCmdDrawTextWithCursor{0x140534170, 0x1406013A0}; + WEAK symbol R_RegisterFont{0x1405130B0, 0x1405DFAC0}; WEAK symbol R_SyncRenderThread{0x140535AF0, 0x140602D30}; - WEAK symbol R_TextWidth{0x140513390, 0x1405DFDB0}; + WEAK symbol R_TextWidth{0x140513390, 0x1405DFDB0}; WEAK symbol ScrPlace_GetViewPlacement{0x14024D150, 0x1402F6D40}; @@ -203,20 +203,20 @@ namespace game WEAK symbol SV_Cmd_EndTokenizedString{0, 0x1403F8110}; WEAK symbol SV_MatchEnd{0x0, 0x14047A090}; - WEAK symbol SV_DirectConnect{0, 0x140471390}; + WEAK symbol SV_DirectConnect{0, 0x140471390}; WEAK symbol SV_GameSendServerCommand{0x140490F40, 0x1404758C0}; WEAK symbol SV_Loaded{0x140491820, 0x1404770C0}; WEAK symbol SV_StartMap{0, 0x140470170}; WEAK symbol SV_StartMapForParty{0, 0x1404702F0}; - WEAK symbol SV_AddBot{0, 0x140470920}; + WEAK symbol SV_AddBot{0, 0x140470920}; WEAK symbol SV_BotIsBot{0, 0x140461340}; WEAK symbol SV_BotGetRandomName{0, 0x140460B80}; - WEAK symbol SV_SpawnTestClient{0, 0x1404740A0}; + WEAK symbol SV_SpawnTestClient{0, 0x1404740A0}; WEAK symbol SV_ExecuteClientCommand{0, 0x140472430}; WEAK symbol SV_FastRestart{0x14048B890, 0x14046F440}; - WEAK symbol SV_GetPlayerstateForClientNum{0x140490F80, 0x140475A10}; + WEAK symbol SV_GetPlayerstateForClientNum{0x140490F80, 0x140475A10}; WEAK symbol SV_GetGuid{0, 0x140475990}; WEAK symbol SV_KickClientNum{0, 0x14046F730}; WEAK symbol SV_SetConfigstring{0, 0x140477450}; @@ -225,7 +225,7 @@ namespace game WEAK symbol Sys_IsDatabaseReady2{0x1403C2D40, 0x140423920}; WEAK symbol Sys_Milliseconds{0x14043D2A0, 0x140501CA0}; WEAK symbol Sys_ShowConsole{0x14043E650, 0x140503130}; - WEAK symbol Sys_SendPacket{0x14043D000, 0x140501A00}; + WEAK symbol Sys_SendPacket{0x14043D000, 0x140501A00}; WEAK symbol Sys_GetValue{0x1403C2C30, 0x1404237D0}; WEAK symbol Sys_IsMainThread{0x1478FC470, 0x140423950}; WEAK symbol Sys_CreateFile{0x140434B10, 0x1404F8FD0}; @@ -257,7 +257,7 @@ namespace game WEAK symbol cmd_args{0x144CE7F70, 0x144518480}; WEAK symbol sv_cmd_args{0x144CE8020, 0x144518530}; - WEAK symbol cmd_functions{0x144CE80C8, 0x1445185D8}; + WEAK symbol cmd_functions{0x144CE80C8, 0x1445185D8}; WEAK symbol dvarCount{0x1458CBA3C, 0x1478EADF4}; WEAK symbol sortedDvars{0x1458CBA60, 0x1478EAE10}; @@ -300,20 +300,20 @@ namespace game WEAK symbol threadIds{0x144DE6640, 0x1446B4960}; - WEAK symbol gfxDrawMethod{0x145F525A8, 0x1480350D8}; + WEAK symbol gfxDrawMethod{0x145F525A8, 0x1480350D8}; namespace sp { - WEAK symbol g_entities{0x143C91600, 0}; + WEAK symbol g_entities{0x143C91600, 0}; WEAK symbol g_zones_0{0x1434892D8, 0}; } namespace mp { - WEAK symbol cgArray{0, 0x14176EC00}; + WEAK symbol cgArray{0, 0x14176EC00}; - WEAK symbol g_entities{0, 0x14427A0E0}; + WEAK symbol g_entities{0, 0x14427A0E0}; WEAK symbol svs_clientCount{0, 0x14647B28C}; WEAK symbol svs_clients{0, 0x14647B290}; -- 2.34.1 From 27d1f617890f1a158b7b186754a2f8ffe8da337b Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:13:16 +0100 Subject: [PATCH 02/14] Added hook and fn stub to clean color from agent overhead name. --- src/client/component/colors.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/component/colors.cpp b/src/client/component/colors.cpp index 7912ce5..d84818f 100644 --- a/src/client/component/colors.cpp +++ b/src/client/component/colors.cpp @@ -98,6 +98,16 @@ namespace colors return result; } + int clean_agent_name_stub(char* out, int max_size, const char* fmt, const char* in) + { + // format agent overhead name like [%s] + const auto length = sprintf_s(out, max_size, fmt, in); + + utils::string::strip(in, out, std::min(static_cast(length), static_cast(max_size))); + + return length; + } + void rb_lookup_color_stub(const char index, DWORD* color) { *color = RGB(255, 255, 255); @@ -143,6 +153,9 @@ namespace colors // don't apply colors to overhead names utils::hook::call(0x14025CE79, get_client_name_stub); + // don't apply colors to overhead names of agents (like dogs or juggernauts) + utils::hook::call(0x1402CF760, clean_agent_name_stub); + // patch I_CleanStr utils::hook::jump(0x1404F63C0, i_clean_str_stub); } -- 2.34.1 From adc33983a8d6a9d889bf8d1982d8580f66938f37 Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:15:46 +0100 Subject: [PATCH 03/14] Improved naming for function 'get_client_name_stub'. --- src/client/component/colors.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/component/colors.cpp b/src/client/component/colors.cpp index d84818f..007d141 100644 --- a/src/client/component/colors.cpp +++ b/src/client/component/colors.cpp @@ -86,14 +86,14 @@ namespace colors return string; } - size_t get_client_name_stub(const int local_client_num, const int index, char* buf, const int size, - const size_t unk, const size_t unk2) + size_t get_client_name_stub(const int local_client_num, const int index, char* name_buf, + const int max_name_size, char* clan_buf, const int max_clan_size) { - // CL_GetClientName (CL_GetClientNameAndClantag?) - const auto result = reinterpret_cast(0x1402CF790)( - local_client_num, index, buf, size, unk, unk2); + // CL_GetClientNameAndClanTag (wrapper for CL_GetClientNameAndClanTagColorize) + const auto result = reinterpret_cast(0x1402CF790)( + local_client_num, index, name_buf, max_name_size, clan_buf, max_clan_size); - utils::string::strip(buf, buf, size); + utils::string::strip(name_buf, name_buf, static_cast(max_name_size)); return result; } -- 2.34.1 From e5a547650c5c2b5dba7976169eef6a3eb44f0ea9 Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:21:50 +0100 Subject: [PATCH 04/14] Added helper function to strip colors from strings. --- src/common/utils/string.cpp | 26 ++++++++++++++++++++++++++ src/common/utils/string.hpp | 2 ++ 2 files changed, 28 insertions(+) diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 6236584..2e0bbad 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -140,6 +140,32 @@ namespace utils::string *out = '\0'; } + std::string strip(std::string_view sv, bool strip_default_color) + { + std::string in(sv); + + for (std::size_t i = 0; i + 1 < in.size();) + { + if (in[i] == '^' && static_cast(in[i + 1] - '0') < 0xC) + { + if (in[i + 1] != '7' || strip_default_color) + { + in.erase(in.begin() + i, in.begin() + i + 2); + } + else + { + i += 2; + } + } + else + { + i += 1; + } + } + + return in; + } + std::string convert(const std::wstring& wstr) { std::string result; diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index 6a50c3e..285bc77 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -1,6 +1,7 @@ #pragma once #include "memory.hpp" #include +#include template constexpr auto ARRAY_COUNT(Type(&)[n]) { return n; } @@ -91,6 +92,7 @@ namespace utils::string std::string get_clipboard_data(); void strip(const char* in, char* out, size_t max); + std::string strip(std::string_view sv, bool strip_default_color = false); std::string convert(const std::wstring& wstr); std::wstring convert(const std::string& str); -- 2.34.1 From 50aa4097bb34c245241fdbee5b69b9f641c43945 Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:22:34 +0100 Subject: [PATCH 05/14] Fixed two bugs in the old color strip function. --- src/common/utils/string.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 2e0bbad..496cad3 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -115,13 +115,13 @@ namespace utils::string void strip(const char* in, char* out, size_t max) { - if (!in || !out) return; + if (!in || !out || !max) return; max--; auto current = 0u; while (*in != 0 && current < max) { - const auto color_index = (*(in + 1) - 48) >= 0xC ? 7 : (*(in + 1) - 48); + const auto color_index = (static_cast(*(in + 1) - 48)) >= 0xC ? 7 : (*(in + 1) - 48); if (*in == '^' && (color_index != 7 || *(in + 1) == '7')) { -- 2.34.1 From 51e831cbfec54299ca80b6eb6b3bea8edd1e74be Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:26:29 +0100 Subject: [PATCH 06/14] Added helper util functions for (de)compression. --- src/common/utils/compression.cpp | 80 ++++++++++++++++++++++++++++++++ src/common/utils/compression.hpp | 5 ++ 2 files changed, 85 insertions(+) diff --git a/src/common/utils/compression.cpp b/src/common/utils/compression.cpp index 1688cee..9c50a4a 100644 --- a/src/common/utils/compression.cpp +++ b/src/common/utils/compression.cpp @@ -109,6 +109,86 @@ namespace utils::compression result.resize(length); return result; } + + std::vector decompress(std::span input, std::size_t exp_output_size, double growth_rate) + { + auto decompress_internal = [](std::uint8_t* output, uLong* output_size, const std::uint8_t* input, uLong* input_size) + { + return uncompress2( + reinterpret_cast(output), output_size, + reinterpret_cast(input), input_size + ); + }; + + auto output_size = static_cast(std::max(exp_output_size, std::size_t(256))); + std::vector output(output_size); + + auto input_size = static_cast(input.size()); + auto status = decompress_internal(output.data(), &output_size, input.data(), &input_size); + + if (status == Z_BUF_ERROR) + { + const auto decompression_ratio = (input_size / static_cast(output.size())) - 0.04; + output_size = static_cast(std::max(input.size() / decompression_ratio, static_cast(output_size))); + output.resize(output_size); + + input_size = static_cast(input.size()); + status = decompress_internal(output.data(), &output_size, input.data(), &input_size); + } + + for (growth_rate = std::max(growth_rate, 1.0); status == Z_BUF_ERROR;) + { + output_size = 4 + static_cast(growth_rate * output.size()); + output.resize(output_size); + + input_size = static_cast(input.size()); + status = decompress_internal(output.data(), &output_size, input.data(), &input_size); + } + + if (status != Z_OK) + { + output.clear(); + return output; + } + + output.resize(output_size); + return output; + } + + std::vector compress(std::span input, std::size_t exp_output_size, double growth_rate) + { + auto compress_internal = [](std::uint8_t* output, uLong* output_size, const std::uint8_t* input, uLong input_size) + { + return compress2( + reinterpret_cast(output), output_size, + reinterpret_cast(input), input_size, + Z_BEST_COMPRESSION + ); + }; + + auto output_size = static_cast(std::max(exp_output_size, std::size_t(256))); + std::vector output(output_size); + + const auto input_size = static_cast(input.size()); + auto status = compress_internal(output.data(), &output_size, input.data(), input_size); + + for (growth_rate = std::max(growth_rate, 1.0); status == Z_BUF_ERROR;) + { + output_size = 4 + static_cast(growth_rate * output.size()); + output.resize(output_size); + + status = compress_internal(output.data(), &output_size, input.data(), input_size); + } + + if (status != Z_OK) + { + output.clear(); + return output; + } + + output.resize(output_size); + return output; + } } namespace zip diff --git a/src/common/utils/compression.hpp b/src/common/utils/compression.hpp index dfe36ad..d1a2a37 100644 --- a/src/common/utils/compression.hpp +++ b/src/common/utils/compression.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #define CHUNK 16384u @@ -11,6 +13,9 @@ namespace utils::compression { std::string compress(const std::string& data); std::string decompress(const std::string& data); + + std::vector compress(std::span input, std::size_t exp_output_size, double growth_rate = 2.0); + std::vector decompress(std::span input, std::size_t exp_output_size, double growth_rate = 2.0); } namespace zip -- 2.34.1 From 1db89c64c666805d8332046c0b0d6961531b537a Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:28:21 +0100 Subject: [PATCH 07/14] Added functions to parse the command line arguments. --- src/common/utils/command_line.cpp | 35 +++++++++++++++++++++++++++++++ src/common/utils/command_line.hpp | 9 ++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/common/utils/command_line.cpp create mode 100644 src/common/utils/command_line.hpp diff --git a/src/common/utils/command_line.cpp b/src/common/utils/command_line.cpp new file mode 100644 index 0000000..c07d513 --- /dev/null +++ b/src/common/utils/command_line.cpp @@ -0,0 +1,35 @@ +#include "command_line.hpp" +#include "nt.hpp" + +#include +#include + +namespace utils::command_line +{ + template + constexpr std::unique_ptr make_unique_ptr(type* ptr, deleter&& del) + { + return std::unique_ptr(ptr, std::forward(del)); + } + + const std::vector& get_args() + { + static const auto args = []() + { + auto argc = 0; + const auto cmd_line = winrt::check_pointer(::GetCommandLineW()); + const auto up = make_unique_ptr(winrt::check_pointer(::CommandLineToArgvW(cmd_line, &argc)), ::LocalFree); + const auto argv = up.get(); + + std::vector vec(argc); + std::transform(argv, argv + argc, vec.begin(), [](const auto* path) + { + return std::filesystem::path(path); + }); + + return vec; + }(); + + return args; + } +} diff --git a/src/common/utils/command_line.hpp b/src/common/utils/command_line.hpp new file mode 100644 index 0000000..b1b13b0 --- /dev/null +++ b/src/common/utils/command_line.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace utils::command_line +{ + [[nodiscard]] const std::vector& get_args(); +} \ No newline at end of file -- 2.34.1 From 2516a48fc18ba2c6e69a113e2f057b7bab192f7d Mon Sep 17 00:00:00 2001 From: Caball Date: Mon, 30 Dec 2024 23:35:17 +0100 Subject: [PATCH 08/14] Added fallback logic for current path / working directory. --- src/client/main.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index e50f1cc..0581a00 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -4,6 +4,7 @@ #include "loader/component_loader.hpp" #include "game/game.hpp" +#include "utils/command_line.hpp" #include #include #include @@ -167,8 +168,23 @@ FARPROC load_binary(const launcher::mode mode) std::string data; if (!utils::io::read_file(binary, &data)) { - throw std::runtime_error( - "Failed to read game binary! Please select the correct path in the launcher settings."); + // Check the first argument to see if the current directory needs to changed + // Required when the game is used to open a file (like a demo file) + const auto& args = utils::command_line::get_args(); + if (!args.empty()) + { + const auto& binary_dir = args.front(); + if (binary_dir.filename().string().ends_with("iw6-mod.exe")) + { + std::filesystem::current_path(binary_dir.parent_path()); + } + } + + if (!utils::io::read_file(binary, &data)) + { + throw std::runtime_error( + "Failed to read game binary! Please select the correct path in the launcher settings."); + } } #ifdef INJECT_HOST_AS_LIB -- 2.34.1 From 3553cd49ff2c5bfd79fdc275e68e89b4bb804867 Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:01:31 +0100 Subject: [PATCH 09/14] Updated game structs, variable symbols and references. --- src/client/component/bots.cpp | 8 +- src/client/component/dedicated_info.cpp | 2 +- src/client/component/discord.cpp | 6 +- src/client/component/game_console.cpp | 2 +- src/client/component/notifies.cpp | 2 +- src/client/component/party.cpp | 20 +- src/client/component/patches.cpp | 2 +- src/client/component/rcon.cpp | 2 +- src/client/component/slowmotion.cpp | 4 +- src/client/game/structs.hpp | 459 ++++++++++++++++++++++-- src/client/game/symbols.hpp | 19 +- 11 files changed, 468 insertions(+), 58 deletions(-) diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp index dd4f2ec..7183809 100644 --- a/src/client/component/bots.cpp +++ b/src/client/component/bots.cpp @@ -21,7 +21,7 @@ namespace bots bool can_add() { - return party::get_client_count() < *game::mp::svs_clientCount; + return party::get_client_count() < game::mp::svs->clientCount; } void bot_team_join(const int entity_num) @@ -29,14 +29,14 @@ namespace bots // schedule the select team call scheduler::once([entity_num]() { - game::SV_ExecuteClientCommand(&game::mp::svs_clients[entity_num], + game::SV_ExecuteClientCommand(&game::mp::svs->clients[entity_num], utils::string::va("lui 68 2 %i", *game::mp::sv_serverId_value), false); // scheduler the select class call scheduler::once([entity_num]() { - game::SV_ExecuteClientCommand(&game::mp::svs_clients[entity_num], + game::SV_ExecuteClientCommand(&game::mp::svs->clients[entity_num], utils::string::va("lui 5 %i %i", (rand() % 5) + 10, *game::mp::sv_serverId_value), false); }, scheduler::pipeline::server, 1s); @@ -175,7 +175,7 @@ namespace bots num_bots = std::strtoul(params.get(1), nullptr, 10); } - num_bots = std::min(num_bots, static_cast(*game::mp::svs_clientCount)); + num_bots = std::min(num_bots, static_cast(game::mp::svs->clientCount)); console::info("Spawning %zu %s\n", num_bots, (num_bots == 1 ? "bot" : "bots")); diff --git a/src/client/component/dedicated_info.cpp b/src/client/component/dedicated_info.cpp index 9f0de69..de3f8aa 100644 --- a/src/client/component/dedicated_info.cpp +++ b/src/client/component/dedicated_info.cpp @@ -41,7 +41,7 @@ namespace dedicated_info for (auto i = 0; i < sv_maxclients->current.integer; i++) { - auto* client = &game::mp::svs_clients[i]; + auto* client = &game::mp::svs->clients[i]; auto* self = &game::mp::g_entities[i]; if (client->header.state > game::CS_FREE && self && self->client) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 32bd91f..1a9cfc4 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -54,8 +54,8 @@ namespace discord discord_presence.details = utils::string::va("%s on %s", gametype, map); - discord_presence.partySize = game::mp::cgArray->snap != nullptr - ? game::mp::cgArray->snap->numClients + discord_presence.partySize = game::mp::cg->snap != nullptr + ? game::mp::cg->snap->numClients : 1; if (game::Dvar_GetBool("xblive_privatematch")) @@ -65,7 +65,7 @@ namespace discord } else { - auto* host_name = reinterpret_cast(0x14187EBC4); + auto* host_name = game::mp::cgs->szHostName; utils::string::strip(host_name, host_name, std::strlen(host_name) + 1); discord_presence.state = host_name; diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp index be9f95c..550ed9d 100644 --- a/src/client/component/game_console.cpp +++ b/src/client/component/game_console.cpp @@ -505,7 +505,7 @@ namespace game_console { if (key == game::keyNum_t::K_F10) { - if(game::mp::svs_clients[localClientNum].header.state > game::CS_FREE) + if(game::mp::svs->clients[localClientNum].header.state > game::CS_FREE) { return false; } diff --git a/src/client/component/notifies.cpp b/src/client/component/notifies.cpp index 72f35ff..5d018f7 100644 --- a/src/client/component/notifies.cpp +++ b/src/client/component/notifies.cpp @@ -61,7 +61,7 @@ namespace notifies scheduler::once([params, message, client_num] { const auto* guid = game::SV_GetGuid(client_num); - const auto* name = game::mp::svs_clients[client_num].name; + const auto* name = game::mp::svs->clients[client_num].name; game_log::g_log_printf("%s;%s;%i;%s;%s\n", params.get(0), diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 5266941..5316622 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -120,9 +120,9 @@ namespace party int get_client_count() { auto count = 0; - for (auto i = 0; i < *game::mp::svs_clientCount; ++i) + for (auto i = 0; i < game::mp::svs->clientCount; ++i) { - if (game::mp::svs_clients[i].header.state >= game::CS_CONNECTED) + if (game::mp::svs->clients[i].header.state >= game::CS_CONNECTED) { ++count; } @@ -134,10 +134,10 @@ namespace party int get_bot_count() { auto count = 0; - for (auto i = 0; i < *game::mp::svs_clientCount; ++i) + for (auto i = 0; i < game::mp::svs->clientCount; ++i) { - if (game::mp::svs_clients[i].header.state >= game::CS_CONNECTED && - game::mp::svs_clients[i].testClient != game::TC_NONE) + if (game::mp::svs->clients[i].header.state >= game::CS_CONNECTED && + game::mp::svs->clients[i].testClient != game::TC_NONE) { ++count; } @@ -158,7 +158,7 @@ namespace party int get_client_num_from_name(const std::string& name) { - for (auto i = 0; !name.empty() && i < *game::mp::svs_clientCount; ++i) + for (auto i = 0; !name.empty() && i < game::mp::svs->clientCount; ++i) { if (game::mp::g_entities[i].client) { @@ -283,7 +283,7 @@ namespace party } const auto client_num = atoi(params.get(1)); - if (client_num < 0 || client_num >= *game::mp::svs_clientCount) + if (client_num < 0 || client_num >= game::mp::svs->clientCount) { return; } @@ -320,7 +320,7 @@ namespace party const std::string name = params.get(1); if (name == "all"s) { - for (auto i = 0; i < *game::mp::svs_clientCount; ++i) + for (auto i = 0; i < game::mp::svs->clientCount; ++i) { scheduler::once([i, reason] { @@ -331,7 +331,7 @@ namespace party } const auto client_num = get_client_num_from_name(name); - if (client_num < 0 || client_num >= *game::mp::svs_clientCount) + if (client_num < 0 || client_num >= game::mp::svs->clientCount) { return; } @@ -418,7 +418,7 @@ namespace party info.set("isPrivate", dvars::get_string("g_password").empty() ? "0" : "1"); info.set("clients", std::to_string(get_client_count())); info.set("bots", std::to_string(get_bot_count())); - info.set("sv_maxclients", std::to_string(*game::mp::svs_clientCount)); + info.set("sv_maxclients", std::to_string(game::mp::svs->clientCount)); info.set("protocol", std::to_string(PROTOCOL)); info.set("shortversion", SHORTVERSION); diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 3afc21a..a1c7ced 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -191,7 +191,7 @@ namespace patches { command::params_sv params{}; const auto menu_id = atoi(params.get(1)); - const auto client = &game::mp::svs_clients[ent->s.clientNum]; + const auto client = &game::mp::svs->clients[ent->s.clientNum]; // 9 => "end_game" if (menu_id == 9 && client->header.netchan.remoteAddress.type != game::NA_LOOPBACK) diff --git a/src/client/component/rcon.cpp b/src/client/component/rcon.cpp index 0371a68..3221898 100644 --- a/src/client/component/rcon.cpp +++ b/src/client/component/rcon.cpp @@ -50,7 +50,7 @@ namespace rcon for (int i = 0; i < sv_maxclients->current.integer; i++) { - const auto client = &game::mp::svs_clients[i]; + const auto client = &game::mp::svs->clients[i]; auto self = &game::mp::g_entities[i]; char clean_name[32]{}; diff --git a/src/client/component/slowmotion.cpp b/src/client/component/slowmotion.cpp index d688083..dac5286 100644 --- a/src/client/component/slowmotion.cpp +++ b/src/client/component/slowmotion.cpp @@ -50,8 +50,8 @@ namespace slowmotion for (auto i = 0; i < game::Dvar_FindVar("sv_maxclients")->current.integer; i++) { - auto* client = &game::mp::svs_clients[i]; - client->nextSnapshotTime = *game::mp::serverTime - 1; + auto* client = &game::mp::svs->clients[i]; + client->nextSnapshotTime = game::mp::svs->time - 1; } } } diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index d097247..a05684c 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -792,6 +792,22 @@ namespace game enum ConfigString { + CS_TIMESCALE = 0xA, + + CS_VISIONSET_NORMAL = 0x49C, + CS_VISIONSET_NIGHT = 0x49D, + CS_VISIONSET_MISSILECAM = 0x49E, + CS_VISIONSET_THERMAL = 0x49F, + CS_VISIONSET_PAIN = 0x4A0, + CS_VISIONSET_POST_APPLY = 0x4A1, + + CS_VISIONSET_FIRST = CS_VISIONSET_NORMAL, + CS_VISIONSET_LAST = CS_VISIONSET_POST_APPLY, + CS_VISIONSET_COUNT = CS_VISIONSET_POST_APPLY - CS_VISIONSET_NORMAL + 1 + + // many of the indices below are incorrect + + /* CS_FIRST = 0x0, CS_SERVERINFO = 0x0, CS_SYSTEMINFO = 0x1, @@ -863,7 +879,8 @@ namespace game CS_MP_ANIMS_LAST = 0xD21, CS_ITEMS = 0xD22, CS_LEADERBOARDS = 0xD23, - MAX_CONFIGSTRINGS = 0xD24, + MAX_CONFIGSTRINGS = 0xD24 + */ }; enum LiveClientDropType @@ -1289,6 +1306,16 @@ namespace game vec2_t corridorSpeeds; }; + struct ClientArchiveData + { + int serverTime; + vec3_t origin; + vec3_t velocity; + int bobCycle; + int movementDir; + PlayerVehicleState playerVehStateClientArchive; + }; + struct PlayerActiveWeaponState { int weapAnim; @@ -1411,18 +1438,26 @@ namespace game { int overflowed; int readOnly; - char* data; - char* splitData; + const char* data; + const char* splitData; int maxsize; int cursize; int splitSize; int readcount; int bit; int lastEntityRef; - netsrc_t targetLocalNetID; int useZlib; }; + struct gameState_t + { + int stringOffsets[3524]; + char stringData[131'072]; + int dataCount; + }; + + static_assert(sizeof(game::gameState_t) == 145'172); + struct HsvColor { unsigned char h; @@ -2099,7 +2134,9 @@ namespace game int clientNum; int viewmodelIndex; vec3_t viewangles; - char _0x190[0xAC]; + char _0x190[0x58]; + int viewlocked_entNum; + char _0x1EC[0x50]; int locationSelectionInfo; SprintState_t sprintState; float holdBreathScale; @@ -2131,14 +2168,11 @@ namespace game vec3_t weapLockedPos; int weaponIdleTime; int lastStowedWeapon; - char __0x638[0x2CE8]; + char __0x638[0x2CEC]; }; -#pragma pack(pop) - struct ClientCustomizationInfo - { - unsigned int modelIndex[3]; - }; + static_assert(sizeof(playerState_t) == 0x3328); +#pragma pack(pop) struct ScoreInfo { @@ -2154,6 +2188,140 @@ namespace game unsigned short adrenaline; }; + struct ScoreboardInfo + { + int teamScores[2]; + ScoreInfo clientScores[18]; + }; + + struct clSnapshot_t + { + mp::playerState_t ps; + ScoreboardInfo scores; + int valid; + int snapFlags; + int serverTime; + int messageNum; + int deltaNum; + int ping; + int cmdNum; + int numEntities; + int numClients; + int parseEntitiesIndex; + int parseClientsIndex; + int parseOmnvarsIdx; + int numAgents; + int parseAgentsIndex; + int scriptablesParseCount; + int scriptablesParseIndex; + int serverCommandNum; + }; + + static_assert(sizeof(clSnapshot_t) == 0x34B8); + + struct clientActive_t + { + char usingAds; + char padding1[3]; + int timeoutcount; + clSnapshot_t snap; + int oldSnapServerTime; + int oldServerTime; + int extrapolatedSnapshot; + char alwaysFalse; + char padding2[3]; + int serverTime; + int oldFrameServerTime; + int serverTimeDelta; + int newSnapshots; + int serverId; + char mapname[64]; + int mapcrc; + int parseEntitiesIndex; + int parseClientsIndex; + int parseOmnvarsIdx; + int mouseDx[2]; + int mouseDy[2]; + int mouseIndex; + char stanceHeld; + char padding3[3]; + int stance; + int stancePosition; + int stanceTime; + char wasSprinting; + char padding4[3]; + int cgameUserCmdWeapon; + int cgameUserCmdOffHand; + int cgameUserCmdAlternate; + float cgameFOVSensitivityScale; + float cgameMaxPitchSpeed; + float cgameMaxYawSpeed; + vec3_t cgameKickAngles; + vec3_t cgameOrigin; + vec3_t cgameVelocity; + PlayerVehicleState cgamePlayerVehState; + int cgameBobCycle; + int cgameMovementDir; + int cgameExtraButtons; + int cgamePredictedDataServerTime; + vec3_t clViewangles; + char _361C[12]; + usercmd_t cmds[128]; + int cmdNumber; + ClientArchiveData clientArchive[256]; + int clientArchiveIndex; + int packetBackupCount; + int packetBackupMask; + int parseEntitiesCount; + int parseClientsCount; + int parseOmnvarsCount; + // missing fields + }; + + struct clientStatic_t + { + int quit; + int hunkUsersStarted; + char servername[256]; + int rendererStarted; + int soundStarted; + int inUpdateSound; + int uiStarted; + int frametime; + int frametime_base; + int realtime; + char _0x124[0x80DBC]; + gameState_t gameState; + // missing fields + }; + + struct clientConnection_t + { + int qport; + int clientNum; + netadr_t serverAddress; + int connectLastSendTime; + int connectPacketCount; + char serverMessage[256]; + int challenge; + int checksumFeed; + int reliableSequence; + int reliableAcknowledge; + char reliableCommands[128][1024]; + int serverMessageSequence; + int serverCommandSequence; + int lastExecutedServerCommand; + char serverCommands[128][1024]; + char isServerRestarting; + char padding[3]; + // missing fields + }; + + struct ClientCustomizationInfo + { + unsigned int modelIndex[3]; + }; + union OmnvarValue { bool enabled; @@ -2186,44 +2354,51 @@ namespace game int modelindex; int dualWielding; char _0x10[0x30]; - char name[0x10]; + char name[16]; int rank; int prestige; - char _0x00[0x10]; int nameplateFriendlyIndex; int nameplateEnemyIndex; unsigned int perks[2]; - char _0x78[0x80]; + char _0x68[0x40]; + char clanAbbrev[8]; + char _0xB0[0x28]; }; + static_assert(sizeof(clientState_t) == 0xD8); + struct clientSession_t { int sessionState; int forceSpectatorClient; int killCamEntity; int isInKillcam; - char __0x332C[0x14]; + char _0x10[0x10]; usercmd_t cmd; usercmd_t oldcmd; int localClient; - char __0x33C4[0x24]; + char _0x9C[0x34]; float moveSpeedScaleMultiplier; int viewmodelIndex; int noSpectate; clientState_t cs; ScoreInfo scores; - char __pad[0x1B0]; + char _0x1E6[0x1C0]; }; + static_assert(sizeof(clientSession_t) == 0x388); + struct gclient_t { playerState_t ps; clientSession_t sess; int flags; int spectatorClient; - char __0x36B4[0x3B0]; + char _0x36B8[0x3B0]; }; + static_assert(sizeof(gclient_t) == 0x3A68); + struct LerpEntityState { int entFlags; @@ -2258,6 +2433,8 @@ namespace game char _0xD8[0x30]; }; + static_assert(sizeof(EntityState) == 0x108); + struct EntityShared { char _0x108[0x38]; @@ -2266,6 +2443,13 @@ namespace game char _0x14C[0xC]; }; + struct centity_t + { + char _0x0[0x238]; + }; + + static_assert(sizeof(centity_t) == 0x238); + struct gentity_t { EntityState s; @@ -2281,19 +2465,230 @@ namespace game struct snapshot_t { - char _0x0[0x3C78]; + playerState_t ps; + ScoreboardInfo scores; + OmnvarData omnvars[256]; + int snapFlags; int ping; int serverTime; int numEntities; int numClients; + // missing fields }; + enum DemoType + { + DEMO_TYPE_NONE = 0x0, + DEMO_TYPE_CLIENT = 0x1, + DEMO_TYPE_SERVER = 0x2 + }; + + struct characterInfo_t + { + char _0x0[0x5D8]; + }; + + static_assert(sizeof(characterInfo_t) == 0x5D8); + + struct clientInfo_t + { + char _0x0[0x70]; + }; + + static_assert(sizeof(clientInfo_t) == 0x70); + + struct bgs_t + { + char _0x0[0x459F0]; + characterInfo_t characterinfo[42]; + clientInfo_t clientinfo[18]; + }; + + static_assert(sizeof(bgs_t) == 0x55740); + + struct visionSetVarsBase_t + { + int filmEnable; + float filmBrightness; + float filmContrast; + float filmDesaturation; + float filmDesaturationDark; + float filmInvert; + vec3_t filmLightTint; + vec3_t filmMediumTint; + vec3_t filmDarkTint; + int charPrimaryUseTweaks; + float charPrimaryDiffuseScale; + float charPrimarySpecularScale; + vec3_t charLightAmbient; + int viewModelPrimaryUseTweaks; + float viewModelPrimaryDiffuseScale; + float viewModelPrimarySpecularScale; + vec3_t viewModelLightAmbient; + float primaryLightOverride; + int materialBloomHQEnable; + float materialBloomHQHaziness; + float materialBloomHQGamma; + float materialBloomHQDesaturation; + float materialBloomIntensity; + float materialBloomRadius; + float materialBloomPinch; + float materialBloomLuminanceCutoff; + float materialBloomDesaturation; + int volumeLightScatter; + float volumeLightScatterLinearAtten; + float volumeLightScatterQuadraticAtten; + float volumeLightScatterAngularAtten; + float volumeLightScatterDepthAttenNear; + float volumeLightScatterDepthAttenFar; + float volumeLightScatterBackgroundDistance; + vec3_t volumeLightScatterColor; + float ssaoStrength; + float ssaoPower; + float ssaoBlurRadius; + int glareEnable; + float glareNormalStart; + float glareNormalEnd; + float glareNormalPower; + float glareNormalAmount; + float glareMirrorStart; + float glareMirrorEnd; + float glareMirrorPower; + float glareMirrorAmount; + float rimLightPitch; + float rimLightHeading; + float rimLightDiffuseIntensity; + float rimLightSpecIntensity; + float rimLightBias; + float rimLightPower; + vec3_t rimLightColor; + int overrideExistingMask; + float unlitSurfaceHDRScalar; + float litSurfaceHDRScalar; + float hdrSkyIntensity; + vec3_t hdrSkyColorTint; + }; + + static_assert(sizeof(visionSetVarsBase_t) == 0x12C); + + struct visionSetVars_t + { + visionSetVarsBase_t baseclass_0; + void* _0x12C[3]; + }; + + static_assert(sizeof(visionSetVars_t) == 0x148); + + struct interpolationVisionSetVars_t + { + visionSetVarsBase_t baseclass_0; + void* _0x12C[0x2F]; + }; + + static_assert(sizeof(interpolationVisionSetVars_t) == 0x2A8); + + struct visionSetLerpData_t + { + int timeStart; + int timeDuration; + int style; + }; + + struct GfxFog + { + char _0x0[0x38]; + int startTime; + int finishTime; + int color; + float density; + int sunColor; + }; + + struct ClientVisionSetData + { + visionSetVars_t visionSetPreLoaded[4]; + char visionSetPreLoadedName[4][64]; + int visionSetPreLoadedUsedTime[4]; + interpolationVisionSetVars_t visionSetFrom[6]; + interpolationVisionSetVars_t visionSetTo[6]; + interpolationVisionSetVars_t visionSetCurrent[6]; + interpolationVisionSetVars_t visionSetPrevious; + visionSetLerpData_t visionSetLerpData[6]; + char visionName[6][64]; + char visionBlendActive[6]; + char thermalVisionActive; + char remoteMissileCam; + char painVisionActive; + float painVisionLerp; + int postApplyLerpTimeStart; + int postApplyLerpTimeDuration; + float postApplyLerpStart; + float postApplyLerpDest; + float postApplyLerpCurrent; + GfxFog lastClientFog; + int scriptFogActive; + int scriptFogLerpStartTime; + int scriptFogLerpEndTime; + }; + + static_assert(sizeof(ClientVisionSetData) == 0x3AF0); + + struct cpose_t + { + char _0x0[0x90]; + }; + + static_assert(sizeof(cpose_t) == 0x90); + struct cg_t { playerState_t ps; - char _0x3324[0x64]; + char _0x3324[0x38]; + int clientNum; + int localClientNum; + DemoType demoType; + char _0x336C[0x1C]; snapshot_t* snap; - snapshot_t* nextSnap; // theres alot after this cba rn + snapshot_t* nextSnap; + char _0x3398[0xA66F8]; + vec3_t refdefViewAngles; + char _0xA9A9C[0x8CC4]; + int clientSideVisionSetsEnabled; + int voiceTime; + Weapon weaponSelect; + char _0xB276C[0x2C10]; + int inKillCam; + char _0xB5380[0x148]; + bgs_t bgs; + cpose_t viewModelPose; + cpose_t viewModelPoseLeftHand; + ClientVisionSetData cvsData; + int stagedVisionStateTo; + int stagedVisionStateFrom; + int stagedVisionStateEndTime; + int stagedVisionStateStartTime; + // missing fields + }; + + struct cgs_t + { + int viewX; + int viewY; + int viewWidth; + int viewHeight; + float viewAspect; + float _0x14; + int serverCommandSequence; + int processedSnapshotNum; + int localServer; + char gametype[32]; + char szHostName[256]; + int hardcore; + int maxclients; + int privateClients; + char mapname[64]; + int gameEndTime; + // missing fields }; struct netProfilePacket_t @@ -2343,7 +2738,7 @@ namespace game struct __declspec(align(8)) clientHeader_t { - int state; + clientstate_t state; int sendAsActive; int deltaMessage; int rateDelayed; @@ -2377,7 +2772,10 @@ namespace game int reliableAcknowledge; int reliableSent; int messageAcknowledge; - char _0xC30[0x41238]; + int largeCommandSequence; + int gamestateMessageNum; + int challenge; + char _0xC3C[0x4122C]; gentity_t* gentity; char name[16]; int lastPacketTime; @@ -2400,6 +2798,8 @@ namespace game char _0x41E94[0x416A0]; }; + static_assert(sizeof(client_t) == 0x83570); + struct XZone { char name[64]; @@ -2419,9 +2819,18 @@ namespace game }; static_assert(sizeof(missileFireParms) == 0x20); - } - static_assert(sizeof(mp::client_t) == 0x83570); + struct serverStatic_t + { + char _0x0[0x1681D00]; + int time; + int loopbackProcessStopTime; + int snapFlagServerBit; + int clientCount; + client_t clients[18]; + // missing fields + }; + } union playerState_t { diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 770cb0b..d517a3a 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -311,19 +311,20 @@ namespace game namespace mp { - WEAK symbol cgArray{0, 0x14176EC00}; + WEAK symbol cg{0, 0x14176EC00}; + WEAK symbol cgs{0, 0x14187EB80}; + WEAK symbol cl{0, 0x1419E1C70}; + WEAK symbol clc{0, 0x141CB5350}; + WEAK symbol cls{0, 0x141D1AB60}; + WEAK symbol svs{0, 0x144DF9580}; WEAK symbol g_entities{0, 0x14427A0E0}; - - WEAK symbol svs_clientCount{0, 0x14647B28C}; - WEAK symbol svs_clients{0, 0x14647B290}; - - WEAK symbol sv_serverId_value{0, 0x144DF9478}; - - WEAK symbol gameTime{0, 0x1443F4B6C}; - WEAK symbol serverTime{0, 0x14647B280}; + WEAK symbol centities{0, 0x141887350}; WEAK symbol g_zones_0{0, 0x143A46498}; + WEAK symbol connstate{0, 0x1419E1AE0}; + WEAK symbol gameTime{0, 0x1443F4B6C}; + WEAK symbol sv_serverId_value{0, 0x144DF9478}; } namespace hks -- 2.34.1 From 7954966c6013c8eaf6152e0518533ee4ee1e3949 Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:12:38 +0100 Subject: [PATCH 10/14] Updated function symbols and references. Replaced all occurrences of 0x0 with 0 for consistency. --- src/client/component/branding.cpp | 3 +- src/client/component/input.cpp | 4 +- src/client/component/slowmotion.cpp | 2 +- src/client/game/symbols.hpp | 81 ++++++++++++++++++++--------- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/client/component/branding.cpp b/src/client/component/branding.cpp index 3602fb4..d963862 100644 --- a/src/client/component/branding.cpp +++ b/src/client/component/branding.cpp @@ -41,8 +41,7 @@ namespace branding localized_strings::override("LUA_MENU_LEGAL_COPYRIGHT", "iw6-mod: " VERSION " by AlterWare.\n"); utils::hook::call(SELECT_VALUE(0x1403BDABA, 0x140414424), dvar_set_string_stub); - ui_get_formatted_build_number_hook.create( - SELECT_VALUE(0x140415FD0, 0x1404D7C00), ui_get_formatted_build_number_stub); + ui_get_formatted_build_number_hook.create(game::LiveStorage_FetchFFotD, ui_get_formatted_build_number_stub); scheduler::loop([]() { diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index 6c3ecb9..beffee2 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -50,8 +50,8 @@ namespace input return; } - cl_char_event_hook.create(SELECT_VALUE(0x14023CE50, 0x1402C2AE0), cl_char_event_stub); - cl_key_event_hook.create(SELECT_VALUE(0x14023D070, 0x1402C2CE0), cl_key_event_stub); + cl_char_event_hook.create(game::CL_CharEvent, cl_char_event_stub); + cl_key_event_hook.create(game::CL_KeyEvent, cl_key_event_stub); } }; } diff --git a/src/client/component/slowmotion.cpp b/src/client/component/slowmotion.cpp index dac5286..42302e3 100644 --- a/src/client/component/slowmotion.cpp +++ b/src/client/component/slowmotion.cpp @@ -66,7 +66,7 @@ namespace slowmotion utils::hook::jump(0x1403B4A10, scr_cmd_set_slow_motion); // Detour used here instead of call hook because Com_TimeScaleMsec is called from arxan encrypted function - com_timescale_msec_hook.create(0x140415D50, com_timescale_msec); + com_timescale_msec_hook.create(game::Com_TimeScaleMsec, com_timescale_msec); } }; } diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index d517a3a..6e4b74d 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -36,15 +36,31 @@ namespace game WEAK symbol Cbuf_AddText{0x1403B3050, 0x1403F6B50}; WEAK symbol Cbuf_ExecuteBufferInternal{0x1403B3160, 0x1403F6C60}; - WEAK symbol CL_IsCgameInitialized{0x140234DA0, 0x1402B9A70}; - + WEAK symbol CG_ExecuteNewServerCommands{0, 0x140288500}; WEAK symbol CG_GameMessage{0x1401F2E20, 0x140271320}; - WEAK symbol CG_SetClientDvarFromServer{0x0, 0x14028A2C0}; + WEAK symbol CG_GetVehicleDef{0, 0x140229760}; + WEAK symbol CG_HandleTurretFire{0, 0x140269DF0}; + WEAK symbol CG_SetClientDvarFromServer{0, 0x14028A2C0}; + WEAK symbol CG_UpdateOmnvars{0, 0x14028E770}; + + WEAK symbol CL_CharEvent{0x14023CE50, 0x1402C2AE0}; + WEAK symbol CL_GetClientNameColorize{0, 0x1402CFA60}; + WEAK symbol CL_GetPredictedPlayerInformationForServerTime{0, 0x1402CC710}; + WEAK symbol CL_GetPredictedVehicleForServerTime{0, 0x1402CC7F0}; + WEAK symbol CL_IsCgameInitialized{0x140234DA0, 0x1402B9A70}; + WEAK symbol CL_KeyEvent{0x14023D070, 0x1402C2CE0}; + WEAK symbol CL_ParseServerMessage{0, 0x1402CDFC0}; + WEAK symbol CG_SelectWeapon{0, 0x1402AB310}; + WEAK symbol CL_SetCGameTime{0, 0x1402B9D80}; + WEAK symbol CL_WritePacket{0, 0x1402C1E70}; WEAK symbol Cmd_AddCommandInternal{0x1403B3570, 0x1403F7070}; WEAK symbol Cmd_ExecuteSingleCommand{0x1403B3B10, 0x1403F7680}; + WEAK symbol Com_TimeScaleMsec{0, 0x140415D50}; - WEAK symbol DB_EnumXAssets_FastFile{0x140271F50, 0x14031EF90}; + WEAK symbol DB_EnumXAssets_FastFile{0x140271F50, 0x14031EF90}; WEAK symbol DB_EnumXAssets_Internal{0x140271FC0, 0x14031F000}; WEAK symbol DB_FindXAssetEntry{0x140272230, 0x14031F2D0}; WEAK symbol DB_GetXAssetName{0x14024FB10, 0x1402FB160}; @@ -54,11 +70,11 @@ namespace game WEAK symbol DB_FindXAssetHeader{0x140272300, 0x14031F3A0}; WEAK symbol DB_XAssetExists{0x140276200, 0x1403245E0}; WEAK symbol DB_IsXAssetDefault{0x140273480 , 0x1403204D0}; - WEAK symbol DB_GetRawFileLen{0x0140272E80, 0x14031FF80}; + WEAK symbol DB_GetRawFileLen{0x140272E80, 0x14031FF80}; WEAK symbol DB_GetRawBuffer{0x140272D50, 0x14031FE50}; WEAK symbol DB_IsLocalized{0x140273210, 0x140320360}; - WEAK symbol PMem_AllocFromSource_NoDebug{0x0140430B80, 0x001404F46C0}; + WEAK symbol PMem_AllocFromSource_NoDebug{0x140430B80, 0x001404F46C0}; WEAK symbol PMem_Free{0x140430EC0 , 0x1404F4A30}; WEAK symbol Hunk_AllocateTempMemoryHighInternal{0x140423C70, 0x1404E4E20}; @@ -93,6 +109,8 @@ namespace game WEAK symbol FS_AddGameDirectory{0x14041A120, 0x1404DC570}; WEAK symbol FS_AddLocalizedGameDirectory{0x14041A2F0, 0x1404DC760}; + WEAK symbol GetRemoteEyeValues{0, 0x1402A0190}; + WEAK symbol G_FindItem{0x140462490, 0x14021B7E0}; WEAK symbol G_GivePlayerWeapon{0x140359E20, 0x1403DA5E0}; WEAK symbol G_GetWeaponForName{0x140359890, 0x1403DA060}; @@ -101,10 +119,10 @@ namespace game WEAK symbol G_SelectWeapon{0x14035A200, 0x1403DA880}; WEAK symbol G_TakePlayerWeapon{0x14035A350, 0x1403DA9C0}; WEAK symbol G_FindConfigstringIndex{0x0, 0x140161F90}; - WEAK symbol G_RunFrame{0x0, 0x1403A05E0}; + const char* errormsg)> G_FindConfigstringIndex{0, 0x140161F90}; + WEAK symbol G_RunFrame{0, 0x1403A05E0}; - WEAK symbol HudElem_Alloc{0x0, 0x1403997E0}; + WEAK symbol HudElem_Alloc{0, 0x1403997E0}; WEAK symbol I_CleanStr{0x140432460, 0x1404F63C0}; @@ -116,6 +134,7 @@ namespace game WEAK symbol Live_SyncOnlineDataFlags{0, 0x1405ABF70}; + WEAK symbol LiveStorage_FetchFFotD{0x140415FD0, 0x1404D7C00}; WEAK symbol LiveStorage_PlayerDataSetIntByName{0x1403B8C20, 0x140404730}; WEAK symbol LiveStorage_PlayerDataSetReservedInt{0x1403B8D00, 0x140404820}; WEAK symbol LiveStorage_PlayerDataGetReservedInt{0x1403B84F0, 0x140403CF0}; @@ -135,24 +154,30 @@ namespace game WEAK symbol StringTable_GetColumnValueForRow{0, 0x1404E61A0}; WEAK symbol StringTable_LookupRowNumForValue{0, 0x1404E6260}; + WEAK symbol LUI_IntermissionBegan{0, 0x1401CEB40}; WEAK symbol LUI_OpenMenu{0x1403FD460, 0x1404B3610}; // Made up name, replaced by ScopedCriticalSection on Black Ops 3 WEAK symbol LUI_EnterCriticalSection{0x1401AE940, 0x1401CD040}; WEAK symbol LUI_LeaveCriticalSection{0x1401B0AA0, 0x1401CF1A0}; - WEAK symbol Menu_IsMenuOpenAndVisible{0x0, 0x1404B38A0}; + WEAK symbol Menu_IsMenuOpenAndVisible{0, 0x1404B38A0}; WEAK symbol Material_RegisterHandle{0x140523D90, 0x1405F0E20}; + WEAK symbol MSG_ReadLong{0, 0x1404181C0}; + WEAK symbol MSG_WriteBits{0, 0x140418740}; + WEAK symbol NET_OutOfBandPrint{0, 0x14041D5C0}; WEAK symbol NET_SendLoopPacket{0, 0x14041D780}; WEAK symbol NET_StringToAdr{0, 0x14041D870}; WEAK symbol NetadrToSockadr{0, 0x1404E53D0}; + WEAK symbol Omnvar_GetDef{0, 0x1404F3E80}; + WEAK symbol R_AddCmdDrawStretchPic{0x140234460, 0x140600BE0}; - WEAK symbol R_AddCmdDrawText{0x140533E40, 0x140601070}; - WEAK symbol R_AddCmdDrawTextWithCursor{0x140534170, 0x1406013A0}; + WEAK symbol R_AddCmdDrawText{0x140533E40, 0x140601070}; + WEAK symbol R_AddCmdDrawTextWithCursor{0x140534170, 0x1406013A0}; WEAK symbol R_RegisterFont{0x1405130B0, 0x1405DFAC0}; WEAK symbol R_SyncRenderThread{0x140535AF0, 0x140602D30}; WEAK symbol R_TextWidth{0x140513390, 0x1405DFDB0}; @@ -166,27 +191,27 @@ namespace game WEAK symbol GetEntityFieldValue{0x1403DC810, 0x140437860}; WEAK symbol Scr_AllocVector{0x1403D9AF0, 0x140434A10}; WEAK symbol Scr_GetString{0, 0x140439160}; - WEAK symbol Scr_AddInt{0x0, 0x140437E70}; - WEAK symbol Scr_AddString{0x0, 0x1404381D0}; - WEAK symbol Scr_GetInt{0x0, 0x140438E10}; + WEAK symbol Scr_AddInt{0, 0x140437E70}; + WEAK symbol Scr_AddString{0, 0x1404381D0}; + WEAK symbol Scr_GetInt{0, 0x140438E10}; WEAK symbol Scr_GetFloat{0, 0x140438D60}; WEAK symbol Scr_GetNumParam{0x1403DDF60, 0x140438EC0}; WEAK symbol Scr_ClearOutParams{0x1403DD500, 0x140438600}; WEAK symbol Scr_GetEntityIdRef{0x1403DBDC0, 0x140436E10}; - WEAK symbol Scr_AddEntityNum{0x0, 0x140437F60}; + WEAK symbol Scr_AddEntityNum{0, 0x140437F60}; WEAK symbol Scr_SetObjectField{0x140350E70, 0x1403D3FE0}; WEAK symbol Scr_NotifyId{0x1403DE730, 0x140439700}; - WEAK symbol Scr_NotifyLevel{0x0, 0x1404397D0}; - WEAK symbol Scr_GetEntityId{0x0, 0x140436D60}; - WEAK symbol Scr_CastString{0x0, 0x140434AC0}; + WEAK symbol Scr_NotifyLevel{0, 0x1404397D0}; + WEAK symbol Scr_GetEntityId{0, 0x140436D60}; + WEAK symbol Scr_CastString{0, 0x140434AC0}; WEAK symbol Scr_ExecThread{0x1403DD600, 0x1404386E0}; WEAK symbol Scr_LoadScript{0x1403D3C50, 0x14042EAA0}; WEAK symbol Scr_GetFunctionHandle{0x1403D3AD0 , 0x14042E920}; WEAK symbol Scr_RegisterFunction{0x1403D3530, 0x14042E330}; - WEAK symbol Scr_ErrorInternal{0x0, 0x140438660}; + WEAK symbol Scr_ErrorInternal{0, 0x140438660}; - WEAK symbol GetObjectType{0x0, 0x140433CF0}; + WEAK symbol GetObjectType{0, 0x140433CF0}; WEAK symbol VM_Execute{0, 0x14043A280}; @@ -201,7 +226,7 @@ namespace game WEAK symbol SV_Cmd_ArgvBuffer{0x1403B4560, 0x1403F80D0}; WEAK symbol SV_Cmd_TokenizeString{0, 0x1403F8150}; WEAK symbol SV_Cmd_EndTokenizedString{0, 0x1403F8110}; - WEAK symbol SV_MatchEnd{0x0, 0x14047A090}; + WEAK symbol SV_MatchEnd{0, 0x14047A090}; WEAK symbol SV_DirectConnect{0, 0x140471390}; WEAK symbol SV_GameSendServerCommand{0x140490F40, 0x1404758C0}; @@ -219,6 +244,7 @@ namespace game WEAK symbol SV_GetPlayerstateForClientNum{0x140490F80, 0x140475A10}; WEAK symbol SV_GetGuid{0, 0x140475990}; WEAK symbol SV_KickClientNum{0, 0x14046F730}; + WEAK symbol SV_Netchan_Transmit{0, 0x14047CC60}; WEAK symbol SV_SetConfigstring{0, 0x140477450}; WEAK symbol Sys_Error{0x14043AC20, 0x1404FF510}; @@ -237,6 +263,9 @@ namespace game WEAK symbol UI_LocalizeMapname{0, 0x1404B96D0}; WEAK symbol UI_LocalizeGametype{0, 0x1404B90F0}; + WEAK symbol VehicleCam_UpdatePlayerControlCam{0, 0x140568CA0}; + WEAK symbol dwGetLogOnStatus{0, 0x140589490}; WEAK symbol PM_trace{0, 0x140225DB0}; - WEAK symbol Jump_ClearState{0x0, 0x140213120}; + WEAK symbol Jump_ClearState{0, 0x140213120}; WEAK symbol longjmp{0x14062E030, 0x140738060}; WEAK symbol _setjmp{0x14062F030, 0x140739060}; @@ -294,9 +323,9 @@ namespace game WEAK symbol scr_function_stack{0x1455BE708, 0x144D57808}; WEAK symbol scr_levelEntityId{0x1452A9F30, 0x144A43020}; - WEAK symbol level_time{0x0, 0x1443F4B6C}; - WEAK symbol level_finished{0x0, 0x1443F6FAC}; - WEAK symbol level_savepersist{0x0, 0x1443F5710}; + WEAK symbol level_time{0, 0x1443F4B6C}; + WEAK symbol level_finished{0, 0x1443F6FAC}; + WEAK symbol level_savepersist{0, 0x1443F5710}; WEAK symbol threadIds{0x144DE6640, 0x1446B4960}; -- 2.34.1 From d26a5fe43f37de71e958db591d656eb0ee3e237b Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:17:24 +0100 Subject: [PATCH 11/14] Added a few changes. Added missing includes to the list of standard headers. Added an assertion to the asmjit assemble function to get an early warning if asmjit fails silently. --- src/client/component/dedicated_info.cpp | 2 +- src/client/std_include.hpp | 4 ++++ src/common/utils/hook.cpp | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/component/dedicated_info.cpp b/src/client/component/dedicated_info.cpp index de3f8aa..a505924 100644 --- a/src/client/component/dedicated_info.cpp +++ b/src/client/component/dedicated_info.cpp @@ -59,7 +59,7 @@ namespace dedicated_info utils::string::strip(sv_hostname->current.string, cleaned_hostname.data(), cleaned_hostname.size() + 1); - console::set_title(utils::string::va("%s on %s [%d/%d] (%d)", cleaned_hostname.data(), + console::set_title(utils::string::va("%s on %s [%d/%d] (%d)", cleaned_hostname.c_str(), mapname->current.string, client_count, sv_maxclients->current.integer, bot_count)); }, scheduler::pipeline::main, 1s); diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index 3f7c182..b2c2d33 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -46,7 +46,10 @@ #undef min #endif +#include +#include #include +#include #include #include #include @@ -54,6 +57,7 @@ #include #include #include +#include #include #include #include diff --git a/src/common/utils/hook.cpp b/src/common/utils/hook.cpp index be05cb9..f25821e 100644 --- a/src/common/utils/hook.cpp +++ b/src/common/utils/hook.cpp @@ -2,6 +2,7 @@ #include "string.hpp" #include +#include namespace utils::hook { @@ -279,6 +280,7 @@ namespace utils::hook void* result = nullptr; runtime.add(&result, &code); + assert(result); return result; } -- 2.34.1 From d424dc2c45bf6b86423034d48ae7271760d4bb20 Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:23:57 +0100 Subject: [PATCH 12/14] Added workaround to enforce max fps limit. Without this workaround my fps occasionally exceeds 1500 or even 2000 fps. --- src/client/component/fps.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/client/component/fps.cpp b/src/client/component/fps.cpp index 3614829..154cf8c 100644 --- a/src/client/component/fps.cpp +++ b/src/client/component/fps.cpp @@ -74,6 +74,23 @@ namespace fps ++data->index; } + void enforce_fps_limit() + { + const auto* maxfps = game::Dvar_FindVar("com_maxfps"); + const auto max = (maxfps) ? std::min(2 * maxfps->current.integer, maxfps->domain.integer.max + 250) : 1250; + const auto fps = static_cast(static_cast(1000.0f / + static_cast(cg_perf.average)) + 9.313225746154785e-10); + + if (fps > max) + { + // workaround to limit fps + scheduler::once([]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + }, scheduler::pipeline::renderer); + } + } + void perf_update() { cg_perf.count = 32; @@ -84,6 +101,7 @@ namespace fps cg_perf.previous_ms = cg_perf.current_ms; perf_calc_fps(&cg_perf, cg_perf.frame_ms); + enforce_fps_limit(); utils::hook::invoke(SELECT_VALUE(0x1405806E0, 0x140658E30)); } -- 2.34.1 From d76f41deb9e1322a047c3ac1d7290f97e6ee8f5f Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:30:58 +0100 Subject: [PATCH 13/14] Added changes to outside code. --- src/client/component/dvar_cheats.cpp | 7 +++++++ src/client/component/fps.cpp | 6 ++++++ src/client/component/fps.hpp | 6 ++++++ src/client/component/party.cpp | 6 ++++++ src/client/component/party.hpp | 2 ++ 5 files changed, 27 insertions(+) create mode 100644 src/client/component/fps.hpp diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp index bda4b71..4b96ba3 100644 --- a/src/client/component/dvar_cheats.cpp +++ b/src/client/component/dvar_cheats.cpp @@ -5,6 +5,7 @@ #include "game/dvars.hpp" #include "console.hpp" +#include "demo_playback.hpp" #include #include @@ -13,6 +14,12 @@ namespace dvar_cheats { void apply_sv_cheats(const game::dvar_t* dvar, const game::DvarSetSource source, game::dvar_value* value) { + // return early because sv_cheats is enabled when playing back demos + if (demo_playback::playing()) + { + return; + } + if (dvar && dvar->name == "sv_cheats"s) { // if dedi, do not allow internal to change value so servers can allow cheats if they want to diff --git a/src/client/component/fps.cpp b/src/client/component/fps.cpp index 154cf8c..d38ca1c 100644 --- a/src/client/component/fps.cpp +++ b/src/client/component/fps.cpp @@ -1,5 +1,6 @@ #include #include "loader/component_loader.hpp" +#include "fps.hpp" #include "scheduler.hpp" #include "game/game.hpp" @@ -173,6 +174,11 @@ namespace fps } } + float get_avg_fps() + { + return 1000.0f / (cg_perf.average + std::numeric_limits::epsilon()); + } + class component final : public component_interface { public: diff --git a/src/client/component/fps.hpp b/src/client/component/fps.hpp new file mode 100644 index 0000000..d0a537f --- /dev/null +++ b/src/client/component/fps.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace fps +{ + [[nodiscard]] float get_avg_fps(); +} diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 5316622..628b159 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -83,6 +83,12 @@ namespace party }); } + void connect_to_dummy_party(const std::string& mapname, const std::string& gametype) + { + const game::netadr_t dummy_target{}; + connect_to_party(dummy_target, mapname, gametype); + } + void switch_gamemode_if_necessary(const std::string& gametype) { const auto target_mode = gametype == "aliens" ? game::CODPLAYMODE_ALIENS : game::CODPLAYMODE_CORE; diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 85d1a82..d3e985b 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -2,6 +2,8 @@ namespace party { + void connect_to_dummy_party(const std::string& mapname, const std::string& gametype); + void switch_gamemode_if_necessary(const std::string& gametype); void perform_game_initialization(); -- 2.34.1 From c3a7f633365eb869d2b3e6ecdc55ab0719e14f6e Mon Sep 17 00:00:00 2001 From: Caball Date: Tue, 31 Dec 2024 00:45:51 +0100 Subject: [PATCH 14/14] Added demo code. Includes client and server recording code, and playback code. --- src/client/component/demo_playback.cpp | 946 ++++++++++++ src/client/component/demo_playback.hpp | 9 + src/client/component/demo_recording.cpp | 746 ++++++++++ src/client/component/demo_recording.hpp | 8 + src/client/component/demo_sv_recording.cpp | 664 +++++++++ src/client/component/demo_sv_recording.hpp | 8 + src/client/component/demo_timescale.cpp | 139 ++ src/client/component/demo_utils.cpp | 1526 ++++++++++++++++++++ src/client/component/demo_utils.hpp | 217 +++ 9 files changed, 4263 insertions(+) create mode 100644 src/client/component/demo_playback.cpp create mode 100644 src/client/component/demo_playback.hpp create mode 100644 src/client/component/demo_recording.cpp create mode 100644 src/client/component/demo_recording.hpp create mode 100644 src/client/component/demo_sv_recording.cpp create mode 100644 src/client/component/demo_sv_recording.hpp create mode 100644 src/client/component/demo_timescale.cpp create mode 100644 src/client/component/demo_utils.cpp create mode 100644 src/client/component/demo_utils.hpp diff --git a/src/client/component/demo_playback.cpp b/src/client/component/demo_playback.cpp new file mode 100644 index 0000000..26ca198 --- /dev/null +++ b/src/client/component/demo_playback.cpp @@ -0,0 +1,946 @@ +#include "std_include.hpp" +#include "loader/component_loader.hpp" + +#include "demo_playback.hpp" +#include "demo_utils.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "utils/command_line.hpp" +#include "utils/hook.hpp" + +using namespace demo_utils; + +namespace // demo class +{ + class demo_playback_t + { + public: + demo_playback_t(); + + [[nodiscard]] bool is_demo_playing() const; + [[nodiscard]] bool is_demo_repeating() const; + [[nodiscard]] const std::optional& get_gamestate() const; + [[nodiscard]] bool rewind_demo(std::optional msec); + [[nodiscard]] bool start(std::filesystem::path& path, bool repeat); + [[nodiscard]] std::optional> restart(); + template auto use_predicted_data(Func func, Args&&... args); + template auto handle_fast_forward(Func func, Args&&... args); + + void parse_demo_wrapper(); + void stop(); + + private: + void repeat(); + + bool repeat_{}; + bool skip_timeout_{}; + bool retain_timescale_{}; + std::size_t post_map_header_file_offset_{}; + std::size_t pred_data_index_{}; + std::array pred_data_{}; + std::optional rewind_snap_svr_time_; + std::optional gs_; + std::vector buffer_; + std::filesystem::path path_; + std::ifstream file_; + } demo; + + demo_playback_t::demo_playback_t() + { + buffer_.reserve(MAX_SIZE); + } + + bool demo_playback_t::is_demo_playing() const + { + return file_.good() && file_.is_open(); + } + + bool demo_playback_t::is_demo_repeating() const + { + return repeat_; + } + + const std::optional& demo_playback_t::get_gamestate() const + { + return gs_; + } + + template + auto demo_playback_t::use_predicted_data(Func func, Args&& ...args) + { + return func(pred_data_, pred_data_index_, std::forward(args)...); + } + + template + auto demo_playback_t::handle_fast_forward(Func func, Args&& ...args) + { + return func(rewind_snap_svr_time_, skip_timeout_, std::forward(args)...); + } + + bool demo_playback_t::rewind_demo(std::optional msec) + { + if (!is_demo_playing()) + { + return false; + } + + // trigger demo end-of-file which will repeat the demo + repeat_ = true; + file_.seekg(0, std::ios::end); + + if (msec) + { + retain_timescale_ = true; + + const auto snap_svr_time = game::mp::cl->snap.serverTime; + if (const auto delta_time = snap_svr_time - static_cast(*msec) - 50; delta_time > 0) + { + rewind_snap_svr_time_ = delta_time; + } + } + + // workaround to get rewinding working without delay at low timescales + fast_forward_demo(1000); + + return true; + } + + bool demo_playback_t::start(std::filesystem::path& path, bool repeat) + { + // check if the file can be opened before taking any action with the demo file stream + std::ifstream file(path, std::ios::binary); + + const auto can_open = file.good() && file.is_open(); + if (!can_open) + { + return false; + } + + stop(); + + file_ = std::move(file); + assert(file_.good() && file_.is_open()); + + // (re)set data + repeat_ = repeat; + gs_.reset(); + path_ = std::move(path); + get_persistent_data() = {}; + + if (auto* demo_timescale = game::Dvar_FindVar("demotimescale"); demo_timescale) + { + demo_timescale->current.value = 1.0f; + } + + if (game::Live_SyncOnlineDataFlags(0)) + { + console::info("the demo should start to load in a few seconds\n"); + } + + return true; + } + + std::optional> demo_playback_t::restart() + { + if (!start(path_, repeat_)) + { + return path_; + } + + return std::nullopt; + } + + void demo_playback_t::parse_demo_wrapper() + { + if (!parse_demo(file_, buffer_, post_map_header_file_offset_, pred_data_, pred_data_index_, gs_)) + { + if (is_demo_repeating() && *game::mp::connstate != game::CA_DISCONNECTED) + { // don't repeat demo if it was stopped manually + repeat(); + } + else + { + stop(); + } + } + + assert(*game::mp::connstate != game::CA_DISCONNECTED || !is_demo_playing()); + } + + void demo_playback_t::stop() + { + skip_timeout_ = {}; + retain_timescale_ = {}; + post_map_header_file_offset_ = {}; + pred_data_index_ = {}; + pred_data_.fill({}); + rewind_snap_svr_time_ = {}; + gs_.reset(); + buffer_.clear(); + file_.clear(); + file_.close(); + + // ensure that sv_cheats is always disabled after demo playback + if (auto* sv_cheats = game::Dvar_FindVar("sv_cheats"); sv_cheats) + { + sv_cheats->current.enabled = false; + } + } + + void demo_playback_t::repeat() + { + assert(post_map_header_file_offset_ < 256); + + if (auto& connstate = *game::mp::connstate; connstate == game::CA_ACTIVE) + { + // prevent old snapshots from being processed; would mess up the server command sequences + connstate = game::CA_PRIMED; + } + + // reset game data + game::mp::clc->serverCommandSequence = {}; + game::mp::clc->lastExecutedServerCommand = {}; + game::mp::cgs->serverCommandSequence = {}; + get_persistent_data() = {}; + + if (auto* demo_timescale = game::Dvar_FindVar("demotimescale"); demo_timescale) + { + if (!retain_timescale_) + { + demo_timescale->current.value = 1.0f; + } + } + + // reset class data + skip_timeout_ = {}; + retain_timescale_ = {}; + pred_data_index_ = {}; + + file_.clear(); + file_.seekg(post_map_header_file_offset_, std::ios::beg); + } +} + +namespace // hooks +{ + utils::hook::detour CG_UpdateOmnvars_hook; + utils::hook::detour CG_VisionSetStartLerp_To_hook; + utils::hook::detour CL_GetPredictedPlayerInformationForServerTime_hook; + utils::hook::detour CL_GetPredictedVehicleForServerTime_hook; + utils::hook::detour CL_SetCGameTime_hook; + utils::hook::detour CL_WritePacket_hook; + utils::hook::detour GetRemoteEyeValues_hook; + utils::hook::detour LUI_IntermissionBegan_hook; + utils::hook::detour VehicleCam_UpdatePlayerControlCam_hook; + + game::dvar_t* demo_vision; + + static constexpr std::array VISION_SETS{ + "disabled", "no vision", "default", + + "ac130", "ac130_enhanced_mp", "ac130_inverted", "aftermath", "aftermath_glow", "aftermath_post", "black_bw", + "cheat_bw", "coup_sunblind", "default_night", "default_night_mp", "end_game", "missilecam", "mpintro", + "mpnuke", "mpnuke_aftermath", "mpoutro", "near_death", "near_death_mp", "nuke_global_flash", "thermal_mp", "thermal_snowlevel_mp", + + "aftermath_map", static_cast(nullptr) + }; + static_assert( + std::string_view(VISION_SETS[0]) == "disabled" && + std::string_view(VISION_SETS[1]) == "no vision" && + std::string_view(VISION_SETS[2]) == "default" && + std::string_view(VISION_SETS[VISION_SETS.size() - 2]) == "aftermath_map" && + VISION_SETS.back() == nullptr // enum dvars require a nullptr as last value + ); + + bool handle_vision_override(game::mp::ClientVisionSetData& cvs_data, std::int32_t index, const char* vision_name, std::int32_t duration, std::int32_t time) + { + if (demo.is_demo_playing()) + { + const auto vision_set = static_cast((demo_vision) ? demo_vision->current.integer : 0); + + if (vision_set == 1) + { + // enable 'post apply' vision + cvs_data.visionSetLerpData[index].style = 1; + cvs_data.postApplyLerpDest = 1.0f; + + return true; + } + else if (vision_set >= 2 && vision_set + 2 <= VISION_SETS.size()) + { + // enable 'post apply' vision + cvs_data.visionSetLerpData[index].style = 0; + cvs_data.postApplyLerpDest = 1.0f; + + if (vision_set + 2 == VISION_SETS.size()) + { + if (const auto mapname = get_mapname(false); mapname.empty()) + { + return CG_VisionSetStartLerp_To_hook.invoke(&cvs_data, index, "aftermath_post", duration, time); + } + else + { + // a small number of maps have a vision called 'aftermath_mp_mapname' + return CG_VisionSetStartLerp_To_hook.invoke(&cvs_data, index, std::format("aftermath_{}", mapname).c_str(), duration, time); + } + } + else + { + return CG_VisionSetStartLerp_To_hook.invoke(&cvs_data, index, VISION_SETS[vision_set], duration, time); + } + } + } + + return CG_VisionSetStartLerp_To_hook.invoke(&cvs_data, index, vision_name, duration, time); + } + + void handle_vision() + { + if (demo_vision && demo_vision->modified) + { + const auto vision_set = demo_vision->current.integer; + + for (auto i = 0; i < game::CS_VISIONSET_COUNT; ++i) + { + const std::string_view vision_name = (!vision_set) + ? game::mp::cg->cvsData.visionName[i] + : ""; + + handle_vision_override(game::mp::cg->cvsData, i, vision_name.data(), 1, game::mp::cl->serverTime); + } + + if (!vision_set) + { + // disable 'post apply' vision if the game hasn't used it yet + if (game::mp::cg->cvsData.visionName[5][0] == '\0') + { + game::mp::cg->cvsData.postApplyLerpDest = 0.0f; + } + } + else if (vision_set == 1) + { + std::memset(&game::mp::cg->cvsData.visionSetCurrent, 0, sizeof(game::mp::cg->cvsData.visionSetCurrent)); + } + + // reset modified flag to avoid doing the same work each frame + demo_vision->modified = false; + } + } + + void handle_fast_forward(auto old_connstate) + { + if (const auto new_connstate = *game::mp::connstate; new_connstate == game::CA_ACTIVE) + { + const auto server_time = game::mp::cl->serverTime; + const auto snap_svr_time = game::mp::cl->snap.serverTime; + + demo.handle_fast_forward([old_connstate, server_time, snap_svr_time]( + std::optional& rewind_snap_svr_time, bool& skip_timeout) + { + if (old_connstate == game::CA_PRIMED) + { + if (rewind_snap_svr_time) + { + // handle fast forward after rewinding + if (*rewind_snap_svr_time > snap_svr_time) + { + fast_forward_demo(*rewind_snap_svr_time - snap_svr_time); + } + + rewind_snap_svr_time = {}; + } + } + else if (old_connstate == game::CA_ACTIVE && !skip_timeout) + { + if (const auto delta_time = snap_svr_time - server_time; delta_time >= 1000) + { + // skip timeout between first and second snapshots (for user demos) + fast_forward_demo(delta_time); + skip_timeout = true; + } + } + }); + } + } + + void handle_demo_patches() + { + // set cg_t->demoType to DEMO_TYPE_CLIENT to disable mouse input and avoid frame interpolation issues + game::mp::cg->demoType = game::mp::DEMO_TYPE_CLIENT; + + // ensure that sv_cheats is always enabled during demo playback + if (auto* sv_cheats = game::Dvar_FindVar("sv_cheats"); sv_cheats) + { + sv_cheats->current.enabled = true; + } + + // sometimes the hud isn't updated after a weapon change; force update the weapon name and ammo (clip) when changing weapons + // various other Call of Duty games are also affected by this bug + if (game::mp::cg->weaponSelect.data != static_cast(game::mp::cg->ps.weapon)) + { + game::CG_SelectWeapon(0, game::mp::cg->ps.weapon, 0); + } + } + + void handle_demo_reading(const std::int32_t client_num) + { + if (game::Live_SyncOnlineDataFlags(0) || !demo.is_demo_playing()) + { + // set demoType to default value + // normally the game does this unconditionally, but that code has been removed + game::mp::cg->demoType = game::mp::DEMO_TYPE_NONE; + + CL_SetCGameTime_hook.invoke(client_num); + } + else + { + const auto old_connstate = *game::mp::connstate; + if (old_connstate == game::CA_ACTIVE) + { + // reset cl->newSnapshots to prevent CL_SetCGameTime from making a call to CL_AdjustTimeDelta, + // which interferes with the timescale + game::mp::cl->newSnapshots = 0; + } + + CL_SetCGameTime_hook.invoke(client_num); + + handle_fast_forward(old_connstate); + handle_vision(); + handle_demo_patches(); + + demo.parse_demo_wrapper(); + } + } + + void write_packet(const std::int32_t client_num) + { + if (!demo.is_demo_playing()) + { + CL_WritePacket_hook.invoke(client_num); + } + } + + bool get_predicted_player_data(const game::mp::clientActive_t& cl, std::int32_t ps_time, game::mp::playerState_t& ps) + { + if (!demo.is_demo_playing()) + { + return CL_GetPredictedPlayerInformationForServerTime_hook.invoke(&cl, ps_time, &ps); + } + + // hide regular hud when showing special hud for killstreak reward + // must not unset this 15th bit when dead to avoid a crash in CG_SimulateBulletFire + ps.otherFlags = (ps.pm_type == 7 || ps.remoteControlEnt != 2047 || ps.vehicleState.entity != 2047) + ? ps.otherFlags | 0x4000 + : ps.otherFlags & ~0x4000; + + demo.use_predicted_data([&ps, ps_time](const std::array& pred_data, std::size_t pred_data_index) + { + static constexpr auto pred_data_size = std::tuple_size_v>; + + // find most recent predicted player data + for (std::size_t count = 0, index = pred_data_index - 1; count < pred_data_size; ++count, --index) + { + const auto& p_data = pred_data[index % pred_data_size]; + if (p_data.cad.serverTime <= ps_time && ps_time - p_data.cad.serverTime < 500) + { + std::memcpy(&ps.origin[0], &p_data.cad.origin[0], sizeof(ps.origin)); + std::memcpy(&ps.velocity[0], &p_data.cad.velocity[0], sizeof(ps.velocity)); + std::memcpy(&ps.viewangles[0], &p_data.viewangles[0], sizeof(ps.viewangles)); + ps.bobCycle = p_data.cad.bobCycle; + ps.movementDir = p_data.cad.movementDir; + + break; + } + } + }); + + // must return true otherwise branch is taken that undoes the work of this function + return true; + } + + bool get_predicted_vehicle_data(const game::mp::clientActive_t& cl, std::int32_t ps_time, game::PlayerVehicleState& ps_vehicle) + { + if (!demo.is_demo_playing()) + { + return CL_GetPredictedVehicleForServerTime_hook.invoke(&cl, ps_time, &ps_vehicle); + } + + demo.use_predicted_data([&ps_vehicle, ps_time](const std::array& pred_data, std::size_t pred_data_index) + { + static constexpr auto pred_data_size = std::tuple_size_v>; + + // find most recent predicted vehicle data + for (std::size_t count = 0, index = pred_data_index - 1; count < pred_data_size; ++count, --index) + { + const auto& p_data = pred_data[index % pred_data_size]; + if (p_data.cad.serverTime <= ps_time && ps_time - p_data.cad.serverTime < 500) + { + const auto org_entity = ps_vehicle.entity; + ps_vehicle = p_data.cad.playerVehStateClientArchive; + ps_vehicle.entity = org_entity; + + break; + } + } + }); + + // must return true otherwise branch is taken that undoes the work of this function + return true; + } + + bool show_intermission_menu(const std::int32_t client_num, void* lua_vm) + { + // it appeared easier to prevent the intermission menu from being shown instead of closing or hiding it + + if (!demo.is_demo_playing() || !demo.is_demo_repeating()) + { + return LUI_IntermissionBegan_hook.invoke(client_num, lua_vm); + } + + // rewind demo and don't display intermission menu for repeated demos + static_cast(demo.rewind_demo(std::nullopt)); + + return false; + } + + void control_ui_element(const std::int32_t client_num, const game::mp::snapshot_t& old_snap, game::mp::snapshot_t& new_snap) + { + // it appeared easier to prevent these menus from being shown instead of closing or hiding them + + if (demo.is_demo_playing()) + { + // skip team selection menu (and loadout selection menu) + assert(std::string_view(*reinterpret_cast(game::Omnvar_GetDef(103))) == "ui_options_menu"); + new_snap.omnvars[103] = {}; + + if (demo.is_demo_repeating()) + { + // skip extinction mode end-of-game score / stats menu + assert(std::string_view(*reinterpret_cast(game::Omnvar_GetDef(194))) == "ui_alien_show_eog_score"); + new_snap.omnvars[194] = {}; + } + } + + CG_UpdateOmnvars_hook.invoke(client_num, &old_snap, &new_snap); + } + + std::int32_t update_gamestate_config_strings(game::msg_t& msg) + { + if (demo.is_demo_playing()) + { + // update command sequences and overwrite config strings if that data is available + if (const auto& gs = demo.get_gamestate(); gs) + { + game::mp::cls->gameState = gs->data; + game::mp::clc->serverCommandSequence = gs->svr_cmd_seq; + game::mp::clc->lastExecutedServerCommand = gs->svr_cmd_seq; + game::mp::cgs->serverCommandSequence = gs->svr_cmd_seq; + } + else + { + assert(is_gamestate_valid(game::mp::cls->gameState, false)); + + game::mp::clc->lastExecutedServerCommand = game::mp::clc->serverCommandSequence; + game::mp::cgs->serverCommandSequence = game::mp::clc->serverCommandSequence; + } + } + + // call MSG_ReadLong that this hook replaces + return game::MSG_ReadLong(&msg); + } + + void ignore_mouse_input_killstreak_reward_1(const std::int32_t client_num, game::mp::cg_t& cg, game::vec3_t& out_view_origin, game::vec3_t& out_view_angles) + { + auto& in_killcam = cg.inKillCam; + + if (!demo.is_demo_playing() || in_killcam) + { + VehicleCam_UpdatePlayerControlCam_hook.invoke(client_num, &cg, &out_view_origin, &out_view_angles); + } + else + { + // required for e.g. the Gryphon killstreak reward + in_killcam = 1; + VehicleCam_UpdatePlayerControlCam_hook.invoke(client_num, &cg, &out_view_origin, &out_view_angles); + in_killcam = 0; + } + } + + bool ignore_mouse_input_killstreak_reward_2(const std::int32_t client_num) + { + auto& other_flags = game::mp::cg->ps.otherFlags; + + if (!demo.is_demo_playing() || !(other_flags & 0x4000)) + { + return GetRemoteEyeValues_hook.invoke(client_num); + } + else + { + // required for e.g. the Trinity Rocket killstreak reward + other_flags &= ~0x4000; + const auto result = GetRemoteEyeValues_hook.invoke(client_num); + other_flags |= 0x4000; + + return result; + } + } +} + +namespace // command execution +{ + bool execute_playback_command_internal(std::filesystem::path& file_path, bool repeat, bool print_error) + { + if (demo.start(file_path, repeat)) + { + return true; + } + + if (print_error) + { + console::error("could not play demo %s\n", file_path.string().c_str()); + } + + return false; + } + + bool execute_playback_command_internal(std::string_view file_name, bool repeat) + { + const auto fs_basepath = get_base_path(false); + if (fs_basepath.empty()) + { + console::error("could not find fs_basepath\n"); + return false; + } + + const auto mod = get_mod_directory(); + + auto file_path1 = std::filesystem::path(fs_basepath) / std::format("demos/client/{2}/user/{0}{1}", file_name, DEMO_EXTENSION, mod); + if (execute_playback_command_internal(file_path1, repeat, false)) + { + return true; + } + + auto file_path2 = std::filesystem::path(fs_basepath) / std::format("demos/client/{2}/auto/{0}{1}", file_name, DEMO_EXTENSION, mod); + if (execute_playback_command_internal(file_path2, repeat, false)) + { + return true; + } + + auto file_path3 = std::filesystem::path(fs_basepath) / std::format("demos/client/{0}{1}", file_name, DEMO_EXTENSION); + if (execute_playback_command_internal(file_path3, repeat, false)) + { + return true; + } + + auto file_path4 = std::filesystem::path(fs_basepath) / std::format("demos/server/{2}/{0}{1}", file_name, DEMO_EXTENSION, mod); + if (execute_playback_command_internal(file_path4, repeat, false)) + { + return true; + } + + auto file_path5 = std::filesystem::path(fs_basepath) / std::format("demos/server/{0}{1}", file_name, DEMO_EXTENSION); + if (execute_playback_command_internal(file_path5, repeat, false)) + { + return true; + } + + auto file_path6 = std::filesystem::path(fs_basepath) / std::format("demos/{0}{1}", file_name, DEMO_EXTENSION); + if (execute_playback_command_internal(file_path6, repeat, false)) + { + return true; + } + + console::error("could not play demo %s\n", file_path1.string().c_str()); + console::error("could not play demo %s\n", file_path2.string().c_str()); + console::error("could not play demo %s\n", file_path3.string().c_str()); + console::error("could not play demo %s\n", file_path4.string().c_str()); + console::error("could not play demo %s\n", file_path5.string().c_str()); + console::error("could not play demo %s\n", file_path6.string().c_str()); + + return false; + } + + template + bool execute_playback_command(const command::params& params) + { + struct args_t + { + bool fullpath{}; + bool repeat{}; + }; + + auto parsed_arguments = [¶ms]() -> std::optional + { + if (params.size() == 3) + { + if (std::string_view(params.get(2)) == "fullpath") + { + return args_t{ true, false }; + } + else if (std::string_view(params.get(2)) == "repeat") + { + return args_t{ false, true }; + } + else + { + return {}; + } + } + else if (params.size() == 4) + { + if (std::string_view(params.get(2)) == "fullpath" && std::string_view(params.get(3)) == "repeat") + { + return args_t{ true, true }; + } + else + { + return {}; + } + } + else + { + return args_t{ false, false }; + } + }(); + + if (params.size() < 2 || params.size() > 4 || !parsed_arguments) + { + console::info("usage: demoplay , fullpath(optional), repeat(optional)\n"); + return false; + } + + if constexpr (!force_playback) + { + if (!demo.is_demo_playing() && game::CL_IsCgameInitialized()) + { + console::warn("could not play demo; please disconnect first\n"); + return false; + } + } + + if (!parsed_arguments->fullpath) + { + return execute_playback_command_internal(params.get(1), parsed_arguments->repeat); + } + else + { + auto file_path = std::filesystem::path(params.get(1)); + return execute_playback_command_internal(file_path, parsed_arguments->repeat, true); + } + } + + void execute_playback_instant_play_command(const command::params& params) + { + // the demo recording code (demo_recording_t::shutdown_watcher) should take care of properly closing any recording demo files! + + const auto demo_playing = demo.is_demo_playing(); + if (execute_playback_command(params)) + { + if (auto* sv_running = game::Dvar_FindVar("sv_running"); sv_running && sv_running->current.enabled) + { + // demo cannot play if local server is still considered to be running + sv_running->current.enabled = false; + } + + if (!demo_playing) + { + if (auto& connstate = *game::mp::connstate; connstate == game::CA_ACTIVE) + { + // prevent old snapshots from being processed, + // would mess up the server command sequences and server times + connstate = game::CA_PRIMED; + } + } + } + } + + void execute_playback_replay_command(const command::params& params) + { + if (params.size() != 1) + { + console::info("usage: demoreplay\n"); + return; + } + + if (const auto path = demo.restart(); path) + { + console::error("could not play demo %s\n", path->get().string().c_str()); + } + } + + void execute_playback_rewind_command(const command::params& params) + { + if (params.size() < 1 || params.size() > 2) + { + console::info("usage: demorewind (optional)\n"); + return; + } + + if (params.size() == 2) + { + const auto msec = std::atoi(params.get(1)); + if (msec <= 0) + { + console::error("demorewind invalid value %s\n", params.get(1)); + return; + } + + if (!demo.rewind_demo(msec)) + { + console::error("demo must be playing to rewind\n"); + } + } + else if (!demo.rewind_demo(std::nullopt)) + { + console::error("demo must be playing to rewind\n"); + } + } + + void execute_playback_fast_forward_command(const command::params& params) + { + if (params.size() != 2) + { + console::info("usage: demoforward \n"); + return; + } + + const auto msec = std::atoi(params.get(1)); + if (msec <= 0) + { + console::error("demoforward invalid value %s\n", params.get(1)); + return; + } + + if (demo.is_demo_playing()) + { + fast_forward_demo(static_cast(msec)); + } + else + { + console::error("demo must be playing to fast forward\n"); + } + } +} + +namespace demo_playback +{ + bool playing() + { + return demo.is_demo_playing(); + } + + bool startplay(std::string_view file_name, bool repeat) + { + return execute_playback_command_internal(file_name, repeat); + } + + bool startplay(std::filesystem::path& file_name, bool repeat) + { + return execute_playback_command_internal(file_name, repeat, true); + } + + void stopplay() + { + demo.stop(); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + // check run-time address assertions + check_address_assertions(); + + // primary hook for demo playback; read demo data after server time has been updated + CL_SetCGameTime_hook.create(game::CL_SetCGameTime, handle_demo_reading); + + // don't write packets when playing a demo + CL_WritePacket_hook.create(game::CL_WritePacket, write_packet); + + // replace functions to get predicted data, because they skip the viewangles + CL_GetPredictedPlayerInformationForServerTime_hook.create( + game::CL_GetPredictedPlayerInformationForServerTime, get_predicted_player_data); + CL_GetPredictedVehicleForServerTime_hook.create( + game::CL_GetPredictedVehicleForServerTime, get_predicted_vehicle_data); + + // ignore mouse input when updating the camera for certain killstreak rewards + VehicleCam_UpdatePlayerControlCam_hook.create( + game::VehicleCam_UpdatePlayerControlCam, ignore_mouse_input_killstreak_reward_1); + GetRemoteEyeValues_hook.create( + game::GetRemoteEyeValues, ignore_mouse_input_killstreak_reward_2); + + // optional skip of intermission menu + LUI_IntermissionBegan_hook.create(game::LUI_IntermissionBegan, show_intermission_menu); + + // don't open any menu when demo playback has started + CG_UpdateOmnvars_hook.create(game::CG_UpdateOmnvars, control_ui_element); + + // allow user to override visual settings + CG_VisionSetStartLerp_To_hook.create(0x1402A3200, handle_vision_override); + demo_vision = game::Dvar_RegisterEnum( + "demovision", const_cast(VISION_SETS.data()), 0, game::DVAR_FLAG_NONE, "Override vision set for demos"); + + // update the gamestate configs strings with 'live' data from the demo (in CL_ParseGamestate) + utils::hook::call(0x1402CCD56, update_gamestate_config_strings); + + // don't let the game set cg_t->demoType in CG_DrawActiveFrame, + // otherwise demo playback will have mouse input and suffer from frame interpolation issues + utils::hook::nop(0x14029981C, 6); + + // replace thread local storage lookup for cg.bgs in function CG_EmissiveBlendCommand with hardcoded address + // when CG_ExecuteNewServerCommands is called manually (e.g. from demo_utils::process_server_commands), + // this address in tls may be a nullptr and the game could crash here (in extinction mode at least) + static constexpr auto patch_address = 0x14028844B; + static constexpr std::array patch{ 0x48, 0x8D, 0x1D, 0x68, 0xBC, 0x59, 0x01 }; + + utils::hook::nop(patch_address, 0xE); + utils::hook::nop(patch_address + 0x1C, 4); + utils::hook::nop(patch_address + 0x22, 4); + utils::hook::copy(patch_address + 0xE, patch.data(), patch.size()); // lea rbx, [0x1418240C8] (point RBX to cg.bgs) + assert(std::bit_cast(&game::mp::cg->bgs) == + patch_address + 0xE + patch.size() + *reinterpret_cast(patch_address + 0x11)); + + // add console command support to play demos + command::add("demoplay", execute_playback_command); + + // add experimental command console support to play demos, + // skip disconnecting from local or online server and skip map (re)loading + command::add("demoinstantplay", execute_playback_instant_play_command); + + // replay most recent demo (if any) + command::add("demoreplay", execute_playback_replay_command); + + // rewind demo (by N msec) + command::add("demorewind", execute_playback_rewind_command); + + // fast forward demo by N msec + command::add("demoforward", execute_playback_fast_forward_command); + + // add command line support to play demos; this makes it possible to double click on a demo file to play it + const auto& args = utils::command_line::get_args(); + for (const auto& arg : args) + { + if (arg.extension() == DEMO_EXTENSION) + { + scheduler::on_game_initialized([cmd = std::format(R"(demoplay "{}" fullpath repeat)", arg.string())]() mutable + { + command::execute(std::move(cmd), true); + }, scheduler::main); + + console::info("the demo should start to load in a few seconds\n"); + break; + } + } + } + }; +} + +REGISTER_COMPONENT(demo_playback::component) diff --git a/src/client/component/demo_playback.hpp b/src/client/component/demo_playback.hpp new file mode 100644 index 0000000..149978f --- /dev/null +++ b/src/client/component/demo_playback.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace demo_playback +{ + [[nodiscard]] bool playing(); + [[nodiscard]] bool startplay(std::string_view file_name, bool repeat = true); + [[nodiscard]] bool startplay(std::filesystem::path& file_path, bool repeat = true); + [[nodiscard]] void stopplay(); +} diff --git a/src/client/component/demo_recording.cpp b/src/client/component/demo_recording.cpp new file mode 100644 index 0000000..66c1e6d --- /dev/null +++ b/src/client/component/demo_recording.cpp @@ -0,0 +1,746 @@ +#include "std_include.hpp" +#include "loader/component_loader.hpp" + +#include "demo_playback.hpp" +#include "demo_recording.hpp" +#include "demo_utils.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "game/game.hpp" +#include "scheduler.hpp" +#include "utils/hook.hpp" + +using namespace demo_utils; + +namespace // demo class +{ + game::dvar_t* demo_autorecord; + + class demo_recording_t + { + static constexpr auto NON_DELTA_SNAPSHOT_COUNT = 4; + + struct stored_times_t + { + std::int32_t flush_time{}; + std::int32_t predicted_data_time{}; + std::int32_t cur_svr_time{}; + std::int32_t first_svr_time_user_file{}; + std::int32_t first_svr_time_auto_file{}; + }; + + public: + demo_recording_t() = default; + ~demo_recording_t(); + demo_recording_t(const demo_recording_t&) = delete; + demo_recording_t(demo_recording_t&&) noexcept = delete; + demo_recording_t& operator=(const demo_recording_t&) = delete; + demo_recording_t& operator=(demo_recording_t&&) noexcept = delete; + + [[nodiscard]] std::uint32_t get_demo_count() const; + [[nodiscard]] std::int32_t get_demo_length() const; + [[nodiscard]] bool is_recording() const; + [[nodiscard]] bool non_delta_data_requested() const; + [[nodiscard]] bool start(const std::filesystem::path& user_path, const std::filesystem::path& auto_path); + [[nodiscard]] bool stop(); + + void shutdown_watcher(); + void process_network_data(auto old_connstate, auto new_connstate, std::span network_data); + void process_predicted_data(); + + private: + [[nodiscard]] bool is_auto_recording() const; + [[nodiscard]] bool non_delta_data_request_acknowledged(); + [[nodiscard]] bool start_user_file(const std::filesystem::path& path); + [[nodiscard]] bool start_auto_file(const std::filesystem::path& path); + [[nodiscard]] static bool stop_internal(std::ofstream& file, std::int32_t first_svr_time, std::int32_t last_svr_time); + + void flush_filestreams(); + void check_auto_recording(); + void process_old_data(); + void process_new_gamestate(std::span network_data); + void process_first_snapshot(std::span network_data); + void process_special_network_data(auto old_connstate, auto new_connstate, std::span network_data); + void process_helo_pilot_turret_fire(); + + std::uint32_t demo_count_{}; + std::uint32_t non_delta_count_{}; + stored_times_t times_; + buffer_t crit_data_; + std::ofstream user_file_; + buffer_t auto_buffer_; + std::ofstream auto_file_; + } demo; + + demo_recording_t::~demo_recording_t() + { + process_old_data(); + } + + std::uint32_t demo_recording_t::get_demo_count() const + { + return demo_count_; + } + + std::int32_t demo_recording_t::get_demo_length() const + { + return (times_.cur_svr_time - times_.first_svr_time_user_file) / 1000; + } + + bool demo_recording_t::is_recording() const + { + return user_file_.good() && user_file_.is_open(); + } + + bool demo_recording_t::is_auto_recording() const + { + return auto_file_.good() && auto_file_.is_open(); + } + + bool demo_recording_t::non_delta_data_requested() const + { + return non_delta_count_ > 0; + } + + bool demo_recording_t::non_delta_data_request_acknowledged() + { + if (non_delta_data_requested()) + { + const auto delta_num = game::mp::cl->snap.deltaNum; + if (static constexpr auto no_delta = -1; delta_num == no_delta) + { + --non_delta_count_; + return true; + } + } + + return false; + } + + bool demo_recording_t::start_user_file(const std::filesystem::path& path) + { + const auto crit_data = crit_data_.get(); + if (!crit_data.size()) + { + return false; + } + + std::ofstream file(path, std::ios::binary); + if (!file.good() || !file.is_open()) + { + return false; + } + + write_general_header(file); + + // store mod name so if there's mod support the mod can be loaded + write_mod_header(file); + + // store map name and game type so that the map can be preloaded + if (!write_map_header(file)) + { + return false; + } + + // store the current gamestate config strings so that the original strings are overwritten if they were modified + if (!write_gamestate_data(file)) + { + return false; + } + + // required for demos (and live matches) to function at all + file.write(reinterpret_cast(crit_data.data()), crit_data.size()); + + // set the non-delta snapshot count, the demo won't be valid otherwise + non_delta_count_ = NON_DELTA_SNAPSHOT_COUNT; + + // store first server time so that demo length can be tracked + times_.first_svr_time_user_file = game::mp::cl->snap.serverTime; + + // increase demo count for the text render function to work properly + ++demo_count_; + + user_file_ = std::move(file); + + return true; + } + + bool demo_recording_t::start_auto_file(const std::filesystem::path& path) + { + std::ofstream file(path, std::ios::binary); + if (!file.good() || !file.is_open()) + { + return false; + } + + const auto buffer = auto_buffer_.get(); + file.write(reinterpret_cast(buffer.data()), buffer.size()); + + auto_file_ = std::move(file); + auto_buffer_.clear(); + + return true; + } + + bool demo_recording_t::start(const std::filesystem::path& user_path, const std::filesystem::path& auto_path) + { + if (!is_recording()) + { + if (!is_auto_recording() && !start_auto_file(auto_path)) + { + return false; + } + + if (!start_user_file(user_path)) + { + return false; + } + + return true; + } + + return false; + } + + bool demo_recording_t::stop_internal(std::ofstream& file, std::int32_t first_svr_time, std::int32_t last_svr_time) + { + write_general_footer(file, first_svr_time, last_svr_time); + write_end_of_file(file); + + file.flush(); + file.close(); + + return file.good() && !file.is_open(); + } + + bool demo_recording_t::stop() + { + return is_recording() && stop_internal(user_file_, times_.first_svr_time_user_file, times_.cur_svr_time); + } + + void demo_recording_t::process_old_data() + { + if (is_recording() && !stop()) + { + console::error("could not stop user demo\n"); + } + + if (is_auto_recording() && !stop_internal(auto_file_, times_.first_svr_time_auto_file, times_.cur_svr_time)) + { + console::error("could not close auto demo\n"); + } + + demo_count_ = {}; + non_delta_count_ = {}; + times_ = {}; + crit_data_.clear(); + auto_buffer_.clear(); + assert(!is_recording()); + assert(!is_auto_recording()); + } + + void demo_recording_t::shutdown_watcher() + { + auto execute_on_disconnect = [this, cgame_init = false]() mutable + { + if (demo_playback::playing()) + { + if (is_recording() || is_auto_recording()) + { + // this is only relevant when playing back demos without disconnecting from the server (instant play) + cgame_init = false; + process_old_data(); + } + return; + } + + if (!cgame_init && game::CL_IsCgameInitialized()) + { + cgame_init = true; + } + + if (cgame_init && !game::CL_IsCgameInitialized()) + { + cgame_init = false; + process_old_data(); + } + }; + + scheduler::loop(execute_on_disconnect, scheduler::main); + } + + void demo_recording_t::flush_filestreams() + { + const auto real_time = game::mp::cls->realtime; + if (real_time >= times_.flush_time + 15'000) + { + times_.flush_time = real_time; + + if (is_recording()) + { + user_file_.flush(); + } + if (is_auto_recording()) + { + auto_file_.flush(); + } + } + } + + void demo_recording_t::check_auto_recording() + { + if (!is_auto_recording() && times_.first_svr_time_auto_file && demo_autorecord && demo_autorecord->current.enabled) + { + assert(!game::Live_SyncOnlineDataFlags(0) && game::CL_IsCgameInitialized()); + + const auto opt_auto_dir = create_directory_auto_demo(); + if (!opt_auto_dir) + { + console::error("could not create demo auto directory\n"); + return; + } + + const auto opt_auto_path = create_path_auto_demo(*opt_auto_dir); + if (!opt_auto_path) + { + console::error("could not create demo auto file\n"); + return; + } + + if (!start_auto_file(*opt_auto_path)) + { + // turn auto recording off to prevent spamming the console with errors + demo_autorecord->current.enabled = false; + + console::error("could not create demo auto file %s\n", opt_auto_path->string().c_str()); + return; + } + } + } + + void demo_recording_t::process_new_gamestate(std::span network_data) + { + process_old_data(); + + // store gamestate for user file + write_network_data(crit_data_, network_data); + + // reserve memory (low size if auto recording is enabled, because then the demo writes almost directly to disk) + const auto reserve_size = (demo_autorecord && demo_autorecord->current.enabled) ? 8192 : 1024 * 1024; + auto_buffer_.reserve_memory(reserve_size); + + // write headers to auto buffer (ahead of the gamestate itself) + write_general_header(auto_buffer_); + write_mod_header(auto_buffer_); + write_map_header(auto_buffer_); + } + + void demo_recording_t::process_first_snapshot(std::span network_data) + { + assert(!is_recording() && crit_data_.size() + && !times_.cur_svr_time && !times_.first_svr_time_user_file && !times_.first_svr_time_auto_file); + + // store first snapshot for user file + write_network_data(crit_data_, network_data); + + times_.first_svr_time_auto_file = game::mp::cl->snap.serverTime; + } + + void demo_recording_t::process_special_network_data(auto old_connstate, auto new_connstate, std::span network_data) + { + assert(new_connstate >= game::CA_PRIMED); + + if (old_connstate < game::CA_LOADING) + { + assert(!game::mp::cl->snap.valid && !game::mp::cl->snap.serverTime); + + process_new_gamestate(network_data); + } + else if (old_connstate >= game::CA_PRIMED) + { + if (game::mp::cl->snap.valid && game::mp::cl->snap.serverTime && !times_.cur_svr_time) + { + process_first_snapshot(network_data); + } + + times_.cur_svr_time = game::mp::cl->snap.serverTime; + } + } + + void demo_recording_t::process_network_data(auto old_connstate, auto new_connstate, std::span network_data) + { + if (new_connstate < game::CA_PRIMED) + { + return; + } + + process_special_network_data(old_connstate, new_connstate, network_data); + check_auto_recording(); + + if (is_recording()) + { + if (!non_delta_data_requested() || non_delta_data_request_acknowledged()) + { + write_network_data(user_file_, network_data); + } + } + + if (is_auto_recording()) + { + write_network_data(auto_file_, network_data); + } + else + { + write_network_data(auto_buffer_, network_data); + } + + flush_filestreams(); + } + + void demo_recording_t::process_helo_pilot_turret_fire() + { + const auto turret_fire = cpy_cast(reinterpret_cast(0x1419A9394)); + if (turret_fire > 0) + { + if (is_recording()) + { + write_helo_pilot_turret_fire(user_file_, turret_fire); + } + + if (is_auto_recording()) + { + write_helo_pilot_turret_fire(auto_file_, turret_fire); + } + else + { + write_helo_pilot_turret_fire(auto_buffer_, turret_fire); + } + } + } + + void demo_recording_t::process_predicted_data() + { + process_helo_pilot_turret_fire(); + + if (is_recording()) + { + // only one predicted data packet is needed per (1000 / sv_fps) msec + // the best way to limit the amount of predicted data is to attempt to predict the next playerState_t::commandTime + // because during demo playback the playerState_t::commandTime determines which predicted data is used + + const auto& cl = *game::mp::cl; + const auto cmd_number = static_cast(cl.cmdNumber); + const auto pred_svr_time_delta = cl.cmds[cmd_number % std::size(cl.cmds)].serverTime + - cl.cmds[(cmd_number - 1) % std::size(cl.cmds)].serverTime; + + const auto pred_svr_time = cl.cgamePredictedDataServerTime; + const auto svr_time_delta = std::clamp(cl.snap.serverTime - cl.oldSnapServerTime, 50, 100); + + const auto cur_cmd_time = cl.snap.ps.commandTime; + const auto next_cmd_time = cur_cmd_time + svr_time_delta; + const auto threshold = (3 * pred_svr_time_delta) / 2; + + if (pred_svr_time + threshold >= next_cmd_time && next_cmd_time + threshold >= pred_svr_time) + { + times_.predicted_data_time = cl.serverTime; + write_predicted_data(user_file_); + } + else if (cl.serverTime >= times_.predicted_data_time + svr_time_delta - 5) + { + times_.predicted_data_time = cl.serverTime; + write_predicted_data(user_file_); + } + } + + if (is_auto_recording()) + { + write_predicted_data(auto_file_); + } + else + { + write_predicted_data(auto_buffer_); + } + } +} + +namespace // hooks +{ + utils::hook::detour CL_ParseServerMessage_hook; + + void capture_data(const std::int32_t client_num, game::msg_t& msg) + { + const auto old_connstate = *game::mp::connstate; + CL_ParseServerMessage_hook.invoke(client_num, &msg); + const auto new_connstate = *game::mp::connstate; + + if (demo_playback::playing()) + { + return; + } + + if (new_connstate < game::CA_DISCONNECTED || new_connstate > game::CA_ACTIVE) + { + console::warn("invalid connection state after processing data in CL_PacketEvent\n"); + return; + } + + if (!msg.data || msg.overflowed || static_cast(msg.cursize) > MAX_SIZE) + { + if (demo.is_recording()) + { + console::warn("invalid data in CL_PacketEvent\n"); + } + return; + } + + demo.process_network_data(old_connstate, new_connstate, + std::span(reinterpret_cast(msg.data), msg.cursize)); + } + + void capture_predicted_data(game::msg_t& msg, std::int32_t value, std::uint32_t bits) + { + if (!demo_playback::playing()) + { + demo.process_predicted_data(); + } + + // call MSG_WriteBits that this hook replaces + game::MSG_WriteBits(&msg, value, bits); + } + + void request_non_delta_data(game::msg_t& msg, std::int32_t value, std::uint32_t bits) + { + if (!demo_playback::playing() && demo.non_delta_data_requested()) + { + // request a non-delta snapshot from the server + // to have a valid start for a new (user) demo + value = 1; + } + + // call MSG_WriteBits that this hook replaces + game::MSG_WriteBits(&msg, value, bits); + } +} + +namespace // command execution +{ + void add_recording_text_to_hud(const std::filesystem::path& path) + { + const auto* font = game::R_RegisterFont("fonts/normalfont"); + if (!font) + { + console::error("could not register font\n"); + return; + } + + const auto* view_placement = game::ScrPlace_GetViewPlacement(); + if (!view_placement) + { + console::error("could not find view placement\n"); + return; + } + + auto create_resources = [&path, font, view_placement]() + { + struct resources_t + { + const game::Font_t* font{}; + float y{}; + std::int32_t demo_length{}; + std::uint32_t demo_count{}; + std::uint32_t org_text_size{}; + std::string text; + }; + + resources_t res{ + .font = font, + .y = view_placement->realViewportSize[1], + .demo_length = demo.get_demo_length(), + .demo_count = demo.get_demo_count(), + .text = "recording: " + path.stem().string() + }; + + res.org_text_size = static_cast(res.text.size()); + res.text.reserve(res.text.size() + 12); + + return res; + }; + + scheduler::schedule([res = create_resources()]() mutable + { + if (!demo.is_recording() || res.demo_count != demo.get_demo_count() || !game::CL_IsCgameInitialized()) + { + return scheduler::cond_end; + } + + if (res.demo_length != demo.get_demo_length()) + { + res.demo_length = demo.get_demo_length(); + + // resize to original size and append demo length in seconds + res.text.resize(res.org_text_size); + res.text += std::format(" ({} sec)", res.demo_length); + } + + static constexpr std::array color_red{ 178.0f / 255.0f, 34.0f / 255.0f, 34.0f / 255.0f, 1.0f }; + + // draw in bottom left corner + game::R_AddCmdDrawText(res.text.c_str(), std::numeric_limits::max(), + res.font, 10.0f, res.y - 5.0f, 0.75f, 0.75f, 0.0f, color_red.data(), 0); + + return scheduler::cond_continue; + }, scheduler::renderer); + } + + bool execute_demo_start_command_internal(std::string_view file_name, bool overwrite) + { + if (game::Live_SyncOnlineDataFlags(0) || !game::CL_IsCgameInitialized() || *game::mp::connstate != game::CA_ACTIVE) + { + console::warn("must be in a match to record a demo\n"); + return false; + } + + if (demo.is_recording()) + { + console::warn("must stop demo recording before starting to record\n"); + return false; + } + + if (demo_playback::playing()) + { + console::warn("cannot record a demo during demo playback\n"); + return false; + } + + const auto opt_user_dir = create_directory_user_demo(); + if (!opt_user_dir) + { + console::error("could not create demo user directory\n"); + return false; + } + + const auto opt_auto_dir = create_directory_auto_demo(); + if (!opt_auto_dir) + { + console::error("could not create demo auto directory\n"); + return false; + } + + const auto opt_user_path = (file_name.empty()) + ? create_path_user_demo(*opt_user_dir) + : create_path_user_demo(*opt_user_dir, file_name, overwrite); + if (!opt_user_path) + { + console::error("could not create demo user file\n"); + return false; + } + + const auto opt_auto_path = create_path_auto_demo(*opt_auto_dir); + if (!opt_auto_path) + { + console::error("could not create demo auto file\n"); + return false; + } + + if (!demo.start(*opt_user_path, *opt_auto_path)) + { + console::error("could not create demo user file %s\n", opt_user_path->string().c_str()); + console::error("could not create demo auto file %s\n", opt_auto_path->string().c_str()); + return false; + } + + add_recording_text_to_hud(*opt_user_path); + + return true; + } + + void execute_demo_start_command(const command::params& params) + { + if (params.size() < 1 || params.size() > 3 || (params.size() == 3 && std::string_view(params.get(2)) != "overwrite")) + { + console::info("usage: demostart (optional), overwrite(optional)\n"); + return; + } + + assert(params.size() != 3 || (params.size() == 3 && std::string_view(params.get(2)) == "overwrite")); + execute_demo_start_command_internal((params.size() >= 2) ? params.get(1) : std::string_view{}, (params.size() == 3)); + } + + void execute_demo_stop_command(const command::params& params) + { + if (params.size() != 1) + { + console::info("usage: demostop\n"); + return; + } + + if (demo.is_recording() && !demo.stop()) + { + console::error("could not stop demo\n"); + } + } +} + +namespace demo_recording +{ + bool recording() + { + return demo.is_recording(); + } + + bool startrecord(std::string_view file_name, bool overwrite) + { + return execute_demo_start_command_internal(file_name, overwrite); + } + + bool demo_recording::stoprecord() + { + return demo.stop(); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + // check run-time address assertions + check_address_assertions(); + + // capture incoming packets + CL_ParseServerMessage_hook.create(game::CL_ParseServerMessage, capture_data); + + // capture client predicted data (in CL_WritePacket) + utils::hook::call(0x1402C21B9, capture_predicted_data); + + // request the server for no delta data (in CL_WritePacket) + utils::hook::call(0x1402C2026, request_non_delta_data); + + // add support to auto record + demo_autorecord = game::Dvar_RegisterBool( + "demoautorecord", false, game::DVAR_FLAG_NONE, "Automatically start recording a demo."); + + // add console command support to record demos + command::add("demostart", execute_demo_start_command); + command::add("demostop", execute_demo_stop_command); + + scheduler::on_game_initialized([]() + { + demo.shutdown_watcher(); + + // check if demo directories exist / could be created + if (!can_create_demo_directories()) + { + console::error("could not create demo directories\n"); + } + }, scheduler::main); + } + }; +} + +REGISTER_COMPONENT(demo_recording::component) diff --git a/src/client/component/demo_recording.hpp b/src/client/component/demo_recording.hpp new file mode 100644 index 0000000..99d67ea --- /dev/null +++ b/src/client/component/demo_recording.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace demo_recording +{ + [[nodiscard]] bool recording(); + [[nodiscard]] bool startrecord(std::string_view file_name = {}, bool overwrite = {}); + [[nodiscard]] bool stoprecord(); +} diff --git a/src/client/component/demo_sv_recording.cpp b/src/client/component/demo_sv_recording.cpp new file mode 100644 index 0000000..11af494 --- /dev/null +++ b/src/client/component/demo_sv_recording.cpp @@ -0,0 +1,664 @@ +#include "std_include.hpp" +#include "loader/component_loader.hpp" + +#include "demo_sv_recording.hpp" +#include "demo_utils.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "utils/hook.hpp" +#include "utils/string.hpp" + +using namespace demo_utils; + +namespace // checked client num class +{ + inline constexpr std::size_t SV_MAX_CLIENTS = + sizeof(game::mp::serverStatic_t::clients) / sizeof(game::mp::serverStatic_t::clients[0]); + + class sv_client_num_t + { + public: + explicit sv_client_num_t(std::size_t client_num); + explicit sv_client_num_t(std::string_view client_id); + + [[nodiscard]] bool valid() const; + [[nodiscard]] std::uint8_t value() const; + + private: + [[nodiscard]] static std::optional check_client_num(std::size_t client_num); + [[nodiscard]] static std::optional parse_client_id(std::string_view str); + + const std::optional client_num_; + }; + + sv_client_num_t::sv_client_num_t(std::size_t client_num) + : client_num_(check_client_num(client_num)) + { + assert(valid()); + } + + sv_client_num_t::sv_client_num_t(std::string_view client_id) + : client_num_(parse_client_id(client_id)) + {} + + bool sv_client_num_t::valid() const + { + return client_num_.has_value(); + } + + std::uint8_t sv_client_num_t::value() const + { + assert(valid()); + + return client_num_.value(); + } + + std::optional sv_client_num_t::check_client_num(std::size_t client_num) + { + if (client_num >= SV_MAX_CLIENTS) + { + return {}; + } + + if (client_num >= static_cast(game::mp::svs->clientCount)) + { + return {}; + } + + assert(game::mp::svs->clientCount == game::Dvar_FindVar("sv_maxclients")->current.integer); + assert(client_num == static_cast(game::mp::svs->clients[client_num].gentity->s.number)); + + return static_cast(client_num); + } + + std::optional sv_client_num_t::parse_client_id(std::string_view str) + { + if (str.starts_with("pid")) + { + const auto pid = std::string_view(str.begin() + 3, str.end()); + const auto all_digits = std::all_of(pid.begin(), pid.end(), [](unsigned char c) + { + return std::isdigit(c); + }); + + auto value = std::numeric_limits::max(); + const auto ec = std::from_chars(pid.data(), pid.data() + pid.size(), value).ec; + + if (all_digits && ec == std::errc{}) + { + const auto client_num = check_client_num(value); + if (client_num) + { + return client_num; + } + } + } + + for (const auto& cl : game::mp::svs->clients) + { + if (cl.header.state != game::CS_ACTIVE || str != utils::string::strip(cl.name, true)) + { + continue; + } + + const auto client_num = check_client_num(cl.gentity->s.number); + if (client_num) + { + return client_num; + } + } + + return {}; + } +} + +namespace // server demos class +{ + game::dvar_t* sv_demos; + game::dvar_t* sv_demo_autorecord; + + bool sv_execute_demo_start_command_internal(sv_client_num_t client_num, std::string_view file_name, bool overwrite); + + class server_rotation_t + { + struct data_t + { + bool svr_restart_bit{}; + std::size_t map_hash{}; + std::size_t gametype_hash{}; + }; + + public: + [[nodiscard]] bool has_changed() + { + const auto svr_restart_bit = static_cast(game::mp::svs->snapFlagServerBit & 4); + const auto has_value = data_.has_value(); + + if (!has_value || data_->svr_restart_bit != svr_restart_bit) + { + const auto dvar_mapname = get_mapname(false); + const auto dvar_gametype = get_gametype(false); + + if (dvar_mapname.empty() || dvar_gametype.empty()) + { + console::error("the server demo recording code may not function properly if dvars mapname or g_gametype are not available\n"); + } + + const auto map_hash = std::hash()(dvar_mapname); + const auto gametype_hash = std::hash()(dvar_gametype); + + if (!has_value || (data_->map_hash != map_hash || data_->gametype_hash != gametype_hash)) + { + data_.emplace(data_t{ + .svr_restart_bit = svr_restart_bit, + .map_hash = map_hash, + .gametype_hash = gametype_hash + }); + + return has_value; + } + + // server has not rotated despite restart bit change + data_->svr_restart_bit = svr_restart_bit; + } + + return false; + } + + private: + std::optional data_; + }; + + struct client_data_t + { + private: + struct stored_times_t + { + std::int32_t first_svr_time{}; + std::int32_t cur_svr_time{}; + std::optional last_conn_time; + }; + + public: + [[nodiscard]] bool is_buffer_active() const + { + return buffer_active; + } + + [[nodiscard]] bool is_recording() const + { + return file_active; + } + + bool buffer_active{}; + bool file_active{}; + stored_times_t times; + buffer_t buffer; + std::ofstream file; + }; + + struct sv_demo_recordings_t + { + public: + sv_demo_recordings_t() = default; + ~sv_demo_recordings_t(); + sv_demo_recordings_t(const sv_demo_recordings_t&) = delete; + sv_demo_recordings_t(sv_demo_recordings_t&&) noexcept = delete; + sv_demo_recordings_t& operator=(const sv_demo_recordings_t&) = delete; + sv_demo_recordings_t& operator=(sv_demo_recordings_t&&) noexcept = delete; + + [[nodiscard]] bool is_recording(sv_client_num_t client_num) const; + [[nodiscard]] bool start(sv_client_num_t client_num, const std::filesystem::path& path); + [[nodiscard]] bool stop(sv_client_num_t client_num); + + void write_data(sv_client_num_t client_num, const game::mp::client_t& cl, + std::span network_data, bool client_loading); + void shutdown_watcher(); + + private: + [[nodiscard]] const client_data_t& get_client(sv_client_num_t client_num) const; + [[nodiscard]] client_data_t& get_client(sv_client_num_t client_num); + + bool stop_internal(client_data_t& client, bool reset_client); + bool update_state(sv_client_num_t client_num, std::int32_t last_conn_time, bool client_loading); + void process_old_data(); + + server_rotation_t svr_rotation_data_; + std::array client_data_{}; + } demos; + + sv_demo_recordings_t::~sv_demo_recordings_t() + { + process_old_data(); + } + + const client_data_t& sv_demo_recordings_t::get_client(sv_client_num_t client_num) const + { + assert(client_num.valid()); + + return client_data_[client_num.value()]; + } + + client_data_t& sv_demo_recordings_t::get_client(sv_client_num_t client_num) + { + assert(client_num.valid()); + + return client_data_[client_num.value()]; + } + + bool sv_demo_recordings_t::is_recording(sv_client_num_t client_num) const + { + return client_num.valid() && get_client(client_num).is_recording(); + } + + bool sv_demo_recordings_t::start(sv_client_num_t client_num, const std::filesystem::path& path) + { + if (!client_num.valid()) + { + return false; + } + + auto& client = get_client(client_num); + + if (client.is_buffer_active()) + { + if (!client.is_recording()) + { + std::ofstream file(path, std::ios::binary); + + if (file.good() && file.is_open()) + { + const auto buffer = client.buffer.get(); + + client.file = std::move(file); + client.file.write(reinterpret_cast(buffer.data()), buffer.size()); + client.file.flush(); + client.file_active = true; + + return true; + } + } + } + else + { + console::error("client pid %d cannot be recorded at this time; sv_demos was possibly enabled too late!\n", client_num.value()); + } + + return false; + } + + bool sv_demo_recordings_t::stop_internal(client_data_t& client, bool reset_client) + { + if (client.is_recording()) + { + write_general_footer(client.file, client.times.first_svr_time, client.times.cur_svr_time); + write_end_of_file(client.file); + + client.file.flush(); + client.file.close(); + client.file_active = {}; + } + + if (reset_client) + { + client.buffer_active = {}; + client.times = {}; + client.buffer.clear(); + } + + return client.file.good() && !client.file.is_open(); + } + + bool sv_demo_recordings_t::stop(sv_client_num_t client_num) + { + return client_num.valid() && stop_internal(get_client(client_num), false); + } + + void sv_demo_recordings_t::process_old_data() + { + for (auto& client : client_data_) + { + stop_internal(client, true); + } + + svr_rotation_data_ = {}; + } + + void sv_demo_recordings_t::shutdown_watcher() + { + auto execute_on_shutdown = [this, sv_loaded = false]() mutable + { + if (!sv_loaded && game::SV_Loaded()) + { + sv_loaded = true; + } + + if (sv_loaded && !game::SV_Loaded()) + { + sv_loaded = false; + process_old_data(); + } + }; + + scheduler::loop(execute_on_shutdown, scheduler::main); + } + + bool sv_demo_recordings_t::update_state(sv_client_num_t client_num, std::int32_t last_conn_time, bool client_loading) + { + // handle server map change: close files and reset data for all clients; should ignore fast restarts + if (svr_rotation_data_.has_changed()) + { + process_old_data(); + } + + auto& client = get_client(client_num); + if (!client_loading && !client.is_buffer_active()) + { + // sv_demos was possibly enabled too late for this player for this match! + return false; + } + + // handle client last connect time change: close file and reset data + if (!client.times.last_conn_time || *client.times.last_conn_time != last_conn_time) + { + if (client.times.last_conn_time) + { + stop_internal(client, true); + } + + client.times.last_conn_time = last_conn_time; + } + + // write general, mod and map headers to the buffer once + if (!client.is_buffer_active()) + { + // reserve memory (low size if auto recording is enabled, because then the demo writes almost directly to disk) + const auto reserve_size = (sv_demo_autorecord && sv_demo_autorecord->current.enabled) ? 8192 : 512 * 1024; + client.buffer.reserve_memory(reserve_size); + + sv_write_general_header(client.buffer, game::mp::svs->clients[client_num.value()].name); + write_mod_header(client.buffer); + + if (write_map_header(client.buffer)) + { + client.buffer_active = true; + } + } + + // store first and current server times to keep track of demo length + client.times.cur_svr_time = game::mp::svs->time; + if (!client_loading && !client.times.first_svr_time && client.times.cur_svr_time) + { + client.times.first_svr_time = client.times.cur_svr_time; + } + + // if not already recording, start demo for client when auto recording is enabled + if (!client.is_recording() && sv_demo_autorecord && sv_demo_autorecord->current.enabled) + { + if (!sv_execute_demo_start_command_internal(client_num, std::string_view{}, false)) + { + console::error("failed to start demo automatically for client num %d\n", client_num.value()); + } + } + + return true; + } + + void sv_demo_recordings_t::write_data(sv_client_num_t client_num, + const game::mp::client_t& cl, std::span network_data, bool client_loading) + { + if (!client_num.valid() || !update_state(client_num, cl.lastConnectTime, client_loading)) + { + return; + } + + auto write_data_internal = [&cl, network_data, client_loading](auto& output) + { + const auto send_msg_count = static_cast(cl.header.sendMessageCount); + const auto svr_msg_sequence = static_cast(cl.header.netchan.outgoingSequence); + + if (!client_loading) + { + sv_write_predicted_data(output, cl.gentity->client->ps, send_msg_count); + } + + write_id_and_size(output, 4 + network_data.size(), demo_data_id::network_data); + output.write(reinterpret_cast(&svr_msg_sequence), 4); + output.write(reinterpret_cast(network_data.data()), network_data.size()); + }; + + auto& client = get_client(client_num); + + if (client.is_buffer_active()) + { + write_data_internal(client.buffer); + } + if (client.is_recording()) + { + write_data_internal(client.file); + + if (static_cast(cl.header.sendMessageCount) % 512 == 0) + { + client.file.flush(); + } + } + } +} + +namespace // hooks +{ + utils::hook::detour SV_Netchan_Transmit_hook; + + bool sv_capture_data(game::mp::client_t& cl, const char* data, std::int32_t length) + { + const auto valid_player = cl.gentity && cl.gentity->client && cl.testClient == game::TC_NONE + && (cl.header.state == game::CS_CLIENTLOADING || cl.header.state == game::CS_ACTIVE); + + if (sv_demos && sv_demos->current.enabled && valid_player) + { + const auto client_num = sv_client_num_t(static_cast(cl.gentity->s.number)); + + if (!client_num.valid()) + { + console::error("invalid client num %d\n", cl.gentity->s.number); + } + else + { + const auto client_loading = (cl.header.state == game::CS_CLIENTLOADING); + const auto size = static_cast(length); + + if ((!client_loading || cl.gamestateMessageNum == cl.header.netchan.outgoingSequence) && size < MAX_SIZE) + { + const std::span network_data(reinterpret_cast(data), size); + demos.write_data(client_num, cl, network_data, client_loading); + } + else + { + console::warn("invalid data for client num %d, message count %d, message size %zu\n", + client_num.value(), cl.header.sendMessageCount, size); + } + } + } + + return SV_Netchan_Transmit_hook.invoke(&cl, data, length); + } +} + +namespace // command execution +{ + bool sv_execute_demo_start_command_internal(sv_client_num_t client_num, std::string_view file_name, bool overwrite) + { + if (game::Live_SyncOnlineDataFlags(0)) + { + console::error("server must be initialized to record a demo\n"); + return false; + } + + if (const auto* sv_running = game::Dvar_FindVar("sv_running"); !sv_running || !sv_running->current.enabled) + { + console::error("server must be online to record a demo\n"); + return false; + } + + if (!sv_demos || !sv_demos->current.enabled) + { + console::error("cannot record a demo with sv_demos disabled\n"); + return false; + } + + if (!client_num.valid()) + { + console::error("invalid client num\n"); + return false; + } + + const auto state = game::mp::svs->clients[client_num.value()].header.state; + if (state != game::CS_CLIENTLOADING && state != game::CS_ACTIVE) + { + console::error("client needs to be fully connected\n"); + return false; + } + + const auto client_type = game::mp::svs->clients[client_num.value()].testClient; + if (client_type != game::TC_NONE) + { + console::error("can only record actual players\n"); + return false; + } + + if (demos.is_recording(client_num)) + { + console::error("already recording client\n"); + return false; + } + + const auto opt_dir_path = sv_create_demo_directory(); + if (!opt_dir_path) + { + console::error("could not create demo directory\n"); + return false; + } + + const auto& cl = game::mp::svs->clients[client_num.value()]; + const auto empty_guid = (cl.playerGuid[0] == '\0'); + if (empty_guid) + { + console::warn("player guid appears empty\n"); + } + + const auto opt_server_path = (file_name.empty()) + ? sv_create_path_server_demo(*opt_dir_path, (!empty_guid) ? cl.playerGuid : utils::string::strip(cl.name, true)) + : sv_create_path_server_demo(*opt_dir_path, file_name, overwrite); + + if (!opt_server_path) + { + console::error("could not create demo file\n"); + return false; + } + + if (!demos.start(client_num, *opt_server_path)) + { + console::error("could not create demo file %s\n", opt_server_path->string().c_str()); + return false; + } + + return true; + } + + void sv_execute_demo_start_command(const command::params& params) + { + if (params.size() < 2 || params.size() > 4 || (params.size() == 4 && std::string_view(params.get(3)) != "overwrite")) + { + console::info("usage: sv_demostart , (optional), overwrite(optional)\n"); + return; + } + + assert(params.size() != 4 || (params.size() == 4 && std::string_view(params.get(3)) == "overwrite")); + sv_execute_demo_start_command_internal( + sv_client_num_t(params.get(1)), (params.size() >= 3) ? params.get(2) : std::string_view{}, (params.size() == 4)); + } + + void sv_execute_demo_stop_command(const command::params& params) + { + if (params.size() != 2) + { + console::info("usage: sv_demostop \n"); + return; + } + + const sv_client_num_t client_num(params.get(1)); + if (!client_num.valid()) + { + console::error("invalid client num %s\n", params.get(1)); + } + else if (!demos.stop(client_num)) + { + console::error("could not stop demo\n"); + } + } +} + +namespace demo_sv_recording +{ + bool sv_recording(std::size_t client_num) + { + return demos.is_recording(sv_client_num_t(client_num)); + } + + bool sv_startrecord(std::size_t client_num, std::string_view file_name, bool overwrite) + { + return sv_execute_demo_start_command_internal(sv_client_num_t(client_num), file_name, overwrite); + } + + bool sv_stoprecord(std::size_t client_num) + { + return demos.stop(sv_client_num_t(client_num)); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_dedi()) + { + return; + } + + // check run-time address assertions + check_address_assertions(); + + // capture outgoing packets to client + SV_Netchan_Transmit_hook.create(game::SV_Netchan_Transmit, sv_capture_data); + + // execute server demo code based on this value; if it's is enabled mid-match, + // then the demos recorded during that match (if any) are likely corrupt! + sv_demos = game::Dvar_RegisterBool( + "sv_demos", false, game::DVAR_FLAG_NONE, "Enable server demos"); + + // add support to auto record all players + sv_demo_autorecord = game::Dvar_RegisterBool( + "sv_demoautorecord", false, game::DVAR_FLAG_NONE, + "Automatically start recording a demo for each connected client."); + + // add console command support to record server demos + command::add("sv_demostart", sv_execute_demo_start_command); + command::add("sv_demostop", sv_execute_demo_stop_command); + + scheduler::on_game_initialized([]() + { + demos.shutdown_watcher(); + + // check if demo directory exists / could be created + if (!sv_can_create_demo_directory()) + { + console::error("could not create demo directory\n"); + } + }, scheduler::main); + } + }; +} + +REGISTER_COMPONENT(demo_sv_recording::component) diff --git a/src/client/component/demo_sv_recording.hpp b/src/client/component/demo_sv_recording.hpp new file mode 100644 index 0000000..d0e4c6d --- /dev/null +++ b/src/client/component/demo_sv_recording.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace demo_sv_recording +{ + [[nodiscard]] bool sv_recording(std::size_t client_num); + [[nodiscard]] bool sv_startrecord(std::size_t client_num, std::string_view file_name = {}, bool overwrite = {}); + [[nodiscard]] bool sv_stoprecord(std::size_t client_num); +} diff --git a/src/client/component/demo_timescale.cpp b/src/client/component/demo_timescale.cpp new file mode 100644 index 0000000..dc37194 --- /dev/null +++ b/src/client/component/demo_timescale.cpp @@ -0,0 +1,139 @@ +#include "std_include.hpp" +#include "loader/component_loader.hpp" + +#include "demo_playback.hpp" + +#include "fps.hpp" +#include "game/game.hpp" +#include "utils/hook.hpp" +#include "utils/string.hpp" + +namespace demo_timescale +{ + namespace + { + utils::hook::detour Com_TimeScaleMsec_hook; + + game::dvar_t* demo_timescale; + + void generate_pattern(std::array& pattern, std::size_t ones_count, std::size_t zeros_count) + { + assert(ones_count + zeros_count == pattern.size()); + + for (std::size_t i = 0, zeros = 1, ones = 1; i < pattern.size(); ++i) + { + if (ones * zeros_count < zeros * ones_count) + { + ++ones; + pattern[i] = 1; + } + else + { + ++zeros; + pattern[i] = 0; + } + } + + assert(std::accumulate(pattern.begin(), pattern.end(), 0) > 0); + } + + void calculate_pattern(std::array& pattern, float fps, float timescale) + { + // Com_TimeScaleMsec is called once per frame, so the number of calls it takes to advance 1000 ms + // can be calculated by using the following formula: fps / timescale + + // example: 500 fps and 0.01 timescale -> 500 / 0.01 = 50'000 + // a pattern needs to be generated where 1000 * 1ms and 49'000 * 0ms are interleaved, + // and fit in an array of size 1000 + + const auto call_count_per_sec = static_cast(std::clamp(fps / timescale, 1000.0f, 1'000'000.0f)); + const auto ones_count = static_cast(pattern.size() / (call_count_per_sec / static_cast(pattern.size()))); + const auto zeros_count = pattern.size() - ones_count; + + generate_pattern(pattern, ones_count, zeros_count); + } + + std::int32_t Com_TimeScaleMsec_stub(std::int32_t msec) + { + if (!demo_playback::playing() || !demo_timescale || demo_timescale->current.value == 1.0f) + { + return Com_TimeScaleMsec_hook.invoke(msec); + } + if (demo_timescale->current.value == 0.0f) + { + return 0; // pause game + } + + // the code below generates a pattern of interleaved 0s and 1s based on calculated avg fps + // a new pattern is generated every 1000 frames, or after timescale or maxfps changes + // the pattern determines the speed at which the game advances + + const auto timescale = demo_timescale->current.value; + const auto avg_fps = fps::get_avg_fps(); + const auto frame_time = timescale * 1000.0f / avg_fps; + const auto* com_maxfps = game::Dvar_FindVar("com_maxfps"); + const auto maxfps = (com_maxfps) ? com_maxfps->current.integer : static_cast(avg_fps); + + static auto last_timescale = timescale; + static auto last_maxfps = maxfps; + static std::size_t pattern_index; + static std::array pattern; + + if (last_timescale != timescale || last_maxfps != maxfps) + { + last_timescale = timescale; + last_maxfps = maxfps; + + // update pattern using the maxfps instead of avg fps for now + calculate_pattern(pattern, static_cast(maxfps), timescale); + + // update the pattern again in the near future when the average fps is more accurate + pattern_index = 95 * pattern.size() / 100; + } + + if (frame_time > 1.0f) + { + const auto i_frame_time = static_cast(frame_time); + const auto ones_count = static_cast(frame_time * 1000 - i_frame_time * 1000); + if (ones_count <= 1) + { + return i_frame_time; + } + + if (pattern_index % pattern.size() == 0) + { + const auto zeros_count = pattern.size() - ones_count; + generate_pattern(pattern, ones_count, zeros_count); + } + + return i_frame_time + pattern[pattern_index++ % pattern.size()]; + } + else if (pattern_index % pattern.size() == 0) + { + calculate_pattern(pattern, avg_fps, timescale); + } + + // advance (1ms) or pause (0ms) based on the pattern + return pattern[pattern_index++ % pattern.size()]; + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + // add timescale support for demo playback + Com_TimeScaleMsec_hook.create(game::Com_TimeScaleMsec, Com_TimeScaleMsec_stub); + demo_timescale = game::Dvar_RegisterFloat( + "demotimescale", 1.0f, 0.0f, 1000.0f, game::DVAR_FLAG_NONE, "Set playback speed for demos"); + } + }; +} + +REGISTER_COMPONENT(demo_timescale::component) diff --git a/src/client/component/demo_utils.cpp b/src/client/component/demo_utils.cpp new file mode 100644 index 0000000..0df1890 --- /dev/null +++ b/src/client/component/demo_utils.cpp @@ -0,0 +1,1526 @@ +#include "std_include.hpp" + +#include "demo_utils.hpp" + +#include "console.hpp" +#include "party.hpp" +#include "utils/compression.hpp" +#include "utils/string.hpp" + +namespace demo_utils +{ + namespace types + { + std::size_t buffer_t::size() const + { + return buffer_.size(); + } + + std::span buffer_t::get() const + { + return std::span(buffer_); + } + + void buffer_t::reserve_memory(std::size_t size) + { + buffer_.reserve(size); + } + + void buffer_t::write(const char* src, std::size_t size) + { + buffer_.insert(buffer_.end(), src, src + size); + } + + void buffer_t::clear() + { + buffer_.clear(); + } + + demo_data_id operator|(demo_data_id lhs, demo_data_id rhs) + { + using type = std::underlying_type_t; + return demo_data_id(static_cast(lhs) | static_cast(rhs)); + } + + demo_data_id operator&(demo_data_id lhs, demo_data_id rhs) + { + using type = std::underlying_type_t; + return demo_data_id(static_cast(lhs) & static_cast(rhs)); + } + + demo_data_id operator~(demo_data_id value) + { + using type = std::underlying_type_t; + return demo_data_id(~static_cast(value)); + } + } + + namespace gen_utilities + { + std::string_view get_dvar_string(std::string_view dvar_name, bool valid_string) + { + const auto* dvar = game::Dvar_FindVar(dvar_name.data()); + + if (!dvar || !dvar->current.string || dvar->current.string[0] == '\0') + { + return (valid_string) + ? std::string_view("") + : std::string_view(); + } + + return dvar->current.string; + } + + std::string_view get_mod_directory() + { + return get_dvar_string("fs_game", true); + } + + std::string_view get_base_path(bool default_string) + { + const auto string = get_dvar_string("fs_basepath", true); + if (string.empty() && default_string) + { + return "unknown_basepath"; + } + + return string; + } + + std::string_view get_mapname(bool default_string) + { + const auto string = get_dvar_string("mapname", true); + if (string.empty() && default_string) + { + return "unknown_mapname"; + } + + return string; + } + + std::string get_shortened_mapname_lowercase(bool default_string) + { + const auto mapname = get_mapname(default_string); + if (mapname.starts_with("mp_")) + { + return utils::string::to_lower(std::string(mapname.begin() + 3, mapname.end())); + } + + return utils::string::to_lower(std::string(mapname)); + } + + std::string get_mapname_lowercase(bool default_string) + { + return utils::string::to_lower(std::string(get_mapname(default_string))); + } + + std::string_view get_gametype(bool default_string) + { + const auto string = get_dvar_string("g_gametype", true); + if (string.empty() && default_string) + { + return "unknown_gametype"; + } + + return string; + } + + std::string get_gametype_lowercase(bool default_string) + { + return utils::string::to_lower(std::string(get_gametype(default_string))); + } + + std::string_view get_ui_mapname() + { + static constexpr std::string_view unknown_mapname = "unknown_ui_mapname"; + + const auto dvar_mapname = get_mapname(false); + if (dvar_mapname.empty()) + { + return unknown_mapname; + } + + const auto* ui_mapname = game::UI_LocalizeMapname(dvar_mapname.data()); + if (!ui_mapname) + { + return unknown_mapname; + } + + const auto mapname = std::string_view(ui_mapname); + if (mapname.empty() || mapname.size() >= 64) + { + return unknown_mapname; + } + + return mapname; + } + + std::string_view get_ui_gametype() + { + static constexpr std::string_view unknown_gametype = "unknown_ui_gametype"; + + const auto dvar_gametype = get_gametype(false); + if (dvar_gametype.empty()) + { + return unknown_gametype; + } + + const auto* ui_gametype = game::UI_LocalizeGametype(dvar_gametype.data()); + if (!ui_gametype) + { + return unknown_gametype; + } + + const auto gametype = std::string_view(ui_gametype); + if (gametype.empty() || gametype.size() >= 64) + { + return unknown_gametype; + } + + return gametype; + } + + std::string get_datetime() + { + const auto now = std::chrono::system_clock::now(); + const auto time_zone = std::chrono::current_zone(); + const auto local_time = time_zone->to_local(now); + const auto time_point = std::chrono::time_point_cast(local_time); + const auto yy_mm_dd = std::chrono::year_month_day(time_point); + const auto hh_mm_ss = std::chrono::hh_mm_ss + (std::chrono::duration_cast(local_time.time_since_epoch()) % std::chrono::days(1)); + + return std::format("[{:04}.{:02}.{:02}][{:02}.{:02}.{:02}]", + yy_mm_dd.year().operator int(), + yy_mm_dd.month().operator unsigned int(), + yy_mm_dd.day().operator unsigned int(), + hh_mm_ss.hours().count(), + hh_mm_ss.minutes().count(), + hh_mm_ss.seconds().count() + ); + } + } + + namespace file_directory + { + std::optional create_directory_user_demo() + { + const auto fs_basepath = get_base_path(false); + if (fs_basepath.empty()) + { + console::error("could not find fs_basepath\n"); + return std::nullopt; + } + + const std::optional opt( + std::filesystem::path(fs_basepath) / std::format("demos/client/{}/user", get_mod_directory()) + ); + + const auto& path = *opt; + if (!std::filesystem::exists(path) && !std::filesystem::create_directories(path)) + { + console::error("could not create user demo directory %s\n", path.string().c_str()); + return std::nullopt; + } + + return opt; + } + + std::optional create_directory_auto_demo() + { + const auto fs_basepath = get_base_path(false); + if (fs_basepath.empty()) + { + console::error("could not find fs_basepath\n"); + return std::nullopt; + } + + const std::optional opt( + std::filesystem::path(fs_basepath) / std::format("demos/client/{}/auto", get_mod_directory()) + ); + + const auto& path = *opt; + if (!std::filesystem::exists(path) && !std::filesystem::create_directories(path)) + { + console::error("could not create auto demo directory %s\n", path.string().c_str()); + return std::nullopt; + } + + return opt; + } + + bool can_create_demo_directories() + { + return create_directory_user_demo() && create_directory_auto_demo(); + } + + std::optional create_path_user_demo(const std::filesystem::path& dir_path) + { + const auto mapname = get_shortened_mapname_lowercase(true); + + for (std::size_t index = 0; index < 10'000; ++index) + { + if (index < 5000 && index % 100 == 0) + { + // look ahead + const auto demo_path = dir_path / std::format("{}.{:04}{}", mapname, index + 100, DEMO_EXTENSION); + + if (std::filesystem::exists(demo_path)) + { + index += 99; + continue; + } + } + + const std::optional demo_path = + dir_path / std::format("{}.{:04}{}", mapname, index, DEMO_EXTENSION); + + if (std::filesystem::exists(*demo_path)) + { + continue; + } + + return demo_path; + } + + return std::nullopt; + } + + std::optional create_path_user_demo( + const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite) + { + const std::optional demo_path = dir_path / std::format("{}{}", demo_name, DEMO_EXTENSION); + if (!overwrite && std::filesystem::exists(*demo_path)) + { + return std::nullopt; + } + + return demo_path; + } + + std::optional create_path_auto_demo(const std::filesystem::path& dir_path) + { + const auto mapname = get_shortened_mapname_lowercase(true); + + const std::optional demo_path = + dir_path / std::format("{}[{}]{}", get_datetime(), mapname, DEMO_EXTENSION); + + if (std::filesystem::exists(*demo_path)) + { + return std::nullopt; + } + + return demo_path; + } + } + + namespace file_directory_server + { + std::optional sv_create_demo_directory() + { + const auto fs_basepath = get_base_path(false); + if (fs_basepath.empty()) + { + console::error("could not find fs_basepath\n"); + return std::nullopt; + } + + const std::optional result( + std::filesystem::path(fs_basepath) / std::format("demos/server/{}/", get_mod_directory()) + ); + + const auto& svr_dir_path = *result; + if (!std::filesystem::exists(svr_dir_path) && !std::filesystem::create_directories(svr_dir_path)) + { + console::error("could not create demos directory %s\n", svr_dir_path.string().c_str()); + return std::nullopt; + } + + return result; + } + + bool sv_can_create_demo_directory() + { + return sv_create_demo_directory().has_value(); + } + + std::optional sv_create_path_server_demo( + const std::filesystem::path& dir_path, std::string_view client_id) + { + const auto mapname = get_shortened_mapname_lowercase(true); + + const std::optional demo_path = (!client_id.empty()) + ? dir_path / std::format("{}[{}][{}]{}", get_datetime(), mapname, client_id, DEMO_EXTENSION) + : dir_path / std::format("{}[{}]{}", get_datetime(), mapname, DEMO_EXTENSION); + + if (std::filesystem::exists(*demo_path)) + { + return std::nullopt; + } + + return demo_path; + } + + std::optional sv_create_path_server_demo( + const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite) + { + const std::optional demo_path = dir_path / std::format("{}{}", demo_name, DEMO_EXTENSION); + if (!overwrite && std::filesystem::exists(*demo_path)) + { + return std::nullopt; + } + + return demo_path; + } + } + + namespace serialization + { + // controllable vehicles - killstreak rewards + namespace + { + const auto hash_gryphon = std::hash()("remote_uav_mp"); + bool is_gryphon_ks_reward(std::string_view name) + { + return hash_gryphon == std::hash()(name); + } + + const auto hash_helo_pilot = std::hash()("heli_pilot_mp"); + bool is_helo_pilot_ks_reward(std::string_view name) + { + return hash_helo_pilot == std::hash()(name); + } + + const auto hash_odin = std::hash()("odin_mp"); + bool is_odin_ks_reward(std::string_view name) + { + return hash_odin == std::hash()(name); + } + + predicted_data_id get_predicted_vehicle_type(const game::mp::playerState_t& ps) + { + const auto** vehicle_def = reinterpret_cast(game::CG_GetVehicleDef(&ps)); + const auto* vehicle_name = (vehicle_def) ? *vehicle_def : nullptr; + + if (vehicle_name) + { + if (is_gryphon_ks_reward(vehicle_name)) + { + return predicted_data_id::vehicle_gryphon; + } + else if (is_helo_pilot_ks_reward(vehicle_name)) + { + return predicted_data_id::vehicle_helo_pilot; + } + else if (is_odin_ks_reward(vehicle_name)) + { + return predicted_data_id::vehicle_odin; + } + } + + assert(false); + return predicted_data_id::vehicle_unknown; + } + } + + void write_id_and_size_(auto& output, std::size_t size, demo_data_id id) + { + assert(id <= demo_data_id::eof); + + if (size < 256) + { + const auto complete_id = id | demo_data_id::one_byte_flag; + output.write(reinterpret_cast(&complete_id), 1); + output.write(reinterpret_cast(&size), 1); + } + else + { + const auto complete_id = id; + output.write(reinterpret_cast(&complete_id), 1); + output.write(reinterpret_cast(&size), 4); + } + } + + void write_id_and_size(buffer_t& output, std::size_t size, demo_data_id id) + { + write_id_and_size_(output, size, id); + } + + void write_id_and_size(std::ofstream& output, std::size_t size, demo_data_id id) + { + write_id_and_size_(output, size, id); + } + + void write_network_data_(auto& output, std::span network_data) + { + write_id_and_size(output, network_data.size(), demo_data_id::network_data); + output.write(reinterpret_cast(network_data.data()), network_data.size()); + } + + void write_network_data(buffer_t& output, std::span network_data) + { + write_network_data_(output, network_data); + } + + void write_network_data(std::ofstream& output, std::span network_data) + { + write_network_data_(output, network_data); + } + + void write_predicted_player_data_(auto& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + write_id_and_size(output, 50, demo_data_id::predicted_data); + output.write(reinterpret_cast(&id), 1); + output.write(reinterpret_cast(&cad_index), 1); + + output.write(reinterpret_cast(&cad.serverTime), 4); + output.write(reinterpret_cast(&cad.origin[0]), 12); + output.write(reinterpret_cast(&cad.velocity[0]), 12); + output.write(reinterpret_cast(&cad.bobCycle), 4); + output.write(reinterpret_cast(&cad.movementDir), 4); + output.write(reinterpret_cast(&viewangles[0]), 12); + } + + void write_predicted_player_data(buffer_t& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + write_predicted_player_data_(output, cad, viewangles, cad_index, id); + } + + void write_predicted_player_data(std::ofstream& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + write_predicted_player_data_(output, cad, viewangles, cad_index, id); + } + + void write_predicted_vehicle_data_(auto& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + const auto size = (id != predicted_data_id::vehicle_gryphon) ? 58 : 82; + + write_id_and_size(output, size, demo_data_id::predicted_data); + output.write(reinterpret_cast(&id), 1); + output.write(reinterpret_cast(&cad_index), 1); + + const auto& vehicle = cad.playerVehStateClientArchive; + output.write(reinterpret_cast(&cad.serverTime), 4); + output.write(reinterpret_cast(&cad.origin[0]), 12); + output.write(reinterpret_cast(&viewangles[0]), 12); + output.write(reinterpret_cast(&vehicle.flags), 4); + output.write(reinterpret_cast(&vehicle.origin[0]), 12); + output.write(reinterpret_cast(&vehicle.angles[0]), 12); + + if (id == predicted_data_id::vehicle_gryphon) + { + output.write(reinterpret_cast(&vehicle.velocity[0]), 12); + output.write(reinterpret_cast(&vehicle.angVelocity[0]), 12); + } + + assert(!vehicle.splineId); + } + + void write_predicted_vehicle_data(buffer_t& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + write_predicted_vehicle_data_(output, cad, viewangles, cad_index, id); + } + + void write_predicted_vehicle_data(std::ofstream& output, const game::ClientArchiveData& cad, + const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id) + { + write_predicted_vehicle_data_(output, cad, viewangles, cad_index, id); + } + + void write_helo_pilot_turret_fire_(auto& output, std::uint8_t fire_count) + { + static constexpr auto id = predicted_data_id::vehicle_helo_pilot_turret_fire; + + const auto snap_svr_time = game::mp::cl->snap.serverTime; + + write_id_and_size(output, 6, demo_data_id::predicted_data); + output.write(reinterpret_cast(&id), 1); + output.write(reinterpret_cast(&snap_svr_time), 4); + output.write(reinterpret_cast(&fire_count), 1); + } + + void write_helo_pilot_turret_fire(buffer_t& output, std::uint8_t fire_count) + { + write_helo_pilot_turret_fire_(output, fire_count); + } + + void write_helo_pilot_turret_fire(std::ofstream& output, std::uint8_t fire_count) + { + write_helo_pilot_turret_fire_(output, fire_count); + } + + void write_mod_header_(auto& output) + { + const auto mod = get_mod_directory(); + if (mod.size()) + { + write_id_and_size(output, mod.size() + 1, demo_data_id::mod_header); + + static constexpr auto null_terminator = '\0'; + output.write(mod.data(), mod.size()); + output.write(&null_terminator, 1); + } + } + + void write_mod_header(buffer_t& output) + { + write_mod_header_(output); + } + + void write_mod_header(std::ofstream& output) + { + write_mod_header_(output); + } + + bool write_map_header_(auto& output) + { + const auto mapname = get_mapname_lowercase(false); + const auto gametype = get_gametype_lowercase(false); + + const auto size = mapname.size() + gametype.size() + 2; + if (mapname.empty() || gametype.empty() || size < 4 || size > 127) + { + return false; + } + + write_id_and_size(output, size, demo_data_id::map_header); + + static constexpr auto null_terminator = '\0'; + output.write(mapname.c_str(), mapname.size()); + output.write(&null_terminator, 1); + output.write(gametype.c_str(), gametype.size()); + output.write(&null_terminator, 1); + + return true; + } + + bool write_map_header(buffer_t& output) + { + return write_map_header_(output); + } + + bool write_map_header(std::ofstream& output) + { + return write_map_header_(output); + } + + bool write_gamestate_data_(auto& output) + { + const auto& persistent_data = get_persistent_data(); + const auto& gs = game::mp::cls->gameState; + const auto svr_cmd_seq = game::mp::clc->serverCommandSequence; + const auto strings_size = static_cast(gs.dataCount); + + assert(is_gamestate_valid(game::mp::cls->gameState, false)); + if (!strings_size || strings_size >= sizeof(game::gameState_t::stringData)) + { + return false; + } + + const std::span sd( + reinterpret_cast(&gs.stringData[0]), strings_size); + const std::span so( + reinterpret_cast(&gs.stringOffsets[0]), sizeof(game::gameState_t::stringOffsets)); + + const auto string_data = utils::compression::zlib::compress(sd, 2048); + const auto string_offsets = utils::compression::zlib::compress(so, 1024); + + assert(std::equal(sd.begin(), sd.end(), + utils::compression::zlib::decompress(string_data, 4096).begin())); + assert(std::equal(so.begin(), so.end(), + utils::compression::zlib::decompress(string_offsets, sizeof(game::gameState_t::stringOffsets)).begin())); + + if (string_data.empty() || string_data.size() >= sizeof(game::gameState_t::stringData)) + { + return false; + } + if (string_offsets.empty() || string_offsets.size() > sizeof(game::gameState_t::stringOffsets)) + { + return false; + } + + const packed_gamestate_sizes pgs{ + .string_data = string_data.size(), + .string_offsets = string_offsets.size(), + .compressed = 1 + }; + + assert(pgs.string_data == string_data.size() && pgs.string_offsets == string_offsets.size()); + const auto size = 12 + string_data.size() + string_offsets.size() + sizeof(persistent_data_t); + + write_id_and_size(output, size, demo_data_id::update_gamestate_data); + output.write(reinterpret_cast(&svr_cmd_seq), 4); + output.write(reinterpret_cast(&pgs), 8); + output.write(reinterpret_cast(string_data.data()), string_data.size()); + output.write(reinterpret_cast(string_offsets.data()), string_offsets.size()); + output.write(reinterpret_cast(&persistent_data), sizeof(persistent_data_t)); + + return true; + } + + bool write_gamestate_data(buffer_t& output) + { + return write_gamestate_data_(output); + } + + bool write_gamestate_data(std::ofstream& output) + { + return write_gamestate_data_(output); + } + + void write_predicted_data_(auto& output) + { + const auto& ps = game::mp::cg->ps; + const auto& viewangles = game::mp::cg->refdefViewAngles; + const auto cad_index = static_cast(game::mp::cl->clientArchiveIndex - 1); + const auto cad_array = std::span(game::mp::cl->clientArchive); + + if (const auto& cad = cad_array[cad_index]; cad.serverTime) + { + const auto vehicle_in_use = (ps.vehicleState.entity && ps.vehicleState.entity != 2047); + if (!vehicle_in_use) + { + write_predicted_player_data(output, cad, viewangles, cad_index, predicted_data_id::player); + } + else + { + const auto data_type = get_predicted_vehicle_type(ps); + write_predicted_vehicle_data(output, cad, viewangles, cad_index, data_type); + } + } + } + + void write_predicted_data(buffer_t& output) + { + write_predicted_data_(output); + } + + void write_predicted_data(std::ofstream& output) + { + write_predicted_data_(output); + } + + void write_general_header_internal(auto& output, + std::string_view demo_description, std::string_view svr_name_sv, std::string_view player_name_sv) + { + const auto datetime = get_datetime(); + + const auto protocol = std::to_string(PROTOCOL); + const auto* build_num_ptr = game::LiveStorage_FetchFFotD(); + const auto build_num = (build_num_ptr) + ? std::string_view(build_num_ptr) + : "unknown_build_number"; + + const auto svr_name = utils::string::strip(svr_name_sv, true); + const auto player_name = utils::string::strip(player_name_sv, true); + + const auto ui_mapname = get_ui_mapname(); + const auto ui_gametype = get_ui_gametype(); + + const auto size = datetime.size() + 1 + + demo_description.size() + 1 + + DEMO_CODE_VERSION.size() + 1 + + protocol.size() + 1 + + build_num.size() + 1 + + svr_name.size() + 1 + + player_name.size() + 1 + + ui_mapname.size() + 1 + + ui_gametype.size() + 1; + + write_id_and_size(output, size, demo_data_id::gen_header); + + static constexpr auto null_terminator = '\0'; + output.write(datetime.c_str(), datetime.size()); + output.write(&null_terminator, 1); + output.write(demo_description.data(), demo_description.size()); + output.write(&null_terminator, 1); + output.write(DEMO_CODE_VERSION.data(), DEMO_CODE_VERSION.size()); + output.write(&null_terminator, 1); + output.write(protocol.c_str(), protocol.size()); + output.write(&null_terminator, 1); + output.write(build_num.data(), build_num.size()); + output.write(&null_terminator, 1); + output.write(svr_name.data(), svr_name.size()); + output.write(&null_terminator, 1); + output.write(player_name.data(), player_name.size()); + output.write(&null_terminator, 1); + output.write(ui_mapname.data(), ui_mapname.size()); + output.write(&null_terminator, 1); + output.write(ui_gametype.data(), ui_gametype.size()); + output.write(&null_terminator, 1); + } + + void write_general_header_(auto& output) + { + static constexpr std::string_view description = "Call of Duty: Ghosts - client demo"; + + const auto* host_bptr = game::mp::cgs->szHostName; + const auto* host_eptr = std::find(host_bptr, host_bptr + 128, '\0'); + const auto host_name = (std::distance(host_bptr, host_eptr) > 0 && std::distance(host_bptr, host_eptr) < 128) + ? std::string_view(host_bptr, host_eptr) + : "unknown_server_host_name"; + + std::array buffer{}; + std::string_view player_name; + + const auto success = game::CL_GetClientNameColorize(0, game::mp::cg->clientNum, buffer.data(), static_cast(buffer.size())); + if (success) + { + player_name = std::string_view(buffer.data()); + } + + if (player_name.empty()) + { + player_name = get_dvar_string("name", true); + } + + if (player_name.empty()) + { + player_name = "unknown_player_name"; + } + + write_general_header_internal(output, description, host_name, player_name); + } + + void write_general_header(buffer_t& output) + { + write_general_header_(output); + } + + void write_general_header(std::ofstream& output) + { + write_general_header_(output); + } + + void write_general_footer_(auto& output, std::int32_t first_svr_time, std::int32_t last_svr_time) + { + assert(first_svr_time > 0 && last_svr_time > 0 && last_svr_time > first_svr_time); + + const auto duration = (last_svr_time - first_svr_time) / 1000; + const auto fmt_duration = std::format("[{:02}:{:02}:{:02}]", duration / 3600, (duration % 3600) / 60, duration % 60); + const auto size = 2 * 4 + fmt_duration.size() + 1; + + write_id_and_size(output, size, demo_data_id::gen_footer); + + static constexpr auto null_terminator = '\0'; + output.write(reinterpret_cast(&first_svr_time), 4); + output.write(reinterpret_cast(&last_svr_time), 4); + output.write(fmt_duration.c_str(), fmt_duration.size()); + output.write(&null_terminator, 1); + } + + void write_general_footer(buffer_t& output, std::int32_t first_svr_time, std::int32_t last_svr_time) + { + write_general_footer_(output, first_svr_time, last_svr_time); + } + + void write_general_footer(std::ofstream& output, std::int32_t first_svr_time, std::int32_t last_svr_time) + { + write_general_footer_(output, first_svr_time, last_svr_time); + } + + void write_end_of_file_(auto& output) + { + write_id_and_size(output, 0, demo_data_id::eof); + } + + void write_end_of_file(buffer_t& output) + { + write_end_of_file_(output); + } + + void write_end_of_file(std::ofstream& output) + { + write_end_of_file_(output); + } + } + + namespace serialization_server + { + void sv_write_predicted_player_data_(auto& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + write_id_and_size(output, 50, demo_data_id::predicted_data); + output.write(reinterpret_cast(&id), 1); + output.write(reinterpret_cast(&cad_index), 1); + + output.write(reinterpret_cast(&ps.commandTime), 4); + output.write(reinterpret_cast(&ps.origin[0]), 12); + output.write(reinterpret_cast(&ps.velocity[0]), 12); + output.write(reinterpret_cast(&ps.bobCycle), 4); + output.write(reinterpret_cast(&ps.movementDir), 4); + output.write(reinterpret_cast(&ps.viewangles[0]), 12); + } + + void sv_write_predicted_player_data(buffer_t& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + sv_write_predicted_player_data_(output, ps, cad_index, id); + } + + void sv_write_predicted_player_data(std::ofstream& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + sv_write_predicted_player_data_(output, ps, cad_index, id); + } + + void sv_write_predicted_vehicle_data_(auto& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + const auto size = (id != predicted_data_id::vehicle_gryphon) ? 58 : 82; + + write_id_and_size(output, size, demo_data_id::predicted_data); + output.write(reinterpret_cast(&id), 1); + output.write(reinterpret_cast(&cad_index), 1); + + const auto& vehicle = ps.vehicleState; + output.write(reinterpret_cast(&ps.commandTime), 4); + output.write(reinterpret_cast(&ps.origin[0]), 12); + output.write(reinterpret_cast(&ps.viewangles[0]), 12); + output.write(reinterpret_cast(&vehicle.flags), 4); + output.write(reinterpret_cast(&vehicle.origin[0]), 12); + output.write(reinterpret_cast(&vehicle.angles[0]), 12); + + if (id == predicted_data_id::vehicle_gryphon) + { + output.write(reinterpret_cast(&vehicle.velocity[0]), 12); + output.write(reinterpret_cast(&vehicle.angVelocity[0]), 12); + } + + assert(!vehicle.splineId); + } + + void sv_write_predicted_vehicle_data(buffer_t& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + sv_write_predicted_vehicle_data_(output, ps, cad_index, id); + } + + void sv_write_predicted_vehicle_data(std::ofstream& output, const game::mp::playerState_t& ps, + std::uint8_t cad_index, predicted_data_id id) + { + sv_write_predicted_vehicle_data_(output, ps, cad_index, id); + } + + void sv_write_predicted_data_(auto& output, const game::mp::playerState_t& ps, std::size_t send_msg_count) + { + const auto vehicle_in_use = (ps.vehicleState.entity && ps.vehicleState.entity != 2047); + if (!vehicle_in_use) + { + sv_write_predicted_player_data(output, ps, static_cast(send_msg_count), predicted_data_id::player); + } + else + { + const auto data_type = get_predicted_vehicle_type(ps); + if (data_type == predicted_data_id::vehicle_helo_pilot) + { + const auto is_firing = static_cast(ps.eFlags & 0x800000); + if (is_firing && send_msg_count % 3 == 0) + { + write_helo_pilot_turret_fire(output, 1); + } + } + + sv_write_predicted_vehicle_data(output, ps, static_cast(send_msg_count), data_type); + } + } + + void sv_write_predicted_data(buffer_t& output, const game::mp::playerState_t& ps, std::size_t send_msg_count) + { + sv_write_predicted_data_(output, ps, send_msg_count); + } + + void sv_write_predicted_data(std::ofstream& output, const game::mp::playerState_t& ps, std::size_t send_msg_count) + { + sv_write_predicted_data_(output, ps, send_msg_count); + } + + void sv_write_general_header_(auto& output, std::string_view player_name) + { + static constexpr std::string_view description = "Call of Duty: Ghosts - server demo"; + + const auto sv_hostname = get_dvar_string("sv_hostname", true); + const auto host_name = (sv_hostname.size() < 128) + ? sv_hostname + : "unknown_server_host_name"; + + if (player_name.empty()) + { + player_name = "unknown_player_name"; + } + + write_general_header_internal(output, description, host_name, player_name); + } + + void sv_write_general_header(buffer_t& output, std::string_view player_name) + { + sv_write_general_header_(output, player_name); + } + + void sv_write_general_header(std::ofstream& output, std::string_view player_name) + { + sv_write_general_header_(output, player_name); + } + } + + namespace deserialization + { + void process_mod_header(std::span buffer) + { + const std::string_view mod(reinterpret_cast(buffer.data()), buffer.size()); + if (mod.size() > 5 && mod.starts_with("mods/")) + { + if (const auto* fs_game = game::Dvar_FindVar("fs_game"); fs_game) + { + game::Dvar_SetString(fs_game, mod.data() + 5); + } + } + } + + void process_map_header(std::span buffer) + { + assert(!game::Live_SyncOnlineDataFlags(0)); + + const std::string_view strings(reinterpret_cast(buffer.data()), buffer.size()); + const std::string_view mapname(strings.begin(), std::find(strings.begin(), strings.end(), '\0')); + const auto begin = strings.begin() + mapname.size() + ((mapname.size() > 0) ? 1 : 0); + const std::string_view gametype(begin, std::find(begin, strings.end(), '\0')); + + // call CL_ConnectAndPreloadMap to preload the map; this preparatory work is required for the map to load properly + party::connect_to_dummy_party(std::string(mapname), std::string(gametype)); + } + + void process_network_data(std::span buffer) + { + if (buffer.size() > 8) + { + const auto svr_msg_sequence = cpy_cast(buffer.data()); + const auto reliable_acknowledge = cpy_cast(buffer.data() + 4); + const std::span data(buffer.begin() + 8, buffer.end()); + + // store server message sequence; is used by the game for delta snapshots + game::mp::clc->serverMessageSequence = svr_msg_sequence; + + // to prevent 'Client command overflow' error (EXE_ERR_CLIENT_CMD_OVERFLOW) + game::mp::clc->reliableAcknowledge = game::mp::clc->reliableSequence; + + game::msg_t msg{ + .data = reinterpret_cast(data.data()), + .cursize = static_cast(std::ssize(data)), + .useZlib = reliable_acknowledge >> 31 + }; + + // parse the network data + game::CL_ParseServerMessage(0, &msg); + } + } + + void trigger_helo_pilot_turret_fire(std::size_t fire_count) + { + const std::array entity_nums{ game::mp::cg->ps.viewlocked_entNum, game::mp::cg->ps.vehicleState.entity }; + for (const auto entity_num : entity_nums) + { + if (entity_num >= 2047 || entity_num <= 0) + { + continue; + } + + for (std::size_t i = 0; i < std::min(fire_count, std::size_t(10)); ++i) + { + game::CG_HandleTurretFire(0, &game::mp::centities[entity_num], 58, 0, false); + } + + break; + } + } + + void process_predicted_data(std::span buffer, + std::array& p_data, std::size_t& p_data_index) + { + static constexpr auto array_size = std::tuple_size_v>; + + if (buffer.size() < 6) + { + return; + } + + auto inc_offset = [offset = std::size_t(0)](std::size_t add_offset) mutable + { + const auto old_offset = offset; + offset += add_offset; + + return old_offset; + }; + + const auto id = cpy_cast(buffer.data() + inc_offset(1)); + + if (id == predicted_data_id::vehicle_helo_pilot_turret_fire) + { + if (buffer.size() == 6) + { + [[maybe_unused]] const auto svr_time = cpy_cast(buffer.data() + inc_offset(4)); + const auto fire_count = cpy_cast(buffer.data() + inc_offset(1)); + + if (fire_count) + { + trigger_helo_pilot_turret_fire(fire_count); + } + } + } + else + { + [[maybe_unused]] const auto index = cpy_cast(buffer.data() + inc_offset(1)); + const auto svr_time = cpy_cast(buffer.data() + inc_offset(4)); + + auto& data = p_data[p_data_index % array_size]; + auto& vehicle = data.cad.playerVehStateClientArchive; + + if (id == predicted_data_id::player) + { + if (buffer.size() == 50) + { + data.cad.serverTime = svr_time; + std::memcpy(&data.cad.origin[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&data.cad.velocity[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&data.cad.bobCycle, buffer.data() + inc_offset(4), 4); + std::memcpy(&data.cad.movementDir, buffer.data() + inc_offset(4), 4); + std::memcpy(&data.viewangles[0], buffer.data() + inc_offset(12), 12); + + ++p_data_index %= array_size; + } + } + else + { + auto process_vehicle_data = [&inc_offset, &data, &vehicle, &p_data_index, buffer, svr_time](std::size_t expected_buffer_size) + { + if (buffer.size() != expected_buffer_size) + { + return false; + } + + data.cad.serverTime = svr_time; + std::memcpy(&data.cad.origin[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&data.viewangles[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&vehicle.flags, buffer.data() + inc_offset(4), 4); + std::memcpy(&vehicle.origin[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&vehicle.angles[0], buffer.data() + inc_offset(12), 12); + + vehicle.angles[0] = data.viewangles[0]; + vehicle.angles[1] = data.viewangles[1]; + + data.cad.velocity[0] = {}; + data.cad.velocity[1] = {}; + data.cad.velocity[2] = {}; + data.cad.bobCycle = {}; + data.cad.movementDir = {}; + + ++p_data_index %= array_size; + return true; + }; + + if (id == predicted_data_id::vehicle_gryphon) + { + if (process_vehicle_data(82)) + { + std::memcpy(&vehicle.velocity[0], buffer.data() + inc_offset(12), 12); + std::memcpy(&vehicle.angVelocity[0], buffer.data() + inc_offset(12), 12); + }; + } + else if (id == predicted_data_id::vehicle_helo_pilot) + { + if (process_vehicle_data(58)) + { + vehicle.targetEntity = -1; + vehicle.gunAngles[0] = -2.0f; + vehicle.gunAngles[1] = 0.75f; + }; + } + else if (id == predicted_data_id::vehicle_odin) + { + if (process_vehicle_data(58)) + { + vehicle.angles[0] = {}; + vehicle.angles[2] = {}; + }; + } + else if (id == predicted_data_id::vehicle_unknown) + { + process_vehicle_data(58); + } + } + } + + assert(inc_offset(0) == buffer.size()); + } + + bool is_gamestate_valid(game::gameState_t& gs, bool sanitize) + { + // attempt to validate the data because CoD command / config strings are notoriously susceptible to exploits + + const auto new_string_data = std::span(gs.stringData); + const auto new_string_offsets = std::span(gs.stringOffsets); + + if (new_string_data.front() != '\0' || new_string_data.back() != '\0') + { + return false; + } + + for (const auto signed_index : new_string_offsets) + { + const auto unsigned_index = static_cast(signed_index); + if (unsigned_index >= new_string_data.size()) + { + return false; + } + + const std::span str(new_string_data.begin() + unsigned_index, + std::find(new_string_data.begin() + unsigned_index, new_string_data.end(), '\0')); + if (str.size() >= 8192) + { + return false; + } + + if (sanitize) + { + for (auto& c : str) + { + if (c == static_cast(146)) + { + // CoD string sanitization: [’] -> ['] + c = static_cast(39); + } + else if (c <= static_cast(30) || c >= static_cast(127)) + { + // Quake3 string sanitization: non char -> [.] + c = static_cast(46); + } + } + } + else + { + const auto non_char_count = std::count_if(str.begin(), str.end(), [](char c) + { + return (c <= static_cast(30) || c >= static_cast(127)); + }); + + if ((unsigned_index >= 2 && non_char_count >= 16) || non_char_count >= 32) + { + return false; + } + } + } + + return true; + } + + std::optional create_gamestate(std::span string_data, std::span string_offsets) + { + if (string_data.size() >= sizeof(game::gameState_t::stringData)) + { + return {}; + } + if (string_offsets.size() != sizeof(game::gameState_t::stringOffsets)) + { + return {}; + } + + std::optional opt(std::in_place_t{}); + + auto& gs = *opt; + gs.dataCount = static_cast(string_data.size()); + std::memcpy(&gs.stringData[0], string_data.data(), string_data.size()); + std::memcpy(&gs.stringOffsets[0], string_offsets.data(), string_offsets.size()); + + if (!is_gamestate_valid(gs, true)) + { + return {}; + } + + return opt; + } + + void process_gamestate_data(std::span buffer, std::optional& opt_gs) + { + if (buffer.size() < 12) + { + return; + } + + auto inc_offset = [offset = std::size_t(0)](std::size_t add_offset) mutable + { + const auto old_offset = offset; + offset += add_offset; + + return old_offset; + }; + + const auto svr_cmd_seq = cpy_cast(buffer.data() + inc_offset(4)); + const auto pgs = cpy_cast(buffer.data() + inc_offset(8)); + const auto size = inc_offset(0) + pgs.string_data + pgs.string_offsets + sizeof(persistent_data_t); + + if (!pgs.compressed || buffer.size() != size) + { + return; + } + + namespace zlib = utils::compression::zlib; + const std::span sd(buffer.data() + inc_offset(pgs.string_data), pgs.string_data); + const auto string_data = zlib::decompress(sd, 4096); + + const std::span so(buffer.data() + inc_offset(pgs.string_offsets), pgs.string_offsets); + const auto string_offsets = zlib::decompress(so, sizeof(game::gameState_t::stringOffsets)); + + const auto gs = create_gamestate(string_data, string_offsets); + if (!gs) + { + return; + } + + opt_gs.emplace(gamestate_t{ + .svr_cmd_seq = svr_cmd_seq, + .data = *gs + }); + + const auto* src = buffer.data() + inc_offset(sizeof(persistent_data_t)); + auto& dst = get_persistent_data(); + std::memcpy(&dst, src, sizeof(persistent_data_t)); + + assert(inc_offset(0) == buffer.size()); + } + + bool read_demo_data(std::ifstream& file, std::vector& buffer, bool one_byte) + { + buffer.clear(); + + std::size_t size{}; + file.read(reinterpret_cast(&size), (one_byte) ? 1 : 4); + + if (size > MAX_SIZE) + { + return false; + } + + buffer.resize(size); + file.read(reinterpret_cast(buffer.data()), size); + + return file.good() && file.is_open() && !file.eof(); + } + + void process_server_commands() + { + static_assert(std::size(game::mp::clientConnection_t{}.serverCommands) > 100); + assert(game::mp::clc->lastExecutedServerCommand <= game::mp::clc->serverCommandSequence); + + // process server commands before they're overwritten + if (game::mp::clc->lastExecutedServerCommand + 100 <= game::mp::clc->serverCommandSequence) + { + game::CG_ExecuteNewServerCommands(0, game::mp::clc->serverCommandSequence); + } + } + + bool continue_demo_reading() + { + process_server_commands(); + + // this affects the playback speed + return game::mp::cl->snap.serverTime <= game::mp::cl->serverTime; + } + + std::tuple read_demo_data_id(std::ifstream& file) + { + auto id(demo_data_id::eof | demo_data_id::one_byte_flag); + file.read(reinterpret_cast(&id), 1); + + return { + id, + id & ~demo_data_id::flags, + static_cast(id & demo_data_id::unused_flag_1), + static_cast(id & demo_data_id::unused_flag_2), + + // check if the id byte contains the flag to indicate the size is one byte instead of four + static_cast(id & demo_data_id::one_byte_flag) + }; + } + + bool parse_demo(std::ifstream& file, std::vector& buffer, + std::size_t& post_map_header_file_offset, std::array& pred_data, + std::size_t& pred_data_index, std::optional& gs) + { + while (continue_demo_reading()) + { + const auto [full_id, id, flag1, flag2, one_byte] = read_demo_data_id(file); + + if (id == demo_data_id::eof) + { + return false; + } + if (flag1 || flag2) + { + console::warn("could not parse demo data id %d\n", full_id); + return false; + } + if (!read_demo_data(file, buffer, one_byte)) + { + console::warn("could not parse demo data\n"); + return false; + } + + switch (id) + { + case demo_data_id::mod_header: + process_mod_header(buffer); + continue; + case demo_data_id::map_header: + // store the file offset here to skip map preloading when rewinding + post_map_header_file_offset = static_cast(file.tellg()); + + process_map_header(buffer); + continue; + case demo_data_id::network_data: + process_network_data(buffer); + continue; + case demo_data_id::predicted_data: + process_predicted_data(buffer, pred_data, pred_data_index); + continue; + case demo_data_id::update_gamestate_data: + process_gamestate_data(buffer, gs); + continue; + + case demo_data_id::gen_header: + case demo_data_id::gen_footer: + continue; + } + + assert(false); + continue; + } + + return true; + } + } + + namespace misc + { + persistent_data_t& get_persistent_data() + { + // not sure if this is strictly well-defined behavior + return *reinterpret_cast(0x1445A71A4); + } + + void fast_forward_demo(std::uint32_t msec) + { + if (*game::mp::connstate == game::CA_ACTIVE) + { + game::mp::cls->realtime += msec; + } + } + + void check_address_assertions() + { + assert(std::bit_cast(game::mp::connstate.get()) == 0x1419E1AE0); + assert(std::bit_cast(&game::mp::cls->realtime) == 0x141D1AC80); + + static_assert(offsetof(game::mp::clientStatic_t, gameState) == 0x80EE0); + assert(std::bit_cast(&game::mp::cls->gameState) == 0x141D9BA40); + + static_assert(offsetof(game::mp::clientActive_t, snap) == 0x8 && offsetof(game::mp::clSnapshot_t, serverTime) == 0x347C); + assert(std::bit_cast(&game::mp::cl->snap.serverTime) == 0x1419E50F4); + + static_assert(offsetof(game::mp::clientActive_t, snap) == 0x8 && offsetof(game::mp::clSnapshot_t, deltaNum) == 0x3484); + assert(std::bit_cast(&game::mp::cl->snap.deltaNum) == 0x1419E50FC); + + static_assert(offsetof(game::mp::clientActive_t, serverTime) == 0x34D0); + assert(std::bit_cast(&game::mp::cl->serverTime) == 0x1419E5140); + + static_assert(offsetof(game::mp::clientActive_t, newSnapshots) == 0x34DC); + assert(std::bit_cast(&game::mp::cl->newSnapshots) == 0x1419E514C); + + static_assert(offsetof(game::mp::clientActive_t, clientArchive) == 0x542C); + assert(std::bit_cast(&game::mp::cl->clientArchive) == 0x1419E709C); + + static_assert(offsetof(game::mp::clientActive_t, clientArchiveIndex) == 0xE02C); + assert(std::bit_cast(&game::mp::cl->clientArchiveIndex) == 0x1419EFC9C); + + static_assert(offsetof(game::mp::clientConnection_t, reliableSequence) == 0x12C); + assert(std::bit_cast(&game::mp::clc->reliableSequence) == 0x141CB547C); + + static_assert(offsetof(game::mp::clientConnection_t, reliableAcknowledge) == 0x130); + assert(std::bit_cast(&game::mp::clc->reliableAcknowledge) == 0x141CB5480); + + static_assert(offsetof(game::mp::clientConnection_t, serverCommandSequence) == 0x20138); + assert(std::bit_cast(&game::mp::clc->serverCommandSequence) == 0x141CD5488); + + static_assert(offsetof(game::mp::clientConnection_t, lastExecutedServerCommand) == 0x2013C); + assert(std::bit_cast(&game::mp::clc->lastExecutedServerCommand) == 0x141CD548C); + + static_assert(offsetof(game::mp::cgs_t, serverCommandSequence) == 0x18); + assert(std::bit_cast(&game::mp::cgs->serverCommandSequence) == 0x14187EB98); + + static_assert(offsetof(game::mp::cgs_t, szHostName) == 0x44); + assert(std::bit_cast(&game::mp::cgs->szHostName) == 0x14187EBC4); + + static_assert(offsetof(game::mp::cg_t, clientNum) == 0x3360); + assert(std::bit_cast(&game::mp::cg->clientNum) == 0x141771F60); + + static_assert(offsetof(game::mp::cg_t, demoType) == 0x3368); + assert(std::bit_cast(&game::mp::cg->demoType) == 0x141771F68); + + static_assert(offsetof(game::mp::cg_t, refdefViewAngles) == 0xA9A90); + assert(std::bit_cast(&game::mp::cg->refdefViewAngles) == 0x141818690); + + static_assert(offsetof(game::mp::cg_t, weaponSelect) == 0xB2768); + assert(std::bit_cast(&game::mp::cg->weaponSelect) == 0x141821368); + + static_assert(offsetof(game::mp::cg_t, inKillCam) == 0xB537C); + assert(std::bit_cast(&game::mp::cg->inKillCam) == 0x141823F7C); + + static_assert(offsetof(game::mp::cg_t, bgs) == 0xB54C8); + assert(std::bit_cast(&game::mp::cg->bgs) == 0x1418240C8); + + static_assert(offsetof(game::mp::cg_t, cvsData) == 0x10AD28); + assert(std::bit_cast(&game::mp::cg->cvsData) == 0x141879928); + + static_assert(offsetof(game::mp::cg_t, ps) == 0 + && offsetof(game::mp::playerState_t, vehicleState) == 0xBC + && offsetof(game::PlayerVehicleState, entity) == 0); + assert(std::bit_cast(&game::mp::cg->ps.vehicleState.entity) == 0x14176ECBC); + + static_assert(offsetof(game::mp::cg_t, ps) == 0 && offsetof(game::mp::playerState_t, otherFlags) == 0x10); + assert(std::bit_cast(&game::mp::cg->ps.otherFlags) == 0x14176EC10); + + static_assert(offsetof(game::mp::cg_t, ps) == 0 && offsetof(game::mp::playerState_t, viewlocked_entNum) == 0x1E8); + assert(std::bit_cast(&game::mp::cg->ps.viewlocked_entNum) == 0x14176EDE8); + + static_assert(offsetof(game::mp::playerState_t, bobCycle) == 0x18); + static_assert(offsetof(game::mp::playerState_t, origin) == 0x1C); + static_assert(offsetof(game::mp::playerState_t, velocity) == 0x28); + static_assert(offsetof(game::mp::playerState_t, movementDir) == 0xA8); + static_assert(offsetof(game::mp::playerState_t, viewangles) == 0x184); + + static_assert(offsetof(game::msg_t, useZlib) == 0x30); + + static_assert(offsetof(game::mp::client_t, gentity) == 0x41E68); + static_assert(offsetof(game::mp::client_t, name) == 0x41E70); + static_assert(offsetof(game::mp::client_t, lastConnectTime) == 0x41E84); + static_assert(offsetof(game::mp::client_t, playerGuid) == 0x41EB4); + static_assert(offsetof(game::mp::client_t, testClient) == 0x41ECC); + + static_assert(offsetof(game::mp::serverStatic_t, time) == 0x1681D00); + assert(std::bit_cast(&game::mp::svs->time) == 0x14647B280); + + static_assert(offsetof(game::mp::serverStatic_t, snapFlagServerBit) == 0x1681D08); + assert(std::bit_cast(&game::mp::svs->snapFlagServerBit) == 0x14647B288); + + static_assert(offsetof(game::mp::serverStatic_t, clientCount) == 0x1681D0C); + assert(std::bit_cast(&game::mp::svs->clientCount) == 0x14647B28C); + + static_assert(offsetof(game::mp::serverStatic_t, clients) == 0x1681D10); + assert(std::bit_cast(&game::mp::svs->clients) == 0x14647B290); + } + } +} diff --git a/src/client/component/demo_utils.hpp b/src/client/component/demo_utils.hpp new file mode 100644 index 0000000..94217b1 --- /dev/null +++ b/src/client/component/demo_utils.hpp @@ -0,0 +1,217 @@ +#pragma once + +#include "game/game.hpp" + +namespace demo_utils +{ + namespace globals + { + inline constexpr std::string_view DEMO_CODE_VERSION = "v0.0.1"; + inline constexpr std::string_view DEMO_EXTENSION = ".dm_iw6"; + inline constexpr std::size_t MAX_SIZE = 131'072; + } + using namespace globals; + + namespace types + { + enum struct demo_data_id : std::uint8_t + { + mod_header = 0, + map_header = 1, + network_data = 2, + predicted_data = 3, + update_gamestate_data = 4, + gen_header = 5, + gen_footer = 6, + + eof = 31, + + unused_flag_1 = 0b0010'0000, + unused_flag_2 = 0b0100'0000, + one_byte_flag = 0b1000'0000, + + flags = (unused_flag_1 | unused_flag_2 | one_byte_flag) + }; + + demo_data_id operator|(demo_data_id lhs, demo_data_id rhs); + demo_data_id operator&(demo_data_id lhs, demo_data_id rhs); + demo_data_id operator~(demo_data_id value); + + enum struct predicted_data_id : std::uint8_t + { + player = 0, + vehicle_gryphon = 1, + vehicle_helo_pilot = 2, + vehicle_odin = 3, + vehicle_unknown = 4, + vehicle_helo_pilot_turret_fire = 5 + }; + + struct persistent_data_t + { + // ksr -> killstreak reward + std::int32_t cur_kill_count{}; + std::int32_t req_kill_count_next_ksr{}; + std::int32_t unknown1{ 1 }; + std::int32_t active_ksr_count{}; + std::int32_t unknown2{ -1 }; + std::int32_t active_ksr_flags{}; + std::int32_t unknown3{}; + std::int32_t first_ksr_icon{}; + std::int32_t second_ksr_icon{}; + std::int32_t third_ksr_icon{}; + }; + + struct packed_gamestate_sizes + { + std::uint64_t string_data : 20{}; + std::uint64_t string_offsets : 16{}; + std::uint64_t unused : 27{}; + std::uint64_t compressed : 1{}; + }; + static_assert(sizeof(packed_gamestate_sizes) == 8); + + struct predicted_data_t + { + game::vec3_t viewangles{}; + game::ClientArchiveData cad{}; + }; + + struct gamestate_t + { + std::uint32_t svr_cmd_seq{}; + game::gameState_t data{}; + }; + + class buffer_t + { + public: + [[nodiscard]] std::size_t size() const; + [[nodiscard]] std::span get() const; + void reserve_memory(std::size_t size); + void write(const char* src, std::size_t size); + void clear(); + + private: + std::vector buffer_; + }; + } + using namespace types; + + namespace gen_utilities + { + std::string_view get_dvar_string(std::string_view dvar_name, bool valid_string); + std::string_view get_mod_directory(); + std::string_view get_base_path(bool default_string); + std::string_view get_mapname(bool default_string); + std::string get_shortened_mapname_lowercase(bool default_string); + std::string get_mapname_lowercase(bool default_string); + std::string_view get_gametype(bool default_string); + std::string get_gametype_lowercase(bool default_string); + //std::string get_datetime(); + } + using namespace gen_utilities; + + namespace file_directory + { + std::optional create_directory_user_demo(); + std::optional create_directory_auto_demo(); + bool can_create_demo_directories(); + std::optional create_path_user_demo(const std::filesystem::path& dir_path); + std::optional create_path_user_demo( + const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite); + std::optional create_path_auto_demo(const std::filesystem::path& dir_path); + } + using namespace file_directory; + + namespace file_directory_server + { + std::optional sv_create_demo_directory(); + bool sv_can_create_demo_directory(); + std::optional sv_create_path_server_demo( + const std::filesystem::path& dir_path, std::string_view client_id); + std::optional sv_create_path_server_demo( + const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite); + } + using namespace file_directory_server; + + namespace serialization + { + void write_id_and_size(buffer_t& output, std::size_t size, demo_data_id id); + void write_id_and_size(std::ofstream& output, std::size_t size, demo_data_id id); + + void write_network_data(buffer_t& output, std::span network_data); + void write_network_data(std::ofstream& output, std::span network_data); + + //void write_predicted_player_data(buffer_t& output, const game::ClientArchiveData& cad, + //const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id); + //void write_predicted_player_data(std::ofstream& output, const game::ClientArchiveData& cad, + //const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id); + + //void write_predicted_vehicle_data(buffer_t& output, const game::ClientArchiveData& cad, + //const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id); + //void write_predicted_vehicle_data(std::ofstream& output, const game::ClientArchiveData& cad, + //const game::vec3_t& viewangles, std::uint8_t cad_index, predicted_data_id id); + + void write_helo_pilot_turret_fire(buffer_t& output, std::uint8_t fire_count); + void write_helo_pilot_turret_fire(std::ofstream& output, std::uint8_t fire_count); + + void write_mod_header(buffer_t& output); + void write_mod_header(std::ofstream& output); + + bool write_map_header(buffer_t& output); + bool write_map_header(std::ofstream& output); + + bool write_gamestate_data(buffer_t& output); + bool write_gamestate_data(std::ofstream& output); + + void write_predicted_data(buffer_t& output); + void write_predicted_data(std::ofstream& output); + + void write_general_header(buffer_t& output); + void write_general_header(std::ofstream& output); + + //void write_general_footer(buffer_t& output, std::int32_t first_svr_time, std::int32_t last_svr_time); + void write_general_footer(std::ofstream& output, std::int32_t first_svr_time, std::int32_t last_svr_time); + + //void write_end_of_file(buffer_t& output); + void write_end_of_file(std::ofstream& output); + } + using namespace serialization; + + namespace serialization_server + { + void sv_write_predicted_data(buffer_t& output, const game::mp::playerState_t& ps, std::size_t send_msg_count); + void sv_write_predicted_data(std::ofstream& output, const game::mp::playerState_t& ps, std::size_t send_msg_count); + + void sv_write_general_header(buffer_t& output, std::string_view player_name); + void sv_write_general_header(std::ofstream& output, std::string_view player_name); + } + using namespace serialization_server; + + namespace deserialization + { + bool is_gamestate_valid(game::gameState_t& gs, bool sanitize); + bool parse_demo(std::ifstream& file, std::vector& buffer, + std::size_t& post_map_header_file_offset, std::array& pred_data, + std::size_t& pred_data_index, std::optional& gs); + } + using namespace deserialization; + + namespace misc + { + template + requires (std::is_trivially_copyable_v&& std::is_trivially_copyable_v) + [[nodiscard]] To cpy_cast(const From* src) + { + To dst; + std::memcpy(&dst, src, sizeof(To)); + return dst; + } + + persistent_data_t& get_persistent_data(); + void fast_forward_demo(std::uint32_t msec); + void check_address_assertions(); + } + using namespace misc; +} -- 2.34.1