Add console, dedicated, serverlist and more

This commit is contained in:
Joelrau 2021-01-17 09:56:48 +02:00
parent 0614305235
commit a66c2efe99
11 changed files with 1217 additions and 34 deletions

View File

@ -0,0 +1,204 @@
#include <std_include.hpp>
#include "console.hpp"
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "scheduler.hpp"
#include "game_console.hpp"
#include <utils/thread.hpp>
#include <utils/flags.hpp>
namespace console
{
namespace
{
void hide_console()
{
auto* const con_window = GetConsoleWindow();
DWORD process;
GetWindowThreadProcessId(con_window, &process);
if(process == GetCurrentProcessId() || IsDebuggerPresent())
{
ShowWindow(con_window, SW_HIDE);
}
}
}
class component final : public component_interface
{
public:
component()
{
hide_console();
_pipe(this->handles_, 1024, _O_TEXT);
_dup2(this->handles_[1], 1);
_dup2(this->handles_[1], 2);
//setvbuf(stdout, nullptr, _IONBF, 0);
//setvbuf(stderr, nullptr, _IONBF, 0);
}
void post_start() override
{
scheduler::loop([this]()
{
this->log_messages();
}, scheduler::pipeline::main);
this->console_runner_ = utils::thread::create_named_thread("Console IO", [this]
{
this->runner();
});
}
void pre_destroy() override
{
printf("\r\n");
_flushall();
this->terminate_runner_ = true;
if (this->console_runner_.joinable())
{
this->console_runner_.join();
}
_close(this->handles_[0]);
_close(this->handles_[1]);
}
void post_unpack() override
{
this->initialize();
}
private:
volatile bool console_initialized_ = false;
volatile bool terminate_runner_ = false;
std::mutex mutex_;
std::thread console_runner_;
std::queue<std::string> message_queue_;
int handles_[2]{};
void initialize()
{
utils::thread::create_named_thread("Console", [this]()
{
if (game::environment::is_dedi())
{
game::Sys_ShowConsole();
}
else if(!utils::flags::has_flag("noconsole"))
{
game::Sys_ShowConsole();
}
if (!game::environment::is_dedi())
{
// Hide that shit
ShowWindow(console::get_window(), SW_MINIMIZE);
}
{
std::lock_guard<std::mutex> _(this->mutex_);
this->console_initialized_ = true;
}
MSG msg;
while (!this->terminate_runner_)
{
if (PeekMessageA(&msg, nullptr, NULL, NULL, PM_REMOVE))
{
if (msg.message == WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
std::this_thread::sleep_for(1ms);
}
}
exit(0);
}).detach();
}
void log_messages()
{
while (this->console_initialized_ && !this->message_queue_.empty())
{
std::queue<std::string> message_queue_copy;
{
std::lock_guard<std::mutex> _(this->mutex_);
message_queue_copy = std::move(this->message_queue_);
this->message_queue_ = {};
}
while (!message_queue_copy.empty())
{
log_message(message_queue_copy.front());
message_queue_copy.pop();
}
}
fflush(stdout);
fflush(stderr);
}
static void log_message(const std::string& message)
{
OutputDebugStringA(message.data());
game::Conbuf_AppendText(message.data());
}
void runner()
{
char buffer[1024];
while (!this->terminate_runner_ && this->handles_[0])
{
const auto len = _read(this->handles_[0], buffer, sizeof(buffer));
if (len > 0)
{
std::lock_guard<std::mutex> _(this->mutex_);
this->message_queue_.push(std::string(buffer, len));
}
else
{
std::this_thread::sleep_for(10ms);
}
}
std::this_thread::yield();
}
};
HWND get_window()
{
return *reinterpret_cast<HWND*>((SELECT_VALUE(0x14A9F6070, 0x14B5B94C0)));
}
void set_title(std::string title)
{
SetWindowText(get_window(), title.data());
}
void set_size(const int width, const int height)
{
RECT rect;
GetWindowRect(get_window(), &rect);
SetWindowPos(get_window(), nullptr, rect.left, rect.top, width, height, 0);
auto logoWindow = *reinterpret_cast<HWND*>(SELECT_VALUE(0x14A9F6080, 0x14B5B94D0));
SetWindowPos(logoWindow, 0, 5, 5, width - 25, 60, 0);
}
}
REGISTER_COMPONENT(console::component)

View File

@ -0,0 +1,8 @@
#pragma once
namespace console
{
HWND get_window();
void set_title(std::string title);
void set_size(int width, int height);
}

View File

@ -0,0 +1,265 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "scheduler.hpp"
#include "server_list.hpp"
#include "network.hpp"
#include "command.hpp"
#include "game/game.hpp"
#include "fastfiles.hpp"
#include "dvars.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
namespace dedicated
{
namespace
{
utils::hook::detour dvar_get_string_hook;
void init_dedicated_server()
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// R_LoadGraphicsAssets
reinterpret_cast<void(*)()>(0x1405A54F0)();
}
void send_heartbeat()
{
game::netadr_s target{};
if (server_list::get_master_server(target))
{
network::send(target, "heartbeat", "S1");
}
}
std::vector<std::string>& get_startup_command_queue()
{
static std::vector<std::string> startup_command_queue;
return startup_command_queue;
}
void execute_startup_command(int client, int controllerIndex, const char* command)
{
if (game::Live_SyncOnlineDataFlags(0) == 0)
{
game::Cbuf_ExecuteBufferInternal(0, 0, command, game::Cmd_ExecuteSingleCommand);
}
else
{
get_startup_command_queue().emplace_back(command);
}
}
void execute_startup_command_queue()
{
const auto queue = get_startup_command_queue();
get_startup_command_queue().clear();
for (const auto& command : queue)
{
game::Cbuf_ExecuteBufferInternal(0, 0, command.data(), game::Cmd_ExecuteSingleCommand);
}
}
std::vector<std::string>& get_console_command_queue()
{
static std::vector<std::string> console_command_queue;
return console_command_queue;
}
void execute_console_command(const int client, const char* command)
{
if (game::Live_SyncOnlineDataFlags(0) == 0)
{
game::Cbuf_AddText(client, command);
game::Cbuf_AddText(client, "\n");
}
else
{
get_console_command_queue().emplace_back(command);
}
}
void execute_console_command_queue()
{
const auto queue = get_console_command_queue();
get_console_command_queue().clear();
for (const auto& command : queue)
{
game::Cbuf_AddText(0, command.data());
game::Cbuf_AddText(0, "\n");
}
}
void sync_gpu_stub()
{
std::this_thread::sleep_for(1ms);
}
utils::hook::detour com_quit_f_hook;
void kill_server()
{
for (auto i = 0; i < *game::mp::svs_numclients; ++i)
{
if (game::mp::svs_clients[i].header.state >= 3)
{
game::SV_GameSendServerCommand(i, game::SV_CMD_CAN_IGNORE, utils::string::va("r \"%s\"", "EXE_ENDOFGAME"));
}
}
com_quit_f_hook.invoke<void>();
}
}
void initialize()
{
command::execute("onlinegame 1", true);
command::execute("xblive_privatematch 1", true);
}
class component final : public component_interface
{
public:
void* load_import(const std::string& library, const std::string& function) override
{
return nullptr;
}
void post_unpack() override
{
if (!game::environment::is_dedi())
{
return;
}
// Don't allow sv_hostname to be changed by the game
dvars::disable::Dvar_SetString("sv_hostname");
// Hook R_SyncGpu
utils::hook::jump(0x1405A7630, sync_gpu_stub);
utils::hook::jump(0x14020C6B0, init_dedicated_server);
// delay startup commands until the initialization is done
//utils::hook::call(0x1403CDF63, execute_startup_command);
// delay console commands until the initialization is done
utils::hook::call(0x1403CEC35, execute_console_command);
utils::hook::nop(0x1403CEC4B, 5);
utils::hook::nop(0x1404AE6AE, 5); // don't load config file
utils::hook::nop(0x1403AF719, 5); // ^
utils::hook::set<uint8_t>(0x1403D2490, 0xC3); // don't save config file
utils::hook::set<uint8_t>(0x14022AFC0, 0xC3); // disable self-registration
utils::hook::set<uint8_t>(0x1404DA780, 0xC3); // init sound system (1)
utils::hook::set<uint8_t>(0x14062BC10, 0xC3); // init sound system (2)
utils::hook::set<uint8_t>(0x1405F31A0, 0xC3); // render thread
utils::hook::set<uint8_t>(0x140213C20, 0xC3); // called from Com_Frame, seems to do renderer stuff
utils::hook::set<uint8_t>(0x1402085C0, 0xC3);
// CL_CheckForResend, which tries to connect to the local server constantly
utils::hook::set<uint8_t>(0x14059B854, 0); // r_loadForRenderer default to 0
utils::hook::set<uint8_t>(0x1404D6952, 0xC3); // recommended settings check - TODO: Check hook
utils::hook::set<uint8_t>(0x1404D9BA0, 0xC3); // some mixer-related function called on shutdown
utils::hook::set<uint8_t>(0x1403B2860, 0xC3); // dont load ui gametype stuff
utils::hook::nop(0x14043ABB8, 6); // unknown check in SV_ExecuteClientMessage
utils::hook::nop(0x140439F15, 4); // allow first slot to be occupied
utils::hook::nop(0x14020E01C, 2); // properly shut down dedicated servers
utils::hook::nop(0x14020DFE9, 2); // ^
utils::hook::nop(0x14020E047, 5); // don't shutdown renderer
utils::hook::set<uint8_t>(0x140057D40, 0xC3); // something to do with blendShapeVertsView
utils::hook::nop(0x14062EA17, 8); // sound thing
utils::hook::set<uint8_t>(0x1404D6960, 0xC3); // cpu detection stuff?
utils::hook::set<uint8_t>(0x1405AEC00, 0xC3); // gfx stuff during fastfile loading
utils::hook::set<uint8_t>(0x1405AEB10, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405AEBA0, 0xC3); // ^
utils::hook::set<uint8_t>(0x140275640, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405AEB60, 0xC3); // ^
utils::hook::set<uint8_t>(0x140572640, 0xC3); // directx stuff
utils::hook::set<uint8_t>(0x1405A1340, 0xC3); // ^
utils::hook::set<uint8_t>(0x140021D60, 0xC3); // ^ - mutex
utils::hook::set<uint8_t>(0x1405A17E0, 0xC3); // ^
utils::hook::set<uint8_t>(0x1400534F0, 0xC3); // rendering stuff
utils::hook::set<uint8_t>(0x1405A1AB0, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A1BB0, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A21F0, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A2D60, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A3400, 0xC3); // ^
// shaders
utils::hook::set<uint8_t>(0x140057BC0, 0xC3); // ^
utils::hook::set<uint8_t>(0x140057B40, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405EE040, 0xC3); // ^ - mutex
utils::hook::set<uint8_t>(0x1404DAF30, 0xC3); // idk
utils::hook::set<uint8_t>(0x1405736B0, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A6E70, 0xC3); // R_Shutdown
utils::hook::set<uint8_t>(0x1405732D0, 0xC3); // shutdown stuff
utils::hook::set<uint8_t>(0x1405A6F40, 0xC3); // ^
utils::hook::set<uint8_t>(0x1405A61A0, 0xC3); // ^
utils::hook::set<uint8_t>(0x14062C550, 0xC3); // sound crashes
utils::hook::set<uint8_t>(0x140445070, 0xC3); // disable host migration
utils::hook::set<uint8_t>(0x1403E1A50, 0xC3); // render synchronization lock
utils::hook::set<uint8_t>(0x1403E1990, 0xC3); // render synchronization unlock
utils::hook::nop(0x1404CC482, 5); // Disable sound pak file loading
utils::hook::nop(0x1404CC471, 2); // ^
utils::hook::set<uint8_t>(0x140279B80, 0xC3); // Disable image pak file loading
// initialize the game after onlinedataflags is 32 (workaround)
scheduler::schedule([=]()
{
if (game::Live_SyncOnlineDataFlags(0) == 32 && game::Sys_IsDatabaseReady2())
{
scheduler::once([]()
{
command::execute("xstartprivateparty", true);
command::execute("disconnect", true); // 32 -> 0
}, scheduler::pipeline::main, 1s);
return scheduler::cond_end;
}
return scheduler::cond_continue;
}, scheduler::pipeline::main, 1s);
scheduler::on_game_initialized([]()
{
initialize();
printf("==================================\n");
printf("Server started!\n");
printf("==================================\n");
MessageBoxA(nullptr, "server started!", "", 0);
// remove disconnect command
game::Cmd_RemoveCommand((const char*)751);
execute_startup_command_queue();
execute_console_command_queue();
// Send heartbeat to dpmaster
scheduler::once(send_heartbeat, scheduler::pipeline::server);
scheduler::loop(send_heartbeat, scheduler::pipeline::server, 2min);
command::add("heartbeat", send_heartbeat);
}, scheduler::pipeline::main, 1s);
command::add("killserver", kill_server);
com_quit_f_hook.create(0x1403D08C0, &kill_server);
}
};
}
REGISTER_COMPONENT(dedicated::component)

View File

@ -0,0 +1,6 @@
#pragma once
namespace dedicated
{
void initialize();
}

View File

@ -0,0 +1,60 @@
#include <std_include.hpp>
#include "console.hpp"
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "scheduler.hpp"
#include <utils\string.hpp>
namespace dedicated_info
{
class component final : public component_interface
{
public:
void post_unpack() override
{
if (!game::environment::is_dedi())
{
return;
}
scheduler::loop([]()
{
auto* sv_running = game::Dvar_FindVar("sv_running");
if (!sv_running || !sv_running->current.enabled)
{
console::set_title("S1-Mod Dedicated Server");
return;
}
auto* const sv_hostname = game::Dvar_FindVar("sv_hostname");
auto* const sv_maxclients = game::Dvar_FindVar("sv_maxclients");
auto* const mapname = game::Dvar_FindVar("mapname");
auto clientCount = 0;
for (auto i = 0; i < sv_maxclients->current.integer; i++)
{
auto* client = &game::mp::svs_clients[i];
auto* self = &game::mp::g_entities[i];
if (client->header.state >= 1 && self && self->client)
{
clientCount++;
}
}
std::string cleaned_hostname;
cleaned_hostname.resize(static_cast<int>(strlen(sv_hostname->current.string) + 1));
utils::string::strip(sv_hostname->current.string, cleaned_hostname.data(),
static_cast<int>(strlen(sv_hostname->current.string)) + 1);
console::set_title(utils::string::va("%s on %s [%d/%d]", cleaned_hostname.data(),
mapname->current.string, clientCount,
sv_maxclients->current.integer));
}, scheduler::pipeline::main, 1s);
}
};
}
REGISTER_COMPONENT(dedicated_info::component)

View File

@ -4,6 +4,7 @@
#include "game/game.hpp" #include "game/game.hpp"
#include "game_console.hpp" #include "game_console.hpp"
#include "server_list.hpp"
#include <utils/hook.hpp> #include <utils/hook.hpp>
@ -31,6 +32,11 @@ namespace input
return; return;
} }
if (game::environment::is_mp() && !server_list::sl_key_event(key, down))
{
return;
}
cl_key_event_hook.invoke<void>(local_client_num, key, down, arg4); cl_key_event_hook.invoke<void>(local_client_num, key, down, arg4);
} }
} }

View File

@ -0,0 +1,156 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "command.hpp"
#include "scheduler.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
#include "game/game.hpp"
namespace map_rotation
{
namespace
{
void set_dvar(const std::string& dvar, const std::string& value)
{
command::execute(utils::string::va("%s \"%s\"", dvar.data(), value.data()), true);
}
void set_gametype(const std::string& gametype)
{
set_dvar("g_gametype", gametype);
}
void launch_map(const std::string& mapname)
{
command::execute(utils::string::va("map %s", mapname.data()), false);
}
void launch_default_map()
{
auto* mapname = game::Dvar_FindVar("mapname");
if (mapname && mapname->current.string && strlen(mapname->current.string) && mapname->current.string != "mp_vlobby_room"s)
{
launch_map(mapname->current.string);
}
else
{
if (game::Com_GetCurrentCoDPlayMode() == game::CODPLAYMODE_ZOMBIES)
{
launch_map("mp_zombie_lab");
}
else
{
launch_map("mp_venus");
}
}
}
std::string load_current_map_rotation()
{
auto* rotation = game::Dvar_FindVar("sv_mapRotationCurrent");
if (!strlen(rotation->current.string))
{
rotation = game::Dvar_FindVar("sv_mapRotation");
set_dvar("sv_mapRotationCurrent", rotation->current.string);
}
return rotation->current.string;
}
std::vector<std::string> parse_current_map_rotation()
{
const auto rotation = load_current_map_rotation();
return utils::string::split(rotation, ' ');
}
void store_new_rotation(const std::vector<std::string>& elements, const size_t index)
{
std::string value{};
for (auto i = index; i < elements.size(); ++i)
{
if (i != index)
{
value.push_back(' ');
}
value.append(elements[i]);
}
set_dvar("sv_mapRotationCurrent", value);
}
void perform_map_rotation()
{
if (game::Live_SyncOnlineDataFlags(0) != 0)
{
scheduler::on_game_initialized(perform_map_rotation, scheduler::pipeline::main, 1s);
return;
}
const auto rotation = parse_current_map_rotation();
for (size_t i = 0; !rotation.empty() && i < (rotation.size() - 1); i += 2)
{
const auto& key = rotation[i];
const auto& value = rotation[i + 1];
if (key == "gametype")
{
set_gametype(value);
}
else if (key == "map")
{
store_new_rotation(rotation, i + 2);
launch_map(value);
return;
}
else
{
printf("Invalid map rotation key: %s\n", key.data());
}
}
launch_default_map();
}
void trigger_map_rotation()
{
scheduler::schedule([]()
{
if (game::CL_IsCgameInitialized())
{
return scheduler::cond_continue;
}
command::execute("map_rotate", false);
return scheduler::cond_end;
}, scheduler::pipeline::main, 1s);
}
}
class component final : public component_interface
{
public:
void post_unpack() override
{
if (!game::environment::is_dedi())
{
return;
}
scheduler::once([]()
{
game::Dvar_RegisterString("sv_mapRotation", "", 0, "");
game::Dvar_RegisterString("sv_mapRotationCurrent", "", 0, "");
}, scheduler::pipeline::main);
command::add("map_rotate", &perform_map_rotation);
// Hook GScr_ExitLevel
utils::hook::jump(0x14032E490, &trigger_map_rotation);
}
};
}
REGISTER_COMPONENT(map_rotation::component)

View File

@ -6,7 +6,7 @@
#include "command.hpp" #include "command.hpp"
#include "network.hpp" #include "network.hpp"
#include "scheduler.hpp" #include "scheduler.hpp"
//#include "server_list.hpp" #include "server_list.hpp"
#include "steam/steam.hpp" #include "steam/steam.hpp"
@ -107,6 +107,30 @@ namespace party
} }
} }
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, 16);
game::I_CleanStr(client_name);
if (client_name == name)
{
return i;
}
}
}
return -1;
}
void reset_connect_state()
{
connect_state = {};
}
int get_client_count() int get_client_count()
{ {
auto count = 0; auto count = 0;
@ -143,7 +167,7 @@ namespace party
return; return;
} }
command::execute("lui_open popup_acceptinginvite", false); command::execute("lui_open_popup popup_acceptinginvite", false);
connect_state.host = target; connect_state.host = target;
connect_state.challenge = utils::cryptography::random::get_challenge(); connect_state.challenge = utils::cryptography::random::get_challenge();
@ -181,9 +205,20 @@ namespace party
return; return;
} }
// starting map like this crashes the game.
printf("Starting map: %s\n", mapname.data()); printf("Starting map: %s\n", mapname.data());
game::SV_StartMapForParty(0, mapname.data(), true);
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);
// StartServer
reinterpret_cast<void(*)(unsigned int)>(0x140492260)(0);
// this dun work.
//game::SV_StartMapForParty(0, mapname.data(), false, false);
} }
} }
@ -241,34 +276,11 @@ namespace party
} }
}); });
command::add("reconnect", [](const command::params& params) command::add("kickClient", [](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) if (params.size() < 2)
{ {
printf("usage: clientkick <num>\n"); printf("usage: kickClient <num>\n");
return; return;
} }
const auto client_num = atoi(params.get(1)); const auto client_num = atoi(params.get(1));
@ -280,6 +292,33 @@ namespace party
game::SV_KickClientNum(client_num, "EXE_PLAYERKICKED"); game::SV_KickClientNum(client_num, "EXE_PLAYERKICKED");
}); });
command::add("kick", [](const command::params& params)
{
if (params.size() < 2)
{
printf("usage: kick <name>\n");
return;
}
const std::string name = params.get(1);
if (name == "all"s)
{
for (auto i = 0; i < *game::mp::svs_numclients; ++i)
{
game::SV_KickClientNum(i, "EXE_PLAYERKICKED");
}
return;
}
const auto client_num = get_client_num_by_name(name);
if (client_num < 0 || client_num >= *game::mp::svs_numclients)
{
return;
}
game::SV_KickClientNum(client_num, "EXE_PLAYERKICKED");
});
scheduler::once([]() scheduler::once([]()
{ {
game::Dvar_RegisterString("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, game::Dvar_RegisterString("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE,
@ -297,7 +336,7 @@ namespace party
const auto message = params.join(2); const auto message = params.join(2);
const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string;
game::SV_GameSendServerCommand(client_num, 0, game::SV_GameSendServerCommand(client_num, game::SV_CMD_CAN_IGNORE,
utils::string::va("%c \"%s: %s\"", 84, name, message.data())); utils::string::va("%c \"%s: %s\"", 84, name, message.data()));
printf("%s -> %i: %s\n", name, client_num, message.data()); printf("%s -> %i: %s\n", name, client_num, message.data());
}); });
@ -312,7 +351,7 @@ namespace party
const auto client_num = atoi(params.get(1)); const auto client_num = atoi(params.get(1));
const auto message = params.join(2); const auto message = params.join(2);
game::SV_GameSendServerCommand(client_num, 0, utils::string::va("%c \"%s\"", 84, message.data())); 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()); printf("%i: %s\n", client_num, message.data());
}); });
@ -327,7 +366,7 @@ namespace party
const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string; const auto* const name = game::Dvar_FindVar("sv_sayName")->current.string;
game::SV_GameSendServerCommand( game::SV_GameSendServerCommand(
-1, 0, utils::string::va("%c \"%s: %s\"", 84, name, message.data())); -1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s: %s\"", 84, name, message.data()));
printf("%s: %s\n", name, message.data()); printf("%s: %s\n", name, message.data());
}); });
@ -340,7 +379,7 @@ namespace party
const auto message = params.join(1); const auto message = params.join(1);
game::SV_GameSendServerCommand(-1, 0, utils::string::va("%c \"%s\"", 84, message.data())); game::SV_GameSendServerCommand(-1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data()));
printf("%s\n", message.data()); printf("%s\n", message.data());
}); });
@ -362,6 +401,7 @@ namespace party
info.set("sv_maxclients", utils::string::va("%i", *game::mp::svs_numclients)); info.set("sv_maxclients", utils::string::va("%i", *game::mp::svs_numclients));
info.set("protocol", utils::string::va("%i", PROTOCOL)); info.set("protocol", utils::string::va("%i", PROTOCOL));
info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode()));
info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running")));
network::send(target, "infoResponse", info.build(), '\n'); network::send(target, "infoResponse", info.build(), '\n');
}); });
@ -369,7 +409,7 @@ namespace party
network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data) network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data)
{ {
const utils::info_string info{data}; const utils::info_string info{data};
//server_list::handle_info_response(target, info); server_list::handle_info_response(target, info);
if (connect_state.host != target) if (connect_state.host != target)
{ {
@ -402,6 +442,15 @@ namespace party
return; 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"); const auto mapname = info.get("mapname");
if (mapname.empty()) if (mapname.empty())
{ {

View File

@ -3,9 +3,13 @@
namespace party namespace party
{ {
void reset_connect_state();
void connect(const game::netadr_s& target); void connect(const game::netadr_s& target);
void start_map(const std::string& mapname); void start_map(const std::string& mapname);
int get_client_num_by_name(const std::string& name);
int get_client_count(); int get_client_count();
int get_bot_count(); int get_bot_count();
} }

View File

@ -0,0 +1,413 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "server_list.hpp"
#include "game_console.hpp"
#include "command.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>
namespace server_list
{
namespace
{
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_index = 0;
int server_list_page = 0;
volatile bool update_server_list = false;
void refresh_server_list()
{
{
std::lock_guard<std::mutex> _(mutex);
servers.clear();
master_state.queued_servers.clear();
server_list_index = 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) + server_list_index;
if (i < servers.size())
{
// double click disabled for now
/*static size_t last_index = 0xFFFFFFFF;
if (last_index != i)
{
last_index = i;
}
else*/
{
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;
}
auto count = static_cast<int>(servers.size());
return count > 15 ? 15 : count;
}
const char* ui_feeder_item_text(int /*localClientNum*/, void* /*a2*/, void* /*a3*/, const size_t index,
const size_t column)
{
std::lock_guard<std::mutex> _(mutex);
const auto i = server_list_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() ? "" : 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();
}
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 scroll_down()
{
if (!is_server_list_open())
{
return false;
}
if (server_list_index + 15 < servers.size())
{
server_list_index += 15; // next page
server_list_page += 1;
trigger_refresh();
}
return true;
}
bool scroll_up()
{
if (!is_server_list_open())
{
return false;
}
if (server_list_index > 0)
{
server_list_index -= 15; // prev page
server_list_page -= 1;
trigger_refresh();
}
return true;
}
void resize_host_name(std::string& name)
{
size_t resize_size = 32;
name.resize(resize_size);
game::Font_s* font;
if (game::Com_GetCurrentCoDPlayMode() == game::CODPLAYMODE_ZOMBIES)
{
font = game::R_RegisterFont("fonts/zmBodyFont");
}
else
{
font = game::R_RegisterFont("fonts/bodyFont");
}
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.resize(resize_size);
resize_size--;
}
}
void lui_open_menu_stub(int /*controllerIndex*/, const char* /*menu*/, int /*a3*/, int /*a4*/,
unsigned int /*a5*/)
{
refresh_server_list();
game::Cmd_ExecuteSingleCommand(0, 0, "lui_open menu_systemlink_join\n");
}
}
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 false;
return game::NET_StringToAdr("91.159.5.176:20810", &address);
}
void handle_info_response(const game::netadr_s& address, const utils::info_string& info)
{
// Don't show servers that aren't running!
auto sv_running = std::atoi(info.get("sv_running").data());
if (!sv_running)
{
return;
}
// Only handle servers of the same playmode!
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 = (now - start_time) > 999 ? 999 : (now - start_time);
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.");
localized_strings::override("MENU_NUMPLAYERS", "Players");
// hook LUI_OpenMenu to show server list instead of store popup
utils::hook::call(0x1404D5550, &lui_open_menu_stub);
// replace UI_RunMenuScript call in LUI_CoD_LuaCall_RefreshServerList to our refresh_servers
utils::hook::call(0x1400F5AA1, &refresh_server_list);
utils::hook::call(0x1400F5F26, &join_server);
utils::hook::nop(0x1400F5F45, 5);
// do feeder stuff
utils::hook::call(0x1400F5B55, &ui_feeder_count);
utils::hook::call(0x1400F5D35, &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)
{
{
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)

View 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);
}