diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp new file mode 100644 index 00000000..e9a25628 --- /dev/null +++ b/src/client/component/bots.cpp @@ -0,0 +1,106 @@ +#include +#include "loader/component_loader.hpp" + +#include "command.hpp" +#include "scheduler.hpp" +#include "network.hpp" +#include "party.hpp" + +#include "game/game.hpp" + +#include +#include +#include + +namespace bots +{ + namespace + { + bool can_add() + { + if (party::get_client_count() < *game::mp::svs_numclients) + { + return true; + } + return false; + } + + // TODO: when scripting comes, fix this to use better notifies + void bot_team_join(const int entity_num) + { + scheduler::once([entity_num]() + { + game::SV_ExecuteClientCommand(&game::mp::svs_clients[entity_num], + utils::string::va("lui 68 2 %i", *game::mp::sv_serverId_value), + false); + + // scheduler the select class call + scheduler::once([entity_num]() + { + game::SV_ExecuteClientCommand(&game::mp::svs_clients[entity_num], + utils::string::va("lui 5 %i %i", (rand() % 5) + 10, + *game::mp::sv_serverId_value), false); + }, scheduler::pipeline::server, 1s); + }, scheduler::pipeline::server, 1s); + } + + void spawn_bot(const int entity_num) + { + game::SV_SpawnTestClient(&game::mp::g_entities[entity_num]); + if (game::Com_GetCurrentCoDPlayMode() == game::CODPLAYMODE_CORE) + { + bot_team_join(entity_num); + } + } + + void add_bot() + { + if (!can_add()) + { + return; + } + + // SV_BotGetRandomName + const auto* const bot_name = game::SV_BotGetRandomName(); + auto* bot_ent = game::SV_AddBot(bot_name); + if (bot_ent) + { + spawn_bot(bot_ent->s.entityNum); + } + else if (can_add()) // workaround since first bot won't ever spawn + { + add_bot(); + } + } + } + + 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() || game::VirtualLobby_Loaded()) return; + + auto num_bots = 1; + if (params.size() == 2) + { + num_bots = atoi(params.get(1)); + } + + for (auto i = 0; i < (num_bots > *game::mp::svs_numclients ? *game::mp::svs_numclients : num_bots); i++) + { + scheduler::once(add_bot, scheduler::pipeline::server, 100ms * i); + } + }); + } + }; +} + +REGISTER_COMPONENT(bots::component) \ No newline at end of file diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp new file mode 100644 index 00000000..7d55c989 --- /dev/null +++ b/src/client/component/discord.cpp @@ -0,0 +1,144 @@ +#include +#include "loader/component_loader.hpp" +#include "scheduler.hpp" +#include "game/game.hpp" + +#include "console.hpp" +#include "command.hpp" +#include "network.hpp" +#include "party.hpp" + +#include + +#include + +namespace discord +{ + namespace + { + DiscordRichPresence discord_presence; + + void update_discord() + { + Discord_RunCallbacks(); + + if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) + { + discord_presence.details = game::environment::is_sp() ? "Singleplayer" : "Multiplayer"; + discord_presence.state = "Main Menu"; + + auto firingRangeDvar = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (firingRangeDvar && firingRangeDvar->current.enabled == 1) + { + discord_presence.state = "Firing Range"; + } + + discord_presence.partySize = 0; + discord_presence.partyMax = 0; + discord_presence.startTimestamp = 0; + discord_presence.largeImageKey = game::environment::is_sp() ? "menu_singleplayer" : "menu_multiplayer"; + } + else + { + const auto map = game::Dvar_FindVar("mapname")->current.string; + const auto mapname = game::UI_SafeTranslateString(utils::string::va("PRESENCE_%s%s", (game::environment::is_sp() ? "SP_" : ""), map)); + + if (game::environment::is_mp()) + { + const auto gametype = game::UI_GetGameTypeDisplayName(game::Dvar_FindVar("g_gametype")->current.string); + + discord_presence.details = utils::string::va("%s on %s", gametype, mapname); + + auto host_name = game::Dvar_FindVar("sv_hostname")->current.string; + auto max_clients = game::Dvar_FindVar("sv_maxclients")->current.integer; + if (game::SV_Loaded()) + { + max_clients = party::server_client_count(); + } + + auto clients = *(reinterpret_cast(0x14621BE00)); + discord_presence.partySize = clients; + discord_presence.partyMax = max_clients; + discord_presence.state = host_name; + discord_presence.largeImageKey = map; + } + else if (game::environment::is_sp()) + { + discord_presence.state = ""; + discord_presence.largeImageKey = map; + discord_presence.details = mapname; + } + + if (!discord_presence.startTimestamp) + { + discord_presence.startTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } + } + + Discord_UpdatePresence(&discord_presence); + } + } + + class component final : public component_interface + { + public: + void post_load() override + { + if (game::environment::is_dedi()) + { + return; + } + + DiscordEventHandlers handlers; + ZeroMemory(&handlers, sizeof(handlers)); + handlers.ready = ready; + handlers.errored = errored; + handlers.disconnected = errored; + handlers.joinGame = nullptr; + handlers.spectateGame = nullptr; + handlers.joinRequest = nullptr; + + Discord_Initialize("947125042930667530", &handlers, 1, nullptr); + + scheduler::once([]() + { + scheduler::once(update_discord, scheduler::pipeline::async); + scheduler::loop(update_discord, scheduler::pipeline::async, 5s); + }, scheduler::pipeline::main); + + initialized_ = true; + } + + void pre_destroy() override + { + if (!initialized_ || game::environment::is_dedi()) + { + return; + } + + Discord_Shutdown(); + } + + private: + bool initialized_ = false; + + static void ready(const DiscordUser* /*request*/) + { + ZeroMemory(&discord_presence, sizeof(discord_presence)); + + discord_presence.instance = 1; + + console::info("Discord: Ready\n"); + + Discord_UpdatePresence(&discord_presence); + } + + static void errored(const int error_code, const char* message) + { + console::error("Discord: Error (%i): %s\n", error_code, message); + } + }; +} + +REGISTER_COMPONENT(discord::component) \ No newline at end of file diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 5df17798..b12f3c92 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -11,8 +11,11 @@ namespace network { + SOCKET sock; + namespace { + std::unordered_map& get_callbacks() { static std::unordered_map callbacks{}; @@ -33,6 +36,9 @@ namespace network const std::string_view data(message->data + offset, message->cursize - offset); handler->second(*address, data); +#ifdef DEBUG + console::info("[Network] Handling command %s\n", cmd_string.data()); +#endif return true; } @@ -107,7 +113,7 @@ namespace network { sockaddr s = {}; game::NetadrToSockadr(a3, &s); - return sendto(*game::query_socket, src, size, 0, &s, 16) >= 0; + return sendto(sock, src, size, 0, &s, 16) >= 0; } void send(const game::netadr_s& address, const std::string& command, const std::string& data, const char separator) @@ -117,6 +123,10 @@ namespace network packet.push_back(separator); packet.append(data); +#ifdef DEBUG + console::info("[Network] Sending command %s\n", command.data()); +#endif + send_data(address, packet); } @@ -182,6 +192,53 @@ namespace network return dvar; } + utils::hook::detour bind_socket_hook; + + SOCKET bind_socket_stub(const char* net_interface, u_short port, int protocol) + { +#ifdef DEBUG + printf("[Socket] Attempting to create socket\n"); +#endif + + sock = socket(2, 2, protocol); + u_long argp; + char optval; + struct sockaddr name; + + memset(&name, 0, sizeof(name)); + name.sa_family = 2; + + if (sock == -1) + { +#ifdef DEBUG + printf("[Socket] Error creating socket\n"); +#endif + WSAGetLastError(); + return 0; + } + + argp = 1; + optval = 1; + if (ioctlsocket(sock, -2147195266, &argp) == -1 || setsockopt(sock, 0xFFFF, 32, &optval, 4) == -1) + return 0; + + *(WORD*)name.sa_data = ntohs(port); + + if (bind(sock, &name, 16) != -1) + { +#ifdef DEBUG + printf("[Socket] Socket binded!\n"); +#endif + return sock; + } + +#ifdef DEBUG + printf("[Socket] Closing socket\n"); +#endif + closesocket(sock); + return 0; + } + class component final : public component_interface { public: @@ -193,6 +250,9 @@ namespace network return; } + // creating our own variable for socket use + bind_socket_hook.create(0x140512B40, bind_socket_stub); + // redirect dw_sendto to raw socket //utils::hook::jump(0x1404D850A, reinterpret_cast(0x1404D849A)); utils::hook::call(0x140513467, dw_send_to_stub); // H1MP64(1.4) diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp new file mode 100644 index 00000000..fc84d928 --- /dev/null +++ b/src/client/component/party.cpp @@ -0,0 +1,623 @@ +#include +#include "loader/component_loader.hpp" + +#include "party.hpp" +#include "console.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{}; + bool hostDefined{false}; + } connect_state; + + std::string sv_motd; + int sv_maxclients; + + void perform_game_initialization() + { + command::execute("onlinegame 1", 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) != 0) + { + // initialize the game after onlinedataflags is 32 (workaround) + if (game::Live_SyncOnlineDataFlags(0) == 32) + { + scheduler::once([=]() + { + command::execute("xstartprivateparty", true); + command::execute("disconnect", true); // 32 -> 0 + + connect_to_party(target, mapname, gametype); + }, scheduler::pipeline::main, 1s); + return; + } + else + { + scheduler::once([=]() + { + connect_to_party(target, mapname, gametype); + }, scheduler::pipeline::main, 1s); + return; + } + } + + perform_game_initialization(); + + // exit from virtuallobby + reinterpret_cast(0x140256D40)(); + + // CL_ConnectFromParty + char session_info[0x100] = {}; + reinterpret_cast(0x140251560)( + 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_dvar_int(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.integer) + { + return dvar_value->current.integer; + } + + return -1; + } + + bool get_dvar_bool(const std::string& dvar) + { + auto* dvar_value = game::Dvar_FindVar(dvar.data()); + if (dvar_value && dvar_value->current.enabled) + { + return dvar_value->current.enabled; + } + + return false; + } + + void didyouknow_stub(const char* dvar_name, const char* string) + { + if (!party::sv_motd.empty()) + { + string = party::sv_motd.data(); + } + + // This function either does Dvar_SetString or Dvar_RegisterString for the given dvar + reinterpret_cast(0x1404FB210)(dvar_name, string); + } + + void disconnect_stub() + { + if (!game::VirtualLobby_Loaded()) + { + if (game::CL_IsCgameInitialized()) + { + // CL_ForwardCommandToServer + reinterpret_cast(0x140253480)(0, "disconnect"); + // CL_WritePacket + reinterpret_cast(0x14024DB10)(0); + } + // CL_Disconnect + reinterpret_cast(0x140252060)(0); + } + } + + utils::hook::detour cldisconnect_hook; + + void cldisconnect_stub(int a1) + { + party::sv_motd.clear(); + cldisconnect_hook.invoke(a1); + } + + const auto drop_reason_stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.mov(rdx, rdi); + a.mov(ecx, 2); + a.jmp(0x140251F78); + }); + } + + int get_client_num_by_name(const std::string& name) + { + for (auto i = 0; !name.empty() && i < *game::mp::svs_numclients; ++i) + { + if (game::mp::g_entities[i].client) + { + char client_name[16] = {0}; + strncpy_s(client_name, game::mp::g_entities[i].client->name, sizeof(client_name)); + game::I_CleanStr(client_name); + + if (client_name == name) + { + return i; + } + } + } + return -1; + } + + void reset_connect_state() + { + connect_state = {}; + } + + 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 >= 1) + { + ++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 >= 1 && + game::SV_BotIsBot(i)) + { + ++count; + } + } + + return count; + } + + void connect(const game::netadr_s& target) + { + if (game::environment::is_sp()) + { + return; + } + + command::execute("lui_open_popup popup_acceptinginvite", false); + + connect_state.host = target; + connect_state.challenge = utils::cryptography::random::get_challenge(); + connect_state.hostDefined = true; + + network::send(target, "getInfo", connect_state.challenge); + } + + void start_map(const std::string& mapname) + { + if (game::Live_SyncOnlineDataFlags(0) > 32) + { + scheduler::once([=]() + { + command::execute("map " + mapname, false); + }, scheduler::pipeline::main, 1s); + } + else + { + if (!game::SV_MapExists(mapname.data())) + { + console::info("Map '%s' doesn't exist.\n", 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() && !game::VirtualLobby_Loaded())) + { + console::info("Restarting map: %s\n", mapname.data()); + command::execute("map_restart", false); + return; + } + + if (!game::environment::is_dedi()) + { + if (game::SV_Loaded()) + { + const auto* args = "Leave"; + game::UI_RunMenuScript(0, &args); + } + + perform_game_initialization(); + } + + console::info("Starting map: %s\n", mapname.data()); + + auto* gametype = game::Dvar_FindVar("g_gametype"); + if (gametype && gametype->current.string) + { + command::execute(utils::string::va("ui_gametype %s", gametype->current.string), true); + } + command::execute(utils::string::va("ui_mapname %s", mapname.data()), true); + + /*auto* maxclients = game::Dvar_FindVar("sv_maxclients"); + if (maxclients) + { + command::execute(utils::string::va("ui_maxclients %i", maxclients->current.integer), true); + command::execute(utils::string::va("party_maxplayers %i", maxclients->current.integer), true); + }*/ + + const auto* args = "StartServer"; + game::UI_RunMenuScript(0, &args); + } + } + + int server_client_count() + { + return party::sv_maxclients; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + // hook disconnect command function + utils::hook::jump(0x1402521C7, disconnect_stub); + + // detour CL_Disconnect to clear motd + cldisconnect_hook.create(0x140252060, cldisconnect_stub); + + if (game::environment::is_mp()) + { + // show custom drop reason + utils::hook::nop(0x140251EFB, 13); + utils::hook::jump(0x140251EFB, drop_reason_stub, true); + } + // enable custom kick reason in GScr_KickPlayer + utils::hook::set(0x140376A1D, 0xEB); + + command::add("map", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + start_map(argument[1]); + }); + + command::add("map_restart", []() + { + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + *reinterpret_cast(0x14A3A91D0) = 1; // sv_map_restart + *reinterpret_cast(0x14A3A91D4) = 1; // sv_loadScripts + *reinterpret_cast(0x14A3A91D8) = 0; // sv_migrate + reinterpret_cast(0x14047E7F0)(); // SV_CheckLoadGame + }); + + command::add("fast_restart", []() + { + if (game::SV_Loaded() && !game::VirtualLobby_Loaded()) + { + game::SV_FastRestart(0); + } + }); + + command::add("reconnect", [](const command::params& argument) + { + if (!connect_state.hostDefined) + { + console::info("Cannot connect to server.\n"); + return; + } + + if (game::CL_IsCgameInitialized()) + { + command::execute("disconnect"); + command::execute("reconnect"); + } + else + { + connect(connect_state.host); + } + }); + + 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("kickClient", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: kickClient , (optional)\n"); + return; + } + + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + + std::string reason; + if (params.size() > 2) + { + reason = params.join(2); + } + if (reason.empty()) + { + reason = "EXE_PLAYERKICKED"; + } + + const auto client_num = atoi(params.get(1)); + if (client_num < 0 || client_num >= *game::mp::svs_numclients) + { + return; + } + + scheduler::once([client_num, reason]() + { + game::SV_KickClientNum(client_num, reason.data()); + }, scheduler::pipeline::server); + }); + + command::add("kick", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: kick , (optional)\n"); + return; + } + + if (!game::SV_Loaded() || game::VirtualLobby_Loaded()) + { + return; + } + + std::string reason; + if (params.size() > 2) + { + reason = params.join(2); + } + if (reason.empty()) + { + reason = "EXE_PLAYERKICKED"; + } + + const std::string name = params.get(1); + if (name == "all"s) + { + for (auto i = 0; i < *game::mp::svs_numclients; ++i) + { + scheduler::once([i, reason]() + { + game::SV_KickClientNum(i, reason.data()); + }, scheduler::pipeline::server); + } + return; + } + + const auto client_num = get_client_num_by_name(name); + if (client_num < 0 || client_num >= *game::mp::svs_numclients) + { + return; + } + + scheduler::once([client_num, reason]() + { + game::SV_KickClientNum(client_num, reason.data()); + }, scheduler::pipeline::server); + }); + + scheduler::once([]() + { + const auto hash = game::generateHashValue("sv_sayName"); + game::Dvar_RegisterString(hash, "sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE); + }, 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, game::SV_CMD_CAN_IGNORE, + 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, game::SV_CMD_CAN_IGNORE, + 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, game::SV_CMD_CAN_IGNORE, 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, game::SV_CMD_CAN_IGNORE, + utils::string::va("%c \"%s\"", 84, message.data())); + printf("%s\n", message.data()); + }); + + utils::hook::call(0x1404C6E8D, didyouknow_stub); // allow custom didyouknow based on sv_motd + + 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("sv_running", utils::string::va("%i", get_dvar_bool("sv_running"))); + info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); + + 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 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; + } + + const auto playmode = info.get("playmode"); + if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode()) + { + const auto str = "Invalid playmode."; + printf("%s\n", str); + game::Com_Error(game::ERR_DROP, str); + return; + } + + const auto sv_running = info.get("sv_running"); + if (!std::atoi(sv_running.data())) + { + const auto str = "Server not running."; + 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; + } + + party::sv_motd = info.get("sv_motd"); + party::sv_maxclients = std::stoi(info.get("sv_maxclients")); + + connect_to_party(target, mapname, gametype); + }); + } + }; +} + +REGISTER_COMPONENT(party::component) \ No newline at end of file diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp new file mode 100644 index 00000000..cd90ae9f --- /dev/null +++ b/src/client/component/party.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "game/game.hpp" + +namespace party +{ + void reset_connect_state(); + + void connect(const game::netadr_s& target); + void start_map(const std::string& mapname); + + int server_client_count(); + + int get_client_num_by_name(const std::string& name); + + int get_client_count(); + int get_bot_count(); +} \ No newline at end of file diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp new file mode 100644 index 00000000..ea0dedf3 --- /dev/null +++ b/src/client/component/server_list.cpp @@ -0,0 +1,445 @@ +#include +#include "loader/component_loader.hpp" +#include "server_list.hpp" +#include "localized_strings.hpp" +#include "network.hpp" +#include "scheduler.hpp" +#include "party.hpp" +#include "game/game.hpp" + +#include +#include +#include + +#include "console.hpp" + +namespace server_list +{ + namespace + { + const int server_limit = 18; + + struct server_info + { + // gotta add more to this + int clients; + int max_clients; + int bots; + int ping; + std::string host_name; + std::string map_name; + std::string game_type; + game::CodPlayMode play_mode; + char in_game; + game::netadr_s address; + }; + + struct + { + game::netadr_s address{}; + volatile bool requesting = false; + std::unordered_map queued_servers{}; + } master_state; + + std::mutex mutex; + std::vector servers; + + size_t server_list_page = 0; + volatile bool update_server_list = false; + std::chrono::high_resolution_clock::time_point last_scroll{}; + + size_t get_page_count() + { + const auto count = servers.size() / server_limit; + return count + (servers.size() % server_limit > 0); + } + + size_t get_page_base_index() + { + return server_list_page * server_limit; + } + + void refresh_server_list() + { + { + std::lock_guard _(mutex); + servers.clear(); + master_state.queued_servers.clear(); + server_list_page = 0; + } + + party::reset_connect_state(); + + if (get_master_server(master_state.address)) + { + master_state.requesting = true; + + network::send(master_state.address, "getservers", utils::string::va("S1 %i full empty", PROTOCOL)); + } + } + + void join_server(int, int, const int index) + { + std::lock_guard _(mutex); + + const auto i = static_cast(index) + get_page_base_index(); + if (i < servers.size()) + { + static auto last_index = ~0ull; + if (last_index != i) + { + last_index = i; + } + else + { + console::info("Connecting to (%d - %zu): %s\n", index, i, servers[i].host_name.data()); + party::connect(servers[i].address); + } + } + } + + void trigger_refresh() + { + update_server_list = true; + } + + int ui_feeder_count() + { + std::lock_guard _(mutex); + if (update_server_list) + { + update_server_list = false; + return 0; + } + const auto count = static_cast(servers.size()); + const auto index = get_page_base_index(); + const auto diff = count - index; + return diff > server_limit ? server_limit : static_cast(diff); + } + + const char* ui_feeder_item_text(int /*localClientNum*/, void* /*a2*/, void* /*a3*/, const int index, + const int column) + { + std::lock_guard _(mutex); + + const auto i = get_page_base_index() + index; + + if (i >= servers.size()) + { + return ""; + } + + if (column == 0) + { + return servers[i].host_name.empty() ? "" : utils::string::va("%s", servers[i].host_name.data()); + } + + if (column == 1) + { + return servers[i].map_name.empty() ? "Unknown" : utils::string::va("%s", servers[i].map_name.data()); + } + + if (column == 2) + { + return utils::string::va("%d/%d [%d]", servers[i].clients, servers[index].max_clients, + servers[i].bots); + } + + if (column == 3) + { + return servers[i].game_type.empty() ? "" : utils::string::va("%s", servers[i].game_type.data()); + } + + return ""; + } + + void sort_serverlist() + { + std::stable_sort(servers.begin(), servers.end(), [](const server_info& a, const server_info& b) + { + if (a.clients == b.clients) + { + return a.ping < b.ping; + } + + return a.clients > b.clients; + }); + } + + void insert_server(server_info&& server) + { + std::lock_guard _(mutex); + servers.emplace_back(std::move(server)); + sort_serverlist(); + trigger_refresh(); + } + + void do_frame_work() + { + auto& queue = master_state.queued_servers; + if (queue.empty()) + { + return; + } + + std::lock_guard _(mutex); + + size_t queried_servers = 0; + const size_t query_limit = 3; + + for (auto i = queue.begin(); i != queue.end();) + { + if (i->second) + { + const auto now = game::Sys_Milliseconds(); + if (now - i->second > 10'000) + { + i = queue.erase(i); + continue; + } + } + else if (queried_servers++ < query_limit) + { + i->second = game::Sys_Milliseconds(); + network::send(i->first, "getInfo", utils::cryptography::random::get_challenge()); + } + + ++i; + } + } + + bool is_server_list_open() + { + return game::Menu_IsMenuOpenAndVisible(0, "menu_systemlink_join"); + } + + bool is_scrolling_disabled() + { + return update_server_list || (std::chrono::high_resolution_clock::now() - last_scroll) < 500ms; + } + + bool scroll_down() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page + 1 < get_page_count()) + { + last_scroll = std::chrono::high_resolution_clock::now(); + ++server_list_page; + trigger_refresh(); + } + + return true; + } + + bool scroll_up() + { + if (!is_server_list_open()) + { + return false; + } + + if (!is_scrolling_disabled() && server_list_page > 0) + { + last_scroll = std::chrono::high_resolution_clock::now(); + --server_list_page; + trigger_refresh(); + } + + return true; + } + + void resize_host_name(std::string& name) + { + name = utils::string::split(name, '\n').front(); + + game::Font_s* font = game::R_RegisterFont("fonts/default.otf", 18); + auto text_size = game::UI_TextWidth(name.data(), 32, font, 1.0f); + + while (text_size > 450) + { + text_size = game::UI_TextWidth(name.data(), 32, font, 1.0f); + name.pop_back(); + } + } + + utils::hook::detour lui_open_menu_hook; + + void lui_open_menu_stub(int controllerIndex, const char* menu, int a3, int a4, unsigned int a5) + { +#ifdef DEBUG + console::info("[LUI] %s\n", menu); +#endif + + if (!strcmp(menu, "menu_systemlink_join")) + { + refresh_server_list(); + } + + lui_open_menu_hook.invoke(controllerIndex, menu, a3, a4, a5); + } + } + + bool sl_key_event(const int key, const int down) + { + if (down) + { + if (key == game::keyNum_t::K_MWHEELUP) + { + return !scroll_up(); + } + + if (key == game::keyNum_t::K_MWHEELDOWN) + { + return !scroll_down(); + } + } + + return true; + } + + bool get_master_server(game::netadr_s& address) + { + return game::NET_StringToAdr("master.xlabs.dev:20810", &address); // localhost works, but not outside localhost + // return game::NET_StringToAdr("master.xlabs.dev:20810", &address); + // return game::NET_StringToAdr("master.ff.h1p.co:20180", &address); + } + + void handle_info_response(const game::netadr_s& address, const utils::info_string& info) + { + // Don't show servers that aren't dedicated! + const auto dedicated = std::atoi(info.get("dedicated").data()); + if (!dedicated) + { + printf("not dedi\n"); + return; + } + + // Don't show servers that aren't running! + const auto sv_running = std::atoi(info.get("sv_running").data()); + if (!sv_running) + { + return; + } + + // Only handle servers of the same playmode! + const auto playmode = game::CodPlayMode(std::atoi(info.get("playmode").data())); + if (game::Com_GetCurrentCoDPlayMode() != playmode) + { + return; + } + + int start_time{}; + const auto now = game::Sys_Milliseconds(); + + { + std::lock_guard _(mutex); + const auto entry = master_state.queued_servers.find(address); + + if (entry == master_state.queued_servers.end() || !entry->second) + { + return; + } + + start_time = entry->second; + master_state.queued_servers.erase(entry); + } + + server_info server{}; + server.address = address; + server.host_name = info.get("hostname"); + server.map_name = game::UI_GetMapDisplayName(info.get("mapname").data()); + server.game_type = game::UI_GetGameTypeDisplayName(info.get("gametype").data()); + server.play_mode = playmode; + server.clients = atoi(info.get("clients").data()); + server.max_clients = atoi(info.get("sv_maxclients").data()); + server.bots = atoi(info.get("bots").data()); + server.ping = std::min(now - start_time, 999); + + server.in_game = 1; + + resize_host_name(server.host_name); + + insert_server(std::move(server)); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) return; + + localized_strings::override("PLATFORM_SYSTEM_LINK_TITLE", "SERVER LIST"); + localized_strings::override("LUA_MENU_STORE", "Server List"); + localized_strings::override("LUA_MENU_STORE_DESC", "Browse available servers."); + + // shitty ping workaround + // localized_strings::override("MENU_NUMPLAYERS", "Type"); + + // hook LUI_OpenMenu to refresh server list for system link menu + lui_open_menu_hook.create(game::LUI_OpenMenu, lui_open_menu_stub); + + // replace UI_RunMenuScript call in LUI_CoD_LuaCall_RefreshServerList to our refresh_servers + utils::hook::call(0x14018A0C9, &refresh_server_list); + utils::hook::call(0x14018A5E3, &join_server); + utils::hook::nop(0x14018A5FD, 5); + + // do feeder stuff + utils::hook::call(0x14018A199, &ui_feeder_count); + utils::hook::call(0x14018A3B1, &ui_feeder_item_text); + + scheduler::loop(do_frame_work, scheduler::pipeline::main); + + network::on("getServersResponse", [](const game::netadr_s& target, const std::string_view& data) + { + console::info("getServersResponse\n"); + { + std::lock_guard _(mutex); + if (!master_state.requesting || master_state.address != target) + { + return; + } + + master_state.requesting = false; + + std::optional start{}; + for (size_t i = 0; i + 6 < data.size(); ++i) + { + if (data[i + 6] == '\\') + { + start.emplace(i); + break; + } + } + + if (!start.has_value()) + { + return; + } + + for (auto i = start.value(); i + 6 < data.size(); i += 7) + { + if (data[i + 6] != '\\') + { + break; + } + + game::netadr_s address{}; + address.type = game::NA_IP; + address.localNetID = game::NS_CLIENT1; + memcpy(&address.ip[0], data.data() + i + 0, 4); + memcpy(&address.port, data.data() + i + 4, 2); + + master_state.queued_servers[address] = 0; + } + } + }); + } + }; +} + +REGISTER_COMPONENT(server_list::component) \ No newline at end of file diff --git a/src/client/component/server_list.hpp b/src/client/component/server_list.hpp new file mode 100644 index 00000000..d9974cfa --- /dev/null +++ b/src/client/component/server_list.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "game/game.hpp" +#include + +namespace server_list +{ + bool get_master_server(game::netadr_s& address); + void handle_info_response(const game::netadr_s& address, const utils::info_string& info); + + bool sl_key_event(int key, int down); +} \ No newline at end of file diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 8134d627..1bc8a558 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -92,12 +92,19 @@ namespace game WEAK symbol LUI_OpenMenu{0, 0x1404CD210}; + WEAK symbol Menu_IsMenuOpenAndVisible{0, 0x1404C7320}; + WEAK symbol SL_FindString{0x140314AF0, 0x14043B470}; WEAK symbol SV_DirectConnect{0, 0x140480860}; WEAK symbol SV_Cmd_TokenizeString{0x1402EF050, 0x140404D20}; WEAK symbol SV_Cmd_EndTokenizedString{0x140344700, 0x140404CE0}; + + WEAK symbol SV_AddBot{0, 0x140480190}; WEAK symbol SV_BotIsBot{0, 0x14046E6C0}; + WEAK symbol SV_BotGetRandomName{0, 0x14046DBA0}; + WEAK symbol SV_SpawnTestClient{ 0, 0x1404832A0 }; + WEAK symbol SV_GetGuid{0, 0x140484B90}; WEAK symbol SV_GetClientPing{0, 0x140484B70}; WEAK symbol SV_GetPlayerstateForClientNum{0x1404426D0, 0x140484C10}; @@ -105,14 +112,12 @@ namespace game WEAK symbol SV_Loaded{0x140442F60, 0x1404864A0}; WEAK symbol SV_KickClientNum{0, 0x14047ED00}; WEAK symbol SV_MapExists{0, 0x14047ED60}; + WEAK symbol SV_ExecuteClientCommand{0, 0x140481870}; WEAK symbol SV_FastRestart{0, 0x14047E990}; WEAK symbol SV_GameSendServerCommand{ 0x1403F3A70, 0x140484AD0 }; - WEAK symbol Menu_IsMenuOpenAndVisible{ 0, 0x1404C7320 }; - WEAK symbol UI_SafeTranslateString{ 0x140350430, 0x1405A2930 }; - WEAK symbol Sys_ShowConsole{0x1403E3B90, 0x140514910}; WEAK symbol Sys_Error{0x1403E0C40, 0x140511520}; WEAK symbol @@ -127,6 +132,8 @@ namespace game WEAK symbol UI_RunMenuScript{0, 0x1404CFE60}; WEAK symbol UI_TextWidth{0, 0x1404D21A0}; + WEAK symbol UI_SafeTranslateString{0x140350430, 0x14041C580}; + /*************************************************************** * Variables **************************************************************/ @@ -160,6 +167,8 @@ namespace game WEAK symbol svs_numclients{0, 0x14B204A0C}; WEAK symbol gameTime{0, 0x14621BDBC}; + WEAK symbol sv_serverId_value{0, 0x14A3E99B8}; + WEAK symbol virtualLobby_loaded{0, 0x142D077FD}; }