new components + more symbols (#10)
* add bots, discord, party components + more symbols * proper check for server player count * progress on party/server list * fix socket, server list now shows S1 servers
This commit is contained in:
parent
4f170ca1cf
commit
6db0cde0ce
106
src/client/component/bots.cpp
Normal file
106
src/client/component/bots.cpp
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#include <std_include.hpp>
|
||||||
|
#include "loader/component_loader.hpp"
|
||||||
|
|
||||||
|
#include "command.hpp"
|
||||||
|
#include "scheduler.hpp"
|
||||||
|
#include "network.hpp"
|
||||||
|
#include "party.hpp"
|
||||||
|
|
||||||
|
#include "game/game.hpp"
|
||||||
|
|
||||||
|
#include <utils/hook.hpp>
|
||||||
|
#include <utils/string.hpp>
|
||||||
|
#include <utils/cryptography.hpp>
|
||||||
|
|
||||||
|
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)
|
144
src/client/component/discord.cpp
Normal file
144
src/client/component/discord.cpp
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#include <std_include.hpp>
|
||||||
|
#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 <utils/string.hpp>
|
||||||
|
|
||||||
|
#include <discord_rpc.h>
|
||||||
|
|
||||||
|
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<int*>(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::seconds>(
|
||||||
|
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)
|
@ -11,8 +11,11 @@
|
|||||||
|
|
||||||
namespace network
|
namespace network
|
||||||
{
|
{
|
||||||
|
SOCKET sock;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
std::unordered_map<std::string, callback>& get_callbacks()
|
std::unordered_map<std::string, callback>& get_callbacks()
|
||||||
{
|
{
|
||||||
static std::unordered_map<std::string, callback> callbacks{};
|
static std::unordered_map<std::string, callback> callbacks{};
|
||||||
@ -33,6 +36,9 @@ namespace network
|
|||||||
const std::string_view data(message->data + offset, message->cursize - offset);
|
const std::string_view data(message->data + offset, message->cursize - offset);
|
||||||
|
|
||||||
handler->second(*address, data);
|
handler->second(*address, data);
|
||||||
|
#ifdef DEBUG
|
||||||
|
console::info("[Network] Handling command %s\n", cmd_string.data());
|
||||||
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +113,7 @@ namespace network
|
|||||||
{
|
{
|
||||||
sockaddr s = {};
|
sockaddr s = {};
|
||||||
game::NetadrToSockadr(a3, &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)
|
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.push_back(separator);
|
||||||
packet.append(data);
|
packet.append(data);
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
console::info("[Network] Sending command %s\n", command.data());
|
||||||
|
#endif
|
||||||
|
|
||||||
send_data(address, packet);
|
send_data(address, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +192,53 @@ namespace network
|
|||||||
return dvar;
|
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
|
class component final : public component_interface
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -193,6 +250,9 @@ namespace network
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// creating our own variable for socket use
|
||||||
|
bind_socket_hook.create(0x140512B40, bind_socket_stub);
|
||||||
|
|
||||||
// redirect dw_sendto to raw socket
|
// redirect dw_sendto to raw socket
|
||||||
//utils::hook::jump(0x1404D850A, reinterpret_cast<void*>(0x1404D849A));
|
//utils::hook::jump(0x1404D850A, reinterpret_cast<void*>(0x1404D849A));
|
||||||
utils::hook::call(0x140513467, dw_send_to_stub); // H1MP64(1.4)
|
utils::hook::call(0x140513467, dw_send_to_stub); // H1MP64(1.4)
|
||||||
|
623
src/client/component/party.cpp
Normal file
623
src/client/component/party.cpp
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
#include <std_include.hpp>
|
||||||
|
#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 <utils/string.hpp>
|
||||||
|
#include <utils/info_string.hpp>
|
||||||
|
#include <utils/cryptography.hpp>
|
||||||
|
#include <utils/hook.hpp>
|
||||||
|
|
||||||
|
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<void(*)()>(0x140256D40)();
|
||||||
|
|
||||||
|
// CL_ConnectFromParty
|
||||||
|
char session_info[0x100] = {};
|
||||||
|
reinterpret_cast<void(*)(int, char*, const game::netadr_s*, const char*, const char*)>(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<void(*)(const char*, const char*)>(0x1404FB210)(dvar_name, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect_stub()
|
||||||
|
{
|
||||||
|
if (!game::VirtualLobby_Loaded())
|
||||||
|
{
|
||||||
|
if (game::CL_IsCgameInitialized())
|
||||||
|
{
|
||||||
|
// CL_ForwardCommandToServer
|
||||||
|
reinterpret_cast<void (*)(int, const char*)>(0x140253480)(0, "disconnect");
|
||||||
|
// CL_WritePacket
|
||||||
|
reinterpret_cast<void (*)(int)>(0x14024DB10)(0);
|
||||||
|
}
|
||||||
|
// CL_Disconnect
|
||||||
|
reinterpret_cast<void (*)(int)>(0x140252060)(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils::hook::detour cldisconnect_hook;
|
||||||
|
|
||||||
|
void cldisconnect_stub(int a1)
|
||||||
|
{
|
||||||
|
party::sv_motd.clear();
|
||||||
|
cldisconnect_hook.invoke<void>(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<uint8_t>(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<int*>(0x14A3A91D0) = 1; // sv_map_restart
|
||||||
|
*reinterpret_cast<int*>(0x14A3A91D4) = 1; // sv_loadScripts
|
||||||
|
*reinterpret_cast<int*>(0x14A3A91D8) = 0; // sv_migrate
|
||||||
|
reinterpret_cast<void(*)()>(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 <num>, <reason>(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 <name>, <reason>(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)
|
17
src/client/component/party.hpp
Normal file
17
src/client/component/party.hpp
Normal file
@ -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();
|
||||||
|
}
|
445
src/client/component/server_list.cpp
Normal file
445
src/client/component/server_list.cpp
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
#include <std_include.hpp>
|
||||||
|
#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 <utils/cryptography.hpp>
|
||||||
|
#include <utils/string.hpp>
|
||||||
|
#include <utils/hook.hpp>
|
||||||
|
|
||||||
|
#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<game::netadr_s, int> queued_servers{};
|
||||||
|
} master_state;
|
||||||
|
|
||||||
|
std::mutex mutex;
|
||||||
|
std::vector<server_info> 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<std::mutex> _(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<std::mutex> _(mutex);
|
||||||
|
|
||||||
|
const auto i = static_cast<size_t>(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<std::mutex> _(mutex);
|
||||||
|
if (update_server_list)
|
||||||
|
{
|
||||||
|
update_server_list = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto count = static_cast<int>(servers.size());
|
||||||
|
const auto index = get_page_base_index();
|
||||||
|
const auto diff = count - index;
|
||||||
|
return diff > server_limit ? server_limit : static_cast<int>(diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ui_feeder_item_text(int /*localClientNum*/, void* /*a2*/, void* /*a3*/, const int index,
|
||||||
|
const int column)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> _(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<std::mutex> _(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<std::mutex> _(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<void>(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<std::mutex> _(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<std::mutex> _(mutex);
|
||||||
|
if (!master_state.requesting || master_state.address != target)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
master_state.requesting = false;
|
||||||
|
|
||||||
|
std::optional<size_t> 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)
|
12
src/client/component/server_list.hpp
Normal file
12
src/client/component/server_list.hpp
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/game.hpp"
|
||||||
|
#include <utils/info_string.hpp>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
@ -92,12 +92,19 @@ namespace game
|
|||||||
|
|
||||||
WEAK symbol<void(int clientNum, const char* menu, int a3, int a4, unsigned int a5)> LUI_OpenMenu{0, 0x1404CD210};
|
WEAK symbol<void(int clientNum, const char* menu, int a3, int a4, unsigned int a5)> LUI_OpenMenu{0, 0x1404CD210};
|
||||||
|
|
||||||
|
WEAK symbol<bool(int clientNum, const char* menu)> Menu_IsMenuOpenAndVisible{0, 0x1404C7320};
|
||||||
|
|
||||||
WEAK symbol<scr_string_t(const char* str)> SL_FindString{0x140314AF0, 0x14043B470};
|
WEAK symbol<scr_string_t(const char* str)> SL_FindString{0x140314AF0, 0x14043B470};
|
||||||
|
|
||||||
WEAK symbol<void(netadr_s* from)> SV_DirectConnect{0, 0x140480860};
|
WEAK symbol<void(netadr_s* from)> SV_DirectConnect{0, 0x140480860};
|
||||||
WEAK symbol<void(const char* text_in)> SV_Cmd_TokenizeString{0x1402EF050, 0x140404D20};
|
WEAK symbol<void(const char* text_in)> SV_Cmd_TokenizeString{0x1402EF050, 0x140404D20};
|
||||||
WEAK symbol<void()> SV_Cmd_EndTokenizedString{0x140344700, 0x140404CE0};
|
WEAK symbol<void()> SV_Cmd_EndTokenizedString{0x140344700, 0x140404CE0};
|
||||||
|
|
||||||
|
WEAK symbol<mp::gentity_s*(const char* name)> SV_AddBot{0, 0x140480190};
|
||||||
WEAK symbol<bool(int clientNum)> SV_BotIsBot{0, 0x14046E6C0};
|
WEAK symbol<bool(int clientNum)> SV_BotIsBot{0, 0x14046E6C0};
|
||||||
|
WEAK symbol<const char* ()> SV_BotGetRandomName{0, 0x14046DBA0};
|
||||||
|
WEAK symbol<int(mp::gentity_s* ent)> SV_SpawnTestClient{ 0, 0x1404832A0 };
|
||||||
|
|
||||||
WEAK symbol<const char* (int clientNum)> SV_GetGuid{0, 0x140484B90};
|
WEAK symbol<const char* (int clientNum)> SV_GetGuid{0, 0x140484B90};
|
||||||
WEAK symbol<int(int clientNum)> SV_GetClientPing{0, 0x140484B70};
|
WEAK symbol<int(int clientNum)> SV_GetClientPing{0, 0x140484B70};
|
||||||
WEAK symbol<playerState_s* (int num)> SV_GetPlayerstateForClientNum{0x1404426D0, 0x140484C10};
|
WEAK symbol<playerState_s* (int num)> SV_GetPlayerstateForClientNum{0x1404426D0, 0x140484C10};
|
||||||
@ -105,14 +112,12 @@ namespace game
|
|||||||
WEAK symbol<bool()> SV_Loaded{0x140442F60, 0x1404864A0};
|
WEAK symbol<bool()> SV_Loaded{0x140442F60, 0x1404864A0};
|
||||||
WEAK symbol<void(int clientNum, const char* reason)> SV_KickClientNum{0, 0x14047ED00};
|
WEAK symbol<void(int clientNum, const char* reason)> SV_KickClientNum{0, 0x14047ED00};
|
||||||
WEAK symbol<bool(const char* map)> SV_MapExists{0, 0x14047ED60};
|
WEAK symbol<bool(const char* map)> SV_MapExists{0, 0x14047ED60};
|
||||||
|
WEAK symbol<void(mp::client_t*, const char*, int)> SV_ExecuteClientCommand{0, 0x140481870};
|
||||||
WEAK symbol<void(int localClientNum)> SV_FastRestart{0, 0x14047E990};
|
WEAK symbol<void(int localClientNum)> SV_FastRestart{0, 0x14047E990};
|
||||||
WEAK symbol<void(int clientNum, svscmd_type type, const char* text)> SV_GameSendServerCommand{
|
WEAK symbol<void(int clientNum, svscmd_type type, const char* text)> SV_GameSendServerCommand{
|
||||||
0x1403F3A70, 0x140484AD0
|
0x1403F3A70, 0x140484AD0
|
||||||
};
|
};
|
||||||
|
|
||||||
WEAK symbol<bool(int clientNum, const char* menu)> Menu_IsMenuOpenAndVisible{ 0, 0x1404C7320 };
|
|
||||||
WEAK symbol<const char* (const char* string)> UI_SafeTranslateString{ 0x140350430, 0x1405A2930 };
|
|
||||||
|
|
||||||
WEAK symbol<void()> Sys_ShowConsole{0x1403E3B90, 0x140514910};
|
WEAK symbol<void()> Sys_ShowConsole{0x1403E3B90, 0x140514910};
|
||||||
WEAK symbol<void(const char* error, ...)> Sys_Error{0x1403E0C40, 0x140511520};
|
WEAK symbol<void(const char* error, ...)> Sys_Error{0x1403E0C40, 0x140511520};
|
||||||
WEAK symbol<void(char* path, int pathSize, Sys_Folder folder, const char* filename, const char* ext)>
|
WEAK symbol<void(char* path, int pathSize, Sys_Folder folder, const char* filename, const char* ext)>
|
||||||
@ -127,6 +132,8 @@ namespace game
|
|||||||
WEAK symbol<void(unsigned int localClientNum, const char** args)> UI_RunMenuScript{0, 0x1404CFE60};
|
WEAK symbol<void(unsigned int localClientNum, const char** args)> UI_RunMenuScript{0, 0x1404CFE60};
|
||||||
WEAK symbol<int(const char* text, int maxChars, Font_s* font, float scale)> UI_TextWidth{0, 0x1404D21A0};
|
WEAK symbol<int(const char* text, int maxChars, Font_s* font, float scale)> UI_TextWidth{0, 0x1404D21A0};
|
||||||
|
|
||||||
|
WEAK symbol<const char*(const char* string)> UI_SafeTranslateString{0x140350430, 0x14041C580};
|
||||||
|
|
||||||
/***************************************************************
|
/***************************************************************
|
||||||
* Variables
|
* Variables
|
||||||
**************************************************************/
|
**************************************************************/
|
||||||
@ -160,6 +167,8 @@ namespace game
|
|||||||
WEAK symbol<int> svs_numclients{0, 0x14B204A0C};
|
WEAK symbol<int> svs_numclients{0, 0x14B204A0C};
|
||||||
WEAK symbol<int> gameTime{0, 0x14621BDBC};
|
WEAK symbol<int> gameTime{0, 0x14621BDBC};
|
||||||
|
|
||||||
|
WEAK symbol<int> sv_serverId_value{0, 0x14A3E99B8};
|
||||||
|
|
||||||
WEAK symbol<bool> virtualLobby_loaded{0, 0x142D077FD};
|
WEAK symbol<bool> virtualLobby_loaded{0, 0x142D077FD};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user