2022-03-10 01:12:16 +00:00
|
|
|
#include <std_include.hpp>
|
2022-03-23 13:48:13 +00:00
|
|
|
#include <loader/module_loader.hpp>
|
2022-12-09 14:50:13 +01:00
|
|
|
#include "game/game.hpp"
|
2022-12-30 13:57:46 +01:00
|
|
|
#include "game/dvars.hpp"
|
2022-12-09 14:50:13 +01:00
|
|
|
|
2022-03-10 01:12:16 +00:00
|
|
|
#include "command.hpp"
|
2023-01-27 14:21:54 +00:00
|
|
|
#include "console.hpp"
|
2023-02-21 01:35:51 +01:00
|
|
|
#include "scheduler.hpp"
|
|
|
|
#include "test_clients.hpp"
|
|
|
|
|
|
|
|
#include "gsc/script_error.hpp"
|
|
|
|
#include "gsc/script_extension.hpp"
|
|
|
|
|
|
|
|
#include <utils/hook.hpp>
|
|
|
|
#include <utils/string.hpp>
|
2022-03-10 01:12:16 +00:00
|
|
|
|
|
|
|
bool test_clients::can_add()
|
|
|
|
{
|
|
|
|
auto i = 0;
|
|
|
|
while (i < *game::native::svs_clientCount)
|
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
if (game::native::mp::svs_clients[i].header.state == game::native::CS_FREE)
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
|
|
|
// Free slot was found
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
|
|
|
return i < *game::native::svs_clientCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
game::native::gentity_s* test_clients::sv_add_test_client()
|
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
if (!can_add())
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-12-30 13:57:46 +01:00
|
|
|
assert((*dvars::com_sv_running)->current.enabled);
|
|
|
|
assert((*dvars::sv_maxclients)->current.integer <= *game::native::svs_clientCount);
|
2022-03-10 12:32:22 +00:00
|
|
|
|
2022-03-10 01:12:16 +00:00
|
|
|
static auto bot_port = 0;
|
|
|
|
char user_info[1024] = {0};
|
2022-03-10 12:32:22 +00:00
|
|
|
char xuid[32] = {0};
|
|
|
|
|
|
|
|
const std::uint64_t random_xuid = std::rand();
|
|
|
|
game::native::XUIDToString(&random_xuid, xuid);
|
2022-03-10 01:12:16 +00:00
|
|
|
|
2022-03-10 12:32:22 +00:00
|
|
|
// Most basic string the game will accept.
|
2022-03-10 01:12:16 +00:00
|
|
|
_snprintf_s(user_info, _TRUNCATE,
|
2022-03-10 13:54:10 +00:00
|
|
|
"connect bot%d \"\\invited\\0\\cg_predictItems\\1\\cl_anonymous\\0\\color\\4\\head\\default\\model\\multi\\snaps\\20\\rate\\5000\\name\\bot%d\\xuid\\%s\\xnaddr\\%s\\natType\\2\\protocol\\%d\\checksum\\%u\\statver\\%d %u\\qport\\%d\"",
|
2022-03-10 12:32:22 +00:00
|
|
|
bot_port, bot_port, xuid, "", game::native::GetProtocolVersion(), game::native::BG_NetDataChecksum(),
|
2022-03-10 13:54:10 +00:00
|
|
|
game::native::LiveStorage_GetPersistentDataDefVersion(), game::native::LiveStorage_GetPersistentDataDefFormatChecksum(), bot_port);
|
2022-03-10 01:12:16 +00:00
|
|
|
|
|
|
|
game::native::netadr_s adr;
|
2023-03-02 12:59:40 +00:00
|
|
|
ZeroMemory(&adr, sizeof(game::native::netadr_s));
|
2022-03-10 01:12:16 +00:00
|
|
|
|
|
|
|
game::native::SV_Cmd_TokenizeString(user_info);
|
2022-03-10 13:54:10 +00:00
|
|
|
|
|
|
|
adr.port = static_cast<std::uint16_t>(bot_port++);
|
2022-12-09 14:50:13 +01:00
|
|
|
game::native::NetAdr_SetType(&adr, game::native::NA_BOT);
|
2022-03-10 13:54:10 +00:00
|
|
|
|
2022-03-10 01:12:16 +00:00
|
|
|
game::native::SV_DirectConnect(adr);
|
2022-03-10 13:54:10 +00:00
|
|
|
|
2022-03-10 01:12:16 +00:00
|
|
|
game::native::SV_Cmd_EndTokenizedString();
|
|
|
|
|
|
|
|
// Find the bot
|
2022-05-16 14:02:15 +02:00
|
|
|
int idx;
|
2022-03-10 12:32:22 +00:00
|
|
|
for (idx = 0; idx < *game::native::svs_clientCount; idx++)
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
if (game::native::mp::svs_clients[idx].header.state == game::native::CS_FREE)
|
2022-03-10 01:12:16 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
if (game::native::mp::svs_clients[idx].header.netchan.remoteAddress.type == adr.type
|
|
|
|
&& game::native::mp::svs_clients[idx].header.netchan.remoteAddress.port == adr.port)
|
2023-02-21 01:35:51 +01:00
|
|
|
break; // Found the bot
|
2022-03-10 01:12:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (idx == *game::native::svs_clientCount)
|
|
|
|
{
|
|
|
|
// Failed to add test client
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-03-16 20:25:41 +00:00
|
|
|
const auto client = &game::native::mp::svs_clients[idx];
|
2022-03-10 01:12:16 +00:00
|
|
|
client->bIsTestClient = 1;
|
|
|
|
|
|
|
|
game::native::SV_SendClientGameState(client);
|
|
|
|
|
|
|
|
game::native::usercmd_s cmd;
|
2022-12-09 14:50:13 +01:00
|
|
|
ZeroMemory(&cmd, sizeof(game::native::usercmd_s));
|
2022-03-10 01:12:16 +00:00
|
|
|
|
|
|
|
game::native::SV_ClientEnterWorld(client, &cmd);
|
|
|
|
|
2022-12-20 15:49:56 +01:00
|
|
|
assert(client->gentity);
|
2022-03-10 01:12:16 +00:00
|
|
|
|
|
|
|
return client->gentity;
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:26:51 +01:00
|
|
|
void test_clients::gscr_add_test_client()
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
const auto* ent = sv_add_test_client();
|
2022-03-10 01:12:16 +00:00
|
|
|
|
2022-12-20 15:49:56 +01:00
|
|
|
if (ent)
|
2022-04-07 22:39:38 +02:00
|
|
|
{
|
|
|
|
game::native::Scr_AddEntityNum(ent->s.number, 0);
|
|
|
|
}
|
2022-03-10 01:12:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 15:26:51 +01:00
|
|
|
void test_clients::spawn(const int count)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
scheduler::once([]
|
2022-04-25 15:26:51 +01:00
|
|
|
{
|
|
|
|
auto* ent = sv_add_test_client();
|
|
|
|
if (ent == nullptr) return;
|
|
|
|
|
|
|
|
game::native::Scr_AddEntityNum(ent->s.number, 0);
|
2022-12-09 14:50:13 +01:00
|
|
|
scheduler::once([ent]
|
2022-04-25 15:26:51 +01:00
|
|
|
{
|
|
|
|
game::native::Scr_AddString("autoassign");
|
|
|
|
game::native::Scr_AddString("team_marinesopfor");
|
|
|
|
game::native::Scr_Notify(ent, static_cast<std::uint16_t>(game::native::SL_GetString("menuresponse", 0)), 2);
|
|
|
|
|
2022-12-09 14:50:13 +01:00
|
|
|
scheduler::once([ent]
|
2022-04-25 15:26:51 +01:00
|
|
|
{
|
2022-04-25 16:10:57 +01:00
|
|
|
game::native::Scr_AddString(utils::string::va("class%i", std::rand() % 5));
|
2022-04-25 15:26:51 +01:00
|
|
|
game::native::Scr_AddString("changeclass");
|
|
|
|
game::native::Scr_Notify(ent, static_cast<std::uint16_t>(game::native::SL_GetString("menuresponse", 0)), 2);
|
|
|
|
}, scheduler::pipeline::server, 2s);
|
|
|
|
|
|
|
|
}, scheduler::pipeline::server, 1s);
|
|
|
|
|
|
|
|
}, scheduler::pipeline::server, 2s * (i + 1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-10 01:18:42 +00:00
|
|
|
void test_clients::scr_shutdown_system_mp_stub(unsigned char sys)
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
|
|
|
game::native::SV_DropAllBots();
|
2022-05-21 21:39:22 +02:00
|
|
|
utils::hook::invoke<void>(0x569E30, sys);
|
2022-03-10 01:12:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
__declspec(naked) void test_clients::reset_reliable_mp()
|
|
|
|
{
|
|
|
|
// client_t ptr is in esi
|
|
|
|
__asm
|
|
|
|
{
|
|
|
|
cmp [esi + 0x41CB4], 1 // bIsTestClient
|
|
|
|
jnz resume
|
|
|
|
|
|
|
|
mov eax, [esi + 0x20E70] // Move reliableSequence to eax
|
|
|
|
mov [esi + 0x20E74], eax // Move eax to reliableAcknowledge
|
|
|
|
|
|
|
|
resume:
|
2022-03-10 01:18:42 +00:00
|
|
|
push 0x5CE740 // Sys_Milliseconds
|
2022-03-10 01:12:16 +00:00
|
|
|
retn
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 20:25:41 +00:00
|
|
|
bool test_clients::check_timeouts(const game::native::mp::client_t* client)
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
2023-02-21 11:17:54 +01:00
|
|
|
return (!client->bIsTestClient || client->header.state == game::native::CS_ZOMBIE) &&
|
|
|
|
client->header.netchan.remoteAddress.type != game::native::NA_LOOPBACK;
|
2022-03-10 01:12:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
__declspec(naked) void test_clients::check_timeouts_stub_mp()
|
|
|
|
{
|
|
|
|
__asm
|
|
|
|
{
|
|
|
|
push eax
|
|
|
|
pushad
|
|
|
|
|
|
|
|
lea esi, [ebx - 0x2146C]
|
|
|
|
push esi
|
2022-12-09 14:50:13 +01:00
|
|
|
call check_timeouts
|
2022-03-10 01:12:16 +00:00
|
|
|
add esp, 4
|
|
|
|
|
|
|
|
mov [esp + 0x20], eax
|
|
|
|
popad
|
|
|
|
pop eax
|
|
|
|
|
|
|
|
test al, al
|
|
|
|
|
|
|
|
push 0x576DD3
|
|
|
|
retn
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void test_clients::post_load()
|
|
|
|
{
|
|
|
|
if (game::is_mp()) this->patch_mp();
|
2022-12-09 14:50:13 +01:00
|
|
|
else return; // No sp bots for now :(
|
2022-03-10 01:12:16 +00:00
|
|
|
|
2022-04-25 15:26:51 +01:00
|
|
|
command::add("spawnBot", [](const command::params& params)
|
2022-03-10 01:12:16 +00:00
|
|
|
{
|
2022-12-09 14:50:13 +01:00
|
|
|
char* end;
|
|
|
|
|
2022-04-25 15:26:51 +01:00
|
|
|
if (params.size() < 2)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:50:13 +01:00
|
|
|
const auto* input = params.get(1);
|
|
|
|
const auto count = std::strtol(input, &end, 10);
|
|
|
|
if (input == end)
|
|
|
|
{
|
2023-01-27 14:21:54 +00:00
|
|
|
console::info("%s is not a valid input\nUsage: %s <number of bots>\n", input, params.get(0));
|
2022-12-09 14:50:13 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
spawn(count);
|
2022-03-10 01:12:16 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void test_clients::patch_mp()
|
|
|
|
{
|
|
|
|
utils::hook::nop(0x639803, 5); // LiveSteamServer_RunFrame
|
2022-03-10 13:54:10 +00:00
|
|
|
utils::hook::nop(0x5CD65A, 5); // Do not crash for silly fatal error
|
|
|
|
|
2022-12-09 14:50:13 +01:00
|
|
|
utils::hook::set<std::uint8_t>(0x572879, 0xEB); // Skip checks in SV_DirectConnect
|
|
|
|
utils::hook::set<std::uint8_t>(0x5728D4, 0xEB);
|
|
|
|
utils::hook::set<std::uint8_t>(0x57293D, 0xEB);
|
2022-03-10 13:54:10 +00:00
|
|
|
|
2022-12-09 14:50:13 +01:00
|
|
|
utils::hook(0x50C147, &scr_shutdown_system_mp_stub, HOOK_CALL).install()->quick(); // G_ShutdownGame
|
|
|
|
utils::hook(0x57BBF9, &reset_reliable_mp, HOOK_CALL).install()->quick(); // SV_SendMessageToClient
|
|
|
|
utils::hook(0x576DCC, &check_timeouts_stub_mp, HOOK_JUMP).install()->quick(); // SV_CheckTimeouts
|
2022-04-07 22:39:38 +02:00
|
|
|
|
|
|
|
// Replace nullsubbed gsc func "GScr_AddTestClient" with our spawn
|
2022-12-09 14:50:13 +01:00
|
|
|
utils::hook::set<void(*)()>(0x8AC8DC, gscr_add_test_client);
|
2023-02-21 01:35:51 +01:00
|
|
|
|
|
|
|
gsc::register_method("IsTestClient", [](const game::native::scr_entref_t entref)
|
|
|
|
{
|
|
|
|
gsc::get_entity(entref);
|
|
|
|
|
|
|
|
if (game::native::g_entities[entref.entnum].client == nullptr)
|
|
|
|
{
|
|
|
|
gsc::scr_error("IsTestClient: entity must be a player entity");
|
|
|
|
}
|
|
|
|
|
|
|
|
game::native::Scr_AddInt(game::native::SV_IsTestClient(entref.entnum));
|
|
|
|
});
|
2022-03-10 01:12:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
REGISTER_MODULE(test_clients);
|