#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<const std::uint8_t> buffer_t::get() const
		{
			return std::span<const std::uint8_t>(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<demo_data_id>;
			return demo_data_id(static_cast<type>(lhs) | static_cast<type>(rhs));
		}

		demo_data_id operator&(demo_data_id lhs, demo_data_id rhs)
		{
			using type = std::underlying_type_t<demo_data_id>;
			return demo_data_id(static_cast<type>(lhs) & static_cast<type>(rhs));
		}

		demo_data_id operator~(demo_data_id value)
		{
			using type = std::underlying_type_t<demo_data_id>;
			return demo_data_id(~static_cast<type>(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<std::chrono::days>(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::seconds>
				(std::chrono::duration_cast<std::chrono::seconds>(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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> create_path_user_demo(
			const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite)
		{
			const std::optional<std::filesystem::path> 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<std::filesystem::path> create_path_auto_demo(const std::filesystem::path& dir_path)
		{
			const auto mapname = get_shortened_mapname_lowercase(true);

			const std::optional<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path> sv_create_path_server_demo(
			const std::filesystem::path& dir_path, std::string_view demo_name, bool overwrite)
		{
			const std::optional<std::filesystem::path> 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<std::string_view>()("remote_uav_mp");
			bool is_gryphon_ks_reward(std::string_view name)
			{
				return hash_gryphon == std::hash<std::string_view>()(name);
			}

			const auto hash_helo_pilot = std::hash<std::string_view>()("heli_pilot_mp");
			bool is_helo_pilot_ks_reward(std::string_view name)
			{
				return hash_helo_pilot == std::hash<std::string_view>()(name);
			}

			const auto hash_odin = std::hash<std::string_view>()("odin_mp");
			bool is_odin_ks_reward(std::string_view name)
			{
				return hash_odin == std::hash<std::string_view>()(name);
			}

			predicted_data_id get_predicted_vehicle_type(const game::mp::playerState_t& ps)
			{
				const auto** vehicle_def = reinterpret_cast<const char**>(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<const char*>(&complete_id), 1);
				output.write(reinterpret_cast<const char*>(&size), 1);
			}
			else
			{
				const auto complete_id = id;
				output.write(reinterpret_cast<const char*>(&complete_id), 1);
				output.write(reinterpret_cast<const char*>(&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<const std::uint8_t> network_data)
		{
			write_id_and_size(output, network_data.size(), demo_data_id::network_data);
			output.write(reinterpret_cast<const char*>(network_data.data()), network_data.size());
		}

		void write_network_data(buffer_t& output, std::span<const std::uint8_t> network_data)
		{
			write_network_data_(output, network_data);
		}

		void write_network_data(std::ofstream& output, std::span<const std::uint8_t> 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<const char*>(&id), 1);
			output.write(reinterpret_cast<const char*>(&cad_index), 1);

			output.write(reinterpret_cast<const char*>(&ps.commandTime), 4);
			output.write(reinterpret_cast<const char*>(&ps.origin[0]), 12);
			output.write(reinterpret_cast<const char*>(&ps.velocity[0]), 12);
			output.write(reinterpret_cast<const char*>(&ps.bobCycle), 4);
			output.write(reinterpret_cast<const char*>(&ps.movementDir), 4);
			output.write(reinterpret_cast<const char*>(&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<const char*>(&id), 1);
			output.write(reinterpret_cast<const char*>(&cad_index), 1);

			const auto& vehicle = ps.vehicleState;
			output.write(reinterpret_cast<const char*>(&ps.commandTime), 4);
			output.write(reinterpret_cast<const char*>(&ps.origin[0]), 12);
			output.write(reinterpret_cast<const char*>(&ps.viewangles[0]), 12);
			output.write(reinterpret_cast<const char*>(&vehicle.flags), 4);
			output.write(reinterpret_cast<const char*>(&vehicle.origin[0]), 12);
			output.write(reinterpret_cast<const char*>(&vehicle.angles[0]), 12);

			if (id == predicted_data_id::vehicle_gryphon)
			{
				output.write(reinterpret_cast<const char*>(&vehicle.velocity[0]), 12);
				output.write(reinterpret_cast<const char*>(&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<const char*>(&id), 1);
			output.write(reinterpret_cast<const char*>(&snap_svr_time), 4);
			output.write(reinterpret_cast<const char*>(&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<std::size_t>(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<const std::uint8_t> sd(
				reinterpret_cast<const std::uint8_t*>(&gs.stringData[0]), strings_size);
			const std::span<const std::uint8_t> so(
				reinterpret_cast<const std::uint8_t*>(&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<const char*>(&svr_cmd_seq), 4);
			output.write(reinterpret_cast<const char*>(&pgs), 8);
			output.write(reinterpret_cast<const char*>(string_data.data()), string_data.size());
			output.write(reinterpret_cast<const char*>(string_offsets.data()), string_offsets.size());
			output.write(reinterpret_cast<const char*>(&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<std::uint8_t>(cl.clientArchiveIndex), predicted_data_id::player);
			}
			else
			{
				const auto data_type = get_predicted_vehicle_type(ps);
				write_predicted_vehicle_data(output, ps, static_cast<std::uint8_t>(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<char, 64> buffer{};
			std::string_view player_name;

			const auto success = game::CL_GetClientNameColorize(0, game::mp::cg->clientNum, buffer.data(), static_cast<int>(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<const char*>(&first_svr_time), 4);
			output.write(reinterpret_cast<const char*>(&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<std::uint8_t>(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<bool>(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<std::uint8_t>(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<const std::uint8_t> buffer)
		{
			const std::string_view mod(reinterpret_cast<const char*>(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<const std::uint8_t> buffer)
		{
			assert(!game::Live_SyncOnlineDataFlags(0));

			const std::string_view strings(reinterpret_cast<const char*>(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<const std::uint8_t> buffer)
		{
			if (buffer.size() > 8)
			{
				const auto svr_msg_sequence = cpy_cast<std::uint32_t>(buffer.data());
				const auto reliable_acknowledge = cpy_cast<std::uint32_t>(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<const char*>(data.data()),
					.cursize = static_cast<std::int32_t>(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<const std::uint8_t> buffer,
			std::array<predicted_data_t, 256>& p_data, std::size_t& p_data_index)
		{
			static constexpr auto array_size = std::tuple_size_v<std::remove_reference_t<decltype(p_data)>>;

			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<predicted_data_id>(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<std::int32_t>(buffer.data() + inc_offset(4));
					const auto fire_count = cpy_cast<std::uint8_t>(buffer.data() + inc_offset(1));

					if (fire_count)
					{
						trigger_helo_pilot_turret_fire(fire_count);
					}
				}
			}
			else
			{
				[[maybe_unused]] const auto index = cpy_cast<std::uint8_t>(buffer.data() + inc_offset(1));
				const auto svr_time = cpy_cast<std::int32_t>(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<std::size_t>(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<char>(146))
						{
							// CoD string sanitization: [’] -> [']
							c = static_cast<char>(39);
						}
						else if (c <= static_cast<char>(30) || c >= static_cast<char>(127))
						{
							// Quake3 string sanitization: non char -> [.]
							c = static_cast<char>(46);
						}
					}
				}
				else
				{
					const auto non_char_count = std::count_if(str.begin(), str.end(), [](char c)
					{
						return (c <= static_cast<char>(30) || c >= static_cast<char>(127));
					});

					if ((unsigned_index >= 2 && non_char_count >= 16) || non_char_count >= 32)
					{
						return false;
					}
				}
			}

			return true;
		}

		std::optional<game::gameState_t> create_gamestate(std::span<const std::uint8_t> string_data, std::span<const std::uint8_t> string_offsets)
		{
			if (string_data.size() >= sizeof(game::gameState_t::stringData))
			{
				return {};
			}
			if (string_offsets.size() != sizeof(game::gameState_t::stringOffsets))
			{
				return {};
			}

			std::optional<game::gameState_t> opt(std::in_place_t{});

			auto& gs = *opt;
			gs.dataCount = static_cast<std::int32_t>(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<const std::uint8_t> buffer, std::optional<gamestate_t>& 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<std::uint32_t>(buffer.data() + inc_offset(4));
			const auto pgs = cpy_cast<packed_gamestate_sizes>(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<const std::uint8_t> sd(buffer.data() + inc_offset(pgs.string_data), pgs.string_data);
			const auto string_data = zlib::decompress(sd, 4096);

			const std::span<const std::uint8_t> 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<std::uint8_t>& buffer, bool one_byte)
		{
			buffer.clear();

			std::size_t size{};
			file.read(reinterpret_cast<char*>(&size), (one_byte) ? 1 : 4);

			if (size > MAX_SIZE)
			{
				return false;
			}

			buffer.resize(size);
			file.read(reinterpret_cast<char*>(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<demo_data_id, demo_data_id, bool, bool, bool> read_demo_data_id(std::ifstream& file)
		{
			auto id(demo_data_id::eof | demo_data_id::one_byte_flag);
			file.read(reinterpret_cast<char*>(&id), 1);

			return {
				id,
				id & ~demo_data_id::flags,
				static_cast<bool>(id & demo_data_id::unused_flag_1),
				static_cast<bool>(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<bool>(id & demo_data_id::one_byte_flag)
			};
		}

		bool parse_demo(std::ifstream& file, std::vector<std::uint8_t>& buffer,
			std::size_t& post_map_header_file_offset, std::array<predicted_data_t, 256>& pred_data,
			std::size_t& pred_data_index, std::optional<gamestate_t>& 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<std::size_t>(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<persistent_data_t*>(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<std::size_t>(game::mp::connstate.get()) == 0x1419E1AE0);
			assert(std::bit_cast<std::size_t>(&game::mp::cls->realtime) == 0x141D1AC80);

			static_assert(offsetof(game::mp::clientStatic_t, gameState) == 0x80EE0);
			assert(std::bit_cast<std::size_t>(&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<std::size_t>(&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<std::size_t>(&game::mp::cl->snap.deltaNum) == 0x1419E50FC);

			static_assert(offsetof(game::mp::clientActive_t, serverTime) == 0x34D0);
			assert(std::bit_cast<std::size_t>(&game::mp::cl->serverTime) == 0x1419E5140);

			static_assert(offsetof(game::mp::clientActive_t, newSnapshots) == 0x34DC);
			assert(std::bit_cast<std::size_t>(&game::mp::cl->newSnapshots) == 0x1419E514C);

			static_assert(offsetof(game::mp::clientActive_t, clientArchive) == 0x542C);
			assert(std::bit_cast<std::size_t>(&game::mp::cl->clientArchive) == 0x1419E709C);

			static_assert(offsetof(game::mp::clientActive_t, clientArchiveIndex) == 0xE02C);
			assert(std::bit_cast<std::size_t>(&game::mp::cl->clientArchiveIndex) == 0x1419EFC9C);

			static_assert(offsetof(game::mp::clientConnection_t, reliableSequence) == 0x12C);
			assert(std::bit_cast<std::size_t>(&game::mp::clc->reliableSequence) == 0x141CB547C);

			static_assert(offsetof(game::mp::clientConnection_t, reliableAcknowledge) == 0x130);
			assert(std::bit_cast<std::size_t>(&game::mp::clc->reliableAcknowledge) == 0x141CB5480);

			static_assert(offsetof(game::mp::clientConnection_t, serverCommandSequence) == 0x20138);
			assert(std::bit_cast<std::size_t>(&game::mp::clc->serverCommandSequence) == 0x141CD5488);

			static_assert(offsetof(game::mp::clientConnection_t, lastExecutedServerCommand) == 0x2013C);
			assert(std::bit_cast<std::size_t>(&game::mp::clc->lastExecutedServerCommand) == 0x141CD548C);

			static_assert(offsetof(game::mp::cgs_t, serverCommandSequence) == 0x18);
			assert(std::bit_cast<std::size_t>(&game::mp::cgs->serverCommandSequence) == 0x14187EB98);

			static_assert(offsetof(game::mp::cgs_t, szHostName) == 0x44);
			assert(std::bit_cast<std::size_t>(&game::mp::cgs->szHostName) == 0x14187EBC4);

			static_assert(offsetof(game::mp::cg_t, clientNum) == 0x3360);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->clientNum) == 0x141771F60);

			static_assert(offsetof(game::mp::cg_t, demoType) == 0x3368);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->demoType) == 0x141771F68);

			static_assert(offsetof(game::mp::cg_t, refdefViewAngles) == 0xA9A90);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->refdefViewAngles) == 0x141818690);

			static_assert(offsetof(game::mp::cg_t, weaponSelect) == 0xB2768);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->weaponSelect) == 0x141821368);

			static_assert(offsetof(game::mp::cg_t, inKillCam) == 0xB537C);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->inKillCam) == 0x141823F7C);

			static_assert(offsetof(game::mp::cg_t, bgs) == 0xB54C8);
			assert(std::bit_cast<std::size_t>(&game::mp::cg->bgs) == 0x1418240C8);

			static_assert(offsetof(game::mp::cg_t, cvsData) == 0x10AD28);
			assert(std::bit_cast<std::size_t>(&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<std::size_t>(&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<std::size_t>(&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<std::size_t>(&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<std::size_t>(&game::mp::svs->time) == 0x14647B280);

			static_assert(offsetof(game::mp::serverStatic_t, snapFlagServerBit) == 0x1681D08);
			assert(std::bit_cast<std::size_t>(&game::mp::svs->snapFlagServerBit) == 0x14647B288);

			static_assert(offsetof(game::mp::serverStatic_t, clientCount) == 0x1681D0C);
			assert(std::bit_cast<std::size_t>(&game::mp::svs->clientCount) == 0x14647B28C);

			static_assert(offsetof(game::mp::serverStatic_t, clients) == 0x1681D10);
			assert(std::bit_cast<std::size_t>(&game::mp::svs->clients) == 0x14647B290);
		}
	}
}
