diff --git a/src/client/component/auth.cpp b/src/client/component/auth.cpp index 0a453fd..fad4e32 100644 --- a/src/client/component/auth.cpp +++ b/src/client/component/auth.cpp @@ -89,6 +89,7 @@ namespace auth } const utils::info_string info_string{ std::string{params[2]} }; + const auto steam_id = info_string.get("xuid"); const auto challenge = info_string.get("challenge"); @@ -104,8 +105,10 @@ namespace auth const auto xuid = strtoull(steam_id.data(), nullptr, 16); if (xuid != key.get_hash()) { - network::send(*from, "error", "XUID doesn't match the certificate!", '\n'); - return; + // xuid is always 0 + //MessageBoxA(nullptr, steam_id.data(), std::to_string(key.get_hash()).data(), 0); + //network::send(*from, "error", "XUID doesn't match the certificate!", '\n'); + //return; } if (!key.is_valid() || !verify_message(key, challenge, info.signature())) @@ -125,12 +128,11 @@ namespace auth a.movaps(xmmword_ptr(rsp, 0x20), xmm0); a.pushad64(); - a.mov(rdx, rsi); + a.mov(rdx, rdi); a.call_aligned(direct_connect); a.popad64(); - a.mov(eax, 0x140442317); - a.jmp(eax); + a.jmp(0x140442317); }); } } @@ -165,8 +167,8 @@ namespace auth utils::hook::jump(0x14053995F, 0x1405399A0); utils::hook::jump(0x140539E70, 0x140539EB6); - //utils::hook::jump(0x1404421F6, get_direct_connect_stub(), true); - //utils::hook::call(0x140208C54, send_connect_data_stub); + utils::hook::jump(0x1404421F6, get_direct_connect_stub(), true); + utils::hook::call(0x140208C54, send_connect_data_stub); } } }; diff --git a/src/client/component/binding.cpp b/src/client/component/binding.cpp new file mode 100644 index 0000000..5bceff2 --- /dev/null +++ b/src/client/component/binding.cpp @@ -0,0 +1,133 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include +#include + +namespace binding +{ + namespace + { + std::vector custom_binds = {}; + + utils::hook::detour cl_execute_key_hook; + + int key_write_bindings_to_buffer_stub(int /*localClientNum*/, char* buffer, const int buffer_size) + { + auto bytes_used = 0; + const auto buffer_size_align = static_cast(buffer_size) - 4; + + for (auto key_index = 0; key_index < 256; ++key_index) + { + const auto* const key_button = game::Key_KeynumToString(key_index, 0, 1); + auto value = game::playerKeys->keys[key_index].binding; + + if (value && value < 100) + { + const auto len = sprintf_s(&buffer[bytes_used], (buffer_size_align - bytes_used), + "bind %s \"%s\"\n", key_button, game::command_whitelist[value]); + + if (len < 0) + { + return bytes_used; + } + + bytes_used += len; + } + else if (value >= 100) + { + value -= 100; + if (static_cast(value) < custom_binds.size() && !custom_binds[value].empty()) + { + const auto len = sprintf_s(&buffer[bytes_used], (buffer_size_align - bytes_used), + "bind %s \"%s\"\n", key_button, custom_binds[value].data()); + + if (len < 0) + { + return bytes_used; + } + + bytes_used += len; + } + } + } + + buffer[bytes_used] = 0; + return bytes_used; + } + + int get_binding_for_custom_command(const char* command) + { + auto index = 0; + for (auto& bind : custom_binds) + { + if (bind == command) + { + return index; + } + index++; + } + + custom_binds.emplace_back(command); + index = static_cast(custom_binds.size()) - 1; + + return index; + } + + int key_get_binding_for_cmd_stub(const char* command) + { + // original binds + for (auto i = 0; i <= 100; i++) + { + if (game::command_whitelist[i] && !strcmp(command, game::command_whitelist[i])) + { + return i; + } + } + + // custom binds + return 100 + get_binding_for_custom_command(command); + } + + void cl_execute_key_stub(const int local_client_num, unsigned int key, const unsigned int down, const unsigned int time) + { + if (key >= 100) + { + key -= 100; + + if (static_cast(key) < custom_binds.size() && !custom_binds[key].empty()) + { + game::Cbuf_AddText(local_client_num, utils::string::va("%s\n", custom_binds[key].data())); + } + + return; + } + + cl_execute_key_hook.invoke(local_client_num, key, down, time); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + // write all bindings to config file + utils::hook::call(SELECT_VALUE(0x1, 0x1402081CB), key_write_bindings_to_buffer_stub); + + // links a custom command to an index + utils::hook::jump(SELECT_VALUE(0x1402EE5A0, 0x1403AFB50), key_get_binding_for_cmd_stub); + + // execute custom binds + cl_execute_key_hook.create(SELECT_VALUE(0x14013B110, 0x140202BE0), &cl_execute_key_stub); + } + }; +} + +REGISTER_COMPONENT(binding::component) diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp new file mode 100644 index 0000000..37d6925 --- /dev/null +++ b/src/client/component/bots.cpp @@ -0,0 +1,79 @@ +#include +#include "loader/component_loader.hpp" +#include "command.hpp" +#include "scheduler.hpp" +#include "game/game.hpp" +#include "party.hpp" + +#include + +namespace bots +{ + namespace + { + bool can_spawn() + { + // disable spawning for now, causes a crash if more than svs_numclients. + + //const auto index = *game::mp::svs_numclients - 1; + //const auto cant = game::mp::svs_clients[index].header.state; + //if (cant) + //{ + return false; + //} + //return true; + } + + void add_bot() + { + if (!can_spawn()) + { + return; + } + + auto* bot_ent = game::SV_AddTestClient(0); + if (&bot_ent) + { + game::SV_SpawnTestClient(bot_ent); + } + + // SV_BotGetRandomName + /*auto* bot_name = reinterpret_cast(0x1404267E0)(); + auto* bot_ent = game::SV_AddBot(bot_name); + if (&bot_ent) + { + game::SV_SpawnTestClient(bot_ent); + }*/ + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + command::add("spawnBot", [](const command::params& params) + { + if (!game::SV_Loaded()) return; + + auto num_bots = 1; + if (params.size() == 2) + { + num_bots = atoi(params.get(1)); + } + + for (auto i = 0; i < num_bots; i++) + { + scheduler::once(add_bot, scheduler::pipeline::server, 100ms * i); + } + }); + } + }; +} + +REGISTER_COMPONENT(bots::component) diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp index afe9630..b03e2e5 100644 --- a/src/client/component/command.cpp +++ b/src/client/component/command.cpp @@ -301,70 +301,6 @@ namespace command static void add_commands_mp() { client_command_hook.create(0x1402E98F0, &client_command); - - add("map", [](const command::params& params) - { - if (params.size() != 2) - { - return; - } - - auto mapname = params.get(1); - - if (!game::SV_MapExists(mapname)) - { - printf("Map '%s' doesn't exist.", mapname); - return; - } - - auto* current_mapname = game::Dvar_FindVar("mapname"); - if (current_mapname && utils::string::to_lower(current_mapname->current.string) == utils::string::to_lower(mapname) && game::SV_Loaded()) - { - printf("Restarting map: %s\n", mapname); - command::execute("map_restart", false); - return; - } - - printf("Starting map: %s\n", mapname); - //game::SV_StartMap(0, mapname); - game::SV_StartMapForParty(0, mapname, false); - }); - - add("map_restart", []() - { - if (!game::SV_Loaded()) - { - return; - } - *reinterpret_cast(0x1488692B0) = 1; // sv_map_restart - *reinterpret_cast(0x1488692B4) = 1; // sv_loadScripts - *reinterpret_cast(0x1488692B8) = 0; // sv_migrate - reinterpret_cast(0x140437460)(0); // SV_CheckLoadGame - }); - - add("fast_restart", []() - { - if (game::SV_Loaded()) - { - game::SV_FastRestart(0); - } - }); - - add("clientkick", [](const command::params& params) - { - if (params.size() < 2) - { - printf("usage: clientkick \n"); - return; - } - const auto client_num = atoi(params.get(1)); - if (client_num < 0 || client_num >= *game::svs_numclients) - { - return; - } - - game::SV_KickClientNum(client_num, "EXE_PLAYERKICKED"); - }); } }; } diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp new file mode 100644 index 0000000..3af73e5 --- /dev/null +++ b/src/client/component/dvar_cheats.cpp @@ -0,0 +1,187 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "game_console.hpp" +#include "scheduler.hpp" + +#include +#include + +namespace dvar_cheats +{ + void apply_sv_cheats(const game::dvar_t* dvar, const game::DvarSetSource source, game::dvar_value* value) + { + 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 + if (game::environment::is_dedi() && source == game::DvarSetSource::DVAR_SOURCE_INTERNAL) + { + value->enabled = dvar->current.enabled; + } + + // if sv_cheats was enabled and it changes to disabled, we need to reset all cheat dvars + else if (dvar->current.enabled && !value->enabled) + { + for (auto i = 0; i < *game::dvarCount; ++i) + { + const auto var = game::sortedDvars[i]; + if (var && (var->flags & game::DvarFlags::DVAR_FLAG_CHEAT)) + { + game::Dvar_Reset(var, game::DvarSetSource::DVAR_SOURCE_INTERNAL); + } + } + } + } + } + + bool dvar_flag_checks(const game::dvar_t* dvar, const game::DvarSetSource source) + { + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_WRITE)) + { + game_console::print(game_console::con_type_error, "%s is write protected", dvar->name); + return false; + } + + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_READ)) + { + game_console::print(game_console::con_type_error, "%s is read only", dvar->name); + return false; + } + + // only check cheat/replicated values when the source is external + if (source == game::DvarSetSource::DVAR_SOURCE_EXTERNAL) + { + const auto cl_ingame = game::Dvar_FindVar("cl_ingame"); + const auto sv_running = game::Dvar_FindVar("sv_running"); + + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_REPLICATED) && (cl_ingame && cl_ingame->current.enabled) && ( + sv_running && !sv_running->current.enabled)) + { + game_console::print(game_console::con_type_error, "%s can only be changed by the server", dvar->name); + return false; + } + + const auto sv_cheats = game::Dvar_FindVar("sv_cheats"); + if ((dvar->flags & game::DvarFlags::DVAR_FLAG_CHEAT) && (sv_cheats && !sv_cheats->current.enabled)) + { + game_console::print(game_console::con_type_error, "%s is cheat protected", dvar->name); + return false; + } + } + + // pass all the flag checks, allow dvar to be changed + return true; + } + + const auto dvar_flag_checks_stub = utils::hook::assemble([](utils::hook::assembler& a) + { + const auto can_set_value = a.newLabel(); + const auto zero_source = a.newLabel(); + + a.pushad64(); + a.mov(r8, rdi); + a.mov(edx, esi); + a.mov(rcx, rbx); + a.call_aligned(apply_sv_cheats); //check if we are setting sv_cheats + a.popad64(); + a.cmp(esi, 0); + a.jz(zero_source); //if the SetSource is 0 (INTERNAL) ignore flag checks + + a.pushad64(); + a.mov(edx, esi); //source + a.mov(rcx, rbx); //dvar + a.call_aligned(dvar_flag_checks); //protect read/write/cheat/replicated dvars + a.cmp(al, 1); + a.jz(can_set_value); + + // if we get here, we are non-zero source and CANNOT set values + a.popad64(); // if I do this before the jz it won't work. for some reason the popad64 is affecting the ZR flag + a.jmp(0x1404C3D45); + + // if we get here, we are non-zero source and CAN set values + a.bind(can_set_value); + a.popad64(); // if I do this before the jz it won't work. for some reason the popad64 is affecting the ZR flag + a.jmp(0x1404C3AAE); + + // if we get here, we are zero source and ignore flags + a.bind(zero_source); + a.jmp(0x1404C3AF4); + }); + + void cg_set_client_dvar_from_server(const int local_client_num, void* cg, const char* dvar_id, const char* value) + { + const auto* dvar = game::Dvar_FindVar(dvar_id); + if (dvar) + { + // If we send as string, it can't be set with source SERVERCMD because the game only allows that source on real server cmd dvars. + // Just use external instead as if it was being set by the console + game::Dvar_SetFromStringByNameFromSource(dvar_id, value, game::DvarSetSource::DVAR_SOURCE_EXTERNAL); + } + else + { + // Not a dvar name, assume it is an id and the game will handle normally + game::CG_SetClientDvarFromServer(local_client_num, cg, dvar_id, value); + } + } + + void set_client_dvar_by_string(const int entity_num, const char* value) + { + const auto* dvar = game::Scr_GetString(0); // grab the original dvar again since it's never stored on stack + const auto* command = utils::string::va("q %s \"%s\"", dvar, value); + + game::SV_GameSendServerCommand(entity_num, 1, command); + } + + const auto player_cmd_set_client_dvar = utils::hook::assemble([](utils::hook::assembler& a) + { + const auto set_by_string = a.newLabel(); + + a.pushad64(); + + // check if we didn't find a network dvar index + a.mov(ecx, dword_ptr(rsp, 0x8C8)); + a.cmp(ecx, 0); + a.je(set_by_string); + + // we found an index, handle normally + a.popad64(); + a.mov(r8d, ptr(rsp, 0x848)); + a.lea(r9, ptr(rsp, 0x30)); + a.jmp(0x1402E2E57); + + // no index, let's send the dvar as a string + a.bind(set_by_string); + a.movzx(ecx, word_ptr(rsp, 0x8C0)); //entity_num + a.lea(rdx, ptr(rsp, 0xB0)); //value + a.call_aligned(set_client_dvar_by_string); + a.popad64(); + a.jmp(0x1402E2E7D); + }); + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) return; + + utils::hook::nop(0x1404C3A93, 4); // let our stub handle zero-source sets + utils::hook::jump(0x1404C3A9A, dvar_flag_checks_stub, true); // check extra dvar flags when setting values + + utils::hook::nop(0x1402E2E03, 5); // remove error in PlayerCmd_SetClientDvar if setting a non-network dvar + utils::hook::set(0x1402E2DCF, 0xEB); // don't check flags on the dvars, send any existing dvar instead + utils::hook::jump(0x1402E2E4A, player_cmd_set_client_dvar, true); // send non-network dvars as string + utils::hook::call(0x1401BB782, cg_set_client_dvar_from_server); // check for dvars being sent as string before parsing ids + + scheduler::once([]() + { + game::Dvar_RegisterBool("sv_cheats", false, game::DvarFlags::DVAR_FLAG_REPLICATED, + "Allow cheat commands and dvars on this server"); + }, scheduler::pipeline::main); + } + }; +} + +REGISTER_COMPONENT(dvar_cheats::component) diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp index 7d2e5b5..6aef5d4 100644 --- a/src/client/component/game_console.cpp +++ b/src/client/component/game_console.cpp @@ -65,21 +65,23 @@ namespace game_console matches.clear(); } - void print(const std::string& data) + void print_internal(const std::string& data) { if (con.visible_line_count > 0 && con.display_line_offset == (con.output.size() - con.visible_line_count)) { con.display_line_offset++; } + con.output.push_back(data.data()); + if (con.output.size() > 512) + { + con.output.pop_front(); + } + } - //con.output.push_back(data); - + void print(const std::string& data) + { + //print_internal(data.data()); printf("%s\n", data.data()); - - //if (con.output.size() > 512) - //{ - // con.output.pop_front(); - //} } void toggle_console() @@ -661,11 +663,7 @@ namespace game_console result = vsnprintf(buffer, 256, format, args); va_end(args); std::cout << buffer; - con.output.push_back(buffer); - if (con.output.size() > 512) - { - con.output.pop_front(); - } + print_internal(buffer); return result; } diff --git a/src/client/component/gameplay.cpp b/src/client/component/gameplay.cpp new file mode 100644 index 0000000..696f8a2 --- /dev/null +++ b/src/client/component/gameplay.cpp @@ -0,0 +1,52 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include + +namespace gameplay +{ + namespace + { + void stuck_in_client_stub(void* entity) + { + if (dvars::g_playerEjection->current.enabled) + { + reinterpret_cast(0x1402DA310)(entity); // StuckInClient + } + } + + void cm_transformed_capsule_trace_stub(struct trace_t* results, const float* start, const float* end, + struct Bounds* bounds, struct Bounds* capsule, int contents, const float* origin, const float* angles) + { + if (dvars::g_playerCollision->current.enabled) + { + reinterpret_cast + (0x1403AB1C0) + (results, start, end, bounds, capsule, contents, origin, angles); // CM_TransformedCapsuleTrace + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) return; + + // Implement player ejection dvar + dvars::g_playerEjection = game::Dvar_RegisterBool("g_playerEjection", true, game::DVAR_FLAG_REPLICATED, "Flag whether player ejection is on or off"); + utils::hook::call(0x1402D5E4A, stuck_in_client_stub); + + // Implement player collision dvar + dvars::g_playerCollision = game::Dvar_RegisterBool("g_playerCollision", true, game::DVAR_FLAG_REPLICATED, "Flag whether player collision is on or off"); + utils::hook::call(0x1404563DA, cm_transformed_capsule_trace_stub); // SV_ClipMoveToEntity + utils::hook::call(0x1401F7F8F, cm_transformed_capsule_trace_stub); // CG_ClipMoveToEntity + } + }; +} + +REGISTER_COMPONENT(gameplay::component) diff --git a/src/client/component/lui.cpp b/src/client/component/lui.cpp new file mode 100644 index 0000000..879a1b6 --- /dev/null +++ b/src/client/component/lui.cpp @@ -0,0 +1,35 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "command.hpp" +#include "game_console.hpp" + +#include + +namespace lui +{ + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) return; + + command::add("lui_open", [](const command::params& params) + { + if (params.size() <= 1) + { + game_console::print(game_console::con_type_info, "usage: lui_open \n"); + return; + } + + game::LUI_OpenMenu(0, params[1], 1, 0, 0); + }); + } + }; +} + +REGISTER_COMPONENT(lui::component) \ No newline at end of file diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 4f30fb2..004a27b 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -36,7 +36,7 @@ namespace network return true; } - /*void handle_command_stub(utils::hook::assembler& a) + void handle_command_stub(utils::hook::assembler& a) { const auto return_unhandled = a.newLabel(); @@ -53,14 +53,13 @@ namespace network // Command handled a.popad64(); - a.mov(eax, 0x14020AA0E); - a.jmp(eax); + a.mov(al, 1); + a.jmp(0x14020AA10); a.bind(return_unhandled); a.popad64(); - a.mov(eax, 0x14020A19A); - a.jmp(eax); - }*/ + a.jmp(0x14020A19A); + } int net_compare_base_address(const game::netadr_s* a1, const game::netadr_s* a2) { @@ -104,6 +103,13 @@ namespace network get_callbacks()[utils::string::to_lower(command)] = callback; } + void dw_send_to(const unsigned int size, const char* src, game::netadr_s* a3) + { + sockaddr s = {}; + game::NetadrToSockadr(a3, &s); + sendto(*game::query_socket, src, size - 2, 0, &s, 16); + } + void send(const game::netadr_s& address, const std::string& command, const std::string& data, const char separator) { std::string packet = "\xFF\xFF\xFF\xFF"; @@ -156,34 +162,6 @@ namespace network return "bad"; } - /*void set_xuid_config_string_stub(utils::hook::assembler& a) - { - const auto return_regular = a.newLabel(); - - a.mov(rax, ptr(rsp)); - a.mov(r9, 0x); // This is the evil one :( - a.cmp(rax, r9); - a.jne(return_regular); - - // Do the original work - a.call_aligned(return_regular); - - // Jump to success branch - a.mov(rax, 0x); - a.mov(ptr(rsp), rax); - - a.ret(); - - a.bind(return_regular); - - a.sub(rsp, 0x38); - a.mov(eax, ptr(rcx)); - a.mov(r9d, ptr(rcx, 4)); - a.mov(r10, rdx); - - a.jmp(0x1404B725D); - }*/ - game::dvar_t* register_netport_stub(const char* dvarName, int value, int min, int max, unsigned int flags, const char* description) { @@ -207,13 +185,14 @@ namespace network } // redirect dw_sendto to raw socket - utils::hook::jump(0x1404D850A, reinterpret_cast(0x1404D849A)); + //utils::hook::jump(0x1404D850A, reinterpret_cast(0x1404D849A)); + utils::hook::call(0x1404D851F, dw_send_to); // intercept command handling - //utils::hook::jump(0x14020A175, utils::hook::assemble(handle_command_stub), true); + utils::hook::jump(0x14020A175, utils::hook::assemble(handle_command_stub), true); // handle xuid without secure connection - //utils::hook::jump(0x1404B7250, utils::hook::assemble(set_xuid_config_string_stub), true); + utils::hook::nop(0x14043FFF8, 6); utils::hook::jump(0x1403DA700, net_compare_address); utils::hook::jump(0x1403DA750, net_compare_base_address); diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp new file mode 100644 index 0000000..9ceb071 --- /dev/null +++ b/src/client/component/party.cpp @@ -0,0 +1,397 @@ +#include +#include "loader/component_loader.hpp" +#include "party.hpp" + +#include "command.hpp" +#include "network.hpp" +#include "scheduler.hpp" +//#include "server_list.hpp" + +#include "steam/steam.hpp" + +#include +#include +#include +#include + +namespace party +{ + namespace + { + struct + { + game::netadr_s host{}; + std::string challenge{}; + } connect_state; + + std::string sv_motd; + + void perform_game_initialization() + { + // This fixes several crashes and impure client stuff + command::execute("onlinegame 1", true); + command::execute("exec default_xboxlive.cfg", true); + command::execute("xstartprivateparty", true); + command::execute("xblive_privatematch 1", true); + command::execute("startentitlements", true); + } + + void connect_to_party(const game::netadr_s& target, const std::string& mapname, const std::string& gametype) + { + if (game::environment::is_sp()) + { + return; + } + + if (game::Live_SyncOnlineDataFlags(0)) + { + scheduler::once([=]() + { + connect_to_party(target, mapname, gametype); + }, scheduler::pipeline::main, 1s); + return; + } + + perform_game_initialization(); + + // CL_ConnectFromParty + char session_info[0x100] = {}; + reinterpret_cast(0x140209360)( + 0, session_info, &target, mapname.data(), gametype.data()); + } + + std::string get_dvar_string(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.string) + { + return dvar_value->current.string; + } + + return {}; + } + } + + int get_client_count() + { + auto count = 0; + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + //if (game::mp::svs_clients[i].header.state >= 3) + //{ + // ++count; + //} + } + + return count; + } + + int get_bot_count() + { + auto count = 0; + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + //if (game::mp::svs_clients[i].header.state >= 3 && + // game::mp::svs_clients[i].testClient != game::TC_NONE) + //{ + // ++count; + //} + } + + return count; + } + + void connect(const game::netadr_s& target) + { + if (game::environment::is_sp()) + { + return; + } + + command::execute("lui_open popup_acceptinginvite", false); + + connect_state.host = target; + connect_state.challenge = utils::cryptography::random::get_challenge(); + + network::send(target, "getInfo", connect_state.challenge); + } + + void start_map(const std::string& mapname) + { + if (game::Live_SyncOnlineDataFlags(0) != 0) + { + scheduler::on_game_initialized([mapname]() + { + command::execute("map " + mapname, false); + }, scheduler::pipeline::main, 1s); + } + else + { + if (!game::environment::is_dedi()) + { + perform_game_initialization(); + } + + if (!game::SV_MapExists(mapname.data())) + { + printf("Map '%s' doesn't exist.", mapname.data()); + return; + } + + auto* current_mapname = game::Dvar_FindVar("mapname"); + if (current_mapname && utils::string::to_lower(current_mapname->current.string) == utils::string::to_lower(mapname) && game::SV_Loaded()) + { + printf("Restarting map: %s\n", mapname.data()); + command::execute("map_restart", false); + return; + } + + // starting map doesn't work, only private match -> start + printf("Starting map: %s\n", mapname.data()); + game::SV_StartMapForParty(0, mapname.data(), true); + //game::SV_StartMap(0, mapname.data()); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + command::add("map", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + start_map(argument[1]); + }); + + command::add("map_restart", []() + { + if (!game::SV_Loaded()) + { + return; + } + *reinterpret_cast(0x1488692B0) = 1; // sv_map_restart + *reinterpret_cast(0x1488692B4) = 1; // sv_loadScripts + *reinterpret_cast(0x1488692B8) = 0; // sv_migrate + reinterpret_cast(0x140437460)(0); // SV_CheckLoadGame + }); + + command::add("fast_restart", []() + { + if (game::SV_Loaded()) + { + game::SV_FastRestart(0); + } + }); + + command::add("connect", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + game::netadr_s target{}; + if (game::NET_StringToAdr(argument[1], &target)) + { + connect(target); + } + }); + + command::add("reconnect", [](const command::params& params) + { + if (!game::CL_IsCgameInitialized()) + { + return; + } + const auto host = connect_state.host; + if (host.type == game::NA_LOOPBACK) + { + command::execute("map_restart", true); + return; + } + if (host.type != game::NA_IP) + { + return; + } + command::execute("disconnect", true); + scheduler::once([host]() + { + connect(host); + }, scheduler::pipeline::async, 2s); + }); + + command::add("clientkick", [](const command::params& params) + { + if (params.size() < 2) + { + printf("usage: clientkick \n"); + return; + } + const auto client_num = atoi(params.get(1)); + if (client_num < 0 || client_num >= *game::mp::svs_numclients) + { + return; + } + + game::SV_KickClientNum(client_num, "EXE_PLAYERKICKED"); + }); + + scheduler::once([]() + { + game::Dvar_RegisterString("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, + "The name to pose as for 'say' commands"); + }, scheduler::pipeline::main); + + command::add("tell", [](const command::params& params) + { + if (params.size() < 3) + { + return; + } + + const auto client_num = atoi(params.get(1)); + const auto message = params.join(2); + const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; + + game::SV_GameSendServerCommand(client_num, 0, + utils::string::va("%c \"%s: %s\"", 84, name, message.data())); + printf("%s -> %i: %s\n", name, client_num, message.data()); + }); + + command::add("tellraw", [](const command::params& params) + { + if (params.size() < 3) + { + return; + } + + const auto client_num = atoi(params.get(1)); + const auto message = params.join(2); + + game::SV_GameSendServerCommand(client_num, 0, utils::string::va("%c \"%s\"", 84, message.data())); + printf("%i: %s\n", client_num, message.data()); + }); + + command::add("say", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto message = params.join(1); + const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; + + game::SV_GameSendServerCommand( + -1, 0, utils::string::va("%c \"%s: %s\"", 84, name, message.data())); + printf("%s: %s\n", name, message.data()); + }); + + command::add("sayraw", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto message = params.join(1); + + game::SV_GameSendServerCommand(-1, 0, utils::string::va("%c \"%s\"", 84, message.data())); + printf("%s\n", message.data()); + }); + + network::on("getInfo", [](const game::netadr_s& target, const std::string_view& data) + { + utils::info_string info{}; + info.set("challenge", std::string{data}); + info.set("gamename", "S1"); + info.set("hostname", get_dvar_string("sv_hostname")); + info.set("gametype", get_dvar_string("g_gametype")); + info.set("sv_motd", get_dvar_string("sv_motd")); + info.set("xuid", utils::string::va("%llX", steam::SteamUser()->GetSteamID().bits)); + info.set("mapname", get_dvar_string("mapname")); + info.set("isPrivate", get_dvar_string("g_password").empty() ? "0" : "1"); + info.set("clients", utils::string::va("%i", get_client_count())); + info.set("bots", utils::string::va("%i", get_bot_count())); + info.set("sv_maxclients", utils::string::va("%i", *game::mp::svs_numclients)); + info.set("protocol", utils::string::va("%i", PROTOCOL)); + info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); + //info.set("shortversion", SHORTVERSION); + //info.set("hc", (Dvar::Var("g_hardcore").get() ? "1" : "0")); + + network::send(target, "infoResponse", info.build(), '\n'); + }); + + network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data) + { + const utils::info_string info{data}; + //server_list::handle_info_response(target, info); + + if (connect_state.host != target) + { + return; + } + + if (info.get("challenge") != connect_state.challenge) + { + const auto str = "Invalid challenge."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + const auto playmode = info.get("playmode"); + if (playmode.empty()) + { + const auto str = "Invalid playmode."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + const auto mapname = info.get("mapname"); + if (mapname.empty()) + { + const auto str = "Invalid map."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + const auto gametype = info.get("gametype"); + if (gametype.empty()) + { + const auto str = "Invalid gametype."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + const auto gamename = info.get("gamename"); + if (gamename != "S1"s) + { + const auto str = "Invalid gamename."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + party::sv_motd = info.get("sv_motd"); + + connect_to_party(target, mapname, gametype); + }); + } + }; +} + +REGISTER_COMPONENT(party::component) diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp new file mode 100644 index 0000000..60131fe --- /dev/null +++ b/src/client/component/party.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "game/game.hpp" + +namespace party +{ + void connect(const game::netadr_s& target); + void start_map(const std::string& mapname); + + int get_client_count(); + int get_bot_count(); +} diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 840b5e8..15512ac 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -22,6 +22,18 @@ namespace patches return game::Dvar_FindVar("name")->current.string; } + utils::hook::detour sv_kick_client_num_hook; + + void sv_kick_client_num(const int clientNum, const char* reason) + { + // Don't kick bot to equalize team balance. + if (reason == "EXE_PLAYERKICKED_BOT_BALANCE"s) + { + return; + } + return sv_kick_client_num_hook.invoke(clientNum, reason); + } + utils::hook::detour com_register_dvars_hook; void com_register_dvars() @@ -96,6 +108,18 @@ namespace patches { game::Com_Error(game::ERR_DROP, utils::string::va("MISSING FILE\n%s.ff", fastfiles::get_current_fastfile())); } + + void bsp_sys_error_stub(const char* error, const char* arg1) + { + if (game::environment::is_dedi()) + { + game::Sys_Error(error, arg1); + } + else + { + game::Com_Error(game::ERR_DROP, error, arg1); + } + } } class component final : public component_interface @@ -149,6 +173,12 @@ namespace patches { // Use name dvar live_get_local_client_name_hook.create(0x1404D47F0, &live_get_local_client_name); + + // Patch SV_KickClientNum + sv_kick_client_num_hook.create(0x1404377A0, &sv_kick_client_num); + + // patch "Couldn't find the bsp for this map." error to not be fatal in mp + utils::hook::call(0x14026E63B, bsp_sys_error_stub); } static void patch_sp() diff --git a/src/client/game/dvars.cpp b/src/client/game/dvars.cpp index aaf5b37..03b9d3f 100644 --- a/src/client/game/dvars.cpp +++ b/src/client/game/dvars.cpp @@ -15,6 +15,9 @@ namespace dvars game::dvar_t* con_inputDvarInactiveValueColor = nullptr; game::dvar_t* con_inputCmdMatchColor = nullptr; + game::dvar_t* g_playerEjection = nullptr; + game::dvar_t* g_playerCollision = nullptr; + game::dvar_t* r_fullbright = nullptr; std::string dvar_get_vector_domain(const int components, const game::dvar_limits& domain) diff --git a/src/client/game/dvars.hpp b/src/client/game/dvars.hpp index c6c290b..8dfbb58 100644 --- a/src/client/game/dvars.hpp +++ b/src/client/game/dvars.hpp @@ -15,6 +15,9 @@ namespace dvars extern game::dvar_t* con_inputDvarInactiveValueColor; extern game::dvar_t* con_inputCmdMatchColor; + extern game::dvar_t* g_playerCollision; + extern game::dvar_t* g_playerEjection; + extern game::dvar_t* r_fullbright; std::string dvar_get_vector_domain(const int components, const game::dvar_limits& domain); diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index cb83d47..2e5fa9c 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -9,6 +9,17 @@ namespace game typedef vec_t vec3_t[3]; typedef vec_t vec4_t[4]; + enum CodPlayMode + { + CODPLAYMODE_NONE = 0x0, + CODPLAYMODE_SP = 0x1, + CODPLAYMODE_CORE = 0x2, + CODPLAYMODE_SURVIVAL = 0x3, + CODPLAYMODE_ZOMBIES = 0x4, + CODPLAYMODE_GOLIATH = 0x5, + CODPLAYMODE_COUNT = 0x6, + }; + enum DWOnlineStatus { DW_LIVE_DISCONNECTED = 0x0, @@ -876,17 +887,37 @@ namespace game Material* material; }; - struct client_t - { - - }; - namespace mp { + struct gclient_s + { + + }; + + struct EntityState + { + + }; + struct gentity_s { }; + + struct playerState_s + { + + }; + + struct clientHeader_t + { + + }; + + struct client_t + { + + }; } namespace sp @@ -906,4 +937,10 @@ namespace game }; } + + union playerState_s + { + sp::playerState_s* sp; + mp::playerState_s* mp; + }; } \ No newline at end of file diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 0e44b50..7744d9d 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -10,6 +10,7 @@ namespace game WEAK symbol Com_Error{ 0x1402F7570, 0x1403CE480 }; WEAK symbol Com_Frame_Try_Block_Function{ 0x1402F7E10, 0x1403CEF30 }; + WEAK symbol Com_GetCurrentCoDPlayMode{ 0, 0x1404C9690 }; WEAK symbol Com_Quit_f{ 0x1402F9390, 0x1403D08C0 }; WEAK symbol Cmd_AddCommandInternal{ 0x1402EDDB0, 0x1403AF2C0 }; @@ -19,6 +20,7 @@ namespace game WEAK symbol Conbuf_AppendText{ 0x14038F220, 0x1404D9040 }; + WEAK symbol Cbuf_AddCall{ 0x1402ED820, 0x1403AECF0 }; WEAK symbol Cbuf_AddText{ 0x1402ED890, 0x1403AED70 }; WEAK symbol Cbuf_ExecuteBufferInternal{ 0x1402ED9A0, 0x1403AEE80 }; @@ -26,6 +28,8 @@ namespace game WEAK symbol CL_IsCgameInitialized{ 0x140136560, 0x1401FD510 }; WEAK symbol CG_GameMessage{ 0x1400EE500, 0x1401A3050 }; + WEAK symbol CG_SetClientDvarFromServer + { 0, 0x1401BF0A0 }; WEAK symbol DB_EnumXAssets_FastFile{ 0x14017D7C0, 0x14026EC10 }; WEAK symbol DB_GetXAssetTypeSize{ 0x140151C20, 0x140240DF0 }; @@ -33,6 +37,7 @@ namespace game WEAK symbol Dvar_FindVar{ 0x140370860, 0x1404BF8B0 }; WEAK symbol Dvar_GetCombinedString{ 0x1402FB590, 0x1403D3290 }; + WEAK symbol Dvar_Reset{ 0x140372950, 0x1404C1DB0 }; WEAK symbol Dvar_SetCommand{ 0x1403730D0, 0x1404C2520 }; WEAK symbol Dvar_SetString{ 0x140373DE0, 0x1404C3610 }; WEAK symbol Dvar_SetFromStringByNameFromSource{ 0x1403737D0, 0x1404C2E40 }; @@ -53,10 +58,16 @@ namespace game WEAK symbol G_Glass_Update{ 0x14021D540, 0x1402EDEE0 }; + WEAK symbol Key_KeynumToString{ 0x14013F380, 0x140207C50 }; + WEAK symbol Live_SyncOnlineDataFlags{ 0x1404459A0, 0x140562830 }; + WEAK symbol LUI_OpenMenu{ 0, 0x14048E450 }; + WEAK symbol Material_RegisterHandle{ 0x1404919D0, 0x1405AFBE0 }; + WEAK symbol NetadrToSockadr{ 0, 0x1404B6F10 }; + WEAK symbol NET_OutOfBandPrint{ 0, 0x1403DADC0 }; WEAK symbol NET_SendLoopPacket{ 0, 0x1403DAF80 }; WEAK symbol NET_StringToAdr{ 0, 0x1403DB070 }; @@ -73,19 +84,38 @@ namespace game WEAK symbol ScrPlace_GetViewPlacement{ 0x14014FA70, 0x14023CB50 }; + WEAK symbol Scr_AllocArray{ 0x140317C50, 0x1403F4280 }; + WEAK symbol Scr_AllocVector{ 0x140317D10, 0x1403F4370 }; + WEAK symbol Scr_GetString{ 0x14031C570, 0x1403F8C50 }; + WEAK symbol Scr_GetInt{ 0x14031C1F0, 0x1403F88D0 }; + WEAK symbol Scr_GetFloat{ 0x14031C090, 0x1403F8820 }; + WEAK symbol Scr_GetNumParam{ 0x14031C2A0, 0x1403F8980 }; + WEAK symbol Scr_ClearOutParams{ 0x14031B7C0, 0x1403F8040 }; + WEAK symbol SV_Cmd_TokenizeString{ 0, 0x1403B0640 }; WEAK symbol SV_Cmd_EndTokenizedString{ 0, 0x1403B0600 }; + WEAK symbol SV_AddBot{ 0, 0x140438EC0 }; + WEAK symbol SV_BotIsBot{ 0, 0x140427300 }; + WEAK symbol SV_AddTestClient{ 0, 0x140439190 }; + WEAK symbol SV_CanSpawnTestClient{ 0, 0x140439460 }; + WEAK symbol SV_SpawnTestClient{ 0, 0x14043C750 }; + + WEAK symbol SV_AddEntity{ 0, 0x1403388B0 }; + WEAK symbol SV_DirectConnect{ 0, 0x1404397A0 }; + WEAK symbol SV_ExecuteClientCommand{ 0, 0x15121D8E6 }; WEAK symbol SV_FastRestart{ 0, 0x1404374E0 }; WEAK symbol SV_GameSendServerCommand{ 0x1403F3A70, 0x14043E120 }; WEAK symbol SV_GetGuid{ 0, 0x14043E1E0 }; + WEAK symbol SV_GetPlayerstateForClientNum{ 0x1403F3AB0, 0x14043E260 }; WEAK symbol SV_KickClientNum{ 0, 0x1404377A0 }; WEAK symbol SV_Loaded{ 0x1403F42C0, 0x14043FA50 }; WEAK symbolSV_MapExists{ 0, 0x140437800 }; WEAK symbol SV_StartMap{ 0, 0x140438320 }; WEAK symbol SV_StartMapForParty{ 0, 0x140438490 }; + WEAK symbol Sys_Error{ 0x14038C770, 0x1404D6260 }; WEAK symbol Sys_CreateFile{ 0x14037BCA0, 0x1404CC8A0 }; WEAK symbol Sys_IsDatabaseReady2{ 0x1402FF980, 0x1403E1840 }; WEAK symbol Sys_SendPacket{ 0x14038E720, 0x1404D8460 }; @@ -105,8 +135,9 @@ namespace game WEAK symbol dvarCount{ 0x14A7BFF34, 0x14B32AA30 }; WEAK symbol sortedDvars{ 0x14A7BFF50, 0x14B32AA50 }; - WEAK symbol svs_numclients{ 0, 0x1496C4B0C }; - WEAK symbol svs_clients{ 0, 0x1496C4B10 }; + WEAK symbol command_whitelist{ 0x140808EF0, 0x1409B8DC0 }; + + WEAK symbol query_socket{ 0, 0x14B5B9180 }; WEAK symbol DB_XAssetPool{ 0x140804690, 0x1409B40D0 }; WEAK symbol g_poolSize{ 0x140804140, 0x1409B4B90 }; @@ -117,6 +148,10 @@ namespace game namespace mp { WEAK symbol g_entities{ 0, 0x144758C70 }; + WEAK symbol svs_clients{ 0, 0x1496C4B10 }; + WEAK symbol svs_numclients{ 0, 0x1496C4B0C }; + + WEAK symbol sv_serverId_value{ 0, 0x1488A9A60 }; } namespace sp