#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::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 write_predicted_player_data(buffer_t& output, const game::mp::playerState_t& ps, std::uint8_t cad_index, predicted_data_id id) { write_predicted_player_data_(output, ps, cad_index, id); } void write_predicted_player_data(std::ofstream& output, const game::mp::playerState_t& ps, std::uint8_t cad_index, predicted_data_id id) { write_predicted_player_data_(output, ps, cad_index, id); } void 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 write_predicted_vehicle_data(buffer_t& output, const game::mp::playerState_t& ps, std::uint8_t cad_index, predicted_data_id id) { write_predicted_vehicle_data_(output, ps, cad_index, id); } void write_predicted_vehicle_data(std::ofstream& output, const game::mp::playerState_t& ps, std::uint8_t cad_index, predicted_data_id id) { write_predicted_vehicle_data_(output, ps, 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) { auto& cl = *game::mp::cl; auto& ps = cl.snap.ps; const auto& cg = *game::mp::cg; ps.viewangles[0] = cg.refdefViewAngles[0]; ps.viewangles[1] = cg.refdefViewAngles[1]; ps.viewangles[2] = cg.refdefViewAngles[2]; const auto vehicle_in_use = (ps.vehicleState.entity && ps.vehicleState.entity != 2047); if (!vehicle_in_use) { write_predicted_player_data(output, ps, static_cast(cl.clientArchiveIndex), predicted_data_id::player); } else { const auto data_type = get_predicted_vehicle_type(ps); write_predicted_vehicle_data(output, ps, static_cast(cl.clientArchiveIndex), 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_bptr = game::LiveStorage_FetchFFotD(); const auto* build_num_eptr = build_num_bptr ? std::find(build_num_bptr, build_num_bptr + 128, '\0') : nullptr; const auto build_num = (build_num_bptr && std::distance(build_num_bptr, build_num_eptr) > 0 && std::distance(build_num_bptr, build_num_eptr) < 128) ? std::string_view(build_num_bptr, build_num_eptr) : "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.begin(), std::find(buffer.begin(), buffer.end(), '\0')); } if (player_name.empty() || player_name.size() == buffer.size()) { player_name = get_dvar_string("name", true); if (player_name.empty() || player_name.size() >= 64) { 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_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) { 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); } } 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.size() >= 64) { 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); } } }